@hraness/slopcamera 3.2.4

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 (441) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +60 -0
  3. package/PRIVACY.md +46 -0
  4. package/README.md +311 -0
  5. package/SECURITY.md +21 -0
  6. package/apps/desktop/README.md +966 -0
  7. package/apps/desktop/analysis/protocol.ts +223 -0
  8. package/apps/desktop/application/capability-binding.ts +594 -0
  9. package/apps/desktop/application/context.ts +97 -0
  10. package/apps/desktop/application/creative-iteration.ts +1385 -0
  11. package/apps/desktop/application/default-registry.ts +146 -0
  12. package/apps/desktop/application/directing-contract.ts +218 -0
  13. package/apps/desktop/application/directing-plan.ts +178 -0
  14. package/apps/desktop/application/errors.ts +18 -0
  15. package/apps/desktop/application/gateway-port.ts +867 -0
  16. package/apps/desktop/application/html-overlay-browser-runtime.ts +687 -0
  17. package/apps/desktop/application/html-overlay-integrity.ts +337 -0
  18. package/apps/desktop/application/html-overlay-renderer.ts +45 -0
  19. package/apps/desktop/application/index.ts +13 -0
  20. package/apps/desktop/application/operation-completion-checkpoint.ts +333 -0
  21. package/apps/desktop/application/operation-effects.ts +94 -0
  22. package/apps/desktop/application/operation.ts +224 -0
  23. package/apps/desktop/application/operations/analysis/faces.ts +244 -0
  24. package/apps/desktop/application/operations/analysis/music.ts +248 -0
  25. package/apps/desktop/application/operations/analysis/project-auto-zooms.ts +302 -0
  26. package/apps/desktop/application/operations/analysis/project-inactivity.ts +381 -0
  27. package/apps/desktop/application/operations/analysis/scenes.ts +272 -0
  28. package/apps/desktop/application/operations/analysis/shared.ts +202 -0
  29. package/apps/desktop/application/operations/derive/edit-batch.ts +967 -0
  30. package/apps/desktop/application/operations/derive/follow-faces.ts +454 -0
  31. package/apps/desktop/application/operations/edit/bind-candidate-revision.ts +91 -0
  32. package/apps/desktop/application/operations/edit/create-candidate-revision.ts +377 -0
  33. package/apps/desktop/application/operations/edit/create-revision.ts +255 -0
  34. package/apps/desktop/application/operations/edit/freeze-revision.ts +182 -0
  35. package/apps/desktop/application/operations/gateway/image.ts +41 -0
  36. package/apps/desktop/application/operations/gateway/index.ts +17 -0
  37. package/apps/desktop/application/operations/gateway/shared.ts +122 -0
  38. package/apps/desktop/application/operations/gateway/speech.ts +41 -0
  39. package/apps/desktop/application/operations/gateway/transcription.ts +42 -0
  40. package/apps/desktop/application/operations/gateway/video.ts +41 -0
  41. package/apps/desktop/application/operations/index.ts +31 -0
  42. package/apps/desktop/application/operations/iteration/create-candidate.ts +240 -0
  43. package/apps/desktop/application/operations/iteration/create-matrix.ts +117 -0
  44. package/apps/desktop/application/operations/iteration/select.ts +155 -0
  45. package/apps/desktop/application/operations/iteration/shared.ts +250 -0
  46. package/apps/desktop/application/operations/media/audio-effects.ts +376 -0
  47. package/apps/desktop/application/operations/media/binding.ts +79 -0
  48. package/apps/desktop/application/operations/media/capabilities.ts +112 -0
  49. package/apps/desktop/application/operations/media/color-grade.ts +343 -0
  50. package/apps/desktop/application/operations/media/html-overlay.ts +802 -0
  51. package/apps/desktop/application/operations/media/index.ts +8 -0
  52. package/apps/desktop/application/operations/media/ingest.ts +303 -0
  53. package/apps/desktop/application/operations/media/overlay.ts +729 -0
  54. package/apps/desktop/application/operations/media/shared.ts +845 -0
  55. package/apps/desktop/application/operations/media/transform-platform.ts +120 -0
  56. package/apps/desktop/application/operations/project/commit-edits.ts +2054 -0
  57. package/apps/desktop/application/operations/project/promote-selection.ts +495 -0
  58. package/apps/desktop/application/operations/project/snapshot.ts +95 -0
  59. package/apps/desktop/application/operations/recording/index.ts +5 -0
  60. package/apps/desktop/application/operations/recording/pause.ts +34 -0
  61. package/apps/desktop/application/operations/recording/resume.ts +34 -0
  62. package/apps/desktop/application/operations/recording/shared.ts +199 -0
  63. package/apps/desktop/application/operations/recording/start.ts +169 -0
  64. package/apps/desktop/application/operations/recording/stop.ts +34 -0
  65. package/apps/desktop/application/operations/render/bind-candidate-output.ts +324 -0
  66. package/apps/desktop/application/operations/render/materialize-selection.ts +322 -0
  67. package/apps/desktop/application/operations/render/project-caption-plan.ts +287 -0
  68. package/apps/desktop/application/operations/render/project-plan.ts +939 -0
  69. package/apps/desktop/application/operations/render/project-spatial-receipt.ts +70 -0
  70. package/apps/desktop/application/operations/render/project.ts +1922 -0
  71. package/apps/desktop/application/operations/shared.ts +67 -0
  72. package/apps/desktop/application/operations/slopcamera-portable.ts +1188 -0
  73. package/apps/desktop/application/operations/slopcamera-visuals.ts +784 -0
  74. package/apps/desktop/application/operations/spatial-project.ts +209 -0
  75. package/apps/desktop/application/operations/spatial-render.ts +415 -0
  76. package/apps/desktop/application/operations/spatial-scene.ts +97 -0
  77. package/apps/desktop/application/operations/studio.ts +143 -0
  78. package/apps/desktop/application/output-publication-lease.ts +163 -0
  79. package/apps/desktop/application/project-publication-lease.ts +233 -0
  80. package/apps/desktop/application/project-store.ts +196 -0
  81. package/apps/desktop/application/receipts.ts +533 -0
  82. package/apps/desktop/application/registry.ts +182 -0
  83. package/apps/desktop/application/spatial-assets.ts +675 -0
  84. package/apps/desktop/application/spatial-compositor-cadence.ts +23 -0
  85. package/apps/desktop/application/spatial-project-authority.ts +35 -0
  86. package/apps/desktop/application/spatial-project-lease.ts +126 -0
  87. package/apps/desktop/application/spatial-project-render-preparation.ts +225 -0
  88. package/apps/desktop/application/spatial-project-store.ts +286 -0
  89. package/apps/desktop/application/spatial-render-projection.ts +201 -0
  90. package/apps/desktop/application/spatial-render.ts +677 -0
  91. package/apps/desktop/application/spatial-spz.ts +44 -0
  92. package/apps/desktop/application/spatial-world-import.ts +135 -0
  93. package/apps/desktop/application/spatial-world-provenance.ts +46 -0
  94. package/apps/desktop/application/studio-port.ts +58 -0
  95. package/apps/desktop/application/verified-receipt-reconciliation.ts +1574 -0
  96. package/apps/desktop/capture/protocol.ts +689 -0
  97. package/apps/desktop/cli/analyzer.ts +406 -0
  98. package/apps/desktop/cli/args.ts +3181 -0
  99. package/apps/desktop/cli/asset-ingest.ts +747 -0
  100. package/apps/desktop/cli/atomic-render-effects.ts +210 -0
  101. package/apps/desktop/cli/atomic-render-platform.ts +205 -0
  102. package/apps/desktop/cli/atomic-render.ts +50 -0
  103. package/apps/desktop/cli/audio-analysis.ts +252 -0
  104. package/apps/desktop/cli/bounded-map.ts +39 -0
  105. package/apps/desktop/cli/bundle-service.ts +256 -0
  106. package/apps/desktop/cli/capabilities.ts +212 -0
  107. package/apps/desktop/cli/capture-bundle.ts +1117 -0
  108. package/apps/desktop/cli/command-host-resources.ts +251 -0
  109. package/apps/desktop/cli/commands.ts +6836 -0
  110. package/apps/desktop/cli/directing-blob.ts +291 -0
  111. package/apps/desktop/cli/directing-media.ts +517 -0
  112. package/apps/desktop/cli/directing-quote.ts +89 -0
  113. package/apps/desktop/cli/directing-service.ts +288 -0
  114. package/apps/desktop/cli/emoji-assets.ts +538 -0
  115. package/apps/desktop/cli/errors.ts +29 -0
  116. package/apps/desktop/cli/face-analysis-service.ts +940 -0
  117. package/apps/desktop/cli/filter-script.ts +76 -0
  118. package/apps/desktop/cli/gateway-application-port.ts +1142 -0
  119. package/apps/desktop/cli/gateway-credential.ts +81 -0
  120. package/apps/desktop/cli/gateway-media-artifacts.ts +723 -0
  121. package/apps/desktop/cli/gateway-media-catalog.ts +922 -0
  122. package/apps/desktop/cli/gateway-media-service.ts +3106 -0
  123. package/apps/desktop/cli/gateway-media-signature.ts +122 -0
  124. package/apps/desktop/cli/gateway-provider-options.ts +562 -0
  125. package/apps/desktop/cli/gateway-scene-provider.ts +391 -0
  126. package/apps/desktop/cli/help.ts +501 -0
  127. package/apps/desktop/cli/html-overlay-gpu.ts +97 -0
  128. package/apps/desktop/cli/html-overlay-renderer.ts +2478 -0
  129. package/apps/desktop/cli/io.ts +290 -0
  130. package/apps/desktop/cli/main.ts +123 -0
  131. package/apps/desktop/cli/media-effects-service.ts +885 -0
  132. package/apps/desktop/cli/media-ingest-model.ts +364 -0
  133. package/apps/desktop/cli/media-ingest-platform.ts +227 -0
  134. package/apps/desktop/cli/media-ingest-program.ts +200 -0
  135. package/apps/desktop/cli/media-ingest.ts +30 -0
  136. package/apps/desktop/cli/music-analysis-service.ts +434 -0
  137. package/apps/desktop/cli/mutation-lock.ts +530 -0
  138. package/apps/desktop/cli/native-assets.d.ts +12 -0
  139. package/apps/desktop/cli/native-media-runtime.macos.ts +7 -0
  140. package/apps/desktop/cli/overlay-playback.ts +177 -0
  141. package/apps/desktop/cli/paths.ts +265 -0
  142. package/apps/desktop/cli/portable-surface.ts +242 -0
  143. package/apps/desktop/cli/project-camera-receipt.ts +112 -0
  144. package/apps/desktop/cli/project-face-camera.ts +394 -0
  145. package/apps/desktop/cli/project-inactivity-service.ts +552 -0
  146. package/apps/desktop/cli/project-media-integrity.ts +121 -0
  147. package/apps/desktop/cli/project-renderer.ts +1526 -0
  148. package/apps/desktop/cli/project-service.ts +519 -0
  149. package/apps/desktop/cli/project-state-transaction.ts +520 -0
  150. package/apps/desktop/cli/query-limits.ts +1 -0
  151. package/apps/desktop/cli/recording-controller.ts +1567 -0
  152. package/apps/desktop/cli/recording-daemon.ts +931 -0
  153. package/apps/desktop/cli/recording-ref.ts +56 -0
  154. package/apps/desktop/cli/renamed-environment.ts +6 -0
  155. package/apps/desktop/cli/renderer.ts +1439 -0
  156. package/apps/desktop/cli/run-cli-test-helper.ts +60 -0
  157. package/apps/desktop/cli/scene-analysis-service.ts +879 -0
  158. package/apps/desktop/cli/spatial-compositor-verifier.ts +213 -0
  159. package/apps/desktop/cli/spatial-project-service.ts +90 -0
  160. package/apps/desktop/cli/spatial-scene-service.ts +105 -0
  161. package/apps/desktop/cli/spatial-world-service.ts +39 -0
  162. package/apps/desktop/cli/speech-analysis-service.ts +682 -0
  163. package/apps/desktop/cli/studio-assemble.ts +58 -0
  164. package/apps/desktop/cli/studio-command.ts +60 -0
  165. package/apps/desktop/cli/studio-custody.ts +60 -0
  166. package/apps/desktop/cli/studio-encode.ts +216 -0
  167. package/apps/desktop/cli/studio-exr.ts +52 -0
  168. package/apps/desktop/cli/studio-files.ts +179 -0
  169. package/apps/desktop/cli/studio-output-validation.ts +162 -0
  170. package/apps/desktop/cli/studio-process.ts +145 -0
  171. package/apps/desktop/cli/studio-runtime.ts +77 -0
  172. package/apps/desktop/cli/studio-scaffold.ts +70 -0
  173. package/apps/desktop/cli/studio-service.ts +301 -0
  174. package/apps/desktop/cli/studio-spatial-asset.ts +248 -0
  175. package/apps/desktop/cli/studio-template-names.ts +3 -0
  176. package/apps/desktop/cli/studio-workflow.ts +19 -0
  177. package/apps/desktop/cli/time.ts +25 -0
  178. package/apps/desktop/cli/workflow-code.ts +584 -0
  179. package/apps/desktop/cli/workflow-runs.ts +576 -0
  180. package/apps/desktop/code/advanced.ts +139 -0
  181. package/apps/desktop/code/application-node-planner.ts +1288 -0
  182. package/apps/desktop/code/compiler.ts +329 -0
  183. package/apps/desktop/code/contracts.ts +13 -0
  184. package/apps/desktop/code/creative-recipes.ts +94 -0
  185. package/apps/desktop/code/define-workflow.ts +115 -0
  186. package/apps/desktop/code/editing.ts +284 -0
  187. package/apps/desktop/code/file-candidate-provenance.ts +420 -0
  188. package/apps/desktop/code/file-candidate.ts +69 -0
  189. package/apps/desktop/code/graph-builder.ts +10 -0
  190. package/apps/desktop/code/host-resource-policy.ts +55 -0
  191. package/apps/desktop/code/plan-contracts.ts +159 -0
  192. package/apps/desktop/code/planning.ts +361 -0
  193. package/apps/desktop/code/public.ts +134 -0
  194. package/apps/desktop/code/reference-projection.ts +62 -0
  195. package/apps/desktop/code/run-contracts.ts +466 -0
  196. package/apps/desktop/code/run-store.ts +2478 -0
  197. package/apps/desktop/code/runtime-identity.ts +328 -0
  198. package/apps/desktop/code/scheduler.ts +3067 -0
  199. package/apps/desktop/code/semantic-builder.ts +1436 -0
  200. package/apps/desktop/code/source-bundle.ts +1641 -0
  201. package/apps/desktop/code/source-typecheck.ts +211 -0
  202. package/apps/desktop/code/testing.ts +79 -0
  203. package/apps/desktop/code/worker-client.ts +1675 -0
  204. package/apps/desktop/code/worker-effects.ts +141 -0
  205. package/apps/desktop/code/worker-entry.ts +435 -0
  206. package/apps/desktop/code/worker-lease-guardian.ts +179 -0
  207. package/apps/desktop/code/worker-process-identity.ts +484 -0
  208. package/apps/desktop/code/worker-protocol.ts +211 -0
  209. package/apps/desktop/code/workflow-effects.ts +132 -0
  210. package/apps/desktop/contracts/analysis.ts +949 -0
  211. package/apps/desktop/contracts/edit.ts +576 -0
  212. package/apps/desktop/contracts/index.ts +9 -0
  213. package/apps/desktop/contracts/media-effects.ts +139 -0
  214. package/apps/desktop/contracts/project-render.ts +644 -0
  215. package/apps/desktop/contracts/project.ts +740 -0
  216. package/apps/desktop/contracts/recording.ts +1434 -0
  217. package/apps/desktop/contracts/render.ts +334 -0
  218. package/apps/desktop/contracts/runtime.ts +155 -0
  219. package/apps/desktop/contracts/spatial-compositor.ts +58 -0
  220. package/apps/desktop/contracts/spatial-project.ts +159 -0
  221. package/apps/desktop/contracts/spatial-world.ts +48 -0
  222. package/apps/desktop/contracts/video-effects.ts +120 -0
  223. package/apps/desktop/core/alignment-apply.ts +400 -0
  224. package/apps/desktop/core/audio-alignment.ts +466 -0
  225. package/apps/desktop/core/auto-zoom.ts +209 -0
  226. package/apps/desktop/core/canonical-json.ts +14 -0
  227. package/apps/desktop/core/emoji.ts +210 -0
  228. package/apps/desktop/core/events.ts +409 -0
  229. package/apps/desktop/core/face-framing.ts +533 -0
  230. package/apps/desktop/core/face-tracks.ts +326 -0
  231. package/apps/desktop/core/inactivity.ts +137 -0
  232. package/apps/desktop/core/index.ts +30 -0
  233. package/apps/desktop/core/intervals.ts +123 -0
  234. package/apps/desktop/core/montage.ts +236 -0
  235. package/apps/desktop/core/music-analysis.ts +518 -0
  236. package/apps/desktop/core/plan.ts +244 -0
  237. package/apps/desktop/core/project-auto-zoom.ts +75 -0
  238. package/apps/desktop/core/project-camera-spatial.ts +676 -0
  239. package/apps/desktop/core/project-camera.ts +403 -0
  240. package/apps/desktop/core/project-captions.ts +888 -0
  241. package/apps/desktop/core/project-inactivity.ts +125 -0
  242. package/apps/desktop/core/project-metadata.ts +927 -0
  243. package/apps/desktop/core/project-plan.ts +375 -0
  244. package/apps/desktop/core/project-render-plan.ts +303 -0
  245. package/apps/desktop/core/project-time.ts +469 -0
  246. package/apps/desktop/core/render-plan.ts +442 -0
  247. package/apps/desktop/core/scene-cache.ts +174 -0
  248. package/apps/desktop/core/scene-sampling.ts +448 -0
  249. package/apps/desktop/core/spatial-compositor.ts +22 -0
  250. package/apps/desktop/core/spatial-durability.ts +99 -0
  251. package/apps/desktop/core/spatial-project.ts +169 -0
  252. package/apps/desktop/core/spatial-shot-clock.ts +32 -0
  253. package/apps/desktop/core/speech-analysis.ts +333 -0
  254. package/apps/desktop/core/storage.ts +970 -0
  255. package/apps/desktop/core/time-map.ts +91 -0
  256. package/apps/desktop/core/time.ts +61 -0
  257. package/apps/desktop/core/video-effects.ts +660 -0
  258. package/apps/desktop/dist/cli/NebulaSans-Bold-26se8aek.otf +0 -0
  259. package/apps/desktop/dist/cli/NebulaSans-Bold-bcz7y08t.woff2 +0 -0
  260. package/apps/desktop/dist/cli/NebulaSans-Book-5ax05zvn.woff2 +0 -0
  261. package/apps/desktop/dist/cli/NebulaSans-Book-8cenzchw.otf +0 -0
  262. package/apps/desktop/dist/cli/main.js +5086 -0
  263. package/apps/desktop/html-overlay/catalog.ts +171 -0
  264. package/apps/desktop/html-overlay/contracts.ts +372 -0
  265. package/apps/desktop/html-overlay/execution-profile.ts +77 -0
  266. package/apps/desktop/html-overlay/index.ts +98 -0
  267. package/apps/desktop/html-overlay/libraries.ts +263 -0
  268. package/apps/desktop/html-overlay/random.ts +49 -0
  269. package/apps/desktop/html-overlay/runtime.ts +813 -0
  270. package/apps/desktop/html-overlay/scaffolds.ts +792 -0
  271. package/apps/desktop/html-overlay/spark-runtime.ts +88 -0
  272. package/apps/desktop/html-overlay/spatial-splat-runtime.ts +60 -0
  273. package/apps/desktop/html-overlay/spatial.ts +720 -0
  274. package/apps/desktop/studio/assets/README.md +39 -0
  275. package/apps/desktop/studio/assets/contracts.ts +129 -0
  276. package/apps/desktop/studio/assets/network.ts +64 -0
  277. package/apps/desktop/studio/assets/poly-haven.ts +148 -0
  278. package/apps/desktop/studio/drivers/blender_driver.py +466 -0
  279. package/apps/desktop/studio/drivers/cadquery_driver.py +176 -0
  280. package/apps/desktop/studio/education/driver.py +308 -0
  281. package/apps/desktop/studio/education/lesson.py +180 -0
  282. package/apps/desktop/studio/education/toolkit.py +249 -0
  283. package/apps/desktop/workflows/chaptered-demo.ts +48 -0
  284. package/apps/desktop/workflows/creative-iteration.ts +107 -0
  285. package/apps/desktop/workflows/creative-selection.ts +92 -0
  286. package/apps/desktop/workflows/fragments.ts +486 -0
  287. package/apps/desktop/workflows/index.ts +93 -0
  288. package/apps/desktop/workflows/polished-screen-demo.ts +128 -0
  289. package/apps/desktop/workflows/scene-direction.ts +12 -0
  290. package/apps/desktop/workflows/social-variants.ts +75 -0
  291. package/apps/desktop/workflows/talking-head-cleanup.ts +53 -0
  292. package/dist/NebulaSans-Bold-26se8aek.otf +0 -0
  293. package/dist/NebulaSans-Bold-bcz7y08t.woff2 +0 -0
  294. package/dist/NebulaSans-Book-5ax05zvn.woff2 +0 -0
  295. package/dist/NebulaSans-Book-8cenzchw.otf +0 -0
  296. package/dist/cli.js +479 -0
  297. package/dist/code/advanced.js +270 -0
  298. package/dist/code/index.js +2240 -0
  299. package/dist/generate.js +24 -0
  300. package/dist/host-resources.js +26 -0
  301. package/dist/index-42zsesc1.js +2257 -0
  302. package/dist/index-7308egqr.js +48 -0
  303. package/dist/index-8txs6fkn.js +712 -0
  304. package/dist/index-mcy8z0br.js +192 -0
  305. package/dist/index-p63wavx0.js +2029 -0
  306. package/dist/index-r7gdhmsp.js +424 -0
  307. package/dist/index-sh6xbav6.js +1219 -0
  308. package/dist/index-z1w83f81.js +4 -0
  309. package/dist/index-z7239b4h.js +2363 -0
  310. package/dist/index-zfeg9hmw.js +2013 -0
  311. package/dist/index.js +241 -0
  312. package/dist/operations.js +29 -0
  313. package/dist/skill-install-dh0ntqba.js +10 -0
  314. package/dist/vectorize/worker.js +142 -0
  315. package/dist/workflow.js +16 -0
  316. package/docs/README.md +38 -0
  317. package/docs/studio.md +215 -0
  318. package/examples/studio/blender/README.md +26 -0
  319. package/examples/studio/blender/character.py +152 -0
  320. package/examples/studio/blender/cloth.py +101 -0
  321. package/examples/studio/blender/color_chart.py +29 -0
  322. package/examples/studio/blender/fluid.py +78 -0
  323. package/examples/studio/blender/import_model.py +74 -0
  324. package/examples/studio/blender/product.py +53 -0
  325. package/examples/studio/blender/shaded_street.py +481 -0
  326. package/examples/studio/blender/studio_scene.py +147 -0
  327. package/examples/studio/cadquery/README.md +13 -0
  328. package/examples/studio/cadquery/bracket.py +36 -0
  329. package/examples/studio/cadquery/import_step.py +12 -0
  330. package/examples/studio/education/README.md +46 -0
  331. package/examples/studio/education/lesson.json +99 -0
  332. package/examples/studio/education/scene.py +7 -0
  333. package/examples/studio/hybrid-scene.ts +96 -0
  334. package/examples/studio/native-workflow.ts +14 -0
  335. package/examples/studio/vgpu/README.md +64 -0
  336. package/examples/studio/vgpu/heat-field.mjs +290 -0
  337. package/package.json +304 -0
  338. package/packages/scene/src/index.ts +1 -0
  339. package/packages/scene/src/scene.ts +215 -0
  340. package/schema/diagram.schema.json +334 -0
  341. package/skills/slopcamera/SKILL.md +45 -0
  342. package/skills/slopcamera/agents/openai.yaml +4 -0
  343. package/skills/slopcamera/references/customization.md +77 -0
  344. package/skills/slopcamera/references/diagrams.md +153 -0
  345. package/skills/slopcamera/references/directed-scenes.md +42 -0
  346. package/skills/slopcamera/references/directing-video.md +18 -0
  347. package/skills/slopcamera/references/educational-video.md +26 -0
  348. package/skills/slopcamera/references/gateway-media.md +134 -0
  349. package/skills/slopcamera/references/install.md +62 -0
  350. package/skills/slopcamera/references/native-studio.md +31 -0
  351. package/skills/slopcamera/references/reference-led-3d.md +140 -0
  352. package/skills/slopcamera/references/rubber-stamp-examples/poster-example-1.jpg +0 -0
  353. package/skills/slopcamera/references/rubber-stamp-examples/poster-example-2.jpg +0 -0
  354. package/skills/slopcamera/references/rubber-stamp-examples/stamp-style-1.png +0 -0
  355. package/skills/slopcamera/references/rubber-stamp-examples/stamp-style-2.png +0 -0
  356. package/skills/slopcamera/references/rubber-stamp-field-notes.md +144 -0
  357. package/skills/slopcamera/references/vectorization.md +29 -0
  358. package/skills/slopcamera/references/video-projects.md +187 -0
  359. package/skills/slopcamera/references/visual-communication.md +120 -0
  360. package/skills/slopcamera/references/workflows-sdk.md +45 -0
  361. package/skills/slopcamera/scripts/compose-rubber-stamp-field-note.ts +229 -0
  362. package/src/artifacts.ts +106 -0
  363. package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
  364. package/src/assets/fonts/nebula-sans/NebulaSans-Bold.otf +0 -0
  365. package/src/assets/fonts/nebula-sans/NebulaSans-Bold.woff2 +0 -0
  366. package/src/assets/fonts/nebula-sans/NebulaSans-Book.otf +0 -0
  367. package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
  368. package/src/assets/fonts/nebula-sans/PROVENANCE.md +21 -0
  369. package/src/assets.d.ts +9 -0
  370. package/src/cli.ts +601 -0
  371. package/src/cloud-errors.ts +26 -0
  372. package/src/code/advanced.ts +10 -0
  373. package/src/code/boundary.ts +21 -0
  374. package/src/code/canonical-json.ts +129 -0
  375. package/src/code/compiler.ts +953 -0
  376. package/src/code/contracts.ts +576 -0
  377. package/src/code/define-workflow.ts +334 -0
  378. package/src/code/errors.ts +41 -0
  379. package/src/code/graph-builder.ts +945 -0
  380. package/src/code/index.ts +121 -0
  381. package/src/code/json-snapshot.ts +899 -0
  382. package/src/code/json-utf8.ts +88 -0
  383. package/src/code/portable-builder.ts +136 -0
  384. package/src/code/projection.ts +378 -0
  385. package/src/code/public-operations.ts +450 -0
  386. package/src/code/runtime.ts +580 -0
  387. package/src/code/sha256.ts +36 -0
  388. package/src/config.ts +180 -0
  389. package/src/desktop.ts +253 -0
  390. package/src/fs.ts +10 -0
  391. package/src/generate.ts +704 -0
  392. package/src/host-resource-posix.ts +161 -0
  393. package/src/host-resources.ts +1749 -0
  394. package/src/icons.ts +58 -0
  395. package/src/index.ts +200 -0
  396. package/src/label-layout.ts +130 -0
  397. package/src/layout.ts +413 -0
  398. package/src/lint.ts +127 -0
  399. package/src/mcp/boundary.ts +354 -0
  400. package/src/mcp/index.ts +33 -0
  401. package/src/mcp/server.ts +272 -0
  402. package/src/mcp/tools.ts +963 -0
  403. package/src/mcp/types.ts +71 -0
  404. package/src/operations.ts +775 -0
  405. package/src/parse.ts +861 -0
  406. package/src/process-environment.ts +17 -0
  407. package/src/render.ts +437 -0
  408. package/src/skill-install.ts +49 -0
  409. package/src/spatial-scene/authoring.ts +27 -0
  410. package/src/spatial-scene/camera-track.ts +83 -0
  411. package/src/spatial-scene/contracts.ts +277 -0
  412. package/src/spatial-scene/evaluate.ts +145 -0
  413. package/src/spatial-scene/gltf.ts +558 -0
  414. package/src/spatial-scene/identity.ts +260 -0
  415. package/src/spatial-scene/index.ts +10 -0
  416. package/src/spatial-scene/inspect.ts +68 -0
  417. package/src/spatial-scene/math.ts +280 -0
  418. package/src/spatial-scene/patch.ts +131 -0
  419. package/src/spatial-scene/time.ts +53 -0
  420. package/src/studio/contracts.ts +129 -0
  421. package/src/studio/index.ts +2 -0
  422. package/src/studio/plan.ts +156 -0
  423. package/src/studio/shared.ts +41 -0
  424. package/src/theme.ts +68 -0
  425. package/src/tldr.ts +474 -0
  426. package/src/types.ts +212 -0
  427. package/src/vectorize/archive.ts +101 -0
  428. package/src/vectorize/command.ts +797 -0
  429. package/src/vectorize/index.ts +27 -0
  430. package/src/vectorize/limits.ts +61 -0
  431. package/src/vectorize/metrics.ts +351 -0
  432. package/src/vectorize/pixels.ts +323 -0
  433. package/src/vectorize/supervisor.ts +339 -0
  434. package/src/vectorize/svg.ts +188 -0
  435. package/src/vectorize/tool.ts +542 -0
  436. package/src/vectorize/types.ts +113 -0
  437. package/src/vectorize/vectorize.ts +634 -0
  438. package/src/vectorize/worker-protocol.ts +40 -0
  439. package/src/vectorize/worker.ts +209 -0
  440. package/src/version.ts +1 -0
  441. package/src/workflow.ts +409 -0
@@ -0,0 +1,2478 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { constants, watch, type FSWatcher } from "node:fs";
3
+ import {
4
+ chmod,
5
+ lchmod,
6
+ lstat,
7
+ mkdir,
8
+ mkdtemp,
9
+ open,
10
+ opendir,
11
+ readFile,
12
+ readdir,
13
+ realpath,
14
+ rename,
15
+ rm,
16
+ rmdir,
17
+ symlink,
18
+ unlink,
19
+ type FileHandle,
20
+ } from "node:fs/promises";
21
+ import { hostname } from "node:os";
22
+ import { basename, dirname, join } from "node:path";
23
+
24
+ import { z } from "zod";
25
+
26
+ import {
27
+ chromium,
28
+ type Browser,
29
+ type JSHandle,
30
+ type LaunchOptions,
31
+ type Page,
32
+ type Route,
33
+ } from "playwright-core";
34
+
35
+ import type {
36
+ BoundHtmlOverlayResource,
37
+ HtmlOverlayFrameRenderRequest,
38
+ HtmlOverlayFrameRenderResult,
39
+ HtmlOverlayRenderer,
40
+ } from "../application/html-overlay-renderer";
41
+ import { ApplicationError } from "../application/errors";
42
+ import {
43
+ assertHtmlOverlayBrowserRuntimeManifest,
44
+ inspectSupportedHtmlOverlayMacBrowserProvenance,
45
+ inspectHtmlOverlayBrowserRuntime,
46
+ type HtmlOverlayBrowserRuntimeBinding,
47
+ type HtmlOverlayBrowserRuntimeEntry,
48
+ } from "../application/html-overlay-browser-runtime";
49
+ import {
50
+ HTML_OVERLAY_RENDERER_CONTRACT,
51
+ assertHtmlOverlayExecutionProfileLibraries,
52
+ createHtmlOverlayExecutionBundle,
53
+ htmlOverlayRendererContract,
54
+ } from "../application/html-overlay-integrity";
55
+ import type { HtmlOverlayExecutionProfile, HtmlOverlayGpuEvidence } from "../html-overlay/execution-profile";
56
+ import { createHtmlOverlayGpuEvidence, inspectHtmlOverlayBrowserGpu, installHtmlOverlayGpuProbe, type HtmlOverlayGpuProbe } from "./html-overlay-gpu";
57
+ import { canonicalJson } from "../core/canonical-json";
58
+ import {
59
+ createHtmlOverlayRuntimeFrame,
60
+ htmlOverlayAssetLocalUrl,
61
+ htmlOverlayFrameCount,
62
+ htmlOverlayLibraryLocalUrl,
63
+ type HtmlOverlayActiveLibraryLock,
64
+ type HtmlOverlayDeclaredResource,
65
+ type HtmlOverlayLibrarySpecifier,
66
+ type HtmlOverlayRuntimeFrame,
67
+ } from "../html-overlay";
68
+ import { BunProcessRunner, type RunResult } from "./io";
69
+
70
+ const DOCUMENT_URL = HTML_OVERLAY_RENDERER_CONTRACT.documentUrl;
71
+ const SYNTHETIC_ORIGIN = new URL(DOCUMENT_URL).origin;
72
+ const MAXIMUM_DIAGNOSTICS = 32;
73
+ const MAXIMUM_DIAGNOSTIC_LENGTH = 1_000;
74
+ const MAXIMUM_LIBRARY_BYTES = 4 * 1024 * 1024;
75
+ function maximumLibraryBytes(lock: HtmlOverlayActiveLibraryLock): number {
76
+ // Only this approved content-addressed Spark module exceeds the legacy ceiling.
77
+ return lock.specifier === "@sparkjsdev/spark" && lock.version === "2.1.0"
78
+ && lock.bytes === 5_063_871 && lock.sha256 === "70050257ce2326c2ce1d2688e6f58987147adf764f8ff7ba4aa17ab7ae5870c0"
79
+ ? lock.bytes : MAXIMUM_LIBRARY_BYTES;
80
+ }
81
+ const DEFAULT_BROWSER_STEP_TIMEOUT_MS = 60_000;
82
+ const MAXIMUM_BROWSER_STEP_TIMEOUT_MS = 5 * 60_000;
83
+
84
+ export function createHtmlOverlayBrowserLaunchArgs(
85
+ libraries: readonly HtmlOverlayLibrarySpecifier[],
86
+ executionProfile?: HtmlOverlayExecutionProfile,
87
+ ): string[] {
88
+ assertHtmlOverlayExecutionProfileLibraries(libraries, executionProfile);
89
+ const contract = htmlOverlayRendererContract(executionProfile);
90
+ return [
91
+ ...contract.launch.args,
92
+ ...(libraries.includes("vgpu")
93
+ ? HTML_OVERLAY_RENDERER_CONTRACT.launch.libraryArgs.vgpu
94
+ : []),
95
+ ];
96
+ }
97
+ const BROWSER_CLEANUP_TIMEOUT_MS = 30_000;
98
+ const BROWSER_RUNTIME_FLAGS_TIMEOUT_MS = 60_000;
99
+ const BROWSER_RUNTIME_FLAGS_MAXIMUM_OUTPUT_BYTES = 16 * 1024;
100
+ const MACOS_BROWSER_RUNTIME_SNAPSHOT_ANCHOR = "/private/tmp";
101
+ const BROWSER_RUNTIME_SNAPSHOT_PREFIX = ".slopcamera-browser-runtime-";
102
+ const BROWSER_RUNTIME_SNAPSHOT_NAME = /^\.slopcamera-browser-runtime-[A-Za-z0-9]{6}$/u;
103
+ const BROWSER_RUNTIME_SNAPSHOT_RECLAIM_PREFIX = ".slopcamera-browser-reclaim-";
104
+ const BROWSER_RUNTIME_SNAPSHOT_RECLAIM_NAME = /^\.slopcamera-browser-reclaim-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u;
105
+ const BROWSER_RUNTIME_SNAPSHOT_LEASE_FILE = ".slopcamera-runtime-lease.json";
106
+ const BROWSER_RUNTIME_SNAPSHOT_LEASE_MAXIMUM_BYTES = 4 * 1024;
107
+ const BROWSER_RUNTIME_SNAPSHOT_STALE_AFTER_MS = 5 * 60_000;
108
+ const BROWSER_RUNTIME_SNAPSHOT_MAXIMUM_SCAN_ENTRIES = 4_096;
109
+ const BROWSER_RUNTIME_SNAPSHOT_MAXIMUM_CANDIDATES = 64;
110
+ const BROWSER_RUNTIME_SNAPSHOT_MAXIMUM_OPEN_STATE_PROBES = 4;
111
+ const BROWSER_RUNTIME_SNAPSHOT_MAXIMUM_RECLAIMS = 4;
112
+
113
+ const BrowserRuntimeSnapshotLeaseBase = {
114
+ acquiredAt: z.string().datetime({ offset: true }),
115
+ hostname: z.string().min(1).max(255),
116
+ pid: z.number().int().positive(),
117
+ schemaVersion: z.literal(1),
118
+ token: z.string().uuid(),
119
+ } as const;
120
+ const BrowserRuntimeSnapshotLeaseSchema = z.discriminatedUnion("state", [
121
+ z.strictObject({
122
+ ...BrowserRuntimeSnapshotLeaseBase,
123
+ state: z.literal("active"),
124
+ }),
125
+ z.strictObject({
126
+ ...BrowserRuntimeSnapshotLeaseBase,
127
+ releasedAt: z.string().datetime({ offset: true }),
128
+ state: z.literal("released"),
129
+ }),
130
+ ]);
131
+ type BrowserRuntimeSnapshotLease = Readonly<
132
+ z.infer<typeof BrowserRuntimeSnapshotLeaseSchema>
133
+ >;
134
+
135
+ interface HtmlOverlayHostController {
136
+ renderFrame(frame: HtmlOverlayRuntimeFrame): Promise<void>;
137
+ securityViolationCount(): number;
138
+ }
139
+
140
+ interface PreparedRoute {
141
+ readonly body: Buffer;
142
+ readonly contentType: string;
143
+ }
144
+
145
+ export interface PlaywrightHtmlOverlayRendererOptions {
146
+ readonly browserStepTimeoutMs?: number;
147
+ readonly cacheRoot: string;
148
+ readonly fetch?: (
149
+ input: RequestInfo | URL,
150
+ init?: RequestInit,
151
+ ) => Promise<Response>;
152
+ readonly frameTimeoutMs?: number;
153
+ readonly launch?: (options: LaunchOptions) => Promise<Browser>;
154
+ }
155
+
156
+ function sha256(bytes: Uint8Array): string {
157
+ return createHash("sha256").update(bytes).digest("hex");
158
+ }
159
+
160
+ function boundedDiagnostic(value: string): string {
161
+ return value.replaceAll(/\s+/gu, " ").trim().slice(0, MAXIMUM_DIAGNOSTIC_LENGTH);
162
+ }
163
+
164
+ function cancellationReason(signal: AbortSignal): Error {
165
+ return signal.reason instanceof Error
166
+ ? signal.reason
167
+ : new ApplicationError(
168
+ "cancelled",
169
+ "HTML overlay browser rendering was cancelled.",
170
+ );
171
+ }
172
+
173
+ /** A closed Playwright transport alone does not establish native process exit. */
174
+ export class HtmlOverlayBrowserCleanupError extends ApplicationError {
175
+ constructor(label: string, cleanupError: unknown, primaryError?: unknown) {
176
+ super("unavailable", `HTML overlay browser ${label} did not settle; native process exit is unproven.`, {
177
+ cleanupError: boundedDiagnostic(cleanupError instanceof Error ? cleanupError.message : String(cleanupError)),
178
+ ...(primaryError === undefined ? {} : { primaryError: boundedDiagnostic(primaryError instanceof Error ? primaryError.message : String(primaryError)) }),
179
+ });
180
+ this.name = "HtmlOverlayBrowserCleanupError";
181
+ this.cause = primaryError === undefined ? cleanupError : new AggregateError([primaryError, cleanupError], "Browser operation and cleanup failed.");
182
+ }
183
+ }
184
+
185
+ export async function boundedBrowserStep<T>(
186
+ start: () => Promise<T>,
187
+ signal: AbortSignal,
188
+ timeoutMs: number,
189
+ label: string,
190
+ onLateSuccess?: (value: T) => Promise<void> | void,
191
+ ): Promise<T> {
192
+ if (signal.aborted) throw cancellationReason(signal);
193
+ const task = start();
194
+ let timeout: ReturnType<typeof setTimeout> | undefined;
195
+ let removeAbortListener = (): void => undefined;
196
+ const stopWaiting = (): void => {
197
+ if (timeout !== undefined) clearTimeout(timeout);
198
+ timeout = undefined;
199
+ removeAbortListener();
200
+ removeAbortListener = (): void => undefined;
201
+ };
202
+ const timedOut = new Promise<never>((_, reject) => {
203
+ timeout = setTimeout(() => {
204
+ reject(new ApplicationError(
205
+ "unavailable",
206
+ `HTML overlay browser ${label} exceeded ${String(timeoutMs)}ms.`,
207
+ ));
208
+ }, timeoutMs);
209
+ });
210
+ const cancelled = new Promise<never>((_, reject) => {
211
+ const onAbort = (): void => reject(cancellationReason(signal));
212
+ signal.addEventListener("abort", onAbort, { once: true });
213
+ removeAbortListener = () => signal.removeEventListener("abort", onAbort);
214
+ if (signal.aborted) onAbort();
215
+ });
216
+ try {
217
+ return await Promise.race([cancelled, task, timedOut]);
218
+ } catch (error) {
219
+ stopWaiting();
220
+ if (onLateSuccess !== undefined) {
221
+ const lateCleanup = task.then(
222
+ async value => await onLateSuccess(value),
223
+ () => undefined,
224
+ );
225
+ try {
226
+ await boundedBrowserStep(() => lateCleanup, new AbortController().signal, timeoutMs, `${label} late settlement`);
227
+ } catch (cleanupError) {
228
+ throw new HtmlOverlayBrowserCleanupError(`${label} late settlement`, cleanupError, error);
229
+ }
230
+ }
231
+ throw error;
232
+ } finally {
233
+ stopWaiting();
234
+ }
235
+ }
236
+
237
+ export async function requiredBrowserCleanup(
238
+ label: string,
239
+ start: () => Promise<unknown>,
240
+ timeoutMs = BROWSER_CLEANUP_TIMEOUT_MS,
241
+ ): Promise<void> {
242
+ try {
243
+ await boundedBrowserStep(start, new AbortController().signal, timeoutMs, label);
244
+ } catch (error) {
245
+ throw new HtmlOverlayBrowserCleanupError(label, error);
246
+ }
247
+ }
248
+
249
+ async function htmlWithHostImports(
250
+ page: Page,
251
+ html: string,
252
+ importMap: Readonly<{ readonly imports: Readonly<Record<string, string>> }>,
253
+ signal: AbortSignal,
254
+ timeoutMs: number,
255
+ ): Promise<string> {
256
+ const authoredMaps = await boundedBrowserStep(
257
+ async () => await page.evaluate((authoredHtml) => {
258
+ const document = new DOMParser().parseFromString(
259
+ authoredHtml,
260
+ "text/html",
261
+ );
262
+ return [...document.querySelectorAll("script")]
263
+ .filter(script => (
264
+ script.getAttribute("type")?.trim().toLowerCase() === "importmap"
265
+ ))
266
+ .map(script => script.textContent ?? "");
267
+ }, html),
268
+ signal,
269
+ timeoutMs,
270
+ "authored document parsing",
271
+ );
272
+ if (authoredMaps.length > 1) {
273
+ throw new ApplicationError("invalid-data", "HTML overlays may declare at most one import map.");
274
+ }
275
+ if (authoredMaps.length === 1) {
276
+ let authored: unknown;
277
+ try {
278
+ authored = JSON.parse(authoredMaps[0]!);
279
+ } catch {
280
+ throw new ApplicationError("invalid-data", "The authored HTML overlay import map is invalid JSON.");
281
+ }
282
+ if (canonicalJson(authored) !== canonicalJson(importMap)) {
283
+ throw new ApplicationError(
284
+ "invalid-data",
285
+ "The authored import map must exactly match the overlay's approved library selection.",
286
+ );
287
+ }
288
+ }
289
+ return await boundedBrowserStep(
290
+ async () => await page.evaluate((input) => {
291
+ const document = new DOMParser().parseFromString(input.html, "text/html");
292
+ for (const script of document.querySelectorAll("script")) {
293
+ if (
294
+ script.getAttribute("type")?.trim().toLowerCase() === "importmap"
295
+ ) {
296
+ script.remove();
297
+ }
298
+ }
299
+ const map = document.createElement("script");
300
+ map.type = "importmap";
301
+ map.textContent = JSON.stringify(input.importMap);
302
+ const style = document.createElement("style");
303
+ style.textContent = [
304
+ ":root { color-scheme: light; }",
305
+ "html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: transparent; }",
306
+ "*, *::before, *::after { box-sizing: border-box; }",
307
+ ].join("\\n");
308
+ document.head.prepend(style);
309
+ document.head.prepend(map);
310
+ return `<!doctype html>${document.documentElement.outerHTML}`;
311
+ }, { html, importMap }),
312
+ signal,
313
+ timeoutMs,
314
+ "host document preparation",
315
+ );
316
+ }
317
+
318
+ async function ensurePrivateCacheRoot(path: string): Promise<string> {
319
+ await mkdir(path, { mode: 0o700, recursive: true });
320
+ const details = await lstat(path);
321
+ if (details.isSymbolicLink() || !details.isDirectory() || (details.mode & 0o077) !== 0) {
322
+ throw new ApplicationError(
323
+ "unsafe-path",
324
+ "The HTML-overlay module cache must be a private physical directory.",
325
+ );
326
+ }
327
+ return path;
328
+ }
329
+
330
+ function isFileSystemError(error: unknown, code: string): boolean {
331
+ return error instanceof Error && "code" in error && error.code === code;
332
+ }
333
+
334
+ async function assertPathAbsent(path: string, label: string): Promise<void> {
335
+ try {
336
+ await lstat(path);
337
+ } catch (error) {
338
+ if (isFileSystemError(error, "ENOENT")) return;
339
+ throw error;
340
+ }
341
+ throw new ApplicationError(
342
+ "conflict",
343
+ `${label} already exists; Slopcamera will not replace it.`,
344
+ );
345
+ }
346
+
347
+ interface PreparedBrowserRuntimeSnapshot {
348
+ readonly anchorIdentity: SnapshotAnchorIdentity;
349
+ readonly anchorPath: string;
350
+ readonly browserHome: string;
351
+ readonly browserTemporaryDirectory: string;
352
+ readonly containerChildren: readonly SnapshotDirectChildIdentity[];
353
+ readonly containerIdentity: SnapshotPathIdentity;
354
+ readonly directory: string;
355
+ readonly executablePath: string;
356
+ readonly identity: readonly SnapshotPathIdentity[];
357
+ readonly lease: BrowserRuntimeSnapshotLease;
358
+ readonly leaseHandle: FileHandle;
359
+ readonly leaseIdentity: SnapshotLeaseFileIdentity;
360
+ readonly runtimeEntries: readonly HtmlOverlayBrowserRuntimeEntry[];
361
+ readonly runtimeImmutable: boolean;
362
+ readonly runtimeRoot: string;
363
+ }
364
+
365
+ interface SnapshotAnchorIdentity {
366
+ readonly dev: string;
367
+ readonly gid: string;
368
+ readonly ino: string;
369
+ readonly mode: number;
370
+ readonly path: string;
371
+ readonly uid: string;
372
+ }
373
+
374
+ interface SnapshotPathIdentity {
375
+ readonly ctimeNs: string;
376
+ readonly dev: string;
377
+ readonly ino: string;
378
+ readonly mode: number;
379
+ readonly path: string;
380
+ readonly size: string;
381
+ }
382
+
383
+ interface SnapshotDirectChildIdentity {
384
+ readonly dev: string;
385
+ readonly ino: string;
386
+ readonly kind: "directory" | "file" | "other" | "symlink";
387
+ readonly mode: number;
388
+ readonly name: string;
389
+ }
390
+
391
+ function sameSnapshotPathNodeIdentity(
392
+ left: SnapshotPathIdentity,
393
+ right: SnapshotPathIdentity,
394
+ ): boolean {
395
+ return left.dev === right.dev
396
+ && left.ino === right.ino
397
+ && left.mode === right.mode
398
+ && left.size === right.size;
399
+ }
400
+
401
+ function snapshotEntryPath(
402
+ runtimeRoot: string,
403
+ entryPath: string,
404
+ ): string {
405
+ return entryPath === "."
406
+ ? runtimeRoot
407
+ : join(runtimeRoot, ...entryPath.split("/"));
408
+ }
409
+
410
+ async function copyRuntimeEntry(
411
+ sourceRoot: string,
412
+ runtimeRoot: string,
413
+ entry: HtmlOverlayBrowserRuntimeEntry,
414
+ signal: AbortSignal,
415
+ ): Promise<void> {
416
+ if (signal.aborted) throw cancellationReason(signal);
417
+ const source = snapshotEntryPath(sourceRoot, entry.path);
418
+ const destination = snapshotEntryPath(runtimeRoot, entry.path);
419
+ if (entry.kind === "directory") {
420
+ // Keep directories writable while descendants are materialized. Exact
421
+ // source modes are restored from the leaves upward after the copy.
422
+ await mkdir(destination, { mode: 0o700 });
423
+ if (signal.aborted) throw cancellationReason(signal);
424
+ return;
425
+ }
426
+ if (entry.kind === "symlink") {
427
+ await symlink(entry.target, destination);
428
+ if (process.platform === "darwin") {
429
+ // macOS app bundles can carry non-default symlink mode bits, and the
430
+ // runtime manifest binds those bits exactly.
431
+ await lchmod(destination, entry.mode);
432
+ }
433
+ if (signal.aborted) throw cancellationReason(signal);
434
+ const copied = await lstat(destination);
435
+ if (!copied.isSymbolicLink()) {
436
+ throw new ApplicationError("conflict", "Browser runtime symlink copy failed.");
437
+ }
438
+ return;
439
+ }
440
+ const sourceHandle = await open(
441
+ source,
442
+ constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0),
443
+ );
444
+ let destinationHandle: Awaited<ReturnType<typeof open>> | undefined;
445
+ try {
446
+ const sourceBefore = await sourceHandle.stat();
447
+ if (
448
+ !sourceBefore.isFile()
449
+ || sourceBefore.size !== entry.bytes
450
+ || (sourceBefore.mode & 0o7777) !== entry.mode
451
+ ) {
452
+ throw new ApplicationError(
453
+ "conflict",
454
+ `Browser runtime file changed before snapshot copy: ${entry.path}`,
455
+ );
456
+ }
457
+ destinationHandle = await open(
458
+ destination,
459
+ constants.O_CREAT
460
+ | constants.O_EXCL
461
+ | constants.O_WRONLY
462
+ | (constants.O_NOFOLLOW ?? 0),
463
+ 0o600,
464
+ );
465
+ const buffer = Buffer.allocUnsafe(4 * 1024 * 1024);
466
+ let offset = 0;
467
+ while (offset < sourceBefore.size) {
468
+ if (signal.aborted) throw cancellationReason(signal);
469
+ const result = await sourceHandle.read(
470
+ buffer,
471
+ 0,
472
+ Math.min(buffer.byteLength, sourceBefore.size - offset),
473
+ offset,
474
+ );
475
+ if (result.bytesRead === 0) {
476
+ throw new ApplicationError(
477
+ "conflict",
478
+ `Browser runtime file ended during snapshot copy: ${entry.path}`,
479
+ );
480
+ }
481
+ let written = 0;
482
+ while (written < result.bytesRead) {
483
+ if (signal.aborted) throw cancellationReason(signal);
484
+ const writeResult = await destinationHandle.write(
485
+ buffer,
486
+ written,
487
+ result.bytesRead - written,
488
+ offset + written,
489
+ );
490
+ if (writeResult.bytesWritten === 0) {
491
+ throw new ApplicationError("internal", "Browser runtime snapshot copy made no progress.");
492
+ }
493
+ written += writeResult.bytesWritten;
494
+ }
495
+ offset += result.bytesRead;
496
+ }
497
+ const sourceAfter = await sourceHandle.stat();
498
+ if (
499
+ sourceBefore.dev !== sourceAfter.dev
500
+ || sourceBefore.ino !== sourceAfter.ino
501
+ || sourceBefore.size !== sourceAfter.size
502
+ || sourceBefore.mode !== sourceAfter.mode
503
+ || sourceBefore.mtimeMs !== sourceAfter.mtimeMs
504
+ ) {
505
+ throw new ApplicationError(
506
+ "conflict",
507
+ `Browser runtime file changed during snapshot copy: ${entry.path}`,
508
+ );
509
+ }
510
+ await destinationHandle.chmod(entry.mode);
511
+ } finally {
512
+ await destinationHandle?.close().catch(() => undefined);
513
+ await sourceHandle.close();
514
+ }
515
+ }
516
+
517
+ async function captureBrowserRuntimeSnapshotIdentity(
518
+ runtimeRoot: string,
519
+ entries: readonly HtmlOverlayBrowserRuntimeEntry[],
520
+ signal: AbortSignal,
521
+ ): Promise<readonly SnapshotPathIdentity[]> {
522
+ const identities: SnapshotPathIdentity[] = [];
523
+ for (const entry of entries) {
524
+ if (signal.aborted) throw cancellationReason(signal);
525
+ const details = await lstat(snapshotEntryPath(runtimeRoot, entry.path), {
526
+ bigint: true,
527
+ });
528
+ identities.push({
529
+ ctimeNs: details.ctimeNs.toString(),
530
+ dev: details.dev.toString(),
531
+ ino: details.ino.toString(),
532
+ mode: Number(details.mode & 0o177777n),
533
+ path: entry.path,
534
+ size: details.size.toString(),
535
+ });
536
+ }
537
+ return identities;
538
+ }
539
+
540
+ async function captureBrowserRuntimeSnapshotContainerIdentity(
541
+ directory: string,
542
+ signal: AbortSignal,
543
+ ): Promise<SnapshotPathIdentity> {
544
+ if (signal.aborted) throw cancellationReason(signal);
545
+ const details = await lstat(directory, { bigint: true });
546
+ if (!details.isDirectory() || details.isSymbolicLink()) {
547
+ throw new ApplicationError(
548
+ "conflict",
549
+ "The private browser runtime snapshot container is no longer a physical directory.",
550
+ );
551
+ }
552
+ return {
553
+ ctimeNs: details.ctimeNs.toString(),
554
+ dev: details.dev.toString(),
555
+ ino: details.ino.toString(),
556
+ mode: Number(details.mode & 0o177777n),
557
+ path: "<snapshot-container>",
558
+ size: details.size.toString(),
559
+ };
560
+ }
561
+
562
+ async function captureBrowserRuntimeSnapshotDirectChildren(
563
+ directory: string,
564
+ signal: AbortSignal,
565
+ ): Promise<readonly SnapshotDirectChildIdentity[]> {
566
+ if (signal.aborted) throw cancellationReason(signal);
567
+ const names = (await readdir(directory)).sort();
568
+ const identities: SnapshotDirectChildIdentity[] = [];
569
+ for (const name of names) {
570
+ if (signal.aborted) throw cancellationReason(signal);
571
+ const details = await lstat(join(directory, name), { bigint: true });
572
+ identities.push({
573
+ dev: details.dev.toString(),
574
+ ino: details.ino.toString(),
575
+ kind: details.isSymbolicLink()
576
+ ? "symlink"
577
+ : details.isDirectory()
578
+ ? "directory"
579
+ : details.isFile() ? "file" : "other",
580
+ mode: Number(details.mode & 0o177777n),
581
+ name,
582
+ });
583
+ }
584
+ return Object.freeze(identities);
585
+ }
586
+
587
+ async function inspectBrowserRuntimeSnapshotAnchor(
588
+ path: string,
589
+ requireRootOwnedStickyAnchor: boolean,
590
+ signal: AbortSignal,
591
+ ): Promise<SnapshotAnchorIdentity> {
592
+ if (signal.aborted) throw cancellationReason(signal);
593
+ const [details, physicalPath] = await Promise.all([
594
+ lstat(path, { bigint: true }),
595
+ realpath(path),
596
+ ]);
597
+ if (
598
+ details.isSymbolicLink()
599
+ || !details.isDirectory()
600
+ || physicalPath !== path
601
+ ) {
602
+ throw new ApplicationError(
603
+ "unsafe-path",
604
+ "The browser runtime snapshot anchor must be a physical directory.",
605
+ );
606
+ }
607
+ if (
608
+ requireRootOwnedStickyAnchor
609
+ && (
610
+ details.uid !== 0n
611
+ || (details.mode & 0o1000n) === 0n
612
+ || (details.mode & 0o002n) === 0n
613
+ )
614
+ ) {
615
+ throw new ApplicationError(
616
+ "unsafe-path",
617
+ "The macOS browser runtime snapshot anchor must be root-owned, sticky, and world-writable.",
618
+ );
619
+ }
620
+ return {
621
+ dev: details.dev.toString(),
622
+ gid: details.gid.toString(),
623
+ ino: details.ino.toString(),
624
+ mode: Number(details.mode & 0o177777n),
625
+ path,
626
+ uid: details.uid.toString(),
627
+ };
628
+ }
629
+
630
+ async function browserRuntimeSnapshotAnchor(
631
+ cacheRootInput: string,
632
+ signal: AbortSignal,
633
+ ): Promise<SnapshotAnchorIdentity> {
634
+ if (process.platform === "darwin") {
635
+ return await inspectBrowserRuntimeSnapshotAnchor(
636
+ MACOS_BROWSER_RUNTIME_SNAPSHOT_ANCHOR,
637
+ true,
638
+ signal,
639
+ );
640
+ }
641
+ return await inspectBrowserRuntimeSnapshotAnchor(
642
+ await ensurePrivateCacheRoot(cacheRootInput),
643
+ false,
644
+ signal,
645
+ );
646
+ }
647
+
648
+ async function assertBrowserRuntimeSnapshotIdentity(
649
+ snapshot: PreparedBrowserRuntimeSnapshot,
650
+ entries: readonly HtmlOverlayBrowserRuntimeEntry[],
651
+ label: string,
652
+ signal: AbortSignal,
653
+ ): Promise<void> {
654
+ const current = await captureBrowserRuntimeSnapshotIdentity(
655
+ snapshot.runtimeRoot,
656
+ entries,
657
+ signal,
658
+ );
659
+ const rootHasLaunchManagedCtime = snapshot.runtimeImmutable
660
+ && entries[0]?.kind === "directory";
661
+ const comparable = (entry: SnapshotPathIdentity | undefined) => (
662
+ rootHasLaunchManagedCtime && entry?.path === "."
663
+ ? { ...entry, ctimeNs: "macos-launch-managed" }
664
+ : entry
665
+ );
666
+ if (
667
+ canonicalJson(current.map(comparable))
668
+ !== canonicalJson(snapshot.identity.map(comparable))
669
+ ) {
670
+ const changedIndex = current.findIndex((entry, index) => (
671
+ canonicalJson(comparable(entry))
672
+ !== canonicalJson(comparable(snapshot.identity[index]))
673
+ ));
674
+ const changed = changedIndex === -1 ? undefined : current[changedIndex];
675
+ throw new ApplicationError(
676
+ "conflict",
677
+ `Browser runtime snapshot identity changed ${label}${changed === undefined ? "" : ` at ${changed.path}`}; refusing to accept the render.`,
678
+ {
679
+ after: changed,
680
+ before: changedIndex === -1 ? undefined : snapshot.identity[changedIndex],
681
+ },
682
+ );
683
+ }
684
+ }
685
+
686
+ async function assertBrowserRuntimeSnapshotContainerIdentity(
687
+ snapshot: PreparedBrowserRuntimeSnapshot,
688
+ label: string,
689
+ signal: AbortSignal,
690
+ ): Promise<void> {
691
+ const current = await captureBrowserRuntimeSnapshotContainerIdentity(
692
+ snapshot.directory,
693
+ signal,
694
+ );
695
+ if (canonicalJson(current) !== canonicalJson(snapshot.containerIdentity)) {
696
+ throw new ApplicationError(
697
+ "conflict",
698
+ `Browser runtime snapshot container identity changed ${label}; refusing to accept the render.`,
699
+ { after: current, before: snapshot.containerIdentity },
700
+ );
701
+ }
702
+ }
703
+
704
+ async function assertBrowserRuntimeSnapshotAnchorIdentity(
705
+ snapshot: PreparedBrowserRuntimeSnapshot,
706
+ label: string,
707
+ signal: AbortSignal,
708
+ ): Promise<void> {
709
+ const current = await inspectBrowserRuntimeSnapshotAnchor(
710
+ snapshot.anchorPath,
711
+ process.platform === "darwin",
712
+ signal,
713
+ );
714
+ if (canonicalJson(current) !== canonicalJson(snapshot.anchorIdentity)) {
715
+ throw new ApplicationError(
716
+ "conflict",
717
+ `Browser runtime snapshot anchor identity changed ${label}; refusing to accept the render.`,
718
+ { after: current, before: snapshot.anchorIdentity },
719
+ );
720
+ }
721
+ }
722
+
723
+ interface SnapshotLeaseFileIdentity {
724
+ readonly ctimeMs: number;
725
+ readonly dev: number;
726
+ readonly ino: number;
727
+ readonly mode: number;
728
+ readonly mtimeMs: number;
729
+ readonly nlink: number;
730
+ readonly size: number;
731
+ readonly uid: number;
732
+ }
733
+
734
+ export type SnapshotTreeOpenState = "closed" | "open" | "unknown";
735
+
736
+ export interface BrowserRuntimeSnapshotScavengeOptions {
737
+ readonly anchorPath: string;
738
+ readonly currentUid?: number;
739
+ readonly now?: () => Date;
740
+ readonly processAlive?: (pid: number) => boolean;
741
+ readonly releaseAndRemove?: (path: string) => Promise<void>;
742
+ readonly signal?: AbortSignal;
743
+ readonly staleAfterMs?: number;
744
+ readonly treeOpenState?: (
745
+ path: string,
746
+ signal: AbortSignal,
747
+ ) => Promise<SnapshotTreeOpenState>;
748
+ }
749
+
750
+ function snapshotLeaseFileIdentity(details: {
751
+ readonly ctimeMs: number;
752
+ readonly dev: number;
753
+ readonly ino: number;
754
+ readonly mode: number;
755
+ readonly mtimeMs: number;
756
+ readonly nlink: number;
757
+ readonly size: number;
758
+ readonly uid: number;
759
+ }): SnapshotLeaseFileIdentity {
760
+ return {
761
+ ctimeMs: details.ctimeMs,
762
+ dev: details.dev,
763
+ ino: details.ino,
764
+ mode: details.mode,
765
+ mtimeMs: details.mtimeMs,
766
+ nlink: details.nlink,
767
+ size: details.size,
768
+ uid: details.uid,
769
+ };
770
+ }
771
+
772
+ function sameSnapshotLeaseFileIdentity(
773
+ left: SnapshotLeaseFileIdentity,
774
+ right: SnapshotLeaseFileIdentity,
775
+ ): boolean {
776
+ return canonicalJson(left) === canonicalJson(right);
777
+ }
778
+
779
+ function sameSnapshotLeaseNodeIdentity(
780
+ left: SnapshotLeaseFileIdentity,
781
+ right: SnapshotLeaseFileIdentity,
782
+ ): boolean {
783
+ return left.dev === right.dev
784
+ && left.ino === right.ino
785
+ && left.mode === right.mode
786
+ && left.nlink === right.nlink
787
+ && left.size === right.size
788
+ && left.uid === right.uid;
789
+ }
790
+
791
+ function defaultSnapshotOwnerProcessAlive(pid: number): boolean {
792
+ try {
793
+ process.kill(pid, 0);
794
+ return true;
795
+ } catch (error) {
796
+ return !isFileSystemError(error, "ESRCH");
797
+ }
798
+ }
799
+
800
+ export function classifySnapshotTreeOpenStateResult(
801
+ result: Readonly<Pick<RunResult, "exitCode" | "stderr" | "stdout">>,
802
+ ): SnapshotTreeOpenState {
803
+ if (/^p[0-9]+$/mu.test(result.stdout)) return "open";
804
+ return result.exitCode === 1
805
+ && result.stdout.trim() === ""
806
+ && result.stderr.trim() === ""
807
+ ? "closed"
808
+ : "unknown";
809
+ }
810
+
811
+ async function defaultSnapshotTreeOpenState(
812
+ path: string,
813
+ signal: AbortSignal,
814
+ ): Promise<SnapshotTreeOpenState> {
815
+ if (process.platform !== "darwin") return "unknown";
816
+ if (signal.aborted) throw cancellationReason(signal);
817
+ try {
818
+ const result = await new BunProcessRunner().run([
819
+ "/usr/sbin/lsof",
820
+ "-Fp",
821
+ "+D",
822
+ path,
823
+ ], {
824
+ abortSignal: signal,
825
+ env: {
826
+ LANG: "C",
827
+ LC_ALL: "C",
828
+ PATH: "/usr/bin:/bin:/usr/sbin:/sbin",
829
+ },
830
+ maxOutputBytes: 32 * 1024,
831
+ stdin: "ignore",
832
+ timeoutMs: 5_000,
833
+ });
834
+ if (signal.aborted) throw cancellationReason(signal);
835
+ return classifySnapshotTreeOpenStateResult(result);
836
+ } catch {
837
+ if (signal.aborted) throw cancellationReason(signal);
838
+ return "unknown";
839
+ }
840
+ }
841
+
842
+ async function readBrowserRuntimeSnapshotLease(
843
+ candidate: string,
844
+ expectedUid: number,
845
+ ): Promise<Readonly<{
846
+ directoryIdentity: SnapshotLeaseFileIdentity;
847
+ lease: BrowserRuntimeSnapshotLease;
848
+ leaseIdentity: SnapshotLeaseFileIdentity;
849
+ }> | undefined> {
850
+ const directoryDetails = await lstat(candidate);
851
+ if (
852
+ directoryDetails.isSymbolicLink()
853
+ || !directoryDetails.isDirectory()
854
+ || directoryDetails.uid !== expectedUid
855
+ || (directoryDetails.mode & 0o777) !== 0o700
856
+ || await realpath(candidate) !== candidate
857
+ ) return undefined;
858
+ const leasePath = join(candidate, BROWSER_RUNTIME_SNAPSHOT_LEASE_FILE);
859
+ const leaseDetails = await lstat(leasePath);
860
+ if (
861
+ leaseDetails.isSymbolicLink()
862
+ || !leaseDetails.isFile()
863
+ || leaseDetails.uid !== expectedUid
864
+ || leaseDetails.nlink !== 1
865
+ || (leaseDetails.mode & 0o777) !== 0o600
866
+ || leaseDetails.size < 1
867
+ || leaseDetails.size > BROWSER_RUNTIME_SNAPSHOT_LEASE_MAXIMUM_BYTES
868
+ ) return undefined;
869
+ const leaseIdentity = snapshotLeaseFileIdentity(leaseDetails);
870
+ const handle = await open(
871
+ leasePath,
872
+ constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0),
873
+ );
874
+ try {
875
+ const before = snapshotLeaseFileIdentity(await handle.stat());
876
+ if (!sameSnapshotLeaseFileIdentity(leaseIdentity, before)) return undefined;
877
+ const text = await handle.readFile({ encoding: "utf8" });
878
+ const after = snapshotLeaseFileIdentity(await handle.stat());
879
+ if (!sameSnapshotLeaseFileIdentity(before, after)) return undefined;
880
+ const parsed = BrowserRuntimeSnapshotLeaseSchema.safeParse(
881
+ JSON.parse(text) as unknown,
882
+ );
883
+ if (!parsed.success) return undefined;
884
+ return {
885
+ directoryIdentity: snapshotLeaseFileIdentity(directoryDetails),
886
+ lease: parsed.data,
887
+ leaseIdentity: after,
888
+ };
889
+ } catch {
890
+ return undefined;
891
+ } finally {
892
+ await handle.close();
893
+ }
894
+ }
895
+
896
+ async function releaseAndRemoveStaleBrowserRuntimeSnapshot(
897
+ path: string,
898
+ ): Promise<void> {
899
+ await removeQuarantinedBrowserRuntimeSnapshot(path, true);
900
+ }
901
+
902
+ async function removeQuarantinedBrowserRuntimeSnapshot(
903
+ path: string,
904
+ releaseImmutableFlags: boolean,
905
+ ): Promise<void> {
906
+ if (releaseImmutableFlags) {
907
+ await setBrowserRuntimePathImmutable(
908
+ path,
909
+ false,
910
+ true,
911
+ new AbortController().signal,
912
+ );
913
+ }
914
+ const leasePath = join(path, BROWSER_RUNTIME_SNAPSHOT_LEASE_FILE);
915
+ const names = (await readdir(path)).sort();
916
+ const hasLease = names.includes(BROWSER_RUNTIME_SNAPSHOT_LEASE_FILE);
917
+ const leaseBytes = hasLease ? await readFile(leasePath) : undefined;
918
+ for (const name of names) {
919
+ if (name === BROWSER_RUNTIME_SNAPSHOT_LEASE_FILE) continue;
920
+ await rm(join(path, name), { force: true, recursive: true });
921
+ }
922
+ const remaining = (await readdir(path)).sort();
923
+ const expectedRemaining = hasLease
924
+ ? [BROWSER_RUNTIME_SNAPSHOT_LEASE_FILE]
925
+ : [];
926
+ if (canonicalJson(remaining) !== canonicalJson(expectedRemaining)) {
927
+ throw new ApplicationError(
928
+ "conflict",
929
+ "Browser runtime quarantine changed during lease-last cleanup; its recovery marker was preserved.",
930
+ );
931
+ }
932
+ if (hasLease) await unlink(leasePath);
933
+ try {
934
+ await rmdir(path);
935
+ } catch (error) {
936
+ try {
937
+ await lstat(path);
938
+ } catch (inspectionError) {
939
+ if (isFileSystemError(inspectionError, "ENOENT")) return;
940
+ throw error;
941
+ }
942
+ if (leaseBytes !== undefined) {
943
+ try {
944
+ const replacement = await open(
945
+ leasePath,
946
+ constants.O_CREAT
947
+ | constants.O_EXCL
948
+ | constants.O_WRONLY
949
+ | (constants.O_NOFOLLOW ?? 0),
950
+ 0o600,
951
+ );
952
+ try {
953
+ await replacement.writeFile(leaseBytes);
954
+ await replacement.sync();
955
+ } finally {
956
+ await replacement.close();
957
+ }
958
+ } catch {
959
+ // Preserve the original cleanup failure; a concurrently changed
960
+ // quarantine is never recursively retried without a new validation.
961
+ }
962
+ }
963
+ throw error;
964
+ }
965
+ }
966
+
967
+ export async function scavengeStaleBrowserRuntimeSnapshots(
968
+ options: BrowserRuntimeSnapshotScavengeOptions,
969
+ ): Promise<readonly string[]> {
970
+ const signal = options.signal ?? new AbortController().signal;
971
+ if (signal.aborted) throw cancellationReason(signal);
972
+ const currentUid = options.currentUid
973
+ ?? (typeof process.getuid === "function" ? process.getuid() : undefined);
974
+ if (currentUid === undefined) return [];
975
+ const now = (options.now ?? (() => new Date()))();
976
+ if (!Number.isFinite(now.getTime())) return [];
977
+ const staleAfterMs = options.staleAfterMs
978
+ ?? BROWSER_RUNTIME_SNAPSHOT_STALE_AFTER_MS;
979
+ if (!Number.isSafeInteger(staleAfterMs) || staleAfterMs <= 0) return [];
980
+ const processAlive = options.processAlive ?? defaultSnapshotOwnerProcessAlive;
981
+ const treeOpenState = options.treeOpenState ?? defaultSnapshotTreeOpenState;
982
+ const releaseAndRemove = options.releaseAndRemove
983
+ ?? releaseAndRemoveStaleBrowserRuntimeSnapshot;
984
+ const names: string[] = [];
985
+ let directory;
986
+ try {
987
+ directory = await opendir(options.anchorPath);
988
+ } catch {
989
+ if (signal.aborted) throw cancellationReason(signal);
990
+ return [];
991
+ }
992
+ let scannedEntries = 0;
993
+ try {
994
+ for await (const entry of directory) {
995
+ if (signal.aborted) throw cancellationReason(signal);
996
+ scannedEntries += 1;
997
+ if (scannedEntries > BROWSER_RUNTIME_SNAPSHOT_MAXIMUM_SCAN_ENTRIES) break;
998
+ if (
999
+ !BROWSER_RUNTIME_SNAPSHOT_NAME.test(entry.name)
1000
+ && !BROWSER_RUNTIME_SNAPSHOT_RECLAIM_NAME.test(entry.name)
1001
+ ) continue;
1002
+ names.push(entry.name);
1003
+ if (names.length >= BROWSER_RUNTIME_SNAPSHOT_MAXIMUM_CANDIDATES) break;
1004
+ }
1005
+ } catch {
1006
+ if (signal.aborted) throw cancellationReason(signal);
1007
+ return [];
1008
+ } finally {
1009
+ try {
1010
+ await directory.close();
1011
+ } catch {
1012
+ // Node and Bun may close a directory automatically when its async
1013
+ // iterator finishes or is stopped early.
1014
+ }
1015
+ }
1016
+ const reclaimed: string[] = [];
1017
+ let openStateProbes = 0;
1018
+ for (const name of names.sort()) {
1019
+ if (signal.aborted) throw cancellationReason(signal);
1020
+ if (
1021
+ reclaimed.length >= BROWSER_RUNTIME_SNAPSHOT_MAXIMUM_RECLAIMS
1022
+ || openStateProbes >= BROWSER_RUNTIME_SNAPSHOT_MAXIMUM_OPEN_STATE_PROBES
1023
+ ) break;
1024
+ const candidate = join(options.anchorPath, name);
1025
+ let quarantineCommitted = false;
1026
+ let quarantineValidated = false;
1027
+ try {
1028
+ const observed = await readBrowserRuntimeSnapshotLease(candidate, currentUid);
1029
+ if (signal.aborted) throw cancellationReason(signal);
1030
+ if (observed === undefined) continue;
1031
+ const ageMs = now.getTime() - Date.parse(observed.lease.acquiredAt);
1032
+ const releasedRecovery = BROWSER_RUNTIME_SNAPSHOT_RECLAIM_NAME.test(name)
1033
+ && observed.lease.state === "released";
1034
+ if (
1035
+ observed.lease.hostname !== hostname()
1036
+ || (
1037
+ !releasedRecovery
1038
+ && (ageMs < staleAfterMs || processAlive(observed.lease.pid))
1039
+ )
1040
+ ) continue;
1041
+ openStateProbes += 1;
1042
+ if (await treeOpenState(candidate, signal) !== "closed") continue;
1043
+ if (signal.aborted) throw cancellationReason(signal);
1044
+ const current = await readBrowserRuntimeSnapshotLease(candidate, currentUid);
1045
+ if (signal.aborted) throw cancellationReason(signal);
1046
+ if (
1047
+ current === undefined
1048
+ || !sameSnapshotLeaseFileIdentity(
1049
+ current.directoryIdentity,
1050
+ observed.directoryIdentity,
1051
+ )
1052
+ || !sameSnapshotLeaseFileIdentity(
1053
+ current.leaseIdentity,
1054
+ observed.leaseIdentity,
1055
+ )
1056
+ || canonicalJson(current.lease) !== canonicalJson(observed.lease)
1057
+ ) continue;
1058
+
1059
+ // The outer container stays mutable by design; only the verified runtime
1060
+ // tree is recursively immutable. Move the pathname atomically before any
1061
+ // flag clearing or recursive deletion can touch it.
1062
+ const quarantine = join(
1063
+ options.anchorPath,
1064
+ `${BROWSER_RUNTIME_SNAPSHOT_RECLAIM_PREFIX}${randomUUID()}`,
1065
+ );
1066
+ await assertPathAbsent(quarantine, "The browser runtime reclaim path");
1067
+ if (signal.aborted) throw cancellationReason(signal);
1068
+ await rename(candidate, quarantine);
1069
+ quarantineCommitted = true;
1070
+ const quarantined = await readBrowserRuntimeSnapshotLease(
1071
+ quarantine,
1072
+ currentUid,
1073
+ );
1074
+ if (signal.aborted && quarantined === undefined) {
1075
+ throw cancellationReason(signal);
1076
+ }
1077
+ if (
1078
+ quarantined === undefined
1079
+ || !sameSnapshotLeaseNodeIdentity(
1080
+ quarantined.directoryIdentity,
1081
+ current.directoryIdentity,
1082
+ )
1083
+ || !sameSnapshotLeaseFileIdentity(
1084
+ quarantined.leaseIdentity,
1085
+ current.leaseIdentity,
1086
+ )
1087
+ || canonicalJson(quarantined.lease) !== canonicalJson(current.lease)
1088
+ ) {
1089
+ throw new ApplicationError(
1090
+ "conflict",
1091
+ "A browser runtime snapshot changed while it was atomically quarantined; the moved tree was preserved.",
1092
+ );
1093
+ }
1094
+ quarantineValidated = true;
1095
+
1096
+ // After the atomic rename succeeds, this exact path can never collide
1097
+ // with a new runtime container. Re-prove that no descendant was opened
1098
+ // across the first lsof/rename gap, using a fresh cleanup signal so a
1099
+ // caller abort cannot skip this safety decision.
1100
+ const cleanupSignal = new AbortController().signal;
1101
+ if (await treeOpenState(quarantine, cleanupSignal) !== "closed") {
1102
+ if (signal.aborted) throw cancellationReason(signal);
1103
+ continue;
1104
+ }
1105
+
1106
+ // Finish deletion only after the post-quarantine open-tree proof, then
1107
+ // propagate any caller cancellation.
1108
+ await releaseAndRemove(quarantine);
1109
+ reclaimed.push(candidate);
1110
+ if (signal.aborted) throw cancellationReason(signal);
1111
+ } catch (error) {
1112
+ if (signal.aborted) throw cancellationReason(signal);
1113
+ if (quarantineCommitted && !quarantineValidated) throw error;
1114
+ // Hygiene must never make a safe new snapshot fail. Ambiguous, unsafe,
1115
+ // live, open, or concurrently changed candidates are preserved.
1116
+ }
1117
+ }
1118
+ if (signal.aborted) throw cancellationReason(signal);
1119
+ return Object.freeze(reclaimed);
1120
+ }
1121
+
1122
+ async function createBrowserRuntimeSnapshotLease(
1123
+ directory: string,
1124
+ ): Promise<Readonly<{
1125
+ handle: FileHandle;
1126
+ identity: SnapshotLeaseFileIdentity;
1127
+ lease: BrowserRuntimeSnapshotLease;
1128
+ }>> {
1129
+ const handle = await open(
1130
+ join(directory, BROWSER_RUNTIME_SNAPSHOT_LEASE_FILE),
1131
+ constants.O_CREAT
1132
+ | constants.O_EXCL
1133
+ | constants.O_WRONLY
1134
+ | (constants.O_NOFOLLOW ?? 0),
1135
+ 0o600,
1136
+ );
1137
+ try {
1138
+ const lease = BrowserRuntimeSnapshotLeaseSchema.parse({
1139
+ acquiredAt: new Date().toISOString(),
1140
+ hostname: hostname(),
1141
+ pid: process.pid,
1142
+ schemaVersion: 1,
1143
+ state: "active",
1144
+ token: randomUUID(),
1145
+ });
1146
+ await handle.writeFile(`${canonicalJson(lease)}\n`, { encoding: "utf8" });
1147
+ await handle.sync();
1148
+ return {
1149
+ handle,
1150
+ identity: snapshotLeaseFileIdentity(await handle.stat()),
1151
+ lease,
1152
+ };
1153
+ } catch (error) {
1154
+ await handle.close();
1155
+ throw error;
1156
+ }
1157
+ }
1158
+
1159
+ async function setBrowserRuntimePathImmutable(
1160
+ path: string,
1161
+ immutable: boolean,
1162
+ recursive: boolean,
1163
+ signal: AbortSignal,
1164
+ ): Promise<void> {
1165
+ if (process.platform !== "darwin") {
1166
+ throw new ApplicationError(
1167
+ "internal",
1168
+ "Signed HTML-overlay browser snapshots require macOS filesystem flags.",
1169
+ );
1170
+ }
1171
+ if (signal.aborted) throw cancellationReason(signal);
1172
+ let result: RunResult;
1173
+ try {
1174
+ result = await new BunProcessRunner().run([
1175
+ "/usr/bin/chflags",
1176
+ ...(recursive ? ["-R", "-P"] : []),
1177
+ immutable ? "uchg" : "nouchg",
1178
+ path,
1179
+ ], {
1180
+ abortSignal: signal,
1181
+ env: {
1182
+ LANG: "en_US.UTF-8",
1183
+ LC_ALL: "en_US.UTF-8",
1184
+ PATH: "/usr/bin:/bin",
1185
+ },
1186
+ maxOutputBytes: BROWSER_RUNTIME_FLAGS_MAXIMUM_OUTPUT_BYTES,
1187
+ stdin: "ignore",
1188
+ timeoutMs: BROWSER_RUNTIME_FLAGS_TIMEOUT_MS,
1189
+ });
1190
+ } catch (error) {
1191
+ if (signal.aborted) throw cancellationReason(signal);
1192
+ throw new ApplicationError(
1193
+ "unavailable",
1194
+ `Could not ${immutable ? "protect" : "release"} the private browser runtime snapshot.`,
1195
+ { cause: error instanceof Error ? error.message : String(error) },
1196
+ );
1197
+ }
1198
+ if (signal.aborted) throw cancellationReason(signal);
1199
+ if (result.exitCode !== 0) {
1200
+ throw new ApplicationError(
1201
+ "unavailable",
1202
+ `Could not ${immutable ? "protect" : "release"} the private browser runtime snapshot.`,
1203
+ {
1204
+ exitCode: result.exitCode,
1205
+ stderr: boundedDiagnostic(result.stderr),
1206
+ },
1207
+ );
1208
+ }
1209
+ }
1210
+
1211
+ interface BrowserRuntimeSnapshotRemoval {
1212
+ readonly containerChildren?: readonly SnapshotDirectChildIdentity[] | undefined;
1213
+ readonly containerIdentity?: SnapshotPathIdentity | undefined;
1214
+ readonly directory: string;
1215
+ readonly lease?: BrowserRuntimeSnapshotLease | undefined;
1216
+ readonly leaseHandle?: FileHandle | undefined;
1217
+ readonly leaseIdentity?: SnapshotLeaseFileIdentity | undefined;
1218
+ readonly releaseAndRemove?: ((path: string) => Promise<void>) | undefined;
1219
+ readonly identity?: readonly SnapshotPathIdentity[] | undefined;
1220
+ readonly runtimeEntries?: readonly HtmlOverlayBrowserRuntimeEntry[] | undefined;
1221
+ readonly runtimeImmutable: boolean;
1222
+ readonly runtimeRoot: string;
1223
+ }
1224
+
1225
+ async function markBrowserRuntimeSnapshotLeaseReleased(
1226
+ quarantine: string,
1227
+ lease: BrowserRuntimeSnapshotLease,
1228
+ ): Promise<BrowserRuntimeSnapshotLease> {
1229
+ const released = BrowserRuntimeSnapshotLeaseSchema.parse(
1230
+ lease.state === "released"
1231
+ ? lease
1232
+ : {
1233
+ ...lease,
1234
+ releasedAt: new Date().toISOString(),
1235
+ state: "released",
1236
+ },
1237
+ );
1238
+ const temporary = join(
1239
+ quarantine,
1240
+ `.slopcamera-runtime-release-${randomUUID()}.tmp`,
1241
+ );
1242
+ const handle = await open(
1243
+ temporary,
1244
+ constants.O_CREAT
1245
+ | constants.O_EXCL
1246
+ | constants.O_WRONLY
1247
+ | (constants.O_NOFOLLOW ?? 0),
1248
+ 0o600,
1249
+ );
1250
+ try {
1251
+ await handle.writeFile(`${canonicalJson(released)}\n`, { encoding: "utf8" });
1252
+ await handle.sync();
1253
+ } finally {
1254
+ await handle.close();
1255
+ }
1256
+ try {
1257
+ await rename(temporary, join(quarantine, BROWSER_RUNTIME_SNAPSHOT_LEASE_FILE));
1258
+ const directoryHandle = await open(quarantine, constants.O_RDONLY);
1259
+ try {
1260
+ await directoryHandle.sync();
1261
+ } finally {
1262
+ await directoryHandle.close();
1263
+ }
1264
+ } catch (error) {
1265
+ await rm(temporary, { force: true }).catch(() => undefined);
1266
+ throw error;
1267
+ }
1268
+ const currentUid = typeof process.getuid === "function" ? process.getuid() : 0;
1269
+ const observed = await readBrowserRuntimeSnapshotLease(quarantine, currentUid);
1270
+ if (
1271
+ observed === undefined
1272
+ || canonicalJson(observed.lease) !== canonicalJson(released)
1273
+ ) {
1274
+ throw new ApplicationError(
1275
+ "conflict",
1276
+ "Browser runtime cleanup could not authenticate its released lease marker.",
1277
+ );
1278
+ }
1279
+ return released;
1280
+ }
1281
+
1282
+ export async function removeBrowserRuntimeSnapshot(
1283
+ snapshot: Readonly<BrowserRuntimeSnapshotRemoval>,
1284
+ ): Promise<void> {
1285
+ const closeLease = async (): Promise<void> => {
1286
+ await snapshot.leaseHandle?.close().catch(() => undefined);
1287
+ };
1288
+ if (join(snapshot.directory, basename(snapshot.runtimeRoot)) !== snapshot.runtimeRoot) {
1289
+ await closeLease();
1290
+ throw new ApplicationError(
1291
+ "unsafe-path",
1292
+ "Browser runtime cleanup received a root outside its snapshot container.",
1293
+ );
1294
+ }
1295
+ let containerChildren = snapshot.containerChildren;
1296
+ let containerIdentity = snapshot.containerIdentity;
1297
+ try {
1298
+ containerIdentity ??= await captureBrowserRuntimeSnapshotContainerIdentity(
1299
+ snapshot.directory,
1300
+ new AbortController().signal,
1301
+ );
1302
+ containerChildren ??= await captureBrowserRuntimeSnapshotDirectChildren(
1303
+ snapshot.directory,
1304
+ new AbortController().signal,
1305
+ );
1306
+ } catch (error) {
1307
+ await closeLease();
1308
+ if (isFileSystemError(error, "ENOENT")) return;
1309
+ throw error;
1310
+ }
1311
+ const quarantine = join(
1312
+ dirname(snapshot.directory),
1313
+ `${BROWSER_RUNTIME_SNAPSHOT_RECLAIM_PREFIX}${randomUUID()}`,
1314
+ );
1315
+ await assertPathAbsent(quarantine, "The browser runtime reclaim path");
1316
+ try {
1317
+ await rename(snapshot.directory, quarantine);
1318
+ } catch (error) {
1319
+ await closeLease();
1320
+ throw error;
1321
+ }
1322
+ try {
1323
+ const movedContainer = await captureBrowserRuntimeSnapshotContainerIdentity(
1324
+ quarantine,
1325
+ new AbortController().signal,
1326
+ );
1327
+ if (!sameSnapshotPathNodeIdentity(movedContainer, containerIdentity)) {
1328
+ throw new ApplicationError(
1329
+ "conflict",
1330
+ "A browser runtime cleanup target changed while it was atomically quarantined; the moved tree was preserved.",
1331
+ );
1332
+ }
1333
+ const movedChildren = await captureBrowserRuntimeSnapshotDirectChildren(
1334
+ quarantine,
1335
+ new AbortController().signal,
1336
+ );
1337
+ if (canonicalJson(movedChildren) !== canonicalJson(containerChildren)) {
1338
+ throw new ApplicationError(
1339
+ "conflict",
1340
+ "Browser runtime cleanup children changed while their container was atomically quarantined; the moved tree was preserved.",
1341
+ );
1342
+ }
1343
+ const hasRuntimeBinding = snapshot.identity !== undefined
1344
+ || snapshot.runtimeEntries !== undefined;
1345
+ if (
1346
+ hasRuntimeBinding
1347
+ && (snapshot.identity === undefined || snapshot.runtimeEntries === undefined)
1348
+ ) {
1349
+ throw new ApplicationError(
1350
+ "internal",
1351
+ "Browser runtime cleanup received an incomplete runtime-tree binding.",
1352
+ );
1353
+ }
1354
+ if (snapshot.identity !== undefined && snapshot.runtimeEntries !== undefined) {
1355
+ const movedRuntimeIdentity = await captureBrowserRuntimeSnapshotIdentity(
1356
+ join(quarantine, basename(snapshot.runtimeRoot)),
1357
+ snapshot.runtimeEntries,
1358
+ new AbortController().signal,
1359
+ );
1360
+ const rootHasLaunchManagedCtime = snapshot.runtimeImmutable
1361
+ && snapshot.runtimeEntries[0]?.kind === "directory";
1362
+ const comparable = (entry: SnapshotPathIdentity | undefined) => (
1363
+ rootHasLaunchManagedCtime && entry?.path === "."
1364
+ ? { ...entry, ctimeNs: "macos-launch-managed" }
1365
+ : entry
1366
+ );
1367
+ if (
1368
+ canonicalJson(movedRuntimeIdentity.map(comparable))
1369
+ !== canonicalJson(snapshot.identity.map(comparable))
1370
+ ) {
1371
+ throw new ApplicationError(
1372
+ "conflict",
1373
+ "Browser runtime cleanup descendants changed before immutable flags could be cleared; the moved tree was preserved.",
1374
+ );
1375
+ }
1376
+ }
1377
+ const hasLeaseBinding = snapshot.lease !== undefined
1378
+ || snapshot.leaseHandle !== undefined
1379
+ || snapshot.leaseIdentity !== undefined;
1380
+ if (
1381
+ hasLeaseBinding
1382
+ && (
1383
+ snapshot.lease === undefined
1384
+ || snapshot.leaseHandle === undefined
1385
+ || snapshot.leaseIdentity === undefined
1386
+ )
1387
+ ) {
1388
+ throw new ApplicationError(
1389
+ "internal",
1390
+ "Browser runtime cleanup received an incomplete lease binding.",
1391
+ );
1392
+ }
1393
+ if (
1394
+ snapshot.lease !== undefined
1395
+ && snapshot.leaseHandle !== undefined
1396
+ && snapshot.leaseIdentity !== undefined
1397
+ ) {
1398
+ const heldLeaseIdentity = snapshotLeaseFileIdentity(
1399
+ await snapshot.leaseHandle.stat(),
1400
+ );
1401
+ const currentUid = typeof process.getuid === "function"
1402
+ ? process.getuid()
1403
+ : snapshot.leaseIdentity.uid;
1404
+ const movedLease = await readBrowserRuntimeSnapshotLease(
1405
+ quarantine,
1406
+ currentUid,
1407
+ );
1408
+ if (
1409
+ !sameSnapshotLeaseFileIdentity(
1410
+ heldLeaseIdentity,
1411
+ snapshot.leaseIdentity,
1412
+ )
1413
+ || movedLease === undefined
1414
+ || !sameSnapshotLeaseFileIdentity(
1415
+ movedLease.leaseIdentity,
1416
+ snapshot.leaseIdentity,
1417
+ )
1418
+ || canonicalJson(movedLease.lease) !== canonicalJson(snapshot.lease)
1419
+ ) {
1420
+ throw new ApplicationError(
1421
+ "conflict",
1422
+ "A browser runtime lease changed while its container was atomically quarantined; the moved tree was preserved.",
1423
+ );
1424
+ }
1425
+ await markBrowserRuntimeSnapshotLeaseReleased(
1426
+ quarantine,
1427
+ snapshot.lease,
1428
+ );
1429
+ }
1430
+ await closeLease();
1431
+ await (snapshot.releaseAndRemove
1432
+ ?? (async path => await removeQuarantinedBrowserRuntimeSnapshot(
1433
+ path,
1434
+ snapshot.runtimeImmutable,
1435
+ )))(quarantine);
1436
+ } finally {
1437
+ await closeLease();
1438
+ }
1439
+ }
1440
+
1441
+ async function assertNoBrowserRuntimeMutationEvents(
1442
+ mutations: ReadonlySet<string>,
1443
+ label: string,
1444
+ ): Promise<void> {
1445
+ await new Promise<void>(resolve => setImmediate(resolve));
1446
+ if (mutations.size > 0) {
1447
+ throw new ApplicationError(
1448
+ "conflict",
1449
+ `Browser runtime filesystem changed ${label}: ${[...mutations].sort().slice(0, 8).join(", ")}`,
1450
+ );
1451
+ }
1452
+ }
1453
+
1454
+ function discardProvenLaunchManagedRootMetadataEvent(
1455
+ mutations: Set<string>,
1456
+ runtimeName: string,
1457
+ ): void {
1458
+ // Darwin/FSEvents reports Gatekeeper's app-root metadata update as either a
1459
+ // change or rename. Call this only after the immutable container's exact
1460
+ // ctime identity has been re-proved; an actual child rename changes that
1461
+ // ctime and fails before this event can be discarded.
1462
+ mutations.delete(`change:${runtimeName}`);
1463
+ mutations.delete(`rename:${runtimeName}`);
1464
+ }
1465
+
1466
+ async function prepareBrowserRuntimeSnapshot(
1467
+ cacheRootInput: string,
1468
+ binding: HtmlOverlayBrowserRuntimeBinding,
1469
+ signal: AbortSignal,
1470
+ ): Promise<PreparedBrowserRuntimeSnapshot> {
1471
+ if (signal.aborted) throw cancellationReason(signal);
1472
+ const anchorBefore = await browserRuntimeSnapshotAnchor(cacheRootInput, signal);
1473
+ await scavengeStaleBrowserRuntimeSnapshots({
1474
+ anchorPath: anchorBefore.path,
1475
+ signal,
1476
+ });
1477
+ const sourceBefore = await inspectHtmlOverlayBrowserRuntime(
1478
+ binding.sourceRoot,
1479
+ binding.manifest.layout,
1480
+ binding.manifest.executableRelativePath,
1481
+ signal,
1482
+ );
1483
+ assertHtmlOverlayBrowserRuntimeManifest(
1484
+ sourceBefore,
1485
+ binding.manifest,
1486
+ "before its private snapshot",
1487
+ );
1488
+ const directory = await mkdtemp(join(
1489
+ anchorBefore.path,
1490
+ BROWSER_RUNTIME_SNAPSHOT_PREFIX,
1491
+ ));
1492
+ await chmod(directory, 0o700);
1493
+ const [directoryDetails, anchorIdentity] = await Promise.all([
1494
+ lstat(directory),
1495
+ inspectBrowserRuntimeSnapshotAnchor(
1496
+ anchorBefore.path,
1497
+ process.platform === "darwin",
1498
+ signal,
1499
+ ),
1500
+ ]);
1501
+ if (canonicalJson(anchorIdentity) !== canonicalJson(anchorBefore)) {
1502
+ await rm(directory, { force: true, recursive: true });
1503
+ throw new ApplicationError(
1504
+ "conflict",
1505
+ "The browser runtime snapshot anchor changed while its unique container was created.",
1506
+ );
1507
+ }
1508
+ if (
1509
+ directoryDetails.isSymbolicLink()
1510
+ || !directoryDetails.isDirectory()
1511
+ || (directoryDetails.mode & 0o777) !== 0o700
1512
+ ) {
1513
+ await rm(directory, { force: true, recursive: true });
1514
+ throw new ApplicationError(
1515
+ "unsafe-path",
1516
+ "The browser runtime snapshot directory is not private and physical.",
1517
+ );
1518
+ }
1519
+ const runtimeName = binding.manifest.layout === "macos-app-bundle"
1520
+ ? basename(binding.sourceRoot)
1521
+ : "browser";
1522
+ if (
1523
+ runtimeName === ""
1524
+ || runtimeName === "."
1525
+ || runtimeName === ".."
1526
+ || runtimeName.includes("/")
1527
+ ) {
1528
+ await rm(directory, { force: true, recursive: true });
1529
+ throw new ApplicationError("unsafe-path", "Browser runtime has an unsafe root name.");
1530
+ }
1531
+ const runtimeRoot = join(directory, runtimeName);
1532
+ let leaseBinding: Readonly<{
1533
+ handle: FileHandle;
1534
+ identity: SnapshotLeaseFileIdentity;
1535
+ lease: BrowserRuntimeSnapshotLease;
1536
+ }> | undefined;
1537
+ let runtimeImmutable = false;
1538
+ try {
1539
+ leaseBinding = await createBrowserRuntimeSnapshotLease(directory);
1540
+ for (const entry of binding.manifest.entries) {
1541
+ await copyRuntimeEntry(binding.sourceRoot, runtimeRoot, entry, signal);
1542
+ }
1543
+ for (const entry of [...binding.manifest.entries].reverse()) {
1544
+ if (entry.kind === "directory") {
1545
+ await chmod(snapshotEntryPath(runtimeRoot, entry.path), entry.mode);
1546
+ }
1547
+ }
1548
+ const [sourceAfter, snapshot] = await Promise.all([
1549
+ inspectHtmlOverlayBrowserRuntime(
1550
+ binding.sourceRoot,
1551
+ binding.manifest.layout,
1552
+ binding.manifest.executableRelativePath,
1553
+ signal,
1554
+ ),
1555
+ inspectHtmlOverlayBrowserRuntime(
1556
+ runtimeRoot,
1557
+ binding.manifest.layout,
1558
+ binding.manifest.executableRelativePath,
1559
+ signal,
1560
+ ),
1561
+ ]);
1562
+ assertHtmlOverlayBrowserRuntimeManifest(
1563
+ sourceAfter,
1564
+ sourceBefore,
1565
+ "while its private snapshot was copied",
1566
+ );
1567
+ assertHtmlOverlayBrowserRuntimeManifest(
1568
+ snapshot,
1569
+ binding.manifest,
1570
+ "inside its private snapshot",
1571
+ );
1572
+ if (binding.provenance.kind === "verified-macos-code-signature") {
1573
+ const snapshotProvenance = await inspectSupportedHtmlOverlayMacBrowserProvenance(
1574
+ runtimeRoot,
1575
+ snapshotEntryPath(
1576
+ runtimeRoot,
1577
+ binding.manifest.executableRelativePath,
1578
+ ),
1579
+ signal,
1580
+ );
1581
+ if (canonicalJson(snapshotProvenance) !== canonicalJson(binding.provenance)) {
1582
+ throw new ApplicationError(
1583
+ "conflict",
1584
+ "Private browser runtime snapshot changed its signed distribution identity.",
1585
+ );
1586
+ }
1587
+ const browserHome = join(directory, "home");
1588
+ const browserTemporaryDirectory = join(directory, "tmp");
1589
+ await mkdir(browserHome, { mode: 0o700 });
1590
+ await mkdir(browserTemporaryDirectory, { mode: 0o700 });
1591
+ runtimeImmutable = true;
1592
+ await setBrowserRuntimePathImmutable(runtimeRoot, true, true, signal);
1593
+ const identity = await captureBrowserRuntimeSnapshotIdentity(
1594
+ runtimeRoot,
1595
+ binding.manifest.entries,
1596
+ signal,
1597
+ );
1598
+ const containerIdentity = await captureBrowserRuntimeSnapshotContainerIdentity(
1599
+ directory,
1600
+ signal,
1601
+ );
1602
+ const containerChildren = await captureBrowserRuntimeSnapshotDirectChildren(
1603
+ directory,
1604
+ signal,
1605
+ );
1606
+ return {
1607
+ anchorIdentity,
1608
+ anchorPath: anchorBefore.path,
1609
+ browserHome,
1610
+ browserTemporaryDirectory,
1611
+ containerChildren,
1612
+ containerIdentity,
1613
+ directory,
1614
+ executablePath: snapshotEntryPath(
1615
+ runtimeRoot,
1616
+ binding.manifest.executableRelativePath,
1617
+ ),
1618
+ identity,
1619
+ lease: leaseBinding.lease,
1620
+ leaseHandle: leaseBinding.handle,
1621
+ leaseIdentity: leaseBinding.identity,
1622
+ runtimeEntries: binding.manifest.entries,
1623
+ runtimeImmutable,
1624
+ runtimeRoot,
1625
+ };
1626
+ }
1627
+ const browserHome = join(directory, "home");
1628
+ const browserTemporaryDirectory = join(directory, "tmp");
1629
+ await mkdir(browserHome, { mode: 0o700 });
1630
+ await mkdir(browserTemporaryDirectory, { mode: 0o700 });
1631
+ const identity = await captureBrowserRuntimeSnapshotIdentity(
1632
+ runtimeRoot,
1633
+ binding.manifest.entries,
1634
+ signal,
1635
+ );
1636
+ const containerIdentity = await captureBrowserRuntimeSnapshotContainerIdentity(
1637
+ directory,
1638
+ signal,
1639
+ );
1640
+ const containerChildren = await captureBrowserRuntimeSnapshotDirectChildren(
1641
+ directory,
1642
+ signal,
1643
+ );
1644
+ return {
1645
+ anchorIdentity,
1646
+ anchorPath: anchorBefore.path,
1647
+ browserHome,
1648
+ browserTemporaryDirectory,
1649
+ containerChildren,
1650
+ containerIdentity,
1651
+ directory,
1652
+ executablePath: snapshotEntryPath(
1653
+ runtimeRoot,
1654
+ binding.manifest.executableRelativePath,
1655
+ ),
1656
+ identity,
1657
+ lease: leaseBinding.lease,
1658
+ leaseHandle: leaseBinding.handle,
1659
+ leaseIdentity: leaseBinding.identity,
1660
+ runtimeEntries: binding.manifest.entries,
1661
+ runtimeImmutable,
1662
+ runtimeRoot,
1663
+ };
1664
+ } catch (error) {
1665
+ await removeBrowserRuntimeSnapshot({
1666
+ directory,
1667
+ lease: leaseBinding?.lease,
1668
+ leaseHandle: leaseBinding?.handle,
1669
+ leaseIdentity: leaseBinding?.identity,
1670
+ runtimeImmutable,
1671
+ runtimeRoot,
1672
+ });
1673
+ throw error;
1674
+ }
1675
+ }
1676
+
1677
+ async function verifyBrowserRuntimeSnapshot(
1678
+ snapshot: PreparedBrowserRuntimeSnapshot,
1679
+ binding: HtmlOverlayBrowserRuntimeBinding,
1680
+ label: string,
1681
+ signal: AbortSignal,
1682
+ ): Promise<void> {
1683
+ const current = await inspectHtmlOverlayBrowserRuntime(
1684
+ snapshot.runtimeRoot,
1685
+ binding.manifest.layout,
1686
+ binding.manifest.executableRelativePath,
1687
+ signal,
1688
+ );
1689
+ assertHtmlOverlayBrowserRuntimeManifest(current, binding.manifest, label);
1690
+ }
1691
+
1692
+ async function boundedBestEffortBrowserCleanup(
1693
+ label: string,
1694
+ start: () => Promise<unknown>,
1695
+ ): Promise<void> {
1696
+ try {
1697
+ await boundedBrowserStep(
1698
+ start,
1699
+ new AbortController().signal,
1700
+ BROWSER_CLEANUP_TIMEOUT_MS,
1701
+ label,
1702
+ );
1703
+ } catch {
1704
+ // The render result is already decided. Cleanup stays bounded and
1705
+ // deliberately cannot replace the primary render or cancellation error.
1706
+ }
1707
+ }
1708
+
1709
+ async function verifyCachedLibrary(
1710
+ path: string,
1711
+ lock: HtmlOverlayActiveLibraryLock,
1712
+ ): Promise<Buffer | undefined> {
1713
+ try {
1714
+ const details = await lstat(path);
1715
+ if (
1716
+ details.isSymbolicLink()
1717
+ || !details.isFile()
1718
+ || details.size !== lock.bytes
1719
+ || details.size > maximumLibraryBytes(lock)
1720
+ ) {
1721
+ return undefined;
1722
+ }
1723
+ const bytes = await readFile(path);
1724
+ return sha256(bytes) === lock.sha256 ? bytes : undefined;
1725
+ } catch (error) {
1726
+ if (isFileSystemError(error, "ENOENT")) {
1727
+ return undefined;
1728
+ }
1729
+ throw error;
1730
+ }
1731
+ }
1732
+
1733
+ async function publishCachedLibrary(
1734
+ cacheRoot: string,
1735
+ path: string,
1736
+ bytes: Buffer,
1737
+ ): Promise<void> {
1738
+ const temporary = join(cacheRoot, `.module-${randomUUID()}.tmp`);
1739
+ const handle = await open(
1740
+ temporary,
1741
+ constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW | constants.O_WRONLY,
1742
+ 0o600,
1743
+ );
1744
+ try {
1745
+ await handle.writeFile(bytes);
1746
+ await handle.sync();
1747
+ } finally {
1748
+ await handle.close();
1749
+ }
1750
+ try {
1751
+ await rename(temporary, path);
1752
+ } finally {
1753
+ await rm(temporary, { force: true });
1754
+ }
1755
+ }
1756
+
1757
+ async function responseBytes(
1758
+ response: Response,
1759
+ maximumBytes: number,
1760
+ ): Promise<Buffer> {
1761
+ if (!response.ok) {
1762
+ throw new ApplicationError(
1763
+ "unavailable",
1764
+ `Approved overlay library download failed with HTTP ${String(response.status)}.`,
1765
+ );
1766
+ }
1767
+ const declared = Number(response.headers.get("content-length"));
1768
+ if (Number.isFinite(declared) && declared > maximumBytes) {
1769
+ throw new ApplicationError("invalid-data", "Approved overlay library exceeds its byte limit.");
1770
+ }
1771
+ const reader = response.body?.getReader();
1772
+ if (reader === undefined) {
1773
+ throw new ApplicationError("unavailable", "Approved overlay library returned no response body.");
1774
+ }
1775
+ const chunks: Uint8Array[] = [];
1776
+ let total = 0;
1777
+ while (true) {
1778
+ const result = await reader.read();
1779
+ if (result.done) break;
1780
+ total += result.value.byteLength;
1781
+ if (total > maximumBytes) {
1782
+ await reader.cancel();
1783
+ throw new ApplicationError("invalid-data", "Approved overlay library exceeds its byte limit.");
1784
+ }
1785
+ chunks.push(result.value);
1786
+ }
1787
+ return Buffer.concat(chunks, total);
1788
+ }
1789
+
1790
+ async function exactResourceBytes(
1791
+ resource: BoundHtmlOverlayResource,
1792
+ ): Promise<Buffer> {
1793
+ const details = await lstat(resource.absolutePath);
1794
+ if (
1795
+ details.isSymbolicLink()
1796
+ || !details.isFile()
1797
+ || details.size !== resource.bytes
1798
+ ) {
1799
+ throw new ApplicationError(
1800
+ "conflict",
1801
+ `HTML-overlay resource changed before browser rendering: ${resource.name}`,
1802
+ );
1803
+ }
1804
+ const bytes = await readFile(resource.absolutePath);
1805
+ if (sha256(bytes) !== resource.sha256) {
1806
+ throw new ApplicationError(
1807
+ "conflict",
1808
+ `HTML-overlay resource changed before browser rendering: ${resource.name}`,
1809
+ );
1810
+ }
1811
+ return bytes;
1812
+ }
1813
+
1814
+ function contentSecurityPolicy(executionProfile: HtmlOverlayExecutionProfile | undefined, resources: readonly HtmlOverlayDeclaredResource[]): string {
1815
+ return htmlOverlayRendererContract(executionProfile, resources).contentSecurityPolicy.join("; ");
1816
+ }
1817
+
1818
+ async function fulfillPreparedRoute(
1819
+ route: Route,
1820
+ routes: ReadonlyMap<string, PreparedRoute>,
1821
+ onBlocked: () => void,
1822
+ executionProfile: HtmlOverlayExecutionProfile | undefined,
1823
+ resources: readonly HtmlOverlayDeclaredResource[],
1824
+ ): Promise<void> {
1825
+ const requestedUrl = new URL(route.request().url());
1826
+ const prepared = requestedUrl.origin === SYNTHETIC_ORIGIN
1827
+ ? routes.get(requestedUrl.pathname)
1828
+ : undefined;
1829
+ if (prepared === undefined) {
1830
+ onBlocked();
1831
+ await route.abort("blockedbyclient");
1832
+ return;
1833
+ }
1834
+ await route.fulfill({
1835
+ body: prepared.body,
1836
+ contentType: prepared.contentType,
1837
+ headers: {
1838
+ "Cache-Control": "no-store",
1839
+ "Content-Security-Policy": contentSecurityPolicy(executionProfile, resources),
1840
+ "Cross-Origin-Resource-Policy": "same-origin",
1841
+ },
1842
+ status: 200,
1843
+ });
1844
+ }
1845
+
1846
+ export class PlaywrightHtmlOverlayRenderer implements HtmlOverlayRenderer {
1847
+ readonly #browserStepTimeoutMs: number;
1848
+ readonly #cacheRoot: string;
1849
+ readonly #fetch: (
1850
+ input: RequestInfo | URL,
1851
+ init?: RequestInit,
1852
+ ) => Promise<Response>;
1853
+ readonly #frameTimeoutMs: number;
1854
+ readonly #launch: (options: LaunchOptions) => Promise<Browser>;
1855
+
1856
+ constructor(options: PlaywrightHtmlOverlayRendererOptions) {
1857
+ const browserStepTimeoutMs = options.browserStepTimeoutMs
1858
+ ?? DEFAULT_BROWSER_STEP_TIMEOUT_MS;
1859
+ if (
1860
+ !Number.isSafeInteger(browserStepTimeoutMs)
1861
+ || browserStepTimeoutMs < 100
1862
+ || browserStepTimeoutMs > MAXIMUM_BROWSER_STEP_TIMEOUT_MS
1863
+ ) {
1864
+ throw new ApplicationError(
1865
+ "usage",
1866
+ `HTML overlay browser step timeout must be an integer from 100 through ${String(MAXIMUM_BROWSER_STEP_TIMEOUT_MS)}ms.`,
1867
+ );
1868
+ }
1869
+ this.#browserStepTimeoutMs = browserStepTimeoutMs;
1870
+ const frameTimeoutMs = options.frameTimeoutMs ?? browserStepTimeoutMs;
1871
+ if (
1872
+ !Number.isSafeInteger(frameTimeoutMs)
1873
+ || frameTimeoutMs < 100
1874
+ || frameTimeoutMs > MAXIMUM_BROWSER_STEP_TIMEOUT_MS
1875
+ ) {
1876
+ throw new ApplicationError(
1877
+ "usage",
1878
+ `HTML overlay frame timeout must be an integer from 100 through ${String(MAXIMUM_BROWSER_STEP_TIMEOUT_MS)}ms.`,
1879
+ );
1880
+ }
1881
+ this.#frameTimeoutMs = frameTimeoutMs;
1882
+ this.#cacheRoot = options.cacheRoot;
1883
+ this.#fetch = options.fetch ?? globalThis.fetch;
1884
+ this.#launch = options.launch ?? (
1885
+ async launchOptions => await chromium.launch(launchOptions)
1886
+ );
1887
+ }
1888
+
1889
+ /** @internal Test seam for proving seek-stable adapters in the real browser. */
1890
+ protected orderedFrameIndexes(frameCount: number): readonly number[] {
1891
+ return Array.from({ length: frameCount }, (_, index) => index);
1892
+ }
1893
+
1894
+ async #libraryBytes(
1895
+ lock: HtmlOverlayActiveLibraryLock,
1896
+ signal: AbortSignal,
1897
+ ): Promise<Buffer> {
1898
+ const cacheRoot = await ensurePrivateCacheRoot(this.#cacheRoot);
1899
+ const path = join(cacheRoot, `${lock.sha256}.mjs`);
1900
+ const cached = await verifyCachedLibrary(path, lock);
1901
+ if (cached !== undefined) return cached;
1902
+ const response = await this.#fetch(lock.url, {
1903
+ redirect: "error",
1904
+ signal,
1905
+ });
1906
+ const bytes = await responseBytes(response, Math.min(lock.bytes, maximumLibraryBytes(lock)));
1907
+ if (bytes.byteLength !== lock.bytes || sha256(bytes) !== lock.sha256) {
1908
+ throw new ApplicationError(
1909
+ "conflict",
1910
+ `Approved overlay library bytes do not match the lock: ${lock.specifier}`,
1911
+ );
1912
+ }
1913
+ await publishCachedLibrary(cacheRoot, path, bytes);
1914
+ const published = await verifyCachedLibrary(path, lock);
1915
+ if (published === undefined) {
1916
+ throw new ApplicationError("conflict", "Approved overlay library cache publication failed.");
1917
+ }
1918
+ return published;
1919
+ }
1920
+
1921
+ async renderFrames(
1922
+ request: HtmlOverlayFrameRenderRequest,
1923
+ signal: AbortSignal,
1924
+ ): Promise<HtmlOverlayFrameRenderResult> {
1925
+ if (signal.aborted) throw cancellationReason(signal);
1926
+ const execution = createHtmlOverlayExecutionBundle(
1927
+ request.authoring,
1928
+ request.browserRuntime,
1929
+ request.executionProfile,
1930
+ );
1931
+ if (request.executionProfile !== undefined && process.platform !== "darwin") throw new ApplicationError("unsupported-plan", "Hardware scene profiles currently require qualified macOS ANGLE Metal execution.");
1932
+ const locks = execution.libraryLocks;
1933
+ const [libraryBodies, resourceBodies] = await Promise.all([
1934
+ Promise.all(locks.map(async lock => await this.#libraryBytes(lock, signal))),
1935
+ Promise.all(request.resources.map(async resource => await exactResourceBytes(resource))),
1936
+ ]);
1937
+ const routes = new Map<string, PreparedRoute>();
1938
+ const importMap = execution.importMap;
1939
+ routes.set(new URL(DOCUMENT_URL).pathname, {
1940
+ body: Buffer.from("<!doctype html><html><head></head><body></body></html>"),
1941
+ contentType: "text/html; charset=utf-8",
1942
+ });
1943
+ locks.forEach((lock, index) => {
1944
+ routes.set(htmlOverlayLibraryLocalUrl(lock), {
1945
+ body: libraryBodies[index]!,
1946
+ contentType: "text/javascript; charset=utf-8",
1947
+ });
1948
+ });
1949
+ request.resources.forEach((resource, index) => {
1950
+ routes.set(htmlOverlayAssetLocalUrl(resource), {
1951
+ body: resourceBodies[index]!,
1952
+ contentType: resource.mediaType,
1953
+ });
1954
+ });
1955
+
1956
+ const finalFrameDirectory = join(request.outputDirectory, "frames");
1957
+ await assertPathAbsent(
1958
+ finalFrameDirectory,
1959
+ "The HTML-overlay frame directory",
1960
+ );
1961
+ const stagingFrameDirectory = await mkdtemp(
1962
+ join(request.outputDirectory, ".html-overlay-frames-"),
1963
+ );
1964
+ const frameCount = htmlOverlayFrameCount(request.authoring.timing);
1965
+ const diagnostics: string[] = [];
1966
+ const pageErrors: string[] = [];
1967
+ let browserRuntimeSnapshot: PreparedBrowserRuntimeSnapshot | undefined;
1968
+ const browserRuntimeWatchers: FSWatcher[] = [];
1969
+ const browserRuntimeMutations = new Set<string>();
1970
+ let renderFailure: Readonly<{ error: unknown }> | undefined;
1971
+ let renderResult: HtmlOverlayFrameRenderResult | undefined;
1972
+ let gpuEvidence: HtmlOverlayGpuEvidence | undefined;
1973
+ let browserShutdownRequired = false;
1974
+ try {
1975
+ const preparedBrowserRuntime = await prepareBrowserRuntimeSnapshot(
1976
+ this.#cacheRoot,
1977
+ request.browserRuntime,
1978
+ signal,
1979
+ );
1980
+ browserRuntimeSnapshot = preparedBrowserRuntime;
1981
+ const runtimeName = basename(preparedBrowserRuntime.runtimeRoot);
1982
+ const snapshotName = basename(preparedBrowserRuntime.directory);
1983
+ const anchorWatcher = watch(
1984
+ preparedBrowserRuntime.anchorPath,
1985
+ { persistent: false },
1986
+ (eventType, filename) => {
1987
+ const path = filename?.toString() ?? "<snapshot-anchor>";
1988
+ if (path === snapshotName) {
1989
+ browserRuntimeMutations.add(`anchor-${eventType}:${path}`);
1990
+ }
1991
+ },
1992
+ );
1993
+ anchorWatcher.on("error", error => {
1994
+ browserRuntimeMutations.add(`anchor-watch-error:${error.message}`);
1995
+ });
1996
+ browserRuntimeWatchers.push(anchorWatcher);
1997
+ const containerWatcher = watch(
1998
+ preparedBrowserRuntime.directory,
1999
+ { persistent: false, recursive: process.platform === "darwin" },
2000
+ (eventType, filename) => {
2001
+ const path = filename?.toString().replaceAll("\\", "/")
2002
+ ?? "<snapshot-container>";
2003
+ if (
2004
+ path === "home"
2005
+ || path.startsWith("home/")
2006
+ || path === "tmp"
2007
+ || path.startsWith("tmp/")
2008
+ ) return;
2009
+ if (path === runtimeName && eventType === "change") {
2010
+ // Gatekeeper may change the app root's first-launch metadata. The
2011
+ // immutable parent and its exact ctime identity independently
2012
+ // prove that this pathname was never removed or replaced.
2013
+ return;
2014
+ }
2015
+ if (
2016
+ request.browserRuntime.provenance.kind === "test-only-unverified"
2017
+ && request.browserRuntime.manifest.layout === "single-executable"
2018
+ && path === runtimeName
2019
+ && eventType === "rename"
2020
+ ) {
2021
+ // Darwin reports synthetic rename events for accesses to the
2022
+ // test-only single-file fixture. Production rejects this layout;
2023
+ // its exact path identity still detects fixture substitution.
2024
+ return;
2025
+ }
2026
+ if (path === runtimeName || path.startsWith(`${runtimeName}/`)) {
2027
+ browserRuntimeMutations.add(`${eventType}:${path}`);
2028
+ return;
2029
+ }
2030
+ browserRuntimeMutations.add(`${eventType}:${path}`);
2031
+ },
2032
+ );
2033
+ containerWatcher.on("error", error => {
2034
+ browserRuntimeMutations.add(`watch-error:${error.message}`);
2035
+ });
2036
+ browserRuntimeWatchers.push(containerWatcher);
2037
+ await new Promise<void>(resolve => setTimeout(resolve, 25));
2038
+ if (signal.aborted) throw cancellationReason(signal);
2039
+ // FSEvents can deliver the just-created direct-child notification after
2040
+ // both watches attach. Clear that bounded baseline before re-verifying
2041
+ // every manifest and path identity. A swap during this window changes
2042
+ // the already-captured container ctime and therefore fails below.
2043
+ browserRuntimeMutations.clear();
2044
+ let browser: Browser | undefined;
2045
+ let browserFailure: Readonly<{ error: unknown }> | undefined;
2046
+ try {
2047
+ await verifyBrowserRuntimeSnapshot(
2048
+ preparedBrowserRuntime,
2049
+ request.browserRuntime,
2050
+ "immediately before launch",
2051
+ signal,
2052
+ );
2053
+ await assertBrowserRuntimeSnapshotIdentity(
2054
+ preparedBrowserRuntime,
2055
+ request.browserRuntime.manifest.entries,
2056
+ "immediately before launch",
2057
+ signal,
2058
+ );
2059
+ await assertBrowserRuntimeSnapshotContainerIdentity(
2060
+ preparedBrowserRuntime,
2061
+ "immediately before launch",
2062
+ signal,
2063
+ );
2064
+ await assertBrowserRuntimeSnapshotAnchorIdentity(
2065
+ preparedBrowserRuntime,
2066
+ "immediately before launch",
2067
+ signal,
2068
+ );
2069
+ discardProvenLaunchManagedRootMetadataEvent(
2070
+ browserRuntimeMutations,
2071
+ runtimeName,
2072
+ );
2073
+ await assertNoBrowserRuntimeMutationEvents(
2074
+ browserRuntimeMutations,
2075
+ "before browser launch",
2076
+ );
2077
+ const launchedBrowser = await boundedBrowserStep(
2078
+ async () => {
2079
+ browserShutdownRequired = true;
2080
+ try {
2081
+ return await this.#launch({
2082
+ args: createHtmlOverlayBrowserLaunchArgs(
2083
+ request.authoring.libraries,
2084
+ request.executionProfile,
2085
+ ),
2086
+ env: {
2087
+ ...HTML_OVERLAY_RENDERER_CONTRACT.environment.fixed,
2088
+ HOME: preparedBrowserRuntime.browserHome,
2089
+ TMPDIR: preparedBrowserRuntime.browserTemporaryDirectory,
2090
+ },
2091
+ executablePath: preparedBrowserRuntime.executablePath,
2092
+ headless: HTML_OVERLAY_RENDERER_CONTRACT.launch.headless,
2093
+ timeout: this.#browserStepTimeoutMs,
2094
+ });
2095
+ } catch (error) {
2096
+ // A rejected launch promise has settled without exposing a browser.
2097
+ browserShutdownRequired = false;
2098
+ throw error;
2099
+ }
2100
+ },
2101
+ signal,
2102
+ this.#browserStepTimeoutMs,
2103
+ "launch",
2104
+ async lateBrowser => {
2105
+ await requiredBrowserCleanup(
2106
+ "late browser close",
2107
+ async () => await lateBrowser.close(),
2108
+ );
2109
+ browserShutdownRequired = false;
2110
+ },
2111
+ );
2112
+ browser = launchedBrowser;
2113
+ await verifyBrowserRuntimeSnapshot(
2114
+ preparedBrowserRuntime,
2115
+ request.browserRuntime,
2116
+ "during browser launch",
2117
+ signal,
2118
+ );
2119
+ await assertBrowserRuntimeSnapshotIdentity(
2120
+ preparedBrowserRuntime,
2121
+ request.browserRuntime.manifest.entries,
2122
+ "during browser launch",
2123
+ signal,
2124
+ );
2125
+ await assertBrowserRuntimeSnapshotContainerIdentity(
2126
+ preparedBrowserRuntime,
2127
+ "during browser launch",
2128
+ signal,
2129
+ );
2130
+ await assertBrowserRuntimeSnapshotAnchorIdentity(
2131
+ preparedBrowserRuntime,
2132
+ "during browser launch",
2133
+ signal,
2134
+ );
2135
+ discardProvenLaunchManagedRootMetadataEvent(
2136
+ browserRuntimeMutations,
2137
+ runtimeName,
2138
+ );
2139
+ await assertNoBrowserRuntimeMutationEvents(
2140
+ browserRuntimeMutations,
2141
+ "during browser launch",
2142
+ );
2143
+ const contextContract = HTML_OVERLAY_RENDERER_CONTRACT.browserContext;
2144
+ const context = await boundedBrowserStep(
2145
+ async () => await launchedBrowser.newContext({
2146
+ acceptDownloads: contextContract.acceptDownloads,
2147
+ colorScheme: contextContract.colorScheme,
2148
+ deviceScaleFactor: request.authoring.canvas.deviceScaleFactor,
2149
+ locale: contextContract.locale,
2150
+ offline: contextContract.offline,
2151
+ permissions: [...contextContract.permissions],
2152
+ reducedMotion: contextContract.reducedMotion,
2153
+ serviceWorkers: contextContract.serviceWorkers,
2154
+ timezoneId: contextContract.timezoneId,
2155
+ viewport: {
2156
+ height: request.authoring.canvas.height,
2157
+ width: request.authoring.canvas.width,
2158
+ },
2159
+ }),
2160
+ signal,
2161
+ this.#browserStepTimeoutMs,
2162
+ "context creation",
2163
+ );
2164
+ try {
2165
+ let blockedRequestCount = 0;
2166
+ let childFrameCount = 0;
2167
+ let policyViolationCount = 0;
2168
+ let unexpectedPageCount = 0;
2169
+ await boundedBrowserStep(
2170
+ async () => await context.route("**/*", async route => {
2171
+ await fulfillPreparedRoute(
2172
+ route,
2173
+ routes,
2174
+ () => {
2175
+ blockedRequestCount += 1;
2176
+ },
2177
+ request.executionProfile,
2178
+ request.authoring.resources,
2179
+ );
2180
+ }),
2181
+ signal,
2182
+ this.#browserStepTimeoutMs,
2183
+ "route installation",
2184
+ );
2185
+ const page = await boundedBrowserStep(
2186
+ async () => await context.newPage(),
2187
+ signal,
2188
+ this.#browserStepTimeoutMs,
2189
+ "page creation",
2190
+ );
2191
+ context.on("page", (candidate) => {
2192
+ if (candidate === page) return;
2193
+ unexpectedPageCount += 1;
2194
+ void candidate.close().catch(() => undefined);
2195
+ });
2196
+ page.on("frameattached", (frame) => {
2197
+ if (frame !== page.mainFrame()) {
2198
+ childFrameCount += 1;
2199
+ }
2200
+ });
2201
+ page.on("console", (message) => {
2202
+ const messageText = message.text();
2203
+ if (
2204
+ message.type() === "error"
2205
+ && messageText.includes("Content Security Policy")
2206
+ ) {
2207
+ policyViolationCount += 1;
2208
+ }
2209
+ if (
2210
+ diagnostics.length < MAXIMUM_DIAGNOSTICS
2211
+ && (message.type() === "error" || message.type() === "warning")
2212
+ ) {
2213
+ diagnostics.push(boundedDiagnostic(`${message.type()}: ${messageText}`));
2214
+ }
2215
+ });
2216
+ page.on("pageerror", (error) => {
2217
+ if (pageErrors.length < MAXIMUM_DIAGNOSTICS) {
2218
+ pageErrors.push(boundedDiagnostic(error.message));
2219
+ }
2220
+ });
2221
+ const assertNoDeniedBrowserActivity = async (
2222
+ host: JSHandle<HtmlOverlayHostController>,
2223
+ ): Promise<void> => {
2224
+ const securityViolationCount = await boundedBrowserStep(
2225
+ async () => await host.evaluate(
2226
+ controller => controller.securityViolationCount(),
2227
+ ),
2228
+ signal,
2229
+ this.#browserStepTimeoutMs,
2230
+ "policy inspection",
2231
+ );
2232
+ if (
2233
+ blockedRequestCount > 0
2234
+ || childFrameCount > 0
2235
+ || policyViolationCount > 0
2236
+ || unexpectedPageCount > 0
2237
+ || securityViolationCount > 0
2238
+ ) {
2239
+ throw new ApplicationError(
2240
+ "invalid-data",
2241
+ "HTML overlay attempted undeclared browser access.",
2242
+ {
2243
+ blockedRequestCount,
2244
+ childFrameCount,
2245
+ policyViolationCount,
2246
+ securityViolationCount,
2247
+ unexpectedPageCount,
2248
+ },
2249
+ );
2250
+ }
2251
+ };
2252
+ const runtimeSource = execution.runtimeSource;
2253
+ await boundedBrowserStep(
2254
+ async () => await page.goto(DOCUMENT_URL, {
2255
+ timeout: this.#browserStepTimeoutMs,
2256
+ waitUntil: "load",
2257
+ }),
2258
+ signal,
2259
+ this.#browserStepTimeoutMs,
2260
+ "initial navigation",
2261
+ );
2262
+ const host = await boundedBrowserStep(
2263
+ async () => await page.evaluateHandle<HtmlOverlayHostController>(
2264
+ runtimeSource,
2265
+ ),
2266
+ signal,
2267
+ this.#browserStepTimeoutMs,
2268
+ "runtime installation",
2269
+ );
2270
+ let gpuProbe: JSHandle<HtmlOverlayGpuProbe> | undefined;
2271
+ try {
2272
+ if (request.executionProfile !== undefined) gpuProbe = await boundedBrowserStep(
2273
+ async () => await installHtmlOverlayGpuProbe(page), signal, this.#browserStepTimeoutMs, "hardware probe installation",
2274
+ );
2275
+ const inspectGpu = async () => {
2276
+ if (gpuProbe === undefined || request.executionProfile === undefined) return;
2277
+ const probe = gpuProbe;
2278
+ const actualContext = await boundedBrowserStep(async () => await probe.evaluate(value => value.inspect()), signal, this.#browserStepTimeoutMs, "active hardware context inspection");
2279
+ const browserGpu = gpuEvidence?.browserGpu ?? await boundedBrowserStep(async () => await inspectHtmlOverlayBrowserGpu(launchedBrowser), signal, this.#browserStepTimeoutMs, "browser hardware identity inspection");
2280
+ const evidence = createHtmlOverlayGpuEvidence(request.executionProfile, actualContext, browserGpu);
2281
+ const width = request.authoring.canvas.width * request.authoring.canvas.deviceScaleFactor;
2282
+ const height = request.authoring.canvas.height * request.authoring.canvas.deviceScaleFactor;
2283
+ if (Math.max(width, height) > Math.min(actualContext.maxTextureSize, actualContext.maxRenderbufferSize)
2284
+ || width > actualContext.maxViewportDimensions[0] || height > actualContext.maxViewportDimensions[1]) throw new ApplicationError("unsupported-plan", "Scene dimensions exceed the observed hardware limits.");
2285
+ if (gpuEvidence !== undefined && canonicalJson(evidence) !== canonicalJson(gpuEvidence)) throw new ApplicationError("conflict", "The active GPU changed during scene capture.");
2286
+ gpuEvidence = evidence;
2287
+ };
2288
+ const html = await htmlWithHostImports(
2289
+ page,
2290
+ request.authoring.html,
2291
+ importMap,
2292
+ signal,
2293
+ this.#browserStepTimeoutMs,
2294
+ );
2295
+ // document.open(), which powers setContent(), preserves this
2296
+ // Window. Installing the runtime first lets authored scripts
2297
+ // register ready/onFrame callbacks during initial evaluation.
2298
+ await boundedBrowserStep(
2299
+ async () => await page.setContent(html, {
2300
+ timeout: this.#browserStepTimeoutMs,
2301
+ waitUntil: "load",
2302
+ }),
2303
+ signal,
2304
+ this.#browserStepTimeoutMs,
2305
+ "document load",
2306
+ );
2307
+ await assertNoDeniedBrowserActivity(host);
2308
+ for (const frameIndex of this.orderedFrameIndexes(frameCount)) {
2309
+ if (signal.aborted) throw cancellationReason(signal);
2310
+ const frame = createHtmlOverlayRuntimeFrame(
2311
+ frameIndex,
2312
+ request.authoring.canvas,
2313
+ request.authoring.timing,
2314
+ );
2315
+ await boundedBrowserStep(
2316
+ async () => await host.evaluate(
2317
+ async (
2318
+ controller,
2319
+ nextFrame,
2320
+ ) => await controller.renderFrame(nextFrame),
2321
+ frame,
2322
+ ),
2323
+ signal,
2324
+ this.#frameTimeoutMs,
2325
+ `frame ${String(frameIndex)} evaluation`,
2326
+ );
2327
+ await assertNoDeniedBrowserActivity(host);
2328
+ if (pageErrors.length > 0) {
2329
+ throw new ApplicationError(
2330
+ "invalid-data",
2331
+ `HTML overlay failed during frame ${String(frameIndex)}: ${pageErrors.join(" | ")}`,
2332
+ );
2333
+ }
2334
+ await inspectGpu();
2335
+ await boundedBrowserStep(
2336
+ async () => await page.screenshot({
2337
+ omitBackground: HTML_OVERLAY_RENDERER_CONTRACT.screenshot.omitBackground,
2338
+ path: join(
2339
+ stagingFrameDirectory,
2340
+ `frame-${String(frameIndex).padStart(8, "0")}.png`,
2341
+ ),
2342
+ scale: HTML_OVERLAY_RENDERER_CONTRACT.screenshot.scale,
2343
+ type: HTML_OVERLAY_RENDERER_CONTRACT.screenshot.type,
2344
+ }),
2345
+ signal,
2346
+ this.#browserStepTimeoutMs,
2347
+ `frame ${String(frameIndex)} screenshot`,
2348
+ );
2349
+ await inspectGpu();
2350
+ await assertNoDeniedBrowserActivity(host);
2351
+ if (pageErrors.length > 0) {
2352
+ throw new ApplicationError(
2353
+ "invalid-data",
2354
+ `HTML overlay failed during frame ${String(frameIndex)}: ${pageErrors.join(" | ")}`,
2355
+ );
2356
+ }
2357
+ }
2358
+ if (gpuEvidence !== undefined) {
2359
+ const finalBrowserGpu = await boundedBrowserStep(async () => await inspectHtmlOverlayBrowserGpu(launchedBrowser), signal, this.#browserStepTimeoutMs, "final browser hardware identity inspection");
2360
+ if (canonicalJson(finalBrowserGpu) !== canonicalJson(gpuEvidence.browserGpu)) throw new ApplicationError("conflict", "The browser GPU changed before scene capture completed.");
2361
+ await inspectGpu();
2362
+ await assertNoDeniedBrowserActivity(host);
2363
+ }
2364
+ if (request.executionProfile !== undefined && gpuEvidence === undefined) throw new ApplicationError("invalid-data", "Hardware scene capture produced no measured GPU evidence.");
2365
+ } finally {
2366
+ if (gpuProbe !== undefined) await boundedBestEffortBrowserCleanup("hardware probe disposal", async () => await gpuProbe!.dispose());
2367
+ await boundedBestEffortBrowserCleanup(
2368
+ "host handle disposal",
2369
+ async () => await host.dispose(),
2370
+ );
2371
+ }
2372
+ } finally {
2373
+ await boundedBestEffortBrowserCleanup(
2374
+ "context close",
2375
+ async () => await context.close(),
2376
+ );
2377
+ }
2378
+ } catch (error) {
2379
+ if (error instanceof HtmlOverlayBrowserCleanupError) browserFailure = { error };
2380
+ else if (signal.aborted) browserFailure = { error: cancellationReason(signal) };
2381
+ else if (error instanceof ApplicationError) browserFailure = { error };
2382
+ else {
2383
+ const suffix = diagnostics.length === 0 ? "" : ` Browser diagnostics: ${diagnostics.join(" | ")}`;
2384
+ browserFailure = { error: new ApplicationError("invalid-data", `HTML overlay browser rendering failed.${suffix}`,
2385
+ { cause: error instanceof Error ? error.message : String(error) }) };
2386
+ }
2387
+ } finally {
2388
+ const launchedBrowser = browser;
2389
+ if (launchedBrowser !== undefined) {
2390
+ try {
2391
+ await requiredBrowserCleanup("close", async () => await launchedBrowser.close());
2392
+ browserShutdownRequired = false;
2393
+ } catch (cleanupError) {
2394
+ browserFailure = { error: new HtmlOverlayBrowserCleanupError("close", cleanupError instanceof HtmlOverlayBrowserCleanupError ? cleanupError.cause : cleanupError, browserFailure?.error) };
2395
+ }
2396
+ }
2397
+ }
2398
+ if (browserFailure !== undefined) throw browserFailure.error;
2399
+ await verifyBrowserRuntimeSnapshot(
2400
+ preparedBrowserRuntime,
2401
+ request.browserRuntime,
2402
+ "through browser shutdown",
2403
+ signal,
2404
+ );
2405
+ await assertBrowserRuntimeSnapshotIdentity(
2406
+ preparedBrowserRuntime,
2407
+ request.browserRuntime.manifest.entries,
2408
+ "through browser shutdown",
2409
+ signal,
2410
+ );
2411
+ await assertBrowserRuntimeSnapshotContainerIdentity(
2412
+ preparedBrowserRuntime,
2413
+ "through browser shutdown",
2414
+ signal,
2415
+ );
2416
+ await assertBrowserRuntimeSnapshotAnchorIdentity(
2417
+ preparedBrowserRuntime,
2418
+ "through browser shutdown",
2419
+ signal,
2420
+ );
2421
+ discardProvenLaunchManagedRootMetadataEvent(
2422
+ browserRuntimeMutations,
2423
+ runtimeName,
2424
+ );
2425
+ await assertNoBrowserRuntimeMutationEvents(
2426
+ browserRuntimeMutations,
2427
+ "through browser shutdown",
2428
+ );
2429
+ renderResult = {
2430
+ ...(gpuEvidence === undefined ? {} : { gpuEvidence }),
2431
+ executionIntegrity: execution.integrity,
2432
+ frameCount,
2433
+ framePattern: join(finalFrameDirectory, "frame-%08d.png"),
2434
+ libraryLocks: locks,
2435
+ };
2436
+ } catch (error) {
2437
+ renderFailure = { error };
2438
+ await rm(stagingFrameDirectory, { force: true, recursive: true })
2439
+ .catch(() => undefined);
2440
+ } finally {
2441
+ for (const watcher of browserRuntimeWatchers) watcher.close();
2442
+ if (browserRuntimeSnapshot !== undefined) {
2443
+ if (browserShutdownRequired) {
2444
+ const retainedError = new ApplicationError("unavailable", "HTML overlay browser shutdown is unproven; its active runtime snapshot was retained and frames were not published.", {
2445
+ runtimeSnapshot: browserRuntimeSnapshot.directory,
2446
+ runtimeRoot: browserRuntimeSnapshot.runtimeRoot,
2447
+ leaseState: "active",
2448
+ cause: boundedDiagnostic(renderFailure?.error instanceof Error ? renderFailure.error.message : String(renderFailure?.error)),
2449
+ ...(renderFailure?.error instanceof HtmlOverlayBrowserCleanupError ? { cleanup: renderFailure.error.details } : {}),
2450
+ });
2451
+ retainedError.cause = renderFailure?.error;
2452
+ try { await browserRuntimeSnapshot.leaseHandle.close(); }
2453
+ catch (leaseError) { retainedError.cause = new AggregateError([renderFailure?.error, leaseError], "Browser cleanup and retained lease-handle close failed."); }
2454
+ renderFailure = { error: retainedError };
2455
+ } else {
2456
+ try {
2457
+ await removeBrowserRuntimeSnapshot(browserRuntimeSnapshot);
2458
+ } catch (cleanupError) {
2459
+ renderFailure ??= { error: cleanupError };
2460
+ }
2461
+ }
2462
+ }
2463
+ }
2464
+ if (renderFailure !== undefined) {
2465
+ await rm(stagingFrameDirectory, { force: true, recursive: true })
2466
+ .catch(() => undefined);
2467
+ throw renderFailure.error;
2468
+ }
2469
+ if (renderResult === undefined) {
2470
+ throw new ApplicationError(
2471
+ "internal",
2472
+ "HTML overlay rendering completed without a result or failure.",
2473
+ );
2474
+ }
2475
+ await rename(stagingFrameDirectory, finalFrameDirectory);
2476
+ return renderResult;
2477
+ }
2478
+ }