@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,627 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service router — single entrypoint mapping HTTP requests to command
|
|
3
|
+
* verbs, enveloping results, applying security guards.
|
|
4
|
+
*
|
|
5
|
+
* Guard order: Host → Bearer auth → mutation Content-Type → raw body
|
|
6
|
+
* size cap → body read/parse → dispatch. Auth happens BEFORE any
|
|
7
|
+
* body read/parse (unauth malformed body → 401, not 400). Promote
|
|
8
|
+
* accepts install OR operator bearer (constant-time). Cross-site
|
|
9
|
+
* mutation failures map to 403 (CSRF).
|
|
10
|
+
*
|
|
11
|
+
* The pieces of each guard live in sibling modules (`router-guards.ts`
|
|
12
|
+
* for body-cap + parse + status mapping, `http-utils.ts` for the
|
|
13
|
+
* envelope → Response builder, `security/host-origin.ts` for the host
|
|
14
|
+
* check + `resolveHost`, `security/auth.ts` for the bearer check).
|
|
15
|
+
* This file is intentionally the orchestration: read the function
|
|
16
|
+
* top-to-bottom and the guard order is the sequence of statements.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { errEnvelope, okEnvelope, parseEnvelope } from "../shared/contracts/envelope";
|
|
20
|
+
import {
|
|
21
|
+
CommandRequestSchema,
|
|
22
|
+
CommandResultSchema,
|
|
23
|
+
checkCommandImplemented,
|
|
24
|
+
type CommandRequest,
|
|
25
|
+
} from "../shared/contracts/commands";
|
|
26
|
+
import { FacetError } from "../shared/errors/facet-error";
|
|
27
|
+
import { join, normalize, relative } from "node:path";
|
|
28
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
29
|
+
|
|
30
|
+
import { requireAnyBearer, checkMutationSecurityHeaders } from "./security/auth";
|
|
31
|
+
import { checkHostOrigin, resolveHost, type HostOriginResult } from "./security/host-origin";
|
|
32
|
+
import { dispatch, type DispatcherDeps } from "./dispatcher";
|
|
33
|
+
import { handleStream, type RevisionBroadcaster } from "./stream";
|
|
34
|
+
import { envelopeResponse, generateRequestId, pickRequestId } from "./http-utils";
|
|
35
|
+
import {
|
|
36
|
+
INVALID_JSON,
|
|
37
|
+
RAW_BODY_CAP_BYTES,
|
|
38
|
+
parseBody,
|
|
39
|
+
readCappedBody,
|
|
40
|
+
readRequestMeta,
|
|
41
|
+
resolveGalleryRequest,
|
|
42
|
+
statusForHostCheck,
|
|
43
|
+
statusFor,
|
|
44
|
+
type RequestMeta,
|
|
45
|
+
} from "./router-guards";
|
|
46
|
+
import type { FacetLogger } from "../shared/logging/logger";
|
|
47
|
+
import { buildFrameDocument } from "../gallery-web/frame-html";
|
|
48
|
+
import { isResolvedGalleryTheme } from "../gallery-web/theme";
|
|
49
|
+
import { ARTIFACT_TYPES } from "../shared/contracts/artifact-types";
|
|
50
|
+
import { ArtifactTypeSchema } from "../shared/contracts/artifact";
|
|
51
|
+
import { latestStoredVerdict } from "./stored-verdict";
|
|
52
|
+
import { readStoredRenderEvidence } from "./export";
|
|
53
|
+
import { resolveGalleryRoot } from "../shared/config/paths";
|
|
54
|
+
|
|
55
|
+
const ROUTE_API = "/api/v1/commands";
|
|
56
|
+
const ROUTE_STREAM = "/api/v1/stream";
|
|
57
|
+
const ROUTE_GALLERY = "/gallery";
|
|
58
|
+
const ROUTE_FRAME = "/gallery/frame";
|
|
59
|
+
const ROUTE_BOOTSTRAP = "/api/v1/gallery/bootstrap";
|
|
60
|
+
const ROUTE_RELEASE = "/api/v1/gallery/release";
|
|
61
|
+
const ROUTE_SOURCE = "/api/v1/gallery/source";
|
|
62
|
+
const ROUTE_EVIDENCE = "/api/v1/gallery/evidence";
|
|
63
|
+
const FRAME_RUNTIME_PREFIX = `${ROUTE_FRAME}/runtime/`;
|
|
64
|
+
const FRAME_CHUNK_PREFIX = `${ROUTE_FRAME}/chunks/`;
|
|
65
|
+
const TIER1_TRACE = process.env.FACET_TIER1_TRACE === "1";
|
|
66
|
+
|
|
67
|
+
function traceTier1Transport(stage: string): void {
|
|
68
|
+
if (!TIER1_TRACE) return;
|
|
69
|
+
process.stderr.write(`[tier1-transport] ${stage}\n`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isFrameAssetPath(path: string): boolean {
|
|
73
|
+
return (
|
|
74
|
+
((path.startsWith(FRAME_RUNTIME_PREFIX) || path.startsWith(FRAME_CHUNK_PREFIX)) &&
|
|
75
|
+
path.endsWith(".js")) ||
|
|
76
|
+
path === `${ROUTE_FRAME}/frame.css` ||
|
|
77
|
+
path === `${ROUTE_FRAME}/artifact.css`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function startRequestHeartbeat(requestId: string, command: string): () => void {
|
|
82
|
+
if (!TIER1_TRACE) return () => {};
|
|
83
|
+
let ticks = 0;
|
|
84
|
+
const heartbeat = setInterval(() => {
|
|
85
|
+
ticks += 1;
|
|
86
|
+
traceTier1Transport(
|
|
87
|
+
`request:heartbeat tick=${ticks} command=${command} requestId=${requestId}`,
|
|
88
|
+
);
|
|
89
|
+
}, 1_000);
|
|
90
|
+
heartbeat.unref();
|
|
91
|
+
traceTier1Transport(`request:heartbeat:start command=${command} requestId=${requestId}`);
|
|
92
|
+
return () => {
|
|
93
|
+
clearInterval(heartbeat);
|
|
94
|
+
traceTier1Transport(
|
|
95
|
+
`request:heartbeat:stop ticks=${ticks} command=${command} requestId=${requestId}`,
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Re-export so `import { RAW_BODY_CAP_BYTES } from "./router"` keeps
|
|
101
|
+
// working — the constant moved to `router-guards.ts` for size reasons.
|
|
102
|
+
export { RAW_BODY_CAP_BYTES };
|
|
103
|
+
|
|
104
|
+
export interface RouterDeps extends DispatcherDeps {
|
|
105
|
+
readonly installToken: string;
|
|
106
|
+
readonly promoteToken: string | null;
|
|
107
|
+
readonly logger: FacetLogger;
|
|
108
|
+
readonly expectedHost: string | (() => string);
|
|
109
|
+
readonly ownOrigin: string | (() => string);
|
|
110
|
+
readonly startTime: number;
|
|
111
|
+
/** Per-artifact SSE fan-out the stream route registers into. */
|
|
112
|
+
readonly broadcaster: RevisionBroadcaster;
|
|
113
|
+
readonly heartbeatIntervalMs?: number;
|
|
114
|
+
readonly galleryBootstrap?: Map<
|
|
115
|
+
string,
|
|
116
|
+
{ readonly artifactId: string; readonly revisionSha: string; readonly leaseId: string }
|
|
117
|
+
>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function handleCommand(deps: RouterDeps, req: Request): Promise<Response> {
|
|
121
|
+
const startNs = performance.now();
|
|
122
|
+
const meta: RequestMeta = readRequestMeta(req);
|
|
123
|
+
const requestId = pickRequestId(null);
|
|
124
|
+
|
|
125
|
+
// 1. Host exact-match (DNS-rebinding defense). Reject missing Host.
|
|
126
|
+
const hostCheck: HostOriginResult = checkHostOrigin({
|
|
127
|
+
method: meta.method,
|
|
128
|
+
host: meta.host,
|
|
129
|
+
origin: meta.origin,
|
|
130
|
+
secFetchSite: meta.secFetchSite,
|
|
131
|
+
expectedHost: resolveHost(deps.expectedHost),
|
|
132
|
+
ownOrigin: resolveHost(deps.ownOrigin),
|
|
133
|
+
});
|
|
134
|
+
if (!hostCheck.ok) {
|
|
135
|
+
deps.logger.warn("request.rejected", {
|
|
136
|
+
reason: "host_origin",
|
|
137
|
+
errorCode: hostCheck.error.code,
|
|
138
|
+
url: meta.url,
|
|
139
|
+
});
|
|
140
|
+
return envelopeResponse(
|
|
141
|
+
errEnvelope(requestId, hostCheck.error.toBody()),
|
|
142
|
+
statusForHostCheck(hostCheck.error),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (meta.method.toUpperCase() === "GET") {
|
|
147
|
+
return envelopeResponse(
|
|
148
|
+
errEnvelope(requestId, {
|
|
149
|
+
code: "invalid_request",
|
|
150
|
+
message: "GET is not supported on the command endpoint",
|
|
151
|
+
retryable: false,
|
|
152
|
+
}),
|
|
153
|
+
405,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 2. Bearer auth BEFORE any body read. Install or operator bearer is
|
|
158
|
+
// accepted; the per-verb gate below enforces the operator requirement
|
|
159
|
+
// for promote.
|
|
160
|
+
const authCandidates: string[] = [deps.installToken];
|
|
161
|
+
if (deps.promoteToken !== null) authCandidates.push(deps.promoteToken);
|
|
162
|
+
const authResult = requireAnyBearer(meta.authorization, authCandidates);
|
|
163
|
+
if (!authResult.ok) {
|
|
164
|
+
deps.logger.warn("auth.failed", {
|
|
165
|
+
requestId,
|
|
166
|
+
errorCode: authResult.error.code,
|
|
167
|
+
});
|
|
168
|
+
return envelopeResponse(errEnvelope(requestId, authResult.error.toBody()), 401);
|
|
169
|
+
}
|
|
170
|
+
const matchedIsOperator = authResult.matchedIndex > 0; // index 0 = install, 1 = operator
|
|
171
|
+
|
|
172
|
+
// 3. Mutation Content-Type. GET already handled; mutations require JSON.
|
|
173
|
+
const contentCheck = checkMutationSecurityHeaders({
|
|
174
|
+
method: meta.method,
|
|
175
|
+
contentType: meta.contentType,
|
|
176
|
+
});
|
|
177
|
+
if (!contentCheck.ok) {
|
|
178
|
+
deps.logger.warn("request.rejected", {
|
|
179
|
+
reason: "content_type",
|
|
180
|
+
errorCode: contentCheck.error.code,
|
|
181
|
+
});
|
|
182
|
+
return envelopeResponse(errEnvelope(requestId, contentCheck.error.toBody()), 400);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 4. Read body, enforcing the raw size cap BEFORE JSON.parse.
|
|
186
|
+
let bodyText: string;
|
|
187
|
+
try {
|
|
188
|
+
bodyText = await readCappedBody(req, meta.contentLength);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
const err = FacetError.from(error);
|
|
191
|
+
deps.logger.warn("request.rejected", {
|
|
192
|
+
reason: "body_size",
|
|
193
|
+
errorCode: err.code,
|
|
194
|
+
});
|
|
195
|
+
return envelopeResponse(
|
|
196
|
+
errEnvelope(requestId, err.toBody()),
|
|
197
|
+
err.code === "payload_too_large" ? 413 : 400,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const bodyRaw = parseBody(bodyText);
|
|
201
|
+
if (bodyRaw === INVALID_JSON) {
|
|
202
|
+
const err = new FacetError("invalid_envelope", "Request body is not valid JSON", {
|
|
203
|
+
retryable: false,
|
|
204
|
+
});
|
|
205
|
+
return envelopeResponse(errEnvelope(requestId, err.toBody()), 400);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 5. Parse envelope.
|
|
209
|
+
const env = parseEnvelope(bodyRaw);
|
|
210
|
+
if (!env.ok) {
|
|
211
|
+
return envelopeResponse(errEnvelope(requestId, env.body), 400);
|
|
212
|
+
}
|
|
213
|
+
if (env.envelope.ok === false) {
|
|
214
|
+
return envelopeResponse(env.envelope, 400);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 6. Parse the command.
|
|
218
|
+
const cmdParse = CommandRequestSchema.safeParse(env.envelope.data);
|
|
219
|
+
if (!cmdParse.success) {
|
|
220
|
+
const err = new FacetError("invalid_request", "Command request failed validation", {
|
|
221
|
+
retryable: false,
|
|
222
|
+
details: { issueCount: cmdParse.error.issues.length },
|
|
223
|
+
});
|
|
224
|
+
return envelopeResponse(errEnvelope(requestId, err.toBody()), 400);
|
|
225
|
+
}
|
|
226
|
+
const command: CommandRequest = cmdParse.data;
|
|
227
|
+
|
|
228
|
+
// 7. Reserved-verb gate.
|
|
229
|
+
const reserved = checkCommandImplemented(command.command);
|
|
230
|
+
if (reserved !== null) {
|
|
231
|
+
return envelopeResponse(errEnvelope(requestId, reserved.toBody()), 400);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 8. Promote requires the operator token (constant-time compare).
|
|
235
|
+
if (command.command === "promote") {
|
|
236
|
+
if (!matchedIsOperator) {
|
|
237
|
+
const err = new FacetError("invalid_envelope", "Promote requires the operator token", {
|
|
238
|
+
retryable: false,
|
|
239
|
+
details: { reason: "operator_token_required" },
|
|
240
|
+
});
|
|
241
|
+
return envelopeResponse(errEnvelope(requestId, err.toBody()), 403);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
deps.idle.acquire(`request:${requestId}`);
|
|
246
|
+
const stopHeartbeat = startRequestHeartbeat(requestId, command.command);
|
|
247
|
+
try {
|
|
248
|
+
const result = await dispatch(deps, command, requestId);
|
|
249
|
+
traceTier1Transport(
|
|
250
|
+
`router:dispatch-result-received command=${command.command} requestId=${requestId}`,
|
|
251
|
+
);
|
|
252
|
+
let resultForWire = result;
|
|
253
|
+
if (command.command === "open") {
|
|
254
|
+
const openResult = result as {
|
|
255
|
+
readonly artifactId: string;
|
|
256
|
+
readonly revisionSha: string;
|
|
257
|
+
readonly lease: { readonly leaseId: string; readonly expiresAt: number };
|
|
258
|
+
};
|
|
259
|
+
const token = crypto.randomUUID();
|
|
260
|
+
deps.galleryBootstrap?.set(token, {
|
|
261
|
+
artifactId: openResult.artifactId,
|
|
262
|
+
revisionSha: openResult.revisionSha,
|
|
263
|
+
leaseId: openResult.lease.leaseId,
|
|
264
|
+
});
|
|
265
|
+
resultForWire = {
|
|
266
|
+
...(result as Record<string, unknown>),
|
|
267
|
+
frameUrl: `${resolveHost(deps.ownOrigin)}/gallery#bootstrap=${encodeURIComponent(token)}`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const safeResult = CommandResultSchema.parse(resultForWire);
|
|
271
|
+
traceTier1Transport(
|
|
272
|
+
`router:envelope-response:start command=${command.command} requestId=${requestId}`,
|
|
273
|
+
);
|
|
274
|
+
const elapsedMs = Math.round(performance.now() - startNs);
|
|
275
|
+
deps.logger.info("command.completed", {
|
|
276
|
+
requestId,
|
|
277
|
+
command: command.command,
|
|
278
|
+
durationMs: elapsedMs,
|
|
279
|
+
});
|
|
280
|
+
return envelopeResponse(okEnvelope(requestId, safeResult), 200);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
const facetError = FacetError.from(error);
|
|
283
|
+
const elapsedMs = Math.round(performance.now() - startNs);
|
|
284
|
+
deps.logger.warn("command.failed", {
|
|
285
|
+
requestId,
|
|
286
|
+
command: command.command,
|
|
287
|
+
errorCode: facetError.code,
|
|
288
|
+
durationMs: elapsedMs,
|
|
289
|
+
});
|
|
290
|
+
return envelopeResponse(errEnvelope(requestId, facetError.toBody()), statusFor(facetError));
|
|
291
|
+
} finally {
|
|
292
|
+
stopHeartbeat();
|
|
293
|
+
deps.idle.release(`request:${requestId}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function buildRouter(deps: RouterDeps): {
|
|
298
|
+
fetch: (req: Request) => Promise<Response> | Response;
|
|
299
|
+
} {
|
|
300
|
+
const bootstrap = new Map<
|
|
301
|
+
string,
|
|
302
|
+
{ readonly artifactId: string; readonly revisionSha: string; readonly leaseId: string }
|
|
303
|
+
>();
|
|
304
|
+
const packageRoot = join(import.meta.dir, "../..");
|
|
305
|
+
const galleryRoot = resolveGalleryRoot(packageRoot);
|
|
306
|
+
// style-src allows 'unsafe-inline' because mermaid injects its theme
|
|
307
|
+
// as a <style> element into the rendered SVG (operator-ruled posture
|
|
308
|
+
// for this local-only tool); script-src stays 'self' blob: — no
|
|
309
|
+
// script relaxation.
|
|
310
|
+
const galleryCsp =
|
|
311
|
+
"default-src 'self'; script-src 'self' blob:; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data: https:; font-src 'self' data:; frame-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'";
|
|
312
|
+
let galleryBuild: Promise<void> | null = null;
|
|
313
|
+
|
|
314
|
+
const ensureGalleryBuild = async (): Promise<void> => {
|
|
315
|
+
const requiredAssets = [
|
|
316
|
+
join(galleryRoot, "index.html"),
|
|
317
|
+
join(galleryRoot, "frame", "frame.css"),
|
|
318
|
+
join(galleryRoot, "frame", "artifact.css"),
|
|
319
|
+
...ARTIFACT_TYPES.map((artifactType) =>
|
|
320
|
+
join(galleryRoot, "frame", "runtime", `${artifactType}.js`),
|
|
321
|
+
),
|
|
322
|
+
];
|
|
323
|
+
if ((await Promise.all(requiredAssets.map((asset) => Bun.file(asset).exists()))).every(Boolean))
|
|
324
|
+
return;
|
|
325
|
+
galleryBuild ??= (async () => {
|
|
326
|
+
const process = Bun.spawn(["bun", "scripts/build-gallery.ts"], {
|
|
327
|
+
cwd: packageRoot,
|
|
328
|
+
stderr: "pipe",
|
|
329
|
+
stdout: "ignore",
|
|
330
|
+
});
|
|
331
|
+
const exitCode = await process.exited;
|
|
332
|
+
if (exitCode !== 0) {
|
|
333
|
+
const details = await new Response(process.stderr).text();
|
|
334
|
+
throw new Error(`Gallery build failed (exit ${exitCode})${details ? `: ${details}` : ""}`);
|
|
335
|
+
}
|
|
336
|
+
})().finally(() => {
|
|
337
|
+
galleryBuild = null;
|
|
338
|
+
});
|
|
339
|
+
await galleryBuild;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const galleryResponse = async (path: string): Promise<Response> => {
|
|
343
|
+
try {
|
|
344
|
+
await ensureGalleryBuild();
|
|
345
|
+
} catch (error) {
|
|
346
|
+
return new Response(error instanceof Error ? error.message : "Gallery build failed", {
|
|
347
|
+
status: 500,
|
|
348
|
+
headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" },
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
const requested = path === ROUTE_GALLERY ? "index.html" : path.replace(/^\/gallery\/?/, "");
|
|
352
|
+
return serveGalleryFile(requested, galleryCsp);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const serveGalleryFile = async (
|
|
356
|
+
requested: string,
|
|
357
|
+
csp?: string,
|
|
358
|
+
extraHeaders: Record<string, string> = {},
|
|
359
|
+
): Promise<Response> => {
|
|
360
|
+
let decoded: string;
|
|
361
|
+
try {
|
|
362
|
+
decoded = decodeURIComponent(requested);
|
|
363
|
+
} catch {
|
|
364
|
+
return new Response("Not found", { status: 404 });
|
|
365
|
+
}
|
|
366
|
+
const candidate = normalize(join(galleryRoot, decoded));
|
|
367
|
+
const rootRelative = relative(galleryRoot, candidate);
|
|
368
|
+
if (rootRelative.startsWith("..") || rootRelative.includes("/..")) {
|
|
369
|
+
return new Response("Not found", { status: 404 });
|
|
370
|
+
}
|
|
371
|
+
const file = Bun.file(candidate);
|
|
372
|
+
if (!(await file.exists())) return new Response("Not found", { status: 404 });
|
|
373
|
+
return new Response(file, {
|
|
374
|
+
status: 200,
|
|
375
|
+
headers: {
|
|
376
|
+
"cache-control": "no-store",
|
|
377
|
+
"content-type": file.type || "application/octet-stream",
|
|
378
|
+
...extraHeaders,
|
|
379
|
+
...(csp === undefined ? {} : { "content-security-policy": csp }),
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
async fetch(req: Request): Promise<Response> {
|
|
386
|
+
const url = new URL(req.url);
|
|
387
|
+
const path = url.pathname;
|
|
388
|
+
if (isFrameAssetPath(path) && req.method.toUpperCase() === "GET") {
|
|
389
|
+
try {
|
|
390
|
+
await ensureGalleryBuild();
|
|
391
|
+
} catch (error) {
|
|
392
|
+
return new Response(error instanceof Error ? error.message : "Gallery build failed", {
|
|
393
|
+
status: 500,
|
|
394
|
+
headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" },
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return serveGalleryFile(path.replace(/^\/gallery\//, ""));
|
|
398
|
+
}
|
|
399
|
+
if (path === ROUTE_FRAME && req.method.toUpperCase() === "GET") {
|
|
400
|
+
const artifactType = ArtifactTypeSchema.safeParse(url.searchParams.get("type"));
|
|
401
|
+
if (!artifactType.success) {
|
|
402
|
+
return new Response("Invalid frame artifact type", {
|
|
403
|
+
status: 400,
|
|
404
|
+
headers: {
|
|
405
|
+
"cache-control": "no-store",
|
|
406
|
+
"content-type": "text/plain; charset=utf-8",
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
const theme = url.searchParams.get("theme") ?? "dark";
|
|
411
|
+
if (!isResolvedGalleryTheme(theme)) {
|
|
412
|
+
return new Response("Invalid frame theme", {
|
|
413
|
+
status: 400,
|
|
414
|
+
headers: {
|
|
415
|
+
"cache-control": "no-store",
|
|
416
|
+
"content-type": "text/plain; charset=utf-8",
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
await ensureGalleryBuild();
|
|
422
|
+
} catch (error) {
|
|
423
|
+
return new Response(error instanceof Error ? error.message : "Gallery build failed", {
|
|
424
|
+
status: 500,
|
|
425
|
+
headers: {
|
|
426
|
+
"content-type": "text/plain; charset=utf-8",
|
|
427
|
+
"cache-control": "no-store",
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return new Response(
|
|
432
|
+
buildFrameDocument({
|
|
433
|
+
artifactType: artifactType.data,
|
|
434
|
+
runtimeUrl: `${FRAME_RUNTIME_PREFIX}${artifactType.data}.js`,
|
|
435
|
+
theme,
|
|
436
|
+
}),
|
|
437
|
+
{
|
|
438
|
+
status: 200,
|
|
439
|
+
headers: {
|
|
440
|
+
"cache-control": "no-store",
|
|
441
|
+
"content-security-policy": galleryCsp,
|
|
442
|
+
"content-type": "text/html; charset=utf-8",
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
if (
|
|
448
|
+
(path === ROUTE_GALLERY || path.startsWith(`${ROUTE_GALLERY}/`)) &&
|
|
449
|
+
req.method.toUpperCase() === "GET"
|
|
450
|
+
) {
|
|
451
|
+
return galleryResponse(path);
|
|
452
|
+
}
|
|
453
|
+
if (req.method.toUpperCase() === "GET" && path !== "/") {
|
|
454
|
+
// `ensureGalleryBuild` requires index.html to exist, so a direct
|
|
455
|
+
// navigation to /index.html (or any other .html document) reaches
|
|
456
|
+
// this fallback and must carry the same gallery CSP as /gallery —
|
|
457
|
+
// otherwise the gallery document ships without its CSP purely
|
|
458
|
+
// because of which URL served it.
|
|
459
|
+
const isDocument = /\.html$/i.test(path);
|
|
460
|
+
if (isDocument) {
|
|
461
|
+
try {
|
|
462
|
+
await ensureGalleryBuild();
|
|
463
|
+
} catch (error) {
|
|
464
|
+
return new Response(error instanceof Error ? error.message : "Gallery build failed", {
|
|
465
|
+
status: 500,
|
|
466
|
+
headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" },
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const rootAssetResponse = await serveGalleryFile(
|
|
471
|
+
path.replace(/^\//, ""),
|
|
472
|
+
isDocument ? galleryCsp : undefined,
|
|
473
|
+
{},
|
|
474
|
+
);
|
|
475
|
+
if (rootAssetResponse.status !== 404) return rootAssetResponse;
|
|
476
|
+
}
|
|
477
|
+
if (path === ROUTE_BOOTSTRAP && req.method.toUpperCase() === "POST") {
|
|
478
|
+
const body = (await req.json().catch(() => null)) as { token?: unknown } | null;
|
|
479
|
+
const token = typeof body?.token === "string" ? body.token : null;
|
|
480
|
+
const handoff = token === null ? undefined : bootstrap.get(token);
|
|
481
|
+
if (token !== null) bootstrap.delete(token);
|
|
482
|
+
const lease =
|
|
483
|
+
handoff === undefined
|
|
484
|
+
? undefined
|
|
485
|
+
: deps.leases.list().find((entry) => entry.leaseId === handoff.leaseId);
|
|
486
|
+
if (handoff === undefined || lease === undefined) {
|
|
487
|
+
return envelopeResponse(
|
|
488
|
+
errEnvelope(generateRequestId(), {
|
|
489
|
+
code: "invalid_envelope",
|
|
490
|
+
message: "Bootstrap capability is invalid or already used",
|
|
491
|
+
retryable: false,
|
|
492
|
+
}),
|
|
493
|
+
401,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
return new Response(
|
|
497
|
+
JSON.stringify({
|
|
498
|
+
authorization: `Bearer ${deps.installToken}`,
|
|
499
|
+
artifactId: handoff.artifactId,
|
|
500
|
+
revisionSha: handoff.revisionSha,
|
|
501
|
+
lease: { leaseId: lease.leaseId, expiresAt: lease.expiresAt },
|
|
502
|
+
}),
|
|
503
|
+
{
|
|
504
|
+
status: 200,
|
|
505
|
+
headers: { "content-type": "application/json", "cache-control": "no-store" },
|
|
506
|
+
},
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
if (path === ROUTE_RELEASE && req.method.toUpperCase() === "POST") {
|
|
510
|
+
const meta = readRequestMeta(req);
|
|
511
|
+
const auth = requireAnyBearer(meta.authorization, [deps.installToken]);
|
|
512
|
+
const leaseId = meta.headers.get("x-gallery-lease");
|
|
513
|
+
const artifactId = meta.headers.get("x-gallery-artifact");
|
|
514
|
+
if (!auth.ok || leaseId === null || artifactId === null) {
|
|
515
|
+
return envelopeResponse(
|
|
516
|
+
errEnvelope(generateRequestId(), {
|
|
517
|
+
code: "invalid_envelope",
|
|
518
|
+
message: "Display release requires authorization and lease headers",
|
|
519
|
+
retryable: false,
|
|
520
|
+
}),
|
|
521
|
+
401,
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
const lease = deps.leases
|
|
525
|
+
.list()
|
|
526
|
+
.find((entry) => entry.leaseId === leaseId && entry.artifactId === artifactId);
|
|
527
|
+
if (lease !== undefined) {
|
|
528
|
+
deps.leases.release(leaseId);
|
|
529
|
+
deps.idle.release(`lease:${leaseId}`);
|
|
530
|
+
}
|
|
531
|
+
return new Response(null, { status: 204, headers: { "cache-control": "no-store" } });
|
|
532
|
+
}
|
|
533
|
+
if (path === ROUTE_SOURCE && req.method.toUpperCase() === "GET") {
|
|
534
|
+
const resolved = resolveGalleryRequest(req, deps);
|
|
535
|
+
if (!resolved.ok) return resolved.response;
|
|
536
|
+
const { artifactId, revision, artifact } = resolved;
|
|
537
|
+
return new Response(
|
|
538
|
+
JSON.stringify({
|
|
539
|
+
artifactId,
|
|
540
|
+
slug: artifact.slug,
|
|
541
|
+
title: artifact.title,
|
|
542
|
+
revisionSha: revision.sha256,
|
|
543
|
+
artifactType: revision.artifactType,
|
|
544
|
+
renderer: revision.renderer,
|
|
545
|
+
source: new TextDecoder().decode(revision.source),
|
|
546
|
+
verdict: latestStoredVerdict(deps.repository, revision),
|
|
547
|
+
...(revision.artifactType === "tsx"
|
|
548
|
+
? (() => {
|
|
549
|
+
const run = deps.repository.listRenderRuns({
|
|
550
|
+
revisionId: revision.id,
|
|
551
|
+
tier: 0,
|
|
552
|
+
})[0];
|
|
553
|
+
const compiledPath = run?.compiledPath ?? null;
|
|
554
|
+
const root = deps.repository.getEvidenceRoot();
|
|
555
|
+
if (compiledPath === null || root === undefined) return {};
|
|
556
|
+
try {
|
|
557
|
+
const candidate = realpathSync(compiledPath);
|
|
558
|
+
const evidenceRoot = realpathSync(root);
|
|
559
|
+
const rootRelative = relative(evidenceRoot, candidate);
|
|
560
|
+
if (
|
|
561
|
+
rootRelative.length === 0 ||
|
|
562
|
+
rootRelative === ".." ||
|
|
563
|
+
rootRelative.startsWith("../")
|
|
564
|
+
) {
|
|
565
|
+
return {};
|
|
566
|
+
}
|
|
567
|
+
return {
|
|
568
|
+
renderBytesBase64: Buffer.from(readFileSync(candidate)).toString("base64"),
|
|
569
|
+
execution: revision.execution ?? "static",
|
|
570
|
+
};
|
|
571
|
+
} catch {
|
|
572
|
+
return {};
|
|
573
|
+
}
|
|
574
|
+
})()
|
|
575
|
+
: {}),
|
|
576
|
+
}),
|
|
577
|
+
{
|
|
578
|
+
status: 200,
|
|
579
|
+
headers: { "content-type": "application/json", "cache-control": "no-store" },
|
|
580
|
+
},
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
if (path === ROUTE_EVIDENCE && req.method.toUpperCase() === "GET") {
|
|
584
|
+
const resolved = resolveGalleryRequest(req, deps);
|
|
585
|
+
if (!resolved.ok) return resolved.response;
|
|
586
|
+
const { revision, artifact } = resolved;
|
|
587
|
+
try {
|
|
588
|
+
const evidence = readStoredRenderEvidence({
|
|
589
|
+
repository: deps.repository,
|
|
590
|
+
artifact,
|
|
591
|
+
revision,
|
|
592
|
+
});
|
|
593
|
+
return new Response(evidence.bytes, {
|
|
594
|
+
status: 200,
|
|
595
|
+
headers: { "content-type": evidence.mediaType, "cache-control": "no-store" },
|
|
596
|
+
});
|
|
597
|
+
} catch (error) {
|
|
598
|
+
if (error instanceof FacetError && error.code === "evidence_unavailable") {
|
|
599
|
+
return envelopeResponse(errEnvelope(generateRequestId(), error.toBody()), 404);
|
|
600
|
+
}
|
|
601
|
+
throw error;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (path === ROUTE_API) return handleCommand({ ...deps, galleryBootstrap: bootstrap }, req);
|
|
605
|
+
if (path === ROUTE_STREAM) {
|
|
606
|
+
const meta = readRequestMeta(req);
|
|
607
|
+
return handleStream(deps, {
|
|
608
|
+
url: meta.url,
|
|
609
|
+
method: meta.method,
|
|
610
|
+
host: meta.host,
|
|
611
|
+
origin: meta.origin,
|
|
612
|
+
secFetchSite: meta.secFetchSite,
|
|
613
|
+
authorization: meta.authorization,
|
|
614
|
+
headers: meta.headers,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
return envelopeResponse(
|
|
618
|
+
errEnvelope(generateRequestId(), {
|
|
619
|
+
code: "invalid_request",
|
|
620
|
+
message: "Unknown route",
|
|
621
|
+
retryable: false,
|
|
622
|
+
}),
|
|
623
|
+
404,
|
|
624
|
+
);
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
}
|