1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
|
/**
* Which browser this harness should drive.
*
* Upstream only ever drives a browser on localhost. This fork adds a remote
* target so pi can drive a shared Chrome on another host (see FORK.md).
*
* Resolution is pure — no I/O — so the status command can report the configured
* intent without touching the network, and the daemon can resolve the same way
* in its own process.
*/
/** Default remote CDP endpoint: castle's Chrome, exposed on the wire network. */
export const DEFAULT_REMOTE_CDP = "10.88.0.25:9223";
/** Values of BU_CDP_HTTP that mean "ignore the default, use this machine". */
const LOCAL_ALIASES = new Set(["local", "localhost", "off", "none", "0", "no"]);
/**
* Where a target came from. "ws" is upstream's BU_CDP_WS, "env" is BU_CDP_HTTP,
* "stored" is /browser-target, "default" is this fork's built-in. Only
* "default" is allowed to fall back.
*/
export type CdpTargetSource = "ws" | "env" | "stored" | "default";
export type CdpTarget =
| {
readonly kind: "remote";
readonly host: string;
readonly port: number;
readonly source: CdpTargetSource;
}
| { readonly kind: "local"; readonly source: Exclude<CdpTargetSource, "default" | "ws"> }
| { readonly kind: "invalid"; readonly raw: string; readonly reason: string; readonly source: CdpTargetSource };
/**
* Precedence: BU_CDP_WS, then BU_CDP_HTTP, then the /browser-target choice,
* then the built-in default.
*
* BU_CDP_WS -> upstream's pinned WebSocket URL; discoverEndpoint() returns
* it before any of the rest is consulted, so it is reported
* here only so status, /browser-target and the prompt agree
* with what the daemon actually dials
*
* "host:port" -> that remote, and a failure to reach it is fatal
* local|off|… -> upstream behaviour, this machine's own browser
* neither set -> DEFAULT_REMOTE_CDP, falling back to local when unreachable
*
* The environment wins over the stored choice so that a one-off
* `BU_CDP_HTTP=… pi` is never silently overridden by a setting made days ago.
*
* The fallback asymmetry is deliberate. An explicit endpoint — from either
* source — is an instruction, so silently driving a different browser instead
* would be wrong: it would automate the operator's own signed-in Chrome rather
* than the shared one. The built-in default is only a preference, so a laptop
* away from the wire network still gets the local browser the extension was
* originally written for.
*
* `stored` is passed in rather than read here so this stays pure and
* synchronous: /browser-status can describe the target without I/O, and the
* daemon can resolve identically in its own process.
*/
export const resolveCdpTarget = (
env: NodeJS.ProcessEnv = process.env,
stored: string | null = null,
): CdpTarget => {
// FORK: mirror upstream's endpointFromEnv() in discovery.ts, which wins.
const pinnedWs = env["BU_CDP_WS"]?.trim();
if (pinnedWs) {
const parsed = parseWsUrl(pinnedWs);
if (!parsed) {
return { kind: "invalid", raw: redactWsUrl(pinnedWs), source: "ws", reason: "expected a ws:// or wss:// URL" };
}
return { kind: "remote", host: parsed.host, port: parsed.port, source: "ws" };
}
const fromEnv = env["BU_CDP_HTTP"]?.trim();
const fromStore = stored?.trim();
const raw = fromEnv !== undefined && fromEnv !== "" ? fromEnv : fromStore;
const source: CdpTargetSource = fromEnv !== undefined && fromEnv !== "" ? "env" : "stored";
if (raw === undefined || raw === "") {
const parsed = parseHostPort(DEFAULT_REMOTE_CDP);
// Unreachable in practice: the constant above is validated by its own test.
if (!parsed) return { kind: "local", source: "env" };
return { kind: "remote", host: parsed.host, port: parsed.port, source: "default" };
}
if (LOCAL_ALIASES.has(raw.toLowerCase())) return { kind: "local", source };
const parsed = parseHostPort(raw);
if (!parsed) {
return {
kind: "invalid",
raw,
source,
reason: `expected "host:port" or one of ${[...LOCAL_ALIASES].join(", ")}`,
};
}
return { kind: "remote", host: parsed.host, port: parsed.port, source };
};
/** The vocabulary /browser-target accepts, for help text and validation. */
export const localAliases = (): ReadonlyArray<string> => [...LOCAL_ALIASES];
/** True when `value` is something resolveCdpTarget can make sense of. */
export const isValidTargetValue = (value: string): boolean => {
const v = value.trim();
if (v === "") return false;
return LOCAL_ALIASES.has(v.toLowerCase()) || parseHostPort(v) !== null;
};
const parseHostPort = (value: string): { host: string; port: number } | null => {
const sep = value.lastIndexOf(":");
if (sep === -1) return null;
const host = value.slice(0, sep).trim();
const port = Number(value.slice(sep + 1).trim());
if (!host) return null;
if (!Number.isInteger(port) || port <= 0 || port >= 65536) return null;
return { host, port };
};
/**
* host:port of a BU_CDP_WS URL. Only that much is ever shown: the path carries
* the browser's id, and a proxied endpoint may carry a token or credentials.
*/
const parseWsUrl = (value: string): { host: string; port: number } | null => {
let url: URL;
try {
url = new URL(value);
} catch {
return null;
}
if (url.protocol !== "ws:" && url.protocol !== "wss:") return null;
const host = url.hostname.replace(/^\[(.*)\]$/, "$1");
if (!host) return null;
const port = url.port === "" ? (url.protocol === "wss:" ? 443 : 80) : Number(url.port);
return { host, port };
};
/** A BU_CDP_WS value safe to print: host:port when parseable, otherwise a placeholder. */
export const redactWsUrl = (value: string): string => {
const parsed = parseWsUrl(value.trim());
return parsed ? `${parsed.host}:${parsed.port}` : "<unparseable URL>";
};
const sourceLabel = (source: CdpTargetSource): string =>
source === "ws"
? "BU_CDP_WS, pinned WebSocket"
: source === "env"
? "BU_CDP_HTTP"
: source === "stored"
? "/browser-target"
: "built-in default";
/** One-line description for /browser-status and error messages. */
export const describeCdpTarget = (target: CdpTarget): string => {
switch (target.kind) {
case "remote":
return `${target.host}:${target.port} (${sourceLabel(target.source)})`;
case "local":
return `this machine's own browser (${sourceLabel(target.source)})`;
case "invalid":
return `invalid target "${target.raw}" from ${sourceLabel(target.source)} — ${target.reason}`;
}
};
|