repoburg 1.3.255 → 1.3.257
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/daemon/dist/index.js +61 -3
- package/daemon/dist/staticServing.js +2 -2
- package/daemon/dist/tabtape/api/follow.js +174 -0
- package/daemon/dist/tabtape/api/http.js +221 -0
- package/daemon/dist/tabtape/api/record.js +149 -0
- package/daemon/dist/tabtape/api/router.js +519 -0
- package/daemon/dist/tabtape/api/router.spec.js +604 -0
- package/daemon/dist/tabtape/api/static.js +70 -0
- package/daemon/dist/tabtape/api/ws.js +166 -0
- package/daemon/dist/tabtape/buffer/buffer.js +347 -0
- package/daemon/dist/tabtape/buffer/buffer.spec.js +137 -0
- package/daemon/dist/tabtape/config.js +108 -0
- package/daemon/dist/tabtape/e2e-server.js +96 -0
- package/daemon/dist/tabtape/errors.js +77 -0
- package/daemon/dist/tabtape/export/export.js +331 -0
- package/daemon/dist/tabtape/export/export.spec.js +262 -0
- package/daemon/dist/tabtape/har/gostd.js +395 -0
- package/daemon/dist/tabtape/har/gostd.spec.js +139 -0
- package/daemon/dist/tabtape/har/har.js +582 -0
- package/daemon/dist/tabtape/har/har.spec.js +325 -0
- package/daemon/dist/tabtape/index.js +134 -0
- package/daemon/dist/tabtape/index.spec.js +104 -0
- package/daemon/dist/tabtape/lock.js +67 -0
- package/daemon/dist/tabtape/log.js +32 -0
- package/daemon/dist/tabtape/mcp/definitions.js +362 -0
- package/daemon/dist/tabtape/mcp/gofmt.js +227 -0
- package/daemon/dist/tabtape/mcp/params.js +345 -0
- package/daemon/dist/tabtape/mcp/params.spec.js +172 -0
- package/daemon/dist/tabtape/mcp/recordLink.js +122 -0
- package/daemon/dist/tabtape/mcp/server.js +411 -0
- package/daemon/dist/tabtape/mcp/server.spec.js +722 -0
- package/daemon/dist/tabtape/mcp/tools.js +1085 -0
- package/daemon/dist/tabtape/query/format.js +195 -0
- package/daemon/dist/tabtape/query/network.js +133 -0
- package/daemon/dist/tabtape/query/perf.js +93 -0
- package/daemon/dist/tabtape/query/query.spec.js +233 -0
- package/daemon/dist/tabtape/query/reader.js +602 -0
- package/daemon/dist/tabtape/query/types.js +274 -0
- package/daemon/dist/tabtape/redact.js +37 -0
- package/daemon/dist/tabtape/repro/lang.js +95 -0
- package/daemon/dist/tabtape/repro/repro.js +422 -0
- package/daemon/dist/tabtape/repro/repro.spec.js +210 -0
- package/daemon/dist/tabtape/sink.js +24 -0
- package/daemon/dist/tabtape/store/record.js +99 -0
- package/daemon/dist/tabtape/store/segment.js +267 -0
- package/daemon/dist/tabtape/store/store.js +846 -0
- package/daemon/dist/tabtape/store/store.spec.js +485 -0
- package/daemon/dist/tabtape/testing/querytest.js +119 -0
- package/daemon/dist/tabtape/testing/tmp.js +33 -0
- package/package.json +3 -2
- package/tabtape/extension/dist/background.js +8 -0
- package/tabtape/extension/dist/background.js.map +7 -0
- package/tabtape/extension/dist/bridge.js +3 -0
- package/tabtape/extension/dist/bridge.js.map +7 -0
- package/tabtape/extension/dist/icons/128.png +0 -0
- package/tabtape/extension/dist/icons/16.png +0 -0
- package/tabtape/extension/dist/icons/32.png +0 -0
- package/tabtape/extension/dist/icons/48.png +0 -0
- package/tabtape/extension/dist/manifest.json +28 -0
- package/tabtape/extension/dist/popup.css +62 -0
- package/tabtape/extension/dist/popup.html +34 -0
- package/tabtape/extension/dist/popup.js +4 -0
- package/tabtape/extension/dist/popup.js.map +7 -0
- package/tabtape/extension/dist/recorder.js +133 -0
- package/tabtape/extension/dist/recorder.js.map +1 -0
- package/tabtape/extension/dist/state.js +2 -0
- package/tabtape/extension/dist/state.js.map +1 -0
- package/tabtape/playwright/mcp.mjs +58 -0
- package/tabtape/playwright/tabtape.d.mts +37 -0
- package/tabtape/playwright/tabtape.mjs +146 -0
- package/tabtape/ui/dist/app/assets/index-Canv3Ljk.css +1 -0
- package/tabtape/ui/dist/app/assets/index-iWNSSrAE.js +145 -0
- package/tabtape/ui/dist/app/index.html +14 -0
- package/tabtape/ui/dist/export/viewer.css +2 -0
- package/tabtape/ui/dist/export/viewer.js +143 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/recorder/util.ts", "../src/cdp/limit.ts", "../src/cdp/timing.ts", "../src/cdp/network.ts", "../src/cdp/debugger.ts", "../src/match.ts", "../src/links.ts", "../src/protocol.ts", "../src/background.ts"],
|
|
4
|
+
"sourcesContent": ["// Helpers for code running in the page's MAIN world. The recorder is injected\n// at document_start, before page scripts run, so the references captured here\n// are the browser's own even if the page later patches the globals.\n\nexport const stringify = JSON.stringify;\nexport const parse = JSON.parse;\nconst toStringTag = Object.prototype.toString;\n\nexport const MASK = '********';\nconst PASSWORD_KEY = /passw(or)?d|^pwd$|^pass$/i;\n\n/** Bodies with these content types are recorded as text (in the page and in deep mode). */\nexport const TEXT_MIME = /json|text\\/|xml|javascript|ecmascript|graphql|x-www-form-urlencoded|csv|yaml/i;\n\nexport const clip = (s: string, n: number): string => (s.length > n ? s.slice(0, n - 1) + '\u2026' : s);\nexport const round = (n: number): number => Math.round(n * 10) / 10;\n\nexport function isPasswordKey(key: string): boolean {\n return PASSWORD_KEY.test(key);\n}\n\nexport function absoluteUrl(url: string): string {\n try {\n return new URL(url, location.href).href;\n } catch {\n return url;\n }\n}\n\n/**\n * The URL a resource element (img, script, link, media) failed to load,\n * redacted and at most 1000 characters (data: URLs): currentSrc names the\n * image picked from srcset or <picture>, the src and href properties are\n * resolved against <base>. Elements without a string property (SVG <image>)\n * fall back to the attribute. Empty when there is none.\n */\nexport function resourceUrl(el: Pick<Element, 'getAttribute'>): string {\n const props = el as unknown as Record<string, unknown>;\n let url = '';\n for (const key of ['currentSrc', 'src', 'href']) {\n const v = props[key];\n if (typeof v === 'string' && v) {\n url = v;\n break;\n }\n }\n if (!url) {\n const attr = el.getAttribute('src') || el.getAttribute('href');\n if (attr) url = absoluteUrl(attr);\n }\n return url ? clip(redactUrl(url), 1000) : '';\n}\n\n/** Masks password-like query parameters. */\nexport function redactParams(query: string): string {\n const params = new URLSearchParams(query);\n let changed = false;\n for (const key of new Set(params.keys())) {\n if (isPasswordKey(key)) {\n params.set(key, MASK);\n changed = true;\n }\n }\n return changed ? params.toString() : query;\n}\n\nexport function redactUrl(url: string): string {\n const q = url.indexOf('?');\n if (q < 0) return url;\n const hash = url.indexOf('#', q);\n const query = url.slice(q + 1, hash < 0 ? undefined : hash);\n const redacted = redactParams(query);\n return redacted === query ? url : url.slice(0, q + 1) + redacted + (hash < 0 ? '' : url.slice(hash));\n}\n\nfunction redactValue(v: unknown): boolean {\n let changed = false;\n if (Array.isArray(v)) {\n for (const item of v) changed = redactValue(item) || changed;\n } else if (v && typeof v === 'object') {\n const obj = v as Record<string, unknown>;\n for (const key of Object.keys(obj)) {\n const val = obj[key];\n if (isPasswordKey(key) && (typeof val === 'string' || typeof val === 'number')) {\n obj[key] = MASK;\n changed = true;\n } else {\n changed = redactValue(val) || changed;\n }\n }\n }\n return changed;\n}\n\nconst QUERY_LIKE = /^[\\w.%[\\]-]+=[^&]*(&[\\w.%[\\]-]+=[^&]*)*$/;\nconst MULTIPART = /multipart\\/form-data/i;\n/** A multipart body without its content type: \"--boundary\" and a form-data part. */\nconst MULTIPART_START = /^--[^\\r\\n]{1,200}\\r?\\ncontent-disposition:\\s*form-data/i;\n\n/** Masks password fields in JSON, form-encoded and multipart/form-data request bodies. */\nexport function redactBody(text: string, contentType = ''): string {\n const t = text.trimStart();\n if (t.startsWith('{') || t.startsWith('[')) {\n try {\n const value = parse(text);\n return redactValue(value) ? stringify(value) : text;\n } catch {\n return text;\n }\n }\n if (MULTIPART.test(contentType) || MULTIPART_START.test(t)) return redactMultipart(text, contentType);\n if (/x-www-form-urlencoded/i.test(contentType) || (t.length < 100_000 && QUERY_LIKE.test(t))) {\n return redactParams(text);\n }\n return text;\n}\n\n/**\n * Masks the values of password fields in a multipart/form-data body. The\n * boundary comes from the content type or the body's first line; file parts\n * are left alone.\n */\nexport function redactMultipart(text: string, contentType = ''): string {\n const m = /boundary=(?:\"([^\"]+)\"|([^;,\\s]+))/i.exec(contentType);\n const boundary = m?.[1] ?? m?.[2] ?? /^--([^\\r\\n]+)/.exec(text.trimStart())?.[1];\n if (!boundary) return text;\n const delimiter = `--${boundary}`;\n const parts = text.split(delimiter);\n let changed = false;\n for (let i = 1; i < parts.length; i++) {\n const part = parts[i];\n const sep = /\\r?\\n\\r?\\n/.exec(part);\n if (!sep) continue;\n const head = part.slice(0, sep.index);\n if (/;\\s*filename\\*?=/i.test(head)) continue;\n const name = /;\\s*name=\"([^\"]*)\"/i.exec(head)?.[1] ?? /;\\s*name=([^;\\s]+)/i.exec(head)?.[1];\n if (name === undefined || !isPasswordKey(name)) continue;\n const start = sep.index + sep[0].length;\n // The line break before the next delimiter belongs to the delimiter.\n const end = part.endsWith('\\r\\n') ? part.length - 2 : part.endsWith('\\n') ? part.length - 1 : part.length;\n parts[i] = part.slice(0, start) + MASK + part.slice(Math.max(start, end));\n changed = true;\n }\n return changed ? parts.join(delimiter) : text;\n}\n\n/** \"key\": value pairs (a string or a number) anywhere in a text. */\nconst KEY_VALUE = /(\"([^\"\\\\]*)\"\\s*:\\s*)(\"(?:[^\"\\\\]|\\\\.)*\"|-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?)/g;\n\n/**\n * Masks password values in a text that may hold JSON, like a WebSocket\n * message: JSON is parsed and its password keys masked; other texts get the\n * same for every \"password\": \"\u2026\" pair a regex finds (Socket.IO messages such\n * as 42[\"login\",{\"password\":\"\u2026\"}] are not JSON) and for form-encoded text.\n */\nexport function maskPasswordsInText(text: string): string {\n const t = text.trimStart();\n if (t.startsWith('{') || t.startsWith('[')) {\n try {\n const value = parse(text);\n return redactValue(value) ? stringify(value) : text;\n } catch {\n // not JSON after all: look for pairs below\n }\n }\n if (!/pass|pwd/i.test(text)) return text;\n if (t.length < 100_000 && QUERY_LIKE.test(t)) return redactParams(text);\n return text.replace(KEY_VALUE, (pair: string, prefix: string, key: string) => (isPasswordKey(key) ? `${prefix}\"${MASK}\"` : pair));\n}\n\n// ---- serialization of arbitrary values (console arguments, tabtape.track props) ----\n\nexport interface SerializeLimits {\n /** Objects nested deeper than this become \"[Object]\" / \"[Array(n)]\". */\n depth: number;\n /** Array items and object keys kept per level. */\n items: number;\n /** Characters kept per string. */\n string: number;\n}\n\n/** The console argument limits. */\nexport const SERIALIZE_LIMITS: SerializeLimits = { depth: 4, items: 50, string: 2000 };\n\ninterface ErrorLike {\n name: string;\n message: string;\n stack?: string;\n}\n\nexport function isErrorLike(v: unknown): v is ErrorLike {\n if (!v || typeof v !== 'object') return false;\n if (v instanceof Error) return true;\n const e = v as Partial<ErrorLike>;\n return typeof e.message === 'string' && typeof e.name === 'string' && 'stack' in e;\n}\n\nfunction describeNode(node: Node): string {\n if (node.nodeType === 1) {\n const el = node as Element;\n let s = '<' + el.tagName.toLowerCase();\n if (el.id) s += '#' + el.id;\n const cls = el.getAttribute('class')?.trim();\n if (cls) s += '.' + cls.split(/\\s+/).slice(0, 3).join('.');\n return s + '>';\n }\n if (node.nodeType === 3) return `#text \"${clip(node.textContent ?? '', 60)}\"`;\n return node.nodeName;\n}\n\n/** Converts any value into a bounded, JSON-safe structure. */\nexport function serialize(value: unknown, limits: SerializeLimits = SERIALIZE_LIMITS): unknown {\n return walk(value, 0, new WeakSet(), limits);\n}\n\nfunction walk(value: unknown, depth: number, seen: WeakSet<object>, lim: SerializeLimits): unknown {\n switch (typeof value) {\n case 'string':\n return clip(value, lim.string);\n case 'number':\n return Number.isFinite(value) ? value : String(value);\n case 'boolean':\n return value;\n case 'undefined':\n return 'undefined';\n case 'bigint':\n return `${value}n`;\n case 'symbol':\n return value.toString();\n case 'function':\n return `\u0192 ${value.name || 'anonymous'}()`;\n }\n if (value === null) return null;\n const obj = value as object;\n if (seen.has(obj)) return '[Circular]';\n try {\n if (isErrorLike(obj)) {\n return { name: obj.name, message: clip(String(obj.message), lim.string), stack: clip(String(obj.stack ?? ''), Math.min(4000, lim.string * 2)) };\n }\n const n = obj as Node;\n if (typeof n.nodeType === 'number' && typeof n.nodeName === 'string') return describeNode(n);\n if ((obj as Window).window === obj) return '[Window]';\n const tag = toStringTag.call(obj).slice(8, -1);\n if (tag === 'Date') {\n const d = obj as Date;\n return Number.isNaN(d.getTime()) ? 'Invalid Date' : d.toISOString();\n }\n if (tag === 'RegExp') return String(obj);\n if (tag === 'Promise') return '[Promise]';\n if (obj instanceof ArrayBuffer || ArrayBuffer.isView(obj)) return `[${tag} ${(obj as ArrayBuffer).byteLength} bytes]`;\n if (depth >= lim.depth) return Array.isArray(obj) ? `[Array(${obj.length})]` : `[${tag}]`;\n\n seen.add(obj);\n if (Array.isArray(obj)) {\n const out = obj.slice(0, lim.items).map((v) => walk(v, depth + 1, seen, lim));\n if (obj.length > lim.items) out.push(`\u2026 ${obj.length - lim.items} more`);\n return out;\n }\n if (tag === 'Map') return { '[[Map]]': walk([...(obj as Map<unknown, unknown>)].slice(0, lim.items), depth + 1, seen, lim) };\n if (tag === 'Set') return { '[[Set]]': walk([...(obj as Set<unknown>)].slice(0, lim.items), depth + 1, seen, lim) };\n\n const out: Record<string, unknown> = {};\n const keys = Object.keys(obj);\n for (const key of keys.slice(0, lim.items)) {\n try {\n out[key] = walk((obj as Record<string, unknown>)[key], depth + 1, seen, lim);\n } catch {\n out[key] = '[unreadable]';\n }\n }\n if (keys.length > lim.items) out['\u2026'] = `${keys.length - lim.items} more keys`;\n return out;\n } catch {\n return '[unserializable]';\n }\n}\n\nfunction preview(original: unknown, serialized: unknown): string {\n if (typeof original === 'string') return original;\n if (isErrorLike(original)) return `${original.name}: ${original.message}`;\n if (typeof serialized === 'string') return serialized;\n try {\n return clip(stringify(serialized) ?? String(serialized), 1000);\n } catch {\n return String(serialized);\n }\n}\n\n/** Formats console arguments roughly like DevTools does, including %s/%d/%o/%c. */\nexport function formatArgs(args: unknown[], serialized: unknown[]): string {\n const parts: string[] = [];\n let i = 0;\n if (typeof args[0] === 'string' && args[0].includes('%')) {\n i = 1;\n parts.push(\n args[0].replace(/%([sdifoOc%])/g, (m, f: string) => {\n if (f === '%') return '%';\n if (i >= args.length) return m;\n const idx = i++;\n const a = args[idx];\n switch (f) {\n case 'c':\n return '';\n case 'd':\n case 'i':\n return String(Number.parseInt(String(a), 10));\n case 'f':\n return String(Number(a));\n default:\n return preview(a, serialized[idx]);\n }\n }),\n );\n }\n for (; i < args.length; i++) parts.push(preview(args[i], serialized[i]));\n return clip(parts.join(' '), 5000);\n}\n\n/** Drops the recorder's own frames from a stack trace. */\nexport function cleanStack(stack: string | undefined): string | undefined {\n if (!stack) return undefined;\n const lines = stack.split('\\n').filter((l, idx) => idx > 0 && !l.includes('chrome-extension://'));\n return lines.length ? clip(lines.join('\\n'), 4000) : undefined;\n}\n", "// Limits for WebSocket messages in deep mode (NET-R12, NET-R13) and the byte\n// helpers they need. Pure: no chrome.* and no DOM.\n\nexport const WS_LIMITS = {\n /** Messages written per connection and second; the rest are dropped and counted. */\n framesPerSecond: 200,\n /** Message data written per connection and second; later messages of that second are written without data. */\n dataBytesPerSecond: 1_000_000,\n /** Data kept of one text message. */\n messageBytes: 64 * 1024,\n};\n\n/**\n * Fixed one-second windows per connection: a window starts with the first\n * message after the previous one ended. Over the frame limit a message is\n * dropped (and reported with the next written one); once a message's data\n * does not fit the window's data budget, the remaining messages of that\n * window are written without data.\n */\nexport class FrameLimiter {\n private start = Number.NEGATIVE_INFINITY;\n private frames = 0;\n private bytes = 0;\n private full = false;\n private dropped = 0;\n\n constructor(\n private readonly maxFrames = WS_LIMITS.framesPerSecond,\n private readonly maxBytes = WS_LIMITS.dataBytesPerSecond,\n private readonly windowMs = 1000,\n ) {}\n\n /** A message at `t` (ms, monotonic): false if it is over the frame limit and must be dropped. */\n admit(t: number): boolean {\n if (t - this.start >= this.windowMs || t < this.start) {\n this.start = t;\n this.frames = 0;\n this.bytes = 0;\n this.full = false;\n }\n if (this.frames >= this.maxFrames) {\n this.dropped++;\n return false;\n }\n this.frames++;\n return true;\n }\n\n /** Whether `n` bytes of data still fit the current window; if so they are counted. */\n fits(n: number): boolean {\n if (this.full || this.bytes + n > this.maxBytes) {\n this.full = true;\n return false;\n }\n this.bytes += n;\n return true;\n }\n\n /** Messages dropped since the previous call. */\n takeDropped(): number {\n const n = this.dropped;\n this.dropped = 0;\n return n;\n }\n}\n\n/** UTF-8 size of a string in bytes (a lone surrogate counts as U+FFFD, 3 bytes). */\nexport function utf8Length(s: string): number {\n let n = 0;\n for (let i = 0; i < s.length; i++) {\n const c = s.charCodeAt(i);\n if (c < 0x80) n += 1;\n else if (c < 0x800) n += 2;\n else if (isPair(s, i, c)) {\n n += 4;\n i++;\n } else n += 3;\n }\n return n;\n}\n\n/** Cuts `s` to at most `maxBytes` of UTF-8 without splitting a character. */\nexport function cutUtf8(s: string, maxBytes: number): { text: string; bytes: number; truncated: boolean } {\n let n = 0;\n for (let i = 0; i < s.length; i++) {\n const c = s.charCodeAt(i);\n const pair = c >= 0x800 && isPair(s, i, c);\n const w = c < 0x80 ? 1 : c < 0x800 ? 2 : pair ? 4 : 3;\n if (n + w > maxBytes) return { text: s.slice(0, i), bytes: n, truncated: true };\n n += w;\n if (pair) i++;\n }\n return { text: s, bytes: n, truncated: false };\n}\n\nfunction isPair(s: string, i: number, c: number): boolean {\n return c >= 0xd800 && c <= 0xdbff && i + 1 < s.length && (s.charCodeAt(i + 1) & 0xfc00) === 0xdc00;\n}\n\n/** Size in bytes of the data a base64 string encodes. */\nexport function base64Size(b64: string): number {\n const len = b64.length;\n if (!len) return 0;\n const pad = b64.endsWith('==') ? 2 : b64.endsWith('=') ? 1 : 0;\n return Math.floor((len * 3) / 4) - pad;\n}\n\nexport function base64ToBytes(b64: string): Uint8Array {\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n}\n\n/** Decodes text in the given charset, UTF-8 if Chrome named one TextDecoder does not know. */\nexport function decodeText(bytes: Uint8Array, charset?: string): string {\n try {\n return new TextDecoder(charset || 'utf-8').decode(bytes);\n } catch {\n return new TextDecoder().decode(bytes);\n }\n}\n", "// Deep mode timings: CDP's ResourceTiming turned into HAR 1.2 phases the way\n// Chrome DevTools' HAR export does it (front_end/models/har/HARLog.ts,\n// #buildTimings), so tabtape's numbers match a HAR saved from DevTools. One\n// difference: DevTools starts connect where \"blocked\" ends, so on a fresh\n// connection it covers the DNS lookup a second time and the phases add up to\n// more than the request took; here connect starts when DNS ends. connect\n// includes ssl, as HAR asks. Pure: no chrome.* and no DOM.\n\nimport type { NetworkTiming } from '../../../shared/events';\n\n/** CDP Network.ResourceTiming: requestTime in seconds, the rest in ms after it (-1: did not happen). */\nexport interface CdpTiming {\n requestTime: number;\n proxyStart: number;\n proxyEnd: number;\n dnsStart: number;\n dnsEnd: number;\n connectStart: number;\n connectEnd: number;\n sslStart: number;\n sslEnd: number;\n sendStart: number;\n sendEnd: number;\n receiveHeadersEnd: number;\n}\n\nexport interface TimingInput {\n /** requestWillBeSent.timestamp (s, monotonic). */\n issueTime: number;\n /** response.timing; leave it out for memory cache hits (it describes the original load). */\n timing?: CdpTiming;\n /** When the response headers arrived (responseReceived.timestamp, s); absent without a response. */\n responseTime?: number;\n /** loadingFinished / loadingFailed timestamp (s); absent while the request is in flight. */\n endTime?: number;\n}\n\nexport interface Times {\n timing?: NetworkTiming;\n /** ms from the request's start until the response headers. */\n ttfb?: number;\n /** ms from the request's start until it finished or failed. */\n duration?: number;\n}\n\nconst round = (n: number): number => Math.round(n * 10) / 10;\n\nexport function computeTimes(input: TimingInput): Times {\n const { issueTime } = input;\n const t = input.timing && input.timing.requestTime > 0 ? input.timing : undefined;\n // DevTools' NetworkRequest: the headers time comes from the timing when it\n // has one (more precise), and the end is never before it.\n let responseTime = input.responseTime ?? -1;\n if (t) {\n const headers = t.requestTime + t.receiveHeadersEnd / 1000;\n if (responseTime < 0 || responseTime > headers) responseTime = headers;\n if (t.requestTime > responseTime) responseTime = t.requestTime;\n }\n let endTime = input.endTime ?? -1;\n if (input.endTime !== undefined) {\n if (t) endTime = Math.max(endTime, responseTime);\n else if (responseTime > endTime) responseTime = endTime;\n }\n\n const out: Times = {};\n if (responseTime >= 0) out.ttfb = round(Math.max(0, (responseTime - issueTime) * 1000));\n if (endTime >= 0) out.duration = round(Math.max(0, (endTime - issueTime) * 1000));\n const timing = harTiming(issueTime, t, responseTime, endTime);\n if (timing) out.timing = timing;\n return out;\n}\n\n/** HAR phases in ms; -1 (did not apply) is left out. */\nfunction harTiming(issueTime: number, t: CdpTiming | undefined, responseTime: number, endTime: number): NetworkTiming | undefined {\n const ms = (s: number) => s * 1000;\n if (!t) {\n // No details (memory cache, failures before a response): what is known.\n if (responseTime < 0) return endTime >= 0 ? clean({ blocked: ms(endTime - issueTime), send: 0, wait: 0, receive: 0 }) : undefined;\n const waitEnd = ms(responseTime - issueTime);\n return clean({ send: 0, wait: waitEnd, receive: endTime >= 0 ? Math.max(ms(endTime - issueTime) - waitEnd, 0) : -1 });\n }\n\n // \"blocked\": queued in the browser plus stalled before the first byte of\n // DNS, connect or send; a proxy lookup counts if it took longer.\n const queued = issueTime < t.requestTime ? ms(t.requestTime - issueTime) : 0;\n const least = leastNonNegative([t.dnsStart, t.connectStart, t.sendStart]);\n const blockedStart = least === Infinity ? 0 : least;\n let blocked = queued + blockedStart;\n const proxy = t.proxyEnd !== -1 ? t.proxyEnd - t.proxyStart : 0;\n if (proxy > blocked) blocked = proxy;\n\n const dnsEnd = t.dnsEnd >= 0 ? t.dnsEnd : -1;\n const dns = t.dnsEnd >= 0 ? t.dnsEnd - blockedStart : -1;\n const sslEnd = t.sslEnd > 0 ? t.sslEnd : -1;\n const ssl = t.sslEnd > 0 ? t.sslEnd - t.sslStart : -1;\n const connectEnd = t.connectEnd >= 0 ? t.connectEnd : -1;\n const connect = t.connectEnd >= 0 ? t.connectEnd - Math.max(dnsEnd, blockedStart) : -1;\n const sendEnd = t.sendEnd >= 0 ? t.sendEnd : 0;\n // QUIC sometimes reports sendStart before connectEnd.\n const send = t.sendEnd >= 0 ? Math.max(t.sendEnd - Math.max(connectEnd, dnsEnd, blockedStart), 0) : 0;\n const highest = Math.max(sendEnd, connectEnd, sslEnd, dnsEnd, blockedStart, 0);\n const waitEnd = ms(responseTime - t.requestTime);\n const wait = Math.max(waitEnd - highest, 0);\n const receive = endTime >= 0 ? Math.max(ms(endTime - t.requestTime) - waitEnd, 0) : -1;\n return clean({ blocked, dns, connect, ssl, send, wait, receive });\n}\n\nfunction leastNonNegative(values: number[]): number {\n return values.reduce((best, v) => (v >= 0 && v < best ? v : best), Infinity);\n}\n\nfunction clean(t: Partial<Record<keyof NetworkTiming, number>>): NetworkTiming {\n const out: NetworkTiming = {};\n for (const [k, v] of Object.entries(t) as [keyof NetworkTiming, number | undefined][]) {\n if (v !== undefined && v >= 0 && Number.isFinite(v)) out[k] = round(v);\n }\n return out;\n}\n", "// Deep network mode (NET-R4 to NET-R13): turns Chrome DevTools Protocol\n// Network events into tabtape records. One \"network\" record per HTTP request\n// (every redirect hop is its own record) and per WebSocket connection, one\n// \"ws\" record per WebSocket message.\n//\n// Pure: no chrome.* calls. debugger.ts feeds the events in (with the child\n// session id for worker requests) and fetches bodies through the injected\n// functions; records come out through emit().\n//\n// The main events (requestWillBeSent, responseReceived, loadingFinished or\n// loadingFailed) come from the renderer, the *ExtraInfo events (raw headers\n// with cookies, blocked cookies, the status on the wire) from the network\n// service, in no particular order relative to each other. As in DevTools,\n// the n-th ExtraInfo of a request id belongs to its n-th hop (a redirect\n// keeps the request id), and a finished hop waits a moment for ExtraInfo\n// that was announced (hasExtraInfo) but has not arrived. ExtraInfo nobody\n// claims (WebSocket handshakes, requests of targets we do not watch) never\n// becomes an HTTP record and expires.\n//\n// Request ids are unique across the tab's sessions (DevTools relies on that\n// too), and one request's events can arrive on different sessions: a\n// dedicated worker's requests have their main events on the worker's session\n// but their ExtraInfo on the page's, and the worker script starts on the\n// page's session and finishes on the worker's. So requests are keyed by\n// request id alone; the session of requestWillBeSent says whose request it\n// is (`worker`), and the body is asked from the session that finished it.\n\nimport type { BlockedCookie, CorsErrorStatus, NetworkData, NetworkKind, WsFrameCounts, WsFrameData } from '../../../shared/events';\nimport { maskPasswordsInText, redactBody, redactUrl, round, TEXT_MIME } from '../recorder/util';\nimport { base64Size, base64ToBytes, cutUtf8, decodeText, FrameLimiter, utf8Length, WS_LIMITS } from './limit';\nimport { computeTimes, type CdpTiming } from './timing';\n\nexport type CdpRecord = { ts: number; type: 'network'; data: NetworkData } | { ts: number; type: 'ws'; data: WsFrameData };\n\nexport interface WorkerInfo {\n type: 'worker' | 'shared_worker' | 'service_worker';\n url: string;\n}\n\nexport interface CdpNetworkOptions {\n captureBodies: boolean;\n maxBodyChars: number;\n emit(record: CdpRecord): void;\n /** Network.getResponseBody on the session that saw the request (undefined: the page's). */\n getResponseBody(sessionId: string | undefined, requestId: string): Promise<{ body: string; base64Encoded: boolean }>;\n /** Network.getRequestPostData, likewise. */\n getRequestPostData(sessionId: string | undefined, requestId: string): Promise<{ postData: string; base64Encoded?: boolean }>;\n /** Requests not to record (made by tabtape's own pages). */\n ignore?(request: { url: string; documentURL?: string }): boolean;\n}\n\n// ---- the parts of CDP's Network domain used here ----\n\ntype CdpHeaders = Record<string, string>;\n\ninterface CdpRequest {\n url: string;\n urlFragment?: string;\n method?: string;\n headers?: CdpHeaders;\n postData?: string;\n hasPostData?: boolean;\n postDataEntries?: { bytes?: string }[];\n}\n\ninterface CdpResponse {\n status?: number;\n statusText?: string;\n headers?: CdpHeaders;\n mimeType?: string;\n charset?: string;\n requestHeaders?: CdpHeaders;\n remoteIPAddress?: string;\n remotePort?: number;\n fromDiskCache?: boolean;\n fromPrefetchCache?: boolean;\n fromServiceWorker?: boolean;\n serviceWorkerResponseSource?: string;\n encodedDataLength?: number;\n timing?: CdpTiming;\n protocol?: string;\n}\n\ninterface CdpCookie {\n name?: string;\n domain?: string;\n path?: string;\n}\n\ninterface RequestWillBeSent {\n requestId: string;\n documentURL?: string;\n request?: CdpRequest;\n timestamp: number;\n wallTime?: number;\n redirectHasExtraInfo?: boolean;\n redirectResponse?: CdpResponse;\n type?: string;\n frameId?: string;\n}\n\ninterface RequestWillBeSentExtraInfo {\n requestId: string;\n headers?: CdpHeaders;\n associatedCookies?: { cookie?: CdpCookie; blockedReasons?: unknown }[];\n /** Before Chrome 120 the blocked request cookies came in blockedCookies. */\n blockedCookies?: { cookie?: CdpCookie; blockedReasons?: unknown }[];\n}\n\ninterface ResponseReceived {\n requestId: string;\n timestamp: number;\n type?: string;\n response?: CdpResponse;\n hasExtraInfo?: boolean;\n}\n\ninterface ResponseReceivedExtraInfo {\n requestId: string;\n headers?: CdpHeaders;\n statusCode?: number;\n headersText?: string;\n blockedCookies?: { cookie?: CdpCookie; cookieLine?: string; blockedReasons?: unknown }[];\n}\n\ninterface LoadingFinished {\n requestId: string;\n timestamp: number;\n encodedDataLength?: number;\n}\n\ninterface LoadingFailed {\n requestId: string;\n timestamp: number;\n type?: string;\n errorText?: string;\n canceled?: boolean;\n blockedReason?: string;\n corsErrorStatus?: { corsError?: string; failedParameter?: string };\n}\n\ninterface WsHandshakeRequest {\n requestId: string;\n timestamp?: number;\n wallTime?: number;\n request?: { headers?: CdpHeaders };\n}\n\ninterface WsHandshakeResponse {\n requestId: string;\n timestamp?: number;\n response?: { status?: number; statusText?: string; headers?: CdpHeaders; requestHeaders?: CdpHeaders };\n}\n\ninterface WsFrameEvent {\n requestId: string;\n timestamp?: number;\n response?: { opcode?: number; payloadData?: string };\n errorMessage?: string;\n}\n\n// ---- state ----\n\n/** What is kept of a requestWillBeSentExtraInfo (no cookie values). */\ninterface RequestExtra {\n headers?: CdpHeaders;\n blockedCookies?: BlockedCookie[];\n}\n\n/** What is kept of a responseReceivedExtraInfo. */\ninterface ResponseExtra {\n headers?: CdpHeaders;\n status?: number;\n statusText?: string;\n blockedSetCookies?: BlockedCookie[];\n}\n\n/** Everything CDP said under one request id. */\ninterface Entry {\n requestId: string;\n /** The session of the request's main events (requestWillBeSent, webSocketCreated): a worker's, or undefined for the page. */\n sessionId?: string;\n hops: Hop[];\n /** ExtraInfo in arrival order: the n-th belongs to hops[n]. */\n reqExtras: RequestExtra[];\n resExtras: ResponseExtra[];\n /** Date.now() when created, to expire ExtraInfo nobody claims. */\n seenAt: number;\n ws?: WsConn;\n}\n\n/**\n * open: in flight; waiting: finished, waiting for its ExtraInfo; building:\n * bodies are being fetched; done: written (or skipped).\n */\ntype HopState = 'open' | 'waiting' | 'building' | 'done';\n\n/** One HTTP request: what a request id asked first, or a redirect hop. */\ninterface Hop {\n entry: Entry;\n index: number;\n id: string;\n /** Not recorded (tabtape's own pages, data: URLs), but it still takes its ExtraInfo. */\n skip: boolean;\n url: string;\n /** Epoch ms. */\n ts: number;\n /** requestWillBeSent.timestamp (s, monotonic). */\n issueTime: number;\n request: CdpRequest;\n type?: string;\n frameId?: string;\n response?: CdpResponse;\n responseTime?: number;\n hasExtraInfo?: boolean;\n /** The session that reported the response ('' for the page): its buffer has the body. */\n dataSession?: string;\n memoryCache?: boolean;\n dataLength: number;\n encodedDataLength?: number;\n endTime?: number;\n redirected?: boolean;\n error?: string;\n blockedReason?: string;\n corsErrorStatus?: CorsErrorStatus;\n state: HopState;\n timer?: ReturnType<typeof setTimeout>;\n /** The record while its bodies are fetched. */\n data?: NetworkData;\n /** Bodies still being fetched. */\n awaiting: { req: boolean; res: boolean };\n}\n\ninterface WsConn {\n entry: Entry;\n id: string;\n url: string;\n /** Date.now() at webSocketCreated: the record's ts if no handshake was seen. */\n seenAt: number;\n /** Epoch ms of the handshake request. */\n ts?: number;\n /** Handshake request time (s, monotonic). */\n t0?: number;\n handshakeHeaders?: CdpHeaders;\n response?: WsHandshakeResponse['response'];\n responseTime?: number;\n closedAt?: number;\n error?: string;\n counts: WsFrameCounts;\n dropped: number;\n dataOmitted: number;\n limiter: FrameLimiter;\n emitted: boolean;\n}\n\n/** How long a finished request waits for ExtraInfo it announced. */\nconst EXTRA_INFO_WAIT_MS = 1000;\n/** A request CORS blocked had a response on the wire whose ExtraInfo may still be coming. */\nconst CORS_EXTRA_INFO_WAIT_MS = 200;\n/** ExtraInfo nobody claimed is dropped after this. */\nconst ORPHAN_TTL_MS = 10_000;\n/** Requests kept in flight at most; beyond, the oldest are written as pending. */\nconst MAX_ENTRIES = 5000;\nconst NO_BODY_STATUS = new Set([101, 204, 205, 304]);\n/** Not HTTP: inline data, extension resources, browser pages. */\nconst SKIP_URL = /^(data|chrome-extension|chrome|devtools):/i;\n/** Request cookies Chrome did not send because they belong to another host, path or port: not \"blocked\". */\nconst NOT_FOR_THIS_URL = new Set(['DomainMismatch', 'NotOnPath', 'PortMismatch']);\n/** Header values holding a URL get the request URL's password masking. */\nconst URL_HEADERS = new Set(['referer', 'location', 'content-location', ':path']);\nconst MULTIPART = /multipart\\/form-data/i;\nconst STOPPED_NOTE = 'not captured: deep network mode stopped';\n/** A body Chrome has not handed over by then is not coming (a command to a session that went away is never answered). */\nconst BODY_TIMEOUT_MS = 10_000;\n\nexport class CdpNetwork {\n private readonly entries = new Map<string, Entry>();\n private readonly workers = new Map<string, WorkerInfo>();\n /** wallTime - timestamp (s) per session ('' is the page's), to date events that only have a timestamp. */\n private readonly clocks = new Map<string, number>();\n private readonly building = new Set<Hop>();\n private readonly jobs = new Set<Promise<void>>();\n /** Frames of cross-site iframes with a target of their own (not watched). */\n private readonly iframes = new Set<string>();\n private mainFrame: string | undefined;\n private httpSeq = 0;\n private wsSeq = 0;\n private lastExpiry = 0;\n private closed = false;\n\n constructor(private readonly opts: CdpNetworkOptions) {}\n\n /** The tab's main frame: its Document requests are navigations, other frames' are resources. */\n setMainFrame(frameId: string | undefined): void {\n this.mainFrame = frameId;\n }\n\n /**\n * A cross-site iframe got a target of its own; its id is the frame's id.\n * The iframe's document request started on the page's session but\n * finishes on the iframe's, which is not watched: it ends with its\n * response instead of staying in flight until capture stops.\n */\n iframeAttached(frameId: string): void {\n if (this.closed) return;\n this.iframes.add(frameId);\n for (const e of [...this.entries.values()]) {\n const hop = e.hops.at(-1);\n if (hop?.state === 'open' && hop.response) this.endWithResponse(hop);\n }\n }\n\n iframeDetached(frameId: string): void {\n this.iframes.delete(frameId);\n }\n\n /** A worker's child session: its requests carry `worker`. */\n addWorker(sessionId: string, info: WorkerInfo): void {\n this.workers.set(sessionId, { type: info.type, url: redactUrl(info.url) });\n }\n\n /** Feeds one CDP event in; events of other domains are ignored. Never throws. */\n handle(method: string, params: unknown, sessionId?: string): void {\n if (this.closed || !params || typeof params !== 'object') return;\n if (typeof (params as { requestId?: unknown }).requestId !== 'string') return;\n try {\n switch (method) {\n case 'Network.requestWillBeSent':\n this.onRequest(params as RequestWillBeSent, sessionId);\n break;\n case 'Network.requestWillBeSentExtraInfo':\n this.onRequestExtra(params as RequestWillBeSentExtraInfo);\n break;\n case 'Network.responseReceived':\n this.onResponse(params as ResponseReceived, sessionId);\n break;\n case 'Network.responseReceivedExtraInfo':\n this.onResponseExtra(params as ResponseReceivedExtraInfo);\n break;\n case 'Network.requestServedFromCache': {\n const hop = this.openHop((params as { requestId: string }).requestId);\n if (hop) hop.memoryCache = true;\n break;\n }\n case 'Network.dataReceived': {\n const p = params as { requestId: string; dataLength?: number };\n const hop = this.openHop(p.requestId);\n if (hop && typeof p.dataLength === 'number' && p.dataLength > 0) hop.dataLength += p.dataLength;\n break;\n }\n case 'Network.loadingFinished':\n this.onFinished(params as LoadingFinished, sessionId);\n break;\n case 'Network.loadingFailed':\n this.onFailed(params as LoadingFailed);\n break;\n case 'Network.webSocketCreated':\n this.onWsCreated(params as { requestId: string; url?: string }, sessionId);\n break;\n case 'Network.webSocketWillSendHandshakeRequest':\n this.onWsHandshakeRequest(params as WsHandshakeRequest, sessionId);\n break;\n case 'Network.webSocketHandshakeResponseReceived':\n this.onWsHandshakeResponse(params as WsHandshakeResponse);\n break;\n case 'Network.webSocketFrameSent':\n this.onWsFrame(params as WsFrameEvent, sessionId, 'send');\n break;\n case 'Network.webSocketFrameReceived':\n this.onWsFrame(params as WsFrameEvent, sessionId, 'recv');\n break;\n case 'Network.webSocketFrameError':\n this.onWsError(params as WsFrameEvent, sessionId);\n break;\n case 'Network.webSocketClosed':\n this.onWsClosed(params as WsFrameEvent, sessionId);\n break;\n }\n } catch {\n // a malformed event must not stop the capture\n }\n this.expire();\n }\n\n /**\n * A worker went away or the page crashed: its requests will not finish\n * and are written as pending, and bodies asked from it will not come.\n */\n endSession(sessionId: string | undefined, why = 'the worker ended'): void {\n if (this.closed) return;\n const gone = sessionId ?? '';\n for (const hop of [...this.building]) {\n if ((hop.entry.sessionId ?? '') !== gone && this.bodySession(hop) !== sessionId) continue;\n if (hop.data && hop.awaiting.req) hop.data.reqBodyNote = `not available: ${why}`;\n if (hop.data && hop.awaiting.res) hop.data.resBodyNote = `not available: ${why}`;\n this.finish(hop);\n }\n for (const e of [...this.entries.values()]) if (e.sessionId === sessionId) this.flushEntry(e);\n if (sessionId !== undefined) {\n this.workers.delete(sessionId);\n this.clocks.delete(sessionId);\n }\n }\n\n /**\n * Deep capture ends: requests in flight are written as pending and open\n * WebSockets with pending set; bodies being fetched get `timeoutMs`, then\n * their records are written without them. Later events are ignored.\n */\n async stop(timeoutMs: number): Promise<void> {\n if (this.closed) return;\n this.closed = true;\n for (const e of [...this.entries.values()]) this.flushEntry(e);\n if (this.jobs.size) {\n let timer: ReturnType<typeof setTimeout> | undefined;\n await Promise.race([Promise.all(this.jobs), new Promise<void>((resolve) => (timer = setTimeout(resolve, timeoutMs)))]);\n clearTimeout(timer);\n }\n for (const hop of [...this.building]) {\n if (hop.data && hop.awaiting.req) hop.data.reqBodyNote = STOPPED_NOTE;\n if (hop.data && hop.awaiting.res) hop.data.resBodyNote = STOPPED_NOTE;\n this.finish(hop);\n }\n this.entries.clear();\n }\n\n // ---- HTTP ----\n\n private onRequest(p: RequestWillBeSent, sid: string | undefined): void {\n const req = p.request;\n if (!req || typeof req.url !== 'string') return;\n this.learnClock(sid, p.timestamp, p.wallTime);\n let e = this.entries.get(p.requestId);\n const last = e?.hops.at(-1);\n if (last?.state === 'open') {\n if (!p.redirectResponse) return; // repeated (DevTools ignores these too)\n // The previous hop ends with the redirect response.\n last.response = p.redirectResponse;\n last.responseTime = p.timestamp;\n last.hasExtraInfo = p.redirectHasExtraInfo;\n last.endTime = p.timestamp;\n const size = p.redirectResponse.encodedDataLength;\n if (typeof size === 'number' && size > 0) last.encodedDataLength = size;\n last.redirected = true;\n this.complete(last);\n } else if (e && last) {\n // The id was used by a request that finished: never merge two requests.\n this.entries.delete(p.requestId);\n e = undefined;\n }\n e ??= this.newEntry(p.requestId);\n setSession(e, sid);\n const url = req.url + (req.urlFragment ?? '');\n const skip = SKIP_URL.test(url) || !!this.opts.ignore?.({ url, documentURL: p.documentURL });\n e.hops.push({\n entry: e,\n index: e.hops.length,\n id: skip ? '' : `cdp-${++this.httpSeq}`,\n skip,\n url,\n ts: typeof p.wallTime === 'number' ? Math.round(p.wallTime * 1000) : this.epochMs(sid, p.timestamp),\n issueTime: p.timestamp,\n request: req,\n type: p.type,\n frameId: p.frameId,\n dataLength: 0,\n state: 'open',\n awaiting: { req: false, res: false },\n });\n }\n\n /** ExtraInfo comes on the page's session even for a worker's requests: matched by request id only. */\n private onRequestExtra(p: RequestWillBeSentExtraInfo): void {\n const e = this.entryFor(p.requestId);\n e.reqExtras.push({ headers: p.headers, blockedCookies: blockedRequestCookies(p.associatedCookies ?? p.blockedCookies) });\n this.extraArrived(e.hops[e.reqExtras.length - 1]);\n }\n\n private onResponseExtra(p: ResponseReceivedExtraInfo): void {\n const e = this.entryFor(p.requestId);\n e.resExtras.push({\n headers: p.headers,\n status: typeof p.statusCode === 'number' && p.statusCode > 0 ? p.statusCode : undefined,\n statusText: reasonPhrase(p.headersText),\n blockedSetCookies: blockedSetCookies(p.blockedCookies),\n });\n this.extraArrived(e.hops[e.resExtras.length - 1]);\n }\n\n private onResponse(p: ResponseReceived, sid: string | undefined): void {\n const hop = this.openHop(p.requestId);\n if (!hop || !p.response) return;\n hop.response = p.response;\n hop.responseTime = p.timestamp;\n hop.hasExtraInfo = p.hasExtraInfo;\n hop.dataSession = sid ?? '';\n if (p.type) hop.type = p.type;\n this.endWithResponse(hop);\n }\n\n /** The document of a cross-site iframe: loadingFinished will come on the iframe's session. */\n private endWithResponse(hop: Hop): void {\n if (hop.type !== 'Document' || hop.frameId === undefined || !this.iframes.has(hop.frameId)) return;\n hop.endTime = hop.responseTime;\n this.complete(hop);\n }\n\n private onFinished(p: LoadingFinished, sid: string | undefined): void {\n const hop = this.openHop(p.requestId);\n if (!hop) return;\n hop.endTime = p.timestamp;\n hop.dataSession = sid ?? '';\n if (typeof p.encodedDataLength === 'number' && p.encodedDataLength > 0) hop.encodedDataLength = p.encodedDataLength;\n this.complete(hop);\n }\n\n private onFailed(p: LoadingFailed): void {\n const hop = this.openHop(p.requestId);\n if (!hop) return;\n hop.endTime = p.timestamp;\n hop.error = p.errorText || (p.canceled ? 'canceled' : 'failed');\n if (p.blockedReason) hop.blockedReason = p.blockedReason;\n const cors = p.corsErrorStatus;\n if (cors?.corsError) hop.corsErrorStatus = cors.failedParameter ? { corsError: cors.corsError, failedParameter: cors.failedParameter } : { corsError: cors.corsError };\n if (p.type) hop.type = p.type;\n this.complete(hop);\n }\n\n /** The hop finished, failed or was redirected: written once its ExtraInfo is there (or late). */\n private complete(hop: Hop): void {\n hop.state = 'waiting';\n const wait = this.extraWait(hop);\n if (wait > 0) hop.timer = setTimeout(() => this.build(hop, false), wait);\n else this.build(hop, false);\n }\n\n private extraWait(hop: Hop): number {\n const e = hop.entry;\n const req = e.reqExtras[hop.index] !== undefined;\n const res = e.resExtras[hop.index] !== undefined;\n if (hop.hasExtraInfo) return req && res ? 0 : EXTRA_INFO_WAIT_MS;\n if (hop.corsErrorStatus && !res) return CORS_EXTRA_INFO_WAIT_MS;\n return 0;\n }\n\n private extraArrived(hop: Hop | undefined): void {\n if (hop?.state === 'waiting' && this.extraWait(hop) === 0) this.build(hop, false);\n }\n\n /** Describes the hop and fetches its bodies; `pending`: it is still in flight. */\n private build(hop: Hop, pending: boolean): void {\n if (hop.state === 'building' || hop.state === 'done') return;\n clearTimeout(hop.timer);\n if (hop.skip) {\n hop.state = 'done';\n this.release(hop);\n return;\n }\n hop.state = 'building';\n const d = this.describe(hop, pending);\n hop.data = d;\n const job = this.opts.captureBodies ? this.bodies(hop, d, pending) : undefined;\n if (!job) {\n this.finish(hop);\n return;\n }\n this.building.add(hop);\n const tracked: Promise<void> = job\n .catch(() => {})\n .then(() => {\n this.jobs.delete(tracked);\n this.finish(hop);\n });\n this.jobs.add(tracked);\n }\n\n private finish(hop: Hop): void {\n const d = hop.data;\n if (hop.state !== 'building' || !d) return;\n hop.state = 'done';\n hop.data = undefined;\n this.building.delete(hop);\n this.emit({ ts: hop.ts, type: 'network', data: d });\n this.release(hop);\n }\n\n /** Forgets a request id once all its hops are written (a redirected last hop has its next hop coming). */\n private release(hop: Hop): void {\n const e = hop.entry;\n if (e.ws || e.hops.at(-1)?.redirected || !e.hops.every((h) => h.state === 'done')) return;\n if (this.entries.get(e.requestId) === e) this.entries.delete(e.requestId);\n }\n\n /** The record of a hop from what CDP said; bodies come later. */\n private describe(hop: Hop, pending: boolean): NetworkData {\n const e = hop.entry;\n const reqExtra = e.reqExtras[hop.index];\n const resExtra = e.resExtras[hop.index];\n const res = hop.response;\n const resourceType = (hop.type || 'Other').toLowerCase();\n const kind = this.kindOf(hop);\n const d: NetworkData = { id: hop.id, kind, method: hop.request.method || 'GET', url: redactUrl(hop.url) };\n if (res?.status) d.status = res.status;\n if (res?.statusText) d.statusText = res.statusText;\n // ExtraInfo has the status on the wire: 304 for a revalidated cache entry\n // (the page sees 200), and the only status of a response CORS blocked.\n if (resExtra?.status && resExtra.status !== d.status) {\n d.status = resExtra.status;\n if (resExtra.statusText) d.statusText = resExtra.statusText;\n else delete d.statusText;\n }\n const times = computeTimes({\n issueTime: hop.issueTime,\n // A memory cache hit carries the timing of the load that filled the cache.\n timing: hop.memoryCache ? undefined : res?.timing,\n responseTime: res ? hop.responseTime : undefined,\n endTime: pending ? undefined : hop.endTime,\n });\n if (pending) d.duration = round(Math.max(0, Date.now() - hop.ts));\n else if (times.duration !== undefined) d.duration = times.duration;\n if (times.ttfb !== undefined) d.ttfb = times.ttfb;\n // The raw headers (ExtraInfo) have Cookie and Set-Cookie; Blink's do not.\n const reqHeaders = headerMap(reqExtra?.headers ?? res?.requestHeaders ?? hop.request.headers);\n if (reqHeaders) d.reqHeaders = reqHeaders;\n const resHeaders = headerMap(resExtra?.headers ?? res?.headers);\n if (resHeaders) d.resHeaders = resHeaders;\n if (hop.dataLength > 0) d.resSize = hop.dataLength;\n if (hop.encodedDataLength) d.transferSize = hop.encodedDataLength;\n if (res?.mimeType) d.mime = res.mimeType;\n if (hop.error) d.error = hop.error;\n if (pending) d.pending = true;\n if (kind === 'resource') d.initiator = resourceType;\n d.source = 'cdp';\n d.resourceType = resourceType;\n if (times.timing) d.timing = times.timing;\n if (res?.protocol) d.protocol = res.protocol;\n const address = remoteAddress(res);\n if (address) d.remoteAddress = address;\n const cache = hop.memoryCache ? 'memory' : res?.fromDiskCache ? 'disk' : res?.fromPrefetchCache ? 'prefetch' : undefined;\n if (cache) d.cache = cache;\n if (res?.fromServiceWorker) d.fromServiceWorker = true;\n if (res?.serviceWorkerResponseSource) d.swSource = res.serviceWorkerResponseSource;\n if (hop.blockedReason) d.blockedReason = hop.blockedReason;\n if (hop.corsErrorStatus) d.corsErrorStatus = hop.corsErrorStatus;\n if (reqExtra?.blockedCookies) d.blockedCookies = reqExtra.blockedCookies;\n if (resExtra?.blockedSetCookies) d.blockedSetCookies = resExtra.blockedSetCookies;\n const worker = this.workerOf(e.sessionId);\n if (worker) d.worker = worker;\n return d;\n }\n\n private kindOf(hop: Hop): NetworkKind {\n switch (hop.type) {\n case 'Fetch':\n return 'fetch';\n case 'XHR':\n return 'xhr';\n case 'Ping':\n return 'beacon';\n case 'Document':\n // The main frame's document; iframes' documents are resources.\n if (hop.entry.sessionId === undefined && (this.mainFrame === undefined || hop.frameId === undefined || hop.frameId === this.mainFrame)) {\n return 'navigation';\n }\n }\n return 'resource';\n }\n\n /** Bodies follow the in-page rules: request bodies of fetch, XHR and beacons, response bodies of fetch and XHR. */\n private bodies(hop: Hop, d: NetworkData, pending: boolean): Promise<void> | undefined {\n const jobs: Promise<void>[] = [];\n if (d.kind === 'fetch' || d.kind === 'xhr' || d.kind === 'beacon') {\n const job = this.requestBody(hop, d);\n if (job) jobs.push(job);\n }\n if (!pending && (d.kind === 'fetch' || d.kind === 'xhr')) {\n const job = this.responseBody(hop, d);\n if (job) jobs.push(job);\n }\n return jobs.length ? Promise.all(jobs).then(() => undefined) : undefined;\n }\n\n private requestBody(hop: Hop, d: NetworkData): Promise<void> | undefined {\n const req = hop.request;\n const contentType = d.reqHeaders?.['content-type'] ?? '';\n if (typeof req.postData === 'string') {\n this.setRequestBody(d, req.postData, contentType);\n return undefined;\n }\n const inline = entriesBytes(req.postDataEntries);\n if (inline) {\n this.setRequestBytes(d, inline, contentType);\n return undefined;\n }\n if (!req.hasPostData) return undefined;\n // Too long to come with requestWillBeSent (maxPostDataSize): ask for it.\n hop.awaiting.req = true;\n return withTimeout(this.opts.getRequestPostData(hop.entry.sessionId, hop.entry.requestId), BODY_TIMEOUT_MS)\n .then(\n (r) => {\n if (hop.state !== 'building') return;\n if (r.base64Encoded) this.setRequestBytes(d, base64ToBytes(r.postData), contentType);\n else this.setRequestBody(d, String(r.postData ?? ''), contentType);\n },\n (err: unknown) => {\n if (hop.state === 'building') d.reqBodyNote = `not available: ${errorText(err)}`;\n },\n )\n .finally(() => {\n hop.awaiting.req = false;\n });\n }\n\n private setRequestBytes(d: NetworkData, bytes: Uint8Array, contentType: string): void {\n if (contentType && isBinaryType(contentType)) d.reqBodyNote = `binary (${contentType}), ${bytes.length} bytes`;\n else this.setRequestBody(d, decodeText(bytes), contentType, bytes.length);\n }\n\n private setRequestBody(d: NetworkData, text: string, contentType: string, bytes?: number): void {\n const binary = contentType ? isBinaryType(contentType) : /[\\u0000\uFFFD]/.test(text.slice(0, 4096));\n if (binary) {\n d.reqBodyNote = `binary${contentType ? ` (${contentType})` : ''}, ${bytes ?? utf8Length(text)} bytes`;\n return;\n }\n // Masked on the whole body (JSON must parse), then cut.\n const body = clipText(redactBody(text, contentType), this.opts.maxBodyChars);\n d.reqBody = body.text;\n if (body.truncated) d.reqBodyTruncated = true;\n }\n\n private responseBody(hop: Hop, d: NetworkData): Promise<void> | undefined {\n const res = hop.response;\n if (!res || hop.error || hop.redirected || d.method === 'HEAD' || NO_BODY_STATUS.has(res.status ?? 0)) return undefined;\n const mime = res.mimeType ?? '';\n if (/event-stream/i.test(mime)) {\n d.resBodyNote = 'event stream, not captured';\n return undefined;\n }\n if (mime && !TEXT_MIME.test(mime)) {\n d.resBodyNote = `binary (${mime}${d.resSize ? `, ${d.resSize} bytes` : ''})`;\n return undefined;\n }\n hop.awaiting.res = true;\n return withTimeout(this.opts.getResponseBody(this.bodySession(hop), hop.entry.requestId), BODY_TIMEOUT_MS)\n .then(\n (r) => {\n if (hop.state !== 'building') return;\n let text = String(r.body ?? '');\n let size: number | undefined;\n if (r.base64Encoded) {\n const bytes = base64ToBytes(text);\n size = bytes.length;\n text = decodeText(bytes, res.charset);\n }\n d.resSize ??= size ?? utf8Length(text);\n const body = clipText(text, this.opts.maxBodyChars);\n d.resBody = body.text;\n if (body.truncated) d.resBodyTruncated = true;\n },\n (err: unknown) => {\n // Chrome dropped it from its buffer, or the debugger is gone.\n if (hop.state === 'building') d.resBodyNote = `not available: ${errorText(err)}`;\n },\n )\n .finally(() => {\n hop.awaiting.res = false;\n });\n }\n\n // ---- WebSocket ----\n\n private onWsCreated(p: { requestId: string; url?: string }, sid: string | undefined): void {\n if (typeof p.url !== 'string') return;\n const e = this.entryFor(p.requestId);\n if (e.ws) return;\n setSession(e, sid);\n e.ws = {\n entry: e,\n id: `ws-${++this.wsSeq}`,\n url: redactUrl(p.url),\n seenAt: Date.now(),\n counts: { sent: 0, received: 0, sentBytes: 0, receivedBytes: 0 },\n dropped: 0,\n dataOmitted: 0,\n limiter: new FrameLimiter(),\n emitted: false,\n };\n }\n\n private onWsHandshakeRequest(p: WsHandshakeRequest, sid: string | undefined): void {\n this.learnClock(sid, p.timestamp, p.wallTime);\n const conn = this.conn(p.requestId);\n if (!conn) return;\n if (typeof p.wallTime === 'number') conn.ts = Math.round(p.wallTime * 1000);\n if (typeof p.timestamp === 'number') conn.t0 = p.timestamp;\n conn.handshakeHeaders = p.request?.headers;\n }\n\n private onWsHandshakeResponse(p: WsHandshakeResponse): void {\n const conn = this.conn(p.requestId);\n if (!conn || !p.response) return;\n conn.response = p.response;\n conn.responseTime = p.timestamp;\n }\n\n /**\n * One message (NET-R12, NET-R13): text keeps at most 64KB of data, masked\n * first; at most 200 messages and 1MB of data per second and connection.\n */\n private onWsFrame(p: WsFrameEvent, sid: string | undefined, dir: 'send' | 'recv'): void {\n const conn = this.conn(p.requestId);\n if (!conn) return; // opened before deep mode started: DevTools skips these too\n const opcode = typeof p.response?.opcode === 'number' ? p.response.opcode : 0;\n const payload = typeof p.response?.payloadData === 'string' ? p.response.payloadData : '';\n // Text messages arrive as text, all others base64-encoded.\n const text = opcode === 1;\n const size = text ? utf8Length(payload) : base64Size(payload);\n const c = conn.counts;\n if (dir === 'send') {\n c.sent++;\n c.sentBytes += size;\n } else {\n c.received++;\n c.receivedBytes += size;\n }\n if (!conn.limiter.admit(typeof p.timestamp === 'number' ? p.timestamp * 1000 : Date.now())) {\n conn.dropped++;\n return;\n }\n const rec: WsFrameData = { conn: conn.id, dir, opcode, size };\n if (text) {\n const cut = cutUtf8(maskPasswordsInText(payload), WS_LIMITS.messageBytes);\n if (conn.limiter.fits(cut.bytes)) {\n rec.data = cut.text;\n if (cut.truncated) rec.truncated = true;\n } else {\n rec.dataOmitted = true;\n conn.dataOmitted++;\n }\n }\n this.emitFrame(conn, rec, sid, p.timestamp);\n }\n\n private onWsError(p: WsFrameEvent, sid: string | undefined): void {\n const conn = this.conn(p.requestId);\n if (!conn) return;\n const error = typeof p.errorMessage === 'string' && p.errorMessage ? p.errorMessage : 'WebSocket error';\n // A failed handshake shows on the connection too.\n conn.error ??= error;\n this.emitFrame(conn, { conn: conn.id, dir: 'recv', opcode: 0, size: 0, error }, sid, p.timestamp);\n }\n\n private onWsClosed(p: WsFrameEvent, sid: string | undefined): void {\n const conn = this.conn(p.requestId);\n if (!conn) return;\n conn.closedAt = this.epochMs(sid, p.timestamp);\n this.emitConn(conn);\n }\n\n private emitFrame(conn: WsConn, rec: WsFrameData, sid: string | undefined, timestamp: unknown): void {\n const dropped = conn.limiter.takeDropped();\n if (dropped) rec.dropped = dropped;\n this.emit({ ts: this.epochMs(sid, timestamp), type: 'ws', data: rec });\n }\n\n /** The connection's record: when it closed, or with pending set when capture stops first. */\n private emitConn(conn: WsConn): void {\n if (conn.emitted) return;\n conn.emitted = true;\n const e = conn.entry;\n // The handshake's raw headers (with cookies) come as ExtraInfo under the socket's id.\n const reqExtra = e.reqExtras[0];\n const resExtra = e.resExtras[0];\n const res = conn.response;\n const ts = conn.ts ?? conn.seenAt;\n const d: NetworkData = { id: conn.id, kind: 'websocket', method: 'GET', url: conn.url };\n const status = res?.status || resExtra?.status;\n if (status) d.status = status;\n const statusText = res?.status ? res.statusText : resExtra?.statusText;\n if (statusText) d.statusText = statusText;\n d.duration = round(Math.max(0, (conn.closedAt ?? Date.now()) - ts));\n if (res && conn.t0 !== undefined && typeof conn.responseTime === 'number') d.ttfb = round(Math.max(0, (conn.responseTime - conn.t0) * 1000));\n const reqHeaders = headerMap(reqExtra?.headers ?? res?.requestHeaders ?? conn.handshakeHeaders);\n if (reqHeaders) d.reqHeaders = reqHeaders;\n const resHeaders = headerMap(resExtra?.headers ?? res?.headers);\n if (resHeaders) d.resHeaders = resHeaders;\n if (conn.error) d.error = conn.error;\n if (conn.closedAt === undefined) d.pending = true;\n d.source = 'cdp';\n d.resourceType = 'websocket';\n if (reqExtra?.blockedCookies) d.blockedCookies = reqExtra.blockedCookies;\n if (resExtra?.blockedSetCookies) d.blockedSetCookies = resExtra.blockedSetCookies;\n const worker = this.workerOf(e.sessionId);\n if (worker) d.worker = worker;\n if (conn.closedAt !== undefined) d.closedAt = conn.closedAt;\n const frames: WsFrameCounts = { ...conn.counts };\n if (conn.dropped) frames.dropped = conn.dropped;\n if (conn.dataOmitted) frames.dataOmitted = conn.dataOmitted;\n d.frames = frames;\n this.emit({ ts, type: 'network', data: d });\n if (!e.hops.length && this.entries.get(e.requestId) === e) this.entries.delete(e.requestId);\n }\n\n private conn(requestId: string): WsConn | undefined {\n const c = this.entries.get(requestId)?.ws;\n return c && !c.emitted ? c : undefined;\n }\n\n // ---- bookkeeping ----\n\n /** In-flight requests become pending records, waiting ones are written as they are. */\n private flushEntry(e: Entry): void {\n for (const hop of e.hops) {\n if (hop.state === 'open') this.build(hop, true);\n else if (hop.state === 'waiting') this.build(hop, false);\n }\n if (e.ws) this.emitConn(e.ws);\n }\n\n /** The session whose buffer has the response body: the one that reported the response. */\n private bodySession(hop: Hop): string | undefined {\n return hop.dataSession !== undefined ? hop.dataSession || undefined : hop.entry.sessionId;\n }\n\n private openHop(requestId: string): Hop | undefined {\n const hop = this.entries.get(requestId)?.hops.at(-1);\n return hop?.state === 'open' ? hop : undefined;\n }\n\n private entryFor(requestId: string): Entry {\n return this.entries.get(requestId) ?? this.newEntry(requestId);\n }\n\n private newEntry(requestId: string): Entry {\n if (this.entries.size >= MAX_ENTRIES) this.makeRoom();\n const e: Entry = { requestId, hops: [], reqExtras: [], resExtras: [], seenAt: Date.now() };\n this.entries.set(requestId, e);\n return e;\n }\n\n /**\n * Too much at once: ExtraInfo nobody claimed within a second goes first;\n * if that is not enough (a page that never finishes its requests), the\n * oldest request is written as pending.\n */\n private makeRoom(): void {\n const now = Date.now();\n for (const [key, e] of this.entries) if (!e.hops.length && !e.ws && now - e.seenAt > 1000) this.entries.delete(key);\n if (this.entries.size < MAX_ENTRIES) return;\n for (const e of this.entries.values()) {\n if (e.ws) continue;\n this.entries.delete(e.requestId);\n this.flushEntry(e);\n return;\n }\n }\n\n /** Drops ExtraInfo nobody claimed (at most once a second). */\n private expire(): void {\n const now = Date.now();\n if (now - this.lastExpiry < 1000) return;\n this.lastExpiry = now;\n for (const [key, e] of this.entries) {\n if (!e.hops.length && !e.ws && now - e.seenAt > ORPHAN_TTL_MS) this.entries.delete(key);\n }\n }\n\n private workerOf(sessionId: string | undefined): WorkerInfo | undefined {\n const w = sessionId !== undefined ? this.workers.get(sessionId) : undefined;\n return w ? { ...w } : undefined;\n }\n\n private learnClock(sid: string | undefined, timestamp: unknown, wallTime: unknown): void {\n if (typeof timestamp === 'number' && typeof wallTime === 'number' && wallTime > 0) this.clocks.set(sid ?? '', wallTime - timestamp);\n }\n\n /** Epoch ms of a monotonic CDP timestamp (WebSocket messages have no wallTime). */\n private epochMs(sid: string | undefined, timestamp: unknown): number {\n const offset = this.clocks.get(sid ?? '') ?? this.clocks.get('');\n return typeof timestamp === 'number' && offset !== undefined ? Math.round((timestamp + offset) * 1000) : Date.now();\n }\n\n private emit(record: CdpRecord): void {\n try {\n this.opts.emit(record);\n } catch {\n // the batch is not ours to break\n }\n }\n}\n\n// ---- helpers ----\n\nfunction setSession(e: Entry, sessionId: string | undefined): void {\n if (sessionId === undefined) delete e.sessionId;\n else e.sessionId = sessionId;\n}\n\n/** Lowercased names like the in-page capture; URL-valued headers masked like URLs. Cookie values stay (the server redacts exports). */\nfunction headerMap(h: CdpHeaders | undefined): Record<string, string> | undefined {\n if (!h || typeof h !== 'object') return undefined;\n const out: Record<string, string> = {};\n for (const [name, raw] of Object.entries(h)) {\n const k = name.toLowerCase();\n const v = URL_HEADERS.has(k) ? redactUrl(String(raw)) : String(raw);\n out[k] = Object.hasOwn(out, k) ? `${out[k]}\\n${v}` : v;\n }\n return Object.keys(out).length ? out : undefined;\n}\n\nfunction remoteAddress(res: CdpResponse | undefined): string | undefined {\n if (!res?.remoteIPAddress) return undefined;\n const ip = res.remoteIPAddress;\n const host = ip.includes(':') && !ip.startsWith('[') ? `[${ip}]` : ip;\n return res.remotePort ? `${host}:${res.remotePort}` : host;\n}\n\nfunction reasonsOf(v: unknown): string[] {\n return Array.isArray(v) ? v.filter((r): r is string => typeof r === 'string') : [];\n}\n\nfunction cookieOf(name: string, reasons: string[], cookie: CdpCookie | undefined): BlockedCookie {\n const out: BlockedCookie = { name, reasons };\n if (cookie?.domain) out.domain = cookie.domain;\n if (cookie?.path) out.path = cookie.path;\n return out;\n}\n\n/** Request cookies Chrome did not send (NET-R7); never their values. */\nfunction blockedRequestCookies(list: RequestWillBeSentExtraInfo['associatedCookies']): BlockedCookie[] | undefined {\n if (!Array.isArray(list)) return undefined;\n const out: BlockedCookie[] = [];\n for (const c of list) {\n const reasons = reasonsOf(c?.blockedReasons);\n if (!reasons.length || reasons.some((r) => NOT_FOR_THIS_URL.has(r)) || typeof c.cookie?.name !== 'string') continue;\n out.push(cookieOf(c.cookie.name, reasons, c.cookie));\n }\n return out.length ? out : undefined;\n}\n\n/** Set-Cookie lines Chrome rejected (NET-R7); the name only, never the line. */\nfunction blockedSetCookies(list: ResponseReceivedExtraInfo['blockedCookies']): BlockedCookie[] | undefined {\n if (!Array.isArray(list)) return undefined;\n const out: BlockedCookie[] = [];\n for (const c of list) {\n const reasons = reasonsOf(c?.blockedReasons);\n if (!reasons.length) continue;\n out.push(cookieOf(typeof c.cookie?.name === 'string' ? c.cookie.name : cookieName(c.cookieLine), reasons, c.cookie));\n }\n return out.length ? out : undefined;\n}\n\n/** The name of a Set-Cookie line; \"\" for a line without name=value, whose text is the value. */\nfunction cookieName(line: unknown): string {\n if (typeof line !== 'string') return '';\n const eq = line.indexOf('=');\n const semi = line.indexOf(';');\n return eq < 0 || (semi >= 0 && semi < eq) ? '' : line.slice(0, eq).trim();\n}\n\n/** The reason phrase of an HTTP/1.x status line (\"Not Modified\"); HTTP/2 has none. */\nfunction reasonPhrase(headersText: unknown): string | undefined {\n if (typeof headersText !== 'string') return undefined;\n return /^HTTP\\/\\S+[ \\t]+\\d{3}[ \\t]+([^\\r\\n]+)/.exec(headersText)?.[1]?.trim() || undefined;\n}\n\nfunction isBinaryType(contentType: string): boolean {\n return !TEXT_MIME.test(contentType) && !MULTIPART.test(contentType);\n}\n\n/** postDataEntries carry the body base64-encoded; files have no bytes (then the body is fetched). */\nfunction entriesBytes(entries: CdpRequest['postDataEntries']): Uint8Array | undefined {\n if (!entries?.length || !entries.every((x) => typeof x?.bytes === 'string')) return undefined;\n const parts = entries.map((x) => base64ToBytes(x.bytes as string));\n const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));\n let at = 0;\n for (const p of parts) {\n out.set(p, at);\n at += p.length;\n }\n return out;\n}\n\nfunction withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const late = new Promise<never>((_, reject) => (timer = setTimeout(() => reject(new Error('Chrome did not answer')), ms)));\n return Promise.race([p, late]).finally(() => clearTimeout(timer));\n}\n\nfunction clipText(text: string, max: number): { text: string; truncated: boolean } {\n return text.length > max ? { text: text.slice(0, max), truncated: true } : { text, truncated: false };\n}\n\n/** chrome.debugger rejects with the protocol error as JSON: {\"code\":-32001,\"message\":\"Session with given id not found.\"}. */\nfunction errorText(err: unknown): string {\n const text = err instanceof Error ? err.message : String(err);\n if (!text.startsWith('{')) return text;\n try {\n const message = (JSON.parse(text) as { message?: unknown }).message;\n return typeof message === 'string' && message ? message : text;\n } catch {\n return text;\n }\n}\n", "// Deep network mode: records the recorded tab's network through\n// chrome.debugger instead of in the page. This module owns the debugger\n// session: attach, Network.enable, auto-attach to workers (flat child\n// sessions, Chrome 125+), event routing and detach. network.ts turns the\n// events into records; they are batched here and handed to the upload queue\n// like the page's batches.\n//\n// An open debugger session keeps the service worker alive (Chrome 118+), so\n// the state below lives as long as the session does. Chrome shows its\n// \"started debugging this browser\" bar while we are attached.\n\nimport { CdpNetwork, type CdpRecord, type WorkerInfo } from './network';\n\nconst PROTOCOL_VERSION = '1.3';\n/**\n * Bodies are fetched right after their request finishes, so the buffers\n * only hold them briefly. Short request bodies come with requestWillBeSent.\n */\nconst NETWORK_ENABLE = { maxTotalBufferSize: 64_000_000, maxResourceBufferSize: 16_000_000, maxPostDataSize: 65_536 };\n/**\n * New workers and iframes wait for us, so Network.enable reaches a worker's\n * session before its first request. Every waiting target is resumed,\n * whatever happens: one left waiting would hang the page's worker or iframe.\n */\nconst AUTO_ATTACH = { autoAttach: true, waitForDebuggerOnStart: true, flatten: true };\nconst WORKER_TYPES = new Set(['worker', 'shared_worker', 'service_worker']);\n/** A worker is resumed even if Network.enable has not answered by then. */\nconst WORKER_ENABLE_TIMEOUT_MS = 1000;\n/** How long stopping waits for bodies still being fetched. */\nconst STOP_TIMEOUT_MS = 1500;\n/** Lines are posted every flushMs(), or right away above this size. */\nconst FLUSH_BYTES = 1_000_000;\n\nexport interface DeepOptions {\n tabId: number;\n captureBodies: boolean;\n maxBodyChars: number;\n /** Appends NDJSON lines to the recording. */\n post(lines: string): void;\n /** The batch interval now: shorter while the recording is watched live. */\n flushMs(): number;\n /** Requests not to record (made by tabtape's own pages). */\n ignore?(request: { url: string; documentURL?: string }): boolean;\n}\n\n/** The debugger detached by itself: canceled_by_user, target_closed. */\nexport type DetachListener = (tabId: number, reason: string) => Promise<void> | void;\n\nclass DeepSession {\n readonly net: CdpNetwork;\n /** We are detaching (stop, simulateDetach): Chrome's onDetach is not an early end. */\n ending = false;\n /** The debugger is gone: commands fail right away. */\n detached = false;\n private lines: string[] = [];\n private bytes = 0;\n private timer: ReturnType<typeof setTimeout> | undefined;\n private disposed = false;\n\n constructor(readonly opts: DeepOptions) {\n this.net = new CdpNetwork({\n captureBodies: opts.captureBodies,\n maxBodyChars: opts.maxBodyChars,\n emit: (record) => this.push(record),\n getResponseBody: (sessionId, requestId) => this.send<{ body: string; base64Encoded: boolean }>('Network.getResponseBody', { requestId }, sessionId),\n getRequestPostData: (sessionId, requestId) =>\n this.send<{ postData: string; base64Encoded?: boolean }>('Network.getRequestPostData', { requestId }, sessionId),\n ignore: opts.ignore,\n });\n }\n\n get tabId(): number {\n return this.opts.tabId;\n }\n\n /** A CDP command to the tab, or to a child session (worker). Never throws synchronously. */\n send<T = unknown>(method: string, params?: Record<string, unknown>, sessionId?: string): Promise<T> {\n if (this.detached) return Promise.reject(new Error('deep network mode stopped'));\n try {\n const target: chrome.debugger.DebuggerSession = sessionId ? { tabId: this.tabId, sessionId } : { tabId: this.tabId };\n const sent = params ? chrome.debugger.sendCommand(target, method, params) : chrome.debugger.sendCommand(target, method);\n return sent as Promise<T>;\n } catch (err) {\n return Promise.reject(err);\n }\n }\n\n onEvent(method: string, params: unknown, sessionId: string | undefined): void {\n if (method === 'Target.attachedToTarget') {\n this.onAttached(params);\n } else if (method.startsWith('Network.')) {\n this.net.handle(method, params, sessionId);\n } else if (method === 'Target.detachedFromTarget') {\n const p = params as { sessionId?: unknown; targetId?: unknown } | undefined;\n if (typeof p?.sessionId === 'string') this.net.endSession(p.sessionId);\n if (typeof p?.targetId === 'string') this.net.iframeDetached(p.targetId);\n } else if (method === 'Inspector.targetCrashed' && sessionId === undefined) {\n // The page's requests in flight will never finish.\n this.net.endSession(undefined, 'the page crashed');\n }\n }\n\n /**\n * A worker or iframe was auto-attached (its child session id is in the\n * params; the event's own session is the parent's). Workers get\n * Network.enable before they run (NET-R9); every waiting target is resumed.\n */\n private onAttached(params: unknown): void {\n const p = (params ?? {}) as { sessionId?: unknown; targetInfo?: { targetId?: unknown; type?: unknown; url?: unknown }; waitingForDebugger?: unknown };\n if (typeof p.sessionId !== 'string') return;\n const child = p.sessionId;\n const resume = () => {\n if (p.waitingForDebugger === true) void this.send('Runtime.runIfWaitingForDebugger', undefined, child).catch(() => {});\n };\n const type = typeof p.targetInfo?.type === 'string' ? p.targetInfo.type : '';\n if (!WORKER_TYPES.has(type) || this.ending) {\n resume();\n // A cross-site iframe's id is its frame's: its document request finishes over there.\n if (type === 'iframe' && typeof p.targetInfo?.targetId === 'string') this.net.iframeAttached(p.targetInfo.targetId);\n return;\n }\n this.net.addWorker(child, { type: type as WorkerInfo['type'], url: typeof p.targetInfo?.url === 'string' ? p.targetInfo.url : '' });\n let timer: ReturnType<typeof setTimeout> | undefined;\n void Promise.race([this.send('Network.enable', NETWORK_ENABLE, child), new Promise((resolve) => (timer = setTimeout(resolve, WORKER_ENABLE_TIMEOUT_MS)))])\n .catch(() => {})\n .finally(() => {\n clearTimeout(timer);\n resume();\n });\n }\n\n private push(record: CdpRecord): void {\n if (this.disposed) return;\n let line: string;\n try {\n line = JSON.stringify(record);\n } catch {\n return;\n }\n this.lines.push(line);\n this.bytes += line.length;\n if (this.bytes >= FLUSH_BYTES) this.flush();\n else this.timer ??= setTimeout(() => this.flush(), this.opts.flushMs());\n }\n\n flush(): void {\n clearTimeout(this.timer);\n this.timer = undefined;\n if (!this.lines.length) return;\n const lines = this.lines.join('\\n');\n this.lines = [];\n this.bytes = 0;\n try {\n this.opts.post(lines);\n } catch (err) {\n console.warn('tabtape: deep mode lines dropped', err);\n }\n }\n\n /** Stops batching; unsent lines are posted, or dropped with `discard`. */\n dispose(discard = false): void {\n if (!discard) this.flush();\n clearTimeout(this.timer);\n this.timer = undefined;\n this.lines = [];\n this.disposed = true;\n }\n}\n\n/** The session recording a tab. */\nlet current: DeepSession | null = null;\n/** Sessions still attached, by tab; one being stopped still resumes the targets that attach meanwhile. */\nconst attached = new Map<number, DeepSession>();\nlet detachListener: DetachListener = () => {};\n\nfunction forget(s: DeepSession): void {\n if (attached.get(s.tabId) === s) attached.delete(s.tabId);\n}\n\n/** Registers the chrome.debugger listeners: call once, synchronously when the service worker starts. */\nexport function initDeep(onDetached: DetachListener): void {\n detachListener = onDetached;\n chrome.debugger.onEvent.addListener((source, method, params) => {\n const s = source.tabId !== undefined ? attached.get(source.tabId) : undefined;\n if (!s) return;\n try {\n s.onEvent(method, params, source.sessionId);\n } catch (err) {\n console.warn('tabtape: deep mode event', method, err);\n }\n });\n chrome.debugger.onDetach.addListener((source, reason) => {\n const s = source.tabId !== undefined ? attached.get(source.tabId) : undefined;\n if (!s) return;\n forget(s);\n s.detached = true;\n if (s.ending || s !== current) return;\n void Promise.resolve()\n .then(() => detachListener(s.tabId, reason))\n .catch((err: unknown) => console.warn('tabtape: deep mode end', err));\n });\n}\n\n/** The tab deep mode is recording, if any. */\nexport function deepTabId(): number | undefined {\n return current?.tabId;\n}\n\n/**\n * Starts recording a tab's network: attach (with `resume` the debugger is\n * attached already: the service worker restarted), find the main frame,\n * Network.enable, auto-attach to workers. Throws if the debugger cannot be\n * attached or Network cannot be enabled; it is then detached again.\n */\nexport async function startDeep(o: DeepOptions, resume = false): Promise<void> {\n await stopDeep();\n const target = { tabId: o.tabId };\n if (!resume) await chrome.debugger.attach(target, PROTOCOL_VERSION);\n const s = new DeepSession(o);\n current = s;\n attached.set(o.tabId, s);\n try {\n // Before Network.enable, so the first document request is classified.\n s.net.setMainFrame(await mainFrameId(s));\n await s.send('Network.enable', NETWORK_ENABLE);\n } catch (err) {\n if (current === s) current = null;\n s.ending = true;\n void s.net.stop(0);\n s.dispose(true);\n await chrome.debugger.detach(target).catch(() => {});\n forget(s);\n s.detached = true;\n throw err;\n }\n // Workers (NET-R9) are a bonus: the page's own requests are recorded without them.\n await s.send('Target.setAutoAttach', AUTO_ATTACH).catch((err: unknown) => console.warn('tabtape: deep mode cannot follow workers:', err));\n}\n\n/** The main frame's id: Page.getFrameTree, or the page target's id, which is the same. */\nasync function mainFrameId(s: DeepSession): Promise<string | undefined> {\n try {\n const r = await s.send<{ frameTree?: { frame?: { id?: string } } }>('Page.getFrameTree');\n if (r?.frameTree?.frame?.id) return r.frameTree.frame.id;\n } catch {\n // try the target list\n }\n try {\n return (await chrome.debugger.getTargets()).find((t) => t.tabId === s.tabId && t.type === 'page')?.id;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Stops deep capture (the recording stops, NET-R17): requests in flight\n * become pending lines, bodies being fetched get a moment, the debugger\n * detaches (Chrome's bar goes away) and the last lines are posted.\n */\nexport async function stopDeep(): Promise<void> {\n const s = current;\n if (!s) return;\n current = null;\n s.ending = true;\n await s.net.stop(STOP_TIMEOUT_MS);\n await chrome.debugger.detach({ tabId: s.tabId }).catch(() => {});\n forget(s);\n s.detached = true;\n s.dispose();\n}\n\n/** The debugger detached by itself (NET-R14): what was in flight becomes pending lines, posted. */\nexport async function endDeep(tabId: number): Promise<void> {\n const s = current;\n if (!s || s.tabId !== tabId) return;\n current = null;\n s.detached = true;\n forget(s);\n await s.net.stop(STOP_TIMEOUT_MS);\n s.dispose();\n}\n\n/** Detaches without writing anything (the recording could not start). */\nexport async function discardDeep(): Promise<void> {\n const s = current;\n if (!s) return;\n current = null;\n s.ending = true;\n void s.net.stop(0);\n s.dispose(true);\n await chrome.debugger.detach({ tabId: s.tabId }).catch(() => {});\n forget(s);\n s.detached = true;\n}\n\n/** Test hook: really detaches, then runs the onDetach handling with `reason`. */\nexport async function simulateDetach(reason: string): Promise<boolean> {\n const s = current;\n if (!s) return false;\n s.ending = true; // Chrome's onDetach for this detach, if it sends one, must not end deep mode twice\n await chrome.debugger.detach({ tabId: s.tabId }).catch(() => {});\n forget(s);\n s.detached = true;\n await detachListener(s.tabId, reason);\n return true;\n}\n", "// Chrome match patterns (https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns),\n// enough of them to decide which tabs buffer mode records:\n// <all_urls>, *://*.example.com/*, http://localhost/*, http://localhost:3000/app/*\n// A pattern without a port matches every port.\n\nconst PATTERN = /^(\\*|https?|file):\\/\\/(\\*|\\*\\.[^/*:]+|[^/*:]+)?(?::(\\d+|\\*))?(\\/.*)$/;\n\nexport function validPattern(p: string): boolean {\n return p === '<all_urls>' || PATTERN.test(p.trim());\n}\n\nexport function matches(pattern: string, url: string): boolean {\n let u: URL;\n try {\n u = new URL(url);\n } catch {\n return false;\n }\n const scheme = u.protocol.slice(0, -1);\n if (pattern === '<all_urls>') return scheme === 'http' || scheme === 'https' || scheme === 'file';\n const m = PATTERN.exec(pattern.trim());\n if (!m) return false;\n const [, ps, host = '', port, path] = m;\n if (ps === '*' ? scheme !== 'http' && scheme !== 'https' : ps !== scheme) return false;\n if (host && host !== '*') {\n if (host.startsWith('*.')) {\n const base = host.slice(2).toLowerCase();\n if (u.hostname !== base && !u.hostname.endsWith(`.${base}`)) return false;\n } else if (u.hostname !== host.toLowerCase()) {\n return false;\n }\n }\n if (port && port !== '*' && (u.port || (scheme === 'https' ? '443' : '80')) !== port) return false;\n return glob(path, u.pathname + u.search);\n}\n\nexport function matchesAny(patterns: string[], url: string): boolean {\n return patterns.some((p) => matches(p, url));\n}\n\n/** Whether a tab showing `url` can be recorded: web and file pages, but not the Web Store. */\nexport function isRecordable(url: string | undefined): boolean {\n if (!url || !/^(https?|file):/.test(url)) return false;\n return !/^https:\\/\\/(chromewebstore\\.google\\.com|chrome\\.google\\.com\\/webstore)/.test(url);\n}\n\nconst LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]']);\n\n/**\n * Whether `url` is one of tabtape's own pages (the replay UI, the live view,\n * record pages) on the server at `serverUrl`: same origin (localhost,\n * 127.0.0.1 and [::1] count as the same host) and at or below the server's\n * path. The path matters: the Repoburg daemon serves tabtape under /tabtape\n * and other pages on the same origin (the agent pane), which stay\n * recordable. A server URL without a path covers its whole origin. Needed\n * because match patterns ignore ports: http://127.0.0.1/* also matches the\n * server.\n */\nexport function isServerPage(url: string | undefined, serverUrl: string | undefined): boolean {\n if (!url || !serverUrl) return false;\n let u: URL;\n let s: URL;\n try {\n u = new URL(url);\n s = new URL(serverUrl);\n } catch {\n return false;\n }\n if (u.protocol !== s.protocol) return false;\n const port = (x: URL) => x.port || (x.protocol === 'https:' ? '443' : '80');\n const host = (x: URL) => (LOOPBACK.has(x.hostname) ? 'loopback' : x.hostname);\n if (port(u) !== port(s) || host(u) !== host(s)) return false;\n // Express routes ignore case: /TabTape/ is tabtape's too.\n const base = serverPath(s).toLowerCase();\n const path = u.pathname.toLowerCase();\n return !base || path === base || path.startsWith(`${base}/`);\n}\n\n/** The path the server is mounted at, without a trailing slash: \"/tabtape\", or \"\" at the root of its origin. */\nexport function serverPath(serverUrl: URL): string {\n return serverUrl.pathname.replace(/\\/+$/, '');\n}\n\n/**\n * A server URL as settings keep it: http or https, an optional path (the\n * mount, /tabtape in the Repoburg daemon), no trailing slash, no query or\n * fragment. Null when `input` is not one.\n */\nexport function normalizeServerUrl(input: string): string | null {\n let u: URL;\n try {\n u = new URL(input.trim());\n } catch {\n return null;\n }\n if ((u.protocol !== 'http:' && u.protocol !== 'https:') || u.search || u.hash || u.username || u.password) return null;\n return `${u.origin}${serverPath(u)}`;\n}\n\nfunction glob(pattern: string, text: string): boolean {\n const re = new RegExp(`^${pattern.split('*').map(escape).join('.*')}$`);\n return re.test(text);\n}\n\nfunction escape(s: string): string {\n return s.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n", "// Record links (served by tabtape in the Repoburg daemon): a browser that\n// can only be told to open URLs, such as one driven by Playwright MCP,\n// starts and stops recordings by opening them. <server> is the server URL\n// with its path, http://127.0.0.1:9998/tabtape by default.\n// <server>/record?token=\u2026[&name=\u2026][&url=\u2026] record this tab, then open url\n// <server>/record/stop?token=\u2026 stop the recording\n\nimport { isServerPage, serverPath } from './match';\n\nexport type RecordLink = { action: 'start'; token: string; name?: string; url?: string } | { action: 'stop'; token: string };\n\nconst NAME_MAX = 200;\n\n/** The record link `url` is, for the server at `serverUrl`; null for any other URL. */\nexport function parseRecordLink(url: string | undefined, serverUrl: string | undefined): RecordLink | null {\n if (!url || !serverUrl || !isServerPage(url, serverUrl)) return null;\n const u = new URL(url);\n const token = u.searchParams.get('token');\n if (!token) return null;\n const record = `${serverPath(new URL(serverUrl))}/record`;\n if (u.pathname === `${record}/stop`) return { action: 'stop', token };\n if (u.pathname !== record) return null;\n return { action: 'start', token, name: recordingName(u.searchParams.get('name')), url: u.searchParams.get('url')?.trim() || undefined };\n}\n\n/** A recording's title as given by automation: trimmed, at most 200 characters, undefined when empty. */\nexport function recordingName(name: unknown): string | undefined {\n return typeof name === 'string' ? name.trim().slice(0, NAME_MAX) || undefined : undefined;\n}\n", "// Messages between the extension's parts.\n//\n// recorder.js (page MAIN world) --DOM CustomEvents--> bridge.js (isolated world)\n// bridge.js --chrome.runtime messages--> background.js --HTTP--> tabtape (Repoburg daemon, /tabtape)\n\n/** DOM event names used between the recorder (MAIN world) and the bridge. */\nexport const DOM_EVENT = {\n /** recorder -> bridge, detail: one NDJSON line */\n data: '__tabtape:data',\n /** recorder -> bridge: send what you have now (page is going away) */\n flush: '__tabtape:flush',\n /** recorder -> bridge: I am loaded, tell me what to do */\n hello: '__tabtape:hello',\n /** bridge -> recorder, detail: JSON ControlMessage */\n control: '__tabtape:control',\n /** bridge -> recorder: report PageInfo now (answered synchronously with infoReply) */\n info: '__tabtape:info',\n /** recorder -> bridge, detail: JSON PageInfo */\n infoReply: '__tabtape:info-reply',\n} as const;\n\n/** The state hook bundle (state.js) publishes its API on window under this symbol. */\nexport const STATE_HOOK_KEY = 'tabtape.state';\n\nexport interface RecorderOptions {\n captureBodies: boolean;\n maxBodyChars: number;\n /** Periodic full snapshots keep replays usable after ring buffer eviction. */\n checkoutEveryNms: number;\n /**\n * In-page network capture (fetch, XHR, beacons, Resource and Navigation\n * Timing). Off while deep mode records the tab's network through the\n * debugger, so no request is written twice.\n */\n inPageNetwork: boolean;\n /** Redux / Zustand state recording; needs the state hook (state.js) on the page. */\n recordState: boolean;\n}\n\n/** What the recorder knows about its page; the popup asks for it (ST-R13). */\nexport interface PageInfo {\n /**\n * early: the Redux DevTools hook was defined before the page's scripts ran;\n * late: injected into a page that was already open (stores created at load\n * missed it); off: state recording is off (no state.js on the page).\n */\n stateHook: 'early' | 'late' | 'off';\n /** Names of the stores connected to the hook. */\n stores: string[];\n}\n\n/** Where a tab's lines go: a recording, or the tab's buffer (buffer mode). */\nexport type Target = { kind: 'session'; id: string } | { kind: 'buffer'; key: string; windowMs: number };\n\nexport type ControlMessage =\n | { cmd: 'start'; sessionId: string; options: RecorderOptions }\n | { cmd: 'stop' }\n | { cmd: 'disable' };\n\n/** How often the bridge sends its lines; faster while someone watches the recording live. */\nexport const FLUSH_MS = { normal: 1000, live: 250 };\n\nexport type Decision =\n | {\n record: true;\n target: Target;\n options: RecorderOptions;\n /** Bridge flush interval, when not FLUSH_MS.normal. */\n flushMs?: number;\n }\n | { record: false };\n\nexport type RuntimeMessage =\n | { type: 'hello' }\n | { type: 'batch'; target: Target; lines: string }\n | { type: 'status'; tabId?: number }\n /** deep: record this recording's network through the debugger (deep mode, NET-R1) */\n | { type: 'start'; tabId: number; deep?: boolean }\n | { type: 'stop' }\n | { type: 'save-buffer'; tabId: number }\n | { type: 'settings-changed' };\n\n/** background -> bridge */\nexport type BridgeMessage =\n /** stop recording; the response carries the lines not sent yet */\n | { type: 'stop' }\n /** hand over the lines not sent yet without stopping */\n | { type: 'take' }\n /** live viewers came or went: send batches every `ms` */\n | { type: 'flush-interval'; ms: number }\n /** the recording's options changed (deep mode ended): ask for a new decision */\n | { type: 'redecide' }\n /** popup -> bridge: the response is the recorder's PageInfo, or null without a recorder */\n | { type: 'page-info' };\n\nexport interface Settings {\n /** tabtape's server with its path, no trailing slash: URLs are built as `${serverUrl}/api/\u2026`. */\n serverUrl: string;\n captureBodies: boolean;\n checkoutMinutes: number;\n /** Buffer mode: keep the last minutes of allow-listed tabs on the server. */\n bufferEnabled: boolean;\n /** Chrome match patterns, e.g. http://localhost/* */\n bufferSites: string[];\n bufferMinutes: number;\n /** Recordings started on a buffered tab begin with the buffered minutes. */\n startWithBuffer: boolean;\n /** Record Redux / Zustand stores through the Redux DevTools hook (ST-R1). */\n recordState: boolean;\n}\n\nexport const DEFAULT_SETTINGS: Settings = {\n // tabtape runs inside the Repoburg daemon, mounted under /tabtape.\n serverUrl: 'http://127.0.0.1:9998/tabtape',\n captureBodies: true,\n checkoutMinutes: 5,\n bufferEnabled: false,\n bufferSites: ['http://localhost/*', 'http://127.0.0.1/*'],\n bufferMinutes: 2,\n startWithBuffer: true,\n recordState: true,\n};\n\n/** The default server URL of tabtape's former standalone server. */\nconst STANDALONE_SERVER_URL = 'http://127.0.0.1:7777';\n\n/**\n * Stored settings over the defaults. The server URL loses trailing slashes;\n * one saved with the standalone server's default moves to the daemon.\n */\nexport function withDefaults(stored: Partial<Settings> | undefined): Settings {\n const s: Settings = { ...DEFAULT_SETTINGS, ...stored };\n const url = typeof s.serverUrl === 'string' ? s.serverUrl.trim().replace(/\\/+$/, '') : '';\n s.serverUrl = !url || url === STANDALONE_SERVER_URL ? DEFAULT_SETTINGS.serverUrl : url;\n return s;\n}\n\nexport const BUFFER_MINUTES = { min: 1, max: 10 };\n\n/** A checkout every quarter window (15 s \u2013 1 min) keeps every save replayable. */\nexport function bufferCheckoutMs(windowMs: number): number {\n return Math.min(Math.max(windowMs / 4, 15_000), 60_000);\n}\n\nexport const MAX_BODY_CHARS = 1_000_000;\n\nexport interface Recording {\n sessionId: string;\n tabId: number;\n startedAt: number;\n serverUrl: string;\n title?: string;\n url?: string;\n /** Deep mode (NET-R1): set when the recording was started with it. */\n deep?: DeepStatus;\n}\n\n/**\n * on: the debugger is attached and records the tab's network;\n * ended: it detached early (the user canceled Chrome's debugging bar, the tab\n * crashed, the extension restarted) and the page captures requests again.\n */\nexport interface DeepStatus {\n state: 'on' | 'ended';\n /** Why it ended, in words for the popup. */\n reason?: string;\n}\n\nexport interface BufferTab {\n key: string;\n /** The tab currently shows an allow-listed page and is being buffered. */\n active: boolean;\n url?: string;\n}\n\nexport interface Status {\n recording: Recording | null;\n lastSession: { sessionId: string; serverUrl: string } | null;\n server: { ok: boolean; url: string; version?: string; error?: string };\n stats: { lines: number; bytes: number; queuedBytes: number; dropped: number; lastError?: string };\n buffer: { enabled: boolean; minutes: number; tab: BufferTab | null; error?: string };\n}\n\nexport type Result<T = object> = ({ ok: true } & T) | { ok: false; error: string };\n", "// Background service worker: owns the recording state, injects the recorder\n// into the recorded tab (and into every page it navigates to), and uploads\n// batches to the tabtape server (tabtape in the Repoburg daemon) in order\n// (the tab sends them faster while the recording is watched live). In buffer\n// mode it also streams allow-listed tabs to their buffers on the server.\n// tabtape's own pages are never recorded.\n\nimport { deepTabId, discardDeep, endDeep, initDeep, simulateDetach, startDeep, stopDeep, type DeepOptions } from './cdp/debugger';\nimport { parseRecordLink, recordingName, type RecordLink } from './links';\nimport { isRecordable, isServerPage, matchesAny, validPattern } from './match';\nimport {\n BUFFER_MINUTES,\n bufferCheckoutMs,\n FLUSH_MS,\n MAX_BODY_CHARS,\n withDefaults,\n type BridgeMessage,\n type BufferTab,\n type Decision,\n type Recording,\n type RuntimeMessage,\n type Settings,\n type Status,\n type Target,\n} from './protocol';\n\nconst SCRIPT_IDS = ['tabtape-bridge', 'tabtape-recorder'];\n\n/**\n * The recorded tab's scripts. With state recording on, state.js (the Redux\n * DevTools hook) runs first, before the page's own scripts (ST-R2).\n */\nfunction sessionScripts(s: Settings): chrome.scripting.RegisteredContentScript[] {\n const common = { matches: ['<all_urls>'], runAt: 'document_start' as const, allFrames: false, persistAcrossSessions: false };\n return [\n { ...common, id: SCRIPT_IDS[0], js: ['bridge.js'], world: 'ISOLATED' },\n { ...common, id: SCRIPT_IDS[1], js: pageScripts(s), world: 'MAIN' },\n ];\n}\n\n/** MAIN world scripts, in order. */\nfunction pageScripts(s: Settings): string[] {\n return s.recordState ? ['state.js', 'recorder.js'] : ['recorder.js'];\n}\nconst BUFFER_SCRIPT_IDS = ['tabtape-buffer-bridge', 'tabtape-buffer-recorder'];\nconst BADGE = { rec: '#e5484d', buf: '#71717a' };\nconst QUEUE_LIMIT_BYTES = 50_000_000;\nconst STOP_DRAIN_TIMEOUT_MS = 5000;\nconst SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;\n\n// ---------- state ----------\n\nasync function getRecording(): Promise<Recording | null> {\n const { recording } = await chrome.storage.session.get('recording');\n return (recording as Recording | undefined) ?? null;\n}\n\nasync function setRecording(rec: Recording | null): Promise<void> {\n if (rec) await chrome.storage.session.set({ recording: rec });\n else await chrome.storage.session.remove('recording');\n}\n\nasync function getSettings(): Promise<Settings> {\n const { settings } = await chrome.storage.local.get('settings');\n const s = withDefaults(settings as Partial<Settings> | undefined);\n s.bufferMinutes = Math.min(Math.max(Math.round(s.bufferMinutes) || 2, BUFFER_MINUTES.min), BUFFER_MINUTES.max);\n s.bufferSites = s.bufferSites.filter(validPattern);\n return s;\n}\n\n/** Buffer keys per tab, kept for the life of the tab. */\nasync function getBufferTabs(): Promise<Record<string, BufferTab>> {\n const { bufferTabs } = await chrome.storage.session.get('bufferTabs');\n return (bufferTabs as Record<string, BufferTab> | undefined) ?? {};\n}\n\nlet bufferTabsLock: Promise<unknown> = Promise.resolve();\n\n/** Read-modify-write of the buffer tab map, one at a time. */\nfunction updateBufferTabs<T>(fn: (tabs: Record<string, BufferTab>) => T): Promise<T> {\n const run = bufferTabsLock.then(async () => {\n const tabs = await getBufferTabs();\n const out = fn(tabs);\n await chrome.storage.session.set({ bufferTabs: tabs });\n return out;\n });\n bufferTabsLock = run.catch(() => {});\n return run;\n}\n\n// ---------- upload queue ----------\n\ninterface Job {\n url: string;\n body: string;\n contentType: string;\n lines?: number;\n /** Set on event batches: the reply tells how many live viewers the session has. */\n sessionId?: string;\n}\n\nclass HttpError extends Error {\n constructor(\n readonly status: number,\n message: string,\n ) {\n super(message);\n }\n}\n\nconst queue: Job[] = [];\nlet queuedBytes = 0;\nlet draining: Promise<void> | null = null;\nconst stats = { lines: 0, bytes: 0, dropped: 0, lastError: undefined as string | undefined };\n\nfunction enqueue(job: Job): Promise<void> {\n queue.push(job);\n queuedBytes += job.body.length;\n // If the server stays down, keep the newest data and drop the oldest.\n while (queuedBytes > QUEUE_LIMIT_BYTES && queue.length > 1) {\n const dropped = queue.shift()!;\n queuedBytes -= dropped.body.length;\n stats.dropped++;\n }\n return drain();\n}\n\nfunction drain(): Promise<void> {\n // runQueue always awaits before finishing when the queue is non-empty, so\n // `draining` is assigned before its finally block clears it.\n if (!draining && queue.length) draining = runQueue();\n return draining ?? Promise.resolve();\n}\n\nasync function runQueue(): Promise<void> {\n let backoff = 500;\n try {\n while (queue.length) {\n const job = queue[0];\n try {\n await post(job);\n stats.lastError = undefined;\n backoff = 500;\n } catch (err) {\n stats.lastError = errorMessage(err);\n const permanent = err instanceof HttpError && err.status >= 400 && err.status < 500 && err.status !== 408 && err.status !== 429;\n if (!permanent) {\n await sleep(backoff);\n backoff = Math.min(backoff * 2, 5000);\n continue;\n }\n stats.dropped++;\n }\n queue.shift();\n queuedBytes -= job.body.length;\n }\n } finally {\n // Same microtask as the empty-queue check: a job enqueued afterwards\n // starts a new run instead of waiting behind a finished one.\n draining = null;\n }\n}\n\nasync function post(job: Job): Promise<void> {\n const res = await fetch(job.url, {\n method: 'POST',\n headers: { 'Content-Type': job.contentType, 'X-Tabtape-Client': 'extension' },\n body: job.body,\n });\n if (!res.ok) throw new HttpError(res.status, `HTTP ${res.status}: ${(await res.text()).trim()}`);\n if (job.lines) {\n stats.lines += job.lines;\n stats.bytes += job.body.length;\n }\n if (job.sessionId) {\n // Never fails the job: the batch is stored, whatever the reply says.\n const reply = (await res.json().catch(() => null)) as { followers?: unknown } | null;\n onFollowers(job.sessionId, reply?.followers);\n }\n}\n\n/** The bridge flush interval last sent to the recorded tab. */\nlet flushInterval: { sessionId: string; ms: number } | null = null;\n\n/**\n * The ingest reply counts the session's live viewers. While there are any,\n * the recorded tab's bridge flushes every 250 ms instead of every second.\n */\nfunction onFollowers(sessionId: string, followers: unknown): void {\n const ms = typeof followers === 'number' && followers > 0 ? FLUSH_MS.live : FLUSH_MS.normal;\n if (flushInterval?.sessionId === sessionId && flushInterval.ms === ms) return;\n flushInterval = { sessionId, ms };\n void (async () => {\n const rec = await getRecording();\n if (rec?.sessionId !== sessionId) return;\n await chrome.tabs.sendMessage(rec.tabId, { type: 'flush-interval', ms } satisfies BridgeMessage, { frameId: 0 });\n })().catch(() => {\n // the page is navigating; its next bridge gets the interval with its decision\n });\n}\n\nlet inbox: Promise<void> = Promise.resolve();\n\nfunction onBatch(target: Target | undefined, lines: string): void {\n if (target?.kind === 'session') onSessionBatch(target.id, lines);\n else if (target?.kind === 'buffer') void postBuffer(target.key, target.windowMs, lines);\n}\n\nlet bufferError: string | undefined;\n\n/**\n * Buffer data is only useful while it is fresh: it is posted right away and\n * dropped if the server cannot take it, never queued.\n */\nasync function postBuffer(key: string, windowMs: number, lines: string): Promise<void> {\n if (!SESSION_ID.test(key) || !lines) return;\n const { serverUrl } = await getSettings();\n try {\n const res = await fetch(`${serverUrl}/api/buffers/${key}/events?window=${Math.round(windowMs)}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-ndjson', 'X-Tabtape-Client': 'extension' },\n body: lines,\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}: ${(await res.text()).trim()}`);\n bufferError = undefined;\n } catch (err) {\n bufferError = errorMessage(err);\n }\n}\n\n/** Batches are queued in arrival order, even across service worker restarts. */\nfunction onSessionBatch(sessionId: string, lines: string): void {\n if (!SESSION_ID.test(sessionId) || !lines) return;\n inbox = inbox\n .then(async () => {\n const rec = await getRecording();\n const serverUrl = rec?.sessionId === sessionId ? rec.serverUrl : (await getSettings()).serverUrl;\n void enqueue({\n url: `${serverUrl}/api/sessions/${sessionId}/events`,\n contentType: 'application/x-ndjson',\n body: lines,\n lines: countLines(lines),\n sessionId,\n });\n })\n .catch((err) => console.warn('tabtape: batch dropped', err));\n}\n\n// ---------- recording ----------\n\nlet lock: Promise<unknown> = Promise.resolve();\n\n/** Runs start/stop one at a time. */\nfunction exclusive<T>(fn: () => Promise<T>): Promise<T> {\n const run = lock.then(fn, fn);\n lock = run.catch(() => {});\n return run;\n}\n\ninterface StartOptions {\n /**\n * Record the tab's network through the debugger (deep mode, NET-R1); if\n * that cannot start, the recording starts without it and the result\n * carries a warning.\n */\n deep?: boolean;\n /** The recording's title in the list; the page title by default. */\n name?: string;\n /** \"automation\": started by the automation API or a record link. */\n source?: 'automation';\n /**\n * Record from the tab's next page on and inject nothing now: the tab\n * shows a page that is not recorded (about:blank, a record link).\n */\n nextPage?: boolean;\n}\n\nasync function startRecording(tabId: number, opts: StartOptions = {}): Promise<Recording & { warning?: string }> {\n if (await getRecording()) await stopRecording();\n const tab = await chrome.tabs.get(tabId);\n const settings = await getSettings();\n if (!opts.nextPage) {\n if (!isRecordable(tab.url)) throw new Error('Browser pages (chrome://, Web Store, \u2026) cannot be recorded.');\n if (isServerPage(tab.url, settings.serverUrl)) throw new Error('tabtape\u2019s own pages (replay, live view) are never recorded.');\n }\n const server = await health(settings.serverUrl);\n if (!server.ok) throw new Error(notRunning(settings.serverUrl));\n\n // On a buffered tab the recording can begin with the buffered minutes.\n const buffered = !opts.nextPage && settings.bufferEnabled && settings.startWithBuffer ? (await getBufferTabs())[tabId] : undefined;\n let fromBuffer: string | null = null;\n if (buffered?.active) {\n fromBuffer = await saveBufferTo(tabId, buffered.key, settings, true).catch(() => null);\n }\n // Waiting for the next page, the URL and title are not known yet: the\n // server takes them from the first page view.\n const page = opts.nextPage ? undefined : tab;\n const rec: Recording = {\n sessionId: fromBuffer ?? newSessionId(),\n tabId,\n startedAt: Date.now(),\n serverUrl: settings.serverUrl,\n title: opts.name ?? page?.title,\n url: page?.url,\n };\n if (!fromBuffer) {\n await post({\n url: `${rec.serverUrl}/api/sessions`,\n contentType: 'application/json',\n body: JSON.stringify({ id: rec.sessionId, startedAt: rec.startedAt, url: rec.url, title: rec.title, userAgent: navigator.userAgent, source: opts.source }),\n });\n }\n // Deep mode attaches before the recorder is injected: the decision the\n // recorder gets then already turns in-page network capture off.\n let warning: string | undefined;\n if (opts.deep) {\n try {\n await startDeep(deepOptions(rec, settings));\n rec.deep = { state: 'on' };\n } catch (err) {\n warning = `Deep network mode could not start: ${errorMessage(err)}`;\n }\n }\n await setRecording(rec);\n Object.assign(stats, { lines: 0, bytes: 0, dropped: 0, lastError: undefined });\n try {\n await registerScripts(settings);\n if (!opts.nextPage) await injectInto(tabId, settings);\n } catch (err) {\n if (rec.deep) await discardDeep();\n await setRecording(null);\n await unregisterScripts();\n // Don't leave an empty recording behind.\n await fetch(`${rec.serverUrl}/api/sessions/${rec.sessionId}`, { method: 'DELETE', headers: { 'X-Tabtape-Client': 'extension' } }).catch(() => {});\n throw new Error(`Could not inject the recorder: ${errorMessage(err)}`);\n }\n if (warning) writeMarker(rec.sessionId, warning);\n await updateBadge(rec);\n return warning ? { ...rec, warning } : rec;\n}\n\n/** Saves the tab's buffer as a session (left open when continuing into a recording). */\nasync function saveBufferTo(tabId: number, key: string, settings: Settings, keepOpen: boolean): Promise<string> {\n // Lines the bridge has not sent yet belong in the saved session too.\n try {\n const res = (await chrome.tabs.sendMessage(tabId, { type: 'take' } satisfies BridgeMessage, { frameId: 0 })) as { lines?: string } | undefined;\n if (res?.lines) await postBuffer(key, settings.bufferMinutes * 60_000, res.lines);\n } catch {\n // the page has no bridge (yet); save what the server has\n }\n const res = await fetch(`${settings.serverUrl}/api/buffers/${key}/save`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Tabtape-Client': 'extension' },\n body: JSON.stringify({ minutes: settings.bufferMinutes, continue: keepOpen }),\n });\n if (!res.ok) throw new Error(res.status === 404 ? 'Nothing buffered yet for this tab.' : `HTTP ${res.status}: ${(await res.text()).trim()}`);\n const session = (await res.json()) as { id: string };\n return session.id;\n}\n\nasync function saveBuffer(tabId: number): Promise<{ sessionId: string; serverUrl: string }> {\n const settings = await getSettings();\n const tab = (await getBufferTabs())[tabId];\n if (!settings.bufferEnabled || !tab) throw new Error('This tab is not in buffer mode. Add the site to the buffer list in Settings.');\n const sessionId = await saveBufferTo(tabId, tab.key, settings, false);\n const lastSession = { sessionId, serverUrl: settings.serverUrl };\n await chrome.storage.session.set({ lastSession, lastBufferSave: { ...lastSession, tabId, at: Date.now() } });\n return lastSession;\n}\n\nasync function stopRecording(): Promise<{ sessionId: string; serverUrl: string } | null> {\n const rec = await getRecording();\n if (!rec) return null;\n // Deep mode stops first (NET-R17): requests in flight become pending\n // lines, and the debugger detaches, which removes Chrome's bar.\n if (deepTabId() !== undefined) await stopDeep();\n else if (rec.deep?.state === 'on') await chrome.debugger.detach({ tabId: rec.tabId }).catch(() => {});\n await setRecording(null);\n await unregisterScripts().catch(() => {});\n try {\n const res = (await chrome.tabs.sendMessage(rec.tabId, { type: 'stop' } satisfies BridgeMessage, { frameId: 0 })) as { lines?: string } | undefined;\n if (res?.lines) onSessionBatch(rec.sessionId, res.lines);\n } catch {\n // tab closed, or showing a page the recorder cannot run on\n }\n await inbox;\n void enqueue({ url: `${rec.serverUrl}/api/sessions/${rec.sessionId}/end`, contentType: 'application/json', body: JSON.stringify({ endedAt: Date.now() }) });\n await Promise.race([drain(), sleep(STOP_DRAIN_TIMEOUT_MS)]);\n if (flushInterval?.sessionId === rec.sessionId) flushInterval = null;\n await chrome.storage.session.set({ lastSession: { sessionId: rec.sessionId, serverUrl: rec.serverUrl } });\n await updateBadge(null);\n // A buffered tab goes back to buffering.\n if ((await getBufferTabs())[rec.tabId]) await injectInto(rec.tabId, await getSettings()).catch(() => {});\n return { sessionId: rec.sessionId, serverUrl: rec.serverUrl };\n}\n\n// ---------- deep mode ----------\n\nfunction deepOptions(rec: Recording, s: Settings): DeepOptions {\n return {\n tabId: rec.tabId,\n captureBodies: s.captureBodies,\n maxBodyChars: MAX_BODY_CHARS,\n post: (lines) => onSessionBatch(rec.sessionId, lines),\n flushMs: () => (flushInterval?.sessionId === rec.sessionId ? flushInterval.ms : FLUSH_MS.normal),\n // tabtape's own pages are never recorded, even when the recorded tab shows one.\n ignore: ({ documentURL }) => isServerPage(documentURL, rec.serverUrl) || isServerPage(documentURL, s.serverUrl),\n };\n}\n\n/** chrome.debugger.onDetach reasons, as the marker and the popup say them. */\nconst DEEP_END_REASONS: Record<string, string> = {\n canceled_by_user: \"canceled in Chrome's debugging bar\",\n target_closed: 'the page closed or crashed',\n restarted: 'the extension restarted',\n};\n\nfunction writeMarker(sessionId: string, label: string): void {\n onSessionBatch(sessionId, JSON.stringify({ ts: Date.now(), type: 'event', data: { name: 'marker', label } }));\n}\n\n/**\n * Deep mode ended before the recording (NET-R14): the recording goes on, a\n * marker says why, and the page captures requests again.\n */\nasync function markDeepEnded(rec: Recording, why: string): Promise<void> {\n const reason = DEEP_END_REASONS[why] ?? why;\n writeMarker(rec.sessionId, `Deep network mode stopped: ${reason}`);\n await setRecording({ ...rec, deep: { state: 'ended', reason } });\n // The bridge asks for a new decision, which turns in-page capture back on.\n await chrome.tabs.sendMessage(rec.tabId, { type: 'redecide' } satisfies BridgeMessage, { frameId: 0 }).catch(() => {});\n}\n\n/** chrome.debugger.onDetach for the recorded tab, when it was not our own detach. */\nasync function onDeepDetached(tabId: number, why: string): Promise<void> {\n // Pending lines for what was in flight; posted while the rest goes on.\n const flushed = endDeep(tabId);\n try {\n await exclusive(async () => {\n const rec = await getRecording();\n // A recording that stopped meanwhile (the tab closed) has nothing to update.\n if (rec?.tabId === tabId && rec.deep?.state === 'on') await markDeepEnded(rec, why);\n });\n await flushed;\n } catch (err) {\n console.warn('tabtape: deep mode end', errorMessage(err));\n }\n}\n\n/**\n * A service worker restart loses deep mode's state. Recording goes on with\n * a fresh capture if the debugger is still attached to the tab, otherwise\n * deep mode ends and the page captures requests again.\n */\nasync function checkDeepAfterRestart(): Promise<void> {\n const rec = await getRecording();\n if (rec?.deep?.state !== 'on' || deepTabId() === rec.tabId) return;\n try {\n await startDeep(deepOptions(rec, await getSettings()), true);\n } catch {\n await markDeepEnded(rec, 'restarted');\n }\n}\n\nasync function registerScripts(s: Settings): Promise<void> {\n await unregisterScripts();\n await chrome.scripting.registerContentScripts(sessionScripts(s));\n}\n\nasync function unregisterScripts(): Promise<void> {\n const existing = await chrome.scripting.getRegisteredContentScripts({ ids: SCRIPT_IDS });\n if (existing.length) await chrome.scripting.unregisterContentScripts({ ids: existing.map((s) => s.id) });\n}\n\n/**\n * Registered scripts only cover future page loads; the current page needs a\n * direct injection. The state hook arrives after the page's scripts here:\n * stores created at load missed it (the popup suggests a reload, ST-R13).\n */\nasync function injectInto(tabId: number, s: Settings): Promise<void> {\n await chrome.scripting.executeScript({ target: { tabId }, files: ['bridge.js'], world: 'ISOLATED', injectImmediately: true });\n await chrome.scripting.executeScript({ target: { tabId }, files: pageScripts(s), world: 'MAIN', injectImmediately: true });\n}\n\nasync function decide(sender: chrome.runtime.MessageSender): Promise<Decision> {\n const tabId = sender.tab?.id;\n if (tabId === undefined || (sender.frameId ?? 0) !== 0) return { record: false };\n const [rec, s] = await Promise.all([getRecording(), getSettings()]);\n const url = sender.url ?? sender.tab?.url ?? '';\n // tabtape's own pages (replay UI, live view) are never recorded or buffered,\n // even if the recorded tab navigates there. Other pages of the daemon's\n // origin (the Repoburg agent pane) are.\n const own = isServerPage(url, s.serverUrl) || isServerPage(url, rec?.serverUrl);\n if (rec?.tabId === tabId && !own) {\n return {\n record: true,\n target: { kind: 'session', id: rec.sessionId },\n options: {\n captureBodies: s.captureBodies,\n maxBodyChars: MAX_BODY_CHARS,\n checkoutEveryNms: Math.max(0, s.checkoutMinutes) * 60_000,\n inPageNetwork: rec.deep?.state !== 'on',\n recordState: s.recordState,\n },\n flushMs: flushInterval?.sessionId === rec.sessionId ? flushInterval.ms : undefined,\n };\n }\n const buffering = !own && s.bufferEnabled && matchesAny(s.bufferSites, url);\n const key = await updateBufferTabs((tabs) => {\n const t = tabs[tabId];\n if (buffering) tabs[tabId] = { key: t?.key ?? newBufferKey(tabId), active: true, url };\n else if (t) t.active = false;\n return tabs[tabId]?.key;\n });\n await badgeForTab(tabId, rec);\n if (!buffering || !key) return { record: false };\n const windowMs = s.bufferMinutes * 60_000;\n return {\n record: true,\n target: { kind: 'buffer', key, windowMs },\n // Deep mode is never used for buffers (NET-R3).\n options: { captureBodies: s.captureBodies, maxBodyChars: MAX_BODY_CHARS, checkoutEveryNms: bufferCheckoutMs(windowMs), inPageNetwork: true, recordState: s.recordState },\n };\n}\n\nfunction newBufferKey(tabId: number): string {\n const rand = Array.from(crypto.getRandomValues(new Uint8Array(3)), (b) => b.toString(16).padStart(2, '0')).join('');\n return `tab${tabId}-${rand}`;\n}\n\n/** Registers the buffer scripts for the allow-listed sites and starts buffering open tabs. */\nasync function syncBufferScripts(): Promise<void> {\n const s = await getSettings();\n const existing = await chrome.scripting.getRegisteredContentScripts({ ids: BUFFER_SCRIPT_IDS });\n if (existing.length) await chrome.scripting.unregisterContentScripts({ ids: existing.map((x) => x.id) });\n if (!s.bufferEnabled || !s.bufferSites.length) {\n // Buffering stops; tell open buffered tabs and free their buffers.\n const tabs = await updateBufferTabs((t) => {\n const all = { ...t };\n for (const k of Object.keys(t)) delete t[k];\n return all;\n });\n for (const [tabId, t] of Object.entries(tabs)) {\n void deleteBuffer(t.key, s.serverUrl);\n await injectInto(Number(tabId), s).catch(() => {}); // the decision is now \"off\"\n await badgeForTab(Number(tabId), await getRecording());\n }\n return;\n }\n const common = { matches: s.bufferSites, runAt: 'document_start' as const, allFrames: false, persistAcrossSessions: true };\n await chrome.scripting.registerContentScripts([\n { ...common, id: BUFFER_SCRIPT_IDS[0], js: ['bridge.js'], world: 'ISOLATED' },\n { ...common, id: BUFFER_SCRIPT_IDS[1], js: pageScripts(s), world: 'MAIN' },\n ]);\n for (const tab of await chrome.tabs.query({})) {\n if (tab.id === undefined || !tab.url || isServerPage(tab.url, s.serverUrl)) continue;\n if (matchesAny(s.bufferSites, tab.url)) await injectInto(tab.id, s).catch(() => {});\n }\n}\n\nfunction deleteBuffer(key: string, serverUrl: string): Promise<unknown> {\n return fetch(`${serverUrl}/api/buffers/${key}`, { method: 'DELETE', headers: { 'X-Tabtape-Client': 'extension' } }).catch(() => {});\n}\n\nasync function health(serverUrl: string): Promise<Status['server']> {\n try {\n const res = await fetch(`${serverUrl}/api/health`, { signal: AbortSignal.timeout(1500) });\n if (!res.ok) return { ok: false, url: serverUrl, error: `HTTP ${res.status}` };\n const body = (await res.json()) as { version?: string };\n return { ok: true, url: serverUrl, version: body.version };\n } catch {\n return { ok: false, url: serverUrl, error: 'not reachable' };\n }\n}\n\nasync function status(tabId?: number): Promise<Status> {\n const [rec, settings, { lastSession }, bufferTabs] = await Promise.all([\n getRecording(),\n getSettings(),\n chrome.storage.session.get('lastSession'),\n getBufferTabs(),\n ]);\n return {\n recording: rec,\n lastSession: (lastSession as Status['lastSession']) ?? null,\n server: await health(rec?.serverUrl ?? settings.serverUrl),\n stats: { ...stats, queuedBytes },\n buffer: {\n enabled: settings.bufferEnabled,\n minutes: settings.bufferMinutes,\n tab: tabId !== undefined ? (bufferTabs[tabId] ?? null) : null,\n error: settings.bufferEnabled ? bufferError : undefined,\n },\n };\n}\n\nasync function updateBadge(rec: Recording | null): Promise<void> {\n await chrome.action.setBadgeText({ text: rec ? 'REC' : '' });\n await chrome.action.setBadgeBackgroundColor({ color: BADGE.rec });\n await chrome.action.setBadgeTextColor({ color: '#ffffff' });\n await chrome.action.setTitle({ title: rec ? `tabtape: recording \u201C${rec.title ?? rec.url ?? ''}\u201D` : 'tabtape: record this tab' });\n for (const tabId of Object.keys(await getBufferTabs())) await badgeForTab(Number(tabId), rec);\n if (rec) await badgeForTab(rec.tabId, rec);\n}\n\n/**\n * Tab-specific badges: buffered tabs show a grey BUF, the recorded tab its\n * red REC (which wins); other tabs fall back to the global badge.\n */\nasync function badgeForTab(tabId: number, rec: Recording | null): Promise<void> {\n const buffered = (await getBufferTabs())[tabId]?.active;\n try {\n if (rec?.tabId === tabId) {\n await chrome.action.setBadgeText({ tabId, text: 'REC' });\n await chrome.action.setBadgeBackgroundColor({ tabId, color: BADGE.rec });\n } else if (buffered) {\n await chrome.action.setBadgeText({ tabId, text: 'BUF' });\n await chrome.action.setBadgeBackgroundColor({ tabId, color: BADGE.buf });\n } else {\n await chrome.action.setBadgeText({ tabId, text: null as unknown as string });\n }\n } catch {\n // the tab is gone\n }\n}\n\n// ---------- events ----------\n\nchrome.runtime.onMessage.addListener((msg: RuntimeMessage, sender, sendResponse) => {\n // Popup and other extension pages; content scripts report the page URL instead.\n const fromExtensionPage = sender.id === chrome.runtime.id && !!sender.url?.startsWith(chrome.runtime.getURL(''));\n switch (msg?.type) {\n case 'hello':\n decide(sender).then(sendResponse, () => sendResponse({ record: false }));\n return true;\n case 'batch':\n onBatch(msg.target, msg.lines);\n sendResponse({ ok: true });\n return false;\n case 'status':\n if (!fromExtensionPage) return false;\n status(msg.tabId).then(sendResponse, (err) => sendResponse({ ok: false, error: errorMessage(err) }));\n return true;\n case 'save-buffer':\n if (!fromExtensionPage) return false;\n reply(saveBuffer(msg.tabId), sendResponse);\n return true;\n case 'settings-changed':\n if (!fromExtensionPage) return false;\n reply(\n (async () => {\n await syncBufferScripts();\n // Pages the recorded tab loads next follow the new settings (state hook on or off).\n if (await getRecording()) await registerScripts(await getSettings());\n return {};\n })(),\n sendResponse,\n );\n return true;\n case 'start':\n if (!fromExtensionPage) return false;\n reply(exclusive(() => startRecording(msg.tabId, { deep: msg.deep === true })), sendResponse);\n return true;\n case 'stop':\n if (!fromExtensionPage) return false;\n reply(exclusive(() => stopRecording()), sendResponse);\n return true;\n }\n return false;\n});\n\nchrome.tabs.onRemoved.addListener((tabId) => {\n void exclusive(async () => {\n if ((await getRecording())?.tabId === tabId) await stopRecording();\n });\n void (async () => {\n const t = await updateBufferTabs((tabs) => {\n const gone = tabs[tabId];\n delete tabs[tabId];\n return gone;\n });\n if (t) await deleteBuffer(t.key, (await getSettings()).serverUrl);\n })();\n});\n\n// Record links (links.ts): opening one starts or stops a recording, once the\n// server has confirmed its token, so web pages opening such URLs start\n// nothing. The tab then shows the outcome on the server's <server>/record\n// page. The filter keeps Chrome from waking this worker for other pages; the\n// server only answers on loopback hosts. It matches /record anywhere in the\n// path, whatever the server's mount (/tabtape/record in the daemon), and\n// parseRecordLink checks the exact path.\nchrome.webNavigation.onCommitted.addListener(\n (d) => {\n // Back or forward onto a record link revisits it; handling it again would\n // end the recording and start another. (leaveRecordLink keeps the links\n // out of the tab's history; this covers the file: pages it cannot.)\n if (d.frameId === 0 && !d.transitionQualifiers.includes('forward_back')) void onTabUrl(d.tabId, d.url);\n },\n { url: ['127.0.0.1', 'localhost', '[::1]'].map((hostEquals) => ({ schemes: ['http', 'https'], hostEquals, pathContains: '/record' })) },\n);\n\n// A fresh profile (Playwright MCP starts every run with one) installs the\n// extension as the browser opens, and a record link opened right away comes\n// before this service worker listens: look for one now.\nvoid chrome.tabs.query({}).then((tabs) => {\n for (const t of tabs) if (t.id !== undefined && t.url) void onTabUrl(t.id, t.url);\n});\n\n/** Lets the page that was recorded before the stop link send its last lines. */\nconst STOP_SETTLE_MS = 300;\n\n/** Tabs whose record link is being handled: the startup scan and the event may both see it. */\nconst handlingLink = new Set<number>();\n\nasync function onTabUrl(tabId: number, url: string): Promise<void> {\n if (!url.includes('/record')) return;\n const s = await getSettings();\n const link = parseRecordLink(url, s.serverUrl);\n if (!link || handlingLink.has(tabId)) return;\n handlingLink.add(tabId);\n try {\n // Handled already: the tab moved on to the page it opened.\n if ((await chrome.tabs.get(tabId).catch(() => undefined))?.url !== url) return;\n await handleRecordLink(tabId, url, link, s);\n } finally {\n handlingLink.delete(tabId);\n }\n}\n\n/**\n * Sends the tab from the record link at `linkUrl` on to `to`, replacing the\n * link in the tab's history (location.replace), so Back skips it rather than\n * opening it again. Nothing happens when the tab has moved on meanwhile. A\n * web page cannot open file: pages, and a page that cannot be scripted has\n * no location to replace: those get a new history entry.\n */\nasync function leaveRecordLink(tabId: number, linkUrl: string, to: string): Promise<void> {\n if (!to.startsWith('file:')) {\n try {\n await chrome.scripting.executeScript({\n target: { tabId },\n injectImmediately: true,\n func: (from: string, next: string) => {\n if (location.href === from) location.replace(next);\n },\n args: [linkUrl, to],\n });\n return;\n } catch {\n // fall through\n }\n }\n if ((await chrome.tabs.get(tabId)).url === linkUrl) await chrome.tabs.update(tabId, { url: to });\n}\n\nasync function handleRecordLink(tabId: number, linkUrl: string, link: RecordLink, s: Settings): Promise<void> {\n // The record page under the server's path: http://127.0.0.1:9998/tabtape/record?\u2026\n const outcome = (params: Record<string, string>) => `${s.serverUrl}/record?${new URLSearchParams(params)}`;\n try {\n await verifyRecordToken(s.serverUrl, link.token);\n if (link.action === 'start') {\n if (link.url !== undefined && (!isRecordable(link.url) || isServerPage(link.url, s.serverUrl))) {\n throw new Error(`${link.url} cannot be recorded: only http, https and file pages can.`);\n }\n const rec = await exclusive(() => startRecording(tabId, { name: link.name, source: 'automation', nextPage: true }));\n const started: Record<string, string> = { started: rec.sessionId };\n if (link.name) started.name = link.name;\n await leaveRecordLink(tabId, linkUrl, link.url ?? outcome(started));\n } else {\n await sleep(STOP_SETTLE_MS);\n const stopped = await exclusive(() => stopRecording());\n await leaveRecordLink(tabId, linkUrl, outcome(stopped ? { stopped: stopped.sessionId } : { error: 'Nothing was being recorded.' }));\n }\n } catch (err) {\n await leaveRecordLink(tabId, linkUrl, outcome({ error: errorMessage(err) })).catch(() => {});\n }\n}\n\nasync function verifyRecordToken(serverUrl: string, token: string): Promise<void> {\n let res: Response;\n try {\n res = await fetch(`${serverUrl}/api/record/verify`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Tabtape-Client': 'extension' },\n body: JSON.stringify({ token }),\n signal: AbortSignal.timeout(3000),\n });\n } catch {\n throw new Error(notRunning(serverUrl));\n }\n if (res.status === 403) throw new Error('This record link is not valid (did the Repoburg daemon restart?): get a new one from get_record_link.');\n if (!res.ok) throw new Error(`HTTP ${res.status}: ${(await res.text()).trim()}`);\n}\n\n/** tabtape runs inside the Repoburg daemon: no answer means the daemon is not running (or tabtape is off). */\nfunction notRunning(serverUrl: string): string {\n return `tabtape is not reachable at ${serverUrl}. It runs inside the Repoburg daemon: start it with \"repoburg-daemon\".`;\n}\n\nchrome.commands.onCommand.addListener((command, tab) => {\n if (command === 'save-buffer') {\n void (async () => {\n const target = tab?.id !== undefined ? tab : (await chrome.tabs.query({ active: true, currentWindow: true }))[0];\n if (target?.id === undefined) return;\n try {\n const saved = await saveBuffer(target.id);\n await chrome.action.setBadgeText({ tabId: target.id, text: 'SAVED' });\n setTimeout(() => void badgeForTab(target.id!, null), 2500);\n console.info('tabtape: saved buffer as', saved.sessionId);\n } catch (err) {\n console.warn('tabtape:', errorMessage(err));\n }\n })();\n return;\n }\n if (command !== 'toggle-recording') return;\n exclusive(async () => {\n if (await getRecording()) {\n await stopRecording();\n return;\n }\n const target = tab?.id !== undefined ? tab : (await chrome.tabs.query({ active: true, currentWindow: true }))[0];\n if (target?.id !== undefined) await startRecording(target.id);\n }).catch((err) => console.warn('tabtape:', errorMessage(err)));\n});\n\nasync function reset(): Promise<void> {\n await setRecording(null);\n await unregisterScripts();\n await chrome.storage.session.remove('bufferTabs');\n await syncBufferScripts().catch((err) => console.warn('tabtape: buffer scripts', errorMessage(err)));\n await updateBadge(null);\n}\nchrome.runtime.onStartup.addListener(() => void reset());\nchrome.runtime.onInstalled.addListener(() => void reset());\n\ninitDeep(onDeepDetached);\nvoid exclusive(checkDeepAfterRestart).catch((err) => console.warn('tabtape: deep mode after restart', errorMessage(err)));\n\n// Automation API, for Playwright and other CDP clients that reach this\n// service worker: `await sw.evaluate((id) => tabtape.start(id, { name: 'checkout' }), tabId)`.\n// playwright/tabtape.mjs wraps it. start, stop and status are the stable\n// part; saveBuffer, syncBufferScripts and simulateDetach serve e2e/run.mjs.\nObject.assign(globalThis, {\n tabtape: {\n /**\n * Records the tab as an \"automation\" recording titled `name`; a blank tab\n * (about:blank) is recorded from its first page on. The result carries\n * the sessionId, and a warning when deep mode could not start.\n */\n start: (tabId: number, opts: { deep?: boolean; name?: string } = {}) =>\n exclusive(async () => {\n const tab = await chrome.tabs.get(tabId);\n const blank = !tab.url || tab.url === 'about:blank';\n return startRecording(tabId, { deep: opts.deep === true, name: recordingName(opts.name), source: 'automation', nextPage: blank });\n }),\n /** Stops the recording; resolves to { sessionId, serverUrl }, or null when nothing was recorded. */\n stop: () => exclusive(() => stopRecording()),\n status,\n saveBuffer,\n syncBufferScripts,\n /** Detaches the debugger for real, then handles it like Chrome's onDetach with this reason. */\n simulateDetach: (reason: 'canceled_by_user' | 'target_closed') => simulateDetach(reason),\n },\n});\n\n// ---------- helpers ----------\n\nfunction reply<T extends object | null>(p: Promise<T>, sendResponse: (r: unknown) => void): void {\n p.then(\n (value) => sendResponse({ ok: true, ...value }),\n (err) => sendResponse({ ok: false, error: errorMessage(err) }),\n );\n}\n\nfunction newSessionId(): string {\n const d = new Date();\n const p = (n: number) => String(n).padStart(2, '0');\n const rand = Array.from(crypto.getRandomValues(new Uint8Array(3)), (b) => b.toString(16).padStart(2, '0')).join('');\n return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${rand}`;\n}\n\nfunction countLines(s: string): number {\n let n = 1;\n for (let i = s.indexOf('\\n'); i >= 0; i = s.indexOf('\\n', i + 1)) n++;\n return n;\n}\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["stringify", "parse", "MASK", "PASSWORD_KEY", "TEXT_MIME", "round", "n", "isPasswordKey", "key", "PASSWORD_KEY", "redactParams", "query", "params", "changed", "key", "isPasswordKey", "MASK", "redactUrl", "url", "q", "hash", "redacted", "redactValue", "v", "item", "obj", "val", "QUERY_LIKE", "MULTIPART", "MULTIPART_START", "redactBody", "text", "contentType", "value", "parse", "stringify", "redactMultipart", "m", "boundary", "delimiter", "parts", "i", "part", "sep", "head", "name", "start", "end", "KEY_VALUE", "maskPasswordsInText", "t", "pair", "prefix", "WS_LIMITS", "FrameLimiter", "maxFrames", "maxBytes", "windowMs", "t", "n", "utf8Length", "s", "i", "c", "isPair", "cutUtf8", "pair", "w", "base64Size", "b64", "len", "pad", "base64ToBytes", "bin", "out", "decodeText", "bytes", "charset", "round", "n", "computeTimes", "input", "issueTime", "responseTime", "headers", "endTime", "out", "timing", "harTiming", "t", "ms", "s", "clean", "waitEnd", "queued", "least", "leastNonNegative", "blockedStart", "blocked", "proxy", "dnsEnd", "dns", "sslEnd", "ssl", "connectEnd", "connect", "sendEnd", "send", "highest", "wait", "receive", "values", "best", "v", "k", "EXTRA_INFO_WAIT_MS", "CORS_EXTRA_INFO_WAIT_MS", "ORPHAN_TTL_MS", "MAX_ENTRIES", "NO_BODY_STATUS", "SKIP_URL", "NOT_FOR_THIS_URL", "URL_HEADERS", "MULTIPART", "STOPPED_NOTE", "BODY_TIMEOUT_MS", "CdpNetwork", "opts", "frameId", "e", "hop", "sessionId", "info", "redactUrl", "method", "params", "p", "why", "gone", "timeoutMs", "timer", "resolve", "sid", "req", "last", "size", "setSession", "url", "skip", "blockedRequestCookies", "reasonPhrase", "blockedSetCookies", "cors", "wait", "res", "pending", "d", "job", "tracked", "h", "reqExtra", "resExtra", "resourceType", "kind", "times", "computeTimes", "round", "reqHeaders", "headerMap", "resHeaders", "address", "remoteAddress", "cache", "worker", "jobs", "contentType", "inline", "entriesBytes", "withTimeout", "r", "base64ToBytes", "err", "errorText", "bytes", "isBinaryType", "decodeText", "text", "utf8Length", "body", "clipText", "redactBody", "mime", "TEXT_MIME", "FrameLimiter", "conn", "dir", "opcode", "payload", "base64Size", "c", "rec", "cut", "cutUtf8", "maskPasswordsInText", "WS_LIMITS", "error", "timestamp", "dropped", "ts", "status", "statusText", "frames", "requestId", "now", "key", "w", "wallTime", "offset", "record", "out", "name", "raw", "k", "v", "ip", "host", "reasonsOf", "cookieOf", "reasons", "cookie", "list", "cookieName", "line", "eq", "semi", "headersText", "entries", "x", "parts", "n", "at", "ms", "late", "_", "reject", "max", "message", "PROTOCOL_VERSION", "NETWORK_ENABLE", "AUTO_ATTACH", "WORKER_TYPES", "WORKER_ENABLE_TIMEOUT_MS", "STOP_TIMEOUT_MS", "FLUSH_BYTES", "DeepSession", "opts", "CdpNetwork", "record", "sessionId", "requestId", "method", "params", "target", "err", "p", "child", "resume", "type", "timer", "resolve", "line", "lines", "discard", "current", "attached", "detachListener", "forget", "s", "initDeep", "onDetached", "source", "reason", "deepTabId", "startDeep", "o", "stopDeep", "mainFrameId", "r", "t", "endDeep", "tabId", "discardDeep", "simulateDetach", "PATTERN", "validPattern", "p", "matches", "pattern", "url", "u", "scheme", "m", "ps", "host", "port", "path", "base", "glob", "matchesAny", "patterns", "isRecordable", "LOOPBACK", "isServerPage", "serverUrl", "s", "x", "serverPath", "glob", "pattern", "text", "escape", "s", "NAME_MAX", "parseRecordLink", "url", "serverUrl", "isServerPage", "u", "token", "record", "serverPath", "recordingName", "name", "FLUSH_MS", "DEFAULT_SETTINGS", "STANDALONE_SERVER_URL", "withDefaults", "stored", "s", "url", "BUFFER_MINUTES", "bufferCheckoutMs", "windowMs", "MAX_BODY_CHARS", "SCRIPT_IDS", "sessionScripts", "s", "common", "pageScripts", "BUFFER_SCRIPT_IDS", "BADGE", "QUEUE_LIMIT_BYTES", "STOP_DRAIN_TIMEOUT_MS", "SESSION_ID", "getRecording", "recording", "setRecording", "rec", "getSettings", "settings", "withDefaults", "BUFFER_MINUTES", "validPattern", "getBufferTabs", "bufferTabs", "bufferTabsLock", "updateBufferTabs", "fn", "run", "tabs", "out", "HttpError", "status", "message", "queue", "queuedBytes", "draining", "stats", "enqueue", "job", "dropped", "drain", "runQueue", "backoff", "post", "err", "errorMessage", "sleep", "res", "reply", "onFollowers", "flushInterval", "sessionId", "followers", "ms", "FLUSH_MS", "inbox", "onBatch", "target", "lines", "onSessionBatch", "postBuffer", "bufferError", "key", "windowMs", "serverUrl", "countLines", "lock", "exclusive", "startRecording", "tabId", "opts", "stopRecording", "tab", "isRecordable", "isServerPage", "health", "notRunning", "buffered", "fromBuffer", "saveBufferTo", "page", "newSessionId", "warning", "startDeep", "deepOptions", "registerScripts", "injectInto", "discardDeep", "unregisterScripts", "writeMarker", "updateBadge", "keepOpen", "saveBuffer", "lastSession", "deepTabId", "stopDeep", "MAX_BODY_CHARS", "documentURL", "DEEP_END_REASONS", "label", "markDeepEnded", "why", "reason", "onDeepDetached", "flushed", "endDeep", "checkDeepAfterRestart", "existing", "decide", "sender", "url", "own", "buffering", "matchesAny", "t", "newBufferKey", "badgeForTab", "bufferCheckoutMs", "rand", "b", "syncBufferScripts", "x", "all", "k", "deleteBuffer", "body", "msg", "sendResponse", "fromExtensionPage", "gone", "d", "onTabUrl", "hostEquals", "STOP_SETTLE_MS", "handlingLink", "link", "parseRecordLink", "handleRecordLink", "leaveRecordLink", "linkUrl", "to", "from", "next", "outcome", "params", "verifyRecordToken", "started", "stopped", "token", "command", "saved", "reset", "initDeep", "blank", "recordingName", "simulateDetach", "p", "value", "n", "i", "resolve"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"use strict";(()=>{var n={data:"__tabtape:data",flush:"__tabtape:flush",hello:"__tabtape:hello",control:"__tabtape:control",info:"__tabtape:info",infoReply:"__tabtape:info-reply"};var f={normal:1e3,live:250};var _=1e6;globalThis.__tabtapeBridge?globalThis.__tabtapeBridge.hello():E();function E(){let t=null,r=[],l=0,i,c=!1,u=f.normal,p=e=>document.dispatchEvent(new CustomEvent(n.control,{detail:JSON.stringify(e)}));function g(e){t=e,m(e.record?e.flushMs:void 0),y()}function m(e){let s=typeof e=="number"&&e>=100&&e<=5e3?e:f.normal,o=s<u;u=s,o&&i!==void 0&&(clearTimeout(i),i=setTimeout(a,u))}function y(){if(!t)return;if(!t.record){p({cmd:"disable"});return}let e=t.target;p({cmd:"start",sessionId:e.kind==="session"?e.id:`buffer:${e.key}`,options:t.options})}function b(){clearTimeout(i),i=void 0;let e=r.join(`
|
|
2
|
+
`);return r=[],l=0,e}function a(){if(c||!r.length||!t?.record)return;let e={type:"batch",target:t.target,lines:b()};h(e)}function h(e){try{chrome.runtime.sendMessage(e).catch(()=>{})}catch{}}function d(){try{chrome.runtime.sendMessage({type:"hello"}).then(e=>g(e??{record:!1}),()=>g({record:!1}))}catch{g({record:!1})}}document.addEventListener(n.data,e=>{let s=e.detail;typeof s!="string"||!t?.record||(r.push(s),l+=s.length,l>=_?a():i??=setTimeout(a,u))}),document.addEventListener(n.hello,y),document.addEventListener(n.flush,a),addEventListener("pagehide",a),addEventListener("pageshow",e=>{e.persisted&&d()}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&a()}),chrome.runtime.onMessage.addListener((e,s,o)=>{if(e?.type==="take")return o({lines:b()}),!1;if(e?.type==="flush-interval")return m(e.ms),o({}),!1;if(e?.type==="redecide")return d(),o({}),!1;if(e?.type==="page-info")return o(M()),!1;if(e?.type!=="stop")return!1;c=!0,p({cmd:"stop"}),c=!1;let v=b();return t={record:!1},u=f.normal,o({lines:v}),!1}),globalThis.__tabtapeBridge={hello:d},d()}function M(){let t=null,r=l=>{try{t=JSON.parse(String(l.detail))}catch{}};document.addEventListener(n.infoReply,r);try{document.dispatchEvent(new CustomEvent(n.info))}finally{document.removeEventListener(n.infoReply,r)}return t}})();
|
|
3
|
+
//# sourceMappingURL=bridge.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/protocol.ts", "../src/bridge.ts"],
|
|
4
|
+
"sourcesContent": ["// Messages between the extension's parts.\n//\n// recorder.js (page MAIN world) --DOM CustomEvents--> bridge.js (isolated world)\n// bridge.js --chrome.runtime messages--> background.js --HTTP--> tabtape (Repoburg daemon, /tabtape)\n\n/** DOM event names used between the recorder (MAIN world) and the bridge. */\nexport const DOM_EVENT = {\n /** recorder -> bridge, detail: one NDJSON line */\n data: '__tabtape:data',\n /** recorder -> bridge: send what you have now (page is going away) */\n flush: '__tabtape:flush',\n /** recorder -> bridge: I am loaded, tell me what to do */\n hello: '__tabtape:hello',\n /** bridge -> recorder, detail: JSON ControlMessage */\n control: '__tabtape:control',\n /** bridge -> recorder: report PageInfo now (answered synchronously with infoReply) */\n info: '__tabtape:info',\n /** recorder -> bridge, detail: JSON PageInfo */\n infoReply: '__tabtape:info-reply',\n} as const;\n\n/** The state hook bundle (state.js) publishes its API on window under this symbol. */\nexport const STATE_HOOK_KEY = 'tabtape.state';\n\nexport interface RecorderOptions {\n captureBodies: boolean;\n maxBodyChars: number;\n /** Periodic full snapshots keep replays usable after ring buffer eviction. */\n checkoutEveryNms: number;\n /**\n * In-page network capture (fetch, XHR, beacons, Resource and Navigation\n * Timing). Off while deep mode records the tab's network through the\n * debugger, so no request is written twice.\n */\n inPageNetwork: boolean;\n /** Redux / Zustand state recording; needs the state hook (state.js) on the page. */\n recordState: boolean;\n}\n\n/** What the recorder knows about its page; the popup asks for it (ST-R13). */\nexport interface PageInfo {\n /**\n * early: the Redux DevTools hook was defined before the page's scripts ran;\n * late: injected into a page that was already open (stores created at load\n * missed it); off: state recording is off (no state.js on the page).\n */\n stateHook: 'early' | 'late' | 'off';\n /** Names of the stores connected to the hook. */\n stores: string[];\n}\n\n/** Where a tab's lines go: a recording, or the tab's buffer (buffer mode). */\nexport type Target = { kind: 'session'; id: string } | { kind: 'buffer'; key: string; windowMs: number };\n\nexport type ControlMessage =\n | { cmd: 'start'; sessionId: string; options: RecorderOptions }\n | { cmd: 'stop' }\n | { cmd: 'disable' };\n\n/** How often the bridge sends its lines; faster while someone watches the recording live. */\nexport const FLUSH_MS = { normal: 1000, live: 250 };\n\nexport type Decision =\n | {\n record: true;\n target: Target;\n options: RecorderOptions;\n /** Bridge flush interval, when not FLUSH_MS.normal. */\n flushMs?: number;\n }\n | { record: false };\n\nexport type RuntimeMessage =\n | { type: 'hello' }\n | { type: 'batch'; target: Target; lines: string }\n | { type: 'status'; tabId?: number }\n /** deep: record this recording's network through the debugger (deep mode, NET-R1) */\n | { type: 'start'; tabId: number; deep?: boolean }\n | { type: 'stop' }\n | { type: 'save-buffer'; tabId: number }\n | { type: 'settings-changed' };\n\n/** background -> bridge */\nexport type BridgeMessage =\n /** stop recording; the response carries the lines not sent yet */\n | { type: 'stop' }\n /** hand over the lines not sent yet without stopping */\n | { type: 'take' }\n /** live viewers came or went: send batches every `ms` */\n | { type: 'flush-interval'; ms: number }\n /** the recording's options changed (deep mode ended): ask for a new decision */\n | { type: 'redecide' }\n /** popup -> bridge: the response is the recorder's PageInfo, or null without a recorder */\n | { type: 'page-info' };\n\nexport interface Settings {\n /** tabtape's server with its path, no trailing slash: URLs are built as `${serverUrl}/api/\u2026`. */\n serverUrl: string;\n captureBodies: boolean;\n checkoutMinutes: number;\n /** Buffer mode: keep the last minutes of allow-listed tabs on the server. */\n bufferEnabled: boolean;\n /** Chrome match patterns, e.g. http://localhost/* */\n bufferSites: string[];\n bufferMinutes: number;\n /** Recordings started on a buffered tab begin with the buffered minutes. */\n startWithBuffer: boolean;\n /** Record Redux / Zustand stores through the Redux DevTools hook (ST-R1). */\n recordState: boolean;\n}\n\nexport const DEFAULT_SETTINGS: Settings = {\n // tabtape runs inside the Repoburg daemon, mounted under /tabtape.\n serverUrl: 'http://127.0.0.1:9998/tabtape',\n captureBodies: true,\n checkoutMinutes: 5,\n bufferEnabled: false,\n bufferSites: ['http://localhost/*', 'http://127.0.0.1/*'],\n bufferMinutes: 2,\n startWithBuffer: true,\n recordState: true,\n};\n\n/** The default server URL of tabtape's former standalone server. */\nconst STANDALONE_SERVER_URL = 'http://127.0.0.1:7777';\n\n/**\n * Stored settings over the defaults. The server URL loses trailing slashes;\n * one saved with the standalone server's default moves to the daemon.\n */\nexport function withDefaults(stored: Partial<Settings> | undefined): Settings {\n const s: Settings = { ...DEFAULT_SETTINGS, ...stored };\n const url = typeof s.serverUrl === 'string' ? s.serverUrl.trim().replace(/\\/+$/, '') : '';\n s.serverUrl = !url || url === STANDALONE_SERVER_URL ? DEFAULT_SETTINGS.serverUrl : url;\n return s;\n}\n\nexport const BUFFER_MINUTES = { min: 1, max: 10 };\n\n/** A checkout every quarter window (15 s \u2013 1 min) keeps every save replayable. */\nexport function bufferCheckoutMs(windowMs: number): number {\n return Math.min(Math.max(windowMs / 4, 15_000), 60_000);\n}\n\nexport const MAX_BODY_CHARS = 1_000_000;\n\nexport interface Recording {\n sessionId: string;\n tabId: number;\n startedAt: number;\n serverUrl: string;\n title?: string;\n url?: string;\n /** Deep mode (NET-R1): set when the recording was started with it. */\n deep?: DeepStatus;\n}\n\n/**\n * on: the debugger is attached and records the tab's network;\n * ended: it detached early (the user canceled Chrome's debugging bar, the tab\n * crashed, the extension restarted) and the page captures requests again.\n */\nexport interface DeepStatus {\n state: 'on' | 'ended';\n /** Why it ended, in words for the popup. */\n reason?: string;\n}\n\nexport interface BufferTab {\n key: string;\n /** The tab currently shows an allow-listed page and is being buffered. */\n active: boolean;\n url?: string;\n}\n\nexport interface Status {\n recording: Recording | null;\n lastSession: { sessionId: string; serverUrl: string } | null;\n server: { ok: boolean; url: string; version?: string; error?: string };\n stats: { lines: number; bytes: number; queuedBytes: number; dropped: number; lastError?: string };\n buffer: { enabled: boolean; minutes: number; tab: BufferTab | null; error?: string };\n}\n\nexport type Result<T = object> = ({ ok: true } & T) | { ok: false; error: string };\n", "// Bridge: isolated-world content script. Asks the background whether this\n// tab is being recorded, relays that decision to the recorder (MAIN world),\n// and forwards recorded lines to the background in batches.\n\nimport { DOM_EVENT, FLUSH_MS, type BridgeMessage, type ControlMessage, type Decision, type PageInfo, type RuntimeMessage } from './protocol';\n\nconst FLUSH_BYTES = 1_000_000;\n\ndeclare global {\n // eslint-disable-next-line no-var\n var __tabtapeBridge: { hello(): void } | undefined;\n}\n\nif (globalThis.__tabtapeBridge) globalThis.__tabtapeBridge.hello();\nelse init();\n\nfunction init(): void {\n let decision: Decision | null = null;\n let buffer: string[] = [];\n let bytes = 0;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let stopping = false;\n let flushMs = FLUSH_MS.normal;\n\n const control = (msg: ControlMessage) => document.dispatchEvent(new CustomEvent(DOM_EVENT.control, { detail: JSON.stringify(msg) }));\n\n function setDecision(d: Decision): void {\n decision = d;\n setFlushInterval(d.record ? d.flushMs : undefined);\n applyDecision();\n }\n\n /** 250 ms while the recording is watched live, 1 s otherwise. */\n function setFlushInterval(ms: unknown): void {\n const next = typeof ms === 'number' && ms >= 100 && ms <= 5000 ? ms : FLUSH_MS.normal;\n const sooner = next < flushMs;\n flushMs = next;\n if (sooner && timer !== undefined) {\n clearTimeout(timer);\n timer = setTimeout(flush, flushMs);\n }\n }\n\n function applyDecision(): void {\n if (!decision) return;\n if (!decision.record) {\n control({ cmd: 'disable' });\n return;\n }\n const t = decision.target;\n // The recorder only needs to know when the destination changes.\n control({ cmd: 'start', sessionId: t.kind === 'session' ? t.id : `buffer:${t.key}`, options: decision.options });\n }\n\n function take(): string {\n clearTimeout(timer);\n timer = undefined;\n const lines = buffer.join('\\n');\n buffer = [];\n bytes = 0;\n return lines;\n }\n\n function flush(): void {\n if (stopping || !buffer.length || !decision?.record) return;\n const msg: RuntimeMessage = { type: 'batch', target: decision.target, lines: take() };\n send(msg);\n }\n\n function send(msg: RuntimeMessage): void {\n try {\n chrome.runtime.sendMessage(msg).catch(() => {});\n } catch {\n // extension reloaded: this content script is orphaned\n }\n }\n\n function hello(): void {\n try {\n chrome.runtime.sendMessage({ type: 'hello' } satisfies RuntimeMessage).then(\n (d: Decision | undefined) => setDecision(d ?? { record: false }),\n () => setDecision({ record: false }),\n );\n } catch {\n setDecision({ record: false });\n }\n }\n\n document.addEventListener(DOM_EVENT.data, (e) => {\n const line = (e as CustomEvent).detail;\n if (typeof line !== 'string' || !decision?.record) return;\n buffer.push(line);\n bytes += line.length;\n if (bytes >= FLUSH_BYTES) flush();\n else timer ??= setTimeout(flush, flushMs);\n });\n // The recorder may load before or after us; whoever is second triggers\n // the decision being applied.\n document.addEventListener(DOM_EVENT.hello, applyDecision);\n document.addEventListener(DOM_EVENT.flush, flush);\n addEventListener('pagehide', flush);\n addEventListener('pageshow', (e) => {\n if (e.persisted) hello(); // back/forward cache: the recording may have changed\n });\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden') flush();\n });\n\n chrome.runtime.onMessage.addListener((msg: BridgeMessage, _sender, sendResponse) => {\n if (msg?.type === 'take') {\n sendResponse({ lines: take() });\n return false;\n }\n if (msg?.type === 'flush-interval') {\n setFlushInterval(msg.ms);\n sendResponse({});\n return false;\n }\n if (msg?.type === 'redecide') {\n hello();\n sendResponse({});\n return false;\n }\n if (msg?.type === 'page-info') {\n sendResponse(pageInfo());\n return false;\n }\n if (msg?.type !== 'stop') return false;\n // The recorder emits its last lines synchronously while stopping; keep\n // them out of a separate batch so the response carries everything.\n stopping = true;\n control({ cmd: 'stop' });\n stopping = false;\n const lines = take();\n decision = { record: false };\n flushMs = FLUSH_MS.normal;\n sendResponse({ lines });\n return false;\n });\n\n globalThis.__tabtapeBridge = { hello };\n hello();\n}\n\n/** Asks the recorder, which answers synchronously; null when the page has none. */\nfunction pageInfo(): PageInfo | null {\n let info: PageInfo | null = null;\n const onReply = (e: Event) => {\n try {\n info = JSON.parse(String((e as CustomEvent).detail)) as PageInfo;\n } catch {\n // ignore\n }\n };\n document.addEventListener(DOM_EVENT.infoReply, onReply);\n try {\n document.dispatchEvent(new CustomEvent(DOM_EVENT.info));\n } finally {\n document.removeEventListener(DOM_EVENT.infoReply, onReply);\n }\n return info;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["DOM_EVENT", "FLUSH_MS", "FLUSH_BYTES", "init", "decision", "buffer", "bytes", "timer", "stopping", "flushMs", "FLUSH_MS", "control", "msg", "DOM_EVENT", "setDecision", "d", "setFlushInterval", "applyDecision", "ms", "next", "sooner", "flush", "t", "take", "lines", "send", "hello", "line", "_sender", "sendResponse", "pageInfo", "info", "onReply", "e"]
|
|
7
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "tabtape recorder",
|
|
4
|
+
"short_name": "tabtape",
|
|
5
|
+
"version": "0.5.0",
|
|
6
|
+
"description": "Records the current tab (DOM, clicks, network, console, app state) to tabtape in your local Repoburg daemon for replay.",
|
|
7
|
+
"minimum_chrome_version": "125",
|
|
8
|
+
"icons": { "16": "icons/16.png", "32": "icons/32.png", "48": "icons/48.png", "128": "icons/128.png" },
|
|
9
|
+
"action": {
|
|
10
|
+
"default_popup": "popup.html",
|
|
11
|
+
"default_title": "tabtape: record this tab",
|
|
12
|
+
"default_icon": { "16": "icons/16.png", "32": "icons/32.png" }
|
|
13
|
+
},
|
|
14
|
+
"background": { "service_worker": "background.js", "type": "module" },
|
|
15
|
+
"permissions": ["debugger", "scripting", "storage", "tabs", "webNavigation"],
|
|
16
|
+
"host_permissions": ["<all_urls>"],
|
|
17
|
+
"commands": {
|
|
18
|
+
"toggle-recording": {
|
|
19
|
+
"suggested_key": { "default": "Alt+Shift+R" },
|
|
20
|
+
"description": "Start or stop recording the current tab"
|
|
21
|
+
},
|
|
22
|
+
"save-buffer": {
|
|
23
|
+
"suggested_key": { "default": "Alt+Shift+S" },
|
|
24
|
+
"description": "Save the last minutes of this tab (buffer mode)"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"web_accessible_resources": [{ "resources": ["recorder.js.map", "state.js.map"], "matches": ["<all_urls>"] }]
|
|
28
|
+
}
|