@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,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 0 SVG parser.
|
|
3
|
+
*
|
|
4
|
+
* Uses `fast-xml-parser` to tokenize the source as XML, then walks the
|
|
5
|
+
* tree checking structural invariants. The Tier 0 verdict never
|
|
6
|
+
* executes any SVG; it only verifies that the artifact cannot smuggle
|
|
7
|
+
* executable content past the renderer:
|
|
8
|
+
*
|
|
9
|
+
* - `<script>` elements are rejected at any depth.
|
|
10
|
+
* - `<foreignObject>` may not contain `<script>` either (a nested
|
|
11
|
+
* escape vector).
|
|
12
|
+
* - Event-handler attributes (`on*`) on every element are rejected.
|
|
13
|
+
* - URLs in `href`/`xlink:href` must NOT reference external schemes
|
|
14
|
+
* (http(s), file, ftp, javascript, data, blob). Inline-only.
|
|
15
|
+
* - The root element must be `<svg>` (not, e.g., a hostile XML
|
|
16
|
+
* payload that happens to parse as XML).
|
|
17
|
+
* - The root must carry a `viewBox` (verifiers check `viewBoxes`).
|
|
18
|
+
*
|
|
19
|
+
* The parser does not enforce renderer-side layout (Tier 1's job); it
|
|
20
|
+
* only validates structural safety.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { XMLParser } from "fast-xml-parser";
|
|
24
|
+
|
|
25
|
+
import { MAX_SVG_BYTES, MAX_SVG_ROOTS } from "../../shared/config/limits";
|
|
26
|
+
import type { DiscriminativeError, VerdictObserved } from "../../shared/contracts/validation";
|
|
27
|
+
|
|
28
|
+
const XML_OPTIONS = {
|
|
29
|
+
ignoreAttributes: false,
|
|
30
|
+
attributeNamePrefix: "@_",
|
|
31
|
+
allowBooleanAttributes: true,
|
|
32
|
+
parseTagValue: false,
|
|
33
|
+
parseAttributeValue: false,
|
|
34
|
+
trimValues: true,
|
|
35
|
+
// Treat these as void so a hostile DTD/ENTITY doesn't expand.
|
|
36
|
+
processEntities: false,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface SvgParseOk {
|
|
40
|
+
readonly status: "ok";
|
|
41
|
+
readonly observed: VerdictObserved;
|
|
42
|
+
readonly viewBoxes: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SvgParseFail {
|
|
46
|
+
readonly status: "error";
|
|
47
|
+
readonly observed: VerdictObserved;
|
|
48
|
+
readonly errors: readonly DiscriminativeError[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type SvgParseResult = SvgParseOk | SvgParseFail;
|
|
52
|
+
|
|
53
|
+
const EVENT_HANDLER_RE = /^on[a-z]+$/i;
|
|
54
|
+
const EXTERNAL_SCHEME_RE = /^(?:https?|ftp|file|javascript|data|blob|vbscript|jar):/i;
|
|
55
|
+
const DANGEROUS_TAGS = new Set(["script"]);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Recursively walk the parsed XML tree, counting the elements/attributes
|
|
59
|
+
* Tier 0 must reject. The walk does NOT execute anything; it only
|
|
60
|
+
* collects evidence for the verdict.
|
|
61
|
+
*/
|
|
62
|
+
function walkNode(
|
|
63
|
+
node: unknown,
|
|
64
|
+
name: string,
|
|
65
|
+
ctx: {
|
|
66
|
+
scriptElements: number;
|
|
67
|
+
eventHandlerAttributes: number;
|
|
68
|
+
externalRefAttributes: number;
|
|
69
|
+
rootSvgCount: number;
|
|
70
|
+
viewBoxes: string[];
|
|
71
|
+
},
|
|
72
|
+
isRoot: boolean,
|
|
73
|
+
): void {
|
|
74
|
+
if (name === "script" || DANGEROUS_TAGS.has(name)) {
|
|
75
|
+
ctx.scriptElements += 1;
|
|
76
|
+
}
|
|
77
|
+
if (!isPlainObject(node)) return;
|
|
78
|
+
// Detect attributes on the current element.
|
|
79
|
+
for (const [key, value] of Object.entries(node)) {
|
|
80
|
+
if (!key.startsWith("@_")) continue;
|
|
81
|
+
const attrName = key.slice(2);
|
|
82
|
+
const attrValue = typeof value === "string" ? value : String(value ?? "");
|
|
83
|
+
if (EVENT_HANDLER_RE.test(attrName)) {
|
|
84
|
+
ctx.eventHandlerAttributes += 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (attrName === "href" || attrName === "xlink:href") {
|
|
88
|
+
if (EXTERNAL_SCHEME_RE.test(attrValue.trim())) {
|
|
89
|
+
ctx.externalRefAttributes += 1;
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (isRoot && attrName === "viewBox") {
|
|
94
|
+
ctx.viewBoxes.push(attrValue);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (isRoot && name === "svg") {
|
|
98
|
+
ctx.rootSvgCount += 1;
|
|
99
|
+
}
|
|
100
|
+
// Recurse into children (each key maps to a sub-node or array of sub-nodes).
|
|
101
|
+
for (const [key, value] of Object.entries(node)) {
|
|
102
|
+
if (key.startsWith("@_")) continue;
|
|
103
|
+
if (key === "#text") continue;
|
|
104
|
+
if (Array.isArray(value)) {
|
|
105
|
+
for (const child of value) walkNode(child, key, ctx, false);
|
|
106
|
+
} else if (value !== undefined) {
|
|
107
|
+
walkNode(value, key, ctx, false);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
113
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parse the source bytes as SVG. The parse is structural: no DOM, no
|
|
118
|
+
* rendering, no URL fetch. A hostile source that smuggles
|
|
119
|
+
* `<script>`/handlers/external URLs is rejected at Tier 0 so it can
|
|
120
|
+
* never reach a renderer.
|
|
121
|
+
*/
|
|
122
|
+
export function parseSvg(bytes: Uint8Array): SvgParseResult {
|
|
123
|
+
if (bytes.byteLength > MAX_SVG_BYTES) {
|
|
124
|
+
return {
|
|
125
|
+
status: "error",
|
|
126
|
+
observed: {
|
|
127
|
+
rendererRootSvgCount: 0,
|
|
128
|
+
graphCount: 0,
|
|
129
|
+
mermaidNodeCount: 0,
|
|
130
|
+
visibleSvgCount: 0,
|
|
131
|
+
errorCount: 1,
|
|
132
|
+
opaqueRegionCount: 0,
|
|
133
|
+
externalImageCount: 0,
|
|
134
|
+
},
|
|
135
|
+
errors: [
|
|
136
|
+
{
|
|
137
|
+
code: "svg_too_large",
|
|
138
|
+
message: `SVG bytes exceed MAX_SVG_BYTES (${MAX_SVG_BYTES})`,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
145
|
+
const parser = new XMLParser(XML_OPTIONS);
|
|
146
|
+
let parsed: unknown;
|
|
147
|
+
try {
|
|
148
|
+
parsed = parser.parse(text);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
151
|
+
return {
|
|
152
|
+
status: "error",
|
|
153
|
+
observed: {
|
|
154
|
+
rendererRootSvgCount: 0,
|
|
155
|
+
graphCount: 0,
|
|
156
|
+
mermaidNodeCount: 0,
|
|
157
|
+
visibleSvgCount: 0,
|
|
158
|
+
errorCount: 1,
|
|
159
|
+
opaqueRegionCount: 0,
|
|
160
|
+
externalImageCount: 0,
|
|
161
|
+
},
|
|
162
|
+
errors: [{ code: "svg_xml_error", message }],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// The parsed root is an object whose keys are the top-level XML
|
|
167
|
+
// elements. We walk it as if the outermost key were the document
|
|
168
|
+
// element; for an SVG document the key is "svg" and the inner
|
|
169
|
+
// children are the elements we want to scan.
|
|
170
|
+
const ctx = {
|
|
171
|
+
scriptElements: 0,
|
|
172
|
+
eventHandlerAttributes: 0,
|
|
173
|
+
externalRefAttributes: 0,
|
|
174
|
+
rootSvgCount: 0,
|
|
175
|
+
viewBoxes: [] as string[],
|
|
176
|
+
};
|
|
177
|
+
if (isPlainObject(parsed)) {
|
|
178
|
+
for (const [topName, topValue] of Object.entries(parsed)) {
|
|
179
|
+
const children = Array.isArray(topValue) ? topValue : [topValue];
|
|
180
|
+
for (const child of children) {
|
|
181
|
+
walkNode(child, topName, ctx, true);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (ctx.rootSvgCount === 0) {
|
|
187
|
+
return {
|
|
188
|
+
status: "error",
|
|
189
|
+
observed: {
|
|
190
|
+
rendererRootSvgCount: 0,
|
|
191
|
+
graphCount: 0,
|
|
192
|
+
mermaidNodeCount: 0,
|
|
193
|
+
visibleSvgCount: 0,
|
|
194
|
+
errorCount: 1,
|
|
195
|
+
opaqueRegionCount: 0,
|
|
196
|
+
externalImageCount: 0,
|
|
197
|
+
},
|
|
198
|
+
errors: [
|
|
199
|
+
{
|
|
200
|
+
code: "svg_no_root",
|
|
201
|
+
message: "Document does not contain a top-level <svg> element",
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (ctx.rootSvgCount > MAX_SVG_ROOTS) {
|
|
207
|
+
return {
|
|
208
|
+
status: "error",
|
|
209
|
+
observed: {
|
|
210
|
+
rendererRootSvgCount: ctx.rootSvgCount,
|
|
211
|
+
graphCount: 0,
|
|
212
|
+
mermaidNodeCount: 0,
|
|
213
|
+
visibleSvgCount: 0,
|
|
214
|
+
errorCount: 1,
|
|
215
|
+
opaqueRegionCount: 0,
|
|
216
|
+
externalImageCount: 0,
|
|
217
|
+
},
|
|
218
|
+
errors: [
|
|
219
|
+
{
|
|
220
|
+
code: "svg_too_many_roots",
|
|
221
|
+
message: `SVG contains ${ctx.rootSvgCount} top-level <svg> elements; cap is ${MAX_SVG_ROOTS}`,
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (ctx.viewBoxes.length === 0) {
|
|
227
|
+
return {
|
|
228
|
+
status: "error",
|
|
229
|
+
observed: {
|
|
230
|
+
rendererRootSvgCount: ctx.rootSvgCount,
|
|
231
|
+
graphCount: 0,
|
|
232
|
+
mermaidNodeCount: 0,
|
|
233
|
+
visibleSvgCount: 0,
|
|
234
|
+
errorCount: 1,
|
|
235
|
+
opaqueRegionCount: 0,
|
|
236
|
+
externalImageCount: 0,
|
|
237
|
+
},
|
|
238
|
+
errors: [
|
|
239
|
+
{
|
|
240
|
+
code: "svg_missing_viewbox",
|
|
241
|
+
message:
|
|
242
|
+
"Top-level <svg> element must declare a viewBox; Tier 0 rejects ambiguous bounds",
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (ctx.scriptElements > 0) {
|
|
248
|
+
return {
|
|
249
|
+
status: "error",
|
|
250
|
+
observed: {
|
|
251
|
+
rendererRootSvgCount: ctx.rootSvgCount,
|
|
252
|
+
graphCount: 0,
|
|
253
|
+
mermaidNodeCount: 0,
|
|
254
|
+
visibleSvgCount: 0,
|
|
255
|
+
errorCount: ctx.scriptElements,
|
|
256
|
+
opaqueRegionCount: 0,
|
|
257
|
+
externalImageCount: 0,
|
|
258
|
+
},
|
|
259
|
+
errors: [
|
|
260
|
+
{
|
|
261
|
+
code: "svg_script_element",
|
|
262
|
+
message: `SVG contains ${ctx.scriptElements} <script> element(s); Tier 0 rejects them`,
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (ctx.eventHandlerAttributes > 0) {
|
|
268
|
+
return {
|
|
269
|
+
status: "error",
|
|
270
|
+
observed: {
|
|
271
|
+
rendererRootSvgCount: ctx.rootSvgCount,
|
|
272
|
+
graphCount: 0,
|
|
273
|
+
mermaidNodeCount: 0,
|
|
274
|
+
visibleSvgCount: 0,
|
|
275
|
+
errorCount: ctx.eventHandlerAttributes,
|
|
276
|
+
opaqueRegionCount: 0,
|
|
277
|
+
externalImageCount: 0,
|
|
278
|
+
},
|
|
279
|
+
errors: [
|
|
280
|
+
{
|
|
281
|
+
code: "svg_event_handler",
|
|
282
|
+
message: `SVG contains ${ctx.eventHandlerAttributes} on*= event-handler attribute(s); Tier 0 rejects them`,
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (ctx.externalRefAttributes > 0) {
|
|
288
|
+
return {
|
|
289
|
+
status: "error",
|
|
290
|
+
observed: {
|
|
291
|
+
rendererRootSvgCount: ctx.rootSvgCount,
|
|
292
|
+
graphCount: 0,
|
|
293
|
+
mermaidNodeCount: 0,
|
|
294
|
+
visibleSvgCount: 0,
|
|
295
|
+
errorCount: ctx.externalRefAttributes,
|
|
296
|
+
opaqueRegionCount: 0,
|
|
297
|
+
externalImageCount: 0,
|
|
298
|
+
},
|
|
299
|
+
errors: [
|
|
300
|
+
{
|
|
301
|
+
code: "svg_external_reference",
|
|
302
|
+
message: `SVG contains ${ctx.externalRefAttributes} external-scheme URL attribute(s); Tier 0 rejects them`,
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
status: "ok",
|
|
310
|
+
observed: {
|
|
311
|
+
rendererRootSvgCount: ctx.rootSvgCount,
|
|
312
|
+
graphCount: 0,
|
|
313
|
+
mermaidNodeCount: 0,
|
|
314
|
+
visibleSvgCount: ctx.rootSvgCount,
|
|
315
|
+
errorCount: 0,
|
|
316
|
+
opaqueRegionCount: 0,
|
|
317
|
+
externalImageCount: 0,
|
|
318
|
+
},
|
|
319
|
+
viewBoxes: ctx.viewBoxes,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun build plugin that enforces the TSX module allowlist at the resolver
|
|
3
|
+
* layer.
|
|
4
|
+
*
|
|
5
|
+
* D6 of the TSX design declares a fixed vendored allowlist. The AST policy
|
|
6
|
+
* rejects non-allowlisted `import` declarations with a typed verdict, but
|
|
7
|
+
* the AST walker is one line of defense — `require()`, dynamic paths, and
|
|
8
|
+
* other indirect routes can slip past it. This plugin is the second line:
|
|
9
|
+
* it intercepts every bare module specifier and redirects non-allowlisted
|
|
10
|
+
* names to a sentinel that fails to load. A bypass of the AST layer still
|
|
11
|
+
* cannot pull a forbidden module into a bundle.
|
|
12
|
+
*
|
|
13
|
+
* Bun's `onResolve` API returns `{ path, namespace?, external? }` — there is
|
|
14
|
+
* no first-class way to emit a typed error from the resolver hook. We work
|
|
15
|
+
* around this by redirecting to a fixed sentinel path and throwing from
|
|
16
|
+
* `onLoad` when the bundler tries to load it; the resulting build log
|
|
17
|
+
* carries the violated module name. This is intentionally clunky — the
|
|
18
|
+
* AST walker produces the structured `DiscriminativeError` verdict, and
|
|
19
|
+
* this plugin exists only so a code path that bypasses the walker still
|
|
20
|
+
* cannot bundle a forbidden package.
|
|
21
|
+
*
|
|
22
|
+
* ## Limitation
|
|
23
|
+
*
|
|
24
|
+
* The plugin intercepts BARE specifiers (anything that does not start with
|
|
25
|
+
* `.`, `/`, or `\`). Relative imports are checked by the AST walker; URL
|
|
26
|
+
* imports are blocked at the AST layer; this plugin handles the node_modules
|
|
27
|
+
* path. Anything that reaches the bundler via a hook the plugin cannot see
|
|
28
|
+
* (Bun's built-in pre-resolution, native module records) is OUT OF SCOPE.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import type { BunPlugin } from "bun";
|
|
32
|
+
|
|
33
|
+
import { TSX_ALLOWED_MODULES } from "../../../shared/tsx/import-policy";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Sentinel path the plugin redirects every non-allowlisted import to.
|
|
37
|
+
* Resolving or loading this path fails, and the `onLoad` hook throws a
|
|
38
|
+
* named error so the build log carries the violated module.
|
|
39
|
+
*/
|
|
40
|
+
const SENTINEL_PATH = "/__facet_tsx_allowlist_violation__";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build the allowlist resolver plugin. Pass it to `Bun.build({ plugins })`
|
|
44
|
+
* so every bare module resolution goes through the gate.
|
|
45
|
+
*/
|
|
46
|
+
export function tsxAllowlistResolverPlugin(): BunPlugin {
|
|
47
|
+
const violations: string[] = [];
|
|
48
|
+
return {
|
|
49
|
+
name: "facet-tsx-allowlist-resolver",
|
|
50
|
+
setup(build) {
|
|
51
|
+
build.onResolve({ filter: /^[^./\\:]/ }, (args) => {
|
|
52
|
+
// The entrypoint is passed through onResolve with kind =
|
|
53
|
+
// "entry-point-build"; we let it pass to the default resolver.
|
|
54
|
+
if (args.kind === "entry-point-build") return undefined;
|
|
55
|
+
// Transitive dependencies (imports from inside node_modules) are
|
|
56
|
+
// trusted to resolve themselves; only the user's top-level
|
|
57
|
+
// imports are gated by the allowlist.
|
|
58
|
+
if (args.importer.includes("/node_modules/")) return undefined;
|
|
59
|
+
if (TSX_ALLOWED_MODULES.has(args.path)) {
|
|
60
|
+
return undefined; // let the default resolver find it
|
|
61
|
+
}
|
|
62
|
+
violations.push(args.path);
|
|
63
|
+
return { path: SENTINEL_PATH };
|
|
64
|
+
});
|
|
65
|
+
build.onLoad({ filter: /.*/ }, (args) => {
|
|
66
|
+
if (args.path === SENTINEL_PATH) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`TSX allowlist violation: ${violations.join(", ")} ` +
|
|
69
|
+
`is not in the vendored allowlist ` +
|
|
70
|
+
`(allowed: ${[...TSX_ALLOWED_MODULES].join(", ")})`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
|
|
3
|
+
import type { DiscriminativeError } from "../../../shared/contracts/validation";
|
|
4
|
+
import {
|
|
5
|
+
ALIAS_DENIED_NAMES,
|
|
6
|
+
formatArgument,
|
|
7
|
+
identifierText,
|
|
8
|
+
isDeniedGlobalEval,
|
|
9
|
+
isDeniedGlobalName,
|
|
10
|
+
push,
|
|
11
|
+
resolveStringFragments,
|
|
12
|
+
TSX_CAPABILITY_CODES,
|
|
13
|
+
} from "./ast-policy-shared";
|
|
14
|
+
|
|
15
|
+
type ShadowCheck = (node: ts.Identifier) => boolean;
|
|
16
|
+
|
|
17
|
+
export function checkCallExpression(
|
|
18
|
+
node: ts.CallExpression,
|
|
19
|
+
errors: DiscriminativeError[],
|
|
20
|
+
sourceFile: ts.SourceFile,
|
|
21
|
+
isShadowed: ShadowCheck,
|
|
22
|
+
): void {
|
|
23
|
+
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
24
|
+
push(
|
|
25
|
+
errors,
|
|
26
|
+
TSX_CAPABILITY_CODES.dynamicImport,
|
|
27
|
+
"TSX dynamic import() is not allowed",
|
|
28
|
+
node,
|
|
29
|
+
sourceFile,
|
|
30
|
+
);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (
|
|
34
|
+
ts.isIdentifier(node.expression) &&
|
|
35
|
+
node.expression.text === "require" &&
|
|
36
|
+
!isShadowed(node.expression)
|
|
37
|
+
) {
|
|
38
|
+
push(
|
|
39
|
+
errors,
|
|
40
|
+
TSX_CAPABILITY_CODES.requireCall,
|
|
41
|
+
"TSX require(...) is not allowed; the vendored allowlist is ESM-only",
|
|
42
|
+
node,
|
|
43
|
+
sourceFile,
|
|
44
|
+
);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const expression = node.expression;
|
|
48
|
+
if (
|
|
49
|
+
ts.isIdentifier(expression) &&
|
|
50
|
+
isDeniedGlobalName(expression.text) &&
|
|
51
|
+
!isShadowed(expression)
|
|
52
|
+
) {
|
|
53
|
+
push(
|
|
54
|
+
errors,
|
|
55
|
+
TSX_CAPABILITY_CODES.fetch,
|
|
56
|
+
`TSX global "${expression.text}(...)" is not allowed`,
|
|
57
|
+
node,
|
|
58
|
+
sourceFile,
|
|
59
|
+
);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (ts.isParenthesizedExpression(expression)) {
|
|
63
|
+
const inner = expression.expression;
|
|
64
|
+
if (
|
|
65
|
+
ts.isBinaryExpression(inner) &&
|
|
66
|
+
inner.operatorToken.kind === ts.SyntaxKind.CommaToken &&
|
|
67
|
+
ts.isNumericLiteral(inner.left) &&
|
|
68
|
+
ts.isIdentifier(inner.right) &&
|
|
69
|
+
isDeniedGlobalEval(inner.right.text) &&
|
|
70
|
+
!isShadowed(inner.right)
|
|
71
|
+
) {
|
|
72
|
+
push(
|
|
73
|
+
errors,
|
|
74
|
+
TSX_CAPABILITY_CODES.evalCall,
|
|
75
|
+
'TSX indirect eval "(0, eval)(...)" is not allowed',
|
|
76
|
+
node,
|
|
77
|
+
sourceFile,
|
|
78
|
+
);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (
|
|
83
|
+
ts.isIdentifier(expression) &&
|
|
84
|
+
isDeniedGlobalEval(expression.text) &&
|
|
85
|
+
!isShadowed(expression)
|
|
86
|
+
) {
|
|
87
|
+
push(
|
|
88
|
+
errors,
|
|
89
|
+
TSX_CAPABILITY_CODES.evalCall,
|
|
90
|
+
`TSX global "${expression.text}(...)" is not allowed`,
|
|
91
|
+
node,
|
|
92
|
+
sourceFile,
|
|
93
|
+
);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
97
|
+
const targetName = identifierText(expression.expression);
|
|
98
|
+
const propertyName = expression.name.text;
|
|
99
|
+
if (
|
|
100
|
+
targetName !== null &&
|
|
101
|
+
(targetName === "globalThis" ||
|
|
102
|
+
targetName === "window" ||
|
|
103
|
+
targetName === "self" ||
|
|
104
|
+
targetName === "global") &&
|
|
105
|
+
(isDeniedGlobalName(propertyName) || isDeniedGlobalEval(propertyName))
|
|
106
|
+
) {
|
|
107
|
+
push(
|
|
108
|
+
errors,
|
|
109
|
+
propertyName === "eval" ? TSX_CAPABILITY_CODES.evalCall : TSX_CAPABILITY_CODES.fetch,
|
|
110
|
+
`TSX global "${targetName}.${propertyName}(...)" is not allowed`,
|
|
111
|
+
node,
|
|
112
|
+
sourceFile,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function checkNewExpression(
|
|
119
|
+
node: ts.NewExpression,
|
|
120
|
+
errors: DiscriminativeError[],
|
|
121
|
+
sourceFile: ts.SourceFile,
|
|
122
|
+
isShadowed: ShadowCheck,
|
|
123
|
+
): void {
|
|
124
|
+
const expression = node.expression;
|
|
125
|
+
if (!ts.isIdentifier(expression) || isShadowed(expression)) return;
|
|
126
|
+
const name = expression.text;
|
|
127
|
+
if (name === "Function") {
|
|
128
|
+
push(
|
|
129
|
+
errors,
|
|
130
|
+
TSX_CAPABILITY_CODES.functionConstructor,
|
|
131
|
+
'TSX "new Function(...)" is not allowed',
|
|
132
|
+
node,
|
|
133
|
+
sourceFile,
|
|
134
|
+
);
|
|
135
|
+
} else if (name === "Worker") {
|
|
136
|
+
push(
|
|
137
|
+
errors,
|
|
138
|
+
TSX_CAPABILITY_CODES.worker,
|
|
139
|
+
'TSX "new Worker(...)" is not allowed',
|
|
140
|
+
node,
|
|
141
|
+
sourceFile,
|
|
142
|
+
);
|
|
143
|
+
} else if (name === "SharedWorker") {
|
|
144
|
+
push(
|
|
145
|
+
errors,
|
|
146
|
+
TSX_CAPABILITY_CODES.sharedWorker,
|
|
147
|
+
'TSX "new SharedWorker(...)" is not allowed',
|
|
148
|
+
node,
|
|
149
|
+
sourceFile,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function checkVariableStatement(
|
|
155
|
+
node: ts.VariableStatement,
|
|
156
|
+
errors: DiscriminativeError[],
|
|
157
|
+
sourceFile: ts.SourceFile,
|
|
158
|
+
): void {
|
|
159
|
+
for (const declaration of node.declarationList.declarations) {
|
|
160
|
+
if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined) continue;
|
|
161
|
+
if (!isDeniedAliasInitializer(declaration.initializer)) continue;
|
|
162
|
+
push(
|
|
163
|
+
errors,
|
|
164
|
+
TSX_CAPABILITY_CODES.aliasOfDeniedGlobal,
|
|
165
|
+
`TSX binding "${declaration.name.text}" aliases a denied global or constructor; this is not allowed`,
|
|
166
|
+
node,
|
|
167
|
+
sourceFile,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isDeniedAliasInitializer(init: ts.Expression): boolean {
|
|
173
|
+
if (ts.isIdentifier(init)) return ALIAS_DENIED_NAMES.has(init.text);
|
|
174
|
+
if (ts.isPropertyAccessExpression(init)) {
|
|
175
|
+
const target = identifierText(init.expression);
|
|
176
|
+
if (target === null || !ALIAS_DENIED_NAMES.has(target)) return false;
|
|
177
|
+
return ALIAS_DENIED_NAMES.has(init.name.text);
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function checkPropertyAccess(
|
|
183
|
+
node: ts.PropertyAccessExpression,
|
|
184
|
+
errors: DiscriminativeError[],
|
|
185
|
+
sourceFile: ts.SourceFile,
|
|
186
|
+
): void {
|
|
187
|
+
const target = identifierText(node.expression);
|
|
188
|
+
if (target === null || !["globalThis", "window", "self", "global"].includes(target)) return;
|
|
189
|
+
const property = node.name.text;
|
|
190
|
+
if (isDeniedGlobalName(property) || isDeniedGlobalEval(property)) {
|
|
191
|
+
push(
|
|
192
|
+
errors,
|
|
193
|
+
property === "eval" ? TSX_CAPABILITY_CODES.evalCall : TSX_CAPABILITY_CODES.fetch,
|
|
194
|
+
`TSX global "${target}.${property}" is not allowed`,
|
|
195
|
+
node,
|
|
196
|
+
sourceFile,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function checkElementAccess(
|
|
202
|
+
node: ts.ElementAccessExpression,
|
|
203
|
+
errors: DiscriminativeError[],
|
|
204
|
+
sourceFile: ts.SourceFile,
|
|
205
|
+
): void {
|
|
206
|
+
const target = identifierText(node.expression);
|
|
207
|
+
if (target === null || !["globalThis", "window", "self", "global"].includes(target)) return;
|
|
208
|
+
const resolved = resolveStringFragments(node.argumentExpression);
|
|
209
|
+
if (resolved !== null) {
|
|
210
|
+
if (isDeniedGlobalName(resolved)) {
|
|
211
|
+
push(
|
|
212
|
+
errors,
|
|
213
|
+
TSX_CAPABILITY_CODES.fetch,
|
|
214
|
+
`TSX computed global "${target}[${formatArgument(node.argumentExpression)}]" resolves to "${resolved}"`,
|
|
215
|
+
node,
|
|
216
|
+
sourceFile,
|
|
217
|
+
);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (isDeniedGlobalEval(resolved)) {
|
|
221
|
+
push(
|
|
222
|
+
errors,
|
|
223
|
+
TSX_CAPABILITY_CODES.evalCall,
|
|
224
|
+
`TSX computed global "${target}[${formatArgument(node.argumentExpression)}]" resolves to "${resolved}"`,
|
|
225
|
+
node,
|
|
226
|
+
sourceFile,
|
|
227
|
+
);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
push(
|
|
232
|
+
errors,
|
|
233
|
+
TSX_CAPABILITY_CODES.unsupportedComputedGlobal,
|
|
234
|
+
`TSX computed access on "${target}[...]" cannot be statically resolved; review the artifact`,
|
|
235
|
+
node,
|
|
236
|
+
sourceFile,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
|
|
3
|
+
import type { DiscriminativeError } from "../../../shared/contracts/validation";
|
|
4
|
+
|
|
5
|
+
export const TSX_CAPABILITY_CODES = {
|
|
6
|
+
fetch: "tsx_capability_fetch",
|
|
7
|
+
evalCall: "tsx_capability_eval",
|
|
8
|
+
functionConstructor: "tsx_capability_function_constructor",
|
|
9
|
+
dynamicImport: "tsx_capability_dynamic_import",
|
|
10
|
+
worker: "tsx_capability_worker",
|
|
11
|
+
sharedWorker: "tsx_capability_shared_worker",
|
|
12
|
+
unsupportedComputedGlobal: "tsx_capability_computed_global",
|
|
13
|
+
requireCall: "tsx_capability_require",
|
|
14
|
+
aliasOfDeniedGlobal: "tsx_capability_global_alias",
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export function locationFor(node: ts.Node, sourceFile: ts.SourceFile): string {
|
|
18
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
19
|
+
return `${line + 1}:${character + 1}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function push(
|
|
23
|
+
errors: DiscriminativeError[],
|
|
24
|
+
code: string,
|
|
25
|
+
message: string,
|
|
26
|
+
node: ts.Node,
|
|
27
|
+
sourceFile: ts.SourceFile,
|
|
28
|
+
): void {
|
|
29
|
+
errors.push({ code, message, location: locationFor(node, sourceFile) });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isDeniedGlobalName(name: string): boolean {
|
|
33
|
+
return name === "fetch";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isDeniedGlobalEval(name: string): boolean {
|
|
37
|
+
return name === "eval";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const ALIAS_DENIED_NAMES = new Set([
|
|
41
|
+
"globalThis",
|
|
42
|
+
"window",
|
|
43
|
+
"self",
|
|
44
|
+
"global",
|
|
45
|
+
"Worker",
|
|
46
|
+
"SharedWorker",
|
|
47
|
+
"Function",
|
|
48
|
+
"fetch",
|
|
49
|
+
"eval",
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
export function identifierText(node: ts.Expression): string | null {
|
|
53
|
+
if (ts.isIdentifier(node)) return node.text;
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function formatArgument(node: ts.Expression): string {
|
|
58
|
+
if (ts.isStringLiteral(node)) return JSON.stringify(node.text);
|
|
59
|
+
return "<expression>";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resolveStringFragments(node: ts.Expression): string | null {
|
|
63
|
+
if (ts.isStringLiteral(node)) return node.text;
|
|
64
|
+
if (ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
65
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
66
|
+
const left = resolveStringFragments(node.left);
|
|
67
|
+
const right = resolveStringFragments(node.right);
|
|
68
|
+
if (left !== null && right !== null) return left + right;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|