openzoo 0.34.8 → 0.34.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -210,6 +210,71 @@ async function handleStreamChat(req, res, body, log) {
210
210
  else { const end = Buffer.from('{}'); const h = Buffer.alloc(5); h.writeUInt8(0x02, 0); h.writeUInt32BE(end.length, 1); res.end(Buffer.concat([h, end])); }
211
211
  }
212
212
 
213
+ /** Headers that are illegal to copy onto an HTTP/2 response (RFC 7540 §8.1.2.2),
214
+ * plus the length/encoding fields that no longer describe our re-buffered body. */
215
+ const HOP_BY_HOP = new Set([
216
+ 'connection', 'keep-alive', 'upgrade', 'proxy-connection',
217
+ 'transfer-encoding', 'te', 'trailer', 'proxy-authenticate',
218
+ 'proxy-authorization', 'content-encoding', 'content-length',
219
+ ]);
220
+
221
+ /**
222
+ * FORWARD A NON-INFERENCE REQUEST TO THE REAL ANTHROPIC API.
223
+ *
224
+ * We hijack api.anthropic.com at the DNS level, which catches far more than
225
+ * inference: the desktop app is OAuth/subscription-bound and boots by calling
226
+ * /api/organizations, /api/bootstrap, /api/account on the SAME host. Those are
227
+ * not ours to answer — the local proxy 404s them, the app cannot establish a
228
+ * session, and it hangs until macOS offers Force Quit.
229
+ *
230
+ * So for anything that is not inference we act as a plain reverse proxy to the
231
+ * genuine API, preserving the client's auth headers verbatim so the app's own
232
+ * OAuth session keeps working untouched.
233
+ *
234
+ * WE CANNOT RESOLVE THE NAME NORMALLY — /etc/hosts points it at us, so a plain
235
+ * fetch would loop straight back into this server. Resolve against public DNS
236
+ * over UDP (which ignores /etc/hosts entirely), cache it, and dial the address
237
+ * with the Host/SNI still set to the real hostname so TLS and routing succeed.
238
+ */
239
+ let realIpCache = null;
240
+ async function resolveRealAnthropic(host) {
241
+ if (realIpCache) return realIpCache;
242
+ const { Resolver } = await import('node:dns/promises');
243
+ const r = new Resolver();
244
+ r.setServers(['1.1.1.1', '8.8.8.8']);
245
+ const [ip] = await r.resolve4(host);
246
+ realIpCache = ip;
247
+ return ip;
248
+ }
249
+
250
+ async function passthroughToRealAnthropic(req, res, body, host, full, log) {
251
+ try {
252
+ const ip = await resolveRealAnthropic(host);
253
+ const headers = {};
254
+ for (const [k, v] of Object.entries(req.headers)) {
255
+ if (k.startsWith(':') || HOP_BY_HOP.has(k)) continue;
256
+ headers[k] = Array.isArray(v) ? v.join(', ') : String(v);
257
+ }
258
+ headers.host = host;
259
+ const up = await fetch(`https://${ip}${full}`, {
260
+ method: req.method,
261
+ headers,
262
+ body: (req.method === 'GET' || req.method === 'HEAD') ? undefined : body,
263
+ redirect: 'manual',
264
+ });
265
+ const buf = Buffer.from(await up.arrayBuffer());
266
+ const h = {};
267
+ up.headers.forEach((v, k) => { if (!HOP_BY_HOP.has(k.toLowerCase())) h[k] = v; });
268
+ res.writeHead(up.status, h);
269
+ res.end(buf);
270
+ log(`cursor-backend: ${req.method} ${full} -> REAL anthropic (${up.status}, ${buf.length}b)`);
271
+ } catch (e) {
272
+ res.writeHead(502, { 'content-type': 'application/json' });
273
+ res.end(JSON.stringify({ type: 'error', error: { type: 'api_error', message: `passthrough failed: ${e.message}` } }));
274
+ log(`cursor-backend: passthrough ${full} FAILED: ${e.message}`);
275
+ }
276
+ }
277
+
213
278
  /**
214
279
  * Answer one Connect/gRPC-web call. `method` is the trailing method name,
215
280
  * `models` the catalog to publish. Non-catalog methods get an empty-OK body.
@@ -296,7 +361,20 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
296
361
  // whole translate+x402 path. (SNI is checked because the Host header may be
297
362
  // absent on h2.)
298
363
  const host = String(req.headers[':authority'] || req.headers.host || req.socket?.servername || '');
299
- if (/anthropic\.com$/.test(host) || /\/v1\/messages\b/.test(full)) {
364
+ const isAnthropic = /anthropic\.com$/.test(host);
365
+ // INTERCEPT INFERENCE ONLY. Matching the whole HOST sent every request the
366
+ // desktop app makes to the local proxy — including the session bootstrap
367
+ // (/api/organizations, /api/bootstrap, /api/account), which the proxy does
368
+ // not implement and answered 404. The app is OAuth/subscription-bound, so
369
+ // a 404 on bootstrap means it can never establish a session: it wedges and
370
+ // macOS offers Force Quit. Only /v1/messages (and /v1/complete) are ours;
371
+ // everything else on this host MUST reach the real Anthropic API.
372
+ const isInference = /\/v1\/(messages|complete)\b/.test(full);
373
+ if (isAnthropic && !isInference) {
374
+ await passthroughToRealAnthropic(req, res, body, host, full, log);
375
+ return;
376
+ }
377
+ if (isAnthropic || isInference) {
300
378
  try {
301
379
  const up = await fetch(`http://127.0.0.1:8402${full}`, {
302
380
  method: req.method,
@@ -313,12 +391,7 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
313
391
  // desktop request killed the whole backend process (uncaught, since
314
392
  // this is inside the async handler), and every later attempt got
315
393
  // ECONNRESET with nothing listening. Content-length goes too: the
316
- // body was re-buffered, so let Node recompute it.
317
- const HOP_BY_HOP = new Set([
318
- 'connection', 'keep-alive', 'upgrade', 'proxy-connection',
319
- 'transfer-encoding', 'te', 'trailer', 'proxy-authenticate',
320
- 'proxy-authorization', 'content-encoding', 'content-length',
321
- ]);
394
+ // body was re-buffered, so let Node recompute it. (Shared set above.)
322
395
  up.headers.forEach((v, k) => { if (!HOP_BY_HOP.has(k.toLowerCase())) h[k] = v; });
323
396
  res.writeHead(up.status, h);
324
397
  res.end(buf);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.34.8",
3
+ "version": "0.34.10",
4
4
  "description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
5
5
  "license": "MIT",
6
6
  "type": "module",