@legionworks/facet 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +23 -0
- package/README.md +158 -0
- package/dist/gallery/chunk-4g8rem85.css +656 -0
- package/dist/gallery/chunk-c6s9k9ty.js +5970 -0
- package/dist/gallery/frame/artifact.css +8424 -0
- package/dist/gallery/frame/chunks/abnfDiagram-N423BO3Z-jj74szbm.js +155 -0
- package/dist/gallery/frame/chunks/architecture-TIHT7OUA-cvdjn7dt.js +10 -0
- package/dist/gallery/frame/chunks/architectureDiagram-T3A2C74G-n4z77eg6.js +8675 -0
- package/dist/gallery/frame/chunks/blockDiagram-VBNYF7ZC-7xsqzrd1.js +3529 -0
- package/dist/gallery/frame/chunks/c4Diagram-5PPSVZJV-z2zjs7wx.js +2413 -0
- package/dist/gallery/frame/chunks/classDiagram-JCYQIIEL-ve8rngqp.js +47 -0
- package/dist/gallery/frame/chunks/classDiagram-v2-OCEON4UE-8cfaxb8z.js +47 -0
- package/dist/gallery/frame/chunks/cose-bilkent-JH36ORCC-sn92qexx.js +4822 -0
- package/dist/gallery/frame/chunks/cynefin-VYW2F7L2-tqjv913d.js +10 -0
- package/dist/gallery/frame/chunks/cynefinDiagram-MW4NZA55-c1c0dj12.js +500 -0
- package/dist/gallery/frame/chunks/dagre-VZM6K2ZE-7tyg4yt2.js +476 -0
- package/dist/gallery/frame/chunks/diagram-7IWD3JNH-y4eng9sc.js +526 -0
- package/dist/gallery/frame/chunks/diagram-B4RE2ZJO-rn3p7j5h.js +673 -0
- package/dist/gallery/frame/chunks/diagram-LBJQPF4R-w4a2cm9q.js +255 -0
- package/dist/gallery/frame/chunks/diagram-Q27KOJAE-aqc2v7wy.js +575 -0
- package/dist/gallery/frame/chunks/diagram-UB23O5K3-xncy8nyd.js +355 -0
- package/dist/gallery/frame/chunks/ebnfDiagram-BXEA7PRR-acwgxz16.js +175 -0
- package/dist/gallery/frame/chunks/erDiagram-JOGREHBK-mzp5nswt.js +1332 -0
- package/dist/gallery/frame/chunks/eventmodeling-45OFAUF4-sg07qr0c.js +10 -0
- package/dist/gallery/frame/chunks/flowDiagram-UKHOOZJN-2e5ygs44.js +29 -0
- package/dist/gallery/frame/chunks/ganttDiagram-PKOTCBZU-p2hhh7hr.js +2620 -0
- package/dist/gallery/frame/chunks/gitGraph-TEB2WS4Q-j6kw0naa.js +10 -0
- package/dist/gallery/frame/chunks/gitGraphDiagram-DS77QQ5N-4z9z8wjm.js +1347 -0
- package/dist/gallery/frame/chunks/info-DKCQHKI2-mpt80jd3.js +10 -0
- package/dist/gallery/frame/chunks/infoDiagram-6WML65LV-5rqm3mgg.js +65 -0
- package/dist/gallery/frame/chunks/ishikawaDiagram-WSZJBQD7-b51z96jy.js +975 -0
- package/dist/gallery/frame/chunks/journeyDiagram-NVQOT4AX-9jbfp0pk.js +1272 -0
- package/dist/gallery/frame/chunks/kanban-definition-27J2QSJJ-4x0vx4se.js +1116 -0
- package/dist/gallery/frame/chunks/katex-xfgk95tv.js +14034 -0
- package/dist/gallery/frame/chunks/markdown-1s5vwkf2.js +9 -0
- package/dist/gallery/frame/chunks/markdown-2h6bwsms.js +3239 -0
- package/dist/gallery/frame/chunks/markdown-2x113yh8.js +107 -0
- package/dist/gallery/frame/chunks/markdown-30b7rwmp.js +1943 -0
- package/dist/gallery/frame/chunks/markdown-356bfj4g.js +2031 -0
- package/dist/gallery/frame/chunks/markdown-44n9ea69.js +2406 -0
- package/dist/gallery/frame/chunks/markdown-56enrf2r.js +39 -0
- package/dist/gallery/frame/chunks/markdown-5avymm8t.js +725 -0
- package/dist/gallery/frame/chunks/markdown-6r4svars.js +33 -0
- package/dist/gallery/frame/chunks/markdown-6zz8efx2.js +24 -0
- package/dist/gallery/frame/chunks/markdown-7jp6prnb.js +19 -0
- package/dist/gallery/frame/chunks/markdown-809k7q41.js +55 -0
- package/dist/gallery/frame/chunks/markdown-82xmzhzd.js +1076 -0
- package/dist/gallery/frame/chunks/markdown-92n37gd4.js +7627 -0
- package/dist/gallery/frame/chunks/markdown-ag2sssf8.js +36 -0
- package/dist/gallery/frame/chunks/markdown-b4s7ntq2.js +19 -0
- package/dist/gallery/frame/chunks/markdown-c9k0fvp1.js +100 -0
- package/dist/gallery/frame/chunks/markdown-ceefymn8.js +125 -0
- package/dist/gallery/frame/chunks/markdown-d1wfbs70.js +243 -0
- package/dist/gallery/frame/chunks/markdown-dy5sgjhc.js +11022 -0
- package/dist/gallery/frame/chunks/markdown-dz3z5w30.js +30868 -0
- package/dist/gallery/frame/chunks/markdown-eg6ngz7y.js +30340 -0
- package/dist/gallery/frame/chunks/markdown-ff5wvb43.js +1149 -0
- package/dist/gallery/frame/chunks/markdown-ffg06nfz.js +86 -0
- package/dist/gallery/frame/chunks/markdown-fvfncadf.js +472 -0
- package/dist/gallery/frame/chunks/markdown-gvyszqpv.js +19 -0
- package/dist/gallery/frame/chunks/markdown-gx1p0ew0.js +88 -0
- package/dist/gallery/frame/chunks/markdown-gxye5frz.js +65 -0
- package/dist/gallery/frame/chunks/markdown-h7pzv34e.js +105 -0
- package/dist/gallery/frame/chunks/markdown-hab789t9.js +47 -0
- package/dist/gallery/frame/chunks/markdown-hjp6sj8t.js +125 -0
- package/dist/gallery/frame/chunks/markdown-jka64qgp.js +1993 -0
- package/dist/gallery/frame/chunks/markdown-k8xkn8mw.js +56 -0
- package/dist/gallery/frame/chunks/markdown-m7frq8x8.js +112 -0
- package/dist/gallery/frame/chunks/markdown-p740v4qm.js +36 -0
- package/dist/gallery/frame/chunks/markdown-pxfeh231.js +57 -0
- package/dist/gallery/frame/chunks/markdown-pzgd8z3s.js +87 -0
- package/dist/gallery/frame/chunks/markdown-qx3cngz2.js +1673 -0
- package/dist/gallery/frame/chunks/markdown-rawhfvvc.js +85 -0
- package/dist/gallery/frame/chunks/markdown-rd8w0ng9.js +49 -0
- package/dist/gallery/frame/chunks/markdown-rm2yxfjj.js +2773 -0
- package/dist/gallery/frame/chunks/markdown-s1jatrnk.js +993 -0
- package/dist/gallery/frame/chunks/markdown-sgfg5vvq.js +59 -0
- package/dist/gallery/frame/chunks/markdown-t4ethv9d.js +409 -0
- package/dist/gallery/frame/chunks/markdown-tnmabqvz.js +36 -0
- package/dist/gallery/frame/chunks/markdown-tvdpppw8.js +5292 -0
- package/dist/gallery/frame/chunks/markdown-v0x27twr.js +22 -0
- package/dist/gallery/frame/chunks/markdown-v14td68x.js +36 -0
- package/dist/gallery/frame/chunks/markdown-w6sgd54z.js +36 -0
- package/dist/gallery/frame/chunks/markdown-xw8dsk8b.js +2125 -0
- package/dist/gallery/frame/chunks/markdown-y18mve73.js +85 -0
- package/dist/gallery/frame/chunks/markdown-yengsms5.js +427 -0
- package/dist/gallery/frame/chunks/markdown-z1wc0v0d.js +1826 -0
- package/dist/gallery/frame/chunks/markdown-zazx9h4n.js +329 -0
- package/dist/gallery/frame/chunks/mermaid-de7p4v0x.js +30 -0
- package/dist/gallery/frame/chunks/mindmap-definition-FAOFIHXS-ra44cbem.js +1216 -0
- package/dist/gallery/frame/chunks/packet-7NZHBO7P-xppm5913.js +10 -0
- package/dist/gallery/frame/chunks/pegDiagram-VL7TDLO6-a2a4wg03.js +163 -0
- package/dist/gallery/frame/chunks/pie-RZYD4A2V-hnbxk152.js +10 -0
- package/dist/gallery/frame/chunks/pieDiagram-7S7Q4E2Y-f1vxjtmx.js +311 -0
- package/dist/gallery/frame/chunks/quadrantDiagram-CIZ2JOQS-ywafa6kh.js +1390 -0
- package/dist/gallery/frame/chunks/radar-I7S5WNFK-ewqd27se.js +10 -0
- package/dist/gallery/frame/chunks/railroad-3IZDKUUU-5hy4bdx3.js +10 -0
- package/dist/gallery/frame/chunks/railroad-abnf-AHOZXSZD-bh5sxz0d.js +10 -0
- package/dist/gallery/frame/chunks/railroad-ebnf-EBAXGLYW-6b9c3zeh.js +10 -0
- package/dist/gallery/frame/chunks/railroad-peg-LSFZ7HO6-avc8v39s.js +10 -0
- package/dist/gallery/frame/chunks/railroadDiagram-AXF67PYL-qc61wax4.js +131 -0
- package/dist/gallery/frame/chunks/requirementDiagram-LRYGKXZP-9bs4786c.js +1295 -0
- package/dist/gallery/frame/chunks/sankeyDiagram-W5VNT64P-awt1em8b.js +1339 -0
- package/dist/gallery/frame/chunks/sequenceDiagram-SI44F4Z6-qaf9mz3t.js +4324 -0
- package/dist/gallery/frame/chunks/sizeCapture-X5ZJPWSS-9fq685xd.js +64 -0
- package/dist/gallery/frame/chunks/stateDiagram-OKZ733FA-vx7a7hh0.js +456 -0
- package/dist/gallery/frame/chunks/stateDiagram-v2-UEYNNEHI-x4m6ws5t.js +46 -0
- package/dist/gallery/frame/chunks/swimlanes-SLNWSIFB-e4mxm1gh.js +8287 -0
- package/dist/gallery/frame/chunks/swimlanesDiagram-ULZ7WXOC-0h93e2qq.js +42 -0
- package/dist/gallery/frame/chunks/timeline-definition-Z64GVDOM-wp0bhbzg.js +1549 -0
- package/dist/gallery/frame/chunks/treeView-QDETBFTQ-hcx7n0vt.js +10 -0
- package/dist/gallery/frame/chunks/treemap-6X3UGDF4-av02skdw.js +10 -0
- package/dist/gallery/frame/chunks/vennDiagram-T6HMQDX7-pmvk1p7n.js +2566 -0
- package/dist/gallery/frame/chunks/wardley-OPB4EBWU-njsp6e40.js +10 -0
- package/dist/gallery/frame/chunks/wardleyDiagram-T6FBY63Y-eggmnv4z.js +979 -0
- package/dist/gallery/frame/chunks/xychartDiagram-ELKLHX3M-q40tnpe5.js +1954 -0
- package/dist/gallery/frame/frame.css +183 -0
- package/dist/gallery/frame/runtime/chart.js +42744 -0
- package/dist/gallery/frame/runtime/html.js +14 -0
- package/dist/gallery/frame/runtime/markdown.js +1403 -0
- package/dist/gallery/frame/runtime/mermaid.js +32 -0
- package/dist/gallery/frame/runtime/svg.js +14 -0
- package/dist/gallery/frame/runtime/tsx.js +52 -0
- package/dist/gallery/index.html +104 -0
- package/docs/reference/cli.md +163 -0
- package/docs/reference/export.md +106 -0
- package/docs/reference/html.md +215 -0
- package/docs/reference/http.md +85 -0
- package/docs/reference/mcp.md +78 -0
- package/docs/reference/security.md +119 -0
- package/docs/reference/storage.md +93 -0
- package/docs/reference/tsx.md +101 -0
- package/docs/reference/validation.md +212 -0
- package/package.json +89 -0
- package/scripts/build-gallery.ts +127 -0
- package/scripts/launch-netns.sh +67 -0
- package/skills/facet/SKILL.md +100 -0
- package/src/cli/client.ts +311 -0
- package/src/cli/commands/create.ts +49 -0
- package/src/cli/commands/doctor.ts +249 -0
- package/src/cli/commands/export.ts +179 -0
- package/src/cli/commands/instantiate.ts +35 -0
- package/src/cli/commands/list.ts +49 -0
- package/src/cli/commands/open.ts +75 -0
- package/src/cli/commands/pin.ts +39 -0
- package/src/cli/commands/promote.ts +49 -0
- package/src/cli/commands/publish.ts +122 -0
- package/src/cli/commands/read-back.ts +56 -0
- package/src/cli/commands/status.ts +114 -0
- package/src/cli/commands/watch.ts +146 -0
- package/src/cli/main.ts +522 -0
- package/src/cli/output.ts +106 -0
- package/src/cli/parser.ts +376 -0
- package/src/cli/presenter.ts +212 -0
- package/src/cli/service-metadata.ts +134 -0
- package/src/cli/spawn-service.ts +326 -0
- package/src/css.d.ts +4 -0
- package/src/gallery-web/app.ts +1581 -0
- package/src/gallery-web/export-menu.ts +121 -0
- package/src/gallery-web/export.ts +112 -0
- package/src/gallery-web/favicon.ts +54 -0
- package/src/gallery-web/frame/entries/chart.ts +6 -0
- package/src/gallery-web/frame/entries/html.ts +6 -0
- package/src/gallery-web/frame/entries/markdown.ts +6 -0
- package/src/gallery-web/frame/entries/mermaid.ts +6 -0
- package/src/gallery-web/frame/entries/svg.ts +6 -0
- package/src/gallery-web/frame/entries/tsx.ts +6 -0
- package/src/gallery-web/frame/frame-payload.ts +91 -0
- package/src/gallery-web/frame/renderer-validation.ts +13 -0
- package/src/gallery-web/frame/renderers/chart.ts +182 -0
- package/src/gallery-web/frame/renderers/dompurify-shim.ts +112 -0
- package/src/gallery-web/frame/renderers/html.ts +48 -0
- package/src/gallery-web/frame/renderers/markdown.ts +81 -0
- package/src/gallery-web/frame/renderers/mermaid.ts +120 -0
- package/src/gallery-web/frame/renderers/registry.ts +260 -0
- package/src/gallery-web/frame/renderers/svg.ts +393 -0
- package/src/gallery-web/frame/renderers/tsx.ts +61 -0
- package/src/gallery-web/frame/runtime.ts +501 -0
- package/src/gallery-web/frame/styles/artifact.css +8424 -0
- package/src/gallery-web/frame/styles/frame.css +183 -0
- package/src/gallery-web/frame/styles/html-source.css +23 -0
- package/src/gallery-web/frame/view-box.ts +33 -0
- package/src/gallery-web/frame-html.ts +72 -0
- package/src/gallery-web/index.html +104 -0
- package/src/gallery-web/session.ts +107 -0
- package/src/gallery-web/sse-client.ts +212 -0
- package/src/gallery-web/styles/app.css +420 -0
- package/src/gallery-web/styles/tokens.css +112 -0
- package/src/gallery-web/styles/verdict.css +186 -0
- package/src/gallery-web/swap.ts +64 -0
- package/src/gallery-web/theme.ts +40 -0
- package/src/gallery-web/view-state.ts +93 -0
- package/src/harness-adapters/claude-code/README.md +10 -0
- package/src/harness-adapters/claude-code/facet.sh +4 -0
- package/src/harness-adapters/codex/README.md +10 -0
- package/src/harness-adapters/codex/facet.sh +4 -0
- package/src/harness-adapters/mcp/README.md +13 -0
- package/src/harness-adapters/mcp/cli-bridge.ts +216 -0
- package/src/harness-adapters/mcp/main.ts +15 -0
- package/src/harness-adapters/mcp/server.ts +164 -0
- package/src/harness-adapters/mcp/tool-schemas.ts +53 -0
- package/src/harness-adapters/opencode/README.md +10 -0
- package/src/harness-adapters/opencode/facet.sh +4 -0
- package/src/runtime/compiled-entrypoints.ts +69 -0
- package/src/service/dispatcher.ts +717 -0
- package/src/service/export.ts +218 -0
- package/src/service/http-utils.ts +55 -0
- package/src/service/lexical/expectations.ts +210 -0
- package/src/service/lifecycle/idle-controller.ts +125 -0
- package/src/service/lifecycle/orphan-cleanup.ts +128 -0
- package/src/service/lifecycle/process-lock.ts +235 -0
- package/src/service/main.ts +136 -0
- package/src/service/process.ts +240 -0
- package/src/service/router-guards.ts +239 -0
- package/src/service/router.ts +627 -0
- package/src/service/security/auth.ts +168 -0
- package/src/service/security/host-origin.ts +138 -0
- package/src/service/security/http-guards.ts +13 -0
- package/src/service/security/leases.ts +182 -0
- package/src/service/security/token-store.ts +138 -0
- package/src/service/server.ts +356 -0
- package/src/service/store/database.ts +61 -0
- package/src/service/store/evidence-retention.ts +234 -0
- package/src/service/store/migrations.ts +167 -0
- package/src/service/store/repository-lifecycle.ts +121 -0
- package/src/service/store/repository.ts +732 -0
- package/src/service/store/schema.ts +218 -0
- package/src/service/stored-verdict.ts +97 -0
- package/src/service/stream.ts +301 -0
- package/src/service/verdict-enrichment.ts +45 -0
- package/src/shared/build/frame-bundle-plugins.ts +61 -0
- package/src/shared/build-mode.ts +28 -0
- package/src/shared/config/evidence-read.ts +98 -0
- package/src/shared/config/limits.ts +56 -0
- package/src/shared/config/paths.ts +96 -0
- package/src/shared/contracts/artifact-types.ts +2 -0
- package/src/shared/contracts/artifact.ts +98 -0
- package/src/shared/contracts/commands/_shared.ts +59 -0
- package/src/shared/contracts/commands/guards.ts +86 -0
- package/src/shared/contracts/commands/index.ts +204 -0
- package/src/shared/contracts/commands/names.ts +35 -0
- package/src/shared/contracts/commands/requests.ts +149 -0
- package/src/shared/contracts/commands/results.ts +200 -0
- package/src/shared/contracts/envelope.ts +140 -0
- package/src/shared/contracts/events.ts +41 -0
- package/src/shared/contracts/observed-counts.ts +19 -0
- package/src/shared/contracts/renderers.ts +6 -0
- package/src/shared/contracts/validation.ts +296 -0
- package/src/shared/errors/facet-error.ts +127 -0
- package/src/shared/errors/store-error.ts +68 -0
- package/src/shared/evidence-image.ts +37 -0
- package/src/shared/export.ts +31 -0
- package/src/shared/html/policy.ts +108 -0
- package/src/shared/html/style-vocabulary.ts +352 -0
- package/src/shared/logging/logger.ts +108 -0
- package/src/shared/logging/redact.ts +49 -0
- package/src/shared/security/frozen-csp.ts +13 -0
- package/src/shared/storage-version.ts +2 -0
- package/src/shared/tsx/execution.ts +29 -0
- package/src/shared/tsx/import-policy.ts +94 -0
- package/src/shared/util/dir-permissions.ts +94 -0
- package/src/shared/util/mermaid-nodes.ts +230 -0
- package/src/shared/util/process.ts +67 -0
- package/src/shared/util/time.ts +21 -0
- package/src/shared/version.ts +3 -0
- package/src/validation/sandbox/limits.ts +41 -0
- package/src/validation/sandbox/netns.ts +177 -0
- package/src/validation/tier0/chart.ts +177 -0
- package/src/validation/tier0/dom-shim.ts +54 -0
- package/src/validation/tier0/html.ts +408 -0
- package/src/validation/tier0/markdown.ts +285 -0
- package/src/validation/tier0/mermaid.ts +98 -0
- package/src/validation/tier0/runner.ts +504 -0
- package/src/validation/tier0/svg.ts +321 -0
- package/src/validation/tier0/tsx/allowlist-resolver.ts +77 -0
- package/src/validation/tier0/tsx/ast-policy-capabilities.ts +238 -0
- package/src/validation/tier0/tsx/ast-policy-shared.ts +71 -0
- package/src/validation/tier0/tsx/ast-policy.ts +181 -0
- package/src/validation/tier0/tsx/compiler.ts +156 -0
- package/src/validation/tier0/worker-dispatch.ts +211 -0
- package/src/validation/tier0/worker-entry.ts +96 -0
- package/src/validation/tier0/worker-input.ts +75 -0
- package/src/validation/tier1/browser-process.ts +147 -0
- package/src/validation/tier1/cdp-pipe.ts +281 -0
- package/src/validation/tier1/entries/chart.ts +5 -0
- package/src/validation/tier1/entries/html.ts +6 -0
- package/src/validation/tier1/entries/markdown.ts +5 -0
- package/src/validation/tier1/entries/mermaid.ts +5 -0
- package/src/validation/tier1/entries/svg.ts +5 -0
- package/src/validation/tier1/entries/tsx.ts +5 -0
- package/src/validation/tier1/frame-target.ts +162 -0
- package/src/validation/tier1/harness-entry.ts +261 -0
- package/src/validation/tier1/harness.ts +215 -0
- package/src/validation/tier1/isolated-probe.ts +94 -0
- package/src/validation/tier1/launcher.ts +177 -0
- package/src/validation/tier1/limits.ts +80 -0
- package/src/validation/tier1/nonce.ts +17 -0
- package/src/validation/tier1/protocol-probe.ts +519 -0
- package/src/validation/tier1/runner.ts +1122 -0
- package/src/validation/tier1/verdict.ts +246 -0
- package/src/validation/tier1/webp.ts +69 -0
- package/templates/README.md +88 -0
- package/templates/bar-compare.vl.json +63 -0
- package/templates/capacity-report.tsx +102 -0
- package/templates/decision-record.md +24 -0
- package/templates/deployment-state.mmd +40 -0
- package/templates/exemplar.md +91 -0
- package/templates/facet-story.tsx +799 -0
- package/templates/fleet-dashboard.html +141 -0
- package/templates/html-release-ledger.html +91 -0
- package/templates/html-status-report.html +114 -0
- package/templates/incident-console.tsx +147 -0
- package/templates/legion-boundaries.mmd +23 -0
- package/templates/legion-flow.mmd +15 -0
- package/templates/legion-sequence.mmd +14 -0
- package/templates/legion-state.mmd +7 -0
- package/templates/metric-card.svg +29 -0
- package/templates/observability-map.svg +101 -0
- package/templates/pipeline-audit.md +108 -0
- package/templates/release-metrics.vl.json +98 -0
- package/templates/service-topology.mmd +47 -0
- package/templates/status-report.md +32 -0
- package/templates/system-map.svg +53 -0
- package/templates/timeseries.vl.json +71 -0
- package/templates/tsx-interactive-counter.tsx +19 -0
- package/templates/tsx-status-report.tsx +31 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOMPurify shim for bundled frame contexts.
|
|
3
|
+
*
|
|
4
|
+
* DOMPurify captures `document` methods at instance-construction time
|
|
5
|
+
* (`createNodeIterator`, `createTreeWalker`, `getElementsByTagName`,
|
|
6
|
+
* `importNode`, `createDocumentFragment`) and calls them with `this`
|
|
7
|
+
* detached from the document. Standalone this survives; inside the
|
|
8
|
+
* mermaid/d3/katex frame bundle the detached calls surface as
|
|
9
|
+
* `TypeError: Illegal invocation` on the first HTML-label sanitize
|
|
10
|
+
* (plain-text labels skip the NodeIterator path).
|
|
11
|
+
*
|
|
12
|
+
* Fix: DOMPurify's factory form builds a fresh instance against an
|
|
13
|
+
* explicit window, and the frame's document carries the captured
|
|
14
|
+
* methods as OWN properties pre-bound to the real document — the own
|
|
15
|
+
* property wins over the prototype at capture time, so every detached
|
|
16
|
+
* `.call(...)` lands on the right `this`. DOMPurify also reassigns its
|
|
17
|
+
* internal document to `template.content.ownerDocument` (a DIFFERENT
|
|
18
|
+
* Document object) when HTMLTemplateElement exists, so that document
|
|
19
|
+
* gets the bound methods too. The import-time default instance is
|
|
20
|
+
* never trusted in the verifier's inlined harness bundle.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import purifyFactory from "dompurify";
|
|
24
|
+
|
|
25
|
+
type PurifyInstance = typeof purifyFactory & { isSupported?: boolean };
|
|
26
|
+
|
|
27
|
+
/** Document methods DOMPurify captures and calls unbound. */
|
|
28
|
+
const DOCUMENT_METHODS = [
|
|
29
|
+
"createNodeIterator",
|
|
30
|
+
"createTreeWalker",
|
|
31
|
+
"getElementsByTagName",
|
|
32
|
+
"getElementsByTagNameNS",
|
|
33
|
+
"importNode",
|
|
34
|
+
"createDocumentFragment",
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Shadow DOMPurify's captured methods with pre-bound OWN properties on
|
|
39
|
+
* the given document. Behavior is identical for all other callers — a
|
|
40
|
+
* bound copy of the same method.
|
|
41
|
+
*/
|
|
42
|
+
function bindDocumentMethods(realDocument: Document): void {
|
|
43
|
+
for (const name of DOCUMENT_METHODS) {
|
|
44
|
+
const existing = Object.getOwnPropertyDescriptor(realDocument, name);
|
|
45
|
+
if (existing !== undefined) continue;
|
|
46
|
+
const method = (realDocument as unknown as Record<string, unknown>)[name];
|
|
47
|
+
if (typeof method === "function") {
|
|
48
|
+
const fn = method as (...args: unknown[]) => unknown;
|
|
49
|
+
Object.defineProperty(realDocument, name, {
|
|
50
|
+
// `this`-AWARE, never hard-bound. DOMPurify parses input into a
|
|
51
|
+
// FRESH document and invokes these against it; a copy bound to
|
|
52
|
+
// `realDocument` would query the wrong document, find nothing,
|
|
53
|
+
// and silently return empty output (every mermaid label came
|
|
54
|
+
// back blank). Honour the call-time receiver and fall back to
|
|
55
|
+
// the real document only for the detached calls that produced
|
|
56
|
+
// `TypeError: Illegal invocation` in the bundled frame.
|
|
57
|
+
value: function shadowed(this: unknown, ...args: unknown[]): unknown {
|
|
58
|
+
const receiver =
|
|
59
|
+
this === undefined || this === null || this === globalThis ? realDocument : this;
|
|
60
|
+
return fn.apply(receiver, args);
|
|
61
|
+
},
|
|
62
|
+
configurable: true,
|
|
63
|
+
writable: true,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function ensureBoundDocumentMethods(realWindow: Window & typeof globalThis): boolean {
|
|
70
|
+
const realDocument = realWindow.document;
|
|
71
|
+
if (realDocument === undefined) return false;
|
|
72
|
+
bindDocumentMethods(realDocument);
|
|
73
|
+
// DOMPurify REASSIGNS its internal document to
|
|
74
|
+
// `template.content.ownerDocument` when HTMLTemplateElement exists —
|
|
75
|
+
// a DIFFERENT Document object (the template's inert document). That
|
|
76
|
+
// one must carry the bound methods too, or the HTML-label path
|
|
77
|
+
// still hits the unbound capture.
|
|
78
|
+
try {
|
|
79
|
+
const probeTemplate = realDocument.createElement("template");
|
|
80
|
+
const contentDoc = probeTemplate.content?.ownerDocument;
|
|
81
|
+
if (contentDoc !== undefined && contentDoc !== null && contentDoc !== realDocument) {
|
|
82
|
+
bindDocumentMethods(contentDoc);
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// template probing is best-effort; the main document patch stands
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let bound: PurifyInstance | null = null;
|
|
91
|
+
|
|
92
|
+
function resolveBound(): PurifyInstance {
|
|
93
|
+
if (bound !== null) return bound;
|
|
94
|
+
const w = globalThis.window as (Window & typeof globalThis) | undefined;
|
|
95
|
+
if (w !== undefined) ensureBoundDocumentMethods(w);
|
|
96
|
+
const candidate = purifyFactory(w) as PurifyInstance;
|
|
97
|
+
// A supported instance carries `sanitize`; an unsupported factory
|
|
98
|
+
// shell does not — fall back to the import-time default rather than
|
|
99
|
+
// break the render path.
|
|
100
|
+
bound = typeof candidate.sanitize === "function" ? candidate : purifyFactory;
|
|
101
|
+
return bound;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const shim = new Proxy({} as PurifyInstance, {
|
|
105
|
+
get(_target, prop, receiver) {
|
|
106
|
+
const instance = resolveBound();
|
|
107
|
+
const value = Reflect.get(instance, prop, receiver);
|
|
108
|
+
return typeof value === "function" ? value.bind(instance) : value;
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export default shim;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { decodeArtifactBytes, type RenderContext } from "./registry";
|
|
2
|
+
import {
|
|
3
|
+
isAllowedHtmlUrl,
|
|
4
|
+
isHtmlDeniedElement,
|
|
5
|
+
isHtmlEventHandlerAttribute,
|
|
6
|
+
isHtmlInlineStyleAttribute,
|
|
7
|
+
isHtmlUrlAttributeName,
|
|
8
|
+
isHtmlUrlBearingAttribute,
|
|
9
|
+
} from "../../../shared/html/policy";
|
|
10
|
+
|
|
11
|
+
function shouldKeepAttribute(elementName: string, name: string, value: string): boolean {
|
|
12
|
+
if (isHtmlEventHandlerAttribute(name) || isHtmlInlineStyleAttribute(name)) return false;
|
|
13
|
+
if (isHtmlUrlBearingAttribute(elementName, name))
|
|
14
|
+
return isAllowedHtmlUrl(elementName, name, value);
|
|
15
|
+
return !isHtmlUrlAttributeName(name);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sanitizeParsedHtml(root: HTMLElement): void {
|
|
19
|
+
for (const element of Array.from(root.querySelectorAll("*"))) {
|
|
20
|
+
const tagName = element.localName.toLowerCase();
|
|
21
|
+
if (isHtmlDeniedElement(tagName)) {
|
|
22
|
+
element.remove();
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
for (const attribute of Array.from(element.attributes)) {
|
|
26
|
+
if (!shouldKeepAttribute(tagName, attribute.name.toLowerCase(), attribute.value))
|
|
27
|
+
element.removeAttribute(attribute.name);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createHtmlRendererRoot(ownerDocument: Document): HTMLElement {
|
|
33
|
+
const root = ownerDocument.createElement("div");
|
|
34
|
+
root.setAttribute("data-facet-renderer-root", "true");
|
|
35
|
+
root.className = "facet-html-root";
|
|
36
|
+
return root;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function renderHtml(ctx: RenderContext, bytes: Uint8Array): Promise<void> {
|
|
40
|
+
const ownerDocument = ctx.container.ownerDocument;
|
|
41
|
+
const root = createHtmlRendererRoot(ownerDocument);
|
|
42
|
+
const parsed = new DOMParser().parseFromString(decodeArtifactBytes(bytes), "text/html");
|
|
43
|
+
const parsedBody = parsed.body;
|
|
44
|
+
sanitizeParsedHtml(parsedBody);
|
|
45
|
+
for (const child of Array.from(parsedBody.childNodes))
|
|
46
|
+
root.appendChild(ownerDocument.adoptNode(child));
|
|
47
|
+
ctx.container.replaceChildren(root);
|
|
48
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown renderer — Marked with raw HTML treated as DATA.
|
|
3
|
+
*
|
|
4
|
+
* Marked passes raw HTML tokens through to the output BY DEFAULT. The
|
|
5
|
+
* renderer override escapes them at TOKEN level — before any HTML is
|
|
6
|
+
* attached to the DOM — so raw `<script>`, `<img onerror=…>`, inline
|
|
7
|
+
* handlers, and iframes remain VISIBLE TEXT. Sanitize-after-insert is
|
|
8
|
+
* too late (the parser already ran); the frame CSP is the backstop.
|
|
9
|
+
*
|
|
10
|
+
* Fenced ```mermaid blocks are delegated to the real mermaid renderer
|
|
11
|
+
* and their `<pre>` is replaced by the sanitized SVG; the markdown
|
|
12
|
+
* render only resolves after every embedded diagram promise settles.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Marked } from "marked";
|
|
16
|
+
|
|
17
|
+
import { type RenderContext, decodeArtifactBytes } from "./registry";
|
|
18
|
+
|
|
19
|
+
const ESCAPES: ReadonlyMap<string, string> = new Map([
|
|
20
|
+
["&", "&"],
|
|
21
|
+
["<", "<"],
|
|
22
|
+
[">", ">"],
|
|
23
|
+
['"', """],
|
|
24
|
+
["'", "'"],
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
export function escapeHtml(text: string): string {
|
|
28
|
+
return text.replace(/[&<>"']/g, (char) => ESCAPES.get(char) ?? char);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Instance-scoped Marked: the html-token override escapes raw HTML
|
|
33
|
+
* (block AND inline) so no raw-HTML token can ever create an element.
|
|
34
|
+
*/
|
|
35
|
+
const marked = new Marked({
|
|
36
|
+
gfm: true,
|
|
37
|
+
renderer: {
|
|
38
|
+
html(token) {
|
|
39
|
+
return escapeHtml(token.text);
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Pure stage: markdown text → HTML string with raw-HTML tokens
|
|
46
|
+
* escaped at TOKEN level. No DOM — testable without a browser.
|
|
47
|
+
*/
|
|
48
|
+
export async function markdownToSanitizedHtml(text: string): Promise<string> {
|
|
49
|
+
return await Promise.resolve(marked.parse(text));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Render a markdown artifact. The HTML is assembled in a detached
|
|
54
|
+
* `<template>`, every mermaid fence is awaited and replaced by its
|
|
55
|
+
* sanitized SVG, and only then does the fragment cross into the live
|
|
56
|
+
* container — one attach, after all diagram promises settle.
|
|
57
|
+
*/
|
|
58
|
+
export async function renderMarkdown(ctx: RenderContext, bytes: Uint8Array): Promise<void> {
|
|
59
|
+
const text = decodeArtifactBytes(bytes);
|
|
60
|
+
const html = await markdownToSanitizedHtml(text);
|
|
61
|
+
const template = document.createElement("template");
|
|
62
|
+
template.innerHTML = html;
|
|
63
|
+
const mermaidBlocks = Array.from(
|
|
64
|
+
template.content.querySelectorAll("pre > code.language-mermaid"),
|
|
65
|
+
);
|
|
66
|
+
// Plain markdown must not pay Mermaid's multi-megabyte load + parse cost.
|
|
67
|
+
const renderMermaidInto =
|
|
68
|
+
mermaidBlocks.length === 0 ? null : (await import("./mermaid")).renderMermaidInto;
|
|
69
|
+
for (const code of mermaidBlocks) {
|
|
70
|
+
const source = code.textContent ?? "";
|
|
71
|
+
const pre = code.parentElement;
|
|
72
|
+
if (pre === null) continue;
|
|
73
|
+
const region = document.createElement("div");
|
|
74
|
+
region.setAttribute("data-facet-diagram-region", "true");
|
|
75
|
+
region.tabIndex = 0;
|
|
76
|
+
await renderMermaidInto!(region, source, ctx.theme);
|
|
77
|
+
if (region.firstElementChild !== null) pre.replaceWith(region);
|
|
78
|
+
else pre.remove();
|
|
79
|
+
}
|
|
80
|
+
ctx.container.appendChild(template.content);
|
|
81
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mermaid renderer — the REAL mermaid runtime, `securityLevel: "loose"`.
|
|
3
|
+
*
|
|
4
|
+
* Renders the diagram to an SVG string via `mermaid.render()`, awaits
|
|
5
|
+
* completion, then imports the result through the ONE sanitized-SVG
|
|
6
|
+
* import path (`svg.ts`). Mermaid's strict/sandbox levels add a
|
|
7
|
+
* whole-SVG DOMPurify pass on top — redundant here, and broken in the
|
|
8
|
+
* verifier's inlined harness bundle: the shim DOMPurify's SVG-input `_initDocument` path
|
|
9
|
+
* resolves `body` to null and returns `""` for every diagram. "loose"
|
|
10
|
+
* skips that pass (`!isLooseSecurityLevel` guard in mermaid's render),
|
|
11
|
+
* so `importSanitizedSvgText` is the SOLE outer-SVG sanitizer — script,
|
|
12
|
+
* foreignObject, on* handlers, and non-fragment URLs are still stripped
|
|
13
|
+
* there; the frame CSP is the backstop. Per-label `sanitizeText`
|
|
14
|
+
* (DOMPurify via the shim) is unaffected by the security level.
|
|
15
|
+
*
|
|
16
|
+
* Two render-time hazards are handled explicitly:
|
|
17
|
+
* 1. `mermaid.render()` leaves its sandbox container (`d<id>` div)
|
|
18
|
+
* in the document — it is removed after every render so it can
|
|
19
|
+
* never inflate the verifier's SVG census.
|
|
20
|
+
* 2. `suppressErrorRendering` keeps parse failures from injecting
|
|
21
|
+
* mermaid's own error SVG; a failed render surfaces as a
|
|
22
|
+
* facet-error marker instead.
|
|
23
|
+
*
|
|
24
|
+
* `flowchart.htmlLabels` is OFF: labels render as SVG text, so markup
|
|
25
|
+
* inside a label is DATA (visible text), never an HTML subtree — the
|
|
26
|
+
* nested-`<svg>`-in-label forgery probe has no surface to land on.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import mermaid from "mermaid";
|
|
30
|
+
|
|
31
|
+
import type { ResolvedGalleryTheme } from "../../theme";
|
|
32
|
+
import { FacetRenderError, type RenderContext, decodeArtifactBytes } from "./registry";
|
|
33
|
+
import { importSanitizedSvgText } from "./svg";
|
|
34
|
+
|
|
35
|
+
let initialized = false;
|
|
36
|
+
|
|
37
|
+
export function mermaidInitializeConfig(theme: ResolvedGalleryTheme) {
|
|
38
|
+
return {
|
|
39
|
+
startOnLoad: false,
|
|
40
|
+
securityLevel: "loose" as const,
|
|
41
|
+
suppressErrorRendering: true,
|
|
42
|
+
theme: theme === "dark" ? ("dark" as const) : ("default" as const),
|
|
43
|
+
darkMode: theme === "dark",
|
|
44
|
+
// htmlLabels OFF at EVERY level that can re-enable it. An HTML label
|
|
45
|
+
// is emitted inside a `<foreignObject>`, which the SVG import path
|
|
46
|
+
// strips as an XSS vector — so an HTML label renders as an EMPTY
|
|
47
|
+
// box. Text labels keep markup as DATA and survive the sanitizer.
|
|
48
|
+
htmlLabels: false,
|
|
49
|
+
flowchart: { htmlLabels: false },
|
|
50
|
+
class: { htmlLabels: false },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ensureMermaidInitialized(theme: ResolvedGalleryTheme): void {
|
|
55
|
+
if (initialized) return;
|
|
56
|
+
initialized = true;
|
|
57
|
+
mermaid.initialize(mermaidInitializeConfig(theme));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let renderCounter = 0;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Render one mermaid diagram source into the container. Awaits
|
|
64
|
+
* `mermaid.render()` completion BEFORE the SVG crosses the sanitized
|
|
65
|
+
* import path — the direct render promise depends on this barrier.
|
|
66
|
+
*/
|
|
67
|
+
export async function renderMermaidInto(
|
|
68
|
+
container: HTMLElement,
|
|
69
|
+
source: string,
|
|
70
|
+
theme: ResolvedGalleryTheme,
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
ensureMermaidInitialized(theme);
|
|
73
|
+
renderCounter += 1;
|
|
74
|
+
const id = `facet-mermaid-${renderCounter}-${crypto.randomUUID()}`;
|
|
75
|
+
let svg: string;
|
|
76
|
+
try {
|
|
77
|
+
const result = await mermaid.render(id, source);
|
|
78
|
+
svg = result.svg;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
81
|
+
throw new FacetRenderError(`mermaid render failed: ${message}`, "mermaid_render_error");
|
|
82
|
+
} finally {
|
|
83
|
+
// Mermaid parks its sandbox in `d<id>` and does not remove it.
|
|
84
|
+
document.getElementById(`d${id}`)?.remove();
|
|
85
|
+
}
|
|
86
|
+
await importSanitizedSvgText(container, svg, { marker: "graph" });
|
|
87
|
+
restoreNaturalSize(container.lastElementChild);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Mermaid stamps `width="100%"` + `style="max-width:<natural>px"` on its
|
|
92
|
+
* root, which SHRINKS a wide diagram to the container — a 10:1 flowchart
|
|
93
|
+
* fit-to-width renders as an unreadable strip (live-measured: 3791×377
|
|
94
|
+
* viewBox squeezed to 1248×124, 33% text scale). Readability wins: pin
|
|
95
|
+
* the root at its natural viewBox width and let the scrollable stage
|
|
96
|
+
* (both axes, top-left origin) reach the rest. viewBox-native zoom still
|
|
97
|
+
* applies on top — zoom mutates the viewBox, not these styles.
|
|
98
|
+
*/
|
|
99
|
+
function restoreNaturalSize(el: Element | null): void {
|
|
100
|
+
if (el === null || el.tagName.toLowerCase() !== "svg") return;
|
|
101
|
+
const viewBox = el.getAttribute("viewBox");
|
|
102
|
+
if (viewBox === null) return;
|
|
103
|
+
const parts = viewBox
|
|
104
|
+
.trim()
|
|
105
|
+
.split(/[\s,]+/)
|
|
106
|
+
.map(Number);
|
|
107
|
+
const width = parts[2];
|
|
108
|
+
if (parts.length !== 4 || width === undefined || !Number.isFinite(width) || width <= 0) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const svg = el as SVGElement;
|
|
112
|
+
svg.style.width = `${Math.ceil(width)}px`;
|
|
113
|
+
svg.style.maxWidth = "none";
|
|
114
|
+
svg.style.height = "auto";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Render a standalone mermaid artifact (artifactType "mermaid"). */
|
|
118
|
+
export async function renderMermaidDocument(ctx: RenderContext, bytes: Uint8Array): Promise<void> {
|
|
119
|
+
await renderMermaidInto(ctx.container, decodeArtifactBytes(bytes), ctx.theme);
|
|
120
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import type { ArtifactType } from "../../../shared/contracts/artifact-types";
|
|
2
|
+
import { isRenderer, type Renderer as RendererKind } from "../../../shared/contracts/renderers";
|
|
3
|
+
import { HTML_STRUCTURAL_GROUPS } from "../../../shared/html/policy";
|
|
4
|
+
import type { VerdictObserved } from "../../../shared/contracts/validation";
|
|
5
|
+
import type { ObservedCountKey } from "../../../shared/contracts/observed-counts";
|
|
6
|
+
import { isTsxExecutionMode, type TsxExecutionMode } from "../../../shared/tsx/execution";
|
|
7
|
+
import type { ResolvedGalleryTheme } from "../../theme";
|
|
8
|
+
|
|
9
|
+
export { ARTIFACT_TYPES, type ArtifactType } from "../../../shared/contracts/artifact-types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Frame-side renderer registry — keyed by ArtifactType.
|
|
13
|
+
*
|
|
14
|
+
* Runs INSIDE the artifact frame. Gallery and Tier 1 have paired
|
|
15
|
+
* type-specific entries that instantiate this registry with the SAME
|
|
16
|
+
* renderer modules; the build-metafile parity gate turns red if those
|
|
17
|
+
* module sets diverge. Artifact bytes are DATA: dispatch routes them to
|
|
18
|
+
* the typed renderer and every renderer builds DOM through trusted APIs.
|
|
19
|
+
*
|
|
20
|
+
* No zod in the frame bundle: the wire shape is plain JS here.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export interface RenderContext {
|
|
24
|
+
readonly container: HTMLElement;
|
|
25
|
+
readonly theme: ResolvedGalleryTheme;
|
|
26
|
+
readonly nonce?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type Renderer = (
|
|
30
|
+
ctx: RenderContext,
|
|
31
|
+
bytes: Uint8Array,
|
|
32
|
+
renderer: RendererKind,
|
|
33
|
+
execution?: TsxExecutionMode,
|
|
34
|
+
) => Promise<void>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Typed render failure. `code` travels into the facet-error element's
|
|
38
|
+
* text so the verifier's discriminative surface can name the reason.
|
|
39
|
+
*/
|
|
40
|
+
export class FacetRenderError extends Error {
|
|
41
|
+
constructor(
|
|
42
|
+
message: string,
|
|
43
|
+
readonly code: string,
|
|
44
|
+
) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = "FacetRenderError";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function decodeArtifactBytes(bytes: Uint8Array): string {
|
|
51
|
+
return new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const RENDER_ERROR_ATTRIBUTE = "data-facet-error";
|
|
55
|
+
export const RENDER_ERROR_ELEMENT = "facet-error";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Append the renderer-owned error marker. The element carries
|
|
59
|
+
* `data-facet-error` so every observation channel (page shim,
|
|
60
|
+
* protocol probes) counts it the same way.
|
|
61
|
+
*/
|
|
62
|
+
function renderErrorMessage(value: unknown): string {
|
|
63
|
+
if (value instanceof Error) return value.message;
|
|
64
|
+
if (typeof value === "string") return value;
|
|
65
|
+
if (typeof value === "object" && value !== null) {
|
|
66
|
+
const event = value as {
|
|
67
|
+
readonly error?: unknown;
|
|
68
|
+
readonly message?: unknown;
|
|
69
|
+
readonly reason?: unknown;
|
|
70
|
+
};
|
|
71
|
+
for (const candidate of [event.reason, event.error, event.message]) {
|
|
72
|
+
if (candidate instanceof Error) return candidate.message;
|
|
73
|
+
if (typeof candidate === "string") return candidate;
|
|
74
|
+
if (
|
|
75
|
+
typeof candidate === "object" &&
|
|
76
|
+
candidate !== null &&
|
|
77
|
+
"message" in candidate &&
|
|
78
|
+
typeof candidate.message === "string"
|
|
79
|
+
) {
|
|
80
|
+
return candidate.message;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return "interactive TSX runtime error";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function appendRenderError(container: HTMLElement, value: unknown): void {
|
|
88
|
+
const el = container.ownerDocument.createElement(RENDER_ERROR_ELEMENT);
|
|
89
|
+
el.setAttribute(RENDER_ERROR_ATTRIBUTE, "true");
|
|
90
|
+
el.textContent = renderErrorMessage(value);
|
|
91
|
+
container.appendChild(el);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type PageShimCounts = Pick<VerdictObserved, ObservedCountKey | "html" | "errorCount">;
|
|
95
|
+
|
|
96
|
+
const RENDERER_ROOT_SELECTOR = 'svg[data-facet-renderer-root="true"]';
|
|
97
|
+
const MARKED_ROOT_SELECTOR = '[data-facet-renderer-root="true"]';
|
|
98
|
+
const RENDERER_GRAPH_ATTRIBUTE = "data-facet-renderer-graph";
|
|
99
|
+
|
|
100
|
+
function safeSelectorElementsWithin(scope: ParentNode, selector: string): Element[] {
|
|
101
|
+
// The shim lives in the PAGE world on purpose: a hostile page can
|
|
102
|
+
// monkey-patch querySelectorAll and lie here — the Tier 1 verdict
|
|
103
|
+
// detects that lie against the protocol authority. Reading through
|
|
104
|
+
// the patchable API is the design, not a bug.
|
|
105
|
+
try {
|
|
106
|
+
return Array.from(scope.querySelectorAll(selector));
|
|
107
|
+
} catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function safeSelectorElements(selector: string): Element[] {
|
|
113
|
+
return safeSelectorElementsWithin(document, selector);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function hasMarkedRootAncestor(root: Element, roots: ReadonlySet<Element>): boolean {
|
|
117
|
+
let parent = root.parentElement;
|
|
118
|
+
while (parent !== null) {
|
|
119
|
+
if (roots.has(parent)) return true;
|
|
120
|
+
parent = parent.parentElement;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function nonDegenerateViewBox(svg: Element): boolean {
|
|
126
|
+
const parts = (svg.getAttribute("viewBox") ?? "")
|
|
127
|
+
.trim()
|
|
128
|
+
.split(/[\s,]+/)
|
|
129
|
+
.map((part) => Number.parseFloat(part));
|
|
130
|
+
if (parts.length !== 4) return false;
|
|
131
|
+
const [, , width, height] = parts as [number, number, number, number];
|
|
132
|
+
return Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Page-world self-report the frame's direct render promise resolves with. UNTRUSTED by design. */
|
|
136
|
+
export function countPageShim(): PageShimCounts {
|
|
137
|
+
const markedCandidates = safeSelectorElements(RENDERER_ROOT_SELECTOR);
|
|
138
|
+
const candidateSet = new Set(markedCandidates);
|
|
139
|
+
const roots = markedCandidates.filter((root) => !hasMarkedRootAncestor(root, candidateSet));
|
|
140
|
+
const graphRoots = roots.filter((root) => root.getAttribute(RENDERER_GRAPH_ATTRIBUTE) === "true");
|
|
141
|
+
const allMarkedCandidates = safeSelectorElements(MARKED_ROOT_SELECTOR);
|
|
142
|
+
const markedSet = new Set(allMarkedCandidates);
|
|
143
|
+
const htmlRoots = allMarkedCandidates.filter(
|
|
144
|
+
(root) => root.nodeName.toLowerCase() !== "svg" && !hasMarkedRootAncestor(root, markedSet),
|
|
145
|
+
);
|
|
146
|
+
const html =
|
|
147
|
+
htmlRoots.length === 0
|
|
148
|
+
? undefined
|
|
149
|
+
: {
|
|
150
|
+
rendererRootCount: htmlRoots.length,
|
|
151
|
+
headingCount: 0,
|
|
152
|
+
tableCount: 0,
|
|
153
|
+
listCount: 0,
|
|
154
|
+
imageCount: 0,
|
|
155
|
+
canvasCount: 0,
|
|
156
|
+
externalImageCount: 0,
|
|
157
|
+
};
|
|
158
|
+
for (const root of htmlRoots) {
|
|
159
|
+
html!.headingCount += safeSelectorElementsWithin(
|
|
160
|
+
root,
|
|
161
|
+
HTML_STRUCTURAL_GROUPS.headings.join(","),
|
|
162
|
+
).length;
|
|
163
|
+
html!.tableCount += safeSelectorElementsWithin(
|
|
164
|
+
root,
|
|
165
|
+
HTML_STRUCTURAL_GROUPS.tables.join(","),
|
|
166
|
+
).length;
|
|
167
|
+
html!.listCount += safeSelectorElementsWithin(
|
|
168
|
+
root,
|
|
169
|
+
HTML_STRUCTURAL_GROUPS.lists.join(","),
|
|
170
|
+
).length;
|
|
171
|
+
const images = safeSelectorElementsWithin(root, HTML_STRUCTURAL_GROUPS.images.join(","));
|
|
172
|
+
html!.imageCount += images.length;
|
|
173
|
+
html!.externalImageCount += images.filter((image) => {
|
|
174
|
+
try {
|
|
175
|
+
return new URL(image.getAttribute("src") ?? "").protocol === "https:";
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}).length;
|
|
180
|
+
html!.canvasCount += safeSelectorElementsWithin(
|
|
181
|
+
root,
|
|
182
|
+
HTML_STRUCTURAL_GROUPS.canvases.join(","),
|
|
183
|
+
).length;
|
|
184
|
+
}
|
|
185
|
+
const mermaidNodeCount = graphRoots.reduce(
|
|
186
|
+
(count, root) => count + safeSelectorElementsWithin(root, "g.node").length,
|
|
187
|
+
0,
|
|
188
|
+
);
|
|
189
|
+
// Marker-scoped renderer counts deliberately differ from this document-wide
|
|
190
|
+
// canvas census: a smuggled canvas outside a renderer root remains observable.
|
|
191
|
+
// getContext() would create the surface being observed and turn the probe into a false positive.
|
|
192
|
+
const opaqueRegionCount = safeSelectorElements("*").filter(
|
|
193
|
+
(element) => element.nodeName.toLowerCase() === "canvas",
|
|
194
|
+
).length;
|
|
195
|
+
return {
|
|
196
|
+
rendererRootSvgCount: roots.length,
|
|
197
|
+
graphCount: graphRoots.length,
|
|
198
|
+
mermaidNodeCount,
|
|
199
|
+
visibleSvgCount: roots.filter(nonDegenerateViewBox).length,
|
|
200
|
+
opaqueRegionCount,
|
|
201
|
+
externalImageCount: html?.externalImageCount ?? 0,
|
|
202
|
+
errorCount: safeSelectorElements("[data-facet-error]").length,
|
|
203
|
+
...(html === undefined ? {} : { html }),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface RendererRegistry {
|
|
208
|
+
readonly get: (type: string) => Renderer | undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function createRendererRegistry(
|
|
212
|
+
entries: readonly (readonly [ArtifactType, Renderer])[],
|
|
213
|
+
): RendererRegistry {
|
|
214
|
+
const registry = new Map<string, Renderer>(entries);
|
|
215
|
+
return {
|
|
216
|
+
get(type: string): Renderer | undefined {
|
|
217
|
+
return registry.get(type);
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Dispatch one artifact through the registry. Unknown types throw — the caller
|
|
224
|
+
* turns the throw into a facet-error marker, never executable content.
|
|
225
|
+
*/
|
|
226
|
+
export async function dispatchRender(
|
|
227
|
+
registry: RendererRegistry,
|
|
228
|
+
ctx: RenderContext,
|
|
229
|
+
payload: {
|
|
230
|
+
readonly artifactType: string;
|
|
231
|
+
readonly renderer: RendererKind;
|
|
232
|
+
readonly bytes: Uint8Array;
|
|
233
|
+
readonly theme: ResolvedGalleryTheme;
|
|
234
|
+
readonly execution?: TsxExecutionMode;
|
|
235
|
+
},
|
|
236
|
+
): Promise<void> {
|
|
237
|
+
if (!isRenderer(payload.renderer)) {
|
|
238
|
+
throw new FacetRenderError(
|
|
239
|
+
`Artifact renderer '${payload.renderer}' is not supported in this frame`,
|
|
240
|
+
"invalid_request",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
const renderer = registry.get(payload.artifactType);
|
|
244
|
+
if (renderer === undefined) {
|
|
245
|
+
throw new FacetRenderError(
|
|
246
|
+
`Artifact type '${payload.artifactType}' has no renderer in this frame`,
|
|
247
|
+
"unsupported_reserved_type",
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (payload.artifactType === "tsx" && !isTsxExecutionMode(payload.execution)) {
|
|
251
|
+
throw new FacetRenderError("TSX artifact payload is missing execution", "invalid_request");
|
|
252
|
+
}
|
|
253
|
+
if (payload.artifactType !== "tsx" && payload.execution === "interactive") {
|
|
254
|
+
throw new FacetRenderError(
|
|
255
|
+
"Interactive execution is only supported for TSX artifacts",
|
|
256
|
+
"invalid_request",
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
await renderer(ctx, payload.bytes, payload.renderer, payload.execution);
|
|
260
|
+
}
|