openzoo 0.34.4 → 0.34.6
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/bin/cursor-backend.js +21 -5
- package/lib/cursorbackend.js +38 -6
- package/package.json +1 -1
package/bin/cursor-backend.js
CHANGED
|
@@ -19,15 +19,31 @@ const port = Number(process.argv[2] || 443);
|
|
|
19
19
|
const modelsPath = process.argv[3];
|
|
20
20
|
const logPath = process.argv[4];
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
// WRITE EACH LINE ONCE. The launcher now redirects this process's stdout+stderr
|
|
23
|
+
// with `>>` to the SAME logPath (so a startup crash leaves evidence), so doing
|
|
24
|
+
// both an explicit appendFileSync AND a stdout write duplicated every single
|
|
25
|
+
// line in the file. stdout alone is the right channel: it reaches the log via
|
|
26
|
+
// the redirect, and it still shows up when the backend is run by hand on a
|
|
27
|
+
// terminal for debugging.
|
|
28
|
+
const log = (s) => { process.stdout.write(`${s}\n`); };
|
|
27
29
|
|
|
28
30
|
let models = [];
|
|
29
31
|
try { models = JSON.parse(fs.readFileSync(modelsPath, 'utf8')); }
|
|
30
32
|
catch (e) { log(`cursor-backend: could not read models (${e.message})`); process.exit(1); }
|
|
31
33
|
|
|
34
|
+
// NEVER LET ONE BAD REQUEST KILL THE SERVER. The h2 header bug threw out of an
|
|
35
|
+
// async handler, which is an unhandled rejection / uncaught throw at process
|
|
36
|
+
// level — node tore the whole backend down mid-request, so :443 went dead and
|
|
37
|
+
// every later attempt logged ECONNRESET with no listener behind it. This is a
|
|
38
|
+
// static impersonation server; staying up while logging the fault is always
|
|
39
|
+
// better than exiting and blackholing the host the app depends on.
|
|
40
|
+
process.on('uncaughtException', (e) => {
|
|
41
|
+
log(`cursor-backend: UNCAUGHT ${e && e.code ? e.code + ' ' : ''}${e && e.message}`);
|
|
42
|
+
if (e && e.stack) log(String(e.stack).split('\n').slice(1, 4).join('\n'));
|
|
43
|
+
});
|
|
44
|
+
process.on('unhandledRejection', (e) => {
|
|
45
|
+
log(`cursor-backend: UNHANDLED REJECTION ${e && e.message ? e.message : e}`);
|
|
46
|
+
});
|
|
47
|
+
|
|
32
48
|
startCursorBackend({ port, models, log });
|
|
33
49
|
log(`cursor-backend: standalone up on :${port} (${models.length} models)`);
|
package/lib/cursorbackend.js
CHANGED
|
@@ -35,21 +35,39 @@ import { encodeAvailableModels, encodeForMethod } from './cursorapi.js';
|
|
|
35
35
|
|
|
36
36
|
const TLS_DIR = path.join(os.homedir(), '.openzoo', 'cursor-tls');
|
|
37
37
|
const CURSOR_HOSTS = ['api2.cursor.sh', 'api3.cursor.sh', 'api4.cursor.sh', 'repo42.cursor.sh'];
|
|
38
|
+
// THE CERT MUST COVER ANTHROPIC TOO. This server impersonates api.anthropic.com
|
|
39
|
+
// for `claude --desktop`, but the SAN only ever listed the cursor hosts, so the
|
|
40
|
+
// client got a cert valid for a name it never asked for and killed the
|
|
41
|
+
// handshake before ALPN — logged as `HANDSHAKE FAILED ECONNRESET alpn=?`, with
|
|
42
|
+
// no request ever reaching us. Same cert, one more pair of names.
|
|
43
|
+
const ANTHROPIC_TLS_HOSTS = ['api.anthropic.com', 'api-staging.anthropic.com'];
|
|
44
|
+
const TLS_HOSTS = [...CURSOR_HOSTS, ...ANTHROPIC_TLS_HOSTS];
|
|
38
45
|
|
|
39
|
-
/** Generate (once) a self-signed cert covering
|
|
40
|
-
* on every mac/linux; this is the only external tool and it is not a trust op. */
|
|
46
|
+
/** Generate (once) a self-signed cert covering every host we impersonate. openssl
|
|
47
|
+
* is on every mac/linux; this is the only external tool and it is not a trust op. */
|
|
41
48
|
export function ensureCert(log = () => {}) {
|
|
42
49
|
const cert = path.join(TLS_DIR, 'cert.pem');
|
|
43
50
|
const key = path.join(TLS_DIR, 'key.pem');
|
|
44
|
-
|
|
51
|
+
// STALE-CERT INVALIDATION. A plain existence check meant anyone who had ever
|
|
52
|
+
// run an older build kept their cursor-only cert forever and never got the
|
|
53
|
+
// anthropic names — the fix would ship and appear to do nothing. Record the
|
|
54
|
+
// SAN set the cert was minted for and re-mint whenever that set changes.
|
|
55
|
+
const stamp = path.join(TLS_DIR, 'san.txt');
|
|
56
|
+
const want = TLS_HOSTS.join(',');
|
|
57
|
+
try {
|
|
58
|
+
fs.accessSync(cert); fs.accessSync(key);
|
|
59
|
+
if (fs.readFileSync(stamp, 'utf8').trim() === want) return { cert, key };
|
|
60
|
+
log('cursor-tls: cert predates the current host list — re-minting');
|
|
61
|
+
} catch { /* make it */ }
|
|
45
62
|
fs.mkdirSync(TLS_DIR, { recursive: true });
|
|
46
|
-
const san = `subjectAltName=${
|
|
63
|
+
const san = `subjectAltName=${TLS_HOSTS.map((h) => `DNS:${h}`).join(',')}`;
|
|
47
64
|
execFileSync('openssl', [
|
|
48
65
|
'req', '-x509', '-newkey', 'rsa:2048', '-nodes',
|
|
49
66
|
'-keyout', key, '-out', cert, '-days', '3650',
|
|
50
67
|
'-subj', '/CN=api2.cursor.sh', '-addext', san,
|
|
51
68
|
], { stdio: 'ignore' });
|
|
52
|
-
|
|
69
|
+
fs.writeFileSync(stamp, want);
|
|
70
|
+
log(`cursor-tls: self-signed cert minted at ${TLS_DIR} covering ${TLS_HOSTS.length} hosts (no CA, no trust prompt)`);
|
|
53
71
|
return { cert, key };
|
|
54
72
|
}
|
|
55
73
|
|
|
@@ -287,7 +305,21 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
|
|
|
287
305
|
});
|
|
288
306
|
const buf = Buffer.from(await up.arrayBuffer());
|
|
289
307
|
const h = { ...CORS };
|
|
290
|
-
|
|
308
|
+
// STRIP EVERY CONNECTION-SPECIFIC HEADER, NOT JUST `connection`.
|
|
309
|
+
// RFC 7540 §8.1.2.2 bans all of these on HTTP/2, and Node enforces it
|
|
310
|
+
// by THROWING ERR_HTTP2_INVALID_CONNECTION_HEADERS out of writeHead.
|
|
311
|
+
// The local proxy answers with `keep-alive`, which sailed past a
|
|
312
|
+
// denylist that only knew about `connection` — so the very first
|
|
313
|
+
// desktop request killed the whole backend process (uncaught, since
|
|
314
|
+
// this is inside the async handler), and every later attempt got
|
|
315
|
+
// 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
|
+
]);
|
|
322
|
+
up.headers.forEach((v, k) => { if (!HOP_BY_HOP.has(k.toLowerCase())) h[k] = v; });
|
|
291
323
|
res.writeHead(up.status, h);
|
|
292
324
|
res.end(buf);
|
|
293
325
|
log(`cursor-backend: #${conns} ${req.method} ${full} ANTHROPIC -> proxy (${up.status}, ${buf.length}b)`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.6",
|
|
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",
|