@immediately-run/sdk 0.32.0 → 0.33.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 (45) hide show
  1. package/dist/boot.d.cts +2 -2
  2. package/dist/boot.d.ts +2 -2
  3. package/dist/components/Include.d.cts +2 -3
  4. package/dist/components/Include.d.ts +2 -3
  5. package/dist/components/MainContent.d.cts +4 -4
  6. package/dist/components/MainContent.d.ts +4 -4
  7. package/dist/components/MountImage.d.cts +2 -2
  8. package/dist/components/MountImage.d.ts +2 -2
  9. package/dist/components/Routes.d.cts +2 -2
  10. package/dist/components/Routes.d.ts +2 -2
  11. package/dist/components/defaults.d.cts +2 -2
  12. package/dist/components/defaults.d.ts +2 -2
  13. package/dist/components/errors.d.cts +2 -2
  14. package/dist/components/errors.d.ts +2 -2
  15. package/dist/index.d.cts +1 -2
  16. package/dist/index.d.ts +1 -2
  17. package/dist/loading.d.cts +5 -5
  18. package/dist/loading.d.ts +5 -5
  19. package/dist/moduleCache.d.cts +1 -2
  20. package/dist/moduleCache.d.ts +1 -2
  21. package/dist/safeContent/SafeContent.cjs.map +1 -1
  22. package/dist/safeContent/SafeContent.js.map +1 -1
  23. package/dist/safeContent/index.cjs.map +1 -1
  24. package/dist/safeContent/index.d.cts +1 -1
  25. package/dist/safeContent/index.d.ts +1 -1
  26. package/dist/safeContent/index.js.map +1 -1
  27. package/dist/safeContent/mdastDeps.cjs +10697 -0
  28. package/dist/safeContent/mdastDeps.js +10680 -0
  29. package/dist/safeContent/parseSafeMdast.cjs +17 -15
  30. package/dist/safeContent/parseSafeMdast.cjs.map +1 -1
  31. package/dist/safeContent/parseSafeMdast.d.cts +16 -2
  32. package/dist/safeContent/parseSafeMdast.d.ts +16 -2
  33. package/dist/safeContent/parseSafeMdast.js +21 -15
  34. package/dist/safeContent/parseSafeMdast.js.map +1 -1
  35. package/dist/safeContent/renderMdast.cjs +5 -1
  36. package/dist/safeContent/renderMdast.cjs.map +1 -1
  37. package/dist/safeContent/renderMdast.js +5 -1
  38. package/dist/safeContent/renderMdast.js.map +1 -1
  39. package/dist/version.cjs +1 -1
  40. package/dist/version.cjs.map +1 -1
  41. package/dist/version.d.cts +1 -1
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/dist/version.js.map +1 -1
  45. package/package.json +9 -7
@@ -31,36 +31,38 @@ __export(parseSafeMdast_exports, {
31
31
  parseSafeMdast: () => parseSafeMdast
32
32
  });
33
33
  module.exports = __toCommonJS(parseSafeMdast_exports);
34
+ var import_mdx_plugins = require("@immediately-run/mdx-plugins");
34
35
  let depsPromise = null;
35
36
  function loadDeps() {
36
37
  if (!depsPromise) {
37
- depsPromise = Promise.all([
38
- import("mdast-util-from-markdown"),
39
- import("micromark-extension-mdx-jsx"),
40
- import("mdast-util-mdx-jsx"),
41
- import("micromark-extension-gfm"),
42
- import("mdast-util-gfm")
43
- ]).then(
44
- ([fromMd, mdxJsxExt, mdxJsxMdast, gfmExt, gfmMdast]) => ({
45
- fromMarkdown: fromMd.fromMarkdown,
46
- mdxJsx: mdxJsxExt.mdxJsx,
47
- mdxJsxFromMarkdown: mdxJsxMdast.mdxJsxFromMarkdown,
48
- gfm: gfmExt.gfm,
49
- gfmFromMarkdown: gfmMdast.gfmFromMarkdown
38
+ depsPromise = import("./mdastDeps").then(
39
+ (m) => ({
40
+ fromMarkdown: m.fromMarkdown,
41
+ mdxJsx: m.mdxJsx,
42
+ mdxJsxFromMarkdown: m.mdxJsxFromMarkdown,
43
+ gfm: m.gfm,
44
+ gfmFromMarkdown: m.gfmFromMarkdown
50
45
  })
51
46
  );
52
47
  }
53
48
  return depsPromise;
54
49
  }
55
- async function parseSafeMdast(source) {
50
+ const admonitions = import_mdx_plugins.remarkAdmonitions;
51
+ const wikiLinks = import_mdx_plugins.remarkWikiLinks;
52
+ const headingAnchors = import_mdx_plugins.remarkHeadingAnchors;
53
+ async function parseSafeMdast(source, options = {}) {
56
54
  const { fromMarkdown, mdxJsx, mdxJsxFromMarkdown, gfm, gfmFromMarkdown } = await loadDeps();
57
- return fromMarkdown(source, {
55
+ const tree = fromMarkdown(source, {
58
56
  // `mdxJsx()` WITHOUT an acorn option → JSX tags + literal attrs; expressions are
59
57
  // raw strings, never estree. The mdx *expression* extension is intentionally
60
58
  // absent, so `{…}` in body text stays literal. GFM for tables/task-lists.
61
59
  extensions: [mdxJsx(), gfm()],
62
60
  mdastExtensions: [mdxJsxFromMarkdown(), gfmFromMarkdown()]
63
61
  });
62
+ admonitions()(tree);
63
+ wikiLinks()(tree);
64
+ headingAnchors({ sectionIds: options.sectionIds !== false })(tree);
65
+ return tree;
64
66
  }
65
67
  // Annotate the CommonJS export names for ESM import in node:
66
68
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/safeContent/parseSafeMdast.ts"],"sourcesContent":["// The no-acorn parse pipeline for the safe content renderer (TRUST_MODES_SPEC §5.1,\n// empirically verified 2026-07-07 against `@mdx-js/mdx` v3 / `micromark-extension-\n// mdx-jsx`). This is the load-bearing half of the fail-safe: `<Component/>` syntax\n// parses to an element node with **literal** attributes, and every expression\n// (`n={1+1}`, `f={fetch(\"/x\")}`, `{...spread}`) is captured as an **inert raw string\n// with no estree** — because we pass **NO acorn option** to `micromark-extension-mdx-\n// jsx`. There is therefore no evaluator anywhere in the pipeline (fail-safe), unlike\n// `@mdx-js/mdx`'s `compile({format:'mdx'})` which always wires acorn.\n//\n// The §5.1 residual guarantees are also here: the expression micromark extension is\n// **not** enabled (body `{…}` stays literal text), and **no `rehype-raw`** / raw-HTML\n// passthrough exists — raw HTML blocks arrive as inert `html` mdast nodes the renderer\n// prints as text, never injected.\n//\n// `micromark`/`mdast` are ESM-only. They are loaded with a memoised dynamic `import()`\n// (the transpiler's `compile.ts` pattern) so this module sits in the package's CJS\n// build too — a CJS consumer that never renders untrusted content never `require()`s\n// an ESM-only package, and one that does gets it via Node's ESM-from-CJS import. The\n// dynamic load changes only *when* the deps resolve, never the produced tree.\n\n// A minimal structural mdast shape — we deliberately avoid a type dependency on the\n// ESM-only `mdast`/`unist` packages (which would break the CJS build's type surface).\nexport interface SafeMdastNode {\n type: string;\n value?: string;\n name?: string | null;\n url?: string;\n alt?: string | null;\n title?: string | null;\n depth?: number;\n ordered?: boolean;\n lang?: string | null;\n children?: SafeMdastNode[];\n attributes?: SafeMdxAttribute[];\n [key: string]: unknown;\n}\n\nexport interface SafeMdxAttribute {\n type: 'mdxJsxAttribute' | 'mdxJsxExpressionAttribute';\n name?: string;\n // A literal attribute carries a string; an expression attribute carries an\n // `mdxJsxAttributeValueExpression` object (a raw string, NO estree) or a string.\n value?: string | { type: string; value?: string } | null;\n}\n\ntype Deps = {\n fromMarkdown: (\n value: string,\n options: { extensions: unknown[]; mdastExtensions: unknown[] },\n ) => SafeMdastNode;\n mdxJsx: () => unknown;\n mdxJsxFromMarkdown: () => unknown;\n gfm: () => unknown;\n gfmFromMarkdown: () => unknown;\n};\n\nlet depsPromise: Promise<Deps> | null = null;\n\nfunction loadDeps(): Promise<Deps> {\n if (!depsPromise) {\n depsPromise = Promise.all([\n import('mdast-util-from-markdown'),\n import('micromark-extension-mdx-jsx'),\n import('mdast-util-mdx-jsx'),\n import('micromark-extension-gfm'),\n import('mdast-util-gfm'),\n ]).then(\n ([fromMd, mdxJsxExt, mdxJsxMdast, gfmExt, gfmMdast]) =>\n ({\n fromMarkdown: (fromMd as unknown as { fromMarkdown: Deps['fromMarkdown'] }).fromMarkdown,\n mdxJsx: (mdxJsxExt as unknown as { mdxJsx: Deps['mdxJsx'] }).mdxJsx,\n mdxJsxFromMarkdown: (mdxJsxMdast as unknown as { mdxJsxFromMarkdown: Deps['mdxJsxFromMarkdown'] })\n .mdxJsxFromMarkdown,\n gfm: (gfmExt as unknown as { gfm: Deps['gfm'] }).gfm,\n gfmFromMarkdown: (gfmMdast as unknown as { gfmFromMarkdown: Deps['gfmFromMarkdown'] }).gfmFromMarkdown,\n }) as Deps,\n );\n }\n return depsPromise;\n}\n\n/**\n * Parse untrusted Markdown/MDX-syntax source to an mdast tree with **no evaluator in\n * the pipeline**: JSX-as-data (no acorn), GFM on, the expression extension OFF, no\n * raw-HTML re-parse. The returned tree is safe to hand to `renderMdast` — expression\n * attributes are inert strings and raw HTML is inert `html` nodes.\n */\nexport async function parseSafeMdast(source: string): Promise<SafeMdastNode> {\n const { fromMarkdown, mdxJsx, mdxJsxFromMarkdown, gfm, gfmFromMarkdown } = await loadDeps();\n return fromMarkdown(source, {\n // `mdxJsx()` WITHOUT an acorn option → JSX tags + literal attrs; expressions are\n // raw strings, never estree. The mdx *expression* extension is intentionally\n // absent, so `{…}` in body text stays literal. GFM for tables/task-lists.\n extensions: [mdxJsx(), gfm()],\n mdastExtensions: [mdxJsxFromMarkdown(), gfmFromMarkdown()],\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAwDA,IAAI,cAAoC;AAExC,SAAS,WAA0B;AACjC,MAAI,CAAC,aAAa;AAChB,kBAAc,QAAQ,IAAI;AAAA,MACxB,OAAO,0BAA0B;AAAA,MACjC,OAAO,6BAA6B;AAAA,MACpC,OAAO,oBAAoB;AAAA,MAC3B,OAAO,yBAAyB;AAAA,MAChC,OAAO,gBAAgB;AAAA,IACzB,CAAC,EAAE;AAAA,MACD,CAAC,CAAC,QAAQ,WAAW,aAAa,QAAQ,QAAQ,OAC/C;AAAA,QACC,cAAe,OAA6D;AAAA,QAC5E,QAAS,UAAoD;AAAA,QAC7D,oBAAqB,YAClB;AAAA,QACH,KAAM,OAA2C;AAAA,QACjD,iBAAkB,SAAqE;AAAA,MACzF;AAAA,IACJ;AAAA,EACF;AACA,SAAO;AACT;AAQA,eAAsB,eAAe,QAAwC;AAC3E,QAAM,EAAE,cAAc,QAAQ,oBAAoB,KAAK,gBAAgB,IAAI,MAAM,SAAS;AAC1F,SAAO,aAAa,QAAQ;AAAA;AAAA;AAAA;AAAA,IAI1B,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;AAAA,IAC5B,iBAAiB,CAAC,mBAAmB,GAAG,gBAAgB,CAAC;AAAA,EAC3D,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../../src/safeContent/parseSafeMdast.ts"],"sourcesContent":["// The no-acorn parse pipeline for the safe content renderer (TRUST_MODES_SPEC §5.1,\n// empirically verified 2026-07-07 against `@mdx-js/mdx` v3 / `micromark-extension-\n// mdx-jsx`). This is the load-bearing half of the fail-safe: `<Component/>` syntax\n// parses to an element node with **literal** attributes, and every expression\n// (`n={1+1}`, `f={fetch(\"/x\")}`, `{...spread}`) is captured as an **inert raw string\n// with no estree** — because we pass **NO acorn option** to `micromark-extension-mdx-\n// jsx`. There is therefore no evaluator anywhere in the pipeline (fail-safe), unlike\n// `@mdx-js/mdx`'s `compile({format:'mdx'})` which always wires acorn.\n//\n// The §5.1 residual guarantees are also here: the expression micromark extension is\n// **not** enabled (body `{…}` stays literal text), and **no `rehype-raw`** / raw-HTML\n// passthrough exists — raw HTML blocks arrive as inert `html` mdast nodes the renderer\n// prints as text, never injected.\n//\n// `micromark`/`mdast` are ESM-only. They are loaded with a memoised dynamic `import()`\n// (the transpiler's `compile.ts` pattern) so this module sits in the package's CJS\n// build too — a CJS consumer that never renders untrusted content never `require()`s\n// an ESM-only package, and one that does gets it via Node's ESM-from-CJS import. The\n// dynamic load changes only *when* the deps resolve, never the produced tree.\n//\n// The dynamic import targets `./mdastDeps` — a SINGLE self-contained artifact that a\n// dedicated build pass (`scripts/build-safecontent-deps.mjs`) inlines the whole ESM tree\n// into (see `mdastDeps.ts` for the WHY). This keeps the `import()` boundary (a consumer\n// that never renders safe content never loads the ~300KB parser) while making the imported\n// target one already-bundled file the immediately.run sandbox resolver can fetch without\n// walking the unified/micromark conditional-exports tree — the R3-213 on-host packaging\n// gap. In jest/ts-jest and the e2e bundle the same `./mdastDeps` resolves to the source\n// re-export module, which pulls the real deps from node_modules; the produced tree is\n// identical either way.\n\nimport {\n remarkAdmonitions,\n remarkWikiLinks,\n remarkHeadingAnchors,\n type HeadingAnchorOptions,\n} from '@immediately-run/mdx-plugins';\n\n// A minimal structural mdast shape — we deliberately avoid a type dependency on the\n// ESM-only `mdast`/`unist` packages (which would break the CJS build's type surface).\nexport interface SafeMdastNode {\n type: string;\n value?: string;\n name?: string | null;\n url?: string;\n alt?: string | null;\n title?: string | null;\n depth?: number;\n ordered?: boolean;\n lang?: string | null;\n children?: SafeMdastNode[];\n attributes?: SafeMdxAttribute[];\n [key: string]: unknown;\n}\n\nexport interface SafeMdxAttribute {\n type: 'mdxJsxAttribute' | 'mdxJsxExpressionAttribute';\n name?: string;\n // A literal attribute carries a string; an expression attribute carries an\n // `mdxJsxAttributeValueExpression` object (a raw string, NO estree) or a string.\n value?: string | { type: string; value?: string } | null;\n}\n\ntype Deps = {\n fromMarkdown: (\n value: string,\n options: { extensions: unknown[]; mdastExtensions: unknown[] },\n ) => SafeMdastNode;\n mdxJsx: () => unknown;\n mdxJsxFromMarkdown: () => unknown;\n gfm: () => unknown;\n gfmFromMarkdown: () => unknown;\n};\n\nlet depsPromise: Promise<Deps> | null = null;\n\nfunction loadDeps(): Promise<Deps> {\n if (!depsPromise) {\n // ONE dynamic import of the pre-inlined artifact (see the header note + `mdastDeps.ts`)\n // instead of five bare ESM specifiers the sandbox resolver can't walk on-host.\n depsPromise = import('./mdastDeps').then(\n (m) =>\n ({\n fromMarkdown: (m as unknown as { fromMarkdown: Deps['fromMarkdown'] }).fromMarkdown,\n mdxJsx: (m as unknown as { mdxJsx: Deps['mdxJsx'] }).mdxJsx,\n mdxJsxFromMarkdown: (m as unknown as { mdxJsxFromMarkdown: Deps['mdxJsxFromMarkdown'] })\n .mdxJsxFromMarkdown,\n gfm: (m as unknown as { gfm: Deps['gfm'] }).gfm,\n gfmFromMarkdown: (m as unknown as { gfmFromMarkdown: Deps['gfmFromMarkdown'] }).gfmFromMarkdown,\n }) as Deps,\n );\n }\n return depsPromise;\n}\n\nexport interface ParseSafeMdastOptions {\n /** `false` ⇒ headings use plain text-slug ids (the R3-186 base; no `sec-…` section\n * ids, no `data-slug`) — the `sectionIds:false` frontmatter opt-out, matching the\n * compiled path. Default (undefined/true) ⇒ section-like headings get `sec-…` ids\n * (R3-211). */\n sectionIds?: boolean;\n}\n\n// The three kernel remark plugins for the MDX *safe subset* — the SAME source the\n// compiled path (transpiler `compileMdx`) runs, imported from the shared package so the\n// two render standards can't drift (TRUST_MODES_SPEC §5 / §5.1, R3-213). They are pure\n// mdast transforms: `(options?) => (tree) => void`. `unified`'s `Plugin` type widens the\n// return to `Transformer | void`; here we invoke the transformer directly on the tree,\n// so narrow it to a plain in-place mutator.\ntype MdastMutator = (tree: SafeMdastNode) => void;\nconst admonitions = remarkAdmonitions as unknown as () => MdastMutator;\nconst wikiLinks = remarkWikiLinks as unknown as () => MdastMutator;\nconst headingAnchors = remarkHeadingAnchors as unknown as (o?: HeadingAnchorOptions) => MdastMutator;\n\n/**\n * Parse untrusted Markdown/MDX-syntax source to an mdast tree with **no evaluator in\n * the pipeline**: JSX-as-data (no acorn), GFM on, the expression extension OFF, no\n * raw-HTML re-parse. The returned tree is safe to hand to `renderMdast` — expression\n * attributes are inert strings and raw HTML is inert `html` nodes.\n *\n * After the no-acorn parse it runs the SHARED kernel remark plugins — in the SAME order\n * as the compiled path's `compile.ts` (admonitions §12 → wiki-links §13 → heading/section\n * anchors §15/R3-211; GFM is already applied via the micromark extension above) — so the\n * safe subset renders identically in both standards. The plugins only emit element nodes\n * with LITERAL attributes and set `data.hProperties.id` on headings; they add no evaluator\n * and no acorn, preserving the §5.1 fail-safe.\n */\nexport async function parseSafeMdast(\n source: string,\n options: ParseSafeMdastOptions = {},\n): Promise<SafeMdastNode> {\n const { fromMarkdown, mdxJsx, mdxJsxFromMarkdown, gfm, gfmFromMarkdown } = await loadDeps();\n const tree = fromMarkdown(source, {\n // `mdxJsx()` WITHOUT an acorn option → JSX tags + literal attrs; expressions are\n // raw strings, never estree. The mdx *expression* extension is intentionally\n // absent, so `{…}` in body text stays literal. GFM for tables/task-lists.\n extensions: [mdxJsx(), gfm()],\n mdastExtensions: [mdxJsxFromMarkdown(), gfmFromMarkdown()],\n });\n admonitions()(tree);\n wikiLinks()(tree);\n headingAnchors({ sectionIds: options.sectionIds !== false })(tree);\n return tree;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA8BA,yBAKO;AAsCP,IAAI,cAAoC;AAExC,SAAS,WAA0B;AACjC,MAAI,CAAC,aAAa;AAGhB,kBAAc,OAAO,aAAa,EAAE;AAAA,MAClC,CAAC,OACE;AAAA,QACC,cAAe,EAAwD;AAAA,QACvE,QAAS,EAA4C;AAAA,QACrD,oBAAqB,EAClB;AAAA,QACH,KAAM,EAAsC;AAAA,QAC5C,iBAAkB,EAA8D;AAAA,MAClF;AAAA,IACJ;AAAA,EACF;AACA,SAAO;AACT;AAiBA,MAAM,cAAc;AACpB,MAAM,YAAY;AAClB,MAAM,iBAAiB;AAevB,eAAsB,eACpB,QACA,UAAiC,CAAC,GACV;AACxB,QAAM,EAAE,cAAc,QAAQ,oBAAoB,KAAK,gBAAgB,IAAI,MAAM,SAAS;AAC1F,QAAM,OAAO,aAAa,QAAQ;AAAA;AAAA;AAAA;AAAA,IAIhC,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;AAAA,IAC5B,iBAAiB,CAAC,mBAAmB,GAAG,gBAAgB,CAAC;AAAA,EAC3D,CAAC;AACD,cAAY,EAAE,IAAI;AAClB,YAAU,EAAE,IAAI;AAChB,iBAAe,EAAE,YAAY,QAAQ,eAAe,MAAM,CAAC,EAAE,IAAI;AACjE,SAAO;AACT;","names":[]}
@@ -20,12 +20,26 @@ interface SafeMdxAttribute {
20
20
  value?: string;
21
21
  } | null;
22
22
  }
23
+ interface ParseSafeMdastOptions {
24
+ /** `false` ⇒ headings use plain text-slug ids (the R3-186 base; no `sec-…` section
25
+ * ids, no `data-slug`) — the `sectionIds:false` frontmatter opt-out, matching the
26
+ * compiled path. Default (undefined/true) ⇒ section-like headings get `sec-…` ids
27
+ * (R3-211). */
28
+ sectionIds?: boolean;
29
+ }
23
30
  /**
24
31
  * Parse untrusted Markdown/MDX-syntax source to an mdast tree with **no evaluator in
25
32
  * the pipeline**: JSX-as-data (no acorn), GFM on, the expression extension OFF, no
26
33
  * raw-HTML re-parse. The returned tree is safe to hand to `renderMdast` — expression
27
34
  * attributes are inert strings and raw HTML is inert `html` nodes.
35
+ *
36
+ * After the no-acorn parse it runs the SHARED kernel remark plugins — in the SAME order
37
+ * as the compiled path's `compile.ts` (admonitions §12 → wiki-links §13 → heading/section
38
+ * anchors §15/R3-211; GFM is already applied via the micromark extension above) — so the
39
+ * safe subset renders identically in both standards. The plugins only emit element nodes
40
+ * with LITERAL attributes and set `data.hProperties.id` on headings; they add no evaluator
41
+ * and no acorn, preserving the §5.1 fail-safe.
28
42
  */
29
- declare function parseSafeMdast(source: string): Promise<SafeMdastNode>;
43
+ declare function parseSafeMdast(source: string, options?: ParseSafeMdastOptions): Promise<SafeMdastNode>;
30
44
 
31
- export { type SafeMdastNode, type SafeMdxAttribute, parseSafeMdast };
45
+ export { type ParseSafeMdastOptions, type SafeMdastNode, type SafeMdxAttribute, parseSafeMdast };
@@ -20,12 +20,26 @@ interface SafeMdxAttribute {
20
20
  value?: string;
21
21
  } | null;
22
22
  }
23
+ interface ParseSafeMdastOptions {
24
+ /** `false` ⇒ headings use plain text-slug ids (the R3-186 base; no `sec-…` section
25
+ * ids, no `data-slug`) — the `sectionIds:false` frontmatter opt-out, matching the
26
+ * compiled path. Default (undefined/true) ⇒ section-like headings get `sec-…` ids
27
+ * (R3-211). */
28
+ sectionIds?: boolean;
29
+ }
23
30
  /**
24
31
  * Parse untrusted Markdown/MDX-syntax source to an mdast tree with **no evaluator in
25
32
  * the pipeline**: JSX-as-data (no acorn), GFM on, the expression extension OFF, no
26
33
  * raw-HTML re-parse. The returned tree is safe to hand to `renderMdast` — expression
27
34
  * attributes are inert strings and raw HTML is inert `html` nodes.
35
+ *
36
+ * After the no-acorn parse it runs the SHARED kernel remark plugins — in the SAME order
37
+ * as the compiled path's `compile.ts` (admonitions §12 → wiki-links §13 → heading/section
38
+ * anchors §15/R3-211; GFM is already applied via the micromark extension above) — so the
39
+ * safe subset renders identically in both standards. The plugins only emit element nodes
40
+ * with LITERAL attributes and set `data.hProperties.id` on headings; they add no evaluator
41
+ * and no acorn, preserving the §5.1 fail-safe.
28
42
  */
29
- declare function parseSafeMdast(source: string): Promise<SafeMdastNode>;
43
+ declare function parseSafeMdast(source: string, options?: ParseSafeMdastOptions): Promise<SafeMdastNode>;
30
44
 
31
- export { type SafeMdastNode, type SafeMdxAttribute, parseSafeMdast };
45
+ export { type ParseSafeMdastOptions, type SafeMdastNode, type SafeMdxAttribute, parseSafeMdast };
@@ -1,33 +1,39 @@
1
+ import {
2
+ remarkAdmonitions,
3
+ remarkWikiLinks,
4
+ remarkHeadingAnchors
5
+ } from "@immediately-run/mdx-plugins";
1
6
  let depsPromise = null;
2
7
  function loadDeps() {
3
8
  if (!depsPromise) {
4
- depsPromise = Promise.all([
5
- import("mdast-util-from-markdown"),
6
- import("micromark-extension-mdx-jsx"),
7
- import("mdast-util-mdx-jsx"),
8
- import("micromark-extension-gfm"),
9
- import("mdast-util-gfm")
10
- ]).then(
11
- ([fromMd, mdxJsxExt, mdxJsxMdast, gfmExt, gfmMdast]) => ({
12
- fromMarkdown: fromMd.fromMarkdown,
13
- mdxJsx: mdxJsxExt.mdxJsx,
14
- mdxJsxFromMarkdown: mdxJsxMdast.mdxJsxFromMarkdown,
15
- gfm: gfmExt.gfm,
16
- gfmFromMarkdown: gfmMdast.gfmFromMarkdown
9
+ depsPromise = import("./mdastDeps").then(
10
+ (m) => ({
11
+ fromMarkdown: m.fromMarkdown,
12
+ mdxJsx: m.mdxJsx,
13
+ mdxJsxFromMarkdown: m.mdxJsxFromMarkdown,
14
+ gfm: m.gfm,
15
+ gfmFromMarkdown: m.gfmFromMarkdown
17
16
  })
18
17
  );
19
18
  }
20
19
  return depsPromise;
21
20
  }
22
- async function parseSafeMdast(source) {
21
+ const admonitions = remarkAdmonitions;
22
+ const wikiLinks = remarkWikiLinks;
23
+ const headingAnchors = remarkHeadingAnchors;
24
+ async function parseSafeMdast(source, options = {}) {
23
25
  const { fromMarkdown, mdxJsx, mdxJsxFromMarkdown, gfm, gfmFromMarkdown } = await loadDeps();
24
- return fromMarkdown(source, {
26
+ const tree = fromMarkdown(source, {
25
27
  // `mdxJsx()` WITHOUT an acorn option → JSX tags + literal attrs; expressions are
26
28
  // raw strings, never estree. The mdx *expression* extension is intentionally
27
29
  // absent, so `{…}` in body text stays literal. GFM for tables/task-lists.
28
30
  extensions: [mdxJsx(), gfm()],
29
31
  mdastExtensions: [mdxJsxFromMarkdown(), gfmFromMarkdown()]
30
32
  });
33
+ admonitions()(tree);
34
+ wikiLinks()(tree);
35
+ headingAnchors({ sectionIds: options.sectionIds !== false })(tree);
36
+ return tree;
31
37
  }
32
38
  export {
33
39
  parseSafeMdast
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/safeContent/parseSafeMdast.ts"],"sourcesContent":["// The no-acorn parse pipeline for the safe content renderer (TRUST_MODES_SPEC §5.1,\n// empirically verified 2026-07-07 against `@mdx-js/mdx` v3 / `micromark-extension-\n// mdx-jsx`). This is the load-bearing half of the fail-safe: `<Component/>` syntax\n// parses to an element node with **literal** attributes, and every expression\n// (`n={1+1}`, `f={fetch(\"/x\")}`, `{...spread}`) is captured as an **inert raw string\n// with no estree** — because we pass **NO acorn option** to `micromark-extension-mdx-\n// jsx`. There is therefore no evaluator anywhere in the pipeline (fail-safe), unlike\n// `@mdx-js/mdx`'s `compile({format:'mdx'})` which always wires acorn.\n//\n// The §5.1 residual guarantees are also here: the expression micromark extension is\n// **not** enabled (body `{…}` stays literal text), and **no `rehype-raw`** / raw-HTML\n// passthrough exists — raw HTML blocks arrive as inert `html` mdast nodes the renderer\n// prints as text, never injected.\n//\n// `micromark`/`mdast` are ESM-only. They are loaded with a memoised dynamic `import()`\n// (the transpiler's `compile.ts` pattern) so this module sits in the package's CJS\n// build too — a CJS consumer that never renders untrusted content never `require()`s\n// an ESM-only package, and one that does gets it via Node's ESM-from-CJS import. The\n// dynamic load changes only *when* the deps resolve, never the produced tree.\n\n// A minimal structural mdast shape — we deliberately avoid a type dependency on the\n// ESM-only `mdast`/`unist` packages (which would break the CJS build's type surface).\nexport interface SafeMdastNode {\n type: string;\n value?: string;\n name?: string | null;\n url?: string;\n alt?: string | null;\n title?: string | null;\n depth?: number;\n ordered?: boolean;\n lang?: string | null;\n children?: SafeMdastNode[];\n attributes?: SafeMdxAttribute[];\n [key: string]: unknown;\n}\n\nexport interface SafeMdxAttribute {\n type: 'mdxJsxAttribute' | 'mdxJsxExpressionAttribute';\n name?: string;\n // A literal attribute carries a string; an expression attribute carries an\n // `mdxJsxAttributeValueExpression` object (a raw string, NO estree) or a string.\n value?: string | { type: string; value?: string } | null;\n}\n\ntype Deps = {\n fromMarkdown: (\n value: string,\n options: { extensions: unknown[]; mdastExtensions: unknown[] },\n ) => SafeMdastNode;\n mdxJsx: () => unknown;\n mdxJsxFromMarkdown: () => unknown;\n gfm: () => unknown;\n gfmFromMarkdown: () => unknown;\n};\n\nlet depsPromise: Promise<Deps> | null = null;\n\nfunction loadDeps(): Promise<Deps> {\n if (!depsPromise) {\n depsPromise = Promise.all([\n import('mdast-util-from-markdown'),\n import('micromark-extension-mdx-jsx'),\n import('mdast-util-mdx-jsx'),\n import('micromark-extension-gfm'),\n import('mdast-util-gfm'),\n ]).then(\n ([fromMd, mdxJsxExt, mdxJsxMdast, gfmExt, gfmMdast]) =>\n ({\n fromMarkdown: (fromMd as unknown as { fromMarkdown: Deps['fromMarkdown'] }).fromMarkdown,\n mdxJsx: (mdxJsxExt as unknown as { mdxJsx: Deps['mdxJsx'] }).mdxJsx,\n mdxJsxFromMarkdown: (mdxJsxMdast as unknown as { mdxJsxFromMarkdown: Deps['mdxJsxFromMarkdown'] })\n .mdxJsxFromMarkdown,\n gfm: (gfmExt as unknown as { gfm: Deps['gfm'] }).gfm,\n gfmFromMarkdown: (gfmMdast as unknown as { gfmFromMarkdown: Deps['gfmFromMarkdown'] }).gfmFromMarkdown,\n }) as Deps,\n );\n }\n return depsPromise;\n}\n\n/**\n * Parse untrusted Markdown/MDX-syntax source to an mdast tree with **no evaluator in\n * the pipeline**: JSX-as-data (no acorn), GFM on, the expression extension OFF, no\n * raw-HTML re-parse. The returned tree is safe to hand to `renderMdast` — expression\n * attributes are inert strings and raw HTML is inert `html` nodes.\n */\nexport async function parseSafeMdast(source: string): Promise<SafeMdastNode> {\n const { fromMarkdown, mdxJsx, mdxJsxFromMarkdown, gfm, gfmFromMarkdown } = await loadDeps();\n return fromMarkdown(source, {\n // `mdxJsx()` WITHOUT an acorn option → JSX tags + literal attrs; expressions are\n // raw strings, never estree. The mdx *expression* extension is intentionally\n // absent, so `{…}` in body text stays literal. GFM for tables/task-lists.\n extensions: [mdxJsx(), gfm()],\n mdastExtensions: [mdxJsxFromMarkdown(), gfmFromMarkdown()],\n });\n}\n"],"mappings":"AAwDA,IAAI,cAAoC;AAExC,SAAS,WAA0B;AACjC,MAAI,CAAC,aAAa;AAChB,kBAAc,QAAQ,IAAI;AAAA,MACxB,OAAO,0BAA0B;AAAA,MACjC,OAAO,6BAA6B;AAAA,MACpC,OAAO,oBAAoB;AAAA,MAC3B,OAAO,yBAAyB;AAAA,MAChC,OAAO,gBAAgB;AAAA,IACzB,CAAC,EAAE;AAAA,MACD,CAAC,CAAC,QAAQ,WAAW,aAAa,QAAQ,QAAQ,OAC/C;AAAA,QACC,cAAe,OAA6D;AAAA,QAC5E,QAAS,UAAoD;AAAA,QAC7D,oBAAqB,YAClB;AAAA,QACH,KAAM,OAA2C;AAAA,QACjD,iBAAkB,SAAqE;AAAA,MACzF;AAAA,IACJ;AAAA,EACF;AACA,SAAO;AACT;AAQA,eAAsB,eAAe,QAAwC;AAC3E,QAAM,EAAE,cAAc,QAAQ,oBAAoB,KAAK,gBAAgB,IAAI,MAAM,SAAS;AAC1F,SAAO,aAAa,QAAQ;AAAA;AAAA;AAAA;AAAA,IAI1B,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;AAAA,IAC5B,iBAAiB,CAAC,mBAAmB,GAAG,gBAAgB,CAAC;AAAA,EAC3D,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../../src/safeContent/parseSafeMdast.ts"],"sourcesContent":["// The no-acorn parse pipeline for the safe content renderer (TRUST_MODES_SPEC §5.1,\n// empirically verified 2026-07-07 against `@mdx-js/mdx` v3 / `micromark-extension-\n// mdx-jsx`). This is the load-bearing half of the fail-safe: `<Component/>` syntax\n// parses to an element node with **literal** attributes, and every expression\n// (`n={1+1}`, `f={fetch(\"/x\")}`, `{...spread}`) is captured as an **inert raw string\n// with no estree** — because we pass **NO acorn option** to `micromark-extension-mdx-\n// jsx`. There is therefore no evaluator anywhere in the pipeline (fail-safe), unlike\n// `@mdx-js/mdx`'s `compile({format:'mdx'})` which always wires acorn.\n//\n// The §5.1 residual guarantees are also here: the expression micromark extension is\n// **not** enabled (body `{…}` stays literal text), and **no `rehype-raw`** / raw-HTML\n// passthrough exists — raw HTML blocks arrive as inert `html` mdast nodes the renderer\n// prints as text, never injected.\n//\n// `micromark`/`mdast` are ESM-only. They are loaded with a memoised dynamic `import()`\n// (the transpiler's `compile.ts` pattern) so this module sits in the package's CJS\n// build too — a CJS consumer that never renders untrusted content never `require()`s\n// an ESM-only package, and one that does gets it via Node's ESM-from-CJS import. The\n// dynamic load changes only *when* the deps resolve, never the produced tree.\n//\n// The dynamic import targets `./mdastDeps` — a SINGLE self-contained artifact that a\n// dedicated build pass (`scripts/build-safecontent-deps.mjs`) inlines the whole ESM tree\n// into (see `mdastDeps.ts` for the WHY). This keeps the `import()` boundary (a consumer\n// that never renders safe content never loads the ~300KB parser) while making the imported\n// target one already-bundled file the immediately.run sandbox resolver can fetch without\n// walking the unified/micromark conditional-exports tree — the R3-213 on-host packaging\n// gap. In jest/ts-jest and the e2e bundle the same `./mdastDeps` resolves to the source\n// re-export module, which pulls the real deps from node_modules; the produced tree is\n// identical either way.\n\nimport {\n remarkAdmonitions,\n remarkWikiLinks,\n remarkHeadingAnchors,\n type HeadingAnchorOptions,\n} from '@immediately-run/mdx-plugins';\n\n// A minimal structural mdast shape — we deliberately avoid a type dependency on the\n// ESM-only `mdast`/`unist` packages (which would break the CJS build's type surface).\nexport interface SafeMdastNode {\n type: string;\n value?: string;\n name?: string | null;\n url?: string;\n alt?: string | null;\n title?: string | null;\n depth?: number;\n ordered?: boolean;\n lang?: string | null;\n children?: SafeMdastNode[];\n attributes?: SafeMdxAttribute[];\n [key: string]: unknown;\n}\n\nexport interface SafeMdxAttribute {\n type: 'mdxJsxAttribute' | 'mdxJsxExpressionAttribute';\n name?: string;\n // A literal attribute carries a string; an expression attribute carries an\n // `mdxJsxAttributeValueExpression` object (a raw string, NO estree) or a string.\n value?: string | { type: string; value?: string } | null;\n}\n\ntype Deps = {\n fromMarkdown: (\n value: string,\n options: { extensions: unknown[]; mdastExtensions: unknown[] },\n ) => SafeMdastNode;\n mdxJsx: () => unknown;\n mdxJsxFromMarkdown: () => unknown;\n gfm: () => unknown;\n gfmFromMarkdown: () => unknown;\n};\n\nlet depsPromise: Promise<Deps> | null = null;\n\nfunction loadDeps(): Promise<Deps> {\n if (!depsPromise) {\n // ONE dynamic import of the pre-inlined artifact (see the header note + `mdastDeps.ts`)\n // instead of five bare ESM specifiers the sandbox resolver can't walk on-host.\n depsPromise = import('./mdastDeps').then(\n (m) =>\n ({\n fromMarkdown: (m as unknown as { fromMarkdown: Deps['fromMarkdown'] }).fromMarkdown,\n mdxJsx: (m as unknown as { mdxJsx: Deps['mdxJsx'] }).mdxJsx,\n mdxJsxFromMarkdown: (m as unknown as { mdxJsxFromMarkdown: Deps['mdxJsxFromMarkdown'] })\n .mdxJsxFromMarkdown,\n gfm: (m as unknown as { gfm: Deps['gfm'] }).gfm,\n gfmFromMarkdown: (m as unknown as { gfmFromMarkdown: Deps['gfmFromMarkdown'] }).gfmFromMarkdown,\n }) as Deps,\n );\n }\n return depsPromise;\n}\n\nexport interface ParseSafeMdastOptions {\n /** `false` ⇒ headings use plain text-slug ids (the R3-186 base; no `sec-…` section\n * ids, no `data-slug`) — the `sectionIds:false` frontmatter opt-out, matching the\n * compiled path. Default (undefined/true) ⇒ section-like headings get `sec-…` ids\n * (R3-211). */\n sectionIds?: boolean;\n}\n\n// The three kernel remark plugins for the MDX *safe subset* — the SAME source the\n// compiled path (transpiler `compileMdx`) runs, imported from the shared package so the\n// two render standards can't drift (TRUST_MODES_SPEC §5 / §5.1, R3-213). They are pure\n// mdast transforms: `(options?) => (tree) => void`. `unified`'s `Plugin` type widens the\n// return to `Transformer | void`; here we invoke the transformer directly on the tree,\n// so narrow it to a plain in-place mutator.\ntype MdastMutator = (tree: SafeMdastNode) => void;\nconst admonitions = remarkAdmonitions as unknown as () => MdastMutator;\nconst wikiLinks = remarkWikiLinks as unknown as () => MdastMutator;\nconst headingAnchors = remarkHeadingAnchors as unknown as (o?: HeadingAnchorOptions) => MdastMutator;\n\n/**\n * Parse untrusted Markdown/MDX-syntax source to an mdast tree with **no evaluator in\n * the pipeline**: JSX-as-data (no acorn), GFM on, the expression extension OFF, no\n * raw-HTML re-parse. The returned tree is safe to hand to `renderMdast` — expression\n * attributes are inert strings and raw HTML is inert `html` nodes.\n *\n * After the no-acorn parse it runs the SHARED kernel remark plugins — in the SAME order\n * as the compiled path's `compile.ts` (admonitions §12 → wiki-links §13 → heading/section\n * anchors §15/R3-211; GFM is already applied via the micromark extension above) — so the\n * safe subset renders identically in both standards. The plugins only emit element nodes\n * with LITERAL attributes and set `data.hProperties.id` on headings; they add no evaluator\n * and no acorn, preserving the §5.1 fail-safe.\n */\nexport async function parseSafeMdast(\n source: string,\n options: ParseSafeMdastOptions = {},\n): Promise<SafeMdastNode> {\n const { fromMarkdown, mdxJsx, mdxJsxFromMarkdown, gfm, gfmFromMarkdown } = await loadDeps();\n const tree = fromMarkdown(source, {\n // `mdxJsx()` WITHOUT an acorn option → JSX tags + literal attrs; expressions are\n // raw strings, never estree. The mdx *expression* extension is intentionally\n // absent, so `{…}` in body text stays literal. GFM for tables/task-lists.\n extensions: [mdxJsx(), gfm()],\n mdastExtensions: [mdxJsxFromMarkdown(), gfmFromMarkdown()],\n });\n admonitions()(tree);\n wikiLinks()(tree);\n headingAnchors({ sectionIds: options.sectionIds !== false })(tree);\n return tree;\n}\n"],"mappings":"AA8BA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAsCP,IAAI,cAAoC;AAExC,SAAS,WAA0B;AACjC,MAAI,CAAC,aAAa;AAGhB,kBAAc,OAAO,aAAa,EAAE;AAAA,MAClC,CAAC,OACE;AAAA,QACC,cAAe,EAAwD;AAAA,QACvE,QAAS,EAA4C;AAAA,QACrD,oBAAqB,EAClB;AAAA,QACH,KAAM,EAAsC;AAAA,QAC5C,iBAAkB,EAA8D;AAAA,MAClF;AAAA,IACJ;AAAA,EACF;AACA,SAAO;AACT;AAiBA,MAAM,cAAc;AACpB,MAAM,YAAY;AAClB,MAAM,iBAAiB;AAevB,eAAsB,eACpB,QACA,UAAiC,CAAC,GACV;AACxB,QAAM,EAAE,cAAc,QAAQ,oBAAoB,KAAK,gBAAgB,IAAI,MAAM,SAAS;AAC1F,QAAM,OAAO,aAAa,QAAQ;AAAA;AAAA;AAAA;AAAA,IAIhC,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;AAAA,IAC5B,iBAAiB,CAAC,mBAAmB,GAAG,gBAAgB,CAAC;AAAA,EAC3D,CAAC;AACD,cAAY,EAAE,IAAI;AAClB,YAAU,EAAE,IAAI;AAChB,iBAAe,EAAE,YAAY,QAAQ,eAAe,MAAM,CAAC,EAAE,IAAI;AACjE,SAAO;AACT;","names":[]}
@@ -65,7 +65,11 @@ function renderNode(node, opts, index = 0) {
65
65
  return (0, import_react.createElement)("p", { key }, ...renderChildren(node, opts));
66
66
  case "heading": {
67
67
  const tag = HEADING_TAGS[Math.min(Math.max((node.depth ?? 1) - 1, 0), 5)];
68
- return (0, import_react.createElement)(tag, { key }, ...renderChildren(node, opts));
68
+ const hp = node.data?.hProperties;
69
+ const headingProps = {};
70
+ if (typeof hp?.id === "string") headingProps.id = hp.id;
71
+ if (typeof hp?.["data-slug"] === "string") headingProps["data-slug"] = hp["data-slug"];
72
+ return (0, import_react.createElement)(tag, { key, ...headingProps }, ...renderChildren(node, opts));
69
73
  }
70
74
  case "strong":
71
75
  return (0, import_react.createElement)("strong", { key }, ...renderChildren(node, opts));
@@ -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/** 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, resolve: RenderMdastOptions['resolveWikiLink']): ReactNode {\n const label = token.label ?? token.target;\n const href = resolve?.(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('a', { href: safe, 'data-wikilink': token.target, key: nextKey() }, label);\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.resolveWikiLink)),\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 return createElement(tag, { key }, ...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('a', { key, href, title: node.title ?? undefined }, ...renderChildren(node, opts));\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('img', { key, src, alt: node.alt ?? '', title: node.title ?? undefined });\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 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/** 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, resolve: RenderMdastOptions['resolveWikiLink']): ReactNode {\n const label = token.label ?? token.target;\n const href = resolve?.(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('a', { href: safe, 'data-wikilink': token.target, key: nextKey() }, label);\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.resolveWikiLink)),\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('a', { key, href, title: node.title ?? undefined }, ...renderChildren(node, opts));\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('img', { key, src, alt: node.alt ?? '', title: node.title ?? undefined });\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":[]}
@@ -42,7 +42,11 @@ function renderNode(node, opts, index = 0) {
42
42
  return createElement("p", { key }, ...renderChildren(node, opts));
43
43
  case "heading": {
44
44
  const tag = HEADING_TAGS[Math.min(Math.max((node.depth ?? 1) - 1, 0), 5)];
45
- return createElement(tag, { key }, ...renderChildren(node, opts));
45
+ const hp = node.data?.hProperties;
46
+ const headingProps = {};
47
+ if (typeof hp?.id === "string") headingProps.id = hp.id;
48
+ if (typeof hp?.["data-slug"] === "string") headingProps["data-slug"] = hp["data-slug"];
49
+ return createElement(tag, { key, ...headingProps }, ...renderChildren(node, opts));
46
50
  }
47
51
  case "strong":
48
52
  return createElement("strong", { key }, ...renderChildren(node, opts));
@@ -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/** 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, resolve: RenderMdastOptions['resolveWikiLink']): ReactNode {\n const label = token.label ?? token.target;\n const href = resolve?.(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('a', { href: safe, 'data-wikilink': token.target, key: nextKey() }, label);\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.resolveWikiLink)),\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 return createElement(tag, { key }, ...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('a', { key, href, title: node.title ?? undefined }, ...renderChildren(node, opts));\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('img', { key, src, alt: node.alt ?? '', title: node.title ?? undefined });\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":"AAAA,SAAS,eAAe,gBAAgC;AACxD,SAAS,mBAAmB;AAC5B,SAAS,sBAA0C;AA2CnD,SAAS,aAAa,YAAoE;AACxF,QAAM,QAAgC,CAAC;AACvC,aAAW,QAAQ,cAAc,CAAC,GAAG;AACnC,QAAI,KAAK,SAAS,kBAAmB;AACrC,QAAI,OAAO,KAAK,SAAS,SAAU;AAInC,QAAI,OAAO,KAAK,UAAU,SAAU,OAAM,KAAK,IAAI,IAAI,KAAK;AAAA,aACnD,KAAK,UAAU,QAAQ,KAAK,UAAU,OAAW,OAAM,KAAK,IAAI,IAAI;AAAA,EAE/E;AACA,SAAO;AACT;AAEA,IAAI,aAAa;AACjB,MAAM,UAAU,MAAM,MAAM,YAAY;AAIxC,SAAS,WAAW,OAAsB,SAA2D;AACnG,QAAM,QAAQ,MAAM,SAAS,MAAM;AACnC,QAAM,OAAO,UAAU,MAAM,MAAM;AACnC,QAAM,OAAO,SAAS,SAAY,YAAY,IAAI,IAAI;AACtD,MAAI,SAAS,OAAW,QAAO;AAC/B,SAAO,cAAc,KAAK,EAAE,MAAM,MAAM,iBAAiB,MAAM,QAAQ,KAAK,QAAQ,EAAE,GAAG,KAAK;AAChG;AAGA,SAAS,WAAW,OAAe,MAAqC;AACtE,QAAM,QAAQ,eAAe,KAAK;AAClC,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,MAAM;AAAA,IAAI,CAAC,GAAG,MACnB,UAAU,IACN,cAAc,UAAU,EAAE,KAAK,IAAI,CAAC,GAAG,GAAG,EAAE,IAAI,IAChD,cAAc,UAAU,EAAE,KAAK,IAAI,CAAC,GAAG,GAAG,WAAW,EAAE,MAAM,KAAK,eAAe,CAAC;AAAA,EACxF;AACF;AAEA,SAAS,eAAe,MAAqB,MAAuC;AAClF,UAAQ,KAAK,YAAY,CAAC,GAAG,IAAI,CAAC,GAAG,MAAM,WAAW,GAAG,MAAM,CAAC,CAAC;AACnE;AAIA,MAAM,eAAe,CAAC,MAAM,MAAM,MAAM,MAAM,MAAM,IAAI;AAExD,SAAS,WAAW,MAAqB,MAA0B,QAAQ,GAAc;AACvF,QAAM,MAAM,IAAI,KAAK;AACrB,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK;AACH,aAAO,cAAc,UAAU,EAAE,IAAI,GAAG,GAAG,eAAe,MAAM,IAAI,CAAC;AAAA,IACvE,KAAK;AACH,aAAO,WAAW,KAAK,SAAS,IAAI,IAAI;AAAA,IAC1C,KAAK;AACH,aAAO,cAAc,KAAK,EAAE,IAAI,GAAG,GAAG,eAAe,MAAM,IAAI,CAAC;AAAA,IAClE,KAAK,WAAW;AACd,YAAM,MAAM,aAAa,KAAK,IAAI,KAAK,KAAK,KAAK,SAAS,KAAK,GAAG,CAAC,GAAG,CAAC,CAAC;AACxE,aAAO,cAAc,KAAK,EAAE,IAAI,GAAG,GAAG,eAAe,MAAM,IAAI,CAAC;AAAA,IAClE;AAAA,IACA,KAAK;AACH,aAAO,cAAc,UAAU,EAAE,IAAI,GAAG,GAAG,eAAe,MAAM,IAAI,CAAC;AAAA,IACvE,KAAK;AACH,aAAO,cAAc,MAAM,EAAE,IAAI,GAAG,GAAG,eAAe,MAAM,IAAI,CAAC;AAAA,IACnE,KAAK;AACH,aAAO,cAAc,OAAO,EAAE,IAAI,GAAG,GAAG,eAAe,MAAM,IAAI,CAAC;AAAA,IACpE,KAAK;AACH,aAAO,cAAc,QAAQ,EAAE,IAAI,GAAG,KAAK,SAAS,EAAE;AAAA,IACxD,KAAK;AACH,aAAO,cAAc,OAAO,EAAE,IAAI,GAAG,cAAc,QAAQ,MAAM,KAAK,SAAS,EAAE,CAAC;AAAA,IACpF,KAAK;AACH,aAAO,cAAc,cAAc,EAAE,IAAI,GAAG,GAAG,eAAe,MAAM,IAAI,CAAC;AAAA,IAC3E,KAAK;AACH,aAAO,cAAc,KAAK,UAAU,OAAO,MAAM,EAAE,IAAI,GAAG,GAAG,eAAe,MAAM,IAAI,CAAC;AAAA,IACzF,KAAK;AACH,aAAO,cAAc,MAAM,EAAE,IAAI,GAAG,GAAG,eAAe,MAAM,IAAI,CAAC;AAAA,IACnE,KAAK;AACH,aAAO,cAAc,MAAM,EAAE,IAAI,CAAC;AAAA,IACpC,KAAK;AACH,aAAO,cAAc,MAAM,EAAE,IAAI,CAAC;AAAA,IACpC,KAAK,QAAQ;AACX,YAAM,OAAO,YAAY,KAAK,GAAG;AAEjC,UAAI,SAAS,OAAW,QAAO,cAAc,UAAU,EAAE,IAAI,GAAG,GAAG,eAAe,MAAM,IAAI,CAAC;AAC7F,aAAO,cAAc,KAAK,EAAE,KAAK,MAAM,OAAO,KAAK,SAAS,OAAU,GAAG,GAAG,eAAe,MAAM,IAAI,CAAC;AAAA,IACxG;AAAA,IACA,KAAK,SAAS;AACZ,YAAM,MAAM,YAAY,KAAK,GAAG;AAChC,UAAI,QAAQ,OAAW,QAAO,KAAK,MAAM,cAAc,UAAU,EAAE,IAAI,GAAG,KAAK,GAAG,IAAI;AACtF,aAAO,cAAc,OAAO,EAAE,KAAK,KAAK,KAAK,KAAK,OAAO,IAAI,OAAO,KAAK,SAAS,OAAU,CAAC;AAAA,IAC/F;AAAA;AAAA,IAEA,KAAK;AACH,aAAO,cAAc,SAAS,EAAE,IAAI,GAAG,cAAc,SAAS,MAAM,GAAG,eAAe,MAAM,IAAI,CAAC,CAAC;AAAA,IACpG,KAAK;AACH,aAAO,cAAc,MAAM,EAAE,IAAI,GAAG,GAAG,eAAe,MAAM,IAAI,CAAC;AAAA,IACnE,KAAK;AACH,aAAO,cAAc,MAAM,EAAE,IAAI,GAAG,GAAG,eAAe,MAAM,IAAI,CAAC;AAAA;AAAA;AAAA,IAGnE,KAAK;AACH,aAAO,cAAc,UAAU,EAAE,IAAI,GAAG,KAAK,SAAS,EAAE;AAAA;AAAA,IAE1D,KAAK;AAAA,IACL,KAAK,qBAAqB;AACxB,YAAM,OAAO,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO;AACzD,YAAM,YAAY,OAAO,KAAK,aAAa,IAAI,IAAI;AACnD,YAAM,WAAW,eAAe,MAAM,IAAI;AAI1C,UAAI,CAAC,UAAW,QAAO,cAAc,UAAU,EAAE,IAAI,GAAG,GAAG,QAAQ;AACnE,aAAO,cAAc,WAAW,EAAE,KAAK,GAAG,aAAa,KAAK,UAAU,EAAE,GAAG,GAAG,QAAQ;AAAA,IACxF;AAAA;AAAA;AAAA,IAGA,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT;AAEE,aAAO,KAAK,WAAW,cAAc,UAAU,EAAE,IAAI,GAAG,GAAG,eAAe,MAAM,IAAI,CAAC,IAAI;AAAA,EAC7F;AACF;AAOO,SAAS,YAAY,MAAqB,UAA8B,CAAC,GAAc;AAC5F,eAAa;AACb,SAAO,WAAW,MAAM,OAAO;AACjC;","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 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/** 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, resolve: RenderMdastOptions['resolveWikiLink']): ReactNode {\n const label = token.label ?? token.target;\n const href = resolve?.(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('a', { href: safe, 'data-wikilink': token.target, key: nextKey() }, label);\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.resolveWikiLink)),\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('a', { key, href, title: node.title ?? undefined }, ...renderChildren(node, opts));\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('img', { key, src, alt: node.alt ?? '', title: node.title ?? undefined });\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":[]}
package/dist/version.cjs CHANGED
@@ -21,7 +21,7 @@ __export(version_exports, {
21
21
  SDK_VERSION: () => SDK_VERSION
22
22
  });
23
23
  module.exports = __toCommonJS(version_exports);
24
- const SDK_VERSION = "0.32.0";
24
+ const SDK_VERSION = "0.33.0";
25
25
  // Annotate the CommonJS export names for ESM import in node:
26
26
  0 && (module.exports = {
27
27
  SDK_VERSION
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.32.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,MAAM,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.33.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,MAAM,cAAc;","names":[]}
@@ -1,4 +1,4 @@
1
1
  /** This SDK's package version, baked from package.json at build (SP2-6). */
2
- declare const SDK_VERSION = "0.32.0";
2
+ declare const SDK_VERSION = "0.33.0";
3
3
 
4
4
  export { SDK_VERSION };
package/dist/version.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /** This SDK's package version, baked from package.json at build (SP2-6). */
2
- declare const SDK_VERSION = "0.32.0";
2
+ declare const SDK_VERSION = "0.33.0";
3
3
 
4
4
  export { SDK_VERSION };
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
- const SDK_VERSION = "0.32.0";
1
+ const SDK_VERSION = "0.33.0";
2
2
  export {
3
3
  SDK_VERSION
4
4
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.32.0';\n"],"mappings":"AAIO,MAAM,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.33.0';\n"],"mappings":"AAIO,MAAM,cAAc;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immediately-run/sdk",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "Runtime SDK for code executing inside an immediately.run sandbox.",
5
5
  "license": "MIT",
6
6
  "repository": "github:immediately-run/immediately-run-sdk",
@@ -27,12 +27,12 @@
27
27
  "scripts": {
28
28
  "gen:version": "node scripts/gen-version.mjs",
29
29
  "prebuild": "node scripts/gen-version.mjs",
30
- "build": "tsup",
30
+ "build": "tsup && node scripts/build-safecontent-deps.mjs",
31
31
  "test": "jest",
32
32
  "check:circular": "node scripts/check-circular.mjs",
33
33
  "api:check": "node scripts/check-api-stability.mjs",
34
34
  "api:update": "node scripts/check-api-stability.mjs --update",
35
- "verify": "npm run check:circular && npm test && npm run test:safe-content && npm run build && npm run api:check",
35
+ "verify": "npm run check:circular && npm run build && npm test && npm run test:safe-content && npm run api:check",
36
36
  "docs": "typedoc --json docs/api.json && node scripts/gen-llms.mjs",
37
37
  "prepublishOnly": "npm run check:circular && npm run build && npm run api:check",
38
38
  "test:safe-content": "node scripts/build-safecontent-e2e.mjs && node --test test/safeContent.e2e.mjs"
@@ -42,15 +42,17 @@
42
42
  "react-dom": "^19.0.0"
43
43
  },
44
44
  "dependencies": {
45
+ "@immediately-run/mdx-plugins": "^0.1.0",
46
+ "react-error-boundary": "^6.0.0"
47
+ },
48
+ "devDependencies": {
49
+ "@testing-library/dom": "^10.4.1",
50
+ "esbuild": "^0.27.0",
45
51
  "mdast-util-from-markdown": "^2.0.3",
46
52
  "mdast-util-gfm": "^3.1.0",
47
53
  "mdast-util-mdx-jsx": "^3.2.0",
48
54
  "micromark-extension-gfm": "^3.0.0",
49
55
  "micromark-extension-mdx-jsx": "^3.0.2",
50
- "react-error-boundary": "^6.0.0"
51
- },
52
- "devDependencies": {
53
- "@testing-library/dom": "^10.4.1",
54
56
  "@testing-library/react": "^16.3.2",
55
57
  "@types/jest": "^29.5.0",
56
58
  "@types/react": "^19.0.0",