openzoo 0.21.1 → 0.21.3
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 +11 -5
- package/lib/tunnel.js +36 -29
- package/package.json +1 -1
package/lib/setup.js
CHANGED
|
@@ -188,18 +188,24 @@ export async function setupEditor(which, target) {
|
|
|
188
188
|
// (see the header of lib/tunnel.js). So the public URL is a HARD dependency
|
|
189
189
|
// of the editor path, and we wait for it — bounded, with progress, because
|
|
190
190
|
// an unbounded wait once left the editor never launching at all.
|
|
191
|
+
// NEVER BLOCK THE LAUNCH ON THE TUNNEL. Waiting inline is what made this
|
|
192
|
+
// command sit on "waiting for the public tunnel....." with nothing else
|
|
193
|
+
// happening — and if the tunnel was slow or failing, the editor never opened
|
|
194
|
+
// at all. The tunnel is genuinely required for the EDITOR path (its server
|
|
195
|
+
// cannot reach localhost), but we already have a rebind hook that rewrites
|
|
196
|
+
// and re-pins the config the moment a URL exists, so the honest design is:
|
|
197
|
+
// launch now with what we have, and follow the tunnel in when it arrives.
|
|
191
198
|
if (!publicUrl) {
|
|
192
|
-
|
|
193
|
-
for (let i = 0; i <
|
|
199
|
+
// A couple of seconds only, in case it is already up — then move on.
|
|
200
|
+
for (let i = 0; i < 6 && !started?.publicUrl; i++) {
|
|
194
201
|
await new Promise((r) => setTimeout(r, 500));
|
|
195
|
-
if (i % 4 === 3) process.stdout.write('.');
|
|
196
202
|
}
|
|
197
|
-
console.log('');
|
|
198
203
|
publicUrl = started?.publicUrl ?? null;
|
|
199
204
|
tunnelKey = started?.tunnelToken ?? tunnelKey;
|
|
200
205
|
}
|
|
201
206
|
if (publicUrl) console.log(`tunnel: ${publicUrl}/v1 api_key ${tunnelKey}`);
|
|
202
|
-
else console.log('tunnel:
|
|
207
|
+
else console.log('tunnel: still coming up — launching now; the editor config is\n'
|
|
208
|
+
+ ' rewritten and re-pinned automatically the moment it is ready.');
|
|
203
209
|
} else {
|
|
204
210
|
console.log(`proxy already running on ${base}`);
|
|
205
211
|
// A proxy someone else started owns the tunnel; ask it for the public URL.
|
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,33 +140,26 @@ 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
|
-
// the
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const winner = await Promise.any(attempts);
|
|
154
|
-
log(`tunnel: up via ${winner.rung}`);
|
|
155
|
-
for (const p of losers) { if (p !== winner.proc) { try { p.kill(); } catch { /* gone */ } } }
|
|
156
|
-
return winner;
|
|
157
|
-
} catch (agg) {
|
|
158
|
-
for (const p of losers) { try { p.kill(); } catch { /* gone */ } }
|
|
159
|
-
const why = agg?.errors?.map((e) => e.message).join(' | ') || agg?.message || 'unknown';
|
|
160
|
-
throw new Error(`no cloudflared configuration produced a working tunnel (${why})`);
|
|
143
|
+
// ONE AT A TIME, best rung first. An earlier version raced all four at once;
|
|
144
|
+
// that is four quick tunnels per invocation against a per-IP-limited free
|
|
145
|
+
// service, which is a good way to get throttled into exactly the failure it
|
|
146
|
+
// was meant to dodge. Rung 1 (ipv4) is the measured winner on this network,
|
|
147
|
+
// so the common path costs one tunnel.
|
|
148
|
+
let lastErr = null;
|
|
149
|
+
for (const rung of TUNNEL_RUNGS) {
|
|
150
|
+
let started = null;
|
|
151
|
+
try {
|
|
152
|
+
started = await startCloudflared(bin, port, log, { args: rung.args });
|
|
153
|
+
} catch (e) { lastErr = e; log(`tunnel: ${rung.why} — ${e.message}`); continue; }
|
|
154
|
+
if (await probeTunnel(started.url, { tries: 5, gapMs: 2000 })) {
|
|
155
|
+
log(`tunnel: up via ${rung.why}`);
|
|
156
|
+
return { ...started, rung: rung.why };
|
|
157
|
+
}
|
|
158
|
+
log(`tunnel: ${rung.why} registered but the edge could not reach it — next`);
|
|
159
|
+
try { started.proc.kill(); } catch { /* already gone */ }
|
|
160
|
+
lastErr = new Error(`${rung.why}: edge could not reach the origin`);
|
|
161
161
|
}
|
|
162
|
+
throw lastErr || new Error('no cloudflared configuration produced a working tunnel');
|
|
162
163
|
}
|
|
163
164
|
|
|
164
165
|
/**
|
|
@@ -194,7 +195,13 @@ export function superviseTunnel({ bin, port, url, proc, log = () => {}, onRebind
|
|
|
194
195
|
/** Start a quick tunnel to localhost:<port>; resolve with its public URL. */
|
|
195
196
|
export function startCloudflared(bin, port, log, { args = [] } = {}) {
|
|
196
197
|
return new Promise((resolve, reject) => {
|
|
197
|
-
|
|
198
|
+
// 127.0.0.1, NOT localhost. MEASURED: the proxy binds IPv4 only, while
|
|
199
|
+
// `localhost` resolves to ::1 FIRST on macOS — cloudflared dialled the IPv6
|
|
200
|
+
// loopback, got connection-refused, and reported "Unable to reach the origin
|
|
201
|
+
// service" (502) on a tunnel that had registered with Cloudflare perfectly.
|
|
202
|
+
// Every rung failed identically, which is what made it look like a network
|
|
203
|
+
// or rate-limit problem when it was a loopback address-family mismatch.
|
|
204
|
+
const proc = spawn(bin, ['tunnel', '--url', `http://127.0.0.1:${port}`, '--no-autoupdate', ...args], {
|
|
198
205
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
199
206
|
});
|
|
200
207
|
let settled = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.3",
|
|
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",
|