@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,717 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Pure handler: maps a parsed-and-validated `CommandRequest` to its
|
|
5
|
+
* repository call, returning the result body that the envelope wrapper
|
|
6
|
+
* will hand back to the caller. The dispatcher never touches HTTP
|
|
7
|
+
* concerns (headers, status codes) — those live in router.ts.
|
|
8
|
+
*
|
|
9
|
+
* Errors are surfaced as `FacetError` so the router's envelope helper
|
|
10
|
+
* can map them to the typed error response.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
checkArtifactTypeSupported,
|
|
15
|
+
checkExecutionSupported,
|
|
16
|
+
checkRendererSupported,
|
|
17
|
+
normalizeReadBackTier,
|
|
18
|
+
type ArtifactEnvelope,
|
|
19
|
+
type CommandRequest,
|
|
20
|
+
} from "../shared/contracts/commands";
|
|
21
|
+
import type { Artifact, ArtifactType, RenderRun, Revision } from "../shared/contracts/artifact";
|
|
22
|
+
import { ArtifactTypeSchema } from "../shared/contracts/artifact";
|
|
23
|
+
import { RevisionCommittedEventSchema } from "../shared/contracts/events";
|
|
24
|
+
import {
|
|
25
|
+
Tier0InputSchema,
|
|
26
|
+
Tier0ResultSchema,
|
|
27
|
+
Tier0WorkerResultSchema,
|
|
28
|
+
Tier1InputSchema,
|
|
29
|
+
Tier1ResultSchema,
|
|
30
|
+
VerdictObservedSchema,
|
|
31
|
+
VerdictSchema,
|
|
32
|
+
type Tier0Input,
|
|
33
|
+
type Tier0WorkerResult,
|
|
34
|
+
type Tier0Runner,
|
|
35
|
+
type Tier1Input,
|
|
36
|
+
type Tier1Result,
|
|
37
|
+
type Tier1Runner,
|
|
38
|
+
type InsecureLevel,
|
|
39
|
+
type InsecureMarker,
|
|
40
|
+
type TsxExecutionMode,
|
|
41
|
+
type Verdict,
|
|
42
|
+
} from "../shared/contracts/validation";
|
|
43
|
+
import { FacetError } from "../shared/errors/facet-error";
|
|
44
|
+
import { SOURCE_CAP_BYTES, TIER1_PINNED_VERSION } from "../shared/config/limits";
|
|
45
|
+
import { computeLexicalExpectations } from "./lexical/expectations";
|
|
46
|
+
import { enrichVerdict, insecureMarker } from "./verdict-enrichment";
|
|
47
|
+
import { exportStoredRender, exportStoredSource } from "./export";
|
|
48
|
+
import { verdictFromStoredRun } from "./stored-verdict";
|
|
49
|
+
|
|
50
|
+
import type { GalleryLeaseManager } from "./security/leases";
|
|
51
|
+
import type { IdleController } from "./lifecycle/idle-controller";
|
|
52
|
+
import type { ArtifactRepository } from "./store/repository";
|
|
53
|
+
import { ensureRunEvidenceDirectory } from "./store/evidence-retention";
|
|
54
|
+
import { readFileSync } from "node:fs";
|
|
55
|
+
import { join } from "node:path";
|
|
56
|
+
|
|
57
|
+
const TIER1_TRACE = process.env.FACET_TIER1_TRACE === "1";
|
|
58
|
+
|
|
59
|
+
function traceTier1Transport(stage: string): void {
|
|
60
|
+
if (!TIER1_TRACE) return;
|
|
61
|
+
process.stderr.write(`[tier1-transport] ${stage}\n`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Tier 0 runner contract lives in `shared/contracts/validation.ts` so
|
|
66
|
+
* the service can depend on it as a TYPE without importing the
|
|
67
|
+
* implementation in `src/validation/`. The default runner is built by
|
|
68
|
+
* callers (`src/cli/`, integration tests) and injected; this keeps the
|
|
69
|
+
* service byte-dumb AND the boundary checker clean.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
export interface DispatcherDeps {
|
|
73
|
+
readonly insecureLevel: InsecureLevel;
|
|
74
|
+
readonly insecureReason: string | null;
|
|
75
|
+
readonly repository: ArtifactRepository;
|
|
76
|
+
readonly leases: GalleryLeaseManager;
|
|
77
|
+
readonly idle: IdleController;
|
|
78
|
+
readonly tier0Runner: Tier0Runner;
|
|
79
|
+
/** Optional Tier 1 verifier, invoked only by explicit visual read-back. */
|
|
80
|
+
readonly tier1Runner: Tier1Runner | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Write-path seam: called AFTER the revision is committed AND its
|
|
83
|
+
* Tier 0 run is recorded, with the
|
|
84
|
+
* canonical `revision:committed` event. The server wires this to
|
|
85
|
+
* the SSE broadcaster so gallery leases see the revision land.
|
|
86
|
+
*/
|
|
87
|
+
readonly onPublished?: (event: {
|
|
88
|
+
readonly type: "revision:committed";
|
|
89
|
+
readonly artifactId: string;
|
|
90
|
+
readonly revisionSha: string;
|
|
91
|
+
readonly revisionNumber: number;
|
|
92
|
+
readonly artifactType: ArtifactType;
|
|
93
|
+
readonly at: string;
|
|
94
|
+
}) => void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function mapArtifact(a: Artifact): ArtifactEnvelope {
|
|
98
|
+
return {
|
|
99
|
+
id: a.id,
|
|
100
|
+
projectId: a.projectId,
|
|
101
|
+
slug: a.slug,
|
|
102
|
+
title: a.title,
|
|
103
|
+
createdAt: a.createdAt,
|
|
104
|
+
updatedAt: a.updatedAt,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildVerdict(input: {
|
|
109
|
+
artifactId: string;
|
|
110
|
+
revisionSha: string;
|
|
111
|
+
tier: 0 | 1;
|
|
112
|
+
status: string;
|
|
113
|
+
observed: unknown;
|
|
114
|
+
insecure?: InsecureMarker;
|
|
115
|
+
screenshotError?: unknown;
|
|
116
|
+
/**
|
|
117
|
+
* Passed only when the artifact is a TSX revision (static or interactive).
|
|
118
|
+
* Non-TSX verdicts carry NO
|
|
119
|
+
* execution field — the field is absent, not null, so the wire
|
|
120
|
+
* form for non-TSX stays byte-identical to the pre-arc shape.
|
|
121
|
+
*/
|
|
122
|
+
execution?: TsxExecutionMode;
|
|
123
|
+
}): Verdict {
|
|
124
|
+
const observed = VerdictObservedSchema.parse(input.observed);
|
|
125
|
+
return VerdictSchema.parse({
|
|
126
|
+
status: input.status,
|
|
127
|
+
tier: input.tier,
|
|
128
|
+
artifactId: input.artifactId,
|
|
129
|
+
revisionSha: input.revisionSha,
|
|
130
|
+
observed,
|
|
131
|
+
...(input.insecure !== undefined ? { insecure: input.insecure } : {}),
|
|
132
|
+
...(input.screenshotError !== undefined ? { screenshotError: input.screenshotError } : {}),
|
|
133
|
+
...(input.execution !== undefined ? { execution: input.execution } : {}),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Run the Tier 0 worker, mapping a thrown `FacetError` to a synthetic
|
|
139
|
+
* Tier0Result with `status: "error"`. The wire response must still be
|
|
140
|
+
* a publish envelope (the revision IS committed), so the run is
|
|
141
|
+
* recorded as an error-tier observation rather than as a publish
|
|
142
|
+
* failure. The original error code is surfaced via
|
|
143
|
+
* `discriminativeErrors[].code` so the read-back verdict carries the
|
|
144
|
+
* typed reason.
|
|
145
|
+
*/
|
|
146
|
+
async function runTier0Safe(runner: Tier0Runner, input: Tier0Input): Promise<Tier0WorkerResult> {
|
|
147
|
+
try {
|
|
148
|
+
return await runner(input);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
const facet = FacetError.from(error);
|
|
151
|
+
return Tier0WorkerResultSchema.parse({
|
|
152
|
+
tier: 0,
|
|
153
|
+
status: "error",
|
|
154
|
+
revisionSha: input.revisionSha,
|
|
155
|
+
expected: input.lexical,
|
|
156
|
+
observed: {
|
|
157
|
+
rendererRootSvgCount: 0,
|
|
158
|
+
graphCount: 0,
|
|
159
|
+
mermaidNodeCount: 0,
|
|
160
|
+
visibleSvgCount: 0,
|
|
161
|
+
opaqueRegionCount: 0,
|
|
162
|
+
externalImageCount: 0,
|
|
163
|
+
viewBoxes: [],
|
|
164
|
+
errorCount: 1,
|
|
165
|
+
discriminativeErrors: [{ code: facet.code, message: facet.message }],
|
|
166
|
+
},
|
|
167
|
+
...(input.execution === undefined ? {} : { execution: input.execution }),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function retainCompiledEvidence(
|
|
173
|
+
repository: ArtifactRepository,
|
|
174
|
+
artifactId: string,
|
|
175
|
+
revisionSha: string,
|
|
176
|
+
compiled: Tier0WorkerResult["compiled"],
|
|
177
|
+
): Promise<string | null> {
|
|
178
|
+
if (compiled === undefined) return null;
|
|
179
|
+
const bytes = Uint8Array.from(Buffer.from(compiled.bytesBase64, "base64"));
|
|
180
|
+
const actualSha = new Bun.CryptoHasher("sha256");
|
|
181
|
+
actualSha.update(bytes);
|
|
182
|
+
const actualSha256 = actualSha.digest("hex");
|
|
183
|
+
if (actualSha256 !== compiled.sha256) {
|
|
184
|
+
throw new FacetError("tier0_protocol_error", "Compiled TSX bytes failed SHA-256 validation", {
|
|
185
|
+
retryable: false,
|
|
186
|
+
details: { expectedSha256: compiled.sha256, actualSha256 },
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const evidenceRoot = repository.getEvidenceRoot();
|
|
190
|
+
if (evidenceRoot === undefined) return null;
|
|
191
|
+
const runId = crypto.randomUUID();
|
|
192
|
+
const evidence = ensureRunEvidenceDirectory({ evidenceRoot, artifactId, revisionSha, runId });
|
|
193
|
+
const path = join(
|
|
194
|
+
evidence.directory,
|
|
195
|
+
compiled.mediaType === "text/html" ? "compiled.html" : "compiled.js",
|
|
196
|
+
);
|
|
197
|
+
await Bun.write(path, bytes);
|
|
198
|
+
return path;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Map a Tier 1 runner failure into a synthetic Tier1Result with
|
|
203
|
+
* `status: "error"`. A failed launch is distinct from a rendered
|
|
204
|
+
* verdict failure, so callers can tell an actionable environment fault
|
|
205
|
+
* from an observed page defect.
|
|
206
|
+
*/
|
|
207
|
+
async function runTier1Safe(
|
|
208
|
+
runner: Tier1Runner | undefined,
|
|
209
|
+
input: Tier1Input,
|
|
210
|
+
artifactId: string,
|
|
211
|
+
): Promise<Tier1Result> {
|
|
212
|
+
try {
|
|
213
|
+
if (runner === undefined) {
|
|
214
|
+
throw new FacetError("tier1_unavailable", "Tier 1 runner is not configured", {
|
|
215
|
+
retryable: false,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return Tier1ResultSchema.parse(await runner(input));
|
|
219
|
+
} catch (error) {
|
|
220
|
+
return tier1Failure(input, artifactId, error);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function tier1Failure(input: Tier1Input, artifactId: string, error: unknown): Tier1Result {
|
|
225
|
+
const facet = FacetError.from(error);
|
|
226
|
+
return Tier1ResultSchema.parse({
|
|
227
|
+
tier: 1,
|
|
228
|
+
status: "error",
|
|
229
|
+
artifactId,
|
|
230
|
+
revisionSha: input.revisionSha,
|
|
231
|
+
expected: input.lexical,
|
|
232
|
+
observed: {
|
|
233
|
+
rendererRootSvgCount: 0,
|
|
234
|
+
graphCount: 0,
|
|
235
|
+
mermaidNodeCount: 0,
|
|
236
|
+
visibleSvgCount: 0,
|
|
237
|
+
opaqueRegionCount: 0,
|
|
238
|
+
externalImageCount: 0,
|
|
239
|
+
viewBoxes: [],
|
|
240
|
+
errorCount: 1,
|
|
241
|
+
discriminativeErrors: [{ code: facet.code, message: facet.message }],
|
|
242
|
+
},
|
|
243
|
+
screenshotPath: null,
|
|
244
|
+
consolePath: null,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function tier1InputForRevision(
|
|
249
|
+
revision: Revision,
|
|
250
|
+
tier0Run: RenderRun,
|
|
251
|
+
evidenceDir: string | undefined,
|
|
252
|
+
): Tier1Input {
|
|
253
|
+
const compiledPath = tier0Run.compiledPath ?? null;
|
|
254
|
+
const source =
|
|
255
|
+
revision.artifactType === "tsx" && compiledPath !== null
|
|
256
|
+
? new Uint8Array(readFileSync(compiledPath))
|
|
257
|
+
: new Uint8Array(revision.source);
|
|
258
|
+
return Tier1InputSchema.parse({
|
|
259
|
+
revisionSha: revision.sha256,
|
|
260
|
+
artifactType: revision.artifactType,
|
|
261
|
+
renderer: revision.renderer,
|
|
262
|
+
source,
|
|
263
|
+
lexical: JSON.parse(tier0Run.expectedJson),
|
|
264
|
+
...(revision.artifactType === "tsx" ? { execution: revision.execution ?? "static" } : {}),
|
|
265
|
+
launcherVersion: TIER1_PINNED_VERSION,
|
|
266
|
+
networkNamespace: "facet-tier1-egress-isolated",
|
|
267
|
+
// Thread the canonical evidence root through to the Tier 1 runner so
|
|
268
|
+
// its screenshot/console evidence lands in the SAME root the service
|
|
269
|
+
// (and the parent CLI) agreed on — otherwise the runner falls back to
|
|
270
|
+
// `computeFacetPaths().evidence` and re-introduces the XDG divergence.
|
|
271
|
+
...(evidenceDir !== undefined ? { evidenceDir } : {}),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function recordTier1Run(
|
|
276
|
+
deps: DispatcherDeps,
|
|
277
|
+
revision: Revision,
|
|
278
|
+
result: Tier1Result,
|
|
279
|
+
): Tier1Result {
|
|
280
|
+
const execution = revision.artifactType === "tsx" ? (revision.execution ?? "static") : undefined;
|
|
281
|
+
const enriched = Tier1ResultSchema.parse(
|
|
282
|
+
enrichVerdict(
|
|
283
|
+
result,
|
|
284
|
+
revision.artifactId,
|
|
285
|
+
revision.sha256,
|
|
286
|
+
insecureMarker(deps.insecureLevel, deps.insecureReason),
|
|
287
|
+
execution,
|
|
288
|
+
),
|
|
289
|
+
);
|
|
290
|
+
deps.repository.recordRenderRun({
|
|
291
|
+
revisionId: revision.id,
|
|
292
|
+
tier: 1,
|
|
293
|
+
status: enriched.status,
|
|
294
|
+
expected: enriched.expected,
|
|
295
|
+
observed: enriched.observed,
|
|
296
|
+
...(enriched.screenshotPath !== null ? { screenshotPath: enriched.screenshotPath } : {}),
|
|
297
|
+
...(enriched.screenshotFormat !== undefined
|
|
298
|
+
? { screenshotFormat: enriched.screenshotFormat }
|
|
299
|
+
: {}),
|
|
300
|
+
...(enriched.consolePath !== null ? { consolePath: enriched.consolePath } : {}),
|
|
301
|
+
...(enriched.screenshotError !== undefined
|
|
302
|
+
? { screenshotError: enriched.screenshotError }
|
|
303
|
+
: {}),
|
|
304
|
+
insecure: enriched.insecure ?? null,
|
|
305
|
+
});
|
|
306
|
+
return enriched;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function dispatch(
|
|
310
|
+
deps: DispatcherDeps,
|
|
311
|
+
command: CommandRequest,
|
|
312
|
+
requestId: string,
|
|
313
|
+
): Promise<unknown> {
|
|
314
|
+
switch (command.command) {
|
|
315
|
+
case "create": {
|
|
316
|
+
const project = deps.repository.getOrCreateProjectById(command.projectId, "/facet");
|
|
317
|
+
const artifact = deps.repository.createArtifact({
|
|
318
|
+
projectId: project.id,
|
|
319
|
+
slug: command.slug,
|
|
320
|
+
title: command.title,
|
|
321
|
+
});
|
|
322
|
+
return { command: "create", requestId, artifact: mapArtifact(artifact) };
|
|
323
|
+
}
|
|
324
|
+
case "publish": {
|
|
325
|
+
const unsupported = checkArtifactTypeSupported(command.artifactType);
|
|
326
|
+
if (unsupported !== null) throw unsupported;
|
|
327
|
+
const rendererError = checkRendererSupported(command.artifactType, command.renderer);
|
|
328
|
+
if (rendererError !== null) throw rendererError;
|
|
329
|
+
const executionError = checkExecutionSupported(command.artifactType, command.execution);
|
|
330
|
+
if (executionError !== null) throw executionError;
|
|
331
|
+
// The reserved-type gate above rejected `html`; re-parse so the
|
|
332
|
+
// rest of the publish path holds the narrowed supported type.
|
|
333
|
+
const artifactType: ArtifactType = ArtifactTypeSchema.parse(command.artifactType);
|
|
334
|
+
// Decode the base64 string into a Uint8Array. The schema validates
|
|
335
|
+
// base64 syntax; here we enforce the SOURCE_CAP_BYTES on the
|
|
336
|
+
// decoded length BEFORE any further work, throwing a typed
|
|
337
|
+
// payload_too_large so the wire response is 413 (not 400).
|
|
338
|
+
const bytes = Uint8Array.from(Buffer.from(command.bytes, "base64"));
|
|
339
|
+
if (bytes.byteLength > SOURCE_CAP_BYTES) {
|
|
340
|
+
throw new FacetError("payload_too_large", "Publish bytes exceed SOURCE_CAP_BYTES", {
|
|
341
|
+
retryable: false,
|
|
342
|
+
details: { sizeBytes: bytes.byteLength, capBytes: SOURCE_CAP_BYTES },
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
// D2: TSX execution mode is declared on the request and stored
|
|
346
|
+
// on the revision row. Non-TSX artifacts carry 'static' on disk
|
|
347
|
+
// (the canonical default) and the wire form simply omits the
|
|
348
|
+
// field; TSX artifacts carry the declared value end-to-end.
|
|
349
|
+
const executionMode = artifactType === "tsx" ? (command.execution ?? "static") : "static";
|
|
350
|
+
if (deps.repository.getArtifactById(command.artifactId) === null) {
|
|
351
|
+
throw new FacetError("artifact_not_found", "Artifact not found", {
|
|
352
|
+
retryable: false,
|
|
353
|
+
details: { artifactId: command.artifactId },
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
const revision = deps.repository.publishRevision({
|
|
357
|
+
artifactId: command.artifactId,
|
|
358
|
+
artifactType,
|
|
359
|
+
renderer: command.renderer,
|
|
360
|
+
source: bytes,
|
|
361
|
+
...(command.note !== undefined ? { note: command.note } : {}),
|
|
362
|
+
...(command.parentRevisionId !== undefined
|
|
363
|
+
? { parentRevisionId: command.parentRevisionId }
|
|
364
|
+
: {}),
|
|
365
|
+
execution: executionMode,
|
|
366
|
+
});
|
|
367
|
+
// 2. Run Tier 0 over the SAME immutable bytes — never re-decode
|
|
368
|
+
// from the wire or substitute. The runner lives in
|
|
369
|
+
// src/validation (netns'd subprocess); only its contract type
|
|
370
|
+
// is visible to the service. A thrown FacetError here (unshare
|
|
371
|
+
// unavailable, worker died, protocol error, …) aborts the
|
|
372
|
+
// publish AFTER the revision is committed; we still record the
|
|
373
|
+
// partial run so the wire response can carry a verdict-shaped
|
|
374
|
+
// body. A non-ok Tier0Result is normal: the parser rejected the
|
|
375
|
+
// artifact and we persist that decision as a render_run.
|
|
376
|
+
const lexical = computeLexicalExpectations(bytes, artifactType, command.renderer);
|
|
377
|
+
const legacyCounters =
|
|
378
|
+
artifactType === "html"
|
|
379
|
+
? {
|
|
380
|
+
rendererRootSvgCount: 0,
|
|
381
|
+
mermaidNodeCount: 0,
|
|
382
|
+
visibleSvgCount: 0,
|
|
383
|
+
opaqueRegionCount: 0,
|
|
384
|
+
externalImageCount: 0,
|
|
385
|
+
}
|
|
386
|
+
: {
|
|
387
|
+
rendererRootSvgCount: lexical.expectedRendererRoots,
|
|
388
|
+
mermaidNodeCount: lexical.mermaidNodeCount,
|
|
389
|
+
visibleSvgCount: 0,
|
|
390
|
+
opaqueRegionCount: lexical.expectedOpaqueRegions,
|
|
391
|
+
externalImageCount: 0,
|
|
392
|
+
};
|
|
393
|
+
const tier0Input = Tier0InputSchema.parse({
|
|
394
|
+
revisionSha: revision.sha256,
|
|
395
|
+
artifactType,
|
|
396
|
+
renderer: command.renderer,
|
|
397
|
+
source: bytes,
|
|
398
|
+
lexical: legacyCounters,
|
|
399
|
+
...(artifactType === "tsx" ? { execution: executionMode } : {}),
|
|
400
|
+
});
|
|
401
|
+
const insecure = insecureMarker(deps.insecureLevel, deps.insecureReason);
|
|
402
|
+
// Execution is revision metadata for TSX only. Other types omit the
|
|
403
|
+
// field rather than serializing null; TSX uses its declared mode.
|
|
404
|
+
const verdictExecution: TsxExecutionMode | undefined =
|
|
405
|
+
artifactType === "tsx" ? executionMode : undefined;
|
|
406
|
+
if (deps.insecureLevel === 3) {
|
|
407
|
+
const verdict = enrichVerdict(
|
|
408
|
+
buildVerdict({
|
|
409
|
+
artifactId: command.artifactId,
|
|
410
|
+
revisionSha: revision.sha256,
|
|
411
|
+
tier: 0,
|
|
412
|
+
status: "insecure:unvalidated",
|
|
413
|
+
observed: {
|
|
414
|
+
rendererRootSvgCount: 0,
|
|
415
|
+
graphCount: 0,
|
|
416
|
+
mermaidNodeCount: 0,
|
|
417
|
+
visibleSvgCount: 0,
|
|
418
|
+
opaqueRegionCount: 0,
|
|
419
|
+
externalImageCount: 0,
|
|
420
|
+
errorCount: 0,
|
|
421
|
+
},
|
|
422
|
+
...(verdictExecution !== undefined ? { execution: verdictExecution } : {}),
|
|
423
|
+
}),
|
|
424
|
+
command.artifactId,
|
|
425
|
+
revision.sha256,
|
|
426
|
+
insecure,
|
|
427
|
+
verdictExecution,
|
|
428
|
+
);
|
|
429
|
+
deps.repository.recordRenderRun({
|
|
430
|
+
revisionId: revision.id,
|
|
431
|
+
tier: 0,
|
|
432
|
+
status: verdict.status,
|
|
433
|
+
expected: tier0Input.lexical,
|
|
434
|
+
observed: verdict.observed,
|
|
435
|
+
insecure: verdict.insecure ?? null,
|
|
436
|
+
});
|
|
437
|
+
deps.onPublished?.(
|
|
438
|
+
RevisionCommittedEventSchema.parse({
|
|
439
|
+
type: "revision:committed",
|
|
440
|
+
artifactId: command.artifactId,
|
|
441
|
+
revisionSha: revision.sha256,
|
|
442
|
+
revisionNumber: revision.revisionNumber,
|
|
443
|
+
artifactType,
|
|
444
|
+
at: new Date().toISOString(),
|
|
445
|
+
}),
|
|
446
|
+
);
|
|
447
|
+
const { source: _source, ...envelope } = revision;
|
|
448
|
+
void _source;
|
|
449
|
+
return {
|
|
450
|
+
command: "publish",
|
|
451
|
+
requestId,
|
|
452
|
+
revision: envelope,
|
|
453
|
+
verdict,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
const tier0Result = await runTier0Safe(deps.tier0Runner, tier0Input);
|
|
457
|
+
const compiledPath =
|
|
458
|
+
artifactType === "tsx"
|
|
459
|
+
? await retainCompiledEvidence(
|
|
460
|
+
deps.repository,
|
|
461
|
+
command.artifactId,
|
|
462
|
+
revision.sha256,
|
|
463
|
+
tier0Result.compiled,
|
|
464
|
+
)
|
|
465
|
+
: null;
|
|
466
|
+
// 3. Bind the verdict to (artifactId, revisionSha) via the
|
|
467
|
+
// canonical render_run row so read-back returns it later.
|
|
468
|
+
const enriched = Tier0ResultSchema.parse(
|
|
469
|
+
enrichVerdict(tier0Result, command.artifactId, revision.sha256, insecure, verdictExecution),
|
|
470
|
+
);
|
|
471
|
+
deps.repository.recordRenderRun({
|
|
472
|
+
revisionId: revision.id,
|
|
473
|
+
tier: 0,
|
|
474
|
+
status: enriched.status,
|
|
475
|
+
expected: enriched.expected,
|
|
476
|
+
observed: enriched.observed,
|
|
477
|
+
insecure: enriched.insecure ?? null,
|
|
478
|
+
...(compiledPath === null ? {} : { compiledPath }),
|
|
479
|
+
});
|
|
480
|
+
// 4. Write-path SSE seam: the revision is committed and its Tier 0
|
|
481
|
+
// verdict is recorded, so gallery streams bound to this
|
|
482
|
+
// artifact may now learn the exact revision to fetch + swap to.
|
|
483
|
+
deps.onPublished?.(
|
|
484
|
+
RevisionCommittedEventSchema.parse({
|
|
485
|
+
type: "revision:committed",
|
|
486
|
+
artifactId: command.artifactId,
|
|
487
|
+
revisionSha: revision.sha256,
|
|
488
|
+
revisionNumber: revision.revisionNumber,
|
|
489
|
+
artifactType,
|
|
490
|
+
at: new Date().toISOString(),
|
|
491
|
+
}),
|
|
492
|
+
);
|
|
493
|
+
const { source: _source, ...envelope } = revision;
|
|
494
|
+
void _source;
|
|
495
|
+
return {
|
|
496
|
+
command: "publish",
|
|
497
|
+
requestId,
|
|
498
|
+
revision: envelope,
|
|
499
|
+
verdict: enriched,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
case "list": {
|
|
503
|
+
const project = deps.repository.getOrCreateProjectById(command.projectId, "/facet");
|
|
504
|
+
const artifacts = deps.repository.listArtifacts({
|
|
505
|
+
projectId: project.id,
|
|
506
|
+
...(command.slugPrefix !== undefined ? { slugPrefix: command.slugPrefix } : {}),
|
|
507
|
+
...(command.limit !== undefined ? { limit: command.limit } : {}),
|
|
508
|
+
});
|
|
509
|
+
return {
|
|
510
|
+
command: "list",
|
|
511
|
+
requestId,
|
|
512
|
+
artifacts: artifacts.map(mapArtifact),
|
|
513
|
+
nextCursor: null,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
case "readBack": {
|
|
517
|
+
const revision =
|
|
518
|
+
command.revisionSha === undefined
|
|
519
|
+
? deps.repository.getLatestRevision(command.artifactId)
|
|
520
|
+
: deps.repository.getRevisionBySha(command.artifactId, command.revisionSha);
|
|
521
|
+
if (revision === null) {
|
|
522
|
+
throw new FacetError("revision_not_found", "Revision not found", {
|
|
523
|
+
retryable: false,
|
|
524
|
+
details: {
|
|
525
|
+
artifactId: command.artifactId,
|
|
526
|
+
...(command.revisionSha === undefined ? {} : { revisionSha: command.revisionSha }),
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
const tier = normalizeReadBackTier(command.tier);
|
|
531
|
+
let runs = deps.repository.listRenderRuns({ revisionId: revision.id, tier });
|
|
532
|
+
if (tier === 1 && runs.length === 0) {
|
|
533
|
+
const tier0Run = deps.repository.listRenderRuns({ revisionId: revision.id, tier: 0 })[0];
|
|
534
|
+
if (tier0Run === undefined) {
|
|
535
|
+
throw new FacetError("tier1_unavailable", "Tier 1 requires a Tier 0 render run", {
|
|
536
|
+
retryable: false,
|
|
537
|
+
details: { revisionId: revision.id },
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
const tier1Input = tier1InputForRevision(
|
|
541
|
+
revision,
|
|
542
|
+
tier0Run,
|
|
543
|
+
deps.repository.getEvidenceRoot(),
|
|
544
|
+
);
|
|
545
|
+
const tier1Result =
|
|
546
|
+
revision.artifactType === "tsx" && tier0Run.compiledPath === null
|
|
547
|
+
? tier1Failure(
|
|
548
|
+
tier1Input,
|
|
549
|
+
revision.artifactId,
|
|
550
|
+
new FacetError(
|
|
551
|
+
"tier1_unavailable",
|
|
552
|
+
"Tier 1 requires compiled TSX evidence from a successful Tier 0 run",
|
|
553
|
+
{ retryable: false },
|
|
554
|
+
),
|
|
555
|
+
)
|
|
556
|
+
: await runTier1Safe(deps.tier1Runner, tier1Input, revision.artifactId);
|
|
557
|
+
recordTier1Run(deps, revision, tier1Result);
|
|
558
|
+
runs = deps.repository.listRenderRuns({ revisionId: revision.id, tier });
|
|
559
|
+
}
|
|
560
|
+
if (runs.length === 0) {
|
|
561
|
+
throw new FacetError("revision_not_found", "No render runs recorded for revision", {
|
|
562
|
+
retryable: false,
|
|
563
|
+
details: { revisionId: revision.id, tier },
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
// Single source of truth for cross-boundary verdict reconstruction:
|
|
567
|
+
// `verdictFromStoredRun` handles the tolerant observed read,
|
|
568
|
+
// the execution marker for TSX revisions, and the
|
|
569
|
+
// insecure/screenshotError conditional-spreads. Re-deriving
|
|
570
|
+
// any of these inline drifts; the export sidecar and the
|
|
571
|
+
// gallery router use this helper, and the read-back route must
|
|
572
|
+
// use it too.
|
|
573
|
+
const verdict = verdictFromStoredRun(revision, runs[0]!);
|
|
574
|
+
traceTier1Transport(`readback:build-complete status=${verdict.status}`);
|
|
575
|
+
return { command: "readBack", requestId, renderer: revision.renderer, verdict };
|
|
576
|
+
}
|
|
577
|
+
case "status": {
|
|
578
|
+
const counts =
|
|
579
|
+
command.artifactId === undefined
|
|
580
|
+
? undefined
|
|
581
|
+
: deps.repository.statusForArtifact({ artifactId: command.artifactId });
|
|
582
|
+
const latestRevision =
|
|
583
|
+
command.artifactId === undefined
|
|
584
|
+
? undefined
|
|
585
|
+
: deps.repository.getLatestRevision(command.artifactId);
|
|
586
|
+
return {
|
|
587
|
+
command: "status",
|
|
588
|
+
requestId,
|
|
589
|
+
state: "active",
|
|
590
|
+
...(command.artifactId === undefined ? {} : { artifactId: command.artifactId }),
|
|
591
|
+
...(counts === undefined
|
|
592
|
+
? {}
|
|
593
|
+
: {
|
|
594
|
+
revisionCount: counts.revisionCount,
|
|
595
|
+
pinnedCount: counts.pinnedCount,
|
|
596
|
+
templateCount: counts.templateCount,
|
|
597
|
+
...(latestRevision === undefined || latestRevision === null
|
|
598
|
+
? {}
|
|
599
|
+
: { latestRevisionSha: latestRevision.sha256 }),
|
|
600
|
+
}),
|
|
601
|
+
activeLeases: deps.leases.list().length,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
case "open": {
|
|
605
|
+
const revision =
|
|
606
|
+
command.revisionSha === undefined
|
|
607
|
+
? deps.repository.getLatestRevision(command.artifactId)
|
|
608
|
+
: deps.repository.getRevisionBySha(command.artifactId, command.revisionSha);
|
|
609
|
+
if (revision === null) {
|
|
610
|
+
throw new FacetError("revision_not_found", "Revision not found", {
|
|
611
|
+
retryable: false,
|
|
612
|
+
details: {
|
|
613
|
+
artifactId: command.artifactId,
|
|
614
|
+
...(command.revisionSha === undefined ? {} : { revisionSha: command.revisionSha }),
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
const lease = deps.leases.issue({ artifactId: command.artifactId, pid: process.pid });
|
|
619
|
+
deps.idle.acquire(`lease:${lease.leaseId}`);
|
|
620
|
+
// The lease id is NOT embedded in the frameUrl — clients carry
|
|
621
|
+
// it out-of-band (header / cache) so a URL log line, referrer,
|
|
622
|
+
// or browser history cannot leak the lease. The frameUrl only
|
|
623
|
+
// identifies the artifact + revision; the SSE route takes the
|
|
624
|
+
// lease id via `X-Gallery-Lease` header.
|
|
625
|
+
const frameUrl = `facet://frame/${command.artifactId}/${revision.sha256}`;
|
|
626
|
+
return {
|
|
627
|
+
command: "open",
|
|
628
|
+
requestId,
|
|
629
|
+
artifactId: command.artifactId,
|
|
630
|
+
revisionSha: revision.sha256,
|
|
631
|
+
frameUrl,
|
|
632
|
+
launched: false,
|
|
633
|
+
lease: {
|
|
634
|
+
leaseId: lease.leaseId,
|
|
635
|
+
expiresAt: lease.expiresAt,
|
|
636
|
+
},
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
case "promote": {
|
|
640
|
+
const template = deps.repository.promoteRevision({
|
|
641
|
+
...(command.artifactId !== undefined ? { artifactId: command.artifactId } : {}),
|
|
642
|
+
revisionId: command.revisionId,
|
|
643
|
+
name: command.name,
|
|
644
|
+
promotedBy: command.promotedBy,
|
|
645
|
+
...(command.description !== undefined ? { description: command.description } : {}),
|
|
646
|
+
});
|
|
647
|
+
return { command: "promote", requestId, template };
|
|
648
|
+
}
|
|
649
|
+
case "instantiate": {
|
|
650
|
+
const template = deps.repository.findTemplateByName(command.name);
|
|
651
|
+
if (template === null) {
|
|
652
|
+
throw new FacetError("template_not_found", "Template not found", {
|
|
653
|
+
retryable: false,
|
|
654
|
+
details: { name: command.name },
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
const project = deps.repository.getOrCreateProjectById(
|
|
658
|
+
command.projectId ?? template.artifactId,
|
|
659
|
+
"/facet",
|
|
660
|
+
);
|
|
661
|
+
const artifact = deps.repository.createArtifact({
|
|
662
|
+
projectId: project.id,
|
|
663
|
+
slug: command.newSlug,
|
|
664
|
+
title: template.name,
|
|
665
|
+
});
|
|
666
|
+
const source = deps.repository.getRevisionById(template.revisionId);
|
|
667
|
+
if (source === null) {
|
|
668
|
+
throw new FacetError("revision_not_found", "Template revision not found", {
|
|
669
|
+
retryable: false,
|
|
670
|
+
details: { revisionId: template.revisionId },
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
deps.repository.publishRevision({
|
|
674
|
+
artifactId: artifact.id,
|
|
675
|
+
artifactType: source.artifactType,
|
|
676
|
+
renderer: source.renderer,
|
|
677
|
+
source: new Uint8Array(source.source),
|
|
678
|
+
note: `Instantiated from template ${template.name}`,
|
|
679
|
+
// Template instantiation preserves declared TSX mode. Non-TSX source
|
|
680
|
+
// omits it and the publish path applies its existing default.
|
|
681
|
+
...(source.execution !== undefined ? { execution: source.execution } : {}),
|
|
682
|
+
});
|
|
683
|
+
return {
|
|
684
|
+
command: "instantiate",
|
|
685
|
+
requestId,
|
|
686
|
+
artifact: mapArtifact(artifact),
|
|
687
|
+
template,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
case "pin": {
|
|
691
|
+
const revision = deps.repository.getRevisionById(command.revisionId);
|
|
692
|
+
if (revision === null) {
|
|
693
|
+
throw new FacetError("revision_not_found", "Revision not found", {
|
|
694
|
+
retryable: false,
|
|
695
|
+
details: { revisionId: command.revisionId },
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
deps.repository.pinRevision(command.revisionId, command.pinned);
|
|
699
|
+
return {
|
|
700
|
+
command: "pin",
|
|
701
|
+
requestId,
|
|
702
|
+
revisionId: command.revisionId,
|
|
703
|
+
pinned: command.pinned,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
case "export": {
|
|
707
|
+
return command.format === "render"
|
|
708
|
+
? exportStoredRender({ repository: deps.repository, command, requestId })
|
|
709
|
+
: exportStoredSource({ repository: deps.repository, command, requestId });
|
|
710
|
+
}
|
|
711
|
+
default: {
|
|
712
|
+
const exhaustive: never = command;
|
|
713
|
+
void exhaustive;
|
|
714
|
+
throw new FacetError("invalid_request", "Unhandled command", { retryable: false });
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|