@weatherboard/annotation 0.4.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADOPTING.md +147 -10
- package/COMPATIBILITY.md +29 -0
- package/README.md +84 -10
- package/dist/anchor.cjs +1 -1
- package/dist/anchor.cjs.map +2 -2
- package/dist/anchor.mjs.map +2 -2
- package/dist/arming.d.cts +18 -21
- package/dist/arming.d.mts +18 -21
- package/dist/arming.d.ts +18 -21
- package/dist/bootstrap-runtime.d.cts +30 -0
- package/dist/bootstrap-runtime.d.mts +30 -0
- package/dist/bootstrap-runtime.d.ts +30 -0
- package/dist/bootstrap.cjs +2 -0
- package/dist/bootstrap.cjs.map +7 -0
- package/dist/bootstrap.d.cts +8 -0
- package/dist/bootstrap.d.mts +8 -0
- package/dist/bootstrap.d.ts +8 -0
- package/dist/bootstrap.mjs +2 -0
- package/dist/bootstrap.mjs.map +7 -0
- package/dist/host.cjs +1 -1
- package/dist/host.cjs.map +3 -3
- package/dist/host.mjs +1 -1
- package/dist/host.mjs.map +3 -3
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.cts +10 -4
- package/dist/index.d.mts +10 -4
- package/dist/index.d.ts +10 -4
- package/dist/index.mjs +4 -4
- package/dist/index.mjs.map +4 -4
- package/dist/markers.cjs +2 -2
- package/dist/markers.cjs.map +2 -2
- package/dist/markers.mjs +2 -2
- package/dist/markers.mjs.map +2 -2
- package/dist/page-capture.cjs +2 -0
- package/dist/page-capture.cjs.map +7 -0
- package/dist/page-capture.d.cts +115 -0
- package/dist/page-capture.d.mts +115 -0
- package/dist/page-capture.d.ts +115 -0
- package/dist/page-capture.mjs +2 -0
- package/dist/page-capture.mjs.map +7 -0
- package/dist/protocol.cjs +1 -1
- package/dist/protocol.cjs.map +3 -3
- package/dist/protocol.d.cts +19 -0
- package/dist/protocol.d.mts +19 -0
- package/dist/protocol.d.ts +19 -0
- package/dist/protocol.mjs +1 -1
- package/dist/protocol.mjs.map +3 -3
- package/dist/read.cjs +1 -1
- package/dist/read.cjs.map +2 -2
- package/dist/read.mjs +1 -1
- package/dist/read.mjs.map +2 -2
- package/dist/screenshot.d.cts +56 -7
- package/dist/screenshot.d.mts +56 -7
- package/dist/screenshot.d.ts +56 -7
- package/dist/selection.cjs +5 -0
- package/dist/selection.cjs.map +7 -0
- package/dist/selection.d.cts +27 -0
- package/dist/selection.d.mts +27 -0
- package/dist/selection.d.ts +27 -0
- package/dist/selection.mjs +5 -0
- package/dist/selection.mjs.map +7 -0
- package/dist/session.d.cts +17 -0
- package/dist/session.d.mts +17 -0
- package/dist/session.d.ts +17 -0
- package/dist/shortcut.d.cts +52 -0
- package/dist/shortcut.d.mts +52 -0
- package/dist/shortcut.d.ts +52 -0
- package/dist/toolbar.d.cts +13 -0
- package/dist/toolbar.d.mts +13 -0
- package/dist/toolbar.d.ts +13 -0
- package/dist/transport.d.cts +10 -0
- package/dist/transport.d.mts +10 -0
- package/dist/transport.d.ts +10 -0
- package/package.json +45 -10
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/protocol.ts", "../src/session.ts", "../src/bootstrap-runtime.ts", "../src/arming.ts", "../src/shortcut.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 * A hosted document declares first-party annotation support with this exact\n * response header. It describes the BUILD, never a token, cookie or attempt.\n */\nexport const ANNOTATION_MODE_HEADER = \"x-weatherboard-annotation-mode\";\nexport const FIRST_PARTY_ANNOTATION_MODE = \"first-party\";\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 /**\n * `w`/`h` and `canvases` arrive with the in-page viewport capture (A-2385);\n * `canvases` counts the canvases in view and how many read back blank or\n * unreadable, so a black rectangle in the picture is explained, not silent.\n */\n screenshot?: {\n dataUrl: string;\n region: \"element\" | \"viewport\";\n w?: number;\n h?: number;\n canvases?: { total: number; blank: number; unreadable: number };\n };\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\n/**\n * A message that got past both guards and matched no `case` (A-359).\n *\n * ===========================================================================\n * THE DEFECT\n * ===========================================================================\n *\n * `host.ts` and `sdk.ts` hold the two halves of this protocol, one `switch`\n * each, and neither had a `default`. Measured before the fix: adding\n * `| { type: \"annotate:probe\" }` to `ParentToFrame` and\n * `| { type: \"annotate:pong\" }` to `FrameToParent`, then running\n * `tsc --noEmit` in this package, gave exit 0 and zero errors. A message type\n * added to the union is dropped on the floor by whichever half was not\n * updated, and the two halves are the only things in the world that can\n * disagree about what a message means.\n *\n * ===========================================================================\n * WHY IT DOES NOT THROW, WHERE THE MANAGER'S TWO EQUIVALENTS DO\n * ===========================================================================\n *\n * This is the house pattern with one deliberate departure, and the departure is\n * the boundary rather than taste.\n *\n * Both switches run inside a `message` listener, reached from `postMessage`.\n * The host's listener runs in the management console over a frame that may be\n * serving anything; the SDK's runs in a product page we do not own. A throw\n * there does not propagate to a caller \u2014 there is no caller \u2014 it becomes an\n * uncaught error on somebody else's page, once per message. Any party that can\n * reach this branch can therefore reach it repeatedly, and the loudest\n * available answer would be handing them a way to fill a product's error\n * reporting with our noise.\n *\n * The narrower reason the throw would buy little: `isFrameMessage` and\n * `isParentMessage` gate on fixed sets of type strings, so a message an\n * outsider invents never reaches the switch at all. What DOES reach it is a\n * type in the guard's set with no `case` \u2014 which is our own two lists having\n * drifted apart, in this repository, in the commit that widened one of them.\n *\n * So the answer is: act on nothing, and say so. Acting on nothing is what a\n * trust boundary owes an input it does not understand; saying so is what makes\n * this different from the silent drop that was there before, which is\n * indistinguishable from the message never having been sent.\n *\n * `console.error` rather than a callback, because both call sites are inside\n * listeners with no error channel of their own, and because in the frame the\n * SDK's own `StateCollector` patches `console.error` \u2014 so the drift is\n * collected into the very next annotation payload, where somebody debugging\n * exactly this will already be looking.\n *\n * The type-level half of the guard is NOT here. It is `const unhandled: never`\n * in each `default` branch, which is what makes a widened union a compile\n * error; this function only makes the runtime half observable, and it is\n * exported so `exhaustive.test.ts` can call it. Both halves are pinned\n * separately, because deleting either leaves the other looking correct.\n */\n/**\n * `JSON.stringify` that cannot throw, for any input at all (A-359 cycle 2).\n *\n * ===========================================================================\n * WHY THIS IS NEEDED \u2014 THE DEFECT IT REPLACES\n * ===========================================================================\n *\n * `unhandledMessage` interpolated `JSON.stringify(message)` unguarded, and so\n * became the thing it was written to prevent. `postMessage` serialises with\n * the STRUCTURED CLONE algorithm, and structured clone PRESERVES CYCLES where\n * JSON cannot represent them \u2014 so `{a: {b: a}}` survives a `postMessage`\n * intact, arrives at that branch, and `JSON.stringify` throws `Converting\n * circular structure to JSON` from inside a `message` listener on a page this\n * SDK is a guest on, once per message, at the sender's discretion.\n *\n * Every word of the argument on `unhandledMessage` was correct, and the\n * implementation defeated it on its own terms. Both cases are pinned in\n * `exhaustive.test.ts` and both were confirmed RED before this existed.\n *\n * ===========================================================================\n * WHY NOT `describeArg`, WHICH DOES THIS ALREADY \u2014 TWO REASONS, ONE MEASURED\n * ===========================================================================\n *\n * `describeArg` in `state.ts` solves the same problem and its comment names\n * the same hazard, so reusing it was the first thing tried rather than a road\n * not taken. It was reverted for two reasons, and the second is the one that\n * decided it:\n *\n * 1. MEASURED, by running `node build.mjs` on both: reuse costs protocol\n * 869 -> 1008 (+139) and host 649 -> 793 (+144). This local version costs\n * +39 and +37. `describeArg` is built for console ARGUMENTS \u2014 strings,\n * Errors, a prototype walk for named objects \u2014 and almost none of that is\n * reachable for a wire message, so the extra 100 bytes per entry point buy\n * nothing here. In a package with a per-entry gzip budget that is not a\n * rounding error.\n * 2. THIS MODULE HAS NO IMPORTS AND THAT IS A PROPERTY WORTH KEEPING.\n * `protocol.ts` is the pure wire contract \u2014 it mirrors\n * `docs/contracts/types.ts`, `annotation-next` imports it so a build tool\n * can learn two strings without pulling in a DOM, and `state.ts` already\n * imports FROM it. Importing back the other way makes the contract depend\n * on the console-patching state collector and closes a module cycle that\n * is only invisible because `state.ts`'s side of it is `import type`.\n *\n * The cost of the choice, stated rather than left for somebody to find: there\n * are now two safe-stringify helpers in this package. They cannot drift in a\n * way that matters \u2014 each is a total function with one behaviour and its own\n * test \u2014 but if a third appears, that is the moment to put one in a module\n * both can depend on, rather than making the contract depend on the collector.\n *\n * The fallback is a constant, deliberately. Reaching for `String(message)` or\n * a constructor name in the `catch` is the obvious way to say something more\n * useful, and it is how this function would learn to throw again: the input\n * that got us here is one whose property access already threw.\n */\nfunction safeJson(message: unknown): string {\n try {\n return JSON.stringify(message) ?? String(message);\n } catch {\n return \"[unserialisable]\";\n }\n}\n\nexport function unhandledMessage(half: \"host\" | \"frame\", message: unknown): void {\n /**\n * `safeJson` RATHER THAN A BARE `JSON.stringify`, AND THAT IS THE WHOLE\n * DIFFERENCE BETWEEN THIS FUNCTION AND ITS FIRST DRAFT (A-359 cycle 2).\n *\n * The first draft interpolated `JSON.stringify(message)` unguarded, which\n * made this function do the thing it exists to avoid. The full argument, the\n * measurement, and why `describeArg` was tried and reverted are on\n * `safeJson` above rather than repeated here.\n *\n * Short on purpose too, and the shortness is measured rather than stylistic.\n * This package ships inside a product's bundle and this package's own\n * `build.mjs` enforces a gzip budget per entry point; the first draft of this\n * sentence was 230 characters and put `protocol`, `host` and `index` over\n * theirs at once. What it must still carry is which half, what arrived, and\n * that nothing was done with it \u2014 the last being the only thing that\n * distinguishes this from the silent drop it replaced.\n */\n console.error(`[annotation] ${half}: protocol drift, nothing was acted on: ${safeJson(message)}`);\n}\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 READ token's query parameter (A-277).\n *\n * ===========================================================================\n * WHY THE MARKER LAYER GETS ITS OWN CREDENTIAL AND NOT `ai_capture`\n * ===========================================================================\n *\n * Fourth name on the same wire, and the reason it is a fourth is A-276's\n * disclosure decision rather than tidiness. The capture token is minted for\n * WRITING and lives four hours so a reviewer can work through a session; reusing\n * it for the read would make one leaked deep link a write capability AND a read\n * capability over every reviewer's comments on that origin for the same window.\n * The scope is a signed field, `verifyCaptureToken` requires an expectation at\n * every call site, and the read endpoint refuses a write claim \u2014 so handing\n * `ai_capture` to the reader does not degrade to a read, it fails.\n *\n * The read claim's TTL is ONE hour rather than four (`READ_TOKEN_TTL_S`), which\n * is the practical consequence a reader of this constant should know: the\n * toolbar outlives the markers, so a reviewer who leaves a tab open all\n * afternoon will find the count line saying it cannot read before they find the\n * Send button broken. That is the intended ordering \u2014 the shorter credential is\n * the one that discloses other people's words.\n *\n * Exported here for the reason the three above are: the console writes it into a\n * deep link and the product's bundle reads it, from two repositories, and a\n * mismatch produces no error anywhere \u2014 just a page with no markers, which is\n * indistinguishable from nine other causes.\n */\nexport const READ_TOKEN_PARAM = \"ai_read\";\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", "/** Per-tab state. None of these values are cookies or sent by the browser. */\nexport const CAPTURE_TOKEN_STORAGE_KEY = \"ai.annotation.capture\";\nexport const READ_TOKEN_STORAGE_KEY = \"ai.annotation.read\";\nexport const REVIEW_RUN_STORAGE_KEY = \"ai.annotation.run\";\n\n/** A credential-free record that an annotation deep link was opened. */\nexport const ANNOTATION_ATTEMPT_STORAGE_KEY = \"ai.annotation.attempt\";\nexport const ANNOTATION_ATTEMPT_GLOBAL = \"__weatherboardAnnotationAttemptV1\";\nexport const ANNOTATION_STARTUP_STATUS_ID = \"ai-annotation-startup-status\";\n\n/** Small enough to remain in the root entry point for Done's full teardown. */\nexport function clearAnnotationBootstrap(\n win: Window | null,\n storage: Pick<Storage, \"removeItem\"> | null,\n doc?: Document,\n): void {\n if (win !== null) {\n try {\n delete (win as unknown as Record<string, unknown>)[ANNOTATION_ATTEMPT_GLOBAL];\n } catch {\n /* The session copy is cleared separately. */\n }\n }\n if (storage !== null) {\n try {\n storage.removeItem(ANNOTATION_ATTEMPT_STORAGE_KEY);\n } catch {\n /* Storage refusal must not block cookie or DOM teardown. */\n }\n }\n doc?.getElementById?.(ANNOTATION_STARTUP_STATUS_ID)?.remove();\n}\n\n/**\n * A credential-free note that the shortcut asked to open the toolbar but had\n * to re-arm first (A-2384). The value is the request time in epoch ms; the\n * next armed mount in this tab opens the toolbar once if it is fresh.\n */\nexport const ANNOTATION_OPEN_INTENT_STORAGE_KEY = \"ai.annotation.open\";\nexport const OPEN_INTENT_TTL_MS = 10 * 60 * 1000;\n", "import {\n ANNOTATION_ARMED_COOKIES,\n ANNOTATION_TOKEN_PARAMS,\n CAPTURE_TOKEN_PARAM,\n READ_TOKEN_PARAM,\n REVIEW_RUN_PARAM,\n} from \"./protocol\";\nimport {\n ANNOTATION_ATTEMPT_GLOBAL,\n ANNOTATION_ATTEMPT_STORAGE_KEY,\n ANNOTATION_STARTUP_STATUS_ID,\n CAPTURE_TOKEN_STORAGE_KEY,\n READ_TOKEN_STORAGE_KEY,\n REVIEW_RUN_STORAGE_KEY,\n} from \"./session\";\n\nexport type AnnotationAttempt = {\n /** `accepted` means server middleware left a readable arming cookie. */\n status: \"accepted\" | \"rejected\";\n /** Presence only. The credential itself is never part of this signal. */\n capture: \"present\" | \"missing\";\n};\n\nexport type AnnotationStartupFailure =\n | \"rejected-arming\"\n | \"missing-capture\"\n | \"import-failed\"\n | \"mount-failed\";\n\ntype BootstrapConfig = {\n tokenParams: readonly string[];\n armedCookies: readonly string[];\n captureParam: string;\n readParam: string;\n runParam: string;\n captureKey: string;\n readKey: string;\n runKey: string;\n attemptKey: string;\n attemptGlobal: string;\n};\n\nconst CONFIG: BootstrapConfig = {\n tokenParams: ANNOTATION_TOKEN_PARAMS,\n armedCookies: ANNOTATION_ARMED_COOKIES,\n captureParam: CAPTURE_TOKEN_PARAM,\n readParam: READ_TOKEN_PARAM,\n runParam: REVIEW_RUN_PARAM,\n captureKey: CAPTURE_TOKEN_STORAGE_KEY,\n readKey: READ_TOKEN_STORAGE_KEY,\n runKey: REVIEW_RUN_STORAGE_KEY,\n attemptKey: ANNOTATION_ATTEMPT_STORAGE_KEY,\n attemptGlobal: ANNOTATION_ATTEMPT_GLOBAL,\n};\n\n/**\n * The executable is literal source rather than `Function#toString()`. Build\n * tools are allowed to wrap named functions with private helpers; stringifying\n * such a function produces a script that refers to a helper the page does not\n * have. This string is the published artifact, exercised after packing and in\n * Chromium. It is the only implementation of the pre-hydration phase.\n */\nconst INLINE_BOOTSTRAP_SOURCE =\n \"(function(w,d,c){\" +\n \"function s(f,x){try{return f()}catch(e){return x}}\" +\n \"var u=s(function(){return new URL(d.URL)},null);if(!u)return;\" +\n \"var a=c.tokenParams.some(function(n){return u.searchParams.has(n)}),\" +\n \"p=u.searchParams.get(c.captureParam)||null,\" +\n \"r=u.searchParams.get(c.readParam)||null,\" +\n \"v=u.searchParams.get(c.runParam)||null,\" +\n \"n=c.tokenParams.concat([c.captureParam,c.readParam,c.runParam]);\" +\n \"if(n.some(function(k){return u.searchParams.has(k)})){\" +\n \"n.forEach(function(k){u.searchParams.delete(k)});\" +\n \"s(function(){w.history.replaceState(w.history.state==null?null:w.history.state,'',u.toString())})}\" +\n \"var armed=s(function(){return d.cookie},'').split(';').some(function(e){\" +\n \"e=e.trimStart();return c.armedCookies.some(function(k){return e.startsWith(k+'=')})}),\" +\n \"st=s(function(){return w.sessionStorage},null);\" +\n \"if(a){var m={status:armed?'accepted':'rejected',capture:p===null?'missing':'present'};\" +\n \"s(function(){w[c.attemptGlobal]=m});if(st)s(function(){st.setItem(c.attemptKey,JSON.stringify(m))})}\" +\n \"if(!st)return;\" +\n \"function del(k){s(function(){st.removeItem(k)})}\" +\n \"function put(k,x){if(x===null)del(k);else s(function(){st.setItem(k,x)})}\" +\n \"if(a)[c.captureKey,c.readKey,c.runKey].forEach(del);if(!armed)return;\" +\n \"if(p!==null)put(c.captureKey,p);if(r!==null)put(c.readKey,r);if(v!==null)put(c.runKey,v)}\" +\n \")\";\n\n/**\n * Source for the first inline script in `<head>`.\n *\n * It strips every annotation credential synchronously and preserves the\n * router's current history state. Credentials are written only after server\n * middleware has set an armed cookie. A rejected link stores only two enums.\n */\nexport function annotationBootstrapScript(): string {\n const config = JSON.stringify(CONFIG).replaceAll(\"<\", \"\\\\u003c\");\n return `;${INLINE_BOOTSTRAP_SOURCE}(window,document,${config});`;\n}\n\nfunction validAttempt(value: unknown): value is AnnotationAttempt {\n if (value === null || typeof value !== \"object\") return false;\n const attempt = value as Partial<AnnotationAttempt>;\n return (\n (attempt.status === \"accepted\" || attempt.status === \"rejected\") &&\n (attempt.capture === \"present\" || attempt.capture === \"missing\")\n );\n}\n\nexport interface ConsumeAnnotationAttemptOptions {\n win?: Window;\n storage?: Pick<Storage, \"getItem\" | \"removeItem\"> | null;\n}\n\n/** Read the credential-free attempt once, then remove both of its copies. */\nexport function consumeAnnotationAttempt(\n options: ConsumeAnnotationAttemptOptions = {},\n): AnnotationAttempt | null {\n const win = options.win ?? window;\n const storage = options.storage === undefined\n ? (() => {\n try {\n return win.sessionStorage;\n } catch {\n return null;\n }\n })()\n : options.storage;\n\n let attempt: unknown = null;\n try {\n attempt = (win as unknown as Record<string, unknown>)[ANNOTATION_ATTEMPT_GLOBAL] ?? null;\n delete (win as unknown as Record<string, unknown>)[ANNOTATION_ATTEMPT_GLOBAL];\n } catch {\n /* The session copy below remains available. */\n }\n if (!validAttempt(attempt) && storage !== null) {\n try {\n const raw = storage.getItem(ANNOTATION_ATTEMPT_STORAGE_KEY);\n attempt = raw === null ? null : JSON.parse(raw);\n } catch {\n attempt = null;\n }\n }\n if (storage !== null) {\n try {\n storage.removeItem(ANNOTATION_ATTEMPT_STORAGE_KEY);\n } catch {\n /* Consuming the in-memory copy still succeeds when storage is blocked. */\n }\n }\n return validAttempt(attempt)\n ? { status: attempt.status, capture: attempt.capture }\n : null;\n}\n\nconst STARTUP_COPY: Readonly<Record<AnnotationStartupFailure, string>> = {\n \"rejected-arming\":\n \"Annotation could not start because this link was not accepted. Open a fresh review link and try again.\",\n \"missing-capture\":\n \"Annotation is open, but this link cannot send feedback. Open a fresh review link and try again.\",\n \"import-failed\": \"Annotation could not load. Reload the page or open a fresh review link.\",\n \"mount-failed\": \"Annotation could not start. Reload the page or open a fresh review link.\",\n};\n\nexport interface AnnotationStartupFailureOptions {\n doc?: Document;\n}\n\n/**\n * Show one generic, credential-free startup error. Repeated calls replace the\n * copy in the same host. The returned cleanup is safe after replacement.\n */\nexport function reportAnnotationStartupFailure(\n reason: AnnotationStartupFailure,\n options: AnnotationStartupFailureOptions = {},\n): () => void {\n const doc = options.doc ?? document;\n let host = doc.getElementById(ANNOTATION_STARTUP_STATUS_ID);\n if (host === null) {\n host = doc.createElement(\"div\");\n host.id = ANNOTATION_STARTUP_STATUS_ID;\n host.setAttribute(\"role\", \"alert\");\n host.setAttribute(\n \"style\",\n \"--ai-status-bg:#18181b;--ai-status-fg:#fafafa;--ai-status-edge:16px;\" +\n \"--ai-status-pad-block:12px;--ai-status-pad-inline:14px;\" +\n \"position:fixed;right:var(--ai-status-edge);bottom:var(--ai-status-edge);\" +\n \"z-index:2147483002;max-width:360px;\" +\n \"padding:var(--ai-status-pad-block) var(--ai-status-pad-inline);\" +\n \"border-radius:8px;background:var(--ai-status-bg);color:var(--ai-status-fg);\" +\n \"font:13px/1.45 ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif\",\n );\n (doc.body ?? doc.documentElement).append(host);\n }\n const revision = String(Number(host.getAttribute(\"data-annotation-revision\") ?? \"0\") + 1);\n host.setAttribute(\"data-annotation-revision\", revision);\n host.textContent = STARTUP_COPY[reason];\n const owned = host;\n return () => {\n if (\n owned === doc.getElementById(ANNOTATION_STARTUP_STATUS_ID) &&\n owned.getAttribute(\"data-annotation-revision\") === revision\n ) {\n owned.remove();\n }\n };\n}\n\nexport function clearAnnotationStartupFailure(doc: Document = document): void {\n doc.getElementById?.(ANNOTATION_STARTUP_STATUS_ID)?.remove();\n}\n", "import {\n ANNOTATION_ARMED_COOKIES,\n ANNOTATION_TOKEN_PARAMS,\n CAPTURE_TOKEN_PARAM,\n READ_TOKEN_PARAM,\n REVIEW_RUN_PARAM,\n clearedCookie,\n isAnnotationArmed,\n type BuildInfo,\n} from \"./protocol\";\nimport {\n clearAnnotationBootstrap,\n CAPTURE_TOKEN_STORAGE_KEY,\n READ_TOKEN_STORAGE_KEY,\n REVIEW_RUN_STORAGE_KEY,\n} from \"./session\";\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\n * `?ai_annotate=<arming>&ai_capture=<signed>&ai_read=<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 four 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 * | read token (`ai_read`) | the console, at the read endpoint | couriers it: keeps it for the tab, hands it to `createAnnotationReader` (A-277) |\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 * Both signed tokens are stripped, not just the capture one. The read token is\n * the weaker of the two \u2014 it cannot file anything \u2014 but it discloses every\n * reviewer's comments on the origin for an hour, which is a thing to keep out of\n * a `Referer` header for the same reasons.\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 AND A SHORT-LIVED COOKIE\n * ===========================================================================\n *\n * The middleware's marker remains the only authority to arm. Accepted tokens\n * stay in per-tab sessionStorage and are also copied into a Secure, SameSite=Lax\n * cookie so a new tab can recover them. A cookie goes with product-origin\n * requests, including assets and APIs; the product must avoid logging it.\n * Both copies expire for UI purposes according to the signed claim's unverified\n * expiry field. Only the console verifies the signature when a capture arrives.\n */\n\n/** Per-tab storage keys. The separate shared cookie is sent with origin requests. */\nexport { CAPTURE_TOKEN_STORAGE_KEY, READ_TOKEN_STORAGE_KEY, REVIEW_RUN_STORAGE_KEY } from \"./session\";\n/**\n * The READ token, per tab (A-277). Same storage and the same rules as the\n * capture token above, because it is the same kind of thing: a signed,\n * short-lived console credential the product couriers and never verifies.\n *\n * It is a SEPARATE key and not a second field on one stored object, so that a\n * tab holding a live capture token and an expired read token is an ordinary\n * state rather than an all-or-nothing one \u2014 their TTLs differ by a factor of\n * four, so that state is the common case rather than the edge.\n */\n/** The subset of `Storage` this needs. `null` disables persistence entirely. */\nexport type StorageLike = Pick<Storage, \"getItem\" | \"setItem\" | \"removeItem\">;\nexport const ANNOTATION_TOKENS_COOKIE = \"ai_annotation_tokens\";\nexport const REARM_BEFORE_EXPIRY_S = 5 * 60;\n\n/** The console's v3 claim ends in issued-at, expiry, signature. This is not verification. */\nexport function tokenExpiry(token: string | null): number | null {\n if (!token) return null;\n const parts = token.split(\".\");\n if (parts[0] !== \"v3\" || (parts.length !== 9 && parts.length !== 10)) return null;\n const value = parts[parts.length - 2];\n if (!value || !/^\\d+$/.test(value)) return null;\n const expiry = Number(value);\n return Number.isSafeInteger(expiry) ? expiry : null;\n}\n\nexport function needsArming(captureToken: string | null, readToken: string | null, nowEpochS = Math.floor(Date.now() / 1000)): boolean {\n const captureExpiry = tokenExpiry(captureToken);\n if (captureExpiry === null || captureExpiry - nowEpochS <= REARM_BEFORE_EXPIRY_S) return true;\n if (readToken !== null) {\n const readExpiry = tokenExpiry(readToken);\n if (readExpiry === null || readExpiry - nowEpochS <= REARM_BEFORE_EXPIRY_S) return true;\n }\n return false;\n}\n\nfunction cookieTokens(cookie: string): { captureToken: string; readToken: string | null; runId: string | null } | null {\n const value = cookie.split(\";\").map((part) => part.trim()).find((part) => part.startsWith(`${ANNOTATION_TOKENS_COOKIE}=`));\n if (!value) return null;\n return safely(() => {\n const parsed: unknown = JSON.parse(decodeURIComponent(value.slice(ANNOTATION_TOKENS_COOKIE.length + 1)));\n if (!parsed || typeof parsed !== \"object\") return null;\n const entry = parsed as Record<string, unknown>;\n if (typeof entry.captureToken !== \"string\" || (entry.readToken !== null && typeof entry.readToken !== \"string\") || (entry.runId !== null && typeof entry.runId !== \"string\")) return null;\n return { captureToken: entry.captureToken, readToken: entry.readToken, runId: entry.runId };\n }, null);\n}\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` to disable per-tab storage;\n * an accepted token can still be recovered from the cross-tab cookie.\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 /**\n * The READ-scoped token, for `createAnnotationReader` (A-277). Never\n * validated here, and never the same string as `captureToken` \u2014 the console\n * refuses each at the other's endpoint.\n *\n * `null` on a link minted before markers shipped, which is ordinary: the\n * toolbar works and the marker layer says it has no credential, rather than\n * the page rendering an empty marker layer with no way to say why.\n */\n readToken: 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. */\nexport const STRIPPED_PARAMS: readonly string[] = [\n ...ANNOTATION_TOKEN_PARAMS,\n CAPTURE_TOKEN_PARAM,\n READ_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 readToken: string | null = null;\n let runId: string | null = null;\n\n if (url !== null) {\n captureToken = url.searchParams.get(CAPTURE_TOKEN_PARAM) || null;\n readToken = url.searchParams.get(READ_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 safely(() => { doc.cookie = `${ANNOTATION_TOKENS_COOKIE}=; Max-Age=0; Secure; SameSite=Lax; Path=/`; }, undefined);\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(READ_TOKEN_STORAGE_KEY), undefined);\n safely(() => storage.removeItem(REVIEW_RUN_STORAGE_KEY), undefined);\n }\n clearAnnotationBootstrap(win, storage, doc);\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: session storage and the shared cookie\n * are per-origin, so one product cannot read another's token. Binding beyond\n * that belongs to the console, which can see product and environment in the\n * claim it signed.\n */\n if (!armed) {\n if (cookieTokens(safely(() => doc.cookie, \"\"))) {\n safely(() => { doc.cookie = `${ANNOTATION_TOKENS_COOKIE}=; Max-Age=0; Secure; SameSite=Lax; Path=/`; }, undefined);\n }\n return { armed: false, captureToken: null, readToken: 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 (readToken !== null) {\n safely(() => storage.setItem(READ_TOKEN_STORAGE_KEY, readToken as string), undefined);\n } else {\n readToken = safely(() => storage.getItem(READ_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 cookies = safely(() => doc.cookie, \"\");\n if (captureToken === null) {\n const shared = cookieTokens(cookies);\n if (shared && !needsArming(shared.captureToken, shared.readToken)) {\n captureToken = shared.captureToken;\n readToken ??= shared.readToken;\n runId ??= shared.runId;\n if (storage) {\n safely(() => storage.setItem(CAPTURE_TOKEN_STORAGE_KEY, captureToken as string), undefined);\n if (readToken) safely(() => storage.setItem(READ_TOKEN_STORAGE_KEY, readToken as string), undefined);\n if (runId) safely(() => storage.setItem(REVIEW_RUN_STORAGE_KEY, runId as string), undefined);\n }\n }\n }\n\n if (captureToken !== null) {\n const expiry = tokenExpiry(captureToken);\n const readExpiry = readToken === null ? null : tokenExpiry(readToken);\n const now = Math.floor(Date.now() / 1000);\n const end = Math.min(expiry ?? 0, readToken === null ? Infinity : readExpiry ?? 0);\n if (end > now) {\n const value = encodeURIComponent(JSON.stringify({ captureToken, readToken, runId }));\n safely(() => { doc.cookie = `${ANNOTATION_TOKENS_COOKIE}=${value}; Max-Age=${end - now}; Secure; SameSite=Lax; Path=/`; }, undefined);\n }\n }\n\n return { armed, captureToken, readToken, 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 { isAnnotatableBuild, STRIPPED_PARAMS } from \"./arming\";\nimport type { BuildInfo } from \"./protocol\";\nimport { ANNOTATION_OPEN_INTENT_STORAGE_KEY } from \"./session\";\n\n/**\n * The in-app shortcut's pre-arm half (A-2384).\n *\n * WHY THIS LIVES IN `./bootstrap` AND NOT IN `mountAnnotation`\n * A host loads the toolbar behind a dynamic `import()` that only an armed page\n * reaches (ADOPTING.md, step 6). On an ordinary staging page there is no\n * marker, so nothing from the index entry point is loaded, and a listener\n * there cannot hear the chord. This one is installed by the small, statically\n * imported entry point on every annotatable page, armed or not.\n *\n * ONE DECISION, MADE BY WHOEVER IS MOUNTED\n * The mounted toolbar's own listener toggles the toolbar when the page is armed\n * with a fresh token, and calls `preventDefault()`. This listener waits for the\n * dispatch to finish and re-arms only if nothing claimed the chord: no toolbar\n * loaded, a toolbar whose token is missing or within five minutes of expiry,\n * or a toolbar the reviewer ended with Done. So the two listeners cannot both\n * act, and freshness is judged in exactly one place.\n *\n * WHAT RE-ARMING IS\n * A top-level navigation to Studio's `/annotate/arm`, which checks the signed-in\n * reviewer, mints the tokens and redirects back to the return path with them in\n * the query; the host's middleware arms on that request and the bootstrap\n * script strips the query. The only thing this side carries is a return path\n * with every annotation parameter removed, plus a credential-free open intent\n * in session storage so the armed mount on return opens the toolbar.\n *\n * PRODUCTION\n * `isAnnotatableBuild` refuses production at install: no listener is added.\n */\n\n/** The default chord: Command or Control, Shift and the full stop. */\nexport const DEFAULT_HOTKEY = \"Mod+Shift+Period\";\n\n/** Exact modifiers and a KeyboardEvent code or key, such as Mod+Shift+Period. */\nexport function matchesHotkey(event: Pick<KeyboardEvent, \"key\" | \"code\" | \"metaKey\" | \"ctrlKey\" | \"shiftKey\" | \"altKey\" | \"repeat\">, chord: string): boolean {\n const parts = chord.split(\"+\");\n const key = parts.pop();\n if (!key || event.repeat || parts.some((part) => ![\"Mod\", \"Meta\", \"Ctrl\", \"Shift\", \"Alt\"].includes(part))) return false;\n const mod = parts.includes(\"Mod\");\n const meta = parts.includes(\"Meta\");\n const ctrl = parts.includes(\"Ctrl\");\n const modifierMatches = mod\n ? (event.metaKey !== event.ctrlKey && !meta && !ctrl)\n : (event.metaKey === meta && event.ctrlKey === ctrl);\n return modifierMatches && event.shiftKey === parts.includes(\"Shift\") && event.altKey === parts.includes(\"Alt\") &&\n (event.code === key || event.key.toLowerCase() === key.toLowerCase() || (key === \"Period\" && event.key === \".\"));\n}\n\nexport interface AnnotationShortcutOptions {\n /** The same build the mount is given. Production installs nothing. */\n build: Pick<BuildInfo, \"env\">;\n /**\n * Studio's arm endpoint for this product's staging target, for example\n * `https://studio.example/annotate/arm?product=<product-id>&target=staging`. Absolute\n * http(s), and carrying no `return`: the shortcut adds it.\n */\n armUrl: string;\n /** The chord. The mount must be given the same one. */\n hotkey: string;\n /** Defaults to the ambient `document`. */\n doc?: Document;\n}\n\n/** The URL the chord navigates to: `armUrl` plus a token-free return path. */\nexport function annotationArmUrl(armUrl: string, current: string): string {\n const here = new URL(current);\n for (const name of STRIPPED_PARAMS) here.searchParams.delete(name);\n const target = new URL(armUrl);\n target.searchParams.set(\"return\", `${here.pathname}${here.search}${here.hash}`);\n return target.href;\n}\n\n/** Install the pre-arm listener. Returns its removal. */\nexport function installAnnotationShortcut(options: AnnotationShortcutOptions): () => void {\n const doc = options.doc ?? document;\n if (!isAnnotatableBuild(options.build)) return () => {};\n const arm = new URL(options.armUrl);\n if ((arm.protocol !== \"https:\" && arm.protocol !== \"http:\") || arm.searchParams.has(\"return\")) {\n throw new Error(\"[annotation] armUrl must be an absolute http(s) URL without a return parameter\");\n }\n const win = doc.defaultView;\n if (!win) return () => {};\n const onKeydown = (event: KeyboardEvent) => {\n if (!matchesHotkey(event, options.hotkey)) return;\n // After this dispatch: a mounted, freshly armed toolbar will have claimed it.\n win.setTimeout(() => {\n if (event.defaultPrevented) return;\n try {\n win.sessionStorage.setItem(ANNOTATION_OPEN_INTENT_STORAGE_KEY, String(Date.now()));\n } catch {\n /* Re-arming still works; the reviewer presses the chord once more. */\n }\n win.location.assign(annotationArmUrl(options.armUrl, win.location.href));\n }, 0);\n };\n doc.addEventListener(\"keydown\", onKeydown, true);\n return () => doc.removeEventListener(\"keydown\", onKeydown, true);\n}\n"],
|
|
5
|
+
"mappings": "AAkYO,IAAMA,EAAyB,cACzBC,EAA0B,oBA0B1BC,EAAsB,aAetBC,EAAmB,SA8BnBC,EAAmB,UAgEzB,SAASC,EAAcC,EAAsB,CAClD,MAAO,GAAGA,CAAI,mFAChB,CAwCO,IAAMC,EAAgC,gBAChCC,EAAiC,sBA2CjCC,EAA0B,CACrCC,EACAH,CACF,EAGaI,EAA2B,CACtCC,EACAJ,CACF,EAUO,SAASK,EAAkBC,EAAyB,CACzD,OAAOA,EACJ,MAAM,GAAG,EACT,KAAMC,GAAU,CACf,IAAMC,EAAUD,EAAM,UAAU,EAChC,OAAOJ,EAAyB,KAAML,GAASU,EAAQ,WAAW,GAAGV,CAAI,GAAG,CAAC,CAC/E,CAAC,CACL,CCznBO,IAAMW,EAA4B,wBAC5BC,EAAyB,qBACzBC,EAAyB,oBAGzBC,EAAiC,wBACjCC,EAA4B,oCAC5BC,EAA+B,+BAGrC,SAASC,EACdC,EACAC,EACAC,EACM,CACN,GAAIF,IAAQ,KACV,GAAI,CACF,OAAQA,EAA2CH,CAAyB,CAC9E,MAAQ,CAER,CAEF,GAAII,IAAY,KACd,GAAI,CACFA,EAAQ,WAAWL,CAA8B,CACnD,MAAQ,CAER,CAEFM,GAAK,iBAAiBJ,CAA4B,GAAG,OAAO,CAC9D,CAOO,IAAMK,EAAqC,qBACrCC,GAAqB,IAAU,ICG5C,IAAMC,EAA0B,CAC9B,YAAaC,EACb,aAAcC,EACd,aAAcC,EACd,UAAWC,EACX,SAAUC,EACV,WAAYC,EACZ,QAASC,EACT,OAAQC,EACR,WAAYC,EACZ,cAAeC,CACjB,EASMC,EACJ,uvCA8BK,SAASC,GAAoC,CAClD,IAAMC,EAAS,KAAK,UAAUb,CAAM,EAAE,WAAW,IAAK,SAAS,EAC/D,MAAO,IAAIW,CAAuB,oBAAoBE,CAAM,IAC9D,CAEA,SAASC,EAAaC,EAA4C,CAChE,GAAIA,IAAU,MAAQ,OAAOA,GAAU,SAAU,MAAO,GACxD,IAAMC,EAAUD,EAChB,OACGC,EAAQ,SAAW,YAAcA,EAAQ,SAAW,cACpDA,EAAQ,UAAY,WAAaA,EAAQ,UAAY,UAE1D,CAQO,SAASC,EACdC,EAA2C,CAAC,EAClB,CAC1B,IAAMC,EAAMD,EAAQ,KAAO,OACrBE,EAAUF,EAAQ,UAAY,QAC/B,IAAM,CACL,GAAI,CACF,OAAOC,EAAI,cACb,MAAQ,CACN,OAAO,IACT,CACF,GAAG,EACHD,EAAQ,QAERF,EAAmB,KACvB,GAAI,CACFA,EAAWG,EAA2CT,CAAyB,GAAK,KACpF,OAAQS,EAA2CT,CAAyB,CAC9E,MAAQ,CAER,CACA,GAAI,CAACI,EAAaE,CAAO,GAAKI,IAAY,KACxC,GAAI,CACF,IAAMC,EAAMD,EAAQ,QAAQX,CAA8B,EAC1DO,EAAUK,IAAQ,KAAO,KAAO,KAAK,MAAMA,CAAG,CAChD,MAAQ,CACNL,EAAU,IACZ,CAEF,GAAII,IAAY,KACd,GAAI,CACFA,EAAQ,WAAWX,CAA8B,CACnD,MAAQ,CAER,CAEF,OAAOK,EAAaE,CAAO,EACvB,CAAE,OAAQA,EAAQ,OAAQ,QAASA,EAAQ,OAAQ,EACnD,IACN,CAEA,IAAMM,EAAmE,CACvE,kBACE,yGACF,kBACE,kGACF,gBAAiB,0EACjB,eAAgB,0EAClB,EAUO,SAASC,EACdC,EACAN,EAA2C,CAAC,EAChC,CACZ,IAAMO,EAAMP,EAAQ,KAAO,SACvBQ,EAAOD,EAAI,eAAeE,CAA4B,EACtDD,IAAS,OACXA,EAAOD,EAAI,cAAc,KAAK,EAC9BC,EAAK,GAAKC,EACVD,EAAK,aAAa,OAAQ,OAAO,EACjCA,EAAK,aACH,QACA,0bAOF,GACCD,EAAI,MAAQA,EAAI,iBAAiB,OAAOC,CAAI,GAE/C,IAAME,EAAW,OAAO,OAAOF,EAAK,aAAa,0BAA0B,GAAK,GAAG,EAAI,CAAC,EACxFA,EAAK,aAAa,2BAA4BE,CAAQ,EACtDF,EAAK,YAAcJ,EAAaE,CAAM,EACtC,IAAMK,EAAQH,EACd,MAAO,IAAM,CAETG,IAAUJ,EAAI,eAAeE,CAA4B,GACzDE,EAAM,aAAa,0BAA0B,IAAMD,GAEnDC,EAAM,OAAO,CAEjB,CACF,CAEO,SAASC,EAA8BL,EAAgB,SAAgB,CAC5EA,EAAI,iBAAiBE,CAA4B,GAAG,OAAO,CAC7D,CCvEO,IAAMI,EAA2B,uBAC3BC,EAAwB,IAG9B,SAASC,EAAYC,EAAqC,CAC/D,GAAI,CAACA,EAAO,OAAO,KACnB,IAAMC,EAAQD,EAAM,MAAM,GAAG,EAC7B,GAAIC,EAAM,CAAC,IAAM,MAASA,EAAM,SAAW,GAAKA,EAAM,SAAW,GAAK,OAAO,KAC7E,IAAMC,EAAQD,EAAMA,EAAM,OAAS,CAAC,EACpC,GAAI,CAACC,GAAS,CAAC,QAAQ,KAAKA,CAAK,EAAG,OAAO,KAC3C,IAAMC,EAAS,OAAOD,CAAK,EAC3B,OAAO,OAAO,cAAcC,CAAM,EAAIA,EAAS,IACjD,CAEO,SAASC,EAAYC,EAA6BC,EAA0BC,EAAY,KAAK,MAAM,KAAK,IAAI,EAAI,GAAI,EAAY,CACrI,IAAMC,EAAgBT,EAAYM,CAAY,EAC9C,GAAIG,IAAkB,MAAQA,EAAgBD,GAAaT,EAAuB,MAAO,GACzF,GAAIQ,IAAc,KAAM,CACtB,IAAMG,EAAaV,EAAYO,CAAS,EACxC,GAAIG,IAAe,MAAQA,EAAaF,GAAaT,EAAuB,MAAO,EACrF,CACA,MAAO,EACT,CAEA,SAASY,EAAaC,EAAiG,CACrH,IAAMT,EAAQS,EAAO,MAAM,GAAG,EAAE,IAAKC,GAASA,EAAK,KAAK,CAAC,EAAE,KAAMA,GAASA,EAAK,WAAW,GAAGf,CAAwB,GAAG,CAAC,EACzH,OAAKK,EACEW,EAAO,IAAM,CAClB,IAAMC,EAAkB,KAAK,MAAM,mBAAmBZ,EAAM,MAAML,EAAyB,OAAS,CAAC,CAAC,CAAC,EACvG,GAAI,CAACiB,GAAU,OAAOA,GAAW,SAAU,OAAO,KAClD,IAAMC,EAAQD,EACd,OAAI,OAAOC,EAAM,cAAiB,UAAaA,EAAM,YAAc,MAAQ,OAAOA,EAAM,WAAc,UAAcA,EAAM,QAAU,MAAQ,OAAOA,EAAM,OAAU,SAAkB,KAC9K,CAAE,aAAcA,EAAM,aAAc,UAAWA,EAAM,UAAW,MAAOA,EAAM,KAAM,CAC5F,EAAG,IAAI,EAPY,IAQrB,CAsCO,IAAMC,EAAqC,CAChD,GAAGC,EACHC,EACAC,EACAC,CACF,EAQA,SAASP,EAAUQ,EAAcC,EAAgB,CAC/C,GAAI,CACF,OAAOD,EAAI,CACb,MAAQ,CACN,OAAOC,CACT,CACF,CAEA,SAASC,GAAeC,EAAwC,CAC9D,OAAOX,EAAO,IAAMW,GAAK,gBAAkB,KAAM,IAAI,CACvD,CASO,SAASC,GAAWC,EAAyB,CAAC,EAAW,CAC9D,IAAMC,EAAMD,EAAQ,KAAO,SACrBF,EAAMG,EAAI,aAAe,KACzBC,EAAUF,EAAQ,UAAY,OAAYH,GAAeC,CAAG,EAAIE,EAAQ,QAExEG,EAAOH,EAAQ,KAAOC,EAAI,IAC1BG,EAAMjB,EAAO,IAAM,IAAI,IAAIgB,CAAI,EAAG,IAAI,EAExCxB,EAA8B,KAC9BC,EAA2B,KAC3ByB,EAAuB,KAE3B,GAAID,IAAQ,OACVzB,EAAeyB,EAAI,aAAa,IAAIZ,CAAmB,GAAK,KAC5DZ,EAAYwB,EAAI,aAAa,IAAIX,CAAgB,GAAK,KACtDY,EAAQD,EAAI,aAAa,IAAIV,CAAgB,GAAK,KAElCJ,EAAgB,KAAMgB,GAASF,EAAI,aAAa,IAAIE,CAAI,CAAC,GAC5D,CACX,QAAWA,KAAQhB,EAAiBc,EAAI,aAAa,OAAOE,CAAI,EAYhE,IAAMC,EAAUP,EAAQ,SAAWF,GAAK,QACxCX,EAAO,IAAM,CACXoB,GAAS,aAAaT,GAAK,SAAS,OAAS,KAAM,GAAIM,EAAI,SAAS,CAAC,CACvE,EAAG,MAAS,CACd,CASF,IAAMI,EAAS,IAAM,CACnBrB,EAAO,IAAM,CAAEc,EAAI,OAAS,GAAG9B,CAAwB,4CAA8C,EAAG,MAAS,EACjH,QAAWmC,KAAQG,EACjBtB,EAAO,IAAM,CACXc,EAAI,OAASS,EAAcJ,CAAI,CACjC,EAAG,MAAS,EAEVJ,IACFf,EAAO,IAAMe,EAAQ,WAAWS,CAAyB,EAAG,MAAS,EACrExB,EAAO,IAAMe,EAAQ,WAAWU,CAAsB,EAAG,MAAS,EAClEzB,EAAO,IAAMe,EAAQ,WAAWW,CAAsB,EAAG,MAAS,GAEpEC,EAAyBhB,EAAKI,EAASD,CAAG,CAC5C,EAMMc,EAAQC,EAAkB7B,EAAO,IAAMc,EAAI,OAAQ,EAAE,CAAC,EAoC5D,GAAI,CAACc,EACH,OAAI/B,EAAaG,EAAO,IAAMc,EAAI,OAAQ,EAAE,CAAC,GAC3Cd,EAAO,IAAM,CAAEc,EAAI,OAAS,GAAG9B,CAAwB,4CAA8C,EAAG,MAAS,EAE5G,CAAE,MAAO,GAAO,aAAc,KAAM,UAAW,KAAM,MAAO,KAAM,OAAAqC,CAAO,EAG9EN,IACEvB,IAAiB,KACnBQ,EAAO,IAAMe,EAAQ,QAAQS,EAA2BhC,CAAsB,EAAG,MAAS,EAE1FA,EAAeQ,EAAO,IAAMe,EAAQ,QAAQS,CAAyB,EAAG,IAAI,EAE1E/B,IAAc,KAChBO,EAAO,IAAMe,EAAQ,QAAQU,EAAwBhC,CAAmB,EAAG,MAAS,EAEpFA,EAAYO,EAAO,IAAMe,EAAQ,QAAQU,CAAsB,EAAG,IAAI,EAEpEP,IAAU,KACZlB,EAAO,IAAMe,EAAQ,QAAQW,EAAwBR,CAAe,EAAG,MAAS,EAEhFA,EAAQlB,EAAO,IAAMe,EAAQ,QAAQW,CAAsB,EAAG,IAAI,GAItE,IAAMI,EAAU9B,EAAO,IAAMc,EAAI,OAAQ,EAAE,EAC3C,GAAItB,IAAiB,KAAM,CACzB,IAAMuC,EAASlC,EAAaiC,CAAO,EAC/BC,GAAU,CAACxC,EAAYwC,EAAO,aAAcA,EAAO,SAAS,IAC9DvC,EAAeuC,EAAO,aACtBtC,MAAcsC,EAAO,WACrBb,MAAUa,EAAO,OACbhB,IACFf,EAAO,IAAMe,EAAQ,QAAQS,EAA2BhC,CAAsB,EAAG,MAAS,EACtFC,GAAWO,EAAO,IAAMe,EAAQ,QAAQU,EAAwBhC,CAAmB,EAAG,MAAS,EAC/FyB,GAAOlB,EAAO,IAAMe,EAAQ,QAAQW,EAAwBR,CAAe,EAAG,MAAS,GAGjG,CAEA,GAAI1B,IAAiB,KAAM,CACzB,IAAMF,EAASJ,EAAYM,CAAY,EACjCI,EAAaH,IAAc,KAAO,KAAOP,EAAYO,CAAS,EAC9DuC,EAAM,KAAK,MAAM,KAAK,IAAI,EAAI,GAAI,EAClCC,EAAM,KAAK,IAAI3C,GAAU,EAAGG,IAAc,KAAO,IAAWG,GAAc,CAAC,EACjF,GAAIqC,EAAMD,EAAK,CACb,IAAM3C,EAAQ,mBAAmB,KAAK,UAAU,CAAE,aAAAG,EAAc,UAAAC,EAAW,MAAAyB,CAAM,CAAC,CAAC,EACnFlB,EAAO,IAAM,CAAEc,EAAI,OAAS,GAAG9B,CAAwB,IAAIK,CAAK,aAAa4C,EAAMD,CAAG,gCAAkC,EAAG,MAAS,CACtI,CACF,CAEA,MAAO,CAAE,MAAAJ,EAAO,aAAApC,EAAc,UAAAC,EAAW,MAAAyB,EAAO,OAAAG,CAAO,CACzD,CAwDA,IAAMa,GAAgD,CAAC,UAAW,UAAW,aAAa,EAEnF,SAASC,EAAmBC,EAAwC,CACzE,OAAOF,GAAiB,SAASE,EAAM,GAAG,CAC5C,CClaO,IAAMC,GAAiB,mBAGvB,SAASC,EAAcC,EAAuGC,EAAwB,CAC3J,IAAMC,EAAQD,EAAM,MAAM,GAAG,EACvBE,EAAMD,EAAM,IAAI,EACtB,GAAI,CAACC,GAAOH,EAAM,QAAUE,EAAM,KAAME,GAAS,CAAC,CAAC,MAAO,OAAQ,OAAQ,QAAS,KAAK,EAAE,SAASA,CAAI,CAAC,EAAG,MAAO,GAClH,IAAMC,EAAMH,EAAM,SAAS,KAAK,EAC1BI,EAAOJ,EAAM,SAAS,MAAM,EAC5BK,EAAOL,EAAM,SAAS,MAAM,EAIlC,OAHwBG,EACnBL,EAAM,UAAYA,EAAM,SAAW,CAACM,GAAQ,CAACC,EAC7CP,EAAM,UAAYM,GAAQN,EAAM,UAAYO,IACvBP,EAAM,WAAaE,EAAM,SAAS,OAAO,GAAKF,EAAM,SAAWE,EAAM,SAAS,KAAK,IAC1GF,EAAM,OAASG,GAAOH,EAAM,IAAI,YAAY,IAAMG,EAAI,YAAY,GAAMA,IAAQ,UAAYH,EAAM,MAAQ,IAC/G,CAkBO,SAASQ,EAAiBC,EAAgBC,EAAyB,CACxE,IAAMC,EAAO,IAAI,IAAID,CAAO,EAC5B,QAAWE,KAAQC,EAAiBF,EAAK,aAAa,OAAOC,CAAI,EACjE,IAAME,EAAS,IAAI,IAAIL,CAAM,EAC7B,OAAAK,EAAO,aAAa,IAAI,SAAU,GAAGH,EAAK,QAAQ,GAAGA,EAAK,MAAM,GAAGA,EAAK,IAAI,EAAE,EACvEG,EAAO,IAChB,CAGO,SAASC,GAA0BC,EAAgD,CACxF,IAAMC,EAAMD,EAAQ,KAAO,SAC3B,GAAI,CAACE,EAAmBF,EAAQ,KAAK,EAAG,MAAO,IAAM,CAAC,EACtD,IAAMG,EAAM,IAAI,IAAIH,EAAQ,MAAM,EAClC,GAAKG,EAAI,WAAa,UAAYA,EAAI,WAAa,SAAYA,EAAI,aAAa,IAAI,QAAQ,EAC1F,MAAM,IAAI,MAAM,gFAAgF,EAElG,IAAMC,EAAMH,EAAI,YAChB,GAAI,CAACG,EAAK,MAAO,IAAM,CAAC,EACxB,IAAMC,EAAarB,GAAyB,CACrCD,EAAcC,EAAOgB,EAAQ,MAAM,GAExCI,EAAI,WAAW,IAAM,CACnB,GAAI,CAAApB,EAAM,iBACV,IAAI,CACFoB,EAAI,eAAe,QAAQE,EAAoC,OAAO,KAAK,IAAI,CAAC,CAAC,CACnF,MAAQ,CAER,CACAF,EAAI,SAAS,OAAOZ,EAAiBQ,EAAQ,OAAQI,EAAI,SAAS,IAAI,CAAC,EACzE,EAAG,CAAC,CACN,EACA,OAAAH,EAAI,iBAAiB,UAAWI,EAAW,EAAI,EACxC,IAAMJ,EAAI,oBAAoB,UAAWI,EAAW,EAAI,CACjE",
|
|
6
|
+
"names": ["ANNOTATION_TOKEN_PARAM", "ANNOTATION_ARMED_COOKIE", "CAPTURE_TOKEN_PARAM", "REVIEW_RUN_PARAM", "READ_TOKEN_PARAM", "clearedCookie", "name", "LEGACY_ANNOTATION_TOKEN_PARAM", "LEGACY_ANNOTATION_ARMED_COOKIE", "ANNOTATION_TOKEN_PARAMS", "ANNOTATION_TOKEN_PARAM", "ANNOTATION_ARMED_COOKIES", "ANNOTATION_ARMED_COOKIE", "isAnnotationArmed", "cookie", "entry", "trimmed", "CAPTURE_TOKEN_STORAGE_KEY", "READ_TOKEN_STORAGE_KEY", "REVIEW_RUN_STORAGE_KEY", "ANNOTATION_ATTEMPT_STORAGE_KEY", "ANNOTATION_ATTEMPT_GLOBAL", "ANNOTATION_STARTUP_STATUS_ID", "clearAnnotationBootstrap", "win", "storage", "doc", "ANNOTATION_OPEN_INTENT_STORAGE_KEY", "OPEN_INTENT_TTL_MS", "CONFIG", "ANNOTATION_TOKEN_PARAMS", "ANNOTATION_ARMED_COOKIES", "CAPTURE_TOKEN_PARAM", "READ_TOKEN_PARAM", "REVIEW_RUN_PARAM", "CAPTURE_TOKEN_STORAGE_KEY", "READ_TOKEN_STORAGE_KEY", "REVIEW_RUN_STORAGE_KEY", "ANNOTATION_ATTEMPT_STORAGE_KEY", "ANNOTATION_ATTEMPT_GLOBAL", "INLINE_BOOTSTRAP_SOURCE", "annotationBootstrapScript", "config", "validAttempt", "value", "attempt", "consumeAnnotationAttempt", "options", "win", "storage", "raw", "STARTUP_COPY", "reportAnnotationStartupFailure", "reason", "doc", "host", "ANNOTATION_STARTUP_STATUS_ID", "revision", "owned", "clearAnnotationStartupFailure", "ANNOTATION_TOKENS_COOKIE", "REARM_BEFORE_EXPIRY_S", "tokenExpiry", "token", "parts", "value", "expiry", "needsArming", "captureToken", "readToken", "nowEpochS", "captureExpiry", "readExpiry", "cookieTokens", "cookie", "part", "safely", "parsed", "entry", "STRIPPED_PARAMS", "ANNOTATION_TOKEN_PARAMS", "CAPTURE_TOKEN_PARAM", "READ_TOKEN_PARAM", "REVIEW_RUN_PARAM", "run", "fallback", "defaultStorage", "win", "readArming", "options", "doc", "storage", "href", "url", "runId", "name", "history", "disarm", "ANNOTATION_ARMED_COOKIES", "clearedCookie", "CAPTURE_TOKEN_STORAGE_KEY", "READ_TOKEN_STORAGE_KEY", "REVIEW_RUN_STORAGE_KEY", "clearAnnotationBootstrap", "armed", "isAnnotationArmed", "cookies", "shared", "now", "end", "ANNOTATABLE_ENVS", "isAnnotatableBuild", "build", "DEFAULT_HOTKEY", "matchesHotkey", "event", "chord", "parts", "key", "part", "mod", "meta", "ctrl", "annotationArmUrl", "armUrl", "current", "here", "name", "STRIPPED_PARAMS", "target", "installAnnotationShortcut", "options", "doc", "isAnnotatableBuild", "arm", "win", "onKeydown", "ANNOTATION_OPEN_INTENT_STORAGE_KEY"]
|
|
7
|
+
}
|
package/dist/host.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var i=Object.defineProperty;var
|
|
1
|
+
"use strict";var i=Object.defineProperty;var l=Object.getOwnPropertyDescriptor;var g=Object.getOwnPropertyNames;var f=Object.prototype.hasOwnProperty;var m=(e,t)=>{for(var o in t)i(e,o,{get:t[o],enumerable:!0})},A=(e,t,o,a)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of g(t))!f.call(e,r)&&r!==o&&i(e,r,{get:()=>t[r],enumerable:!(a=l(t,r))||a.enumerable});return e};var y=e=>A(i({},"__esModule",{value:!0}),e);var O={};m(O,{createAnnotationHost:()=>b,isFrameMessage:()=>d});module.exports=y(O);function T(e){try{return JSON.stringify(e)??String(e)}catch{return"[unserialisable]"}}function u(e,t){console.error(`[annotation] ${e}: protocol drift, nothing was acted on: ${T(t)}`)}function c(e){if(!e||e==="*")throw new Error("[annotation] a concrete parent origin is required; '*' would broadcast annotation payloads to any embedder.");return e}var x=new Set(["annotate:ready","annotate:hover","annotate:capture","annotate:cancel"]);function d(e){if(typeof e!="object"||e===null)return!1;let t=e.type;return typeof t=="string"&&x.has(t)}function b(e){let t=c(e.frameOrigin),o=!1,a=!1,r=n=>{a||e.frame.postMessage(n,t)},s=n=>{if(n.origin===t&&n.source===e.frame&&d(n.data))switch(n.data.type){case"annotate:ready":o=!0,e.onReady({sdkVersion:n.data.sdkVersion,build:n.data.build});return;case"annotate:hover":e.onHover(n.data.target);return;case"annotate:capture":e.onCapture(n.data.payload);return;case"annotate:cancel":e.onCancel();return;default:{let p=n.data;u("host",p);return}}};return e.hostWindow.addEventListener("message",s),{enable:()=>r({type:"annotate:enable"}),disable:()=>r({type:"annotate:disable"}),ping:()=>r({type:"annotate:ping"}),isReady:()=>o,destroy(){a=!0,o=!1,e.hostWindow.removeEventListener("message",s)}}}
|
|
2
2
|
//# sourceMappingURL=host.cjs.map
|
package/dist/host.cjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/host.ts", "../src/protocol.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n assertConcreteOrigin,\n type AnnotationPayload,\n type BuildInfo,\n type FrameToParent,\n type ParentToFrame,\n type TargetInfo,\n} from \"./protocol\";\n\n/**\n * The parent half of the annotation channel (A-30).\n *\n * WHY THIS LIVES IN THE SDK AND NOT IN `apps/admin`.\n *\n * The protocol is this package's domain. It already owns the message union,\n * `isParentMessage` and `assertConcreteOrigin`; a second implementation of the\n * same handshake, written host-side because that is where the UI is, is exactly\n * the shape SHARED-PACKAGES.md warns about. The two halves of a protocol\n * drifting apart is not a hypothetical \u2014 it is the normal outcome when they are\n * maintained in different packages by different changes.\n *\n * Concretely: the frame's `sdk.ts` and this file are the only two things that\n * can disagree about what `annotate:capture` means, and keeping them in one\n * package means a change to the union is a compile error in both.\n *\n * Admin gets the UI. This gets the wire.\n */\n\nexport interface AnnotationHostOptions {\n /**\n * The window being framed. Passed in rather than looked up, because the host\n * knows which iframe it means and this module must never guess.\n */\n frame: Window;\n /**\n * The frame's origin \u2014 where the product is served from. Required and\n * concrete, checked by `assertConcreteOrigin`: posting to `*` would hand an\n * annotation payload, including console errors from a real session, to\n * whatever happens to be loaded.\n */\n frameOrigin: string;\n /** The window that receives messages. Injected so tests need no real iframe. */\n hostWindow: Window;\n\n onReady(info: { sdkVersion: string; build: BuildInfo }): void;\n onHover(target: TargetInfo): void;\n onCapture(payload: AnnotationPayload): void;\n onCancel(): void;\n}\n\nexport interface AnnotationHost {\n enable(): void;\n disable(): void;\n ping(): void;\n /** True once the frame has announced itself. */\n isReady(): boolean;\n destroy(): void;\n}\n\nconst FRAME_TYPES = new Set([\n \"annotate:ready\",\n \"annotate:hover\",\n \"annotate:capture\",\n \"annotate:cancel\",\n]);\n\n/**\n * Is this one of ours?\n *\n * The mirror of `isParentMessage`, and it carries the same warning: origin is\n * checked separately and FIRST. A well-formed message from the wrong origin is\n * precisely the case this guard must not be trusted to catch \u2014 anything can\n * post `{type: \"annotate:capture\"}` to a window.\n */\nexport function isFrameMessage(data: unknown): data is FrameToParent {\n if (typeof data !== \"object\" || data === null) return false;\n const type = (data as { type?: unknown }).type;\n return typeof type === \"string\" && FRAME_TYPES.has(type);\n}\n\nexport function createAnnotationHost(options: AnnotationHostOptions): AnnotationHost {\n const frameOrigin = assertConcreteOrigin(options.frameOrigin);\n let ready = false;\n let destroyed = false;\n\n const post = (message: ParentToFrame) => {\n if (destroyed) return;\n // Targeted, never `*`. `enable` is not sensitive in itself, but a habit of\n // posting to `*` is what eventually posts something that is.\n options.frame.postMessage(message, frameOrigin);\n };\n\n const onMessage = (event: MessageEvent) => {\n /**\n * Origin first, and by exact match.\n *\n * Not `endsWith`, not a prefix: for a product served from `example.com`,\n * `https://evil-example.com` ends with nothing suspicious and\n * `https://example.com.evil.test` starts with the right characters. The browser hands us a normalised origin, so an\n * equality test is both correct and the only one that cannot be widened by\n * a hostile hostname.\n */\n if (event.origin !== frameOrigin) return;\n\n /**\n * And the source must be the frame we are talking to.\n *\n * Origin alone is not enough: any other window on the same origin \u2014 a popup\n * the product opened, another iframe \u2014 can post a well-formed message from\n * an origin that matches. This is the check that makes \"the frame said so\"\n * mean the frame.\n */\n if (event.source !== options.frame) return;\n\n if (!isFrameMessage(event.data)) return;\n\n switch (event.data.type) {\n case \"annotate:ready\":\n ready = true;\n options.onReady({ sdkVersion: event.data.sdkVersion, build: event.data.build });\n return;\n case \"annotate:hover\":\n options.onHover(event.data.target);\n return;\n case \"annotate:capture\":\n options.onCapture(event.data.payload);\n return;\n case \"annotate:cancel\":\n options.onCancel();\n return;\n }\n };\n\n options.hostWindow.addEventListener(\"message\", onMessage);\n\n return {\n enable: () => post({ type: \"annotate:enable\" }),\n disable: () => post({ type: \"annotate:disable\" }),\n ping: () => post({ type: \"annotate:ping\" }),\n isReady: () => ready,\n destroy() {\n destroyed = true;\n ready = false;\n options.hostWindow.removeEventListener(\"message\", onMessage);\n },\n };\n}\n", "/**\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 READ token's query parameter (A-277).\n *\n * ===========================================================================\n * WHY THE MARKER LAYER GETS ITS OWN CREDENTIAL AND NOT `ai_capture`\n * ===========================================================================\n *\n * Fourth name on the same wire, and the reason it is a fourth is A-276's\n * disclosure decision rather than tidiness. The capture token is minted for\n * WRITING and lives four hours so a reviewer can work through a session; reusing\n * it for the read would make one leaked deep link a write capability AND a read\n * capability over every reviewer's comments on that origin for the same window.\n * The scope is a signed field, `verifyCaptureToken` requires an expectation at\n * every call site, and the read endpoint refuses a write claim \u2014 so handing\n * `ai_capture` to the reader does not degrade to a read, it fails.\n *\n * The read claim's TTL is ONE hour rather than four (`READ_TOKEN_TTL_S`), which\n * is the practical consequence a reader of this constant should know: the\n * toolbar outlives the markers, so a reviewer who leaves a tab open all\n * afternoon will find the count line saying it cannot read before they find the\n * Send button broken. That is the intended ordering \u2014 the shorter credential is\n * the one that discloses other people's words.\n *\n * Exported here for the reason the three above are: the console writes it into a\n * deep link and the product's bundle reads it, from two repositories, and a\n * mismatch produces no error anywhere \u2014 just a page with no markers, which is\n * indistinguishable from nine other causes.\n */\nexport const READ_TOKEN_PARAM = \"ai_read\";\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"],
|
|
5
|
-
"mappings": "yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,0BAAAE,EAAA,mBAAAC,IAAA,eAAAC,EAAAJ,
|
|
6
|
-
"names": ["host_exports", "__export", "createAnnotationHost", "isFrameMessage", "__toCommonJS", "assertConcreteOrigin", "origin", "FRAME_TYPES", "isFrameMessage", "data", "type", "createAnnotationHost", "options", "frameOrigin", "assertConcreteOrigin", "ready", "destroyed", "post", "message", "onMessage", "event"]
|
|
4
|
+
"sourcesContent": ["import {\n assertConcreteOrigin,\n type AnnotationPayload,\n type BuildInfo,\n type FrameToParent,\n type ParentToFrame,\n type TargetInfo,\n unhandledMessage,\n} from \"./protocol\";\n\n/**\n * The parent half of the annotation channel (A-30).\n *\n * WHY THIS LIVES IN THE SDK AND NOT IN `apps/admin`.\n *\n * The protocol is this package's domain. It already owns the message union,\n * `isParentMessage` and `assertConcreteOrigin`; a second implementation of the\n * same handshake, written host-side because that is where the UI is, is exactly\n * the shape SHARED-PACKAGES.md warns about. The two halves of a protocol\n * drifting apart is not a hypothetical \u2014 it is the normal outcome when they are\n * maintained in different packages by different changes.\n *\n * Concretely: the frame's `sdk.ts` and this file are the only two things that\n * can disagree about what `annotate:capture` means, and keeping them in one\n * package means a change to the union is a compile error in both.\n *\n * Admin gets the UI. This gets the wire.\n */\n\nexport interface AnnotationHostOptions {\n /**\n * The window being framed. Passed in rather than looked up, because the host\n * knows which iframe it means and this module must never guess.\n */\n frame: Window;\n /**\n * The frame's origin \u2014 where the product is served from. Required and\n * concrete, checked by `assertConcreteOrigin`: posting to `*` would hand an\n * annotation payload, including console errors from a real session, to\n * whatever happens to be loaded.\n */\n frameOrigin: string;\n /** The window that receives messages. Injected so tests need no real iframe. */\n hostWindow: Window;\n\n onReady(info: { sdkVersion: string; build: BuildInfo }): void;\n onHover(target: TargetInfo): void;\n onCapture(payload: AnnotationPayload): void;\n onCancel(): void;\n}\n\nexport interface AnnotationHost {\n enable(): void;\n disable(): void;\n ping(): void;\n /** True once the frame has announced itself. */\n isReady(): boolean;\n destroy(): void;\n}\n\nconst FRAME_TYPES = new Set([\n \"annotate:ready\",\n \"annotate:hover\",\n \"annotate:capture\",\n \"annotate:cancel\",\n]);\n\n/**\n * Is this one of ours?\n *\n * The mirror of `isParentMessage`, and it carries the same warning: origin is\n * checked separately and FIRST. A well-formed message from the wrong origin is\n * precisely the case this guard must not be trusted to catch \u2014 anything can\n * post `{type: \"annotate:capture\"}` to a window.\n */\nexport function isFrameMessage(data: unknown): data is FrameToParent {\n if (typeof data !== \"object\" || data === null) return false;\n const type = (data as { type?: unknown }).type;\n return typeof type === \"string\" && FRAME_TYPES.has(type);\n}\n\nexport function createAnnotationHost(options: AnnotationHostOptions): AnnotationHost {\n const frameOrigin = assertConcreteOrigin(options.frameOrigin);\n let ready = false;\n let destroyed = false;\n\n const post = (message: ParentToFrame) => {\n if (destroyed) return;\n // Targeted, never `*`. `enable` is not sensitive in itself, but a habit of\n // posting to `*` is what eventually posts something that is.\n options.frame.postMessage(message, frameOrigin);\n };\n\n const onMessage = (event: MessageEvent) => {\n /**\n * Origin first, and by exact match.\n *\n * Not `endsWith`, not a prefix: for a product served from `example.com`,\n * `https://evil-example.com` ends with nothing suspicious and\n * `https://example.com.evil.test` starts with the right characters. The browser hands us a normalised origin, so an\n * equality test is both correct and the only one that cannot be widened by\n * a hostile hostname.\n */\n if (event.origin !== frameOrigin) return;\n\n /**\n * And the source must be the frame we are talking to.\n *\n * Origin alone is not enough: any other window on the same origin \u2014 a popup\n * the product opened, another iframe \u2014 can post a well-formed message from\n * an origin that matches. This is the check that makes \"the frame said so\"\n * mean the frame.\n */\n if (event.source !== options.frame) return;\n\n if (!isFrameMessage(event.data)) return;\n\n switch (event.data.type) {\n case \"annotate:ready\":\n ready = true;\n options.onReady({ sdkVersion: event.data.sdkVersion, build: event.data.build });\n return;\n case \"annotate:hover\":\n options.onHover(event.data.target);\n return;\n case \"annotate:capture\":\n options.onCapture(event.data.payload);\n return;\n case \"annotate:cancel\":\n options.onCancel();\n return;\n default: {\n /**\n * A-359. `const unhandled: never` is the compile-time half \u2014 widen\n * `FrameToParent` and this line is the error, naming this file. The\n * call is the runtime half, and it deliberately does not throw; the\n * reasoning is on `unhandledMessage` in `protocol.ts`, and it is about\n * this being a `postMessage` boundary rather than an internal call.\n *\n * Note what is NOT claimed: this cannot see a message type added to\n * `FRAME_TYPES` above and to no union, because such a message is not a\n * `FrameToParent` in the first place. The set and the union are two\n * lists that must agree, and `exhaustive.test.ts` is what holds them\n * to it.\n */\n const unhandled: never = event.data;\n unhandledMessage(\"host\", unhandled);\n return;\n }\n }\n };\n\n options.hostWindow.addEventListener(\"message\", onMessage);\n\n return {\n enable: () => post({ type: \"annotate:enable\" }),\n disable: () => post({ type: \"annotate:disable\" }),\n ping: () => post({ type: \"annotate:ping\" }),\n isReady: () => ready,\n destroy() {\n destroyed = true;\n ready = false;\n options.hostWindow.removeEventListener(\"message\", onMessage);\n },\n };\n}\n", "/**\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 * A hosted document declares first-party annotation support with this exact\n * response header. It describes the BUILD, never a token, cookie or attempt.\n */\nexport const ANNOTATION_MODE_HEADER = \"x-weatherboard-annotation-mode\";\nexport const FIRST_PARTY_ANNOTATION_MODE = \"first-party\";\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 /**\n * `w`/`h` and `canvases` arrive with the in-page viewport capture (A-2385);\n * `canvases` counts the canvases in view and how many read back blank or\n * unreadable, so a black rectangle in the picture is explained, not silent.\n */\n screenshot?: {\n dataUrl: string;\n region: \"element\" | \"viewport\";\n w?: number;\n h?: number;\n canvases?: { total: number; blank: number; unreadable: number };\n };\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\n/**\n * A message that got past both guards and matched no `case` (A-359).\n *\n * ===========================================================================\n * THE DEFECT\n * ===========================================================================\n *\n * `host.ts` and `sdk.ts` hold the two halves of this protocol, one `switch`\n * each, and neither had a `default`. Measured before the fix: adding\n * `| { type: \"annotate:probe\" }` to `ParentToFrame` and\n * `| { type: \"annotate:pong\" }` to `FrameToParent`, then running\n * `tsc --noEmit` in this package, gave exit 0 and zero errors. A message type\n * added to the union is dropped on the floor by whichever half was not\n * updated, and the two halves are the only things in the world that can\n * disagree about what a message means.\n *\n * ===========================================================================\n * WHY IT DOES NOT THROW, WHERE THE MANAGER'S TWO EQUIVALENTS DO\n * ===========================================================================\n *\n * This is the house pattern with one deliberate departure, and the departure is\n * the boundary rather than taste.\n *\n * Both switches run inside a `message` listener, reached from `postMessage`.\n * The host's listener runs in the management console over a frame that may be\n * serving anything; the SDK's runs in a product page we do not own. A throw\n * there does not propagate to a caller \u2014 there is no caller \u2014 it becomes an\n * uncaught error on somebody else's page, once per message. Any party that can\n * reach this branch can therefore reach it repeatedly, and the loudest\n * available answer would be handing them a way to fill a product's error\n * reporting with our noise.\n *\n * The narrower reason the throw would buy little: `isFrameMessage` and\n * `isParentMessage` gate on fixed sets of type strings, so a message an\n * outsider invents never reaches the switch at all. What DOES reach it is a\n * type in the guard's set with no `case` \u2014 which is our own two lists having\n * drifted apart, in this repository, in the commit that widened one of them.\n *\n * So the answer is: act on nothing, and say so. Acting on nothing is what a\n * trust boundary owes an input it does not understand; saying so is what makes\n * this different from the silent drop that was there before, which is\n * indistinguishable from the message never having been sent.\n *\n * `console.error` rather than a callback, because both call sites are inside\n * listeners with no error channel of their own, and because in the frame the\n * SDK's own `StateCollector` patches `console.error` \u2014 so the drift is\n * collected into the very next annotation payload, where somebody debugging\n * exactly this will already be looking.\n *\n * The type-level half of the guard is NOT here. It is `const unhandled: never`\n * in each `default` branch, which is what makes a widened union a compile\n * error; this function only makes the runtime half observable, and it is\n * exported so `exhaustive.test.ts` can call it. Both halves are pinned\n * separately, because deleting either leaves the other looking correct.\n */\n/**\n * `JSON.stringify` that cannot throw, for any input at all (A-359 cycle 2).\n *\n * ===========================================================================\n * WHY THIS IS NEEDED \u2014 THE DEFECT IT REPLACES\n * ===========================================================================\n *\n * `unhandledMessage` interpolated `JSON.stringify(message)` unguarded, and so\n * became the thing it was written to prevent. `postMessage` serialises with\n * the STRUCTURED CLONE algorithm, and structured clone PRESERVES CYCLES where\n * JSON cannot represent them \u2014 so `{a: {b: a}}` survives a `postMessage`\n * intact, arrives at that branch, and `JSON.stringify` throws `Converting\n * circular structure to JSON` from inside a `message` listener on a page this\n * SDK is a guest on, once per message, at the sender's discretion.\n *\n * Every word of the argument on `unhandledMessage` was correct, and the\n * implementation defeated it on its own terms. Both cases are pinned in\n * `exhaustive.test.ts` and both were confirmed RED before this existed.\n *\n * ===========================================================================\n * WHY NOT `describeArg`, WHICH DOES THIS ALREADY \u2014 TWO REASONS, ONE MEASURED\n * ===========================================================================\n *\n * `describeArg` in `state.ts` solves the same problem and its comment names\n * the same hazard, so reusing it was the first thing tried rather than a road\n * not taken. It was reverted for two reasons, and the second is the one that\n * decided it:\n *\n * 1. MEASURED, by running `node build.mjs` on both: reuse costs protocol\n * 869 -> 1008 (+139) and host 649 -> 793 (+144). This local version costs\n * +39 and +37. `describeArg` is built for console ARGUMENTS \u2014 strings,\n * Errors, a prototype walk for named objects \u2014 and almost none of that is\n * reachable for a wire message, so the extra 100 bytes per entry point buy\n * nothing here. In a package with a per-entry gzip budget that is not a\n * rounding error.\n * 2. THIS MODULE HAS NO IMPORTS AND THAT IS A PROPERTY WORTH KEEPING.\n * `protocol.ts` is the pure wire contract \u2014 it mirrors\n * `docs/contracts/types.ts`, `annotation-next` imports it so a build tool\n * can learn two strings without pulling in a DOM, and `state.ts` already\n * imports FROM it. Importing back the other way makes the contract depend\n * on the console-patching state collector and closes a module cycle that\n * is only invisible because `state.ts`'s side of it is `import type`.\n *\n * The cost of the choice, stated rather than left for somebody to find: there\n * are now two safe-stringify helpers in this package. They cannot drift in a\n * way that matters \u2014 each is a total function with one behaviour and its own\n * test \u2014 but if a third appears, that is the moment to put one in a module\n * both can depend on, rather than making the contract depend on the collector.\n *\n * The fallback is a constant, deliberately. Reaching for `String(message)` or\n * a constructor name in the `catch` is the obvious way to say something more\n * useful, and it is how this function would learn to throw again: the input\n * that got us here is one whose property access already threw.\n */\nfunction safeJson(message: unknown): string {\n try {\n return JSON.stringify(message) ?? String(message);\n } catch {\n return \"[unserialisable]\";\n }\n}\n\nexport function unhandledMessage(half: \"host\" | \"frame\", message: unknown): void {\n /**\n * `safeJson` RATHER THAN A BARE `JSON.stringify`, AND THAT IS THE WHOLE\n * DIFFERENCE BETWEEN THIS FUNCTION AND ITS FIRST DRAFT (A-359 cycle 2).\n *\n * The first draft interpolated `JSON.stringify(message)` unguarded, which\n * made this function do the thing it exists to avoid. The full argument, the\n * measurement, and why `describeArg` was tried and reverted are on\n * `safeJson` above rather than repeated here.\n *\n * Short on purpose too, and the shortness is measured rather than stylistic.\n * This package ships inside a product's bundle and this package's own\n * `build.mjs` enforces a gzip budget per entry point; the first draft of this\n * sentence was 230 characters and put `protocol`, `host` and `index` over\n * theirs at once. What it must still carry is which half, what arrived, and\n * that nothing was done with it \u2014 the last being the only thing that\n * distinguishes this from the silent drop it replaced.\n */\n console.error(`[annotation] ${half}: protocol drift, nothing was acted on: ${safeJson(message)}`);\n}\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 READ token's query parameter (A-277).\n *\n * ===========================================================================\n * WHY THE MARKER LAYER GETS ITS OWN CREDENTIAL AND NOT `ai_capture`\n * ===========================================================================\n *\n * Fourth name on the same wire, and the reason it is a fourth is A-276's\n * disclosure decision rather than tidiness. The capture token is minted for\n * WRITING and lives four hours so a reviewer can work through a session; reusing\n * it for the read would make one leaked deep link a write capability AND a read\n * capability over every reviewer's comments on that origin for the same window.\n * The scope is a signed field, `verifyCaptureToken` requires an expectation at\n * every call site, and the read endpoint refuses a write claim \u2014 so handing\n * `ai_capture` to the reader does not degrade to a read, it fails.\n *\n * The read claim's TTL is ONE hour rather than four (`READ_TOKEN_TTL_S`), which\n * is the practical consequence a reader of this constant should know: the\n * toolbar outlives the markers, so a reviewer who leaves a tab open all\n * afternoon will find the count line saying it cannot read before they find the\n * Send button broken. That is the intended ordering \u2014 the shorter credential is\n * the one that discloses other people's words.\n *\n * Exported here for the reason the three above are: the console writes it into a\n * deep link and the product's bundle reads it, from two repositories, and a\n * mismatch produces no error anywhere \u2014 just a page with no markers, which is\n * indistinguishable from nine other causes.\n */\nexport const READ_TOKEN_PARAM = \"ai_read\";\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"],
|
|
5
|
+
"mappings": "yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,0BAAAE,EAAA,mBAAAC,IAAA,eAAAC,EAAAJ,GCqTA,SAASK,EAASC,EAA0B,CAC1C,GAAI,CACF,OAAO,KAAK,UAAUA,CAAO,GAAK,OAAOA,CAAO,CAClD,MAAQ,CACN,MAAO,kBACT,CACF,CAEO,SAASC,EAAiBC,EAAwBF,EAAwB,CAkB/E,QAAQ,MAAM,gBAAgBE,CAAI,2CAA2CH,EAASC,CAAO,CAAC,EAAE,CAClG,CAwBO,SAASG,EAAqBC,EAAwB,CAC3D,GAAI,CAACA,GAAUA,IAAW,IACxB,MAAM,IAAI,MACR,6GAEF,EAEF,OAAOA,CACT,CDpTA,IAAMC,EAAc,IAAI,IAAI,CAC1B,iBACA,iBACA,mBACA,iBACF,CAAC,EAUM,SAASC,EAAeC,EAAsC,CACnE,GAAI,OAAOA,GAAS,UAAYA,IAAS,KAAM,MAAO,GACtD,IAAMC,EAAQD,EAA4B,KAC1C,OAAO,OAAOC,GAAS,UAAYH,EAAY,IAAIG,CAAI,CACzD,CAEO,SAASC,EAAqBC,EAAgD,CACnF,IAAMC,EAAcC,EAAqBF,EAAQ,WAAW,EACxDG,EAAQ,GACRC,EAAY,GAEVC,EAAQC,GAA2B,CACnCF,GAGJJ,EAAQ,MAAM,YAAYM,EAASL,CAAW,CAChD,EAEMM,EAAaC,GAAwB,CAUzC,GAAIA,EAAM,SAAWP,GAUjBO,EAAM,SAAWR,EAAQ,OAExBJ,EAAeY,EAAM,IAAI,EAE9B,OAAQA,EAAM,KAAK,KAAM,CACvB,IAAK,iBACHL,EAAQ,GACRH,EAAQ,QAAQ,CAAE,WAAYQ,EAAM,KAAK,WAAY,MAAOA,EAAM,KAAK,KAAM,CAAC,EAC9E,OACF,IAAK,iBACHR,EAAQ,QAAQQ,EAAM,KAAK,MAAM,EACjC,OACF,IAAK,mBACHR,EAAQ,UAAUQ,EAAM,KAAK,OAAO,EACpC,OACF,IAAK,kBACHR,EAAQ,SAAS,EACjB,OACF,QAAS,CAcP,IAAMS,EAAmBD,EAAM,KAC/BE,EAAiB,OAAQD,CAAS,EAClC,MACF,CACF,CACF,EAEA,OAAAT,EAAQ,WAAW,iBAAiB,UAAWO,CAAS,EAEjD,CACL,OAAQ,IAAMF,EAAK,CAAE,KAAM,iBAAkB,CAAC,EAC9C,QAAS,IAAMA,EAAK,CAAE,KAAM,kBAAmB,CAAC,EAChD,KAAM,IAAMA,EAAK,CAAE,KAAM,eAAgB,CAAC,EAC1C,QAAS,IAAMF,EACf,SAAU,CACRC,EAAY,GACZD,EAAQ,GACRH,EAAQ,WAAW,oBAAoB,UAAWO,CAAS,CAC7D,CACF,CACF",
|
|
6
|
+
"names": ["host_exports", "__export", "createAnnotationHost", "isFrameMessage", "__toCommonJS", "safeJson", "message", "unhandledMessage", "half", "assertConcreteOrigin", "origin", "FRAME_TYPES", "isFrameMessage", "data", "type", "createAnnotationHost", "options", "frameOrigin", "assertConcreteOrigin", "ready", "destroyed", "post", "message", "onMessage", "event", "unhandled", "unhandledMessage"]
|
|
7
7
|
}
|
package/dist/host.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function s(e){if(!e||e==="*")throw new Error("[annotation] a concrete parent origin is required; '*' would broadcast annotation payloads to any embedder.");return e}var
|
|
1
|
+
function d(e){try{return JSON.stringify(e)??String(e)}catch{return"[unserialisable]"}}function s(e,n){console.error(`[annotation] ${e}: protocol drift, nothing was acted on: ${d(n)}`)}function u(e){if(!e||e==="*")throw new Error("[annotation] a concrete parent origin is required; '*' would broadcast annotation payloads to any embedder.");return e}var p=new Set(["annotate:ready","annotate:hover","annotate:capture","annotate:cancel"]);function l(e){if(typeof e!="object"||e===null)return!1;let n=e.type;return typeof n=="string"&&p.has(n)}function m(e){let n=u(e.frameOrigin),r=!1,a=!1,o=t=>{a||e.frame.postMessage(t,n)},i=t=>{if(t.origin===n&&t.source===e.frame&&l(t.data))switch(t.data.type){case"annotate:ready":r=!0,e.onReady({sdkVersion:t.data.sdkVersion,build:t.data.build});return;case"annotate:hover":e.onHover(t.data.target);return;case"annotate:capture":e.onCapture(t.data.payload);return;case"annotate:cancel":e.onCancel();return;default:{let c=t.data;s("host",c);return}}};return e.hostWindow.addEventListener("message",i),{enable:()=>o({type:"annotate:enable"}),disable:()=>o({type:"annotate:disable"}),ping:()=>o({type:"annotate:ping"}),isReady:()=>r,destroy(){a=!0,r=!1,e.hostWindow.removeEventListener("message",i)}}}export{m as createAnnotationHost,l as isFrameMessage};
|
|
2
2
|
//# sourceMappingURL=host.mjs.map
|
package/dist/host.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/protocol.ts", "../src/host.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 READ token's query parameter (A-277).\n *\n * ===========================================================================\n * WHY THE MARKER LAYER GETS ITS OWN CREDENTIAL AND NOT `ai_capture`\n * ===========================================================================\n *\n * Fourth name on the same wire, and the reason it is a fourth is A-276's\n * disclosure decision rather than tidiness. The capture token is minted for\n * WRITING and lives four hours so a reviewer can work through a session; reusing\n * it for the read would make one leaked deep link a write capability AND a read\n * capability over every reviewer's comments on that origin for the same window.\n * The scope is a signed field, `verifyCaptureToken` requires an expectation at\n * every call site, and the read endpoint refuses a write claim \u2014 so handing\n * `ai_capture` to the reader does not degrade to a read, it fails.\n *\n * The read claim's TTL is ONE hour rather than four (`READ_TOKEN_TTL_S`), which\n * is the practical consequence a reader of this constant should know: the\n * toolbar outlives the markers, so a reviewer who leaves a tab open all\n * afternoon will find the count line saying it cannot read before they find the\n * Send button broken. That is the intended ordering \u2014 the shorter credential is\n * the one that discloses other people's words.\n *\n * Exported here for the reason the three above are: the console writes it into a\n * deep link and the product's bundle reads it, from two repositories, and a\n * mismatch produces no error anywhere \u2014 just a page with no markers, which is\n * indistinguishable from nine other causes.\n */\nexport const READ_TOKEN_PARAM = \"ai_read\";\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", "import {\n assertConcreteOrigin,\n type AnnotationPayload,\n type BuildInfo,\n type FrameToParent,\n type ParentToFrame,\n type TargetInfo,\n} from \"./protocol\";\n\n/**\n * The parent half of the annotation channel (A-30).\n *\n * WHY THIS LIVES IN THE SDK AND NOT IN `apps/admin`.\n *\n * The protocol is this package's domain. It already owns the message union,\n * `isParentMessage` and `assertConcreteOrigin`; a second implementation of the\n * same handshake, written host-side because that is where the UI is, is exactly\n * the shape SHARED-PACKAGES.md warns about. The two halves of a protocol\n * drifting apart is not a hypothetical \u2014 it is the normal outcome when they are\n * maintained in different packages by different changes.\n *\n * Concretely: the frame's `sdk.ts` and this file are the only two things that\n * can disagree about what `annotate:capture` means, and keeping them in one\n * package means a change to the union is a compile error in both.\n *\n * Admin gets the UI. This gets the wire.\n */\n\nexport interface AnnotationHostOptions {\n /**\n * The window being framed. Passed in rather than looked up, because the host\n * knows which iframe it means and this module must never guess.\n */\n frame: Window;\n /**\n * The frame's origin \u2014 where the product is served from. Required and\n * concrete, checked by `assertConcreteOrigin`: posting to `*` would hand an\n * annotation payload, including console errors from a real session, to\n * whatever happens to be loaded.\n */\n frameOrigin: string;\n /** The window that receives messages. Injected so tests need no real iframe. */\n hostWindow: Window;\n\n onReady(info: { sdkVersion: string; build: BuildInfo }): void;\n onHover(target: TargetInfo): void;\n onCapture(payload: AnnotationPayload): void;\n onCancel(): void;\n}\n\nexport interface AnnotationHost {\n enable(): void;\n disable(): void;\n ping(): void;\n /** True once the frame has announced itself. */\n isReady(): boolean;\n destroy(): void;\n}\n\nconst FRAME_TYPES = new Set([\n \"annotate:ready\",\n \"annotate:hover\",\n \"annotate:capture\",\n \"annotate:cancel\",\n]);\n\n/**\n * Is this one of ours?\n *\n * The mirror of `isParentMessage`, and it carries the same warning: origin is\n * checked separately and FIRST. A well-formed message from the wrong origin is\n * precisely the case this guard must not be trusted to catch \u2014 anything can\n * post `{type: \"annotate:capture\"}` to a window.\n */\nexport function isFrameMessage(data: unknown): data is FrameToParent {\n if (typeof data !== \"object\" || data === null) return false;\n const type = (data as { type?: unknown }).type;\n return typeof type === \"string\" && FRAME_TYPES.has(type);\n}\n\nexport function createAnnotationHost(options: AnnotationHostOptions): AnnotationHost {\n const frameOrigin = assertConcreteOrigin(options.frameOrigin);\n let ready = false;\n let destroyed = false;\n\n const post = (message: ParentToFrame) => {\n if (destroyed) return;\n // Targeted, never `*`. `enable` is not sensitive in itself, but a habit of\n // posting to `*` is what eventually posts something that is.\n options.frame.postMessage(message, frameOrigin);\n };\n\n const onMessage = (event: MessageEvent) => {\n /**\n * Origin first, and by exact match.\n *\n * Not `endsWith`, not a prefix: for a product served from `example.com`,\n * `https://evil-example.com` ends with nothing suspicious and\n * `https://example.com.evil.test` starts with the right characters. The browser hands us a normalised origin, so an\n * equality test is both correct and the only one that cannot be widened by\n * a hostile hostname.\n */\n if (event.origin !== frameOrigin) return;\n\n /**\n * And the source must be the frame we are talking to.\n *\n * Origin alone is not enough: any other window on the same origin \u2014 a popup\n * the product opened, another iframe \u2014 can post a well-formed message from\n * an origin that matches. This is the check that makes \"the frame said so\"\n * mean the frame.\n */\n if (event.source !== options.frame) return;\n\n if (!isFrameMessage(event.data)) return;\n\n switch (event.data.type) {\n case \"annotate:ready\":\n ready = true;\n options.onReady({ sdkVersion: event.data.sdkVersion, build: event.data.build });\n return;\n case \"annotate:hover\":\n options.onHover(event.data.target);\n return;\n case \"annotate:capture\":\n options.onCapture(event.data.payload);\n return;\n case \"annotate:cancel\":\n options.onCancel();\n return;\n }\n };\n\n options.hostWindow.addEventListener(\"message\", onMessage);\n\n return {\n enable: () => post({ type: \"annotate:enable\" }),\n disable: () => post({ type: \"annotate:disable\" }),\n ping: () => post({ type: \"annotate:ping\" }),\n isReady: () => ready,\n destroy() {\n destroyed = true;\n ready = false;\n options.hostWindow.removeEventListener(\"message\", onMessage);\n },\n };\n}\n"],
|
|
5
|
-
"mappings": "
|
|
6
|
-
"names": ["assertConcreteOrigin", "origin", "FRAME_TYPES", "isFrameMessage", "data", "type", "createAnnotationHost", "options", "frameOrigin", "assertConcreteOrigin", "ready", "destroyed", "post", "message", "onMessage", "event"]
|
|
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 * A hosted document declares first-party annotation support with this exact\n * response header. It describes the BUILD, never a token, cookie or attempt.\n */\nexport const ANNOTATION_MODE_HEADER = \"x-weatherboard-annotation-mode\";\nexport const FIRST_PARTY_ANNOTATION_MODE = \"first-party\";\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 /**\n * `w`/`h` and `canvases` arrive with the in-page viewport capture (A-2385);\n * `canvases` counts the canvases in view and how many read back blank or\n * unreadable, so a black rectangle in the picture is explained, not silent.\n */\n screenshot?: {\n dataUrl: string;\n region: \"element\" | \"viewport\";\n w?: number;\n h?: number;\n canvases?: { total: number; blank: number; unreadable: number };\n };\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\n/**\n * A message that got past both guards and matched no `case` (A-359).\n *\n * ===========================================================================\n * THE DEFECT\n * ===========================================================================\n *\n * `host.ts` and `sdk.ts` hold the two halves of this protocol, one `switch`\n * each, and neither had a `default`. Measured before the fix: adding\n * `| { type: \"annotate:probe\" }` to `ParentToFrame` and\n * `| { type: \"annotate:pong\" }` to `FrameToParent`, then running\n * `tsc --noEmit` in this package, gave exit 0 and zero errors. A message type\n * added to the union is dropped on the floor by whichever half was not\n * updated, and the two halves are the only things in the world that can\n * disagree about what a message means.\n *\n * ===========================================================================\n * WHY IT DOES NOT THROW, WHERE THE MANAGER'S TWO EQUIVALENTS DO\n * ===========================================================================\n *\n * This is the house pattern with one deliberate departure, and the departure is\n * the boundary rather than taste.\n *\n * Both switches run inside a `message` listener, reached from `postMessage`.\n * The host's listener runs in the management console over a frame that may be\n * serving anything; the SDK's runs in a product page we do not own. A throw\n * there does not propagate to a caller \u2014 there is no caller \u2014 it becomes an\n * uncaught error on somebody else's page, once per message. Any party that can\n * reach this branch can therefore reach it repeatedly, and the loudest\n * available answer would be handing them a way to fill a product's error\n * reporting with our noise.\n *\n * The narrower reason the throw would buy little: `isFrameMessage` and\n * `isParentMessage` gate on fixed sets of type strings, so a message an\n * outsider invents never reaches the switch at all. What DOES reach it is a\n * type in the guard's set with no `case` \u2014 which is our own two lists having\n * drifted apart, in this repository, in the commit that widened one of them.\n *\n * So the answer is: act on nothing, and say so. Acting on nothing is what a\n * trust boundary owes an input it does not understand; saying so is what makes\n * this different from the silent drop that was there before, which is\n * indistinguishable from the message never having been sent.\n *\n * `console.error` rather than a callback, because both call sites are inside\n * listeners with no error channel of their own, and because in the frame the\n * SDK's own `StateCollector` patches `console.error` \u2014 so the drift is\n * collected into the very next annotation payload, where somebody debugging\n * exactly this will already be looking.\n *\n * The type-level half of the guard is NOT here. It is `const unhandled: never`\n * in each `default` branch, which is what makes a widened union a compile\n * error; this function only makes the runtime half observable, and it is\n * exported so `exhaustive.test.ts` can call it. Both halves are pinned\n * separately, because deleting either leaves the other looking correct.\n */\n/**\n * `JSON.stringify` that cannot throw, for any input at all (A-359 cycle 2).\n *\n * ===========================================================================\n * WHY THIS IS NEEDED \u2014 THE DEFECT IT REPLACES\n * ===========================================================================\n *\n * `unhandledMessage` interpolated `JSON.stringify(message)` unguarded, and so\n * became the thing it was written to prevent. `postMessage` serialises with\n * the STRUCTURED CLONE algorithm, and structured clone PRESERVES CYCLES where\n * JSON cannot represent them \u2014 so `{a: {b: a}}` survives a `postMessage`\n * intact, arrives at that branch, and `JSON.stringify` throws `Converting\n * circular structure to JSON` from inside a `message` listener on a page this\n * SDK is a guest on, once per message, at the sender's discretion.\n *\n * Every word of the argument on `unhandledMessage` was correct, and the\n * implementation defeated it on its own terms. Both cases are pinned in\n * `exhaustive.test.ts` and both were confirmed RED before this existed.\n *\n * ===========================================================================\n * WHY NOT `describeArg`, WHICH DOES THIS ALREADY \u2014 TWO REASONS, ONE MEASURED\n * ===========================================================================\n *\n * `describeArg` in `state.ts` solves the same problem and its comment names\n * the same hazard, so reusing it was the first thing tried rather than a road\n * not taken. It was reverted for two reasons, and the second is the one that\n * decided it:\n *\n * 1. MEASURED, by running `node build.mjs` on both: reuse costs protocol\n * 869 -> 1008 (+139) and host 649 -> 793 (+144). This local version costs\n * +39 and +37. `describeArg` is built for console ARGUMENTS \u2014 strings,\n * Errors, a prototype walk for named objects \u2014 and almost none of that is\n * reachable for a wire message, so the extra 100 bytes per entry point buy\n * nothing here. In a package with a per-entry gzip budget that is not a\n * rounding error.\n * 2. THIS MODULE HAS NO IMPORTS AND THAT IS A PROPERTY WORTH KEEPING.\n * `protocol.ts` is the pure wire contract \u2014 it mirrors\n * `docs/contracts/types.ts`, `annotation-next` imports it so a build tool\n * can learn two strings without pulling in a DOM, and `state.ts` already\n * imports FROM it. Importing back the other way makes the contract depend\n * on the console-patching state collector and closes a module cycle that\n * is only invisible because `state.ts`'s side of it is `import type`.\n *\n * The cost of the choice, stated rather than left for somebody to find: there\n * are now two safe-stringify helpers in this package. They cannot drift in a\n * way that matters \u2014 each is a total function with one behaviour and its own\n * test \u2014 but if a third appears, that is the moment to put one in a module\n * both can depend on, rather than making the contract depend on the collector.\n *\n * The fallback is a constant, deliberately. Reaching for `String(message)` or\n * a constructor name in the `catch` is the obvious way to say something more\n * useful, and it is how this function would learn to throw again: the input\n * that got us here is one whose property access already threw.\n */\nfunction safeJson(message: unknown): string {\n try {\n return JSON.stringify(message) ?? String(message);\n } catch {\n return \"[unserialisable]\";\n }\n}\n\nexport function unhandledMessage(half: \"host\" | \"frame\", message: unknown): void {\n /**\n * `safeJson` RATHER THAN A BARE `JSON.stringify`, AND THAT IS THE WHOLE\n * DIFFERENCE BETWEEN THIS FUNCTION AND ITS FIRST DRAFT (A-359 cycle 2).\n *\n * The first draft interpolated `JSON.stringify(message)` unguarded, which\n * made this function do the thing it exists to avoid. The full argument, the\n * measurement, and why `describeArg` was tried and reverted are on\n * `safeJson` above rather than repeated here.\n *\n * Short on purpose too, and the shortness is measured rather than stylistic.\n * This package ships inside a product's bundle and this package's own\n * `build.mjs` enforces a gzip budget per entry point; the first draft of this\n * sentence was 230 characters and put `protocol`, `host` and `index` over\n * theirs at once. What it must still carry is which half, what arrived, and\n * that nothing was done with it \u2014 the last being the only thing that\n * distinguishes this from the silent drop it replaced.\n */\n console.error(`[annotation] ${half}: protocol drift, nothing was acted on: ${safeJson(message)}`);\n}\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 READ token's query parameter (A-277).\n *\n * ===========================================================================\n * WHY THE MARKER LAYER GETS ITS OWN CREDENTIAL AND NOT `ai_capture`\n * ===========================================================================\n *\n * Fourth name on the same wire, and the reason it is a fourth is A-276's\n * disclosure decision rather than tidiness. The capture token is minted for\n * WRITING and lives four hours so a reviewer can work through a session; reusing\n * it for the read would make one leaked deep link a write capability AND a read\n * capability over every reviewer's comments on that origin for the same window.\n * The scope is a signed field, `verifyCaptureToken` requires an expectation at\n * every call site, and the read endpoint refuses a write claim \u2014 so handing\n * `ai_capture` to the reader does not degrade to a read, it fails.\n *\n * The read claim's TTL is ONE hour rather than four (`READ_TOKEN_TTL_S`), which\n * is the practical consequence a reader of this constant should know: the\n * toolbar outlives the markers, so a reviewer who leaves a tab open all\n * afternoon will find the count line saying it cannot read before they find the\n * Send button broken. That is the intended ordering \u2014 the shorter credential is\n * the one that discloses other people's words.\n *\n * Exported here for the reason the three above are: the console writes it into a\n * deep link and the product's bundle reads it, from two repositories, and a\n * mismatch produces no error anywhere \u2014 just a page with no markers, which is\n * indistinguishable from nine other causes.\n */\nexport const READ_TOKEN_PARAM = \"ai_read\";\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", "import {\n assertConcreteOrigin,\n type AnnotationPayload,\n type BuildInfo,\n type FrameToParent,\n type ParentToFrame,\n type TargetInfo,\n unhandledMessage,\n} from \"./protocol\";\n\n/**\n * The parent half of the annotation channel (A-30).\n *\n * WHY THIS LIVES IN THE SDK AND NOT IN `apps/admin`.\n *\n * The protocol is this package's domain. It already owns the message union,\n * `isParentMessage` and `assertConcreteOrigin`; a second implementation of the\n * same handshake, written host-side because that is where the UI is, is exactly\n * the shape SHARED-PACKAGES.md warns about. The two halves of a protocol\n * drifting apart is not a hypothetical \u2014 it is the normal outcome when they are\n * maintained in different packages by different changes.\n *\n * Concretely: the frame's `sdk.ts` and this file are the only two things that\n * can disagree about what `annotate:capture` means, and keeping them in one\n * package means a change to the union is a compile error in both.\n *\n * Admin gets the UI. This gets the wire.\n */\n\nexport interface AnnotationHostOptions {\n /**\n * The window being framed. Passed in rather than looked up, because the host\n * knows which iframe it means and this module must never guess.\n */\n frame: Window;\n /**\n * The frame's origin \u2014 where the product is served from. Required and\n * concrete, checked by `assertConcreteOrigin`: posting to `*` would hand an\n * annotation payload, including console errors from a real session, to\n * whatever happens to be loaded.\n */\n frameOrigin: string;\n /** The window that receives messages. Injected so tests need no real iframe. */\n hostWindow: Window;\n\n onReady(info: { sdkVersion: string; build: BuildInfo }): void;\n onHover(target: TargetInfo): void;\n onCapture(payload: AnnotationPayload): void;\n onCancel(): void;\n}\n\nexport interface AnnotationHost {\n enable(): void;\n disable(): void;\n ping(): void;\n /** True once the frame has announced itself. */\n isReady(): boolean;\n destroy(): void;\n}\n\nconst FRAME_TYPES = new Set([\n \"annotate:ready\",\n \"annotate:hover\",\n \"annotate:capture\",\n \"annotate:cancel\",\n]);\n\n/**\n * Is this one of ours?\n *\n * The mirror of `isParentMessage`, and it carries the same warning: origin is\n * checked separately and FIRST. A well-formed message from the wrong origin is\n * precisely the case this guard must not be trusted to catch \u2014 anything can\n * post `{type: \"annotate:capture\"}` to a window.\n */\nexport function isFrameMessage(data: unknown): data is FrameToParent {\n if (typeof data !== \"object\" || data === null) return false;\n const type = (data as { type?: unknown }).type;\n return typeof type === \"string\" && FRAME_TYPES.has(type);\n}\n\nexport function createAnnotationHost(options: AnnotationHostOptions): AnnotationHost {\n const frameOrigin = assertConcreteOrigin(options.frameOrigin);\n let ready = false;\n let destroyed = false;\n\n const post = (message: ParentToFrame) => {\n if (destroyed) return;\n // Targeted, never `*`. `enable` is not sensitive in itself, but a habit of\n // posting to `*` is what eventually posts something that is.\n options.frame.postMessage(message, frameOrigin);\n };\n\n const onMessage = (event: MessageEvent) => {\n /**\n * Origin first, and by exact match.\n *\n * Not `endsWith`, not a prefix: for a product served from `example.com`,\n * `https://evil-example.com` ends with nothing suspicious and\n * `https://example.com.evil.test` starts with the right characters. The browser hands us a normalised origin, so an\n * equality test is both correct and the only one that cannot be widened by\n * a hostile hostname.\n */\n if (event.origin !== frameOrigin) return;\n\n /**\n * And the source must be the frame we are talking to.\n *\n * Origin alone is not enough: any other window on the same origin \u2014 a popup\n * the product opened, another iframe \u2014 can post a well-formed message from\n * an origin that matches. This is the check that makes \"the frame said so\"\n * mean the frame.\n */\n if (event.source !== options.frame) return;\n\n if (!isFrameMessage(event.data)) return;\n\n switch (event.data.type) {\n case \"annotate:ready\":\n ready = true;\n options.onReady({ sdkVersion: event.data.sdkVersion, build: event.data.build });\n return;\n case \"annotate:hover\":\n options.onHover(event.data.target);\n return;\n case \"annotate:capture\":\n options.onCapture(event.data.payload);\n return;\n case \"annotate:cancel\":\n options.onCancel();\n return;\n default: {\n /**\n * A-359. `const unhandled: never` is the compile-time half \u2014 widen\n * `FrameToParent` and this line is the error, naming this file. The\n * call is the runtime half, and it deliberately does not throw; the\n * reasoning is on `unhandledMessage` in `protocol.ts`, and it is about\n * this being a `postMessage` boundary rather than an internal call.\n *\n * Note what is NOT claimed: this cannot see a message type added to\n * `FRAME_TYPES` above and to no union, because such a message is not a\n * `FrameToParent` in the first place. The set and the union are two\n * lists that must agree, and `exhaustive.test.ts` is what holds them\n * to it.\n */\n const unhandled: never = event.data;\n unhandledMessage(\"host\", unhandled);\n return;\n }\n }\n };\n\n options.hostWindow.addEventListener(\"message\", onMessage);\n\n return {\n enable: () => post({ type: \"annotate:enable\" }),\n disable: () => post({ type: \"annotate:disable\" }),\n ping: () => post({ type: \"annotate:ping\" }),\n isReady: () => ready,\n destroy() {\n destroyed = true;\n ready = false;\n options.hostWindow.removeEventListener(\"message\", onMessage);\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "AAqTA,SAASA,EAASC,EAA0B,CAC1C,GAAI,CACF,OAAO,KAAK,UAAUA,CAAO,GAAK,OAAOA,CAAO,CAClD,MAAQ,CACN,MAAO,kBACT,CACF,CAEO,SAASC,EAAiBC,EAAwBF,EAAwB,CAkB/E,QAAQ,MAAM,gBAAgBE,CAAI,2CAA2CH,EAASC,CAAO,CAAC,EAAE,CAClG,CAwBO,SAASG,EAAqBC,EAAwB,CAC3D,GAAI,CAACA,GAAUA,IAAW,IACxB,MAAM,IAAI,MACR,6GAEF,EAEF,OAAOA,CACT,CCpTA,IAAMC,EAAc,IAAI,IAAI,CAC1B,iBACA,iBACA,mBACA,iBACF,CAAC,EAUM,SAASC,EAAeC,EAAsC,CACnE,GAAI,OAAOA,GAAS,UAAYA,IAAS,KAAM,MAAO,GACtD,IAAMC,EAAQD,EAA4B,KAC1C,OAAO,OAAOC,GAAS,UAAYH,EAAY,IAAIG,CAAI,CACzD,CAEO,SAASC,EAAqBC,EAAgD,CACnF,IAAMC,EAAcC,EAAqBF,EAAQ,WAAW,EACxDG,EAAQ,GACRC,EAAY,GAEVC,EAAQC,GAA2B,CACnCF,GAGJJ,EAAQ,MAAM,YAAYM,EAASL,CAAW,CAChD,EAEMM,EAAaC,GAAwB,CAUzC,GAAIA,EAAM,SAAWP,GAUjBO,EAAM,SAAWR,EAAQ,OAExBJ,EAAeY,EAAM,IAAI,EAE9B,OAAQA,EAAM,KAAK,KAAM,CACvB,IAAK,iBACHL,EAAQ,GACRH,EAAQ,QAAQ,CAAE,WAAYQ,EAAM,KAAK,WAAY,MAAOA,EAAM,KAAK,KAAM,CAAC,EAC9E,OACF,IAAK,iBACHR,EAAQ,QAAQQ,EAAM,KAAK,MAAM,EACjC,OACF,IAAK,mBACHR,EAAQ,UAAUQ,EAAM,KAAK,OAAO,EACpC,OACF,IAAK,kBACHR,EAAQ,SAAS,EACjB,OACF,QAAS,CAcP,IAAMS,EAAmBD,EAAM,KAC/BE,EAAiB,OAAQD,CAAS,EAClC,MACF,CACF,CACF,EAEA,OAAAT,EAAQ,WAAW,iBAAiB,UAAWO,CAAS,EAEjD,CACL,OAAQ,IAAMF,EAAK,CAAE,KAAM,iBAAkB,CAAC,EAC9C,QAAS,IAAMA,EAAK,CAAE,KAAM,kBAAmB,CAAC,EAChD,KAAM,IAAMA,EAAK,CAAE,KAAM,eAAgB,CAAC,EAC1C,QAAS,IAAMF,EACf,SAAU,CACRC,EAAY,GACZD,EAAQ,GACRH,EAAQ,WAAW,oBAAoB,UAAWO,CAAS,CAC7D,CACF,CACF",
|
|
6
|
+
"names": ["safeJson", "message", "unhandledMessage", "half", "assertConcreteOrigin", "origin", "FRAME_TYPES", "isFrameMessage", "data", "type", "createAnnotationHost", "options", "frameOrigin", "assertConcreteOrigin", "ready", "destroyed", "post", "message", "onMessage", "event", "unhandled", "unhandledMessage"]
|
|
7
7
|
}
|