openzoo 0.29.3 → 0.29.4

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.
@@ -26,7 +26,7 @@
26
26
  * content-type and answer in kind: bare message for connect unary proto, enveloped
27
27
  * + trailers for grpc-web. Both are handled below.
28
28
  */
29
- import https from 'node:https';
29
+ import http2 from 'node:http2';
30
30
  import fs from 'node:fs';
31
31
  import os from 'node:os';
32
32
  import path from 'node:path';
@@ -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;
130
- const server = https.createServer(
175
+ // HTTP/2 with ALPN, HTTP/1.1 fallback. The editor's Connect-RPC client speaks
176
+ // h2; a plain https (h1-only) server with no ALPN made most handshakes fail
177
+ // with ERR_SSL_NO_APPLICATION_PROTOCOL / ECONNRESET (measured in the log), so
178
+ // only a couple of h1 requests ever completed. allowHTTP1 keeps h1 working too;
179
+ // the (req,res) compatibility handler serves both.
180
+ const server = http2.createSecureServer(
131
181
  {
132
182
  cert: fs.readFileSync(cert),
133
183
  key: fs.readFileSync(key),
134
- // Log which hostname the editor asked for on the TLS handshake — this is
135
- // the single clearest proof the redirect is working and the editor is
136
- // reaching US instead of the real backend.
184
+ allowHTTP1: true,
185
+ ALPNProtocols: ['h2', '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
- log(`cursor-backend: -> ${method === 'AvailableModels' ? `AvailableModels (${models.length} models, gates open)` : 'empty-ok'}`);
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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.29.3",
3
+ "version": "0.29.4",
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",