botmux 3.18.3 → 3.18.5
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/.runtime-build-id +1 -1
- package/dist/adapters/hook-installer.d.ts.map +1 -1
- package/dist/adapters/hook-installer.js +75 -2
- package/dist/adapters/hook-installer.js.map +1 -1
- package/dist/cli/current-actor.d.ts.map +1 -1
- package/dist/cli/current-actor.js +2 -1
- package/dist/cli/current-actor.js.map +1 -1
- package/dist/cli/dashboard-endpoint.d.ts.map +1 -1
- package/dist/cli/dashboard-endpoint.js +5 -2
- package/dist/cli/dashboard-endpoint.js.map +1 -1
- package/dist/cli/exact-chat-grant-client.d.ts.map +1 -1
- package/dist/cli/exact-chat-grant-client.js +2 -1
- package/dist/cli/exact-chat-grant-client.js.map +1 -1
- package/dist/cli/vc-agent.d.ts.map +1 -1
- package/dist/cli/vc-agent.js +2 -1
- package/dist/cli/vc-agent.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +105 -11
- package/dist/cli.js.map +1 -1
- package/dist/core/binary-install-shape.d.ts +77 -0
- package/dist/core/binary-install-shape.d.ts.map +1 -0
- package/dist/core/binary-install-shape.js +117 -0
- package/dist/core/binary-install-shape.js.map +1 -0
- package/dist/core/binary-self-update.d.ts +82 -0
- package/dist/core/binary-self-update.d.ts.map +1 -0
- package/dist/core/binary-self-update.js +239 -0
- package/dist/core/binary-self-update.js.map +1 -0
- package/dist/core/daemon-ipc-auth.d.ts +9 -1
- package/dist/core/daemon-ipc-auth.d.ts.map +1 -1
- package/dist/core/daemon-ipc-auth.js +14 -3
- package/dist/core/daemon-ipc-auth.js.map +1 -1
- package/dist/core/loopback-fetch.d.ts +59 -0
- package/dist/core/loopback-fetch.d.ts.map +1 -0
- package/dist/core/loopback-fetch.js +347 -0
- package/dist/core/loopback-fetch.js.map +1 -0
- package/dist/core/maintenance.d.ts +88 -1
- package/dist/core/maintenance.d.ts.map +1 -1
- package/dist/core/maintenance.js +149 -8
- package/dist/core/maintenance.js.map +1 -1
- package/dist/core/managed-origin-attestation.d.ts.map +1 -1
- package/dist/core/managed-origin-attestation.js +2 -1
- package/dist/core/managed-origin-attestation.js.map +1 -1
- package/dist/core/release-download.d.ts +13 -0
- package/dist/core/release-download.d.ts.map +1 -0
- package/dist/core/release-download.js +130 -0
- package/dist/core/release-download.js.map +1 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +2 -1
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/aggregator.d.ts.map +1 -1
- package/dist/dashboard/aggregator.js +2 -1
- package/dist/dashboard/aggregator.js.map +1 -1
- package/dist/dashboard/daemon-internal-client.d.ts.map +1 -1
- package/dist/dashboard/daemon-internal-client.js +6 -1
- package/dist/dashboard/daemon-internal-client.js.map +1 -1
- package/dist/dashboard/groups-action-helpers.d.ts +0 -10
- package/dist/dashboard/groups-action-helpers.d.ts.map +1 -1
- package/dist/dashboard/groups-action-helpers.js +3 -2
- package/dist/dashboard/groups-action-helpers.js.map +1 -1
- package/dist/dashboard/hd2d-assets.d.ts +2 -3
- package/dist/dashboard/hd2d-assets.d.ts.map +1 -1
- package/dist/dashboard/hd2d-assets.js +7 -109
- package/dist/dashboard/hd2d-assets.js.map +1 -1
- package/dist/dashboard/web/app.d.ts.map +1 -1
- package/dist/dashboard/web/app.js +7 -1
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/settings-page.d.ts.map +1 -1
- package/dist/dashboard/web/settings-page.js.map +1 -1
- package/dist/dashboard-web/app.js +1 -1
- package/dist/dashboard.js +122 -14
- package/dist/dashboard.js.map +1 -1
- package/dist/services/hook-runner.d.ts.map +1 -1
- package/dist/services/hook-runner.js +2 -1
- package/dist/services/hook-runner.js.map +1 -1
- package/dist/services/voice/openai.d.ts.map +1 -1
- package/dist/services/voice/openai.js +10 -1
- package/dist/services/voice/openai.js.map +1 -1
- package/dist/services/worktree-slug-ai.d.ts.map +1 -1
- package/dist/services/worktree-slug-ai.js +8 -1
- package/dist/services/worktree-slug-ai.js.map +1 -1
- package/dist/utils/file-lock.d.ts +25 -0
- package/dist/utils/file-lock.d.ts.map +1 -1
- package/dist/utils/file-lock.js +34 -4
- package/dist/utils/file-lock.js.map +1 -1
- package/dist/utils/global-install.d.ts +19 -0
- package/dist/utils/global-install.d.ts.map +1 -1
- package/dist/utils/global-install.js +28 -1
- package/dist/utils/global-install.js.map +1 -1
- package/dist/utils/install-info.d.ts +37 -1
- package/dist/utils/install-info.d.ts.map +1 -1
- package/dist/utils/install-info.js +45 -3
- package/dist/utils/install-info.js.map +1 -1
- package/dist/workflows/v3/daemon-ipc-client.d.ts.map +1 -1
- package/dist/workflows/v3/daemon-ipc-client.js +2 -1
- package/dist/workflows/v3/daemon-ipc-client.js.map +1 -1
- package/dist/workflows/v3/session-relay-client.d.ts.map +1 -1
- package/dist/workflows/v3/session-relay-client.js +2 -1
- package/dist/workflows/v3/session-relay-client.js.map +1 -1
- package/package.json +8 -7
- package/scripts/install-path-entry.mjs +263 -0
- package/scripts/postinstall-bin.mjs +41 -7
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fetch`-shaped client for **loopback-only** HTTP, built on `node:http`.
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ WHY THIS EXISTS — DO NOT "SIMPLIFY" ANY CALLER BACK TO THE GLOBAL `fetch`.
|
|
5
|
+
*
|
|
6
|
+
* Bun's `fetch` automatically routes through `$http_proxy`, and its `no_proxy`
|
|
7
|
+
* handling is **literal-match only**: it does not parse CIDR notation and does
|
|
8
|
+
* not treat `localhost` as covering `127.0.0.1`. Corporate dev machines commonly
|
|
9
|
+
* export exactly that shape (`http_proxy=…` plus `no_proxy=…,127.0.0.0/8,…`), so
|
|
10
|
+
* a request to our OWN loopback port gets handed to the corporate proxy, which
|
|
11
|
+
* refuses to forward an internal address and answers with an nginx HTML
|
|
12
|
+
* `403 Forbidden`. The user sees a failure naming a service that is running
|
|
13
|
+
* perfectly well two ports away.
|
|
14
|
+
*
|
|
15
|
+
* Measured on Bun 1.4.0 (source AND `bun build --compile`), one fresh process per
|
|
16
|
+
* row because Bun snapshots proxy configuration at startup:
|
|
17
|
+
*
|
|
18
|
+
* no_proxy unset / "" → proxied
|
|
19
|
+
* no_proxy=127.0.0.0/8 → proxied ← the common corporate shape
|
|
20
|
+
* no_proxy=localhost → proxied ← does NOT cover 127.0.0.1
|
|
21
|
+
* no_proxy=127.0.0.1 → direct
|
|
22
|
+
* no_proxy=* → direct
|
|
23
|
+
*
|
|
24
|
+
* And the obvious escapes do not work:
|
|
25
|
+
*
|
|
26
|
+
* fetch(…, {proxy: ''|undefined|null}) → still proxied (option exists ≠ works)
|
|
27
|
+
* delete process.env.http_proxy at run → still proxied (startup snapshot)
|
|
28
|
+
* setting process.env.http_proxy later → still direct (same reason)
|
|
29
|
+
*
|
|
30
|
+
* `node:http` ignores proxy environment variables entirely, in both Node and Bun,
|
|
31
|
+
* which is the only reliable way to guarantee a loopback dial stays loopback.
|
|
32
|
+
* Related precedent: `platform/platform-http.ts` (deliberately avoids undici) and
|
|
33
|
+
* `dashboard/hd2d-assets.ts` (the reverse case — it WANTS the proxy).
|
|
34
|
+
*/
|
|
35
|
+
/** The subset of RequestInit our loopback callers use. */
|
|
36
|
+
export interface LoopbackFetchInit {
|
|
37
|
+
method?: string;
|
|
38
|
+
headers?: HeadersInit;
|
|
39
|
+
/** Accepts what `fetch` accepts; streams are the one thing we cannot forward. */
|
|
40
|
+
body?: BodyInit | null;
|
|
41
|
+
signal?: AbortSignal | null;
|
|
42
|
+
}
|
|
43
|
+
/** Install (or clear, with `undefined`) the test transport. Returns the previous one. */
|
|
44
|
+
export declare function __setLoopbackTransportForTests(next: ((url: string, init: LoopbackFetchInit) => Promise<Response>) | undefined): ((url: string, init: LoopbackFetchInit) => Promise<Response>) | undefined;
|
|
45
|
+
export declare function loopbackFetch(url: string, init?: LoopbackFetchInit): Promise<Response>;
|
|
46
|
+
/** `typeof fetch`-compatible view, for defaulting an injectable `fetchImpl`. */
|
|
47
|
+
export declare const loopbackFetchImpl: typeof fetch;
|
|
48
|
+
/**
|
|
49
|
+
* Is this URL a literal loopback target, i.e. safe (and necessary) to send through
|
|
50
|
+
* {@link loopbackFetch}? For call sites whose base URL is configurable and may
|
|
51
|
+
* legitimately point at a remote host — those must keep using the global fetch,
|
|
52
|
+
* which handles TLS, redirects and real proxying.
|
|
53
|
+
*
|
|
54
|
+
* Literal only, deliberately: a DNS name that currently resolves to 127.0.0.1
|
|
55
|
+
* is not a loopback guarantee (it can be re-pointed), and `loopbackFetch`'s own
|
|
56
|
+
* guard would reject it anyway.
|
|
57
|
+
*/
|
|
58
|
+
export declare function isLoopbackUrl(url: string): boolean;
|
|
59
|
+
//# sourceMappingURL=loopback-fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loopback-fetch.d.ts","sourceRoot":"","sources":["../../src/core/loopback-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAKH,0DAA0D;AAC1D,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,iFAAiF;IACjF,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CAC7B;AA4FD,yFAAyF;AACzF,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,SAAS,GAC9E,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,SAAS,CAI3E;AAED,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,iBAAsB,GAC3B,OAAO,CAAC,QAAQ,CAAC,CAyJnB;AAED,gFAAgF;AAChF,eAAO,MAAM,iBAAiB,EAA+B,OAAO,KAAK,CAAC;AAE1E;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAWlD"}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fetch`-shaped client for **loopback-only** HTTP, built on `node:http`.
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ WHY THIS EXISTS — DO NOT "SIMPLIFY" ANY CALLER BACK TO THE GLOBAL `fetch`.
|
|
5
|
+
*
|
|
6
|
+
* Bun's `fetch` automatically routes through `$http_proxy`, and its `no_proxy`
|
|
7
|
+
* handling is **literal-match only**: it does not parse CIDR notation and does
|
|
8
|
+
* not treat `localhost` as covering `127.0.0.1`. Corporate dev machines commonly
|
|
9
|
+
* export exactly that shape (`http_proxy=…` plus `no_proxy=…,127.0.0.0/8,…`), so
|
|
10
|
+
* a request to our OWN loopback port gets handed to the corporate proxy, which
|
|
11
|
+
* refuses to forward an internal address and answers with an nginx HTML
|
|
12
|
+
* `403 Forbidden`. The user sees a failure naming a service that is running
|
|
13
|
+
* perfectly well two ports away.
|
|
14
|
+
*
|
|
15
|
+
* Measured on Bun 1.4.0 (source AND `bun build --compile`), one fresh process per
|
|
16
|
+
* row because Bun snapshots proxy configuration at startup:
|
|
17
|
+
*
|
|
18
|
+
* no_proxy unset / "" → proxied
|
|
19
|
+
* no_proxy=127.0.0.0/8 → proxied ← the common corporate shape
|
|
20
|
+
* no_proxy=localhost → proxied ← does NOT cover 127.0.0.1
|
|
21
|
+
* no_proxy=127.0.0.1 → direct
|
|
22
|
+
* no_proxy=* → direct
|
|
23
|
+
*
|
|
24
|
+
* And the obvious escapes do not work:
|
|
25
|
+
*
|
|
26
|
+
* fetch(…, {proxy: ''|undefined|null}) → still proxied (option exists ≠ works)
|
|
27
|
+
* delete process.env.http_proxy at run → still proxied (startup snapshot)
|
|
28
|
+
* setting process.env.http_proxy later → still direct (same reason)
|
|
29
|
+
*
|
|
30
|
+
* `node:http` ignores proxy environment variables entirely, in both Node and Bun,
|
|
31
|
+
* which is the only reliable way to guarantee a loopback dial stays loopback.
|
|
32
|
+
* Related precedent: `platform/platform-http.ts` (deliberately avoids undici) and
|
|
33
|
+
* `dashboard/hd2d-assets.ts` (the reverse case — it WANTS the proxy).
|
|
34
|
+
*/
|
|
35
|
+
import { Buffer } from 'node:buffer';
|
|
36
|
+
import { requestLiteralLoopback } from './loopback-target.js';
|
|
37
|
+
/** Normalise a fetch-style body to bytes. Streams are rejected loudly rather
|
|
38
|
+
* than silently sent as "[object ReadableStream]". */
|
|
39
|
+
function bodyToBuffer(body) {
|
|
40
|
+
if (body == null)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (typeof body === 'string')
|
|
43
|
+
return Buffer.from(body);
|
|
44
|
+
if (Buffer.isBuffer(body))
|
|
45
|
+
return body;
|
|
46
|
+
if (body instanceof URLSearchParams)
|
|
47
|
+
return Buffer.from(body.toString());
|
|
48
|
+
if (body instanceof ArrayBuffer)
|
|
49
|
+
return Buffer.from(new Uint8Array(body));
|
|
50
|
+
if (ArrayBuffer.isView(body)) {
|
|
51
|
+
return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
|
52
|
+
}
|
|
53
|
+
throw new TypeError('loopbackFetch: unsupported body type (streams/FormData are not forwarded)');
|
|
54
|
+
}
|
|
55
|
+
function headerEntries(headers) {
|
|
56
|
+
if (!headers)
|
|
57
|
+
return {};
|
|
58
|
+
// `Headers` (and anything iterable of pairs) normalises casing for us; a plain
|
|
59
|
+
// object is passed through as written.
|
|
60
|
+
const out = {};
|
|
61
|
+
if (headers instanceof Headers) {
|
|
62
|
+
headers.forEach((value, key) => { out[key] = value; });
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(headers)) {
|
|
66
|
+
for (const [k, v] of headers)
|
|
67
|
+
out[k] = String(v);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
for (const [k, v] of Object.entries(headers))
|
|
71
|
+
out[k] = String(v);
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The error an aborted request must fail with.
|
|
76
|
+
*
|
|
77
|
+
* ⚠️ Callers branch on `err.name === 'AbortError'` to tell "timed out" from
|
|
78
|
+
* "nothing listening" — e.g. daemon.ts returns 504 `wait_timeout` vs 503
|
|
79
|
+
* `daemon_offline`, and command-handler.ts reports `busy` vs `failed`. A plain
|
|
80
|
+
* `new Error('aborted')` (or node:http's own ECONNRESET) silently sends every
|
|
81
|
+
* timeout down the wrong branch. Measured against the global fetch these replace:
|
|
82
|
+
* it rejects with `name=AbortError`, while the first version of this module gave
|
|
83
|
+
* `name=Error, code=ECONNRESET`.
|
|
84
|
+
*
|
|
85
|
+
* `signal.reason` is what the platform puts there (a DOMException named
|
|
86
|
+
* AbortError, or whatever the caller passed to `abort(reason)`); we only
|
|
87
|
+
* synthesise an equivalent when it is somehow absent.
|
|
88
|
+
*/
|
|
89
|
+
function abortError(signal) {
|
|
90
|
+
const reason = signal?.reason;
|
|
91
|
+
if (reason !== undefined && reason !== null)
|
|
92
|
+
return reason;
|
|
93
|
+
// Node and browsers both expose DOMException; fall back to a shaped Error.
|
|
94
|
+
if (typeof DOMException === 'function') {
|
|
95
|
+
return new DOMException('The operation was aborted.', 'AbortError');
|
|
96
|
+
}
|
|
97
|
+
const err = new Error('The operation was aborted.');
|
|
98
|
+
err.name = 'AbortError';
|
|
99
|
+
return err;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Issue one request to a literal loopback host and resolve a real `Response`, so
|
|
103
|
+
* call sites keep using `.ok` / `.status` / `.json()` / `.text()` unchanged.
|
|
104
|
+
*
|
|
105
|
+
* ⚠️ Resolves as soon as the response HEADERS arrive, with the body exposed as a
|
|
106
|
+
* `ReadableStream` — it must NOT buffer to completion first. `dashboard/aggregator.ts`
|
|
107
|
+
* subscribes to a never-ending SSE stream (`/api/events`) and reads frames off
|
|
108
|
+
* `res.body`; a buffer-all implementation never resolves for those callers and the
|
|
109
|
+
* whole session/event aggregation silently stops. Measured: a server that writes
|
|
110
|
+
* one frame and holds the connection open produced no Response at all until this
|
|
111
|
+
* was made streaming. `.text()`/`.json()` still work, because `Response` drains
|
|
112
|
+
* the stream for them.
|
|
113
|
+
*
|
|
114
|
+
* Rejects the way `fetch` does (a transport failure) rather than resolving a
|
|
115
|
+
* synthetic error status, because callers distinguish "no server there" from
|
|
116
|
+
* "server said no".
|
|
117
|
+
*/
|
|
118
|
+
/**
|
|
119
|
+
* Test-only transport override.
|
|
120
|
+
*
|
|
121
|
+
* ⚠️ Why this seam exists: several suites drive loopback code paths by replacing
|
|
122
|
+
* `globalThis.fetch`. Moving those call sites onto node:http silently bypassed
|
|
123
|
+
* that stub, so the tests began making REAL connections to ports like 4310/39003
|
|
124
|
+
* — `fetchMock` recorded 0 calls and the assertions failed for a reason unrelated
|
|
125
|
+
* to what they test. Rather than push every caller back onto the global fetch (the
|
|
126
|
+
* whole defect), the transport itself is injectable: production keeps node:http,
|
|
127
|
+
* tests install a mock here.
|
|
128
|
+
*
|
|
129
|
+
* Not for production use — `__setLoopbackTransportForTests(undefined)` restores the default.
|
|
130
|
+
*/
|
|
131
|
+
let transportOverride;
|
|
132
|
+
/** Install (or clear, with `undefined`) the test transport. Returns the previous one. */
|
|
133
|
+
export function __setLoopbackTransportForTests(next) {
|
|
134
|
+
const previous = transportOverride;
|
|
135
|
+
transportOverride = next;
|
|
136
|
+
return previous;
|
|
137
|
+
}
|
|
138
|
+
export function loopbackFetch(url, init = {}) {
|
|
139
|
+
if (transportOverride)
|
|
140
|
+
return transportOverride(url, init);
|
|
141
|
+
const target = new URL(url);
|
|
142
|
+
// ⚠️ FAIL CLOSED on the scheme. This transport IS node:http, so an `https:` URL
|
|
143
|
+
// would be dialled as PLAINTEXT — measured: `loopbackFetch('https://127.0.0.1:P')`
|
|
144
|
+
// against a plain HTTP server returned `200 PLAIN`, i.e. a caller that believed it
|
|
145
|
+
// was using TLS would have sent its credentials in the clear. `isLoopbackUrl`
|
|
146
|
+
// already refuses https, but a public helper must not depend on callers routing
|
|
147
|
+
// through a predicate first.
|
|
148
|
+
if (target.protocol !== 'http:') {
|
|
149
|
+
return Promise.reject(new TypeError(`loopbackFetch: only http: is supported (got ${target.protocol})`));
|
|
150
|
+
}
|
|
151
|
+
// `URL.hostname` keeps IPv6 in brackets (`[::1]`), but node:http — and
|
|
152
|
+
// requestLiteralLoopback's allow-list — want the bare address. Passing the
|
|
153
|
+
// bracketed form through was rejected as `remote_host_forbidden`, i.e. a legal
|
|
154
|
+
// IPv6 loopback URL could never be dialled (measured against a `::1` server).
|
|
155
|
+
const host = normaliseLoopbackHost(target.hostname);
|
|
156
|
+
// `URL` normalises away a scheme's DEFAULT port, so both `http://127.0.0.1/x` and
|
|
157
|
+
// an explicit `http://127.0.0.1:80/x` yield `port === ''` — `Number('')` is 0 and
|
|
158
|
+
// was rejected as `invalid_port` (measured against a local server on :80, which the
|
|
159
|
+
// native fetch reaches fine). Fill in the scheme default.
|
|
160
|
+
const port = target.port ? Number(target.port) : 80;
|
|
161
|
+
const method = (init.method ?? 'GET').toUpperCase();
|
|
162
|
+
const headers = headerEntries(init.headers);
|
|
163
|
+
const body = bodyToBuffer(init.body);
|
|
164
|
+
// A body without a length makes some servers wait for more; be explicit.
|
|
165
|
+
if (body && headers['content-length'] === undefined && headers['Content-Length'] === undefined) {
|
|
166
|
+
headers['content-length'] = String(body.byteLength);
|
|
167
|
+
}
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
let settled = false;
|
|
170
|
+
const finish = (fn) => { if (!settled) {
|
|
171
|
+
settled = true;
|
|
172
|
+
fn();
|
|
173
|
+
} };
|
|
174
|
+
// Set once the signal fires, so the body stream can fail with the SAME
|
|
175
|
+
// AbortError instead of the socket's ECONNRESET.
|
|
176
|
+
let aborted;
|
|
177
|
+
// ⚠️ The abort listener MUST be removed once this request can no longer be
|
|
178
|
+
// aborted. `{ once: true }` only fires-and-forgets on a real abort; a request
|
|
179
|
+
// that simply COMPLETED left its listener attached, and `dashboard/aggregator.ts`
|
|
180
|
+
// reuses one AbortController across an unbounded SSE reconnect loop — measured,
|
|
181
|
+
// 12 completed requests left 12 listeners, each pinning a finished req/res and
|
|
182
|
+
// its closure, and a later abort would destroy every stale request in turn.
|
|
183
|
+
let detachAbort = () => { };
|
|
184
|
+
let req;
|
|
185
|
+
try {
|
|
186
|
+
req = requestLiteralLoopback(
|
|
187
|
+
// Dial the parsed host/port explicitly (and validated as loopback) rather
|
|
188
|
+
// than handing over the URL, so nothing can re-target the request.
|
|
189
|
+
{ host, port }, {
|
|
190
|
+
path: `${target.pathname}${target.search}`,
|
|
191
|
+
method,
|
|
192
|
+
headers,
|
|
193
|
+
}, res => {
|
|
194
|
+
const status = res.statusCode ?? 0;
|
|
195
|
+
// Fetch's null-body statuses that can actually ARRIVE HERE are 204/205/304,
|
|
196
|
+
// plus any HEAD response. 101 is deliberately absent: node:http never calls
|
|
197
|
+
// this callback for it (it emits `upgrade` instead — handled below), so a
|
|
198
|
+
// `status === 101` branch here would be dead code that reads as covered. `new Response(stream, {status:205})` THROWS,
|
|
199
|
+
// and it throws inside this async callback — on Node that escaped as an
|
|
200
|
+
// uncaught exception and killed the process, while Bun happens to tolerate
|
|
201
|
+
// 205, which is exactly why it went unnoticed.
|
|
202
|
+
const bodyless = status === 204 || status === 205
|
|
203
|
+
|| status === 304 || method === 'HEAD';
|
|
204
|
+
const stream = bodyless ? null : new ReadableStream({
|
|
205
|
+
start(controller) {
|
|
206
|
+
res.on('data', (chunk) => {
|
|
207
|
+
try {
|
|
208
|
+
controller.enqueue(new Uint8Array(Buffer.from(chunk)));
|
|
209
|
+
}
|
|
210
|
+
catch { /* already closed by a cancel() */ }
|
|
211
|
+
});
|
|
212
|
+
res.on('end', () => {
|
|
213
|
+
detachAbort();
|
|
214
|
+
try {
|
|
215
|
+
controller.close();
|
|
216
|
+
}
|
|
217
|
+
catch { /* already closed */ }
|
|
218
|
+
});
|
|
219
|
+
res.on('error', err => {
|
|
220
|
+
detachAbort();
|
|
221
|
+
// An abort must surface as AbortError, not the ECONNRESET that
|
|
222
|
+
// destroying the socket produces.
|
|
223
|
+
try {
|
|
224
|
+
controller.error(aborted ?? err);
|
|
225
|
+
}
|
|
226
|
+
catch { /* already errored */ }
|
|
227
|
+
});
|
|
228
|
+
},
|
|
229
|
+
// A consumer that stops reading (or aborts) must tear the socket down,
|
|
230
|
+
// otherwise a never-ending SSE keeps the connection and the process alive.
|
|
231
|
+
cancel() { detachAbort(); res.destroy(); req.destroy(); },
|
|
232
|
+
});
|
|
233
|
+
if (bodyless)
|
|
234
|
+
detachAbort();
|
|
235
|
+
// Wrapped: any status the Response constructor refuses (an upstream
|
|
236
|
+
// sending something illegal) must reject this promise, never escape as an
|
|
237
|
+
// uncaught exception out of an HTTP event callback.
|
|
238
|
+
let response;
|
|
239
|
+
try {
|
|
240
|
+
response = new Response(stream, {
|
|
241
|
+
status,
|
|
242
|
+
statusText: res.statusMessage ?? '',
|
|
243
|
+
headers: Object.fromEntries(Object.entries(res.headers)
|
|
244
|
+
.filter((entry) => typeof entry[1] === 'string')),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
detachAbort();
|
|
249
|
+
res.destroy();
|
|
250
|
+
finish(() => reject(err));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
finish(() => resolve(response));
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
// Invalid host/port (the loopback guard) — surface it, do not hang.
|
|
258
|
+
detachAbort();
|
|
259
|
+
finish(() => reject(err));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// ⚠️ A 101 arrives as `upgrade`, NOT as a response. Without this listener the
|
|
263
|
+
// promise never settles: measured against a server answering 101, the call only
|
|
264
|
+
// ended when an external AbortSignal fired at 800ms (and `requestDashboardAt`
|
|
265
|
+
// passes no signal at all, so it would hang forever if the recorded port happened
|
|
266
|
+
// to be a WebSocket service). The native fetch rejects such a response in ~1ms;
|
|
267
|
+
// do the same, since a fetch-shaped client cannot express a protocol switch.
|
|
268
|
+
req.on('upgrade', (_res, socket) => {
|
|
269
|
+
socket.destroy();
|
|
270
|
+
req.destroy();
|
|
271
|
+
detachAbort();
|
|
272
|
+
finish(() => reject(new TypeError('loopbackFetch: server switched protocols (101); this client does not support upgrades')));
|
|
273
|
+
});
|
|
274
|
+
// Only a failure BEFORE the response headers can reject the promise; once the
|
|
275
|
+
// Response exists, later socket errors surface through the stream instead.
|
|
276
|
+
req.on('error', err => { detachAbort(); finish(() => reject(aborted ?? err)); });
|
|
277
|
+
if (init.signal) {
|
|
278
|
+
const signal = init.signal;
|
|
279
|
+
const onAbort = () => {
|
|
280
|
+
aborted = abortError(signal);
|
|
281
|
+
req.destroy();
|
|
282
|
+
finish(() => reject(aborted));
|
|
283
|
+
};
|
|
284
|
+
if (signal.aborted) {
|
|
285
|
+
onAbort();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
289
|
+
detachAbort = () => {
|
|
290
|
+
signal.removeEventListener('abort', onAbort);
|
|
291
|
+
detachAbort = () => { };
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (body)
|
|
295
|
+
req.end(body);
|
|
296
|
+
else
|
|
297
|
+
req.end();
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
/** `typeof fetch`-compatible view, for defaulting an injectable `fetchImpl`. */
|
|
301
|
+
export const loopbackFetchImpl = loopbackFetch;
|
|
302
|
+
/**
|
|
303
|
+
* Is this URL a literal loopback target, i.e. safe (and necessary) to send through
|
|
304
|
+
* {@link loopbackFetch}? For call sites whose base URL is configurable and may
|
|
305
|
+
* legitimately point at a remote host — those must keep using the global fetch,
|
|
306
|
+
* which handles TLS, redirects and real proxying.
|
|
307
|
+
*
|
|
308
|
+
* Literal only, deliberately: a DNS name that currently resolves to 127.0.0.1
|
|
309
|
+
* is not a loopback guarantee (it can be re-pointed), and `loopbackFetch`'s own
|
|
310
|
+
* guard would reject it anyway.
|
|
311
|
+
*/
|
|
312
|
+
export function isLoopbackUrl(url) {
|
|
313
|
+
try {
|
|
314
|
+
const parsed = new URL(url);
|
|
315
|
+
// `http:` only — this transport IS node:http, so reporting an `https:` URL as
|
|
316
|
+
// loopback-safe would steer a caller onto a transport that cannot serve it.
|
|
317
|
+
if (parsed.protocol !== 'http:')
|
|
318
|
+
return false;
|
|
319
|
+
const host = normaliseLoopbackHost(parsed.hostname);
|
|
320
|
+
return host === '127.0.0.1' || host === '::1';
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Canonicalise a hostname for the loopback allow-list.
|
|
328
|
+
*
|
|
329
|
+
* · Strips the brackets `URL.hostname` keeps around IPv6 literals.
|
|
330
|
+
* · Maps `localhost` to `127.0.0.1`.
|
|
331
|
+
*
|
|
332
|
+
* ⚠️ `localhost` USED to be rejected here, on the reasoning that "a DNS name is
|
|
333
|
+
* not a loopback guarantee". That was backwards: refusing it did not avoid a
|
|
334
|
+
* lookup, it pushed the request onto the GLOBAL fetch, which then proxied it —
|
|
335
|
+
* measured with a self-hosted TTS endpoint, the `Authorization: Bearer …` header
|
|
336
|
+
* arrived at the stand-in proxy. `localhost` is reserved for loopback by RFC 6761,
|
|
337
|
+
* and mapping it here means we dial the literal address and never resolve DNS at
|
|
338
|
+
* all, which is strictly safer than the alternative. Every OTHER hostname stays
|
|
339
|
+
* rejected — that is where the rebinding concern actually applies.
|
|
340
|
+
*/
|
|
341
|
+
function normaliseLoopbackHost(hostname) {
|
|
342
|
+
const bare = hostname.startsWith('[') && hostname.endsWith(']')
|
|
343
|
+
? hostname.slice(1, -1)
|
|
344
|
+
: hostname;
|
|
345
|
+
return bare === 'localhost' ? '127.0.0.1' : bare;
|
|
346
|
+
}
|
|
347
|
+
//# sourceMappingURL=loopback-fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loopback-fetch.js","sourceRoot":"","sources":["../../src/core/loopback-fetch.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,6 +20,7 @@ import { spawn } from 'node:child_process';
|
|
|
20
20
|
import { type MaintenanceConfig } from '../global-config.js';
|
|
21
21
|
import { type RestartIntent } from '../services/restart-intent-store.js';
|
|
22
22
|
import { type GlobalInstallPlan } from '../utils/global-install.js';
|
|
23
|
+
import { type BinaryInstallShape, type UpdateStrategy } from './binary-self-update.js';
|
|
23
24
|
export interface MaintenanceState {
|
|
24
25
|
/** Local date the auto-update run was last handled (fired or skipped). */
|
|
25
26
|
autoUpdate?: {
|
|
@@ -39,6 +40,30 @@ export interface MaintenanceDeps {
|
|
|
39
40
|
currentVersion: () => string;
|
|
40
41
|
/** Updates the owning npm/pnpm/Bun global install (download/install only). */
|
|
41
42
|
runUpdate: () => void;
|
|
43
|
+
/**
|
|
44
|
+
* What `runUpdate` actually installed, or '' when it could not be determined.
|
|
45
|
+
*
|
|
46
|
+
* ⚠️ WHY THIS EXISTS — THE BAKED VERSION SHADOWS A COMPLETED UPDATE, ON BOTH
|
|
47
|
+
* STRATEGIES. A compiled binary's version is baked in at compile time, and
|
|
48
|
+
* `botmuxVersionAt` returns that value for ANY directory you ask about. So
|
|
49
|
+
* `currentVersion()` after an update still reports this process's OLD version:
|
|
50
|
+
*
|
|
51
|
+
* · self-replace — the file on disk was swapped; nothing on disk is read.
|
|
52
|
+
* · package-manager — npm rewrote the install's package.json, but the baked
|
|
53
|
+
* value takes priority over reading it (measured:
|
|
54
|
+
* package.json says 3.19.0, the call returns 3.18.4).
|
|
55
|
+
*
|
|
56
|
+
* Either way `after === before`, so the tick logs "already on the latest
|
|
57
|
+
* version" and NEVER restarts onto what it just installed. Both branches
|
|
58
|
+
* therefore report the installed version explicitly — the self-replace path from
|
|
59
|
+
* what it downloaded, the package-manager path via `diskVersionAt` (which
|
|
60
|
+
* deliberately ignores the baked value).
|
|
61
|
+
*
|
|
62
|
+
* Returns '' when undeterminable, and the tick falls back to the re-read
|
|
63
|
+
* comparison. Under Node the baked value is absent, so all of this is a no-op
|
|
64
|
+
* and behaviour is byte-identical to before.
|
|
65
|
+
*/
|
|
66
|
+
installedVersion?: () => string;
|
|
42
67
|
writeIntent: (intent: RestartIntent) => void;
|
|
43
68
|
/** Spawn a detached `botmux restart` (this process is then killed by pm2). */
|
|
44
69
|
triggerRestart: () => void;
|
|
@@ -73,6 +98,23 @@ export declare function maintenanceRestartLogPath(): string;
|
|
|
73
98
|
export declare function globalInstallUpdateCwd(): string;
|
|
74
99
|
/** Run the ownership-aware update synchronously. */
|
|
75
100
|
export declare function installLatestBotmuxSync(plan?: GlobalInstallPlan): void;
|
|
101
|
+
/**
|
|
102
|
+
* Apply an update for whatever install shape this process is, resolving the
|
|
103
|
+
* strategy first so a compiled binary is handled instead of being rejected as an
|
|
104
|
+
* "unsupported install".
|
|
105
|
+
*
|
|
106
|
+
* Shared by the CLI (`botmux update`), the dashboard's `/api/update/run` and the
|
|
107
|
+
* scheduled auto-update so the three cannot drift — they previously all funnelled
|
|
108
|
+
* into `resolveGlobalInstallPlan`, which is exactly the call that fails in
|
|
109
|
+
* compiled mode.
|
|
110
|
+
*
|
|
111
|
+
* @param spec the version spec for the package-manager path, and the version to
|
|
112
|
+
* download for the self-replace path ('latest' resolves upstream).
|
|
113
|
+
*/
|
|
114
|
+
export declare function applyBotmuxUpdate(installRoot: string, version: string): Promise<{
|
|
115
|
+
strategy: UpdateStrategy['kind'];
|
|
116
|
+
detail: string;
|
|
117
|
+
}>;
|
|
76
118
|
/**
|
|
77
119
|
* Cross-process lock target that serializes global botmux updates
|
|
78
120
|
* between the scheduled auto-update (this daemon process) and a
|
|
@@ -92,12 +134,43 @@ export declare function globalInstallUpdateLockTarget(): string;
|
|
|
92
134
|
* restarts the rest (the 2026-06-11 incident). `setsid` starts it in a brand
|
|
93
135
|
* new session, reparented to init, immune to botmux-0's teardown. Without
|
|
94
136
|
* setsid we fall back to a plain spawn (still detached by the caller).
|
|
137
|
+
*
|
|
138
|
+
* ⚠️ COMPILED BINARY: there is no `cli.js` to pass. Under Node the shape is
|
|
139
|
+
* `node <dist/cli.js> restart`, where argv[2] is `restart`. A single-file
|
|
140
|
+
* executable IS the CLI, so the same shape becomes `<binary> <cli.js path>
|
|
141
|
+
* restart` — and argv[2] is then the PATH, not `restart`. MEASURED on the real
|
|
142
|
+
* v3.18.4 binary: it printed the help banner and **exited 0**, i.e. a restart
|
|
143
|
+
* that silently never happened while reporting success. So in standalone mode the
|
|
144
|
+
* entry argument is dropped entirely and the binary is invoked as
|
|
145
|
+
* `<binary> restart`.
|
|
95
146
|
*/
|
|
96
|
-
export declare function buildRestartLauncher(node: string, cliEntry: string, hasSetsid: boolean): {
|
|
147
|
+
export declare function buildRestartLauncher(node: string, cliEntry: string, hasSetsid: boolean, standalone?: boolean): {
|
|
97
148
|
cmd: string;
|
|
98
149
|
args: string[];
|
|
99
150
|
};
|
|
100
151
|
export declare function detachedRestartEnv(inheritedEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
152
|
+
/**
|
|
153
|
+
* Resolve the executable a detached restart should launch.
|
|
154
|
+
*
|
|
155
|
+
* ⚠️ WHY `process.execPath` IS NOT ALWAYS RIGHT FOR A COMPILED BINARY. For the
|
|
156
|
+
* self-replacing (install.sh) shape it is exactly right: the path is stable and we
|
|
157
|
+
* just swapped the bytes underneath it. But for a PACKAGE-MANAGER-owned binary the
|
|
158
|
+
* running path can be VERSIONED — pnpm's virtual store yields
|
|
159
|
+
* `…/.pnpm/botmux-linux-x64@<old version>/node_modules/botmux-linux-x64/botmux`.
|
|
160
|
+
* After the update, npm/pnpm's postinstall has re-pointed the stable launcher at
|
|
161
|
+
* the NEW subpackage, while this process's `execPath` still names the OLD one. So
|
|
162
|
+
* restarting from `execPath` either fails with ENOENT (the old store entry was
|
|
163
|
+
* pruned) or silently starts the OLD binary — an update that reports success and
|
|
164
|
+
* does not take effect.
|
|
165
|
+
*
|
|
166
|
+
* The stable launcher (`~/.botmux/bin/botmux`) is what both installers maintain and
|
|
167
|
+
* re-point, so it is the correct target after a package-manager update. Fall back
|
|
168
|
+
* to `execPath` when it is missing (nothing else to go on) — that is the
|
|
169
|
+
* pre-existing behaviour.
|
|
170
|
+
*
|
|
171
|
+
* Pure over its inputs so every branch is testable without a compiled binary.
|
|
172
|
+
*/
|
|
173
|
+
export declare function resolveStandaloneRestartExecutable(standalone: boolean, execPath: string, shape: BinaryInstallShape, launcherPath: string, launcherExists: boolean): string;
|
|
101
174
|
/**
|
|
102
175
|
* Spawn a detached `botmux restart`, immune to this process's own teardown
|
|
103
176
|
* (setsid → a new session reparented to init, so PM2 killing the current
|
|
@@ -108,6 +181,20 @@ export declare function detachedRestartEnv(inheritedEnv?: NodeJS.ProcessEnv): No
|
|
|
108
181
|
* @param reason short tag written to the log (e.g. 'auto-update', 'dashboard').
|
|
109
182
|
*/
|
|
110
183
|
export declare function spawnDetachedRestart(reason: string, activePackageRoot?: string, restartLeaseId?: string): ReturnType<typeof spawn>;
|
|
184
|
+
/**
|
|
185
|
+
* Perform a binary self-replace in a CHILD process and block until it finishes.
|
|
186
|
+
*
|
|
187
|
+
* WHY A CHILD AND NOT AN `await` HERE: `runMaintenanceTick` is synchronous and
|
|
188
|
+
* runs inside `withFileLockSync`, so it cannot await the download. Re-execing this
|
|
189
|
+
* same binary with a hidden `__self-update` subcommand keeps the lock semantics
|
|
190
|
+
* byte-identical to the package-manager branch (spawn one child, wait, throw on a
|
|
191
|
+
* non-zero exit) without turning the whole tick async — which would change the
|
|
192
|
+
* locking shape for the npm path too, the one that is currently working.
|
|
193
|
+
*
|
|
194
|
+
* Returns the version that was installed (printed by the child on its last line),
|
|
195
|
+
* or '' when the child reported success without a parseable version.
|
|
196
|
+
*/
|
|
197
|
+
export declare function runSelfReplaceBlocking(version?: string): string;
|
|
111
198
|
/** Start the maintenance loop. Call only on the primary daemon (bot-0). */
|
|
112
199
|
export declare function startMaintenance(): void;
|
|
113
200
|
export declare function stopMaintenance(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"maintenance.d.ts","sourceRoot":"","sources":["../../src/core/maintenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAY,KAAK,EAAa,MAAM,oBAAoB,CAAC;AAMhE,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAG/E,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,qCAAqC,CAAC;
|
|
1
|
+
{"version":3,"file":"maintenance.d.ts","sourceRoot":"","sources":["../../src/core/maintenance.ts"],"names":[],"mappings":"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"}
|