@join-pantheon/cli 0.28.0 → 0.29.1
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/dist/accounts.js +13 -2
- package/dist/agents.js +377 -9
- package/dist/cli.js +378 -192
- package/dist/client.js +73 -12
- package/dist/config.js +15 -0
- package/dist/doctor.js +13 -10
- package/dist/grants.js +16 -1
- package/dist/hooks-install.js +4 -2
- package/dist/host-env.js +51 -6
- package/dist/install.js +216 -16
- package/dist/kiro-files.js +23 -6
- package/dist/ledger.js +8 -2
- package/dist/machine.js +286 -60
- package/dist/mcp-supervisor.js +5 -0
- package/dist/mcp.js +33 -6
- package/dist/network.js +258 -57
- package/dist/pending.js +14 -7
- package/dist/quickstart.js +186 -22
- package/dist/refusal.js +29 -0
- package/dist/relay.js +738 -447
- package/dist/roster-file.js +19 -3
- package/dist/run-failure.js +18 -7
- package/dist/service.js +192 -40
- package/dist/setup-guide.js +21 -4
- package/dist/store.js +9 -3
- package/dist/which.js +66 -4
- package/dist/workdir.js +7 -1
- package/dist/worker.js +522 -101
- package/helper/macos/menubar.swift +11 -3
- package/package.json +1 -1
package/dist/network.js
CHANGED
|
@@ -44,9 +44,12 @@ export function redactProxy(url) {
|
|
|
44
44
|
return u.toString().replace(/\/$/, "");
|
|
45
45
|
}
|
|
46
46
|
catch {
|
|
47
|
-
return
|
|
47
|
+
return redactText(url);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
export function redactText(raw) {
|
|
51
|
+
return raw.replace(/^(\s*(?:[a-z][a-z0-9+.-]*:\/\/)?)[^@/\s]*@/i, "$1***@");
|
|
52
|
+
}
|
|
50
53
|
export function splitNoProxy(raw) {
|
|
51
54
|
return [...new Set([...(raw ?? "").split(/[,\s]+/).map((s) => s.trim().toLowerCase()).filter(Boolean), ...LOOPBACK])];
|
|
52
55
|
}
|
|
@@ -133,6 +136,8 @@ const run = (cmd, args) => {
|
|
|
133
136
|
return null;
|
|
134
137
|
}
|
|
135
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`;
|
|
136
141
|
function savedFromConfig(env) {
|
|
137
142
|
try {
|
|
138
143
|
const home = env.PANTHEON_HOME ?? join(env.HOME ?? env.USERPROFILE ?? "", ".pantheon");
|
|
@@ -150,18 +155,30 @@ export const defaultNetworkDeps = () => ({
|
|
|
150
155
|
os: platform(),
|
|
151
156
|
saved: () => savedFromConfig(process.env),
|
|
152
157
|
scutil: () => run("/usr/sbin/scutil", ["--proxy"]),
|
|
153
|
-
winReg: () => run(
|
|
158
|
+
winReg: () => run(windowsRegExe(), ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"]),
|
|
154
159
|
});
|
|
155
160
|
export function resolveProxyPlan(d = defaultNetworkDeps()) {
|
|
156
161
|
const noProxy = splitNoProxy(pick(d.env, "no_proxy", "NO_PROXY"));
|
|
157
162
|
const forced = d.env.PANTHEON_PROXY?.trim();
|
|
158
163
|
if (forced) {
|
|
159
164
|
if (/^(none|direct|off|false|0)$/i.test(forced))
|
|
160
|
-
return { ...NONE, noProxy };
|
|
165
|
+
return { ...NONE, noProxy, directChosen: true };
|
|
161
166
|
const p = normalizeProxy(forced);
|
|
162
167
|
if (p)
|
|
163
168
|
return { https: p, http: p, noProxy, source: "PANTHEON_PROXY", fallbackOnly: false };
|
|
164
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
|
+
}
|
|
165
182
|
const envHttps = pick(d.env, "https_proxy", "HTTPS_PROXY", "all_proxy", "ALL_PROXY");
|
|
166
183
|
const envHttp = pick(d.env, "http_proxy", "HTTP_PROXY", "all_proxy", "ALL_PROXY");
|
|
167
184
|
if (envHttps || envHttp) {
|
|
@@ -170,12 +187,9 @@ export function resolveProxyPlan(d = defaultNetworkDeps()) {
|
|
|
170
187
|
if (hs || h)
|
|
171
188
|
return { https: hs, http: h, noProxy, source: "env", fallbackOnly: false };
|
|
172
189
|
}
|
|
173
|
-
const
|
|
174
|
-
if (
|
|
175
|
-
return
|
|
176
|
-
const sp = saved?.proxy ? normalizeProxy(saved.proxy) : null;
|
|
177
|
-
if (sp)
|
|
178
|
-
return { https: sp, http: sp, noProxy: splitNoProxy([saved?.no_proxy, ...noProxy].filter(Boolean).join(",")), source: "config", fallbackOnly: false };
|
|
190
|
+
const auto = fromSaved(true);
|
|
191
|
+
if (auto)
|
|
192
|
+
return auto;
|
|
179
193
|
const sys = d.os === "darwin" ? (() => { const o = d.scutil(); return o ? parseScutil(o) : null; })()
|
|
180
194
|
: d.os === "win32" ? (() => { const o = d.winReg(); return o ? parseWindowsReg(o) : null; })()
|
|
181
195
|
: null;
|
|
@@ -249,12 +263,51 @@ export function tunnelAgent(proxyUrl, secure) {
|
|
|
249
263
|
return new Tunnel({ keepAlive: false });
|
|
250
264
|
}
|
|
251
265
|
let installed = null;
|
|
252
|
-
|
|
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
|
+
}
|
|
253
309
|
export function wsOptions(address) {
|
|
254
|
-
const
|
|
255
|
-
if (plan.fallbackOnly && !fallbackUsed)
|
|
256
|
-
return {};
|
|
257
|
-
const p = proxyFor(address, plan);
|
|
310
|
+
const p = routeFor(address, currentPlan());
|
|
258
311
|
if (!p)
|
|
259
312
|
return {};
|
|
260
313
|
return { agent: tunnelAgent(p, /^wss:/.test(address)) };
|
|
@@ -268,64 +321,193 @@ export async function installNetwork() {
|
|
|
268
321
|
const plan = currentPlan();
|
|
269
322
|
if (plan.https || plan.http) {
|
|
270
323
|
if (!plan.fallbackOnly)
|
|
271
|
-
await
|
|
272
|
-
|
|
273
|
-
|
|
324
|
+
await useRouterForFetch(plan);
|
|
325
|
+
if (hasAlternate(plan))
|
|
326
|
+
wrapFetch(plan);
|
|
274
327
|
}
|
|
275
328
|
return plan;
|
|
276
329
|
}
|
|
277
|
-
|
|
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) {
|
|
278
363
|
if (installed?.active)
|
|
279
364
|
return;
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
noProxy: plan.noProxy.join(","),
|
|
285
|
-
}));
|
|
365
|
+
const u = await import("undici");
|
|
366
|
+
const g = globalThis;
|
|
367
|
+
previousDispatcher ??= { v1: g[DISPATCHER], v2: g[DISPATCHER2] };
|
|
368
|
+
u.setGlobalDispatcher(makeRouter(u, plan));
|
|
286
369
|
if (installed)
|
|
287
370
|
installed.active = plan.https ?? plan.http;
|
|
288
371
|
}
|
|
289
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;
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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) {
|
|
295
402
|
const g = globalThis;
|
|
296
|
-
if (
|
|
403
|
+
if (originalFetch)
|
|
297
404
|
return;
|
|
298
|
-
|
|
299
|
-
|
|
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] });
|
|
300
413
|
g.fetch = (async (input, init) => {
|
|
301
|
-
|
|
302
|
-
|
|
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;
|
|
303
432
|
try {
|
|
304
|
-
|
|
433
|
+
res = await send(input, init, "primary");
|
|
305
434
|
}
|
|
306
435
|
catch (e) {
|
|
307
|
-
|
|
308
|
-
|
|
436
|
+
if (!safeToRetry(e, method))
|
|
437
|
+
throw e;
|
|
438
|
+
if (init?.signal?.aborted && !isCallerTimeout(init.signal.reason))
|
|
309
439
|
throw e;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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;
|
|
313
454
|
}
|
|
455
|
+
if (relayAnsweredFetch(res))
|
|
456
|
+
noteRelayAnswered(url);
|
|
457
|
+
return res;
|
|
314
458
|
});
|
|
315
459
|
}
|
|
316
460
|
export function trySystemFallback(address, e) {
|
|
317
461
|
const plan = currentPlan();
|
|
318
|
-
if (!plan
|
|
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;
|
|
319
467
|
return false;
|
|
320
|
-
|
|
321
|
-
|
|
468
|
+
}
|
|
469
|
+
s.trialUntil = clock() + TRIAL_TTL;
|
|
322
470
|
return true;
|
|
323
471
|
}
|
|
324
|
-
export const
|
|
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
|
+
};
|
|
325
504
|
export function proxyEnvFor(plan = currentPlan()) {
|
|
505
|
+
if (plan.directChosen)
|
|
506
|
+
return { ...DIRECT_ENV };
|
|
326
507
|
if (!(plan.https || plan.http) || plan.source === "env")
|
|
327
508
|
return {};
|
|
328
|
-
|
|
509
|
+
const viaProxy = hasAlternate(plan) ? (lastRelayRoute ? lastRelayRoute.proxy !== null : !plan.fallbackOnly) : true;
|
|
510
|
+
if (!viaProxy)
|
|
329
511
|
return {};
|
|
330
512
|
return {
|
|
331
513
|
...(plan.https ? { HTTPS_PROXY: plan.https, https_proxy: plan.https } : {}),
|
|
@@ -333,23 +515,42 @@ export function proxyEnvFor(plan = currentPlan()) {
|
|
|
333
515
|
NO_PROXY: plan.noProxy.join(","), no_proxy: plan.noProxy.join(","),
|
|
334
516
|
};
|
|
335
517
|
}
|
|
336
|
-
export function resetNetworkForTests(plan) {
|
|
518
|
+
export function resetNetworkForTests(plan, opts = {}) {
|
|
337
519
|
installed = plan ? { plan, active: null } : null;
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
+
}
|
|
341
535
|
}
|
|
342
|
-
export function explainNetError(e,
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
const
|
|
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);
|
|
346
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";
|
|
347
548
|
const orProxy = "Behind a company proxy? pantheon network --proxy http://host:port";
|
|
348
549
|
if (/PROXY_AUTH_REQUIRED|\b407\b/.test(text)) {
|
|
349
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" };
|
|
350
551
|
}
|
|
351
552
|
if (/PROXY_REFUSED/.test(text)) {
|
|
352
|
-
return { why: `the proxy ${shown} would not open a connection to the relay`, fix:
|
|
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" };
|
|
353
554
|
}
|
|
354
555
|
if (/wrong version number|EPROTO|packet length too long|unknown protocol/i.test(text)) {
|
|
355
556
|
return proxy
|
|
@@ -379,14 +580,14 @@ export function explainNetError(e, plan = currentPlan()) {
|
|
|
379
580
|
}
|
|
380
581
|
if (/ECONNRESET|socket hang up/i.test(text)) {
|
|
381
582
|
return proxy
|
|
382
|
-
? { why: `the connection through the proxy ${shown} was cut`, fix:
|
|
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" }
|
|
383
584
|
: { why: "the connection dropped before the relay answered", fix: "try again", share: "connection reset" };
|
|
384
585
|
}
|
|
385
586
|
return null;
|
|
386
587
|
}
|
|
387
|
-
export function describeNetError(e, plan) {
|
|
588
|
+
export function describeNetError(e, target, plan) {
|
|
388
589
|
const raw = e?.message ?? String(e);
|
|
389
|
-
const x = explainNetError(e, plan);
|
|
590
|
+
const x = explainNetError(e, target, plan);
|
|
390
591
|
const first = (raw.split("\n")[0] ?? "").slice(0, 120);
|
|
391
592
|
const fix = x ? x.fix.charAt(0).toUpperCase() + x.fix.slice(1) : "";
|
|
392
593
|
return x ? `${x.why}. ${fix}. (${first})`.replace(/\.\. \(/, ". (").replace(/\?\. \(/, "? (") : raw;
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
}
|