@join-pantheon/cli 0.27.0 → 0.29.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.
@@ -0,0 +1,595 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import http from "node:http";
4
+ import https from "node:https";
5
+ import net from "node:net";
6
+ import tls from "node:tls";
7
+ import { platform } from "node:os";
8
+ import { join } from "node:path";
9
+ const LOOPBACK = ["localhost", "127.0.0.1", "::1", "[::1]"];
10
+ const NONE = { https: null, http: null, noProxy: LOOPBACK, source: "none", fallbackOnly: false };
11
+ const pick = (env, ...names) => {
12
+ for (const n of names) {
13
+ const v = env[n]?.trim();
14
+ if (v)
15
+ return v;
16
+ }
17
+ return undefined;
18
+ };
19
+ export function normalizeProxy(raw) {
20
+ const v = raw.trim();
21
+ if (!v)
22
+ return null;
23
+ try {
24
+ const u = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(v) ? v : `http://${v}`);
25
+ if (u.protocol !== "http:" && u.protocol !== "https:")
26
+ return null;
27
+ if (!u.hostname)
28
+ return null;
29
+ return u.toString().replace(/\/$/, "");
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ export function redactProxy(url) {
36
+ if (!url)
37
+ return "direct";
38
+ try {
39
+ const u = new URL(url);
40
+ if (u.username || u.password) {
41
+ u.username = u.username ? "***" : "";
42
+ u.password = "";
43
+ }
44
+ return u.toString().replace(/\/$/, "");
45
+ }
46
+ catch {
47
+ return redactText(url);
48
+ }
49
+ }
50
+ export function redactText(raw) {
51
+ return raw.replace(/^(\s*(?:[a-z][a-z0-9+.-]*:\/\/)?)[^@/\s]*@/i, "$1***@");
52
+ }
53
+ export function splitNoProxy(raw) {
54
+ return [...new Set([...(raw ?? "").split(/[,\s]+/).map((s) => s.trim().toLowerCase()).filter(Boolean), ...LOOPBACK])];
55
+ }
56
+ export function bypassesProxy(host, port, noProxy) {
57
+ const h = host.toLowerCase().replace(/^\[|\]$/g, "");
58
+ for (const raw of noProxy) {
59
+ if (raw === "*")
60
+ return true;
61
+ let entry = raw.replace(/^\[|\]$/g, "");
62
+ let entryPort;
63
+ const m = /^(.*):(\d+)$/.exec(entry);
64
+ if (m && !entry.includes("::")) {
65
+ entry = m[1];
66
+ entryPort = m[2];
67
+ }
68
+ if (entryPort && entryPort !== port)
69
+ continue;
70
+ if (entry.includes("/") && net.isIP(h)) {
71
+ if (inCidr(h, entry))
72
+ return true;
73
+ continue;
74
+ }
75
+ const bare = entry.replace(/^\*?\./, "");
76
+ if (h === bare || h.endsWith(`.${bare}`))
77
+ return true;
78
+ }
79
+ return false;
80
+ }
81
+ function inCidr(ip, cidr) {
82
+ const [base, bitsRaw] = cidr.split("/");
83
+ const bits = Number(bitsRaw);
84
+ if (!base || net.isIPv4(ip) !== net.isIPv4(base) || !net.isIPv4(ip) || !(bits >= 0 && bits <= 32))
85
+ return false;
86
+ const n = (s) => s.split(".").reduce((a, o) => (a << 8) + Number(o), 0) >>> 0;
87
+ const mask = bits === 0 ? 0 : (~0 << (32 - bits)) >>> 0;
88
+ return (n(ip) & mask) === (n(base) & mask);
89
+ }
90
+ export function parseScutil(out) {
91
+ const val = (k) => new RegExp(`^\\s*${k}\\s*:\\s*(.+?)\\s*$`, "m").exec(out)?.[1];
92
+ const on = (k) => val(k) === "1";
93
+ const hp = (host, port) => (host ? normalizeProxy(`${host}${port ? `:${port}` : ""}`) : null);
94
+ const exceptions = [];
95
+ const block = /ExceptionsList\s*:\s*<array>\s*\{([\s\S]*?)\}/.exec(out)?.[1] ?? "";
96
+ for (const line of block.split("\n")) {
97
+ const m = /:\s*(\S+)\s*$/.exec(line);
98
+ if (m)
99
+ exceptions.push(m[1]);
100
+ }
101
+ const pac = on("ProxyAutoConfigEnable") ? val("ProxyAutoConfigURLString") : undefined;
102
+ return {
103
+ https: on("HTTPSEnable") ? hp(val("HTTPSProxy"), val("HTTPSPort")) : null,
104
+ http: on("HTTPEnable") ? hp(val("HTTPProxy"), val("HTTPPort")) : null,
105
+ noProxy: exceptions,
106
+ ...(pac ? { pac } : {}),
107
+ };
108
+ }
109
+ export function parseWindowsReg(out) {
110
+ const val = (k) => new RegExp(`^\\s*${k}\\s+REG_\\w+\\s+(.*?)\\s*$`, "mi").exec(out)?.[1];
111
+ const pac = val("AutoConfigURL") || undefined;
112
+ const enabled = /^0x0*1$/i.test(val("ProxyEnable") ?? "");
113
+ const server = enabled ? val("ProxyServer") ?? "" : "";
114
+ let httpsP = null, httpP = null;
115
+ if (server.includes("=")) {
116
+ for (const part of server.split(";")) {
117
+ const [k, v] = part.split("=");
118
+ if (k?.trim().toLowerCase() === "https" && v)
119
+ httpsP = normalizeProxy(v);
120
+ if (k?.trim().toLowerCase() === "http" && v)
121
+ httpP = normalizeProxy(v);
122
+ }
123
+ httpsP ??= httpP;
124
+ }
125
+ else if (server) {
126
+ httpsP = httpP = normalizeProxy(server);
127
+ }
128
+ const override = (val("ProxyOverride") ?? "").split(";").map((s) => s.trim()).filter((s) => s && s !== "<local>");
129
+ return { https: httpsP, http: httpP, noProxy: override, ...(pac ? { pac } : {}) };
130
+ }
131
+ const run = (cmd, args) => {
132
+ try {
133
+ return execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 3000 });
134
+ }
135
+ catch {
136
+ return null;
137
+ }
138
+ };
139
+ export const chosenByPerson = (s) => !!s && (s.proxy === "" || /^pantheon network/.test(s.source ?? ""));
140
+ export const windowsRegExe = (env = process.env) => `${env.SystemRoot ?? env.SYSTEMROOT ?? "C:\\Windows"}\\System32\\reg.exe`;
141
+ function savedFromConfig(env) {
142
+ try {
143
+ const home = env.PANTHEON_HOME ?? join(env.HOME ?? env.USERPROFILE ?? "", ".pantheon");
144
+ const f = join(home, "config.json");
145
+ if (!existsSync(f))
146
+ return null;
147
+ return JSON.parse(readFileSync(f, "utf8")).network ?? null;
148
+ }
149
+ catch {
150
+ return null;
151
+ }
152
+ }
153
+ export const defaultNetworkDeps = () => ({
154
+ env: process.env,
155
+ os: platform(),
156
+ saved: () => savedFromConfig(process.env),
157
+ scutil: () => run("/usr/sbin/scutil", ["--proxy"]),
158
+ winReg: () => run(windowsRegExe(), ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"]),
159
+ });
160
+ export function resolveProxyPlan(d = defaultNetworkDeps()) {
161
+ const noProxy = splitNoProxy(pick(d.env, "no_proxy", "NO_PROXY"));
162
+ const forced = d.env.PANTHEON_PROXY?.trim();
163
+ if (forced) {
164
+ if (/^(none|direct|off|false|0)$/i.test(forced))
165
+ return { ...NONE, noProxy, directChosen: true };
166
+ const p = normalizeProxy(forced);
167
+ if (p)
168
+ return { https: p, http: p, noProxy, source: "PANTHEON_PROXY", fallbackOnly: false };
169
+ }
170
+ const saved = d.saved();
171
+ const fromSaved = (retryDirect) => {
172
+ const sp = saved?.proxy ? normalizeProxy(saved.proxy) : null;
173
+ return sp ? { https: sp, http: sp, noProxy: splitNoProxy([saved?.no_proxy, ...noProxy].filter(Boolean).join(",")), source: "config", fallbackOnly: false, ...(retryDirect ? { retryDirect: true } : {}) } : null;
174
+ };
175
+ if (chosenByPerson(saved)) {
176
+ if (saved.proxy === "")
177
+ return { ...NONE, noProxy, directChosen: true };
178
+ const p = fromSaved(false);
179
+ if (p)
180
+ return p;
181
+ }
182
+ const envHttps = pick(d.env, "https_proxy", "HTTPS_PROXY", "all_proxy", "ALL_PROXY");
183
+ const envHttp = pick(d.env, "http_proxy", "HTTP_PROXY", "all_proxy", "ALL_PROXY");
184
+ if (envHttps || envHttp) {
185
+ const hs = envHttps ? normalizeProxy(envHttps) : null;
186
+ const h = envHttp ? normalizeProxy(envHttp) : null;
187
+ if (hs || h)
188
+ return { https: hs, http: h, noProxy, source: "env", fallbackOnly: false };
189
+ }
190
+ const auto = fromSaved(true);
191
+ if (auto)
192
+ return auto;
193
+ const sys = d.os === "darwin" ? (() => { const o = d.scutil(); return o ? parseScutil(o) : null; })()
194
+ : d.os === "win32" ? (() => { const o = d.winReg(); return o ? parseWindowsReg(o) : null; })()
195
+ : null;
196
+ if (sys && (sys.https || sys.http)) {
197
+ return { https: sys.https ?? sys.http, http: sys.http ?? sys.https, noProxy: splitNoProxy([...sys.noProxy, ...noProxy].join(",")),
198
+ source: d.os === "darwin" ? "macos" : "windows", fallbackOnly: true, ...(sys.pac ? { pac: sys.pac } : {}) };
199
+ }
200
+ return { ...NONE, noProxy, ...(sys?.pac ? { pac: sys.pac } : {}) };
201
+ }
202
+ export function proxyFor(target, plan) {
203
+ let u;
204
+ try {
205
+ u = new URL(target);
206
+ }
207
+ catch {
208
+ return null;
209
+ }
210
+ const secure = u.protocol === "https:" || u.protocol === "wss:";
211
+ const port = u.port || (secure ? "443" : "80");
212
+ const p = secure ? plan.https : plan.http;
213
+ if (!p || bypassesProxy(u.hostname, port, plan.noProxy))
214
+ return null;
215
+ return p;
216
+ }
217
+ export function tunnelAgent(proxyUrl, secure) {
218
+ const proxy = new URL(proxyUrl);
219
+ const auth = proxy.username || proxy.password
220
+ ? `Basic ${Buffer.from(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`).toString("base64")}`
221
+ : undefined;
222
+ const Base = secure ? https.Agent : http.Agent;
223
+ class Tunnel extends Base {
224
+ createConnection(options, callback) {
225
+ const cb = (err, s) => callback?.(err, s);
226
+ const host = String(options.hostname ?? options.host ?? "");
227
+ const port = Number(options.port ?? (secure ? 443 : 80));
228
+ const req = (proxy.protocol === "https:" ? https : http).request({
229
+ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === "https:" ? 443 : 80)),
230
+ method: "CONNECT", path: `${host.includes(":") ? `[${host}]` : host}:${port}`,
231
+ headers: { host: `${host}:${port}`, ...(auth ? { "proxy-authorization": auth } : {}) },
232
+ timeout: 10_000,
233
+ });
234
+ let done = false;
235
+ const fail = (e) => { if (!done) {
236
+ done = true;
237
+ cb(e);
238
+ } };
239
+ req.once("connect", (res, socket) => {
240
+ if (res.statusCode !== 200) {
241
+ socket.destroy();
242
+ const err = new Error(`the proxy ${redactProxy(proxyUrl)} answered ${res.statusCode} ${res.statusMessage ?? ""} to a tunnel request for ${host}:${port}`.trim());
243
+ err.code = res.statusCode === 407 ? "PROXY_AUTH_REQUIRED" : "PROXY_REFUSED";
244
+ return fail(err);
245
+ }
246
+ if (!secure) {
247
+ done = true;
248
+ return cb(null, socket);
249
+ }
250
+ const t = tls.connect({ ...options, socket, servername: net.isIP(host) ? undefined : host });
251
+ t.once("secureConnect", () => { if (!done) {
252
+ done = true;
253
+ cb(null, t);
254
+ } });
255
+ t.once("error", fail);
256
+ });
257
+ req.once("timeout", () => { req.destroy(Object.assign(new Error(`the proxy ${redactProxy(proxyUrl)} did not answer within 10s`), { code: "ETIMEDOUT" })); });
258
+ req.once("error", fail);
259
+ req.end();
260
+ return undefined;
261
+ }
262
+ }
263
+ return new Tunnel({ keepAlive: false });
264
+ }
265
+ let installed = null;
266
+ const ALTERNATE_TTL = 10 * 60_000;
267
+ const TRIAL_TTL = 60_000;
268
+ const RETRY_TIMEOUT = 15_000;
269
+ const CONNECT_TIMEOUT = 5_000;
270
+ let clock = () => Date.now();
271
+ const hosts = new Map();
272
+ let lastRelayRoute = null;
273
+ const hostKey = (target) => {
274
+ try {
275
+ const u = new URL(target);
276
+ const secure = u.protocol === "https:" || u.protocol === "wss:";
277
+ return `${u.hostname.toLowerCase()}:${u.port || (secure ? "443" : "80")}`;
278
+ }
279
+ catch {
280
+ return target;
281
+ }
282
+ };
283
+ const stateOf = (target) => {
284
+ const k = hostKey(target);
285
+ let s = hosts.get(k);
286
+ if (!s) {
287
+ s = { alternateUntil: 0, trialUntil: 0 };
288
+ hosts.set(k, s);
289
+ }
290
+ return s;
291
+ };
292
+ const alternateActive = (target) => clock() < (hosts.get(hostKey(target))?.alternateUntil ?? 0);
293
+ const trialArmed = (target) => clock() < (hosts.get(hostKey(target))?.trialUntil ?? 0);
294
+ const anyAlternate = () => [...hosts.values()].some((s) => clock() < s.alternateUntil || clock() < s.trialUntil);
295
+ const hasAlternate = (plan) => !!(plan.https || plan.http) && (plan.fallbackOnly || !!plan.retryDirect);
296
+ const onAlternate = (target) => alternateActive(target) || trialArmed(target);
297
+ export function routeFor(target, plan = currentPlan()) {
298
+ const p = proxyFor(target, plan);
299
+ if (!p || !hasAlternate(plan))
300
+ return p;
301
+ return plan.fallbackOnly ? (onAlternate(target) ? p : null) : (onAlternate(target) ? null : p);
302
+ }
303
+ function planRoute(plan) {
304
+ const p = plan.https ?? plan.http;
305
+ if (!p || !hasAlternate(plan))
306
+ return p;
307
+ return plan.fallbackOnly ? (anyAlternate() ? p : null) : (anyAlternate() ? null : p);
308
+ }
309
+ export function wsOptions(address) {
310
+ const p = routeFor(address, currentPlan());
311
+ if (!p)
312
+ return {};
313
+ return { agent: tunnelAgent(p, /^wss:/.test(address)) };
314
+ }
315
+ export function currentPlan() {
316
+ if (!installed)
317
+ installed = { plan: resolveProxyPlan(), active: null };
318
+ return installed.plan;
319
+ }
320
+ export async function installNetwork() {
321
+ const plan = currentPlan();
322
+ if (plan.https || plan.http) {
323
+ if (!plan.fallbackOnly)
324
+ await useRouterForFetch(plan);
325
+ if (hasAlternate(plan))
326
+ wrapFetch(plan);
327
+ }
328
+ return plan;
329
+ }
330
+ function makeRouter(u, plan) {
331
+ const bound = hasAlternate(plan) ? { connect: { timeout: CONNECT_TIMEOUT } } : {};
332
+ const direct = new u.Agent(bound);
333
+ const proxies = new Map();
334
+ const pick = (origin) => {
335
+ const p = proxyFor(origin, plan);
336
+ if (!p)
337
+ return direct;
338
+ let a = proxies.get(p);
339
+ if (!a) {
340
+ a = new u.ProxyAgent({ uri: p, ...(hasAlternate(plan) ? { proxyTls: { timeout: CONNECT_TIMEOUT } } : {}) });
341
+ proxies.set(p, a);
342
+ }
343
+ return a;
344
+ };
345
+ const all = () => [direct, ...proxies.values()];
346
+ class Router extends u.Dispatcher {
347
+ dispatch(opts, handler) {
348
+ return pick(String(opts.origin ?? "")).dispatch(opts, handler);
349
+ }
350
+ }
351
+ const r = new Router();
352
+ Object.assign(r, {
353
+ close: () => Promise.all(all().map((a) => a.close())).then(() => undefined),
354
+ destroy: () => Promise.all(all().map((a) => a.destroy())).then(() => undefined),
355
+ });
356
+ return r;
357
+ }
358
+ const DISPATCHER = Symbol.for("undici.globalDispatcher.1");
359
+ const DISPATCHER2 = Symbol.for("undici.globalDispatcher.2");
360
+ let previousDispatcher = null;
361
+ let originalFetch = null;
362
+ async function useRouterForFetch(plan) {
363
+ if (installed?.active)
364
+ return;
365
+ const u = await import("undici");
366
+ const g = globalThis;
367
+ previousDispatcher ??= { v1: g[DISPATCHER], v2: g[DISPATCHER2] };
368
+ u.setGlobalDispatcher(makeRouter(u, plan));
369
+ if (installed)
370
+ installed.active = plan.https ?? plan.http;
371
+ }
372
+ const BLOCKED = /ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|EPROTO|wrong version number|UND_ERR_CONNECT_TIMEOUT|UND_ERR_SOCKET|handshake has timed out|socket hang up/i;
373
+ const NOTHING_SENT = /ENOTFOUND|EAI_AGAIN|ECONNREFUSED|EHOSTUNREACH|ENETUNREACH|UND_ERR_CONNECT_TIMEOUT|EPROTO|wrong version number|packet length too long|unknown protocol|ERR_SSL_|before secure TLS connection was established/i;
374
+ function errText(e, depth = 0) {
375
+ if (!e || depth > 4)
376
+ return "";
377
+ const x = e;
378
+ return [typeof x.code === "string" ? x.code : "", typeof x.message === "string" ? x.message : typeof e === "string" ? e : "",
379
+ errText(x.cause, depth + 1), ...(Array.isArray(x.errors) ? x.errors.map((y) => errText(y, depth + 1)) : [])].filter(Boolean).join(" ");
380
+ }
381
+ export const isBlockedPath = (e) => BLOCKED.test(errText(e));
382
+ const isCallerTimeout = (e) => e?.name === "TimeoutError";
383
+ export function safeToRetry(e, method) {
384
+ const m = method.toUpperCase();
385
+ return m === "GET" || m === "HEAD" ? isBlockedPath(e) || isCallerTimeout(e) : NOTHING_SENT.test(errText(e));
386
+ }
387
+ export const relayAnsweredFetch = (res) => /application\/json/i.test(res.headers.get("content-type") ?? "") && ![407, 502, 503, 504, 511].includes(res.status);
388
+ function retrySignal(caller) {
389
+ const own = AbortSignal.timeout(RETRY_TIMEOUT);
390
+ if (!caller)
391
+ return own;
392
+ const ctl = new AbortController();
393
+ const follow = () => { if (!isCallerTimeout(caller.reason))
394
+ ctl.abort(caller.reason); };
395
+ if (caller.aborted)
396
+ follow();
397
+ else
398
+ caller.addEventListener("abort", follow, { once: true });
399
+ return AbortSignal.any([own, ctl.signal]);
400
+ }
401
+ function wrapFetch(plan) {
402
+ const g = globalThis;
403
+ if (originalFetch)
404
+ return;
405
+ const original = g.fetch;
406
+ originalFetch = original;
407
+ let routes = null;
408
+ const dispatchers = () => (routes ??= import("undici").then((u) => {
409
+ const proxied = makeRouter(u, plan), direct = new u.Agent({ connect: { timeout: CONNECT_TIMEOUT } });
410
+ return plan.fallbackOnly ? { primary: direct, alternate: proxied } : { primary: proxied, alternate: direct };
411
+ }));
412
+ const send = async (input, init, which, signal) => original(input, { ...init, ...(signal ? { signal } : {}), dispatcher: (await dispatchers())[which] });
413
+ g.fetch = (async (input, init) => {
414
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
415
+ const method = init?.method ?? (typeof input === "object" && !(input instanceof URL) ? input.method : "GET");
416
+ if (!proxyFor(url, plan))
417
+ return original(input, init);
418
+ if (alternateActive(url)) {
419
+ try {
420
+ const res = await send(input, init, "alternate");
421
+ if (relayAnsweredFetch(res))
422
+ noteRelayAnswered(url);
423
+ return res;
424
+ }
425
+ catch (e) {
426
+ if (isBlockedPath(e))
427
+ stateOf(url).alternateUntil = 0;
428
+ throw e;
429
+ }
430
+ }
431
+ let res;
432
+ try {
433
+ res = await send(input, init, "primary");
434
+ }
435
+ catch (e) {
436
+ if (!safeToRetry(e, method))
437
+ throw e;
438
+ if (init?.signal?.aborted && !isCallerTimeout(init.signal.reason))
439
+ throw e;
440
+ let alt;
441
+ try {
442
+ alt = await send(input, init, "alternate", retrySignal(init?.signal));
443
+ }
444
+ catch {
445
+ throw e;
446
+ }
447
+ if (!relayAnsweredFetch(alt)) {
448
+ void alt.body?.cancel().catch(() => { });
449
+ throw e;
450
+ }
451
+ stateOf(url).alternateUntil = clock() + ALTERNATE_TTL;
452
+ noteRelayAnswered(url);
453
+ return alt;
454
+ }
455
+ if (relayAnsweredFetch(res))
456
+ noteRelayAnswered(url);
457
+ return res;
458
+ });
459
+ }
460
+ export function trySystemFallback(address, e) {
461
+ const plan = currentPlan();
462
+ if (!hasAlternate(plan) || !proxyFor(address, plan) || !isBlockedPath(e) || trialArmed(address))
463
+ return false;
464
+ const s = stateOf(address);
465
+ if (alternateActive(address)) {
466
+ s.alternateUntil = 0;
467
+ return false;
468
+ }
469
+ s.trialUntil = clock() + TRIAL_TTL;
470
+ return true;
471
+ }
472
+ export const systemFallbackPending = (address) => trialArmed(address);
473
+ export function noteRelayAnswered(address) {
474
+ if (proxyFor(address, currentPlan()))
475
+ lastRelayRoute = { proxy: routeFor(address) };
476
+ const s = hosts.get(hostKey(address));
477
+ if (s && clock() < s.trialUntil) {
478
+ s.trialUntil = 0;
479
+ s.alternateUntil = clock() + ALTERNATE_TTL;
480
+ }
481
+ }
482
+ export function settleSystemFallback(address, relayAnswered) {
483
+ if (relayAnswered)
484
+ return noteRelayAnswered(address);
485
+ const s = hosts.get(hostKey(address));
486
+ if (s)
487
+ s.trialUntil = 0;
488
+ }
489
+ export const usedSystemFallback = (address) => currentPlan().fallbackOnly && (address ? alternateActive(address) : [...hosts.values()].some((s) => clock() < s.alternateUntil));
490
+ export function proxyToRemember(relay, plan, saved) {
491
+ if (chosenByPerson(saved))
492
+ return null;
493
+ if (plan.source !== "env" && !(plan.fallbackOnly && usedSystemFallback(relay)))
494
+ return null;
495
+ const via = routeFor(relay, plan);
496
+ if (!via || saved?.proxy === via)
497
+ return null;
498
+ const custom = plan.noProxy.filter((h) => !LOOPBACK.includes(h));
499
+ return { proxy: via, ...(custom.length ? { no_proxy: custom.join(",") } : {}), source: plan.source };
500
+ }
501
+ const DIRECT_ENV = {
502
+ HTTPS_PROXY: "", https_proxy: "", HTTP_PROXY: "", http_proxy: "", ALL_PROXY: "", all_proxy: "", NO_PROXY: "*", no_proxy: "*",
503
+ };
504
+ export function proxyEnvFor(plan = currentPlan()) {
505
+ if (plan.directChosen)
506
+ return { ...DIRECT_ENV };
507
+ if (!(plan.https || plan.http) || plan.source === "env")
508
+ return {};
509
+ const viaProxy = hasAlternate(plan) ? (lastRelayRoute ? lastRelayRoute.proxy !== null : !plan.fallbackOnly) : true;
510
+ if (!viaProxy)
511
+ return {};
512
+ return {
513
+ ...(plan.https ? { HTTPS_PROXY: plan.https, https_proxy: plan.https } : {}),
514
+ ...(plan.http ? { HTTP_PROXY: plan.http, http_proxy: plan.http } : {}),
515
+ NO_PROXY: plan.noProxy.join(","), no_proxy: plan.noProxy.join(","),
516
+ };
517
+ }
518
+ export function resetNetworkForTests(plan, opts = {}) {
519
+ installed = plan ? { plan, active: null } : null;
520
+ hosts.clear();
521
+ lastRelayRoute = null;
522
+ clock = opts.now ?? (() => Date.now());
523
+ if (originalFetch) {
524
+ globalThis.fetch = originalFetch;
525
+ originalFetch = null;
526
+ }
527
+ if (previousDispatcher) {
528
+ const g = globalThis;
529
+ if (previousDispatcher.v1 !== undefined)
530
+ g[DISPATCHER] = previousDispatcher.v1;
531
+ if (previousDispatcher.v2 !== undefined)
532
+ g[DISPATCHER2] = previousDispatcher.v2;
533
+ previousDispatcher = null;
534
+ }
535
+ }
536
+ export function explainNetError(e, target, planArg) {
537
+ const plan = (typeof target === "object" ? target : planArg) ?? currentPlan();
538
+ const url = typeof target === "string" ? target : undefined;
539
+ const text = errText(e);
540
+ const proxy = url ? routeFor(url, plan) : planRoute(plan);
541
+ const shown = proxy ? redactProxy(proxy) : "";
542
+ const host = (() => { try {
543
+ return url ? new URL(url).hostname || null : null;
544
+ }
545
+ catch {
546
+ return null;
547
+ } })() ?? "the relay's address";
548
+ const orProxy = "Behind a company proxy? pantheon network --proxy http://host:port";
549
+ if (/PROXY_AUTH_REQUIRED|\b407\b/.test(text)) {
550
+ return { why: `the proxy ${shown} asked for a login`, fix: "put it in the proxy address: http://user:password@host:port", share: "proxy requires authentication" };
551
+ }
552
+ if (/PROXY_REFUSED/.test(text)) {
553
+ return { why: `the proxy ${shown} would not open a connection to the relay`, fix: `ask whoever runs it to allow HTTPS to ${host}`, share: "proxy refused the tunnel" };
554
+ }
555
+ if (/wrong version number|EPROTO|packet length too long|unknown protocol/i.test(text)) {
556
+ return proxy
557
+ ? { why: `the proxy ${shown} answered in plain text where a secure connection was expected`, fix: "check its address: company proxies are usually http://host:port, even for secure traffic", share: "TLS answered in plain text (via proxy)" }
558
+ : { why: "this network answered the relay's secure connection in plain text, which is what a network that requires a proxy does", fix: "set the proxy and run it again: export HTTPS_PROXY=http://host:port (or save it: pantheon network --proxy http://host:port)", share: "TLS answered in plain text (direct) — likely a required proxy" };
559
+ }
560
+ if (/SELF_SIGNED_CERT_IN_CHAIN|UNABLE_TO_GET_ISSUER_CERT_LOCALLY|UNABLE_TO_VERIFY_LEAF_SIGNATURE|self[- ]signed certificate|unable to get local issuer/i.test(text)) {
561
+ return { why: "the relay's certificate was swapped for one this machine does not trust, which happens on networks that inspect secure traffic", fix: "point Node at your company's root certificate: export NODE_EXTRA_CA_CERTS=/path/to/root.pem (or on Node 22.15+: NODE_USE_SYSTEM_CA=1)", share: "certificate not trusted — TLS inspection" };
562
+ }
563
+ if (/CERT_HAS_EXPIRED|certificate has expired|ERR_TLS_CERT_ALTNAME_INVALID|Hostname\/IP does not match/i.test(text)) {
564
+ return { why: "the relay's certificate looks expired or belongs to another name", fix: "check this machine's date and time, then try another network", share: "certificate expired or for another name" };
565
+ }
566
+ if (/ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(text)) {
567
+ return proxy
568
+ ? { why: `the proxy ${shown} could not find the relay's address`, fix: "check the relay address with pantheon doctor", share: "name did not resolve" }
569
+ : { why: "the relay's address did not resolve — this machine may be offline", fix: `check the connection and try again. ${orProxy}`, share: "name did not resolve" };
570
+ }
571
+ if (/ECONNREFUSED/i.test(text)) {
572
+ return proxy
573
+ ? { why: `nothing is accepting connections at the proxy ${shown}`, fix: "check the proxy address and port, or clear it: pantheon network --clear", share: "proxy refused the connection" }
574
+ : { why: "nothing is accepting connections at the relay's address — it may be down or restarting", fix: "try again in a minute; pantheon doctor shows more", share: "connection refused" };
575
+ }
576
+ if (/ETIMEDOUT|UND_ERR_CONNECT_TIMEOUT|handshake has timed out|timed out|EHOSTUNREACH|ENETUNREACH/i.test(text)) {
577
+ return proxy
578
+ ? { why: `the proxy ${shown} did not answer in time`, fix: "check it is reachable from this machine, or clear it: pantheon network --clear", share: "connection timed out" }
579
+ : { why: "the relay did not answer in time", fix: `check this machine is online and try again. ${orProxy}`, share: "connection timed out" };
580
+ }
581
+ if (/ECONNRESET|socket hang up/i.test(text)) {
582
+ return proxy
583
+ ? { why: `the connection through the proxy ${shown} was cut`, fix: `the proxy may not allow WebSockets; ask for ${host} to be allowed`, share: "connection reset" }
584
+ : { why: "the connection dropped before the relay answered", fix: "try again", share: "connection reset" };
585
+ }
586
+ return null;
587
+ }
588
+ export function describeNetError(e, target, plan) {
589
+ const raw = e?.message ?? String(e);
590
+ const x = explainNetError(e, target, plan);
591
+ const first = (raw.split("\n")[0] ?? "").slice(0, 120);
592
+ const fix = x ? x.fix.charAt(0).toUpperCase() + x.fix.slice(1) : "";
593
+ return x ? `${x.why}. ${fix}. (${first})`.replace(/\.\. \(/, ". (").replace(/\?\. \(/, "? (") : raw;
594
+ }
595
+ //# sourceMappingURL=network.js.map
package/dist/pending.js CHANGED
@@ -44,24 +44,30 @@ export function cursorSeq(session, cfg, home) {
44
44
  const entry = readCursors(cursorPath(home))[keyFor(c, session)];
45
45
  return entry?.seq;
46
46
  }
47
+ let clockSkewMs = 0;
47
48
  export async function reportActivity(opts) {
48
49
  const cfg = opts.cfg ?? load();
49
50
  if (!cfg)
50
51
  return false;
51
52
  const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
52
53
  const seen = opts.seen !== undefined ? String(opts.seen) : "";
53
- const ts = Date.now();
54
- const sig = signActivity(cfg.privateKey, cfg.space, cfg.member, opts.session, opts.state, seen, ts);
55
- const q = new URLSearchParams({
54
+ const query = (ts) => new URLSearchParams({
56
55
  space: cfg.space, token: cfg.token, member: cfg.member,
57
56
  session: opts.session, state: opts.state,
58
57
  ...(opts.seen !== undefined ? { seen } : {}),
59
- ts: String(ts), sig,
58
+ ts: String(ts), sig: signActivity(cfg.privateKey, cfg.space, cfg.member, opts.session, opts.state, seen, ts),
60
59
  });
61
60
  const ac = new AbortController();
62
61
  const timer = setTimeout(() => ac.abort(), LIMITS.pendingTimeoutMs);
63
62
  try {
64
- const res = await fetch(`${base}/activity?${q}`, { method: "POST", signal: ac.signal });
63
+ let res = await fetch(`${base}/activity?${query(Date.now() + clockSkewMs)}`, { method: "POST", signal: ac.signal });
64
+ if (res.status === 403) {
65
+ const there = Date.parse(res.headers.get("date") ?? "");
66
+ if (Number.isFinite(there) && Math.abs(there - Date.now() - clockSkewMs) > 60_000) {
67
+ clockSkewMs = there - Date.now();
68
+ res = await fetch(`${base}/activity?${query(Date.now() + clockSkewMs)}`, { method: "POST", signal: ac.signal });
69
+ }
70
+ }
65
71
  return res.ok;
66
72
  }
67
73
  catch {
@@ -108,7 +114,8 @@ export async function pending(opts) {
108
114
  .filter((r) => r.act !== "JOIN" && r.act !== "LEAVE")
109
115
  .filter((r) => !r.to || r.to.length === 0 || r.to.includes(cfg.member))
110
116
  .filter((r) => !mine.some((m) => m.seq > r.seq && (m.ref === r.id || (!!r.goal && m.goal === r.goal))));
111
- const seq = first
117
+ const rolledBack = !first && typeof head === "number" && head < known.seq;
118
+ const seq = first || rolledBack
112
119
  ? head
113
120
  : historyBody === null
114
121
  ? known.seq
@@ -118,7 +125,7 @@ export async function pending(opts) {
118
125
  if (!opts.peek && seq !== undefined) {
119
126
  const now = Date.now();
120
127
  const next = {};
121
- for (const [k, v] of Object.entries(cursors)) {
128
+ for (const [k, v] of Object.entries(readCursors(file))) {
122
129
  if (now - v.at < LIMITS.cursorTtlMs)
123
130
  next[k] = v;
124
131
  }