@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,140 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The single protocol version this build speaks. Any envelope carrying a
|
|
5
|
+
* different value is rejected with `unknown_schema_version` so future
|
|
6
|
+
* schema bumps can fail loud rather than silently mis-parse.
|
|
7
|
+
*/
|
|
8
|
+
export const FACET_SCHEMA_VERSION = "facet.v1" as const;
|
|
9
|
+
export type FacetSchemaVersion = typeof FACET_SCHEMA_VERSION;
|
|
10
|
+
|
|
11
|
+
export const SchemaVersionSchema = z.literal(FACET_SCHEMA_VERSION);
|
|
12
|
+
|
|
13
|
+
const PrimitiveDetailsSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Wire body of every error response. `details` only accepts JSON-safe
|
|
17
|
+
* primitives so the envelope round-trips through `JSON.parse(JSON.stringify(...))`
|
|
18
|
+
* without losing type information — and without leaking functions, errors,
|
|
19
|
+
* or undefined into the wire format.
|
|
20
|
+
*/
|
|
21
|
+
export const FacetErrorBodySchema = z.object({
|
|
22
|
+
code: z.string().min(1),
|
|
23
|
+
message: z.string().min(1),
|
|
24
|
+
retryable: z.boolean(),
|
|
25
|
+
details: z.record(z.string(), PrimitiveDetailsSchema).optional(),
|
|
26
|
+
});
|
|
27
|
+
export type FacetErrorBody = z.infer<typeof FacetErrorBodySchema>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Both arms use `.strict()` so an envelope can carry ONLY the fields
|
|
31
|
+
* its discriminator allows. `ok:true` may not smuggle an `error` key
|
|
32
|
+
* (and vice versa), and no arm accepts extra top-level keys. Without
|
|
33
|
+
* this, a forged envelope with `ok:true` plus an `error` body would
|
|
34
|
+
* parse cleanly and confuse every downstream consumer that branches on
|
|
35
|
+
* the discriminator.
|
|
36
|
+
*/
|
|
37
|
+
const EnvelopeOkSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
schemaVersion: SchemaVersionSchema,
|
|
40
|
+
requestId: z.string().min(1),
|
|
41
|
+
ok: z.literal(true),
|
|
42
|
+
data: z.unknown(),
|
|
43
|
+
})
|
|
44
|
+
.strict();
|
|
45
|
+
|
|
46
|
+
const EnvelopeErrSchema = z
|
|
47
|
+
.object({
|
|
48
|
+
schemaVersion: SchemaVersionSchema,
|
|
49
|
+
requestId: z.string().min(1),
|
|
50
|
+
ok: z.literal(false),
|
|
51
|
+
error: FacetErrorBodySchema,
|
|
52
|
+
})
|
|
53
|
+
.strict();
|
|
54
|
+
|
|
55
|
+
export const FacetEnvelopeSchema = z.discriminatedUnion("ok", [
|
|
56
|
+
EnvelopeOkSchema,
|
|
57
|
+
EnvelopeErrSchema,
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
export type FacetEnvelope<T> =
|
|
61
|
+
| {
|
|
62
|
+
schemaVersion: FacetSchemaVersion;
|
|
63
|
+
requestId: string;
|
|
64
|
+
ok: true;
|
|
65
|
+
data: T;
|
|
66
|
+
}
|
|
67
|
+
| {
|
|
68
|
+
schemaVersion: FacetSchemaVersion;
|
|
69
|
+
requestId: string;
|
|
70
|
+
ok: false;
|
|
71
|
+
error: FacetErrorBody;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Build a typed ok envelope carrying `data`. */
|
|
75
|
+
export function okEnvelope<T>(requestId: string, data: T): FacetEnvelope<T> {
|
|
76
|
+
return {
|
|
77
|
+
schemaVersion: FACET_SCHEMA_VERSION,
|
|
78
|
+
requestId,
|
|
79
|
+
ok: true,
|
|
80
|
+
data,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Build a typed error envelope carrying `error`. */
|
|
85
|
+
export function errEnvelope(requestId: string, error: FacetErrorBody): FacetEnvelope<never> {
|
|
86
|
+
return {
|
|
87
|
+
schemaVersion: FACET_SCHEMA_VERSION,
|
|
88
|
+
requestId,
|
|
89
|
+
ok: false,
|
|
90
|
+
error,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type ParseEnvelopeResult =
|
|
95
|
+
| { ok: true; envelope: FacetEnvelope<unknown> }
|
|
96
|
+
| { ok: false; body: FacetErrorBody };
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Parse an arbitrary value as a Facet envelope. Returns a typed
|
|
100
|
+
* `unknown_schema_version` body when the value carries a future schema
|
|
101
|
+
* version, a typed `invalid_envelope` body for every other parse failure,
|
|
102
|
+
* and a typed `ok: true` result for well-formed inputs.
|
|
103
|
+
*/
|
|
104
|
+
export function parseEnvelope(input: unknown): ParseEnvelopeResult {
|
|
105
|
+
if (typeof input !== "object" || input === null) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
body: {
|
|
109
|
+
code: "invalid_envelope",
|
|
110
|
+
message: "Envelope must be a JSON object",
|
|
111
|
+
retryable: false,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const candidate = input as Record<string, unknown>;
|
|
116
|
+
if (candidate.schemaVersion !== FACET_SCHEMA_VERSION) {
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
body: {
|
|
120
|
+
code: "unknown_schema_version",
|
|
121
|
+
message: `Unknown schema version: ${String(candidate.schemaVersion)}`,
|
|
122
|
+
retryable: false,
|
|
123
|
+
details: { received: String(candidate.schemaVersion ?? "undefined") },
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const parsed = FacetEnvelopeSchema.safeParse(input);
|
|
128
|
+
if (parsed.success) {
|
|
129
|
+
return { ok: true, envelope: parsed.data as FacetEnvelope<unknown> };
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
body: {
|
|
134
|
+
code: "invalid_envelope",
|
|
135
|
+
message: parsed.error.issues[0]?.message ?? "Envelope failed validation",
|
|
136
|
+
retryable: false,
|
|
137
|
+
details: { issueCount: parsed.error.issues.length },
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import { ArtifactTypeSchema } from "./artifact";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Stream lifecycle events emitted on the SSE channel. The wire is
|
|
7
|
+
* `type`-discriminated so clients can pattern-match without parsing a
|
|
8
|
+
* string union.
|
|
9
|
+
*/
|
|
10
|
+
export const StreamEventSchema = z.discriminatedUnion("type", [
|
|
11
|
+
z.object({
|
|
12
|
+
type: z.literal("stream:open"),
|
|
13
|
+
streamId: z.string().min(1),
|
|
14
|
+
artifactId: z.string().min(1).nullable(),
|
|
15
|
+
at: z.string().datetime({ offset: true }),
|
|
16
|
+
}),
|
|
17
|
+
z.object({
|
|
18
|
+
type: z.literal("stream:heartbeat"),
|
|
19
|
+
streamId: z.string().min(1),
|
|
20
|
+
at: z.string().datetime({ offset: true }),
|
|
21
|
+
}),
|
|
22
|
+
z.object({
|
|
23
|
+
type: z.literal("stream:close"),
|
|
24
|
+
streamId: z.string().min(1),
|
|
25
|
+
at: z.string().datetime({ offset: true }),
|
|
26
|
+
reason: z.string().min(1),
|
|
27
|
+
}),
|
|
28
|
+
]);
|
|
29
|
+
/**
|
|
30
|
+
* Emitted whenever a revision commits to the store. The sha is the
|
|
31
|
+
* authoritative identifier clients use to address the revision; the
|
|
32
|
+
* number is the human-readable position in the artifact's history.
|
|
33
|
+
*/
|
|
34
|
+
export const RevisionCommittedEventSchema = z.object({
|
|
35
|
+
type: z.literal("revision:committed"),
|
|
36
|
+
artifactId: z.string().min(1),
|
|
37
|
+
revisionSha: z.string().regex(/^[a-f0-9]{64}$/),
|
|
38
|
+
revisionNumber: z.number().int().positive(),
|
|
39
|
+
artifactType: ArtifactTypeSchema,
|
|
40
|
+
at: z.string().datetime({ offset: true }),
|
|
41
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const OBSERVED_COUNT_KEYS = [
|
|
2
|
+
"rendererRootSvgCount",
|
|
3
|
+
"graphCount",
|
|
4
|
+
"mermaidNodeCount",
|
|
5
|
+
"visibleSvgCount",
|
|
6
|
+
"opaqueRegionCount",
|
|
7
|
+
"externalImageCount",
|
|
8
|
+
] as const;
|
|
9
|
+
export type ObservedCountKey = (typeof OBSERVED_COUNT_KEYS)[number];
|
|
10
|
+
|
|
11
|
+
export const HTML_OBSERVED_COUNT_KEYS = [
|
|
12
|
+
"rendererRootCount",
|
|
13
|
+
"headingCount",
|
|
14
|
+
"tableCount",
|
|
15
|
+
"listCount",
|
|
16
|
+
"imageCount",
|
|
17
|
+
"canvasCount",
|
|
18
|
+
"externalImageCount",
|
|
19
|
+
] as const;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { EvidenceImageFormatSchema } from "../evidence-image";
|
|
3
|
+
|
|
4
|
+
import { ArtifactTypeSchema, RendererSchema } from "./artifact";
|
|
5
|
+
import { HTML_OBSERVED_COUNT_KEYS, OBSERVED_COUNT_KEYS } from "./observed-counts";
|
|
6
|
+
import { TSX_EXECUTION_MODES } from "../tsx/execution";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validation tier. Tier 0 is the always-on parser worker (no browser);
|
|
10
|
+
* Tier 1 is the explicit, ephemeral, headless-shell-backed verifier.
|
|
11
|
+
* Tier 2 (the user's browser) is display-only and never produces a
|
|
12
|
+
* `RenderRun`.
|
|
13
|
+
*/
|
|
14
|
+
export const ValidationTierSchema = z.union([z.literal(0), z.literal(1)]);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Closed set of render status codes the verifier can produce. The
|
|
18
|
+
* `partial:` prefix is reserved for results that completed some checks
|
|
19
|
+
* but could not finalize the layout pass (e.g., due to viewport
|
|
20
|
+
* unavailability); `tampered` is reserved for hostile pages that
|
|
21
|
+
* attempted to forge the verdict.
|
|
22
|
+
*
|
|
23
|
+
* `partial:unstable` (D11) is the TSX interactive mode verdict for a
|
|
24
|
+
* structure that legitimately changed between the render barrier and
|
|
25
|
+
* the stability window. It is deliberately NOT `tampered` — a
|
|
26
|
+
* legitimately animated or async-loading component also changes
|
|
27
|
+
* structure between observations, and branding it a forgery would
|
|
28
|
+
* manufacture the exact false-verdict class this project has spent
|
|
29
|
+
* three arcs eliminating. `tampered` stays reserved for channel
|
|
30
|
+
* divergence — the page contradicting protocol authority.
|
|
31
|
+
*/
|
|
32
|
+
export const RenderStatusSchema = z.enum([
|
|
33
|
+
"ok",
|
|
34
|
+
"error",
|
|
35
|
+
"partial:layout_unverified",
|
|
36
|
+
"partial:opaque_content",
|
|
37
|
+
"partial:external_resources",
|
|
38
|
+
"partial:unstable",
|
|
39
|
+
"tampered",
|
|
40
|
+
"timeout",
|
|
41
|
+
"shim_only",
|
|
42
|
+
"probe_only",
|
|
43
|
+
"insecure:unvalidated",
|
|
44
|
+
]);
|
|
45
|
+
export type RenderStatus = z.infer<typeof RenderStatusSchema>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* TSX execution mode marker. Carried on TSX static and
|
|
49
|
+
* interactive verdicts; ABSENT for every other artifact type. The
|
|
50
|
+
* shape mirrors the existing top-level marker precedents
|
|
51
|
+
* (`insecure`, `screenshotError`) — a discriminated literal, not a
|
|
52
|
+
* nullable object.
|
|
53
|
+
*/
|
|
54
|
+
export const TsxExecutionModeSchema = z.enum(TSX_EXECUTION_MODES);
|
|
55
|
+
export type TsxExecutionMode = z.infer<typeof TsxExecutionModeSchema>;
|
|
56
|
+
|
|
57
|
+
/** Discriminative errors the verifier chooses to surface beyond the count. */
|
|
58
|
+
export const DiscriminativeErrorSchema = z.object({
|
|
59
|
+
code: z.string().min(1),
|
|
60
|
+
message: z.string().min(1),
|
|
61
|
+
location: z.string().optional(),
|
|
62
|
+
});
|
|
63
|
+
export type DiscriminativeError = z.infer<typeof DiscriminativeErrorSchema>;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Evidence capture failed after the render verdict was derived. This stays
|
|
67
|
+
* outside `observed.discriminativeErrors`, which the verdict ladder consumes.
|
|
68
|
+
*/
|
|
69
|
+
export const ScreenshotErrorSchema = z.object({
|
|
70
|
+
code: z.literal("screenshot_unavailable"),
|
|
71
|
+
message: z.string().min(1),
|
|
72
|
+
});
|
|
73
|
+
export type ScreenshotError = z.infer<typeof ScreenshotErrorSchema>;
|
|
74
|
+
|
|
75
|
+
export const InsecureLevelSchema = z.union([
|
|
76
|
+
z.literal(0),
|
|
77
|
+
z.literal(1),
|
|
78
|
+
z.literal(2),
|
|
79
|
+
z.literal(3),
|
|
80
|
+
]);
|
|
81
|
+
export type InsecureLevel = z.infer<typeof InsecureLevelSchema>;
|
|
82
|
+
|
|
83
|
+
export const InsecureMarkerSchema = z.object({
|
|
84
|
+
level: z.union([z.literal(1), z.literal(2), z.literal(3)]),
|
|
85
|
+
reason: z.string().min(1),
|
|
86
|
+
});
|
|
87
|
+
export type InsecureMarker = z.infer<typeof InsecureMarkerSchema>;
|
|
88
|
+
|
|
89
|
+
const HtmlObservedCountShape = Object.fromEntries(
|
|
90
|
+
HTML_OBSERVED_COUNT_KEYS.map((key) => [key, z.number().int().nonnegative()]),
|
|
91
|
+
) as Record<(typeof HTML_OBSERVED_COUNT_KEYS)[number], z.ZodNumber>;
|
|
92
|
+
|
|
93
|
+
export const HtmlStructureCountsSchema = z.object(HtmlObservedCountShape);
|
|
94
|
+
export type HtmlStructureCounts = z.infer<typeof HtmlStructureCountsSchema>;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Lexical counters computed from the source bytes WITHOUT parsing or
|
|
98
|
+
* rendering. The verifier compares these against its own observations
|
|
99
|
+
* so an attacker cannot make a forged page's `svgCount` agree with
|
|
100
|
+
* the verifier's observation without also matching the lexical
|
|
101
|
+
* expectation.
|
|
102
|
+
*
|
|
103
|
+
* `externalImageCount` is the TYPE-AGNOSTIC count of references to
|
|
104
|
+
* resources the no-egress validation run cannot observe (HTTPS
|
|
105
|
+
* images). It applies to every artifact type that may reference an
|
|
106
|
+
* external resource (markdown native image syntax, raw HTML, html),
|
|
107
|
+
* so the verdict reads it from the top level rather than from the
|
|
108
|
+
* HTML-only `html` subfield.
|
|
109
|
+
*/
|
|
110
|
+
export const LexicalCountersSchema = z.object({
|
|
111
|
+
rendererRootSvgCount: z.number().int().nonnegative(),
|
|
112
|
+
// `null` is an explicit no-expectation marker for Mermaid diagram types
|
|
113
|
+
// whose renderer-owned `g.node` output lacks a maintained lexical grammar.
|
|
114
|
+
mermaidNodeCount: z.number().int().nonnegative().nullable(),
|
|
115
|
+
visibleSvgCount: z.number().int().nonnegative(),
|
|
116
|
+
opaqueRegionCount: z.number().int().nonnegative(),
|
|
117
|
+
externalImageCount: z.number().int().nonnegative(),
|
|
118
|
+
html: HtmlStructureCountsSchema.optional(),
|
|
119
|
+
});
|
|
120
|
+
export type LexicalCounters = z.infer<typeof LexicalCountersSchema>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The ONE canonical verdict-observed shape. Every read-back result and
|
|
124
|
+
* every Tier 0/1 result derives from this. The optional fields
|
|
125
|
+
* (`viewBoxes`, `discriminativeErrors`) are filled by Tier 1 when the
|
|
126
|
+
* layout pass succeeds.
|
|
127
|
+
*
|
|
128
|
+
* `graphCount` is included alongside `mermaidNodeCount` for parity with
|
|
129
|
+
* the acceptance-gate verdict contract — the acceptance tests assert
|
|
130
|
+
* `observed.graphCount` for nested-SVG forgery probes, so a verifier
|
|
131
|
+
* that does not surface it would fail those gates regardless of the
|
|
132
|
+
* status.
|
|
133
|
+
*/
|
|
134
|
+
const ObservedCountShape = Object.fromEntries(
|
|
135
|
+
OBSERVED_COUNT_KEYS.map((key) => [key, z.number().int().nonnegative()]),
|
|
136
|
+
) as Record<(typeof OBSERVED_COUNT_KEYS)[number], z.ZodNumber>;
|
|
137
|
+
|
|
138
|
+
export const VerdictObservedSchema = z.object({
|
|
139
|
+
...ObservedCountShape,
|
|
140
|
+
html: HtmlStructureCountsSchema.optional(),
|
|
141
|
+
viewBoxes: z.array(z.string()).optional(),
|
|
142
|
+
errorCount: z.number().int().nonnegative(),
|
|
143
|
+
discriminativeErrors: z.array(DiscriminativeErrorSchema).optional(),
|
|
144
|
+
});
|
|
145
|
+
export type VerdictObserved = z.infer<typeof VerdictObservedSchema>;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Canonical verdict: every read-back response, every Tier 0/1 result,
|
|
149
|
+
* and the acceptance-gate verdict are all structurally compatible with
|
|
150
|
+
* this shape. Tier results extend it (with `expected` for the
|
|
151
|
+
* expected-vs-observed comparison); read-back uses it directly.
|
|
152
|
+
*
|
|
153
|
+
* `execution` is the top-level marker for TSX static and
|
|
154
|
+
* interactive verdicts. It is ABSENT — not null — for every other
|
|
155
|
+
* artifact type, so the wire form for non-TSX stays byte-identical
|
|
156
|
+
* to the pre-arc shape. The runtime read-back reconstructs this field
|
|
157
|
+
* from the revision's stored execution mode rather than carrying it
|
|
158
|
+
* on the observed counters.
|
|
159
|
+
*/
|
|
160
|
+
export const VerdictSchema = z.object({
|
|
161
|
+
status: RenderStatusSchema,
|
|
162
|
+
tier: ValidationTierSchema,
|
|
163
|
+
artifactId: z.string().min(1),
|
|
164
|
+
revisionSha: z.string().regex(/^[a-f0-9]{64}$/),
|
|
165
|
+
observed: VerdictObservedSchema,
|
|
166
|
+
screenshotError: ScreenshotErrorSchema.optional(),
|
|
167
|
+
insecure: InsecureMarkerSchema.optional(),
|
|
168
|
+
execution: TsxExecutionModeSchema.optional(),
|
|
169
|
+
});
|
|
170
|
+
export type Verdict = z.infer<typeof VerdictSchema>;
|
|
171
|
+
|
|
172
|
+
/** Tier 0 input: pure-parse verifier, no browser, no egress. */
|
|
173
|
+
export const Tier0InputSchema = z.object({
|
|
174
|
+
revisionSha: z.string().regex(/^[a-f0-9]{64}$/),
|
|
175
|
+
artifactType: ArtifactTypeSchema,
|
|
176
|
+
renderer: RendererSchema,
|
|
177
|
+
source: z.instanceof(Uint8Array<ArrayBuffer>),
|
|
178
|
+
lexical: LexicalCountersSchema,
|
|
179
|
+
execution: TsxExecutionModeSchema.optional(),
|
|
180
|
+
});
|
|
181
|
+
export type Tier0Input = z.infer<typeof Tier0InputSchema>;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The parent-side Tier 0 runner contract. The default implementation
|
|
185
|
+
* lives in `src/validation/tier0/runner.ts` (out-of-process Bun
|
|
186
|
+
* subprocess under `unshare --map-current-user --net`). The service
|
|
187
|
+
* imports THIS TYPE only — the runner implementation is constructed
|
|
188
|
+
* by callers (`src/cli/`, tests) and injected into the dispatcher so
|
|
189
|
+
* `src/service/**` stays byte-dumb and the boundary checker remains
|
|
190
|
+
* clean.
|
|
191
|
+
*/
|
|
192
|
+
export type Tier0Runner = ((input: Tier0Input) => Promise<Tier0WorkerResult>) & {
|
|
193
|
+
readonly close?: () => void;
|
|
194
|
+
};
|
|
195
|
+
export type Tier0RunnerFactory = (level: InsecureLevel) => Tier0Runner;
|
|
196
|
+
|
|
197
|
+
export interface IsolationProbeResult {
|
|
198
|
+
readonly available: boolean;
|
|
199
|
+
readonly reason: string | null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export type Tier0IsolationProbe = () => IsolationProbeResult | Promise<IsolationProbeResult>;
|
|
203
|
+
|
|
204
|
+
/** Tier 0 result: extends the canonical verdict with the expected counters. */
|
|
205
|
+
export const Tier0ResultSchema = VerdictSchema.extend({
|
|
206
|
+
tier: z.literal(0),
|
|
207
|
+
expected: LexicalCountersSchema,
|
|
208
|
+
});
|
|
209
|
+
export type Tier0Result = z.infer<typeof Tier0ResultSchema>;
|
|
210
|
+
|
|
211
|
+
export const CompiledArtifactSchema = z.object({
|
|
212
|
+
mediaType: z.enum(["text/html", "text/javascript"]),
|
|
213
|
+
bytesBase64: z.string(),
|
|
214
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
215
|
+
});
|
|
216
|
+
export type CompiledArtifact = z.infer<typeof CompiledArtifactSchema>;
|
|
217
|
+
|
|
218
|
+
/** Identity-blind worker stdout; the parent adds artifactId before persistence. */
|
|
219
|
+
export const Tier0WorkerResultSchema = Tier0ResultSchema.omit({ artifactId: true }).extend({
|
|
220
|
+
compiled: CompiledArtifactSchema.optional(),
|
|
221
|
+
});
|
|
222
|
+
export type Tier0WorkerResult = z.infer<typeof Tier0WorkerResultSchema>;
|
|
223
|
+
|
|
224
|
+
/** Tier 1 input: headless-shell-backed verifier on top of Tier 0. */
|
|
225
|
+
export const Tier1InputSchema = Tier0InputSchema.extend({
|
|
226
|
+
/**
|
|
227
|
+
* Bytes Tier 1 renders. This is the published source for every type except
|
|
228
|
+
* static TSX, which supplies the retained compiled HTML derived from it.
|
|
229
|
+
*/
|
|
230
|
+
source: z.instanceof(Uint8Array<ArrayBuffer>),
|
|
231
|
+
/** Pinned chrome-headless-shell version that must run this verification. */
|
|
232
|
+
launcherVersion: z.string().min(1),
|
|
233
|
+
/** Network namespace name the verifier must be confined to. */
|
|
234
|
+
networkNamespace: z.string().min(1),
|
|
235
|
+
/**
|
|
236
|
+
* Per-run evidence directory (mode 0700). The runner writes the
|
|
237
|
+
* screenshot, the bounded console summary, and the protocol
|
|
238
|
+
* observation JSON under this directory. When omitted, the runner
|
|
239
|
+
* falls back to `computeFacetPaths().evidence` so production
|
|
240
|
+
* callers never have to specify it.
|
|
241
|
+
*/
|
|
242
|
+
evidenceDir: z.string().optional(),
|
|
243
|
+
});
|
|
244
|
+
export type Tier1Input = z.infer<typeof Tier1InputSchema>;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The parent-side Tier 1 runner contract. The default implementation
|
|
248
|
+
* lives in `src/validation/tier1/runner.ts` (an ephemeral netns'd
|
|
249
|
+
* `chrome-headless-shell` driven via CDP pipe). Like `Tier0Runner`,
|
|
250
|
+
* only THIS TYPE is imported by the service — the implementation is
|
|
251
|
+
* constructed by callers (`src/cli/`, tests) and injected so
|
|
252
|
+
* `src/service/**` stays byte-dumb.
|
|
253
|
+
*/
|
|
254
|
+
export type Tier1Runner = (input: Tier1Input) => Promise<Tier1Result>;
|
|
255
|
+
export type Tier1RunnerFactory = (level: InsecureLevel) => Tier1Runner;
|
|
256
|
+
export type Tier1AvailabilityProbe = () => IsolationProbeResult | Promise<IsolationProbeResult>;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Tier 1 result: extends Tier 0 with screenshot/console paths.
|
|
260
|
+
*
|
|
261
|
+
* Partial verdicts require screenshot evidence or a typed explanation for
|
|
262
|
+
* why capture failed after the render verdict was already derived.
|
|
263
|
+
*/
|
|
264
|
+
export const Tier1ResultSchema = Tier0ResultSchema.extend({
|
|
265
|
+
tier: z.literal(1),
|
|
266
|
+
screenshotPath: z.string().nullable(),
|
|
267
|
+
screenshotFormat: EvidenceImageFormatSchema.optional(),
|
|
268
|
+
consolePath: z.string().nullable(),
|
|
269
|
+
screenshotError: ScreenshotErrorSchema.optional(),
|
|
270
|
+
}).refine(
|
|
271
|
+
(value) =>
|
|
272
|
+
!value.status.startsWith("partial:") ||
|
|
273
|
+
value.screenshotPath !== null ||
|
|
274
|
+
value.screenshotError !== undefined,
|
|
275
|
+
{
|
|
276
|
+
message: "partial verdict requires screenshot evidence or a screenshot-unavailable marker",
|
|
277
|
+
path: ["screenshotPath"],
|
|
278
|
+
},
|
|
279
|
+
);
|
|
280
|
+
export type Tier1Result = z.infer<typeof Tier1ResultSchema>;
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* The renderer-OWNED root count the verifier expects from one Tier 1
|
|
284
|
+
* run. Counts ONE top-level renderer-owned root per expected
|
|
285
|
+
* (revision, fence) pair — NOT every descendant `<svg>`. A nested
|
|
286
|
+
* `<svg id="forged">` inside a Mermaid label does NOT inflate this
|
|
287
|
+
* count; that probe is the gate-forgery acceptance contract.
|
|
288
|
+
*/
|
|
289
|
+
export const ProtocolObservationSchema = z.object({
|
|
290
|
+
...ObservedCountShape,
|
|
291
|
+
html: HtmlStructureCountsSchema.optional(),
|
|
292
|
+
viewBoxes: z.array(z.string()),
|
|
293
|
+
errorCount: z.number().int().nonnegative(),
|
|
294
|
+
discriminativeErrors: z.array(DiscriminativeErrorSchema),
|
|
295
|
+
});
|
|
296
|
+
export type ProtocolObservation = z.infer<typeof ProtocolObservationSchema>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { FacetErrorBody } from "../contracts/envelope";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Closed set of error codes the service can return on the wire. The store
|
|
5
|
+
* layer mirrors the same names for parity (database_corrupt, etc.); the
|
|
6
|
+
* protocol layer adds reserved/implemented-style codes that exist purely
|
|
7
|
+
* in the envelope path.
|
|
8
|
+
*/
|
|
9
|
+
export const FacetErrorCodes = {
|
|
10
|
+
// Store-derived codes (mirrored from src/service/store/database.ts).
|
|
11
|
+
database_corrupt: true,
|
|
12
|
+
database_busy: true,
|
|
13
|
+
disk_full: true,
|
|
14
|
+
duplicate_revision: true,
|
|
15
|
+
foreign_key: true,
|
|
16
|
+
immutable_revision: true,
|
|
17
|
+
migration_failed: true,
|
|
18
|
+
invalid_artifact_type: true,
|
|
19
|
+
constraint: true,
|
|
20
|
+
// Protocol-derived codes.
|
|
21
|
+
reserved_not_implemented: true,
|
|
22
|
+
unsupported_reserved_type: true,
|
|
23
|
+
unknown_schema_version: true,
|
|
24
|
+
payload_too_large: true,
|
|
25
|
+
invalid_envelope: true,
|
|
26
|
+
invalid_request: true,
|
|
27
|
+
artifact_not_found: true,
|
|
28
|
+
revision_not_found: true,
|
|
29
|
+
template_not_found: true,
|
|
30
|
+
evidence_unavailable: true,
|
|
31
|
+
output_unwritable: true,
|
|
32
|
+
revision_capacity_pinned: true,
|
|
33
|
+
// Tier 0 worker-level failures. These surface when the parent cannot
|
|
34
|
+
// even obtain a Tier0Result (worker died, bad stdout, wall-clock cap,
|
|
35
|
+
// output cap, unshare/unshare wrapper unavailable). Parser-level
|
|
36
|
+
// failures (mermaid syntax error, hostile svg, etc.) are recorded as
|
|
37
|
+
// a Tier0Result with status="error" and do NOT throw.
|
|
38
|
+
tier0_timeout: true,
|
|
39
|
+
tier0_protocol_error: true,
|
|
40
|
+
tier0_worker_died: true,
|
|
41
|
+
tier0_output_cap: true,
|
|
42
|
+
tier0_unavailable: true,
|
|
43
|
+
tsx_ast_denied: true,
|
|
44
|
+
tsx_compile_error: true,
|
|
45
|
+
tsx_compile_output_cap: true,
|
|
46
|
+
// Tier 1 verifier-level failures. Mirrors the Tier 0 worker set:
|
|
47
|
+
// the parent cannot obtain a Tier1Result at all (the netns wrapper
|
|
48
|
+
// is missing, the pinned shell binary is missing, the CDP pipe
|
|
49
|
+
// died, the probe timed out, or the verifier could not parse the
|
|
50
|
+
// browser protocol payload). Verdict-level divergences — page-shim
|
|
51
|
+
// lying, missing frame, missing render-complete — surface as
|
|
52
|
+
// `tampered` / `shim_only` / `timeout` / `probe_only` inside a
|
|
53
|
+
// Tier1Result, NOT as a throw.
|
|
54
|
+
tier1_unavailable: true,
|
|
55
|
+
tier1_browser_died: true,
|
|
56
|
+
tier1_protocol_error: true,
|
|
57
|
+
tier1_timeout: true,
|
|
58
|
+
tier1_launcher_missing: true,
|
|
59
|
+
// Generic catch-alls.
|
|
60
|
+
internal: true,
|
|
61
|
+
} as const;
|
|
62
|
+
|
|
63
|
+
export type FacetErrorCode = keyof typeof FacetErrorCodes;
|
|
64
|
+
|
|
65
|
+
export type FacetErrorDetails = Record<string, string | number | boolean | null>;
|
|
66
|
+
|
|
67
|
+
export interface FacetErrorOptions {
|
|
68
|
+
readonly retryable?: boolean;
|
|
69
|
+
readonly details?: FacetErrorDetails;
|
|
70
|
+
readonly cause?: unknown;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Typed error class for everything that flows out through the protocol
|
|
75
|
+
* envelope. Carries the wire-friendly `code`, the `retryable` flag, and
|
|
76
|
+
* primitive-only `details` so a `FacetErrorBody` derived from this object
|
|
77
|
+
* is always JSON-round-trippable.
|
|
78
|
+
*
|
|
79
|
+
* `FacetStoreError` extends this class — see `./store-error.ts`. The
|
|
80
|
+
* `from()` bridge relies on that inheritance so a thrown store fault
|
|
81
|
+
* surfaces on the wire with its typed code instead of collapsing to
|
|
82
|
+
* `invalid_envelope`.
|
|
83
|
+
*/
|
|
84
|
+
export class FacetError extends Error {
|
|
85
|
+
override readonly name: string = "FacetError";
|
|
86
|
+
|
|
87
|
+
constructor(
|
|
88
|
+
readonly code: FacetErrorCode,
|
|
89
|
+
message: string,
|
|
90
|
+
readonly options: FacetErrorOptions = {},
|
|
91
|
+
) {
|
|
92
|
+
super(message, { cause: options.cause });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get retryable(): boolean {
|
|
96
|
+
return this.options.retryable ?? false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
get details(): FacetErrorDetails | undefined {
|
|
100
|
+
return this.options.details;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
toBody(): FacetErrorBody {
|
|
104
|
+
const body: FacetErrorBody = {
|
|
105
|
+
code: this.code,
|
|
106
|
+
message: this.message,
|
|
107
|
+
retryable: this.retryable,
|
|
108
|
+
};
|
|
109
|
+
if (this.options.details !== undefined) {
|
|
110
|
+
return { ...body, details: this.options.details };
|
|
111
|
+
}
|
|
112
|
+
return body;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Coerce any thrown value into a FacetError. Pre-existing FacetErrors pass
|
|
117
|
+
* through; everything else is wrapped as `invalid_envelope` (the safest
|
|
118
|
+
* generic code for an untyped boundary failure).
|
|
119
|
+
*/
|
|
120
|
+
static from(error: unknown): FacetError {
|
|
121
|
+
if (error instanceof FacetError) return error;
|
|
122
|
+
if (error instanceof Error) {
|
|
123
|
+
return new FacetError("invalid_envelope", error.message, { cause: error });
|
|
124
|
+
}
|
|
125
|
+
return new FacetError("invalid_envelope", String(error));
|
|
126
|
+
}
|
|
127
|
+
}
|