openzoo 0.34.9 → 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.
- package/lib/cursorbackend.js +80 -7
- package/package.json +1 -1
package/lib/cursorbackend.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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",
|