@basein/runner 0.2.11 → 0.3.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.
- package/README.md +540 -350
- package/dist/auth/client.d.ts +65 -3
- package/dist/auth/client.js +305 -53
- package/dist/bin/bir-hooks.d.ts +72 -31
- package/dist/bin/bir-hooks.js +176 -284
- package/dist/bin/bir-proxy.d.ts +17 -2
- package/dist/bin/bir-proxy.js +222 -28
- package/dist/bin/bir-scenario.d.ts +61 -10
- package/dist/bin/bir-scenario.js +704 -135
- package/dist/bin/bir.d.ts +14 -8
- package/dist/bin/bir.js +2937 -488
- package/dist/bin/investigate.js +41 -16
- package/dist/bin/scenario-edit.d.ts +43 -10
- package/dist/bin/scenario-edit.js +281 -80
- package/dist/bin/scenario-liveness.d.ts +194 -0
- package/dist/bin/scenario-liveness.js +459 -0
- package/dist/bin/scenario-stderr.d.ts +43 -0
- package/dist/bin/scenario-stderr.js +76 -0
- package/dist/bin/service-read.d.ts +88 -0
- package/dist/bin/service-read.js +128 -0
- package/dist/bin/setup.d.ts +19 -0
- package/dist/bin/setup.js +146 -20
- package/dist/config/adapters/claude-code.d.ts +92 -14
- package/dist/config/adapters/claude-code.js +291 -101
- package/dist/config/adapters/generic.d.ts +4 -1
- package/dist/config/adapters/generic.js +13 -14
- package/dist/config/generate.d.ts +392 -14
- package/dist/config/generate.js +666 -19
- package/dist/config/mcp-approval.d.ts +76 -0
- package/dist/config/mcp-approval.js +210 -0
- package/dist/config/node-path.d.ts +42 -0
- package/dist/config/node-path.js +102 -0
- package/dist/config/overrides.d.ts +62 -0
- package/dist/config/overrides.js +136 -0
- package/dist/config/resolve.d.ts +70 -8
- package/dist/config/resolve.js +186 -15
- package/dist/control/client.d.ts +22 -0
- package/dist/control/client.js +26 -7
- package/dist/control/correlation.d.ts +23 -5
- package/dist/control/correlation.js +0 -0
- package/dist/control/daemon.d.ts +400 -18
- package/dist/control/daemon.js +1105 -102
- package/dist/control/discovery.d.ts +77 -5
- package/dist/control/discovery.js +186 -11
- package/dist/control/ensure-hook.d.ts +61 -0
- package/dist/control/ensure-hook.js +160 -8
- package/dist/control/paths.d.ts +26 -0
- package/dist/control/paths.js +34 -0
- package/dist/control/pin-guard.d.ts +83 -0
- package/dist/control/pin-guard.js +534 -0
- package/dist/control/recorder-keeper.d.ts +229 -0
- package/dist/control/recorder-keeper.js +721 -0
- package/dist/control/recorder-main.d.ts +73 -0
- package/dist/control/recorder-main.js +778 -0
- package/dist/control/server.d.ts +707 -31
- package/dist/control/server.js +2803 -306
- package/dist/control/transcript.d.ts +21 -1
- package/dist/control/transcript.js +378 -112
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/jsonrpc/framing.d.ts +76 -12
- package/dist/jsonrpc/framing.js +216 -59
- package/dist/proxy/intercept.d.ts +49 -5
- package/dist/proxy/intercept.js +188 -59
- package/dist/proxy/relay.d.ts +16 -0
- package/dist/proxy/relay.js +77 -5
- package/dist/proxy/revive.d.ts +122 -0
- package/dist/proxy/revive.js +251 -0
- package/dist/proxy/session.d.ts +222 -14
- package/dist/proxy/session.js +708 -99
- package/dist/record/housekeeping.d.ts +71 -0
- package/dist/record/housekeeping.js +417 -0
- package/dist/record/recorder.d.ts +51 -3
- package/dist/record/recorder.js +3 -0
- package/dist/record/redact.d.ts +103 -9
- package/dist/record/redact.js +659 -30
- package/dist/record/remote-recorder.d.ts +327 -32
- package/dist/record/remote-recorder.js +1517 -187
- package/dist/record/switchable-recorder.d.ts +126 -0
- package/dist/record/switchable-recorder.js +351 -0
- package/dist/record/truncate.d.ts +40 -1
- package/dist/record/truncate.js +111 -4
- package/dist/replay/bash-channel.d.ts +35 -0
- package/dist/replay/bash-channel.js +133 -0
- package/dist/replay/bundle.d.ts +10 -0
- package/dist/replay/bundle.js +10 -0
- package/dist/replay/controller.d.ts +371 -20
- package/dist/replay/controller.js +1185 -111
- package/dist/replay/derive.d.ts +1 -0
- package/dist/replay/derive.js +81 -24
- package/dist/replay/executor.d.ts +51 -16
- package/dist/replay/executor.js +114 -41
- package/dist/replay/handover.d.ts +54 -1
- package/dist/replay/handover.js +55 -5
- package/dist/replay/logic.d.ts +62 -10
- package/dist/replay/logic.js +140 -22
- package/dist/replay/plan.d.ts +201 -27
- package/dist/replay/plan.js +581 -141
- package/dist/replay/safety.d.ts +72 -0
- package/dist/replay/safety.js +303 -0
- package/dist/replay/source-run.d.ts +10 -0
- package/dist/replay/source-run.js +80 -35
- package/dist/replay/targets.d.ts +15 -1
- package/dist/replay/targets.js +62 -4
- package/dist/replay/tool-error.d.ts +15 -1
- package/dist/replay/tool-error.js +33 -1
- package/dist/replay/types.d.ts +7 -4
- package/dist/upstream/client.d.ts +56 -3
- package/dist/upstream/client.js +61 -4
- package/dist/upstream/headers.d.ts +43 -0
- package/dist/upstream/headers.js +90 -0
- package/dist/upstream/http-client.d.ts +213 -22
- package/dist/upstream/http-client.js +1034 -168
- package/dist/upstream/http-transport.d.ts +143 -0
- package/dist/upstream/http-transport.js +391 -0
- package/dist/upstream/lazy-client.d.ts +8 -0
- package/dist/upstream/lazy-client.js +29 -1
- package/dist/upstream/sse.d.ts +78 -0
- package/dist/upstream/sse.js +189 -0
- package/dist/upstream/stdio-client.d.ts +61 -1
- package/dist/upstream/stdio-client.js +356 -33
- package/dist/util/atomic-write.d.ts +29 -0
- package/dist/util/atomic-write.js +96 -0
- package/dist/util/journal.d.ts +15 -0
- package/dist/util/journal.js +48 -3
- package/dist/util/log.d.ts +10 -0
- package/dist/util/log.js +28 -1
- package/dist/util/off-switch.d.ts +46 -0
- package/dist/util/off-switch.js +91 -0
- package/dist/util/service-health.d.ts +133 -0
- package/dist/util/service-health.js +217 -0
- package/docs/BaseInstRunner.md +694 -621
- package/docs/calculatedReplay.md +1625 -1244
- package/docs/calculatedReplayGuide.md +1212 -879
- package/docs/installRun.md +1013 -772
- package/docs/loginWeb.md +627 -607
- package/docs/mcpmark.md +49 -16
- package/docs/my-first-sample.md +569 -545
- package/docs/quickstart.md +384 -271
- package/docs/t-bench.md +406 -394
- package/package.json +58 -57
- package/production.md +1371 -0
- package/scripts/install.ps1 +97 -6
- package/scripts/install.sh +104 -7
|
@@ -13,13 +13,27 @@
|
|
|
13
13
|
* reports failures as ordinary text with `isError: false`, `postgres-mcp` among
|
|
14
14
|
* them, is taken at its word on both sides: its `Error: relation does not exist`
|
|
15
15
|
* is a successful call here, as it was when it was recorded. A built-in's
|
|
16
|
-
* hook-shaped response is never judged
|
|
16
|
+
* hook-shaped response is never judged here: the host says whether a built-in
|
|
17
|
+
* failed (`PostToolUseFailure`), and the control server hands that verdict to
|
|
18
|
+
* `ReplayController.postTool` itself.
|
|
17
19
|
*/
|
|
18
20
|
/**
|
|
19
21
|
* The error a serialized `CallToolResult` reports, or undefined when it does
|
|
20
22
|
* not. `serialized` is the proxy's own serialization — the whole result, as JSON.
|
|
21
23
|
*/
|
|
22
24
|
export declare function toolResultError(serialized: string): string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Whether a serialized result is no result at all.
|
|
27
|
+
*
|
|
28
|
+
* A proxy that reports a call its upstream abandoned — the server died
|
|
29
|
+
* mid-call (`proxy/intercept.ts`, `onAbandoned`) — sends `isError` and no
|
|
30
|
+
* result, and the control server threads `serializeCapped(redact(undefined))`,
|
|
31
|
+
* which is `null`. A real `CallToolResult` is always an object, so this can
|
|
32
|
+
* only mean the call never produced one: it was sent, and may or may not have
|
|
33
|
+
* done its work. Read as a success, the next step was pinned and auto-approved
|
|
34
|
+
* on top of a call nobody saw finish.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isNoResult(serialized: string | undefined): boolean;
|
|
23
37
|
/**
|
|
24
38
|
* What a server says when a tool no longer exists — the one failure a grace
|
|
25
39
|
* period cannot cure (plan-services.md D8, kind 2).
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
* reports failures as ordinary text with `isError: false`, `postgres-mcp` among
|
|
14
14
|
* them, is taken at its word on both sides: its `Error: relation does not exist`
|
|
15
15
|
* is a successful call here, as it was when it was recorded. A built-in's
|
|
16
|
-
* hook-shaped response is never judged
|
|
16
|
+
* hook-shaped response is never judged here: the host says whether a built-in
|
|
17
|
+
* failed (`PostToolUseFailure`), and the control server hands that verdict to
|
|
18
|
+
* `ReplayController.postTool` itself.
|
|
17
19
|
*/
|
|
18
20
|
const MAX_ERROR_CHARS = 500;
|
|
19
21
|
/**
|
|
@@ -31,10 +33,40 @@ export function toolResultError(serialized) {
|
|
|
31
33
|
if (!parsed || typeof parsed !== "object")
|
|
32
34
|
return undefined;
|
|
33
35
|
const result = parsed;
|
|
36
|
+
// A protocol-level failure. When a `tools/call` is answered with a JSON-RPC
|
|
37
|
+
// `error` instead of a result, the proxy's step report carries that error
|
|
38
|
+
// object as the "result" (`proxy/intercept.ts`): `{ code, message }`, with no
|
|
39
|
+
// `content` and no `isError`. Read as a success, a steered step the upstream
|
|
40
|
+
// refused (bad arguments, a crashed handler) threaded as if it had worked, and
|
|
41
|
+
// the next step was pinned and auto-approved on top of it. A real
|
|
42
|
+
// `CallToolResult` always carries `content`, so the shape cannot be mistaken
|
|
43
|
+
// for one.
|
|
44
|
+
if (result.content === undefined &&
|
|
45
|
+
typeof result.code === "number" &&
|
|
46
|
+
typeof result.message === "string") {
|
|
47
|
+
return clip(`${result.code}: ${result.message}`);
|
|
48
|
+
}
|
|
34
49
|
if (result.isError !== true)
|
|
35
50
|
return undefined;
|
|
36
51
|
return clip(firstText(result.content) ?? "the tool reported an error");
|
|
37
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Whether a serialized result is no result at all.
|
|
55
|
+
*
|
|
56
|
+
* A proxy that reports a call its upstream abandoned — the server died
|
|
57
|
+
* mid-call (`proxy/intercept.ts`, `onAbandoned`) — sends `isError` and no
|
|
58
|
+
* result, and the control server threads `serializeCapped(redact(undefined))`,
|
|
59
|
+
* which is `null`. A real `CallToolResult` is always an object, so this can
|
|
60
|
+
* only mean the call never produced one: it was sent, and may or may not have
|
|
61
|
+
* done its work. Read as a success, the next step was pinned and auto-approved
|
|
62
|
+
* on top of a call nobody saw finish.
|
|
63
|
+
*/
|
|
64
|
+
export function isNoResult(serialized) {
|
|
65
|
+
if (serialized === undefined)
|
|
66
|
+
return true;
|
|
67
|
+
const trimmed = serialized.trim();
|
|
68
|
+
return trimmed === "" || trimmed === "null" || trimmed === "undefined";
|
|
69
|
+
}
|
|
38
70
|
/**
|
|
39
71
|
* What a server says when a tool no longer exists — the one failure a grace
|
|
40
72
|
* period cannot cure (plan-services.md D8, kind 2).
|
package/dist/replay/types.d.ts
CHANGED
|
@@ -211,10 +211,13 @@ export declare const OUTCOME_RANK: Record<ExecutionOutcome, number>;
|
|
|
211
211
|
* What became of one step of a replay.
|
|
212
212
|
*
|
|
213
213
|
* `ok` — its tool ran here and its output threaded.
|
|
214
|
-
* `recorded` — its tool could not
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
214
|
+
* `recorded` — its tool could not be sent from here, so its recorded output
|
|
215
|
+
* stood in (only with `BIR_REPLAY_RECORDED_OUTPUTS=1`, and `bir
|
|
216
|
+
* replay`). A pass, with an asterisk: the values are from the
|
|
217
|
+
* source run.
|
|
218
|
+
* `skipped` — its call produced no result here: it could not be sent, or it
|
|
219
|
+
* was sent and never answered (its error says which). The run
|
|
220
|
+
* stops at it; nothing after it ran.
|
|
218
221
|
* `failed` — the *scenario's own logic* threw, or its tool ran and reported
|
|
219
222
|
* an error (stage `tool_call`). Either way the step's work did
|
|
220
223
|
* not happen, and a plan cannot claim success past it.
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
* The primitive here is therefore message-level: {@link send} takes any
|
|
13
13
|
* `JsonRpcMessage` verbatim and {@link onMessage} hands back any message the
|
|
14
14
|
* upstream originates, in either role. `request`/`notify` remain, implemented on
|
|
15
|
-
* top, for the few places that genuinely want a call (`bir doctor`, tests
|
|
16
|
-
* they allocate ids in the reserved `bir:` space so they
|
|
17
|
-
* host id passing through (Phase 1.3).
|
|
15
|
+
* top, for the few places that genuinely want a call (`bir doctor`, tests, the
|
|
16
|
+
* replay work loop) — and they allocate ids in the reserved `bir:` space so they
|
|
17
|
+
* can never collide with a host id passing through (Phase 1.3).
|
|
18
18
|
*/
|
|
19
19
|
import type { JsonRpcMessage } from "../jsonrpc/types.js";
|
|
20
20
|
/** Why the upstream connection ended. */
|
|
@@ -23,6 +23,25 @@ export interface UpstreamCloseInfo {
|
|
|
23
23
|
signal?: NodeJS.Signals | null;
|
|
24
24
|
error?: Error;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* An answer the upstream produced but the transport could not deliver — today,
|
|
28
|
+
* a response line over the framing cap. `id` is what could be recovered from
|
|
29
|
+
* the line's ends; the relay uses it to answer the host's request instead of
|
|
30
|
+
* leaving it waiting forever.
|
|
31
|
+
*/
|
|
32
|
+
export interface UndeliverableInfo {
|
|
33
|
+
id?: string | number;
|
|
34
|
+
/**
|
|
35
|
+
* The line was recognisably an answer (its first bytes say `result` or
|
|
36
|
+
* `error`), and no proxy-originated request was out when it arrived. Only
|
|
37
|
+
* then may an answer without a recoverable id be matched to the single host
|
|
38
|
+
* call in flight; anything less is a guess that can hand the wrong call an
|
|
39
|
+
* error and then its real answer as well.
|
|
40
|
+
*/
|
|
41
|
+
response?: boolean;
|
|
42
|
+
bytes: number;
|
|
43
|
+
reason: string;
|
|
44
|
+
}
|
|
26
45
|
export interface UpstreamClient {
|
|
27
46
|
/** Resolves when the transport is usable; rejects if the upstream is unusable. */
|
|
28
47
|
readonly ready: Promise<void>;
|
|
@@ -34,11 +53,28 @@ export interface UpstreamClient {
|
|
|
34
53
|
onMessage(handler: (msg: JsonRpcMessage) => void): void;
|
|
35
54
|
/** Called once when the upstream connection ends, for any reason. */
|
|
36
55
|
onClose(handler: (info: UpstreamCloseInfo) => void): void;
|
|
56
|
+
/**
|
|
57
|
+
* An upstream answer that could not be delivered (see {@link UndeliverableInfo}).
|
|
58
|
+
* Optional: a transport that can never drop an answer need not offer it.
|
|
59
|
+
*/
|
|
60
|
+
onUndeliverable?(handler: (info: UndeliverableInfo) => void): void;
|
|
61
|
+
/**
|
|
62
|
+
* The upstream died and is being restarted (`--restart`). Nothing that was in
|
|
63
|
+
* flight will ever be answered by the new process, so the relay answers it.
|
|
64
|
+
*/
|
|
65
|
+
onRestart?(handler: (info: UpstreamCloseInfo) => void): void;
|
|
37
66
|
/** Convenience call in the reserved `bir:` id space. Never used by the relay. */
|
|
38
67
|
request(method: string, params?: unknown, signal?: AbortSignal): Promise<unknown>;
|
|
39
68
|
/** Fire-and-forget notification toward the upstream. */
|
|
40
69
|
notify(method: string, params?: unknown): void;
|
|
41
70
|
close(): void;
|
|
71
|
+
/**
|
|
72
|
+
* Close politely and wait, bounded by `graceMs` plus a short kill window:
|
|
73
|
+
* the upstream is given the chance to exit on its own before it (and, on
|
|
74
|
+
* Windows, everything it started) is killed. Optional; `close()` is the
|
|
75
|
+
* fallback.
|
|
76
|
+
*/
|
|
77
|
+
shutdown?(graceMs?: number): Promise<void>;
|
|
42
78
|
}
|
|
43
79
|
/**
|
|
44
80
|
* The id prefix reserved for requests the proxy itself originates. Host ids pass
|
|
@@ -57,14 +93,20 @@ export declare abstract class BaseUpstreamClient implements UpstreamClient {
|
|
|
57
93
|
abstract readonly ready: Promise<void>;
|
|
58
94
|
private readonly messageHandlers;
|
|
59
95
|
private readonly closeHandlers;
|
|
96
|
+
private readonly undeliverableHandlers;
|
|
97
|
+
private readonly restartHandlers;
|
|
60
98
|
private readonly pending;
|
|
61
99
|
private nextId;
|
|
62
100
|
private isDead;
|
|
63
101
|
get dead(): boolean;
|
|
102
|
+
/** Proxy-originated (`bir:`) requests still waiting for their answer. */
|
|
103
|
+
protected get ownRequestsInFlight(): number;
|
|
64
104
|
abstract send(msg: JsonRpcMessage): void;
|
|
65
105
|
abstract close(): void;
|
|
66
106
|
onMessage(handler: (msg: JsonRpcMessage) => void): void;
|
|
67
107
|
onClose(handler: (info: UpstreamCloseInfo) => void): void;
|
|
108
|
+
onUndeliverable(handler: (info: UndeliverableInfo) => void): void;
|
|
109
|
+
onRestart(handler: (info: UpstreamCloseInfo) => void): void;
|
|
68
110
|
request(method: string, params?: unknown, signal?: AbortSignal): Promise<unknown>;
|
|
69
111
|
notify(method: string, params?: unknown): void;
|
|
70
112
|
/**
|
|
@@ -72,6 +114,17 @@ export declare abstract class BaseUpstreamClient implements UpstreamClient {
|
|
|
72
114
|
* consumed here and never reach the relay; everything else fans out verbatim.
|
|
73
115
|
*/
|
|
74
116
|
protected accept(msg: JsonRpcMessage): void;
|
|
117
|
+
/**
|
|
118
|
+
* Report an answer that could not be delivered. A `bir:` id is ours and is
|
|
119
|
+
* failed here; anything else goes to the relay, which owns host requests.
|
|
120
|
+
*/
|
|
121
|
+
protected undeliverable(info: UndeliverableInfo): void;
|
|
122
|
+
/**
|
|
123
|
+
* The process behind this client died and a new one is being started. Every
|
|
124
|
+
* in-flight `request()` fails (the new process never saw it), and the relay
|
|
125
|
+
* is told so it can answer the host's in-flight requests likewise.
|
|
126
|
+
*/
|
|
127
|
+
protected restarting(info: UpstreamCloseInfo): void;
|
|
75
128
|
/** Mark the connection over. Idempotent; fails every in-flight `request()`. */
|
|
76
129
|
protected closed(info: UpstreamCloseInfo): void;
|
|
77
130
|
}
|
package/dist/upstream/client.js
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
* The primitive here is therefore message-level: {@link send} takes any
|
|
13
13
|
* `JsonRpcMessage` verbatim and {@link onMessage} hands back any message the
|
|
14
14
|
* upstream originates, in either role. `request`/`notify` remain, implemented on
|
|
15
|
-
* top, for the few places that genuinely want a call (`bir doctor`, tests
|
|
16
|
-
* they allocate ids in the reserved `bir:` space so they
|
|
17
|
-
* host id passing through (Phase 1.3).
|
|
15
|
+
* top, for the few places that genuinely want a call (`bir doctor`, tests, the
|
|
16
|
+
* replay work loop) — and they allocate ids in the reserved `bir:` space so they
|
|
17
|
+
* can never collide with a host id passing through (Phase 1.3).
|
|
18
18
|
*/
|
|
19
19
|
/**
|
|
20
20
|
* The id prefix reserved for requests the proxy itself originates. Host ids pass
|
|
@@ -34,12 +34,18 @@ export function isBirId(id) {
|
|
|
34
34
|
export class BaseUpstreamClient {
|
|
35
35
|
messageHandlers = [];
|
|
36
36
|
closeHandlers = [];
|
|
37
|
+
undeliverableHandlers = [];
|
|
38
|
+
restartHandlers = [];
|
|
37
39
|
pending = new Map();
|
|
38
40
|
nextId = 1;
|
|
39
41
|
isDead = false;
|
|
40
42
|
get dead() {
|
|
41
43
|
return this.isDead;
|
|
42
44
|
}
|
|
45
|
+
/** Proxy-originated (`bir:`) requests still waiting for their answer. */
|
|
46
|
+
get ownRequestsInFlight() {
|
|
47
|
+
return this.pending.size;
|
|
48
|
+
}
|
|
43
49
|
onMessage(handler) {
|
|
44
50
|
this.messageHandlers.push(handler);
|
|
45
51
|
}
|
|
@@ -50,14 +56,33 @@ export class BaseUpstreamClient {
|
|
|
50
56
|
}
|
|
51
57
|
this.closeHandlers.push(handler);
|
|
52
58
|
}
|
|
59
|
+
onUndeliverable(handler) {
|
|
60
|
+
this.undeliverableHandlers.push(handler);
|
|
61
|
+
}
|
|
62
|
+
onRestart(handler) {
|
|
63
|
+
this.restartHandlers.push(handler);
|
|
64
|
+
}
|
|
53
65
|
request(method, params, signal) {
|
|
54
66
|
if (this.isDead)
|
|
55
67
|
return Promise.reject(new Error("upstream is not available"));
|
|
68
|
+
if (signal?.aborted)
|
|
69
|
+
return Promise.reject(new Error(`${method} aborted`));
|
|
56
70
|
const id = `${BIR_ID_PREFIX}${this.nextId++}`;
|
|
57
71
|
return new Promise((resolve, reject) => {
|
|
58
72
|
const onAbort = () => {
|
|
59
|
-
this.pending.delete(id)
|
|
73
|
+
if (!this.pending.delete(id))
|
|
74
|
+
return;
|
|
60
75
|
reject(new Error(`${method} aborted`));
|
|
76
|
+
// The caller has stopped waiting, but the upstream has not stopped
|
|
77
|
+
// working: a replay step that timed out would otherwise still run to
|
|
78
|
+
// completion — after the control server may already have substituted
|
|
79
|
+
// a recorded output for it, doubling a side effect nobody can see.
|
|
80
|
+
// A late answer to this id is dropped by `accept`, which finds no
|
|
81
|
+
// waiter left for it.
|
|
82
|
+
const reason = signal?.reason?.name === "TimeoutError"
|
|
83
|
+
? "timeout"
|
|
84
|
+
: "aborted";
|
|
85
|
+
this.notify("notifications/cancelled", { requestId: id, reason });
|
|
61
86
|
};
|
|
62
87
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
63
88
|
this.pending.set(id, {
|
|
@@ -95,6 +120,38 @@ export class BaseUpstreamClient {
|
|
|
95
120
|
for (const handler of this.messageHandlers)
|
|
96
121
|
handler(msg);
|
|
97
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Report an answer that could not be delivered. A `bir:` id is ours and is
|
|
125
|
+
* failed here; anything else goes to the relay, which owns host requests.
|
|
126
|
+
*/
|
|
127
|
+
undeliverable(info) {
|
|
128
|
+
if (isBirId(info.id)) {
|
|
129
|
+
const waiter = this.pending.get(info.id);
|
|
130
|
+
if (waiter) {
|
|
131
|
+
this.pending.delete(info.id);
|
|
132
|
+
waiter.cleanup();
|
|
133
|
+
waiter.reject(new Error(info.reason));
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
for (const handler of this.undeliverableHandlers)
|
|
138
|
+
handler(info);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The process behind this client died and a new one is being started. Every
|
|
142
|
+
* in-flight `request()` fails (the new process never saw it), and the relay
|
|
143
|
+
* is told so it can answer the host's in-flight requests likewise.
|
|
144
|
+
*/
|
|
145
|
+
restarting(info) {
|
|
146
|
+
const err = info.error ?? new Error("upstream restarted");
|
|
147
|
+
for (const [, waiter] of this.pending) {
|
|
148
|
+
waiter.cleanup();
|
|
149
|
+
waiter.reject(err);
|
|
150
|
+
}
|
|
151
|
+
this.pending.clear();
|
|
152
|
+
for (const handler of this.restartHandlers)
|
|
153
|
+
handler(info);
|
|
154
|
+
}
|
|
98
155
|
/** Mark the connection over. Idempotent; fails every in-flight `request()`. */
|
|
99
156
|
closed(info) {
|
|
100
157
|
if (this.isDead)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* headers — the credentials a remote upstream is reached with.
|
|
3
|
+
*
|
|
4
|
+
* WHERE THEY COME FROM. A wrapped remote server is a stdio entry that runs
|
|
5
|
+
* `bir-proxy --url …`, so its headers reach the proxy at spawn time. Two ways,
|
|
6
|
+
* both kept:
|
|
7
|
+
*
|
|
8
|
+
* - `BIR_UPSTREAM_HEADERS`, a JSON object of header name → string value, in
|
|
9
|
+
* the entry's `env` — where an install puts them now, because an entry's
|
|
10
|
+
* `args` show up in process listings and its `env` does not;
|
|
11
|
+
* - `--header 'Name: value'` arguments, repeatable — what older entries and
|
|
12
|
+
* hand-written `bir wrap` lines carry. On a clash the argument wins.
|
|
13
|
+
*
|
|
14
|
+
* THE VALUES ARE SECRETS. Nothing here logs one, or the raw variable: a
|
|
15
|
+
* problem is described by its shape ("not JSON", "2 entries skipped") and, at
|
|
16
|
+
* most, a count. The variable is removed from the proxy's environment as soon
|
|
17
|
+
* as it is read (bir-proxy.ts), so no process the proxy starts inherits it.
|
|
18
|
+
*/
|
|
19
|
+
/** The environment variable a wrapped remote entry carries its headers in. */
|
|
20
|
+
export declare const UPSTREAM_HEADERS_ENV = "BIR_UPSTREAM_HEADERS";
|
|
21
|
+
export interface ParsedHeaders {
|
|
22
|
+
headers: Record<string, string>;
|
|
23
|
+
/** Why some or all of the input was not used. Never contains a value. */
|
|
24
|
+
problem?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Keep only the headers Node can send. A bad name or value would otherwise
|
|
28
|
+
* make *every* request to the upstream fail at the socket, as "unreachable".
|
|
29
|
+
*/
|
|
30
|
+
export declare function sendableHeaders(input: Record<string, string>): ParsedHeaders;
|
|
31
|
+
/**
|
|
32
|
+
* Parse `BIR_UPSTREAM_HEADERS`. Absent or empty → no headers and no problem.
|
|
33
|
+
* Not JSON, or not an object of strings → whatever is usable, and a problem
|
|
34
|
+
* that says what was wrong without repeating any of it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseUpstreamHeaders(raw: string | undefined): ParsedHeaders;
|
|
37
|
+
/**
|
|
38
|
+
* `base` overlaid with `override`, header names compared case-insensitively
|
|
39
|
+
* (HTTP's rule): `authorization` in one and `Authorization` in the other is
|
|
40
|
+
* one header, and `override`'s spelling and value are the ones kept.
|
|
41
|
+
*/
|
|
42
|
+
export declare function mergeHeaders(base: Record<string, string>, override: Record<string, string>): Record<string, string>;
|
|
43
|
+
//# sourceMappingURL=headers.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* headers — the credentials a remote upstream is reached with.
|
|
3
|
+
*
|
|
4
|
+
* WHERE THEY COME FROM. A wrapped remote server is a stdio entry that runs
|
|
5
|
+
* `bir-proxy --url …`, so its headers reach the proxy at spawn time. Two ways,
|
|
6
|
+
* both kept:
|
|
7
|
+
*
|
|
8
|
+
* - `BIR_UPSTREAM_HEADERS`, a JSON object of header name → string value, in
|
|
9
|
+
* the entry's `env` — where an install puts them now, because an entry's
|
|
10
|
+
* `args` show up in process listings and its `env` does not;
|
|
11
|
+
* - `--header 'Name: value'` arguments, repeatable — what older entries and
|
|
12
|
+
* hand-written `bir wrap` lines carry. On a clash the argument wins.
|
|
13
|
+
*
|
|
14
|
+
* THE VALUES ARE SECRETS. Nothing here logs one, or the raw variable: a
|
|
15
|
+
* problem is described by its shape ("not JSON", "2 entries skipped") and, at
|
|
16
|
+
* most, a count. The variable is removed from the proxy's environment as soon
|
|
17
|
+
* as it is read (bir-proxy.ts), so no process the proxy starts inherits it.
|
|
18
|
+
*/
|
|
19
|
+
/** The environment variable a wrapped remote entry carries its headers in. */
|
|
20
|
+
export const UPSTREAM_HEADERS_ENV = "BIR_UPSTREAM_HEADERS";
|
|
21
|
+
/** An HTTP header name: RFC 9110's `token`. Node refuses anything else at send time. */
|
|
22
|
+
const HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
23
|
+
/** A header value Node will send: no CR, LF, NUL or other control bytes, nothing past Latin-1. */
|
|
24
|
+
const HEADER_VALUE = /^[\t\x20-\x7e\x80-\xff]*$/;
|
|
25
|
+
/**
|
|
26
|
+
* Keep only the headers Node can send. A bad name or value would otherwise
|
|
27
|
+
* make *every* request to the upstream fail at the socket, as "unreachable".
|
|
28
|
+
*/
|
|
29
|
+
export function sendableHeaders(input) {
|
|
30
|
+
const headers = {};
|
|
31
|
+
let skipped = 0;
|
|
32
|
+
for (const [name, value] of Object.entries(input)) {
|
|
33
|
+
if (!HEADER_NAME.test(name) || typeof value !== "string" || !HEADER_VALUE.test(value)) {
|
|
34
|
+
skipped += 1;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
headers[name] = value;
|
|
38
|
+
}
|
|
39
|
+
return skipped ? { headers, problem: `${skipped} header(s) skipped: not a valid HTTP header name or value` } : { headers };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Parse `BIR_UPSTREAM_HEADERS`. Absent or empty → no headers and no problem.
|
|
43
|
+
* Not JSON, or not an object of strings → whatever is usable, and a problem
|
|
44
|
+
* that says what was wrong without repeating any of it.
|
|
45
|
+
*/
|
|
46
|
+
export function parseUpstreamHeaders(raw) {
|
|
47
|
+
if (raw === undefined || raw.trim() === "")
|
|
48
|
+
return { headers: {} };
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(raw);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { headers: {}, problem: "not valid JSON; ignored" };
|
|
55
|
+
}
|
|
56
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
57
|
+
return { headers: {}, problem: "not a JSON object of header name to value; ignored" };
|
|
58
|
+
}
|
|
59
|
+
const strings = {};
|
|
60
|
+
let nonString = 0;
|
|
61
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
62
|
+
if (typeof value === "string")
|
|
63
|
+
strings[name] = value;
|
|
64
|
+
else
|
|
65
|
+
nonString += 1;
|
|
66
|
+
}
|
|
67
|
+
const sendable = sendableHeaders(strings);
|
|
68
|
+
const problems = [
|
|
69
|
+
nonString ? `${nonString} header(s) skipped: the value is not a string` : undefined,
|
|
70
|
+
sendable.problem,
|
|
71
|
+
].filter((p) => p !== undefined);
|
|
72
|
+
return problems.length ? { headers: sendable.headers, problem: problems.join("; ") } : { headers: sendable.headers };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* `base` overlaid with `override`, header names compared case-insensitively
|
|
76
|
+
* (HTTP's rule): `authorization` in one and `Authorization` in the other is
|
|
77
|
+
* one header, and `override`'s spelling and value are the ones kept.
|
|
78
|
+
*/
|
|
79
|
+
export function mergeHeaders(base, override) {
|
|
80
|
+
const out = { ...base };
|
|
81
|
+
for (const [name, value] of Object.entries(override)) {
|
|
82
|
+
const lower = name.toLowerCase();
|
|
83
|
+
for (const key of Object.keys(out))
|
|
84
|
+
if (key !== name && key.toLowerCase() === lower)
|
|
85
|
+
delete out[key];
|
|
86
|
+
out[name] = value;
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=headers.js.map
|