{"version":3,"sources":["../src/protocolRefusal.ts"],"sourcesContent":["// The one place the SDK turns a host reply envelope into a typed, coded rejection (R6).\n//\n// ---------------------------------------------------------------------------\n// THE CENSUS — miscounted four times. COUNT THE GREP, and say which unit.\n// ---------------------------------------------------------------------------\n//\n// R3-708's review said three copies, then five, then \"ten across eight files\". This file\n// then said ten while the list beneath it summed to twelve, and a fourth reviewer caught\n// that. After R3-816 folded the pure copies, and R3-1089 folded `catalog.ts` (the helper\n// learned `retryAfter` for it — the fold's premise holds again), the count is:\n//\n//   **twenty-one CALL SITES across sixteen FILES** — `grep -n 'throwOnRefusal(' src/*.ts`,\n//   minus the declaration here, minus `*.test.*`, and minus this census's own mention of\n//   the pattern (the recipe's literal matches it):\n//\n//   catalog.ts · dnd.ts · editor.ts · feed.ts · ipc.ts (×2) · mounts.ts (×3) · netFetch.ts ·\n//   openBundle.ts · openExternal.ts · openRepository.ts · recents.ts · secrets.ts ·\n//   spacesMode.ts · tasks.ts · theme.ts (×3) · vcs.ts\n//\n// Every miscount came from the same two mistakes: reading files instead of counting the\n// grep, and never saying which unit was being counted. `mounts.ts` has three separate\n// request helpers, each with its own copy; R3-780 touched five FILES and seven SITES.\n// \"Ten\" was neither.\n//\n// R3-816 folded the pure copies of the shape `ipc.ts` had until R3-780 — `recents` and\n// `tasks` here (two of the three the item named; the third, `catalog`, is the exemption\n// above) — and the fold changed nothing observable. R3-817 (merged as #192, 0.74.3) then\n// folded the `res && 'code' in res` family — `openExternal`, `openRepository`, `feed`,\n// `netFetch` and `theme` (×3) — fixing the `TypeError: Cannot use 'in' operator` a string\n// reply used to throw there. That form is gone from main; nothing remains to fold in\n// either family.\n//\n// Still deliberately NOT folded:\n//\n//   · `launch.ts` — it folds `!res.data?.launchId` into the same condition and RETURNS\n//     `{ ok: false, code }` instead of throwing. Different control flow entirely.\n//\n// (`catalog.ts` folded in R3-1089 — the helper now builds R3-954's `retryAfter`, so the\n// exemption's premise is gone.)\n//\n// `check:clones` cannot help with any of this: minLines 6, minTokens 50, and no identifier\n// normalisation, so blocks differing only in type names read as distinct.\n//\n// ---------------------------------------------------------------------------\n// THIS FILE'S OWN TEST IS LOAD-BEARING FOR ALL TWENTY SITES\n// ---------------------------------------------------------------------------\n//\n// Measured on this tree (2026-10-07, the take-over's re-run — the recipe, not the\n// history: apply the injection in a scratch copy and run the suite):\n//\n//   - guard weakened to `r.ok === false` (a REFUSAL stops throwing and a success starts\n//     throwing; an absent reply still throws through the `r &&` short-circuit):\n//     **120 of 1002** tests redden, across **17 suites**;\n//   - the FULL no-op neuter (the helper's body replaced by a bare return): **90 tests**\n//     redden across **17 failed suites** — the sharper probe, and the one the\n//     absent-reply cases redden under (they stay green under the guard-weakening,\n//     which is the reason they exist).\n//\n// (An earlier take-over draft recorded 86 across 42 — a broken probe whose import\n// errors failed suites before their tests ran; the 90/17 pair is the stable re-run,\n// twice.)\n//\n// (An earlier version of this paragraph said \"nine of the twelve sites' suites\", which\n// mixes sites with suites — the exact mistake the census block above is headed about —\n// and omitted `secrets` and `spacesMode` from the green list.)\n//\n// Each inline form this replaced carried its own `!res ||` guard, visible at the call site.\n// That guarantee now lives here alone. Do not weaken `protocolRefusal.test.ts` on the\n// grounds that \"the consumers cover it\" — they do not, and fifteen near-duplicate\n// malformed-reply tests would be the duplication this extraction exists to remove.\n//\n// ---------------------------------------------------------------------------\n// WHICH `ok` THIS READS, AND WHY IT MATTERS\n// ---------------------------------------------------------------------------\n//\n// It reads the DISPATCHER ENVELOPE's `ok` — the host's answer to \"did this call\n// succeed\", not a handler's own return value. site-main declares that envelope\n// (`src/editor/requestDispatcher.ts`):\n//\n//     type SpaceResult =\n//       | { ok: true; data: unknown }\n//       | { ok: false; code: string; message: string };\n//\n// and the sandbox resolves it to the SDK unmodified (`src/protocol/iframe.ts`,\n// `resolve(msg.result)`). So a refusal reaches us as `{ok:false, code, message}` — which\n// is exactly what this function unwraps — and the host produces that frame by THROWING\n// `spaceError(code, message)`. The dispatcher's `.catch` turns the throw into the frame.\n//\n// **A handler that RETURNS `{ok:false, code}` instead of throwing does not produce a\n// refusal.** The dispatcher wraps a return value as `result: {ok: true, data: <return>}`,\n// so the envelope says the call succeeded and the refusal is buried one level down, where\n// nothing looks. Two shipped handlers did this until R3-778 closed it (site-main; audited sdk#184 round 2, 2026-09-25; fixed 2026-09-28 — both now throw, and a dispatcher assertion reddens on the shape):\n//\n//   `handleOpenRepository`  returns `invalid`, `no-activation`\n//   `handleOpenLink`        returns `invalid`, `no-activation`, **`declined`**\n//\n// `declined` is the user pressing \"no\" on the host's confirmation dialog, and\n// `openExternal()` reported that to the app as a successful open, until R3-778 closed it. Driven against the built\n// SDK with each reply shape:\n//\n//     thrown    -> threw, code=no-activation\n//     returned  -> RESOLVED (the app believes the tab opened)\n//\n// That was a live defect in site-main's handlers until R3-778 closed it (2026-09-28), not here — the envelope is the contract\n// and this reads the contract. Filed as R3-778. It is written down here because this is\n// the file a host author will be pointed at when they ask what shape to reply with, and\n// the answer is: **throw `spaceError`; never return `{ok:false}`.**\n//\n// Deliberately NOT done: sniffing `res.data?.ok === false` as a second refusal shape. On a\n// SUCCESS, `data` is whatever the handler returned — so a handler whose legitimate payload\n// happens to carry `ok: false` would be turned into a refusal. It would bless the broken\n// shape and create a second contract where the spec names one.\n\n/** An error carrying a stable, host-supplied `code` the app can branch on. */\nexport interface CodedRefusalError<C extends string = string> extends Error {\n  code: C;\n  /** A provider rate-limit's wait in seconds, when the refusal carried one (R3-954). */\n  retryAfter?: number;\n}\n\n/**\n * Throw a typed, coded error unless the host's reply envelope says the call succeeded.\n *\n * `fallbackMessage` is used when the host refused without a message, and `'unknown'` is\n * the code when it refused without one — a refusal is never reported as a success just\n * because it arrived under-specified.\n *\n * `code` and `message` must be STRINGS to be used. Twenty-three sites call this;\n * **eighteen** had an inline copy it replaced (`spacesMode.ts` was written against the\n * helper in R3-708, `openBundle.ts` against it in R3-808, and `catalog.ts` folded in\n * R3-1089 once the helper learned `retryAfter` — the one site R3-954's copy had kept\n * out). They tested for PRESENCE, in **three** shapes:\n *\n * - **seven** cast the value: `(res?.code as SomeError['code']) ?? 'unknown'` —\n *   `dnd`, `editor`, `vcs`, `mounts` (×3), `secrets`;\n * - **seven** used the `in` operator: `((res && 'code' in res ? res.code : undefined) as\n *   Code) ?? 'unknown'` — `openExternal` and `openRepository` (folded in R3-708) and\n *   `feed`, `netFetch` and `theme` (×3) (folded in R3-817, #192);\n * - **four** used a bare `res?.code ?? 'unknown'` onto `Error & { code?: string }` —\n *   `ipc` (×2), `recents`, `tasks` (folded in R3-816).\n *\n * Every one of them let a host sending `code: 42` surface `err.code === 42` on a field\n * declared `string`, and turned `message: 42` into the string `\"42\"`. Both now fall back,\n * to `'unknown'` and `fallbackMessage`. A deliberate tightening, pinned by\n * `protocolRefusal.test.ts` — and since this package ships no changelog, this paragraph is\n * where a consumer learns of it.\n *\n * The `ipc.ts` pair changed one more thing: their anonymous type declared `code` OPTIONAL\n * and `CodedRefusalError` requires it. No runtime difference for a missing code — the old\n * code assigned `'unknown'` exactly when the new one does (a non-string `code` falls back\n * too, as the paragraph above discloses) — but a consumer narrowing on `'code' in err`\n * can stop.\n *\n * Folding the two `in`-operator sites also removed a latent `TypeError`: `'code' in res`\n * throws on a string reply.\n *\n * ONE revision of this paragraph got that backwards, and this is the fourth attempt to\n * describe it accurately. The original text was right FOR ITS REFERENT — it named\n * `'code' in res` as a shape replaced, without saying which sites, and at the time the\n * sites it referred to were R3-708's four, two of which used that form. R3-780 then folded\n * five DIFFERENT files and inherited the sentence unchanged, so it became false in place\n * without anyone editing it. That silent shift is the likeliest reason the next revision\n * \"corrected\" it to say NO replaced site used it — which is false the other way:\n * `openExternal` and `openRepository` did.\n *\n * The mechanism is worth more than the correction, because it is repeatable. `'code' in\n * res` is GONE from `main` — R3-817 (merged as #192, 0.74.3) folded the five sites that\n * carried it (`feed`, `netFetch`, `theme` ×3), beside R3-708's two (`openExternal`,\n * `openRepository`). A grep of `main` today finds the form only in this file's comments\n * and in test files. The tree that shows the form at the sites is the one before the\n * folds, `d4b07cc1^`. Read that before rewriting this paragraph again.\n *\n * It is an ASSERTION function, not a `void` one, because the inline form it replaced\n * narrowed `res` as a side effect of its `if`/`throw`: `secrets.ts` and `mounts.ts` read\n * `res.data` on the next line and stopped compiling without it. A caller that only needs\n * the throw can ignore the narrowing; one that reads the payload gets it for free. (TS\n * requires the call target to be an explicitly-typed declared name — this is why it is a\n * `function` declaration and not an arrow const.)\n */\nexport function throwOnRefusal(res: unknown, fallbackMessage: string): asserts res is { ok: true; data?: unknown } {\n  const r = res as { ok?: unknown; code?: unknown; message?: unknown; retryAfter?: unknown } | null | undefined;\n  if (r && r.ok === true) return;\n  const err = new Error(typeof r?.message === 'string' ? r.message : fallbackMessage) as CodedRefusalError;\n  err.code = typeof r?.code === 'string' ? r.code : 'unknown';\n  // R3-1089: R3-954's retryAfter (a provider rate-limit's wait in seconds) rides\n  // the refusal here too, so catalog.ts's invoke() folds like every other site.\n  // Number.isFinite, not typeof: NaN and Infinity are numbers the\n  // structured-clone transport carries intact.\n  if (Number.isFinite(r?.retryAfter)) err.retryAfter = (r as { retryAfter: number }).retryAfter;\n  throw err;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAmLO,SAAS,eAAe,KAAc,iBAAsE;AACjH,QAAM,IAAI;AACV,MAAI,KAAK,EAAE,OAAO,KAAM;AACxB,QAAM,MAAM,IAAI,MAAM,OAAO,GAAG,YAAY,WAAW,EAAE,UAAU,eAAe;AAClF,MAAI,OAAO,OAAO,GAAG,SAAS,WAAW,EAAE,OAAO;AAKlD,MAAI,OAAO,SAAS,GAAG,UAAU,EAAG,KAAI,aAAc,EAA6B;AACnF,QAAM;AACR;","names":[]}