openzoo 0.9.5 → 0.9.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.
Files changed (2) hide show
  1. package/lib/proxy.js +30 -5
  2. package/package.json +1 -1
package/lib/proxy.js CHANGED
@@ -271,12 +271,37 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
271
271
  }
272
272
  if (viaTunnel) {
273
273
  const got = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
274
- if (got !== tunnelGate.token) {
275
- log(`public url: 401 ${req.method} ${req.url}`);
276
- jsonErr(res, 401, 'unauthorized: this openzoo public URL requires the api key printed at startup');
277
- return;
274
+ const authed = got === tunnelGate.token;
275
+ const p = (req.url || '').split('?')[0];
276
+ // DISCOVERY IS FREE. An agent probing this URL cold should be able to
277
+ // work out what it is and what to ask its operator for — a bare 401 on
278
+ // every path just sends it spelunking through the operator's machine.
279
+ // Reads that cost nothing and leak nothing (the catalog is public on
280
+ // the zoo anyway) go through without the key; money paths stay gated.
281
+ if (!authed) {
282
+ if (req.method === 'GET' && (p === '/' || p === '/v1' || p === '/v1/info')) {
283
+ res.writeHead(200, { 'content-type': 'application/json' });
284
+ res.end(JSON.stringify({
285
+ name: 'openzoo proxy (public tunnel)',
286
+ what: 'OpenAI-compatible pay-per-call proxy — the operator\'s wallet pays per request via x402; no account, no signup.',
287
+ authentication: 'All POST endpoints require `Authorization: Bearer <api key>`. The key is printed in the operator\'s terminal at proxy startup — ask them for it. It is NOT guessable.',
288
+ endpoints: {
289
+ 'GET /v1/models': 'model catalog with pricing + context_length — no key needed',
290
+ 'GET /v1/models/{id}': 'single-model probe — no key needed',
291
+ 'POST /v1/chat/completions': 'chat (streaming supported, any model id — unknown ids are matched to the nearest served model) — key required',
292
+ },
293
+ docs: 'https://openzoo.fun · https://www.npmjs.com/package/openzoo',
294
+ }, null, 2));
295
+ return;
296
+ }
297
+ const freeRead = req.method === 'GET' && (p === '/v1/models' || p.startsWith('/v1/models/'));
298
+ if (!freeRead) {
299
+ log(`public url: 401 ${req.method} ${req.url}`);
300
+ jsonErr(res, 401, 'unauthorized: this openzoo public URL requires the api key printed at the operator\'s proxy startup — GET / for discovery, GET /v1/models is open');
301
+ return;
302
+ }
278
303
  }
279
- if (tunnelSpent >= tunnelGate.sessionMaxUsd) {
304
+ if (authed && tunnelSpent >= tunnelGate.sessionMaxUsd) {
280
305
  jsonErr(res, 402, `openzoo public-URL session cap reached ($${tunnelGate.sessionMaxUsd}) — restart the proxy or raise OPENZOO_TUNNEL_MAX_USD`);
281
306
  return;
282
307
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.9.5",
3
+ "version": "0.9.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",