openzoo 0.7.2 → 0.8.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.
- package/bin/openzoo.js +6 -5
- package/lib/proxy.js +63 -5
- package/package.json +1 -1
package/bin/openzoo.js
CHANGED
|
@@ -4,10 +4,10 @@ const cmd = process.argv[2] || 'proxy';
|
|
|
4
4
|
const HELP = `openzoo — local x402-paying proxy + MCP server for openzoo.fun
|
|
5
5
|
|
|
6
6
|
usage:
|
|
7
|
-
npx openzoo start the proxy
|
|
7
|
+
npx openzoo start the proxy: http://localhost:8402/v1 (keyless) PLUS a
|
|
8
|
+
public HTTPS url for cloud IDEs (key required, printed at start)
|
|
8
9
|
npx openzoo mcp stdio MCP server (tools: zoo_ask, zoo_models, zoo_wallet)
|
|
9
|
-
npx openzoo tunnel public
|
|
10
|
-
(installs cloudflared itself; mints a required api key)
|
|
10
|
+
npx openzoo tunnel public-url-only mode (everything key-gated, no keyless localhost)
|
|
11
11
|
npx openzoo demo ~1M-token needle demo: direct refuses, the zoo answers
|
|
12
12
|
(run it twice — the second run reuses the bound corpus and is near-free)
|
|
13
13
|
npx openzoo contexts list corpora bound to the zoo (never re-uploaded)
|
|
@@ -35,13 +35,14 @@ env:
|
|
|
35
35
|
OPENZOO_NO_CONTEXT_CACHE (0 — set 1 to always ship the full body)
|
|
36
36
|
OPENZOO_ENABLE_RH (0 — let DEFAULT selection fall through to the Robinhood rail;
|
|
37
37
|
OPENZOO_RAIL=robinhood forces it without this)
|
|
38
|
-
OPENZOO_TUNNEL_MAX_USD (1.00 —
|
|
38
|
+
OPENZOO_TUNNEL_MAX_USD (1.00 — public-url session ceiling) OPENZOO_TUNNEL_TOKEN (pin the api key)
|
|
39
|
+
OPENZOO_NO_TUNNEL (0 — set 1 for localhost-only, no public url)`;
|
|
39
40
|
|
|
40
41
|
async function main() {
|
|
41
42
|
switch (cmd) {
|
|
42
43
|
case 'proxy':
|
|
43
44
|
case 'start':
|
|
44
|
-
await (await import('../lib/proxy.js')).startProxy();
|
|
45
|
+
await (await import('../lib/proxy.js')).startProxy({ autoTunnel: true });
|
|
45
46
|
break;
|
|
46
47
|
case 'mcp':
|
|
47
48
|
await (await import('../lib/mcp.js')).startMcp();
|
package/lib/proxy.js
CHANGED
|
@@ -110,13 +110,23 @@ async function maybeCacheCorpus(req, bodyBuf, log) {
|
|
|
110
110
|
* becomes the only thing between a stranger and your wallet. Both are off by
|
|
111
111
|
* default, so localhost behaviour is unchanged.
|
|
112
112
|
*/
|
|
113
|
-
export async function startProxy({ silent = false, requireToken = null, sessionMaxUsd = null } = {}) {
|
|
113
|
+
export async function startProxy({ silent = false, requireToken = null, sessionMaxUsd = null, autoTunnel = false } = {}) {
|
|
114
114
|
const client = new PayClient();
|
|
115
115
|
const log = silent ? () => {} : (...a) => console.log(...a);
|
|
116
116
|
let sessionSpent = 0;
|
|
117
|
+
let tunnelSpent = 0;
|
|
118
|
+
// Set once cloudflared is up (see below). Gating keys off the REQUEST's
|
|
119
|
+
// origin, not off whether the URL exists yet, so there is no startup window
|
|
120
|
+
// where public traffic slips through ungated.
|
|
121
|
+
let tunnelGate = null;
|
|
117
122
|
|
|
118
123
|
const server = http.createServer(async (req, res) => {
|
|
119
124
|
const url = `${config.apiBase}${req.url}`;
|
|
125
|
+
// Requests that arrived over the public quick-tunnel URL carry cloudflared's
|
|
126
|
+
// headers; nothing dialing 127.0.0.1 directly does. That distinction is what
|
|
127
|
+
// lets localhost stay keyless while the SAME port is safely public.
|
|
128
|
+
const viaTunnel = !requireToken && tunnelGate
|
|
129
|
+
&& Boolean(req.headers['cf-connecting-ip'] || req.headers['cf-ray']);
|
|
120
130
|
// Auth first: refuse before reading a body, forwarding, quoting or paying.
|
|
121
131
|
if (requireToken) {
|
|
122
132
|
const got = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
|
|
@@ -130,6 +140,18 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
130
140
|
return;
|
|
131
141
|
}
|
|
132
142
|
}
|
|
143
|
+
if (viaTunnel) {
|
|
144
|
+
const got = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
|
|
145
|
+
if (got !== tunnelGate.token) {
|
|
146
|
+
log(`public url: 401 ${req.method} ${req.url}`);
|
|
147
|
+
jsonErr(res, 401, 'unauthorized: this openzoo public URL requires the api key printed at startup');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (tunnelSpent >= tunnelGate.sessionMaxUsd) {
|
|
151
|
+
jsonErr(res, 402, `openzoo public-URL session cap reached ($${tunnelGate.sessionMaxUsd}) — restart the proxy or raise OPENZOO_TUNNEL_MAX_USD`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
133
155
|
let bodyBuf;
|
|
134
156
|
try {
|
|
135
157
|
bodyBuf = await readBody(req);
|
|
@@ -175,11 +197,17 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
175
197
|
}
|
|
176
198
|
const { response, paid, receipt } = result;
|
|
177
199
|
if (paid && receipt) {
|
|
178
|
-
if (receipt.ok && typeof receipt.billedUsd === 'number')
|
|
200
|
+
if (receipt.ok && typeof receipt.billedUsd === 'number') {
|
|
201
|
+
sessionSpent += receipt.billedUsd;
|
|
202
|
+
// The public-URL ceiling meters only public-origin spend — your own
|
|
203
|
+
// local calls never eat into it.
|
|
204
|
+
if (viaTunnel) tunnelSpent += receipt.billedUsd;
|
|
205
|
+
}
|
|
179
206
|
const line = receipt.ok ? receipt.line : `paid retry -> HTTP ${receipt.status}`;
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
log(
|
|
207
|
+
// Wherever a running total is the thing to watch, it rides the receipt.
|
|
208
|
+
if (requireToken) log(`${line} · session $${sessionSpent.toFixed(6)}`);
|
|
209
|
+
else if (viaTunnel) log(`${line} · public-url session $${tunnelSpent.toFixed(6)}`);
|
|
210
|
+
else log(line);
|
|
183
211
|
}
|
|
184
212
|
await relay(res, response);
|
|
185
213
|
} catch (err) {
|
|
@@ -252,5 +280,35 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
252
280
|
console.log(` base_url = http://localhost:${config.port}/v1`);
|
|
253
281
|
console.log(' api_key = sk-openzoo (any value works; the zoo takes payment, not keys)');
|
|
254
282
|
}
|
|
283
|
+
|
|
284
|
+
// AUTO-TUNNEL: `npx openzoo` must be end-to-end for cloud IDEs too — their
|
|
285
|
+
// servers cannot dial localhost, so the default command also publishes a
|
|
286
|
+
// quick-tunnel URL. It comes up in the background (never delays localhost),
|
|
287
|
+
// failure degrades to local-only, and public traffic is gated above by
|
|
288
|
+
// token + its own spend ceiling. OPENZOO_NO_TUNNEL=1 opts out.
|
|
289
|
+
if (autoTunnel && process.env.OPENZOO_NO_TUNNEL !== '1') {
|
|
290
|
+
(async () => {
|
|
291
|
+
try {
|
|
292
|
+
const { ensureCloudflared, startCloudflared, mintToken } = await import('./tunnel.js');
|
|
293
|
+
const token = mintToken();
|
|
294
|
+
const cap = Number(process.env.OPENZOO_TUNNEL_MAX_USD || 1);
|
|
295
|
+
const bin = await ensureCloudflared((m) => log(m));
|
|
296
|
+
const { url, proc } = await startCloudflared(bin, config.port, log);
|
|
297
|
+
tunnelGate = { token, sessionMaxUsd: cap };
|
|
298
|
+
const bye = () => { try { proc.kill('SIGTERM'); } catch { /* already gone */ } };
|
|
299
|
+
process.once('SIGINT', () => { bye(); process.exit(0); });
|
|
300
|
+
process.once('SIGTERM', () => { bye(); process.exit(0); });
|
|
301
|
+
process.once('exit', bye);
|
|
302
|
+
log('');
|
|
303
|
+
log('cloud IDE / remote harness? use the public URL (they cannot reach localhost):');
|
|
304
|
+
log(` base_url = ${url}/v1`);
|
|
305
|
+
log(` api_key = ${token}`);
|
|
306
|
+
log(` (key REQUIRED on the public URL — it spends this wallet; capped at $${cap.toFixed(2)}/session,`);
|
|
307
|
+
log(' OPENZOO_TUNNEL_MAX_USD to change, OPENZOO_NO_TUNNEL=1 for localhost-only)');
|
|
308
|
+
} catch (err) {
|
|
309
|
+
log(`public URL unavailable (${err.message}) — localhost still works; OPENZOO_NO_TUNNEL=1 hides this line`);
|
|
310
|
+
}
|
|
311
|
+
})();
|
|
312
|
+
}
|
|
255
313
|
return { server, client, spent: () => sessionSpent };
|
|
256
314
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|