diff options
Diffstat (limited to 'src/endpoint.ts')
| -rw-r--r-- | src/endpoint.ts | 108 |
1 files changed, 108 insertions, 0 deletions
diff --git a/src/endpoint.ts b/src/endpoint.ts new file mode 100644 index 0000000..d3f4b0d --- /dev/null +++ b/src/endpoint.ts @@ -0,0 +1,108 @@ +/** + * Which browser this extension searches through. + * + * Deliberately the *same* configuration as pi-browser-harness, so one decision + * governs both packages and they never drive different browsers in the same + * session: `BU_CDP_HTTP`, then the `/browser-target` file that harness writes, + * then the built-in castle default. + * + * Resolution is pure — no I/O — so it can be unit-tested and reported without + * touching the network. + */ + +/** Default CDP endpoint: castle's Chrome, exposed on the wg8 overlay. */ +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"]); + +/** Port a local Chrome is expected to serve CDP on when the target is "local". */ +export const LOCAL_CDP_PORT = 9222; + +/** + * Where a target came from. "env" is BU_CDP_HTTP, "stored" is the harness's + * `/browser-target`, "default" is the built-in. + */ +export type CdpTargetSource = "env" | "stored" | "default"; + +export type CdpTarget = { + readonly host: string; + readonly port: number; + readonly source: CdpTargetSource; +}; + +export type CdpTargetResolution = + | { readonly kind: "ok"; readonly target: CdpTarget } + | { readonly kind: "invalid"; readonly raw: string; readonly reason: string; readonly source: CdpTargetSource }; + +/** + * Precedence: BU_CDP_HTTP, then the stored `/browser-target` choice, then the + * built-in default. The environment wins so a one-off `BU_CDP_HTTP=… pi` is + * never silently overridden by a choice made days ago. + * + * Unlike pi-browser-harness this never falls back from the default to a local + * browser on its own. The harness can fall back because it drives a browser the + * operator is watching; here a silent fallback would mean *searching from the + * operator's own signed-in Chrome* without saying so. When the configured + * endpoint is unreachable the caller reports that, and the operator can say + * `local` explicitly. + * + * `stored` is passed in rather than read here so this stays pure and synchronous. + */ +export const resolveCdpTarget = ( + env: NodeJS.ProcessEnv = process.env, + stored: string | null = null, +): CdpTargetResolution => { + const fromEnv = env["BU_CDP_HTTP"]?.trim(); + const fromStore = stored?.trim(); + + const hasEnv = fromEnv !== undefined && fromEnv !== ""; + const raw = hasEnv ? fromEnv : fromStore; + const source: CdpTargetSource = hasEnv ? "env" : "stored"; + + if (raw === undefined || raw === "") { + const parsed = parseHostPort(DEFAULT_REMOTE_CDP); + // Unreachable in practice: the constant above is covered by its own test. + if (!parsed) { + return { kind: "invalid", raw: DEFAULT_REMOTE_CDP, source: "default", reason: "built-in default is malformed" }; + } + return { kind: "ok", target: { ...parsed, source: "default" } }; + } + + if (LOCAL_ALIASES.has(raw.toLowerCase())) { + return { kind: "ok", target: { host: "127.0.0.1", port: LOCAL_CDP_PORT, source } }; + } + + const parsed = parseHostPort(raw); + if (!parsed) { + return { + kind: "invalid", + raw, + source, + reason: `expected "host:port" or one of ${[...LOCAL_ALIASES].join(", ")}`, + }; + } + return { kind: "ok", target: { ...parsed, source } }; +}; + +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 }; +}; + +const sourceLabel = (source: CdpTargetSource): string => + source === "env" ? "BU_CDP_HTTP" : source === "stored" ? "/browser-target" : "built-in default"; + +/** One-line description for progress messages and errors. */ +export const describeCdpTarget = (target: CdpTarget): string => + `${target.host}:${target.port} (${sourceLabel(target.source)})`; + +/** One-line description of a resolution failure. */ +export const describeResolutionFailure = ( + failure: Extract<CdpTargetResolution, { kind: "invalid" }>, +): string => `invalid CDP target "${failure.raw}" from ${sourceLabel(failure.source)} — ${failure.reason}`; |
