@immediately-run/sdk 0.43.1 → 0.45.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 (157) hide show
  1. package/dist/ambient.d.ts +47 -0
  2. package/dist/auth.cjs +3 -2
  3. package/dist/auth.cjs.map +1 -1
  4. package/dist/auth.js +3 -2
  5. package/dist/auth.js.map +1 -1
  6. package/dist/boot.cjs +3 -2
  7. package/dist/boot.cjs.map +1 -1
  8. package/dist/boot.js +3 -2
  9. package/dist/boot.js.map +1 -1
  10. package/dist/catalog.cjs +3 -2
  11. package/dist/catalog.cjs.map +1 -1
  12. package/dist/catalog.js +3 -2
  13. package/dist/catalog.js.map +1 -1
  14. package/dist/components/MDXComponents.cjs +14 -1
  15. package/dist/components/MDXComponents.cjs.map +1 -1
  16. package/dist/components/MDXComponents.js +14 -1
  17. package/dist/components/MDXComponents.js.map +1 -1
  18. package/dist/components/WikiLink.cjs +19 -17
  19. package/dist/components/WikiLink.cjs.map +1 -1
  20. package/dist/components/WikiLink.js +19 -17
  21. package/dist/components/WikiLink.js.map +1 -1
  22. package/dist/contribute.cjs +2 -1
  23. package/dist/contribute.cjs.map +1 -1
  24. package/dist/contribute.js +2 -1
  25. package/dist/contribute.js.map +1 -1
  26. package/dist/debug.cjs +6 -5
  27. package/dist/debug.cjs.map +1 -1
  28. package/dist/debug.js +12 -5
  29. package/dist/debug.js.map +1 -1
  30. package/dist/diagnostics.cjs +3 -2
  31. package/dist/diagnostics.cjs.map +1 -1
  32. package/dist/diagnostics.js +3 -2
  33. package/dist/diagnostics.js.map +1 -1
  34. package/dist/dnd.cjs +5 -3
  35. package/dist/dnd.cjs.map +1 -1
  36. package/dist/dnd.js +5 -3
  37. package/dist/dnd.js.map +1 -1
  38. package/dist/editor.cjs +3 -1
  39. package/dist/editor.cjs.map +1 -1
  40. package/dist/editor.js +3 -1
  41. package/dist/editor.js.map +1 -1
  42. package/dist/editorContext.cjs +3 -2
  43. package/dist/editorContext.cjs.map +1 -1
  44. package/dist/editorContext.js +3 -2
  45. package/dist/editorContext.js.map +1 -1
  46. package/dist/formFactor.cjs +3 -2
  47. package/dist/formFactor.cjs.map +1 -1
  48. package/dist/formFactor.js +3 -2
  49. package/dist/formFactor.js.map +1 -1
  50. package/dist/generated/protocol.cjs +23 -0
  51. package/dist/generated/protocol.cjs.map +1 -0
  52. package/dist/generated/protocol.d.cts +1 -0
  53. package/dist/generated/protocol.d.ts +1 -0
  54. package/dist/generated/protocol.js +2 -0
  55. package/dist/generated/protocol.js.map +1 -0
  56. package/dist/hooks.cjs +22 -14
  57. package/dist/hooks.cjs.map +1 -1
  58. package/dist/hooks.d.cts +26 -4
  59. package/dist/hooks.d.ts +26 -4
  60. package/dist/hooks.js +23 -15
  61. package/dist/hooks.js.map +1 -1
  62. package/dist/index.cjs +4 -0
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +3 -1
  65. package/dist/index.d.ts +3 -1
  66. package/dist/index.js +2 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/ipc.cjs +5 -3
  69. package/dist/ipc.cjs.map +1 -1
  70. package/dist/ipc.js +5 -3
  71. package/dist/ipc.js.map +1 -1
  72. package/dist/launch.cjs +5 -3
  73. package/dist/launch.cjs.map +1 -1
  74. package/dist/launch.js +5 -3
  75. package/dist/launch.js.map +1 -1
  76. package/dist/linkSpace.cjs +63 -0
  77. package/dist/linkSpace.cjs.map +1 -0
  78. package/dist/linkSpace.d.cts +44 -0
  79. package/dist/linkSpace.d.ts +44 -0
  80. package/dist/linkSpace.js +37 -0
  81. package/dist/linkSpace.js.map +1 -0
  82. package/dist/llm.cjs +3 -2
  83. package/dist/llm.cjs.map +1 -1
  84. package/dist/llm.js +3 -2
  85. package/dist/llm.js.map +1 -1
  86. package/dist/metadataSource.cjs +53 -0
  87. package/dist/metadataSource.cjs.map +1 -0
  88. package/dist/metadataSource.d.cts +51 -0
  89. package/dist/metadataSource.d.ts +51 -0
  90. package/dist/metadataSource.js +29 -0
  91. package/dist/metadataSource.js.map +1 -0
  92. package/dist/moduleCache.cjs +2 -1
  93. package/dist/moduleCache.cjs.map +1 -1
  94. package/dist/moduleCache.js +2 -1
  95. package/dist/moduleCache.js.map +1 -1
  96. package/dist/mounts.cjs +13 -10
  97. package/dist/mounts.cjs.map +1 -1
  98. package/dist/mounts.js +23 -10
  99. package/dist/mounts.js.map +1 -1
  100. package/dist/netFetch.cjs +4 -2
  101. package/dist/netFetch.cjs.map +1 -1
  102. package/dist/netFetch.js +4 -2
  103. package/dist/netFetch.js.map +1 -1
  104. package/dist/onFsChange.cjs +2 -1
  105. package/dist/onFsChange.cjs.map +1 -1
  106. package/dist/onFsChange.js +2 -1
  107. package/dist/onFsChange.js.map +1 -1
  108. package/dist/protocolSchemes.cjs +46 -0
  109. package/dist/protocolSchemes.cjs.map +1 -0
  110. package/dist/protocolSchemes.d.cts +18 -0
  111. package/dist/protocolSchemes.d.ts +18 -0
  112. package/dist/protocolSchemes.js +37 -0
  113. package/dist/protocolSchemes.js.map +1 -0
  114. package/dist/routing.cjs +2 -1
  115. package/dist/routing.cjs.map +1 -1
  116. package/dist/routing.js +2 -1
  117. package/dist/routing.js.map +1 -1
  118. package/dist/runtime.cjs +4 -2
  119. package/dist/runtime.cjs.map +1 -1
  120. package/dist/runtime.js +4 -2
  121. package/dist/runtime.js.map +1 -1
  122. package/dist/safeContent/renderMdast.cjs +2 -0
  123. package/dist/safeContent/renderMdast.cjs.map +1 -1
  124. package/dist/safeContent/renderMdast.js +2 -0
  125. package/dist/safeContent/renderMdast.js.map +1 -1
  126. package/dist/sandboxTypes.cjs.map +1 -1
  127. package/dist/sandboxTypes.d.cts +40 -6
  128. package/dist/sandboxTypes.d.ts +40 -6
  129. package/dist/secrets.cjs +5 -3
  130. package/dist/secrets.cjs.map +1 -1
  131. package/dist/secrets.js +5 -3
  132. package/dist/secrets.js.map +1 -1
  133. package/dist/tasks.cjs +6 -4
  134. package/dist/tasks.cjs.map +1 -1
  135. package/dist/tasks.js +6 -4
  136. package/dist/tasks.js.map +1 -1
  137. package/dist/theme.cjs +5 -3
  138. package/dist/theme.cjs.map +1 -1
  139. package/dist/theme.js +5 -3
  140. package/dist/theme.js.map +1 -1
  141. package/dist/urlUtils.cjs +3 -4
  142. package/dist/urlUtils.cjs.map +1 -1
  143. package/dist/urlUtils.d.cts +3 -10
  144. package/dist/urlUtils.d.ts +3 -10
  145. package/dist/urlUtils.js +1 -2
  146. package/dist/urlUtils.js.map +1 -1
  147. package/dist/vcs.cjs +5 -3
  148. package/dist/vcs.cjs.map +1 -1
  149. package/dist/vcs.js +5 -3
  150. package/dist/vcs.js.map +1 -1
  151. package/dist/version.cjs +1 -1
  152. package/dist/version.cjs.map +1 -1
  153. package/dist/version.d.cts +1 -1
  154. package/dist/version.d.ts +1 -1
  155. package/dist/version.js +1 -1
  156. package/dist/version.js.map +1 -1
  157. package/package.json +11 -4
@@ -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 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 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(\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":[]}
@@ -2,6 +2,7 @@ import "../chunk-VHAA22YE.js";
2
2
  import { createElement, Fragment } from "react";
3
3
  import { sanitizeUrl } from "./sanitizeUrl";
4
4
  import { splitWikiLinks } from "./wikilink";
5
+ import { resolveLinkTarget } from "../linkSpace";
5
6
  function literalProps(attributes) {
6
7
  const props = {};
7
8
  for (const attr of attributes ?? []) {
@@ -19,6 +20,7 @@ function elementFor(tag, components) {
19
20
  }
20
21
  function renderWiki(token, opts) {
21
22
  const label = token.label ?? token.target;
23
+ if (resolveLinkTarget(token.target).state === "invalid") return label;
22
24
  const href = opts.resolveWikiLink?.(token.target);
23
25
  const safe = href !== void 0 ? sanitizeUrl(href) : void 0;
24
26
  if (safe === void 0) return label;
@@ -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 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 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(\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 +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 */\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/** The paths a {@link MetadataQueryFunction} selected. */\nexport type FileQueryResult = string[]\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 */\nexport type MetadataQueryFunction<T = Metadata> = (filesMetadata: FilesMetadata<T>) => FileQueryResult;\n\n/** One match from {@link MetadataQueryFunction}: the file path paired with its frontmatter. */\nexport type MetadataQueryEntry<T = Metadata> = { path: string; meta: T };\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> = MetadataQueryEntry<T>[] | { 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 = {}> =\n | MetadataQueryEntry<T, E>[]\n | { error: unknown }\n"],"mappings":";;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
@@ -19,20 +19,54 @@ type EvaluationContext = {
19
19
  * The parsed frontmatter of a single file. Apps can supply their own shape as the
20
20
  * `T` type parameter on the metadata hooks for typed field access; it defaults to
21
21
  * an open record.
22
+ *
23
+ * The ENVELOPE this widens is `Frontmatter` from
24
+ * `@immediately-run/platform-constants` (R3-275): string keys, JSON-serializable
25
+ * values, the emitter's object-identity semantics, and the empty-frontmatter drop —
26
+ * one statement of the contract, shared with the CLI that writes the sidecar and the
27
+ * sandbox that reads it. This alias stays `any`-valued deliberately: app code
28
+ * indexes frontmatter fields directly (`meta.title.length`), and tightening it to
29
+ * `JsonValue` would turn every such access into a type error for a guarantee the
30
+ * *values* never made (they are open by the spec's §6 decision).
22
31
  */
23
32
  type Metadata = Record<string, any>;
24
33
  /** The whole metadata store: a map from repo-relative file path to its frontmatter. */
25
34
  type FilesMetadata<T = Metadata> = Record<string, T>;
26
- /** The paths a {@link MetadataQueryFunction} selected. */
27
- type FileQueryResult = string[];
35
+ /**
36
+ * A record a query may return INSTEAD of a bare path (R3-276): the path plus
37
+ * whatever the query computed on the way to selecting it.
38
+ *
39
+ * The motivating case is a query that derives something per match — a sort key, a
40
+ * section, a formatted date — and would otherwise have to recompute it downstream
41
+ * from `meta`, or smuggle it through a closure. Returning it here keeps the
42
+ * derivation next to the selection that needed it.
43
+ */
44
+ type MetadataQueryRecord = {
45
+ path: string;
46
+ } & Record<string, unknown>;
47
+ /** What a {@link MetadataQueryFunction} selected: paths, or {@link MetadataQueryRecord}s.
48
+ * The two forms may not be mixed in one result — a query returns one or the other. */
49
+ type FileQueryResult = string[] | MetadataQueryRecord[];
28
50
  /**
29
51
  * A query over the metadata store: receive every file's frontmatter keyed by path
30
52
  * and return the paths that match. Plain JS — `Object.entries(...).filter(...)` —
31
53
  * no query language to learn.
54
+ *
55
+ * Return bare paths, or records carrying extra fields alongside `path` (R3-276);
56
+ * either way the hook resolves each path to its frontmatter.
32
57
  */
33
58
  type MetadataQueryFunction<T = Metadata> = (filesMetadata: FilesMetadata<T>) => FileQueryResult;
34
- /** One match from {@link MetadataQueryFunction}: the file path paired with its frontmatter. */
35
- type MetadataQueryEntry<T = Metadata> = {
59
+ /**
60
+ * One match from {@link MetadataQueryFunction}: the file path paired with its
61
+ * frontmatter, plus any extra fields the query returned as a
62
+ * {@link MetadataQueryRecord}.
63
+ *
64
+ * `E` defaults to `{}`, so every existing `MetadataQueryEntry<T>` keeps meaning
65
+ * exactly what it meant — the extra-fields form is opt-in at the type level.
66
+ * `path` and `meta` are applied AFTER the record's own fields, so a query cannot
67
+ * shadow them with something else.
68
+ */
69
+ type MetadataQueryEntry<T = Metadata, E extends object = {}> = E & {
36
70
  path: string;
37
71
  meta: T;
38
72
  };
@@ -40,8 +74,8 @@ type MetadataQueryEntry<T = Metadata> = {
40
74
  * The result of running a metadata query: the matched entries (path + frontmatter),
41
75
  * or the error a throwing query produced.
42
76
  */
43
- type MetadataQueryResult<T = Metadata> = MetadataQueryEntry<T>[] | {
77
+ type MetadataQueryResult<T = Metadata, E extends object = {}> = MetadataQueryEntry<T, E>[] | {
44
78
  error: unknown;
45
79
  };
46
80
 
47
- export type { EvaluationContext, FileQueryResult, FilesMetadata, Metadata, MetadataQueryEntry, MetadataQueryFunction, MetadataQueryResult, ModuleExports };
81
+ export type { EvaluationContext, FileQueryResult, FilesMetadata, Metadata, MetadataQueryEntry, MetadataQueryFunction, MetadataQueryRecord, MetadataQueryResult, ModuleExports };
@@ -19,20 +19,54 @@ type EvaluationContext = {
19
19
  * The parsed frontmatter of a single file. Apps can supply their own shape as the
20
20
  * `T` type parameter on the metadata hooks for typed field access; it defaults to
21
21
  * an open record.
22
+ *
23
+ * The ENVELOPE this widens is `Frontmatter` from
24
+ * `@immediately-run/platform-constants` (R3-275): string keys, JSON-serializable
25
+ * values, the emitter's object-identity semantics, and the empty-frontmatter drop —
26
+ * one statement of the contract, shared with the CLI that writes the sidecar and the
27
+ * sandbox that reads it. This alias stays `any`-valued deliberately: app code
28
+ * indexes frontmatter fields directly (`meta.title.length`), and tightening it to
29
+ * `JsonValue` would turn every such access into a type error for a guarantee the
30
+ * *values* never made (they are open by the spec's §6 decision).
22
31
  */
23
32
  type Metadata = Record<string, any>;
24
33
  /** The whole metadata store: a map from repo-relative file path to its frontmatter. */
25
34
  type FilesMetadata<T = Metadata> = Record<string, T>;
26
- /** The paths a {@link MetadataQueryFunction} selected. */
27
- type FileQueryResult = string[];
35
+ /**
36
+ * A record a query may return INSTEAD of a bare path (R3-276): the path plus
37
+ * whatever the query computed on the way to selecting it.
38
+ *
39
+ * The motivating case is a query that derives something per match — a sort key, a
40
+ * section, a formatted date — and would otherwise have to recompute it downstream
41
+ * from `meta`, or smuggle it through a closure. Returning it here keeps the
42
+ * derivation next to the selection that needed it.
43
+ */
44
+ type MetadataQueryRecord = {
45
+ path: string;
46
+ } & Record<string, unknown>;
47
+ /** What a {@link MetadataQueryFunction} selected: paths, or {@link MetadataQueryRecord}s.
48
+ * The two forms may not be mixed in one result — a query returns one or the other. */
49
+ type FileQueryResult = string[] | MetadataQueryRecord[];
28
50
  /**
29
51
  * A query over the metadata store: receive every file's frontmatter keyed by path
30
52
  * and return the paths that match. Plain JS — `Object.entries(...).filter(...)` —
31
53
  * no query language to learn.
54
+ *
55
+ * Return bare paths, or records carrying extra fields alongside `path` (R3-276);
56
+ * either way the hook resolves each path to its frontmatter.
32
57
  */
33
58
  type MetadataQueryFunction<T = Metadata> = (filesMetadata: FilesMetadata<T>) => FileQueryResult;
34
- /** One match from {@link MetadataQueryFunction}: the file path paired with its frontmatter. */
35
- type MetadataQueryEntry<T = Metadata> = {
59
+ /**
60
+ * One match from {@link MetadataQueryFunction}: the file path paired with its
61
+ * frontmatter, plus any extra fields the query returned as a
62
+ * {@link MetadataQueryRecord}.
63
+ *
64
+ * `E` defaults to `{}`, so every existing `MetadataQueryEntry<T>` keeps meaning
65
+ * exactly what it meant — the extra-fields form is opt-in at the type level.
66
+ * `path` and `meta` are applied AFTER the record's own fields, so a query cannot
67
+ * shadow them with something else.
68
+ */
69
+ type MetadataQueryEntry<T = Metadata, E extends object = {}> = E & {
36
70
  path: string;
37
71
  meta: T;
38
72
  };
@@ -40,8 +74,8 @@ type MetadataQueryEntry<T = Metadata> = {
40
74
  * The result of running a metadata query: the matched entries (path + frontmatter),
41
75
  * or the error a throwing query produced.
42
76
  */
43
- type MetadataQueryResult<T = Metadata> = MetadataQueryEntry<T>[] | {
77
+ type MetadataQueryResult<T = Metadata, E extends object = {}> = MetadataQueryEntry<T, E>[] | {
44
78
  error: unknown;
45
79
  };
46
80
 
47
- export type { EvaluationContext, FileQueryResult, FilesMetadata, Metadata, MetadataQueryEntry, MetadataQueryFunction, MetadataQueryResult, ModuleExports };
81
+ export type { EvaluationContext, FileQueryResult, FilesMetadata, Metadata, MetadataQueryEntry, MetadataQueryFunction, MetadataQueryRecord, MetadataQueryResult, ModuleExports };
package/dist/secrets.cjs CHANGED
@@ -28,8 +28,10 @@ __export(secrets_exports, {
28
28
  module.exports = __toCommonJS(secrets_exports);
29
29
  var import_sandboxUtils = require("./sandboxUtils");
30
30
  var import_pushChannel = require("./pushChannel");
31
+ var import_protocol = require("./generated/protocol");
32
+ var import_protocolSchemes = require("./protocolSchemes");
31
33
  const request = async (method, query = {}) => {
32
- const res = await (0, import_sandboxUtils.protocolRequest)("secrets", method, [query]);
34
+ const res = await (0, import_sandboxUtils.protocolRequest)(import_protocolSchemes.SCHEMES[import_protocol.PROTOCOL_SECRETS], method, [query]);
33
35
  if (!res || res.ok !== true) {
34
36
  const err = new Error(res?.message ?? "secret request failed");
35
37
  err.code = res?.code ?? "unknown";
@@ -43,8 +45,8 @@ const revokeSecret = async (id) => {
43
45
  await request("revoke", { id });
44
46
  };
45
47
  const channel = (0, import_pushChannel.createPushChannel)({
46
- pushType: "secrets-metadata",
47
- requestType: "request-secrets-metadata",
48
+ pushType: import_protocol.SECRETS_METADATA,
49
+ requestType: import_protocol.REQUEST_SECRETS_METADATA,
48
50
  initial: [],
49
51
  parse: (msg) => Array.isArray(msg.secrets) ? msg.secrets : void 0
50
52
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/secrets.ts"],"sourcesContent":["// The host-owned secret store — app-facing surface (SECRETS_SPEC §4/§5).\n//\n// An app can ASK the user to store a secret (`requestAddSecret`), be GRANTED the\n// right to USE a specific secret (`requestSecret`, the powerbox flow), LIST\n// secret METADATA (`getSecrets`/`useSecrets`, never values), and REVOKE one\n// (`revokeSecret`). The secret VALUE never crosses this boundary: it is read\n// host-side, once, at the `net:fetch` injection point (SECRETS_SPEC §6). These\n// functions move only hints, metadata, and grant handles.\n//\n// Resolves LLM_AND_AGENTS_SPEC D2 — the host-mediated BYOK key store. Inert until\n// the host implements `protocol-secrets` + the `secrets-metadata` channel\n// (SECRETS_SPEC §3/§6 host work, roadmap P1.E); the contract is shipped here so\n// apps (e.g. the in-browser coding agent, P3-73) can be written against it.\nimport { protocolRequest } from './sandboxUtils';\nimport { createPushChannel } from './pushChannel';\n\n/** The closed secret-type vocabulary (SECRETS_SPEC §2). `api-key` is always\n * origin-bound; `oauth-refresh` is reserved (no substitution in v1). */\nexport type SecretType = 'api-key' | 'bearer-token' | 'oauth-refresh';\n\n/**\n * The metadata-only projection of a stored secret (SECRETS_SPEC §2/§4) — exactly\n * what `secrets:list` and the powerbox return. **There is no `value` field by\n * design**: the plaintext is never part of any record an app receives.\n */\nexport interface SecretView {\n id: string;\n type: SecretType;\n family?: string;\n description: string;\n /** Required for `type:'api-key'` — the one https origin it may be sent to. */\n boundOrigin?: string;\n /** ISO-8601, or null if never used (drives the §8.15 90-day expiry). */\n lastUsedAt: string | null;\n}\n\n/** Hints for the host's \"add secret\" modal (SECRETS_SPEC §4 `secrets:add`). The\n * app supplies only hints; the user types the value into host chrome. */\nexport interface SecretHints {\n type?: SecretType;\n family?: string;\n /** Pre-fill the bound origin (e.g. `https://api.anthropic.com`). */\n suggestedOrigin?: string;\n description?: string;\n}\n\n/** What `requestSecret()` matches against in the powerbox picker (SECRETS_SPEC §5). */\nexport interface SecretQuery {\n type?: SecretType;\n family?: string;\n}\n\n/**\n * The result of a granted `requestSecret()` — a durable `(appKey, secretId)` use\n * grant plus the secret's metadata. **Never the value.** Hold onto nothing but\n * this; the host substitutes the value into matching `net:fetch` requests.\n */\nexport interface SecretGrant {\n /** Opaque handle for the minted `(appKey, secretId)` grant. */\n grantId: string;\n /** Metadata of the bound secret (no value). */\n secret: SecretView;\n}\n\n/** An error from a secret operation, carrying a machine-readable `code`. */\nexport interface SecretError extends Error {\n code: 'auth-required' | 'cancelled' | 'forbidden' | 'not-found' | 'invalid-params' | 'unknown';\n}\n\ntype SecretResult =\n | { ok: true; data: unknown }\n | { ok: false; code: string; message: string };\n\n// Issue a `protocol-secrets` request, unwrapping the host's {ok,data} envelope\n// and throwing a typed SecretError on failure (mirrors mounts.ts `request`).\nconst request = async <T = unknown>(\n method: string,\n query: object = {},\n): Promise<T> => {\n const res = (await protocolRequest('secrets', method, [query])) as SecretResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'secret request failed') as SecretError;\n err.code = (res?.code as SecretError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n/**\n * Ask the user to store a new secret (SECRETS_SPEC §4 `secrets:add`). Opens a\n * **host-drawn** modal (the value is typed into host chrome, never via the app);\n * resolves with the new secret's {@link SecretView} metadata, or rejects with a\n * {@link SecretError} (`cancelled` if the user dismisses the modal). Requires the\n * `secrets:add` capability.\n */\nexport const requestAddSecret = (hints: SecretHints = {}): Promise<SecretView> =>\n request<SecretView>('add', hints);\n\n/**\n * Ask the user to bind one of their stored secrets to this app (SECRETS_SPEC §5,\n * the powerbox flow — modeled on `requestSpace()`). The host draws a picker of\n * **only the user's matching secrets**; the user picks, declines, or creates one.\n * On success the host records a durable `(appKey, secretId)` grant and resolves\n * with a {@link SecretGrant} (handle + metadata, **never the value**).\n *\n * **No existence oracle (T20/T27):** a decline, an ungranted secret, and a\n * nonexistent secret are indistinguishable — all reject with a {@link SecretError}\n * `cancelled`; the app never sees the list it chose from.\n */\nexport const requestSecret = (query: SecretQuery = {}): Promise<SecretGrant> =>\n request<SecretGrant>('request', query);\n\n/**\n * Delete a stored secret and tombstone every dependent per-app use grant\n * (SECRETS_SPEC §4 `secrets:revoke`, §8.15 cascade). Requires `secrets:revoke`.\n */\nexport const revokeSecret = async (id: string): Promise<void> => {\n await request('revoke', { id });\n};\n\n// The metadata-only `secrets-metadata` channel (Recipe A): the host pushes the\n// current secret metadata on change and replays it on register-frame; gated by\n// `secrets:list`. NEVER carries a value (SECRETS_SPEC §4).\nconst channel = createPushChannel<SecretView[]>({\n pushType: 'secrets-metadata',\n requestType: 'request-secrets-metadata',\n initial: [],\n parse: (msg) => (Array.isArray(msg.secrets) ? (msg.secrets as SecretView[]) : undefined),\n});\n\n/** The metadata of the user's stored secrets (never values), `secrets:list`. Poll\n * for a one-off read; use {@link onSecretsChange}/{@link useSecrets} to react. */\nexport const getSecrets = (): SecretView[] => channel.get();\n\n/** Subscribe to secret-metadata changes (added/revoked). Invoked immediately with\n * the current list, then on every change. Returns an unsubscribe fn. */\nexport const onSecretsChange = (listener: (secrets: SecretView[]) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning the user's secret metadata (never values), re-rendering\n * on change. For the Settings app (SECRETS_SPEC §7). */\nexport const useSecrets = (): SecretView[] => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,0BAAgC;AAChC,yBAAkC;AA6DlC,MAAM,UAAU,OACd,QACA,QAAgB,CAAC,MACF;AACf,QAAM,MAAO,UAAM,qCAAgB,WAAW,QAAQ,CAAC,KAAK,CAAC;AAC7D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,uBAAuB;AAC7D,QAAI,OAAQ,KAAK,QAAgC;AACjD,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AASO,MAAM,mBAAmB,CAAC,QAAqB,CAAC,MACrD,QAAoB,OAAO,KAAK;AAa3B,MAAM,gBAAgB,CAAC,QAAqB,CAAC,MAClD,QAAqB,WAAW,KAAK;AAMhC,MAAM,eAAe,OAAO,OAA8B;AAC/D,QAAM,QAAQ,UAAU,EAAE,GAAG,CAAC;AAChC;AAKA,MAAM,cAAU,sCAAgC;AAAA,EAC9C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,CAAC;AAAA,EACV,OAAO,CAAC,QAAS,MAAM,QAAQ,IAAI,OAAO,IAAK,IAAI,UAA2B;AAChF,CAAC;AAIM,MAAM,aAAa,MAAoB,QAAQ,IAAI;AAInD,MAAM,kBAAkB,CAAC,aAC9B,QAAQ,SAAS,QAAQ;AAIpB,MAAM,aAAa,MAAoB,QAAQ,IAAI;","names":[]}
1
+ {"version":3,"sources":["../src/secrets.ts"],"sourcesContent":["// The host-owned secret store — app-facing surface (SECRETS_SPEC §4/§5).\n//\n// An app can ASK the user to store a secret (`requestAddSecret`), be GRANTED the\n// right to USE a specific secret (`requestSecret`, the powerbox flow), LIST\n// secret METADATA (`getSecrets`/`useSecrets`, never values), and REVOKE one\n// (`revokeSecret`). The secret VALUE never crosses this boundary: it is read\n// host-side, once, at the `net:fetch` injection point (SECRETS_SPEC §6). These\n// functions move only hints, metadata, and grant handles.\n//\n// Resolves LLM_AND_AGENTS_SPEC D2 — the host-mediated BYOK key store. Inert until\n// the host implements `protocol-secrets` + the `secrets-metadata` channel\n// (SECRETS_SPEC §3/§6 host work, roadmap P1.E); the contract is shipped here so\n// apps (e.g. the in-browser coding agent, P3-73) can be written against it.\nimport { protocolRequest } from './sandboxUtils';\nimport { createPushChannel } from './pushChannel';\nimport { PROTOCOL_SECRETS, REQUEST_SECRETS_METADATA, SECRETS_METADATA } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/** The closed secret-type vocabulary (SECRETS_SPEC §2). `api-key` is always\n * origin-bound; `oauth-refresh` is reserved (no substitution in v1). */\nexport type SecretType = 'api-key' | 'bearer-token' | 'oauth-refresh';\n\n/**\n * The metadata-only projection of a stored secret (SECRETS_SPEC §2/§4) — exactly\n * what `secrets:list` and the powerbox return. **There is no `value` field by\n * design**: the plaintext is never part of any record an app receives.\n */\nexport interface SecretView {\n id: string;\n type: SecretType;\n family?: string;\n description: string;\n /** Required for `type:'api-key'` — the one https origin it may be sent to. */\n boundOrigin?: string;\n /** ISO-8601, or null if never used (drives the §8.15 90-day expiry). */\n lastUsedAt: string | null;\n}\n\n/** Hints for the host's \"add secret\" modal (SECRETS_SPEC §4 `secrets:add`). The\n * app supplies only hints; the user types the value into host chrome. */\nexport interface SecretHints {\n type?: SecretType;\n family?: string;\n /** Pre-fill the bound origin (e.g. `https://api.anthropic.com`). */\n suggestedOrigin?: string;\n description?: string;\n}\n\n/** What `requestSecret()` matches against in the powerbox picker (SECRETS_SPEC §5). */\nexport interface SecretQuery {\n type?: SecretType;\n family?: string;\n}\n\n/**\n * The result of a granted `requestSecret()` — a durable `(appKey, secretId)` use\n * grant plus the secret's metadata. **Never the value.** Hold onto nothing but\n * this; the host substitutes the value into matching `net:fetch` requests.\n */\nexport interface SecretGrant {\n /** Opaque handle for the minted `(appKey, secretId)` grant. */\n grantId: string;\n /** Metadata of the bound secret (no value). */\n secret: SecretView;\n}\n\n/** An error from a secret operation, carrying a machine-readable `code`. */\nexport interface SecretError extends Error {\n code: 'auth-required' | 'cancelled' | 'forbidden' | 'not-found' | 'invalid-params' | 'unknown';\n}\n\ntype SecretResult =\n | { ok: true; data: unknown }\n | { ok: false; code: string; message: string };\n\n// Issue a `protocol-secrets` request, unwrapping the host's {ok,data} envelope\n// and throwing a typed SecretError on failure (mirrors mounts.ts `request`).\nconst request = async <T = unknown>(\n method: string,\n query: object = {},\n): Promise<T> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_SECRETS], method, [query])) as SecretResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'secret request failed') as SecretError;\n err.code = (res?.code as SecretError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n/**\n * Ask the user to store a new secret (SECRETS_SPEC §4 `secrets:add`). Opens a\n * **host-drawn** modal (the value is typed into host chrome, never via the app);\n * resolves with the new secret's {@link SecretView} metadata, or rejects with a\n * {@link SecretError} (`cancelled` if the user dismisses the modal). Requires the\n * `secrets:add` capability.\n */\nexport const requestAddSecret = (hints: SecretHints = {}): Promise<SecretView> =>\n request<SecretView>('add', hints);\n\n/**\n * Ask the user to bind one of their stored secrets to this app (SECRETS_SPEC §5,\n * the powerbox flow — modeled on `requestSpace()`). The host draws a picker of\n * **only the user's matching secrets**; the user picks, declines, or creates one.\n * On success the host records a durable `(appKey, secretId)` grant and resolves\n * with a {@link SecretGrant} (handle + metadata, **never the value**).\n *\n * **No existence oracle (T20/T27):** a decline, an ungranted secret, and a\n * nonexistent secret are indistinguishable — all reject with a {@link SecretError}\n * `cancelled`; the app never sees the list it chose from.\n */\nexport const requestSecret = (query: SecretQuery = {}): Promise<SecretGrant> =>\n request<SecretGrant>('request', query);\n\n/**\n * Delete a stored secret and tombstone every dependent per-app use grant\n * (SECRETS_SPEC §4 `secrets:revoke`, §8.15 cascade). Requires `secrets:revoke`.\n */\nexport const revokeSecret = async (id: string): Promise<void> => {\n await request('revoke', { id });\n};\n\n// The metadata-only `secrets-metadata` channel (Recipe A): the host pushes the\n// current secret metadata on change and replays it on register-frame; gated by\n// `secrets:list`. NEVER carries a value (SECRETS_SPEC §4).\nconst channel = createPushChannel<SecretView[]>({\n pushType: SECRETS_METADATA,\n requestType: REQUEST_SECRETS_METADATA,\n initial: [],\n parse: (msg) => (Array.isArray(msg.secrets) ? (msg.secrets as SecretView[]) : undefined),\n});\n\n/** The metadata of the user's stored secrets (never values), `secrets:list`. Poll\n * for a one-off read; use {@link onSecretsChange}/{@link useSecrets} to react. */\nexport const getSecrets = (): SecretView[] => channel.get();\n\n/** Subscribe to secret-metadata changes (added/revoked). Invoked immediately with\n * the current list, then on every change. Returns an unsubscribe fn. */\nexport const onSecretsChange = (listener: (secrets: SecretView[]) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning the user's secret metadata (never values), re-rendering\n * on change. For the Settings app (SECRETS_SPEC §7). */\nexport const useSecrets = (): SecretView[] => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,0BAAgC;AAChC,yBAAkC;AAClC,sBAA6E;AAC7E,6BAAwB;AA6DxB,MAAM,UAAU,OACd,QACA,QAAgB,CAAC,MACF;AACf,QAAM,MAAO,UAAM,qCAAgB,+BAAQ,gCAAgB,GAAG,QAAQ,CAAC,KAAK,CAAC;AAC7E,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,uBAAuB;AAC7D,QAAI,OAAQ,KAAK,QAAgC;AACjD,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AASO,MAAM,mBAAmB,CAAC,QAAqB,CAAC,MACrD,QAAoB,OAAO,KAAK;AAa3B,MAAM,gBAAgB,CAAC,QAAqB,CAAC,MAClD,QAAqB,WAAW,KAAK;AAMhC,MAAM,eAAe,OAAO,OAA8B;AAC/D,QAAM,QAAQ,UAAU,EAAE,GAAG,CAAC;AAChC;AAKA,MAAM,cAAU,sCAAgC;AAAA,EAC9C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,CAAC;AAAA,EACV,OAAO,CAAC,QAAS,MAAM,QAAQ,IAAI,OAAO,IAAK,IAAI,UAA2B;AAChF,CAAC;AAIM,MAAM,aAAa,MAAoB,QAAQ,IAAI;AAInD,MAAM,kBAAkB,CAAC,aAC9B,QAAQ,SAAS,QAAQ;AAIpB,MAAM,aAAa,MAAoB,QAAQ,IAAI;","names":[]}
package/dist/secrets.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import "./chunk-VHAA22YE.js";
2
2
  import { protocolRequest } from "./sandboxUtils";
3
3
  import { createPushChannel } from "./pushChannel";
4
+ import { PROTOCOL_SECRETS, REQUEST_SECRETS_METADATA, SECRETS_METADATA } from "./generated/protocol";
5
+ import { SCHEMES } from "./protocolSchemes";
4
6
  const request = async (method, query = {}) => {
5
- const res = await protocolRequest("secrets", method, [query]);
7
+ const res = await protocolRequest(SCHEMES[PROTOCOL_SECRETS], method, [query]);
6
8
  if (!res || res.ok !== true) {
7
9
  const err = new Error(res?.message ?? "secret request failed");
8
10
  err.code = res?.code ?? "unknown";
@@ -16,8 +18,8 @@ const revokeSecret = async (id) => {
16
18
  await request("revoke", { id });
17
19
  };
18
20
  const channel = createPushChannel({
19
- pushType: "secrets-metadata",
20
- requestType: "request-secrets-metadata",
21
+ pushType: SECRETS_METADATA,
22
+ requestType: REQUEST_SECRETS_METADATA,
21
23
  initial: [],
22
24
  parse: (msg) => Array.isArray(msg.secrets) ? msg.secrets : void 0
23
25
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/secrets.ts"],"sourcesContent":["// The host-owned secret store — app-facing surface (SECRETS_SPEC §4/§5).\n//\n// An app can ASK the user to store a secret (`requestAddSecret`), be GRANTED the\n// right to USE a specific secret (`requestSecret`, the powerbox flow), LIST\n// secret METADATA (`getSecrets`/`useSecrets`, never values), and REVOKE one\n// (`revokeSecret`). The secret VALUE never crosses this boundary: it is read\n// host-side, once, at the `net:fetch` injection point (SECRETS_SPEC §6). These\n// functions move only hints, metadata, and grant handles.\n//\n// Resolves LLM_AND_AGENTS_SPEC D2 — the host-mediated BYOK key store. Inert until\n// the host implements `protocol-secrets` + the `secrets-metadata` channel\n// (SECRETS_SPEC §3/§6 host work, roadmap P1.E); the contract is shipped here so\n// apps (e.g. the in-browser coding agent, P3-73) can be written against it.\nimport { protocolRequest } from './sandboxUtils';\nimport { createPushChannel } from './pushChannel';\n\n/** The closed secret-type vocabulary (SECRETS_SPEC §2). `api-key` is always\n * origin-bound; `oauth-refresh` is reserved (no substitution in v1). */\nexport type SecretType = 'api-key' | 'bearer-token' | 'oauth-refresh';\n\n/**\n * The metadata-only projection of a stored secret (SECRETS_SPEC §2/§4) — exactly\n * what `secrets:list` and the powerbox return. **There is no `value` field by\n * design**: the plaintext is never part of any record an app receives.\n */\nexport interface SecretView {\n id: string;\n type: SecretType;\n family?: string;\n description: string;\n /** Required for `type:'api-key'` — the one https origin it may be sent to. */\n boundOrigin?: string;\n /** ISO-8601, or null if never used (drives the §8.15 90-day expiry). */\n lastUsedAt: string | null;\n}\n\n/** Hints for the host's \"add secret\" modal (SECRETS_SPEC §4 `secrets:add`). The\n * app supplies only hints; the user types the value into host chrome. */\nexport interface SecretHints {\n type?: SecretType;\n family?: string;\n /** Pre-fill the bound origin (e.g. `https://api.anthropic.com`). */\n suggestedOrigin?: string;\n description?: string;\n}\n\n/** What `requestSecret()` matches against in the powerbox picker (SECRETS_SPEC §5). */\nexport interface SecretQuery {\n type?: SecretType;\n family?: string;\n}\n\n/**\n * The result of a granted `requestSecret()` — a durable `(appKey, secretId)` use\n * grant plus the secret's metadata. **Never the value.** Hold onto nothing but\n * this; the host substitutes the value into matching `net:fetch` requests.\n */\nexport interface SecretGrant {\n /** Opaque handle for the minted `(appKey, secretId)` grant. */\n grantId: string;\n /** Metadata of the bound secret (no value). */\n secret: SecretView;\n}\n\n/** An error from a secret operation, carrying a machine-readable `code`. */\nexport interface SecretError extends Error {\n code: 'auth-required' | 'cancelled' | 'forbidden' | 'not-found' | 'invalid-params' | 'unknown';\n}\n\ntype SecretResult =\n | { ok: true; data: unknown }\n | { ok: false; code: string; message: string };\n\n// Issue a `protocol-secrets` request, unwrapping the host's {ok,data} envelope\n// and throwing a typed SecretError on failure (mirrors mounts.ts `request`).\nconst request = async <T = unknown>(\n method: string,\n query: object = {},\n): Promise<T> => {\n const res = (await protocolRequest('secrets', method, [query])) as SecretResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'secret request failed') as SecretError;\n err.code = (res?.code as SecretError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n/**\n * Ask the user to store a new secret (SECRETS_SPEC §4 `secrets:add`). Opens a\n * **host-drawn** modal (the value is typed into host chrome, never via the app);\n * resolves with the new secret's {@link SecretView} metadata, or rejects with a\n * {@link SecretError} (`cancelled` if the user dismisses the modal). Requires the\n * `secrets:add` capability.\n */\nexport const requestAddSecret = (hints: SecretHints = {}): Promise<SecretView> =>\n request<SecretView>('add', hints);\n\n/**\n * Ask the user to bind one of their stored secrets to this app (SECRETS_SPEC §5,\n * the powerbox flow — modeled on `requestSpace()`). The host draws a picker of\n * **only the user's matching secrets**; the user picks, declines, or creates one.\n * On success the host records a durable `(appKey, secretId)` grant and resolves\n * with a {@link SecretGrant} (handle + metadata, **never the value**).\n *\n * **No existence oracle (T20/T27):** a decline, an ungranted secret, and a\n * nonexistent secret are indistinguishable — all reject with a {@link SecretError}\n * `cancelled`; the app never sees the list it chose from.\n */\nexport const requestSecret = (query: SecretQuery = {}): Promise<SecretGrant> =>\n request<SecretGrant>('request', query);\n\n/**\n * Delete a stored secret and tombstone every dependent per-app use grant\n * (SECRETS_SPEC §4 `secrets:revoke`, §8.15 cascade). Requires `secrets:revoke`.\n */\nexport const revokeSecret = async (id: string): Promise<void> => {\n await request('revoke', { id });\n};\n\n// The metadata-only `secrets-metadata` channel (Recipe A): the host pushes the\n// current secret metadata on change and replays it on register-frame; gated by\n// `secrets:list`. NEVER carries a value (SECRETS_SPEC §4).\nconst channel = createPushChannel<SecretView[]>({\n pushType: 'secrets-metadata',\n requestType: 'request-secrets-metadata',\n initial: [],\n parse: (msg) => (Array.isArray(msg.secrets) ? (msg.secrets as SecretView[]) : undefined),\n});\n\n/** The metadata of the user's stored secrets (never values), `secrets:list`. Poll\n * for a one-off read; use {@link onSecretsChange}/{@link useSecrets} to react. */\nexport const getSecrets = (): SecretView[] => channel.get();\n\n/** Subscribe to secret-metadata changes (added/revoked). Invoked immediately with\n * the current list, then on every change. Returns an unsubscribe fn. */\nexport const onSecretsChange = (listener: (secrets: SecretView[]) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning the user's secret metadata (never values), re-rendering\n * on change. For the Settings app (SECRETS_SPEC §7). */\nexport const useSecrets = (): SecretView[] => channel.use();\n"],"mappings":";AAaA,SAAS,uBAAuB;AAChC,SAAS,yBAAyB;AA6DlC,MAAM,UAAU,OACd,QACA,QAAgB,CAAC,MACF;AACf,QAAM,MAAO,MAAM,gBAAgB,WAAW,QAAQ,CAAC,KAAK,CAAC;AAC7D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,uBAAuB;AAC7D,QAAI,OAAQ,KAAK,QAAgC;AACjD,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AASO,MAAM,mBAAmB,CAAC,QAAqB,CAAC,MACrD,QAAoB,OAAO,KAAK;AAa3B,MAAM,gBAAgB,CAAC,QAAqB,CAAC,MAClD,QAAqB,WAAW,KAAK;AAMhC,MAAM,eAAe,OAAO,OAA8B;AAC/D,QAAM,QAAQ,UAAU,EAAE,GAAG,CAAC;AAChC;AAKA,MAAM,UAAU,kBAAgC;AAAA,EAC9C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,CAAC;AAAA,EACV,OAAO,CAAC,QAAS,MAAM,QAAQ,IAAI,OAAO,IAAK,IAAI,UAA2B;AAChF,CAAC;AAIM,MAAM,aAAa,MAAoB,QAAQ,IAAI;AAInD,MAAM,kBAAkB,CAAC,aAC9B,QAAQ,SAAS,QAAQ;AAIpB,MAAM,aAAa,MAAoB,QAAQ,IAAI;","names":[]}
1
+ {"version":3,"sources":["../src/secrets.ts"],"sourcesContent":["// The host-owned secret store — app-facing surface (SECRETS_SPEC §4/§5).\n//\n// An app can ASK the user to store a secret (`requestAddSecret`), be GRANTED the\n// right to USE a specific secret (`requestSecret`, the powerbox flow), LIST\n// secret METADATA (`getSecrets`/`useSecrets`, never values), and REVOKE one\n// (`revokeSecret`). The secret VALUE never crosses this boundary: it is read\n// host-side, once, at the `net:fetch` injection point (SECRETS_SPEC §6). These\n// functions move only hints, metadata, and grant handles.\n//\n// Resolves LLM_AND_AGENTS_SPEC D2 — the host-mediated BYOK key store. Inert until\n// the host implements `protocol-secrets` + the `secrets-metadata` channel\n// (SECRETS_SPEC §3/§6 host work, roadmap P1.E); the contract is shipped here so\n// apps (e.g. the in-browser coding agent, P3-73) can be written against it.\nimport { protocolRequest } from './sandboxUtils';\nimport { createPushChannel } from './pushChannel';\nimport { PROTOCOL_SECRETS, REQUEST_SECRETS_METADATA, SECRETS_METADATA } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/** The closed secret-type vocabulary (SECRETS_SPEC §2). `api-key` is always\n * origin-bound; `oauth-refresh` is reserved (no substitution in v1). */\nexport type SecretType = 'api-key' | 'bearer-token' | 'oauth-refresh';\n\n/**\n * The metadata-only projection of a stored secret (SECRETS_SPEC §2/§4) — exactly\n * what `secrets:list` and the powerbox return. **There is no `value` field by\n * design**: the plaintext is never part of any record an app receives.\n */\nexport interface SecretView {\n id: string;\n type: SecretType;\n family?: string;\n description: string;\n /** Required for `type:'api-key'` — the one https origin it may be sent to. */\n boundOrigin?: string;\n /** ISO-8601, or null if never used (drives the §8.15 90-day expiry). */\n lastUsedAt: string | null;\n}\n\n/** Hints for the host's \"add secret\" modal (SECRETS_SPEC §4 `secrets:add`). The\n * app supplies only hints; the user types the value into host chrome. */\nexport interface SecretHints {\n type?: SecretType;\n family?: string;\n /** Pre-fill the bound origin (e.g. `https://api.anthropic.com`). */\n suggestedOrigin?: string;\n description?: string;\n}\n\n/** What `requestSecret()` matches against in the powerbox picker (SECRETS_SPEC §5). */\nexport interface SecretQuery {\n type?: SecretType;\n family?: string;\n}\n\n/**\n * The result of a granted `requestSecret()` — a durable `(appKey, secretId)` use\n * grant plus the secret's metadata. **Never the value.** Hold onto nothing but\n * this; the host substitutes the value into matching `net:fetch` requests.\n */\nexport interface SecretGrant {\n /** Opaque handle for the minted `(appKey, secretId)` grant. */\n grantId: string;\n /** Metadata of the bound secret (no value). */\n secret: SecretView;\n}\n\n/** An error from a secret operation, carrying a machine-readable `code`. */\nexport interface SecretError extends Error {\n code: 'auth-required' | 'cancelled' | 'forbidden' | 'not-found' | 'invalid-params' | 'unknown';\n}\n\ntype SecretResult =\n | { ok: true; data: unknown }\n | { ok: false; code: string; message: string };\n\n// Issue a `protocol-secrets` request, unwrapping the host's {ok,data} envelope\n// and throwing a typed SecretError on failure (mirrors mounts.ts `request`).\nconst request = async <T = unknown>(\n method: string,\n query: object = {},\n): Promise<T> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_SECRETS], method, [query])) as SecretResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'secret request failed') as SecretError;\n err.code = (res?.code as SecretError['code']) ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n/**\n * Ask the user to store a new secret (SECRETS_SPEC §4 `secrets:add`). Opens a\n * **host-drawn** modal (the value is typed into host chrome, never via the app);\n * resolves with the new secret's {@link SecretView} metadata, or rejects with a\n * {@link SecretError} (`cancelled` if the user dismisses the modal). Requires the\n * `secrets:add` capability.\n */\nexport const requestAddSecret = (hints: SecretHints = {}): Promise<SecretView> =>\n request<SecretView>('add', hints);\n\n/**\n * Ask the user to bind one of their stored secrets to this app (SECRETS_SPEC §5,\n * the powerbox flow — modeled on `requestSpace()`). The host draws a picker of\n * **only the user's matching secrets**; the user picks, declines, or creates one.\n * On success the host records a durable `(appKey, secretId)` grant and resolves\n * with a {@link SecretGrant} (handle + metadata, **never the value**).\n *\n * **No existence oracle (T20/T27):** a decline, an ungranted secret, and a\n * nonexistent secret are indistinguishable — all reject with a {@link SecretError}\n * `cancelled`; the app never sees the list it chose from.\n */\nexport const requestSecret = (query: SecretQuery = {}): Promise<SecretGrant> =>\n request<SecretGrant>('request', query);\n\n/**\n * Delete a stored secret and tombstone every dependent per-app use grant\n * (SECRETS_SPEC §4 `secrets:revoke`, §8.15 cascade). Requires `secrets:revoke`.\n */\nexport const revokeSecret = async (id: string): Promise<void> => {\n await request('revoke', { id });\n};\n\n// The metadata-only `secrets-metadata` channel (Recipe A): the host pushes the\n// current secret metadata on change and replays it on register-frame; gated by\n// `secrets:list`. NEVER carries a value (SECRETS_SPEC §4).\nconst channel = createPushChannel<SecretView[]>({\n pushType: SECRETS_METADATA,\n requestType: REQUEST_SECRETS_METADATA,\n initial: [],\n parse: (msg) => (Array.isArray(msg.secrets) ? (msg.secrets as SecretView[]) : undefined),\n});\n\n/** The metadata of the user's stored secrets (never values), `secrets:list`. Poll\n * for a one-off read; use {@link onSecretsChange}/{@link useSecrets} to react. */\nexport const getSecrets = (): SecretView[] => channel.get();\n\n/** Subscribe to secret-metadata changes (added/revoked). Invoked immediately with\n * the current list, then on every change. Returns an unsubscribe fn. */\nexport const onSecretsChange = (listener: (secrets: SecretView[]) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning the user's secret metadata (never values), re-rendering\n * on change. For the Settings app (SECRETS_SPEC §7). */\nexport const useSecrets = (): SecretView[] => channel.use();\n"],"mappings":";AAaA,SAAS,uBAAuB;AAChC,SAAS,yBAAyB;AAClC,SAAS,kBAAkB,0BAA0B,wBAAwB;AAC7E,SAAS,eAAe;AA6DxB,MAAM,UAAU,OACd,QACA,QAAgB,CAAC,MACF;AACf,QAAM,MAAO,MAAM,gBAAgB,QAAQ,gBAAgB,GAAG,QAAQ,CAAC,KAAK,CAAC;AAC7E,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,uBAAuB;AAC7D,QAAI,OAAQ,KAAK,QAAgC;AACjD,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AASO,MAAM,mBAAmB,CAAC,QAAqB,CAAC,MACrD,QAAoB,OAAO,KAAK;AAa3B,MAAM,gBAAgB,CAAC,QAAqB,CAAC,MAClD,QAAqB,WAAW,KAAK;AAMhC,MAAM,eAAe,OAAO,OAA8B;AAC/D,QAAM,QAAQ,UAAU,EAAE,GAAG,CAAC;AAChC;AAKA,MAAM,UAAU,kBAAgC;AAAA,EAC9C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,CAAC;AAAA,EACV,OAAO,CAAC,QAAS,MAAM,QAAQ,IAAI,OAAO,IAAK,IAAI,UAA2B;AAChF,CAAC;AAIM,MAAM,aAAa,MAAoB,QAAQ,IAAI;AAInD,MAAM,kBAAkB,CAAC,aAC9B,QAAQ,SAAS,QAAQ;AAIpB,MAAM,aAAa,MAAoB,QAAQ,IAAI;","names":[]}
package/dist/tasks.cjs CHANGED
@@ -29,10 +29,12 @@ __export(tasks_exports, {
29
29
  module.exports = __toCommonJS(tasks_exports);
30
30
  var import_react = require("react");
31
31
  var import_sandboxUtils = require("./sandboxUtils");
32
+ var import_protocol = require("./generated/protocol");
33
+ var import_protocolSchemes = require("./protocolSchemes");
32
34
  const capFile = (ref, opts) => ({ $cap: "file", mountId: ref.mountId, relPath: ref.relPath, mode: opts.mode });
33
35
  const capDir = (ref, opts) => ({ $cap: "dir", mountId: ref.mountId, relPath: ref.relPath, mode: opts.mode });
34
36
  const invokeTask = async (task, params = {}) => {
35
- const res = await (0, import_sandboxUtils.protocolRequest)("task", "invoke", [{ task, params }]);
37
+ const res = await (0, import_sandboxUtils.protocolRequest)(import_protocolSchemes.SCHEMES[import_protocol.PROTOCOL_TASK], "invoke", [{ task, params }]);
36
38
  if (!res || res.ok !== true) {
37
39
  const err = new Error(res?.message ?? `task '${task}' failed`);
38
40
  err.code = res?.code ?? "unknown";
@@ -42,13 +44,13 @@ const invokeTask = async (task, params = {}) => {
42
44
  };
43
45
  let latestInput = null;
44
46
  const inputListeners = /* @__PURE__ */ new Set();
45
- (0, import_sandboxUtils.addListener)("task-input", (m) => {
47
+ (0, import_sandboxUtils.addListener)(import_protocol.TASK_INPUT, (m) => {
46
48
  latestInput = { task: m.task, params: m.params ?? {} };
47
49
  inputListeners.forEach((l) => l(latestInput));
48
50
  });
49
51
  const getTaskInput = () => latestInput;
50
- const completeTask = (result) => (0, import_sandboxUtils.sendMessage)("task-complete", { result });
51
- const cancelTask = () => (0, import_sandboxUtils.sendMessage)("task-cancel", {});
52
+ const completeTask = (result) => (0, import_sandboxUtils.sendMessage)(import_protocol.TASK_COMPLETE, { result });
53
+ const cancelTask = () => (0, import_sandboxUtils.sendMessage)(import_protocol.TASK_CANCEL, {});
52
54
  const useTaskInput = () => {
53
55
  const [input, setInput] = (0, import_react.useState)(getTaskInput);
54
56
  (0, import_react.useEffect)(() => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tasks.ts"],"sourcesContent":["// Task invocation — apps invoking apps (UI_AS_APPS_SPEC §5.7). The\n// `startActivityForResult` pattern: one app invokes another by TASK CONTRACT\n// (never by app name — the user's override picks the bound app), passes typed\n// params, and awaits a typed result. The callee runs in a host-owned overlay\n// under ITS OWN grants — data crosses, your authority does not (§5.7).\n//\n// Two roles:\n// - CALLER: `invokeTask(task, params)` (Recipe B — a deferred reply the host\n// holds open until the callee finishes). Delegate a file with `capFile(...)`:\n// the host resolves it against YOUR grants and mints an attenuated chroot.\n// - CALLEE: read `useTaskInput()`, then `completeTask(result)` / `cancelTask()`.\nimport { useEffect, useState } from 'react';\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\n\n// ── caller side ─────────────────────────────────────────────────────────────\n\n/** A delegated FILE capability marker for a task param (§5.7). */\nexport interface FileCap {\n $cap: 'file';\n mountId: string;\n relPath: string;\n mode: 'ro' | 'rw';\n}\n\n/**\n * Build a delegated file reference for a task param. The host resolves it against\n * YOUR OWN grants and mints an attenuated, task-scoped chroot for the callee — you\n * can only delegate a path you already hold (attenuation only, never escalation).\n *\n * file: capFile({ mountId: 'space:abc', relPath: 'photos/cat.jpg' }, { mode: 'rw' })\n */\nexport const capFile = (\n ref: { mountId: string; relPath: string },\n opts: { mode: 'ro' | 'rw' },\n): FileCap => ({ $cap: 'file', mountId: ref.mountId, relPath: ref.relPath, mode: opts.mode });\n\n/** A delegated DIRECTORY capability marker for a task param (D2). Like {@link FileCap}\n * but `relPath` names a DIRECTORY: the host chroots the callee AT that directory\n * (the whole subtree). Used for the `pick-file` `roots` — one chroot per root. */\nexport interface DirCap {\n $cap: 'dir';\n mountId: string;\n relPath: string;\n mode: 'ro' | 'rw';\n}\n\n/**\n * Build a delegated DIRECTORY reference for a task param (the directory analogue of\n * {@link capFile}). The host resolves it against YOUR OWN grants and mints an\n * attenuated, task-scoped chroot of that directory for the callee — you can only\n * delegate a directory you already hold (attenuation only, never escalation):\n *\n * roots: [capDir({ mountId: 'space:abc', relPath: 'boards' }, { mode: 'rw' })]\n */\nexport const capDir = (\n ref: { mountId: string; relPath: string },\n opts: { mode: 'ro' | 'rw' },\n): DirCap => ({ $cap: 'dir', mountId: ref.mountId, relPath: ref.relPath, mode: opts.mode });\n\n/**\n * Invoke another app via a task contract and await its typed result (Recipe B).\n * Rejects with a machine `.code` on refusal: `cancelled` (user dismissed the\n * overlay), `timeout` (§5.7.1 liveness), `forbidden` (undeclared task or a file\n * delegation you don't hold), `no-such-task`, `task-cycle`/`task-depth-exceeded`/\n * `task-version-mismatch`, or `invalid-params` (result failed the contract schema).\n */\nexport const invokeTask = async <R = unknown>(\n task: string,\n params: Record<string, unknown> = {},\n): Promise<R> => {\n const res = (await protocolRequest('task', 'invoke', [{ task, params }])) as\n | { ok: true; data: R }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `task '${task}' failed`) as Error & { code?: string };\n err.code = res?.code ?? 'unknown';\n throw err;\n }\n return res.data;\n};\n\n// ── callee side ─────────────────────────────────────────────────────────────\n\n/** The params this app was invoked with as a task callee. */\nexport interface TaskInput {\n task: string;\n params: Record<string, unknown>;\n}\n\nlet latestInput: TaskInput | null = null;\nconst inputListeners = new Set<(i: TaskInput) => void>();\n\n// The host delivers a `task-input` message to the callee's iframe right after it\n// mounts the overlay (the §5.7 \"params via the region's mount event\").\naddListener('task-input', (m: { task: string; params?: Record<string, unknown> }) => {\n latestInput = { task: m.task, params: m.params ?? {} };\n inputListeners.forEach((l) => l(latestInput!));\n});\n\n/** The task params this app was invoked with, or null if it isn't a task callee. */\nexport const getTaskInput = (): TaskInput | null => latestInput;\n\n/**\n * Finish the task, returning a result to the caller. The host validates it against\n * the contract's result schema before resolving the caller (`invalid-params` on\n * violation), then tears down this overlay.\n */\nexport const completeTask = (result: unknown): void => sendMessage('task-complete', { result });\n\n/** Abort the task; the caller's `invokeTask` rejects with `cancelled`. */\nexport const cancelTask = (): void => sendMessage('task-cancel', {});\n\n/** React hook: the task input for this callee, re-rendering when it arrives. */\nexport const useTaskInput = (): TaskInput | null => {\n const [input, setInput] = useState<TaskInput | null>(getTaskInput);\n useEffect(() => {\n const l = (i: TaskInput) => setInput(i);\n inputListeners.add(l);\n if (latestInput) setInput(latestInput);\n return () => {\n inputListeners.delete(l);\n };\n }, []);\n return input;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWA,mBAAoC;AACpC,0BAA0D;AAmBnD,MAAM,UAAU,CACrB,KACA,UACa,EAAE,MAAM,QAAQ,SAAS,IAAI,SAAS,SAAS,IAAI,SAAS,MAAM,KAAK,KAAK;AAoBpF,MAAM,SAAS,CACpB,KACA,UACY,EAAE,MAAM,OAAO,SAAS,IAAI,SAAS,SAAS,IAAI,SAAS,MAAM,KAAK,KAAK;AASlF,MAAM,aAAa,OACxB,MACA,SAAkC,CAAC,MACpB;AACf,QAAM,MAAO,UAAM,qCAAgB,QAAQ,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,CAAC;AAIvE,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,SAAS,IAAI,UAAU;AAC7D,QAAI,OAAO,KAAK,QAAQ;AACxB,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAUA,IAAI,cAAgC;AACpC,MAAM,iBAAiB,oBAAI,IAA4B;AAAA,IAIvD,iCAAY,cAAc,CAAC,MAA0D;AACnF,gBAAc,EAAE,MAAM,EAAE,MAAM,QAAQ,EAAE,UAAU,CAAC,EAAE;AACrD,iBAAe,QAAQ,CAAC,MAAM,EAAE,WAAY,CAAC;AAC/C,CAAC;AAGM,MAAM,eAAe,MAAwB;AAO7C,MAAM,eAAe,CAAC,eAA0B,iCAAY,iBAAiB,EAAE,OAAO,CAAC;AAGvF,MAAM,aAAa,UAAY,iCAAY,eAAe,CAAC,CAAC;AAG5D,MAAM,eAAe,MAAwB;AAClD,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAA2B,YAAY;AACjE,8BAAU,MAAM;AACd,UAAM,IAAI,CAAC,MAAiB,SAAS,CAAC;AACtC,mBAAe,IAAI,CAAC;AACpB,QAAI,YAAa,UAAS,WAAW;AACrC,WAAO,MAAM;AACX,qBAAe,OAAO,CAAC;AAAA,IACzB;AAAA,EACF,GAAG,CAAC,CAAC;AACL,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/tasks.ts"],"sourcesContent":["// Task invocation — apps invoking apps (UI_AS_APPS_SPEC §5.7). The\n// `startActivityForResult` pattern: one app invokes another by TASK CONTRACT\n// (never by app name — the user's override picks the bound app), passes typed\n// params, and awaits a typed result. The callee runs in a host-owned overlay\n// under ITS OWN grants — data crosses, your authority does not (§5.7).\n//\n// Two roles:\n// - CALLER: `invokeTask(task, params)` (Recipe B — a deferred reply the host\n// holds open until the callee finishes). Delegate a file with `capFile(...)`:\n// the host resolves it against YOUR grants and mints an attenuated chroot.\n// - CALLEE: read `useTaskInput()`, then `completeTask(result)` / `cancelTask()`.\nimport { useEffect, useState } from 'react';\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport { PROTOCOL_TASK, TASK_CANCEL, TASK_COMPLETE, TASK_INPUT } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n// ── caller side ─────────────────────────────────────────────────────────────\n\n/** A delegated FILE capability marker for a task param (§5.7). */\nexport interface FileCap {\n $cap: 'file';\n mountId: string;\n relPath: string;\n mode: 'ro' | 'rw';\n}\n\n/**\n * Build a delegated file reference for a task param. The host resolves it against\n * YOUR OWN grants and mints an attenuated, task-scoped chroot for the callee — you\n * can only delegate a path you already hold (attenuation only, never escalation).\n *\n * file: capFile({ mountId: 'space:abc', relPath: 'photos/cat.jpg' }, { mode: 'rw' })\n */\nexport const capFile = (\n ref: { mountId: string; relPath: string },\n opts: { mode: 'ro' | 'rw' },\n): FileCap => ({ $cap: 'file', mountId: ref.mountId, relPath: ref.relPath, mode: opts.mode });\n\n/** A delegated DIRECTORY capability marker for a task param (D2). Like {@link FileCap}\n * but `relPath` names a DIRECTORY: the host chroots the callee AT that directory\n * (the whole subtree). Used for the `pick-file` `roots` — one chroot per root. */\nexport interface DirCap {\n $cap: 'dir';\n mountId: string;\n relPath: string;\n mode: 'ro' | 'rw';\n}\n\n/**\n * Build a delegated DIRECTORY reference for a task param (the directory analogue of\n * {@link capFile}). The host resolves it against YOUR OWN grants and mints an\n * attenuated, task-scoped chroot of that directory for the callee — you can only\n * delegate a directory you already hold (attenuation only, never escalation):\n *\n * roots: [capDir({ mountId: 'space:abc', relPath: 'boards' }, { mode: 'rw' })]\n */\nexport const capDir = (\n ref: { mountId: string; relPath: string },\n opts: { mode: 'ro' | 'rw' },\n): DirCap => ({ $cap: 'dir', mountId: ref.mountId, relPath: ref.relPath, mode: opts.mode });\n\n/**\n * Invoke another app via a task contract and await its typed result (Recipe B).\n * Rejects with a machine `.code` on refusal: `cancelled` (user dismissed the\n * overlay), `timeout` (§5.7.1 liveness), `forbidden` (undeclared task or a file\n * delegation you don't hold), `no-such-task`, `task-cycle`/`task-depth-exceeded`/\n * `task-version-mismatch`, or `invalid-params` (result failed the contract schema).\n */\nexport const invokeTask = async <R = unknown>(\n task: string,\n params: Record<string, unknown> = {},\n): Promise<R> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_TASK], 'invoke', [{ task, params }])) as\n | { ok: true; data: R }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `task '${task}' failed`) as Error & { code?: string };\n err.code = res?.code ?? 'unknown';\n throw err;\n }\n return res.data;\n};\n\n// ── callee side ─────────────────────────────────────────────────────────────\n\n/** The params this app was invoked with as a task callee. */\nexport interface TaskInput {\n task: string;\n params: Record<string, unknown>;\n}\n\nlet latestInput: TaskInput | null = null;\nconst inputListeners = new Set<(i: TaskInput) => void>();\n\n// The host delivers a `task-input` message to the callee's iframe right after it\n// mounts the overlay (the §5.7 \"params via the region's mount event\").\naddListener(TASK_INPUT, (m: { task: string; params?: Record<string, unknown> }) => {\n latestInput = { task: m.task, params: m.params ?? {} };\n inputListeners.forEach((l) => l(latestInput!));\n});\n\n/** The task params this app was invoked with, or null if it isn't a task callee. */\nexport const getTaskInput = (): TaskInput | null => latestInput;\n\n/**\n * Finish the task, returning a result to the caller. The host validates it against\n * the contract's result schema before resolving the caller (`invalid-params` on\n * violation), then tears down this overlay.\n */\nexport const completeTask = (result: unknown): void => sendMessage(TASK_COMPLETE, { result });\n\n/** Abort the task; the caller's `invokeTask` rejects with `cancelled`. */\nexport const cancelTask = (): void => sendMessage(TASK_CANCEL, {});\n\n/** React hook: the task input for this callee, re-rendering when it arrives. */\nexport const useTaskInput = (): TaskInput | null => {\n const [input, setInput] = useState<TaskInput | null>(getTaskInput);\n useEffect(() => {\n const l = (i: TaskInput) => setInput(i);\n inputListeners.add(l);\n if (latestInput) setInput(latestInput);\n return () => {\n inputListeners.delete(l);\n };\n }, []);\n return input;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWA,mBAAoC;AACpC,0BAA0D;AAC1D,sBAAsE;AACtE,6BAAwB;AAmBjB,MAAM,UAAU,CACrB,KACA,UACa,EAAE,MAAM,QAAQ,SAAS,IAAI,SAAS,SAAS,IAAI,SAAS,MAAM,KAAK,KAAK;AAoBpF,MAAM,SAAS,CACpB,KACA,UACY,EAAE,MAAM,OAAO,SAAS,IAAI,SAAS,SAAS,IAAI,SAAS,MAAM,KAAK,KAAK;AASlF,MAAM,aAAa,OACxB,MACA,SAAkC,CAAC,MACpB;AACf,QAAM,MAAO,UAAM,qCAAgB,+BAAQ,6BAAa,GAAG,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,CAAC;AAIvF,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,SAAS,IAAI,UAAU;AAC7D,QAAI,OAAO,KAAK,QAAQ;AACxB,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAUA,IAAI,cAAgC;AACpC,MAAM,iBAAiB,oBAAI,IAA4B;AAAA,IAIvD,iCAAY,4BAAY,CAAC,MAA0D;AACjF,gBAAc,EAAE,MAAM,EAAE,MAAM,QAAQ,EAAE,UAAU,CAAC,EAAE;AACrD,iBAAe,QAAQ,CAAC,MAAM,EAAE,WAAY,CAAC;AAC/C,CAAC;AAGM,MAAM,eAAe,MAAwB;AAO7C,MAAM,eAAe,CAAC,eAA0B,iCAAY,+BAAe,EAAE,OAAO,CAAC;AAGrF,MAAM,aAAa,UAAY,iCAAY,6BAAa,CAAC,CAAC;AAG1D,MAAM,eAAe,MAAwB;AAClD,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAA2B,YAAY;AACjE,8BAAU,MAAM;AACd,UAAM,IAAI,CAAC,MAAiB,SAAS,CAAC;AACtC,mBAAe,IAAI,CAAC;AACpB,QAAI,YAAa,UAAS,WAAW;AACrC,WAAO,MAAM;AACX,qBAAe,OAAO,CAAC;AAAA,IACzB;AAAA,EACF,GAAG,CAAC,CAAC;AACL,SAAO;AACT;","names":[]}
package/dist/tasks.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import "./chunk-VHAA22YE.js";
2
2
  import { useEffect, useState } from "react";
3
3
  import { protocolRequest, sendMessage, addListener } from "./sandboxUtils";
4
+ import { PROTOCOL_TASK, TASK_CANCEL, TASK_COMPLETE, TASK_INPUT } from "./generated/protocol";
5
+ import { SCHEMES } from "./protocolSchemes";
4
6
  const capFile = (ref, opts) => ({ $cap: "file", mountId: ref.mountId, relPath: ref.relPath, mode: opts.mode });
5
7
  const capDir = (ref, opts) => ({ $cap: "dir", mountId: ref.mountId, relPath: ref.relPath, mode: opts.mode });
6
8
  const invokeTask = async (task, params = {}) => {
7
- const res = await protocolRequest("task", "invoke", [{ task, params }]);
9
+ const res = await protocolRequest(SCHEMES[PROTOCOL_TASK], "invoke", [{ task, params }]);
8
10
  if (!res || res.ok !== true) {
9
11
  const err = new Error(res?.message ?? `task '${task}' failed`);
10
12
  err.code = res?.code ?? "unknown";
@@ -14,13 +16,13 @@ const invokeTask = async (task, params = {}) => {
14
16
  };
15
17
  let latestInput = null;
16
18
  const inputListeners = /* @__PURE__ */ new Set();
17
- addListener("task-input", (m) => {
19
+ addListener(TASK_INPUT, (m) => {
18
20
  latestInput = { task: m.task, params: m.params ?? {} };
19
21
  inputListeners.forEach((l) => l(latestInput));
20
22
  });
21
23
  const getTaskInput = () => latestInput;
22
- const completeTask = (result) => sendMessage("task-complete", { result });
23
- const cancelTask = () => sendMessage("task-cancel", {});
24
+ const completeTask = (result) => sendMessage(TASK_COMPLETE, { result });
25
+ const cancelTask = () => sendMessage(TASK_CANCEL, {});
24
26
  const useTaskInput = () => {
25
27
  const [input, setInput] = useState(getTaskInput);
26
28
  useEffect(() => {