@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,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursive redaction. Any property whose KEY (case-insensitive) matches
|
|
3
|
+
* one of the names below has its VALUE replaced with `REDACT_PLACEHOLDER`.
|
|
4
|
+
* Values themselves are NEVER inspected for substring matches — only key
|
|
5
|
+
* names. This keeps accidental "value contained the word 'token'" leaks
|
|
6
|
+
* out of the redaction path and makes the policy predictable.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const REDACT_PLACEHOLDER = "[REDACTED]" as const;
|
|
10
|
+
|
|
11
|
+
const REDACTED_KEYS = new Set(
|
|
12
|
+
[
|
|
13
|
+
"token",
|
|
14
|
+
"authorization",
|
|
15
|
+
"source",
|
|
16
|
+
"content",
|
|
17
|
+
"html",
|
|
18
|
+
"svg",
|
|
19
|
+
"screenshot",
|
|
20
|
+
"dom",
|
|
21
|
+
"console",
|
|
22
|
+
].map((k) => k.toLowerCase()),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
function shouldRedactKey(key: string): boolean {
|
|
26
|
+
return REDACTED_KEYS.has(key.toLowerCase());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function scrub(value: unknown): unknown {
|
|
30
|
+
if (value === null || value === undefined) return value;
|
|
31
|
+
if (Array.isArray(value)) return value.map(scrub);
|
|
32
|
+
if (typeof value === "object") {
|
|
33
|
+
const out: Record<string, unknown> = {};
|
|
34
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
35
|
+
out[k] = shouldRedactKey(k) ? REDACT_PLACEHOLDER : scrub(v);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Return a redacted deep-copy of `value`. Primitives pass through;
|
|
44
|
+
* arrays and objects are walked recursively. See the file header for
|
|
45
|
+
* the exact redaction policy.
|
|
46
|
+
*/
|
|
47
|
+
export function redact(value: unknown): unknown {
|
|
48
|
+
return scrub(value);
|
|
49
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const FROZEN_CSP_TEMPLATE =
|
|
2
|
+
"default-src 'none'; " +
|
|
3
|
+
"script-src 'nonce-<BOOTSTRAP_NONCE>'; " +
|
|
4
|
+
"style-src 'unsafe-inline'; " +
|
|
5
|
+
"img-src data: https:; " +
|
|
6
|
+
"font-src data:; " +
|
|
7
|
+
"worker-src 'none'; " +
|
|
8
|
+
"connect-src 'none'; " +
|
|
9
|
+
"object-src 'none'; " +
|
|
10
|
+
"base-uri 'none'; " +
|
|
11
|
+
"form-action 'none'; " +
|
|
12
|
+
"frame-src 'none'; " +
|
|
13
|
+
"media-src 'none'";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical TSX execution modes (D2 of the TSX design).
|
|
3
|
+
*
|
|
4
|
+
* `--execution` is a DECLARED mode, not an inferred one. The artifact
|
|
5
|
+
* author states which mode the TSX is meant to render under; the
|
|
6
|
+
* trust core then applies the matching verification path:
|
|
7
|
+
*
|
|
8
|
+
* - `static` — compiles to HTML via the existing static HTML
|
|
9
|
+
* pipeline; full predict-and-compare claim.
|
|
10
|
+
* - `interactive` — executes the compiled bundle in a nested
|
|
11
|
+
* opaque-origin frame; observation-only claim.
|
|
12
|
+
*
|
|
13
|
+
* Inference from hook or handler usage was rejected during design:
|
|
14
|
+
* it is fragile and makes the trust claim a function of a heuristic
|
|
15
|
+
* rather than an operator declaration. A static artifact that
|
|
16
|
+
* accidentally trips an inference rule would silently earn the weaker
|
|
17
|
+
* verdict.
|
|
18
|
+
*
|
|
19
|
+
* The exported constant is the single source of truth for TypeScript
|
|
20
|
+
* consumers; the storage CHECK derives its SQL literal from it at schema-build
|
|
21
|
+
* time so a new mode lands in the database contract too.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export const TSX_EXECUTION_MODES = ["static", "interactive"] as const;
|
|
25
|
+
export type TsxExecutionMode = (typeof TSX_EXECUTION_MODES)[number];
|
|
26
|
+
|
|
27
|
+
export function isTsxExecutionMode(value: unknown): value is TsxExecutionMode {
|
|
28
|
+
return typeof value === "string" && TSX_EXECUTION_MODES.includes(value as TsxExecutionMode);
|
|
29
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixed TSX import allowlist.
|
|
3
|
+
*
|
|
4
|
+
* D6 of the TSX design: compilation runs inside the netns worker with no
|
|
5
|
+
* egress. The import set is a fixed vendored allowlist:
|
|
6
|
+
* - `react`, `react-dom`, and `react-dom/client` (default exports);
|
|
7
|
+
* - the JSX runtimes `react/jsx-runtime` and `react/jsx-dev-runtime`.
|
|
8
|
+
*
|
|
9
|
+
* Arbitrary npm is rejected outright: resolving dependencies at publish
|
|
10
|
+
* time would put network egress in the middle of the validation path,
|
|
11
|
+
* which the trust core forbids. A dependency that is not vendored is not
|
|
12
|
+
* available.
|
|
13
|
+
*
|
|
14
|
+
* Policy is exposed as plain data so the AST walker and the boundary
|
|
15
|
+
* check can both consume the same source of truth.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const TSX_ALLOWED_MODULES = new Set([
|
|
19
|
+
"react",
|
|
20
|
+
"react-dom",
|
|
21
|
+
"react-dom/client",
|
|
22
|
+
"react/jsx-runtime",
|
|
23
|
+
"react/jsx-dev-runtime",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
export type TsxImportDenialReason =
|
|
27
|
+
| "tsx_import_relative"
|
|
28
|
+
| "tsx_import_url"
|
|
29
|
+
| "tsx_import_not_allowlisted";
|
|
30
|
+
|
|
31
|
+
export interface TsxImportDenial {
|
|
32
|
+
readonly code: "tsx_import_denied";
|
|
33
|
+
readonly message: string;
|
|
34
|
+
readonly module: string;
|
|
35
|
+
readonly reason: TsxImportDenialReason;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Decide whether a single import specifier is allowed under D6.
|
|
40
|
+
*
|
|
41
|
+
* Reject rules, in order:
|
|
42
|
+
* 1. Anything that looks like a URL (starts with a scheme, `//`, or a
|
|
43
|
+
* `data:` prefix) — `tsx_import_url`.
|
|
44
|
+
* 2. Relative imports (`.`, `..`, `/`) — `tsx_import_relative`.
|
|
45
|
+
* 3. Any package that is not in `TSX_ALLOWED_MODULES` — `tsx_import_not_allowlisted`.
|
|
46
|
+
*
|
|
47
|
+
* Returns `null` when the specifier is allowed.
|
|
48
|
+
*/
|
|
49
|
+
export function classifyTsxImport(specifier: string): TsxImportDenial | null {
|
|
50
|
+
if (
|
|
51
|
+
specifier.startsWith("data:") ||
|
|
52
|
+
specifier.startsWith("http:") ||
|
|
53
|
+
specifier.startsWith("https:") ||
|
|
54
|
+
specifier.startsWith("file:") ||
|
|
55
|
+
specifier.startsWith("//")
|
|
56
|
+
) {
|
|
57
|
+
return {
|
|
58
|
+
code: "tsx_import_denied",
|
|
59
|
+
message: `TSX import not allowed: URL specifier "${specifier}"`,
|
|
60
|
+
module: specifier,
|
|
61
|
+
reason: "tsx_import_url",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (
|
|
65
|
+
specifier.startsWith("./") ||
|
|
66
|
+
specifier.startsWith("../") ||
|
|
67
|
+
specifier === "." ||
|
|
68
|
+
specifier === ".."
|
|
69
|
+
) {
|
|
70
|
+
return {
|
|
71
|
+
code: "tsx_import_denied",
|
|
72
|
+
message: `TSX import not allowed: relative specifier "${specifier}"`,
|
|
73
|
+
module: specifier,
|
|
74
|
+
reason: "tsx_import_relative",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (specifier.startsWith("/")) {
|
|
78
|
+
return {
|
|
79
|
+
code: "tsx_import_denied",
|
|
80
|
+
message: `TSX import not allowed: absolute path "${specifier}"`,
|
|
81
|
+
module: specifier,
|
|
82
|
+
reason: "tsx_import_relative",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (!TSX_ALLOWED_MODULES.has(specifier)) {
|
|
86
|
+
return {
|
|
87
|
+
code: "tsx_import_denied",
|
|
88
|
+
message: `TSX import not allowed: "${specifier}" is not in the vendored allowlist`,
|
|
89
|
+
module: specifier,
|
|
90
|
+
reason: "tsx_import_not_allowlisted",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner-only directory creation.
|
|
3
|
+
*
|
|
4
|
+
* The helper has THREE distinct semantics for THREE distinct segment
|
|
5
|
+
* classes. A regression on any of them breaks either security (the
|
|
6
|
+
* leaf not 0o700) or shared-tree surprise (a pre-existing ancestor
|
|
7
|
+
* silently re-moded).
|
|
8
|
+
*
|
|
9
|
+
* 1. LEAF (the security target): ALWAYS 0o700. Created at 0o700 if
|
|
10
|
+
* missing; chmod'd to 0o700 if pre-existing at a wrong mode.
|
|
11
|
+
*
|
|
12
|
+
* 2. Intermediate ancestors the helper CREATES (didn't exist
|
|
13
|
+
* before this call): 0o700. The chmod-after-mkrid cycle beats
|
|
14
|
+
* an overlapping process umask (e.g. 0o177 strips the owner
|
|
15
|
+
* execute bit, breaking a deeper mkdir).
|
|
16
|
+
*
|
|
17
|
+
* 3. PRE-EXISTING intermediate ancestors (any dir that already
|
|
18
|
+
* existed before this call): UNTOUCHED. The helper must NEVER
|
|
19
|
+
* chmod a directory it did not create — silently re-moding a
|
|
20
|
+
* shared tree (e.g. ~/.local/state) would surprise every other
|
|
21
|
+
* app / backup sharing it.
|
|
22
|
+
*
|
|
23
|
+
* Used by:
|
|
24
|
+
* - `src/service/store/evidence-retention.ts` for the evidence root
|
|
25
|
+
* and the per-run subdirectory
|
|
26
|
+
* - `src/validation/tier1/runner.ts` for the per-run evidence
|
|
27
|
+
* directory the runner captures screenshots / console summaries
|
|
28
|
+
* / protocol observations into
|
|
29
|
+
* - `src/service/security/token-store.ts` for the install-token
|
|
30
|
+
* directory
|
|
31
|
+
* - `src/service/lifecycle/process-lock.ts` for the process-lock
|
|
32
|
+
* directory
|
|
33
|
+
* - `src/service/store/database.ts` for the SQLite database dir
|
|
34
|
+
*
|
|
35
|
+
* The capability-scoped callers (service + validation) MUST go
|
|
36
|
+
* through this helper so a fix lands in one place.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { chmodSync, existsSync, mkdirSync, statSync } from "node:fs";
|
|
40
|
+
import { dirname } from "node:path";
|
|
41
|
+
|
|
42
|
+
/** Owner-only mode — the canonical secret-bearing layout. */
|
|
43
|
+
export const OWNER_ONLY_MODE = 0o700;
|
|
44
|
+
|
|
45
|
+
export function ensureOwnerOnlyDirectory(path: string): string {
|
|
46
|
+
// Walk leaf-up. The first existing ancestor stops the walk; it (and
|
|
47
|
+
// every directory above it) is pre-existing and must NOT be touched.
|
|
48
|
+
// If the leaf itself exists, mark it for the leaf-tighten pass.
|
|
49
|
+
const missing: string[] = [];
|
|
50
|
+
let cursor: string | null = path;
|
|
51
|
+
let leafExists = false;
|
|
52
|
+
while (cursor !== null && cursor.length > 0) {
|
|
53
|
+
if (existsSync(cursor)) {
|
|
54
|
+
if (cursor === path) leafExists = true;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
missing.push(cursor);
|
|
58
|
+
const parent: string = dirname(cursor);
|
|
59
|
+
cursor = parent === cursor ? null : parent;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Create missing ancestors top-down. Each newly-created segment
|
|
63
|
+
// lands at 0o700 (chmod after mkdir beats an overlapping umask).
|
|
64
|
+
// Pre-existing ancestors are not in `missing` — they're never
|
|
65
|
+
// chmod'd by this loop.
|
|
66
|
+
for (const segment of missing.toReversed()) {
|
|
67
|
+
mkdirSync(segment, { mode: OWNER_ONLY_MODE });
|
|
68
|
+
try {
|
|
69
|
+
chmodSync(segment, OWNER_ONLY_MODE);
|
|
70
|
+
} catch {
|
|
71
|
+
// best-effort chmod; ENOENT races with a concurrent teardown.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// LEAF enforcement. If the leaf was missing, the loop above just
|
|
76
|
+
// created it at 0o700. If it pre-existed at a wrong mode, tighten it
|
|
77
|
+
// now — the leaf is the security target and is always enforced.
|
|
78
|
+
if (leafExists) {
|
|
79
|
+
try {
|
|
80
|
+
const stat = statSync(path);
|
|
81
|
+
if ((stat.mode & 0o777) !== OWNER_ONLY_MODE) {
|
|
82
|
+
try {
|
|
83
|
+
chmodSync(path, OWNER_ONLY_MODE);
|
|
84
|
+
} catch {
|
|
85
|
+
// best-effort
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// ignore — best-effort leaf tightening
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return path;
|
|
94
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical lexical prediction for Mermaid flowchart `g.node` elements.
|
|
3
|
+
*
|
|
4
|
+
* Tier 1 observes renderer-owned `.node` groups. This module teaches the
|
|
5
|
+
* diagram grammars whose output can be predicted reliably; callers receive
|
|
6
|
+
* `null` rather than a misleading zero for every other diagram type.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const FLOWCHART_HEADER_RE = /^(?:flowchart|graph)\b/i;
|
|
10
|
+
const SEQUENCE_HEADER_RE = /^sequenceDiagram\b/i;
|
|
11
|
+
const STATE_HEADER_RE = /^stateDiagram(?:-v2)?\b/i;
|
|
12
|
+
const IGNORED_LINE_RE = /^(?:classDef|class|click|direction|end|linkStyle|style|subgraph)\b/i;
|
|
13
|
+
const FENCE_LINE_RE = /^(?:`{3,}|~{3,})/;
|
|
14
|
+
|
|
15
|
+
type MermaidNodeGrammar = "flowchart" | "sequence" | "state" | "unavailable";
|
|
16
|
+
|
|
17
|
+
function isIdentifierStart(char: string): boolean {
|
|
18
|
+
return /[A-Za-z_]/.test(char);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isIdentifierPart(char: string): boolean {
|
|
22
|
+
return /[A-Za-z0-9_-]/.test(char);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function shapeEnd(line: string, start: number): number | null {
|
|
26
|
+
const opening = line.slice(start, start + 2);
|
|
27
|
+
const close =
|
|
28
|
+
opening === "[["
|
|
29
|
+
? "]]"
|
|
30
|
+
: opening === "[("
|
|
31
|
+
? ")]"
|
|
32
|
+
: opening === "[/"
|
|
33
|
+
? "\\]"
|
|
34
|
+
: opening === "[\\"
|
|
35
|
+
? "/]"
|
|
36
|
+
: opening === "(("
|
|
37
|
+
? "))"
|
|
38
|
+
: opening === "(["
|
|
39
|
+
? "])"
|
|
40
|
+
: opening === "{{"
|
|
41
|
+
? "}}"
|
|
42
|
+
: line[start] === "["
|
|
43
|
+
? "]"
|
|
44
|
+
: line[start] === "("
|
|
45
|
+
? ")"
|
|
46
|
+
: line[start] === "{"
|
|
47
|
+
? "}"
|
|
48
|
+
: line[start] === ">"
|
|
49
|
+
? "]"
|
|
50
|
+
: null;
|
|
51
|
+
if (close === null) return null;
|
|
52
|
+
const end = line.indexOf(close, start + (opening === "{{" ? 2 : 1));
|
|
53
|
+
return end === -1 ? line.length : end + close.length;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function grammarFor(text: string): MermaidNodeGrammar {
|
|
57
|
+
let inDirective = false;
|
|
58
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
59
|
+
const line = rawLine.trim();
|
|
60
|
+
if (!line) continue;
|
|
61
|
+
if (FENCE_LINE_RE.test(line)) continue;
|
|
62
|
+
if (inDirective) {
|
|
63
|
+
if (line.includes("}%%")) inDirective = false;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (line.startsWith("%%{")) {
|
|
67
|
+
if (!line.includes("}%%")) inDirective = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (line.startsWith("%%")) continue;
|
|
71
|
+
if (FLOWCHART_HEADER_RE.test(line)) return "flowchart";
|
|
72
|
+
if (SEQUENCE_HEADER_RE.test(line)) return "sequence";
|
|
73
|
+
if (STATE_HEADER_RE.test(line)) return "state";
|
|
74
|
+
return "unavailable";
|
|
75
|
+
}
|
|
76
|
+
return "unavailable";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function countFlowchartNodeIds(text: string): number {
|
|
80
|
+
const ids = new Set<string>();
|
|
81
|
+
let inDirective = false;
|
|
82
|
+
|
|
83
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
84
|
+
let line = rawLine.trim();
|
|
85
|
+
if (!line) continue;
|
|
86
|
+
if (FENCE_LINE_RE.test(line)) continue;
|
|
87
|
+
if (inDirective) {
|
|
88
|
+
if (line.includes("}%%")) inDirective = false;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (line.startsWith("%%{")) {
|
|
92
|
+
if (!line.includes("}%%")) inDirective = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const commentStart = line.indexOf("%%");
|
|
96
|
+
if (commentStart !== -1) line = line.slice(0, commentStart).trim();
|
|
97
|
+
if (!line || FLOWCHART_HEADER_RE.test(line) || IGNORED_LINE_RE.test(line)) continue;
|
|
98
|
+
|
|
99
|
+
// Edge labels are text, never endpoint ids. Strip them before scanning
|
|
100
|
+
// so `A -->|tier 0| B` cannot add `tier` to the prediction.
|
|
101
|
+
line = line.replace(/\|[^|]*\|/g, "");
|
|
102
|
+
let index = 0;
|
|
103
|
+
let expectingNode = true;
|
|
104
|
+
while (index < line.length) {
|
|
105
|
+
const char = line[index]!;
|
|
106
|
+
if (isIdentifierStart(char)) {
|
|
107
|
+
let end = index + 1;
|
|
108
|
+
while (end < line.length && isIdentifierPart(line[end]!)) end += 1;
|
|
109
|
+
const id = line.slice(index, end);
|
|
110
|
+
const endOfShape = shapeEnd(line, end);
|
|
111
|
+
if (expectingNode || endOfShape !== null) ids.add(id);
|
|
112
|
+
expectingNode = false;
|
|
113
|
+
index = endOfShape ?? end;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (char === "&") {
|
|
117
|
+
expectingNode = true;
|
|
118
|
+
index += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (char === "-" || char === "=" || char === ".") {
|
|
122
|
+
let end = index + 1;
|
|
123
|
+
while (end < line.length && /[-=.<>]/.test(line[end]!)) end += 1;
|
|
124
|
+
if (end - index >= 2) expectingNode = true;
|
|
125
|
+
index = end;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
index += 1;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return ids.size;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function stateOperand(line: string): string | null {
|
|
135
|
+
const trimmed = line.trim();
|
|
136
|
+
if (trimmed === "[*]") return trimmed;
|
|
137
|
+
const match = trimmed.match(/^[A-Za-z_][A-Za-z0-9_-]*/);
|
|
138
|
+
return match?.[0] ?? null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function compositeStateId(line: string): string | null {
|
|
142
|
+
const match = line.match(
|
|
143
|
+
/^state\s+(?:"(?:[^"\\]|\\.)*"\s+as\s+)?([A-Za-z_][A-Za-z0-9_-]*)\s*\{\s*$/i,
|
|
144
|
+
);
|
|
145
|
+
return match?.[1] ?? null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* State diagrams render composite states as clusters rather than `g.node`
|
|
150
|
+
* groups. Pseudo-states coalesce by scope and direction, while inline notes
|
|
151
|
+
* are `g.node` groups. Mermaid's parser only reports the diagram type, so
|
|
152
|
+
* this is the smallest grammar needed to match the renderer-owned census.
|
|
153
|
+
*/
|
|
154
|
+
function countStateNodeGroups(text: string): number {
|
|
155
|
+
const states = new Set<string>();
|
|
156
|
+
const compositeStates = new Set<string>();
|
|
157
|
+
const pseudoStates = new Set<string>();
|
|
158
|
+
const scopes = ["root"];
|
|
159
|
+
let nextScope = 0;
|
|
160
|
+
let notes = 0;
|
|
161
|
+
let inDirective = false;
|
|
162
|
+
|
|
163
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
164
|
+
let line = rawLine.trim();
|
|
165
|
+
if (!line) continue;
|
|
166
|
+
if (inDirective) {
|
|
167
|
+
if (line.includes("}%%")) inDirective = false;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (line.startsWith("%%{")) {
|
|
171
|
+
if (!line.includes("}%%")) inDirective = true;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const commentStart = line.indexOf("%%");
|
|
175
|
+
if (commentStart !== -1) line = line.slice(0, commentStart).trim();
|
|
176
|
+
if (!line || STATE_HEADER_RE.test(line)) continue;
|
|
177
|
+
|
|
178
|
+
const composite = compositeStateId(line);
|
|
179
|
+
if (composite !== null) {
|
|
180
|
+
compositeStates.add(composite);
|
|
181
|
+
nextScope += 1;
|
|
182
|
+
scopes.push(`scope-${nextScope}`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (line === "}") {
|
|
186
|
+
scopes.pop();
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (/^note\b/i.test(line)) {
|
|
190
|
+
notes += 1;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const arrow = line.indexOf("-->");
|
|
195
|
+
if (arrow === -1) continue;
|
|
196
|
+
const left = stateOperand(line.slice(0, arrow));
|
|
197
|
+
const right = stateOperand(line.slice(arrow + 3).split(":", 1)[0] ?? "");
|
|
198
|
+
const scope = scopes.at(-1) ?? "root";
|
|
199
|
+
for (const operand of [left, right]) {
|
|
200
|
+
if (operand === null) continue;
|
|
201
|
+
if (operand !== "[*]") states.add(operand);
|
|
202
|
+
}
|
|
203
|
+
if (left === "[*]") pseudoStates.add(`${scope}:entry`);
|
|
204
|
+
if (right === "[*]") pseudoStates.add(`${scope}:exit`);
|
|
205
|
+
}
|
|
206
|
+
let leafStates = 0;
|
|
207
|
+
for (const state of states) {
|
|
208
|
+
if (!compositeStates.has(state)) leafStates += 1;
|
|
209
|
+
}
|
|
210
|
+
return leafStates + pseudoStates.size + notes;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Return the lexical expectation for Tier 1's Mermaid `g.node` census.
|
|
215
|
+
* `null` means the diagram type has no maintained grammar and must skip the
|
|
216
|
+
* node comparison; it is deliberately distinct from the exact zero for
|
|
217
|
+
* sequence diagrams, which render no `g.node` groups.
|
|
218
|
+
*/
|
|
219
|
+
export function countMermaidNodeDeclarations(text: string): number | null {
|
|
220
|
+
switch (grammarFor(text)) {
|
|
221
|
+
case "flowchart":
|
|
222
|
+
return countFlowchartNodeIds(text);
|
|
223
|
+
case "sequence":
|
|
224
|
+
return 0;
|
|
225
|
+
case "state":
|
|
226
|
+
return countStateNodeGroups(text);
|
|
227
|
+
case "unavailable":
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical OS-process helpers shared by every service layer that
|
|
3
|
+
* needs to detect a live pid or read its monotonic start time
|
|
4
|
+
* without taking responsibility for the full lock lifecycle. The
|
|
5
|
+
* lock module (`service/lifecycle/process-lock`) and the Tier 1
|
|
6
|
+
* verifier (`validation/tier1/browser-process`) both import from
|
|
7
|
+
* here so a single bug fix lands in one place.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Probe whether `pid` is alive. Returns true when `process.kill(pid, 0)`
|
|
14
|
+
* succeeds OR returns EPERM (the pid exists but the caller cannot
|
|
15
|
+
* signal it — treat as alive so we never reclaim a foreign-pid lock
|
|
16
|
+
* whose ownership we cannot independently prove). False for
|
|
17
|
+
* non-positive / non-integer pids, for ESRCH (pid does not exist),
|
|
18
|
+
* and for any other error code (conservative: the caller decides
|
|
19
|
+
* whether a non-zero non-EPERM error is "dead" or "indeterminate";
|
|
20
|
+
* in practice every other kill-0 failure on Linux is ESRCH).
|
|
21
|
+
*/
|
|
22
|
+
export function isPidAlive(pid: number): boolean {
|
|
23
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
24
|
+
try {
|
|
25
|
+
process.kill(pid, 0);
|
|
26
|
+
return true;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
29
|
+
if (code === "EPERM") return true;
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read the OS-recorded process start time (clock ticks since boot,
|
|
36
|
+
* field 22 of /proc/<pid>/stat) for a live pid. Returns null when
|
|
37
|
+
* the pid is unreadable (non-integer, non-positive, /proc entry
|
|
38
|
+
* missing, or the start-time field is non-numeric).
|
|
39
|
+
*
|
|
40
|
+
* The Linux start-time field is monotonic and stable across the
|
|
41
|
+
* lifetime of the process, which makes it a far stronger staleness
|
|
42
|
+
* signal than wall-clock time — two pids with the same start-time
|
|
43
|
+
* ticks are guaranteed to be the same process. Both
|
|
44
|
+
* `service/lifecycle/process-lock` (lock metadata) and
|
|
45
|
+
* `validation/tier1/browser-process` (browser pid record for orphan
|
|
46
|
+
* cleanup) read this same field so a future orphan-cleanup hook can
|
|
47
|
+
* cross-reference the two with byte-identical semantics.
|
|
48
|
+
*/
|
|
49
|
+
export function readPidStartTimeTicks(pid: number): number | null {
|
|
50
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
51
|
+
try {
|
|
52
|
+
const text = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
53
|
+
// /proc/<pid>/stat: "pid (comm) state ppid ..." — the comm field
|
|
54
|
+
// can contain spaces or parens, so find the LAST `)` and parse
|
|
55
|
+
// from there.
|
|
56
|
+
const lastParen = text.lastIndexOf(")");
|
|
57
|
+
if (lastParen < 0) return null;
|
|
58
|
+
const after = text.slice(lastParen + 1).trimStart();
|
|
59
|
+
const fields = after.split(/\s+/);
|
|
60
|
+
// The slice starts at field 3 (`state`), so field 22 is index 19.
|
|
61
|
+
const startTime = Number(fields[19]);
|
|
62
|
+
if (!Number.isFinite(startTime)) return null;
|
|
63
|
+
return startTime;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical timestamp + id helpers.
|
|
3
|
+
*
|
|
4
|
+
* Every persisted record stamps itself with `new Date().toISOString()`
|
|
5
|
+
* — single source of truth so wire timestamps and DB timestamps share
|
|
6
|
+
* the same shape (RFC 3339, millisecond precision, trailing `Z`).
|
|
7
|
+
*
|
|
8
|
+
* `generateRequestId` was duplicated between the service's
|
|
9
|
+
* `http-utils.ts` and the CLI; both files now import from here so the
|
|
10
|
+
* format (`req-<uuid>`) cannot drift.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
|
|
15
|
+
export function now(): string {
|
|
16
|
+
return new Date().toISOString();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function generateRequestId(): string {
|
|
20
|
+
return `req-${randomUUID()}`;
|
|
21
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource caps applied to the Tier 0 parse worker.
|
|
3
|
+
*
|
|
4
|
+
* Each cap is named and exposed so tests, the runner, and the netns
|
|
5
|
+
* wrapper can agree on a single value. A failing cap produces a typed
|
|
6
|
+
* Tier 0 failure (timeout, output cap, …) that the parent maps to a
|
|
7
|
+
* `Tier0Result` (or, when the worker cannot even start, a typed
|
|
8
|
+
* `FacetError`).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Wall-clock budget for one Tier 0 worker invocation. */
|
|
12
|
+
export const TIER0_TIMEOUT_MS = 5_000;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* TSX includes worker boot, a cold Bun.build, and static SSR in this budget:
|
|
16
|
+
* the parent starts the timer before the pooled worker has imported Bun and React.
|
|
17
|
+
*/
|
|
18
|
+
export const TIER0_TSX_COLD_P95_BASELINE_MS = 250;
|
|
19
|
+
export const TIER0_TSX_CONTENTION_FACTOR = 3;
|
|
20
|
+
export const TIER0_TSX_TIMEOUT_MS = 2_000;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Maximum number of bytes the worker may write to STDOUT before the
|
|
24
|
+
* parent truncates the stream and records a typed protocol error.
|
|
25
|
+
* The cap accommodates bounded compiled TSX evidence while still
|
|
26
|
+
* catching an unbounded or misbehaving worker.
|
|
27
|
+
*/
|
|
28
|
+
export const TIER0_OUTPUT_CAP_BYTES = 4 * 1024 * 1024;
|
|
29
|
+
|
|
30
|
+
/** Maximum NDJSON request line accepted by the long-lived Tier 0 worker. */
|
|
31
|
+
export const TIER0_INPUT_CAP_BYTES = 8 * 1024 * 1024;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Linux `setrlimit` cap on the worker's resident memory. Enforced via
|
|
35
|
+
* `bash -c 'ulimit -v …; exec "$@"'` inside the netns wrapper so the
|
|
36
|
+
* Bun process inherits the limit on startup. 512 MiB is generous for
|
|
37
|
+
* `marked`/`mermaid`/`vega-lite` `compile()` on the 5 MiB source cap;
|
|
38
|
+
* a hostile artifact that allocates past this cap is killed by SIGKILL
|
|
39
|
+
* and surfaced as `tier0_worker_died`.
|
|
40
|
+
*/
|
|
41
|
+
export const TIER0_MEMORY_CAP_BYTES = 512 * 1024 * 1024;
|