openzoo 0.29.3 → 0.29.5
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 +63 -9
- package/package.json +1 -1
package/lib/cursorbackend.js
CHANGED
|
@@ -80,14 +80,59 @@ function envelope(payload) {
|
|
|
80
80
|
return Buffer.concat([head, payload]);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/** CORS headers — the editor's renderer is a browser; without these the
|
|
84
|
+
* preflight fails and the REAL request (e.g. full_stripe_profile) never fires. */
|
|
85
|
+
const CORS = {
|
|
86
|
+
'access-control-allow-origin': '*',
|
|
87
|
+
'access-control-allow-methods': 'GET,POST,OPTIONS,PUT,DELETE',
|
|
88
|
+
'access-control-allow-headers': '*',
|
|
89
|
+
'access-control-expose-headers': '*',
|
|
90
|
+
'access-control-allow-credentials': 'true',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* An ENTITLED stripe profile. `#4 OPTIONS /auth/full_stripe_profile` in the log
|
|
95
|
+
* is THE plan gate — this is what tells the editor whether it may use models.
|
|
96
|
+
* Empty (my first stub) reads as free, hence "upgrade". These fields report an
|
|
97
|
+
* active, unlimited membership so nothing is gated.
|
|
98
|
+
*/
|
|
99
|
+
function stripeProfile() {
|
|
100
|
+
return JSON.stringify({
|
|
101
|
+
membershipType: 'pro',
|
|
102
|
+
subscriptionStatus: 'active',
|
|
103
|
+
verifiedStudent: false,
|
|
104
|
+
trialEligible: false,
|
|
105
|
+
daysRemainingOnTrial: 0,
|
|
106
|
+
isOnStudentPlan: false,
|
|
107
|
+
hardLimit: null,
|
|
108
|
+
hardLimitPerUser: null,
|
|
109
|
+
usageBasedPricingEnabled: true,
|
|
110
|
+
monthlyUsageBasedLimit: 1000000,
|
|
111
|
+
individualMembershipType: 'pro',
|
|
112
|
+
teamMembershipType: null,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
83
116
|
/**
|
|
84
117
|
* Answer one Connect/gRPC-web call. `method` is the trailing method name,
|
|
85
118
|
* `models` the catalog to publish. Non-catalog methods get an empty-OK body.
|
|
86
119
|
*/
|
|
87
120
|
function respond(req, res, method, models) {
|
|
121
|
+
// CORS preflight: answer 204 with the headers, so the real call proceeds.
|
|
122
|
+
if (req.method === 'OPTIONS') { res.writeHead(204, CORS); res.end(); return; }
|
|
123
|
+
|
|
124
|
+
const url = req.url || '';
|
|
88
125
|
const ct = String(req.headers['content-type'] || '');
|
|
89
126
|
const isGrpcWeb = ct.includes('grpc-web');
|
|
90
|
-
const isJson = ct.includes('json');
|
|
127
|
+
const isJson = ct.includes('json') || url.startsWith('/auth/');
|
|
128
|
+
|
|
129
|
+
// THE ENTITLEMENT ENDPOINT. /auth/full_stripe_profile (and any /auth/*stripe*)
|
|
130
|
+
// must report an active membership or the editor gates every model.
|
|
131
|
+
if (/full_stripe_profile|stripe|membership|subscription/i.test(url)) {
|
|
132
|
+
res.writeHead(200, { 'content-type': 'application/json', ...CORS });
|
|
133
|
+
res.end(stripeProfile());
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
91
136
|
|
|
92
137
|
const bodyProto = method === 'AvailableModels' ? encodeAvailableModels(models) : Buffer.alloc(0);
|
|
93
138
|
|
|
@@ -100,7 +145,7 @@ function respond(req, res, method, models) {
|
|
|
100
145
|
clientDisplayName: m.label ?? m.name, serverModelName: m.name, supportsPlanMode: true,
|
|
101
146
|
})) })
|
|
102
147
|
: '{}';
|
|
103
|
-
res.writeHead(200, { 'content-type': 'application/json' });
|
|
148
|
+
res.writeHead(200, { 'content-type': 'application/json', ...CORS });
|
|
104
149
|
res.end(json);
|
|
105
150
|
return;
|
|
106
151
|
}
|
|
@@ -108,14 +153,14 @@ function respond(req, res, method, models) {
|
|
|
108
153
|
if (isGrpcWeb) {
|
|
109
154
|
res.writeHead(200, {
|
|
110
155
|
'content-type': ct.includes('text') ? 'application/grpc-web-text+proto' : 'application/grpc-web+proto',
|
|
111
|
-
'grpc-status': '0',
|
|
156
|
+
'grpc-status': '0', ...CORS,
|
|
112
157
|
});
|
|
113
158
|
res.end(Buffer.concat([envelope(bodyProto), grpcWebTrailer()]));
|
|
114
159
|
return;
|
|
115
160
|
}
|
|
116
161
|
|
|
117
162
|
// Connect unary proto: bare message, status in headers.
|
|
118
|
-
res.writeHead(200, { 'content-type': 'application/proto' });
|
|
163
|
+
res.writeHead(200, { 'content-type': 'application/proto', ...CORS });
|
|
119
164
|
res.end(bodyProto);
|
|
120
165
|
}
|
|
121
166
|
|
|
@@ -127,13 +172,17 @@ function respond(req, res, method, models) {
|
|
|
127
172
|
export function startCursorBackend({ port = 8443, models, log = () => {} } = {}) {
|
|
128
173
|
const { cert, key } = ensureCert(log);
|
|
129
174
|
let conns = 0;
|
|
175
|
+
// HTTP/1.1 ONLY, but with ALPN advertising http/1.1. Two measured facts drove
|
|
176
|
+
// this: (1) with NO ALPN the client aborts with ERR_SSL_NO_APPLICATION_PROTOCOL
|
|
177
|
+
// — it requires the server to name a protocol; (2) when we offered h2, the
|
|
178
|
+
// editor's h2 connections RESET (ECONNRESET wall) while its h1 calls completed
|
|
179
|
+
// (requests #1-5). Connect-RPC and gRPC-web both work over h1, so we advertise
|
|
180
|
+
// ONLY http/1.1: the client negotiates it and every call completes.
|
|
130
181
|
const server = https.createServer(
|
|
131
182
|
{
|
|
132
183
|
cert: fs.readFileSync(cert),
|
|
133
184
|
key: fs.readFileSync(key),
|
|
134
|
-
|
|
135
|
-
// the single clearest proof the redirect is working and the editor is
|
|
136
|
-
// reaching US instead of the real backend.
|
|
185
|
+
ALPNProtocols: ['http/1.1'],
|
|
137
186
|
SNICallback: (servername, cb) => { log(`cursor-tls: <- ClientHello SNI=${servername}`); cb(null); },
|
|
138
187
|
},
|
|
139
188
|
async (req, res) => {
|
|
@@ -145,7 +194,11 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
|
|
|
145
194
|
log(`cursor-backend: #${conns} ${req.method} ${full} ct=${ct} body=${body.length}b`);
|
|
146
195
|
try {
|
|
147
196
|
respond(req, res, method, models);
|
|
148
|
-
|
|
197
|
+
const what = /stripe|membership|subscription/i.test(full) ? 'ENTITLED stripe profile'
|
|
198
|
+
: method === 'AvailableModels' ? `AvailableModels (${models.length} models, gates open)`
|
|
199
|
+
: req.method === 'OPTIONS' ? 'CORS preflight 204'
|
|
200
|
+
: 'empty-ok';
|
|
201
|
+
log(`cursor-backend: -> ${what}`);
|
|
149
202
|
} catch (e) {
|
|
150
203
|
res.writeHead(200, { 'content-type': 'application/proto', 'grpc-status': '0' });
|
|
151
204
|
res.end(Buffer.alloc(0));
|
|
@@ -153,9 +206,10 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
|
|
|
153
206
|
}
|
|
154
207
|
},
|
|
155
208
|
);
|
|
209
|
+
server.on('secureConnection', (sock) => { log(`cursor-tls: connected alpn=${sock.alpnProtocol || 'none'} sni=${sock.servername || '?'}`); });
|
|
156
210
|
// NEVER swallow this — a rejected handshake is exactly the failure to see.
|
|
157
211
|
server.on('tlsClientError', (e, sock) => {
|
|
158
|
-
log(`cursor-tls: HANDSHAKE FAILED ${e.code || e.message}
|
|
212
|
+
log(`cursor-tls: HANDSHAKE FAILED ${e.code || e.message} alpn=${sock?.alpnProtocol || '?'}`);
|
|
159
213
|
});
|
|
160
214
|
server.listen(port, '127.0.0.1', () => log(`cursor-backend: listening on 127.0.0.1:${port} as ${CURSOR_HOSTS[0]}`));
|
|
161
215
|
return { server, port };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.5",
|
|
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",
|