summaryrefslogtreecommitdiff
path: root/src/endpoint.ts
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-08-03 21:18:02 +0100
committerIgor Soarez <igor@soarez.org>2026-08-03 21:18:02 +0100
commit495de0d5283dd3e4a6ef715b596c4a2892e95915 (patch)
treeaae2ec78d59cb96d8e90c56ab7626a6886d30be2 /src/endpoint.ts
Web search for pi through an existing Chrome over CDP
Attaches to a browser that is already running — never launches one — using the same endpoint configuration as pi-browser-harness, so a single /browser-target choice governs both packages. One tool, castle_cdp_search, deliberately not named web_search so it coexists with pi-web-access rather than shadowing it. Notes from validating against castle's Chrome: - tbs=qdr:*, the parameter Google's own Tools menu writes, renders an empty page on this profile; the older as_qdr=* works. Any date filter combined with udm=14 is also empty, so recency drops udm. - Target.createTarget must not be raced against the abort signal: raceAbort abandons the promise but cannot cancel the command, and the command's side effect is a tab nothing is left holding. - A search cancelled while queued has to be removed from the semaphore queue, or the slot handed to it later is never counted back. - A decaying rate-limit block stops serving /sorry/ and returns an empty results page instead, indistinguishable from a genuine zero-hit search.
Diffstat (limited to 'src/endpoint.ts')
-rw-r--r--src/endpoint.ts108
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}`;