Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106/** * The runner/extension wire protocol, version 1 (see docs/design/protocol.md). * Browser-safe: the extension bundles this file, so it must not import anything from Node. */export const PROTOCOL_VERSION = 1;export const PING_INTERVAL_MS = 15_000;export const MAX_MESSAGE_BYTES = 8 * 1024 * 1024;export const BACKOFF_MS = [500, 1000, 2000, 4000, 8000] as const;/** chrome.alarms backup wake-up, in minutes (30 s is the smallest period Chrome allows). */export const ALARM_PERIOD_MINUTES = 0.5;
/** Websocket close codes (private range). The extension tells "wrong token" from "bridge is down" by these. */export const CLOSE = { badToken: 4401, helloTimeout: 4408, replaced: 4409, protocolMismatch: 4426, silent: 4410,} as const;
export const FAILURE_REASONS = [ "not-found", "ambiguous", "not-visible", "not-enabled", "timeout", "mismatch", "drifted", "navigation", "load-failed", "tab-closed", "connection-lost", "unsupported", "internal",] as const;export type FailureReason = (typeof FAILURE_REASONS)[number];
export const COMMANDS = ["resolve", "act", "verify", "locate", "read", "wait", "outline", "tabs", "navigate", "switchTab", "closeTab"] as const;const IDEMPOTENT: ReadonlySet<string> = new Set(["resolve", "verify", "locate", "read", "wait", "outline", "tabs"]);
/** Only these may be sent again after a reconnect. Anything else (an action, or a command nobody knows) is never repeated. */export function isIdempotent(cmd: string): boolean { return IDEMPOTENT.has(cmd);}
export function backoffDelay(attempt: number): number { const i = Math.max(0, Math.floor(attempt)); return BACKOFF_MS[Math.min(i, BACKOFF_MS.length - 1)] as number;}
export interface Hello { type: "hello"; extVersion: string; chrome: string; profile: string; resume?: string }export interface Welcome { type: "welcome"; runId: string; protocol: number }export interface Ping { type: "ping" }export interface Pong { type: "pong" }export interface EventMessage { type: "event"; kind: string; [key: string]: unknown }export interface Request { id: number; cmd: string; [key: string]: unknown }export interface Response { id: number; ok: boolean; reason?: string; detail?: unknown; [key: string]: unknown }export type Message = Hello | Welcome | Ping | Pong | EventMessage | Request | Response;
export type ParseResult = | { ok: true; message: Message } | { ok: false; reason: "too-large" | "malformed" | "not-an-object" | "unknown-shape" | "invalid-hello" | "invalid-welcome" | "invalid-event" };
const isObject = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v);const isString = (v: unknown): v is string => typeof v === "string";
function textOf(raw: string | ArrayBuffer | ArrayBufferView): string | "too-large" { if (typeof raw === "string") { if (raw.length > MAX_MESSAGE_BYTES) return "too-large"; // Most text is short enough that its byte length cannot exceed the limit; only measure when it might. if (raw.length * 3 > MAX_MESSAGE_BYTES && new TextEncoder().encode(raw).length > MAX_MESSAGE_BYTES) return "too-large"; return raw; } const bytes = raw instanceof ArrayBuffer ? new Uint8Array(raw) : new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength); if (bytes.length > MAX_MESSAGE_BYTES) return "too-large"; return new TextDecoder().decode(bytes);}
/** Reads one websocket message. Never throws: anything unusable comes back as { ok: false, reason }. */export function parseMessage(raw: string | ArrayBuffer | ArrayBufferView): ParseResult { const text = textOf(raw); if (text === "too-large") return { ok: false, reason: "too-large" }; let value: unknown; try { value = JSON.parse(text); } catch { return { ok: false, reason: "malformed" }; } if (!isObject(value)) return { ok: false, reason: "not-an-object" };
if (isString(value.type)) { switch (value.type) { case "hello": { const { extVersion, chrome, profile, resume } = value; if (!isString(extVersion) || !isString(chrome) || !isString(profile) || (resume !== undefined && !isString(resume))) return { ok: false, reason: "invalid-hello" }; return { ok: true, message: { type: "hello", extVersion, chrome, profile, ...(resume !== undefined ? { resume } : {}) } }; } case "welcome": if (!isString(value.runId) || typeof value.protocol !== "number") return { ok: false, reason: "invalid-welcome" }; return { ok: true, message: { type: "welcome", runId: value.runId, protocol: value.protocol } }; case "ping": return { ok: true, message: { type: "ping" } }; case "pong": return { ok: true, message: { type: "pong" } }; case "event": return isString(value.kind) ? { ok: true, message: value as EventMessage } : { ok: false, reason: "invalid-event" }; default: return { ok: false, reason: "unknown-shape" }; } } if (Number.isInteger(value.id)) { if (isString(value.cmd)) return { ok: true, message: value as Request }; if (typeof value.ok === "boolean") return { ok: true, message: value as Response }; } return { ok: false, reason: "unknown-shape" };}