Proxy package

This commit is contained in:
Mario Zechner 2025-10-05 19:02:15 +02:00
parent aaea0f4600
commit 66f092c0c6
9 changed files with 249 additions and 4 deletions

41
package-lock.json generated
View file

@ -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",

View file

@ -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",

67
packages/proxy/README.md Normal file
View file

@ -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=<target>` 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
```

View file

@ -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"
}
}

16
packages/proxy/src/cli.ts Normal file
View file

@ -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);
});

View file

@ -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=<target-url>`);
serve({
fetch: app.fetch,
port,
});
}

View file

@ -0,0 +1 @@
export { createCorsProxy } from "./cors-proxy.js";

View file

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"]
}

View file

@ -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!');