@weatherboard/annotation 0.2.0 → 0.2.1

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/protocol.ts", "../src/overlay.ts", "../src/target.ts", "../src/transport.ts", "../src/screenshot.ts", "../src/sdk.ts", "../src/arming.ts", "../src/toolbar.ts", "../src/state.ts"],
4
- "sourcesContent": ["/**\n * The annotation postMessage protocol (A-36).\n *\n * Mirrors `docs/contracts/types.ts`. That file is the\n * cross-component contract; this is the runtime copy the product bundle ships,\n * because the product cannot import from a docs directory. Changing a shape\n * here is a change to both \u2014 say so in the PR.\n */\n\nexport const SDK_VERSION = \"1\";\n\n/**\n * ===========================================================================\n * THE ATTRIBUTES \u2014 THE OTHER WIRE (A-186)\n * ===========================================================================\n *\n * These live in `protocol.ts`, not in `target.ts` where they are read, because\n * they are a contract between TWO PACKAGES that a consumer installs separately:\n * `@weatherboard/annotation-next` WRITES them at build time and this package\n * READS them at runtime. Two spellings of `data-src` is not a bug anybody\n * catches \u2014 the transform runs, the attributes appear, and every capture comes\n * back with no source ref at all, which is indistinguishable from the transform\n * not running.\n *\n * `annotation-next` imports these from `@weatherboard/annotation/protocol`\n * rather than declaring its own, which is why the subpath has no DOM in it: a\n * build tool importing the page SDK to learn two strings would be absurd.\n */\nexport const SRC_ATTR = \"data-src\";\nexport const NAME_ATTR = \"data-src-name\";\n\nexport interface SourceRef {\n file: string;\n line: number;\n column?: number;\n}\n\nexport interface BuildInfo {\n /** Mandatory. Feedback not pinned to a revision is ambiguous the moment someone merges. */\n gitSha: string;\n /**\n * Where this build came from.\n *\n * `\"development\"` was added in A-37, when the SDK was first mounted in a\n * real app and the two halves turned out not to agree: `resolveBuildInfo` in\n * `resolveBuildInfo` in the product's own Next config package \u2014 the module\n * that stamps the build-env variable, and the only producer of this field \u2014\n * emits\n * `production | preview | development`, mirroring `VERCEL_ENV`. This union\n * instead offered `\"staging\"`, which nothing produces: a staging deploy IS a\n * Vercel preview and reports itself as one. Widening rather than forcing the\n * mount to lie: a locally-annotated build calling itself a preview would put\n * someone's laptop in the corpus as though it were a deployment.\n */\n env: \"production\" | \"preview\" | \"staging\" | \"development\";\n branch?: string;\n /** Set when reviewing an agent's preview deploy, so feedback routes to that run's session. */\n previewRunId?: string;\n}\n\nexport interface ConsoleEntry {\n level: \"error\" | \"warn\";\n message: string;\n stack?: string;\n at: string;\n}\n\nexport interface NetworkEntry {\n url: string;\n method: string;\n status: number;\n at: string;\n}\n\nexport interface TargetInfo {\n /**\n * What the reviewer selected \u2014 one element, or an area (A-179).\n *\n * -------------------------------------------------------------------------\n * WHY THIS EXISTS RATHER THAN BEING INFERRED\n * -------------------------------------------------------------------------\n *\n * A region capture and an element capture are the same SHAPE: a bounding box,\n * a chain of source refs, a DOM path. Nothing in the values distinguishes\n * them, and the two mean opposite things about the chain \u2014 for an element it\n * is ANCESTORS, innermost first, so entry two contains entry one; for a\n * region it is SIBLINGS the rectangle happened to cross, which contain\n * nothing of each other.\n *\n * A reader that guesses will describe a region's siblings as a call-site\n * chain, which is a confident false statement about the page. `describeCapture`\n * hands that description to a model that writes acceptance criteria from it.\n *\n * -------------------------------------------------------------------------\n * WHY IT IS OPTIONAL, AND WHY ABSENT MEANS `element`\n * -------------------------------------------------------------------------\n *\n * The two halves of this protocol live in different repositories and deploy\n * independently (see the note on the legacy parameter names below). Every\n * capture in the store today, and every capture from a product bundle that\n * predates region selection, has no such field \u2014 and all of them are element\n * captures. Absent therefore has to mean `element`, or this field would\n * retroactively make the entire history ambiguous.\n *\n * Required-with-a-default would have the same effect and a worse failure: an\n * older bundle omits it, the type says it is there, and every reader believes\n * a value nobody sent.\n */\n selection?: \"element\" | \"region\";\n source?: SourceRef;\n /**\n * Every `data-src` from the clicked element up to the document root, innermost\n * first \u2014 an addition to the contract, and the reason is structural.\n *\n * Only host elements carry a ref (A-33), so `source` names the file that\n * *writes the markup*. For a composed design-system component that is\n * `MediaCard.tsx`, not the `GuideDetailCard.tsx` a human would say they\n * clicked. The chain is what lets the PM agent and the coding agent see both,\n * and pick the call site over the leaf when the feedback is about content\n * rather than the component itself.\n */\n sourceChain?: SourceRef[];\n componentName?: string;\n /** CSS selector fallback, for when no ancestor carries a ref at all. */\n domPath: string;\n boundingBox: { x: number; y: number; w: number; h: number };\n /** Truncated to 200 characters. */\n textContent?: string;\n}\n\n/**\n * What kind of capture this is, with the default applied once (A-189).\n *\n * `selection` is optional and **absent means `element`** \u2014 every capture taken\n * before A-179, and everything the managed product's live bundle emits today,\n * has no such field. Any other default would retroactively reclassify the whole\n * history as regions, silently.\n *\n * A function rather than a documented convention because the default is the\n * kind of thing two readers implement slightly differently and neither notices:\n * one writes `=== \"region\"`, another writes `?? \"element\"`, a third forgets and\n * treats `undefined` as falsy in a branch that meant something else. This is\n * the protocol's answer, and `compatibility.test.ts` replays every recorded\n * payload through it.\n */\nexport function selectionOf(target: Pick<TargetInfo, \"selection\">): \"element\" | \"region\" {\n return target.selection === \"region\" ? \"region\" : \"element\";\n}\n\nexport interface AnnotationPayload {\n schemaVersion: 1;\n capturedAt: string;\n build: BuildInfo;\n page: {\n url: string;\n /** The matched route pattern, not the resolved URL. */\n route: string;\n viewport: { w: number; h: number; dpr: number };\n userAgent: string;\n };\n target: TargetInfo;\n state: {\n consoleErrors: ConsoleEntry[];\n failedRequests: NetworkEntry[];\n featureFlags: Record<string, unknown>;\n };\n screenshot?: { dataUrl: string; region: \"element\" | \"viewport\" };\n /** The human's words, verbatim. The parent frame fills this in; we never rewrite it. */\n comment: string;\n}\n\nexport type ParentToFrame =\n | { type: \"annotate:enable\" }\n | { type: \"annotate:disable\" }\n | { type: \"annotate:ping\" };\n\nexport type FrameToParent =\n | { type: \"annotate:ready\"; sdkVersion: string; build: BuildInfo }\n | { type: \"annotate:hover\"; target: TargetInfo }\n | { type: \"annotate:capture\"; payload: AnnotationPayload }\n | { type: \"annotate:cancel\" };\n\nconst PARENT_TYPES = new Set([\"annotate:enable\", \"annotate:disable\", \"annotate:ping\"]);\n\n/**\n * Is this a message we recognise?\n *\n * Origin is checked separately and first \u2014 a well-formed message from the wrong\n * origin is the case this guard must not be trusted to catch.\n */\nexport function isParentMessage(data: unknown): data is ParentToFrame {\n if (typeof data !== \"object\" || data === null) return false;\n const type = (data as { type?: unknown }).type;\n return typeof type === \"string\" && PARENT_TYPES.has(type);\n}\n\n/**\n * Reject wildcards outright rather than treating them as \"any origin\".\n *\n * `postMessage(msg, \"*\")` broadcasts the payload to whatever happens to be\n * framing us, and an annotation payload carries page content, console errors and\n * failed request URLs. The spec's rule is no wildcards in either direction; this\n * makes it impossible to configure one by accident.\n */\nexport function assertConcreteOrigin(origin: string): string {\n if (!origin || origin === \"*\") {\n throw new Error(\n \"[annotation] a concrete parent origin is required; '*' would broadcast \" +\n \"annotation payloads to any embedder.\",\n );\n }\n return origin;\n}\n\n/**\n * The query parameter that arms the SDK in a managed product, and the cookie\n * that records it.\n *\n * THESE LIVE IN THE PROTOCOL BECAUSE THE TWO HALVES ARE NOW IN DIFFERENT\n * REPOSITORIES. The managed product's middleware looks for the parameter and\n * sets the cookie; the management app appends the parameter and the SDK reads\n * the cookie. While both halves were one monorepo, a test could read the other\n * side's source and assert the strings matched \u2014 that gate does not survive a\n * repository split, and a mismatch produces no error at all: the frame loads,\n * the cookie is never set, the SDK never boots, and the reviewer sees \"waiting\n * for the page to announce itself\" with nothing to go on.\n *\n * So they stop being two spellings that agree and become one exported\n * constant. A managed product that imports this package cannot drift from it.\n */\nexport const ANNOTATION_TOKEN_PARAM = \"ai_annotate\";\nexport const ANNOTATION_ARMED_COOKIE = \"ai_annotate_armed\";\n\n/**\n * The CAPTURE token's query parameter (A-240).\n *\n * -------------------------------------------------------------------------\n * WHY IT IS HERE AND NOT WHERE IT IS WRITTEN\n * -------------------------------------------------------------------------\n *\n * A second name on the same wire, and a second credential \u2014 see\n * `docs/plans/first-party-annotation.md` \u00A7 *Authentication*. `ai_annotate`\n * answers *should this page offer an annotation UI at all*; this one answers\n * *may this capture become an issue*. Different secrets, different lifetimes,\n * different blast radii, and deliberately not folded into one token.\n *\n * It lives beside `ANNOTATION_TOKEN_PARAM` for that constant's own reason,\n * stated at length above: the console WRITES it into a deep link and the\n * product's bundle READS it, the two halves ship from different repositories,\n * and a mismatch produces no error anywhere \u2014 the reviewer writes a post-it,\n * clicks send, and the ingest rejects an unsigned capture. One exported\n * constant is the only thing that makes those two spellings unable to drift.\n *\n * It has no legacy spelling because it has never had another one: nothing has\n * shipped it yet. It is exported here BEFORE its writer needs it precisely so\n * the writer never has a reason to spell it itself.\n */\nexport const CAPTURE_TOKEN_PARAM = \"ai_capture\";\n\n/**\n * The run under review, when the console knows of one (A-241).\n *\n * Third name on the same wire, here for the reason the two above are: the\n * console writes it and the product's bundle reads it, from two repositories.\n * `apps/manager/src/lib/request-changes.ts` spells it itself today and its own\n * comment asks for exactly this \u2014 the constant to move into the package so the\n * writer has no reason to spell it. **That deletion is not this slice's**\n * (A-243/A-244 are merged and settled), so for now there are two definitions of\n * one string, and `protocol.test.ts` is the only thing standing between them\n * and a silent divergence. Nothing about it is signed: the ingest already\n * treats it as attacker-supplied text and files on it rather than routing on it.\n */\nexport const REVIEW_RUN_PARAM = \"ai_run\";\n\n/**\n * The arming cookie, as a `Set-Cookie` value (A-241).\n *\n * ===========================================================================\n * IT GOT SIMPLER BY MOVING FIRST-PARTY, AND THAT IS THE WHOLE POINT\n * ===========================================================================\n *\n * Framed, this had to be `SameSite=None; Secure` with no `Partitioned` \u2014 a\n * genuine cross-site cookie, which A-211 records would have broken on its own\n * under Chrome's third-party cookie work, independently of the auth problem\n * that killed the frame. Top-level at the product's own origin it is an\n * ordinary cookie and none of that applies.\n *\n * Each attribute, and why:\n *\n * - **`SameSite=Lax`.** The reviewer arrives by clicking *Request changes* in\n * the console, which is a top-level cross-site GET navigation \u2014 the one case\n * `Lax` still sends on. `Strict` would withhold the cookie on that very first\n * render, so the toolbar would be missing on the page the link opened and\n * appear only after an internal click; that reads as a broken deploy.\n * - **`Secure`.** It is a marker rather than a credential, but a marker that\n * travels in clear is a marker an attacker on the path can set.\n * - **`Path=/`.** Arming survives the reviewer navigating the whole app, which\n * is the point of a cookie rather than a URL parameter.\n * - **No `Max-Age` / `Expires`.** Session-scoped: closing the tab disarms.\n *\n * ===========================================================================\n * WHAT IS *NOT* HERE: `HttpOnly`, AND THE TOKEN\n * ===========================================================================\n *\n * No `HttpOnly`, deliberately. Page script both READS this (`isAnnotationArmed`\n * decides whether a toolbar exists) and CLEARS it (*Done* disarms). An\n * `HttpOnly` arming cookie makes *Done* impossible from the page, which is the\n * one control the reviewer has.\n *\n * That is affordable only because THE VALUE IS A MARKER AND NEVER THE TOKEN.\n * The value is a constant and there is no argument that supplies one: the\n * middleware compares the URL parameter against its secret and then calls this,\n * and what it stores is *yes*, not *what was presented*. The single parameter\n * is the cookie's NAME, and it exists only so a product mid-migration can write\n * the legacy spelling \u2014 it is not a value slot wearing a different label, and\n * `arming.test.ts` pins which side of the `=` it lands on.\n *\n * This is a shape argument, not a guarantee about callers. Nothing stops a\n * determined caller passing a secret as the name; what the signature removes is\n * the ACCIDENT \u2014 the middleware author with a verified token in hand and one\n * parameter available, reaching for the obvious.\n */\nexport const ANNOTATION_ARMED_VALUE = \"1\";\n\nexport function armedCookie(name: string = ANNOTATION_ARMED_COOKIE): string {\n return `${name}=${ANNOTATION_ARMED_VALUE}; Path=/; SameSite=Lax; Secure`;\n}\n\n/**\n * The same cookie, expired \u2014 what *Done* and a disarm write.\n *\n * `Max-Age=0` AND an `Expires` in the past, because they are read by different\n * things and the pair is what every browser honours. The path must match the\n * one set above or the browser stores a second cookie and the first survives \u2014\n * a *Done* button that does nothing and reports success.\n */\nexport function clearedCookie(name: string): string {\n return `${name}=; Path=/; SameSite=Lax; Secure; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n}\n\n/**\n * The names these used to have (A-98).\n *\n * -------------------------------------------------------------------------\n * WHY THE RENAME, AND WHY IT COULD NOT JUST BE DONE\n * -------------------------------------------------------------------------\n *\n * They were `pits_annotate` and `pits_annotate_armed` \u2014 one managed product's\n * initialism, in a contract every managed product's bundle has to implement.\n * Onboarding a second product meant asking an unrelated codebase to adopt the\n * first product's name in its cookies and its query strings. That is the bug\n * A-98 is about, and it is a bug in a wire protocol rather than in a variable\n * name, which is what makes it expensive.\n *\n * The two halves live in DIFFERENT REPOSITORIES and deploy independently. A\n * clean rename is therefore a breaking change to every product bundle\n * simultaneously, and its failure mode is silence: the frame loads, the cookie\n * is never set, the SDK never boots, and the reviewer sees \"waiting for the\n * page to announce itself\" with nothing to go on. Nothing throws, nothing logs,\n * and the natural reading is that the agent broke the site.\n *\n * -------------------------------------------------------------------------\n * THE MIGRATION: READ BOTH, WRITE BOTH, THEN STOP WRITING THE OLD ONE\n * -------------------------------------------------------------------------\n *\n * 1. **Now.** Every reader accepts either name. Every writer emits BOTH, so a\n * product still running the old bundle arms on the legacy name and one\n * running the new bundle arms on the new one. Deploy order stops mattering,\n * which is the whole point \u2014 there is no window in which a reviewer sees the\n * silent failure above.\n * 2. **After every managed product's bundle is on this version** (A-102). Drop\n * the legacy names from the writers, then from the readers. Two steps, not\n * one: a reader that stops accepting the old name before the writers stop\n * emitting it re-creates the outage in the other direction.\n *\n * The legacy names are exported rather than inlined so the deletion in step 2\n * is `git grep LEGACY_` rather than an archaeology exercise.\n */\nexport const LEGACY_ANNOTATION_TOKEN_PARAM = \"pits_annotate\";\nexport const LEGACY_ANNOTATION_ARMED_COOKIE = \"pits_annotate_armed\";\n\n/**\n * ===========================================================================\n * AND THE NAMES ARE NOT CHANGING AGAIN FOR THE PUBLISHED PACKAGE (A-184)\n * ===========================================================================\n *\n * When this becomes `@weatherboard/annotation` the obvious next thought is that\n * the wire names should match the package \u2014 `weatherboard_annotate`. They\n * should not, and the reason is the migration described above rather than\n * taste.\n *\n * That migration is STILL IN FLIGHT. Readers accept both spellings; writers\n * emit both; step 2 waits on every managed product's bundle being current.\n * A third spelling introduced now does not replace the second, it joins it:\n * every reader accepts three names, every writer emits three, and the window in\n * which a product can be on any of them widens instead of closing.\n *\n * The cost is not the code. It is that each rename buys another instance of the\n * failure this file exists to prevent \u2014 the frame loads, the cookie is never\n * set, the SDK never boots, and the reviewer sees \"waiting for the page to\n * announce itself\" with nothing to go on. Twice is a migration. Three times is\n * a habit, and the third one would be paid by consumers who never asked for\n * either.\n *\n * `ai_annotate` is already product-neutral, which was the entire point of A-98.\n * A published package needs names that name no product; it does not need names\n * that name the package. Nobody reading a query string cares which npm scope\n * the library came from.\n *\n * The one real objection is collision: `ai_` is a common prefix and some other\n * tool could plausibly use it. That is survivable by construction \u2014 arming\n * requires the parameter to carry a token the product compares exactly, so a\n * coincidental `?ai_annotate=` from an unrelated tool arms nothing. A colliding\n * COOKIE cannot false-positive either; `isAnnotationArmed` matches on the\n * delimiter rather than by substring, for exactly this reason.\n *\n * If a rename is ever genuinely warranted, it happens ONCE, after step 2 has\n * landed and the reader accepts one name again \u2014 not layered on top of an\n * unfinished one.\n */\n\n/** Both spellings of the query parameter, current first. */\nexport const ANNOTATION_TOKEN_PARAMS = [\n ANNOTATION_TOKEN_PARAM,\n LEGACY_ANNOTATION_TOKEN_PARAM,\n] as const;\n\n/** Both spellings of the cookie, current first. */\nexport const ANNOTATION_ARMED_COOKIES = [\n ANNOTATION_ARMED_COOKIE,\n LEGACY_ANNOTATION_ARMED_COOKIE,\n] as const;\n\n/**\n * Whether a cookie jar carries the armed cookie, under either name.\n *\n * Delimiter-aware rather than a substring test: a cookie named\n * `not_ai_annotate_armed`, or another cookie whose VALUE contains the name,\n * satisfies `includes` \u2014 and arming an annotation channel on a coincidence is\n * not a failure anyone would think to look for.\n */\nexport function isAnnotationArmed(cookie: string): boolean {\n return cookie\n .split(\";\")\n .some((entry) => {\n const trimmed = entry.trimStart();\n return ANNOTATION_ARMED_COOKIES.some((name) => trimmed.startsWith(`${name}=`));\n });\n}\n\n/**\n * The arming token from a URL's query string, under either name.\n *\n * Current name wins when both are present. They should never disagree \u2014 the\n * management app writes both from one value \u2014 but if they ever do, the one this\n * version of the protocol owns is the one to believe.\n */\nexport function readAnnotationToken(params: URLSearchParams): string | null {\n for (const name of ANNOTATION_TOKEN_PARAMS) {\n const value = params.get(name);\n if (value !== null && value.length > 0) return value;\n }\n return null;\n}\n", "/**\n * The hover highlight, drawn inside the iframe.\n *\n * The parent frame does not draw this, and that is a deliberate call from\n * `spec/02-annotation-sdk.md`: translating coordinates across the frame boundary\n * means tracking the iframe's position, the page's scroll, and any transform on\n * either side, and getting it wrong puts the box next to the thing rather than\n * on it. Inside the frame, `getBoundingClientRect` is already in the right\n * coordinate space.\n */\n\n/**\n * The SDK'\"'\"'s own node, so this is not a cross-repository contract like the\n * cookie and the query parameter are \u2014 nothing outside this package creates it\n * or looks it up. Renamed with them anyway (A-98): a product whose stylesheet\n * happens to target the old id would be styling the overlay by accident, and\n * the new name is the one a reader will expect to find.\n */\nexport const OVERLAY_ID = \"ai-annotation-overlay\";\n\n/** Above app chrome without being a magic number nobody can justify. */\nconst Z_INDEX = \"2147483000\";\n\nexport interface Overlay {\n show(box: { x: number; y: number; w: number; h: number }, label: string | null): void;\n hide(): void;\n destroy(): void;\n /** The overlay's own nodes, so hit-testing can ignore them. */\n contains(node: unknown): boolean;\n}\n\nexport function createOverlay(doc: Document): Overlay {\n const root = doc.createElement(\"div\");\n root.id = OVERLAY_ID;\n // `pointer-events: none` is what keeps the overlay from hit-testing itself:\n // without it, `elementFromPoint` returns the highlight and the highlight\n // follows the highlight.\n root.style.cssText = [\n \"position:fixed\",\n \"top:0\",\n \"left:0\",\n \"pointer-events:none\",\n `z-index:${Z_INDEX}`,\n \"display:none\",\n \"box-sizing:border-box\",\n \"border:2px solid #2563eb\",\n \"background:rgba(37,99,235,0.12)\",\n \"border-radius:2px\",\n ].join(\";\");\n\n const tag = doc.createElement(\"div\");\n tag.style.cssText = [\n \"position:absolute\",\n \"left:0\",\n \"top:-20px\",\n \"font:11px/16px ui-monospace,SFMono-Regular,Menlo,monospace\",\n \"color:#fff\",\n \"background:#2563eb\",\n \"padding:1px 5px\",\n \"border-radius:2px\",\n \"white-space:nowrap\",\n \"max-width:60vw\",\n \"overflow:hidden\",\n \"text-overflow:ellipsis\",\n ].join(\";\");\n root.appendChild(tag);\n doc.body.appendChild(root);\n\n return {\n show(box, label) {\n root.style.display = \"block\";\n root.style.transform = `translate(${box.x}px, ${box.y}px)`;\n root.style.width = `${box.w}px`;\n root.style.height = `${box.h}px`;\n // A box at the very top of the viewport would render its label off-screen.\n tag.style.top = box.y < 20 ? \"100%\" : \"-20px\";\n tag.textContent = label ?? \"\";\n tag.style.display = label ? \"block\" : \"none\";\n },\n hide() {\n root.style.display = \"none\";\n },\n destroy() {\n root.remove();\n },\n contains(node) {\n return node instanceof Node && root.contains(node);\n },\n };\n}\n", "import { NAME_ATTR, SRC_ATTR, type SourceRef, type TargetInfo } from \"./protocol\";\n\n/**\n * Turning a clicked DOM element into something a coding agent can act on.\n *\n * Everything here is written against the narrowest possible slice of the DOM so\n * it can be tested without a browser \u2014 see `ElementLike`. The parts that need a\n * real browser (hit-testing, layout) live in `sdk.ts`.\n */\n\n/**\n * Re-exported, not declared. They moved to `protocol.ts` (A-186) because the\n * build transform in `@weatherboard/annotation-next` writes what this file\n * reads, and the two packages install separately. Kept exported from here so\n * every existing importer is unaffected.\n */\nexport { NAME_ATTR, SRC_ATTR };\n\nexport const MAX_TEXT = 200;\n/** Deep trees are common; a chain long enough to be useless is not worth sending. */\nexport const MAX_CHAIN = 8;\n\n/** The subset of `Element` this module reads. */\nexport interface ElementLike {\n tagName: string;\n id?: string;\n getAttribute(name: string): string | null;\n parentElement: ElementLike | null;\n classList?: { value?: string } | ArrayLike<string>;\n textContent?: string | null;\n}\n\n/**\n * Parse `path/to/File.tsx:42:7` into its parts.\n *\n * Split from the right: the line and column never contain a colon, and a path\n * conceivably might. Returns null rather than a half-parsed ref \u2014 a ref we\n * cannot read is worse than no ref, because downstream will believe it.\n */\nexport function parseSourceRef(value: string | null): SourceRef | null {\n if (!value) return null;\n\n const colIdx = value.lastIndexOf(\":\");\n if (colIdx <= 0) return null;\n const lineIdx = value.lastIndexOf(\":\", colIdx - 1);\n if (lineIdx <= 0) return null;\n\n const file = value.slice(0, lineIdx);\n const line = Number(value.slice(lineIdx + 1, colIdx));\n const column = Number(value.slice(colIdx + 1));\n\n if (!file || !Number.isFinite(line) || !Number.isFinite(column)) return null;\n return { file, line, column };\n}\n\n/**\n * Every source ref from `el` upwards, innermost first.\n *\n * Consecutive duplicates are collapsed: one JSX element often produces several\n * nested DOM nodes carrying the same ref, and repeating it says nothing.\n */\nexport function collectSourceChain(el: ElementLike | null, max = MAX_CHAIN): SourceRef[] {\n const chain: SourceRef[] = [];\n let node: ElementLike | null = el;\n\n while (node && chain.length < max) {\n const ref = parseSourceRef(node.getAttribute(SRC_ATTR));\n if (ref) {\n const prev = chain[chain.length - 1];\n const isRepeat = prev && prev.file === ref.file && prev.line === ref.line;\n if (!isRepeat) chain.push(ref);\n }\n node = node.parentElement;\n }\n\n return chain;\n}\n\n/** The nearest ancestor (or self) carrying a ref, which is the element a human means. */\nexport function findAnnotatedAncestor(el: ElementLike | null): ElementLike | null {\n let node: ElementLike | null = el;\n while (node) {\n if (parseSourceRef(node.getAttribute(SRC_ATTR))) return node;\n node = node.parentElement;\n }\n return null;\n}\n\nfunction classTokens(el: ElementLike): string[] {\n const list = el.classList as { value?: string } | undefined;\n const raw = typeof list?.value === \"string\" ? list.value : \"\";\n return raw.split(/\\s+/).filter(Boolean);\n}\n\n/**\n * A CSS-ish path to the element, used only when no ancestor carries a ref.\n *\n * Deliberately not guaranteed unique. It is a hint for a human reading the\n * payload, not a selector we promise to re-resolve \u2014 StyleX class names are\n * hashed and change between builds, so a path that looks precise would be\n * quietly wrong on the next deploy. IDs are kept because they survive; classes\n * are capped at two for readability.\n */\nexport function domPath(el: ElementLike | null, maxDepth = 6): string {\n const parts: string[] = [];\n let node: ElementLike | null = el;\n\n while (node && parts.length < maxDepth) {\n const tag = (node.tagName || \"\").toLowerCase();\n if (!tag || tag === \"html\") break;\n\n let part = tag;\n if (node.id) {\n // An id is unique on the page, so nothing above it adds information.\n parts.unshift(`${tag}#${node.id}`);\n return parts.join(\" > \");\n }\n const classes = classTokens(node).slice(0, 2);\n if (classes.length > 0) part += `.${classes.join(\".\")}`;\n parts.unshift(part);\n\n if (tag === \"body\") break;\n node = node.parentElement;\n }\n\n return parts.join(\" > \");\n}\n\n/** Collapse whitespace and truncate, so the payload carries a label rather than a page. */\nexport function summariseText(text: string | null | undefined): string | undefined {\n if (!text) return undefined;\n const collapsed = text.replace(/\\s+/g, \" \").trim();\n if (!collapsed) return undefined;\n return collapsed.length > MAX_TEXT ? `${collapsed.slice(0, MAX_TEXT - 1)}\u2026` : collapsed;\n}\n\nexport interface ResolveTargetOptions {\n boundingBox: { x: number; y: number; w: number; h: number };\n}\n\n/**\n * Build the `TargetInfo` for a clicked or hovered element.\n *\n * `source` is the nearest ref, which names the file that writes the markup \u2014\n * for a composed design-system component that is the design-system file, not\n * the call site. `sourceChain` carries the rest so the agent can choose.\n */\nexport function resolveTarget(el: ElementLike, options: ResolveTargetOptions): TargetInfo {\n const annotated = findAnnotatedAncestor(el);\n const chain = collectSourceChain(el);\n const source = chain[0];\n\n const target: TargetInfo = {\n domPath: domPath(el),\n boundingBox: options.boundingBox,\n };\n\n if (source) target.source = source;\n if (chain.length > 0) target.sourceChain = chain;\n\n const componentName = annotated?.getAttribute(NAME_ATTR);\n if (componentName) target.componentName = componentName;\n\n const text = summariseText(el.textContent);\n if (text) target.textContent = text;\n\n return target;\n}\n", "import { assertConcreteOrigin, type FrameToParent } from \"./protocol\";\n\n/**\n * Egress (A-239).\n *\n * ===========================================================================\n * WHY THIS IS A SEAM AND NOT A BRANCH IN `sdk.ts`\n * ===========================================================================\n *\n * `sdk.ts` had exactly one way out \u2014 `win.parent.postMessage(...)` \u2014 and\n * first-party there is no parent: `win.parent === win`. Every capture then\n * posts a message to the page that sent it and nothing receives it. **That\n * fails silently**, which is the shape to be careful of here: the overlay\n * behaves correctly, the reviewer clicks, and no issue is ever filed.\n *\n * See `docs/plans/first-party-annotation.md`. The console stops framing the\n * product and links out to it instead, so the capture has to leave the page\n * over HTTP. The frame is NOT being deleted \u2014 `postMessageTransport` is\n * today's behaviour, extracted unchanged, and it is still the default.\n *\n * ===========================================================================\n * TWO PROPERTIES THAT HAD TO SURVIVE\n * ===========================================================================\n *\n * 1. **Never a wildcard, in either direction.** `assertConcreteOrigin` still\n * guards the message path, and `assertConcreteEndpoint` is its equivalent\n * for HTTP. Both throw AT CONSTRUCTION rather than at first capture,\n * because a misconfiguration that only surfaces when a reviewer finally\n * clicks something is a misconfiguration nobody finds in review.\n *\n * 2. **The screenshot never costs the capture**, and neither may the network.\n * `attachScreenshot` resolves within its own timeout and never rejects, so\n * the payload goes out either way. An HTTP transport would reintroduce\n * exactly the failure mode that was designed out \u2014 a slow network losing a\n * capture \u2014 unless it carries its own timeout and its own `catch`, and\n * unless a failed send is REPORTABLE rather than swallowed. A dropped\n * capture that looks like a successful one is the worst outcome available\n * here, so `send` rejects loudly and `createEgress` routes that to the\n * caller.\n */\n\n/** Every message the page half can emit. */\nexport type FrameMessageKind = FrameToParent[\"type\"];\n\n/**\n * What the far end said about a capture it accepted (A-240).\n *\n * Everything on it is optional and always will be. `postMessageTransport`\n * cannot answer at all \u2014 `postMessage` has no reply \u2014 and the ingest may\n * answer with nothing. A reviewer who is told *sent* and given no link is in a\n * strictly better position than one told nothing, so an absent `issueUrl` is a\n * successful send and never a failed one.\n */\nexport interface SendReceipt {\n /**\n * The issue this capture became, when the far end names one.\n *\n * ANY VALUE HERE ENDS UP IN A THIRD PARTY'S DOM as an `href`, so the type\n * alone is not the contract \u2014 see `safeIssueUrl`, which is what every value\n * crossing the network boundary is put through before it gets to be one.\n */\n issueUrl?: string;\n}\n\n/** Longer than any issue URL and short enough not to be a payload. */\nconst MAX_ISSUE_URL = 2048;\n\n/**\n * A URL from the network that is safe to put in somebody else's page (A-240).\n *\n * ===========================================================================\n * WHY THIS IS NOT PARANOIA\n * ===========================================================================\n *\n * `httpTransport` reads `issueUrl` out of a RESPONSE BODY and the toolbar sets\n * it as an `<a href>` INSIDE THE CUSTOMER'S PAGE. `javascript:alert(1)` there\n * executes in the product's origin the moment the reviewer clicks *View\n * issue* \u2014 a script injection into a third party's site, shipped by us, on\n * every page of every managed product.\n *\n * `assertConcreteEndpoint` does not cover this and was never going to:\n * it constrains WHERE WE SEND, and this is WHAT COMES BACK. An ingest that is\n * misconfigured, compromised, or simply behind a proxy that rewrites bodies is\n * exactly the case a transport has to survive, and \"our own console would\n * never\" is not a security boundary.\n *\n * It lives HERE and not in the toolbar so the guarantee travels with the\n * receipt rather than with one renderer of it. The toolbar calls it too \u2014 see\n * `showResult` \u2014 because it renders receipts from ANY transport, including one\n * a consumer wrote, which never passes through this function.\n *\n * Anything not `http(s)` is dropped rather than repaired. A dropped link is a\n * toast that says the capture was sent and offers no link, which is the\n * outcome the receipt is already designed to survive.\n */\nexport function safeIssueUrl(value: unknown): string | undefined {\n if (typeof value !== \"string\" || value.length === 0 || value.length > MAX_ISSUE_URL) {\n return undefined;\n }\n let url: URL;\n try {\n // Absolute only: `new URL` throws on a relative one, and a relative href in\n // somebody else's page points somewhere in THEIR app.\n url = new URL(value);\n } catch {\n return undefined;\n }\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") return undefined;\n return url.href;\n}\n\nexport interface AnnotationTransport {\n /** For diagnostics and for tests; never sent anywhere. */\n readonly name: string;\n /**\n * May this transport carry this kind of message?\n *\n * The interface needs this because `annotate:hover` fires once per animation\n * frame while the reviewer moves the mouse. Over `postMessage` that is a\n * structured clone to a frame that is already listening; over HTTP it is one\n * request per frame, which is a denial of service aimed at our own console.\n *\n * A transport that DROPPED such a message silently would be the same bug\n * this issue exists to remove, one layer down. So declining is an explicit\n * answer the SDK asks for and respects, and `send` is never called for a\n * kind that was declined.\n */\n carries(kind: FrameMessageKind): boolean;\n /**\n * Deliver the message. May be synchronous.\n *\n * A returned promise MUST reject on failure. Resolving on a failed send is\n * the one thing a transport may not do.\n */\n send(message: FrameToParent): void | Promise<void | SendReceipt>;\n}\n\n/** What went wrong, handed back to whoever constructed the SDK. */\nexport interface EgressFailure {\n message: FrameToParent;\n transport: string;\n error: unknown;\n}\n\nexport type EgressErrorHandler = (failure: EgressFailure) => void;\n\n/**\n * The parent-window transport \u2014 today's behaviour, extracted unchanged.\n *\n * It carries everything: the frame is a live listener on the other end of a\n * cheap channel, and the handshake (`annotate:ready`) and the hover highlight\n * both belong to it.\n */\nexport function postMessageTransport(\n parentOrigin: string,\n win: Pick<Window, \"parent\"> = window,\n): AnnotationTransport {\n const origin = assertConcreteOrigin(parentOrigin);\n return {\n name: \"postMessage\",\n carries: () => true,\n send(message) {\n // Never a wildcard, in either direction.\n win.parent.postMessage(message, origin);\n },\n };\n}\n\n/**\n * Reject anything that is not one concrete `http(s)` URL.\n *\n * `assertConcreteOrigin`'s argument, applied to an endpoint. A relative URL\n * resolves against whatever page the SDK happens to be on \u2014 so a capture\n * POSTs into the CUSTOMER'S OWN app, 404s, and is lost; and a `javascript:`\n * or `data:` endpoint is a script-injection seam handed to whoever controls\n * the deep link. Neither is a thing to discover at first capture.\n */\nexport function assertConcreteEndpoint(endpoint: string): string {\n if (!endpoint || endpoint === \"*\") {\n throw new Error(\n \"[annotation] a concrete ingest endpoint is required; \" +\n \"'*' is not an address and an empty one posts nowhere.\",\n );\n }\n let url: URL;\n try {\n url = new URL(endpoint);\n } catch {\n throw new Error(\n `[annotation] the ingest endpoint must be an absolute URL, got ${JSON.stringify(endpoint)}; ` +\n \"a relative one resolves against the product's own origin and the capture is lost there.\",\n );\n }\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n throw new Error(\n `[annotation] the ingest endpoint must be http(s), got ${url.protocol}`,\n );\n }\n return url.toString();\n}\n\n/** Node's `fetch` and the browser's, narrowed to what we use. */\nexport type FetchLike = (\n input: string,\n init: {\n method: string;\n headers: Record<string, string>;\n body: string;\n credentials?: \"omit\";\n signal?: AbortSignal;\n },\n) => Promise<{\n ok: boolean;\n status: number;\n /**\n * Optional because `serve()`'s loopback responses are read through the\n * platform `fetch`, which has it, while a stub written to assert the `init`\n * need not. A transport that made the body mandatory would be asserting\n * something about the ingest that the ingest does not promise.\n */\n json?: () => Promise<unknown>;\n}>;\n\nexport interface HttpTransportOptions {\n /** The console's ingest URL. Absolute, `http(s)`, never `*`. */\n endpoint: string;\n /**\n * The capture token from the deep link \u2014 signed by the console, carried by\n * the product, never validated by it. See the plan's *Authentication*\n * section for why this is a second credential and not the arming one.\n */\n captureToken: string;\n /**\n * Own timeout, own catch. The default is deliberately short: a capture the\n * reviewer is told failed can be retried, and a request that hangs is\n * indistinguishable from one that worked.\n */\n timeoutMs?: number;\n /** Injectable only so the tests can drive a transport with no `fetch` at all. */\n fetch?: FetchLike;\n}\n\nexport const HTTP_TRANSPORT_TIMEOUT_MS = 8_000;\n\n/**\n * The first-party transport: POST the capture to the console's ingest.\n *\n * It carries `annotate:capture` AND NOTHING ELSE. `annotate:hover` is one\n * request per animation frame, and `annotate:ready` / `annotate:cancel` are\n * handshake with a parent window that does not exist first-party \u2014 sending\n * them would be filing HTTP requests nobody asked for.\n */\nexport function httpTransport(options: HttpTransportOptions): AnnotationTransport {\n const endpoint = assertConcreteEndpoint(options.endpoint);\n if (!options.captureToken) {\n throw new Error(\n \"[annotation] a capture token is required; the ingest rejects an unsigned capture \" +\n \"and the reviewer would only find out after writing one.\",\n );\n }\n const token = options.captureToken;\n const timeoutMs = options.timeoutMs ?? HTTP_TRANSPORT_TIMEOUT_MS;\n const doFetch: FetchLike | undefined = options.fetch ?? globalThis.fetch;\n\n return {\n name: \"http\",\n carries: (kind) => kind === \"annotate:capture\",\n async send(message) {\n if (!doFetch) {\n throw new Error(\"[annotation] no fetch implementation is available for the ingest\");\n }\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n const response = await doFetch(endpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${token}`,\n },\n // Never the reviewer's cookies. The token is the whole credential,\n // and an ambient one would make this endpoint CSRF-shaped.\n credentials: \"omit\",\n body: JSON.stringify(message),\n signal: controller.signal,\n });\n if (!response.ok) {\n throw new Error(`[annotation] the ingest rejected the capture: HTTP ${response.status}`);\n }\n /**\n * The capture is delivered at this point and nothing below may undo\n * that. A body that is empty, not JSON, or JSON of some other shape is\n * an ingest that chose not to name an issue \u2014 telling the reviewer\n * their capture failed because we could not parse a courtesy would be\n * the same lie as a silent drop, pointing the other way.\n */\n try {\n const body = (await response.json?.()) as { issueUrl?: unknown } | undefined;\n // Untrusted, all of it. `safeIssueUrl` returns undefined for a\n // non-string, a relative URL, an over-long one and any scheme that\n // is not http(s) \u2014 so a hostile body degrades to a capture that was\n // sent and named no issue.\n const issueUrl = safeIssueUrl(body?.issueUrl);\n if (issueUrl !== undefined) return { issueUrl };\n } catch {}\n return {};\n } catch (error) {\n if (controller.signal.aborted) {\n throw new Error(`[annotation] the capture timed out after ${timeoutMs}ms`, {\n cause: error,\n });\n }\n throw error;\n } finally {\n clearTimeout(timer);\n }\n },\n };\n}\n\n/**\n * What became of one message (A-240).\n *\n * `createEgress` used to return `void`, which was enough while the only reader\n * of a failure was `console.error`. The composer has to TELL THE REVIEWER \u2014 the\n * issue link on success, a retry on failure \u2014 and it cannot do that from a\n * function that answers nothing. So the outcome is returned as a value, and the\n * promise NEVER REJECTS: a rejection here would be a second way for a lost\n * capture to disappear, one `await`-less caller away from silence.\n *\n * `declined` is its own case rather than a failure, because it is not one:\n * `annotate:hover` over HTTP is a kind the transport correctly refuses, and\n * calling that a failed capture would put a red toast in front of a reviewer\n * who did nothing wrong. It is only a problem if `annotate:capture` is what was\n * declined, and that is the caller's judgement rather than this function's.\n */\nexport type EgressOutcome =\n | { status: \"sent\"; receipt: SendReceipt }\n | { status: \"declined\" }\n | { status: \"failed\"; error: unknown };\n\n/**\n * The SDK's one way out, given a transport.\n *\n * Pure and DOM-free on purpose, so the two things worth arguing about \u2014 that a\n * declined kind is never sent, and that a failed send reaches the caller \u2014 are\n * testable without a browser. `sdk.ts` holds no other egress path.\n */\nexport function createEgress(\n transport: AnnotationTransport,\n onError?: EgressErrorHandler,\n): (message: FrameToParent) => Promise<EgressOutcome> {\n const report = (message: FrameToParent, error: unknown) => {\n const failure: EgressFailure = { message, transport: transport.name, error };\n if (onError) {\n onError(failure);\n return;\n }\n /**\n * No handler is not a licence to swallow it. Until A-240 puts a toast in\n * front of the reviewer, the console is the only surface there is, and a\n * lost capture must leave a trace somewhere.\n */\n console.error(\"[annotation] a capture could not be delivered\", failure);\n };\n\n /**\n * `async`, and the synchronous half still runs synchronously \u2014 everything up\n * to the first `await` does, which is `transport.send` itself. So\n * `postMessageTransport` still posts before this returns, and a transport\n * that throws is still reported before this returns. That is not an\n * incidental detail: `sdk.test.ts` asserts the frame path's handshake has\n * been posted the instant `createAnnotationSdk` returns, and it must stay\n * true, because the frame is not being deleted.\n */\n return async (message: FrameToParent): Promise<EgressOutcome> => {\n if (!transport.carries(message.type)) return { status: \"declined\" };\n try {\n const receipt = await transport.send(message);\n return { status: \"sent\", receipt: receipt ?? {} };\n } catch (error) {\n report(message, error);\n return { status: \"failed\", error };\n }\n };\n}\n", "/**\n * Screenshot capture for the annotation payload (A-31).\n *\n * WHAT THIS ACTUALLY CAPTURES \u2014 READ THIS BEFORE BELIEVING THE FIELD NAME.\n * This is not a page rasteriser. It captures the bitmap of a single\n * `<img>`, `<canvas>` or `<video>` at or around the clicked element, and\n * nothing else. Click a heading and the payload has no screenshot.\n *\n * WHY NOT html2canvas, WHICH THE SPEC SUGGESTS. `spec/02` offers html2canvas or\n * \"`element.toDataURL` via an offscreen canvas where feasible\". html2canvas is a\n * runtime dependency, and this package deliberately has none \u2014 it ships\n * TypeScript source that two repositories compile into their own bundles, so a\n * dependency here is a dependency, a version and a licence in both of them, for\n * a field `spec/02` itself calls a nice-to-have (\"do not block Phase 1 on\n * screenshots; the source ref is what actually matters\"). The platform gives us\n * no way to rasterise arbitrary DOM: `SVGForeignObject` cannot load\n * cross-origin CSS or fonts, and `getDisplayMedia` prompts the user. So we\n * implement the honest subset rather than a convincing-looking approximation,\n * and the spec's server-side fallback \u2014 render `page.url` at `build.gitSha`\n * headless and crop to `target.boundingBox` \u2014 remains the route to a real\n * picture of the page.\n *\n * The subset is worth having on its own: \"this image is the wrong one\" and\n * \"this video poster is stretched\" are common reports, and for those the\n * element's own bitmap is the entire evidence.\n *\n * NOTHING HERE MAY BREAK A CAPTURE. Every entry point resolves rather than\n * rejects. A tainted canvas (any cross-origin image without CORS headers, which\n * is most of them) makes `toDataURL` throw `SecurityError`; a detached element\n * makes `drawImage` throw; a huge bitmap makes it slow. All of those must end\n * with `screenshot` absent from an otherwise complete payload, never with a\n * capture the reviewer loses.\n */\n\n/** The canvas surface this module needs. Narrow so tests need no browser. */\nexport interface CanvasLike {\n width: number;\n height: number;\n getContext(id: \"2d\"): CanvasContextLike | null;\n toDataURL(type?: string, quality?: number): string;\n}\n\nexport interface CanvasContextLike {\n drawImage(source: unknown, dx: number, dy: number, dw: number, dh: number): void;\n}\n\n/**\n * The subset of an element this module reads.\n *\n * Deliberately structural rather than `instanceof HTMLImageElement`: the SDK\n * runs inside an iframe, and an element from another document fails an\n * `instanceof` check against this realm's constructors even when it is exactly\n * the thing we want.\n */\nexport interface CaptureCandidate {\n tagName: string;\n parentElement?: CaptureCandidate | null;\n querySelector?: (selector: string) => CaptureCandidate | null;\n /** `<img>` */\n naturalWidth?: number;\n naturalHeight?: number;\n complete?: boolean;\n /** `<video>` */\n videoWidth?: number;\n videoHeight?: number;\n /** `<canvas>` */\n width?: number;\n height?: number;\n}\n\nexport type Screenshot = { dataUrl: string; region: \"element\" | \"viewport\" };\n\n/**\n * The byte budget for the encoded data URL.\n *\n * `apps/manager/src/lib/feedback/validate.ts` rejects the whole payload over\n * `MAX_PAYLOAD_BYTES` (128KB) and names screenshots as the usual cause. 48KB\n * leaves the rest of the payload \u2014 twenty console entries with stacks, twenty\n * failed requests, a source chain \u2014 room to be large without the server\n * refusing a report the reviewer has already written. A data URL is base64\n * ASCII, so `dataUrl.length` is its byte cost in the serialised payload, near\n * enough to compare directly.\n *\n * Over budget we drop the screenshot. Emitting it anyway would trade a payload\n * with no picture for a payload the server 413s, which loses the source ref \u2014\n * the only part that actually matters.\n */\nexport const MAX_SCREENSHOT_BYTES = 48 * 1024;\n\n/**\n * Longest edge, in device-independent pixels, of the downscaled capture.\n *\n * A 2000px-wide hero image encodes to well over the budget at any quality worth\n * looking at. 480px is enough to see that the wrong photo is on the card, which\n * is the question this field answers.\n */\nexport const MAX_SCREENSHOT_EDGE = 480;\n\n/**\n * A ceiling on how long we will wait, not on how long the work takes.\n *\n * `toDataURL` is synchronous and cannot be interrupted; this bounds the wait so\n * a pathological encode does not hold the capture open indefinitely, and the\n * payload goes out without the screenshot instead. Said plainly because a\n * timeout that cannot cancel its work is the kind of thing a reader assumes is\n * stronger than it is.\n */\nexport const SCREENSHOT_TIMEOUT_MS = 250;\n\n/**\n * Attempts, in order. The second is a genuine retry at a worse picture rather\n * than a give-up: a slightly muddy 240px thumbnail still answers \"is that the\n * right image\", and a dropped screenshot answers nothing.\n */\nconst ATTEMPTS: ReadonlyArray<{ edge: number; quality: number }> = [\n { edge: MAX_SCREENSHOT_EDGE, quality: 0.7 },\n { edge: MAX_SCREENSHOT_EDGE / 2, quality: 0.5 },\n];\n\n/**\n * WebP first, JPEG second. PNG is never asked for: it is lossless, and a\n * lossless photograph is exactly the thing that blows the byte budget. A canvas\n * that does not know a requested type silently returns PNG, so the result is\n * checked by prefix rather than trusted.\n */\nconst FORMATS = [\"image/webp\", \"image/jpeg\"] as const;\n\nexport interface ScreenshotOptions {\n /**\n * Off switch. `spec/02` treats screenshots as optional, so a host that would\n * rather not encode bitmaps on click \u2014 or that serves images this SDK would\n * only ever taint \u2014 sets this to `false`.\n */\n enabled?: boolean;\n maxBytes?: number;\n maxEdge?: number;\n timeoutMs?: number;\n /**\n * Canvas factory. Injected so the tests can drive the failure paths \u2014 taint,\n * an oversized encode, a context that is not there \u2014 none of which are\n * reachable from a fake DOM object alone.\n */\n createCanvas?: () => CanvasLike | null;\n}\n\ninterface ResolvedOptions {\n enabled: boolean;\n maxBytes: number;\n maxEdge: number;\n timeoutMs: number;\n createCanvas: () => CanvasLike | null;\n}\n\nexport function resolveScreenshotOptions(\n options: ScreenshotOptions | false | undefined,\n doc?: Document,\n): ResolvedOptions {\n /**\n * DEFAULT ON. The subset only fires on media elements, costs one downscaled\n * encode, and cannot enlarge a payload past the budget above; the failure\n * mode of defaulting off is that nobody remembers the flag exists and the\n * field is dead code. Hosts that do not want it pass `screenshot: false`.\n */\n const opts = options === false ? { enabled: false } : (options ?? {});\n return {\n enabled: opts.enabled ?? true,\n maxBytes: opts.maxBytes ?? MAX_SCREENSHOT_BYTES,\n maxEdge: opts.maxEdge ?? MAX_SCREENSHOT_EDGE,\n timeoutMs: opts.timeoutMs ?? SCREENSHOT_TIMEOUT_MS,\n createCanvas: opts.createCanvas ?? (() => doc?.createElement(\"canvas\") ?? null),\n };\n}\n\nconst MEDIA_TAGS = new Set([\"IMG\", \"CANVAS\", \"VIDEO\"]);\nconst MEDIA_SELECTOR = \"img,canvas,video\";\n/** How far up the tree to look. A card wraps its image; a page does not. */\nconst MAX_ANCESTOR_HOPS = 4;\n\nexport function isMediaElement(el: CaptureCandidate | null | undefined): boolean {\n return !!el && MEDIA_TAGS.has(el.tagName.toUpperCase());\n}\n\n/**\n * The nearest thing with a bitmap: the element itself, then a descendant, then\n * an ancestor.\n *\n * Descendant before ancestor because a click lands on whatever is topmost, and\n * on a card that is usually the overlay or the link wrapping the photo \u2014 the\n * photo the reviewer means is inside it, not above it.\n */\nexport function findCaptureSource(\n el: CaptureCandidate | null,\n maxHops = MAX_ANCESTOR_HOPS,\n): CaptureCandidate | null {\n if (!el) return null;\n if (isMediaElement(el)) return el;\n\n const descendant = el.querySelector?.(MEDIA_SELECTOR) ?? null;\n if (isMediaElement(descendant)) return descendant;\n\n let node = el.parentElement ?? null;\n for (let hops = 0; node && hops < maxHops; hops++) {\n if (isMediaElement(node)) return node;\n node = node.parentElement ?? null;\n }\n return null;\n}\n\n/**\n * The source's own pixel dimensions, or null if it has none.\n *\n * A broken `<img>` reports `naturalWidth === 0`, and that is the honest answer:\n * there is no bitmap to capture. The report still carries the source ref and\n * the DOM path, which is what a \"this image is broken\" ticket needs anyway.\n */\nexport function intrinsicSize(el: CaptureCandidate): { w: number; h: number } | null {\n const tag = el.tagName.toUpperCase();\n const pair =\n tag === \"IMG\"\n ? { w: el.naturalWidth, h: el.naturalHeight }\n : tag === \"VIDEO\"\n ? { w: el.videoWidth, h: el.videoHeight }\n : { w: el.width, h: el.height };\n\n const w = Math.floor(pair.w ?? 0);\n const h = Math.floor(pair.h ?? 0);\n if (!Number.isFinite(w) || !Number.isFinite(h) || w < 1 || h < 1) return null;\n if (tag === \"IMG\" && el.complete === false) return null;\n return { w, h };\n}\n\n/** Fit inside `maxEdge` without ever upscaling \u2014 enlarging invents detail. */\nexport function fitWithin(\n size: { w: number; h: number },\n maxEdge: number,\n): { w: number; h: number } {\n const scale = Math.min(1, maxEdge / Math.max(size.w, size.h));\n return { w: Math.max(1, Math.round(size.w * scale)), h: Math.max(1, Math.round(size.h * scale)) };\n}\n\n/**\n * Encode one attempt. Returns null for every failure, including the ones the\n * platform signals by throwing.\n */\nfunction encode(\n source: CaptureCandidate,\n size: { w: number; h: number },\n quality: number,\n options: ResolvedOptions,\n): string | null {\n let canvas: CanvasLike | null;\n try {\n canvas = options.createCanvas();\n } catch {\n return null;\n }\n if (!canvas) return null;\n\n try {\n canvas.width = size.w;\n canvas.height = size.h;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return null;\n ctx.drawImage(source, 0, 0, size.w, size.h);\n\n for (const format of FORMATS) {\n // `toDataURL` throws `SecurityError` the moment the canvas is tainted by\n // a cross-origin bitmap, which is the single most likely outcome on a\n // real page. Per-format so a WebP-less browser still gets its JPEG.\n let dataUrl: string;\n try {\n dataUrl = canvas.toDataURL(format, quality);\n } catch {\n return null;\n }\n if (typeof dataUrl !== \"string\" || !dataUrl.startsWith(`data:${format}`)) continue;\n if (dataUrl.length <= options.maxBytes) return dataUrl;\n // Over budget in this format; a different container will not save an\n // encode this large, so let the next, smaller attempt try instead.\n return null;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Capture synchronously, or return null. Exported for the tests, which need to\n * assert the drop decisions without a timer in the way.\n */\nexport function captureScreenshotSync(\n el: CaptureCandidate | null,\n options: ResolvedOptions,\n): Screenshot | null {\n if (!options.enabled) return null;\n\n const source = findCaptureSource(el);\n if (!source) return null;\n\n const natural = intrinsicSize(source);\n if (!natural) return null;\n\n for (const attempt of ATTEMPTS) {\n const size = fitWithin(natural, Math.min(options.maxEdge, attempt.edge));\n const dataUrl = encode(source, size, attempt.quality, options);\n /**\n * Always `\"element\"`, never `\"viewport\"`. The protocol allows both, but\n * this implementation has no way to rasterise a viewport, and a field that\n * claimed otherwise would send a reviewer looking for page context that is\n * not in the image. `\"viewport\"` is reserved for the server-side renderer.\n */\n if (dataUrl) return { dataUrl, region: \"element\" };\n }\n return null;\n}\n\n/**\n * The entry point the SDK calls. Resolves to a screenshot or to null, within\n * `timeoutMs`, and never rejects.\n */\nexport function captureScreenshot(\n el: CaptureCandidate | null,\n options: ResolvedOptions,\n): Promise<Screenshot | null> {\n if (!options.enabled) return Promise.resolve(null);\n\n return new Promise<Screenshot | null>((resolve) => {\n let settled = false;\n const settle = (value: Screenshot | null) => {\n if (settled) return;\n settled = true;\n resolve(value);\n };\n\n const timer = setTimeout(() => settle(null), options.timeoutMs);\n // `unref` where it exists: a pending timer must not keep a Node test\n // process alive after the payload has already gone out.\n (timer as unknown as { unref?: () => void }).unref?.();\n\n // A microtask, not a straight call: it keeps this function's own contract\n // uniform (always async) so the SDK has one code path, not two.\n void Promise.resolve()\n .then(() => captureScreenshotSync(el, options))\n .then(settle, () => settle(null))\n .finally(() => clearTimeout(timer));\n });\n}\n\n/**\n * Merge a capture into a payload, or hand the payload back untouched.\n *\n * The payload goes in already complete and comes out still complete. The\n * failure mode this shape exists to make impossible is a screenshot problem\n * costing the reviewer the source ref \u2014 so the payload is never awaited into\n * existence, only decorated, and `screenshot` is added as a key or not at all\n * rather than set to `undefined` (which `JSON.stringify` drops anyway, but\n * which reads as \"we tried and got nothing\" to anyone inspecting the object).\n */\nexport async function attachScreenshot<T extends object>(\n payload: T,\n el: CaptureCandidate | null,\n options: ResolvedOptions,\n): Promise<T & { screenshot?: Screenshot }> {\n let screenshot: Screenshot | null;\n try {\n screenshot = await captureScreenshot(el, options);\n } catch {\n // `captureScreenshot` does not reject; this is for a host-supplied\n // `createCanvas` that manages to break the chain in some way it cannot.\n screenshot = null;\n }\n return screenshot ? { ...payload, screenshot } : payload;\n}\n", "import {\n SDK_VERSION,\n assertConcreteOrigin,\n isParentMessage,\n type AnnotationPayload,\n type BuildInfo,\n type TargetInfo,\n} from \"./protocol\";\nimport { createOverlay, type Overlay } from \"./overlay\";\nimport { resolveTarget } from \"./target\";\nimport {\n createEgress,\n postMessageTransport,\n type AnnotationTransport,\n type EgressErrorHandler,\n type EgressOutcome,\n} from \"./transport\";\nimport {\n attachScreenshot,\n resolveScreenshotOptions,\n type CaptureCandidate,\n type ScreenshotOptions,\n} from \"./screenshot\";\n\n/**\n * The annotation SDK (A-36).\n *\n * Lives in the product bundle, listens for the management app's instructions,\n * and turns a click into a payload pinned to a code location.\n */\n\nexport interface AnnotationState {\n consoleErrors: AnnotationPayload[\"state\"][\"consoleErrors\"];\n failedRequests: AnnotationPayload[\"state\"][\"failedRequests\"];\n featureFlags: Record<string, unknown>;\n}\n\n/**\n * What the reviewer clicked, handed to whoever collects the words (A-240).\n *\n * The target is the RESOLVED one, taken from the payload that was already\n * assembled \u2014 not the element, and not something to re-read. See `onClick` for\n * why that distinction is the whole of this feature's correctness.\n */\nexport interface CommentRequest {\n target: TargetInfo;\n /** Viewport coordinates of the click, so a composer can sit near it. */\n point: { x: number; y: number };\n}\n\n/**\n * Ask the human for their words (A-240).\n *\n * Resolves with the comment, or with `null` for cancel \u2014 and cancel means\n * NOTHING IS SENT, which is why it is a distinct value rather than an empty\n * string. An empty string is a reviewer who had nothing to add; `null` is a\n * reviewer who changed their mind, and filing an issue for the second is the\n * kind of thing that gets an annotation tool switched off.\n *\n * Absent, the payload keeps `comment: \"\"` and the frame path is unchanged: the\n * parent window is still the thing that collects the words there.\n */\nexport type CommentCollector = (request: CommentRequest) => Promise<string | null>;\n\nexport interface AnnotationSdkOptions {\n /**\n * The management app's origin, when there is one. Concrete \u2014 see\n * `assertConcreteOrigin`. Messages from anywhere else are ignored, and we\n * never post to anywhere else.\n *\n * OPTIONAL SINCE A-240, and only because first-party there is no parent to\n * listen to. It used to be required for a reason that survives: the inbound\n * `message` listener needs a concrete origin to compare against, and a\n * listener with nothing to compare against is one that accepts instructions\n * from any page that can reach this one. So the resolution is not a default\n * origin \u2014 it is NO LISTENER AT ALL. Omitting this makes the SDK deaf to\n * `postMessage`, which is exactly right for a page with no parent, and it is\n * the only construction in which that is true.\n *\n * Supply it and the frame path is bit-for-bit what it was.\n */\n parentOrigin?: string;\n build: BuildInfo;\n /**\n * The matched route pattern, e.g. `/pub/[slug]`.\n *\n * Injected rather than derived: only the app knows its own route patterns,\n * and `location.pathname` is the resolved URL, which is what the spec\n * explicitly does not want \u2014 `/pub/the-eagle` and `/pub/the-crown` are the\n * same route and should group as one.\n */\n getRoute: () => string;\n /**\n * Page state at capture time. Injected so A-35's ring buffers can supply the\n * real thing without this module knowing how they are collected.\n */\n getState: () => AnnotationState;\n /**\n * Screenshot capture (A-31). Omit for the default, or pass `false` to turn it\n * off entirely. See `screenshot.ts` for what it can and cannot capture \u2014 the\n * short version is that it captures a media element's own bitmap and nothing\n * else, so most clicks produce no screenshot at all, by design.\n */\n screenshot?: ScreenshotOptions | false;\n /**\n * How a capture leaves the page (A-239).\n *\n * Defaults to `postMessageTransport(parentOrigin)`, which is what this SDK\n * has always done. Pass `httpTransport(...)` when the product is being\n * annotated first-party, where `win.parent === win` and a posted message is\n * received by nobody \u2014 see `transport.ts` and\n * `docs/plans/first-party-annotation.md`.\n */\n transport?: AnnotationTransport;\n /**\n * Called when a message could not be delivered.\n *\n * The frame path effectively never fails, so this is new surface for the\n * HTTP one, and it exists because the alternative is a dropped capture that\n * looks like a successful one. With no handler the failure is logged rather\n * than swallowed; A-240 is what puts it in front of the reviewer.\n */\n onEgressError?: EgressErrorHandler;\n /**\n * Collect the reviewer's words after a click (A-240).\n *\n * Omit it and the SDK behaves exactly as it did: `comment: \"\"`, and the\n * parent frame merges the human's words in.\n */\n collectComment?: CommentCollector;\n /**\n * What became of the capture, plus a thunk that sends the SAME payload again.\n *\n * The retry re-sends what was captured rather than re-capturing: the reviewer\n * has moved on by then \u2014 a dialog is open, a row is selected, the page has\n * scrolled \u2014 and re-reading it would file an issue about a screen nobody\n * complained about.\n */\n onCaptureResult?: (outcome: EgressOutcome, retry: () => void) => void;\n /**\n * Elements annotation mode must pretend it cannot see.\n *\n * The toolbar renders INTO THE PAGE IT IS ANNOTATING, so with annotation on,\n * a click on *Stop* is a click on the page and would be captured \u2014 filing an\n * issue about our own button and swallowing the press that was meant to end\n * the session. Shadow DOM makes it worse rather than better: retargeting\n * reports the event's target as the shadow HOST, so the capture would not\n * even name the button.\n *\n * Injected rather than hard-coded to an id, because a hard-coded id is a\n * second definition of who owns that node.\n */\n ignoreElement?: (el: Element) => boolean;\n}\n\nexport interface AnnotationSdk {\n enable(): void;\n disable(): void;\n isEnabled(): boolean;\n destroy(): void;\n}\n\nconst EMPTY_STATE: AnnotationState = {\n consoleErrors: [],\n failedRequests: [],\n featureFlags: {},\n};\n\n/** State until A-35 lands its ring buffers. Empty is honest; invented is not. */\nexport const emptyAnnotationState = (): AnnotationState => ({ ...EMPTY_STATE });\n\nexport function createAnnotationSdk(options: AnnotationSdkOptions): AnnotationSdk {\n const parentOrigin =\n options.parentOrigin === undefined ? null : assertConcreteOrigin(options.parentOrigin);\n const win = window;\n const doc = win.document;\n const screenshotOptions = resolveScreenshotOptions(options.screenshot, doc);\n\n let enabled = false;\n let overlay: Overlay | null = null;\n let hoverFrame = 0;\n let lastHovered: Element | null = null;\n /**\n * A capture whose words are still being written.\n *\n * While this is true the page is frozen as far as annotation is concerned:\n * no second capture, no hover highlight, and Escape belongs to the composer\n * rather than to annotation mode. Without it, a reviewer clicking twice ends\n * up with two composers over one page and no way to tell which is which.\n */\n let collecting = false;\n\n /**\n * One way out, and it is the transport's. A transport that declines a kind\n * (the HTTP one declines `annotate:hover`, which fires once per animation\n * frame) is respected here rather than left to drop it silently.\n */\n const transport =\n options.transport ?? (parentOrigin === null ? null : postMessageTransport(parentOrigin, win));\n if (transport === null) {\n /**\n * At construction, not at first capture \u2014 `transport.ts`'s argument,\n * applied one level up. Neither a parent origin nor a transport is a page\n * with no way out at all, and every capture would be assembled correctly\n * and dropped.\n */\n throw new Error(\n \"[annotation] the SDK needs a `transport` or a `parentOrigin`; with neither there is \" +\n \"nowhere for a capture to go and every one of them would be lost in silence.\",\n );\n }\n const post = createEgress(transport, options.onEgressError);\n\n const boxOf = (el: Element) => {\n const r = el.getBoundingClientRect();\n return { x: r.x, y: r.y, w: r.width, h: r.height };\n };\n\n const targetFor = (el: Element): TargetInfo =>\n resolveTarget(el, { boundingBox: boxOf(el) });\n\n const labelFor = (target: TargetInfo): string | null => {\n if (target.componentName && target.source) {\n const file = target.source.file.split(\"/\").pop() ?? target.source.file;\n return `${target.componentName} \u00B7 ${file}:${target.source.line}`;\n }\n if (target.source) return `${target.source.file}:${target.source.line}`;\n return target.componentName ?? null;\n };\n\n const onMouseMove = (event: MouseEvent) => {\n if (!enabled || collecting) return;\n // Coalesce to one hit-test per frame: mousemove fires far faster than\n // anything downstream can use, and each one costs a layout read.\n if (hoverFrame) return;\n hoverFrame = win.requestAnimationFrame(() => {\n hoverFrame = 0;\n const el = doc.elementFromPoint(event.clientX, event.clientY);\n if (!el || (overlay && overlay.contains(el))) return;\n // Our own toolbar is not part of the product under review.\n if (options.ignoreElement?.(el)) return;\n if (el === lastHovered) return;\n lastHovered = el;\n\n const target = targetFor(el);\n overlay?.show(target.boundingBox, labelFor(target));\n void post({ type: \"annotate:hover\", target });\n });\n };\n\n const buildPayload = (el: Element, state: AnnotationState): AnnotationPayload => ({\n schemaVersion: 1,\n capturedAt: new Date().toISOString(),\n build: options.build,\n page: {\n url: win.location.href,\n route: options.getRoute(),\n viewport: {\n w: win.innerWidth,\n h: win.innerHeight,\n dpr: win.devicePixelRatio,\n },\n userAgent: win.navigator.userAgent,\n },\n target: targetFor(el),\n state: {\n consoleErrors: state.consoleErrors,\n failedRequests: state.failedRequests,\n featureFlags: state.featureFlags,\n },\n // The parent frame collects the human's words and merges them in verbatim.\n comment: \"\",\n });\n\n /**\n * Deliver one capture, and report what became of it.\n *\n * `retry` is this same function over the same message, which is what makes\n * the composer's retry button honest: it sends the capture that was taken at\n * the moment of the click, not a fresh reading of a page the reviewer has\n * since moved on from.\n */\n const deliver = (payload: AnnotationPayload) => {\n const message = { type: \"annotate:capture\", payload } as const;\n const send = () => {\n void post(message).then((outcome) => options.onCaptureResult?.(outcome, send));\n };\n send();\n };\n\n const onClick = (event: MouseEvent) => {\n if (!enabled) return;\n const el = event.target;\n if (!(el instanceof Element)) return;\n /**\n * BEFORE `preventDefault`. The toolbar's own buttons are clicks like any\n * other, and suppressing them would leave *Stop* unable to stop anything.\n */\n if (options.ignoreElement?.(el)) return;\n\n /**\n * Capture phase, and stop it dead. Annotation mode is pointed at a live\n * product: an un-prevented click navigates away, submits a form, or opens a\n * dialog, and the payload describes a page the reviewer is no longer on.\n */\n event.preventDefault();\n event.stopPropagation();\n\n /**\n * ===================================================================\n * ONE COMPOSER AT A TIME \u2014 AND *AFTER* THE SUPPRESSION ABOVE (A-241)\n * ===================================================================\n *\n * A second click while the first post-it is open would put two of them\n * over one page with no way to tell which is which, so it is dropped. The\n * ORDER is the fix: this test used to run BEFORE `preventDefault` /\n * `stopPropagation`, which meant that while the composer was open every\n * click on the product was live. A reviewer half-way through typing who\n * clicked a link navigated away and lost every word of it \u2014 the exact\n * outcome the two guards above exist to prevent, switched off at precisely\n * the moment there is something to lose.\n *\n * Returning here and not earlier means the click is swallowed and nothing\n * else happens, which is what \"the page is frozen while I write\" should\n * mean. The toolbar's own buttons are unaffected: `ignoreElement` returns\n * above this, so Cancel, Send and Escape still work while collecting.\n */\n if (collecting) return;\n\n /**\n * The payload is assembled *now*, synchronously, and the screenshot is\n * merged into it afterwards. Reading page state after an await would read a\n * page the click has already changed, and the whole point of the capture is\n * that it describes the moment of the click.\n */\n const payload = buildPayload(el, options.getState());\n\n /**\n * ===================================================================\n * CAPTURE FIRST, THEN ASK FOR WORDS, THEN MERGE (A-240)\n * ===================================================================\n *\n * The order above is the requirement and this is the line that keeps it.\n * `buildPayload` has ALREADY RUN, synchronously, before anything can\n * `await`; the composer is opened with the target that payload resolved,\n * and the reviewer's answer is merged into it afterwards. Nothing below\n * re-reads the page.\n *\n * Inverting it \u2014 open the composer, then build the payload when Send is\n * pressed \u2014 is the tempting shape, and it captures a page that has had a\n * human typing into it for thirty seconds: a different scroll position, a\n * different route if anything auto-navigated, and console errors the\n * composer itself provoked. The screenshot would be of the post-it.\n */\n const collect = options.collectComment;\n\n if (!collect) {\n /**\n * THE FRAME PATH, UNCHANGED \u2014 including its TIMING.\n *\n * Not an optimisation and not duplication for its own sake. Folding this\n * into the `Promise.all` below would push a screenshot-less capture from\n * SYNCHRONOUS to a microtask later, and `sdk.test.ts` asserts the\n * handshake and the capture have both been posted the instant the click\n * handler returns. A frame that is not being deleted is a frame whose\n * behaviour is not being edited either.\n */\n if (!screenshotOptions.enabled) {\n deliver(payload);\n return;\n }\n void attachScreenshot(payload, el as unknown as CaptureCandidate, screenshotOptions).then(\n (withShot) => deliver(withShot),\n () => deliver(payload),\n );\n return;\n }\n\n const point = { x: event.clientX, y: event.clientY };\n\n /**\n * The screenshot is a nice-to-have and is never allowed to cost us the\n * capture: `captureScreenshot` resolves within its own timeout and never\n * rejects, and the `catch` is belt-and-braces for a caller-supplied\n * `createCanvas` that manages to break the promise chain itself. Either way\n * the payload goes out \u2014 with `screenshot` absent.\n *\n * It runs ALONGSIDE the composer rather than before it. Sequencing them\n * would leave the reviewer looking at an unchanged page for as long as the\n * screenshot takes, which reads as a click that did nothing.\n */\n const captured: Promise<AnnotationPayload> = screenshotOptions.enabled\n ? attachScreenshot(payload, el as unknown as CaptureCandidate, screenshotOptions).then(\n (withShot) => withShot,\n () => payload,\n )\n : Promise.resolve(payload);\n\n /**\n * ===================================================================\n * A COLLECTOR THAT REJECTS MUST NOT WEDGE ANNOTATION MODE\n * ===================================================================\n *\n * `collectComment` is a public, caller-supplied option, so it can reject \u2014\n * and the first version of this let that rejection go unhandled AND left\n * `collecting` true forever. Every later click and every hover highlight\n * was then silently dead for the rest of the session, with the toolbar\n * still reading *Stop annotating* and nothing reported anywhere. That is\n * the exact failure this package is built to not have, reintroduced by the\n * feature that was meant to remove it.\n *\n * So the rejection is caught, turned into \"no words\", and reported through\n * `onCaptureResult` \u2014 and the retry is REAL: `ask` runs the collector\n * again over the SAME already-captured payload, so nothing re-reads a page\n * the reviewer has since moved on from. `attachScreenshot` is defended by\n * its own two-armed `then` for the same reason, one line up.\n */\n const ask = () => {\n collecting = true;\n let failure: { error: unknown } | null = null;\n const words = collect({ target: payload.target, point }).then(\n (comment) => comment,\n (error: unknown) => {\n failure = { error };\n return null;\n },\n );\n\n void Promise.all([captured, words]).then(([finished, comment]) => {\n // Cleared BEFORE anything is reported, so a retry invoked from the\n // handler starts from a page that is not still marked as busy.\n collecting = false;\n if (failure !== null) {\n options.onCaptureResult?.({ status: \"failed\", error: failure.error }, ask);\n return;\n }\n // Cancel sends nothing. Not an empty capture \u2014 nothing.\n if (comment === null) return;\n deliver(comment === \"\" ? finished : { ...finished, comment });\n });\n };\n\n ask();\n };\n\n const onKeyDown = (event: KeyboardEvent) => {\n // While the composer is open, Escape is the composer's \u2014 it cancels the\n // post-it. Leaving annotation mode as well would throw away the words and\n // the toggle in one keystroke.\n if (!enabled || collecting || event.key !== \"Escape\") return;\n void post({ type: \"annotate:cancel\" });\n disable();\n };\n\n function enable() {\n if (enabled) return;\n enabled = true;\n overlay ??= createOverlay(doc);\n doc.addEventListener(\"mousemove\", onMouseMove, true);\n doc.addEventListener(\"click\", onClick, true);\n doc.addEventListener(\"keydown\", onKeyDown, true);\n }\n\n function disable() {\n if (!enabled) return;\n enabled = false;\n lastHovered = null;\n if (hoverFrame) {\n win.cancelAnimationFrame(hoverFrame);\n hoverFrame = 0;\n }\n doc.removeEventListener(\"mousemove\", onMouseMove, true);\n doc.removeEventListener(\"click\", onClick, true);\n doc.removeEventListener(\"keydown\", onKeyDown, true);\n overlay?.hide();\n }\n\n const onMessage = (event: MessageEvent) => {\n // Origin first, shape second. A well-formed message from the wrong origin is\n // exactly what this check exists to stop.\n if (event.origin !== parentOrigin) return;\n if (!isParentMessage(event.data)) return;\n\n switch (event.data.type) {\n case \"annotate:enable\":\n enable();\n break;\n case \"annotate:disable\":\n disable();\n break;\n case \"annotate:ping\":\n void post({ type: \"annotate:ready\", sdkVersion: SDK_VERSION, build: options.build });\n break;\n }\n };\n\n /**\n * ONLY when there is a parent whose origin we can name.\n *\n * `onMessage` compares `event.origin` against `parentOrigin`, so with no\n * parent origin there is nothing to compare against. Registering anyway and\n * letting the `!==` fail on `null` would work today and is the wrong shape:\n * it leaves a listener whose safety depends on a comparison against a value\n * that means \"no comparison\". First-party there is no parent, so there is no\n * listener \u2014 which is also one fewer way into a page we do not own.\n */\n if (parentOrigin !== null) win.addEventListener(\"message\", onMessage);\n\n // Announce ourselves: the parent may have finished loading before we did, in\n // which case its handshake ping has already been and gone.\n void post({ type: \"annotate:ready\", sdkVersion: SDK_VERSION, build: options.build });\n\n return {\n enable,\n disable,\n isEnabled: () => enabled,\n destroy() {\n disable();\n if (parentOrigin !== null) win.removeEventListener(\"message\", onMessage);\n overlay?.destroy();\n overlay = null;\n },\n };\n}\n", "import {\n ANNOTATION_ARMED_COOKIES,\n ANNOTATION_TOKEN_PARAMS,\n CAPTURE_TOKEN_PARAM,\n REVIEW_RUN_PARAM,\n clearedCookie,\n isAnnotationArmed,\n type BuildInfo,\n} from \"./protocol\";\n\n/**\n * Arming, and the production kill-switch (A-241).\n *\n * ===========================================================================\n * THE SEQUENCE THIS FILE IS\n * ===========================================================================\n *\n * The reviewer lands on `?ai_annotate=<arming>&ai_capture=<signed>&ai_run=<id>`\n * from the console's *Request changes* button. This module is what happens in\n * the first few milliseconds after that:\n *\n * 1. read the three parameters\n * 2. **strip them from the URL** with `replaceState`, before anything else\n * 3. keep the capture token for the tab\n * 4. answer *is this page armed* \u2014 from the COOKIE, never from the URL\n *\n * Step 4 is the runtime half of the kill-switch and step 2 is not decoration;\n * both are argued at length below.\n *\n * ===========================================================================\n * TWO CREDENTIALS, AND THIS FILE TOUCHES THEM DIFFERENTLY ON PURPOSE\n * ===========================================================================\n *\n * `docs/plans/first-party-annotation.md` \u00A7 *Authentication* is the argument;\n * what it means for this file is a table:\n *\n * | | who verifies it | what this module does with it |\n * | --- | --- | --- |\n * | arming token (`ai_annotate`) | the product's own middleware, against its `ANNOTATION_TOKEN_<PRODUCT>` | **nothing.** Reads the middleware's answer out of the cookie and drops the token |\n * | capture token (`ai_capture`) | the console, on the way back in | couriers it: keeps it for the tab, hands it to `httpTransport` |\n *\n * **The URL parameter is not arming and this module never treats it as such.**\n * That is the runtime gate, stated as a single sentence, and it is the reason\n * `readArming` does not compare anything: page script holds no secret, so a\n * page that armed itself because a token-shaped string was in its own query\n * string would be armed by anyone who could type one. The middleware compares\n * and sets a cookie; this reads the cookie. A wrong token therefore produces no\n * cookie, and no cookie produces no toolbar \u2014 `arming.test.ts` asserts exactly\n * that, and asserts it with a token-bearing URL present so it cannot pass by\n * accident.\n *\n * ===========================================================================\n * WHY THE TOKENS COME OUT OF THE URL IMMEDIATELY\n * ===========================================================================\n *\n * A capture token in an address bar is a capture token in a screenshot, a\n * `Referer` header, a pasted link and a server access log \u2014 and this one\n * authorises filing issues as the reviewer it was minted for. `replaceState`\n * rewrites the entry in place: no navigation and no new history entry.\n *\n * **MEASURED, not assumed** \u2014 chromium 151.0.7922.34 driven by playwright\n * against a loopback server that logged every `Referer` it received. The page\n * did the same `searchParams.delete` + `replaceState` this module does, with\n * one stylesheet in the initial `<head>` and a second appended by script\n * afterwards:\n *\n * ```\n * /early.css Referer: http://\u2026/page?tab=menu&ai_annotate=\u2026&ai_capture=\u2026&ai_run=\u2026\n * /late.css Referer: http://\u2026/page?tab=menu\n * ```\n *\n * Two things come out of that run, and both are the point:\n *\n * 1. **The residual window is real, and it is bounded.** Anything the page\n * requests before this function runs \u2014 the document's own scripts,\n * stylesheets, fonts, images \u2014 is a SAME-ORIGIN subresource, and under the\n * default `Referrer-Policy` (`strict-origin-when-cross-origin`) a\n * same-origin request carries the FULL URL, query string included. It\n * reaches only the product's own origin, which has already seen the tokens\n * on the request line of the navigation itself, and everything requested\n * after this call sees the stripped URL. That is the case a reviewer of\n * A-244 named and deferred to this slice, answered with a measurement.\n * 2. **Back does not restore the tokens.** The same run then called\n * `goBack()` and landed on the URL from BEFORE the deep link rather than on\n * the token-bearing one \u2014 which is what `replaceState` rather than\n * `pushState` buys, and is why that choice is not stylistic.\n *\n * Closing the window entirely is a RESPONSE HEADER on the product\n * (`Referrer-Policy: strict-origin`, which drops path and query for same-origin\n * requests too), so it belongs to the mount in the product's own repository \u2014\n * N-905, not this package. **This module cannot close it**: by the time any\n * bundled script runs, the head's subresources are already in flight. What is\n * ours is being callable early, which is why nothing here awaits anything and\n * the strip happens before the token is stored or the cookie is read.\n *\n * ===========================================================================\n * SESSION STORAGE FOR THE CAPTURE TOKEN, AND NOT A COOKIE\n * ===========================================================================\n *\n * The arming marker is a cookie because the product's MIDDLEWARE sets it, on\n * the server, before any script runs. The capture token is deliberately not\n * one: a cookie is attached to every request the browser makes to that origin,\n * so it would put the credential into the product's own access logs on every\n * page, image and API call for the rest of the session \u2014 the exact exposure\n * step 2 exists to end. `sessionStorage` is per-tab, survives internal\n * navigation (which is what arming has to survive), is never sent anywhere by\n * the browser, and is gone when the tab closes.\n *\n * The cost, stated: a link opened in a NEW tab has no stored token, so a\n * reviewer who middle-clicks their way around the app gets a toolbar that\n * cannot send. That is the correct failure \u2014 `httpTransport` refuses to\n * construct without a token \u2014 and it is recoverable by using the deep link\n * again.\n */\n\n/** Per-tab, never sent anywhere by the browser. Namespaced, not product-named. */\nexport const CAPTURE_TOKEN_STORAGE_KEY = \"ai.annotation.capture\";\nexport const REVIEW_RUN_STORAGE_KEY = \"ai.annotation.run\";\n\n/** The subset of `Storage` this needs. `null` disables persistence entirely. */\nexport type StorageLike = Pick<Storage, \"getItem\" | \"setItem\" | \"removeItem\">;\n\nexport interface ArmingOptions {\n /** Defaults to the ambient `document`. Needs `cookie`, `URL` and `defaultView`. */\n doc?: Document;\n /** Defaults to `doc.defaultView.history`. Only `replaceState` is used. */\n history?: Pick<History, \"replaceState\">;\n /**\n * Defaults to `sessionStorage`. Pass `null` for a tab that should keep\n * nothing \u2014 arming then lasts exactly as long as the query string does.\n */\n storage?: StorageLike | null;\n /** Overrides the URL read from the document. Tests and server renders only. */\n url?: string;\n}\n\nexport interface Arming {\n /** May this page offer an annotation UI at all? */\n armed: boolean;\n /** The console's signed token, for `httpTransport`. Never validated here. */\n captureToken: string | null;\n /** The run under review, unsigned, couriered. */\n runId: string | null;\n /** *Done*, or an explicit stop. Clears the cookie and the stored token. */\n disarm: () => void;\n}\n\n/** Every name this module takes out of a URL. */\nconst STRIPPED_PARAMS: readonly string[] = [\n ...ANNOTATION_TOKEN_PARAMS,\n CAPTURE_TOKEN_PARAM,\n REVIEW_RUN_PARAM,\n];\n\n/**\n * `sessionStorage` throws rather than returning null in real browsers \u2014 an\n * opaque origin, a `data:` document, Safari's private mode historically, or a\n * user who has blocked site data. A capture surface that threw during boot\n * would take the product's own page down with it, in somebody else's product.\n */\nfunction safely<T>(run: () => T, fallback: T): T {\n try {\n return run();\n } catch {\n return fallback;\n }\n}\n\nfunction defaultStorage(win: Window | null): StorageLike | null {\n return safely(() => win?.sessionStorage ?? null, null);\n}\n\n/**\n * Read the deep link, strip it, and answer whether this page is armed.\n *\n * Safe to call more than once and safe to call on a page that was never\n * linked to: with no parameters and no cookie it does nothing at all, touches\n * no history entry, and returns `armed: false`.\n */\nexport function readArming(options: ArmingOptions = {}): Arming {\n const doc = options.doc ?? document;\n const win = doc.defaultView ?? null;\n const storage = options.storage === undefined ? defaultStorage(win) : options.storage;\n\n const href = options.url ?? doc.URL;\n const url = safely(() => new URL(href), null);\n\n let captureToken: string | null = null;\n let runId: string | null = null;\n\n if (url !== null) {\n captureToken = url.searchParams.get(CAPTURE_TOKEN_PARAM) || null;\n runId = url.searchParams.get(REVIEW_RUN_PARAM) || null;\n\n const present = STRIPPED_PARAMS.some((name) => url.searchParams.has(name));\n if (present) {\n for (const name of STRIPPED_PARAMS) url.searchParams.delete(name);\n /**\n * FIRST, before anything is stored or decided. The strip is the only\n * part of this function with a clock on it.\n *\n * `replaceState` and not `pushState`: a new history entry would put the\n * token-bearing URL one Back press away, which is the same leak with an\n * extra step. The state argument is the CURRENT `history.state`, passed\n * straight back, falling back to `null` only when there is none: we are\n * not the product's router and must not overwrite whatever it has put\n * there.\n */\n const history = options.history ?? win?.history;\n safely(() => {\n history?.replaceState(win?.history?.state ?? null, \"\", url.toString());\n }, undefined);\n }\n }\n\n if (storage) {\n if (captureToken !== null) {\n safely(() => storage.setItem(CAPTURE_TOKEN_STORAGE_KEY, captureToken as string), undefined);\n } else {\n captureToken = safely(() => storage.getItem(CAPTURE_TOKEN_STORAGE_KEY), null);\n }\n if (runId !== null) {\n safely(() => storage.setItem(REVIEW_RUN_STORAGE_KEY, runId as string), undefined);\n } else {\n runId = safely(() => storage.getItem(REVIEW_RUN_STORAGE_KEY), null);\n }\n }\n\n const disarm = () => {\n for (const name of ANNOTATION_ARMED_COOKIES) {\n safely(() => {\n doc.cookie = clearedCookie(name);\n }, undefined);\n }\n if (storage) {\n safely(() => storage.removeItem(CAPTURE_TOKEN_STORAGE_KEY), undefined);\n safely(() => storage.removeItem(REVIEW_RUN_STORAGE_KEY), undefined);\n }\n };\n\n return {\n // The cookie, and only the cookie. See the header.\n armed: isAnnotationArmed(safely(() => doc.cookie, \"\")),\n captureToken,\n runId,\n disarm,\n };\n}\n\n/**\n * The BUILD-TIME gate (A-241), and the second of the two independent ones.\n *\n * ===========================================================================\n * WHY `build.env` AND NOT `process.env.NODE_ENV`\n * ===========================================================================\n *\n * `NODE_ENV` is the reflex and it is WRONG HERE, measurably: a Vercel PREVIEW\n * build \u2014 which is what a staging deploy is, and the only place this toolbar is\n * ever meant to appear \u2014 is built with `NODE_ENV=production`. Gating on it\n * would disable annotation on exactly the deployments it exists for while\n * reading like a security improvement, which is this repository's recurring\n * failure shape.\n *\n * `BuildInfo.env` mirrors `VERCEL_ENV` (`production | preview | development`)\n * and is stamped by the product's own build. It is a literal by the time the\n * bundler sees it, so `if (!isAnnotatableBuild(build)) return` in a production\n * build is a branch a minifier can fold \u2014 which is how the mount stops being in\n * the customer's bundle rather than merely being inert in it. The stronger form\n * of the same claim, excluding the import itself, is the product's own\n * configuration and belongs to the mount (N-905).\n *\n * `\"staging\"` is on the list: `BuildInfo` carries the value even though nothing\n * produces it today, and a build that calls itself staging is by definition not\n * the one serving customers.\n *\n * ===========================================================================\n * AN ALLOWLIST, BECAUSE THE DENYLIST FAILED OPEN\n * ===========================================================================\n *\n * This was `build.env !== \"production\"` and that is the same function for every\n * value the union names \u2014 and the WRONG ONE for every value it does not.\n * `undefined`, `\"\"`, a typo, a `VERCEL_ENV` that was never stamped, a field\n * dropped by a payload shim: each of those is *not* `\"production\"`, so each\n * used to mean *mount the toolbar*.\n *\n * `BuildInfo.env` being a closed TypeScript union is not the guarantee it looks\n * like at this line. **The only producer is `resolveBuildInfo`, in a different\n * repository, reading an environment variable at build time.** A type cannot\n * constrain a string that crosses a repository boundary as data; it constrains\n * the code that was compiled against it, which is not the code that stamped the\n * value.\n *\n * That mattered more here than anywhere else in the package. This is the single\n * check between a customer and a review affordance in a public bundle, this\n * file's own argument is that neither gate may depend on anything else holding,\n * and the denylist depended on another repository's env stamping being correct.\n * A gate that fails open is not a second gate.\n *\n * Failing closed costs exactly one thing, and it is the right one: a\n * misconfigured PREVIEW deploy loses its toolbar. That is visible in a second\n * and fixed by stamping the variable. The denylist's failure was silent and\n * shipped.\n */\nconst ANNOTATABLE_ENVS: readonly BuildInfo[\"env\"][] = [\"preview\", \"staging\", \"development\"];\n\nexport function isAnnotatableBuild(build: Pick<BuildInfo, \"env\">): boolean {\n return ANNOTATABLE_ENVS.includes(build.env);\n}\n", "import type { AnnotationSdk, AnnotationSdkOptions, CommentCollector, CommentRequest } from \"./sdk\";\nimport { createAnnotationSdk } from \"./sdk\";\nimport { isAnnotatableBuild, type Arming } from \"./arming\";\nimport { safeIssueUrl } from \"./transport\";\nimport type { EgressOutcome } from \"./transport\";\n\n/**\n * The toolbar and the post-it (A-240).\n *\n * ===========================================================================\n * WHY THIS EXISTS AT ALL\n * ===========================================================================\n *\n * `sdk.ts` built its payload with `comment: \"\"` and a comment saying why: *the\n * parent frame collects the human's words and merges them in verbatim*. First\n * party there is no parent \u2014 see `docs/plans/first-party-annotation.md` \u2014 so a\n * capture would file an issue carrying a screenshot, a `file:line` and NOTHING\n * SAYING WHAT IS WRONG WITH IT. This is the surface that collects the words,\n * and the surface that turns annotation mode on in the first place.\n *\n * The toggle is the point rather than a convenience. The reviewer navigates the\n * real app with annotation OFF \u2014 clicking rows, opening dialogs, finishing a\n * wizard \u2014 and switches it ON when they arrive somewhere worth commenting on.\n * That is what stops click-only screens being a wall, and it is why this is a\n * toolbar and not a mode the page boots into.\n *\n * ===========================================================================\n * IT RENDERS INSIDE SOMEBODY ELSE'S PRODUCT, AND THAT DECIDES ALMOST EVERYTHING\n * ===========================================================================\n *\n * 1. **Shadow DOM, closed to the product's CSS in both directions.** The\n * product cannot restyle this and this cannot restyle the product. A\n * toolbar inheriting a host's `* { box-sizing: border-box }` \u2014 or its\n * `button { text-transform: uppercase }` \u2014 is a bug report about our own UI,\n * filed against their product, by a tool whose entire output is bug reports.\n * `:host { all: initial }` is the half people forget: a shadow root stops\n * SELECTORS crossing, not INHERITANCE. `e2e/toolbar.spec.ts` proves both\n * halves against a deliberately hostile fixture, and proves them the only\n * way that means anything \u2014 by deleting each and watching it go red.\n *\n * THE ONE THING IT CANNOT DEFEND, said out loud rather than left to be\n * discovered: the HOST is in the light DOM, so the product's own selectors\n * reach it. `#ai-annotation-toolbar { position: static }` in a customer's\n * stylesheet beats `:host`, and puts this in the flow. Nothing shadow DOM\n * offers prevents that; the id is documented so that a collision is at\n * least legible when somebody hits it.\n *\n * 2. **It must not alter what is being reviewed.** Everything is `position:\n * fixed` and nothing is ever appended to the flow, so there is no layout\n * shift and no scrollbar; and NOTHING IS FOCUSED ON MOUNT. The reviewer is\n * here to judge pixels, and a tool that moves one of them has answered the\n * question wrongly before anybody clicks anything.\n *\n * `position: fixed` is doing ALL of that work, and the viewport clamp in\n * `position()` is doing NONE of it \u2014 see the note there, which used to\n * claim otherwise.\n *\n * 3. **No product may be named here (A-98).** Every string in `DEFAULT_LABELS`\n * is generic, and every one of them is overridable by the mounting app. The\n * only text this renders that it did not author is the target's own\n * `file:line`, which is the customer's source path and belongs to them.\n *\n * ===========================================================================\n * WHY THE CSS IS A TEMPLATE STRING AND WHAT THAT COSTS\n * ===========================================================================\n *\n * A `<style>` element inside the shadow root, whose `textContent` is the string\n * below. Not inline styles: `overlay.ts` uses those and carries nine entries in\n * `audit-allowance.json` for it, and that ledger may only ever get shorter.\n *\n * TWO AUDITS READ THIS REPOSITORY AND THEY DISAGREE ABOUT THIS FILE, which is\n * worth knowing before trusting either.\n *\n * `packages/design-audit` \u2014 `pnpm verify`'s ratchet \u2014 applies its colour,\n * space, radius and type rules to `.css` FILES ONLY: `scan.ts` classifies\n * every `.ts` as `tsx`, where the only rules are `inline-style` and the\n * class-name pair. It sees nothing here.\n *\n * **The `design-system` CI job does.** It reads CSS out of `.ts` source, and\n * the ledger it judges against \u2014 at the repository root, alongside\n * `audit-allowance.json` and under the same only-ever-shorter rule \u2014 already\n * carries `overlay.ts` for exactly that. THIS WAS MEASURED RATHER THAN\n * REASONED: the first version of this file was written believing nothing\n * audited it, said so in this comment, and that job failed the pull request\n * with `untokenised-colour` and `untokenised-space` against it. The check won\n * and the assumption is corrected here, where the assumption was.\n *\n * Hence the token block at the top of `CSS`. Every colour, space and radius\n * below is a `var()` into it, so there is one place to change them and the\n * gate can see that there is. They are `--ai-*` and NOT the design system's\n * `--color-*` / `--space-*`, and that is forced rather than chosen: this\n * package has zero dependencies on purpose, so the dictionary is not\n * importable \u2014 and inside a shadow root a custom property resolves against the\n * PRODUCT'S, so `var(--color-fg)` would paint this toolbar with whatever the\n * customer happens to mean by that name. A local, prefixed block cannot\n * collide with theirs.\n */\n\n/**\n * Every word this renders. Generic by default, and all of them injectable.\n *\n * Every field is RENDERED SOMEWHERE. A label nobody displays is an API promise\n * that cannot be kept and bytes on every page of every managed product; three\n * were written and deleted before this shipped for exactly that reason.\n */\nexport interface AnnotationToolbarLabels {\n /** Names the whole surface for a screen reader. */\n region: string;\n expand: string;\n collapse: string;\n start: string;\n stop: string;\n done: string;\n comment: string;\n placeholder: string;\n send: string;\n cancel: string;\n sent: string;\n issue: string;\n failed: string;\n retry: string;\n}\n\nexport const DEFAULT_LABELS: AnnotationToolbarLabels = {\n region: \"Annotation\",\n expand: \"Open the annotation toolbar\",\n collapse: \"Close the annotation toolbar\",\n start: \"Start annotating\",\n stop: \"Stop annotating\",\n done: \"Done\",\n comment: \"What is wrong here?\",\n placeholder: \"The heading wraps onto three lines on mobile\u2026\",\n send: \"Send\",\n cancel: \"Cancel\",\n sent: \"Sent\",\n issue: \"View issue\",\n failed: \"That did not send\",\n retry: \"Retry\",\n};\n\n/** The host's id. Our own node; nothing outside this package looks it up. */\nexport const TOOLBAR_ID = \"ai-annotation-toolbar\";\n\n/** One above `overlay.ts`, so the toolbar is never behind the highlight. */\nconst Z_INDEX = \"2147483001\";\n\nconst COMPOSER_WIDTH = 288;\n/**\n * MEASURED, not reserved: 189.6875px in chromium with the default labels, the\n * empty textarea at its `min-height`, and one line of `file:line`.\n *\n * It is used only to choose ABOVE or BELOW the target, so a value that drifts\n * degrades the placement rather than breaking anything \u2014 a caller who injects\n * labels long enough to wrap the button row gets a composer a little taller\n * than this thinks, and the worst outcome is that it sits below when it had\n * room above. `e2e/toolbar.spec.ts` asserts the real rendered box against the\n * real target box, so the consequence is checked even though the constant is\n * an estimate.\n */\nconst COMPOSER_HEIGHT = 190;\nconst MARGIN = 12;\n\nconst CSS = `\n:host{\n --ai-bg:#fff;\n --ai-fg:#18181b;\n --ai-line:#d4d4d8;\n --ai-line-soft:#e4e4e7;\n --ai-muted:#52525b;\n --ai-idle:#a1a1aa;\n --ai-accent:#2563eb;\n --ai-invert-bg:#18181b;\n --ai-invert-fg:#fafafa;\n --ai-invert-line:#52525b;\n --ai-bad:#7f1d1d;\n --ai-space-2:6px;\n --ai-space-3:8px;\n --ai-space-4:10px;\n --ai-space-5:12px;\n --ai-radius-control:6px;\n --ai-radius-card:8px;\n --ai-radius-pill:999px;\n all:initial;position:fixed;right:16px;bottom:16px;z-index:${Z_INDEX};\n font:13px/1.45 ui-sans-serif,system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;\n color-scheme:light}\n*,*::before,*::after{box-sizing:border-box}\n[hidden]{display:none!important}\n.pill{order:2}\n.bar{display:flex;align-items:center;gap:var(--ai-space-2);justify-content:flex-end}\nbutton{font:inherit;margin:0;border:1px solid var(--ai-line);\n border-radius:var(--ai-radius-control);background:var(--ai-bg);color:var(--ai-fg);\n padding:var(--ai-space-2) var(--ai-space-4);cursor:pointer}\nbutton:focus-visible{outline:2px solid var(--ai-accent);outline-offset:2px}\n.pill{border-radius:var(--ai-radius-pill);background:var(--ai-invert-bg);\n color:var(--ai-invert-fg);border-color:var(--ai-invert-bg);\n padding:var(--ai-space-2) var(--ai-space-5);display:flex;align-items:center;\n gap:var(--ai-space-2)}\n.dot{width:8px;height:8px;border-radius:var(--ai-radius-pill);\n background:var(--ai-idle);display:block}\n.on .dot{background:var(--ai-accent)}\n.panel{display:flex;gap:var(--ai-space-2);align-items:center;background:var(--ai-bg);\n border:1px solid var(--ai-line-soft);border-radius:var(--ai-radius-card);\n padding:var(--ai-space-2)}\n.toggle[aria-pressed=\"true\"]{background:var(--ai-accent);border-color:var(--ai-accent);\n color:var(--ai-invert-fg)}\n.composer{position:fixed;width:${COMPOSER_WIDTH}px;background:var(--ai-bg);color:var(--ai-fg);\n border:1px solid var(--ai-line);border-radius:var(--ai-radius-card);\n padding:var(--ai-space-4);display:flex;flex-direction:column;gap:var(--ai-space-3)}\n.where{margin:0;font:11px/16px ui-monospace,SFMono-Regular,Menlo,monospace;\n color:var(--ai-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\nlabel{font-weight:600}\ntextarea{font:inherit;width:100%;min-height:76px;resize:vertical;\n padding:var(--ai-space-2) var(--ai-space-3);border:1px solid var(--ai-line);\n border-radius:var(--ai-radius-control);background:var(--ai-bg);color:var(--ai-fg)}\ntextarea:focus-visible{outline:2px solid var(--ai-accent);outline-offset:1px}\n.row{display:flex;gap:var(--ai-space-2);justify-content:flex-end}\n.primary{background:var(--ai-accent);border-color:var(--ai-accent);color:var(--ai-invert-fg)}\n.toast{position:fixed;right:16px;bottom:64px;max-width:320px;\n background:var(--ai-invert-bg);color:var(--ai-invert-fg);\n border-radius:var(--ai-radius-card);padding:var(--ai-space-3) var(--ai-space-4);\n display:flex;align-items:center;gap:var(--ai-space-3)}\n.toast.bad{background:var(--ai-bad)}\n.toast a{color:var(--ai-invert-fg)}\n.toast button{background:transparent;border-color:var(--ai-invert-line);\n color:var(--ai-invert-fg)}\n`;\n\nexport interface AnnotationToolbarOptions {\n /**\n * Whether this page may offer an annotation UI at all.\n *\n * INJECTED, and deliberately not decided here. How arming is decided \u2014 the\n * token in the URL, the cookie, the `replaceState` that strips it, the\n * production kill-switch \u2014 is A-241, and a toolbar that read the URL itself\n * would be a second answer to a question that must have exactly one.\n *\n * Unarmed, NOTHING IS APPENDED TO THE DOCUMENT. Not hidden: absent.\n */\n armed?: boolean;\n doc?: Document;\n /** Only for the viewport clamp, so a fake one is two numbers. */\n win?: Pick<Window, \"innerWidth\" | \"innerHeight\">;\n labels?: Partial<AnnotationToolbarLabels>;\n /** *Done* \u2014 the reviewer is finished. A-241 owns what that clears. */\n onDone?: () => void;\n}\n\nexport interface AnnotationToolbar {\n /** Is this node ours? Hand it to `AnnotationSdkOptions.ignoreElement`. */\n owns: (node: unknown) => boolean;\n /** Wire the toggle to a live SDK. */\n attach: (sdk: AnnotationSdk) => void;\n /** Hand to `AnnotationSdkOptions.collectComment`. */\n collectComment: CommentCollector;\n /** Hand to `AnnotationSdkOptions.onCaptureResult`. */\n showResult: (outcome: EgressOutcome, retry: () => void) => void;\n /** Whether the panel is open. For tests and for the mounting app. */\n isExpanded: () => boolean;\n destroy: () => void;\n}\n\n/** `createElement` plus attributes, because half of this file is that call. */\nfunction make(doc: Document, tag: string, attrs?: Record<string, string>): HTMLElement {\n const el = doc.createElement(tag);\n if (attrs) for (const name of Object.keys(attrs)) el.setAttribute(name, attrs[name] ?? \"\");\n return el;\n}\n\nexport function createAnnotationToolbar(\n options: AnnotationToolbarOptions = {},\n): AnnotationToolbar {\n const labels: AnnotationToolbarLabels = { ...DEFAULT_LABELS, ...options.labels };\n const doc = options.doc ?? document;\n const viewport = options.win ?? window;\n\n /**\n * Unarmed is not a hidden toolbar, it is no toolbar. Every method below still\n * answers, so a caller does not have to branch: `collectComment` resolves\n * `\"\"` (a capture with no words is still a capture, and this path is only\n * reachable if something else armed annotation mode), `owns` is false for\n * everything, and nothing is ever added to the page.\n */\n if (options.armed === false) {\n return {\n owns: () => false,\n attach: () => {},\n collectComment: () => Promise.resolve(\"\"),\n showResult: () => {},\n isExpanded: () => false,\n destroy: () => {},\n };\n }\n\n const host = make(doc, \"div\", { id: TOOLBAR_ID });\n const root = host.attachShadow({ mode: \"open\" });\n\n const style = make(doc, \"style\");\n style.textContent = CSS;\n root.appendChild(style);\n\n /** Rewritten per open. A stylesheet, so no element carries a style attribute. */\n const geometry = make(doc, \"style\");\n root.appendChild(geometry);\n\n /* ---------------------------------------------------------------- bar --- */\n\n const bar = make(doc, \"div\", {\n id: \"ai-annotation-bar\",\n class: \"bar\",\n role: \"region\",\n \"aria-label\": labels.region,\n });\n const panel = make(doc, \"div\", { class: \"panel\", id: \"ai-annotation-panel\", hidden: \"\" });\n\n const toggle = make(doc, \"button\", {\n type: \"button\",\n id: \"ai-annotation-toggle\",\n class: \"toggle\",\n \"aria-pressed\": \"false\",\n });\n toggle.textContent = labels.start;\n\n const done = make(doc, \"button\", { type: \"button\", id: \"ai-annotation-done\" });\n done.textContent = labels.done;\n panel.appendChild(toggle);\n panel.appendChild(done);\n\n const pill = make(doc, \"button\", {\n type: \"button\",\n id: \"ai-annotation-pill\",\n class: \"pill\",\n \"aria-expanded\": \"false\",\n \"aria-controls\": \"ai-annotation-panel\",\n \"aria-label\": labels.expand,\n });\n const dot = make(doc, \"span\", { class: \"dot\", \"aria-hidden\": \"true\" });\n const pillText = make(doc, \"span\");\n pillText.textContent = labels.region;\n pill.appendChild(dot);\n pill.appendChild(pillText);\n\n /**\n * PILL FIRST IN THE DOM, and put back on the right with `order` in the CSS.\n *\n * Tab order follows the DOM, not the paint. With the panel first, a reviewer\n * who tabbed to the pill and pressed Enter to expand it found the next Tab\n * took them OUT of the toolbar and back into the product, past the two\n * buttons they had just revealed. The e2e spec caught it; nothing else could.\n */\n bar.appendChild(pill);\n bar.appendChild(panel);\n root.appendChild(bar);\n\n /* ----------------------------------------------------------- composer --- */\n\n const composer = make(doc, \"div\", {\n id: \"ai-annotation-composer\",\n class: \"composer\",\n role: \"dialog\",\n /**\n * `false`, and stated rather than omitted. Modal would mean trapping focus\n * and hiding the rest of the page from assistive technology \u2014 in a product\n * the reviewer is here to READ. The post-it sits beside the thing it is\n * about; it does not take the page hostage.\n */\n \"aria-modal\": \"false\",\n \"aria-labelledby\": \"ai-annotation-comment\",\n hidden: \"\",\n });\n const where = make(doc, \"p\", { class: \"where\" });\n const commentLabel = make(doc, \"label\", {\n id: \"ai-annotation-comment\",\n for: \"ai-annotation-input\",\n });\n commentLabel.textContent = labels.comment;\n const input = make(doc, \"textarea\", {\n id: \"ai-annotation-input\",\n placeholder: labels.placeholder,\n rows: \"3\",\n }) as HTMLTextAreaElement;\n const cancel = make(doc, \"button\", { type: \"button\", id: \"ai-annotation-cancel\" });\n cancel.textContent = labels.cancel;\n const send = make(doc, \"button\", { type: \"button\", id: \"ai-annotation-send\", class: \"primary\" });\n send.textContent = labels.send;\n const row = make(doc, \"div\", { class: \"row\" });\n row.appendChild(cancel);\n row.appendChild(send);\n composer.appendChild(where);\n composer.appendChild(commentLabel);\n composer.appendChild(input);\n composer.appendChild(row);\n root.appendChild(composer);\n\n /* -------------------------------------------------------------- toast --- */\n\n const toast = make(doc, \"div\", { id: \"ai-annotation-toast\", class: \"toast\", role: \"status\", hidden: \"\" });\n const toastText = make(doc, \"span\");\n const toastAction = make(doc, \"a\", {\n id: \"ai-annotation-issue\",\n target: \"_blank\",\n rel: \"noopener noreferrer\",\n hidden: \"\",\n });\n const toastRetry = make(doc, \"button\", { type: \"button\", id: \"ai-annotation-retry\", hidden: \"\" });\n toastRetry.textContent = labels.retry;\n toast.appendChild(toastText);\n toast.appendChild(toastAction);\n toast.appendChild(toastRetry);\n root.appendChild(toast);\n\n doc.body.appendChild(host);\n\n /* --------------------------------------------------------------- wire --- */\n\n let sdk: AnnotationSdk | null = null;\n let expanded = false;\n let settle: ((comment: string | null) => void) | null = null;\n let restoreFocus: Element | null = null;\n let toastTimer: ReturnType<typeof setTimeout> | null = null;\n let retryHandler: () => void = () => {};\n\n const show = (el: HTMLElement, visible: boolean) => {\n if (visible) el.removeAttribute(\"hidden\");\n else el.setAttribute(\"hidden\", \"\");\n };\n\n const setExpanded = (open: boolean) => {\n expanded = open;\n show(panel, open);\n pill.setAttribute(\"aria-expanded\", String(open));\n pill.setAttribute(\"aria-label\", open ? labels.collapse : labels.expand);\n };\n\n const setAnnotating = (on: boolean) => {\n toggle.setAttribute(\"aria-pressed\", String(on));\n toggle.textContent = on ? labels.stop : labels.start;\n bar.setAttribute(\"class\", on ? \"bar on\" : \"bar\");\n if (on) sdk?.enable();\n else sdk?.disable();\n };\n\n pill.addEventListener(\"click\", () => setExpanded(!expanded));\n\n toggle.addEventListener(\"click\", () => {\n setAnnotating(sdk?.isEnabled() !== true);\n });\n\n /**\n * Everything this put on the page, taken back off it.\n *\n * Shared by *Done* and `destroy()` rather than duplicated, so the two cannot\n * come to mean different things \u2014 which is the shape of the bug this whole\n * function exists to close.\n */\n const teardown = () => {\n if (toastTimer !== null) clearTimeout(toastTimer);\n // A promise nobody will ever settle is a capture stuck forever. Cancel it.\n close(null);\n host.remove();\n };\n\n /**\n * ===================================================================\n * *DONE* TEARS DOWN. IT IS A CONTROL, NOT A SUGGESTION (A-241)\n * ===================================================================\n *\n * `docs/plans/first-party-annotation.md` step 5 says *Done clears the cookies\n * and the toolbar disappears*, and until this line only the first half was\n * true. `onDone` disarms \u2014 the cookie and the stored capture token go \u2014 but\n * the host stayed in the DOM, so the toggle went on working for the life of\n * that page: the capture token is already closed over by `httpTransport` and\n * does not care that the cookie is gone. A reviewer who said they were\n * finished still had a live annotation surface until they navigated.\n *\n * On a surface whose entire risk story is *it cannot appear when it should\n * not*, that is the wrong default. After this, nothing on the page can\n * capture again without re-arming through the deep link.\n *\n * ORDER MATTERS: `setAnnotating(false)` first so the SDK drops its document\n * listeners while it still has a toolbar to be told by, then `onDone` \u2014 which\n * is where `mountAnnotation` disarms and destroys the SDK \u2014 and only then the\n * DOM.\n *\n * THE COST, STATED RATHER THAN DISCOVERED: a receipt that lands after *Done*\n * has nowhere to render. Press Send, then Done inside the transport's 8s\n * window, and the capture still goes (the fetch is in flight and teardown\n * does not abort it) but the *View issue* link \u2014 or a failed send's *Retry* \u2014\n * is gone with the toast. That is the accepted price of *Done* meaning done,\n * and it is bounded: the same is already true of navigating away, and the\n * success toast dismisses itself after six seconds regardless.\n */\n done.addEventListener(\"click\", () => {\n setAnnotating(false);\n setExpanded(false);\n options.onDone?.();\n teardown();\n });\n\n /**\n * Closing the composer.\n *\n * `settle = null` IS DEFENSIVE AND PROTECTS NOTHING MEASURABLE. Said plainly,\n * because two previous versions of this comment claimed otherwise and both\n * were wrong \u2014 the second one on the very cycle it was written to fix the\n * first. MEASURED: delete the line and the whole suite still passes.\n *\n * The reasons it protects nothing, since \"it must protect something\" is what\n * produced both wrong answers:\n *\n * - A second `resolve` on a settled promise is already a no-op. Whichever\n * of Send, Cancel and Escape arrives first decides the capture, with or\n * without this line.\n * - It does not gate the two `restoreFocus` lines below; they run\n * unconditionally. And a second pass could not misuse them anyway \u2014\n * `restoreFocus` is `null` by then and `null instanceof HTMLElement` is\n * false, so nothing happens.\n *\n * It stays because it makes the intent \u2014 this closes an OPEN composer, once \u2014\n * legible at the top of the function rather than deducible from promise\n * semantics three files away. That is a real reason and a small one, and it\n * is not a claim that anything breaks without it.\n */\n const close = (comment: string | null) => {\n const resolve = settle;\n settle = null;\n show(composer, false);\n if (resolve) resolve(comment);\n if (restoreFocus instanceof HTMLElement) restoreFocus.focus();\n restoreFocus = null;\n };\n\n cancel.addEventListener(\"click\", () => close(null));\n send.addEventListener(\"click\", () => close(input.value));\n\n composer.addEventListener(\"keydown\", (key) => {\n if (key.key === \"Escape\") {\n /**\n * Ours, and it stops here. `sdk.ts` also listens for Escape on the\n * document in the capture phase, to leave annotation mode \u2014 and it\n * already declines while a comment is being collected, so this is the\n * second of two guards rather than the only one. Two, because a reviewer\n * who loses their words to a stray keystroke does not write them again.\n */\n key.stopPropagation();\n close(null);\n return;\n }\n // The conventional \"submit this textarea\" chord, on both platforms.\n if (key.key === \"Enter\" && (key.metaKey || key.ctrlKey)) {\n key.preventDefault();\n close(input.value);\n }\n });\n\n const collectComment: CommentCollector = (request: CommentRequest) =>\n new Promise<string | null>((resolve) => {\n settle = resolve;\n input.value = \"\";\n where.textContent = describe(request);\n position(request);\n show(toast, false);\n show(composer, true);\n /**\n * Focus moves HERE and nowhere else. This is a response to the\n * reviewer's own click, which is the only moment taking focus is\n * correct \u2014 on mount it would be focus theft in somebody else's product.\n */\n restoreFocus = doc.activeElement;\n input.focus();\n });\n\n /** `file:line`, or the component, or nothing. The customer's own source path. */\n function describe(request: CommentRequest): string {\n const { source, componentName } = request.target;\n if (source) return `${source.file}:${source.line}`;\n return componentName ?? \"\";\n }\n\n /**\n * Anchored near the click WITHOUT COVERING IT, and never outside the viewport.\n *\n * Below the target box when there is room, above it when there is not, and\n * clamped to the viewport with a margin either way.\n *\n * THE CLAMP IS ABOUT REACHABILITY, NOT SCROLLBARS, and the difference is a\n * correction rather than a nuance. This said the clamp was what stopped a box\n * hanging past the right edge putting a horizontal scrollbar on the page. It\n * is not. MEASURED in chromium: a `position: fixed` 300x300 box at\n * `left: 99999px` leaves `scrollWidth` at 1280 against a `clientWidth` of\n * 1280 \u2014 fixed elements contribute nothing to scrollable overflow \u2014 while the\n * identical box at `position: absolute` takes `scrollWidth` to 100299. The\n * no-scrollbar property is bought by `position: fixed` alone.\n *\n * What the clamp actually buys: a composer placed off the viewport is a\n * composer the reviewer cannot see, type into or dismiss, with a capture\n * pending behind it and Escape as the only way out. That is worth the same\n * few lines for a better reason.\n */\n function position(request: CommentRequest) {\n const box = request.target.boundingBox;\n const belowY = box.y + box.h + MARGIN;\n const top =\n belowY + COMPOSER_HEIGHT <= viewport.innerHeight\n ? belowY\n : Math.max(MARGIN, box.y - COMPOSER_HEIGHT - MARGIN);\n const left = Math.min(\n Math.max(MARGIN, box.x),\n Math.max(MARGIN, viewport.innerWidth - COMPOSER_WIDTH - MARGIN),\n );\n geometry.textContent = `.composer{left:${Math.round(left)}px;top:${Math.round(top)}px}`;\n }\n\n const showResult = (outcome: EgressOutcome, retry: () => void) => {\n if (toastTimer !== null) clearTimeout(toastTimer);\n const ok = outcome.status === \"sent\";\n /**\n * SANITISED AGAIN, at the boundary that actually touches the customer's\n * DOM. `httpTransport` already does this to everything it reads off the\n * network, and that is not the same guarantee: this renders the receipt\n * from WHATEVER transport it was given, and a consumer-supplied one never\n * goes near that code. `setAttribute(\"href\", \u2026)` with a `javascript:`\n * value executes in the product's origin, so the check belongs at both\n * ends and there is one function so they cannot disagree.\n */\n const url = outcome.status === \"sent\" ? safeIssueUrl(outcome.receipt.issueUrl) : undefined;\n\n toast.setAttribute(\"class\", ok ? \"toast\" : \"toast bad\");\n /**\n * `alert` for a failure and `status` for a success, set per outcome rather\n * than once at build time. A lost capture that announced itself politely\n * enough to be queued behind other live-region updates is a lost capture\n * the reviewer walks away from.\n */\n toast.setAttribute(\"role\", ok ? \"status\" : \"alert\");\n toastText.textContent = ok ? labels.sent : labels.failed;\n\n show(toastAction, ok && url !== undefined);\n if (url !== undefined) {\n toastAction.setAttribute(\"href\", url);\n toastAction.textContent = labels.issue;\n }\n show(toastRetry, !ok);\n show(toast, true);\n\n /**\n * A success clears itself; a failure does not. The reviewer has to be able\n * to act on a failure, and a retry button that vanished after six seconds\n * is a capture nobody retried.\n */\n if (ok) toastTimer = setTimeout(() => show(toast, false), 6_000);\n retryHandler = retry;\n };\n\n toastRetry.addEventListener(\"click\", () => {\n show(toast, false);\n retryHandler();\n });\n\n return {\n owns: (node) => node instanceof Node && (node === host || host.contains(node)),\n attach: (attached) => {\n sdk = attached;\n // Whatever the SDK already thinks, the button says. A toolbar that\n // rendered `Start` over an enabled SDK would be a lie the reviewer acts on.\n setAnnotating(attached.isEnabled());\n },\n collectComment,\n showResult,\n isExpanded: () => expanded,\n /**\n * The same teardown *Done* runs, and safe to run twice: `clearTimeout` is\n * guarded, `close(null)` on a settled composer is a no-op, and `remove()`\n * on a node with no parent does nothing. A consumer calling `destroy()`\n * after the reviewer clicked *Done* is the normal case, not an error.\n */\n destroy: teardown,\n };\n}\n\n/* ========================================================================== *\n * The whole first-party surface, in one call\n * ========================================================================== */\n\nexport interface MountAnnotationOptions extends AnnotationSdkOptions {\n toolbar?: AnnotationToolbarOptions;\n /**\n * The arming decision, from `readArming()` (A-241).\n *\n * Passing it is how the two gates get applied without every consumer\n * re-deriving them: `armed` decides whether a toolbar exists at all, and\n * *Done* calls `disarm` so the cookie and the stored capture token go with it.\n *\n * OPTIONAL because the frame path still mounts a toolbar-less SDK and has no\n * deep link to read. Supplying both this and `toolbar.armed` throws \u2014 two\n * answers to a question that must have exactly one.\n */\n arming?: Arming;\n}\n\nexport interface MountedAnnotation {\n sdk: AnnotationSdk;\n toolbar: AnnotationToolbar;\n destroy: () => void;\n}\n\n/**\n * Build the SDK and the toolbar and connect them (A-240).\n *\n * Three wires, and each one is a silent failure if it is missing:\n * `collectComment` absent files an issue with an empty comment;\n * `onCaptureResult` absent leaves a reviewer who cannot tell a sent capture\n * from a lost one; `ignoreElement` absent captures our own toolbar. None of\n * them throws when omitted, which is exactly why they belong in one function\n * rather than in three lines of every consumer's bootstrap.\n */\nexport function mountAnnotation(options: MountAnnotationOptions): MountedAnnotation {\n const injected = options.toolbar?.armed;\n if (options.arming && injected !== undefined) {\n throw new Error(\n \"[annotation] pass `arming` OR `toolbar.armed`, not both; two answers to whether this \" +\n \"page may offer an annotation UI is how one of them stops being checked.\",\n );\n }\n\n /**\n * ===================================================================\n * THE TWO GATES, BOTH OF THEM, IN ONE EXPRESSION (A-241)\n * ===================================================================\n *\n * `armed` is the RUNTIME gate: the product's middleware verified the arming\n * token and left a cookie, and `readArming` read it \u2014 never the URL.\n * `isAnnotatableBuild` is the BUILD-TIME one: a production build never mounts\n * this, whatever any cookie says.\n *\n * They are `&&`-ed rather than layered so neither can be satisfied by the\n * other, and each is proven the only way that means anything \u2014 by removing it\n * and watching a NAMED test go red. `toolbar.test.ts` has one per gate, and\n * each holds the OTHER gate open so it cannot pass on its neighbour's work:\n * a production build with `armed: true` renders nothing, and a preview build\n * with `armed: false` renders nothing.\n *\n * `armed: false` is not a hidden toolbar. `createAnnotationToolbar` appends\n * NOTHING to the document \u2014 see its early return \u2014 so a page that somehow\n * shipped this code gets no node, no shadow root and no style element.\n */\n const armed =\n (options.arming ? options.arming.armed : injected) !== false &&\n isAnnotatableBuild(options.build);\n\n const toolbar = createAnnotationToolbar({\n ...options.toolbar,\n armed,\n onDone: () => {\n options.arming?.disarm();\n /**\n * The SDK goes too, and what that buys is SPECIFICALLY THE OVERLAY.\n *\n * Stated narrowly because the obvious claim \u2014 *it removes the document\n * listeners* \u2014 is already `setAnnotating(false)`'s work, and a comment\n * crediting this line with it would be false. MEASURED: delete this call\n * and the only assertion that fails is the one about the overlay node.\n *\n * `enable()` appends the highlight to `doc.body` and `disable()` only\n * HIDES it, so without this a reviewer who clicked *Done* keeps our\n * hidden div in their product's DOM for the life of the page. Small, and\n * exactly the kind of residue a tool that runs inside somebody else's\n * site does not get to leave behind.\n */\n sdk.destroy();\n options.toolbar?.onDone?.();\n },\n });\n const sdk = createAnnotationSdk({\n ...options,\n collectComment: toolbar.collectComment,\n onCaptureResult: toolbar.showResult,\n ignoreElement: toolbar.owns,\n });\n toolbar.attach(sdk);\n return {\n sdk,\n toolbar,\n destroy() {\n toolbar.destroy();\n sdk.destroy();\n },\n };\n}\n", "import type { ConsoleEntry, NetworkEntry } from \"./protocol\";\nimport type { AnnotationState } from \"./sdk\";\n\n/**\n * Page state, captured continuously (A-35).\n *\n * The timing is the whole point. By the time someone notices something is wrong\n * and clicks it, the interesting error has already happened \u2014 reading\n * `console` at capture time gets you nothing, because there is nothing to read.\n * So the buffers fill from page load and the click just takes a copy.\n *\n * Which means this code runs on every page, for every user in the enabled\n * environments, in front of the app's own error handling. It is written to be\n * boring: patch, record, call through, never throw.\n */\n\n/** Twenty each, per the spec. Enough for context, small enough to post. */\nexport const DEFAULT_LIMIT = 20;\n\n/** A stack is useful; a stack the size of the payload is not. */\nconst MAX_MESSAGE = 500;\nconst MAX_STACK = 2000;\n\n/**\n * A fixed-size window over the most recent N items.\n *\n * Oldest-out rather than newest-dropped: a page that logs steadily would\n * otherwise pin the buffer to whatever happened at boot and never show the\n * error the reviewer is looking at.\n */\nexport class RingBuffer<T> {\n readonly #items: T[] = [];\n readonly #limit: number;\n\n constructor(limit: number = DEFAULT_LIMIT) {\n this.#limit = Math.max(1, limit);\n }\n\n push(item: T): void {\n this.#items.push(item);\n if (this.#items.length > this.#limit) this.#items.shift();\n }\n\n /** A copy, so a consumer holding the payload cannot see it mutate underneath them. */\n toArray(): T[] {\n return [...this.#items];\n }\n\n get size(): number {\n return this.#items.length;\n }\n}\n\n/**\n * Render one console argument as text, without ever throwing.\n *\n * Console arguments are arbitrary: DOM nodes, circular objects, proxies that\n * throw on property access. Anything that fails here would fail *inside the\n * app's own `console.error` call*, turning a logged error into a thrown one.\n */\nexport function describeArg(arg: unknown): string {\n if (typeof arg === \"string\") return arg;\n if (arg instanceof Error) return `${arg.name}: ${arg.message}`;\n if (arg === null) return \"null\";\n if (arg === undefined) return \"undefined\";\n if (typeof arg === \"object\") {\n try {\n return JSON.stringify(arg) ?? String(arg);\n } catch {\n /**\n * Circular, or a getter that threw. Reaching for the constructor name is\n * tempting and is itself a property read \u2014 on a Proxy whose `get` trap\n * throws, the fallback throws too, outside the try that was meant to\n * contain it. Ask the prototype instead, which no `get` trap sees.\n */\n try {\n const proto = Object.getPrototypeOf(arg) as { constructor?: { name?: string } } | null;\n return `[${proto?.constructor?.name ?? \"object\"}]`;\n } catch {\n return \"[object]\";\n }\n }\n }\n try {\n return String(arg);\n } catch {\n return \"[unrenderable]\";\n }\n}\n\nconst truncate = (text: string, max: number) =>\n text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;\n\nexport function formatConsoleArgs(args: unknown[]): string {\n return truncate(args.map(describeArg).join(\" \"), MAX_MESSAGE);\n}\n\n/** The first `Error` among the arguments carries the only stack worth keeping. */\nexport function firstStack(args: unknown[]): string | undefined {\n for (const arg of args) {\n if (arg instanceof Error && typeof arg.stack === \"string\") {\n return truncate(arg.stack, MAX_STACK);\n }\n }\n return undefined;\n}\n\n/** A response is interesting when it is not 2xx. `0` marks a request that never got one. */\nexport function isFailedStatus(status: number): boolean {\n return status === 0 || status < 200 || status >= 300;\n}\n\nexport interface StateCollector {\n getState(): AnnotationState;\n /** Restore every patched global. Exposed mainly so tests can prove it is reversible. */\n destroy(): void;\n}\n\nexport interface StateCollectorOptions {\n limit: number;\n /** Feature flags are the app's to know; this module does not guess at them. */\n getFeatureFlags: () => Record<string, unknown>;\n now: () => string;\n}\n\nexport const defaultStateCollectorOptions = (): StateCollectorOptions => ({\n limit: DEFAULT_LIMIT,\n getFeatureFlags: () => ({}),\n now: () => new Date().toISOString(),\n});\n\n/**\n * Patch `console`, `fetch` and `XMLHttpRequest`, and record what goes wrong.\n *\n * Install this as early as the bundle allows \u2014 an error thrown before the patch\n * is an error the payload will not carry.\n */\nexport function installStateCollector(\n win: Window & typeof globalThis,\n options: StateCollectorOptions = defaultStateCollectorOptions(),\n): StateCollector {\n const consoleErrors = new RingBuffer<ConsoleEntry>(options.limit);\n const failedRequests = new RingBuffer<NetworkEntry>(options.limit);\n\n const record = (level: ConsoleEntry[\"level\"], args: unknown[]) => {\n consoleErrors.push({\n level,\n message: formatConsoleArgs(args),\n ...(firstStack(args) ? { stack: firstStack(args) } : {}),\n at: options.now(),\n });\n };\n\n // --- console -------------------------------------------------------------\n\n const originalError = win.console.error;\n const originalWarn = win.console.warn;\n\n win.console.error = function patchedError(...args: unknown[]) {\n try {\n record(\"error\", args);\n } catch {\n // Recording is best-effort. The app's log must go out regardless.\n }\n return originalError.apply(win.console, args as never);\n };\n\n win.console.warn = function patchedWarn(...args: unknown[]) {\n try {\n record(\"warn\", args);\n } catch {\n /* as above */\n }\n return originalWarn.apply(win.console, args as never);\n };\n\n // --- fetch ---------------------------------------------------------------\n\n /**\n * `unbound-method` is right in general and wrong here (A-97). This is a\n * patch-and-restore: the original is saved so it can be called with an\n * explicit receiver (`originalFetch.call(win, \u2026)`, below) and put back on\n * teardown. Binding it instead would make the restore install a bound copy\n * rather than the function the page started with.\n */\n // eslint-disable-next-line @typescript-eslint/unbound-method\n const originalFetch = win.fetch;\n\n const requestInfo = (input: unknown, init?: RequestInit) => {\n const url =\n typeof input === \"string\"\n ? input\n : input instanceof URL\n ? input.href\n : ((input as Request)?.url ?? String(input));\n const method = init?.method ?? (input as Request)?.method ?? \"GET\";\n return { url, method: method.toUpperCase() };\n };\n\n if (typeof originalFetch === \"function\") {\n win.fetch = async function patchedFetch(input: never, init?: RequestInit) {\n try {\n const response = await originalFetch.call(win, input, init);\n if (isFailedStatus(response.status)) {\n const { url, method } = requestInfo(input, init);\n failedRequests.push({ url, method, status: response.status, at: options.now() });\n }\n return response;\n } catch (error) {\n /**\n * A request that never got a response is recorded as status 0. The spec\n * says \"non-2xx\", and a connection that was refused or aborted is the\n * most interesting failure of all \u2014 it is exactly what a reviewer means\n * by \"nothing loaded\". Rethrown untouched: this is an observer.\n */\n const { url, method } = requestInfo(input, init);\n failedRequests.push({ url, method, status: 0, at: options.now() });\n throw error;\n }\n } as typeof win.fetch;\n }\n\n // --- XMLHttpRequest ------------------------------------------------------\n\n const XHR = win.XMLHttpRequest;\n // Saved to be `.apply`d with an explicit `this` and restored on teardown \u2014\n // see the note on `originalFetch` above.\n // eslint-disable-next-line @typescript-eslint/unbound-method\n const originalOpen = XHR?.prototype?.open;\n\n if (typeof originalOpen === \"function\") {\n XHR.prototype.open = function patchedOpen(\n this: XMLHttpRequest & { __aiAnnotateMethod?: string; __aiAnnotateUrl?: string },\n method: string,\n url: string | URL,\n ...rest: unknown[]\n ) {\n this.__aiAnnotateMethod = method.toUpperCase();\n this.__aiAnnotateUrl = String(url);\n\n // `loadend` fires for success, failure, abort and timeout alike, so one\n // listener covers every way a request can finish.\n this.addEventListener(\"loadend\", () => {\n try {\n if (isFailedStatus(this.status)) {\n failedRequests.push({\n url: this.__aiAnnotateUrl ?? \"\",\n method: this.__aiAnnotateMethod ?? \"GET\",\n status: this.status,\n at: options.now(),\n });\n }\n } catch {\n /* observing must never break the request */\n }\n });\n\n return originalOpen.apply(this, [method, url, ...rest] as never);\n };\n }\n\n return {\n getState(): AnnotationState {\n return {\n consoleErrors: consoleErrors.toArray(),\n failedRequests: failedRequests.toArray(),\n featureFlags: options.getFeatureFlags(),\n };\n },\n destroy() {\n win.console.error = originalError;\n win.console.warn = originalWarn;\n if (typeof originalFetch === \"function\") win.fetch = originalFetch;\n if (typeof originalOpen === \"function\") XHR.prototype.open = originalOpen;\n },\n };\n}\n"],
5
- "mappings": "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",
6
- "names": ["SDK_VERSION", "SRC_ATTR", "NAME_ATTR", "selectionOf", "target", "PARENT_TYPES", "isParentMessage", "data", "type", "assertConcreteOrigin", "origin", "ANNOTATION_TOKEN_PARAM", "ANNOTATION_ARMED_COOKIE", "CAPTURE_TOKEN_PARAM", "REVIEW_RUN_PARAM", "ANNOTATION_ARMED_VALUE", "armedCookie", "name", "clearedCookie", "LEGACY_ANNOTATION_TOKEN_PARAM", "LEGACY_ANNOTATION_ARMED_COOKIE", "ANNOTATION_TOKEN_PARAMS", "ANNOTATION_ARMED_COOKIES", "isAnnotationArmed", "cookie", "entry", "trimmed", "readAnnotationToken", "params", "value", "OVERLAY_ID", "Z_INDEX", "createOverlay", "doc", "root", "tag", "box", "label", "node", "MAX_TEXT", "MAX_CHAIN", "parseSourceRef", "value", "colIdx", "lineIdx", "file", "line", "column", "collectSourceChain", "el", "max", "chain", "node", "ref", "SRC_ATTR", "prev", "findAnnotatedAncestor", "classTokens", "list", "domPath", "maxDepth", "parts", "tag", "part", "classes", "summariseText", "text", "collapsed", "resolveTarget", "options", "annotated", "source", "target", "componentName", "NAME_ATTR", "MAX_ISSUE_URL", "safeIssueUrl", "value", "url", "postMessageTransport", "parentOrigin", "win", "origin", "assertConcreteOrigin", "message", "assertConcreteEndpoint", "endpoint", "HTTP_TRANSPORT_TIMEOUT_MS", "httpTransport", "options", "token", "timeoutMs", "doFetch", "kind", "controller", "timer", "response", "body", "issueUrl", "error", "createEgress", "transport", "onError", "report", "failure", "MAX_SCREENSHOT_BYTES", "MAX_SCREENSHOT_EDGE", "SCREENSHOT_TIMEOUT_MS", "ATTEMPTS", "FORMATS", "resolveScreenshotOptions", "options", "doc", "opts", "MEDIA_TAGS", "MEDIA_SELECTOR", "MAX_ANCESTOR_HOPS", "isMediaElement", "el", "findCaptureSource", "maxHops", "descendant", "node", "hops", "intrinsicSize", "tag", "pair", "w", "h", "fitWithin", "size", "maxEdge", "scale", "encode", "source", "quality", "canvas", "ctx", "format", "dataUrl", "captureScreenshotSync", "natural", "attempt", "captureScreenshot", "resolve", "settled", "settle", "value", "timer", "attachScreenshot", "payload", "screenshot", "EMPTY_STATE", "emptyAnnotationState", "createAnnotationSdk", "options", "parentOrigin", "assertConcreteOrigin", "win", "doc", "screenshotOptions", "resolveScreenshotOptions", "enabled", "overlay", "hoverFrame", "lastHovered", "collecting", "transport", "postMessageTransport", "post", "createEgress", "boxOf", "el", "r", "targetFor", "resolveTarget", "labelFor", "target", "file", "onMouseMove", "event", "buildPayload", "state", "deliver", "payload", "message", "send", "outcome", "onClick", "collect", "attachScreenshot", "withShot", "point", "captured", "ask", "failure", "words", "comment", "error", "finished", "onKeyDown", "disable", "enable", "createOverlay", "onMessage", "isParentMessage", "CAPTURE_TOKEN_STORAGE_KEY", "REVIEW_RUN_STORAGE_KEY", "STRIPPED_PARAMS", "ANNOTATION_TOKEN_PARAMS", "CAPTURE_TOKEN_PARAM", "REVIEW_RUN_PARAM", "safely", "run", "fallback", "defaultStorage", "win", "readArming", "options", "doc", "storage", "href", "url", "captureToken", "runId", "name", "history", "disarm", "ANNOTATION_ARMED_COOKIES", "clearedCookie", "isAnnotationArmed", "ANNOTATABLE_ENVS", "isAnnotatableBuild", "build", "DEFAULT_LABELS", "TOOLBAR_ID", "Z_INDEX", "COMPOSER_WIDTH", "COMPOSER_HEIGHT", "MARGIN", "CSS", "make", "doc", "tag", "attrs", "el", "name", "createAnnotationToolbar", "options", "labels", "viewport", "host", "root", "style", "geometry", "bar", "panel", "toggle", "done", "pill", "dot", "pillText", "composer", "where", "commentLabel", "input", "cancel", "send", "row", "toast", "toastText", "toastAction", "toastRetry", "sdk", "expanded", "settle", "restoreFocus", "toastTimer", "retryHandler", "show", "visible", "setExpanded", "open", "setAnnotating", "on", "teardown", "close", "comment", "resolve", "key", "collectComment", "request", "describe", "position", "source", "componentName", "box", "belowY", "top", "left", "showResult", "outcome", "retry", "ok", "url", "safeIssueUrl", "node", "attached", "mountAnnotation", "injected", "armed", "isAnnotatableBuild", "toolbar", "createAnnotationSdk", "DEFAULT_LIMIT", "_items", "_limit", "RingBuffer", "limit", "__privateAdd", "__privateSet", "item", "__privateGet", "describeArg", "arg", "truncate", "text", "max", "formatConsoleArgs", "args", "firstStack", "isFailedStatus", "status", "defaultStateCollectorOptions", "installStateCollector", "win", "options", "consoleErrors", "failedRequests", "record", "level", "originalError", "originalWarn", "originalFetch", "requestInfo", "input", "init", "url", "method", "response", "error", "XHR", "originalOpen", "rest"]
4
+ "sourcesContent": ["/**\n * The annotation postMessage protocol (A-36).\n *\n * Mirrors `docs/contracts/types.ts`. That file is the\n * cross-component contract; this is the runtime copy the product bundle ships,\n * because the product cannot import from a docs directory. Changing a shape\n * here is a change to both \u2014 say so in the PR.\n */\n\nexport const SDK_VERSION = \"1\";\n\n/**\n * ===========================================================================\n * THE ATTRIBUTES \u2014 THE OTHER WIRE (A-186)\n * ===========================================================================\n *\n * These live in `protocol.ts`, not in `target.ts` where they are read, because\n * they are a contract between TWO PACKAGES that a consumer installs separately:\n * `@weatherboard/annotation-next` WRITES them at build time and this package\n * READS them at runtime. Two spellings of `data-src` is not a bug anybody\n * catches \u2014 the transform runs, the attributes appear, and every capture comes\n * back with no source ref at all, which is indistinguishable from the transform\n * not running.\n *\n * `annotation-next` imports these from `@weatherboard/annotation/protocol`\n * rather than declaring its own, which is why the subpath has no DOM in it: a\n * build tool importing the page SDK to learn two strings would be absurd.\n */\nexport const SRC_ATTR = \"data-src\";\nexport const NAME_ATTR = \"data-src-name\";\n\nexport interface SourceRef {\n file: string;\n line: number;\n column?: number;\n}\n\nexport interface BuildInfo {\n /** Mandatory. Feedback not pinned to a revision is ambiguous the moment someone merges. */\n gitSha: string;\n /**\n * Where this build came from.\n *\n * `\"development\"` was added in A-37, when the SDK was first mounted in a\n * real app and the two halves turned out not to agree: `resolveBuildInfo` in\n * `resolveBuildInfo` in the product's own Next config package \u2014 the module\n * that stamps the build-env variable, and the only producer of this field \u2014\n * emits\n * `production | preview | development`, mirroring `VERCEL_ENV`. This union\n * instead offered `\"staging\"`, which nothing produces: a staging deploy IS a\n * Vercel preview and reports itself as one. Widening rather than forcing the\n * mount to lie: a locally-annotated build calling itself a preview would put\n * someone's laptop in the corpus as though it were a deployment.\n */\n env: \"production\" | \"preview\" | \"staging\" | \"development\";\n branch?: string;\n /** Set when reviewing an agent's preview deploy, so feedback routes to that run's session. */\n previewRunId?: string;\n}\n\nexport interface ConsoleEntry {\n level: \"error\" | \"warn\";\n message: string;\n stack?: string;\n at: string;\n}\n\nexport interface NetworkEntry {\n url: string;\n method: string;\n status: number;\n at: string;\n}\n\nexport interface TargetInfo {\n /**\n * What the reviewer selected \u2014 one element, or an area (A-179).\n *\n * -------------------------------------------------------------------------\n * WHY THIS EXISTS RATHER THAN BEING INFERRED\n * -------------------------------------------------------------------------\n *\n * A region capture and an element capture are the same SHAPE: a bounding box,\n * a chain of source refs, a DOM path. Nothing in the values distinguishes\n * them, and the two mean opposite things about the chain \u2014 for an element it\n * is ANCESTORS, innermost first, so entry two contains entry one; for a\n * region it is SIBLINGS the rectangle happened to cross, which contain\n * nothing of each other.\n *\n * A reader that guesses will describe a region's siblings as a call-site\n * chain, which is a confident false statement about the page. `describeCapture`\n * hands that description to a model that writes acceptance criteria from it.\n *\n * -------------------------------------------------------------------------\n * WHY IT IS OPTIONAL, AND WHY ABSENT MEANS `element`\n * -------------------------------------------------------------------------\n *\n * The two halves of this protocol live in different repositories and deploy\n * independently (see the note on the legacy parameter names below). Every\n * capture in the store today, and every capture from a product bundle that\n * predates region selection, has no such field \u2014 and all of them are element\n * captures. Absent therefore has to mean `element`, or this field would\n * retroactively make the entire history ambiguous.\n *\n * Required-with-a-default would have the same effect and a worse failure: an\n * older bundle omits it, the type says it is there, and every reader believes\n * a value nobody sent.\n */\n selection?: \"element\" | \"region\";\n source?: SourceRef;\n /**\n * Every `data-src` from the clicked element up to the document root, innermost\n * first \u2014 an addition to the contract, and the reason is structural.\n *\n * Only host elements carry a ref (A-33), so `source` names the file that\n * *writes the markup*. For a composed design-system component that is\n * `MediaCard.tsx`, not the `GuideDetailCard.tsx` a human would say they\n * clicked. The chain is what lets the PM agent and the coding agent see both,\n * and pick the call site over the leaf when the feedback is about content\n * rather than the component itself.\n */\n sourceChain?: SourceRef[];\n componentName?: string;\n /** CSS selector fallback, for when no ancestor carries a ref at all. */\n domPath: string;\n boundingBox: { x: number; y: number; w: number; h: number };\n /** Truncated to 200 characters. */\n textContent?: string;\n}\n\n/**\n * What kind of capture this is, with the default applied once (A-189).\n *\n * `selection` is optional and **absent means `element`** \u2014 every capture taken\n * before A-179, and everything the managed product's live bundle emits today,\n * has no such field. Any other default would retroactively reclassify the whole\n * history as regions, silently.\n *\n * A function rather than a documented convention because the default is the\n * kind of thing two readers implement slightly differently and neither notices:\n * one writes `=== \"region\"`, another writes `?? \"element\"`, a third forgets and\n * treats `undefined` as falsy in a branch that meant something else. This is\n * the protocol's answer, and `compatibility.test.ts` replays every recorded\n * payload through it.\n */\nexport function selectionOf(target: Pick<TargetInfo, \"selection\">): \"element\" | \"region\" {\n return target.selection === \"region\" ? \"region\" : \"element\";\n}\n\nexport interface AnnotationPayload {\n schemaVersion: 1;\n capturedAt: string;\n build: BuildInfo;\n page: {\n url: string;\n /** The matched route pattern, not the resolved URL. */\n route: string;\n viewport: { w: number; h: number; dpr: number };\n userAgent: string;\n };\n target: TargetInfo;\n state: {\n consoleErrors: ConsoleEntry[];\n failedRequests: NetworkEntry[];\n featureFlags: Record<string, unknown>;\n };\n screenshot?: { dataUrl: string; region: \"element\" | \"viewport\" };\n /** The human's words, verbatim. The parent frame fills this in; we never rewrite it. */\n comment: string;\n}\n\nexport type ParentToFrame =\n | { type: \"annotate:enable\" }\n | { type: \"annotate:disable\" }\n | { type: \"annotate:ping\" };\n\nexport type FrameToParent =\n | { type: \"annotate:ready\"; sdkVersion: string; build: BuildInfo }\n | { type: \"annotate:hover\"; target: TargetInfo }\n | { type: \"annotate:capture\"; payload: AnnotationPayload }\n | { type: \"annotate:cancel\" };\n\nconst PARENT_TYPES = new Set([\"annotate:enable\", \"annotate:disable\", \"annotate:ping\"]);\n\n/**\n * Is this a message we recognise?\n *\n * Origin is checked separately and first \u2014 a well-formed message from the wrong\n * origin is the case this guard must not be trusted to catch.\n */\nexport function isParentMessage(data: unknown): data is ParentToFrame {\n if (typeof data !== \"object\" || data === null) return false;\n const type = (data as { type?: unknown }).type;\n return typeof type === \"string\" && PARENT_TYPES.has(type);\n}\n\n/**\n * Reject wildcards outright rather than treating them as \"any origin\".\n *\n * `postMessage(msg, \"*\")` broadcasts the payload to whatever happens to be\n * framing us, and an annotation payload carries page content, console errors and\n * failed request URLs. The spec's rule is no wildcards in either direction; this\n * makes it impossible to configure one by accident.\n */\nexport function assertConcreteOrigin(origin: string): string {\n if (!origin || origin === \"*\") {\n throw new Error(\n \"[annotation] a concrete parent origin is required; '*' would broadcast \" +\n \"annotation payloads to any embedder.\",\n );\n }\n return origin;\n}\n\n/**\n * The query parameter that arms the SDK in a managed product, and the cookie\n * that records it.\n *\n * THESE LIVE IN THE PROTOCOL BECAUSE THE TWO HALVES ARE NOW IN DIFFERENT\n * REPOSITORIES. The managed product's middleware looks for the parameter and\n * sets the cookie; the management app appends the parameter and the SDK reads\n * the cookie. While both halves were one monorepo, a test could read the other\n * side's source and assert the strings matched \u2014 that gate does not survive a\n * repository split, and a mismatch produces no error at all: the frame loads,\n * the cookie is never set, the SDK never boots, and the reviewer sees \"waiting\n * for the page to announce itself\" with nothing to go on.\n *\n * So they stop being two spellings that agree and become one exported\n * constant. A managed product that imports this package cannot drift from it.\n */\nexport const ANNOTATION_TOKEN_PARAM = \"ai_annotate\";\nexport const ANNOTATION_ARMED_COOKIE = \"ai_annotate_armed\";\n\n/**\n * The CAPTURE token's query parameter (A-240).\n *\n * -------------------------------------------------------------------------\n * WHY IT IS HERE AND NOT WHERE IT IS WRITTEN\n * -------------------------------------------------------------------------\n *\n * A second name on the same wire, and a second credential \u2014 see\n * `docs/plans/first-party-annotation.md` \u00A7 *Authentication*. `ai_annotate`\n * answers *should this page offer an annotation UI at all*; this one answers\n * *may this capture become an issue*. Different secrets, different lifetimes,\n * different blast radii, and deliberately not folded into one token.\n *\n * It lives beside `ANNOTATION_TOKEN_PARAM` for that constant's own reason,\n * stated at length above: the console WRITES it into a deep link and the\n * product's bundle READS it, the two halves ship from different repositories,\n * and a mismatch produces no error anywhere \u2014 the reviewer writes a post-it,\n * clicks send, and the ingest rejects an unsigned capture. One exported\n * constant is the only thing that makes those two spellings unable to drift.\n *\n * It has no legacy spelling because it has never had another one: nothing has\n * shipped it yet. It is exported here BEFORE its writer needs it precisely so\n * the writer never has a reason to spell it itself.\n */\nexport const CAPTURE_TOKEN_PARAM = \"ai_capture\";\n\n/**\n * The run under review, when the console knows of one (A-241).\n *\n * Third name on the same wire, here for the reason the two above are: the\n * console writes it and the product's bundle reads it, from two repositories.\n * `apps/manager/src/lib/request-changes.ts` spells it itself today and its own\n * comment asks for exactly this \u2014 the constant to move into the package so the\n * writer has no reason to spell it. **That deletion is not this slice's**\n * (A-243/A-244 are merged and settled), so for now there are two definitions of\n * one string, and `protocol.test.ts` is the only thing standing between them\n * and a silent divergence. Nothing about it is signed: the ingest already\n * treats it as attacker-supplied text and files on it rather than routing on it.\n */\nexport const REVIEW_RUN_PARAM = \"ai_run\";\n\n/**\n * The arming cookie, as a `Set-Cookie` value (A-241).\n *\n * ===========================================================================\n * IT GOT SIMPLER BY MOVING FIRST-PARTY, AND THAT IS THE WHOLE POINT\n * ===========================================================================\n *\n * Framed, this had to be `SameSite=None; Secure` with no `Partitioned` \u2014 a\n * genuine cross-site cookie, which A-211 records would have broken on its own\n * under Chrome's third-party cookie work, independently of the auth problem\n * that killed the frame. Top-level at the product's own origin it is an\n * ordinary cookie and none of that applies.\n *\n * Each attribute, and why:\n *\n * - **`SameSite=Lax`.** The reviewer arrives by clicking *Request changes* in\n * the console, which is a top-level cross-site GET navigation \u2014 the one case\n * `Lax` still sends on. `Strict` would withhold the cookie on that very first\n * render, so the toolbar would be missing on the page the link opened and\n * appear only after an internal click; that reads as a broken deploy.\n * - **`Secure`.** It is a marker rather than a credential, but a marker that\n * travels in clear is a marker an attacker on the path can set.\n * - **`Path=/`.** Arming survives the reviewer navigating the whole app, which\n * is the point of a cookie rather than a URL parameter.\n * - **No `Max-Age` / `Expires`.** Session-scoped: closing the tab disarms.\n *\n * ===========================================================================\n * WHAT IS *NOT* HERE: `HttpOnly`, AND THE TOKEN\n * ===========================================================================\n *\n * No `HttpOnly`, deliberately. Page script both READS this (`isAnnotationArmed`\n * decides whether a toolbar exists) and CLEARS it (*Done* disarms). An\n * `HttpOnly` arming cookie makes *Done* impossible from the page, which is the\n * one control the reviewer has.\n *\n * That is affordable only because THE VALUE IS A MARKER AND NEVER THE TOKEN.\n * The value is a constant and there is no argument that supplies one: the\n * middleware compares the URL parameter against its secret and then calls this,\n * and what it stores is *yes*, not *what was presented*. The single parameter\n * is the cookie's NAME, and it exists only so a product mid-migration can write\n * the legacy spelling \u2014 it is not a value slot wearing a different label, and\n * `arming.test.ts` pins which side of the `=` it lands on.\n *\n * This is a shape argument, not a guarantee about callers. Nothing stops a\n * determined caller passing a secret as the name; what the signature removes is\n * the ACCIDENT \u2014 the middleware author with a verified token in hand and one\n * parameter available, reaching for the obvious.\n */\nexport const ANNOTATION_ARMED_VALUE = \"1\";\n\nexport function armedCookie(name: string = ANNOTATION_ARMED_COOKIE): string {\n return `${name}=${ANNOTATION_ARMED_VALUE}; Path=/; SameSite=Lax; Secure`;\n}\n\n/**\n * The same cookie, expired \u2014 what *Done* and a disarm write.\n *\n * `Max-Age=0` AND an `Expires` in the past, because they are read by different\n * things and the pair is what every browser honours. The path must match the\n * one set above or the browser stores a second cookie and the first survives \u2014\n * a *Done* button that does nothing and reports success.\n */\nexport function clearedCookie(name: string): string {\n return `${name}=; Path=/; SameSite=Lax; Secure; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;\n}\n\n/**\n * The names these used to have (A-98).\n *\n * -------------------------------------------------------------------------\n * WHY THE RENAME, AND WHY IT COULD NOT JUST BE DONE\n * -------------------------------------------------------------------------\n *\n * They were `pits_annotate` and `pits_annotate_armed` \u2014 one managed product's\n * initialism, in a contract every managed product's bundle has to implement.\n * Onboarding a second product meant asking an unrelated codebase to adopt the\n * first product's name in its cookies and its query strings. That is the bug\n * A-98 is about, and it is a bug in a wire protocol rather than in a variable\n * name, which is what makes it expensive.\n *\n * The two halves live in DIFFERENT REPOSITORIES and deploy independently. A\n * clean rename is therefore a breaking change to every product bundle\n * simultaneously, and its failure mode is silence: the frame loads, the cookie\n * is never set, the SDK never boots, and the reviewer sees \"waiting for the\n * page to announce itself\" with nothing to go on. Nothing throws, nothing logs,\n * and the natural reading is that the agent broke the site.\n *\n * -------------------------------------------------------------------------\n * THE MIGRATION: READ BOTH, WRITE BOTH, THEN STOP WRITING THE OLD ONE\n * -------------------------------------------------------------------------\n *\n * 1. **Now.** Every reader accepts either name. Every writer emits BOTH, so a\n * product still running the old bundle arms on the legacy name and one\n * running the new bundle arms on the new one. Deploy order stops mattering,\n * which is the whole point \u2014 there is no window in which a reviewer sees the\n * silent failure above.\n * 2. **After every managed product's bundle is on this version** (A-102). Drop\n * the legacy names from the writers, then from the readers. Two steps, not\n * one: a reader that stops accepting the old name before the writers stop\n * emitting it re-creates the outage in the other direction.\n *\n * The legacy names are exported rather than inlined so the deletion in step 2\n * is `git grep LEGACY_` rather than an archaeology exercise.\n */\nexport const LEGACY_ANNOTATION_TOKEN_PARAM = \"pits_annotate\";\nexport const LEGACY_ANNOTATION_ARMED_COOKIE = \"pits_annotate_armed\";\n\n/**\n * ===========================================================================\n * AND THE NAMES ARE NOT CHANGING AGAIN FOR THE PUBLISHED PACKAGE (A-184)\n * ===========================================================================\n *\n * When this becomes `@weatherboard/annotation` the obvious next thought is that\n * the wire names should match the package \u2014 `weatherboard_annotate`. They\n * should not, and the reason is the migration described above rather than\n * taste.\n *\n * That migration is STILL IN FLIGHT. Readers accept both spellings; writers\n * emit both; step 2 waits on every managed product's bundle being current.\n * A third spelling introduced now does not replace the second, it joins it:\n * every reader accepts three names, every writer emits three, and the window in\n * which a product can be on any of them widens instead of closing.\n *\n * The cost is not the code. It is that each rename buys another instance of the\n * failure this file exists to prevent \u2014 the frame loads, the cookie is never\n * set, the SDK never boots, and the reviewer sees \"waiting for the page to\n * announce itself\" with nothing to go on. Twice is a migration. Three times is\n * a habit, and the third one would be paid by consumers who never asked for\n * either.\n *\n * `ai_annotate` is already product-neutral, which was the entire point of A-98.\n * A published package needs names that name no product; it does not need names\n * that name the package. Nobody reading a query string cares which npm scope\n * the library came from.\n *\n * The one real objection is collision: `ai_` is a common prefix and some other\n * tool could plausibly use it. That is survivable by construction \u2014 arming\n * requires the parameter to carry a token the product compares exactly, so a\n * coincidental `?ai_annotate=` from an unrelated tool arms nothing. A colliding\n * COOKIE cannot false-positive either; `isAnnotationArmed` matches on the\n * delimiter rather than by substring, for exactly this reason.\n *\n * If a rename is ever genuinely warranted, it happens ONCE, after step 2 has\n * landed and the reader accepts one name again \u2014 not layered on top of an\n * unfinished one.\n */\n\n/** Both spellings of the query parameter, current first. */\nexport const ANNOTATION_TOKEN_PARAMS = [\n ANNOTATION_TOKEN_PARAM,\n LEGACY_ANNOTATION_TOKEN_PARAM,\n] as const;\n\n/** Both spellings of the cookie, current first. */\nexport const ANNOTATION_ARMED_COOKIES = [\n ANNOTATION_ARMED_COOKIE,\n LEGACY_ANNOTATION_ARMED_COOKIE,\n] as const;\n\n/**\n * Whether a cookie jar carries the armed cookie, under either name.\n *\n * Delimiter-aware rather than a substring test: a cookie named\n * `not_ai_annotate_armed`, or another cookie whose VALUE contains the name,\n * satisfies `includes` \u2014 and arming an annotation channel on a coincidence is\n * not a failure anyone would think to look for.\n */\nexport function isAnnotationArmed(cookie: string): boolean {\n return cookie\n .split(\";\")\n .some((entry) => {\n const trimmed = entry.trimStart();\n return ANNOTATION_ARMED_COOKIES.some((name) => trimmed.startsWith(`${name}=`));\n });\n}\n\n/**\n * The arming token from a URL's query string, under either name.\n *\n * Current name wins when both are present. They should never disagree \u2014 the\n * management app writes both from one value \u2014 but if they ever do, the one this\n * version of the protocol owns is the one to believe.\n */\nexport function readAnnotationToken(params: URLSearchParams): string | null {\n for (const name of ANNOTATION_TOKEN_PARAMS) {\n const value = params.get(name);\n if (value !== null && value.length > 0) return value;\n }\n return null;\n}\n", "/**\n * The hover highlight, drawn inside the iframe.\n *\n * The parent frame does not draw this, and that is a deliberate call from\n * `spec/02-annotation-sdk.md`: translating coordinates across the frame boundary\n * means tracking the iframe's position, the page's scroll, and any transform on\n * either side, and getting it wrong puts the box next to the thing rather than\n * on it. Inside the frame, `getBoundingClientRect` is already in the right\n * coordinate space.\n */\n\n/**\n * The SDK'\"'\"'s own node, so this is not a cross-repository contract like the\n * cookie and the query parameter are \u2014 nothing outside this package creates it\n * or looks it up. Renamed with them anyway (A-98): a product whose stylesheet\n * happens to target the old id would be styling the overlay by accident, and\n * the new name is the one a reader will expect to find.\n */\nexport const OVERLAY_ID = \"ai-annotation-overlay\";\n\n/** Above app chrome without being a magic number nobody can justify. */\nconst Z_INDEX = \"2147483000\";\n\nexport interface Overlay {\n show(box: { x: number; y: number; w: number; h: number }, label: string | null): void;\n hide(): void;\n destroy(): void;\n /** The overlay's own nodes, so hit-testing can ignore them. */\n contains(node: unknown): boolean;\n}\n\nexport function createOverlay(doc: Document): Overlay {\n const root = doc.createElement(\"div\");\n root.id = OVERLAY_ID;\n // `pointer-events: none` is what keeps the overlay from hit-testing itself:\n // without it, `elementFromPoint` returns the highlight and the highlight\n // follows the highlight.\n root.style.cssText = [\n \"position:fixed\",\n \"top:0\",\n \"left:0\",\n \"pointer-events:none\",\n `z-index:${Z_INDEX}`,\n \"display:none\",\n \"box-sizing:border-box\",\n \"border:2px solid #2563eb\",\n \"background:rgba(37,99,235,0.12)\",\n \"border-radius:2px\",\n ].join(\";\");\n\n const tag = doc.createElement(\"div\");\n tag.style.cssText = [\n \"position:absolute\",\n \"left:0\",\n \"top:-20px\",\n \"font:11px/16px ui-monospace,SFMono-Regular,Menlo,monospace\",\n \"color:#fff\",\n \"background:#2563eb\",\n \"padding:1px 5px\",\n \"border-radius:2px\",\n \"white-space:nowrap\",\n \"max-width:60vw\",\n \"overflow:hidden\",\n \"text-overflow:ellipsis\",\n ].join(\";\");\n root.appendChild(tag);\n doc.body.appendChild(root);\n\n return {\n show(box, label) {\n root.style.display = \"block\";\n root.style.transform = `translate(${box.x}px, ${box.y}px)`;\n root.style.width = `${box.w}px`;\n root.style.height = `${box.h}px`;\n // A box at the very top of the viewport would render its label off-screen.\n tag.style.top = box.y < 20 ? \"100%\" : \"-20px\";\n tag.textContent = label ?? \"\";\n tag.style.display = label ? \"block\" : \"none\";\n },\n hide() {\n root.style.display = \"none\";\n },\n destroy() {\n root.remove();\n },\n contains(node) {\n return node instanceof Node && root.contains(node);\n },\n };\n}\n", "import { NAME_ATTR, SRC_ATTR, type SourceRef, type TargetInfo } from \"./protocol\";\n\n/**\n * Turning a clicked DOM element into something a coding agent can act on.\n *\n * Everything here is written against the narrowest possible slice of the DOM so\n * it can be tested without a browser \u2014 see `ElementLike`. The parts that need a\n * real browser (hit-testing, layout) live in `sdk.ts`.\n */\n\n/**\n * Re-exported, not declared. They moved to `protocol.ts` (A-186) because the\n * build transform in `@weatherboard/annotation-next` writes what this file\n * reads, and the two packages install separately. Kept exported from here so\n * every existing importer is unaffected.\n */\nexport { NAME_ATTR, SRC_ATTR };\n\nexport const MAX_TEXT = 200;\n/** Deep trees are common; a chain long enough to be useless is not worth sending. */\nexport const MAX_CHAIN = 8;\n\n/** The subset of `Element` this module reads. */\nexport interface ElementLike {\n tagName: string;\n id?: string;\n getAttribute(name: string): string | null;\n parentElement: ElementLike | null;\n classList?: { value?: string } | ArrayLike<string>;\n textContent?: string | null;\n}\n\n/**\n * Parse `path/to/File.tsx:42:7` into its parts.\n *\n * Split from the right: the line and column never contain a colon, and a path\n * conceivably might. Returns null rather than a half-parsed ref \u2014 a ref we\n * cannot read is worse than no ref, because downstream will believe it.\n */\nexport function parseSourceRef(value: string | null): SourceRef | null {\n if (!value) return null;\n\n const colIdx = value.lastIndexOf(\":\");\n if (colIdx <= 0) return null;\n const lineIdx = value.lastIndexOf(\":\", colIdx - 1);\n if (lineIdx <= 0) return null;\n\n const file = value.slice(0, lineIdx);\n const line = Number(value.slice(lineIdx + 1, colIdx));\n const column = Number(value.slice(colIdx + 1));\n\n if (!file || !Number.isFinite(line) || !Number.isFinite(column)) return null;\n return { file, line, column };\n}\n\n/**\n * Every source ref from `el` upwards, innermost first.\n *\n * Consecutive duplicates are collapsed: one JSX element often produces several\n * nested DOM nodes carrying the same ref, and repeating it says nothing.\n */\nexport function collectSourceChain(el: ElementLike | null, max = MAX_CHAIN): SourceRef[] {\n const chain: SourceRef[] = [];\n let node: ElementLike | null = el;\n\n while (node && chain.length < max) {\n const ref = parseSourceRef(node.getAttribute(SRC_ATTR));\n if (ref) {\n const prev = chain[chain.length - 1];\n const isRepeat = prev && prev.file === ref.file && prev.line === ref.line;\n if (!isRepeat) chain.push(ref);\n }\n node = node.parentElement;\n }\n\n return chain;\n}\n\n/** The nearest ancestor (or self) carrying a ref, which is the element a human means. */\nexport function findAnnotatedAncestor(el: ElementLike | null): ElementLike | null {\n let node: ElementLike | null = el;\n while (node) {\n if (parseSourceRef(node.getAttribute(SRC_ATTR))) return node;\n node = node.parentElement;\n }\n return null;\n}\n\nfunction classTokens(el: ElementLike): string[] {\n const list = el.classList as { value?: string } | undefined;\n const raw = typeof list?.value === \"string\" ? list.value : \"\";\n return raw.split(/\\s+/).filter(Boolean);\n}\n\n/**\n * A CSS-ish path to the element, used only when no ancestor carries a ref.\n *\n * Deliberately not guaranteed unique. It is a hint for a human reading the\n * payload, not a selector we promise to re-resolve \u2014 StyleX class names are\n * hashed and change between builds, so a path that looks precise would be\n * quietly wrong on the next deploy. IDs are kept because they survive; classes\n * are capped at two for readability.\n */\nexport function domPath(el: ElementLike | null, maxDepth = 6): string {\n const parts: string[] = [];\n let node: ElementLike | null = el;\n\n while (node && parts.length < maxDepth) {\n const tag = (node.tagName || \"\").toLowerCase();\n if (!tag || tag === \"html\") break;\n\n let part = tag;\n if (node.id) {\n // An id is unique on the page, so nothing above it adds information.\n parts.unshift(`${tag}#${node.id}`);\n return parts.join(\" > \");\n }\n const classes = classTokens(node).slice(0, 2);\n if (classes.length > 0) part += `.${classes.join(\".\")}`;\n parts.unshift(part);\n\n if (tag === \"body\") break;\n node = node.parentElement;\n }\n\n return parts.join(\" > \");\n}\n\n/** Collapse whitespace and truncate, so the payload carries a label rather than a page. */\nexport function summariseText(text: string | null | undefined): string | undefined {\n if (!text) return undefined;\n const collapsed = text.replace(/\\s+/g, \" \").trim();\n if (!collapsed) return undefined;\n return collapsed.length > MAX_TEXT ? `${collapsed.slice(0, MAX_TEXT - 1)}\u2026` : collapsed;\n}\n\nexport interface ResolveTargetOptions {\n boundingBox: { x: number; y: number; w: number; h: number };\n}\n\n/**\n * Build the `TargetInfo` for a clicked or hovered element.\n *\n * `source` is the nearest ref, which names the file that writes the markup \u2014\n * for a composed design-system component that is the design-system file, not\n * the call site. `sourceChain` carries the rest so the agent can choose.\n */\nexport function resolveTarget(el: ElementLike, options: ResolveTargetOptions): TargetInfo {\n const annotated = findAnnotatedAncestor(el);\n const chain = collectSourceChain(el);\n const source = chain[0];\n\n const target: TargetInfo = {\n domPath: domPath(el),\n boundingBox: options.boundingBox,\n };\n\n if (source) target.source = source;\n if (chain.length > 0) target.sourceChain = chain;\n\n const componentName = annotated?.getAttribute(NAME_ATTR);\n if (componentName) target.componentName = componentName;\n\n const text = summariseText(el.textContent);\n if (text) target.textContent = text;\n\n return target;\n}\n", "import { assertConcreteOrigin, type FrameToParent } from \"./protocol\";\n\n/**\n * Egress (A-239).\n *\n * ===========================================================================\n * WHY THIS IS A SEAM AND NOT A BRANCH IN `sdk.ts`\n * ===========================================================================\n *\n * `sdk.ts` had exactly one way out \u2014 `win.parent.postMessage(...)` \u2014 and\n * first-party there is no parent: `win.parent === win`. Every capture then\n * posts a message to the page that sent it and nothing receives it. **That\n * fails silently**, which is the shape to be careful of here: the overlay\n * behaves correctly, the reviewer clicks, and no issue is ever filed.\n *\n * See `docs/plans/first-party-annotation.md`. The console stops framing the\n * product and links out to it instead, so the capture has to leave the page\n * over HTTP. The frame is NOT being deleted \u2014 `postMessageTransport` is\n * today's behaviour, extracted unchanged, and it is still the default.\n *\n * ===========================================================================\n * TWO PROPERTIES THAT HAD TO SURVIVE\n * ===========================================================================\n *\n * 1. **Never a wildcard, in either direction.** `assertConcreteOrigin` still\n * guards the message path, and `assertConcreteEndpoint` is its equivalent\n * for HTTP. Both throw AT CONSTRUCTION rather than at first capture,\n * because a misconfiguration that only surfaces when a reviewer finally\n * clicks something is a misconfiguration nobody finds in review.\n *\n * 2. **The screenshot never costs the capture**, and neither may the network.\n * `attachScreenshot` resolves within its own timeout and never rejects, so\n * the payload goes out either way. An HTTP transport would reintroduce\n * exactly the failure mode that was designed out \u2014 a slow network losing a\n * capture \u2014 unless it carries its own timeout and its own `catch`, and\n * unless a failed send is REPORTABLE rather than swallowed. A dropped\n * capture that looks like a successful one is the worst outcome available\n * here, so `send` rejects loudly and `createEgress` routes that to the\n * caller.\n */\n\n/** Every message the page half can emit. */\nexport type FrameMessageKind = FrameToParent[\"type\"];\n\n/**\n * What the far end said about a capture it accepted (A-240).\n *\n * Everything on it is optional and always will be. `postMessageTransport`\n * cannot answer at all \u2014 `postMessage` has no reply \u2014 and the ingest may\n * answer with nothing. A reviewer who is told *sent* and given no link is in a\n * strictly better position than one told nothing, so an absent `issueUrl` is a\n * successful send and never a failed one.\n */\nexport interface SendReceipt {\n /**\n * The issue this capture became, when the far end names one.\n *\n * ANY VALUE HERE ENDS UP IN A THIRD PARTY'S DOM as an `href`, so the type\n * alone is not the contract \u2014 see `safeIssueUrl`, which is what every value\n * crossing the network boundary is put through before it gets to be one.\n */\n issueUrl?: string;\n}\n\n/** Longer than any issue URL and short enough not to be a payload. */\nconst MAX_ISSUE_URL = 2048;\n\n/**\n * A URL from the network that is safe to put in somebody else's page (A-240).\n *\n * ===========================================================================\n * WHY THIS IS NOT PARANOIA\n * ===========================================================================\n *\n * `httpTransport` reads `issueUrl` out of a RESPONSE BODY and the toolbar sets\n * it as an `<a href>` INSIDE THE CUSTOMER'S PAGE. `javascript:alert(1)` there\n * executes in the product's origin the moment the reviewer clicks *View\n * issue* \u2014 a script injection into a third party's site, shipped by us, on\n * every page of every managed product.\n *\n * `assertConcreteEndpoint` does not cover this and was never going to:\n * it constrains WHERE WE SEND, and this is WHAT COMES BACK. An ingest that is\n * misconfigured, compromised, or simply behind a proxy that rewrites bodies is\n * exactly the case a transport has to survive, and \"our own console would\n * never\" is not a security boundary.\n *\n * It lives HERE and not in the toolbar so the guarantee travels with the\n * receipt rather than with one renderer of it. The toolbar calls it too \u2014 see\n * `showResult` \u2014 because it renders receipts from ANY transport, including one\n * a consumer wrote, which never passes through this function.\n *\n * Anything not `http(s)` is dropped rather than repaired. A dropped link is a\n * toast that says the capture was sent and offers no link, which is the\n * outcome the receipt is already designed to survive.\n */\nexport function safeIssueUrl(value: unknown): string | undefined {\n if (typeof value !== \"string\" || value.length === 0 || value.length > MAX_ISSUE_URL) {\n return undefined;\n }\n let url: URL;\n try {\n // Absolute only: `new URL` throws on a relative one, and a relative href in\n // somebody else's page points somewhere in THEIR app.\n url = new URL(value);\n } catch {\n return undefined;\n }\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") return undefined;\n return url.href;\n}\n\nexport interface AnnotationTransport {\n /** For diagnostics and for tests; never sent anywhere. */\n readonly name: string;\n /**\n * May this transport carry this kind of message?\n *\n * The interface needs this because `annotate:hover` fires once per animation\n * frame while the reviewer moves the mouse. Over `postMessage` that is a\n * structured clone to a frame that is already listening; over HTTP it is one\n * request per frame, which is a denial of service aimed at our own console.\n *\n * A transport that DROPPED such a message silently would be the same bug\n * this issue exists to remove, one layer down. So declining is an explicit\n * answer the SDK asks for and respects, and `send` is never called for a\n * kind that was declined.\n */\n carries(kind: FrameMessageKind): boolean;\n /**\n * Deliver the message. May be synchronous.\n *\n * A returned promise MUST reject on failure. Resolving on a failed send is\n * the one thing a transport may not do.\n */\n send(message: FrameToParent): void | Promise<void | SendReceipt>;\n}\n\n/** What went wrong, handed back to whoever constructed the SDK. */\nexport interface EgressFailure {\n message: FrameToParent;\n transport: string;\n error: unknown;\n}\n\nexport type EgressErrorHandler = (failure: EgressFailure) => void;\n\n/**\n * The parent-window transport \u2014 today's behaviour, extracted unchanged.\n *\n * It carries everything: the frame is a live listener on the other end of a\n * cheap channel, and the handshake (`annotate:ready`) and the hover highlight\n * both belong to it.\n */\nexport function postMessageTransport(\n parentOrigin: string,\n win: Pick<Window, \"parent\"> = window,\n): AnnotationTransport {\n const origin = assertConcreteOrigin(parentOrigin);\n return {\n name: \"postMessage\",\n carries: () => true,\n send(message) {\n // Never a wildcard, in either direction.\n win.parent.postMessage(message, origin);\n },\n };\n}\n\n/**\n * Reject anything that is not one concrete `http(s)` URL.\n *\n * `assertConcreteOrigin`'s argument, applied to an endpoint. A relative URL\n * resolves against whatever page the SDK happens to be on \u2014 so a capture\n * POSTs into the CUSTOMER'S OWN app, 404s, and is lost; and a `javascript:`\n * or `data:` endpoint is a script-injection seam handed to whoever controls\n * the deep link. Neither is a thing to discover at first capture.\n */\nexport function assertConcreteEndpoint(endpoint: string): string {\n if (!endpoint || endpoint === \"*\") {\n throw new Error(\n \"[annotation] a concrete ingest endpoint is required; \" +\n \"'*' is not an address and an empty one posts nowhere.\",\n );\n }\n let url: URL;\n try {\n url = new URL(endpoint);\n } catch {\n throw new Error(\n `[annotation] the ingest endpoint must be an absolute URL, got ${JSON.stringify(endpoint)}; ` +\n \"a relative one resolves against the product's own origin and the capture is lost there.\",\n );\n }\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n throw new Error(\n `[annotation] the ingest endpoint must be http(s), got ${url.protocol}`,\n );\n }\n return url.toString();\n}\n\n/** Node's `fetch` and the browser's, narrowed to what we use. */\nexport type FetchLike = (\n input: string,\n init: {\n method: string;\n headers: Record<string, string>;\n body: string;\n credentials?: \"omit\";\n signal?: AbortSignal;\n },\n) => Promise<{\n ok: boolean;\n status: number;\n /**\n * Optional because `serve()`'s loopback responses are read through the\n * platform `fetch`, which has it, while a stub written to assert the `init`\n * need not. A transport that made the body mandatory would be asserting\n * something about the ingest that the ingest does not promise.\n */\n json?: () => Promise<unknown>;\n}>;\n\nexport interface HttpTransportOptions {\n /** The console's ingest URL. Absolute, `http(s)`, never `*`. */\n endpoint: string;\n /**\n * The capture token from the deep link \u2014 signed by the console, carried by\n * the product, never validated by it. See the plan's *Authentication*\n * section for why this is a second credential and not the arming one.\n */\n captureToken: string;\n /**\n * Own timeout, own catch. The default is deliberately short: a capture the\n * reviewer is told failed can be retried, and a request that hangs is\n * indistinguishable from one that worked.\n */\n timeoutMs?: number;\n /** Injectable only so the tests can drive a transport with no `fetch` at all. */\n fetch?: FetchLike;\n}\n\nexport const HTTP_TRANSPORT_TIMEOUT_MS = 8_000;\n\n/**\n * The first-party transport: POST the capture to the console's ingest.\n *\n * It carries `annotate:capture` AND NOTHING ELSE. `annotate:hover` is one\n * request per animation frame, and `annotate:ready` / `annotate:cancel` are\n * handshake with a parent window that does not exist first-party \u2014 sending\n * them would be filing HTTP requests nobody asked for.\n */\nexport function httpTransport(options: HttpTransportOptions): AnnotationTransport {\n const endpoint = assertConcreteEndpoint(options.endpoint);\n if (!options.captureToken) {\n throw new Error(\n \"[annotation] a capture token is required; the ingest rejects an unsigned capture \" +\n \"and the reviewer would only find out after writing one.\",\n );\n }\n const token = options.captureToken;\n const timeoutMs = options.timeoutMs ?? HTTP_TRANSPORT_TIMEOUT_MS;\n const doFetch: FetchLike | undefined = options.fetch ?? globalThis.fetch;\n\n return {\n name: \"http\",\n carries: (kind) => kind === \"annotate:capture\",\n async send(message) {\n if (!doFetch) {\n throw new Error(\"[annotation] no fetch implementation is available for the ingest\");\n }\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n const response = await doFetch(endpoint, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${token}`,\n },\n // Never the reviewer's cookies. The token is the whole credential,\n // and an ambient one would make this endpoint CSRF-shaped.\n credentials: \"omit\",\n body: JSON.stringify(message),\n signal: controller.signal,\n });\n if (!response.ok) {\n throw new Error(`[annotation] the ingest rejected the capture: HTTP ${response.status}`);\n }\n /**\n * The capture is delivered at this point and nothing below may undo\n * that. A body that is empty, not JSON, or JSON of some other shape is\n * an ingest that chose not to name an issue \u2014 telling the reviewer\n * their capture failed because we could not parse a courtesy would be\n * the same lie as a silent drop, pointing the other way.\n */\n try {\n const body = (await response.json?.()) as { issueUrl?: unknown } | undefined;\n // Untrusted, all of it. `safeIssueUrl` returns undefined for a\n // non-string, a relative URL, an over-long one and any scheme that\n // is not http(s) \u2014 so a hostile body degrades to a capture that was\n // sent and named no issue.\n const issueUrl = safeIssueUrl(body?.issueUrl);\n if (issueUrl !== undefined) return { issueUrl };\n } catch {}\n return {};\n } catch (error) {\n if (controller.signal.aborted) {\n throw new Error(`[annotation] the capture timed out after ${timeoutMs}ms`, {\n cause: error,\n });\n }\n throw error;\n } finally {\n clearTimeout(timer);\n }\n },\n };\n}\n\n/**\n * What became of one message (A-240).\n *\n * `createEgress` used to return `void`, which was enough while the only reader\n * of a failure was `console.error`. The composer has to TELL THE REVIEWER \u2014 the\n * issue link on success, a retry on failure \u2014 and it cannot do that from a\n * function that answers nothing. So the outcome is returned as a value, and the\n * promise NEVER REJECTS: a rejection here would be a second way for a lost\n * capture to disappear, one `await`-less caller away from silence.\n *\n * `declined` is its own case rather than a failure, because it is not one:\n * `annotate:hover` over HTTP is a kind the transport correctly refuses, and\n * calling that a failed capture would put a red toast in front of a reviewer\n * who did nothing wrong. It is only a problem if `annotate:capture` is what was\n * declined, and that is the caller's judgement rather than this function's.\n */\nexport type EgressOutcome =\n | { status: \"sent\"; receipt: SendReceipt }\n | { status: \"declined\" }\n | { status: \"failed\"; error: unknown };\n\n/**\n * The SDK's one way out, given a transport.\n *\n * Pure and DOM-free on purpose, so the two things worth arguing about \u2014 that a\n * declined kind is never sent, and that a failed send reaches the caller \u2014 are\n * testable without a browser. `sdk.ts` holds no other egress path.\n */\nexport function createEgress(\n transport: AnnotationTransport,\n onError?: EgressErrorHandler,\n): (message: FrameToParent) => Promise<EgressOutcome> {\n const report = (message: FrameToParent, error: unknown) => {\n const failure: EgressFailure = { message, transport: transport.name, error };\n if (onError) {\n onError(failure);\n return;\n }\n /**\n * No handler is not a licence to swallow it. Until A-240 puts a toast in\n * front of the reviewer, the console is the only surface there is, and a\n * lost capture must leave a trace somewhere.\n */\n console.error(\"[annotation] a capture could not be delivered\", failure);\n };\n\n /**\n * `async`, and the synchronous half still runs synchronously \u2014 everything up\n * to the first `await` does, which is `transport.send` itself. So\n * `postMessageTransport` still posts before this returns, and a transport\n * that throws is still reported before this returns. That is not an\n * incidental detail: `sdk.test.ts` asserts the frame path's handshake has\n * been posted the instant `createAnnotationSdk` returns, and it must stay\n * true, because the frame is not being deleted.\n */\n return async (message: FrameToParent): Promise<EgressOutcome> => {\n if (!transport.carries(message.type)) return { status: \"declined\" };\n try {\n const receipt = await transport.send(message);\n return { status: \"sent\", receipt: receipt ?? {} };\n } catch (error) {\n report(message, error);\n return { status: \"failed\", error };\n }\n };\n}\n", "/**\n * Screenshot capture for the annotation payload (A-31).\n *\n * WHAT THIS ACTUALLY CAPTURES \u2014 READ THIS BEFORE BELIEVING THE FIELD NAME.\n * This is not a page rasteriser. It captures the bitmap of a single\n * `<img>`, `<canvas>` or `<video>` at or around the clicked element, and\n * nothing else. Click a heading and the payload has no screenshot.\n *\n * WHY NOT html2canvas, WHICH THE SPEC SUGGESTS. `spec/02` offers html2canvas or\n * \"`element.toDataURL` via an offscreen canvas where feasible\". html2canvas is a\n * runtime dependency, and this package deliberately has none \u2014 it ships\n * TypeScript source that two repositories compile into their own bundles, so a\n * dependency here is a dependency, a version and a licence in both of them, for\n * a field `spec/02` itself calls a nice-to-have (\"do not block Phase 1 on\n * screenshots; the source ref is what actually matters\"). The platform gives us\n * no way to rasterise arbitrary DOM: `SVGForeignObject` cannot load\n * cross-origin CSS or fonts, and `getDisplayMedia` prompts the user. So we\n * implement the honest subset rather than a convincing-looking approximation,\n * and the spec's server-side fallback \u2014 render `page.url` at `build.gitSha`\n * headless and crop to `target.boundingBox` \u2014 remains the route to a real\n * picture of the page.\n *\n * The subset is worth having on its own: \"this image is the wrong one\" and\n * \"this video poster is stretched\" are common reports, and for those the\n * element's own bitmap is the entire evidence.\n *\n * NOTHING HERE MAY BREAK A CAPTURE. Every entry point resolves rather than\n * rejects. A tainted canvas (any cross-origin image without CORS headers, which\n * is most of them) makes `toDataURL` throw `SecurityError`; a detached element\n * makes `drawImage` throw; a huge bitmap makes it slow. All of those must end\n * with `screenshot` absent from an otherwise complete payload, never with a\n * capture the reviewer loses.\n */\n\n/** The canvas surface this module needs. Narrow so tests need no browser. */\nexport interface CanvasLike {\n width: number;\n height: number;\n getContext(id: \"2d\"): CanvasContextLike | null;\n toDataURL(type?: string, quality?: number): string;\n}\n\nexport interface CanvasContextLike {\n drawImage(source: unknown, dx: number, dy: number, dw: number, dh: number): void;\n}\n\n/**\n * The subset of an element this module reads.\n *\n * Deliberately structural rather than `instanceof HTMLImageElement`: the SDK\n * runs inside an iframe, and an element from another document fails an\n * `instanceof` check against this realm's constructors even when it is exactly\n * the thing we want.\n */\nexport interface CaptureCandidate {\n tagName: string;\n parentElement?: CaptureCandidate | null;\n querySelector?: (selector: string) => CaptureCandidate | null;\n /** `<img>` */\n naturalWidth?: number;\n naturalHeight?: number;\n complete?: boolean;\n /** `<video>` */\n videoWidth?: number;\n videoHeight?: number;\n /** `<canvas>` */\n width?: number;\n height?: number;\n}\n\nexport type Screenshot = { dataUrl: string; region: \"element\" | \"viewport\" };\n\n/**\n * The byte budget for the encoded data URL.\n *\n * `apps/manager/src/lib/feedback/validate.ts` rejects the whole payload over\n * `MAX_PAYLOAD_BYTES` (128KB) and names screenshots as the usual cause. 48KB\n * leaves the rest of the payload \u2014 twenty console entries with stacks, twenty\n * failed requests, a source chain \u2014 room to be large without the server\n * refusing a report the reviewer has already written. A data URL is base64\n * ASCII, so `dataUrl.length` is its byte cost in the serialised payload, near\n * enough to compare directly.\n *\n * Over budget we drop the screenshot. Emitting it anyway would trade a payload\n * with no picture for a payload the server 413s, which loses the source ref \u2014\n * the only part that actually matters.\n */\nexport const MAX_SCREENSHOT_BYTES = 48 * 1024;\n\n/**\n * Longest edge, in device-independent pixels, of the downscaled capture.\n *\n * A 2000px-wide hero image encodes to well over the budget at any quality worth\n * looking at. 480px is enough to see that the wrong photo is on the card, which\n * is the question this field answers.\n */\nexport const MAX_SCREENSHOT_EDGE = 480;\n\n/**\n * A ceiling on how long we will wait, not on how long the work takes.\n *\n * `toDataURL` is synchronous and cannot be interrupted; this bounds the wait so\n * a pathological encode does not hold the capture open indefinitely, and the\n * payload goes out without the screenshot instead. Said plainly because a\n * timeout that cannot cancel its work is the kind of thing a reader assumes is\n * stronger than it is.\n */\nexport const SCREENSHOT_TIMEOUT_MS = 250;\n\n/**\n * Attempts, in order. The second is a genuine retry at a worse picture rather\n * than a give-up: a slightly muddy 240px thumbnail still answers \"is that the\n * right image\", and a dropped screenshot answers nothing.\n */\nconst ATTEMPTS: ReadonlyArray<{ edge: number; quality: number }> = [\n { edge: MAX_SCREENSHOT_EDGE, quality: 0.7 },\n { edge: MAX_SCREENSHOT_EDGE / 2, quality: 0.5 },\n];\n\n/**\n * WebP first, JPEG second. PNG is never asked for: it is lossless, and a\n * lossless photograph is exactly the thing that blows the byte budget. A canvas\n * that does not know a requested type silently returns PNG, so the result is\n * checked by prefix rather than trusted.\n */\nconst FORMATS = [\"image/webp\", \"image/jpeg\"] as const;\n\nexport interface ScreenshotOptions {\n /**\n * Off switch. `spec/02` treats screenshots as optional, so a host that would\n * rather not encode bitmaps on click \u2014 or that serves images this SDK would\n * only ever taint \u2014 sets this to `false`.\n */\n enabled?: boolean;\n maxBytes?: number;\n maxEdge?: number;\n timeoutMs?: number;\n /**\n * Canvas factory. Injected so the tests can drive the failure paths \u2014 taint,\n * an oversized encode, a context that is not there \u2014 none of which are\n * reachable from a fake DOM object alone.\n */\n createCanvas?: () => CanvasLike | null;\n}\n\ninterface ResolvedOptions {\n enabled: boolean;\n maxBytes: number;\n maxEdge: number;\n timeoutMs: number;\n createCanvas: () => CanvasLike | null;\n}\n\nexport function resolveScreenshotOptions(\n options: ScreenshotOptions | false | undefined,\n doc?: Document,\n): ResolvedOptions {\n /**\n * DEFAULT ON. The subset only fires on media elements, costs one downscaled\n * encode, and cannot enlarge a payload past the budget above; the failure\n * mode of defaulting off is that nobody remembers the flag exists and the\n * field is dead code. Hosts that do not want it pass `screenshot: false`.\n */\n const opts = options === false ? { enabled: false } : (options ?? {});\n return {\n enabled: opts.enabled ?? true,\n maxBytes: opts.maxBytes ?? MAX_SCREENSHOT_BYTES,\n maxEdge: opts.maxEdge ?? MAX_SCREENSHOT_EDGE,\n timeoutMs: opts.timeoutMs ?? SCREENSHOT_TIMEOUT_MS,\n createCanvas: opts.createCanvas ?? (() => doc?.createElement(\"canvas\") ?? null),\n };\n}\n\nconst MEDIA_TAGS = new Set([\"IMG\", \"CANVAS\", \"VIDEO\"]);\nconst MEDIA_SELECTOR = \"img,canvas,video\";\n/** How far up the tree to look. A card wraps its image; a page does not. */\nconst MAX_ANCESTOR_HOPS = 4;\n\nexport function isMediaElement(el: CaptureCandidate | null | undefined): boolean {\n return !!el && MEDIA_TAGS.has(el.tagName.toUpperCase());\n}\n\n/**\n * The nearest thing with a bitmap: the element itself, then a descendant, then\n * an ancestor.\n *\n * Descendant before ancestor because a click lands on whatever is topmost, and\n * on a card that is usually the overlay or the link wrapping the photo \u2014 the\n * photo the reviewer means is inside it, not above it.\n */\nexport function findCaptureSource(\n el: CaptureCandidate | null,\n maxHops = MAX_ANCESTOR_HOPS,\n): CaptureCandidate | null {\n if (!el) return null;\n if (isMediaElement(el)) return el;\n\n const descendant = el.querySelector?.(MEDIA_SELECTOR) ?? null;\n if (isMediaElement(descendant)) return descendant;\n\n let node = el.parentElement ?? null;\n for (let hops = 0; node && hops < maxHops; hops++) {\n if (isMediaElement(node)) return node;\n node = node.parentElement ?? null;\n }\n return null;\n}\n\n/**\n * The source's own pixel dimensions, or null if it has none.\n *\n * A broken `<img>` reports `naturalWidth === 0`, and that is the honest answer:\n * there is no bitmap to capture. The report still carries the source ref and\n * the DOM path, which is what a \"this image is broken\" ticket needs anyway.\n */\nexport function intrinsicSize(el: CaptureCandidate): { w: number; h: number } | null {\n const tag = el.tagName.toUpperCase();\n const pair =\n tag === \"IMG\"\n ? { w: el.naturalWidth, h: el.naturalHeight }\n : tag === \"VIDEO\"\n ? { w: el.videoWidth, h: el.videoHeight }\n : { w: el.width, h: el.height };\n\n const w = Math.floor(pair.w ?? 0);\n const h = Math.floor(pair.h ?? 0);\n if (!Number.isFinite(w) || !Number.isFinite(h) || w < 1 || h < 1) return null;\n if (tag === \"IMG\" && el.complete === false) return null;\n return { w, h };\n}\n\n/** Fit inside `maxEdge` without ever upscaling \u2014 enlarging invents detail. */\nexport function fitWithin(\n size: { w: number; h: number },\n maxEdge: number,\n): { w: number; h: number } {\n const scale = Math.min(1, maxEdge / Math.max(size.w, size.h));\n return { w: Math.max(1, Math.round(size.w * scale)), h: Math.max(1, Math.round(size.h * scale)) };\n}\n\n/**\n * Encode one attempt. Returns null for every failure, including the ones the\n * platform signals by throwing.\n */\nfunction encode(\n source: CaptureCandidate,\n size: { w: number; h: number },\n quality: number,\n options: ResolvedOptions,\n): string | null {\n let canvas: CanvasLike | null;\n try {\n canvas = options.createCanvas();\n } catch {\n return null;\n }\n if (!canvas) return null;\n\n try {\n canvas.width = size.w;\n canvas.height = size.h;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return null;\n ctx.drawImage(source, 0, 0, size.w, size.h);\n\n for (const format of FORMATS) {\n // `toDataURL` throws `SecurityError` the moment the canvas is tainted by\n // a cross-origin bitmap, which is the single most likely outcome on a\n // real page. Per-format so a WebP-less browser still gets its JPEG.\n let dataUrl: string;\n try {\n dataUrl = canvas.toDataURL(format, quality);\n } catch {\n return null;\n }\n if (typeof dataUrl !== \"string\" || !dataUrl.startsWith(`data:${format}`)) continue;\n if (dataUrl.length <= options.maxBytes) return dataUrl;\n // Over budget in this format; a different container will not save an\n // encode this large, so let the next, smaller attempt try instead.\n return null;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Capture synchronously, or return null. Exported for the tests, which need to\n * assert the drop decisions without a timer in the way.\n */\nexport function captureScreenshotSync(\n el: CaptureCandidate | null,\n options: ResolvedOptions,\n): Screenshot | null {\n if (!options.enabled) return null;\n\n const source = findCaptureSource(el);\n if (!source) return null;\n\n const natural = intrinsicSize(source);\n if (!natural) return null;\n\n for (const attempt of ATTEMPTS) {\n const size = fitWithin(natural, Math.min(options.maxEdge, attempt.edge));\n const dataUrl = encode(source, size, attempt.quality, options);\n /**\n * Always `\"element\"`, never `\"viewport\"`. The protocol allows both, but\n * this implementation has no way to rasterise a viewport, and a field that\n * claimed otherwise would send a reviewer looking for page context that is\n * not in the image. `\"viewport\"` is reserved for the server-side renderer.\n */\n if (dataUrl) return { dataUrl, region: \"element\" };\n }\n return null;\n}\n\n/**\n * The entry point the SDK calls. Resolves to a screenshot or to null, within\n * `timeoutMs`, and never rejects.\n */\nexport function captureScreenshot(\n el: CaptureCandidate | null,\n options: ResolvedOptions,\n): Promise<Screenshot | null> {\n if (!options.enabled) return Promise.resolve(null);\n\n return new Promise<Screenshot | null>((resolve) => {\n let settled = false;\n const settle = (value: Screenshot | null) => {\n if (settled) return;\n settled = true;\n resolve(value);\n };\n\n const timer = setTimeout(() => settle(null), options.timeoutMs);\n // `unref` where it exists: a pending timer must not keep a Node test\n // process alive after the payload has already gone out.\n (timer as unknown as { unref?: () => void }).unref?.();\n\n // A microtask, not a straight call: it keeps this function's own contract\n // uniform (always async) so the SDK has one code path, not two.\n void Promise.resolve()\n .then(() => captureScreenshotSync(el, options))\n .then(settle, () => settle(null))\n .finally(() => clearTimeout(timer));\n });\n}\n\n/**\n * Merge a capture into a payload, or hand the payload back untouched.\n *\n * The payload goes in already complete and comes out still complete. The\n * failure mode this shape exists to make impossible is a screenshot problem\n * costing the reviewer the source ref \u2014 so the payload is never awaited into\n * existence, only decorated, and `screenshot` is added as a key or not at all\n * rather than set to `undefined` (which `JSON.stringify` drops anyway, but\n * which reads as \"we tried and got nothing\" to anyone inspecting the object).\n */\nexport async function attachScreenshot<T extends object>(\n payload: T,\n el: CaptureCandidate | null,\n options: ResolvedOptions,\n): Promise<T & { screenshot?: Screenshot }> {\n let screenshot: Screenshot | null;\n try {\n screenshot = await captureScreenshot(el, options);\n } catch {\n // `captureScreenshot` does not reject; this is for a host-supplied\n // `createCanvas` that manages to break the chain in some way it cannot.\n screenshot = null;\n }\n return screenshot ? { ...payload, screenshot } : payload;\n}\n", "import {\n SDK_VERSION,\n assertConcreteOrigin,\n isParentMessage,\n type AnnotationPayload,\n type BuildInfo,\n type TargetInfo,\n} from \"./protocol\";\nimport { createOverlay, type Overlay } from \"./overlay\";\nimport { resolveTarget } from \"./target\";\nimport {\n createEgress,\n postMessageTransport,\n type AnnotationTransport,\n type EgressErrorHandler,\n type EgressOutcome,\n} from \"./transport\";\nimport {\n attachScreenshot,\n resolveScreenshotOptions,\n type CaptureCandidate,\n type ScreenshotOptions,\n} from \"./screenshot\";\n\n/**\n * The annotation SDK (A-36).\n *\n * Lives in the product bundle, listens for the management app's instructions,\n * and turns a click into a payload pinned to a code location.\n */\n\nexport interface AnnotationState {\n consoleErrors: AnnotationPayload[\"state\"][\"consoleErrors\"];\n failedRequests: AnnotationPayload[\"state\"][\"failedRequests\"];\n featureFlags: Record<string, unknown>;\n}\n\n/**\n * What the reviewer clicked, handed to whoever collects the words (A-240).\n *\n * The target is the RESOLVED one, taken from the payload that was already\n * assembled \u2014 not the element, and not something to re-read. See `onClick` for\n * why that distinction is the whole of this feature's correctness.\n */\nexport interface CommentRequest {\n target: TargetInfo;\n /** Viewport coordinates of the click, so a composer can sit near it. */\n point: { x: number; y: number };\n}\n\n/**\n * Ask the human for their words (A-240).\n *\n * Resolves with the comment, or with `null` for cancel \u2014 and cancel means\n * NOTHING IS SENT, which is why it is a distinct value rather than an empty\n * string. An empty string is a reviewer who had nothing to add; `null` is a\n * reviewer who changed their mind, and filing an issue for the second is the\n * kind of thing that gets an annotation tool switched off.\n *\n * Absent, the payload keeps `comment: \"\"` and the frame path is unchanged: the\n * parent window is still the thing that collects the words there.\n */\nexport type CommentCollector = (request: CommentRequest) => Promise<string | null>;\n\nexport interface AnnotationSdkOptions {\n /**\n * The management app's origin, when there is one. Concrete \u2014 see\n * `assertConcreteOrigin`. Messages from anywhere else are ignored, and we\n * never post to anywhere else.\n *\n * OPTIONAL SINCE A-240, and only because first-party there is no parent to\n * listen to. It used to be required for a reason that survives: the inbound\n * `message` listener needs a concrete origin to compare against, and a\n * listener with nothing to compare against is one that accepts instructions\n * from any page that can reach this one. So the resolution is not a default\n * origin \u2014 it is NO LISTENER AT ALL. Omitting this makes the SDK deaf to\n * `postMessage`, which is exactly right for a page with no parent, and it is\n * the only construction in which that is true.\n *\n * Supply it and the frame path is bit-for-bit what it was.\n */\n parentOrigin?: string;\n build: BuildInfo;\n /**\n * The matched route pattern, e.g. `/pub/[slug]`.\n *\n * Injected rather than derived: only the app knows its own route patterns,\n * and `location.pathname` is the resolved URL, which is what the spec\n * explicitly does not want \u2014 `/pub/the-eagle` and `/pub/the-crown` are the\n * same route and should group as one.\n */\n getRoute: () => string;\n /**\n * Page state at capture time. Injected so A-35's ring buffers can supply the\n * real thing without this module knowing how they are collected.\n */\n getState: () => AnnotationState;\n /**\n * Screenshot capture (A-31). Omit for the default, or pass `false` to turn it\n * off entirely. See `screenshot.ts` for what it can and cannot capture \u2014 the\n * short version is that it captures a media element's own bitmap and nothing\n * else, so most clicks produce no screenshot at all, by design.\n */\n screenshot?: ScreenshotOptions | false;\n /**\n * How a capture leaves the page (A-239).\n *\n * Defaults to `postMessageTransport(parentOrigin)`, which is what this SDK\n * has always done. Pass `httpTransport(...)` when the product is being\n * annotated first-party, where `win.parent === win` and a posted message is\n * received by nobody \u2014 see `transport.ts` and\n * `docs/plans/first-party-annotation.md`.\n */\n transport?: AnnotationTransport;\n /**\n * Called when a message could not be delivered.\n *\n * The frame path effectively never fails, so this is new surface for the\n * HTTP one, and it exists because the alternative is a dropped capture that\n * looks like a successful one. With no handler the failure is logged rather\n * than swallowed; A-240 is what puts it in front of the reviewer.\n */\n onEgressError?: EgressErrorHandler;\n /**\n * Collect the reviewer's words after a click (A-240).\n *\n * Omit it and the SDK behaves exactly as it did: `comment: \"\"`, and the\n * parent frame merges the human's words in.\n */\n collectComment?: CommentCollector;\n /**\n * What became of the capture, plus a thunk that sends the SAME payload again.\n *\n * The retry re-sends what was captured rather than re-capturing: the reviewer\n * has moved on by then \u2014 a dialog is open, a row is selected, the page has\n * scrolled \u2014 and re-reading it would file an issue about a screen nobody\n * complained about.\n */\n onCaptureResult?: (outcome: EgressOutcome, retry: () => void) => void;\n /**\n * Elements annotation mode must pretend it cannot see.\n *\n * The toolbar renders INTO THE PAGE IT IS ANNOTATING, so with annotation on,\n * a click on *Stop* is a click on the page and would be captured \u2014 filing an\n * issue about our own button and swallowing the press that was meant to end\n * the session. Shadow DOM makes it worse rather than better: retargeting\n * reports the event's target as the shadow HOST, so the capture would not\n * even name the button.\n *\n * Injected rather than hard-coded to an id, because a hard-coded id is a\n * second definition of who owns that node.\n */\n ignoreElement?: (el: Element) => boolean;\n}\n\nexport interface AnnotationSdk {\n enable(): void;\n disable(): void;\n isEnabled(): boolean;\n destroy(): void;\n}\n\nconst EMPTY_STATE: AnnotationState = {\n consoleErrors: [],\n failedRequests: [],\n featureFlags: {},\n};\n\n/** State until A-35 lands its ring buffers. Empty is honest; invented is not. */\nexport const emptyAnnotationState = (): AnnotationState => ({ ...EMPTY_STATE });\n\nexport function createAnnotationSdk(options: AnnotationSdkOptions): AnnotationSdk {\n const parentOrigin =\n options.parentOrigin === undefined ? null : assertConcreteOrigin(options.parentOrigin);\n const win = window;\n const doc = win.document;\n const screenshotOptions = resolveScreenshotOptions(options.screenshot, doc);\n\n let enabled = false;\n let overlay: Overlay | null = null;\n let hoverFrame = 0;\n let lastHovered: Element | null = null;\n /**\n * A capture whose words are still being written.\n *\n * While this is true the page is frozen as far as annotation is concerned:\n * no second capture, no hover highlight, and Escape belongs to the composer\n * rather than to annotation mode. Without it, a reviewer clicking twice ends\n * up with two composers over one page and no way to tell which is which.\n */\n let collecting = false;\n\n /**\n * One way out, and it is the transport's. A transport that declines a kind\n * (the HTTP one declines `annotate:hover`, which fires once per animation\n * frame) is respected here rather than left to drop it silently.\n */\n const transport =\n options.transport ?? (parentOrigin === null ? null : postMessageTransport(parentOrigin, win));\n if (transport === null) {\n /**\n * At construction, not at first capture \u2014 `transport.ts`'s argument,\n * applied one level up. Neither a parent origin nor a transport is a page\n * with no way out at all, and every capture would be assembled correctly\n * and dropped.\n */\n throw new Error(\n \"[annotation] the SDK needs a `transport` or a `parentOrigin`; with neither there is \" +\n \"nowhere for a capture to go and every one of them would be lost in silence.\",\n );\n }\n const post = createEgress(transport, options.onEgressError);\n\n const boxOf = (el: Element) => {\n const r = el.getBoundingClientRect();\n return { x: r.x, y: r.y, w: r.width, h: r.height };\n };\n\n const targetFor = (el: Element): TargetInfo =>\n resolveTarget(el, { boundingBox: boxOf(el) });\n\n const labelFor = (target: TargetInfo): string | null => {\n if (target.componentName && target.source) {\n const file = target.source.file.split(\"/\").pop() ?? target.source.file;\n return `${target.componentName} \u00B7 ${file}:${target.source.line}`;\n }\n if (target.source) return `${target.source.file}:${target.source.line}`;\n return target.componentName ?? null;\n };\n\n const onMouseMove = (event: MouseEvent) => {\n if (!enabled || collecting) return;\n // Coalesce to one hit-test per frame: mousemove fires far faster than\n // anything downstream can use, and each one costs a layout read.\n if (hoverFrame) return;\n hoverFrame = win.requestAnimationFrame(() => {\n hoverFrame = 0;\n const el = doc.elementFromPoint(event.clientX, event.clientY);\n if (!el || (overlay && overlay.contains(el))) return;\n // Our own toolbar is not part of the product under review.\n if (options.ignoreElement?.(el)) return;\n if (el === lastHovered) return;\n lastHovered = el;\n\n const target = targetFor(el);\n overlay?.show(target.boundingBox, labelFor(target));\n void post({ type: \"annotate:hover\", target });\n });\n };\n\n const buildPayload = (el: Element, state: AnnotationState): AnnotationPayload => ({\n schemaVersion: 1,\n capturedAt: new Date().toISOString(),\n build: options.build,\n page: {\n url: win.location.href,\n route: options.getRoute(),\n viewport: {\n w: win.innerWidth,\n h: win.innerHeight,\n dpr: win.devicePixelRatio,\n },\n userAgent: win.navigator.userAgent,\n },\n target: targetFor(el),\n state: {\n consoleErrors: state.consoleErrors,\n failedRequests: state.failedRequests,\n featureFlags: state.featureFlags,\n },\n // The parent frame collects the human's words and merges them in verbatim.\n comment: \"\",\n });\n\n /**\n * Deliver one capture, and report what became of it.\n *\n * `retry` is this same function over the same message, which is what makes\n * the composer's retry button honest: it sends the capture that was taken at\n * the moment of the click, not a fresh reading of a page the reviewer has\n * since moved on from.\n */\n const deliver = (payload: AnnotationPayload) => {\n const message = { type: \"annotate:capture\", payload } as const;\n const send = () => {\n void post(message).then((outcome) => options.onCaptureResult?.(outcome, send));\n };\n send();\n };\n\n const onClick = (event: MouseEvent) => {\n if (!enabled) return;\n const el = event.target;\n if (!(el instanceof Element)) return;\n /**\n * BEFORE `preventDefault`. The toolbar's own buttons are clicks like any\n * other, and suppressing them would leave *Stop* unable to stop anything.\n */\n if (options.ignoreElement?.(el)) return;\n\n /**\n * Capture phase, and stop it dead. Annotation mode is pointed at a live\n * product: an un-prevented click navigates away, submits a form, or opens a\n * dialog, and the payload describes a page the reviewer is no longer on.\n */\n event.preventDefault();\n event.stopPropagation();\n\n /**\n * ===================================================================\n * ONE COMPOSER AT A TIME \u2014 AND *AFTER* THE SUPPRESSION ABOVE (A-241)\n * ===================================================================\n *\n * A second click while the first post-it is open would put two of them\n * over one page with no way to tell which is which, so it is dropped. The\n * ORDER is the fix: this test used to run BEFORE `preventDefault` /\n * `stopPropagation`, which meant that while the composer was open every\n * click on the product was live. A reviewer half-way through typing who\n * clicked a link navigated away and lost every word of it \u2014 the exact\n * outcome the two guards above exist to prevent, switched off at precisely\n * the moment there is something to lose.\n *\n * Returning here and not earlier means the click is swallowed and nothing\n * else happens, which is what \"the page is frozen while I write\" should\n * mean. The toolbar's own buttons are unaffected: `ignoreElement` returns\n * above this, so Cancel, Send and Escape still work while collecting.\n */\n if (collecting) return;\n\n /**\n * The payload is assembled *now*, synchronously, and the screenshot is\n * merged into it afterwards. Reading page state after an await would read a\n * page the click has already changed, and the whole point of the capture is\n * that it describes the moment of the click.\n */\n const payload = buildPayload(el, options.getState());\n\n /**\n * ===================================================================\n * CAPTURE FIRST, THEN ASK FOR WORDS, THEN MERGE (A-240)\n * ===================================================================\n *\n * The order above is the requirement and this is the line that keeps it.\n * `buildPayload` has ALREADY RUN, synchronously, before anything can\n * `await`; the composer is opened with the target that payload resolved,\n * and the reviewer's answer is merged into it afterwards. Nothing below\n * re-reads the page.\n *\n * Inverting it \u2014 open the composer, then build the payload when Send is\n * pressed \u2014 is the tempting shape, and it captures a page that has had a\n * human typing into it for thirty seconds: a different scroll position, a\n * different route if anything auto-navigated, and console errors the\n * composer itself provoked. The screenshot would be of the post-it.\n */\n const collect = options.collectComment;\n\n if (!collect) {\n /**\n * THE FRAME PATH, UNCHANGED \u2014 including its TIMING.\n *\n * Not an optimisation and not duplication for its own sake. Folding this\n * into the `Promise.all` below would push a screenshot-less capture from\n * SYNCHRONOUS to a microtask later, and `sdk.test.ts` asserts the\n * handshake and the capture have both been posted the instant the click\n * handler returns. A frame that is not being deleted is a frame whose\n * behaviour is not being edited either.\n */\n if (!screenshotOptions.enabled) {\n deliver(payload);\n return;\n }\n void attachScreenshot(payload, el as unknown as CaptureCandidate, screenshotOptions).then(\n (withShot) => deliver(withShot),\n () => deliver(payload),\n );\n return;\n }\n\n const point = { x: event.clientX, y: event.clientY };\n\n /**\n * The screenshot is a nice-to-have and is never allowed to cost us the\n * capture: `captureScreenshot` resolves within its own timeout and never\n * rejects, and the `catch` is belt-and-braces for a caller-supplied\n * `createCanvas` that manages to break the promise chain itself. Either way\n * the payload goes out \u2014 with `screenshot` absent.\n *\n * It runs ALONGSIDE the composer rather than before it. Sequencing them\n * would leave the reviewer looking at an unchanged page for as long as the\n * screenshot takes, which reads as a click that did nothing.\n */\n const captured: Promise<AnnotationPayload> = screenshotOptions.enabled\n ? attachScreenshot(payload, el as unknown as CaptureCandidate, screenshotOptions).then(\n (withShot) => withShot,\n () => payload,\n )\n : Promise.resolve(payload);\n\n /**\n * ===================================================================\n * A COLLECTOR THAT REJECTS MUST NOT WEDGE ANNOTATION MODE\n * ===================================================================\n *\n * `collectComment` is a public, caller-supplied option, so it can reject \u2014\n * and the first version of this let that rejection go unhandled AND left\n * `collecting` true forever. Every later click and every hover highlight\n * was then silently dead for the rest of the session, with the toolbar\n * still reading *Stop annotating* and nothing reported anywhere. That is\n * the exact failure this package is built to not have, reintroduced by the\n * feature that was meant to remove it.\n *\n * So the rejection is caught, turned into \"no words\", and reported through\n * `onCaptureResult` \u2014 and the retry is REAL: `ask` runs the collector\n * again over the SAME already-captured payload, so nothing re-reads a page\n * the reviewer has since moved on from. `attachScreenshot` is defended by\n * its own two-armed `then` for the same reason, one line up.\n */\n const ask = () => {\n collecting = true;\n let failure: { error: unknown } | null = null;\n const words = collect({ target: payload.target, point }).then(\n (comment) => comment,\n (error: unknown) => {\n failure = { error };\n return null;\n },\n );\n\n void Promise.all([captured, words]).then(([finished, comment]) => {\n // Cleared BEFORE anything is reported, so a retry invoked from the\n // handler starts from a page that is not still marked as busy.\n collecting = false;\n if (failure !== null) {\n options.onCaptureResult?.({ status: \"failed\", error: failure.error }, ask);\n return;\n }\n // Cancel sends nothing. Not an empty capture \u2014 nothing.\n if (comment === null) return;\n deliver(comment === \"\" ? finished : { ...finished, comment });\n });\n };\n\n ask();\n };\n\n const onKeyDown = (event: KeyboardEvent) => {\n // While the composer is open, Escape is the composer's \u2014 it cancels the\n // post-it. Leaving annotation mode as well would throw away the words and\n // the toggle in one keystroke.\n if (!enabled || collecting || event.key !== \"Escape\") return;\n void post({ type: \"annotate:cancel\" });\n disable();\n };\n\n function enable() {\n if (enabled) return;\n enabled = true;\n overlay ??= createOverlay(doc);\n doc.addEventListener(\"mousemove\", onMouseMove, true);\n doc.addEventListener(\"click\", onClick, true);\n doc.addEventListener(\"keydown\", onKeyDown, true);\n }\n\n function disable() {\n if (!enabled) return;\n enabled = false;\n lastHovered = null;\n if (hoverFrame) {\n win.cancelAnimationFrame(hoverFrame);\n hoverFrame = 0;\n }\n doc.removeEventListener(\"mousemove\", onMouseMove, true);\n doc.removeEventListener(\"click\", onClick, true);\n doc.removeEventListener(\"keydown\", onKeyDown, true);\n overlay?.hide();\n }\n\n const onMessage = (event: MessageEvent) => {\n // Origin first, shape second. A well-formed message from the wrong origin is\n // exactly what this check exists to stop.\n if (event.origin !== parentOrigin) return;\n if (!isParentMessage(event.data)) return;\n\n switch (event.data.type) {\n case \"annotate:enable\":\n enable();\n break;\n case \"annotate:disable\":\n disable();\n break;\n case \"annotate:ping\":\n void post({ type: \"annotate:ready\", sdkVersion: SDK_VERSION, build: options.build });\n break;\n }\n };\n\n /**\n * ONLY when there is a parent whose origin we can name.\n *\n * `onMessage` compares `event.origin` against `parentOrigin`, so with no\n * parent origin there is nothing to compare against. Registering anyway and\n * letting the `!==` fail on `null` would work today and is the wrong shape:\n * it leaves a listener whose safety depends on a comparison against a value\n * that means \"no comparison\". First-party there is no parent, so there is no\n * listener \u2014 which is also one fewer way into a page we do not own.\n */\n if (parentOrigin !== null) win.addEventListener(\"message\", onMessage);\n\n // Announce ourselves: the parent may have finished loading before we did, in\n // which case its handshake ping has already been and gone.\n void post({ type: \"annotate:ready\", sdkVersion: SDK_VERSION, build: options.build });\n\n return {\n enable,\n disable,\n isEnabled: () => enabled,\n destroy() {\n disable();\n if (parentOrigin !== null) win.removeEventListener(\"message\", onMessage);\n overlay?.destroy();\n overlay = null;\n },\n };\n}\n", "import {\n ANNOTATION_ARMED_COOKIES,\n ANNOTATION_TOKEN_PARAMS,\n CAPTURE_TOKEN_PARAM,\n REVIEW_RUN_PARAM,\n clearedCookie,\n isAnnotationArmed,\n type BuildInfo,\n} from \"./protocol\";\n\n/**\n * Arming, and the production kill-switch (A-241).\n *\n * ===========================================================================\n * THE SEQUENCE THIS FILE IS\n * ===========================================================================\n *\n * The reviewer lands on `?ai_annotate=<arming>&ai_capture=<signed>&ai_run=<id>`\n * from the console's *Request changes* button. This module is what happens in\n * the first few milliseconds after that:\n *\n * 1. read the three parameters\n * 2. **strip them from the URL** with `replaceState`, before anything else\n * 3. answer *is this page armed* \u2014 from the COOKIE, never from the URL\n * 4. only then, and only if armed, keep the capture token for the tab\n *\n * Step 3 is the runtime half of the kill-switch and step 2 is not decoration;\n * both are argued at length below. Steps 3 and 4 were the other way round\n * until A-254, which is how an unarmed page came to persist an\n * attacker-supplied token \u2014 the argument, and the measurement that settled it,\n * are at the gate itself in `readArming`.\n *\n * ===========================================================================\n * TWO CREDENTIALS, AND THIS FILE TOUCHES THEM DIFFERENTLY ON PURPOSE\n * ===========================================================================\n *\n * `docs/plans/first-party-annotation.md` \u00A7 *Authentication* is the argument;\n * what it means for this file is a table:\n *\n * | | who verifies it | what this module does with it |\n * | --- | --- | --- |\n * | arming token (`ai_annotate`) | the product's own middleware, against its `ANNOTATION_TOKEN_<PRODUCT>` | **nothing.** Reads the middleware's answer out of the cookie and drops the token |\n * | capture token (`ai_capture`) | the console, on the way back in | couriers it: keeps it for the tab, hands it to `httpTransport` |\n *\n * **The URL parameter is not arming and this module never treats it as such.**\n * That is the runtime gate, stated as a single sentence, and it is the reason\n * `readArming` does not compare anything: page script holds no secret, so a\n * page that armed itself because a token-shaped string was in its own query\n * string would be armed by anyone who could type one. The middleware compares\n * and sets a cookie; this reads the cookie. A wrong token therefore produces no\n * cookie, and no cookie produces no toolbar \u2014 `arming.test.ts` asserts exactly\n * that, and asserts it with a token-bearing URL present so it cannot pass by\n * accident.\n *\n * ===========================================================================\n * WHY THE TOKENS COME OUT OF THE URL IMMEDIATELY\n * ===========================================================================\n *\n * A capture token in an address bar is a capture token in a screenshot, a\n * `Referer` header, a pasted link and a server access log \u2014 and this one\n * authorises filing issues as the reviewer it was minted for. `replaceState`\n * rewrites the entry in place: no navigation and no new history entry.\n *\n * **MEASURED, not assumed** \u2014 chromium 151.0.7922.34 driven by playwright\n * against a loopback server that logged every `Referer` it received. The page\n * did the same `searchParams.delete` + `replaceState` this module does, with\n * one stylesheet in the initial `<head>` and a second appended by script\n * afterwards:\n *\n * ```\n * /early.css Referer: http://\u2026/page?tab=menu&ai_annotate=\u2026&ai_capture=\u2026&ai_run=\u2026\n * /late.css Referer: http://\u2026/page?tab=menu\n * ```\n *\n * Two things come out of that run, and both are the point:\n *\n * 1. **The residual window is real, and it is bounded.** Anything the page\n * requests before this function runs \u2014 the document's own scripts,\n * stylesheets, fonts, images \u2014 is a SAME-ORIGIN subresource, and under the\n * default `Referrer-Policy` (`strict-origin-when-cross-origin`) a\n * same-origin request carries the FULL URL, query string included. It\n * reaches only the product's own origin, which has already seen the tokens\n * on the request line of the navigation itself, and everything requested\n * after this call sees the stripped URL. That is the case a reviewer of\n * A-244 named and deferred to this slice, answered with a measurement.\n * 2. **Back does not restore the tokens.** The same run then called\n * `goBack()` and landed on the URL from BEFORE the deep link rather than on\n * the token-bearing one \u2014 which is what `replaceState` rather than\n * `pushState` buys, and is why that choice is not stylistic.\n *\n * Closing the window entirely is a RESPONSE HEADER on the product\n * (`Referrer-Policy: strict-origin`, which drops path and query for same-origin\n * requests too), so it belongs to the mount in the product's own repository \u2014\n * N-905, not this package. **This module cannot close it**: by the time any\n * bundled script runs, the head's subresources are already in flight. What is\n * ours is being callable early, which is why nothing here awaits anything and\n * the strip happens before the token is stored or the cookie is read.\n *\n * ===========================================================================\n * SESSION STORAGE FOR THE CAPTURE TOKEN, AND NOT A COOKIE\n * ===========================================================================\n *\n * The arming marker is a cookie because the product's MIDDLEWARE sets it, on\n * the server, before any script runs. The capture token is deliberately not\n * one: a cookie is attached to every request the browser makes to that origin,\n * so it would put the credential into the product's own access logs on every\n * page, image and API call for the rest of the session \u2014 the exact exposure\n * step 2 exists to end. `sessionStorage` is per-tab, survives internal\n * navigation (which is what arming has to survive), is never sent anywhere by\n * the browser, and is gone when the tab closes.\n *\n * The cost, stated: a link opened in a NEW tab has no stored token, so a\n * reviewer who middle-clicks their way around the app gets a toolbar that\n * cannot send. That is the correct failure \u2014 `httpTransport` refuses to\n * construct without a token \u2014 and it is recoverable by using the deep link\n * again.\n */\n\n/** Per-tab, never sent anywhere by the browser. Namespaced, not product-named. */\nexport const CAPTURE_TOKEN_STORAGE_KEY = \"ai.annotation.capture\";\nexport const REVIEW_RUN_STORAGE_KEY = \"ai.annotation.run\";\n\n/** The subset of `Storage` this needs. `null` disables persistence entirely. */\nexport type StorageLike = Pick<Storage, \"getItem\" | \"setItem\" | \"removeItem\">;\n\nexport interface ArmingOptions {\n /** Defaults to the ambient `document`. Needs `cookie`, `URL` and `defaultView`. */\n doc?: Document;\n /** Defaults to `doc.defaultView.history`. Only `replaceState` is used. */\n history?: Pick<History, \"replaceState\">;\n /**\n * Defaults to `sessionStorage`. Pass `null` for a tab that should keep\n * nothing \u2014 arming then lasts exactly as long as the query string does.\n */\n storage?: StorageLike | null;\n /** Overrides the URL read from the document. Tests and server renders only. */\n url?: string;\n}\n\nexport interface Arming {\n /** May this page offer an annotation UI at all? */\n armed: boolean;\n /** The console's signed token, for `httpTransport`. Never validated here. */\n captureToken: string | null;\n /** The run under review, unsigned, couriered. */\n runId: string | null;\n /** *Done*, or an explicit stop. Clears the cookie and the stored token. */\n disarm: () => void;\n}\n\n/** Every name this module takes out of a URL. */\nconst STRIPPED_PARAMS: readonly string[] = [\n ...ANNOTATION_TOKEN_PARAMS,\n CAPTURE_TOKEN_PARAM,\n REVIEW_RUN_PARAM,\n];\n\n/**\n * `sessionStorage` throws rather than returning null in real browsers \u2014 an\n * opaque origin, a `data:` document, Safari's private mode historically, or a\n * user who has blocked site data. A capture surface that threw during boot\n * would take the product's own page down with it, in somebody else's product.\n */\nfunction safely<T>(run: () => T, fallback: T): T {\n try {\n return run();\n } catch {\n return fallback;\n }\n}\n\nfunction defaultStorage(win: Window | null): StorageLike | null {\n return safely(() => win?.sessionStorage ?? null, null);\n}\n\n/**\n * Read the deep link, strip it, and answer whether this page is armed.\n *\n * Safe to call more than once and safe to call on a page that was never\n * linked to: with no parameters and no cookie it does nothing at all, touches\n * no history entry, and returns `armed: false`.\n */\nexport function readArming(options: ArmingOptions = {}): Arming {\n const doc = options.doc ?? document;\n const win = doc.defaultView ?? null;\n const storage = options.storage === undefined ? defaultStorage(win) : options.storage;\n\n const href = options.url ?? doc.URL;\n const url = safely(() => new URL(href), null);\n\n let captureToken: string | null = null;\n let runId: string | null = null;\n\n if (url !== null) {\n captureToken = url.searchParams.get(CAPTURE_TOKEN_PARAM) || null;\n runId = url.searchParams.get(REVIEW_RUN_PARAM) || null;\n\n const present = STRIPPED_PARAMS.some((name) => url.searchParams.has(name));\n if (present) {\n for (const name of STRIPPED_PARAMS) url.searchParams.delete(name);\n /**\n * FIRST, before anything is stored or decided. The strip is the only\n * part of this function with a clock on it.\n *\n * `replaceState` and not `pushState`: a new history entry would put the\n * token-bearing URL one Back press away, which is the same leak with an\n * extra step. The state argument is the CURRENT `history.state`, passed\n * straight back, falling back to `null` only when there is none: we are\n * not the product's router and must not overwrite whatever it has put\n * there.\n */\n const history = options.history ?? win?.history;\n safely(() => {\n history?.replaceState(win?.history?.state ?? null, \"\", url.toString());\n }, undefined);\n }\n }\n\n /**\n * Declared before the `armed` gate below because the unarmed path returns\n * early and still owes the caller a working `disarm` \u2014 a page that is not\n * armed may still be holding a cookie a previous arming set, and clearing it\n * must not depend on how this call happened to return.\n */\n const disarm = () => {\n for (const name of ANNOTATION_ARMED_COOKIES) {\n safely(() => {\n doc.cookie = clearedCookie(name);\n }, undefined);\n }\n if (storage) {\n safely(() => storage.removeItem(CAPTURE_TOKEN_STORAGE_KEY), undefined);\n safely(() => storage.removeItem(REVIEW_RUN_STORAGE_KEY), undefined);\n }\n };\n\n /**\n * The cookie, and only the cookie. See the header. Read BEFORE the storage\n * block below, because the storage block now depends on it (A-254).\n */\n const armed = isAnnotationArmed(safely(() => doc.cookie, \"\"));\n\n /**\n * ARMED FIRST, THEN PERSIST (A-254). The strip above is unconditional and\n * must stay that way \u2014 getting tokens out of an address bar is desirable on\n * every load, armed or not, and `arming.test.ts` pins it. Persisting is the\n * opposite: an unarmed page has no use for a capture token and every load of\n * it is a load nobody authorised.\n *\n * The value is ATTACKER-SUPPLIED. Anyone can send a reviewer a link to the\n * staging origin carrying any `ai_capture` they like; the page refuses to\n * arm, which looks like the gate working, and used to keep the string\n * anyway. MEASURED in `e2e/arming.spec.ts` against the previous version of\n * this file, in chromium with the tab's real `sessionStorage`: an unarmed\n * load of `?ai_annotate=wrong&ai_capture=\u2026` left the token in storage, and a\n * LATER ARMED LOAD IN THE SAME TAB THAT CARRIED NO TOKEN OF ITS OWN THEN\n * RETURNED IT \u2014 so this was an attribution bug in a credential path, not a\n * question of lifetime. It is not merely residue.\n *\n * The console's deep link does carry a fresh token, and that path overwrote\n * the residue before reading it \u2014 which is why the source alone read as\n * safe, and why the ordering was settled by running the sequence.\n *\n * Reading is gated too, not just writing. An unarmed page that adopted a\n * stored token would hand a live credential to whatever asked, and nothing\n * on an unarmed page is supposed to ask.\n *\n * WHAT THIS DOES NOT DO. It does not bind the token to a product or an\n * environment; nothing here can. The claim is signed by the console and the\n * product holds no key, so a check here would be a comparison of unverified\n * strings \u2014 the exact shape this file's header rejects for arming. What\n * scoping exists is the browser's: `sessionStorage` is per-origin AND\n * per-tab, so a token stored by one product's origin is unreachable from\n * another's, and a new tab starts empty. Binding beyond that belongs to the\n * console, which can see product and environment in the claim it signed.\n */\n if (!armed) {\n return { armed: false, captureToken: null, runId: null, disarm };\n }\n\n if (storage) {\n if (captureToken !== null) {\n safely(() => storage.setItem(CAPTURE_TOKEN_STORAGE_KEY, captureToken as string), undefined);\n } else {\n captureToken = safely(() => storage.getItem(CAPTURE_TOKEN_STORAGE_KEY), null);\n }\n if (runId !== null) {\n safely(() => storage.setItem(REVIEW_RUN_STORAGE_KEY, runId as string), undefined);\n } else {\n runId = safely(() => storage.getItem(REVIEW_RUN_STORAGE_KEY), null);\n }\n }\n\n return { armed, captureToken, runId, disarm };\n}\n\n/**\n * The BUILD-TIME gate (A-241), and the second of the two independent ones.\n *\n * ===========================================================================\n * WHY `build.env` AND NOT `process.env.NODE_ENV`\n * ===========================================================================\n *\n * `NODE_ENV` is the reflex and it is WRONG HERE, measurably: a Vercel PREVIEW\n * build \u2014 which is what a staging deploy is, and the only place this toolbar is\n * ever meant to appear \u2014 is built with `NODE_ENV=production`. Gating on it\n * would disable annotation on exactly the deployments it exists for while\n * reading like a security improvement, which is this repository's recurring\n * failure shape.\n *\n * `BuildInfo.env` mirrors `VERCEL_ENV` (`production | preview | development`)\n * and is stamped by the product's own build. It is a literal by the time the\n * bundler sees it, so `if (!isAnnotatableBuild(build)) return` in a production\n * build is a branch a minifier can fold \u2014 which is how the mount stops being in\n * the customer's bundle rather than merely being inert in it. The stronger form\n * of the same claim, excluding the import itself, is the product's own\n * configuration and belongs to the mount (N-905).\n *\n * `\"staging\"` is on the list: `BuildInfo` carries the value even though nothing\n * produces it today, and a build that calls itself staging is by definition not\n * the one serving customers.\n *\n * ===========================================================================\n * AN ALLOWLIST, BECAUSE THE DENYLIST FAILED OPEN\n * ===========================================================================\n *\n * This was `build.env !== \"production\"` and that is the same function for every\n * value the union names \u2014 and the WRONG ONE for every value it does not.\n * `undefined`, `\"\"`, a typo, a `VERCEL_ENV` that was never stamped, a field\n * dropped by a payload shim: each of those is *not* `\"production\"`, so each\n * used to mean *mount the toolbar*.\n *\n * `BuildInfo.env` being a closed TypeScript union is not the guarantee it looks\n * like at this line. **The only producer is `resolveBuildInfo`, in a different\n * repository, reading an environment variable at build time.** A type cannot\n * constrain a string that crosses a repository boundary as data; it constrains\n * the code that was compiled against it, which is not the code that stamped the\n * value.\n *\n * That mattered more here than anywhere else in the package. This is the single\n * check between a customer and a review affordance in a public bundle, this\n * file's own argument is that neither gate may depend on anything else holding,\n * and the denylist depended on another repository's env stamping being correct.\n * A gate that fails open is not a second gate.\n *\n * Failing closed costs exactly one thing, and it is the right one: a\n * misconfigured PREVIEW deploy loses its toolbar. That is visible in a second\n * and fixed by stamping the variable. The denylist's failure was silent and\n * shipped.\n */\nconst ANNOTATABLE_ENVS: readonly BuildInfo[\"env\"][] = [\"preview\", \"staging\", \"development\"];\n\nexport function isAnnotatableBuild(build: Pick<BuildInfo, \"env\">): boolean {\n return ANNOTATABLE_ENVS.includes(build.env);\n}\n", "import type { AnnotationSdk, AnnotationSdkOptions, CommentCollector, CommentRequest } from \"./sdk\";\nimport { createAnnotationSdk } from \"./sdk\";\nimport { isAnnotatableBuild, type Arming } from \"./arming\";\nimport { safeIssueUrl } from \"./transport\";\nimport type { EgressOutcome } from \"./transport\";\n\n/**\n * The toolbar and the post-it (A-240).\n *\n * ===========================================================================\n * WHY THIS EXISTS AT ALL\n * ===========================================================================\n *\n * `sdk.ts` built its payload with `comment: \"\"` and a comment saying why: *the\n * parent frame collects the human's words and merges them in verbatim*. First\n * party there is no parent \u2014 see `docs/plans/first-party-annotation.md` \u2014 so a\n * capture would file an issue carrying a screenshot, a `file:line` and NOTHING\n * SAYING WHAT IS WRONG WITH IT. This is the surface that collects the words,\n * and the surface that turns annotation mode on in the first place.\n *\n * The toggle is the point rather than a convenience. The reviewer navigates the\n * real app with annotation OFF \u2014 clicking rows, opening dialogs, finishing a\n * wizard \u2014 and switches it ON when they arrive somewhere worth commenting on.\n * That is what stops click-only screens being a wall, and it is why this is a\n * toolbar and not a mode the page boots into.\n *\n * ===========================================================================\n * IT RENDERS INSIDE SOMEBODY ELSE'S PRODUCT, AND THAT DECIDES ALMOST EVERYTHING\n * ===========================================================================\n *\n * 1. **Shadow DOM, closed to the product's CSS in both directions.** The\n * product cannot restyle this and this cannot restyle the product. A\n * toolbar inheriting a host's `* { box-sizing: border-box }` \u2014 or its\n * `button { text-transform: uppercase }` \u2014 is a bug report about our own UI,\n * filed against their product, by a tool whose entire output is bug reports.\n * `:host { all: initial }` is the half people forget: a shadow root stops\n * SELECTORS crossing, not INHERITANCE. `e2e/toolbar.spec.ts` proves both\n * halves against a deliberately hostile fixture, and proves them the only\n * way that means anything \u2014 by deleting each and watching it go red.\n *\n * THE ONE THING IT CANNOT DEFEND, said out loud rather than left to be\n * discovered: the HOST is in the light DOM, so the product's own selectors\n * reach it. `#ai-annotation-toolbar { position: static }` in a customer's\n * stylesheet beats `:host`, and puts this in the flow. Nothing shadow DOM\n * offers prevents that; the id is documented so that a collision is at\n * least legible when somebody hits it.\n *\n * 2. **It must not alter what is being reviewed.** Everything is `position:\n * fixed` and nothing is ever appended to the flow, so there is no layout\n * shift and no scrollbar; and NOTHING IS FOCUSED ON MOUNT. The reviewer is\n * here to judge pixels, and a tool that moves one of them has answered the\n * question wrongly before anybody clicks anything.\n *\n * `position: fixed` is doing ALL of that work, and the viewport clamp in\n * `position()` is doing NONE of it \u2014 see the note there, which used to\n * claim otherwise.\n *\n * 3. **No product may be named here (A-98).** Every string in `DEFAULT_LABELS`\n * is generic, and every one of them is overridable by the mounting app. The\n * only text this renders that it did not author is the target's own\n * `file:line`, which is the customer's source path and belongs to them.\n *\n * ===========================================================================\n * WHY THE CSS IS A TEMPLATE STRING AND WHAT THAT COSTS\n * ===========================================================================\n *\n * A `<style>` element inside the shadow root, whose `textContent` is the string\n * below. Not inline styles: `overlay.ts` uses those and carries nine entries in\n * `audit-allowance.json` for it, and that ledger may only ever get shorter.\n *\n * TWO AUDITS READ THIS REPOSITORY AND THEY DISAGREE ABOUT THIS FILE, which is\n * worth knowing before trusting either.\n *\n * `packages/design-audit` \u2014 `pnpm verify`'s ratchet \u2014 applies its colour,\n * space, radius and type rules to `.css` FILES ONLY: `scan.ts` classifies\n * every `.ts` as `tsx`, where the only rules are `inline-style` and the\n * class-name pair. It sees nothing here.\n *\n * **The `design-system` CI job does.** It reads CSS out of `.ts` source, and\n * the ledger it judges against \u2014 at the repository root, alongside\n * `audit-allowance.json` and under the same only-ever-shorter rule \u2014 already\n * carries `overlay.ts` for exactly that. THIS WAS MEASURED RATHER THAN\n * REASONED: the first version of this file was written believing nothing\n * audited it, said so in this comment, and that job failed the pull request\n * with `untokenised-colour` and `untokenised-space` against it. The check won\n * and the assumption is corrected here, where the assumption was.\n *\n * Hence the token block at the top of `CSS`. Every colour, space and radius\n * below is a `var()` into it, so there is one place to change them and the\n * gate can see that there is. They are `--ai-*` and NOT the design system's\n * `--color-*` / `--space-*`, and that is forced rather than chosen: this\n * package has zero dependencies on purpose, so the dictionary is not\n * importable \u2014 and inside a shadow root a custom property resolves against the\n * PRODUCT'S, so `var(--color-fg)` would paint this toolbar with whatever the\n * customer happens to mean by that name. A local, prefixed block cannot\n * collide with theirs.\n */\n\n/**\n * Every word this renders. Generic by default, and all of them injectable.\n *\n * Every field is RENDERED SOMEWHERE. A label nobody displays is an API promise\n * that cannot be kept and bytes on every page of every managed product; three\n * were written and deleted before this shipped for exactly that reason.\n */\nexport interface AnnotationToolbarLabels {\n /** Names the whole surface for a screen reader. */\n region: string;\n expand: string;\n collapse: string;\n start: string;\n stop: string;\n done: string;\n comment: string;\n placeholder: string;\n send: string;\n cancel: string;\n sent: string;\n issue: string;\n failed: string;\n retry: string;\n}\n\nexport const DEFAULT_LABELS: AnnotationToolbarLabels = {\n region: \"Annotation\",\n expand: \"Open the annotation toolbar\",\n collapse: \"Close the annotation toolbar\",\n start: \"Start annotating\",\n stop: \"Stop annotating\",\n done: \"Done\",\n comment: \"What is wrong here?\",\n placeholder: \"The heading wraps onto three lines on mobile\u2026\",\n send: \"Send\",\n cancel: \"Cancel\",\n sent: \"Sent\",\n issue: \"View issue\",\n failed: \"That did not send\",\n retry: \"Retry\",\n};\n\n/** The host's id. Our own node; nothing outside this package looks it up. */\nexport const TOOLBAR_ID = \"ai-annotation-toolbar\";\n\n/** One above `overlay.ts`, so the toolbar is never behind the highlight. */\nconst Z_INDEX = \"2147483001\";\n\nconst COMPOSER_WIDTH = 288;\n/**\n * MEASURED, not reserved: 189.6875px in chromium with the default labels, the\n * empty textarea at its `min-height`, and one line of `file:line`.\n *\n * It is used only to choose ABOVE or BELOW the target, so a value that drifts\n * degrades the placement rather than breaking anything \u2014 a caller who injects\n * labels long enough to wrap the button row gets a composer a little taller\n * than this thinks, and the worst outcome is that it sits below when it had\n * room above. `e2e/toolbar.spec.ts` asserts the real rendered box against the\n * real target box, so the consequence is checked even though the constant is\n * an estimate.\n */\nconst COMPOSER_HEIGHT = 190;\nconst MARGIN = 12;\n\nconst CSS = `\n:host{\n --ai-bg:#fff;\n --ai-fg:#18181b;\n --ai-line:#d4d4d8;\n --ai-line-soft:#e4e4e7;\n --ai-muted:#52525b;\n --ai-idle:#a1a1aa;\n --ai-accent:#2563eb;\n --ai-invert-bg:#18181b;\n --ai-invert-fg:#fafafa;\n --ai-invert-line:#52525b;\n --ai-bad:#7f1d1d;\n --ai-space-2:6px;\n --ai-space-3:8px;\n --ai-space-4:10px;\n --ai-space-5:12px;\n --ai-radius-control:6px;\n --ai-radius-card:8px;\n --ai-radius-pill:999px;\n all:initial;position:fixed;right:16px;bottom:16px;z-index:${Z_INDEX};\n font:13px/1.45 ui-sans-serif,system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;\n color-scheme:light}\n*,*::before,*::after{box-sizing:border-box}\n[hidden]{display:none!important}\n.pill{order:2}\n.bar{display:flex;align-items:center;gap:var(--ai-space-2);justify-content:flex-end}\nbutton{font:inherit;margin:0;border:1px solid var(--ai-line);\n border-radius:var(--ai-radius-control);background:var(--ai-bg);color:var(--ai-fg);\n padding:var(--ai-space-2) var(--ai-space-4);cursor:pointer}\nbutton:focus-visible{outline:2px solid var(--ai-accent);outline-offset:2px}\n.pill{border-radius:var(--ai-radius-pill);background:var(--ai-invert-bg);\n color:var(--ai-invert-fg);border-color:var(--ai-invert-bg);\n padding:var(--ai-space-2) var(--ai-space-5);display:flex;align-items:center;\n gap:var(--ai-space-2)}\n.dot{width:8px;height:8px;border-radius:var(--ai-radius-pill);\n background:var(--ai-idle);display:block}\n.on .dot{background:var(--ai-accent)}\n.panel{display:flex;gap:var(--ai-space-2);align-items:center;background:var(--ai-bg);\n border:1px solid var(--ai-line-soft);border-radius:var(--ai-radius-card);\n padding:var(--ai-space-2)}\n.toggle[aria-pressed=\"true\"]{background:var(--ai-accent);border-color:var(--ai-accent);\n color:var(--ai-invert-fg)}\n.composer{position:fixed;width:${COMPOSER_WIDTH}px;background:var(--ai-bg);color:var(--ai-fg);\n border:1px solid var(--ai-line);border-radius:var(--ai-radius-card);\n padding:var(--ai-space-4);display:flex;flex-direction:column;gap:var(--ai-space-3)}\n.where{margin:0;font:11px/16px ui-monospace,SFMono-Regular,Menlo,monospace;\n color:var(--ai-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\nlabel{font-weight:600}\ntextarea{font:inherit;width:100%;min-height:76px;resize:vertical;\n padding:var(--ai-space-2) var(--ai-space-3);border:1px solid var(--ai-line);\n border-radius:var(--ai-radius-control);background:var(--ai-bg);color:var(--ai-fg)}\ntextarea:focus-visible{outline:2px solid var(--ai-accent);outline-offset:1px}\n.row{display:flex;gap:var(--ai-space-2);justify-content:flex-end}\n.primary{background:var(--ai-accent);border-color:var(--ai-accent);color:var(--ai-invert-fg)}\n.toast{position:fixed;right:16px;bottom:64px;max-width:320px;\n background:var(--ai-invert-bg);color:var(--ai-invert-fg);\n border-radius:var(--ai-radius-card);padding:var(--ai-space-3) var(--ai-space-4);\n display:flex;align-items:center;gap:var(--ai-space-3)}\n.toast.bad{background:var(--ai-bad)}\n.toast a{color:var(--ai-invert-fg)}\n.toast button{background:transparent;border-color:var(--ai-invert-line);\n color:var(--ai-invert-fg)}\n`;\n\nexport interface AnnotationToolbarOptions {\n /**\n * Whether this page may offer an annotation UI at all.\n *\n * INJECTED, and deliberately not decided here. How arming is decided \u2014 the\n * token in the URL, the cookie, the `replaceState` that strips it, the\n * production kill-switch \u2014 is A-241, and a toolbar that read the URL itself\n * would be a second answer to a question that must have exactly one.\n *\n * Unarmed, NOTHING IS APPENDED TO THE DOCUMENT. Not hidden: absent.\n */\n armed?: boolean;\n doc?: Document;\n /** Only for the viewport clamp, so a fake one is two numbers. */\n win?: Pick<Window, \"innerWidth\" | \"innerHeight\">;\n labels?: Partial<AnnotationToolbarLabels>;\n /** *Done* \u2014 the reviewer is finished. A-241 owns what that clears. */\n onDone?: () => void;\n}\n\nexport interface AnnotationToolbar {\n /** Is this node ours? Hand it to `AnnotationSdkOptions.ignoreElement`. */\n owns: (node: unknown) => boolean;\n /** Wire the toggle to a live SDK. */\n attach: (sdk: AnnotationSdk) => void;\n /** Hand to `AnnotationSdkOptions.collectComment`. */\n collectComment: CommentCollector;\n /** Hand to `AnnotationSdkOptions.onCaptureResult`. */\n showResult: (outcome: EgressOutcome, retry: () => void) => void;\n /** Whether the panel is open. For tests and for the mounting app. */\n isExpanded: () => boolean;\n destroy: () => void;\n}\n\n/** `createElement` plus attributes, because half of this file is that call. */\nfunction make(doc: Document, tag: string, attrs?: Record<string, string>): HTMLElement {\n const el = doc.createElement(tag);\n if (attrs) for (const name of Object.keys(attrs)) el.setAttribute(name, attrs[name] ?? \"\");\n return el;\n}\n\nexport function createAnnotationToolbar(\n options: AnnotationToolbarOptions = {},\n): AnnotationToolbar {\n const labels: AnnotationToolbarLabels = { ...DEFAULT_LABELS, ...options.labels };\n const doc = options.doc ?? document;\n const viewport = options.win ?? window;\n\n /**\n * Unarmed is not a hidden toolbar, it is no toolbar. Every method below still\n * answers, so a caller does not have to branch: `collectComment` resolves\n * `\"\"` (a capture with no words is still a capture, and this path is only\n * reachable if something else armed annotation mode), `owns` is false for\n * everything, and nothing is ever added to the page.\n */\n if (options.armed === false) {\n return {\n owns: () => false,\n attach: () => {},\n collectComment: () => Promise.resolve(\"\"),\n showResult: () => {},\n isExpanded: () => false,\n destroy: () => {},\n };\n }\n\n const host = make(doc, \"div\", { id: TOOLBAR_ID });\n const root = host.attachShadow({ mode: \"open\" });\n\n const style = make(doc, \"style\");\n style.textContent = CSS;\n root.appendChild(style);\n\n /** Rewritten per open. A stylesheet, so no element carries a style attribute. */\n const geometry = make(doc, \"style\");\n root.appendChild(geometry);\n\n /* ---------------------------------------------------------------- bar --- */\n\n const bar = make(doc, \"div\", {\n id: \"ai-annotation-bar\",\n class: \"bar\",\n role: \"region\",\n \"aria-label\": labels.region,\n });\n const panel = make(doc, \"div\", { class: \"panel\", id: \"ai-annotation-panel\", hidden: \"\" });\n\n const toggle = make(doc, \"button\", {\n type: \"button\",\n id: \"ai-annotation-toggle\",\n class: \"toggle\",\n \"aria-pressed\": \"false\",\n });\n toggle.textContent = labels.start;\n\n const done = make(doc, \"button\", { type: \"button\", id: \"ai-annotation-done\" });\n done.textContent = labels.done;\n panel.appendChild(toggle);\n panel.appendChild(done);\n\n const pill = make(doc, \"button\", {\n type: \"button\",\n id: \"ai-annotation-pill\",\n class: \"pill\",\n \"aria-expanded\": \"false\",\n \"aria-controls\": \"ai-annotation-panel\",\n \"aria-label\": labels.expand,\n });\n const dot = make(doc, \"span\", { class: \"dot\", \"aria-hidden\": \"true\" });\n const pillText = make(doc, \"span\");\n pillText.textContent = labels.region;\n pill.appendChild(dot);\n pill.appendChild(pillText);\n\n /**\n * PILL FIRST IN THE DOM, and put back on the right with `order` in the CSS.\n *\n * Tab order follows the DOM, not the paint. With the panel first, a reviewer\n * who tabbed to the pill and pressed Enter to expand it found the next Tab\n * took them OUT of the toolbar and back into the product, past the two\n * buttons they had just revealed. The e2e spec caught it; nothing else could.\n */\n bar.appendChild(pill);\n bar.appendChild(panel);\n root.appendChild(bar);\n\n /* ----------------------------------------------------------- composer --- */\n\n const composer = make(doc, \"div\", {\n id: \"ai-annotation-composer\",\n class: \"composer\",\n role: \"dialog\",\n /**\n * `false`, and stated rather than omitted. Modal would mean trapping focus\n * and hiding the rest of the page from assistive technology \u2014 in a product\n * the reviewer is here to READ. The post-it sits beside the thing it is\n * about; it does not take the page hostage.\n */\n \"aria-modal\": \"false\",\n \"aria-labelledby\": \"ai-annotation-comment\",\n hidden: \"\",\n });\n const where = make(doc, \"p\", { class: \"where\" });\n const commentLabel = make(doc, \"label\", {\n id: \"ai-annotation-comment\",\n for: \"ai-annotation-input\",\n });\n commentLabel.textContent = labels.comment;\n const input = make(doc, \"textarea\", {\n id: \"ai-annotation-input\",\n placeholder: labels.placeholder,\n rows: \"3\",\n }) as HTMLTextAreaElement;\n const cancel = make(doc, \"button\", { type: \"button\", id: \"ai-annotation-cancel\" });\n cancel.textContent = labels.cancel;\n const send = make(doc, \"button\", { type: \"button\", id: \"ai-annotation-send\", class: \"primary\" });\n send.textContent = labels.send;\n const row = make(doc, \"div\", { class: \"row\" });\n row.appendChild(cancel);\n row.appendChild(send);\n composer.appendChild(where);\n composer.appendChild(commentLabel);\n composer.appendChild(input);\n composer.appendChild(row);\n root.appendChild(composer);\n\n /* -------------------------------------------------------------- toast --- */\n\n const toast = make(doc, \"div\", { id: \"ai-annotation-toast\", class: \"toast\", role: \"status\", hidden: \"\" });\n const toastText = make(doc, \"span\");\n const toastAction = make(doc, \"a\", {\n id: \"ai-annotation-issue\",\n target: \"_blank\",\n rel: \"noopener noreferrer\",\n hidden: \"\",\n });\n const toastRetry = make(doc, \"button\", { type: \"button\", id: \"ai-annotation-retry\", hidden: \"\" });\n toastRetry.textContent = labels.retry;\n toast.appendChild(toastText);\n toast.appendChild(toastAction);\n toast.appendChild(toastRetry);\n root.appendChild(toast);\n\n doc.body.appendChild(host);\n\n /* --------------------------------------------------------------- wire --- */\n\n let sdk: AnnotationSdk | null = null;\n let expanded = false;\n let settle: ((comment: string | null) => void) | null = null;\n let restoreFocus: Element | null = null;\n let toastTimer: ReturnType<typeof setTimeout> | null = null;\n let retryHandler: () => void = () => {};\n\n const show = (el: HTMLElement, visible: boolean) => {\n if (visible) el.removeAttribute(\"hidden\");\n else el.setAttribute(\"hidden\", \"\");\n };\n\n const setExpanded = (open: boolean) => {\n expanded = open;\n show(panel, open);\n pill.setAttribute(\"aria-expanded\", String(open));\n pill.setAttribute(\"aria-label\", open ? labels.collapse : labels.expand);\n };\n\n const setAnnotating = (on: boolean) => {\n toggle.setAttribute(\"aria-pressed\", String(on));\n toggle.textContent = on ? labels.stop : labels.start;\n bar.setAttribute(\"class\", on ? \"bar on\" : \"bar\");\n if (on) sdk?.enable();\n else sdk?.disable();\n };\n\n pill.addEventListener(\"click\", () => setExpanded(!expanded));\n\n toggle.addEventListener(\"click\", () => {\n setAnnotating(sdk?.isEnabled() !== true);\n });\n\n /**\n * Everything this put on the page, taken back off it.\n *\n * Shared by *Done* and `destroy()` rather than duplicated, so the two cannot\n * come to mean different things \u2014 which is the shape of the bug this whole\n * function exists to close.\n */\n const teardown = () => {\n if (toastTimer !== null) clearTimeout(toastTimer);\n // A promise nobody will ever settle is a capture stuck forever. Cancel it.\n close(null);\n host.remove();\n };\n\n /**\n * ===================================================================\n * *DONE* TEARS DOWN. IT IS A CONTROL, NOT A SUGGESTION (A-241)\n * ===================================================================\n *\n * `docs/plans/first-party-annotation.md` step 5 says *Done clears the cookies\n * and the toolbar disappears*, and until this line only the first half was\n * true. `onDone` disarms \u2014 the cookie and the stored capture token go \u2014 but\n * the host stayed in the DOM, so the toggle went on working for the life of\n * that page: the capture token is already closed over by `httpTransport` and\n * does not care that the cookie is gone. A reviewer who said they were\n * finished still had a live annotation surface until they navigated.\n *\n * On a surface whose entire risk story is *it cannot appear when it should\n * not*, that is the wrong default. After this, nothing on the page can\n * capture again without re-arming through the deep link.\n *\n * ORDER MATTERS: `setAnnotating(false)` first so the SDK drops its document\n * listeners while it still has a toolbar to be told by, then `onDone` \u2014 which\n * is where `mountAnnotation` disarms and destroys the SDK \u2014 and only then the\n * DOM.\n *\n * THE COST, STATED RATHER THAN DISCOVERED: a receipt that lands after *Done*\n * has nowhere to render. Press Send, then Done inside the transport's 8s\n * window, and the capture still goes (the fetch is in flight and teardown\n * does not abort it) but the *View issue* link \u2014 or a failed send's *Retry* \u2014\n * is gone with the toast. That is the accepted price of *Done* meaning done,\n * and it is bounded: the same is already true of navigating away, and the\n * success toast dismisses itself after six seconds regardless.\n */\n done.addEventListener(\"click\", () => {\n setAnnotating(false);\n setExpanded(false);\n options.onDone?.();\n teardown();\n });\n\n /**\n * Closing the composer.\n *\n * `settle = null` IS DEFENSIVE AND PROTECTS NOTHING MEASURABLE. Said plainly,\n * because two previous versions of this comment claimed otherwise and both\n * were wrong \u2014 the second one on the very cycle it was written to fix the\n * first. MEASURED: delete the line and the whole suite still passes.\n *\n * The reasons it protects nothing, since \"it must protect something\" is what\n * produced both wrong answers:\n *\n * - A second `resolve` on a settled promise is already a no-op. Whichever\n * of Send, Cancel and Escape arrives first decides the capture, with or\n * without this line.\n * - It does not gate the two `restoreFocus` lines below; they run\n * unconditionally. And a second pass could not misuse them anyway \u2014\n * `restoreFocus` is `null` by then and `null instanceof HTMLElement` is\n * false, so nothing happens.\n *\n * It stays because it makes the intent \u2014 this closes an OPEN composer, once \u2014\n * legible at the top of the function rather than deducible from promise\n * semantics three files away. That is a real reason and a small one, and it\n * is not a claim that anything breaks without it.\n */\n const close = (comment: string | null) => {\n const resolve = settle;\n settle = null;\n show(composer, false);\n if (resolve) resolve(comment);\n if (restoreFocus instanceof HTMLElement) restoreFocus.focus();\n restoreFocus = null;\n };\n\n cancel.addEventListener(\"click\", () => close(null));\n send.addEventListener(\"click\", () => close(input.value));\n\n composer.addEventListener(\"keydown\", (key) => {\n if (key.key === \"Escape\") {\n /**\n * Ours, and it stops here. `sdk.ts` also listens for Escape on the\n * document in the capture phase, to leave annotation mode \u2014 and it\n * already declines while a comment is being collected, so this is the\n * second of two guards rather than the only one. Two, because a reviewer\n * who loses their words to a stray keystroke does not write them again.\n */\n key.stopPropagation();\n close(null);\n return;\n }\n // The conventional \"submit this textarea\" chord, on both platforms.\n if (key.key === \"Enter\" && (key.metaKey || key.ctrlKey)) {\n key.preventDefault();\n close(input.value);\n }\n });\n\n const collectComment: CommentCollector = (request: CommentRequest) =>\n new Promise<string | null>((resolve) => {\n settle = resolve;\n input.value = \"\";\n where.textContent = describe(request);\n position(request);\n show(toast, false);\n show(composer, true);\n /**\n * Focus moves HERE and nowhere else. This is a response to the\n * reviewer's own click, which is the only moment taking focus is\n * correct \u2014 on mount it would be focus theft in somebody else's product.\n */\n restoreFocus = doc.activeElement;\n input.focus();\n });\n\n /** `file:line`, or the component, or nothing. The customer's own source path. */\n function describe(request: CommentRequest): string {\n const { source, componentName } = request.target;\n if (source) return `${source.file}:${source.line}`;\n return componentName ?? \"\";\n }\n\n /**\n * Anchored near the click WITHOUT COVERING IT, and never outside the viewport.\n *\n * Below the target box when there is room, above it when there is not, and\n * clamped to the viewport with a margin either way.\n *\n * THE CLAMP IS ABOUT REACHABILITY, NOT SCROLLBARS, and the difference is a\n * correction rather than a nuance. This said the clamp was what stopped a box\n * hanging past the right edge putting a horizontal scrollbar on the page. It\n * is not. MEASURED in chromium: a `position: fixed` 300x300 box at\n * `left: 99999px` leaves `scrollWidth` at 1280 against a `clientWidth` of\n * 1280 \u2014 fixed elements contribute nothing to scrollable overflow \u2014 while the\n * identical box at `position: absolute` takes `scrollWidth` to 100299. The\n * no-scrollbar property is bought by `position: fixed` alone.\n *\n * What the clamp actually buys: a composer placed off the viewport is a\n * composer the reviewer cannot see, type into or dismiss, with a capture\n * pending behind it and Escape as the only way out. That is worth the same\n * few lines for a better reason.\n */\n function position(request: CommentRequest) {\n const box = request.target.boundingBox;\n const belowY = box.y + box.h + MARGIN;\n const top =\n belowY + COMPOSER_HEIGHT <= viewport.innerHeight\n ? belowY\n : Math.max(MARGIN, box.y - COMPOSER_HEIGHT - MARGIN);\n const left = Math.min(\n Math.max(MARGIN, box.x),\n Math.max(MARGIN, viewport.innerWidth - COMPOSER_WIDTH - MARGIN),\n );\n geometry.textContent = `.composer{left:${Math.round(left)}px;top:${Math.round(top)}px}`;\n }\n\n const showResult = (outcome: EgressOutcome, retry: () => void) => {\n if (toastTimer !== null) clearTimeout(toastTimer);\n const ok = outcome.status === \"sent\";\n /**\n * SANITISED AGAIN, at the boundary that actually touches the customer's\n * DOM. `httpTransport` already does this to everything it reads off the\n * network, and that is not the same guarantee: this renders the receipt\n * from WHATEVER transport it was given, and a consumer-supplied one never\n * goes near that code. `setAttribute(\"href\", \u2026)` with a `javascript:`\n * value executes in the product's origin, so the check belongs at both\n * ends and there is one function so they cannot disagree.\n */\n const url = outcome.status === \"sent\" ? safeIssueUrl(outcome.receipt.issueUrl) : undefined;\n\n toast.setAttribute(\"class\", ok ? \"toast\" : \"toast bad\");\n /**\n * `alert` for a failure and `status` for a success, set per outcome rather\n * than once at build time. A lost capture that announced itself politely\n * enough to be queued behind other live-region updates is a lost capture\n * the reviewer walks away from.\n */\n toast.setAttribute(\"role\", ok ? \"status\" : \"alert\");\n toastText.textContent = ok ? labels.sent : labels.failed;\n\n show(toastAction, ok && url !== undefined);\n if (url !== undefined) {\n toastAction.setAttribute(\"href\", url);\n toastAction.textContent = labels.issue;\n }\n show(toastRetry, !ok);\n show(toast, true);\n\n /**\n * A success clears itself; a failure does not. The reviewer has to be able\n * to act on a failure, and a retry button that vanished after six seconds\n * is a capture nobody retried.\n */\n if (ok) toastTimer = setTimeout(() => show(toast, false), 6_000);\n retryHandler = retry;\n };\n\n toastRetry.addEventListener(\"click\", () => {\n show(toast, false);\n retryHandler();\n });\n\n return {\n owns: (node) => node instanceof Node && (node === host || host.contains(node)),\n attach: (attached) => {\n sdk = attached;\n // Whatever the SDK already thinks, the button says. A toolbar that\n // rendered `Start` over an enabled SDK would be a lie the reviewer acts on.\n setAnnotating(attached.isEnabled());\n },\n collectComment,\n showResult,\n isExpanded: () => expanded,\n /**\n * The same teardown *Done* runs, and safe to run twice: `clearTimeout` is\n * guarded, `close(null)` on a settled composer is a no-op, and `remove()`\n * on a node with no parent does nothing. A consumer calling `destroy()`\n * after the reviewer clicked *Done* is the normal case, not an error.\n */\n destroy: teardown,\n };\n}\n\n/* ========================================================================== *\n * The whole first-party surface, in one call\n * ========================================================================== */\n\nexport interface MountAnnotationOptions extends AnnotationSdkOptions {\n toolbar?: AnnotationToolbarOptions;\n /**\n * The arming decision, from `readArming()` (A-241).\n *\n * Passing it is how the two gates get applied without every consumer\n * re-deriving them: `armed` decides whether a toolbar exists at all, and\n * *Done* calls `disarm` so the cookie and the stored capture token go with it.\n *\n * OPTIONAL because the frame path still mounts a toolbar-less SDK and has no\n * deep link to read. Supplying both this and `toolbar.armed` throws \u2014 two\n * answers to a question that must have exactly one.\n */\n arming?: Arming;\n}\n\nexport interface MountedAnnotation {\n sdk: AnnotationSdk;\n toolbar: AnnotationToolbar;\n destroy: () => void;\n}\n\n/**\n * Build the SDK and the toolbar and connect them (A-240).\n *\n * Three wires, and each one is a silent failure if it is missing:\n * `collectComment` absent files an issue with an empty comment;\n * `onCaptureResult` absent leaves a reviewer who cannot tell a sent capture\n * from a lost one; `ignoreElement` absent captures our own toolbar. None of\n * them throws when omitted, which is exactly why they belong in one function\n * rather than in three lines of every consumer's bootstrap.\n */\nexport function mountAnnotation(options: MountAnnotationOptions): MountedAnnotation {\n const injected = options.toolbar?.armed;\n if (options.arming && injected !== undefined) {\n throw new Error(\n \"[annotation] pass `arming` OR `toolbar.armed`, not both; two answers to whether this \" +\n \"page may offer an annotation UI is how one of them stops being checked.\",\n );\n }\n\n /**\n * ===================================================================\n * THE TWO GATES, BOTH OF THEM, IN ONE EXPRESSION (A-241)\n * ===================================================================\n *\n * `armed` is the RUNTIME gate: the product's middleware verified the arming\n * token and left a cookie, and `readArming` read it \u2014 never the URL.\n * `isAnnotatableBuild` is the BUILD-TIME one: a production build never mounts\n * this, whatever any cookie says.\n *\n * They are `&&`-ed rather than layered so neither can be satisfied by the\n * other, and each is proven the only way that means anything \u2014 by removing it\n * and watching a NAMED test go red. `toolbar.test.ts` has one per gate, and\n * each holds the OTHER gate open so it cannot pass on its neighbour's work:\n * a production build with `armed: true` renders nothing, and a preview build\n * with `armed: false` renders nothing.\n *\n * `armed: false` is not a hidden toolbar. `createAnnotationToolbar` appends\n * NOTHING to the document \u2014 see its early return \u2014 so a page that somehow\n * shipped this code gets no node, no shadow root and no style element.\n */\n const armed =\n (options.arming ? options.arming.armed : injected) !== false &&\n isAnnotatableBuild(options.build);\n\n const toolbar = createAnnotationToolbar({\n ...options.toolbar,\n armed,\n onDone: () => {\n options.arming?.disarm();\n /**\n * The SDK goes too, and what that buys is SPECIFICALLY THE OVERLAY.\n *\n * Stated narrowly because the obvious claim \u2014 *it removes the document\n * listeners* \u2014 is already `setAnnotating(false)`'s work, and a comment\n * crediting this line with it would be false. MEASURED: delete this call\n * and the only assertion that fails is the one about the overlay node.\n *\n * `enable()` appends the highlight to `doc.body` and `disable()` only\n * HIDES it, so without this a reviewer who clicked *Done* keeps our\n * hidden div in their product's DOM for the life of the page. Small, and\n * exactly the kind of residue a tool that runs inside somebody else's\n * site does not get to leave behind.\n */\n sdk.destroy();\n options.toolbar?.onDone?.();\n },\n });\n const sdk = createAnnotationSdk({\n ...options,\n collectComment: toolbar.collectComment,\n onCaptureResult: toolbar.showResult,\n ignoreElement: toolbar.owns,\n });\n toolbar.attach(sdk);\n return {\n sdk,\n toolbar,\n destroy() {\n toolbar.destroy();\n sdk.destroy();\n },\n };\n}\n", "import type { ConsoleEntry, NetworkEntry } from \"./protocol\";\nimport type { AnnotationState } from \"./sdk\";\n\n/**\n * Page state, captured continuously (A-35).\n *\n * The timing is the whole point. By the time someone notices something is wrong\n * and clicks it, the interesting error has already happened \u2014 reading\n * `console` at capture time gets you nothing, because there is nothing to read.\n * So the buffers fill from page load and the click just takes a copy.\n *\n * Which means this code runs on every page, for every user in the enabled\n * environments, in front of the app's own error handling. It is written to be\n * boring: patch, record, call through, never throw.\n */\n\n/** Twenty each, per the spec. Enough for context, small enough to post. */\nexport const DEFAULT_LIMIT = 20;\n\n/** A stack is useful; a stack the size of the payload is not. */\nconst MAX_MESSAGE = 500;\nconst MAX_STACK = 2000;\n\n/**\n * A fixed-size window over the most recent N items.\n *\n * Oldest-out rather than newest-dropped: a page that logs steadily would\n * otherwise pin the buffer to whatever happened at boot and never show the\n * error the reviewer is looking at.\n */\nexport class RingBuffer<T> {\n readonly #items: T[] = [];\n readonly #limit: number;\n\n constructor(limit: number = DEFAULT_LIMIT) {\n this.#limit = Math.max(1, limit);\n }\n\n push(item: T): void {\n this.#items.push(item);\n if (this.#items.length > this.#limit) this.#items.shift();\n }\n\n /** A copy, so a consumer holding the payload cannot see it mutate underneath them. */\n toArray(): T[] {\n return [...this.#items];\n }\n\n get size(): number {\n return this.#items.length;\n }\n}\n\n/**\n * Render one console argument as text, without ever throwing.\n *\n * Console arguments are arbitrary: DOM nodes, circular objects, proxies that\n * throw on property access. Anything that fails here would fail *inside the\n * app's own `console.error` call*, turning a logged error into a thrown one.\n */\nexport function describeArg(arg: unknown): string {\n if (typeof arg === \"string\") return arg;\n if (arg instanceof Error) return `${arg.name}: ${arg.message}`;\n if (arg === null) return \"null\";\n if (arg === undefined) return \"undefined\";\n if (typeof arg === \"object\") {\n try {\n return JSON.stringify(arg) ?? String(arg);\n } catch {\n /**\n * Circular, or a getter that threw. Reaching for the constructor name is\n * tempting and is itself a property read \u2014 on a Proxy whose `get` trap\n * throws, the fallback throws too, outside the try that was meant to\n * contain it. Ask the prototype instead, which no `get` trap sees.\n */\n try {\n const proto = Object.getPrototypeOf(arg) as { constructor?: { name?: string } } | null;\n return `[${proto?.constructor?.name ?? \"object\"}]`;\n } catch {\n return \"[object]\";\n }\n }\n }\n try {\n return String(arg);\n } catch {\n return \"[unrenderable]\";\n }\n}\n\nconst truncate = (text: string, max: number) =>\n text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;\n\nexport function formatConsoleArgs(args: unknown[]): string {\n return truncate(args.map(describeArg).join(\" \"), MAX_MESSAGE);\n}\n\n/** The first `Error` among the arguments carries the only stack worth keeping. */\nexport function firstStack(args: unknown[]): string | undefined {\n for (const arg of args) {\n if (arg instanceof Error && typeof arg.stack === \"string\") {\n return truncate(arg.stack, MAX_STACK);\n }\n }\n return undefined;\n}\n\n/** A response is interesting when it is not 2xx. `0` marks a request that never got one. */\nexport function isFailedStatus(status: number): boolean {\n return status === 0 || status < 200 || status >= 300;\n}\n\nexport interface StateCollector {\n getState(): AnnotationState;\n /** Restore every patched global. Exposed mainly so tests can prove it is reversible. */\n destroy(): void;\n}\n\nexport interface StateCollectorOptions {\n limit: number;\n /** Feature flags are the app's to know; this module does not guess at them. */\n getFeatureFlags: () => Record<string, unknown>;\n now: () => string;\n}\n\nexport const defaultStateCollectorOptions = (): StateCollectorOptions => ({\n limit: DEFAULT_LIMIT,\n getFeatureFlags: () => ({}),\n now: () => new Date().toISOString(),\n});\n\n/**\n * Patch `console`, `fetch` and `XMLHttpRequest`, and record what goes wrong.\n *\n * Install this as early as the bundle allows \u2014 an error thrown before the patch\n * is an error the payload will not carry.\n */\nexport function installStateCollector(\n win: Window & typeof globalThis,\n options: StateCollectorOptions = defaultStateCollectorOptions(),\n): StateCollector {\n const consoleErrors = new RingBuffer<ConsoleEntry>(options.limit);\n const failedRequests = new RingBuffer<NetworkEntry>(options.limit);\n\n const record = (level: ConsoleEntry[\"level\"], args: unknown[]) => {\n consoleErrors.push({\n level,\n message: formatConsoleArgs(args),\n ...(firstStack(args) ? { stack: firstStack(args) } : {}),\n at: options.now(),\n });\n };\n\n // --- console -------------------------------------------------------------\n\n const originalError = win.console.error;\n const originalWarn = win.console.warn;\n\n win.console.error = function patchedError(...args: unknown[]) {\n try {\n record(\"error\", args);\n } catch {\n // Recording is best-effort. The app's log must go out regardless.\n }\n return originalError.apply(win.console, args as never);\n };\n\n win.console.warn = function patchedWarn(...args: unknown[]) {\n try {\n record(\"warn\", args);\n } catch {\n /* as above */\n }\n return originalWarn.apply(win.console, args as never);\n };\n\n // --- fetch ---------------------------------------------------------------\n\n /**\n * `unbound-method` is right in general and wrong here (A-97). This is a\n * patch-and-restore: the original is saved so it can be called with an\n * explicit receiver (`originalFetch.call(win, \u2026)`, below) and put back on\n * teardown. Binding it instead would make the restore install a bound copy\n * rather than the function the page started with.\n */\n // eslint-disable-next-line @typescript-eslint/unbound-method\n const originalFetch = win.fetch;\n\n const requestInfo = (input: unknown, init?: RequestInit) => {\n const url =\n typeof input === \"string\"\n ? input\n : input instanceof URL\n ? input.href\n : ((input as Request)?.url ?? String(input));\n const method = init?.method ?? (input as Request)?.method ?? \"GET\";\n return { url, method: method.toUpperCase() };\n };\n\n if (typeof originalFetch === \"function\") {\n win.fetch = async function patchedFetch(input: never, init?: RequestInit) {\n try {\n const response = await originalFetch.call(win, input, init);\n if (isFailedStatus(response.status)) {\n const { url, method } = requestInfo(input, init);\n failedRequests.push({ url, method, status: response.status, at: options.now() });\n }\n return response;\n } catch (error) {\n /**\n * A request that never got a response is recorded as status 0. The spec\n * says \"non-2xx\", and a connection that was refused or aborted is the\n * most interesting failure of all \u2014 it is exactly what a reviewer means\n * by \"nothing loaded\". Rethrown untouched: this is an observer.\n */\n const { url, method } = requestInfo(input, init);\n failedRequests.push({ url, method, status: 0, at: options.now() });\n throw error;\n }\n } as typeof win.fetch;\n }\n\n // --- XMLHttpRequest ------------------------------------------------------\n\n const XHR = win.XMLHttpRequest;\n // Saved to be `.apply`d with an explicit `this` and restored on teardown \u2014\n // see the note on `originalFetch` above.\n // eslint-disable-next-line @typescript-eslint/unbound-method\n const originalOpen = XHR?.prototype?.open;\n\n if (typeof originalOpen === \"function\") {\n XHR.prototype.open = function patchedOpen(\n this: XMLHttpRequest & { __aiAnnotateMethod?: string; __aiAnnotateUrl?: string },\n method: string,\n url: string | URL,\n ...rest: unknown[]\n ) {\n this.__aiAnnotateMethod = method.toUpperCase();\n this.__aiAnnotateUrl = String(url);\n\n // `loadend` fires for success, failure, abort and timeout alike, so one\n // listener covers every way a request can finish.\n this.addEventListener(\"loadend\", () => {\n try {\n if (isFailedStatus(this.status)) {\n failedRequests.push({\n url: this.__aiAnnotateUrl ?? \"\",\n method: this.__aiAnnotateMethod ?? \"GET\",\n status: this.status,\n at: options.now(),\n });\n }\n } catch {\n /* observing must never break the request */\n }\n });\n\n return originalOpen.apply(this, [method, url, ...rest] as never);\n };\n }\n\n return {\n getState(): AnnotationState {\n return {\n consoleErrors: consoleErrors.toArray(),\n failedRequests: failedRequests.toArray(),\n featureFlags: options.getFeatureFlags(),\n };\n },\n destroy() {\n win.console.error = originalError;\n win.console.warn = originalWarn;\n if (typeof originalFetch === \"function\") win.fetch = originalFetch;\n if (typeof originalOpen === \"function\") XHR.prototype.open = originalOpen;\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["SDK_VERSION", "SRC_ATTR", "NAME_ATTR", "selectionOf", "target", "PARENT_TYPES", "isParentMessage", "data", "type", "assertConcreteOrigin", "origin", "ANNOTATION_TOKEN_PARAM", "ANNOTATION_ARMED_COOKIE", "CAPTURE_TOKEN_PARAM", "REVIEW_RUN_PARAM", "ANNOTATION_ARMED_VALUE", "armedCookie", "name", "clearedCookie", "LEGACY_ANNOTATION_TOKEN_PARAM", "LEGACY_ANNOTATION_ARMED_COOKIE", "ANNOTATION_TOKEN_PARAMS", "ANNOTATION_ARMED_COOKIES", "isAnnotationArmed", "cookie", "entry", "trimmed", "readAnnotationToken", "params", "value", "OVERLAY_ID", "Z_INDEX", "createOverlay", "doc", "root", "tag", "box", "label", "node", "MAX_TEXT", "MAX_CHAIN", "parseSourceRef", "value", "colIdx", "lineIdx", "file", "line", "column", "collectSourceChain", "el", "max", "chain", "node", "ref", "SRC_ATTR", "prev", "findAnnotatedAncestor", "classTokens", "list", "domPath", "maxDepth", "parts", "tag", "part", "classes", "summariseText", "text", "collapsed", "resolveTarget", "options", "annotated", "source", "target", "componentName", "NAME_ATTR", "MAX_ISSUE_URL", "safeIssueUrl", "value", "url", "postMessageTransport", "parentOrigin", "win", "origin", "assertConcreteOrigin", "message", "assertConcreteEndpoint", "endpoint", "HTTP_TRANSPORT_TIMEOUT_MS", "httpTransport", "options", "token", "timeoutMs", "doFetch", "kind", "controller", "timer", "response", "body", "issueUrl", "error", "createEgress", "transport", "onError", "report", "failure", "MAX_SCREENSHOT_BYTES", "MAX_SCREENSHOT_EDGE", "SCREENSHOT_TIMEOUT_MS", "ATTEMPTS", "FORMATS", "resolveScreenshotOptions", "options", "doc", "opts", "MEDIA_TAGS", "MEDIA_SELECTOR", "MAX_ANCESTOR_HOPS", "isMediaElement", "el", "findCaptureSource", "maxHops", "descendant", "node", "hops", "intrinsicSize", "tag", "pair", "w", "h", "fitWithin", "size", "maxEdge", "scale", "encode", "source", "quality", "canvas", "ctx", "format", "dataUrl", "captureScreenshotSync", "natural", "attempt", "captureScreenshot", "resolve", "settled", "settle", "value", "timer", "attachScreenshot", "payload", "screenshot", "EMPTY_STATE", "emptyAnnotationState", "createAnnotationSdk", "options", "parentOrigin", "assertConcreteOrigin", "win", "doc", "screenshotOptions", "resolveScreenshotOptions", "enabled", "overlay", "hoverFrame", "lastHovered", "collecting", "transport", "postMessageTransport", "post", "createEgress", "boxOf", "el", "r", "targetFor", "resolveTarget", "labelFor", "target", "file", "onMouseMove", "event", "buildPayload", "state", "deliver", "payload", "message", "send", "outcome", "onClick", "collect", "attachScreenshot", "withShot", "point", "captured", "ask", "failure", "words", "comment", "error", "finished", "onKeyDown", "disable", "enable", "createOverlay", "onMessage", "isParentMessage", "CAPTURE_TOKEN_STORAGE_KEY", "REVIEW_RUN_STORAGE_KEY", "STRIPPED_PARAMS", "ANNOTATION_TOKEN_PARAMS", "CAPTURE_TOKEN_PARAM", "REVIEW_RUN_PARAM", "safely", "run", "fallback", "defaultStorage", "win", "readArming", "options", "doc", "storage", "href", "url", "captureToken", "runId", "name", "history", "disarm", "ANNOTATION_ARMED_COOKIES", "clearedCookie", "armed", "isAnnotationArmed", "ANNOTATABLE_ENVS", "isAnnotatableBuild", "build", "DEFAULT_LABELS", "TOOLBAR_ID", "Z_INDEX", "COMPOSER_WIDTH", "COMPOSER_HEIGHT", "MARGIN", "CSS", "make", "doc", "tag", "attrs", "el", "name", "createAnnotationToolbar", "options", "labels", "viewport", "host", "root", "style", "geometry", "bar", "panel", "toggle", "done", "pill", "dot", "pillText", "composer", "where", "commentLabel", "input", "cancel", "send", "row", "toast", "toastText", "toastAction", "toastRetry", "sdk", "expanded", "settle", "restoreFocus", "toastTimer", "retryHandler", "show", "visible", "setExpanded", "open", "setAnnotating", "on", "teardown", "close", "comment", "resolve", "key", "collectComment", "request", "describe", "position", "source", "componentName", "box", "belowY", "top", "left", "showResult", "outcome", "retry", "ok", "url", "safeIssueUrl", "node", "attached", "mountAnnotation", "injected", "armed", "isAnnotatableBuild", "toolbar", "createAnnotationSdk", "DEFAULT_LIMIT", "_items", "_limit", "RingBuffer", "limit", "__privateAdd", "__privateSet", "item", "__privateGet", "describeArg", "arg", "truncate", "text", "max", "formatConsoleArgs", "args", "firstStack", "isFailedStatus", "status", "defaultStateCollectorOptions", "installStateCollector", "win", "options", "consoleErrors", "failedRequests", "record", "level", "originalError", "originalWarn", "originalFetch", "requestInfo", "input", "init", "url", "method", "response", "error", "XHR", "originalOpen", "rest"]
7
7
  }