@immediately-run/sdk 0.46.0 → 0.47.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.
Files changed (189) hide show
  1. package/dist/MDXProvider.cjs +1 -5
  2. package/dist/MDXProvider.cjs.map +1 -1
  3. package/dist/MDXProvider.js +1 -5
  4. package/dist/MDXProvider.js.map +1 -1
  5. package/dist/RoutingSpec.cjs.map +1 -1
  6. package/dist/TinkerableContext.cjs.map +1 -1
  7. package/dist/TinkerableContext.js.map +1 -1
  8. package/dist/auth.cjs.map +1 -1
  9. package/dist/auth.js.map +1 -1
  10. package/dist/boot.cjs +2 -9
  11. package/dist/boot.cjs.map +1 -1
  12. package/dist/boot.d.cts +2 -2
  13. package/dist/boot.d.ts +2 -2
  14. package/dist/boot.js +2 -9
  15. package/dist/boot.js.map +1 -1
  16. package/dist/catalog.cjs.map +1 -1
  17. package/dist/catalog.js.map +1 -1
  18. package/dist/components/Admonition.cjs +4 -13
  19. package/dist/components/Admonition.cjs.map +1 -1
  20. package/dist/components/Admonition.js +4 -13
  21. package/dist/components/Admonition.js.map +1 -1
  22. package/dist/components/FileRouter.cjs +11 -3
  23. package/dist/components/FileRouter.cjs.map +1 -1
  24. package/dist/components/FileRouter.js +11 -3
  25. package/dist/components/FileRouter.js.map +1 -1
  26. package/dist/components/HeadingAnchor.cjs.map +1 -1
  27. package/dist/components/HeadingAnchor.js.map +1 -1
  28. package/dist/components/Link.cjs.map +1 -1
  29. package/dist/components/Link.js.map +1 -1
  30. package/dist/components/MainContent.cjs +11 -1
  31. package/dist/components/MainContent.cjs.map +1 -1
  32. package/dist/components/MainContent.js +11 -1
  33. package/dist/components/MainContent.js.map +1 -1
  34. package/dist/components/MountImage.cjs +1 -9
  35. package/dist/components/MountImage.cjs.map +1 -1
  36. package/dist/components/MountImage.js +1 -9
  37. package/dist/components/MountImage.js.map +1 -1
  38. package/dist/components/Routes.cjs +1 -4
  39. package/dist/components/Routes.cjs.map +1 -1
  40. package/dist/components/Routes.d.cts +1 -1
  41. package/dist/components/Routes.d.ts +1 -1
  42. package/dist/components/Routes.js +1 -4
  43. package/dist/components/Routes.js.map +1 -1
  44. package/dist/components/SafeInclude.cjs +1 -4
  45. package/dist/components/SafeInclude.cjs.map +1 -1
  46. package/dist/components/SafeInclude.js +1 -4
  47. package/dist/components/SafeInclude.js.map +1 -1
  48. package/dist/components/WikiLink.cjs +1 -10
  49. package/dist/components/WikiLink.cjs.map +1 -1
  50. package/dist/components/WikiLink.js +1 -10
  51. package/dist/components/WikiLink.js.map +1 -1
  52. package/dist/components/defaults.cjs.map +1 -1
  53. package/dist/components/defaults.d.cts +1 -1
  54. package/dist/components/defaults.d.ts +1 -1
  55. package/dist/components/defaults.js.map +1 -1
  56. package/dist/components/errors.cjs +3 -1
  57. package/dist/components/errors.cjs.map +1 -1
  58. package/dist/components/errors.js +3 -1
  59. package/dist/components/errors.js.map +1 -1
  60. package/dist/contextUtils.cjs.map +1 -1
  61. package/dist/contextUtils.js.map +1 -1
  62. package/dist/contribute.cjs.map +1 -1
  63. package/dist/contribute.js.map +1 -1
  64. package/dist/debug.cjs +2 -1
  65. package/dist/debug.cjs.map +1 -1
  66. package/dist/debug.js +3 -8
  67. package/dist/debug.js.map +1 -1
  68. package/dist/diagnostics.cjs.map +1 -1
  69. package/dist/diagnostics.js.map +1 -1
  70. package/dist/editor.cjs.map +1 -1
  71. package/dist/editor.js.map +1 -1
  72. package/dist/editorContext.cjs.map +1 -1
  73. package/dist/editorContext.js.map +1 -1
  74. package/dist/fs.cjs.map +1 -1
  75. package/dist/fs.js.map +1 -1
  76. package/dist/hostTransport.cjs.map +1 -1
  77. package/dist/hostTransport.js.map +1 -1
  78. package/dist/index.cjs.map +1 -1
  79. package/dist/index.d.cts +1 -1
  80. package/dist/index.d.ts +1 -1
  81. package/dist/index.js.map +1 -1
  82. package/dist/injectedBundler.cjs.map +1 -1
  83. package/dist/injectedBundler.js.map +1 -1
  84. package/dist/ipc.cjs +1 -4
  85. package/dist/ipc.cjs.map +1 -1
  86. package/dist/ipc.js +1 -4
  87. package/dist/ipc.js.map +1 -1
  88. package/dist/irMarkers.cjs.map +1 -1
  89. package/dist/irMarkers.d.cts +11 -11
  90. package/dist/irMarkers.d.ts +11 -11
  91. package/dist/irMarkers.js.map +1 -1
  92. package/dist/launch.cjs.map +1 -1
  93. package/dist/launch.js.map +1 -1
  94. package/dist/llm.cjs +9 -6
  95. package/dist/llm.cjs.map +1 -1
  96. package/dist/llm.d.cts +38 -1
  97. package/dist/llm.d.ts +38 -1
  98. package/dist/llm.js +8 -6
  99. package/dist/llm.js.map +1 -1
  100. package/dist/loading.cjs +18 -14
  101. package/dist/loading.cjs.map +1 -1
  102. package/dist/loading.d.cts +2 -2
  103. package/dist/loading.d.ts +2 -2
  104. package/dist/loading.js +19 -19
  105. package/dist/loading.js.map +1 -1
  106. package/dist/markers.cjs.map +1 -1
  107. package/dist/markers.js.map +1 -1
  108. package/dist/metadataSource.cjs +1 -5
  109. package/dist/metadataSource.cjs.map +1 -1
  110. package/dist/metadataSource.d.cts +1 -1
  111. package/dist/metadataSource.d.ts +1 -1
  112. package/dist/metadataSource.js +1 -5
  113. package/dist/metadataSource.js.map +1 -1
  114. package/dist/moduleCache.cjs +4 -1
  115. package/dist/moduleCache.cjs.map +1 -1
  116. package/dist/moduleCache.d.cts +1 -1
  117. package/dist/moduleCache.d.ts +1 -1
  118. package/dist/moduleCache.js +4 -1
  119. package/dist/moduleCache.js.map +1 -1
  120. package/dist/mountMatch.cjs.map +1 -1
  121. package/dist/mountMatch.js.map +1 -1
  122. package/dist/mounts.cjs +6 -1
  123. package/dist/mounts.cjs.map +1 -1
  124. package/dist/mounts.d.cts +2 -2
  125. package/dist/mounts.d.ts +2 -2
  126. package/dist/mounts.js +6 -1
  127. package/dist/mounts.js.map +1 -1
  128. package/dist/netFetch.cjs +3 -5
  129. package/dist/netFetch.cjs.map +1 -1
  130. package/dist/netFetch.js +3 -5
  131. package/dist/netFetch.js.map +1 -1
  132. package/dist/onFsChange.cjs.map +1 -1
  133. package/dist/onFsChange.js.map +1 -1
  134. package/dist/pathUtils.cjs +9 -12
  135. package/dist/pathUtils.cjs.map +1 -1
  136. package/dist/pathUtils.js +9 -12
  137. package/dist/pathUtils.js.map +1 -1
  138. package/dist/protocolDeadline.cjs.map +1 -1
  139. package/dist/protocolDeadline.js.map +1 -1
  140. package/dist/protocolStream.cjs +2 -13
  141. package/dist/protocolStream.cjs.map +1 -1
  142. package/dist/protocolStream.js +2 -13
  143. package/dist/protocolStream.js.map +1 -1
  144. package/dist/ready.cjs.map +1 -1
  145. package/dist/ready.js.map +1 -1
  146. package/dist/routing.cjs +3 -1
  147. package/dist/routing.cjs.map +1 -1
  148. package/dist/routing.js +3 -1
  149. package/dist/routing.js.map +1 -1
  150. package/dist/safeContent/index.cjs.map +1 -1
  151. package/dist/safeContent/index.js.map +1 -1
  152. package/dist/safeContent/parseSafeMdast.cjs.map +1 -1
  153. package/dist/safeContent/parseSafeMdast.js.map +1 -1
  154. package/dist/safeContent/renderMdast.cjs.map +1 -1
  155. package/dist/safeContent/renderMdast.js.map +1 -1
  156. package/dist/sandboxTypes.cjs.map +1 -1
  157. package/dist/sandboxUtils.cjs +1 -6
  158. package/dist/sandboxUtils.cjs.map +1 -1
  159. package/dist/sandboxUtils.js +1 -6
  160. package/dist/sandboxUtils.js.map +1 -1
  161. package/dist/scrollToId.cjs.map +1 -1
  162. package/dist/scrollToId.js.map +1 -1
  163. package/dist/secrets.cjs.map +1 -1
  164. package/dist/secrets.js.map +1 -1
  165. package/dist/tasks.cjs +12 -2
  166. package/dist/tasks.cjs.map +1 -1
  167. package/dist/tasks.js +12 -2
  168. package/dist/tasks.js.map +1 -1
  169. package/dist/testing.cjs +1 -3
  170. package/dist/testing.cjs.map +1 -1
  171. package/dist/testing.js +1 -3
  172. package/dist/testing.js.map +1 -1
  173. package/dist/theme.cjs.map +1 -1
  174. package/dist/theme.js.map +1 -1
  175. package/dist/urlUtils.cjs +7 -18
  176. package/dist/urlUtils.cjs.map +1 -1
  177. package/dist/urlUtils.js +7 -18
  178. package/dist/urlUtils.js.map +1 -1
  179. package/dist/vcs.cjs +1 -3
  180. package/dist/vcs.cjs.map +1 -1
  181. package/dist/vcs.js +1 -3
  182. package/dist/vcs.js.map +1 -1
  183. package/dist/version.cjs +1 -1
  184. package/dist/version.cjs.map +1 -1
  185. package/dist/version.d.cts +1 -1
  186. package/dist/version.d.ts +1 -1
  187. package/dist/version.js +1 -1
  188. package/dist/version.js.map +1 -1
  189. package/package.json +10 -4
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/routing.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { use, useContext } from 'react';\n\nimport { sendMessage } from './sandboxUtils';\nimport { NavigationState, TinkerableContext } from './TinkerableContext';\nimport { RouteParams, RoutingRule, RoutingSpec } from './RoutingSpec';\nimport { matchRoute } from './routeMatch';\nimport { constructUrl, isAbsolutePath, parseTarget } from './urlUtils';\nimport { joinPaths } from './pathUtils';\nimport { URLCHANGE } from './generated/protocol';\n\n/** The result of matching a path: the winning {@link RoutingRule} plus its captured params. */\nexport type AppliedRoutingRule = {\n routingRule: RoutingRule,\n pathParameters?: Record<string, string>;\n}\n\n/** Build the full outer href for an in-app target (absolute `sandboxPath` or a\n * path relative to the current route), e.g. for an `href` attribute. */\nexport const useTinkerableLink = (newSandboxLocation: string) => {\n const { outerHref, navigationState: navigation } = use(TinkerableContext);\n let newNavigationState = parseTarget(newSandboxLocation, navigation);\n if (!isAbsolutePath(newSandboxLocation)) {\n newNavigationState.sandboxPath = joinPaths(navigation.sandboxPath, newSandboxLocation)\n } else {\n newNavigationState.sandboxPath = newSandboxLocation\n }\n return constructUrl(outerHref, newNavigationState);\n}\n\n/** Find the first rule in `routingSpec` whose pattern matches the current\n * `sandboxPath`, returning it with the captured params (or `undefined`). */\nexport const applyRoutingRule = (routingSpec:RoutingSpec, navigationState: NavigationState): AppliedRoutingRule | undefined => {\n const { sandboxPath } = navigationState;\n for (const routingRule of routingSpec.routes) {\n const pathParameters = matchRoute(routingRule.pattern, sandboxPath);\n if (pathParameters) {\n return { routingRule, pathParameters };\n }\n }\n return undefined;\n}\n\n/** Render a matched rule, passing params to a `component` and falling back to `element`/`reactNode`. */\nexport const renderRoute = (routingRule: RoutingRule, params: RouteParams): ReactNode => {\n if (routingRule.component) {\n const Component = routingRule.component;\n return <Component params={params} />;\n }\n return routingRule.element ?? routingRule.reactNode ?? null;\n};\n\n/** Render the route matched for the current location (set up by `boot`'s route table). */\nexport const Router = () => {\n const context = useContext(TinkerableContext);\n const {navigationState: {routingRule, pathParameters}} = context;\n if (!routingRule) {\n // TODO: better error\n throw new Error(`No route registered for path ${context.navigationState.sandboxPath}!`);\n }\n\n return renderRoute(routingRule, pathParameters ?? {});\n};\n\n/** Read the current route's matched params (`:name` segments and the `*` wildcard). */\nexport const useRouteParams = <T extends RouteParams = RouteParams>(): T =>\n (use(TinkerableContext).navigationState.pathParameters ?? {}) as T;\n\n/**\n * Read the current route: the matched rule's `name`, its `params`, the app-owned\n * `sandboxPath`, and the read-only platform prefix fields (`mode`, `provider`,\n * `namespace`, `repository`, `ref`) — e.g. to tell `/edit` from `/present`.\n */\nexport const useRoute = () => {\n const { navigationState } = use(TinkerableContext);\n const { routingRule, pathParameters, sandboxPath, mode, provider, namespace, repository, ref } = navigationState;\n return {\n name: routingRule?.name,\n params: (pathParameters ?? {}) as RouteParams,\n sandboxPath,\n mode,\n provider,\n namespace,\n repository,\n ref,\n };\n};\n\n\n/**\n * Navigate within the app. Messages the host to update the URL; the host then\n * pushes the new href back, which drives the actual route change.\n *\n * `opts.viewedDocument` (R3-268) optionally declares which WORKING-TREE file\n * this destination renders — a tri-state rider on the navigation event:\n * - omit the option entirely → the host derives the hint from the URL's\n * `files/` suffix convention (the zero-SDK default);\n * - `null` → this view shows no file (clears the highlight — tag pages,\n * search, home views);\n * - a repo-relative path (the CORPUS path under dispatch — only the viewer\n * can map its own key space) → the file explorer highlights it.\n * The hint is highlight-only by contract (it never scrolls, never moves focus\n * or panes, never switches the editor) and is validated host-side for\n * existence — a wrong path degrades to \"no highlight\", never an error. The\n * host remembers declarations per URL, so back/forward reproduces them\n * without re-announcement.\n */\n// R3-268: an app-registered rule mapping a navigation TARGET to its viewed\n// document, consulted by `navigate()` whenever the caller did not declare one\n// explicitly. Registered ONCE (e.g. at boot) so an app whose links all flow\n// through `<Link>`/`navigate` gets correct declarations everywhere without\n// threading an option through every call site. Return `undefined` for \"no\n// declaration\" (the host falls back to the URL convention), `null` for \"this\n// view shows no file\", or a working-tree repo-relative path.\nlet viewedDocumentResolver: ((targetHref: string) => string | null | undefined) | null = null;\n\n/** Register the app's route→viewed-document rule (R3-268); pass `null` to clear. */\nexport const setViewedDocumentResolver = (\n resolver: ((targetHref: string) => string | null | undefined) | null,\n): void => {\n viewedDocumentResolver = resolver;\n};\n\nexport const navigate = (\n target: string,\n opts?: { viewedDocument?: string | null },\n) => {\n console.log(`[Sandbox] Navigating to ${target}`)\n // Explicit option first; else the registered resolver; else nothing on the\n // wire (the host derives from the URL convention). A resolver throw is\n // swallowed to \"no declaration\" — a mapping bug must never break navigation.\n let declared: { viewedDocument: string | null } | Record<string, never> = {};\n if (opts && 'viewedDocument' in opts) {\n declared = { viewedDocument: opts.viewedDocument ?? null };\n } else if (viewedDocumentResolver) {\n try {\n const v = viewedDocumentResolver(target);\n if (v !== undefined) declared = { viewedDocument: v };\n } catch {\n /* no declaration */\n }\n }\n sendMessage(URLCHANGE, {\n url: target,\n back: false,\n forward: false,\n ...declared,\n });\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA+CW;AA9CX,mBAAgC;AAEhC,0BAA4B;AAC5B,+BAAmD;AAEnD,wBAA2B;AAC3B,sBAA0D;AAC1D,uBAA0B;AAC1B,sBAA0B;AAUnB,MAAM,oBAAoB,CAAC,uBAA+B;AAC/D,QAAM,EAAE,WAAW,iBAAiB,WAAW,QAAI,kBAAI,0CAAiB;AACxE,MAAI,yBAAqB,6BAAY,oBAAoB,UAAU;AACnE,MAAI,KAAC,gCAAe,kBAAkB,GAAG;AACvC,uBAAmB,kBAAc,4BAAU,WAAW,aAAa,kBAAkB;AAAA,EACvF,OAAO;AACL,uBAAmB,cAAc;AAAA,EACnC;AACA,aAAO,8BAAa,WAAW,kBAAkB;AACnD;AAIO,MAAM,mBAAmB,CAAC,aAAyB,oBAAqE;AAC7H,QAAM,EAAE,YAAY,IAAI;AACxB,aAAW,eAAe,YAAY,QAAQ;AAC5C,UAAM,qBAAiB,8BAAW,YAAY,SAAS,WAAW;AAClE,QAAI,gBAAgB;AAClB,aAAO,EAAE,aAAa,eAAe;AAAA,IACvC;AAAA,EACF;AACA,SAAO;AACT;AAGO,MAAM,cAAc,CAAC,aAA0B,WAAmC;AACvF,MAAI,YAAY,WAAW;AACzB,UAAM,YAAY,YAAY;AAC9B,WAAO,4CAAC,aAAU,QAAgB;AAAA,EACpC;AACA,SAAO,YAAY,WAAW,YAAY,aAAa;AACzD;AAGO,MAAM,SAAS,MAAM;AAC1B,QAAM,cAAU,yBAAW,0CAAiB;AAC5C,QAAM,EAAC,iBAAiB,EAAC,aAAa,eAAc,EAAC,IAAI;AACzD,MAAI,CAAC,aAAa;AAEhB,UAAM,IAAI,MAAM,gCAAgC,QAAQ,gBAAgB,WAAW,GAAG;AAAA,EACxF;AAEA,SAAO,YAAY,aAAa,kBAAkB,CAAC,CAAC;AACtD;AAGO,MAAM,iBAAiB,UAC3B,kBAAI,0CAAiB,EAAE,gBAAgB,kBAAkB,CAAC;AAOtD,MAAM,WAAW,MAAM;AAC5B,QAAM,EAAE,gBAAgB,QAAI,kBAAI,0CAAiB;AACjD,QAAM,EAAE,aAAa,gBAAgB,aAAa,MAAM,UAAU,WAAW,YAAY,IAAI,IAAI;AACjG,SAAO;AAAA,IACL,MAAM,aAAa;AAAA,IACnB,QAAS,kBAAkB,CAAC;AAAA,IAC5B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AA4BA,IAAI,yBAAqF;AAGlF,MAAM,4BAA4B,CACvC,aACS;AACT,2BAAyB;AAC3B;AAEO,MAAM,WAAW,CACtB,QACA,SACG;AACH,UAAQ,IAAI,2BAA2B,MAAM,EAAE;AAI/C,MAAI,WAAsE,CAAC;AAC3E,MAAI,QAAQ,oBAAoB,MAAM;AACpC,eAAW,EAAE,gBAAgB,KAAK,kBAAkB,KAAK;AAAA,EAC3D,WAAW,wBAAwB;AACjC,QAAI;AACF,YAAM,IAAI,uBAAuB,MAAM;AACvC,UAAI,MAAM,OAAW,YAAW,EAAE,gBAAgB,EAAE;AAAA,IACtD,QAAQ;AAAA,IAER;AAAA,EACF;AACA,uCAAY,2BAAW;AAAA,IACrB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,GAAG;AAAA,EACL,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../src/routing.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { use, useContext } from 'react';\n\nimport { sendMessage } from './sandboxUtils';\nimport { NavigationState, TinkerableContext } from './TinkerableContext';\nimport { RouteParams, RoutingRule, RoutingSpec } from './RoutingSpec';\nimport { matchRoute } from './routeMatch';\nimport { constructUrl, isAbsolutePath, parseTarget } from './urlUtils';\nimport { joinPaths } from './pathUtils';\nimport { URLCHANGE } from './generated/protocol';\n\n/** The result of matching a path: the winning {@link RoutingRule} plus its captured params. */\nexport type AppliedRoutingRule = {\n routingRule: RoutingRule;\n pathParameters?: Record<string, string>;\n};\n\n/** Build the full outer href for an in-app target (absolute `sandboxPath` or a\n * path relative to the current route), e.g. for an `href` attribute. */\nexport const useTinkerableLink = (newSandboxLocation: string) => {\n const { outerHref, navigationState: navigation } = use(TinkerableContext);\n let newNavigationState = parseTarget(newSandboxLocation, navigation);\n if (!isAbsolutePath(newSandboxLocation)) {\n newNavigationState.sandboxPath = joinPaths(navigation.sandboxPath, newSandboxLocation);\n } else {\n newNavigationState.sandboxPath = newSandboxLocation;\n }\n return constructUrl(outerHref, newNavigationState);\n};\n\n/** Find the first rule in `routingSpec` whose pattern matches the current\n * `sandboxPath`, returning it with the captured params (or `undefined`). */\nexport const applyRoutingRule = (\n routingSpec: RoutingSpec,\n navigationState: NavigationState,\n): AppliedRoutingRule | undefined => {\n const { sandboxPath } = navigationState;\n for (const routingRule of routingSpec.routes) {\n const pathParameters = matchRoute(routingRule.pattern, sandboxPath);\n if (pathParameters) {\n return { routingRule, pathParameters };\n }\n }\n return undefined;\n};\n\n/** Render a matched rule, passing params to a `component` and falling back to `element`/`reactNode`. */\nexport const renderRoute = (routingRule: RoutingRule, params: RouteParams): ReactNode => {\n if (routingRule.component) {\n const Component = routingRule.component;\n return <Component params={params} />;\n }\n return routingRule.element ?? routingRule.reactNode ?? null;\n};\n\n/** Render the route matched for the current location (set up by `boot`'s route table). */\nexport const Router = () => {\n const context = useContext(TinkerableContext);\n const {\n navigationState: { routingRule, pathParameters },\n } = context;\n if (!routingRule) {\n // TODO: better error\n throw new Error(`No route registered for path ${context.navigationState.sandboxPath}!`);\n }\n\n return renderRoute(routingRule, pathParameters ?? {});\n};\n\n/** Read the current route's matched params (`:name` segments and the `*` wildcard). */\nexport const useRouteParams = <T extends RouteParams = RouteParams>(): T =>\n (use(TinkerableContext).navigationState.pathParameters ?? {}) as T;\n\n/**\n * Read the current route: the matched rule's `name`, its `params`, the app-owned\n * `sandboxPath`, and the read-only platform prefix fields (`mode`, `provider`,\n * `namespace`, `repository`, `ref`) — e.g. to tell `/edit` from `/present`.\n */\nexport const useRoute = () => {\n const { navigationState } = use(TinkerableContext);\n const { routingRule, pathParameters, sandboxPath, mode, provider, namespace, repository, ref } = navigationState;\n return {\n name: routingRule?.name,\n params: (pathParameters ?? {}) as RouteParams,\n sandboxPath,\n mode,\n provider,\n namespace,\n repository,\n ref,\n };\n};\n\n/**\n * Navigate within the app. Messages the host to update the URL; the host then\n * pushes the new href back, which drives the actual route change.\n *\n * `opts.viewedDocument` (R3-268) optionally declares which WORKING-TREE file\n * this destination renders — a tri-state rider on the navigation event:\n * - omit the option entirely → the host derives the hint from the URL's\n * `files/` suffix convention (the zero-SDK default);\n * - `null` → this view shows no file (clears the highlight — tag pages,\n * search, home views);\n * - a repo-relative path (the CORPUS path under dispatch — only the viewer\n * can map its own key space) → the file explorer highlights it.\n * The hint is highlight-only by contract (it never scrolls, never moves focus\n * or panes, never switches the editor) and is validated host-side for\n * existence — a wrong path degrades to \"no highlight\", never an error. The\n * host remembers declarations per URL, so back/forward reproduces them\n * without re-announcement.\n */\n// R3-268: an app-registered rule mapping a navigation TARGET to its viewed\n// document, consulted by `navigate()` whenever the caller did not declare one\n// explicitly. Registered ONCE (e.g. at boot) so an app whose links all flow\n// through `<Link>`/`navigate` gets correct declarations everywhere without\n// threading an option through every call site. Return `undefined` for \"no\n// declaration\" (the host falls back to the URL convention), `null` for \"this\n// view shows no file\", or a working-tree repo-relative path.\nlet viewedDocumentResolver: ((targetHref: string) => string | null | undefined) | null = null;\n\n/** Register the app's route→viewed-document rule (R3-268); pass `null` to clear. */\nexport const setViewedDocumentResolver = (\n resolver: ((targetHref: string) => string | null | undefined) | null,\n): void => {\n viewedDocumentResolver = resolver;\n};\n\nexport const navigate = (target: string, opts?: { viewedDocument?: string | null }) => {\n console.log(`[Sandbox] Navigating to ${target}`);\n // Explicit option first; else the registered resolver; else nothing on the\n // wire (the host derives from the URL convention). A resolver throw is\n // swallowed to \"no declaration\" — a mapping bug must never break navigation.\n let declared: { viewedDocument: string | null } | Record<string, never> = {};\n if (opts && 'viewedDocument' in opts) {\n declared = { viewedDocument: opts.viewedDocument ?? null };\n } else if (viewedDocumentResolver) {\n try {\n const v = viewedDocumentResolver(target);\n if (v !== undefined) declared = { viewedDocument: v };\n } catch {\n /* no declaration */\n }\n }\n sendMessage(URLCHANGE, {\n url: target,\n back: false,\n forward: false,\n ...declared,\n });\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkDW;AAjDX,mBAAgC;AAEhC,0BAA4B;AAC5B,+BAAmD;AAEnD,wBAA2B;AAC3B,sBAA0D;AAC1D,uBAA0B;AAC1B,sBAA0B;AAUnB,MAAM,oBAAoB,CAAC,uBAA+B;AAC/D,QAAM,EAAE,WAAW,iBAAiB,WAAW,QAAI,kBAAI,0CAAiB;AACxE,MAAI,yBAAqB,6BAAY,oBAAoB,UAAU;AACnE,MAAI,KAAC,gCAAe,kBAAkB,GAAG;AACvC,uBAAmB,kBAAc,4BAAU,WAAW,aAAa,kBAAkB;AAAA,EACvF,OAAO;AACL,uBAAmB,cAAc;AAAA,EACnC;AACA,aAAO,8BAAa,WAAW,kBAAkB;AACnD;AAIO,MAAM,mBAAmB,CAC9B,aACA,oBACmC;AACnC,QAAM,EAAE,YAAY,IAAI;AACxB,aAAW,eAAe,YAAY,QAAQ;AAC5C,UAAM,qBAAiB,8BAAW,YAAY,SAAS,WAAW;AAClE,QAAI,gBAAgB;AAClB,aAAO,EAAE,aAAa,eAAe;AAAA,IACvC;AAAA,EACF;AACA,SAAO;AACT;AAGO,MAAM,cAAc,CAAC,aAA0B,WAAmC;AACvF,MAAI,YAAY,WAAW;AACzB,UAAM,YAAY,YAAY;AAC9B,WAAO,4CAAC,aAAU,QAAgB;AAAA,EACpC;AACA,SAAO,YAAY,WAAW,YAAY,aAAa;AACzD;AAGO,MAAM,SAAS,MAAM;AAC1B,QAAM,cAAU,yBAAW,0CAAiB;AAC5C,QAAM;AAAA,IACJ,iBAAiB,EAAE,aAAa,eAAe;AAAA,EACjD,IAAI;AACJ,MAAI,CAAC,aAAa;AAEhB,UAAM,IAAI,MAAM,gCAAgC,QAAQ,gBAAgB,WAAW,GAAG;AAAA,EACxF;AAEA,SAAO,YAAY,aAAa,kBAAkB,CAAC,CAAC;AACtD;AAGO,MAAM,iBAAiB,UAC3B,kBAAI,0CAAiB,EAAE,gBAAgB,kBAAkB,CAAC;AAOtD,MAAM,WAAW,MAAM;AAC5B,QAAM,EAAE,gBAAgB,QAAI,kBAAI,0CAAiB;AACjD,QAAM,EAAE,aAAa,gBAAgB,aAAa,MAAM,UAAU,WAAW,YAAY,IAAI,IAAI;AACjG,SAAO;AAAA,IACL,MAAM,aAAa;AAAA,IACnB,QAAS,kBAAkB,CAAC;AAAA,IAC5B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AA2BA,IAAI,yBAAqF;AAGlF,MAAM,4BAA4B,CACvC,aACS;AACT,2BAAyB;AAC3B;AAEO,MAAM,WAAW,CAAC,QAAgB,SAA8C;AACrF,UAAQ,IAAI,2BAA2B,MAAM,EAAE;AAI/C,MAAI,WAAsE,CAAC;AAC3E,MAAI,QAAQ,oBAAoB,MAAM;AACpC,eAAW,EAAE,gBAAgB,KAAK,kBAAkB,KAAK;AAAA,EAC3D,WAAW,wBAAwB;AACjC,QAAI;AACF,YAAM,IAAI,uBAAuB,MAAM;AACvC,UAAI,MAAM,OAAW,YAAW,EAAE,gBAAgB,EAAE;AAAA,IACtD,QAAQ;AAAA,IAER;AAAA,EACF;AACA,uCAAY,2BAAW;AAAA,IACrB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,GAAG;AAAA,EACL,CAAC;AACH;","names":[]}
package/dist/routing.js CHANGED
@@ -36,7 +36,9 @@ const renderRoute = (routingRule, params) => {
36
36
  };
37
37
  const Router = () => {
38
38
  const context = useContext(TinkerableContext);
39
- const { navigationState: { routingRule, pathParameters } } = context;
39
+ const {
40
+ navigationState: { routingRule, pathParameters }
41
+ } = context;
40
42
  if (!routingRule) {
41
43
  throw new Error(`No route registered for path ${context.navigationState.sandboxPath}!`);
42
44
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/routing.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { use, useContext } from 'react';\n\nimport { sendMessage } from './sandboxUtils';\nimport { NavigationState, TinkerableContext } from './TinkerableContext';\nimport { RouteParams, RoutingRule, RoutingSpec } from './RoutingSpec';\nimport { matchRoute } from './routeMatch';\nimport { constructUrl, isAbsolutePath, parseTarget } from './urlUtils';\nimport { joinPaths } from './pathUtils';\nimport { URLCHANGE } from './generated/protocol';\n\n/** The result of matching a path: the winning {@link RoutingRule} plus its captured params. */\nexport type AppliedRoutingRule = {\n routingRule: RoutingRule,\n pathParameters?: Record<string, string>;\n}\n\n/** Build the full outer href for an in-app target (absolute `sandboxPath` or a\n * path relative to the current route), e.g. for an `href` attribute. */\nexport const useTinkerableLink = (newSandboxLocation: string) => {\n const { outerHref, navigationState: navigation } = use(TinkerableContext);\n let newNavigationState = parseTarget(newSandboxLocation, navigation);\n if (!isAbsolutePath(newSandboxLocation)) {\n newNavigationState.sandboxPath = joinPaths(navigation.sandboxPath, newSandboxLocation)\n } else {\n newNavigationState.sandboxPath = newSandboxLocation\n }\n return constructUrl(outerHref, newNavigationState);\n}\n\n/** Find the first rule in `routingSpec` whose pattern matches the current\n * `sandboxPath`, returning it with the captured params (or `undefined`). */\nexport const applyRoutingRule = (routingSpec:RoutingSpec, navigationState: NavigationState): AppliedRoutingRule | undefined => {\n const { sandboxPath } = navigationState;\n for (const routingRule of routingSpec.routes) {\n const pathParameters = matchRoute(routingRule.pattern, sandboxPath);\n if (pathParameters) {\n return { routingRule, pathParameters };\n }\n }\n return undefined;\n}\n\n/** Render a matched rule, passing params to a `component` and falling back to `element`/`reactNode`. */\nexport const renderRoute = (routingRule: RoutingRule, params: RouteParams): ReactNode => {\n if (routingRule.component) {\n const Component = routingRule.component;\n return <Component params={params} />;\n }\n return routingRule.element ?? routingRule.reactNode ?? null;\n};\n\n/** Render the route matched for the current location (set up by `boot`'s route table). */\nexport const Router = () => {\n const context = useContext(TinkerableContext);\n const {navigationState: {routingRule, pathParameters}} = context;\n if (!routingRule) {\n // TODO: better error\n throw new Error(`No route registered for path ${context.navigationState.sandboxPath}!`);\n }\n\n return renderRoute(routingRule, pathParameters ?? {});\n};\n\n/** Read the current route's matched params (`:name` segments and the `*` wildcard). */\nexport const useRouteParams = <T extends RouteParams = RouteParams>(): T =>\n (use(TinkerableContext).navigationState.pathParameters ?? {}) as T;\n\n/**\n * Read the current route: the matched rule's `name`, its `params`, the app-owned\n * `sandboxPath`, and the read-only platform prefix fields (`mode`, `provider`,\n * `namespace`, `repository`, `ref`) — e.g. to tell `/edit` from `/present`.\n */\nexport const useRoute = () => {\n const { navigationState } = use(TinkerableContext);\n const { routingRule, pathParameters, sandboxPath, mode, provider, namespace, repository, ref } = navigationState;\n return {\n name: routingRule?.name,\n params: (pathParameters ?? {}) as RouteParams,\n sandboxPath,\n mode,\n provider,\n namespace,\n repository,\n ref,\n };\n};\n\n\n/**\n * Navigate within the app. Messages the host to update the URL; the host then\n * pushes the new href back, which drives the actual route change.\n *\n * `opts.viewedDocument` (R3-268) optionally declares which WORKING-TREE file\n * this destination renders — a tri-state rider on the navigation event:\n * - omit the option entirely → the host derives the hint from the URL's\n * `files/` suffix convention (the zero-SDK default);\n * - `null` → this view shows no file (clears the highlight — tag pages,\n * search, home views);\n * - a repo-relative path (the CORPUS path under dispatch — only the viewer\n * can map its own key space) → the file explorer highlights it.\n * The hint is highlight-only by contract (it never scrolls, never moves focus\n * or panes, never switches the editor) and is validated host-side for\n * existence — a wrong path degrades to \"no highlight\", never an error. The\n * host remembers declarations per URL, so back/forward reproduces them\n * without re-announcement.\n */\n// R3-268: an app-registered rule mapping a navigation TARGET to its viewed\n// document, consulted by `navigate()` whenever the caller did not declare one\n// explicitly. Registered ONCE (e.g. at boot) so an app whose links all flow\n// through `<Link>`/`navigate` gets correct declarations everywhere without\n// threading an option through every call site. Return `undefined` for \"no\n// declaration\" (the host falls back to the URL convention), `null` for \"this\n// view shows no file\", or a working-tree repo-relative path.\nlet viewedDocumentResolver: ((targetHref: string) => string | null | undefined) | null = null;\n\n/** Register the app's route→viewed-document rule (R3-268); pass `null` to clear. */\nexport const setViewedDocumentResolver = (\n resolver: ((targetHref: string) => string | null | undefined) | null,\n): void => {\n viewedDocumentResolver = resolver;\n};\n\nexport const navigate = (\n target: string,\n opts?: { viewedDocument?: string | null },\n) => {\n console.log(`[Sandbox] Navigating to ${target}`)\n // Explicit option first; else the registered resolver; else nothing on the\n // wire (the host derives from the URL convention). A resolver throw is\n // swallowed to \"no declaration\" — a mapping bug must never break navigation.\n let declared: { viewedDocument: string | null } | Record<string, never> = {};\n if (opts && 'viewedDocument' in opts) {\n declared = { viewedDocument: opts.viewedDocument ?? null };\n } else if (viewedDocumentResolver) {\n try {\n const v = viewedDocumentResolver(target);\n if (v !== undefined) declared = { viewedDocument: v };\n } catch {\n /* no declaration */\n }\n }\n sendMessage(URLCHANGE, {\n url: target,\n back: false,\n forward: false,\n ...declared,\n });\n};\n"],"mappings":";AA+CW;AA9CX,SAAS,KAAK,kBAAkB;AAEhC,SAAS,mBAAmB;AAC5B,SAA0B,yBAAyB;AAEnD,SAAS,kBAAkB;AAC3B,SAAS,cAAc,gBAAgB,mBAAmB;AAC1D,SAAS,iBAAiB;AAC1B,SAAS,iBAAiB;AAUnB,MAAM,oBAAoB,CAAC,uBAA+B;AAC/D,QAAM,EAAE,WAAW,iBAAiB,WAAW,IAAI,IAAI,iBAAiB;AACxE,MAAI,qBAAqB,YAAY,oBAAoB,UAAU;AACnE,MAAI,CAAC,eAAe,kBAAkB,GAAG;AACvC,uBAAmB,cAAc,UAAU,WAAW,aAAa,kBAAkB;AAAA,EACvF,OAAO;AACL,uBAAmB,cAAc;AAAA,EACnC;AACA,SAAO,aAAa,WAAW,kBAAkB;AACnD;AAIO,MAAM,mBAAmB,CAAC,aAAyB,oBAAqE;AAC7H,QAAM,EAAE,YAAY,IAAI;AACxB,aAAW,eAAe,YAAY,QAAQ;AAC5C,UAAM,iBAAiB,WAAW,YAAY,SAAS,WAAW;AAClE,QAAI,gBAAgB;AAClB,aAAO,EAAE,aAAa,eAAe;AAAA,IACvC;AAAA,EACF;AACA,SAAO;AACT;AAGO,MAAM,cAAc,CAAC,aAA0B,WAAmC;AACvF,MAAI,YAAY,WAAW;AACzB,UAAM,YAAY,YAAY;AAC9B,WAAO,oBAAC,aAAU,QAAgB;AAAA,EACpC;AACA,SAAO,YAAY,WAAW,YAAY,aAAa;AACzD;AAGO,MAAM,SAAS,MAAM;AAC1B,QAAM,UAAU,WAAW,iBAAiB;AAC5C,QAAM,EAAC,iBAAiB,EAAC,aAAa,eAAc,EAAC,IAAI;AACzD,MAAI,CAAC,aAAa;AAEhB,UAAM,IAAI,MAAM,gCAAgC,QAAQ,gBAAgB,WAAW,GAAG;AAAA,EACxF;AAEA,SAAO,YAAY,aAAa,kBAAkB,CAAC,CAAC;AACtD;AAGO,MAAM,iBAAiB,MAC3B,IAAI,iBAAiB,EAAE,gBAAgB,kBAAkB,CAAC;AAOtD,MAAM,WAAW,MAAM;AAC5B,QAAM,EAAE,gBAAgB,IAAI,IAAI,iBAAiB;AACjD,QAAM,EAAE,aAAa,gBAAgB,aAAa,MAAM,UAAU,WAAW,YAAY,IAAI,IAAI;AACjG,SAAO;AAAA,IACL,MAAM,aAAa;AAAA,IACnB,QAAS,kBAAkB,CAAC;AAAA,IAC5B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AA4BA,IAAI,yBAAqF;AAGlF,MAAM,4BAA4B,CACvC,aACS;AACT,2BAAyB;AAC3B;AAEO,MAAM,WAAW,CACtB,QACA,SACG;AACH,UAAQ,IAAI,2BAA2B,MAAM,EAAE;AAI/C,MAAI,WAAsE,CAAC;AAC3E,MAAI,QAAQ,oBAAoB,MAAM;AACpC,eAAW,EAAE,gBAAgB,KAAK,kBAAkB,KAAK;AAAA,EAC3D,WAAW,wBAAwB;AACjC,QAAI;AACF,YAAM,IAAI,uBAAuB,MAAM;AACvC,UAAI,MAAM,OAAW,YAAW,EAAE,gBAAgB,EAAE;AAAA,IACtD,QAAQ;AAAA,IAER;AAAA,EACF;AACA,cAAY,WAAW;AAAA,IACrB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,GAAG;AAAA,EACL,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../src/routing.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { use, useContext } from 'react';\n\nimport { sendMessage } from './sandboxUtils';\nimport { NavigationState, TinkerableContext } from './TinkerableContext';\nimport { RouteParams, RoutingRule, RoutingSpec } from './RoutingSpec';\nimport { matchRoute } from './routeMatch';\nimport { constructUrl, isAbsolutePath, parseTarget } from './urlUtils';\nimport { joinPaths } from './pathUtils';\nimport { URLCHANGE } from './generated/protocol';\n\n/** The result of matching a path: the winning {@link RoutingRule} plus its captured params. */\nexport type AppliedRoutingRule = {\n routingRule: RoutingRule;\n pathParameters?: Record<string, string>;\n};\n\n/** Build the full outer href for an in-app target (absolute `sandboxPath` or a\n * path relative to the current route), e.g. for an `href` attribute. */\nexport const useTinkerableLink = (newSandboxLocation: string) => {\n const { outerHref, navigationState: navigation } = use(TinkerableContext);\n let newNavigationState = parseTarget(newSandboxLocation, navigation);\n if (!isAbsolutePath(newSandboxLocation)) {\n newNavigationState.sandboxPath = joinPaths(navigation.sandboxPath, newSandboxLocation);\n } else {\n newNavigationState.sandboxPath = newSandboxLocation;\n }\n return constructUrl(outerHref, newNavigationState);\n};\n\n/** Find the first rule in `routingSpec` whose pattern matches the current\n * `sandboxPath`, returning it with the captured params (or `undefined`). */\nexport const applyRoutingRule = (\n routingSpec: RoutingSpec,\n navigationState: NavigationState,\n): AppliedRoutingRule | undefined => {\n const { sandboxPath } = navigationState;\n for (const routingRule of routingSpec.routes) {\n const pathParameters = matchRoute(routingRule.pattern, sandboxPath);\n if (pathParameters) {\n return { routingRule, pathParameters };\n }\n }\n return undefined;\n};\n\n/** Render a matched rule, passing params to a `component` and falling back to `element`/`reactNode`. */\nexport const renderRoute = (routingRule: RoutingRule, params: RouteParams): ReactNode => {\n if (routingRule.component) {\n const Component = routingRule.component;\n return <Component params={params} />;\n }\n return routingRule.element ?? routingRule.reactNode ?? null;\n};\n\n/** Render the route matched for the current location (set up by `boot`'s route table). */\nexport const Router = () => {\n const context = useContext(TinkerableContext);\n const {\n navigationState: { routingRule, pathParameters },\n } = context;\n if (!routingRule) {\n // TODO: better error\n throw new Error(`No route registered for path ${context.navigationState.sandboxPath}!`);\n }\n\n return renderRoute(routingRule, pathParameters ?? {});\n};\n\n/** Read the current route's matched params (`:name` segments and the `*` wildcard). */\nexport const useRouteParams = <T extends RouteParams = RouteParams>(): T =>\n (use(TinkerableContext).navigationState.pathParameters ?? {}) as T;\n\n/**\n * Read the current route: the matched rule's `name`, its `params`, the app-owned\n * `sandboxPath`, and the read-only platform prefix fields (`mode`, `provider`,\n * `namespace`, `repository`, `ref`) — e.g. to tell `/edit` from `/present`.\n */\nexport const useRoute = () => {\n const { navigationState } = use(TinkerableContext);\n const { routingRule, pathParameters, sandboxPath, mode, provider, namespace, repository, ref } = navigationState;\n return {\n name: routingRule?.name,\n params: (pathParameters ?? {}) as RouteParams,\n sandboxPath,\n mode,\n provider,\n namespace,\n repository,\n ref,\n };\n};\n\n/**\n * Navigate within the app. Messages the host to update the URL; the host then\n * pushes the new href back, which drives the actual route change.\n *\n * `opts.viewedDocument` (R3-268) optionally declares which WORKING-TREE file\n * this destination renders — a tri-state rider on the navigation event:\n * - omit the option entirely → the host derives the hint from the URL's\n * `files/` suffix convention (the zero-SDK default);\n * - `null` → this view shows no file (clears the highlight — tag pages,\n * search, home views);\n * - a repo-relative path (the CORPUS path under dispatch — only the viewer\n * can map its own key space) → the file explorer highlights it.\n * The hint is highlight-only by contract (it never scrolls, never moves focus\n * or panes, never switches the editor) and is validated host-side for\n * existence — a wrong path degrades to \"no highlight\", never an error. The\n * host remembers declarations per URL, so back/forward reproduces them\n * without re-announcement.\n */\n// R3-268: an app-registered rule mapping a navigation TARGET to its viewed\n// document, consulted by `navigate()` whenever the caller did not declare one\n// explicitly. Registered ONCE (e.g. at boot) so an app whose links all flow\n// through `<Link>`/`navigate` gets correct declarations everywhere without\n// threading an option through every call site. Return `undefined` for \"no\n// declaration\" (the host falls back to the URL convention), `null` for \"this\n// view shows no file\", or a working-tree repo-relative path.\nlet viewedDocumentResolver: ((targetHref: string) => string | null | undefined) | null = null;\n\n/** Register the app's route→viewed-document rule (R3-268); pass `null` to clear. */\nexport const setViewedDocumentResolver = (\n resolver: ((targetHref: string) => string | null | undefined) | null,\n): void => {\n viewedDocumentResolver = resolver;\n};\n\nexport const navigate = (target: string, opts?: { viewedDocument?: string | null }) => {\n console.log(`[Sandbox] Navigating to ${target}`);\n // Explicit option first; else the registered resolver; else nothing on the\n // wire (the host derives from the URL convention). A resolver throw is\n // swallowed to \"no declaration\" — a mapping bug must never break navigation.\n let declared: { viewedDocument: string | null } | Record<string, never> = {};\n if (opts && 'viewedDocument' in opts) {\n declared = { viewedDocument: opts.viewedDocument ?? null };\n } else if (viewedDocumentResolver) {\n try {\n const v = viewedDocumentResolver(target);\n if (v !== undefined) declared = { viewedDocument: v };\n } catch {\n /* no declaration */\n }\n }\n sendMessage(URLCHANGE, {\n url: target,\n back: false,\n forward: false,\n ...declared,\n });\n};\n"],"mappings":";AAkDW;AAjDX,SAAS,KAAK,kBAAkB;AAEhC,SAAS,mBAAmB;AAC5B,SAA0B,yBAAyB;AAEnD,SAAS,kBAAkB;AAC3B,SAAS,cAAc,gBAAgB,mBAAmB;AAC1D,SAAS,iBAAiB;AAC1B,SAAS,iBAAiB;AAUnB,MAAM,oBAAoB,CAAC,uBAA+B;AAC/D,QAAM,EAAE,WAAW,iBAAiB,WAAW,IAAI,IAAI,iBAAiB;AACxE,MAAI,qBAAqB,YAAY,oBAAoB,UAAU;AACnE,MAAI,CAAC,eAAe,kBAAkB,GAAG;AACvC,uBAAmB,cAAc,UAAU,WAAW,aAAa,kBAAkB;AAAA,EACvF,OAAO;AACL,uBAAmB,cAAc;AAAA,EACnC;AACA,SAAO,aAAa,WAAW,kBAAkB;AACnD;AAIO,MAAM,mBAAmB,CAC9B,aACA,oBACmC;AACnC,QAAM,EAAE,YAAY,IAAI;AACxB,aAAW,eAAe,YAAY,QAAQ;AAC5C,UAAM,iBAAiB,WAAW,YAAY,SAAS,WAAW;AAClE,QAAI,gBAAgB;AAClB,aAAO,EAAE,aAAa,eAAe;AAAA,IACvC;AAAA,EACF;AACA,SAAO;AACT;AAGO,MAAM,cAAc,CAAC,aAA0B,WAAmC;AACvF,MAAI,YAAY,WAAW;AACzB,UAAM,YAAY,YAAY;AAC9B,WAAO,oBAAC,aAAU,QAAgB;AAAA,EACpC;AACA,SAAO,YAAY,WAAW,YAAY,aAAa;AACzD;AAGO,MAAM,SAAS,MAAM;AAC1B,QAAM,UAAU,WAAW,iBAAiB;AAC5C,QAAM;AAAA,IACJ,iBAAiB,EAAE,aAAa,eAAe;AAAA,EACjD,IAAI;AACJ,MAAI,CAAC,aAAa;AAEhB,UAAM,IAAI,MAAM,gCAAgC,QAAQ,gBAAgB,WAAW,GAAG;AAAA,EACxF;AAEA,SAAO,YAAY,aAAa,kBAAkB,CAAC,CAAC;AACtD;AAGO,MAAM,iBAAiB,MAC3B,IAAI,iBAAiB,EAAE,gBAAgB,kBAAkB,CAAC;AAOtD,MAAM,WAAW,MAAM;AAC5B,QAAM,EAAE,gBAAgB,IAAI,IAAI,iBAAiB;AACjD,QAAM,EAAE,aAAa,gBAAgB,aAAa,MAAM,UAAU,WAAW,YAAY,IAAI,IAAI;AACjG,SAAO;AAAA,IACL,MAAM,aAAa;AAAA,IACnB,QAAS,kBAAkB,CAAC;AAAA,IAC5B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AA2BA,IAAI,yBAAqF;AAGlF,MAAM,4BAA4B,CACvC,aACS;AACT,2BAAyB;AAC3B;AAEO,MAAM,WAAW,CAAC,QAAgB,SAA8C;AACrF,UAAQ,IAAI,2BAA2B,MAAM,EAAE;AAI/C,MAAI,WAAsE,CAAC;AAC3E,MAAI,QAAQ,oBAAoB,MAAM;AACpC,eAAW,EAAE,gBAAgB,KAAK,kBAAkB,KAAK;AAAA,EAC3D,WAAW,wBAAwB;AACjC,QAAI;AACF,YAAM,IAAI,uBAAuB,MAAM;AACvC,UAAI,MAAM,OAAW,YAAW,EAAE,gBAAgB,EAAE;AAAA,IACtD,QAAQ;AAAA,IAER;AAAA,EACF;AACA,cAAY,WAAW;AAAA,IACrB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,GAAG;AAAA,EACL,CAAC;AACH;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/safeContent/index.ts"],"sourcesContent":["// Safe content rendering — non-executable MDX (TRUST_MODES_SPEC §5.1, AGENT_AUTHORING\n// §10). The host/SDK-owned renderer an INTERPRETER app uses to render untrusted\n// Markdown/MDX-syntax content as data, with **no evaluator anywhere in the pipeline**.\n// This is the mandatory terminal for the MDX-from-mount gate (§10 delta 1) — a shared\n// (multi-writer / M3) board or wiki entry MUST render through here, never compiled MDX.\n//\n// It is a **T2 tool, not a platform gate**: it stops content executing as code; it does\n// not certify anything to the host, and CO-4 data-fencing is still required for anything\n// that *reads* the content (the agent case).\n\nexport { SafeContent } from './SafeContent';\nexport type { SafeContentProps } from './SafeContent';\nexport { renderMdast } from './renderMdast';\nexport type {\n RenderMdastOptions,\n SafeContentComponents,\n} from './renderMdast';\nexport { parseSafeMdast } from './parseSafeMdast';\nexport type { SafeMdastNode, SafeMdxAttribute, ParseSafeMdastOptions } from './parseSafeMdast';\nexport { sanitizeUrl } from './sanitizeUrl';\nexport { splitWikiLinks, parseWikiInner } from './wikilink';\nexport type { WikiLinkToken, WikiPart } from './wikilink';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,yBAA4B;AAE5B,yBAA4B;AAK5B,4BAA+B;AAE/B,yBAA4B;AAC5B,sBAA+C;","names":[]}
1
+ {"version":3,"sources":["../../src/safeContent/index.ts"],"sourcesContent":["// Safe content rendering — non-executable MDX (TRUST_MODES_SPEC §5.1, AGENT_AUTHORING\n// §10). The host/SDK-owned renderer an INTERPRETER app uses to render untrusted\n// Markdown/MDX-syntax content as data, with **no evaluator anywhere in the pipeline**.\n// This is the mandatory terminal for the MDX-from-mount gate (§10 delta 1) — a shared\n// (multi-writer / M3) board or wiki entry MUST render through here, never compiled MDX.\n//\n// It is a **T2 tool, not a platform gate**: it stops content executing as code; it does\n// not certify anything to the host, and CO-4 data-fencing is still required for anything\n// that *reads* the content (the agent case).\n\nexport { SafeContent } from './SafeContent';\nexport type { SafeContentProps } from './SafeContent';\nexport { renderMdast } from './renderMdast';\nexport type { RenderMdastOptions, SafeContentComponents } from './renderMdast';\nexport { parseSafeMdast } from './parseSafeMdast';\nexport type { SafeMdastNode, SafeMdxAttribute, ParseSafeMdastOptions } from './parseSafeMdast';\nexport { sanitizeUrl } from './sanitizeUrl';\nexport { splitWikiLinks, parseWikiInner } from './wikilink';\nexport type { WikiLinkToken, WikiPart } from './wikilink';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,yBAA4B;AAE5B,yBAA4B;AAE5B,4BAA+B;AAE/B,yBAA4B;AAC5B,sBAA+C;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/safeContent/index.ts"],"sourcesContent":["// Safe content rendering — non-executable MDX (TRUST_MODES_SPEC §5.1, AGENT_AUTHORING\n// §10). The host/SDK-owned renderer an INTERPRETER app uses to render untrusted\n// Markdown/MDX-syntax content as data, with **no evaluator anywhere in the pipeline**.\n// This is the mandatory terminal for the MDX-from-mount gate (§10 delta 1) — a shared\n// (multi-writer / M3) board or wiki entry MUST render through here, never compiled MDX.\n//\n// It is a **T2 tool, not a platform gate**: it stops content executing as code; it does\n// not certify anything to the host, and CO-4 data-fencing is still required for anything\n// that *reads* the content (the agent case).\n\nexport { SafeContent } from './SafeContent';\nexport type { SafeContentProps } from './SafeContent';\nexport { renderMdast } from './renderMdast';\nexport type {\n RenderMdastOptions,\n SafeContentComponents,\n} from './renderMdast';\nexport { parseSafeMdast } from './parseSafeMdast';\nexport type { SafeMdastNode, SafeMdxAttribute, ParseSafeMdastOptions } from './parseSafeMdast';\nexport { sanitizeUrl } from './sanitizeUrl';\nexport { splitWikiLinks, parseWikiInner } from './wikilink';\nexport type { WikiLinkToken, WikiPart } from './wikilink';\n"],"mappings":";AAUA,SAAS,mBAAmB;AAE5B,SAAS,mBAAmB;AAK5B,SAAS,sBAAsB;AAE/B,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB,sBAAsB;","names":[]}
1
+ {"version":3,"sources":["../../src/safeContent/index.ts"],"sourcesContent":["// Safe content rendering — non-executable MDX (TRUST_MODES_SPEC §5.1, AGENT_AUTHORING\n// §10). The host/SDK-owned renderer an INTERPRETER app uses to render untrusted\n// Markdown/MDX-syntax content as data, with **no evaluator anywhere in the pipeline**.\n// This is the mandatory terminal for the MDX-from-mount gate (§10 delta 1) — a shared\n// (multi-writer / M3) board or wiki entry MUST render through here, never compiled MDX.\n//\n// It is a **T2 tool, not a platform gate**: it stops content executing as code; it does\n// not certify anything to the host, and CO-4 data-fencing is still required for anything\n// that *reads* the content (the agent case).\n\nexport { SafeContent } from './SafeContent';\nexport type { SafeContentProps } from './SafeContent';\nexport { renderMdast } from './renderMdast';\nexport type { RenderMdastOptions, SafeContentComponents } from './renderMdast';\nexport { parseSafeMdast } from './parseSafeMdast';\nexport type { SafeMdastNode, SafeMdxAttribute, ParseSafeMdastOptions } from './parseSafeMdast';\nexport { sanitizeUrl } from './sanitizeUrl';\nexport { splitWikiLinks, parseWikiInner } from './wikilink';\nexport type { WikiLinkToken, WikiPart } from './wikilink';\n"],"mappings":";AAUA,SAAS,mBAAmB;AAE5B,SAAS,mBAAmB;AAE5B,SAAS,sBAAsB;AAE/B,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB,sBAAsB;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/safeContent/parseSafeMdast.ts"],"sourcesContent":["// The no-acorn parse pipeline for the safe content renderer (TRUST_MODES_SPEC §5.1,\n// empirically verified 2026-07-07 against `@mdx-js/mdx` v3 / `micromark-extension-\n// mdx-jsx`). This is the load-bearing half of the fail-safe: `<Component/>` syntax\n// parses to an element node with **literal** attributes, and every expression\n// (`n={1+1}`, `f={fetch(\"/x\")}`, `{...spread}`) is captured as an **inert raw string\n// with no estree** — because we pass **NO acorn option** to `micromark-extension-mdx-\n// jsx`. There is therefore no evaluator anywhere in the pipeline (fail-safe), unlike\n// `@mdx-js/mdx`'s `compile({format:'mdx'})` which always wires acorn.\n//\n// The §5.1 residual guarantees are also here: the expression micromark extension is\n// **not** enabled (body `{…}` stays literal text), and **no `rehype-raw`** / raw-HTML\n// passthrough exists — raw HTML blocks arrive as inert `html` mdast nodes the renderer\n// prints as text, never injected.\n//\n// `micromark`/`mdast` are ESM-only. They are loaded with a memoised dynamic `import()`\n// (the transpiler's `compile.ts` pattern) so this module sits in the package's CJS\n// build too — a CJS consumer that never renders untrusted content never `require()`s\n// an ESM-only package, and one that does gets it via Node's ESM-from-CJS import. The\n// dynamic load changes only *when* the deps resolve, never the produced tree.\n//\n// The dynamic import targets `./mdastDeps` — a SINGLE self-contained artifact that a\n// dedicated build pass (`scripts/build-safecontent-deps.mjs`) inlines the whole ESM tree\n// into (see `mdastDeps.ts` for the WHY). This keeps the `import()` boundary (a consumer\n// that never renders safe content never loads the ~300KB parser) while making the imported\n// target one already-bundled file the immediately.run sandbox resolver can fetch without\n// walking the unified/micromark conditional-exports tree — the R3-213 on-host packaging\n// gap. In jest/ts-jest and the e2e bundle the same `./mdastDeps` resolves to the source\n// re-export module, which pulls the real deps from node_modules; the produced tree is\n// identical either way.\n\n// Type-only — the plugin VALUES are re-exported from (and inlined by) `./mdastDeps`; a\n// value import here would put a bare `@immediately-run/mdx-plugins` specifier in the dist\n// the sandbox resolver can't resolve on-host. `import type` is fully erased at build.\nimport type { HeadingAnchorOptions } from '@immediately-run/mdx-plugins';\n\n// A minimal structural mdast shape — we deliberately avoid a type dependency on the\n// ESM-only `mdast`/`unist` packages (which would break the CJS build's type surface).\nexport interface SafeMdastNode {\n type: string;\n value?: string;\n name?: string | null;\n url?: string;\n alt?: string | null;\n title?: string | null;\n depth?: number;\n ordered?: boolean;\n lang?: string | null;\n children?: SafeMdastNode[];\n attributes?: SafeMdxAttribute[];\n [key: string]: unknown;\n}\n\nexport interface SafeMdxAttribute {\n type: 'mdxJsxAttribute' | 'mdxJsxExpressionAttribute';\n name?: string;\n // A literal attribute carries a string; an expression attribute carries an\n // `mdxJsxAttributeValueExpression` object (a raw string, NO estree) or a string.\n value?: string | { type: string; value?: string } | null;\n}\n\n// `unified`'s `Plugin` type widens the return to `Transformer | void`; the safe path\n// invokes each transformer directly on the tree, so narrow it to a plain in-place mutator.\ntype MdastMutator = (tree: SafeMdastNode) => void;\n\ntype Deps = {\n fromMarkdown: (\n value: string,\n options: { extensions: unknown[]; mdastExtensions: unknown[] },\n ) => SafeMdastNode;\n mdxJsx: () => unknown;\n mdxJsxFromMarkdown: () => unknown;\n gfm: () => unknown;\n gfmFromMarkdown: () => unknown;\n // The three shared kernel remark plugins, also inlined into `./mdastDeps` (see there).\n remarkAdmonitions: () => MdastMutator;\n remarkWikiLinks: () => MdastMutator;\n remarkHeadingAnchors: (o?: HeadingAnchorOptions) => MdastMutator;\n};\n\nlet depsPromise: Promise<Deps> | null = null;\n\nfunction loadDeps(): Promise<Deps> {\n if (!depsPromise) {\n // ONE dynamic import of the pre-inlined artifact (see the header note + `mdastDeps.ts`)\n // — the five ESM parser packages AND the shared remark plugins, none of which the\n // sandbox resolver can walk/resolve as bare specifiers on-host.\n depsPromise = import('./mdastDeps').then((m) => {\n const d = m as unknown as Deps;\n return {\n fromMarkdown: d.fromMarkdown,\n mdxJsx: d.mdxJsx,\n mdxJsxFromMarkdown: d.mdxJsxFromMarkdown,\n gfm: d.gfm,\n gfmFromMarkdown: d.gfmFromMarkdown,\n remarkAdmonitions: d.remarkAdmonitions,\n remarkWikiLinks: d.remarkWikiLinks,\n remarkHeadingAnchors: d.remarkHeadingAnchors,\n } as Deps;\n });\n }\n return depsPromise;\n}\n\nexport interface ParseSafeMdastOptions {\n /** `false` ⇒ headings use plain text-slug ids (the R3-186 base; no `sec-…` section\n * ids, no `data-slug`) — the `sectionIds:false` frontmatter opt-out, matching the\n * compiled path. Default (undefined/true) ⇒ section-like headings get `sec-…` ids\n * (R3-211). */\n sectionIds?: boolean;\n}\n\n/**\n * Parse untrusted Markdown/MDX-syntax source to an mdast tree with **no evaluator in\n * the pipeline**: JSX-as-data (no acorn), GFM on, the expression extension OFF, no\n * raw-HTML re-parse. The returned tree is safe to hand to `renderMdast` — expression\n * attributes are inert strings and raw HTML is inert `html` nodes.\n *\n * After the no-acorn parse it runs the SHARED kernel remark plugins — in the SAME order\n * as the compiled path's `compile.ts` (admonitions §12 → wiki-links §13 → heading/section\n * anchors §15/R3-211; GFM is already applied via the micromark extension above) — so the\n * safe subset renders identically in both standards. The plugins only emit element nodes\n * with LITERAL attributes and set `data.hProperties.id` on headings; they add no evaluator\n * and no acorn, preserving the §5.1 fail-safe.\n */\nexport async function parseSafeMdast(\n source: string,\n options: ParseSafeMdastOptions = {},\n): Promise<SafeMdastNode> {\n const {\n fromMarkdown,\n mdxJsx,\n mdxJsxFromMarkdown,\n gfm,\n gfmFromMarkdown,\n remarkAdmonitions,\n remarkWikiLinks,\n remarkHeadingAnchors,\n } = await loadDeps();\n const tree = fromMarkdown(source, {\n // `mdxJsx()` WITHOUT an acorn option → JSX tags + literal attrs; expressions are\n // raw strings, never estree. The mdx *expression* extension is intentionally\n // absent, so `{…}` in body text stays literal. GFM for tables/task-lists.\n extensions: [mdxJsx(), gfm()],\n mdastExtensions: [mdxJsxFromMarkdown(), gfmFromMarkdown()],\n });\n // The SHARED kernel remark plugins, in the SAME order as the compiled path's `compile.ts`\n // (admonitions §12 → wiki-links §13 → heading/section anchors §15/R3-211).\n remarkAdmonitions()(tree);\n remarkWikiLinks()(tree);\n remarkHeadingAnchors({ sectionIds: options.sectionIds !== false })(tree);\n return tree;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA+EA,IAAI,cAAoC;AAExC,SAAS,WAA0B;AACjC,MAAI,CAAC,aAAa;AAIhB,kBAAc,OAAO,aAAa,EAAE,KAAK,CAAC,MAAM;AAC9C,YAAM,IAAI;AACV,aAAO;AAAA,QACL,cAAc,EAAE;AAAA,QAChB,QAAQ,EAAE;AAAA,QACV,oBAAoB,EAAE;AAAA,QACtB,KAAK,EAAE;AAAA,QACP,iBAAiB,EAAE;AAAA,QACnB,mBAAmB,EAAE;AAAA,QACrB,iBAAiB,EAAE;AAAA,QACnB,sBAAsB,EAAE;AAAA,MAC1B;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAuBA,eAAsB,eACpB,QACA,UAAiC,CAAC,GACV;AACxB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,MAAM,SAAS;AACnB,QAAM,OAAO,aAAa,QAAQ;AAAA;AAAA;AAAA;AAAA,IAIhC,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;AAAA,IAC5B,iBAAiB,CAAC,mBAAmB,GAAG,gBAAgB,CAAC;AAAA,EAC3D,CAAC;AAGD,oBAAkB,EAAE,IAAI;AACxB,kBAAgB,EAAE,IAAI;AACtB,uBAAqB,EAAE,YAAY,QAAQ,eAAe,MAAM,CAAC,EAAE,IAAI;AACvE,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/safeContent/parseSafeMdast.ts"],"sourcesContent":["// The no-acorn parse pipeline for the safe content renderer (TRUST_MODES_SPEC §5.1,\n// empirically verified 2026-07-07 against `@mdx-js/mdx` v3 / `micromark-extension-\n// mdx-jsx`). This is the load-bearing half of the fail-safe: `<Component/>` syntax\n// parses to an element node with **literal** attributes, and every expression\n// (`n={1+1}`, `f={fetch(\"/x\")}`, `{...spread}`) is captured as an **inert raw string\n// with no estree** — because we pass **NO acorn option** to `micromark-extension-mdx-\n// jsx`. There is therefore no evaluator anywhere in the pipeline (fail-safe), unlike\n// `@mdx-js/mdx`'s `compile({format:'mdx'})` which always wires acorn.\n//\n// The §5.1 residual guarantees are also here: the expression micromark extension is\n// **not** enabled (body `{…}` stays literal text), and **no `rehype-raw`** / raw-HTML\n// passthrough exists — raw HTML blocks arrive as inert `html` mdast nodes the renderer\n// prints as text, never injected.\n//\n// `micromark`/`mdast` are ESM-only. They are loaded with a memoised dynamic `import()`\n// (the transpiler's `compile.ts` pattern) so this module sits in the package's CJS\n// build too — a CJS consumer that never renders untrusted content never `require()`s\n// an ESM-only package, and one that does gets it via Node's ESM-from-CJS import. The\n// dynamic load changes only *when* the deps resolve, never the produced tree.\n//\n// The dynamic import targets `./mdastDeps` — a SINGLE self-contained artifact that a\n// dedicated build pass (`scripts/build-safecontent-deps.mjs`) inlines the whole ESM tree\n// into (see `mdastDeps.ts` for the WHY). This keeps the `import()` boundary (a consumer\n// that never renders safe content never loads the ~300KB parser) while making the imported\n// target one already-bundled file the immediately.run sandbox resolver can fetch without\n// walking the unified/micromark conditional-exports tree — the R3-213 on-host packaging\n// gap. In jest/ts-jest and the e2e bundle the same `./mdastDeps` resolves to the source\n// re-export module, which pulls the real deps from node_modules; the produced tree is\n// identical either way.\n\n// Type-only — the plugin VALUES are re-exported from (and inlined by) `./mdastDeps`; a\n// value import here would put a bare `@immediately-run/mdx-plugins` specifier in the dist\n// the sandbox resolver can't resolve on-host. `import type` is fully erased at build.\nimport type { HeadingAnchorOptions } from '@immediately-run/mdx-plugins';\n\n// A minimal structural mdast shape — we deliberately avoid a type dependency on the\n// ESM-only `mdast`/`unist` packages (which would break the CJS build's type surface).\nexport interface SafeMdastNode {\n type: string;\n value?: string;\n name?: string | null;\n url?: string;\n alt?: string | null;\n title?: string | null;\n depth?: number;\n ordered?: boolean;\n lang?: string | null;\n children?: SafeMdastNode[];\n attributes?: SafeMdxAttribute[];\n [key: string]: unknown;\n}\n\nexport interface SafeMdxAttribute {\n type: 'mdxJsxAttribute' | 'mdxJsxExpressionAttribute';\n name?: string;\n // A literal attribute carries a string; an expression attribute carries an\n // `mdxJsxAttributeValueExpression` object (a raw string, NO estree) or a string.\n value?: string | { type: string; value?: string } | null;\n}\n\n// `unified`'s `Plugin` type widens the return to `Transformer | void`; the safe path\n// invokes each transformer directly on the tree, so narrow it to a plain in-place mutator.\ntype MdastMutator = (tree: SafeMdastNode) => void;\n\ntype Deps = {\n fromMarkdown: (value: string, options: { extensions: unknown[]; mdastExtensions: unknown[] }) => SafeMdastNode;\n mdxJsx: () => unknown;\n mdxJsxFromMarkdown: () => unknown;\n gfm: () => unknown;\n gfmFromMarkdown: () => unknown;\n // The three shared kernel remark plugins, also inlined into `./mdastDeps` (see there).\n remarkAdmonitions: () => MdastMutator;\n remarkWikiLinks: () => MdastMutator;\n remarkHeadingAnchors: (o?: HeadingAnchorOptions) => MdastMutator;\n};\n\nlet depsPromise: Promise<Deps> | null = null;\n\nfunction loadDeps(): Promise<Deps> {\n if (!depsPromise) {\n // ONE dynamic import of the pre-inlined artifact (see the header note + `mdastDeps.ts`)\n // — the five ESM parser packages AND the shared remark plugins, none of which the\n // sandbox resolver can walk/resolve as bare specifiers on-host.\n depsPromise = import('./mdastDeps').then((m) => {\n const d = m as unknown as Deps;\n return {\n fromMarkdown: d.fromMarkdown,\n mdxJsx: d.mdxJsx,\n mdxJsxFromMarkdown: d.mdxJsxFromMarkdown,\n gfm: d.gfm,\n gfmFromMarkdown: d.gfmFromMarkdown,\n remarkAdmonitions: d.remarkAdmonitions,\n remarkWikiLinks: d.remarkWikiLinks,\n remarkHeadingAnchors: d.remarkHeadingAnchors,\n } as Deps;\n });\n }\n return depsPromise;\n}\n\nexport interface ParseSafeMdastOptions {\n /** `false` ⇒ headings use plain text-slug ids (the R3-186 base; no `sec-…` section\n * ids, no `data-slug`) — the `sectionIds:false` frontmatter opt-out, matching the\n * compiled path. Default (undefined/true) ⇒ section-like headings get `sec-…` ids\n * (R3-211). */\n sectionIds?: boolean;\n}\n\n/**\n * Parse untrusted Markdown/MDX-syntax source to an mdast tree with **no evaluator in\n * the pipeline**: JSX-as-data (no acorn), GFM on, the expression extension OFF, no\n * raw-HTML re-parse. The returned tree is safe to hand to `renderMdast` — expression\n * attributes are inert strings and raw HTML is inert `html` nodes.\n *\n * After the no-acorn parse it runs the SHARED kernel remark plugins — in the SAME order\n * as the compiled path's `compile.ts` (admonitions §12 → wiki-links §13 → heading/section\n * anchors §15/R3-211; GFM is already applied via the micromark extension above) — so the\n * safe subset renders identically in both standards. The plugins only emit element nodes\n * with LITERAL attributes and set `data.hProperties.id` on headings; they add no evaluator\n * and no acorn, preserving the §5.1 fail-safe.\n */\nexport async function parseSafeMdast(source: string, options: ParseSafeMdastOptions = {}): Promise<SafeMdastNode> {\n const {\n fromMarkdown,\n mdxJsx,\n mdxJsxFromMarkdown,\n gfm,\n gfmFromMarkdown,\n remarkAdmonitions,\n remarkWikiLinks,\n remarkHeadingAnchors,\n } = await loadDeps();\n const tree = fromMarkdown(source, {\n // `mdxJsx()` WITHOUT an acorn option → JSX tags + literal attrs; expressions are\n // raw strings, never estree. The mdx *expression* extension is intentionally\n // absent, so `{…}` in body text stays literal. GFM for tables/task-lists.\n extensions: [mdxJsx(), gfm()],\n mdastExtensions: [mdxJsxFromMarkdown(), gfmFromMarkdown()],\n });\n // The SHARED kernel remark plugins, in the SAME order as the compiled path's `compile.ts`\n // (admonitions §12 → wiki-links §13 → heading/section anchors §15/R3-211).\n remarkAdmonitions()(tree);\n remarkWikiLinks()(tree);\n remarkHeadingAnchors({ sectionIds: options.sectionIds !== false })(tree);\n return tree;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA4EA,IAAI,cAAoC;AAExC,SAAS,WAA0B;AACjC,MAAI,CAAC,aAAa;AAIhB,kBAAc,OAAO,aAAa,EAAE,KAAK,CAAC,MAAM;AAC9C,YAAM,IAAI;AACV,aAAO;AAAA,QACL,cAAc,EAAE;AAAA,QAChB,QAAQ,EAAE;AAAA,QACV,oBAAoB,EAAE;AAAA,QACtB,KAAK,EAAE;AAAA,QACP,iBAAiB,EAAE;AAAA,QACnB,mBAAmB,EAAE;AAAA,QACrB,iBAAiB,EAAE;AAAA,QACnB,sBAAsB,EAAE;AAAA,MAC1B;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAuBA,eAAsB,eAAe,QAAgB,UAAiC,CAAC,GAA2B;AAChH,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,MAAM,SAAS;AACnB,QAAM,OAAO,aAAa,QAAQ;AAAA;AAAA;AAAA;AAAA,IAIhC,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;AAAA,IAC5B,iBAAiB,CAAC,mBAAmB,GAAG,gBAAgB,CAAC;AAAA,EAC3D,CAAC;AAGD,oBAAkB,EAAE,IAAI;AACxB,kBAAgB,EAAE,IAAI;AACtB,uBAAqB,EAAE,YAAY,QAAQ,eAAe,MAAM,CAAC,EAAE,IAAI;AACvE,SAAO;AACT;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/safeContent/parseSafeMdast.ts"],"sourcesContent":["// The no-acorn parse pipeline for the safe content renderer (TRUST_MODES_SPEC §5.1,\n// empirically verified 2026-07-07 against `@mdx-js/mdx` v3 / `micromark-extension-\n// mdx-jsx`). This is the load-bearing half of the fail-safe: `<Component/>` syntax\n// parses to an element node with **literal** attributes, and every expression\n// (`n={1+1}`, `f={fetch(\"/x\")}`, `{...spread}`) is captured as an **inert raw string\n// with no estree** — because we pass **NO acorn option** to `micromark-extension-mdx-\n// jsx`. There is therefore no evaluator anywhere in the pipeline (fail-safe), unlike\n// `@mdx-js/mdx`'s `compile({format:'mdx'})` which always wires acorn.\n//\n// The §5.1 residual guarantees are also here: the expression micromark extension is\n// **not** enabled (body `{…}` stays literal text), and **no `rehype-raw`** / raw-HTML\n// passthrough exists — raw HTML blocks arrive as inert `html` mdast nodes the renderer\n// prints as text, never injected.\n//\n// `micromark`/`mdast` are ESM-only. They are loaded with a memoised dynamic `import()`\n// (the transpiler's `compile.ts` pattern) so this module sits in the package's CJS\n// build too — a CJS consumer that never renders untrusted content never `require()`s\n// an ESM-only package, and one that does gets it via Node's ESM-from-CJS import. The\n// dynamic load changes only *when* the deps resolve, never the produced tree.\n//\n// The dynamic import targets `./mdastDeps` — a SINGLE self-contained artifact that a\n// dedicated build pass (`scripts/build-safecontent-deps.mjs`) inlines the whole ESM tree\n// into (see `mdastDeps.ts` for the WHY). This keeps the `import()` boundary (a consumer\n// that never renders safe content never loads the ~300KB parser) while making the imported\n// target one already-bundled file the immediately.run sandbox resolver can fetch without\n// walking the unified/micromark conditional-exports tree — the R3-213 on-host packaging\n// gap. In jest/ts-jest and the e2e bundle the same `./mdastDeps` resolves to the source\n// re-export module, which pulls the real deps from node_modules; the produced tree is\n// identical either way.\n\n// Type-only — the plugin VALUES are re-exported from (and inlined by) `./mdastDeps`; a\n// value import here would put a bare `@immediately-run/mdx-plugins` specifier in the dist\n// the sandbox resolver can't resolve on-host. `import type` is fully erased at build.\nimport type { HeadingAnchorOptions } from '@immediately-run/mdx-plugins';\n\n// A minimal structural mdast shape — we deliberately avoid a type dependency on the\n// ESM-only `mdast`/`unist` packages (which would break the CJS build's type surface).\nexport interface SafeMdastNode {\n type: string;\n value?: string;\n name?: string | null;\n url?: string;\n alt?: string | null;\n title?: string | null;\n depth?: number;\n ordered?: boolean;\n lang?: string | null;\n children?: SafeMdastNode[];\n attributes?: SafeMdxAttribute[];\n [key: string]: unknown;\n}\n\nexport interface SafeMdxAttribute {\n type: 'mdxJsxAttribute' | 'mdxJsxExpressionAttribute';\n name?: string;\n // A literal attribute carries a string; an expression attribute carries an\n // `mdxJsxAttributeValueExpression` object (a raw string, NO estree) or a string.\n value?: string | { type: string; value?: string } | null;\n}\n\n// `unified`'s `Plugin` type widens the return to `Transformer | void`; the safe path\n// invokes each transformer directly on the tree, so narrow it to a plain in-place mutator.\ntype MdastMutator = (tree: SafeMdastNode) => void;\n\ntype Deps = {\n fromMarkdown: (\n value: string,\n options: { extensions: unknown[]; mdastExtensions: unknown[] },\n ) => SafeMdastNode;\n mdxJsx: () => unknown;\n mdxJsxFromMarkdown: () => unknown;\n gfm: () => unknown;\n gfmFromMarkdown: () => unknown;\n // The three shared kernel remark plugins, also inlined into `./mdastDeps` (see there).\n remarkAdmonitions: () => MdastMutator;\n remarkWikiLinks: () => MdastMutator;\n remarkHeadingAnchors: (o?: HeadingAnchorOptions) => MdastMutator;\n};\n\nlet depsPromise: Promise<Deps> | null = null;\n\nfunction loadDeps(): Promise<Deps> {\n if (!depsPromise) {\n // ONE dynamic import of the pre-inlined artifact (see the header note + `mdastDeps.ts`)\n // — the five ESM parser packages AND the shared remark plugins, none of which the\n // sandbox resolver can walk/resolve as bare specifiers on-host.\n depsPromise = import('./mdastDeps').then((m) => {\n const d = m as unknown as Deps;\n return {\n fromMarkdown: d.fromMarkdown,\n mdxJsx: d.mdxJsx,\n mdxJsxFromMarkdown: d.mdxJsxFromMarkdown,\n gfm: d.gfm,\n gfmFromMarkdown: d.gfmFromMarkdown,\n remarkAdmonitions: d.remarkAdmonitions,\n remarkWikiLinks: d.remarkWikiLinks,\n remarkHeadingAnchors: d.remarkHeadingAnchors,\n } as Deps;\n });\n }\n return depsPromise;\n}\n\nexport interface ParseSafeMdastOptions {\n /** `false` ⇒ headings use plain text-slug ids (the R3-186 base; no `sec-…` section\n * ids, no `data-slug`) — the `sectionIds:false` frontmatter opt-out, matching the\n * compiled path. Default (undefined/true) ⇒ section-like headings get `sec-…` ids\n * (R3-211). */\n sectionIds?: boolean;\n}\n\n/**\n * Parse untrusted Markdown/MDX-syntax source to an mdast tree with **no evaluator in\n * the pipeline**: JSX-as-data (no acorn), GFM on, the expression extension OFF, no\n * raw-HTML re-parse. The returned tree is safe to hand to `renderMdast` — expression\n * attributes are inert strings and raw HTML is inert `html` nodes.\n *\n * After the no-acorn parse it runs the SHARED kernel remark plugins — in the SAME order\n * as the compiled path's `compile.ts` (admonitions §12 → wiki-links §13 → heading/section\n * anchors §15/R3-211; GFM is already applied via the micromark extension above) — so the\n * safe subset renders identically in both standards. The plugins only emit element nodes\n * with LITERAL attributes and set `data.hProperties.id` on headings; they add no evaluator\n * and no acorn, preserving the §5.1 fail-safe.\n */\nexport async function parseSafeMdast(\n source: string,\n options: ParseSafeMdastOptions = {},\n): Promise<SafeMdastNode> {\n const {\n fromMarkdown,\n mdxJsx,\n mdxJsxFromMarkdown,\n gfm,\n gfmFromMarkdown,\n remarkAdmonitions,\n remarkWikiLinks,\n remarkHeadingAnchors,\n } = await loadDeps();\n const tree = fromMarkdown(source, {\n // `mdxJsx()` WITHOUT an acorn option → JSX tags + literal attrs; expressions are\n // raw strings, never estree. The mdx *expression* extension is intentionally\n // absent, so `{…}` in body text stays literal. GFM for tables/task-lists.\n extensions: [mdxJsx(), gfm()],\n mdastExtensions: [mdxJsxFromMarkdown(), gfmFromMarkdown()],\n });\n // The SHARED kernel remark plugins, in the SAME order as the compiled path's `compile.ts`\n // (admonitions §12 → wiki-links §13 → heading/section anchors §15/R3-211).\n remarkAdmonitions()(tree);\n remarkWikiLinks()(tree);\n remarkHeadingAnchors({ sectionIds: options.sectionIds !== false })(tree);\n return tree;\n}\n"],"mappings":";AA+EA,IAAI,cAAoC;AAExC,SAAS,WAA0B;AACjC,MAAI,CAAC,aAAa;AAIhB,kBAAc,OAAO,aAAa,EAAE,KAAK,CAAC,MAAM;AAC9C,YAAM,IAAI;AACV,aAAO;AAAA,QACL,cAAc,EAAE;AAAA,QAChB,QAAQ,EAAE;AAAA,QACV,oBAAoB,EAAE;AAAA,QACtB,KAAK,EAAE;AAAA,QACP,iBAAiB,EAAE;AAAA,QACnB,mBAAmB,EAAE;AAAA,QACrB,iBAAiB,EAAE;AAAA,QACnB,sBAAsB,EAAE;AAAA,MAC1B;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAuBA,eAAsB,eACpB,QACA,UAAiC,CAAC,GACV;AACxB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,MAAM,SAAS;AACnB,QAAM,OAAO,aAAa,QAAQ;AAAA;AAAA;AAAA;AAAA,IAIhC,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;AAAA,IAC5B,iBAAiB,CAAC,mBAAmB,GAAG,gBAAgB,CAAC;AAAA,EAC3D,CAAC;AAGD,oBAAkB,EAAE,IAAI;AACxB,kBAAgB,EAAE,IAAI;AACtB,uBAAqB,EAAE,YAAY,QAAQ,eAAe,MAAM,CAAC,EAAE,IAAI;AACvE,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/safeContent/parseSafeMdast.ts"],"sourcesContent":["// The no-acorn parse pipeline for the safe content renderer (TRUST_MODES_SPEC §5.1,\n// empirically verified 2026-07-07 against `@mdx-js/mdx` v3 / `micromark-extension-\n// mdx-jsx`). This is the load-bearing half of the fail-safe: `<Component/>` syntax\n// parses to an element node with **literal** attributes, and every expression\n// (`n={1+1}`, `f={fetch(\"/x\")}`, `{...spread}`) is captured as an **inert raw string\n// with no estree** — because we pass **NO acorn option** to `micromark-extension-mdx-\n// jsx`. There is therefore no evaluator anywhere in the pipeline (fail-safe), unlike\n// `@mdx-js/mdx`'s `compile({format:'mdx'})` which always wires acorn.\n//\n// The §5.1 residual guarantees are also here: the expression micromark extension is\n// **not** enabled (body `{…}` stays literal text), and **no `rehype-raw`** / raw-HTML\n// passthrough exists — raw HTML blocks arrive as inert `html` mdast nodes the renderer\n// prints as text, never injected.\n//\n// `micromark`/`mdast` are ESM-only. They are loaded with a memoised dynamic `import()`\n// (the transpiler's `compile.ts` pattern) so this module sits in the package's CJS\n// build too — a CJS consumer that never renders untrusted content never `require()`s\n// an ESM-only package, and one that does gets it via Node's ESM-from-CJS import. The\n// dynamic load changes only *when* the deps resolve, never the produced tree.\n//\n// The dynamic import targets `./mdastDeps` — a SINGLE self-contained artifact that a\n// dedicated build pass (`scripts/build-safecontent-deps.mjs`) inlines the whole ESM tree\n// into (see `mdastDeps.ts` for the WHY). This keeps the `import()` boundary (a consumer\n// that never renders safe content never loads the ~300KB parser) while making the imported\n// target one already-bundled file the immediately.run sandbox resolver can fetch without\n// walking the unified/micromark conditional-exports tree — the R3-213 on-host packaging\n// gap. In jest/ts-jest and the e2e bundle the same `./mdastDeps` resolves to the source\n// re-export module, which pulls the real deps from node_modules; the produced tree is\n// identical either way.\n\n// Type-only — the plugin VALUES are re-exported from (and inlined by) `./mdastDeps`; a\n// value import here would put a bare `@immediately-run/mdx-plugins` specifier in the dist\n// the sandbox resolver can't resolve on-host. `import type` is fully erased at build.\nimport type { HeadingAnchorOptions } from '@immediately-run/mdx-plugins';\n\n// A minimal structural mdast shape — we deliberately avoid a type dependency on the\n// ESM-only `mdast`/`unist` packages (which would break the CJS build's type surface).\nexport interface SafeMdastNode {\n type: string;\n value?: string;\n name?: string | null;\n url?: string;\n alt?: string | null;\n title?: string | null;\n depth?: number;\n ordered?: boolean;\n lang?: string | null;\n children?: SafeMdastNode[];\n attributes?: SafeMdxAttribute[];\n [key: string]: unknown;\n}\n\nexport interface SafeMdxAttribute {\n type: 'mdxJsxAttribute' | 'mdxJsxExpressionAttribute';\n name?: string;\n // A literal attribute carries a string; an expression attribute carries an\n // `mdxJsxAttributeValueExpression` object (a raw string, NO estree) or a string.\n value?: string | { type: string; value?: string } | null;\n}\n\n// `unified`'s `Plugin` type widens the return to `Transformer | void`; the safe path\n// invokes each transformer directly on the tree, so narrow it to a plain in-place mutator.\ntype MdastMutator = (tree: SafeMdastNode) => void;\n\ntype Deps = {\n fromMarkdown: (value: string, options: { extensions: unknown[]; mdastExtensions: unknown[] }) => SafeMdastNode;\n mdxJsx: () => unknown;\n mdxJsxFromMarkdown: () => unknown;\n gfm: () => unknown;\n gfmFromMarkdown: () => unknown;\n // The three shared kernel remark plugins, also inlined into `./mdastDeps` (see there).\n remarkAdmonitions: () => MdastMutator;\n remarkWikiLinks: () => MdastMutator;\n remarkHeadingAnchors: (o?: HeadingAnchorOptions) => MdastMutator;\n};\n\nlet depsPromise: Promise<Deps> | null = null;\n\nfunction loadDeps(): Promise<Deps> {\n if (!depsPromise) {\n // ONE dynamic import of the pre-inlined artifact (see the header note + `mdastDeps.ts`)\n // — the five ESM parser packages AND the shared remark plugins, none of which the\n // sandbox resolver can walk/resolve as bare specifiers on-host.\n depsPromise = import('./mdastDeps').then((m) => {\n const d = m as unknown as Deps;\n return {\n fromMarkdown: d.fromMarkdown,\n mdxJsx: d.mdxJsx,\n mdxJsxFromMarkdown: d.mdxJsxFromMarkdown,\n gfm: d.gfm,\n gfmFromMarkdown: d.gfmFromMarkdown,\n remarkAdmonitions: d.remarkAdmonitions,\n remarkWikiLinks: d.remarkWikiLinks,\n remarkHeadingAnchors: d.remarkHeadingAnchors,\n } as Deps;\n });\n }\n return depsPromise;\n}\n\nexport interface ParseSafeMdastOptions {\n /** `false` ⇒ headings use plain text-slug ids (the R3-186 base; no `sec-…` section\n * ids, no `data-slug`) — the `sectionIds:false` frontmatter opt-out, matching the\n * compiled path. Default (undefined/true) ⇒ section-like headings get `sec-…` ids\n * (R3-211). */\n sectionIds?: boolean;\n}\n\n/**\n * Parse untrusted Markdown/MDX-syntax source to an mdast tree with **no evaluator in\n * the pipeline**: JSX-as-data (no acorn), GFM on, the expression extension OFF, no\n * raw-HTML re-parse. The returned tree is safe to hand to `renderMdast` — expression\n * attributes are inert strings and raw HTML is inert `html` nodes.\n *\n * After the no-acorn parse it runs the SHARED kernel remark plugins — in the SAME order\n * as the compiled path's `compile.ts` (admonitions §12 → wiki-links §13 → heading/section\n * anchors §15/R3-211; GFM is already applied via the micromark extension above) — so the\n * safe subset renders identically in both standards. The plugins only emit element nodes\n * with LITERAL attributes and set `data.hProperties.id` on headings; they add no evaluator\n * and no acorn, preserving the §5.1 fail-safe.\n */\nexport async function parseSafeMdast(source: string, options: ParseSafeMdastOptions = {}): Promise<SafeMdastNode> {\n const {\n fromMarkdown,\n mdxJsx,\n mdxJsxFromMarkdown,\n gfm,\n gfmFromMarkdown,\n remarkAdmonitions,\n remarkWikiLinks,\n remarkHeadingAnchors,\n } = await loadDeps();\n const tree = fromMarkdown(source, {\n // `mdxJsx()` WITHOUT an acorn option → JSX tags + literal attrs; expressions are\n // raw strings, never estree. The mdx *expression* extension is intentionally\n // absent, so `{…}` in body text stays literal. GFM for tables/task-lists.\n extensions: [mdxJsx(), gfm()],\n mdastExtensions: [mdxJsxFromMarkdown(), gfmFromMarkdown()],\n });\n // The SHARED kernel remark plugins, in the SAME order as the compiled path's `compile.ts`\n // (admonitions §12 → wiki-links §13 → heading/section anchors §15/R3-211).\n remarkAdmonitions()(tree);\n remarkWikiLinks()(tree);\n remarkHeadingAnchors({ sectionIds: options.sectionIds !== false })(tree);\n return tree;\n}\n"],"mappings":";AA4EA,IAAI,cAAoC;AAExC,SAAS,WAA0B;AACjC,MAAI,CAAC,aAAa;AAIhB,kBAAc,OAAO,aAAa,EAAE,KAAK,CAAC,MAAM;AAC9C,YAAM,IAAI;AACV,aAAO;AAAA,QACL,cAAc,EAAE;AAAA,QAChB,QAAQ,EAAE;AAAA,QACV,oBAAoB,EAAE;AAAA,QACtB,KAAK,EAAE;AAAA,QACP,iBAAiB,EAAE;AAAA,QACnB,mBAAmB,EAAE;AAAA,QACrB,iBAAiB,EAAE;AAAA,QACnB,sBAAsB,EAAE;AAAA,MAC1B;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAuBA,eAAsB,eAAe,QAAgB,UAAiC,CAAC,GAA2B;AAChH,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,MAAM,SAAS;AACnB,QAAM,OAAO,aAAa,QAAQ;AAAA;AAAA;AAAA;AAAA,IAIhC,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;AAAA,IAC5B,iBAAiB,CAAC,mBAAmB,GAAG,gBAAgB,CAAC;AAAA,EAC3D,CAAC;AAGD,oBAAkB,EAAE,IAAI;AACxB,kBAAgB,EAAE,IAAI;AACtB,uBAAqB,EAAE,YAAY,QAAQ,eAAe,MAAM,CAAC,EAAE,IAAI;AACvE,SAAO;AACT;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/safeContent/renderMdast.tsx"],"sourcesContent":["import { createElement, Fragment, type ReactNode } from 'react';\nimport { sanitizeUrl } from './sanitizeUrl';\nimport { splitWikiLinks, type WikiLinkToken } from './wikilink';\nimport { resolveLinkTarget } from '../linkSpace';\nimport type { SafeMdastNode, SafeMdxAttribute } from './parseSafeMdast';\n\n// The mdast→React renderer for the safe content path (TRUST_MODES_SPEC §5.1). PURE\n// and synchronous — it only reads `node.type` off an already-parsed tree, so it\n// carries no ESM dep and is exhaustively unit-testable. Every security property lives\n// here:\n//\n// - **JSX tags resolve ONLY to a component registry, by name.** A `<Component/>`\n// node becomes `registry[name]` with **literal string props only**; an unknown tag\n// renders its children inert (a Fragment, no element) — so author-written `<div\n// onclick=…>`, `<img onerror=…>`, `<script>` never become an intrinsic element with\n// attacker attributes. (Block-level raw HTML arrives as an `html` node, below.)\n// - **Expression attributes are dropped.** `f={fetch(\"/x\")}` / `{...spread}` are\n// `mdxJsxExpressionAttribute` or object-valued `mdxJsxAttribute`s — never passed to\n// a component, never evaluated. (There is also no evaluator to reach — parse used\n// no acorn.)\n// - **Raw HTML is inert text.** `html` nodes render as their literal string via a\n// text node — never `dangerouslySetInnerHTML` (no `rehype-raw`).\n// - **URL-scheme sanitizer** on every `link`/`image` URL (`sanitizeUrl`).\n// - **Wikilinks resolve in-mount only** via an injected resolver; the renderer never\n// fetches.\n\nexport interface SafeContentComponents {\n /** Safe components an app exposes to `<Component/>` syntax (host/app-provided).\n * Looked up by the JSX tag name; anything not here renders inert. */\n [tag: string]: React.ComponentType<Record<string, string>>;\n}\n\nexport interface RenderMdastOptions {\n /** The component registry for `<Component/>` syntax. Absent ⇒ all JSX tags inert. */\n components?: SafeContentComponents;\n /**\n * Resolve a wikilink target to an href, **within the granted mount only** — a pure,\n * synchronous, mount-scoped lookup that MUST NOT fetch or reach out of the mount.\n * Return `undefined` for an unresolvable/out-of-mount target (rendered as inert\n * text). Absent ⇒ every wikilink renders as inert text.\n */\n resolveWikiLink?: (target: string) => string | undefined;\n}\n\n/** Extract the literal string props of a JSX element — `mdxJsxAttribute`s whose value\n * is a plain string. Expression attributes (object value) and spreads are DROPPED. */\nfunction literalProps(attributes: SafeMdxAttribute[] | undefined): Record<string, string> {\n const props: Record<string, string> = {};\n for (const attr of attributes ?? []) {\n if (attr.type !== 'mdxJsxAttribute') continue; // drop `{...spread}`\n if (typeof attr.name !== 'string') continue;\n // A literal attribute's value is a string (or null → boolean-ish `true`). An\n // expression value is an object (`mdxJsxAttributeValueExpression`) — DROP it: it\n // is an inert raw string, never a real value, and must never reach a component.\n if (typeof attr.value === 'string') props[attr.name] = attr.value;\n else if (attr.value === null || attr.value === undefined) props[attr.name] = '';\n // object-valued (expression) → skipped\n }\n return props;\n}\n\nlet keyCounter = 0;\nconst nextKey = () => `sc-${keyCounter++}`;\n\n/**\n * The element to build for a link/image: the host's registered override when it has one,\n * else the intrinsic tag.\n *\n * **Why this is safe.** `components` is the HOST's registry — the same one `<Component/>`\n * syntax resolves against — never author input. An author cannot add to it or choose one\n * here, because the lookup is by a FIXED key (`'a'`/`'img'`), not by anything read out of\n * the document. The URL is still `sanitizeUrl`'d before it is handed over, and only props\n * this renderer controls are passed. The trust boundary is unchanged: author text still\n * reaches nothing but children and a sanitized URL.\n *\n * **Why it is needed.** Without it, a safe-rendered document's links are raw `<a href>`,\n * so a click performs a REAL navigation. Inside an app's sandboxed iframe that is fatal —\n * the frame navigates away from the app, and in a routed host app it dies with\n * `Failed to construct 'URL': Invalid URL`. Hosts route links through a component for\n * exactly this reason, and the compiled-MDX path has always honoured that component; the\n * safe path silently ignored it, so one document behaved differently on the two renderers.\n */\nfunction elementFor(\n tag: 'a' | 'img',\n components: RenderMdastOptions['components'],\n): React.ElementType {\n return (components?.[tag] as React.ElementType) ?? tag;\n}\n\n/** Render a wikilink token via the in-mount resolver, or inert text when it can't\n * resolve (never a network call — resolution is the injected mount-scoped callback). */\nfunction renderWiki(token: WikiLinkToken, opts: RenderMdastOptions): ReactNode {\n const label = token.label ?? token.target;\n // R3-273: a malformed `$fs:` target (not mount-absolute — catches scheme\n // smuggling) is inert BEFORE the injected resolver ever sees it, so every\n // consumer fails closed identically. Consumers implement their resolver on the\n // same shared `resolveLinkTarget` for well-formed targets.\n if (resolveLinkTarget(token.target).state === 'invalid') return label;\n const href = opts.resolveWikiLink?.(token.target);\n const safe = href !== undefined ? sanitizeUrl(href) : undefined;\n if (safe === undefined) return label; // inert text — unresolved or out-of-mount\n return createElement(\n elementFor('a', opts.components),\n { href: safe, 'data-wikilink': token.target, key: nextKey() },\n label,\n );\n}\n\n/** Render a `text` node, splitting any `[[…]]` wikilinks out of it. */\nfunction renderText(value: string, opts: RenderMdastOptions): ReactNode {\n const parts = splitWikiLinks(value);\n if (!parts) return value;\n return parts.map((p, i) =>\n 'text' in p\n ? createElement(Fragment, { key: `t${i}` }, p.text)\n : createElement(Fragment, { key: `w${i}` }, renderWiki(p.wiki, opts)),\n );\n}\n\nfunction renderChildren(node: SafeMdastNode, opts: RenderMdastOptions): ReactNode[] {\n return (node.children ?? []).map((c, i) => renderNode(c, opts, i));\n}\n\n// Standard mdast block/inline nodes → a FIXED, safe React element. We control every\n// attribute; author input only reaches text content and (sanitized) URLs.\nconst HEADING_TAGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const;\n\nfunction renderNode(node: SafeMdastNode, opts: RenderMdastOptions, index = 0): ReactNode {\n const key = `n${index}`;\n switch (node.type) {\n case 'root':\n return createElement(Fragment, { key }, ...renderChildren(node, opts));\n case 'text':\n return renderText(node.value ?? '', opts);\n case 'paragraph':\n return createElement('p', { key }, ...renderChildren(node, opts));\n case 'heading': {\n const tag = HEADING_TAGS[Math.min(Math.max((node.depth ?? 1) - 1, 0), 5)];\n // R3-213: the no-acorn path has no hast stage, so the heading id the shared\n // `remarkHeadingAnchors` plugin sets via `data.hProperties.id` (+ the `data-slug`\n // fallback, R3-211) must be translated to React props HERE, or headings render\n // id-less and deep-linking (`#sec-8-9`) breaks in the safe path. These are\n // plugin-computed literal strings, not author input — safe to pass through.\n const hp = (node.data as { hProperties?: Record<string, unknown> } | undefined)?.hProperties;\n const headingProps: Record<string, string> = {};\n if (typeof hp?.id === 'string') headingProps.id = hp.id;\n if (typeof hp?.['data-slug'] === 'string') headingProps['data-slug'] = hp['data-slug'];\n return createElement(tag, { key, ...headingProps }, ...renderChildren(node, opts));\n }\n case 'strong':\n return createElement('strong', { key }, ...renderChildren(node, opts));\n case 'emphasis':\n return createElement('em', { key }, ...renderChildren(node, opts));\n case 'delete':\n return createElement('del', { key }, ...renderChildren(node, opts));\n case 'inlineCode':\n return createElement('code', { key }, node.value ?? '');\n case 'code':\n return createElement('pre', { key }, createElement('code', null, node.value ?? ''));\n case 'blockquote':\n return createElement('blockquote', { key }, ...renderChildren(node, opts));\n case 'list':\n return createElement(node.ordered ? 'ol' : 'ul', { key }, ...renderChildren(node, opts));\n case 'listItem':\n return createElement('li', { key }, ...renderChildren(node, opts));\n case 'thematicBreak':\n return createElement('hr', { key });\n case 'break':\n return createElement('br', { key });\n case 'link': {\n const href = sanitizeUrl(node.url);\n // A rejected scheme → render the link TEXT only (inert), never an <a href>.\n if (href === undefined) return createElement(Fragment, { key }, ...renderChildren(node, opts));\n return createElement(\n elementFor('a', opts.components),\n { key, href, title: node.title ?? undefined },\n ...renderChildren(node, opts),\n );\n }\n case 'image': {\n const src = sanitizeUrl(node.url);\n if (src === undefined) return node.alt ? createElement(Fragment, { key }, node.alt) : null;\n return createElement(elementFor('img', opts.components), {\n key,\n src,\n alt: node.alt ?? '',\n title: node.title ?? undefined,\n });\n }\n // GFM tables.\n case 'table':\n return createElement('table', { key }, createElement('tbody', null, ...renderChildren(node, opts)));\n case 'tableRow':\n return createElement('tr', { key }, ...renderChildren(node, opts));\n case 'tableCell':\n return createElement('td', { key }, ...renderChildren(node, opts));\n // Raw HTML (`<script>`, `<div onclick>` at block level) → INERT TEXT. No\n // `rehype-raw`, never `dangerouslySetInnerHTML`. The literal markup is shown, not run.\n case 'html':\n return createElement(Fragment, { key }, node.value ?? '');\n // JSX `<Component/>` syntax → registry lookup by NAME, literal props only.\n case 'mdxJsxFlowElement':\n case 'mdxJsxTextElement': {\n const name = typeof node.name === 'string' ? node.name : '';\n const Component = name ? opts.components?.[name] : undefined;\n const children = renderChildren(node, opts);\n // Unknown tag (not in the registry) OR a fragment `<>` → render children inert,\n // NO element, NO author attributes. This is what neutralizes `<script>`/`<div\n // onclick>`/`<img onerror>` written as JSX.\n if (!Component) return createElement(Fragment, { key }, ...children);\n return createElement(Component, { key, ...literalProps(node.attributes) }, ...children);\n }\n // Inert expression nodes (should not occur — expression extension is off — but be\n // defensive if a tree from elsewhere carries them): render nothing.\n case 'mdxFlowExpression':\n case 'mdxTextExpression':\n case 'mdxjsEsm':\n return null;\n default:\n // Unknown node → render its children (never its raw value as markup).\n return node.children ? createElement(Fragment, { key }, ...renderChildren(node, opts)) : null;\n }\n}\n\n/**\n * Render a safe-parsed mdast tree to React. Pure and synchronous; carries every §5.1\n * security property (registry-only JSX, dropped expressions, inert raw HTML, URL\n * sanitizing, in-mount wikilinks). Feed it a tree from {@link parseSafeMdast}.\n */\nexport function renderMdast(tree: SafeMdastNode, options: RenderMdastOptions = {}): ReactNode {\n keyCounter = 0;\n return renderNode(tree, options);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/safeContent/renderMdast.tsx"],"sourcesContent":["import { createElement, Fragment, type ReactNode } from 'react';\nimport { sanitizeUrl } from './sanitizeUrl';\nimport { splitWikiLinks, type WikiLinkToken } from './wikilink';\nimport { resolveLinkTarget } from '../linkSpace';\nimport type { SafeMdastNode, SafeMdxAttribute } from './parseSafeMdast';\n\n// The mdast→React renderer for the safe content path (TRUST_MODES_SPEC §5.1). PURE\n// and synchronous — it only reads `node.type` off an already-parsed tree, so it\n// carries no ESM dep and is exhaustively unit-testable. Every security property lives\n// here:\n//\n// - **JSX tags resolve ONLY to a component registry, by name.** A `<Component/>`\n// node becomes `registry[name]` with **literal string props only**; an unknown tag\n// renders its children inert (a Fragment, no element) — so author-written `<div\n// onclick=…>`, `<img onerror=…>`, `<script>` never become an intrinsic element with\n// attacker attributes. (Block-level raw HTML arrives as an `html` node, below.)\n// - **Expression attributes are dropped.** `f={fetch(\"/x\")}` / `{...spread}` are\n// `mdxJsxExpressionAttribute` or object-valued `mdxJsxAttribute`s — never passed to\n// a component, never evaluated. (There is also no evaluator to reach — parse used\n// no acorn.)\n// - **Raw HTML is inert text.** `html` nodes render as their literal string via a\n// text node — never `dangerouslySetInnerHTML` (no `rehype-raw`).\n// - **URL-scheme sanitizer** on every `link`/`image` URL (`sanitizeUrl`).\n// - **Wikilinks resolve in-mount only** via an injected resolver; the renderer never\n// fetches.\n\nexport interface SafeContentComponents {\n /** Safe components an app exposes to `<Component/>` syntax (host/app-provided).\n * Looked up by the JSX tag name; anything not here renders inert. */\n [tag: string]: React.ComponentType<Record<string, string>>;\n}\n\nexport interface RenderMdastOptions {\n /** The component registry for `<Component/>` syntax. Absent ⇒ all JSX tags inert. */\n components?: SafeContentComponents;\n /**\n * Resolve a wikilink target to an href, **within the granted mount only** — a pure,\n * synchronous, mount-scoped lookup that MUST NOT fetch or reach out of the mount.\n * Return `undefined` for an unresolvable/out-of-mount target (rendered as inert\n * text). Absent ⇒ every wikilink renders as inert text.\n */\n resolveWikiLink?: (target: string) => string | undefined;\n}\n\n/** Extract the literal string props of a JSX element — `mdxJsxAttribute`s whose value\n * is a plain string. Expression attributes (object value) and spreads are DROPPED. */\nfunction literalProps(attributes: SafeMdxAttribute[] | undefined): Record<string, string> {\n const props: Record<string, string> = {};\n for (const attr of attributes ?? []) {\n if (attr.type !== 'mdxJsxAttribute') continue; // drop `{...spread}`\n if (typeof attr.name !== 'string') continue;\n // A literal attribute's value is a string (or null → boolean-ish `true`). An\n // expression value is an object (`mdxJsxAttributeValueExpression`) — DROP it: it\n // is an inert raw string, never a real value, and must never reach a component.\n if (typeof attr.value === 'string') props[attr.name] = attr.value;\n else if (attr.value === null || attr.value === undefined) props[attr.name] = '';\n // object-valued (expression) → skipped\n }\n return props;\n}\n\nlet keyCounter = 0;\nconst nextKey = () => `sc-${keyCounter++}`;\n\n/**\n * The element to build for a link/image: the host's registered override when it has one,\n * else the intrinsic tag.\n *\n * **Why this is safe.** `components` is the HOST's registry — the same one `<Component/>`\n * syntax resolves against — never author input. An author cannot add to it or choose one\n * here, because the lookup is by a FIXED key (`'a'`/`'img'`), not by anything read out of\n * the document. The URL is still `sanitizeUrl`'d before it is handed over, and only props\n * this renderer controls are passed. The trust boundary is unchanged: author text still\n * reaches nothing but children and a sanitized URL.\n *\n * **Why it is needed.** Without it, a safe-rendered document's links are raw `<a href>`,\n * so a click performs a REAL navigation. Inside an app's sandboxed iframe that is fatal —\n * the frame navigates away from the app, and in a routed host app it dies with\n * `Failed to construct 'URL': Invalid URL`. Hosts route links through a component for\n * exactly this reason, and the compiled-MDX path has always honoured that component; the\n * safe path silently ignored it, so one document behaved differently on the two renderers.\n */\nfunction elementFor(tag: 'a' | 'img', components: RenderMdastOptions['components']): React.ElementType {\n return (components?.[tag] as React.ElementType) ?? tag;\n}\n\n/** Render a wikilink token via the in-mount resolver, or inert text when it can't\n * resolve (never a network call — resolution is the injected mount-scoped callback). */\nfunction renderWiki(token: WikiLinkToken, opts: RenderMdastOptions): ReactNode {\n const label = token.label ?? token.target;\n // R3-273: a malformed `$fs:` target (not mount-absolute — catches scheme\n // smuggling) is inert BEFORE the injected resolver ever sees it, so every\n // consumer fails closed identically. Consumers implement their resolver on the\n // same shared `resolveLinkTarget` for well-formed targets.\n if (resolveLinkTarget(token.target).state === 'invalid') return label;\n const href = opts.resolveWikiLink?.(token.target);\n const safe = href !== undefined ? sanitizeUrl(href) : undefined;\n if (safe === undefined) return label; // inert text — unresolved or out-of-mount\n return createElement(\n elementFor('a', opts.components),\n { href: safe, 'data-wikilink': token.target, key: nextKey() },\n label,\n );\n}\n\n/** Render a `text` node, splitting any `[[…]]` wikilinks out of it. */\nfunction renderText(value: string, opts: RenderMdastOptions): ReactNode {\n const parts = splitWikiLinks(value);\n if (!parts) return value;\n return parts.map((p, i) =>\n 'text' in p\n ? createElement(Fragment, { key: `t${i}` }, p.text)\n : createElement(Fragment, { key: `w${i}` }, renderWiki(p.wiki, opts)),\n );\n}\n\nfunction renderChildren(node: SafeMdastNode, opts: RenderMdastOptions): ReactNode[] {\n return (node.children ?? []).map((c, i) => renderNode(c, opts, i));\n}\n\n// Standard mdast block/inline nodes → a FIXED, safe React element. We control every\n// attribute; author input only reaches text content and (sanitized) URLs.\nconst HEADING_TAGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const;\n\nfunction renderNode(node: SafeMdastNode, opts: RenderMdastOptions, index = 0): ReactNode {\n const key = `n${index}`;\n switch (node.type) {\n case 'root':\n return createElement(Fragment, { key }, ...renderChildren(node, opts));\n case 'text':\n return renderText(node.value ?? '', opts);\n case 'paragraph':\n return createElement('p', { key }, ...renderChildren(node, opts));\n case 'heading': {\n const tag = HEADING_TAGS[Math.min(Math.max((node.depth ?? 1) - 1, 0), 5)];\n // R3-213: the no-acorn path has no hast stage, so the heading id the shared\n // `remarkHeadingAnchors` plugin sets via `data.hProperties.id` (+ the `data-slug`\n // fallback, R3-211) must be translated to React props HERE, or headings render\n // id-less and deep-linking (`#sec-8-9`) breaks in the safe path. These are\n // plugin-computed literal strings, not author input — safe to pass through.\n const hp = (node.data as { hProperties?: Record<string, unknown> } | undefined)?.hProperties;\n const headingProps: Record<string, string> = {};\n if (typeof hp?.id === 'string') headingProps.id = hp.id;\n if (typeof hp?.['data-slug'] === 'string') headingProps['data-slug'] = hp['data-slug'];\n return createElement(tag, { key, ...headingProps }, ...renderChildren(node, opts));\n }\n case 'strong':\n return createElement('strong', { key }, ...renderChildren(node, opts));\n case 'emphasis':\n return createElement('em', { key }, ...renderChildren(node, opts));\n case 'delete':\n return createElement('del', { key }, ...renderChildren(node, opts));\n case 'inlineCode':\n return createElement('code', { key }, node.value ?? '');\n case 'code':\n return createElement('pre', { key }, createElement('code', null, node.value ?? ''));\n case 'blockquote':\n return createElement('blockquote', { key }, ...renderChildren(node, opts));\n case 'list':\n return createElement(node.ordered ? 'ol' : 'ul', { key }, ...renderChildren(node, opts));\n case 'listItem':\n return createElement('li', { key }, ...renderChildren(node, opts));\n case 'thematicBreak':\n return createElement('hr', { key });\n case 'break':\n return createElement('br', { key });\n case 'link': {\n const href = sanitizeUrl(node.url);\n // A rejected scheme → render the link TEXT only (inert), never an <a href>.\n if (href === undefined) return createElement(Fragment, { key }, ...renderChildren(node, opts));\n return createElement(\n elementFor('a', opts.components),\n { key, href, title: node.title ?? undefined },\n ...renderChildren(node, opts),\n );\n }\n case 'image': {\n const src = sanitizeUrl(node.url);\n if (src === undefined) return node.alt ? createElement(Fragment, { key }, node.alt) : null;\n return createElement(elementFor('img', opts.components), {\n key,\n src,\n alt: node.alt ?? '',\n title: node.title ?? undefined,\n });\n }\n // GFM tables.\n case 'table':\n return createElement('table', { key }, createElement('tbody', null, ...renderChildren(node, opts)));\n case 'tableRow':\n return createElement('tr', { key }, ...renderChildren(node, opts));\n case 'tableCell':\n return createElement('td', { key }, ...renderChildren(node, opts));\n // Raw HTML (`<script>`, `<div onclick>` at block level) → INERT TEXT. No\n // `rehype-raw`, never `dangerouslySetInnerHTML`. The literal markup is shown, not run.\n case 'html':\n return createElement(Fragment, { key }, node.value ?? '');\n // JSX `<Component/>` syntax → registry lookup by NAME, literal props only.\n case 'mdxJsxFlowElement':\n case 'mdxJsxTextElement': {\n const name = typeof node.name === 'string' ? node.name : '';\n const Component = name ? opts.components?.[name] : undefined;\n const children = renderChildren(node, opts);\n // Unknown tag (not in the registry) OR a fragment `<>` → render children inert,\n // NO element, NO author attributes. This is what neutralizes `<script>`/`<div\n // onclick>`/`<img onerror>` written as JSX.\n if (!Component) return createElement(Fragment, { key }, ...children);\n return createElement(Component, { key, ...literalProps(node.attributes) }, ...children);\n }\n // Inert expression nodes (should not occur — expression extension is off — but be\n // defensive if a tree from elsewhere carries them): render nothing.\n case 'mdxFlowExpression':\n case 'mdxTextExpression':\n case 'mdxjsEsm':\n return null;\n default:\n // Unknown node → render its children (never its raw value as markup).\n return node.children ? createElement(Fragment, { key }, ...renderChildren(node, opts)) : null;\n }\n}\n\n/**\n * Render a safe-parsed mdast tree to React. Pure and synchronous; carries every §5.1\n * security property (registry-only JSX, dropped expressions, inert raw HTML, URL\n * sanitizing, in-mount wikilinks). Feed it a tree from {@link parseSafeMdast}.\n */\nexport function renderMdast(tree: SafeMdastNode, options: RenderMdastOptions = {}): ReactNode {\n keyCounter = 0;\n return renderNode(tree, options);\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/safeContent/renderMdast.tsx"],"sourcesContent":["import { createElement, Fragment, type ReactNode } from 'react';\nimport { sanitizeUrl } from './sanitizeUrl';\nimport { splitWikiLinks, type WikiLinkToken } from './wikilink';\nimport { resolveLinkTarget } from '../linkSpace';\nimport type { SafeMdastNode, SafeMdxAttribute } from './parseSafeMdast';\n\n// The mdast→React renderer for the safe content path (TRUST_MODES_SPEC §5.1). PURE\n// and synchronous — it only reads `node.type` off an already-parsed tree, so it\n// carries no ESM dep and is exhaustively unit-testable. Every security property lives\n// here:\n//\n// - **JSX tags resolve ONLY to a component registry, by name.** A `<Component/>`\n// node becomes `registry[name]` with **literal string props only**; an unknown tag\n// renders its children inert (a Fragment, no element) — so author-written `<div\n// onclick=…>`, `<img onerror=…>`, `<script>` never become an intrinsic element with\n// attacker attributes. (Block-level raw HTML arrives as an `html` node, below.)\n// - **Expression attributes are dropped.** `f={fetch(\"/x\")}` / `{...spread}` are\n// `mdxJsxExpressionAttribute` or object-valued `mdxJsxAttribute`s — never passed to\n// a component, never evaluated. (There is also no evaluator to reach — parse used\n// no acorn.)\n// - **Raw HTML is inert text.** `html` nodes render as their literal string via a\n// text node — never `dangerouslySetInnerHTML` (no `rehype-raw`).\n// - **URL-scheme sanitizer** on every `link`/`image` URL (`sanitizeUrl`).\n// - **Wikilinks resolve in-mount only** via an injected resolver; the renderer never\n// fetches.\n\nexport interface SafeContentComponents {\n /** Safe components an app exposes to `<Component/>` syntax (host/app-provided).\n * Looked up by the JSX tag name; anything not here renders inert. */\n [tag: string]: React.ComponentType<Record<string, string>>;\n}\n\nexport interface RenderMdastOptions {\n /** The component registry for `<Component/>` syntax. Absent ⇒ all JSX tags inert. */\n components?: SafeContentComponents;\n /**\n * Resolve a wikilink target to an href, **within the granted mount only** — a pure,\n * synchronous, mount-scoped lookup that MUST NOT fetch or reach out of the mount.\n * Return `undefined` for an unresolvable/out-of-mount target (rendered as inert\n * text). Absent ⇒ every wikilink renders as inert text.\n */\n resolveWikiLink?: (target: string) => string | undefined;\n}\n\n/** Extract the literal string props of a JSX element — `mdxJsxAttribute`s whose value\n * is a plain string. Expression attributes (object value) and spreads are DROPPED. */\nfunction literalProps(attributes: SafeMdxAttribute[] | undefined): Record<string, string> {\n const props: Record<string, string> = {};\n for (const attr of attributes ?? []) {\n if (attr.type !== 'mdxJsxAttribute') continue; // drop `{...spread}`\n if (typeof attr.name !== 'string') continue;\n // A literal attribute's value is a string (or null → boolean-ish `true`). An\n // expression value is an object (`mdxJsxAttributeValueExpression`) — DROP it: it\n // is an inert raw string, never a real value, and must never reach a component.\n if (typeof attr.value === 'string') props[attr.name] = attr.value;\n else if (attr.value === null || attr.value === undefined) props[attr.name] = '';\n // object-valued (expression) → skipped\n }\n return props;\n}\n\nlet keyCounter = 0;\nconst nextKey = () => `sc-${keyCounter++}`;\n\n/**\n * The element to build for a link/image: the host's registered override when it has one,\n * else the intrinsic tag.\n *\n * **Why this is safe.** `components` is the HOST's registry — the same one `<Component/>`\n * syntax resolves against — never author input. An author cannot add to it or choose one\n * here, because the lookup is by a FIXED key (`'a'`/`'img'`), not by anything read out of\n * the document. The URL is still `sanitizeUrl`'d before it is handed over, and only props\n * this renderer controls are passed. The trust boundary is unchanged: author text still\n * reaches nothing but children and a sanitized URL.\n *\n * **Why it is needed.** Without it, a safe-rendered document's links are raw `<a href>`,\n * so a click performs a REAL navigation. Inside an app's sandboxed iframe that is fatal —\n * the frame navigates away from the app, and in a routed host app it dies with\n * `Failed to construct 'URL': Invalid URL`. Hosts route links through a component for\n * exactly this reason, and the compiled-MDX path has always honoured that component; the\n * safe path silently ignored it, so one document behaved differently on the two renderers.\n */\nfunction elementFor(\n tag: 'a' | 'img',\n components: RenderMdastOptions['components'],\n): React.ElementType {\n return (components?.[tag] as React.ElementType) ?? tag;\n}\n\n/** Render a wikilink token via the in-mount resolver, or inert text when it can't\n * resolve (never a network call — resolution is the injected mount-scoped callback). */\nfunction renderWiki(token: WikiLinkToken, opts: RenderMdastOptions): ReactNode {\n const label = token.label ?? token.target;\n // R3-273: a malformed `$fs:` target (not mount-absolute — catches scheme\n // smuggling) is inert BEFORE the injected resolver ever sees it, so every\n // consumer fails closed identically. Consumers implement their resolver on the\n // same shared `resolveLinkTarget` for well-formed targets.\n if (resolveLinkTarget(token.target).state === 'invalid') return label;\n const href = opts.resolveWikiLink?.(token.target);\n const safe = href !== undefined ? sanitizeUrl(href) : undefined;\n if (safe === undefined) return label; // inert text — unresolved or out-of-mount\n return createElement(\n elementFor('a', opts.components),\n { href: safe, 'data-wikilink': token.target, key: nextKey() },\n label,\n );\n}\n\n/** Render a `text` node, splitting any `[[…]]` wikilinks out of it. */\nfunction renderText(value: string, opts: RenderMdastOptions): ReactNode {\n const parts = splitWikiLinks(value);\n if (!parts) return value;\n return parts.map((p, i) =>\n 'text' in p\n ? createElement(Fragment, { key: `t${i}` }, p.text)\n : createElement(Fragment, { key: `w${i}` }, renderWiki(p.wiki, opts)),\n );\n}\n\nfunction renderChildren(node: SafeMdastNode, opts: RenderMdastOptions): ReactNode[] {\n return (node.children ?? []).map((c, i) => renderNode(c, opts, i));\n}\n\n// Standard mdast block/inline nodes → a FIXED, safe React element. We control every\n// attribute; author input only reaches text content and (sanitized) URLs.\nconst HEADING_TAGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const;\n\nfunction renderNode(node: SafeMdastNode, opts: RenderMdastOptions, index = 0): ReactNode {\n const key = `n${index}`;\n switch (node.type) {\n case 'root':\n return createElement(Fragment, { key }, ...renderChildren(node, opts));\n case 'text':\n return renderText(node.value ?? '', opts);\n case 'paragraph':\n return createElement('p', { key }, ...renderChildren(node, opts));\n case 'heading': {\n const tag = HEADING_TAGS[Math.min(Math.max((node.depth ?? 1) - 1, 0), 5)];\n // R3-213: the no-acorn path has no hast stage, so the heading id the shared\n // `remarkHeadingAnchors` plugin sets via `data.hProperties.id` (+ the `data-slug`\n // fallback, R3-211) must be translated to React props HERE, or headings render\n // id-less and deep-linking (`#sec-8-9`) breaks in the safe path. These are\n // plugin-computed literal strings, not author input — safe to pass through.\n const hp = (node.data as { hProperties?: Record<string, unknown> } | undefined)?.hProperties;\n const headingProps: Record<string, string> = {};\n if (typeof hp?.id === 'string') headingProps.id = hp.id;\n if (typeof hp?.['data-slug'] === 'string') headingProps['data-slug'] = hp['data-slug'];\n return createElement(tag, { key, ...headingProps }, ...renderChildren(node, opts));\n }\n case 'strong':\n return createElement('strong', { key }, ...renderChildren(node, opts));\n case 'emphasis':\n return createElement('em', { key }, ...renderChildren(node, opts));\n case 'delete':\n return createElement('del', { key }, ...renderChildren(node, opts));\n case 'inlineCode':\n return createElement('code', { key }, node.value ?? '');\n case 'code':\n return createElement('pre', { key }, createElement('code', null, node.value ?? ''));\n case 'blockquote':\n return createElement('blockquote', { key }, ...renderChildren(node, opts));\n case 'list':\n return createElement(node.ordered ? 'ol' : 'ul', { key }, ...renderChildren(node, opts));\n case 'listItem':\n return createElement('li', { key }, ...renderChildren(node, opts));\n case 'thematicBreak':\n return createElement('hr', { key });\n case 'break':\n return createElement('br', { key });\n case 'link': {\n const href = sanitizeUrl(node.url);\n // A rejected scheme → render the link TEXT only (inert), never an <a href>.\n if (href === undefined) return createElement(Fragment, { key }, ...renderChildren(node, opts));\n return createElement(\n elementFor('a', opts.components),\n { key, href, title: node.title ?? undefined },\n ...renderChildren(node, opts),\n );\n }\n case 'image': {\n const src = sanitizeUrl(node.url);\n if (src === undefined) return node.alt ? createElement(Fragment, { key }, node.alt) : null;\n return createElement(elementFor('img', opts.components), {\n key,\n src,\n alt: node.alt ?? '',\n title: node.title ?? undefined,\n });\n }\n // GFM tables.\n case 'table':\n return createElement('table', { key }, createElement('tbody', null, ...renderChildren(node, opts)));\n case 'tableRow':\n return createElement('tr', { key }, ...renderChildren(node, opts));\n case 'tableCell':\n return createElement('td', { key }, ...renderChildren(node, opts));\n // Raw HTML (`<script>`, `<div onclick>` at block level) → INERT TEXT. No\n // `rehype-raw`, never `dangerouslySetInnerHTML`. The literal markup is shown, not run.\n case 'html':\n return createElement(Fragment, { key }, node.value ?? '');\n // JSX `<Component/>` syntax → registry lookup by NAME, literal props only.\n case 'mdxJsxFlowElement':\n case 'mdxJsxTextElement': {\n const name = typeof node.name === 'string' ? node.name : '';\n const Component = name ? opts.components?.[name] : undefined;\n const children = renderChildren(node, opts);\n // Unknown tag (not in the registry) OR a fragment `<>` → render children inert,\n // NO element, NO author attributes. This is what neutralizes `<script>`/`<div\n // onclick>`/`<img onerror>` written as JSX.\n if (!Component) return createElement(Fragment, { key }, ...children);\n return createElement(Component, { key, ...literalProps(node.attributes) }, ...children);\n }\n // Inert expression nodes (should not occur — expression extension is off — but be\n // defensive if a tree from elsewhere carries them): render nothing.\n case 'mdxFlowExpression':\n case 'mdxTextExpression':\n case 'mdxjsEsm':\n return null;\n default:\n // Unknown node → render its children (never its raw value as markup).\n return node.children ? createElement(Fragment, { key }, ...renderChildren(node, opts)) : null;\n }\n}\n\n/**\n * Render a safe-parsed mdast tree to React. Pure and synchronous; carries every §5.1\n * security property (registry-only JSX, dropped expressions, inert raw HTML, URL\n * sanitizing, in-mount wikilinks). Feed it a tree from {@link parseSafeMdast}.\n */\nexport function renderMdast(tree: SafeMdastNode, options: RenderMdastOptions = {}): ReactNode {\n keyCounter = 0;\n return renderNode(tree, options);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/safeContent/renderMdast.tsx"],"sourcesContent":["import { createElement, Fragment, type ReactNode } from 'react';\nimport { sanitizeUrl } from './sanitizeUrl';\nimport { splitWikiLinks, type WikiLinkToken } from './wikilink';\nimport { resolveLinkTarget } from '../linkSpace';\nimport type { SafeMdastNode, SafeMdxAttribute } from './parseSafeMdast';\n\n// The mdast→React renderer for the safe content path (TRUST_MODES_SPEC §5.1). PURE\n// and synchronous — it only reads `node.type` off an already-parsed tree, so it\n// carries no ESM dep and is exhaustively unit-testable. Every security property lives\n// here:\n//\n// - **JSX tags resolve ONLY to a component registry, by name.** A `<Component/>`\n// node becomes `registry[name]` with **literal string props only**; an unknown tag\n// renders its children inert (a Fragment, no element) — so author-written `<div\n// onclick=…>`, `<img onerror=…>`, `<script>` never become an intrinsic element with\n// attacker attributes. (Block-level raw HTML arrives as an `html` node, below.)\n// - **Expression attributes are dropped.** `f={fetch(\"/x\")}` / `{...spread}` are\n// `mdxJsxExpressionAttribute` or object-valued `mdxJsxAttribute`s — never passed to\n// a component, never evaluated. (There is also no evaluator to reach — parse used\n// no acorn.)\n// - **Raw HTML is inert text.** `html` nodes render as their literal string via a\n// text node — never `dangerouslySetInnerHTML` (no `rehype-raw`).\n// - **URL-scheme sanitizer** on every `link`/`image` URL (`sanitizeUrl`).\n// - **Wikilinks resolve in-mount only** via an injected resolver; the renderer never\n// fetches.\n\nexport interface SafeContentComponents {\n /** Safe components an app exposes to `<Component/>` syntax (host/app-provided).\n * Looked up by the JSX tag name; anything not here renders inert. */\n [tag: string]: React.ComponentType<Record<string, string>>;\n}\n\nexport interface RenderMdastOptions {\n /** The component registry for `<Component/>` syntax. Absent ⇒ all JSX tags inert. */\n components?: SafeContentComponents;\n /**\n * Resolve a wikilink target to an href, **within the granted mount only** — a pure,\n * synchronous, mount-scoped lookup that MUST NOT fetch or reach out of the mount.\n * Return `undefined` for an unresolvable/out-of-mount target (rendered as inert\n * text). Absent ⇒ every wikilink renders as inert text.\n */\n resolveWikiLink?: (target: string) => string | undefined;\n}\n\n/** Extract the literal string props of a JSX element — `mdxJsxAttribute`s whose value\n * is a plain string. Expression attributes (object value) and spreads are DROPPED. */\nfunction literalProps(attributes: SafeMdxAttribute[] | undefined): Record<string, string> {\n const props: Record<string, string> = {};\n for (const attr of attributes ?? []) {\n if (attr.type !== 'mdxJsxAttribute') continue; // drop `{...spread}`\n if (typeof attr.name !== 'string') continue;\n // A literal attribute's value is a string (or null → boolean-ish `true`). An\n // expression value is an object (`mdxJsxAttributeValueExpression`) — DROP it: it\n // is an inert raw string, never a real value, and must never reach a component.\n if (typeof attr.value === 'string') props[attr.name] = attr.value;\n else if (attr.value === null || attr.value === undefined) props[attr.name] = '';\n // object-valued (expression) → skipped\n }\n return props;\n}\n\nlet keyCounter = 0;\nconst nextKey = () => `sc-${keyCounter++}`;\n\n/**\n * The element to build for a link/image: the host's registered override when it has one,\n * else the intrinsic tag.\n *\n * **Why this is safe.** `components` is the HOST's registry — the same one `<Component/>`\n * syntax resolves against — never author input. An author cannot add to it or choose one\n * here, because the lookup is by a FIXED key (`'a'`/`'img'`), not by anything read out of\n * the document. The URL is still `sanitizeUrl`'d before it is handed over, and only props\n * this renderer controls are passed. The trust boundary is unchanged: author text still\n * reaches nothing but children and a sanitized URL.\n *\n * **Why it is needed.** Without it, a safe-rendered document's links are raw `<a href>`,\n * so a click performs a REAL navigation. Inside an app's sandboxed iframe that is fatal —\n * the frame navigates away from the app, and in a routed host app it dies with\n * `Failed to construct 'URL': Invalid URL`. Hosts route links through a component for\n * exactly this reason, and the compiled-MDX path has always honoured that component; the\n * safe path silently ignored it, so one document behaved differently on the two renderers.\n */\nfunction elementFor(tag: 'a' | 'img', components: RenderMdastOptions['components']): React.ElementType {\n return (components?.[tag] as React.ElementType) ?? tag;\n}\n\n/** Render a wikilink token via the in-mount resolver, or inert text when it can't\n * resolve (never a network call — resolution is the injected mount-scoped callback). */\nfunction renderWiki(token: WikiLinkToken, opts: RenderMdastOptions): ReactNode {\n const label = token.label ?? token.target;\n // R3-273: a malformed `$fs:` target (not mount-absolute — catches scheme\n // smuggling) is inert BEFORE the injected resolver ever sees it, so every\n // consumer fails closed identically. Consumers implement their resolver on the\n // same shared `resolveLinkTarget` for well-formed targets.\n if (resolveLinkTarget(token.target).state === 'invalid') return label;\n const href = opts.resolveWikiLink?.(token.target);\n const safe = href !== undefined ? sanitizeUrl(href) : undefined;\n if (safe === undefined) return label; // inert text — unresolved or out-of-mount\n return createElement(\n elementFor('a', opts.components),\n { href: safe, 'data-wikilink': token.target, key: nextKey() },\n label,\n );\n}\n\n/** Render a `text` node, splitting any `[[…]]` wikilinks out of it. */\nfunction renderText(value: string, opts: RenderMdastOptions): ReactNode {\n const parts = splitWikiLinks(value);\n if (!parts) return value;\n return parts.map((p, i) =>\n 'text' in p\n ? createElement(Fragment, { key: `t${i}` }, p.text)\n : createElement(Fragment, { key: `w${i}` }, renderWiki(p.wiki, opts)),\n );\n}\n\nfunction renderChildren(node: SafeMdastNode, opts: RenderMdastOptions): ReactNode[] {\n return (node.children ?? []).map((c, i) => renderNode(c, opts, i));\n}\n\n// Standard mdast block/inline nodes → a FIXED, safe React element. We control every\n// attribute; author input only reaches text content and (sanitized) URLs.\nconst HEADING_TAGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const;\n\nfunction renderNode(node: SafeMdastNode, opts: RenderMdastOptions, index = 0): ReactNode {\n const key = `n${index}`;\n switch (node.type) {\n case 'root':\n return createElement(Fragment, { key }, ...renderChildren(node, opts));\n case 'text':\n return renderText(node.value ?? '', opts);\n case 'paragraph':\n return createElement('p', { key }, ...renderChildren(node, opts));\n case 'heading': {\n const tag = HEADING_TAGS[Math.min(Math.max((node.depth ?? 1) - 1, 0), 5)];\n // R3-213: the no-acorn path has no hast stage, so the heading id the shared\n // `remarkHeadingAnchors` plugin sets via `data.hProperties.id` (+ the `data-slug`\n // fallback, R3-211) must be translated to React props HERE, or headings render\n // id-less and deep-linking (`#sec-8-9`) breaks in the safe path. These are\n // plugin-computed literal strings, not author input — safe to pass through.\n const hp = (node.data as { hProperties?: Record<string, unknown> } | undefined)?.hProperties;\n const headingProps: Record<string, string> = {};\n if (typeof hp?.id === 'string') headingProps.id = hp.id;\n if (typeof hp?.['data-slug'] === 'string') headingProps['data-slug'] = hp['data-slug'];\n return createElement(tag, { key, ...headingProps }, ...renderChildren(node, opts));\n }\n case 'strong':\n return createElement('strong', { key }, ...renderChildren(node, opts));\n case 'emphasis':\n return createElement('em', { key }, ...renderChildren(node, opts));\n case 'delete':\n return createElement('del', { key }, ...renderChildren(node, opts));\n case 'inlineCode':\n return createElement('code', { key }, node.value ?? '');\n case 'code':\n return createElement('pre', { key }, createElement('code', null, node.value ?? ''));\n case 'blockquote':\n return createElement('blockquote', { key }, ...renderChildren(node, opts));\n case 'list':\n return createElement(node.ordered ? 'ol' : 'ul', { key }, ...renderChildren(node, opts));\n case 'listItem':\n return createElement('li', { key }, ...renderChildren(node, opts));\n case 'thematicBreak':\n return createElement('hr', { key });\n case 'break':\n return createElement('br', { key });\n case 'link': {\n const href = sanitizeUrl(node.url);\n // A rejected scheme → render the link TEXT only (inert), never an <a href>.\n if (href === undefined) return createElement(Fragment, { key }, ...renderChildren(node, opts));\n return createElement(\n elementFor('a', opts.components),\n { key, href, title: node.title ?? undefined },\n ...renderChildren(node, opts),\n );\n }\n case 'image': {\n const src = sanitizeUrl(node.url);\n if (src === undefined) return node.alt ? createElement(Fragment, { key }, node.alt) : null;\n return createElement(elementFor('img', opts.components), {\n key,\n src,\n alt: node.alt ?? '',\n title: node.title ?? undefined,\n });\n }\n // GFM tables.\n case 'table':\n return createElement('table', { key }, createElement('tbody', null, ...renderChildren(node, opts)));\n case 'tableRow':\n return createElement('tr', { key }, ...renderChildren(node, opts));\n case 'tableCell':\n return createElement('td', { key }, ...renderChildren(node, opts));\n // Raw HTML (`<script>`, `<div onclick>` at block level) → INERT TEXT. No\n // `rehype-raw`, never `dangerouslySetInnerHTML`. The literal markup is shown, not run.\n case 'html':\n return createElement(Fragment, { key }, node.value ?? '');\n // JSX `<Component/>` syntax → registry lookup by NAME, literal props only.\n case 'mdxJsxFlowElement':\n case 'mdxJsxTextElement': {\n const name = typeof node.name === 'string' ? node.name : '';\n const Component = name ? opts.components?.[name] : undefined;\n const children = renderChildren(node, opts);\n // Unknown tag (not in the registry) OR a fragment `<>` → render children inert,\n // NO element, NO author attributes. This is what neutralizes `<script>`/`<div\n // onclick>`/`<img onerror>` written as JSX.\n if (!Component) return createElement(Fragment, { key }, ...children);\n return createElement(Component, { key, ...literalProps(node.attributes) }, ...children);\n }\n // Inert expression nodes (should not occur — expression extension is off — but be\n // defensive if a tree from elsewhere carries them): render nothing.\n case 'mdxFlowExpression':\n case 'mdxTextExpression':\n case 'mdxjsEsm':\n return null;\n default:\n // Unknown node → render its children (never its raw value as markup).\n return node.children ? createElement(Fragment, { key }, ...renderChildren(node, opts)) : null;\n }\n}\n\n/**\n * Render a safe-parsed mdast tree to React. Pure and synchronous; carries every §5.1\n * security property (registry-only JSX, dropped expressions, inert raw HTML, URL\n * sanitizing, in-mount wikilinks). Feed it a tree from {@link parseSafeMdast}.\n */\nexport function renderMdast(tree: SafeMdastNode, options: RenderMdastOptions = {}): ReactNode {\n keyCounter = 0;\n return renderNode(tree, options);\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sandboxTypes.ts"],"sourcesContent":["/** The exports object of an evaluated sandbox module (untyped — shape depends on the module). */\nexport type ModuleExports = any\n\n/** The sandbox runtime's per-module evaluation context: a module's exports plus the\n * helpers to dynamically import, resolve, and re-evaluate other modules.\n * (The real type is `EvaluationContext` from `src/bundler/module/Evaluation.ts`.) */\nexport type EvaluationContext = {\n exports: ModuleExports;\n dynamicImport: (moduleToImport: string, symbolToImport:string) => Promise<ModuleExports>;\n getModuleEvaluationContext: (moduleName: string) => Promise<EvaluationContext>;\n resolve: (moduleName: string) => Promise<string>;\n evaluation: {\n module: {\n source: string;\n filepath: string;\n }\n }\n}\n\n/**\n * The parsed frontmatter of a single file. Apps can supply their own shape as the\n * `T` type parameter on the metadata hooks for typed field access; it defaults to\n * an open record.\n *\n * The ENVELOPE this widens is `Frontmatter` from\n * `@immediately-run/platform-constants` (R3-275): string keys, JSON-serializable\n * values, the emitter's object-identity semantics, and the empty-frontmatter drop —\n * one statement of the contract, shared with the CLI that writes the sidecar and the\n * sandbox that reads it. This alias stays `any`-valued deliberately: app code\n * indexes frontmatter fields directly (`meta.title.length`), and tightening it to\n * `JsonValue` would turn every such access into a type error for a guarantee the\n * *values* never made (they are open by the spec's §6 decision).\n */\nexport type Metadata = Record<string, any>;\n\n/** The whole metadata store: a map from repo-relative file path to its frontmatter. */\nexport type FilesMetadata<T = Metadata> = Record<string, T>;\n\n/**\n * A record a query may return INSTEAD of a bare path (R3-276): the path plus\n * whatever the query computed on the way to selecting it.\n *\n * The motivating case is a query that derives something per match — a sort key, a\n * section, a formatted date — and would otherwise have to recompute it downstream\n * from `meta`, or smuggle it through a closure. Returning it here keeps the\n * derivation next to the selection that needed it.\n */\nexport type MetadataQueryRecord = { path: string } & Record<string, unknown>;\n\n/** What a {@link MetadataQueryFunction} selected: paths, or {@link MetadataQueryRecord}s.\n * The two forms may not be mixed in one result — a query returns one or the other. */\nexport type FileQueryResult = string[] | MetadataQueryRecord[]\n\n/**\n * A query over the metadata store: receive every file's frontmatter keyed by path\n * and return the paths that match. Plain JS — `Object.entries(...).filter(...)` —\n * no query language to learn.\n *\n * Return bare paths, or records carrying extra fields alongside `path` (R3-276);\n * either way the hook resolves each path to its frontmatter.\n */\nexport type MetadataQueryFunction<T = Metadata> = (filesMetadata: FilesMetadata<T>) => FileQueryResult;\n\n/**\n * One match from {@link MetadataQueryFunction}: the file path paired with its\n * frontmatter, plus any extra fields the query returned as a\n * {@link MetadataQueryRecord}.\n *\n * `E` defaults to `{}`, so every existing `MetadataQueryEntry<T>` keeps meaning\n * exactly what it meant — the extra-fields form is opt-in at the type level.\n * `path` and `meta` are applied AFTER the record's own fields, so a query cannot\n * shadow them with something else.\n */\nexport type MetadataQueryEntry<T = Metadata, E extends object = {}> = E & {\n path: string;\n meta: T;\n};\n\n/**\n * The result of running a metadata query: the matched entries (path + frontmatter),\n * or the error a throwing query produced.\n */\nexport type MetadataQueryResult<T = Metadata, E extends object = {}> =\n | MetadataQueryEntry<T, E>[]\n | { error: unknown }\n"],"mappings":";;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
1
+ {"version":3,"sources":["../src/sandboxTypes.ts"],"sourcesContent":["/** The exports object of an evaluated sandbox module (untyped — shape depends on the module). */\nexport type ModuleExports = any;\n\n/** The sandbox runtime's per-module evaluation context: a module's exports plus the\n * helpers to dynamically import, resolve, and re-evaluate other modules.\n * (The real type is `EvaluationContext` from `src/bundler/module/Evaluation.ts`.) */\nexport type EvaluationContext = {\n exports: ModuleExports;\n dynamicImport: (moduleToImport: string, symbolToImport: string) => Promise<ModuleExports>;\n getModuleEvaluationContext: (moduleName: string) => Promise<EvaluationContext>;\n resolve: (moduleName: string) => Promise<string>;\n evaluation: {\n module: {\n source: string;\n filepath: string;\n };\n };\n};\n\n/**\n * The parsed frontmatter of a single file. Apps can supply their own shape as the\n * `T` type parameter on the metadata hooks for typed field access; it defaults to\n * an open record.\n *\n * The ENVELOPE this widens is `Frontmatter` from\n * `@immediately-run/platform-constants` (R3-275): string keys, JSON-serializable\n * values, the emitter's object-identity semantics, and the empty-frontmatter drop —\n * one statement of the contract, shared with the CLI that writes the sidecar and the\n * sandbox that reads it. This alias stays `any`-valued deliberately: app code\n * indexes frontmatter fields directly (`meta.title.length`), and tightening it to\n * `JsonValue` would turn every such access into a type error for a guarantee the\n * *values* never made (they are open by the spec's §6 decision).\n */\nexport type Metadata = Record<string, any>;\n\n/** The whole metadata store: a map from repo-relative file path to its frontmatter. */\nexport type FilesMetadata<T = Metadata> = Record<string, T>;\n\n/**\n * A record a query may return INSTEAD of a bare path (R3-276): the path plus\n * whatever the query computed on the way to selecting it.\n *\n * The motivating case is a query that derives something per match — a sort key, a\n * section, a formatted date — and would otherwise have to recompute it downstream\n * from `meta`, or smuggle it through a closure. Returning it here keeps the\n * derivation next to the selection that needed it.\n */\nexport type MetadataQueryRecord = { path: string } & Record<string, unknown>;\n\n/** What a {@link MetadataQueryFunction} selected: paths, or {@link MetadataQueryRecord}s.\n * The two forms may not be mixed in one result — a query returns one or the other. */\nexport type FileQueryResult = string[] | MetadataQueryRecord[];\n\n/**\n * A query over the metadata store: receive every file's frontmatter keyed by path\n * and return the paths that match. Plain JS — `Object.entries(...).filter(...)` —\n * no query language to learn.\n *\n * Return bare paths, or records carrying extra fields alongside `path` (R3-276);\n * either way the hook resolves each path to its frontmatter.\n */\nexport type MetadataQueryFunction<T = Metadata> = (filesMetadata: FilesMetadata<T>) => FileQueryResult;\n\n/**\n * One match from {@link MetadataQueryFunction}: the file path paired with its\n * frontmatter, plus any extra fields the query returned as a\n * {@link MetadataQueryRecord}.\n *\n * `E` defaults to `{}`, so every existing `MetadataQueryEntry<T>` keeps meaning\n * exactly what it meant — the extra-fields form is opt-in at the type level.\n * `path` and `meta` are applied AFTER the record's own fields, so a query cannot\n * shadow them with something else.\n */\nexport type MetadataQueryEntry<T = Metadata, E extends object = {}> = E & {\n path: string;\n meta: T;\n};\n\n/**\n * The result of running a metadata query: the matched entries (path + frontmatter),\n * or the error a throwing query produced.\n */\nexport type MetadataQueryResult<T = Metadata, E extends object = {}> = MetadataQueryEntry<T, E>[] | { error: unknown };\n"],"mappings":";;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
@@ -28,12 +28,7 @@ var import_hostTransport = require("./hostTransport");
28
28
  var import_hostAttention = require("./hostAttention");
29
29
  var import_protocolDeadline = require("./protocolDeadline");
30
30
  var import_hostTransport2 = require("./hostTransport");
31
- const protocolRequest = (protocolName, method, params, opts) => withDeadline(
32
- protocolName,
33
- method,
34
- () => (0, import_hostTransport.transport)().protocolRequest(protocolName, method, params),
35
- opts
36
- );
31
+ const protocolRequest = (protocolName, method, params, opts) => withDeadline(protocolName, method, () => (0, import_hostTransport.transport)().protocolRequest(protocolName, method, params), opts);
37
32
  async function withDeadline(scheme, method, start, opts) {
38
33
  const call = `${scheme}:${method}`;
39
34
  const attendance = (0, import_protocolDeadline.attendanceOf)(scheme, method);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sandboxUtils.ts"],"sourcesContent":["// Bounded host calls (R3-298) + the host-attention-aware deadline (R3-307).\n//\n// The transport primitives this builds on moved to `hostTransport.ts` — see that file for\n// why. `sendMessage` and `addListener` are re-exported here unchanged, so every existing\n// import site (and this module's api-snapshot entry) is exactly as it was.\nimport { transport } from './hostTransport';\nimport { onHostAttentionChange, type HostAttention } from './hostAttention';\n\nimport {\n attendanceOf,\n attendanceReason,\n boundsFor,\n createSuspendableDeadline,\n PENDING_NOTICE_MS,\n ProtocolCancelledError,\n ProtocolTimeoutError,\n type BoundedCallOptions,\n type CallBounds,\n} from './protocolDeadline';\n\nexport { sendMessage, addListener } from './hostTransport';\n\n/**\n * One host protocol request, BOUNDED (R3-298).\n *\n * The bound comes from the call's classification (`protocolDeadline.ts`): an unattended\n * channel round-trip gets tens of seconds, a call that may draw host chrome and wait for a\n * person gets minutes. Nothing is unbounded — an unbounded wait is the failure this fixes.\n *\n * Since R3-307 a call whose prompts the host actually announces runs on the SHORT bound and\n * is suspended only while the host says a person is being asked, so the common\n * grant-already-held path reports a fault in seconds instead of minutes. The absolute\n * ceiling still applies: the signal may extend a deadline, never remove it.\n *\n * The host's own work is NOT cancelled by `signal` or by the deadline. The one-shot\n * transport allocates its `msgId` internally, so the SDK has no handle to send the host a\n * cancel for (streams do, and `consumeStream` uses it). What the caller gets back is\n * control: it stops waiting and gets a typed error instead of hanging. A host prompt that\n * is already on screen stays there until the user dismisses it, which is correct — the SDK\n * must not be able to tear down host chrome the user is looking at.\n */\nexport const protocolRequest = (\n protocolName: string,\n method: string,\n params: Array<any>,\n opts?: BoundedCallOptions,\n): Promise<any> =>\n withDeadline(\n protocolName,\n method,\n () => transport().protocolRequest(protocolName, method, params),\n opts,\n );\n\n/**\n * Race a host call against its deadline, a caller abort, and a pending notice.\n *\n * Kept separate from the transport so it is unit-testable against a promise that simply\n * never settles — which is the whole scenario, and one no live transport reproduces on\n * demand.\n *\n * An explicit `opts.timeoutMs` is the WHOLE bound and is never suspended: a caller that\n * names a number owns the wait, and silently stretching it past what they asked for would\n * be the same class of surprise this machinery exists to remove.\n */\nexport async function withDeadline<T>(\n scheme: string,\n method: string,\n start: () => Promise<T>,\n opts?: BoundedCallOptions,\n): Promise<T> {\n const call = `${scheme}:${method}`;\n const attendance = attendanceOf(scheme, method);\n const bounds: CallBounds =\n opts?.timeoutMs !== undefined\n ? { idleMs: opts.timeoutMs, ceilingMs: opts.timeoutMs }\n : boundsFor(scheme, method);\n const signal = opts?.signal;\n\n if (signal?.aborted) throw new ProtocolCancelledError(call);\n\n const work = start();\n // No bound and no abort wanted: hand back the untouched promise rather than wrapping it\n // in timers that would never fire.\n if (!Number.isFinite(bounds.ceilingMs) && !signal && !opts?.onPending) return work;\n\n let deadline: { setAwaiting: (a: boolean) => void; dispose: () => void } | undefined;\n let notice: ReturnType<typeof setTimeout> | undefined;\n let unsubscribeAttention: (() => void) | undefined;\n let onAbort: (() => void) | undefined;\n const started = Date.now();\n\n try {\n return await new Promise<T>((resolve, reject) => {\n // `work` settling always wins — a call that answered a millisecond before its\n // deadline must not be reported as a timeout.\n work.then(resolve, reject);\n\n // The host's live \"a person is being asked\" signal. Read defensively: a host that\n // never pushes the channel leaves this at \"not awaiting\", which is exactly the\n // pre-R3-307 behaviour for a call on its idle bound.\n let attention: HostAttention | undefined;\n let noticeFired = false;\n const fireNotice = () => {\n try {\n opts?.onPending?.({\n call,\n attendance,\n elapsedMs: Date.now() - started,\n ...(attendanceReason(scheme, method)\n ? { reason: attendanceReason(scheme, method) as string }\n : {}),\n ...(attention?.awaiting\n ? { awaiting: { kind: attention.kind, since: attention.since } }\n : {}),\n });\n } catch {\n /* a caller's render callback must never break the call it describes */\n }\n };\n\n deadline = createSuspendableDeadline({\n bounds,\n onExpire: (bound, elapsedBoundMs) =>\n reject(new ProtocolTimeoutError(call, elapsedBoundMs, attendance, bound)),\n });\n\n if (opts?.onPending) {\n notice = setTimeout(() => {\n noticeFired = true;\n fireNotice();\n }, PENDING_NOTICE_MS);\n }\n\n // Subscribing invokes the listener immediately with the current value, so the\n // deadline starts in the right state even if a prompt was already up.\n //\n // Guarded because this sits on the path of EVERY host call: a fault in the attention\n // channel must degrade to \"no signal\" (the pre-R3-307 behaviour, bounds unsuspended),\n // never take down every request in the SDK.\n try {\n unsubscribeAttention = onHostAttentionChange((next) => {\n const wasAwaiting = attention?.awaiting ?? false;\n attention = next;\n deadline?.setAwaiting(next.awaiting);\n // Re-notify once the notice has fired: \"still waiting\" → \"tap your passkey\" is\n // the whole point, and a caller rendering a waiting state wants the live\n // sentence, not the one that was true three seconds in.\n if (noticeFired && next.awaiting !== wasAwaiting) fireNotice();\n });\n } catch {\n /* no attention signal available — the bounds simply never suspend */\n }\n\n if (signal) {\n onAbort = () => reject(new ProtocolCancelledError(call));\n signal.addEventListener('abort', onAbort, { once: true });\n }\n });\n } finally {\n deadline?.dispose();\n if (notice !== undefined) clearTimeout(notice);\n unsubscribeAttention?.();\n if (signal && onAbort) signal.removeEventListener('abort', onAbort);\n // The host may still answer after we stopped waiting; swallow it so a late rejection\n // does not surface as an unhandled promise rejection in the app's console.\n work.catch(() => undefined);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAKA,2BAA0B;AAC1B,2BAA0D;AAE1D,8BAUO;AAEP,IAAAA,wBAAyC;AAqBlC,MAAM,kBAAkB,CAC7B,cACA,QACA,QACA,SAEA;AAAA,EACE;AAAA,EACA;AAAA,EACA,UAAM,gCAAU,EAAE,gBAAgB,cAAc,QAAQ,MAAM;AAAA,EAC9D;AACF;AAaF,eAAsB,aACpB,QACA,QACA,OACA,MACY;AACZ,QAAM,OAAO,GAAG,MAAM,IAAI,MAAM;AAChC,QAAM,iBAAa,sCAAa,QAAQ,MAAM;AAC9C,QAAM,SACJ,MAAM,cAAc,SAChB,EAAE,QAAQ,KAAK,WAAW,WAAW,KAAK,UAAU,QACpD,mCAAU,QAAQ,MAAM;AAC9B,QAAM,SAAS,MAAM;AAErB,MAAI,QAAQ,QAAS,OAAM,IAAI,+CAAuB,IAAI;AAE1D,QAAM,OAAO,MAAM;AAGnB,MAAI,CAAC,OAAO,SAAS,OAAO,SAAS,KAAK,CAAC,UAAU,CAAC,MAAM,UAAW,QAAO;AAE9E,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,QAAM,UAAU,KAAK,IAAI;AAEzB,MAAI;AACF,WAAO,MAAM,IAAI,QAAW,CAAC,SAAS,WAAW;AAG/C,WAAK,KAAK,SAAS,MAAM;AAKzB,UAAI;AACJ,UAAI,cAAc;AAClB,YAAM,aAAa,MAAM;AACvB,YAAI;AACF,gBAAM,YAAY;AAAA,YAChB;AAAA,YACA;AAAA,YACA,WAAW,KAAK,IAAI,IAAI;AAAA,YACxB,OAAI,0CAAiB,QAAQ,MAAM,IAC/B,EAAE,YAAQ,0CAAiB,QAAQ,MAAM,EAAY,IACrD,CAAC;AAAA,YACL,GAAI,WAAW,WACX,EAAE,UAAU,EAAE,MAAM,UAAU,MAAM,OAAO,UAAU,MAAM,EAAE,IAC7D,CAAC;AAAA,UACP,CAAC;AAAA,QACH,QAAQ;AAAA,QAER;AAAA,MACF;AAEA,qBAAW,mDAA0B;AAAA,QACnC;AAAA,QACA,UAAU,CAAC,OAAO,mBAChB,OAAO,IAAI,6CAAqB,MAAM,gBAAgB,YAAY,KAAK,CAAC;AAAA,MAC5E,CAAC;AAED,UAAI,MAAM,WAAW;AACnB,iBAAS,WAAW,MAAM;AACxB,wBAAc;AACd,qBAAW;AAAA,QACb,GAAG,yCAAiB;AAAA,MACtB;AAQA,UAAI;AACF,mCAAuB,4CAAsB,CAAC,SAAS;AACrD,gBAAM,cAAc,WAAW,YAAY;AAC3C,sBAAY;AACZ,oBAAU,YAAY,KAAK,QAAQ;AAInC,cAAI,eAAe,KAAK,aAAa,YAAa,YAAW;AAAA,QAC/D,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAEA,UAAI,QAAQ;AACV,kBAAU,MAAM,OAAO,IAAI,+CAAuB,IAAI,CAAC;AACvD,eAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAAA,MAC1D;AAAA,IACF,CAAC;AAAA,EACH,UAAE;AACA,cAAU,QAAQ;AAClB,QAAI,WAAW,OAAW,cAAa,MAAM;AAC7C,2BAAuB;AACvB,QAAI,UAAU,QAAS,QAAO,oBAAoB,SAAS,OAAO;AAGlE,SAAK,MAAM,MAAM,MAAS;AAAA,EAC5B;AACF;","names":["import_hostTransport"]}
1
+ {"version":3,"sources":["../src/sandboxUtils.ts"],"sourcesContent":["// Bounded host calls (R3-298) + the host-attention-aware deadline (R3-307).\n//\n// The transport primitives this builds on moved to `hostTransport.ts` — see that file for\n// why. `sendMessage` and `addListener` are re-exported here unchanged, so every existing\n// import site (and this module's api-snapshot entry) is exactly as it was.\nimport { transport } from './hostTransport';\nimport { onHostAttentionChange, type HostAttention } from './hostAttention';\n\nimport {\n attendanceOf,\n attendanceReason,\n boundsFor,\n createSuspendableDeadline,\n PENDING_NOTICE_MS,\n ProtocolCancelledError,\n ProtocolTimeoutError,\n type BoundedCallOptions,\n type CallBounds,\n} from './protocolDeadline';\n\nexport { sendMessage, addListener } from './hostTransport';\n\n/**\n * One host protocol request, BOUNDED (R3-298).\n *\n * The bound comes from the call's classification (`protocolDeadline.ts`): an unattended\n * channel round-trip gets tens of seconds, a call that may draw host chrome and wait for a\n * person gets minutes. Nothing is unbounded — an unbounded wait is the failure this fixes.\n *\n * Since R3-307 a call whose prompts the host actually announces runs on the SHORT bound and\n * is suspended only while the host says a person is being asked, so the common\n * grant-already-held path reports a fault in seconds instead of minutes. The absolute\n * ceiling still applies: the signal may extend a deadline, never remove it.\n *\n * The host's own work is NOT cancelled by `signal` or by the deadline. The one-shot\n * transport allocates its `msgId` internally, so the SDK has no handle to send the host a\n * cancel for (streams do, and `consumeStream` uses it). What the caller gets back is\n * control: it stops waiting and gets a typed error instead of hanging. A host prompt that\n * is already on screen stays there until the user dismisses it, which is correct — the SDK\n * must not be able to tear down host chrome the user is looking at.\n */\nexport const protocolRequest = (\n protocolName: string,\n method: string,\n params: Array<any>,\n opts?: BoundedCallOptions,\n): Promise<any> =>\n withDeadline(protocolName, method, () => transport().protocolRequest(protocolName, method, params), opts);\n\n/**\n * Race a host call against its deadline, a caller abort, and a pending notice.\n *\n * Kept separate from the transport so it is unit-testable against a promise that simply\n * never settles — which is the whole scenario, and one no live transport reproduces on\n * demand.\n *\n * An explicit `opts.timeoutMs` is the WHOLE bound and is never suspended: a caller that\n * names a number owns the wait, and silently stretching it past what they asked for would\n * be the same class of surprise this machinery exists to remove.\n */\nexport async function withDeadline<T>(\n scheme: string,\n method: string,\n start: () => Promise<T>,\n opts?: BoundedCallOptions,\n): Promise<T> {\n const call = `${scheme}:${method}`;\n const attendance = attendanceOf(scheme, method);\n const bounds: CallBounds =\n opts?.timeoutMs !== undefined ? { idleMs: opts.timeoutMs, ceilingMs: opts.timeoutMs } : boundsFor(scheme, method);\n const signal = opts?.signal;\n\n if (signal?.aborted) throw new ProtocolCancelledError(call);\n\n const work = start();\n // No bound and no abort wanted: hand back the untouched promise rather than wrapping it\n // in timers that would never fire.\n if (!Number.isFinite(bounds.ceilingMs) && !signal && !opts?.onPending) return work;\n\n let deadline: { setAwaiting: (a: boolean) => void; dispose: () => void } | undefined;\n let notice: ReturnType<typeof setTimeout> | undefined;\n let unsubscribeAttention: (() => void) | undefined;\n let onAbort: (() => void) | undefined;\n const started = Date.now();\n\n try {\n return await new Promise<T>((resolve, reject) => {\n // `work` settling always wins — a call that answered a millisecond before its\n // deadline must not be reported as a timeout.\n work.then(resolve, reject);\n\n // The host's live \"a person is being asked\" signal. Read defensively: a host that\n // never pushes the channel leaves this at \"not awaiting\", which is exactly the\n // pre-R3-307 behaviour for a call on its idle bound.\n let attention: HostAttention | undefined;\n let noticeFired = false;\n const fireNotice = () => {\n try {\n opts?.onPending?.({\n call,\n attendance,\n elapsedMs: Date.now() - started,\n ...(attendanceReason(scheme, method) ? { reason: attendanceReason(scheme, method) as string } : {}),\n ...(attention?.awaiting ? { awaiting: { kind: attention.kind, since: attention.since } } : {}),\n });\n } catch {\n /* a caller's render callback must never break the call it describes */\n }\n };\n\n deadline = createSuspendableDeadline({\n bounds,\n onExpire: (bound, elapsedBoundMs) => reject(new ProtocolTimeoutError(call, elapsedBoundMs, attendance, bound)),\n });\n\n if (opts?.onPending) {\n notice = setTimeout(() => {\n noticeFired = true;\n fireNotice();\n }, PENDING_NOTICE_MS);\n }\n\n // Subscribing invokes the listener immediately with the current value, so the\n // deadline starts in the right state even if a prompt was already up.\n //\n // Guarded because this sits on the path of EVERY host call: a fault in the attention\n // channel must degrade to \"no signal\" (the pre-R3-307 behaviour, bounds unsuspended),\n // never take down every request in the SDK.\n try {\n unsubscribeAttention = onHostAttentionChange((next) => {\n const wasAwaiting = attention?.awaiting ?? false;\n attention = next;\n deadline?.setAwaiting(next.awaiting);\n // Re-notify once the notice has fired: \"still waiting\" → \"tap your passkey\" is\n // the whole point, and a caller rendering a waiting state wants the live\n // sentence, not the one that was true three seconds in.\n if (noticeFired && next.awaiting !== wasAwaiting) fireNotice();\n });\n } catch {\n /* no attention signal available — the bounds simply never suspend */\n }\n\n if (signal) {\n onAbort = () => reject(new ProtocolCancelledError(call));\n signal.addEventListener('abort', onAbort, { once: true });\n }\n });\n } finally {\n deadline?.dispose();\n if (notice !== undefined) clearTimeout(notice);\n unsubscribeAttention?.();\n if (signal && onAbort) signal.removeEventListener('abort', onAbort);\n // The host may still answer after we stopped waiting; swallow it so a late rejection\n // does not surface as an unhandled promise rejection in the app's console.\n work.catch(() => undefined);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAKA,2BAA0B;AAC1B,2BAA0D;AAE1D,8BAUO;AAEP,IAAAA,wBAAyC;AAqBlC,MAAM,kBAAkB,CAC7B,cACA,QACA,QACA,SAEA,aAAa,cAAc,QAAQ,UAAM,gCAAU,EAAE,gBAAgB,cAAc,QAAQ,MAAM,GAAG,IAAI;AAa1G,eAAsB,aACpB,QACA,QACA,OACA,MACY;AACZ,QAAM,OAAO,GAAG,MAAM,IAAI,MAAM;AAChC,QAAM,iBAAa,sCAAa,QAAQ,MAAM;AAC9C,QAAM,SACJ,MAAM,cAAc,SAAY,EAAE,QAAQ,KAAK,WAAW,WAAW,KAAK,UAAU,QAAI,mCAAU,QAAQ,MAAM;AAClH,QAAM,SAAS,MAAM;AAErB,MAAI,QAAQ,QAAS,OAAM,IAAI,+CAAuB,IAAI;AAE1D,QAAM,OAAO,MAAM;AAGnB,MAAI,CAAC,OAAO,SAAS,OAAO,SAAS,KAAK,CAAC,UAAU,CAAC,MAAM,UAAW,QAAO;AAE9E,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,QAAM,UAAU,KAAK,IAAI;AAEzB,MAAI;AACF,WAAO,MAAM,IAAI,QAAW,CAAC,SAAS,WAAW;AAG/C,WAAK,KAAK,SAAS,MAAM;AAKzB,UAAI;AACJ,UAAI,cAAc;AAClB,YAAM,aAAa,MAAM;AACvB,YAAI;AACF,gBAAM,YAAY;AAAA,YAChB;AAAA,YACA;AAAA,YACA,WAAW,KAAK,IAAI,IAAI;AAAA,YACxB,OAAI,0CAAiB,QAAQ,MAAM,IAAI,EAAE,YAAQ,0CAAiB,QAAQ,MAAM,EAAY,IAAI,CAAC;AAAA,YACjG,GAAI,WAAW,WAAW,EAAE,UAAU,EAAE,MAAM,UAAU,MAAM,OAAO,UAAU,MAAM,EAAE,IAAI,CAAC;AAAA,UAC9F,CAAC;AAAA,QACH,QAAQ;AAAA,QAER;AAAA,MACF;AAEA,qBAAW,mDAA0B;AAAA,QACnC;AAAA,QACA,UAAU,CAAC,OAAO,mBAAmB,OAAO,IAAI,6CAAqB,MAAM,gBAAgB,YAAY,KAAK,CAAC;AAAA,MAC/G,CAAC;AAED,UAAI,MAAM,WAAW;AACnB,iBAAS,WAAW,MAAM;AACxB,wBAAc;AACd,qBAAW;AAAA,QACb,GAAG,yCAAiB;AAAA,MACtB;AAQA,UAAI;AACF,mCAAuB,4CAAsB,CAAC,SAAS;AACrD,gBAAM,cAAc,WAAW,YAAY;AAC3C,sBAAY;AACZ,oBAAU,YAAY,KAAK,QAAQ;AAInC,cAAI,eAAe,KAAK,aAAa,YAAa,YAAW;AAAA,QAC/D,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAEA,UAAI,QAAQ;AACV,kBAAU,MAAM,OAAO,IAAI,+CAAuB,IAAI,CAAC;AACvD,eAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAAA,MAC1D;AAAA,IACF,CAAC;AAAA,EACH,UAAE;AACA,cAAU,QAAQ;AAClB,QAAI,WAAW,OAAW,cAAa,MAAM;AAC7C,2BAAuB;AACvB,QAAI,UAAU,QAAS,QAAO,oBAAoB,SAAS,OAAO;AAGlE,SAAK,MAAM,MAAM,MAAS;AAAA,EAC5B;AACF;","names":["import_hostTransport"]}
@@ -11,12 +11,7 @@ import {
11
11
  ProtocolTimeoutError
12
12
  } from "./protocolDeadline";
13
13
  import { sendMessage, addListener } from "./hostTransport";
14
- const protocolRequest = (protocolName, method, params, opts) => withDeadline(
15
- protocolName,
16
- method,
17
- () => transport().protocolRequest(protocolName, method, params),
18
- opts
19
- );
14
+ const protocolRequest = (protocolName, method, params, opts) => withDeadline(protocolName, method, () => transport().protocolRequest(protocolName, method, params), opts);
20
15
  async function withDeadline(scheme, method, start, opts) {
21
16
  const call = `${scheme}:${method}`;
22
17
  const attendance = attendanceOf(scheme, method);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sandboxUtils.ts"],"sourcesContent":["// Bounded host calls (R3-298) + the host-attention-aware deadline (R3-307).\n//\n// The transport primitives this builds on moved to `hostTransport.ts` — see that file for\n// why. `sendMessage` and `addListener` are re-exported here unchanged, so every existing\n// import site (and this module's api-snapshot entry) is exactly as it was.\nimport { transport } from './hostTransport';\nimport { onHostAttentionChange, type HostAttention } from './hostAttention';\n\nimport {\n attendanceOf,\n attendanceReason,\n boundsFor,\n createSuspendableDeadline,\n PENDING_NOTICE_MS,\n ProtocolCancelledError,\n ProtocolTimeoutError,\n type BoundedCallOptions,\n type CallBounds,\n} from './protocolDeadline';\n\nexport { sendMessage, addListener } from './hostTransport';\n\n/**\n * One host protocol request, BOUNDED (R3-298).\n *\n * The bound comes from the call's classification (`protocolDeadline.ts`): an unattended\n * channel round-trip gets tens of seconds, a call that may draw host chrome and wait for a\n * person gets minutes. Nothing is unbounded — an unbounded wait is the failure this fixes.\n *\n * Since R3-307 a call whose prompts the host actually announces runs on the SHORT bound and\n * is suspended only while the host says a person is being asked, so the common\n * grant-already-held path reports a fault in seconds instead of minutes. The absolute\n * ceiling still applies: the signal may extend a deadline, never remove it.\n *\n * The host's own work is NOT cancelled by `signal` or by the deadline. The one-shot\n * transport allocates its `msgId` internally, so the SDK has no handle to send the host a\n * cancel for (streams do, and `consumeStream` uses it). What the caller gets back is\n * control: it stops waiting and gets a typed error instead of hanging. A host prompt that\n * is already on screen stays there until the user dismisses it, which is correct — the SDK\n * must not be able to tear down host chrome the user is looking at.\n */\nexport const protocolRequest = (\n protocolName: string,\n method: string,\n params: Array<any>,\n opts?: BoundedCallOptions,\n): Promise<any> =>\n withDeadline(\n protocolName,\n method,\n () => transport().protocolRequest(protocolName, method, params),\n opts,\n );\n\n/**\n * Race a host call against its deadline, a caller abort, and a pending notice.\n *\n * Kept separate from the transport so it is unit-testable against a promise that simply\n * never settles — which is the whole scenario, and one no live transport reproduces on\n * demand.\n *\n * An explicit `opts.timeoutMs` is the WHOLE bound and is never suspended: a caller that\n * names a number owns the wait, and silently stretching it past what they asked for would\n * be the same class of surprise this machinery exists to remove.\n */\nexport async function withDeadline<T>(\n scheme: string,\n method: string,\n start: () => Promise<T>,\n opts?: BoundedCallOptions,\n): Promise<T> {\n const call = `${scheme}:${method}`;\n const attendance = attendanceOf(scheme, method);\n const bounds: CallBounds =\n opts?.timeoutMs !== undefined\n ? { idleMs: opts.timeoutMs, ceilingMs: opts.timeoutMs }\n : boundsFor(scheme, method);\n const signal = opts?.signal;\n\n if (signal?.aborted) throw new ProtocolCancelledError(call);\n\n const work = start();\n // No bound and no abort wanted: hand back the untouched promise rather than wrapping it\n // in timers that would never fire.\n if (!Number.isFinite(bounds.ceilingMs) && !signal && !opts?.onPending) return work;\n\n let deadline: { setAwaiting: (a: boolean) => void; dispose: () => void } | undefined;\n let notice: ReturnType<typeof setTimeout> | undefined;\n let unsubscribeAttention: (() => void) | undefined;\n let onAbort: (() => void) | undefined;\n const started = Date.now();\n\n try {\n return await new Promise<T>((resolve, reject) => {\n // `work` settling always wins — a call that answered a millisecond before its\n // deadline must not be reported as a timeout.\n work.then(resolve, reject);\n\n // The host's live \"a person is being asked\" signal. Read defensively: a host that\n // never pushes the channel leaves this at \"not awaiting\", which is exactly the\n // pre-R3-307 behaviour for a call on its idle bound.\n let attention: HostAttention | undefined;\n let noticeFired = false;\n const fireNotice = () => {\n try {\n opts?.onPending?.({\n call,\n attendance,\n elapsedMs: Date.now() - started,\n ...(attendanceReason(scheme, method)\n ? { reason: attendanceReason(scheme, method) as string }\n : {}),\n ...(attention?.awaiting\n ? { awaiting: { kind: attention.kind, since: attention.since } }\n : {}),\n });\n } catch {\n /* a caller's render callback must never break the call it describes */\n }\n };\n\n deadline = createSuspendableDeadline({\n bounds,\n onExpire: (bound, elapsedBoundMs) =>\n reject(new ProtocolTimeoutError(call, elapsedBoundMs, attendance, bound)),\n });\n\n if (opts?.onPending) {\n notice = setTimeout(() => {\n noticeFired = true;\n fireNotice();\n }, PENDING_NOTICE_MS);\n }\n\n // Subscribing invokes the listener immediately with the current value, so the\n // deadline starts in the right state even if a prompt was already up.\n //\n // Guarded because this sits on the path of EVERY host call: a fault in the attention\n // channel must degrade to \"no signal\" (the pre-R3-307 behaviour, bounds unsuspended),\n // never take down every request in the SDK.\n try {\n unsubscribeAttention = onHostAttentionChange((next) => {\n const wasAwaiting = attention?.awaiting ?? false;\n attention = next;\n deadline?.setAwaiting(next.awaiting);\n // Re-notify once the notice has fired: \"still waiting\" → \"tap your passkey\" is\n // the whole point, and a caller rendering a waiting state wants the live\n // sentence, not the one that was true three seconds in.\n if (noticeFired && next.awaiting !== wasAwaiting) fireNotice();\n });\n } catch {\n /* no attention signal available — the bounds simply never suspend */\n }\n\n if (signal) {\n onAbort = () => reject(new ProtocolCancelledError(call));\n signal.addEventListener('abort', onAbort, { once: true });\n }\n });\n } finally {\n deadline?.dispose();\n if (notice !== undefined) clearTimeout(notice);\n unsubscribeAttention?.();\n if (signal && onAbort) signal.removeEventListener('abort', onAbort);\n // The host may still answer after we stopped waiting; swallow it so a late rejection\n // does not surface as an unhandled promise rejection in the app's console.\n work.catch(() => undefined);\n }\n}\n"],"mappings":";AAKA,SAAS,iBAAiB;AAC1B,SAAS,6BAAiD;AAE1D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAEP,SAAS,aAAa,mBAAmB;AAqBlC,MAAM,kBAAkB,CAC7B,cACA,QACA,QACA,SAEA;AAAA,EACE;AAAA,EACA;AAAA,EACA,MAAM,UAAU,EAAE,gBAAgB,cAAc,QAAQ,MAAM;AAAA,EAC9D;AACF;AAaF,eAAsB,aACpB,QACA,QACA,OACA,MACY;AACZ,QAAM,OAAO,GAAG,MAAM,IAAI,MAAM;AAChC,QAAM,aAAa,aAAa,QAAQ,MAAM;AAC9C,QAAM,SACJ,MAAM,cAAc,SAChB,EAAE,QAAQ,KAAK,WAAW,WAAW,KAAK,UAAU,IACpD,UAAU,QAAQ,MAAM;AAC9B,QAAM,SAAS,MAAM;AAErB,MAAI,QAAQ,QAAS,OAAM,IAAI,uBAAuB,IAAI;AAE1D,QAAM,OAAO,MAAM;AAGnB,MAAI,CAAC,OAAO,SAAS,OAAO,SAAS,KAAK,CAAC,UAAU,CAAC,MAAM,UAAW,QAAO;AAE9E,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,QAAM,UAAU,KAAK,IAAI;AAEzB,MAAI;AACF,WAAO,MAAM,IAAI,QAAW,CAAC,SAAS,WAAW;AAG/C,WAAK,KAAK,SAAS,MAAM;AAKzB,UAAI;AACJ,UAAI,cAAc;AAClB,YAAM,aAAa,MAAM;AACvB,YAAI;AACF,gBAAM,YAAY;AAAA,YAChB;AAAA,YACA;AAAA,YACA,WAAW,KAAK,IAAI,IAAI;AAAA,YACxB,GAAI,iBAAiB,QAAQ,MAAM,IAC/B,EAAE,QAAQ,iBAAiB,QAAQ,MAAM,EAAY,IACrD,CAAC;AAAA,YACL,GAAI,WAAW,WACX,EAAE,UAAU,EAAE,MAAM,UAAU,MAAM,OAAO,UAAU,MAAM,EAAE,IAC7D,CAAC;AAAA,UACP,CAAC;AAAA,QACH,QAAQ;AAAA,QAER;AAAA,MACF;AAEA,iBAAW,0BAA0B;AAAA,QACnC;AAAA,QACA,UAAU,CAAC,OAAO,mBAChB,OAAO,IAAI,qBAAqB,MAAM,gBAAgB,YAAY,KAAK,CAAC;AAAA,MAC5E,CAAC;AAED,UAAI,MAAM,WAAW;AACnB,iBAAS,WAAW,MAAM;AACxB,wBAAc;AACd,qBAAW;AAAA,QACb,GAAG,iBAAiB;AAAA,MACtB;AAQA,UAAI;AACF,+BAAuB,sBAAsB,CAAC,SAAS;AACrD,gBAAM,cAAc,WAAW,YAAY;AAC3C,sBAAY;AACZ,oBAAU,YAAY,KAAK,QAAQ;AAInC,cAAI,eAAe,KAAK,aAAa,YAAa,YAAW;AAAA,QAC/D,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAEA,UAAI,QAAQ;AACV,kBAAU,MAAM,OAAO,IAAI,uBAAuB,IAAI,CAAC;AACvD,eAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAAA,MAC1D;AAAA,IACF,CAAC;AAAA,EACH,UAAE;AACA,cAAU,QAAQ;AAClB,QAAI,WAAW,OAAW,cAAa,MAAM;AAC7C,2BAAuB;AACvB,QAAI,UAAU,QAAS,QAAO,oBAAoB,SAAS,OAAO;AAGlE,SAAK,MAAM,MAAM,MAAS;AAAA,EAC5B;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/sandboxUtils.ts"],"sourcesContent":["// Bounded host calls (R3-298) + the host-attention-aware deadline (R3-307).\n//\n// The transport primitives this builds on moved to `hostTransport.ts` — see that file for\n// why. `sendMessage` and `addListener` are re-exported here unchanged, so every existing\n// import site (and this module's api-snapshot entry) is exactly as it was.\nimport { transport } from './hostTransport';\nimport { onHostAttentionChange, type HostAttention } from './hostAttention';\n\nimport {\n attendanceOf,\n attendanceReason,\n boundsFor,\n createSuspendableDeadline,\n PENDING_NOTICE_MS,\n ProtocolCancelledError,\n ProtocolTimeoutError,\n type BoundedCallOptions,\n type CallBounds,\n} from './protocolDeadline';\n\nexport { sendMessage, addListener } from './hostTransport';\n\n/**\n * One host protocol request, BOUNDED (R3-298).\n *\n * The bound comes from the call's classification (`protocolDeadline.ts`): an unattended\n * channel round-trip gets tens of seconds, a call that may draw host chrome and wait for a\n * person gets minutes. Nothing is unbounded — an unbounded wait is the failure this fixes.\n *\n * Since R3-307 a call whose prompts the host actually announces runs on the SHORT bound and\n * is suspended only while the host says a person is being asked, so the common\n * grant-already-held path reports a fault in seconds instead of minutes. The absolute\n * ceiling still applies: the signal may extend a deadline, never remove it.\n *\n * The host's own work is NOT cancelled by `signal` or by the deadline. The one-shot\n * transport allocates its `msgId` internally, so the SDK has no handle to send the host a\n * cancel for (streams do, and `consumeStream` uses it). What the caller gets back is\n * control: it stops waiting and gets a typed error instead of hanging. A host prompt that\n * is already on screen stays there until the user dismisses it, which is correct — the SDK\n * must not be able to tear down host chrome the user is looking at.\n */\nexport const protocolRequest = (\n protocolName: string,\n method: string,\n params: Array<any>,\n opts?: BoundedCallOptions,\n): Promise<any> =>\n withDeadline(protocolName, method, () => transport().protocolRequest(protocolName, method, params), opts);\n\n/**\n * Race a host call against its deadline, a caller abort, and a pending notice.\n *\n * Kept separate from the transport so it is unit-testable against a promise that simply\n * never settles — which is the whole scenario, and one no live transport reproduces on\n * demand.\n *\n * An explicit `opts.timeoutMs` is the WHOLE bound and is never suspended: a caller that\n * names a number owns the wait, and silently stretching it past what they asked for would\n * be the same class of surprise this machinery exists to remove.\n */\nexport async function withDeadline<T>(\n scheme: string,\n method: string,\n start: () => Promise<T>,\n opts?: BoundedCallOptions,\n): Promise<T> {\n const call = `${scheme}:${method}`;\n const attendance = attendanceOf(scheme, method);\n const bounds: CallBounds =\n opts?.timeoutMs !== undefined ? { idleMs: opts.timeoutMs, ceilingMs: opts.timeoutMs } : boundsFor(scheme, method);\n const signal = opts?.signal;\n\n if (signal?.aborted) throw new ProtocolCancelledError(call);\n\n const work = start();\n // No bound and no abort wanted: hand back the untouched promise rather than wrapping it\n // in timers that would never fire.\n if (!Number.isFinite(bounds.ceilingMs) && !signal && !opts?.onPending) return work;\n\n let deadline: { setAwaiting: (a: boolean) => void; dispose: () => void } | undefined;\n let notice: ReturnType<typeof setTimeout> | undefined;\n let unsubscribeAttention: (() => void) | undefined;\n let onAbort: (() => void) | undefined;\n const started = Date.now();\n\n try {\n return await new Promise<T>((resolve, reject) => {\n // `work` settling always wins — a call that answered a millisecond before its\n // deadline must not be reported as a timeout.\n work.then(resolve, reject);\n\n // The host's live \"a person is being asked\" signal. Read defensively: a host that\n // never pushes the channel leaves this at \"not awaiting\", which is exactly the\n // pre-R3-307 behaviour for a call on its idle bound.\n let attention: HostAttention | undefined;\n let noticeFired = false;\n const fireNotice = () => {\n try {\n opts?.onPending?.({\n call,\n attendance,\n elapsedMs: Date.now() - started,\n ...(attendanceReason(scheme, method) ? { reason: attendanceReason(scheme, method) as string } : {}),\n ...(attention?.awaiting ? { awaiting: { kind: attention.kind, since: attention.since } } : {}),\n });\n } catch {\n /* a caller's render callback must never break the call it describes */\n }\n };\n\n deadline = createSuspendableDeadline({\n bounds,\n onExpire: (bound, elapsedBoundMs) => reject(new ProtocolTimeoutError(call, elapsedBoundMs, attendance, bound)),\n });\n\n if (opts?.onPending) {\n notice = setTimeout(() => {\n noticeFired = true;\n fireNotice();\n }, PENDING_NOTICE_MS);\n }\n\n // Subscribing invokes the listener immediately with the current value, so the\n // deadline starts in the right state even if a prompt was already up.\n //\n // Guarded because this sits on the path of EVERY host call: a fault in the attention\n // channel must degrade to \"no signal\" (the pre-R3-307 behaviour, bounds unsuspended),\n // never take down every request in the SDK.\n try {\n unsubscribeAttention = onHostAttentionChange((next) => {\n const wasAwaiting = attention?.awaiting ?? false;\n attention = next;\n deadline?.setAwaiting(next.awaiting);\n // Re-notify once the notice has fired: \"still waiting\" → \"tap your passkey\" is\n // the whole point, and a caller rendering a waiting state wants the live\n // sentence, not the one that was true three seconds in.\n if (noticeFired && next.awaiting !== wasAwaiting) fireNotice();\n });\n } catch {\n /* no attention signal available — the bounds simply never suspend */\n }\n\n if (signal) {\n onAbort = () => reject(new ProtocolCancelledError(call));\n signal.addEventListener('abort', onAbort, { once: true });\n }\n });\n } finally {\n deadline?.dispose();\n if (notice !== undefined) clearTimeout(notice);\n unsubscribeAttention?.();\n if (signal && onAbort) signal.removeEventListener('abort', onAbort);\n // The host may still answer after we stopped waiting; swallow it so a late rejection\n // does not surface as an unhandled promise rejection in the app's console.\n work.catch(() => undefined);\n }\n}\n"],"mappings":";AAKA,SAAS,iBAAiB;AAC1B,SAAS,6BAAiD;AAE1D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAEP,SAAS,aAAa,mBAAmB;AAqBlC,MAAM,kBAAkB,CAC7B,cACA,QACA,QACA,SAEA,aAAa,cAAc,QAAQ,MAAM,UAAU,EAAE,gBAAgB,cAAc,QAAQ,MAAM,GAAG,IAAI;AAa1G,eAAsB,aACpB,QACA,QACA,OACA,MACY;AACZ,QAAM,OAAO,GAAG,MAAM,IAAI,MAAM;AAChC,QAAM,aAAa,aAAa,QAAQ,MAAM;AAC9C,QAAM,SACJ,MAAM,cAAc,SAAY,EAAE,QAAQ,KAAK,WAAW,WAAW,KAAK,UAAU,IAAI,UAAU,QAAQ,MAAM;AAClH,QAAM,SAAS,MAAM;AAErB,MAAI,QAAQ,QAAS,OAAM,IAAI,uBAAuB,IAAI;AAE1D,QAAM,OAAO,MAAM;AAGnB,MAAI,CAAC,OAAO,SAAS,OAAO,SAAS,KAAK,CAAC,UAAU,CAAC,MAAM,UAAW,QAAO;AAE9E,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,QAAM,UAAU,KAAK,IAAI;AAEzB,MAAI;AACF,WAAO,MAAM,IAAI,QAAW,CAAC,SAAS,WAAW;AAG/C,WAAK,KAAK,SAAS,MAAM;AAKzB,UAAI;AACJ,UAAI,cAAc;AAClB,YAAM,aAAa,MAAM;AACvB,YAAI;AACF,gBAAM,YAAY;AAAA,YAChB;AAAA,YACA;AAAA,YACA,WAAW,KAAK,IAAI,IAAI;AAAA,YACxB,GAAI,iBAAiB,QAAQ,MAAM,IAAI,EAAE,QAAQ,iBAAiB,QAAQ,MAAM,EAAY,IAAI,CAAC;AAAA,YACjG,GAAI,WAAW,WAAW,EAAE,UAAU,EAAE,MAAM,UAAU,MAAM,OAAO,UAAU,MAAM,EAAE,IAAI,CAAC;AAAA,UAC9F,CAAC;AAAA,QACH,QAAQ;AAAA,QAER;AAAA,MACF;AAEA,iBAAW,0BAA0B;AAAA,QACnC;AAAA,QACA,UAAU,CAAC,OAAO,mBAAmB,OAAO,IAAI,qBAAqB,MAAM,gBAAgB,YAAY,KAAK,CAAC;AAAA,MAC/G,CAAC;AAED,UAAI,MAAM,WAAW;AACnB,iBAAS,WAAW,MAAM;AACxB,wBAAc;AACd,qBAAW;AAAA,QACb,GAAG,iBAAiB;AAAA,MACtB;AAQA,UAAI;AACF,+BAAuB,sBAAsB,CAAC,SAAS;AACrD,gBAAM,cAAc,WAAW,YAAY;AAC3C,sBAAY;AACZ,oBAAU,YAAY,KAAK,QAAQ;AAInC,cAAI,eAAe,KAAK,aAAa,YAAa,YAAW;AAAA,QAC/D,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAEA,UAAI,QAAQ;AACV,kBAAU,MAAM,OAAO,IAAI,uBAAuB,IAAI,CAAC;AACvD,eAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAAA,MAC1D;AAAA,IACF,CAAC;AAAA,EACH,UAAE;AACA,cAAU,QAAQ;AAClB,QAAI,WAAW,OAAW,cAAa,MAAM;AAC7C,2BAAuB;AACvB,QAAI,UAAU,QAAS,QAAO,oBAAoB,SAAS,OAAO;AAGlE,SAAK,MAAM,MAAM,MAAS;AAAA,EAC5B;AACF;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/scrollToId.ts"],"sourcesContent":["/**\n * Scroll the element addressed by a fragment id into view (deep-linking Capability\n * C, MARKDOWN_SYNTAX_SPEC §13.5). Tries the element's own `id` first — a heading's\n * `sec-…`/text-slug id (§15) — then the `[data-slug]` fallback, so a citation that\n * used the prose text slug still lands even when the heading's own id is the\n * prose-independent `sec-…` id. Returns whether a target was found; the caller\n * decides whether to fall back to top-of-page. **Never throws** — a missing\n * fragment is a soft failure, not an error.\n */\nexport const scrollToId = (id: string): boolean => {\n if (!id || typeof document === 'undefined') return false;\n let el: Element | null = null;\n try {\n el = document.getElementById(id);\n if (!el) {\n // A citation that targeted the prose text slug still resolves via the\n // `data-slug` hook the kernel emits alongside the `sec-…` id.\n const escaped =\n typeof CSS !== 'undefined' && typeof CSS.escape === 'function'\n ? CSS.escape(id)\n : id.replace(/[\"\\\\]/g, '\\\\$&');\n el = document.querySelector(`[data-slug=\"${escaped}\"]`);\n }\n } catch {\n return false;\n }\n if (el && typeof (el as HTMLElement).scrollIntoView === 'function') {\n (el as HTMLElement).scrollIntoView();\n return true;\n }\n return false;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AASO,MAAM,aAAa,CAAC,OAAwB;AACjD,MAAI,CAAC,MAAM,OAAO,aAAa,YAAa,QAAO;AACnD,MAAI,KAAqB;AACzB,MAAI;AACF,SAAK,SAAS,eAAe,EAAE;AAC/B,QAAI,CAAC,IAAI;AAGP,YAAM,UACJ,OAAO,QAAQ,eAAe,OAAO,IAAI,WAAW,aAChD,IAAI,OAAO,EAAE,IACb,GAAG,QAAQ,UAAU,MAAM;AACjC,WAAK,SAAS,cAAc,eAAe,OAAO,IAAI;AAAA,IACxD;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,MAAM,OAAQ,GAAmB,mBAAmB,YAAY;AAClE,IAAC,GAAmB,eAAe;AACnC,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/scrollToId.ts"],"sourcesContent":["/**\n * Scroll the element addressed by a fragment id into view (deep-linking Capability\n * C, MARKDOWN_SYNTAX_SPEC §13.5). Tries the element's own `id` first — a heading's\n * `sec-…`/text-slug id (§15) — then the `[data-slug]` fallback, so a citation that\n * used the prose text slug still lands even when the heading's own id is the\n * prose-independent `sec-…` id. Returns whether a target was found; the caller\n * decides whether to fall back to top-of-page. **Never throws** — a missing\n * fragment is a soft failure, not an error.\n */\nexport const scrollToId = (id: string): boolean => {\n if (!id || typeof document === 'undefined') return false;\n let el: Element | null = null;\n try {\n el = document.getElementById(id);\n if (!el) {\n // A citation that targeted the prose text slug still resolves via the\n // `data-slug` hook the kernel emits alongside the `sec-…` id.\n const escaped =\n typeof CSS !== 'undefined' && typeof CSS.escape === 'function' ? CSS.escape(id) : id.replace(/[\"\\\\]/g, '\\\\$&');\n el = document.querySelector(`[data-slug=\"${escaped}\"]`);\n }\n } catch {\n return false;\n }\n if (el && typeof (el as HTMLElement).scrollIntoView === 'function') {\n (el as HTMLElement).scrollIntoView();\n return true;\n }\n return false;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AASO,MAAM,aAAa,CAAC,OAAwB;AACjD,MAAI,CAAC,MAAM,OAAO,aAAa,YAAa,QAAO;AACnD,MAAI,KAAqB;AACzB,MAAI;AACF,SAAK,SAAS,eAAe,EAAE;AAC/B,QAAI,CAAC,IAAI;AAGP,YAAM,UACJ,OAAO,QAAQ,eAAe,OAAO,IAAI,WAAW,aAAa,IAAI,OAAO,EAAE,IAAI,GAAG,QAAQ,UAAU,MAAM;AAC/G,WAAK,SAAS,cAAc,eAAe,OAAO,IAAI;AAAA,IACxD;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,MAAM,OAAQ,GAAmB,mBAAmB,YAAY;AAClE,IAAC,GAAmB,eAAe;AACnC,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/scrollToId.ts"],"sourcesContent":["/**\n * Scroll the element addressed by a fragment id into view (deep-linking Capability\n * C, MARKDOWN_SYNTAX_SPEC §13.5). Tries the element's own `id` first — a heading's\n * `sec-…`/text-slug id (§15) — then the `[data-slug]` fallback, so a citation that\n * used the prose text slug still lands even when the heading's own id is the\n * prose-independent `sec-…` id. Returns whether a target was found; the caller\n * decides whether to fall back to top-of-page. **Never throws** — a missing\n * fragment is a soft failure, not an error.\n */\nexport const scrollToId = (id: string): boolean => {\n if (!id || typeof document === 'undefined') return false;\n let el: Element | null = null;\n try {\n el = document.getElementById(id);\n if (!el) {\n // A citation that targeted the prose text slug still resolves via the\n // `data-slug` hook the kernel emits alongside the `sec-…` id.\n const escaped =\n typeof CSS !== 'undefined' && typeof CSS.escape === 'function'\n ? CSS.escape(id)\n : id.replace(/[\"\\\\]/g, '\\\\$&');\n el = document.querySelector(`[data-slug=\"${escaped}\"]`);\n }\n } catch {\n return false;\n }\n if (el && typeof (el as HTMLElement).scrollIntoView === 'function') {\n (el as HTMLElement).scrollIntoView();\n return true;\n }\n return false;\n};\n"],"mappings":";AASO,MAAM,aAAa,CAAC,OAAwB;AACjD,MAAI,CAAC,MAAM,OAAO,aAAa,YAAa,QAAO;AACnD,MAAI,KAAqB;AACzB,MAAI;AACF,SAAK,SAAS,eAAe,EAAE;AAC/B,QAAI,CAAC,IAAI;AAGP,YAAM,UACJ,OAAO,QAAQ,eAAe,OAAO,IAAI,WAAW,aAChD,IAAI,OAAO,EAAE,IACb,GAAG,QAAQ,UAAU,MAAM;AACjC,WAAK,SAAS,cAAc,eAAe,OAAO,IAAI;AAAA,IACxD;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,MAAM,OAAQ,GAAmB,mBAAmB,YAAY;AAClE,IAAC,GAAmB,eAAe;AACnC,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/scrollToId.ts"],"sourcesContent":["/**\n * Scroll the element addressed by a fragment id into view (deep-linking Capability\n * C, MARKDOWN_SYNTAX_SPEC §13.5). Tries the element's own `id` first — a heading's\n * `sec-…`/text-slug id (§15) — then the `[data-slug]` fallback, so a citation that\n * used the prose text slug still lands even when the heading's own id is the\n * prose-independent `sec-…` id. Returns whether a target was found; the caller\n * decides whether to fall back to top-of-page. **Never throws** — a missing\n * fragment is a soft failure, not an error.\n */\nexport const scrollToId = (id: string): boolean => {\n if (!id || typeof document === 'undefined') return false;\n let el: Element | null = null;\n try {\n el = document.getElementById(id);\n if (!el) {\n // A citation that targeted the prose text slug still resolves via the\n // `data-slug` hook the kernel emits alongside the `sec-…` id.\n const escaped =\n typeof CSS !== 'undefined' && typeof CSS.escape === 'function' ? CSS.escape(id) : id.replace(/[\"\\\\]/g, '\\\\$&');\n el = document.querySelector(`[data-slug=\"${escaped}\"]`);\n }\n } catch {\n return false;\n }\n if (el && typeof (el as HTMLElement).scrollIntoView === 'function') {\n (el as HTMLElement).scrollIntoView();\n return true;\n }\n return false;\n};\n"],"mappings":";AASO,MAAM,aAAa,CAAC,OAAwB;AACjD,MAAI,CAAC,MAAM,OAAO,aAAa,YAAa,QAAO;AACnD,MAAI,KAAqB;AACzB,MAAI;AACF,SAAK,SAAS,eAAe,EAAE;AAC/B,QAAI,CAAC,IAAI;AAGP,YAAM,UACJ,OAAO,QAAQ,eAAe,OAAO,IAAI,WAAW,aAAa,IAAI,OAAO,EAAE,IAAI,GAAG,QAAQ,UAAU,MAAM;AAC/G,WAAK,SAAS,cAAc,eAAe,OAAO,IAAI;AAAA,IACxD;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,MAAM,OAAQ,GAAmB,mBAAmB,YAAY;AAClE,IAAC,GAAmB,eAAe;AACnC,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}