@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.
Files changed (327) hide show
  1. package/LICENSE-APACHE +202 -0
  2. package/LICENSE-MIT +23 -0
  3. package/README.md +158 -0
  4. package/dist/gallery/chunk-4g8rem85.css +656 -0
  5. package/dist/gallery/chunk-c6s9k9ty.js +5970 -0
  6. package/dist/gallery/frame/artifact.css +8424 -0
  7. package/dist/gallery/frame/chunks/abnfDiagram-N423BO3Z-jj74szbm.js +155 -0
  8. package/dist/gallery/frame/chunks/architecture-TIHT7OUA-cvdjn7dt.js +10 -0
  9. package/dist/gallery/frame/chunks/architectureDiagram-T3A2C74G-n4z77eg6.js +8675 -0
  10. package/dist/gallery/frame/chunks/blockDiagram-VBNYF7ZC-7xsqzrd1.js +3529 -0
  11. package/dist/gallery/frame/chunks/c4Diagram-5PPSVZJV-z2zjs7wx.js +2413 -0
  12. package/dist/gallery/frame/chunks/classDiagram-JCYQIIEL-ve8rngqp.js +47 -0
  13. package/dist/gallery/frame/chunks/classDiagram-v2-OCEON4UE-8cfaxb8z.js +47 -0
  14. package/dist/gallery/frame/chunks/cose-bilkent-JH36ORCC-sn92qexx.js +4822 -0
  15. package/dist/gallery/frame/chunks/cynefin-VYW2F7L2-tqjv913d.js +10 -0
  16. package/dist/gallery/frame/chunks/cynefinDiagram-MW4NZA55-c1c0dj12.js +500 -0
  17. package/dist/gallery/frame/chunks/dagre-VZM6K2ZE-7tyg4yt2.js +476 -0
  18. package/dist/gallery/frame/chunks/diagram-7IWD3JNH-y4eng9sc.js +526 -0
  19. package/dist/gallery/frame/chunks/diagram-B4RE2ZJO-rn3p7j5h.js +673 -0
  20. package/dist/gallery/frame/chunks/diagram-LBJQPF4R-w4a2cm9q.js +255 -0
  21. package/dist/gallery/frame/chunks/diagram-Q27KOJAE-aqc2v7wy.js +575 -0
  22. package/dist/gallery/frame/chunks/diagram-UB23O5K3-xncy8nyd.js +355 -0
  23. package/dist/gallery/frame/chunks/ebnfDiagram-BXEA7PRR-acwgxz16.js +175 -0
  24. package/dist/gallery/frame/chunks/erDiagram-JOGREHBK-mzp5nswt.js +1332 -0
  25. package/dist/gallery/frame/chunks/eventmodeling-45OFAUF4-sg07qr0c.js +10 -0
  26. package/dist/gallery/frame/chunks/flowDiagram-UKHOOZJN-2e5ygs44.js +29 -0
  27. package/dist/gallery/frame/chunks/ganttDiagram-PKOTCBZU-p2hhh7hr.js +2620 -0
  28. package/dist/gallery/frame/chunks/gitGraph-TEB2WS4Q-j6kw0naa.js +10 -0
  29. package/dist/gallery/frame/chunks/gitGraphDiagram-DS77QQ5N-4z9z8wjm.js +1347 -0
  30. package/dist/gallery/frame/chunks/info-DKCQHKI2-mpt80jd3.js +10 -0
  31. package/dist/gallery/frame/chunks/infoDiagram-6WML65LV-5rqm3mgg.js +65 -0
  32. package/dist/gallery/frame/chunks/ishikawaDiagram-WSZJBQD7-b51z96jy.js +975 -0
  33. package/dist/gallery/frame/chunks/journeyDiagram-NVQOT4AX-9jbfp0pk.js +1272 -0
  34. package/dist/gallery/frame/chunks/kanban-definition-27J2QSJJ-4x0vx4se.js +1116 -0
  35. package/dist/gallery/frame/chunks/katex-xfgk95tv.js +14034 -0
  36. package/dist/gallery/frame/chunks/markdown-1s5vwkf2.js +9 -0
  37. package/dist/gallery/frame/chunks/markdown-2h6bwsms.js +3239 -0
  38. package/dist/gallery/frame/chunks/markdown-2x113yh8.js +107 -0
  39. package/dist/gallery/frame/chunks/markdown-30b7rwmp.js +1943 -0
  40. package/dist/gallery/frame/chunks/markdown-356bfj4g.js +2031 -0
  41. package/dist/gallery/frame/chunks/markdown-44n9ea69.js +2406 -0
  42. package/dist/gallery/frame/chunks/markdown-56enrf2r.js +39 -0
  43. package/dist/gallery/frame/chunks/markdown-5avymm8t.js +725 -0
  44. package/dist/gallery/frame/chunks/markdown-6r4svars.js +33 -0
  45. package/dist/gallery/frame/chunks/markdown-6zz8efx2.js +24 -0
  46. package/dist/gallery/frame/chunks/markdown-7jp6prnb.js +19 -0
  47. package/dist/gallery/frame/chunks/markdown-809k7q41.js +55 -0
  48. package/dist/gallery/frame/chunks/markdown-82xmzhzd.js +1076 -0
  49. package/dist/gallery/frame/chunks/markdown-92n37gd4.js +7627 -0
  50. package/dist/gallery/frame/chunks/markdown-ag2sssf8.js +36 -0
  51. package/dist/gallery/frame/chunks/markdown-b4s7ntq2.js +19 -0
  52. package/dist/gallery/frame/chunks/markdown-c9k0fvp1.js +100 -0
  53. package/dist/gallery/frame/chunks/markdown-ceefymn8.js +125 -0
  54. package/dist/gallery/frame/chunks/markdown-d1wfbs70.js +243 -0
  55. package/dist/gallery/frame/chunks/markdown-dy5sgjhc.js +11022 -0
  56. package/dist/gallery/frame/chunks/markdown-dz3z5w30.js +30868 -0
  57. package/dist/gallery/frame/chunks/markdown-eg6ngz7y.js +30340 -0
  58. package/dist/gallery/frame/chunks/markdown-ff5wvb43.js +1149 -0
  59. package/dist/gallery/frame/chunks/markdown-ffg06nfz.js +86 -0
  60. package/dist/gallery/frame/chunks/markdown-fvfncadf.js +472 -0
  61. package/dist/gallery/frame/chunks/markdown-gvyszqpv.js +19 -0
  62. package/dist/gallery/frame/chunks/markdown-gx1p0ew0.js +88 -0
  63. package/dist/gallery/frame/chunks/markdown-gxye5frz.js +65 -0
  64. package/dist/gallery/frame/chunks/markdown-h7pzv34e.js +105 -0
  65. package/dist/gallery/frame/chunks/markdown-hab789t9.js +47 -0
  66. package/dist/gallery/frame/chunks/markdown-hjp6sj8t.js +125 -0
  67. package/dist/gallery/frame/chunks/markdown-jka64qgp.js +1993 -0
  68. package/dist/gallery/frame/chunks/markdown-k8xkn8mw.js +56 -0
  69. package/dist/gallery/frame/chunks/markdown-m7frq8x8.js +112 -0
  70. package/dist/gallery/frame/chunks/markdown-p740v4qm.js +36 -0
  71. package/dist/gallery/frame/chunks/markdown-pxfeh231.js +57 -0
  72. package/dist/gallery/frame/chunks/markdown-pzgd8z3s.js +87 -0
  73. package/dist/gallery/frame/chunks/markdown-qx3cngz2.js +1673 -0
  74. package/dist/gallery/frame/chunks/markdown-rawhfvvc.js +85 -0
  75. package/dist/gallery/frame/chunks/markdown-rd8w0ng9.js +49 -0
  76. package/dist/gallery/frame/chunks/markdown-rm2yxfjj.js +2773 -0
  77. package/dist/gallery/frame/chunks/markdown-s1jatrnk.js +993 -0
  78. package/dist/gallery/frame/chunks/markdown-sgfg5vvq.js +59 -0
  79. package/dist/gallery/frame/chunks/markdown-t4ethv9d.js +409 -0
  80. package/dist/gallery/frame/chunks/markdown-tnmabqvz.js +36 -0
  81. package/dist/gallery/frame/chunks/markdown-tvdpppw8.js +5292 -0
  82. package/dist/gallery/frame/chunks/markdown-v0x27twr.js +22 -0
  83. package/dist/gallery/frame/chunks/markdown-v14td68x.js +36 -0
  84. package/dist/gallery/frame/chunks/markdown-w6sgd54z.js +36 -0
  85. package/dist/gallery/frame/chunks/markdown-xw8dsk8b.js +2125 -0
  86. package/dist/gallery/frame/chunks/markdown-y18mve73.js +85 -0
  87. package/dist/gallery/frame/chunks/markdown-yengsms5.js +427 -0
  88. package/dist/gallery/frame/chunks/markdown-z1wc0v0d.js +1826 -0
  89. package/dist/gallery/frame/chunks/markdown-zazx9h4n.js +329 -0
  90. package/dist/gallery/frame/chunks/mermaid-de7p4v0x.js +30 -0
  91. package/dist/gallery/frame/chunks/mindmap-definition-FAOFIHXS-ra44cbem.js +1216 -0
  92. package/dist/gallery/frame/chunks/packet-7NZHBO7P-xppm5913.js +10 -0
  93. package/dist/gallery/frame/chunks/pegDiagram-VL7TDLO6-a2a4wg03.js +163 -0
  94. package/dist/gallery/frame/chunks/pie-RZYD4A2V-hnbxk152.js +10 -0
  95. package/dist/gallery/frame/chunks/pieDiagram-7S7Q4E2Y-f1vxjtmx.js +311 -0
  96. package/dist/gallery/frame/chunks/quadrantDiagram-CIZ2JOQS-ywafa6kh.js +1390 -0
  97. package/dist/gallery/frame/chunks/radar-I7S5WNFK-ewqd27se.js +10 -0
  98. package/dist/gallery/frame/chunks/railroad-3IZDKUUU-5hy4bdx3.js +10 -0
  99. package/dist/gallery/frame/chunks/railroad-abnf-AHOZXSZD-bh5sxz0d.js +10 -0
  100. package/dist/gallery/frame/chunks/railroad-ebnf-EBAXGLYW-6b9c3zeh.js +10 -0
  101. package/dist/gallery/frame/chunks/railroad-peg-LSFZ7HO6-avc8v39s.js +10 -0
  102. package/dist/gallery/frame/chunks/railroadDiagram-AXF67PYL-qc61wax4.js +131 -0
  103. package/dist/gallery/frame/chunks/requirementDiagram-LRYGKXZP-9bs4786c.js +1295 -0
  104. package/dist/gallery/frame/chunks/sankeyDiagram-W5VNT64P-awt1em8b.js +1339 -0
  105. package/dist/gallery/frame/chunks/sequenceDiagram-SI44F4Z6-qaf9mz3t.js +4324 -0
  106. package/dist/gallery/frame/chunks/sizeCapture-X5ZJPWSS-9fq685xd.js +64 -0
  107. package/dist/gallery/frame/chunks/stateDiagram-OKZ733FA-vx7a7hh0.js +456 -0
  108. package/dist/gallery/frame/chunks/stateDiagram-v2-UEYNNEHI-x4m6ws5t.js +46 -0
  109. package/dist/gallery/frame/chunks/swimlanes-SLNWSIFB-e4mxm1gh.js +8287 -0
  110. package/dist/gallery/frame/chunks/swimlanesDiagram-ULZ7WXOC-0h93e2qq.js +42 -0
  111. package/dist/gallery/frame/chunks/timeline-definition-Z64GVDOM-wp0bhbzg.js +1549 -0
  112. package/dist/gallery/frame/chunks/treeView-QDETBFTQ-hcx7n0vt.js +10 -0
  113. package/dist/gallery/frame/chunks/treemap-6X3UGDF4-av02skdw.js +10 -0
  114. package/dist/gallery/frame/chunks/vennDiagram-T6HMQDX7-pmvk1p7n.js +2566 -0
  115. package/dist/gallery/frame/chunks/wardley-OPB4EBWU-njsp6e40.js +10 -0
  116. package/dist/gallery/frame/chunks/wardleyDiagram-T6FBY63Y-eggmnv4z.js +979 -0
  117. package/dist/gallery/frame/chunks/xychartDiagram-ELKLHX3M-q40tnpe5.js +1954 -0
  118. package/dist/gallery/frame/frame.css +183 -0
  119. package/dist/gallery/frame/runtime/chart.js +42744 -0
  120. package/dist/gallery/frame/runtime/html.js +14 -0
  121. package/dist/gallery/frame/runtime/markdown.js +1403 -0
  122. package/dist/gallery/frame/runtime/mermaid.js +32 -0
  123. package/dist/gallery/frame/runtime/svg.js +14 -0
  124. package/dist/gallery/frame/runtime/tsx.js +52 -0
  125. package/dist/gallery/index.html +104 -0
  126. package/docs/reference/cli.md +163 -0
  127. package/docs/reference/export.md +106 -0
  128. package/docs/reference/html.md +215 -0
  129. package/docs/reference/http.md +85 -0
  130. package/docs/reference/mcp.md +78 -0
  131. package/docs/reference/security.md +119 -0
  132. package/docs/reference/storage.md +93 -0
  133. package/docs/reference/tsx.md +101 -0
  134. package/docs/reference/validation.md +212 -0
  135. package/package.json +89 -0
  136. package/scripts/build-gallery.ts +127 -0
  137. package/scripts/launch-netns.sh +67 -0
  138. package/skills/facet/SKILL.md +100 -0
  139. package/src/cli/client.ts +311 -0
  140. package/src/cli/commands/create.ts +49 -0
  141. package/src/cli/commands/doctor.ts +249 -0
  142. package/src/cli/commands/export.ts +179 -0
  143. package/src/cli/commands/instantiate.ts +35 -0
  144. package/src/cli/commands/list.ts +49 -0
  145. package/src/cli/commands/open.ts +75 -0
  146. package/src/cli/commands/pin.ts +39 -0
  147. package/src/cli/commands/promote.ts +49 -0
  148. package/src/cli/commands/publish.ts +122 -0
  149. package/src/cli/commands/read-back.ts +56 -0
  150. package/src/cli/commands/status.ts +114 -0
  151. package/src/cli/commands/watch.ts +146 -0
  152. package/src/cli/main.ts +522 -0
  153. package/src/cli/output.ts +106 -0
  154. package/src/cli/parser.ts +376 -0
  155. package/src/cli/presenter.ts +212 -0
  156. package/src/cli/service-metadata.ts +134 -0
  157. package/src/cli/spawn-service.ts +326 -0
  158. package/src/css.d.ts +4 -0
  159. package/src/gallery-web/app.ts +1581 -0
  160. package/src/gallery-web/export-menu.ts +121 -0
  161. package/src/gallery-web/export.ts +112 -0
  162. package/src/gallery-web/favicon.ts +54 -0
  163. package/src/gallery-web/frame/entries/chart.ts +6 -0
  164. package/src/gallery-web/frame/entries/html.ts +6 -0
  165. package/src/gallery-web/frame/entries/markdown.ts +6 -0
  166. package/src/gallery-web/frame/entries/mermaid.ts +6 -0
  167. package/src/gallery-web/frame/entries/svg.ts +6 -0
  168. package/src/gallery-web/frame/entries/tsx.ts +6 -0
  169. package/src/gallery-web/frame/frame-payload.ts +91 -0
  170. package/src/gallery-web/frame/renderer-validation.ts +13 -0
  171. package/src/gallery-web/frame/renderers/chart.ts +182 -0
  172. package/src/gallery-web/frame/renderers/dompurify-shim.ts +112 -0
  173. package/src/gallery-web/frame/renderers/html.ts +48 -0
  174. package/src/gallery-web/frame/renderers/markdown.ts +81 -0
  175. package/src/gallery-web/frame/renderers/mermaid.ts +120 -0
  176. package/src/gallery-web/frame/renderers/registry.ts +260 -0
  177. package/src/gallery-web/frame/renderers/svg.ts +393 -0
  178. package/src/gallery-web/frame/renderers/tsx.ts +61 -0
  179. package/src/gallery-web/frame/runtime.ts +501 -0
  180. package/src/gallery-web/frame/styles/artifact.css +8424 -0
  181. package/src/gallery-web/frame/styles/frame.css +183 -0
  182. package/src/gallery-web/frame/styles/html-source.css +23 -0
  183. package/src/gallery-web/frame/view-box.ts +33 -0
  184. package/src/gallery-web/frame-html.ts +72 -0
  185. package/src/gallery-web/index.html +104 -0
  186. package/src/gallery-web/session.ts +107 -0
  187. package/src/gallery-web/sse-client.ts +212 -0
  188. package/src/gallery-web/styles/app.css +420 -0
  189. package/src/gallery-web/styles/tokens.css +112 -0
  190. package/src/gallery-web/styles/verdict.css +186 -0
  191. package/src/gallery-web/swap.ts +64 -0
  192. package/src/gallery-web/theme.ts +40 -0
  193. package/src/gallery-web/view-state.ts +93 -0
  194. package/src/harness-adapters/claude-code/README.md +10 -0
  195. package/src/harness-adapters/claude-code/facet.sh +4 -0
  196. package/src/harness-adapters/codex/README.md +10 -0
  197. package/src/harness-adapters/codex/facet.sh +4 -0
  198. package/src/harness-adapters/mcp/README.md +13 -0
  199. package/src/harness-adapters/mcp/cli-bridge.ts +216 -0
  200. package/src/harness-adapters/mcp/main.ts +15 -0
  201. package/src/harness-adapters/mcp/server.ts +164 -0
  202. package/src/harness-adapters/mcp/tool-schemas.ts +53 -0
  203. package/src/harness-adapters/opencode/README.md +10 -0
  204. package/src/harness-adapters/opencode/facet.sh +4 -0
  205. package/src/runtime/compiled-entrypoints.ts +69 -0
  206. package/src/service/dispatcher.ts +717 -0
  207. package/src/service/export.ts +218 -0
  208. package/src/service/http-utils.ts +55 -0
  209. package/src/service/lexical/expectations.ts +210 -0
  210. package/src/service/lifecycle/idle-controller.ts +125 -0
  211. package/src/service/lifecycle/orphan-cleanup.ts +128 -0
  212. package/src/service/lifecycle/process-lock.ts +235 -0
  213. package/src/service/main.ts +136 -0
  214. package/src/service/process.ts +240 -0
  215. package/src/service/router-guards.ts +239 -0
  216. package/src/service/router.ts +627 -0
  217. package/src/service/security/auth.ts +168 -0
  218. package/src/service/security/host-origin.ts +138 -0
  219. package/src/service/security/http-guards.ts +13 -0
  220. package/src/service/security/leases.ts +182 -0
  221. package/src/service/security/token-store.ts +138 -0
  222. package/src/service/server.ts +356 -0
  223. package/src/service/store/database.ts +61 -0
  224. package/src/service/store/evidence-retention.ts +234 -0
  225. package/src/service/store/migrations.ts +167 -0
  226. package/src/service/store/repository-lifecycle.ts +121 -0
  227. package/src/service/store/repository.ts +732 -0
  228. package/src/service/store/schema.ts +218 -0
  229. package/src/service/stored-verdict.ts +97 -0
  230. package/src/service/stream.ts +301 -0
  231. package/src/service/verdict-enrichment.ts +45 -0
  232. package/src/shared/build/frame-bundle-plugins.ts +61 -0
  233. package/src/shared/build-mode.ts +28 -0
  234. package/src/shared/config/evidence-read.ts +98 -0
  235. package/src/shared/config/limits.ts +56 -0
  236. package/src/shared/config/paths.ts +96 -0
  237. package/src/shared/contracts/artifact-types.ts +2 -0
  238. package/src/shared/contracts/artifact.ts +98 -0
  239. package/src/shared/contracts/commands/_shared.ts +59 -0
  240. package/src/shared/contracts/commands/guards.ts +86 -0
  241. package/src/shared/contracts/commands/index.ts +204 -0
  242. package/src/shared/contracts/commands/names.ts +35 -0
  243. package/src/shared/contracts/commands/requests.ts +149 -0
  244. package/src/shared/contracts/commands/results.ts +200 -0
  245. package/src/shared/contracts/envelope.ts +140 -0
  246. package/src/shared/contracts/events.ts +41 -0
  247. package/src/shared/contracts/observed-counts.ts +19 -0
  248. package/src/shared/contracts/renderers.ts +6 -0
  249. package/src/shared/contracts/validation.ts +296 -0
  250. package/src/shared/errors/facet-error.ts +127 -0
  251. package/src/shared/errors/store-error.ts +68 -0
  252. package/src/shared/evidence-image.ts +37 -0
  253. package/src/shared/export.ts +31 -0
  254. package/src/shared/html/policy.ts +108 -0
  255. package/src/shared/html/style-vocabulary.ts +352 -0
  256. package/src/shared/logging/logger.ts +108 -0
  257. package/src/shared/logging/redact.ts +49 -0
  258. package/src/shared/security/frozen-csp.ts +13 -0
  259. package/src/shared/storage-version.ts +2 -0
  260. package/src/shared/tsx/execution.ts +29 -0
  261. package/src/shared/tsx/import-policy.ts +94 -0
  262. package/src/shared/util/dir-permissions.ts +94 -0
  263. package/src/shared/util/mermaid-nodes.ts +230 -0
  264. package/src/shared/util/process.ts +67 -0
  265. package/src/shared/util/time.ts +21 -0
  266. package/src/shared/version.ts +3 -0
  267. package/src/validation/sandbox/limits.ts +41 -0
  268. package/src/validation/sandbox/netns.ts +177 -0
  269. package/src/validation/tier0/chart.ts +177 -0
  270. package/src/validation/tier0/dom-shim.ts +54 -0
  271. package/src/validation/tier0/html.ts +408 -0
  272. package/src/validation/tier0/markdown.ts +285 -0
  273. package/src/validation/tier0/mermaid.ts +98 -0
  274. package/src/validation/tier0/runner.ts +504 -0
  275. package/src/validation/tier0/svg.ts +321 -0
  276. package/src/validation/tier0/tsx/allowlist-resolver.ts +77 -0
  277. package/src/validation/tier0/tsx/ast-policy-capabilities.ts +238 -0
  278. package/src/validation/tier0/tsx/ast-policy-shared.ts +71 -0
  279. package/src/validation/tier0/tsx/ast-policy.ts +181 -0
  280. package/src/validation/tier0/tsx/compiler.ts +156 -0
  281. package/src/validation/tier0/worker-dispatch.ts +211 -0
  282. package/src/validation/tier0/worker-entry.ts +96 -0
  283. package/src/validation/tier0/worker-input.ts +75 -0
  284. package/src/validation/tier1/browser-process.ts +147 -0
  285. package/src/validation/tier1/cdp-pipe.ts +281 -0
  286. package/src/validation/tier1/entries/chart.ts +5 -0
  287. package/src/validation/tier1/entries/html.ts +6 -0
  288. package/src/validation/tier1/entries/markdown.ts +5 -0
  289. package/src/validation/tier1/entries/mermaid.ts +5 -0
  290. package/src/validation/tier1/entries/svg.ts +5 -0
  291. package/src/validation/tier1/entries/tsx.ts +5 -0
  292. package/src/validation/tier1/frame-target.ts +162 -0
  293. package/src/validation/tier1/harness-entry.ts +261 -0
  294. package/src/validation/tier1/harness.ts +215 -0
  295. package/src/validation/tier1/isolated-probe.ts +94 -0
  296. package/src/validation/tier1/launcher.ts +177 -0
  297. package/src/validation/tier1/limits.ts +80 -0
  298. package/src/validation/tier1/nonce.ts +17 -0
  299. package/src/validation/tier1/protocol-probe.ts +519 -0
  300. package/src/validation/tier1/runner.ts +1122 -0
  301. package/src/validation/tier1/verdict.ts +246 -0
  302. package/src/validation/tier1/webp.ts +69 -0
  303. package/templates/README.md +88 -0
  304. package/templates/bar-compare.vl.json +63 -0
  305. package/templates/capacity-report.tsx +102 -0
  306. package/templates/decision-record.md +24 -0
  307. package/templates/deployment-state.mmd +40 -0
  308. package/templates/exemplar.md +91 -0
  309. package/templates/facet-story.tsx +799 -0
  310. package/templates/fleet-dashboard.html +141 -0
  311. package/templates/html-release-ledger.html +91 -0
  312. package/templates/html-status-report.html +114 -0
  313. package/templates/incident-console.tsx +147 -0
  314. package/templates/legion-boundaries.mmd +23 -0
  315. package/templates/legion-flow.mmd +15 -0
  316. package/templates/legion-sequence.mmd +14 -0
  317. package/templates/legion-state.mmd +7 -0
  318. package/templates/metric-card.svg +29 -0
  319. package/templates/observability-map.svg +101 -0
  320. package/templates/pipeline-audit.md +108 -0
  321. package/templates/release-metrics.vl.json +98 -0
  322. package/templates/service-topology.mmd +47 -0
  323. package/templates/status-report.md +32 -0
  324. package/templates/system-map.svg +53 -0
  325. package/templates/timeseries.vl.json +71 -0
  326. package/templates/tsx-interactive-counter.tsx +19 -0
  327. package/templates/tsx-status-report.tsx +31 -0
@@ -0,0 +1,627 @@
1
+ /**
2
+ * Service router — single entrypoint mapping HTTP requests to command
3
+ * verbs, enveloping results, applying security guards.
4
+ *
5
+ * Guard order: Host → Bearer auth → mutation Content-Type → raw body
6
+ * size cap → body read/parse → dispatch. Auth happens BEFORE any
7
+ * body read/parse (unauth malformed body → 401, not 400). Promote
8
+ * accepts install OR operator bearer (constant-time). Cross-site
9
+ * mutation failures map to 403 (CSRF).
10
+ *
11
+ * The pieces of each guard live in sibling modules (`router-guards.ts`
12
+ * for body-cap + parse + status mapping, `http-utils.ts` for the
13
+ * envelope → Response builder, `security/host-origin.ts` for the host
14
+ * check + `resolveHost`, `security/auth.ts` for the bearer check).
15
+ * This file is intentionally the orchestration: read the function
16
+ * top-to-bottom and the guard order is the sequence of statements.
17
+ */
18
+
19
+ import { errEnvelope, okEnvelope, parseEnvelope } from "../shared/contracts/envelope";
20
+ import {
21
+ CommandRequestSchema,
22
+ CommandResultSchema,
23
+ checkCommandImplemented,
24
+ type CommandRequest,
25
+ } from "../shared/contracts/commands";
26
+ import { FacetError } from "../shared/errors/facet-error";
27
+ import { join, normalize, relative } from "node:path";
28
+ import { readFileSync, realpathSync } from "node:fs";
29
+
30
+ import { requireAnyBearer, checkMutationSecurityHeaders } from "./security/auth";
31
+ import { checkHostOrigin, resolveHost, type HostOriginResult } from "./security/host-origin";
32
+ import { dispatch, type DispatcherDeps } from "./dispatcher";
33
+ import { handleStream, type RevisionBroadcaster } from "./stream";
34
+ import { envelopeResponse, generateRequestId, pickRequestId } from "./http-utils";
35
+ import {
36
+ INVALID_JSON,
37
+ RAW_BODY_CAP_BYTES,
38
+ parseBody,
39
+ readCappedBody,
40
+ readRequestMeta,
41
+ resolveGalleryRequest,
42
+ statusForHostCheck,
43
+ statusFor,
44
+ type RequestMeta,
45
+ } from "./router-guards";
46
+ import type { FacetLogger } from "../shared/logging/logger";
47
+ import { buildFrameDocument } from "../gallery-web/frame-html";
48
+ import { isResolvedGalleryTheme } from "../gallery-web/theme";
49
+ import { ARTIFACT_TYPES } from "../shared/contracts/artifact-types";
50
+ import { ArtifactTypeSchema } from "../shared/contracts/artifact";
51
+ import { latestStoredVerdict } from "./stored-verdict";
52
+ import { readStoredRenderEvidence } from "./export";
53
+ import { resolveGalleryRoot } from "../shared/config/paths";
54
+
55
+ const ROUTE_API = "/api/v1/commands";
56
+ const ROUTE_STREAM = "/api/v1/stream";
57
+ const ROUTE_GALLERY = "/gallery";
58
+ const ROUTE_FRAME = "/gallery/frame";
59
+ const ROUTE_BOOTSTRAP = "/api/v1/gallery/bootstrap";
60
+ const ROUTE_RELEASE = "/api/v1/gallery/release";
61
+ const ROUTE_SOURCE = "/api/v1/gallery/source";
62
+ const ROUTE_EVIDENCE = "/api/v1/gallery/evidence";
63
+ const FRAME_RUNTIME_PREFIX = `${ROUTE_FRAME}/runtime/`;
64
+ const FRAME_CHUNK_PREFIX = `${ROUTE_FRAME}/chunks/`;
65
+ const TIER1_TRACE = process.env.FACET_TIER1_TRACE === "1";
66
+
67
+ function traceTier1Transport(stage: string): void {
68
+ if (!TIER1_TRACE) return;
69
+ process.stderr.write(`[tier1-transport] ${stage}\n`);
70
+ }
71
+
72
+ function isFrameAssetPath(path: string): boolean {
73
+ return (
74
+ ((path.startsWith(FRAME_RUNTIME_PREFIX) || path.startsWith(FRAME_CHUNK_PREFIX)) &&
75
+ path.endsWith(".js")) ||
76
+ path === `${ROUTE_FRAME}/frame.css` ||
77
+ path === `${ROUTE_FRAME}/artifact.css`
78
+ );
79
+ }
80
+
81
+ function startRequestHeartbeat(requestId: string, command: string): () => void {
82
+ if (!TIER1_TRACE) return () => {};
83
+ let ticks = 0;
84
+ const heartbeat = setInterval(() => {
85
+ ticks += 1;
86
+ traceTier1Transport(
87
+ `request:heartbeat tick=${ticks} command=${command} requestId=${requestId}`,
88
+ );
89
+ }, 1_000);
90
+ heartbeat.unref();
91
+ traceTier1Transport(`request:heartbeat:start command=${command} requestId=${requestId}`);
92
+ return () => {
93
+ clearInterval(heartbeat);
94
+ traceTier1Transport(
95
+ `request:heartbeat:stop ticks=${ticks} command=${command} requestId=${requestId}`,
96
+ );
97
+ };
98
+ }
99
+
100
+ // Re-export so `import { RAW_BODY_CAP_BYTES } from "./router"` keeps
101
+ // working — the constant moved to `router-guards.ts` for size reasons.
102
+ export { RAW_BODY_CAP_BYTES };
103
+
104
+ export interface RouterDeps extends DispatcherDeps {
105
+ readonly installToken: string;
106
+ readonly promoteToken: string | null;
107
+ readonly logger: FacetLogger;
108
+ readonly expectedHost: string | (() => string);
109
+ readonly ownOrigin: string | (() => string);
110
+ readonly startTime: number;
111
+ /** Per-artifact SSE fan-out the stream route registers into. */
112
+ readonly broadcaster: RevisionBroadcaster;
113
+ readonly heartbeatIntervalMs?: number;
114
+ readonly galleryBootstrap?: Map<
115
+ string,
116
+ { readonly artifactId: string; readonly revisionSha: string; readonly leaseId: string }
117
+ >;
118
+ }
119
+
120
+ export async function handleCommand(deps: RouterDeps, req: Request): Promise<Response> {
121
+ const startNs = performance.now();
122
+ const meta: RequestMeta = readRequestMeta(req);
123
+ const requestId = pickRequestId(null);
124
+
125
+ // 1. Host exact-match (DNS-rebinding defense). Reject missing Host.
126
+ const hostCheck: HostOriginResult = checkHostOrigin({
127
+ method: meta.method,
128
+ host: meta.host,
129
+ origin: meta.origin,
130
+ secFetchSite: meta.secFetchSite,
131
+ expectedHost: resolveHost(deps.expectedHost),
132
+ ownOrigin: resolveHost(deps.ownOrigin),
133
+ });
134
+ if (!hostCheck.ok) {
135
+ deps.logger.warn("request.rejected", {
136
+ reason: "host_origin",
137
+ errorCode: hostCheck.error.code,
138
+ url: meta.url,
139
+ });
140
+ return envelopeResponse(
141
+ errEnvelope(requestId, hostCheck.error.toBody()),
142
+ statusForHostCheck(hostCheck.error),
143
+ );
144
+ }
145
+
146
+ if (meta.method.toUpperCase() === "GET") {
147
+ return envelopeResponse(
148
+ errEnvelope(requestId, {
149
+ code: "invalid_request",
150
+ message: "GET is not supported on the command endpoint",
151
+ retryable: false,
152
+ }),
153
+ 405,
154
+ );
155
+ }
156
+
157
+ // 2. Bearer auth BEFORE any body read. Install or operator bearer is
158
+ // accepted; the per-verb gate below enforces the operator requirement
159
+ // for promote.
160
+ const authCandidates: string[] = [deps.installToken];
161
+ if (deps.promoteToken !== null) authCandidates.push(deps.promoteToken);
162
+ const authResult = requireAnyBearer(meta.authorization, authCandidates);
163
+ if (!authResult.ok) {
164
+ deps.logger.warn("auth.failed", {
165
+ requestId,
166
+ errorCode: authResult.error.code,
167
+ });
168
+ return envelopeResponse(errEnvelope(requestId, authResult.error.toBody()), 401);
169
+ }
170
+ const matchedIsOperator = authResult.matchedIndex > 0; // index 0 = install, 1 = operator
171
+
172
+ // 3. Mutation Content-Type. GET already handled; mutations require JSON.
173
+ const contentCheck = checkMutationSecurityHeaders({
174
+ method: meta.method,
175
+ contentType: meta.contentType,
176
+ });
177
+ if (!contentCheck.ok) {
178
+ deps.logger.warn("request.rejected", {
179
+ reason: "content_type",
180
+ errorCode: contentCheck.error.code,
181
+ });
182
+ return envelopeResponse(errEnvelope(requestId, contentCheck.error.toBody()), 400);
183
+ }
184
+
185
+ // 4. Read body, enforcing the raw size cap BEFORE JSON.parse.
186
+ let bodyText: string;
187
+ try {
188
+ bodyText = await readCappedBody(req, meta.contentLength);
189
+ } catch (error) {
190
+ const err = FacetError.from(error);
191
+ deps.logger.warn("request.rejected", {
192
+ reason: "body_size",
193
+ errorCode: err.code,
194
+ });
195
+ return envelopeResponse(
196
+ errEnvelope(requestId, err.toBody()),
197
+ err.code === "payload_too_large" ? 413 : 400,
198
+ );
199
+ }
200
+ const bodyRaw = parseBody(bodyText);
201
+ if (bodyRaw === INVALID_JSON) {
202
+ const err = new FacetError("invalid_envelope", "Request body is not valid JSON", {
203
+ retryable: false,
204
+ });
205
+ return envelopeResponse(errEnvelope(requestId, err.toBody()), 400);
206
+ }
207
+
208
+ // 5. Parse envelope.
209
+ const env = parseEnvelope(bodyRaw);
210
+ if (!env.ok) {
211
+ return envelopeResponse(errEnvelope(requestId, env.body), 400);
212
+ }
213
+ if (env.envelope.ok === false) {
214
+ return envelopeResponse(env.envelope, 400);
215
+ }
216
+
217
+ // 6. Parse the command.
218
+ const cmdParse = CommandRequestSchema.safeParse(env.envelope.data);
219
+ if (!cmdParse.success) {
220
+ const err = new FacetError("invalid_request", "Command request failed validation", {
221
+ retryable: false,
222
+ details: { issueCount: cmdParse.error.issues.length },
223
+ });
224
+ return envelopeResponse(errEnvelope(requestId, err.toBody()), 400);
225
+ }
226
+ const command: CommandRequest = cmdParse.data;
227
+
228
+ // 7. Reserved-verb gate.
229
+ const reserved = checkCommandImplemented(command.command);
230
+ if (reserved !== null) {
231
+ return envelopeResponse(errEnvelope(requestId, reserved.toBody()), 400);
232
+ }
233
+
234
+ // 8. Promote requires the operator token (constant-time compare).
235
+ if (command.command === "promote") {
236
+ if (!matchedIsOperator) {
237
+ const err = new FacetError("invalid_envelope", "Promote requires the operator token", {
238
+ retryable: false,
239
+ details: { reason: "operator_token_required" },
240
+ });
241
+ return envelopeResponse(errEnvelope(requestId, err.toBody()), 403);
242
+ }
243
+ }
244
+
245
+ deps.idle.acquire(`request:${requestId}`);
246
+ const stopHeartbeat = startRequestHeartbeat(requestId, command.command);
247
+ try {
248
+ const result = await dispatch(deps, command, requestId);
249
+ traceTier1Transport(
250
+ `router:dispatch-result-received command=${command.command} requestId=${requestId}`,
251
+ );
252
+ let resultForWire = result;
253
+ if (command.command === "open") {
254
+ const openResult = result as {
255
+ readonly artifactId: string;
256
+ readonly revisionSha: string;
257
+ readonly lease: { readonly leaseId: string; readonly expiresAt: number };
258
+ };
259
+ const token = crypto.randomUUID();
260
+ deps.galleryBootstrap?.set(token, {
261
+ artifactId: openResult.artifactId,
262
+ revisionSha: openResult.revisionSha,
263
+ leaseId: openResult.lease.leaseId,
264
+ });
265
+ resultForWire = {
266
+ ...(result as Record<string, unknown>),
267
+ frameUrl: `${resolveHost(deps.ownOrigin)}/gallery#bootstrap=${encodeURIComponent(token)}`,
268
+ };
269
+ }
270
+ const safeResult = CommandResultSchema.parse(resultForWire);
271
+ traceTier1Transport(
272
+ `router:envelope-response:start command=${command.command} requestId=${requestId}`,
273
+ );
274
+ const elapsedMs = Math.round(performance.now() - startNs);
275
+ deps.logger.info("command.completed", {
276
+ requestId,
277
+ command: command.command,
278
+ durationMs: elapsedMs,
279
+ });
280
+ return envelopeResponse(okEnvelope(requestId, safeResult), 200);
281
+ } catch (error) {
282
+ const facetError = FacetError.from(error);
283
+ const elapsedMs = Math.round(performance.now() - startNs);
284
+ deps.logger.warn("command.failed", {
285
+ requestId,
286
+ command: command.command,
287
+ errorCode: facetError.code,
288
+ durationMs: elapsedMs,
289
+ });
290
+ return envelopeResponse(errEnvelope(requestId, facetError.toBody()), statusFor(facetError));
291
+ } finally {
292
+ stopHeartbeat();
293
+ deps.idle.release(`request:${requestId}`);
294
+ }
295
+ }
296
+
297
+ export function buildRouter(deps: RouterDeps): {
298
+ fetch: (req: Request) => Promise<Response> | Response;
299
+ } {
300
+ const bootstrap = new Map<
301
+ string,
302
+ { readonly artifactId: string; readonly revisionSha: string; readonly leaseId: string }
303
+ >();
304
+ const packageRoot = join(import.meta.dir, "../..");
305
+ const galleryRoot = resolveGalleryRoot(packageRoot);
306
+ // style-src allows 'unsafe-inline' because mermaid injects its theme
307
+ // as a <style> element into the rendered SVG (operator-ruled posture
308
+ // for this local-only tool); script-src stays 'self' blob: — no
309
+ // script relaxation.
310
+ const galleryCsp =
311
+ "default-src 'self'; script-src 'self' blob:; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data: https:; font-src 'self' data:; frame-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'";
312
+ let galleryBuild: Promise<void> | null = null;
313
+
314
+ const ensureGalleryBuild = async (): Promise<void> => {
315
+ const requiredAssets = [
316
+ join(galleryRoot, "index.html"),
317
+ join(galleryRoot, "frame", "frame.css"),
318
+ join(galleryRoot, "frame", "artifact.css"),
319
+ ...ARTIFACT_TYPES.map((artifactType) =>
320
+ join(galleryRoot, "frame", "runtime", `${artifactType}.js`),
321
+ ),
322
+ ];
323
+ if ((await Promise.all(requiredAssets.map((asset) => Bun.file(asset).exists()))).every(Boolean))
324
+ return;
325
+ galleryBuild ??= (async () => {
326
+ const process = Bun.spawn(["bun", "scripts/build-gallery.ts"], {
327
+ cwd: packageRoot,
328
+ stderr: "pipe",
329
+ stdout: "ignore",
330
+ });
331
+ const exitCode = await process.exited;
332
+ if (exitCode !== 0) {
333
+ const details = await new Response(process.stderr).text();
334
+ throw new Error(`Gallery build failed (exit ${exitCode})${details ? `: ${details}` : ""}`);
335
+ }
336
+ })().finally(() => {
337
+ galleryBuild = null;
338
+ });
339
+ await galleryBuild;
340
+ };
341
+
342
+ const galleryResponse = async (path: string): Promise<Response> => {
343
+ try {
344
+ await ensureGalleryBuild();
345
+ } catch (error) {
346
+ return new Response(error instanceof Error ? error.message : "Gallery build failed", {
347
+ status: 500,
348
+ headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" },
349
+ });
350
+ }
351
+ const requested = path === ROUTE_GALLERY ? "index.html" : path.replace(/^\/gallery\/?/, "");
352
+ return serveGalleryFile(requested, galleryCsp);
353
+ };
354
+
355
+ const serveGalleryFile = async (
356
+ requested: string,
357
+ csp?: string,
358
+ extraHeaders: Record<string, string> = {},
359
+ ): Promise<Response> => {
360
+ let decoded: string;
361
+ try {
362
+ decoded = decodeURIComponent(requested);
363
+ } catch {
364
+ return new Response("Not found", { status: 404 });
365
+ }
366
+ const candidate = normalize(join(galleryRoot, decoded));
367
+ const rootRelative = relative(galleryRoot, candidate);
368
+ if (rootRelative.startsWith("..") || rootRelative.includes("/..")) {
369
+ return new Response("Not found", { status: 404 });
370
+ }
371
+ const file = Bun.file(candidate);
372
+ if (!(await file.exists())) return new Response("Not found", { status: 404 });
373
+ return new Response(file, {
374
+ status: 200,
375
+ headers: {
376
+ "cache-control": "no-store",
377
+ "content-type": file.type || "application/octet-stream",
378
+ ...extraHeaders,
379
+ ...(csp === undefined ? {} : { "content-security-policy": csp }),
380
+ },
381
+ });
382
+ };
383
+
384
+ return {
385
+ async fetch(req: Request): Promise<Response> {
386
+ const url = new URL(req.url);
387
+ const path = url.pathname;
388
+ if (isFrameAssetPath(path) && req.method.toUpperCase() === "GET") {
389
+ try {
390
+ await ensureGalleryBuild();
391
+ } catch (error) {
392
+ return new Response(error instanceof Error ? error.message : "Gallery build failed", {
393
+ status: 500,
394
+ headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" },
395
+ });
396
+ }
397
+ return serveGalleryFile(path.replace(/^\/gallery\//, ""));
398
+ }
399
+ if (path === ROUTE_FRAME && req.method.toUpperCase() === "GET") {
400
+ const artifactType = ArtifactTypeSchema.safeParse(url.searchParams.get("type"));
401
+ if (!artifactType.success) {
402
+ return new Response("Invalid frame artifact type", {
403
+ status: 400,
404
+ headers: {
405
+ "cache-control": "no-store",
406
+ "content-type": "text/plain; charset=utf-8",
407
+ },
408
+ });
409
+ }
410
+ const theme = url.searchParams.get("theme") ?? "dark";
411
+ if (!isResolvedGalleryTheme(theme)) {
412
+ return new Response("Invalid frame theme", {
413
+ status: 400,
414
+ headers: {
415
+ "cache-control": "no-store",
416
+ "content-type": "text/plain; charset=utf-8",
417
+ },
418
+ });
419
+ }
420
+ try {
421
+ await ensureGalleryBuild();
422
+ } catch (error) {
423
+ return new Response(error instanceof Error ? error.message : "Gallery build failed", {
424
+ status: 500,
425
+ headers: {
426
+ "content-type": "text/plain; charset=utf-8",
427
+ "cache-control": "no-store",
428
+ },
429
+ });
430
+ }
431
+ return new Response(
432
+ buildFrameDocument({
433
+ artifactType: artifactType.data,
434
+ runtimeUrl: `${FRAME_RUNTIME_PREFIX}${artifactType.data}.js`,
435
+ theme,
436
+ }),
437
+ {
438
+ status: 200,
439
+ headers: {
440
+ "cache-control": "no-store",
441
+ "content-security-policy": galleryCsp,
442
+ "content-type": "text/html; charset=utf-8",
443
+ },
444
+ },
445
+ );
446
+ }
447
+ if (
448
+ (path === ROUTE_GALLERY || path.startsWith(`${ROUTE_GALLERY}/`)) &&
449
+ req.method.toUpperCase() === "GET"
450
+ ) {
451
+ return galleryResponse(path);
452
+ }
453
+ if (req.method.toUpperCase() === "GET" && path !== "/") {
454
+ // `ensureGalleryBuild` requires index.html to exist, so a direct
455
+ // navigation to /index.html (or any other .html document) reaches
456
+ // this fallback and must carry the same gallery CSP as /gallery —
457
+ // otherwise the gallery document ships without its CSP purely
458
+ // because of which URL served it.
459
+ const isDocument = /\.html$/i.test(path);
460
+ if (isDocument) {
461
+ try {
462
+ await ensureGalleryBuild();
463
+ } catch (error) {
464
+ return new Response(error instanceof Error ? error.message : "Gallery build failed", {
465
+ status: 500,
466
+ headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" },
467
+ });
468
+ }
469
+ }
470
+ const rootAssetResponse = await serveGalleryFile(
471
+ path.replace(/^\//, ""),
472
+ isDocument ? galleryCsp : undefined,
473
+ {},
474
+ );
475
+ if (rootAssetResponse.status !== 404) return rootAssetResponse;
476
+ }
477
+ if (path === ROUTE_BOOTSTRAP && req.method.toUpperCase() === "POST") {
478
+ const body = (await req.json().catch(() => null)) as { token?: unknown } | null;
479
+ const token = typeof body?.token === "string" ? body.token : null;
480
+ const handoff = token === null ? undefined : bootstrap.get(token);
481
+ if (token !== null) bootstrap.delete(token);
482
+ const lease =
483
+ handoff === undefined
484
+ ? undefined
485
+ : deps.leases.list().find((entry) => entry.leaseId === handoff.leaseId);
486
+ if (handoff === undefined || lease === undefined) {
487
+ return envelopeResponse(
488
+ errEnvelope(generateRequestId(), {
489
+ code: "invalid_envelope",
490
+ message: "Bootstrap capability is invalid or already used",
491
+ retryable: false,
492
+ }),
493
+ 401,
494
+ );
495
+ }
496
+ return new Response(
497
+ JSON.stringify({
498
+ authorization: `Bearer ${deps.installToken}`,
499
+ artifactId: handoff.artifactId,
500
+ revisionSha: handoff.revisionSha,
501
+ lease: { leaseId: lease.leaseId, expiresAt: lease.expiresAt },
502
+ }),
503
+ {
504
+ status: 200,
505
+ headers: { "content-type": "application/json", "cache-control": "no-store" },
506
+ },
507
+ );
508
+ }
509
+ if (path === ROUTE_RELEASE && req.method.toUpperCase() === "POST") {
510
+ const meta = readRequestMeta(req);
511
+ const auth = requireAnyBearer(meta.authorization, [deps.installToken]);
512
+ const leaseId = meta.headers.get("x-gallery-lease");
513
+ const artifactId = meta.headers.get("x-gallery-artifact");
514
+ if (!auth.ok || leaseId === null || artifactId === null) {
515
+ return envelopeResponse(
516
+ errEnvelope(generateRequestId(), {
517
+ code: "invalid_envelope",
518
+ message: "Display release requires authorization and lease headers",
519
+ retryable: false,
520
+ }),
521
+ 401,
522
+ );
523
+ }
524
+ const lease = deps.leases
525
+ .list()
526
+ .find((entry) => entry.leaseId === leaseId && entry.artifactId === artifactId);
527
+ if (lease !== undefined) {
528
+ deps.leases.release(leaseId);
529
+ deps.idle.release(`lease:${leaseId}`);
530
+ }
531
+ return new Response(null, { status: 204, headers: { "cache-control": "no-store" } });
532
+ }
533
+ if (path === ROUTE_SOURCE && req.method.toUpperCase() === "GET") {
534
+ const resolved = resolveGalleryRequest(req, deps);
535
+ if (!resolved.ok) return resolved.response;
536
+ const { artifactId, revision, artifact } = resolved;
537
+ return new Response(
538
+ JSON.stringify({
539
+ artifactId,
540
+ slug: artifact.slug,
541
+ title: artifact.title,
542
+ revisionSha: revision.sha256,
543
+ artifactType: revision.artifactType,
544
+ renderer: revision.renderer,
545
+ source: new TextDecoder().decode(revision.source),
546
+ verdict: latestStoredVerdict(deps.repository, revision),
547
+ ...(revision.artifactType === "tsx"
548
+ ? (() => {
549
+ const run = deps.repository.listRenderRuns({
550
+ revisionId: revision.id,
551
+ tier: 0,
552
+ })[0];
553
+ const compiledPath = run?.compiledPath ?? null;
554
+ const root = deps.repository.getEvidenceRoot();
555
+ if (compiledPath === null || root === undefined) return {};
556
+ try {
557
+ const candidate = realpathSync(compiledPath);
558
+ const evidenceRoot = realpathSync(root);
559
+ const rootRelative = relative(evidenceRoot, candidate);
560
+ if (
561
+ rootRelative.length === 0 ||
562
+ rootRelative === ".." ||
563
+ rootRelative.startsWith("../")
564
+ ) {
565
+ return {};
566
+ }
567
+ return {
568
+ renderBytesBase64: Buffer.from(readFileSync(candidate)).toString("base64"),
569
+ execution: revision.execution ?? "static",
570
+ };
571
+ } catch {
572
+ return {};
573
+ }
574
+ })()
575
+ : {}),
576
+ }),
577
+ {
578
+ status: 200,
579
+ headers: { "content-type": "application/json", "cache-control": "no-store" },
580
+ },
581
+ );
582
+ }
583
+ if (path === ROUTE_EVIDENCE && req.method.toUpperCase() === "GET") {
584
+ const resolved = resolveGalleryRequest(req, deps);
585
+ if (!resolved.ok) return resolved.response;
586
+ const { revision, artifact } = resolved;
587
+ try {
588
+ const evidence = readStoredRenderEvidence({
589
+ repository: deps.repository,
590
+ artifact,
591
+ revision,
592
+ });
593
+ return new Response(evidence.bytes, {
594
+ status: 200,
595
+ headers: { "content-type": evidence.mediaType, "cache-control": "no-store" },
596
+ });
597
+ } catch (error) {
598
+ if (error instanceof FacetError && error.code === "evidence_unavailable") {
599
+ return envelopeResponse(errEnvelope(generateRequestId(), error.toBody()), 404);
600
+ }
601
+ throw error;
602
+ }
603
+ }
604
+ if (path === ROUTE_API) return handleCommand({ ...deps, galleryBootstrap: bootstrap }, req);
605
+ if (path === ROUTE_STREAM) {
606
+ const meta = readRequestMeta(req);
607
+ return handleStream(deps, {
608
+ url: meta.url,
609
+ method: meta.method,
610
+ host: meta.host,
611
+ origin: meta.origin,
612
+ secFetchSite: meta.secFetchSite,
613
+ authorization: meta.authorization,
614
+ headers: meta.headers,
615
+ });
616
+ }
617
+ return envelopeResponse(
618
+ errEnvelope(generateRequestId(), {
619
+ code: "invalid_request",
620
+ message: "Unknown route",
621
+ retryable: false,
622
+ }),
623
+ 404,
624
+ );
625
+ },
626
+ };
627
+ }