From 66f092c0c699197cd7061c0cf47cfb05001a04ca Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 5 Oct 2025 19:02:15 +0200 Subject: [PATCH] Proxy package --- package-lock.json | 41 ++++++++++++++++++ package.json | 4 +- packages/proxy/README.md | 67 +++++++++++++++++++++++++++++ packages/proxy/package.json | 27 ++++++++++++ packages/proxy/src/cli.ts | 16 +++++++ packages/proxy/src/cors-proxy.ts | 73 ++++++++++++++++++++++++++++++++ packages/proxy/src/index.ts | 1 + packages/proxy/tsconfig.json | 8 ++++ scripts/sync-versions.js | 16 ++++++- 9 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 packages/proxy/README.md create mode 100644 packages/proxy/package.json create mode 100644 packages/proxy/src/cli.ts create mode 100644 packages/proxy/src/cors-proxy.ts create mode 100644 packages/proxy/src/index.ts create mode 100644 packages/proxy/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 9d8c081c..9c1ac524 100644 --- a/package-lock.json +++ b/package-lock.json @@ -715,6 +715,18 @@ } } }, + "node_modules/@hono/node-server": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.5.tgz", + "integrity": "sha512-iBuhh+uaaggeAuf+TftcjZyWh2GEgZcVGXkNtskLVoWaXhnJtC5HLHrU8W1KHDoucqO1MswwglmkWLFyiDn4WQ==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -836,6 +848,10 @@ "resolved": "packages/ai", "link": true }, + "node_modules/@mariozechner/pi-proxy": { + "resolved": "packages/proxy", + "link": true + }, "node_modules/@mariozechner/pi-reader-extension": { "resolved": "packages/browser-extension", "link": true @@ -2997,6 +3013,15 @@ "node": ">=12.0.0" } }, + "node_modules/hono": { + "version": "4.9.10", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.9.10.tgz", + "integrity": "sha512-AlI15ijFyKTXR7eHo7QK7OR4RoKIedZvBuRjO8iy4zrxvlY5oFCdiRG/V/lFJHCNXJ0k72ATgnyzx8Yqa5arug==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/html-parse-string": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/html-parse-string/-/html-parse-string-0.0.9.tgz", @@ -5409,6 +5434,22 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "packages/proxy": { + "name": "@mariozechner/pi-proxy", + "version": "0.5.43", + "dependencies": { + "@hono/node-server": "^1.14.0", + "hono": "^4.6.16" + }, + "bin": { + "pi-proxy": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "^22.10.5", + "tsx": "^4.19.2", + "typescript": "^5.7.3" + } + }, "packages/tui": { "name": "@mariozechner/pi-tui", "version": "0.5.43", diff --git a/package.json b/package.json index 74aa9762..0b73ab7f 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ ], "scripts": { "clean": "npm run clean --workspaces", - "build": "npm run build -w @mariozechner/pi-tui && npm run build -w @mariozechner/pi-ai && npm run build -w @mariozechner/pi-web-ui && npm run build -w @mariozechner/pi-reader-extension && npm run build -w @mariozechner/pi-agent && npm run build -w @mariozechner/pi", - "dev": "concurrently --names \"ai,web-ui,browser-ext,tui\" --prefix-colors \"cyan,green,yellow,magenta\" \"npm run dev -w @mariozechner/pi-ai\" \"npm run dev -w @mariozechner/pi-web-ui\" \"npm run dev -w @mariozechner/pi-reader-extension\" \"npm run dev -w @mariozechner/pi-tui\"", + "build": "npm run build -w @mariozechner/pi-tui && npm run build -w @mariozechner/pi-ai && npm run build -w @mariozechner/pi-web-ui && npm run build -w @mariozechner/pi-reader-extension && npm run build -w @mariozechner/pi-agent && npm run build -w @mariozechner/pi-proxy && npm run build -w @mariozechner/pi", + "dev": "concurrently --names \"ai,web-ui,browser-ext,tui,proxy\" --prefix-colors \"cyan,green,yellow,magenta,blue\" \"npm run dev -w @mariozechner/pi-ai\" \"npm run dev -w @mariozechner/pi-web-ui\" \"npm run dev -w @mariozechner/pi-reader-extension\" \"npm run dev -w @mariozechner/pi-tui\" \"npm run dev -w @mariozechner/pi-proxy\"", "check": "biome check --write . && npm run check --workspaces && tsc --noEmit", "test": "npm run test --workspaces --if-present", "version:patch": "npm version patch -ws --no-git-tag-version && node scripts/sync-versions.js", diff --git a/packages/proxy/README.md b/packages/proxy/README.md new file mode 100644 index 00000000..61b721f6 --- /dev/null +++ b/packages/proxy/README.md @@ -0,0 +1,67 @@ +# @mariozechner/pi-proxy + +CORS and authentication proxy for pi-ai. Enables browser clients to access OAuth-protected endpoints. + +## Usage + +### CORS Proxy + +Zero-config CORS proxy for development: + +```bash +# Run directly with tsx +npx tsx packages/proxy/src/cors-proxy.ts 3001 + +# Or use npm script +npm run dev -w @mariozechner/pi-proxy + +# Or install globally and use CLI +npm install -g @mariozechner/pi-proxy +pi-proxy 3001 +``` + +The proxy will forward requests to any URL: + +```javascript +// Instead of: +fetch('https://api.anthropic.com/v1/messages', { ... }) + +// Use: +fetch('http://localhost:3001?url=https://api.anthropic.com/v1/messages', { ... }) +``` + +### OAuth Integration + +For Anthropic OAuth tokens, configure your client to use the proxy: + +```typescript +import Anthropic from '@anthropic-ai/sdk'; + +const client = new Anthropic({ + apiKey: 'oauth_token_here', + baseURL: 'http://localhost:3001?url=https://api.anthropic.com' +}); +``` + +## Future Proxy Types + +- **BunnyCDN Edge Function**: Deploy as edge function +- **Managed Proxy**: Self-hosted with provider key management and credential auth +- **Cloudflare Worker**: Deploy as CF worker + +## Architecture + +The proxy: +1. Accepts requests with `?url=` query parameter +2. Forwards all headers (except `host`, `origin`) +3. Forwards request body for non-GET/HEAD requests +4. Returns response with CORS headers enabled +5. Strips CORS headers from upstream response + +## Development + +```bash +npm install +npm run build +npm run check +``` diff --git a/packages/proxy/package.json b/packages/proxy/package.json new file mode 100644 index 00000000..1322e630 --- /dev/null +++ b/packages/proxy/package.json @@ -0,0 +1,27 @@ +{ + "name": "@mariozechner/pi-proxy", + "version": "0.5.43", + "type": "module", + "description": "CORS and authentication proxy for pi-ai", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "bin": { + "pi-proxy": "dist/cli.js" + }, + "scripts": { + "clean": "rm -rf dist", + "build": "tsc", + "check": "biome check --write .", + "typecheck": "tsc --noEmit", + "dev": "tsx src/cors-proxy.ts 3001" + }, + "dependencies": { + "@hono/node-server": "^1.14.0", + "hono": "^4.6.16" + }, + "devDependencies": { + "@types/node": "^22.10.5", + "tsx": "^4.19.2", + "typescript": "^5.7.3" + } +} diff --git a/packages/proxy/src/cli.ts b/packages/proxy/src/cli.ts new file mode 100644 index 00000000..29ac06b3 --- /dev/null +++ b/packages/proxy/src/cli.ts @@ -0,0 +1,16 @@ +#!/usr/bin/env node +import { spawn } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const port = process.argv[2] || "3001"; + +// Run the CORS proxy +const child = spawn("node", [path.join(__dirname, "cors-proxy.js"), port], { + stdio: "inherit", +}); + +child.on("exit", (code) => { + process.exit(code || 0); +}); diff --git a/packages/proxy/src/cors-proxy.ts b/packages/proxy/src/cors-proxy.ts new file mode 100644 index 00000000..9fbeb387 --- /dev/null +++ b/packages/proxy/src/cors-proxy.ts @@ -0,0 +1,73 @@ +#!/usr/bin/env node +import { serve } from "@hono/node-server"; +import { Hono } from "hono"; +import { cors } from "hono/cors"; + +export function createCorsProxy() { + const app = new Hono(); + + // Enable CORS for all origins + app.use("*", cors()); + + // Proxy all requests + app.all("*", async (c) => { + const url = new URL(c.req.url); + const targetUrl = url.searchParams.get("url"); + + if (!targetUrl) { + return c.json({ error: "Missing 'url' query parameter" }, 400); + } + + try { + // Forward the request + const headers = new Headers(); + c.req.raw.headers.forEach((value, key) => { + // Skip host and origin headers + if (key.toLowerCase() !== "host" && key.toLowerCase() !== "origin") { + headers.set(key, value); + } + }); + + const response = await fetch(targetUrl, { + method: c.req.method, + headers, + body: c.req.method !== "GET" && c.req.method !== "HEAD" ? await c.req.raw.clone().arrayBuffer() : undefined, + }); + + // Forward response headers + const responseHeaders = new Headers(); + response.headers.forEach((value, key) => { + // Skip CORS headers (we handle them) + if (!key.toLowerCase().startsWith("access-control-")) { + responseHeaders.set(key, value); + } + }); + + // Return proxied response + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: responseHeaders, + }); + } catch (error) { + console.error("Proxy error:", error); + return c.json({ error: error instanceof Error ? error.message : "Proxy request failed" }, 502); + } + }); + + return app; +} + +// CLI entry point +if (import.meta.url === `file://${process.argv[1]}`) { + const app = createCorsProxy(); + const port = Number.parseInt(process.argv[2] || "3001", 10); + + console.log(`šŸ”Œ CORS proxy running on http://localhost:${port}`); + console.log(`Usage: http://localhost:${port}?url=`); + + serve({ + fetch: app.fetch, + port, + }); +} diff --git a/packages/proxy/src/index.ts b/packages/proxy/src/index.ts new file mode 100644 index 00000000..4e1bcaee --- /dev/null +++ b/packages/proxy/src/index.ts @@ -0,0 +1 @@ +export { createCorsProxy } from "./cors-proxy.js"; diff --git a/packages/proxy/tsconfig.json b/packages/proxy/tsconfig.json new file mode 100644 index 00000000..3502c876 --- /dev/null +++ b/packages/proxy/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*"] +} diff --git a/scripts/sync-versions.js b/scripts/sync-versions.js index 3ce159f5..eeb4ffe5 100644 --- a/scripts/sync-versions.js +++ b/scripts/sync-versions.js @@ -2,8 +2,8 @@ /** * Syncs inter-package dependency versions in the monorepo - * Updates @mariozechner/pi-tui and @mariozechner/pi-agent versions - * in dependent packages to match their current versions + * Updates internal @mariozechner/* package versions in dependent packages + * to match their current versions */ import { readFileSync, writeFileSync } from 'fs'; @@ -15,11 +15,15 @@ const packagesDir = join(process.cwd(), 'packages'); const tui = JSON.parse(readFileSync(join(packagesDir, 'tui/package.json'), 'utf8')); const agent = JSON.parse(readFileSync(join(packagesDir, 'agent/package.json'), 'utf8')); const pods = JSON.parse(readFileSync(join(packagesDir, 'pods/package.json'), 'utf8')); +const webUi = JSON.parse(readFileSync(join(packagesDir, 'web-ui/package.json'), 'utf8')); +const browserExtension = JSON.parse(readFileSync(join(packagesDir, 'browser-extension/package.json'), 'utf8')); console.log('Current versions:'); console.log(` @mariozechner/pi-tui: ${tui.version}`); console.log(` @mariozechner/pi-agent: ${agent.version}`); console.log(` @mariozechner/pi: ${pods.version}`); +console.log(` @mariozechner/pi-web-ui: ${webUi.version}`); +console.log(` @mariozechner/pi-reader-extension: ${browserExtension.version}`); // Update agent's dependency on tui if (agent.dependencies['@mariozechner/pi-tui']) { @@ -37,4 +41,12 @@ if (pods.dependencies['@mariozechner/pi-agent']) { console.log(`Updated pods' dependency on pi-agent: ${oldVersion} → ^${agent.version}`); } +// Update browser-extension's dependency on web-ui +if (browserExtension.dependencies['@mariozechner/pi-web-ui']) { + const oldVersion = browserExtension.dependencies['@mariozechner/pi-web-ui']; + browserExtension.dependencies['@mariozechner/pi-web-ui'] = `^${webUi.version}`; + writeFileSync(join(packagesDir, 'browser-extension/package.json'), JSON.stringify(browserExtension, null, '\t') + '\n'); + console.log(`Updated browser-extension's dependency on pi-web-ui: ${oldVersion} → ^${webUi.version}`); +} + console.log('\nāœ… Version sync complete!'); \ No newline at end of file