openzoo 0.30.5 → 0.31.0

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.
@@ -97,8 +97,15 @@ const CORS = {
97
97
  * active, unlimited membership so nothing is gated.
98
98
  */
99
99
  function stripeProfile() {
100
+ // MUST MATCH the membershipType written into the applicationUser blob — Cursor
101
+ // refreshes the blob FROM this profile on launch, so a hardcoded 'pro' here
102
+ // overwrote our 'ultra' and the free-plan gate ("free plans can only use Auto")
103
+ // fired anyway. Same env var, one source of truth.
104
+ const m = process.env.OPENZOO_MEMBERSHIP || 'pro';
100
105
  return JSON.stringify({
101
- membershipType: 'pro',
106
+ membershipType: m,
107
+ individualMembershipType: m,
108
+ teamMembershipType: null,
102
109
  subscriptionStatus: 'active',
103
110
  verifiedStudent: false,
104
111
  trialEligible: false,
@@ -108,8 +115,6 @@ function stripeProfile() {
108
115
  hardLimitPerUser: null,
109
116
  usageBasedPricingEnabled: true,
110
117
  monthlyUsageBasedLimit: 1000000,
111
- individualMembershipType: 'pro',
112
- teamMembershipType: null,
113
118
  });
114
119
  }
115
120
 
package/lib/cursorcfg.js CHANGED
@@ -144,14 +144,13 @@ export function writeEditorProviderConfig(which, { baseUrl, models }) {
144
144
  const before = { openAIBaseUrl: doc.openAIBaseUrl, models: (doc.availableAPIKeyModels || []).length };
145
145
  doc.openAIBaseUrl = baseUrl;
146
146
  doc.useOpenAIKey = true;
147
- // THE MODEL-SELECTION GATE READS MEMBERSHIP FROM RIGHT HERE. Traced in the
148
- // bundle: membershipType(){ return this.reactiveStorageService
149
- // .applicationUserPersistentStorage.membershipType } i.e. THIS blob, not
150
- // cursorAuth/stripeMembershipType. Setting it entitles model selection; the
151
- // api2 impersonation keeps the editor from re-syncing it back to free.
152
- // OPENZOO_MEMBERSHIP overrides (pro | pro_plus | enterprise | ultra).
153
- doc.membershipType = process.env.OPENZOO_MEMBERSHIP || 'pro';
154
- doc.subscriptionStatus = 'active';
147
+ // MEMBERSHIP FORGERY IS OPT-IN. Setting membershipType here spoofs Cursor's
148
+ // paid-tier gate on an unpaid account — only do it when the operator explicitly
149
+ // asks with OPENZOO_MEMBERSHIP. Default writes leave the real membership alone.
150
+ if (process.env.OPENZOO_MEMBERSHIP) {
151
+ doc.membershipType = process.env.OPENZOO_MEMBERSHIP;
152
+ doc.subscriptionStatus = 'active';
153
+ }
155
154
  // Merge, don't clobber: a user may have their own custom models listed.
156
155
  const existing = Array.isArray(doc.availableAPIKeyModels) ? doc.availableAPIKeyModels : [];
157
156
  const names = new Set(existing.map((m) => (typeof m === 'string' ? m : m?.name)).filter(Boolean));
@@ -265,7 +264,7 @@ export function writeEditorProviderConfig(which, { baseUrl, models }) {
265
264
  * that clobbers them. Removable with unpinEditorProviderConfig.
266
265
  */
267
266
  export function pinEditorProviderConfig(which, { baseUrl, models }) {
268
- const mem = process.env.OPENZOO_MEMBERSHIP || 'pro';
267
+ const mem = process.env.OPENZOO_MEMBERSHIP || null;
269
268
  const db = storagePath(which);
270
269
  if (!fs.existsSync(db)) return null;
271
270
  const esc = (v) => String(v).replace(/'/g, "''");
@@ -292,8 +291,8 @@ CREATE TRIGGER openzoo_pin AFTER UPDATE ON ItemTable
292
291
  WHEN NEW.key = '${KEY}'
293
292
  AND (json_extract(NEW.value,'$.openAIBaseUrl') IS NOT '${base}'
294
293
  OR json_extract(NEW.value,'$.featureModelConfigs.composer.defaultModel') IS NOT '${primary}'
295
- OR json_array_length(json_extract(NEW.value,'$.availableDefaultModels2')) IS NOT ${models.length}
296
- OR json_extract(NEW.value,'$.membershipType') IS NOT '${mem}')
294
+ OR json_array_length(json_extract(NEW.value,'$.availableDefaultModels2')) IS NOT ${models.length}${mem ? `
295
+ OR json_extract(NEW.value,'$.membershipType') IS NOT '${mem}'` : ''})
297
296
  BEGIN
298
297
  UPDATE ItemTable SET value = json_set(
299
298
  NEW.value,
@@ -302,9 +301,9 @@ BEGIN
302
301
  '$.availableAPIKeyModels', json('${modelJson}'),
303
302
  '$.availableDefaultModels2', json('${uiJson}'),
304
303
  '$.featureModelConfigs.composer.defaultModel', '${primary}',
305
- '$.featureModelConfigs.cmdK.defaultModel', '${primary}',
304
+ '$.featureModelConfigs.cmdK.defaultModel', '${primary}'${mem ? `,
306
305
  '$.membershipType', '${mem}',
307
- '$.subscriptionStatus', 'active'
306
+ '$.subscriptionStatus', 'active'` : ''}
308
307
  ) WHERE key = NEW.key;
309
308
  END;`;
310
309
  try { sqlite(db, sql); } catch (e) { return { error: e.message }; }
package/lib/hosts.js CHANGED
@@ -147,6 +147,9 @@ export function unredirect443() {
147
147
  * runs privileged, never the wallet-holding proxy. unbindBackend443 kills it.
148
148
  */
149
149
  export function bindBackend443(modelsPath, logPath, log = console.log) {
150
+ // sudo strips the environment, so forward the one var the backend needs to
151
+ // report the right membership in the stripe profile it serves.
152
+ const memEnv = `OPENZOO_MEMBERSHIP='${(process.env.OPENZOO_MEMBERSHIP || 'pro').replace(/'/g, '')}'`;
150
153
  const here = path.dirname(fileURLToPath(import.meta.url));
151
154
  const script = path.join(here, '..', 'bin', 'cursor-backend.js');
152
155
  const node = process.execPath;
@@ -172,7 +175,7 @@ export function bindBackend443(modelsPath, logPath, log = console.log) {
172
175
  + `pkill -9 -f 'cursor-backend.js' 2>/dev/null; `
173
176
  + `for p in $(lsof -nP -iTCP:443 -sTCP:LISTEN -t 2>/dev/null) $(lsof -nP -iTCP:8443 -sTCP:LISTEN -t 2>/dev/null); do kill -9 "$p" 2>/dev/null; done; `
174
177
  + `for i in 1 2 3 4 5 6 7 8 9 10; do lsof -nP -iTCP:443 -sTCP:LISTEN -t >/dev/null 2>&1 || break; sleep 0.5; done; `
175
- + `nohup '${node}' '${script}' 443 '${modelsPath}' '${logPath}' >/dev/null 2>&1 & `
178
+ + `nohup ${memEnv} '${node}' '${script}' 443 '${modelsPath}' '${logPath}' >/dev/null 2>&1 & `
176
179
  + 'sleep 1; true',
177
180
  );
178
181
  return { ok };
package/lib/setup.js CHANGED
@@ -335,7 +335,7 @@ export async function setupEditor(which, target) {
335
335
  // rejected our cert — the ECONNRESET-before-ALPN wall in the backend log, and
336
336
  // exactly the model/chat calls we need. NODE_TLS_REJECT_UNAUTHORIZED=0 is the
337
337
  // Node-side switch. Only set under takeover, where we own the endpoint.
338
- if (which === 'cursor' && !process.argv.includes('--no-takeover')) env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
338
+ if (which === 'cursor' && process.argv.includes('--takeover')) env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
339
339
 
340
340
  console.log('');
341
341
  console.log(`mcp: ${mcpFile} (openzoo: zoo_bind, zoo_ask, zoo_models, zoo_wallet, zoo_contexts)`);
@@ -459,7 +459,13 @@ export async function setupEditor(which, target) {
459
459
  // DEFAULT ON. Takeover is what makes a plan-less account route, and it is
460
460
  // harmless on an entitled one (the editor just reads our catalog instead of
461
461
  // theirs), so it runs unless explicitly disabled with --no-takeover.
462
- const doTakeover = target0 === 'cursor' && !process.argv.includes('--no-takeover');
462
+ // OPT-IN ONLY. Takeover impersonates Cursor's backend to unlock paid-tier
463
+ // model selection on an unpaid account — that is defeating Cursor's own
464
+ // subscription gate, not routing, so it is NOT the default. Plain
465
+ // `openzoo cursor` writes the base URL + models and routes Auto (and any model
466
+ // an entitled account can pick) through the zoo. --takeover is the escape
467
+ // hatch for those who understand what it does.
468
+ const doTakeover = target0 === 'cursor' && process.argv.includes('--takeover');
463
469
  if (doTakeover) {
464
470
  try {
465
471
  const { ensureCert } = await import('./cursorbackend.js');
@@ -550,7 +556,7 @@ export async function setupEditor(which, target) {
550
556
  const args = [cwd];
551
557
  // Trust our self-signed impersonation cert without any CA install — this flag
552
558
  // is the entire reason no trust prompt is needed. Only added under --takeover.
553
- if (target0 === 'cursor' && !process.argv.includes('--no-takeover')) args.unshift('--ignore-certificate-errors');
559
+ if (doTakeover) args.unshift('--ignore-certificate-errors');
554
560
  if (useProfile) {
555
561
  fs.mkdirSync(PROFILE_DIR, { recursive: true });
556
562
  args.unshift(`--user-data-dir=${PROFILE_DIR}`, `--extensions-dir=${path.join(PROFILE_DIR, 'extensions')}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.30.5",
3
+ "version": "0.31.0",
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",