openzoo 0.21.0 → 0.21.2
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/setup.js +1 -1
- package/lib/tunnel.js +45 -19
- package/package.json +1 -1
package/lib/setup.js
CHANGED
|
@@ -190,7 +190,7 @@ export async function setupEditor(which, target) {
|
|
|
190
190
|
// an unbounded wait once left the editor never launching at all.
|
|
191
191
|
if (!publicUrl) {
|
|
192
192
|
process.stdout.write('waiting for the public tunnel (the editor\'s server cannot reach localhost)');
|
|
193
|
-
for (let i = 0; i <
|
|
193
|
+
for (let i = 0; i < 240 && !started?.publicUrl; i++) { // up to 120s (rungs race; first to SERVE wins)
|
|
194
194
|
await new Promise((r) => setTimeout(r, 500));
|
|
195
195
|
if (i % 4 === 3) process.stdout.write('.');
|
|
196
196
|
}
|
package/lib/tunnel.js
CHANGED
|
@@ -105,10 +105,18 @@ export async function ensureCloudflared(log = console.log) {
|
|
|
105
105
|
* edge-IP or protocol survives — so cascade instead of giving up.
|
|
106
106
|
*/
|
|
107
107
|
const TUNNEL_RUNGS = [
|
|
108
|
+
// IPv4 FIRST, deliberately. cloudflared prefers IPv6+QUIC to reach the edge
|
|
109
|
+
// whenever the interface merely HAS an IPv6 address — it never checks the
|
|
110
|
+
// route is usable. MEASURED on a box with an IPv6 address and no IPv6 route:
|
|
111
|
+
// failed to dial to edge with quic: write udp6 [::]->[2606:4700:a8::1]:7844:
|
|
112
|
+
// sendmsg: no route to host
|
|
113
|
+
// ...on every retry, so the tunnel registered and immediately died and the
|
|
114
|
+
// edge served 502/530 — which looks exactly like a dead origin or a rate
|
|
115
|
+
// limit, and is neither. Forcing v4 registered first try on the same machine.
|
|
116
|
+
{ args: ['--edge-ip-version', '4'], why: 'ipv4 edge' },
|
|
108
117
|
{ args: [], why: 'default (quic, auto edge ip)' },
|
|
109
|
-
{ args: ['--edge-ip-version', '4'], why: 'ipv4
|
|
118
|
+
{ args: ['--edge-ip-version', '4', '--protocol', 'http2'], why: 'ipv4 + http2' },
|
|
110
119
|
{ args: ['--protocol', 'http2'], why: 'http2 (quic/udp blocked or lossy)' },
|
|
111
|
-
{ args: ['--edge-ip-version', '4', '--protocol', 'http2'], why: 'ipv4 + http2 (most conservative)' },
|
|
112
120
|
];
|
|
113
121
|
|
|
114
122
|
/**
|
|
@@ -132,22 +140,33 @@ export async function probeTunnel(url, { tries = 8, gapMs = 2500 } = {}) {
|
|
|
132
140
|
* Returns { url, proc, rung } or throws when every rung fails.
|
|
133
141
|
*/
|
|
134
142
|
export async function startHealthyTunnel(bin, port, log = () => {}) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
143
|
+
// RACE, DO NOT WALK. Tried serially, four rungs cost up to 60s waiting for a
|
|
144
|
+
// URL plus ~20s of probing EACH — minutes of "waiting for the public tunnel"
|
|
145
|
+
// before the good one is even attempted, and the caller times out first.
|
|
146
|
+
// Quick tunnels are free and independent, so all rungs are started at once and
|
|
147
|
+
// the first one that actually SERVES wins; the losers are killed. Worst case
|
|
148
|
+
// becomes one rung's latency instead of four.
|
|
149
|
+
const losers = [];
|
|
150
|
+
let settled = false;
|
|
151
|
+
const attempts = TUNNEL_RUNGS.map(async (rung) => {
|
|
152
|
+
const started = await startCloudflared(bin, port, log, { args: rung.args });
|
|
153
|
+
losers.push(started.proc);
|
|
154
|
+
const ok = await probeTunnel(started.url, { tries: 6, gapMs: 2000 });
|
|
155
|
+
if (!ok) throw new Error(`${rung.why}: printed ${started.url} but the edge could not reach it`);
|
|
156
|
+
if (settled) throw new Error('another rung won');
|
|
157
|
+
settled = true;
|
|
158
|
+
return { ...started, rung: rung.why };
|
|
159
|
+
});
|
|
160
|
+
try {
|
|
161
|
+
const winner = await Promise.any(attempts);
|
|
162
|
+
log(`tunnel: up via ${winner.rung}`);
|
|
163
|
+
for (const p of losers) { if (p !== winner.proc) { try { p.kill(); } catch { /* gone */ } } }
|
|
164
|
+
return winner;
|
|
165
|
+
} catch (agg) {
|
|
166
|
+
for (const p of losers) { try { p.kill(); } catch { /* gone */ } }
|
|
167
|
+
const why = agg?.errors?.map((e) => e.message).join(' | ') || agg?.message || 'unknown';
|
|
168
|
+
throw new Error(`no cloudflared configuration produced a working tunnel (${why})`);
|
|
149
169
|
}
|
|
150
|
-
throw lastErr || new Error('no cloudflared configuration produced a working tunnel');
|
|
151
170
|
}
|
|
152
171
|
|
|
153
172
|
/**
|
|
@@ -183,7 +202,13 @@ export function superviseTunnel({ bin, port, url, proc, log = () => {}, onRebind
|
|
|
183
202
|
/** Start a quick tunnel to localhost:<port>; resolve with its public URL. */
|
|
184
203
|
export function startCloudflared(bin, port, log, { args = [] } = {}) {
|
|
185
204
|
return new Promise((resolve, reject) => {
|
|
186
|
-
|
|
205
|
+
// 127.0.0.1, NOT localhost. MEASURED: the proxy binds IPv4 only, while
|
|
206
|
+
// `localhost` resolves to ::1 FIRST on macOS — cloudflared dialled the IPv6
|
|
207
|
+
// loopback, got connection-refused, and reported "Unable to reach the origin
|
|
208
|
+
// service" (502) on a tunnel that had registered with Cloudflare perfectly.
|
|
209
|
+
// Every rung failed identically, which is what made it look like a network
|
|
210
|
+
// or rate-limit problem when it was a loopback address-family mismatch.
|
|
211
|
+
const proc = spawn(bin, ['tunnel', '--url', `http://127.0.0.1:${port}`, '--no-autoupdate', ...args], {
|
|
187
212
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
188
213
|
});
|
|
189
214
|
let settled = false;
|
|
@@ -195,7 +220,8 @@ export function startCloudflared(bin, port, log, { args = [] } = {}) {
|
|
|
195
220
|
proc.stderr.on('data', scan); // cloudflared prints the URL on stderr
|
|
196
221
|
proc.on('error', reject);
|
|
197
222
|
proc.on('exit', (c) => { if (!settled) reject(new Error(`cloudflared exited ${c} before printing a URL`)); });
|
|
198
|
-
|
|
223
|
+
const urlWaitMs = Number(process.env.OPENZOO_TUNNEL_URL_WAIT_MS || 25000);
|
|
224
|
+
setTimeout(() => { if (!settled) { try { proc.kill(); } catch { /* gone */ } reject(new Error(`cloudflared did not produce a URL in ${urlWaitMs}ms`)); } }, urlWaitMs);
|
|
199
225
|
});
|
|
200
226
|
}
|
|
201
227
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.2",
|
|
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",
|