@volter/editor-core 0.5.57

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 (719) hide show
  1. package/BUNDLED_NOTICES +1527 -0
  2. package/LICENSE +686 -0
  3. package/LICENSE-APACHE +202 -0
  4. package/NOTICE +18 -0
  5. package/README.md +24 -0
  6. package/dist/build/chunk-JS4D4XZ3.js +175 -0
  7. package/dist/build/chunk-JS4D4XZ3.js.map +7 -0
  8. package/dist/build/vite-plugin-product-contributions.js +100 -0
  9. package/dist/build/vite-plugin-product-contributions.js.map +7 -0
  10. package/dist/build/vite-plugin-shared-react.js +25 -0
  11. package/dist/build/vite-plugin-shared-react.js.map +7 -0
  12. package/dist/build/vite-plugin-shared-three.js +86 -0
  13. package/dist/build/vite-plugin-shared-three.js.map +7 -0
  14. package/dist/server/chunk-KMJC7AGO.js +98 -0
  15. package/dist/server/chunk-KMJC7AGO.js.map +7 -0
  16. package/dist/server/chunk-PSILUMGS.js +46 -0
  17. package/dist/server/chunk-PSILUMGS.js.map +7 -0
  18. package/dist/server/frame-proxy.js +255 -0
  19. package/dist/server/frame-proxy.js.map +7 -0
  20. package/dist/server/open-browser.js +53 -0
  21. package/dist/server/open-browser.js.map +7 -0
  22. package/dist/server/process-shutdown.js +127 -0
  23. package/dist/server/process-shutdown.js.map +7 -0
  24. package/dist/server/session-registry.js +141 -0
  25. package/dist/server/session-registry.js.map +7 -0
  26. package/dist/server/spawn-opener.js +11 -0
  27. package/dist/server/spawn-opener.js.map +7 -0
  28. package/dist/server/worktree-identity.js +11 -0
  29. package/dist/server/worktree-identity.js.map +7 -0
  30. package/dist-server/packaged.mjs +88816 -0
  31. package/package.json +118 -0
  32. package/scripts/build-plugins.mjs +14 -0
  33. package/scripts/build-server.mjs +26 -0
  34. package/scripts/build-session.mjs +9 -0
  35. package/server/account-credentials.ts +60 -0
  36. package/server/account-service.ts +1434 -0
  37. package/server/adapter-region-includes.ts +47 -0
  38. package/server/asset-catalog-v2.ts +175 -0
  39. package/server/asset-history-snapshots.ts +196 -0
  40. package/server/asset-ledger-store.ts +195 -0
  41. package/server/asset-library-routes.ts +1595 -0
  42. package/server/blender-wasm-artifact.ts +430 -0
  43. package/server/boot-timings.ts +106 -0
  44. package/server/canonical-path.ts +50 -0
  45. package/server/checkout-workspace-preflight.ts +349 -0
  46. package/server/cloud-asset-catalog.ts +158 -0
  47. package/server/coding-inference-launch.ts +237 -0
  48. package/server/collaboration-account-client.ts +202 -0
  49. package/server/collaboration-attribution.ts +122 -0
  50. package/server/collaboration-session.ts +934 -0
  51. package/server/comfyui-bridge.ts +248 -0
  52. package/server/console-ledger.ts +499 -0
  53. package/server/creation-site-transform.ts +304 -0
  54. package/server/creation-site-write.ts +648 -0
  55. package/server/data-file-serialize.ts +158 -0
  56. package/server/editor-brand-html.ts +72 -0
  57. package/server/editor-control-socket.ts +366 -0
  58. package/server/editor-server-options.ts +190 -0
  59. package/server/editor-server.ts +1483 -0
  60. package/server/editor-sse.ts +352 -0
  61. package/server/engine-provenance.ts +169 -0
  62. package/server/engine-source-restart.ts +117 -0
  63. package/server/frame-bridge.ts +115 -0
  64. package/server/frame-proxy.ts +490 -0
  65. package/server/frame-workbench.ts +259 -0
  66. package/server/frontend-handoff.ts +229 -0
  67. package/server/game-globals-shadow.ts +113 -0
  68. package/server/gameplay-session-retention.ts +149 -0
  69. package/server/gameplay-sessions.ts +123 -0
  70. package/server/generation-jobs.ts +201 -0
  71. package/server/generation-reconciler.ts +137 -0
  72. package/server/generative-execution-context.ts +59 -0
  73. package/server/git-workflow.ts +612 -0
  74. package/server/harness-chat-caller.ts +36 -0
  75. package/server/harness-chat-service.ts +1681 -0
  76. package/server/idle-shutdown.ts +170 -0
  77. package/server/js-profiling-policy.ts +52 -0
  78. package/server/launcher-settings.ts +84 -0
  79. package/server/local-asset-catalog.ts +410 -0
  80. package/server/managed-account-defaults.ts +22 -0
  81. package/server/mock-control-plane.ts +540 -0
  82. package/server/model-import-conversion.ts +345 -0
  83. package/server/native-credential-store.ts +162 -0
  84. package/server/open-browser.ts +118 -0
  85. package/server/packaged.ts +1192 -0
  86. package/server/play-stall.ts +126 -0
  87. package/server/process-shutdown.ts +218 -0
  88. package/server/product-presets.ts +55 -0
  89. package/server/project-build-artifact.ts +95 -0
  90. package/server/project-components.ts +100 -0
  91. package/server/project-dependency-invalidation.ts +114 -0
  92. package/server/project-file-scan.ts +235 -0
  93. package/server/project-hmr-files.ts +215 -0
  94. package/server/project-install-roots.ts +210 -0
  95. package/server/project-kinds.ts +71 -0
  96. package/server/project-mcp-servers.ts +82 -0
  97. package/server/project-module-freshness.ts +109 -0
  98. package/server/project-module-instance.ts +375 -0
  99. package/server/project-optimize-deps-entries.ts +827 -0
  100. package/server/project-output-writer.ts +679 -0
  101. package/server/project-package-origin.ts +63 -0
  102. package/server/project-root-surface.ts +831 -0
  103. package/server/project-scratch-path.ts +47 -0
  104. package/server/project-script-hmr.ts +619 -0
  105. package/server/project-serving-plugins.ts +236 -0
  106. package/server/project-tools.ts +653 -0
  107. package/server/project-validation.ts +250 -0
  108. package/server/project-verbs.ts +127 -0
  109. package/server/project-view.ts +52 -0
  110. package/server/project-watch.ts +1134 -0
  111. package/server/project-work-coordinator.ts +288 -0
  112. package/server/provider-credentials.ts +331 -0
  113. package/server/recent-projects-store.ts +98 -0
  114. package/server/redact-secrets.ts +71 -0
  115. package/server/repository-presence.ts +267 -0
  116. package/server/routes/account.ts +357 -0
  117. package/server/routes/agents.ts +60 -0
  118. package/server/routes/assets.ts +154 -0
  119. package/server/routes/build.ts +13 -0
  120. package/server/routes/collaboration.ts +595 -0
  121. package/server/routes/configurations.ts +419 -0
  122. package/server/routes/context.ts +264 -0
  123. package/server/routes/control-plane.ts +1811 -0
  124. package/server/routes/logs.ts +272 -0
  125. package/server/routes/project-identity.ts +302 -0
  126. package/server/routes/project-open.ts +429 -0
  127. package/server/routes/project-source.ts +686 -0
  128. package/server/routes/project-state.ts +383 -0
  129. package/server/routes/relay.ts +1029 -0
  130. package/server/routes/route-helpers.ts +29 -0
  131. package/server/routes/served-modules.ts +138 -0
  132. package/server/routes/session-tabs.ts +196 -0
  133. package/server/routes/settings.ts +121 -0
  134. package/server/routes/share-control.ts +486 -0
  135. package/server/routes/themes.ts +116 -0
  136. package/server/routes/tools.ts +230 -0
  137. package/server/routes/worktrees.ts +595 -0
  138. package/server/scoped-game-css.ts +461 -0
  139. package/server/server-utils.ts +1194 -0
  140. package/server/session-product.ts +112 -0
  141. package/server/session-registry.ts +230 -0
  142. package/server/share-claims.ts +138 -0
  143. package/server/share-host.ts +526 -0
  144. package/server/share-session-gateway.ts +1362 -0
  145. package/server/share-tunnel.ts +123 -0
  146. package/server/spawn-opener.ts +74 -0
  147. package/server/support/play/log-format.ts +76 -0
  148. package/server/support/play/session-record.ts +199 -0
  149. package/server/support/project/build-discipline.ts +718 -0
  150. package/server/support/project/inspection-node.ts +119 -0
  151. package/server/support/project/log-naming.ts +54 -0
  152. package/server/support/project/provenance.ts +290 -0
  153. package/server/support/project/run-name.ts +18 -0
  154. package/server/support/project/session-journal.ts +830 -0
  155. package/server/support/project/shared.ts +327 -0
  156. package/server/tab-bootstrap.ts +62 -0
  157. package/server/tab-heartbeat.ts +477 -0
  158. package/server/tab-lifecycle.ts +550 -0
  159. package/server/tab-presence.ts +1508 -0
  160. package/server/team-agent-mirror.ts +95 -0
  161. package/server/twin-auth.ts +138 -0
  162. package/server/vendored-lock-recorder.ts +968 -0
  163. package/server/worktree-identity.ts +121 -0
  164. package/server/worktree-management.ts +398 -0
  165. package/server/worktree-retention.d.mts +13 -0
  166. package/server/worktree-retention.mjs +205 -0
  167. package/src/EditorContext.tsx +169 -0
  168. package/src/account.ts +333 -0
  169. package/src/action-registry.ts +532 -0
  170. package/src/active-product.ts +74 -0
  171. package/src/active-project.ts +155 -0
  172. package/src/adapter-editor-config.ts +25 -0
  173. package/src/adapter-observation.ts +49 -0
  174. package/src/animation/stage-transport.ts +379 -0
  175. package/src/animation/three-clips-subject.ts +172 -0
  176. package/src/api/asset-library-wire.ts +45 -0
  177. package/src/api/assets.ts +365 -0
  178. package/src/api/base.ts +10 -0
  179. package/src/api/build.ts +99 -0
  180. package/src/api/git-wire.ts +56 -0
  181. package/src/api/logs.ts +92 -0
  182. package/src/api/project-identity.ts +74 -0
  183. package/src/api/project-open.ts +355 -0
  184. package/src/api/project-source.ts +162 -0
  185. package/src/api/project-state.ts +107 -0
  186. package/src/api/relay.ts +268 -0
  187. package/src/api/settings.ts +36 -0
  188. package/src/api/themes.ts +45 -0
  189. package/src/api/worktrees.ts +257 -0
  190. package/src/asset-compare-core.ts +171 -0
  191. package/src/asset-compare.ts +294 -0
  192. package/src/asset-editor-context.tsx +100 -0
  193. package/src/asset-events.ts +97 -0
  194. package/src/asset-inspector-actions.ts +87 -0
  195. package/src/asset-preview-framing.ts +357 -0
  196. package/src/asset-preview.ts +2802 -0
  197. package/src/asset-selection-viewer-registry.ts +113 -0
  198. package/src/asset-selection.ts +133 -0
  199. package/src/asset-workflow/asset-capabilities.ts +344 -0
  200. package/src/asset-workflow/asset-import-jobs.ts +106 -0
  201. package/src/asset-workflow/asset-ledger-backend.ts +126 -0
  202. package/src/asset-workflow/asset-ledger.ts +156 -0
  203. package/src/asset-workflow/asset-materialization-report.ts +140 -0
  204. package/src/asset-workflow/asset-pack-manifest.ts +320 -0
  205. package/src/asset-workflow/asset-types.ts +142 -0
  206. package/src/asset-workflow/asset-workflow-quality.ts +61 -0
  207. package/src/asset-workflow/audio-preview-player.ts +193 -0
  208. package/src/asset-workflow/audio-waveform.ts +22 -0
  209. package/src/asset-workflow/cloud-asset-client.ts +263 -0
  210. package/src/asset-workflow/folder-preview.ts +345 -0
  211. package/src/asset-workflow/hosted-asset-materialization.ts +236 -0
  212. package/src/asset-workflow/image-view-scale.ts +32 -0
  213. package/src/asset-workflow/import-contract.ts +124 -0
  214. package/src/asset-workflow/ledger-write-lock.ts +244 -0
  215. package/src/asset-workflow/model-inspection.ts +818 -0
  216. package/src/asset-workflow/pixi-spritesheet.ts +197 -0
  217. package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
  218. package/src/asset-workflow/project-asset-commands.ts +23 -0
  219. package/src/asset-workflow/project-asset-health.ts +139 -0
  220. package/src/asset-workflow/project-asset-operations.ts +154 -0
  221. package/src/asset-workflow/project-asset-roots.ts +68 -0
  222. package/src/asset-workflow/project-content.ts +288 -0
  223. package/src/asset-workflow/project-source-index.ts +550 -0
  224. package/src/asset-workflow/thumbnail-system.ts +256 -0
  225. package/src/authoring/active-adapter.ts +199 -0
  226. package/src/authoring/active-systems.ts +422 -0
  227. package/src/authoring/adapter-key.ts +18 -0
  228. package/src/authoring/authoring-asset-url.ts +27 -0
  229. package/src/authoring/bootstrap-state.ts +49 -0
  230. package/src/authoring/boundary-authoring-adapter.ts +184 -0
  231. package/src/authoring/breakpoint-state.ts +43 -0
  232. package/src/authoring/canvas-scene-guides.ts +76 -0
  233. package/src/authoring/component-instance-root.ts +171 -0
  234. package/src/authoring/composite-authoring-adapter.ts +2110 -0
  235. package/src/authoring/consumer-actions.ts +520 -0
  236. package/src/authoring/css-numeric-style.ts +97 -0
  237. package/src/authoring/design-time-layers.ts +849 -0
  238. package/src/authoring/design-time-mount-registry.ts +235 -0
  239. package/src/authoring/design-time-settle.ts +343 -0
  240. package/src/authoring/edit-mode-authoring.ts +619 -0
  241. package/src/authoring/eyedropper-session.ts +60 -0
  242. package/src/authoring/instance-source-menu-register.ts +13 -0
  243. package/src/authoring/instance-source-menu.ts +135 -0
  244. package/src/authoring/layered-pick.ts +183 -0
  245. package/src/authoring/live-gesture-lock.ts +50 -0
  246. package/src/authoring/live-object-transform.ts +62 -0
  247. package/src/authoring/mount-failure-report.ts +154 -0
  248. package/src/authoring/mounted-root-subjects.ts +144 -0
  249. package/src/authoring/no-authoring-adapter.ts +55 -0
  250. package/src/authoring/null-inspection-subjects.tsx +75 -0
  251. package/src/authoring/object3d-document-persistence.ts +106 -0
  252. package/src/authoring/object3d-document-session-registry.ts +120 -0
  253. package/src/authoring/object3d-document-session.ts +1326 -0
  254. package/src/authoring/object3d-gesture-controller.ts +113 -0
  255. package/src/authoring/panel-authoring.ts +121 -0
  256. package/src/authoring/prefab-instance-inspector-section.tsx +237 -0
  257. package/src/authoring/prefab-instance-section-model.ts +55 -0
  258. package/src/authoring/project-authoring-session.ts +105 -0
  259. package/src/authoring/provenance.ts +99 -0
  260. package/src/authoring/quarks-particle-systems.ts +19 -0
  261. package/src/authoring/react-canvas-navigation.ts +255 -0
  262. package/src/authoring/react-design-canvas-style.ts +20 -0
  263. package/src/authoring/react-story-board.ts +937 -0
  264. package/src/authoring/selection-scope.ts +195 -0
  265. package/src/authoring/shell-document-ops.ts +169 -0
  266. package/src/authoring/shell-object3d-document-write-policy.ts +108 -0
  267. package/src/authoring/shell-viewport-policy.ts +48 -0
  268. package/src/authoring/source-object3d-authoring-adapter.ts +526 -0
  269. package/src/authoring/stories-scope.ts +35 -0
  270. package/src/authoring/story-board-chrome-fit.ts +107 -0
  271. package/src/authoring/story-board-presentation.ts +111 -0
  272. package/src/authoring/three-projection-core.ts +226 -0
  273. package/src/authoring/viewport-pick-context.ts +40 -0
  274. package/src/authoring/viewport-raycast.ts +240 -0
  275. package/src/authoring/viewport-tool-context.ts +73 -0
  276. package/src/authoring/world-canvas-viewport-state.ts +35 -0
  277. package/src/authoring/world-hidden-viewport.ts +152 -0
  278. package/src/authoring/world-pan-state.ts +198 -0
  279. package/src/authoring/world-session-state.ts +101 -0
  280. package/src/authoring/write-pipe.ts +173 -0
  281. package/src/availability-tick.ts +66 -0
  282. package/src/bitmap-label.ts +120 -0
  283. package/src/blender-tab-metrics.ts +161 -0
  284. package/src/board-open-actions.ts +20 -0
  285. package/src/boot-routing.ts +391 -0
  286. package/src/build-identity.ts +15 -0
  287. package/src/bytes-codec.ts +62 -0
  288. package/src/camera-authoring.ts +175 -0
  289. package/src/cancellation-reason.ts +58 -0
  290. package/src/canvas-preview-frames.ts +482 -0
  291. package/src/capture-camera-pose.ts +77 -0
  292. package/src/chrome-registry.ts +159 -0
  293. package/src/chrome-slot-registry.ts +91 -0
  294. package/src/collaboration-client.ts +264 -0
  295. package/src/collaboration-presence.ts +41 -0
  296. package/src/command-dispatch.ts +19 -0
  297. package/src/command-listener.ts +2743 -0
  298. package/src/command-registry.ts +70 -0
  299. package/src/component-board-registry.ts +205 -0
  300. package/src/component-states-registry.ts +200 -0
  301. package/src/components/AgentPresentationNotice.tsx +75 -0
  302. package/src/components/AlignToolbar.tsx +204 -0
  303. package/src/components/AppRoot.tsx +394 -0
  304. package/src/components/ApplicationMenus.tsx +415 -0
  305. package/src/components/AssetBrowser.tsx +2843 -0
  306. package/src/components/AssetEditorShell.tsx +216 -0
  307. package/src/components/AssetInspectorToolSection.tsx +124 -0
  308. package/src/components/BoardRulers.tsx +354 -0
  309. package/src/components/CameraInfo.tsx +81 -0
  310. package/src/components/CanvasSceneViewport.tsx +556 -0
  311. package/src/components/CapabilityCoverageSection.tsx +185 -0
  312. package/src/components/CenterDocuments.tsx +22 -0
  313. package/src/components/ChromeSlot.tsx +20 -0
  314. package/src/components/CodeView.tsx +470 -0
  315. package/src/components/CollaborationHeaderControl.css +74 -0
  316. package/src/components/CompactInspectorCard.tsx +190 -0
  317. package/src/components/CompactInspectorShell.tsx +39 -0
  318. package/src/components/ConsolePanel.css +55 -0
  319. package/src/components/ConsolePanel.tsx +270 -0
  320. package/src/components/DefaultEditorLayout.tsx +106 -0
  321. package/src/components/DocumentHeaderStrip.tsx +96 -0
  322. package/src/components/DocumentShelfRail.tsx +45 -0
  323. package/src/components/DocumentThumbnail.tsx +345 -0
  324. package/src/components/EditorLeaseGuard.tsx +372 -0
  325. package/src/components/ErrorBanner.tsx +123 -0
  326. package/src/components/FolderPreviewTile.tsx +159 -0
  327. package/src/components/GameHierarchy.tsx +3842 -0
  328. package/src/components/GameplaySessionTimeline.tsx +295 -0
  329. package/src/components/InspectionProjection.tsx +968 -0
  330. package/src/components/Inspector.tsx +233 -0
  331. package/src/components/InspectorCanvasPreview.tsx +35 -0
  332. package/src/components/InspectorFieldsSection.tsx +290 -0
  333. package/src/components/InspectorObjectPreview.tsx +57 -0
  334. package/src/components/InspectorStoriesSection.tsx +92 -0
  335. package/src/components/InspectorToolSection.tsx +96 -0
  336. package/src/components/InspectorTransformSection.tsx +245 -0
  337. package/src/components/LightExplorerPanel.tsx +433 -0
  338. package/src/components/MediaProperties.tsx +145 -0
  339. package/src/components/NonThreeAuthoringBootstrap.tsx +19 -0
  340. package/src/components/Object3DDocumentToolbar.css +100 -0
  341. package/src/components/Object3DDocumentToolbar.tsx +454 -0
  342. package/src/components/Object3DDocumentViewport.tsx +58 -0
  343. package/src/components/ProjectHeader.css +617 -0
  344. package/src/components/ProjectHeader.tsx +328 -0
  345. package/src/components/ProjectLayout.tsx +249 -0
  346. package/src/components/ReactCanvasControls.tsx +284 -0
  347. package/src/components/RootSelectionOverlay.tsx +3162 -0
  348. package/src/components/RootTextEditor.tsx +79 -0
  349. package/src/components/SaveStatus.tsx +70 -0
  350. package/src/components/StageHost.tsx +1954 -0
  351. package/src/components/StageOverlays.tsx +21 -0
  352. package/src/components/StartupErrorScreen.tsx +76 -0
  353. package/src/components/StartupLoadingScreen.tsx +65 -0
  354. package/src/components/StatsOverlay.tsx +78 -0
  355. package/src/components/SurfaceStateOverlay.tsx +24 -0
  356. package/src/components/ToolContributionSurfaces.tsx +65 -0
  357. package/src/components/ToolHost.tsx +370 -0
  358. package/src/components/ToolObject3DPreview.tsx +39 -0
  359. package/src/components/Toolbar.tsx +595 -0
  360. package/src/components/TransientHint.tsx +44 -0
  361. package/src/components/TransportStrip.tsx +174 -0
  362. package/src/components/VersionControlSection.tsx +470 -0
  363. package/src/components/VgaiLogo.css +83 -0
  364. package/src/components/VgaiLogo.tsx +35 -0
  365. package/src/components/ViewportControlsHint.tsx +60 -0
  366. package/src/components/ViewportFurniture.tsx +401 -0
  367. package/src/components/ViewportOverlay.tsx +145 -0
  368. package/src/components/ViewportOverlaysMenu.tsx +153 -0
  369. package/src/components/ViewportShadingMenu.tsx +300 -0
  370. package/src/components/ViewportViewMenu.tsx +101 -0
  371. package/src/components/WorkspaceDocumentSurface.tsx +204 -0
  372. package/src/components/WorkspaceUtilitySurface.tsx +22 -0
  373. package/src/components/WorktreeSwitcher.css +239 -0
  374. package/src/components/WorktreeSwitcher.tsx +890 -0
  375. package/src/components/account-documents.tsx +1162 -0
  376. package/src/components/asset-documents.tsx +812 -0
  377. package/src/components/asset-editor-persistence.ts +216 -0
  378. package/src/components/asset-selection-section.tsx +535 -0
  379. package/src/components/asset-thumbnails.tsx +312 -0
  380. package/src/components/asset-viewers/AudioViewer.tsx +201 -0
  381. package/src/components/asset-viewers/EntityModelDocument.tsx +122 -0
  382. package/src/components/asset-viewers/EnvironmentAssetDocument.tsx +440 -0
  383. package/src/components/asset-viewers/GenericJsonViewer.tsx +102 -0
  384. package/src/components/asset-viewers/ImageViewer.tsx +300 -0
  385. package/src/components/asset-viewers/JsonAssetDocument.tsx +93 -0
  386. package/src/components/asset-viewers/LiveModuleDocument.tsx +417 -0
  387. package/src/components/asset-viewers/LutAssetDocument.tsx +444 -0
  388. package/src/components/asset-viewers/ModelAssetDocument.tsx +105 -0
  389. package/src/components/asset-viewers/Object3DPreview.tsx +359 -0
  390. package/src/components/asset-viewers/OnlineAssetDetail.tsx +421 -0
  391. package/src/components/asset-viewers/PasteboardModuleDocument.tsx +67 -0
  392. package/src/components/asset-viewers/QuarksAssetDocument.tsx +526 -0
  393. package/src/components/asset-viewers/ShaderAssetDocument.tsx +743 -0
  394. package/src/components/asset-viewers/SourceAssetViewer.tsx +281 -0
  395. package/src/components/asset-viewers/SpritesheetSpriteView.tsx +102 -0
  396. package/src/components/asset-viewers/VideoViewer.tsx +101 -0
  397. package/src/components/asset-viewers/shader-source.ts +144 -0
  398. package/src/components/asset-workflow.css +731 -0
  399. package/src/components/board-guides.ts +150 -0
  400. package/src/components/compact-inspector.css +539 -0
  401. package/src/components/core-utilities.tsx +90 -0
  402. package/src/components/editor-notifications.css +40 -0
  403. package/src/components/engine-workspace.css +274 -0
  404. package/src/components/inspector-preview-section.tsx +238 -0
  405. package/src/components/inspector-property-grouping.ts +64 -0
  406. package/src/components/inspector-revert-label.ts +20 -0
  407. package/src/components/inspector-selection.ts +42 -0
  408. package/src/components/inspector-stories-gating.ts +171 -0
  409. package/src/components/inspector-transform-subject.ts +11 -0
  410. package/src/components/inspector-transform.ts +75 -0
  411. package/src/components/kind-documents.tsx +500 -0
  412. package/src/components/palette-action-publisher.tsx +140 -0
  413. package/src/components/primitives/DraftColorInput.tsx +74 -0
  414. package/src/components/product-shell.css +1829 -0
  415. package/src/components/project-tool-documents.tsx +410 -0
  416. package/src/components/scene-documents.tsx +223 -0
  417. package/src/components/stage-keyboard.tsx +37 -0
  418. package/src/components/stage-overlay-set.tsx +107 -0
  419. package/src/components/stage-presence-markers.ts +478 -0
  420. package/src/components/standard-viewport-dressing.ts +417 -0
  421. package/src/components/status-contributions.tsx +403 -0
  422. package/src/components/tool-documents.tsx +309 -0
  423. package/src/components/tool-schema-form.tsx +262 -0
  424. package/src/components/use-after-paint.ts +41 -0
  425. package/src/components/use-project-image-assets.ts +86 -0
  426. package/src/components/viewport-header-controls.css +90 -0
  427. package/src/components/viewport-surface-status.tsx +55 -0
  428. package/src/components/workspace-history.ts +32 -0
  429. package/src/components/workspace-static-panel-registry.tsx +152 -0
  430. package/src/components/workspace-surfaces.css +621 -0
  431. package/src/components/world-documents.tsx +586 -0
  432. package/src/components/world-overlay-gestures.ts +1694 -0
  433. package/src/composite-screenshot.ts +1641 -0
  434. package/src/console-sync.ts +131 -0
  435. package/src/constraint-helper.ts +338 -0
  436. package/src/content-entry-source-registry.ts +184 -0
  437. package/src/coverage/authoring-seam-evidence.ts +300 -0
  438. package/src/coverage/canvas-reveal.ts +192 -0
  439. package/src/coverage/design-time-surfaces.ts +101 -0
  440. package/src/coverage/live-seam-evidence.ts +11 -0
  441. package/src/coverage/ontology-invariants.ts +466 -0
  442. package/src/coverage/session-vitals.ts +501 -0
  443. package/src/coverage/system-seam-evidence.ts +72 -0
  444. package/src/crash-null-boundary.ts +36 -0
  445. package/src/creation-site-edit.ts +1479 -0
  446. package/src/creation-site-registry.ts +160 -0
  447. package/src/delegate-harness-registry.ts +143 -0
  448. package/src/document-context-registry.ts +177 -0
  449. package/src/document-open-registry.ts +200 -0
  450. package/src/document-preview-source.ts +20 -0
  451. package/src/document-renderer-session.ts +138 -0
  452. package/src/editor-api.ts +46 -0
  453. package/src/editor-chrome-capture.ts +139 -0
  454. package/src/editor-commands.ts +164 -0
  455. package/src/editor-console.ts +563 -0
  456. package/src/editor-current-view.ts +82 -0
  457. package/src/editor-document-probe.ts +881 -0
  458. package/src/editor-git-client.ts +115 -0
  459. package/src/editor-host-door.ts +462 -0
  460. package/src/editor-hotkeys.ts +842 -0
  461. package/src/editor-lease-view.ts +39 -0
  462. package/src/editor-lease.ts +415 -0
  463. package/src/editor-mode.ts +19 -0
  464. package/src/editor-notifications.ts +140 -0
  465. package/src/editor-presence.ts +563 -0
  466. package/src/editor-presentation-activity.ts +50 -0
  467. package/src/editor-presentation-notice.ts +42 -0
  468. package/src/editor-runtime.tsx +145 -0
  469. package/src/editor-server-response.ts +86 -0
  470. package/src/editor-session-attribution.ts +75 -0
  471. package/src/editor-session-mode.ts +54 -0
  472. package/src/editor-shell-store.ts +1304 -0
  473. package/src/editor-state-facets.ts +74 -0
  474. package/src/editor-styles.css +33 -0
  475. package/src/editor-view-presentation.ts +810 -0
  476. package/src/editor-viewport.ts +5302 -0
  477. package/src/entity-lod.ts +31 -0
  478. package/src/entity-object.ts +91 -0
  479. package/src/files/file-provider.ts +62 -0
  480. package/src/files/project-files.ts +264 -0
  481. package/src/finders/index.ts +136 -0
  482. package/src/finders/scenes-from-entrypoint-selection.ts +387 -0
  483. package/src/frame/bridge.tsx +1408 -0
  484. package/src/frame/product.ts +77 -0
  485. package/src/gameplay-dom-recording.ts +319 -0
  486. package/src/gameplay-export-state.ts +14 -0
  487. package/src/gameplay-replay.ts +417 -0
  488. package/src/gameplay-session-time.ts +9 -0
  489. package/src/gameplay-sessions.ts +204 -0
  490. package/src/harness-chat-types.ts +253 -0
  491. package/src/hierarchy-component-marks.ts +298 -0
  492. package/src/hierarchy-drop.ts +91 -0
  493. package/src/hierarchy-expansion-state.ts +80 -0
  494. package/src/hierarchy-header-slot.ts +52 -0
  495. package/src/hierarchy-internals.ts +197 -0
  496. package/src/hierarchy-kind-icon.ts +217 -0
  497. package/src/hierarchy-mark-reader.ts +73 -0
  498. package/src/hierarchy-menu-registry.ts +67 -0
  499. package/src/hierarchy-node-rows.ts +307 -0
  500. package/src/hierarchy-panel-view.ts +280 -0
  501. package/src/hierarchy-projection.ts +76 -0
  502. package/src/hierarchy-row-cache.ts +243 -0
  503. package/src/hierarchy-row-model.ts +308 -0
  504. package/src/hierarchy-rows.ts +11 -0
  505. package/src/hierarchy-walk.ts +86 -0
  506. package/src/history/editor-session.ts +25 -0
  507. package/src/history/history-commands.ts +147 -0
  508. package/src/history/history-delegate.ts +163 -0
  509. package/src/history/history-limit-notices.ts +43 -0
  510. package/src/history/history-service.ts +1173 -0
  511. package/src/history/persistence-coordinator.ts +35 -0
  512. package/src/history/project-file-history.ts +386 -0
  513. package/src/history/project-root-history-backends.ts +139 -0
  514. package/src/history/resource-registry.ts +209 -0
  515. package/src/history/snapshot-store.ts +103 -0
  516. package/src/history/source-history-backend.ts +546 -0
  517. package/src/history/types.ts +124 -0
  518. package/src/hmr-registration-group.ts +67 -0
  519. package/src/hmr-stable-react-context.ts +23 -0
  520. package/src/hotkeys.ts +188 -0
  521. package/src/inference-diagnostics.ts +69 -0
  522. package/src/initial-project.ts +80 -0
  523. package/src/inspection/active-subject.ts +578 -0
  524. package/src/inspection/active-surface.ts +142 -0
  525. package/src/inspection/compose.ts +1064 -0
  526. package/src/inspection/display.ts +170 -0
  527. package/src/inspection/document-subject.ts +109 -0
  528. package/src/inspection/game-subject.ts +85 -0
  529. package/src/inspection/model.ts +542 -0
  530. package/src/inspection/null-subject.ts +115 -0
  531. package/src/inspection/serialize.ts +357 -0
  532. package/src/inspection/use-active-inspection.ts +180 -0
  533. package/src/inspector-presentation.ts +201 -0
  534. package/src/inspector-section-registry.ts +221 -0
  535. package/src/instance-source-actions.ts +163 -0
  536. package/src/instanced-presentation.ts +164 -0
  537. package/src/js-heap.ts +71 -0
  538. package/src/key-actions.ts +91 -0
  539. package/src/keymap-presets.ts +401 -0
  540. package/src/layout-policy.ts +31 -0
  541. package/src/learn-links.ts +73 -0
  542. package/src/light-explorer-model.ts +134 -0
  543. package/src/live-canvas-frame.ts +55 -0
  544. package/src/live-document.ts +279 -0
  545. package/src/live-module-source.ts +230 -0
  546. package/src/live-session-registry.ts +220 -0
  547. package/src/live-transition.ts +633 -0
  548. package/src/manifest-project.ts +107 -0
  549. package/src/model-thumbnail.ts +523 -0
  550. package/src/native-selection-style.ts +202 -0
  551. package/src/object3d-document-write-policy.ts +137 -0
  552. package/src/packaged-runtime.ts +108 -0
  553. package/src/palettes/maya.palette.json +57 -0
  554. package/src/palettes/substance.palette.json +57 -0
  555. package/src/pasteboard-module.ts +81 -0
  556. package/src/performance-profiler.ts +367 -0
  557. package/src/performance-sources.ts +69 -0
  558. package/src/play-boot-phase.ts +145 -0
  559. package/src/presentation-surface.ts +248 -0
  560. package/src/project-adapter.ts +1143 -0
  561. package/src/project-asset-refresh.ts +26 -0
  562. package/src/project-declaration-refresh.ts +31 -0
  563. package/src/project-local-state.ts +118 -0
  564. package/src/project-manager.ts +243 -0
  565. package/src/project-module-changes.ts +163 -0
  566. package/src/project-module-split.ts +266 -0
  567. package/src/project-provenance.ts +115 -0
  568. package/src/project-ready.ts +42 -0
  569. package/src/project-session-reset.ts +67 -0
  570. package/src/project-shape.ts +68 -0
  571. package/src/project-tool-discovery.ts +66 -0
  572. package/src/project-tools.ts +107 -0
  573. package/src/project-work-types.ts +149 -0
  574. package/src/projection/three.ts +898 -0
  575. package/src/projection/types.ts +44 -0
  576. package/src/readiness.ts +113 -0
  577. package/src/reflection-probe-helper.ts +142 -0
  578. package/src/reported-play-state.ts +90 -0
  579. package/src/resolve-relative-specifier.ts +33 -0
  580. package/src/scene-document-plan.ts +315 -0
  581. package/src/scene-framing.ts +315 -0
  582. package/src/scene-live-open.ts +210 -0
  583. package/src/scene-view-fog.ts +89 -0
  584. package/src/scoped-game-css.ts +152 -0
  585. package/src/session-orphan-record.ts +193 -0
  586. package/src/session-tombstone.ts +126 -0
  587. package/src/settings/settings-provider.ts +82 -0
  588. package/src/settings-store.ts +345 -0
  589. package/src/shared-view-restore.ts +42 -0
  590. package/src/shell-store-door.ts +45 -0
  591. package/src/source-conflict.ts +122 -0
  592. package/src/spatial-handle-visuals.ts +332 -0
  593. package/src/stage-context.ts +341 -0
  594. package/src/stage-store-registry.ts +63 -0
  595. package/src/stale-chunk-recovery.ts +34 -0
  596. package/src/state-report-deferral.ts +73 -0
  597. package/src/storage/host-files-storage.ts +97 -0
  598. package/src/storage/http-storage.ts +174 -0
  599. package/src/storage/index.ts +75 -0
  600. package/src/storage/mem-storage.ts +149 -0
  601. package/src/storage/path-lock.ts +44 -0
  602. package/src/storage/paths.ts +26 -0
  603. package/src/storage/types.ts +116 -0
  604. package/src/stories/StoryComponentThumbnail.tsx +184 -0
  605. package/src/stories/StoryPreviewMount.tsx +306 -0
  606. package/src/stories/component-content-source.tsx +106 -0
  607. package/src/stories/component-states-source.ts +78 -0
  608. package/src/stories/compose-project-stories.ts +255 -0
  609. package/src/stories/pixi-story-model.ts +30 -0
  610. package/src/stories/prefabs-finder.ts +54 -0
  611. package/src/stories/prefabs-from-stories.ts +182 -0
  612. package/src/stories/project-story-discovery.ts +78 -0
  613. package/src/stories/project-story-regions.ts +24 -0
  614. package/src/stories/story-actions.ts +54 -0
  615. package/src/stories/story-capture-command.ts +124 -0
  616. package/src/stories/story-capture.ts +584 -0
  617. package/src/stories/story-declared-medium.ts +126 -0
  618. package/src/stories/story-discovery.ts +176 -0
  619. package/src/stories/story-document-openers.ts +36 -0
  620. package/src/stories/story-dom-runtime.ts +78 -0
  621. package/src/stories/story-grouping.ts +111 -0
  622. package/src/stories/story-lane.ts +88 -0
  623. package/src/stories/story-mount-turn.ts +27 -0
  624. package/src/stories/story-opener.ts +260 -0
  625. package/src/stories/story-pixi-preview.ts +408 -0
  626. package/src/stories/story-presentation.ts +215 -0
  627. package/src/stories/story-registry.ts +517 -0
  628. package/src/stories/story-three-preview.ts +807 -0
  629. package/src/stories/three-story-model.ts +96 -0
  630. package/src/story-three-preview-runtime.ts +56 -0
  631. package/src/surface-keyboard.ts +112 -0
  632. package/src/surface-state.ts +124 -0
  633. package/src/tab-bootstrap.js +628 -0
  634. package/src/tab-census.ts +198 -0
  635. package/src/tab-lifecycle-client.ts +196 -0
  636. package/src/theme-library.ts +844 -0
  637. package/src/theme-preference.ts +378 -0
  638. package/src/theme.css +3437 -0
  639. package/src/three-viewport/bone-selection-highlight.ts +119 -0
  640. package/src/three-viewport/camera-fit.ts +41 -0
  641. package/src/three-viewport/interactive-renderer.ts +132 -0
  642. package/src/three-viewport/selection-brackets.ts +310 -0
  643. package/src/three-viewport/selection-outline.ts +183 -0
  644. package/src/three-viewport/skeleton-helper.ts +61 -0
  645. package/src/three-viewport/source-color.ts +167 -0
  646. package/src/three-viewport/studio-environment.ts +96 -0
  647. package/src/three-viewport-presentation.ts +23 -0
  648. package/src/tool-contribution-play.ts +74 -0
  649. package/src/tool-loader.ts +1798 -0
  650. package/src/transform-mode-request.ts +57 -0
  651. package/src/transient-hint.ts +78 -0
  652. package/src/trigger-volume-helper.ts +116 -0
  653. package/src/ui-source/adapter-region-includes.ts +241 -0
  654. package/src/ui-source/ensure-import.ts +132 -0
  655. package/src/ui-source/file-region-resolver.ts +302 -0
  656. package/src/ui-source/inspect.ts +775 -0
  657. package/src/ui-source/oid-transform.ts +1845 -0
  658. package/src/ui-source/plan-csf-story.ts +196 -0
  659. package/src/ui-source/plan-extract-component.ts +421 -0
  660. package/src/ui-source/plan-fork-component.ts +621 -0
  661. package/src/ui-source/plan-named-style.ts +95 -0
  662. package/src/ui-source/plan-source-edit.ts +353 -0
  663. package/src/ui-source/r3f-contract-resolver.ts +327 -0
  664. package/src/ui-source/r3f-diagnostic-index.ts +165 -0
  665. package/src/ui-source/r3f-environment-binding.ts +133 -0
  666. package/src/ui-source/r3f-joint-binding.ts +251 -0
  667. package/src/ui-source/r3f-lod-binding.ts +91 -0
  668. package/src/ui-source/r3f-particle-binding.ts +235 -0
  669. package/src/ui-source/r3f-physics-binding.ts +483 -0
  670. package/src/ui-source/r3f-project-contracts.ts +444 -0
  671. package/src/ui-source/relative-import-specifier.ts +34 -0
  672. package/src/ui-source/reparent-guard.ts +544 -0
  673. package/src/ui-source/source-edit-request.ts +97 -0
  674. package/src/ui-source/source-write-backend.ts +618 -0
  675. package/src/ui-source/syntactic-prop-specs.ts +296 -0
  676. package/src/ui-source/tier-source-write-backend.ts +279 -0
  677. package/src/ui-source/ts-ast.ts +171 -0
  678. package/src/ui-source/utility-class-support.ts +169 -0
  679. package/src/ui-source/write-component-default.ts +133 -0
  680. package/src/ui-source/writer.ts +1989 -0
  681. package/src/viewport-activation-timings.ts +840 -0
  682. package/src/viewport-authoring-policy.ts +186 -0
  683. package/src/viewport-controls-hint.ts +57 -0
  684. package/src/viewport-door.ts +237 -0
  685. package/src/viewport-shading-boundary.ts +12 -0
  686. package/src/vite-error-surface.ts +141 -0
  687. package/src/wait-until.ts +37 -0
  688. package/src/workspace-areas.ts +156 -0
  689. package/src/workspace-aux-commands.ts +11 -0
  690. package/src/workspace-available-documents.ts +127 -0
  691. package/src/workspace-core-utilities.ts +31 -0
  692. package/src/workspace-document-ids.ts +59 -0
  693. package/src/workspace-document-registry.ts +612 -0
  694. package/src/workspace-document-restore.ts +145 -0
  695. package/src/workspace-host-commands.ts +141 -0
  696. package/src/workspace-persistence-gate.ts +40 -0
  697. package/src/workspace-play-utilities.ts +44 -0
  698. package/src/workspace-presets.ts +425 -0
  699. package/src/workspace-regions.ts +294 -0
  700. package/src/workspace-state-persistence.ts +536 -0
  701. package/src/workspace-static-panels.ts +73 -0
  702. package/src/workspace-status-registry.ts +121 -0
  703. package/src/workspace-style.ts +239 -0
  704. package/src/workspace-utility-commands.ts +74 -0
  705. package/src/workspace-utility-registry.ts +263 -0
  706. package/src/workspace-viewport-rect.ts +97 -0
  707. package/src/world-adoption.ts +115 -0
  708. package/src/world-document-routing.ts +104 -0
  709. package/vite-plugin-creation-site-write.ts +184 -0
  710. package/vite-plugin-creation-site.ts +80 -0
  711. package/vite-plugin-game-static.ts +303 -0
  712. package/vite-plugin-module-doorways.ts +336 -0
  713. package/vite-plugin-product-contributions.ts +197 -0
  714. package/vite-plugin-project-game-static.ts +125 -0
  715. package/vite-plugin-project-jsx-js.ts +65 -0
  716. package/vite-plugin-project-root-absolute-assets.ts +359 -0
  717. package/vite-plugin-shared-react.ts +420 -0
  718. package/vite-plugin-shared-three.ts +203 -0
  719. package/vite-plugin-ui-oid.ts +2106 -0
@@ -0,0 +1,1029 @@
1
+ /**
2
+ * The command relay's HTTP surface: `/__editor/command`,
3
+ * `/__editor/command-result`, `/__editor/command-received`,
4
+ * `/__editor/command-listener`, `/__editor/page-error`,
5
+ * `/__editor/play-phase`, `/__editor/console-entries`, `/__editor/console`,
6
+ * `/__editor/console/ack`, and the `/__editor/recording/*` verbs.
7
+ *
8
+ * Every one of these is a THIN door onto `routes/control-plane.ts`: the POSTs
9
+ * a tab makes land on exactly the handlers its duplex control socket's frames
10
+ * land on, so the two transports cannot answer differently. Nothing here
11
+ * decides anything — if a rule seems to be missing from this file, it is in
12
+ * the control plane, which is the point.
13
+ */
14
+
15
+ import { randomUUID } from 'node:crypto';
16
+ import { createReadStream, existsSync } from 'node:fs';
17
+ import { appendFile, mkdir, readFile, rm, stat, unlink, writeFile } from 'node:fs/promises';
18
+ import { tmpdir } from 'node:os';
19
+ import { basename, dirname, extname, join, resolve } from 'node:path';
20
+ import { isContainedRelativePath } from '@volter/editor-sdk/session/relative-path-guard';
21
+ import type { NextFunction, Request, Response } from 'express';
22
+ import { projectOutputRootOf } from '@volter/editor-sdk/project/output-roots';
23
+ import { PROJECT_SESSION_WRITE_OPERATION } from '../support/project/provenance';
24
+ import {
25
+ BLENDER_WALI_ARTIFACT,
26
+ BLENDER_WASM_FILES,
27
+ blenderGlueText,
28
+ blenderWasmOnDisk,
29
+ blenderWasmReadStream,
30
+ blenderWasmStatus,
31
+ runtimeIndex,
32
+ substrateModule,
33
+ writeRuntimeBlob,
34
+ } from '../blender-wasm-artifact';
35
+ import { type EditorServerRouter, playRunSlug } from '../editor-server';
36
+ import { pruneGameplaySessionClips } from '../gameplay-session-retention';
37
+ import { isProjectOwnedRelativePath, projectFileIndex } from '../project-file-scan';
38
+ import { createProjectOutputWriter, projectOutputMediaType } from '../project-output-writer';
39
+ import {
40
+ allowCrossOriginFrameEmbedding,
41
+ commandResponseFor,
42
+ isCanonicalPathInside,
43
+ } from '../server-utils';
44
+ import type { RouteContext } from './context';
45
+ import type { ControlPlane } from './control-plane';
46
+
47
+ // ---- File bodies, which are NOT commands.
48
+ //
49
+ // A FILE IS A BODY, NOT A JSON FIELD. `blender-read-file` used to answer
50
+ // with the whole file base64'd inside a control-plane message, so a mesh the
51
+ // twin had written came back as a single JSON frame on the command socket:
52
+ // +33% for the encoding, a string the page built one 32 KB `fromCharCode`
53
+ // chunk at a time on the main thread, and a 60 s COMMAND budget applied to
54
+ // what is really a transfer.
55
+ //
56
+ // MEASURED 2026-09-15: `02-hinged-vise` writes a 31 MB
57
+ // `raw-FINAL-HandleKnobA.json`, which became ~41.5 MB of base64 in one
58
+ // message -- within 20% of the server's 50 MB JSON body cap, and the only
59
+ // file in the battery whose read killed the socket (1006). Nothing about
60
+ // that is specific to the parity battery; an exported GLB or a baked texture
61
+ // would do the same.
62
+ //
63
+ // So bytes move as bytes, the way `/__editor/recording/chunk` above already
64
+ // moves video. The page POSTs an octet-stream; the requester GETs it once
65
+ // and it is gone. The command result carries only a length.
66
+ //
67
+ // The spool deliberately keeps PATH AUTHORITY WITH THE REQUESTER: the page
68
+ // never names a destination on disk, it only hands over bytes under an id
69
+ // the caller minted. Adding a page-driven write-anywhere route to make one
70
+ // harness faster would be a far worse trade than the copy this costs.
71
+ function registerBlenderFileRoutes(router: EditorServerRouter, ctx: RouteContext): void {
72
+ const transferRoot = join(tmpdir(), 'vgai-blender-transfer');
73
+ const transferPath = (id: string): string => join(transferRoot, id);
74
+
75
+ // A spool is deleted by the GET that reads it, so the only thing that can
76
+ // accumulate is a transfer nobody ever collected -- a command that failed
77
+ // between the page's POST and the caller's GET. Clearing the whole directory
78
+ // once at startup bounds that exactly, with no age to guess at: every id is
79
+ // a uuid minted by a caller in a PREVIOUS process, so nothing live can be in
80
+ // here yet. A TTL sweep would be a number nothing measured.
81
+ void rm(transferRoot, { recursive: true, force: true });
82
+
83
+ const transferId = (value: unknown): string | null =>
84
+ typeof value === 'string' && /^[0-9a-f-]{36}$/.test(value) ? value : null;
85
+
86
+ // ---- The engine itself: headless Blender, compiled to WebAssembly.
87
+ //
88
+ // `blender-wasm-artifact.ts` owns where it is and the one patch its glue
89
+ // takes; this is the door. The runtime spawns its pthreads by loading
90
+ // `blender_browser.js` from this same URL as a classic worker, so the
91
+ // patched text has to be what the URL answers with -- not something the
92
+ // worker rewrites for itself.
93
+ router.get('/__editor/blender-wasm/status', async (_req: Request, res: Response) => {
94
+ allowCrossOriginFrameEmbedding(res);
95
+ res.json(await blenderWasmStatus());
96
+ });
97
+
98
+ // THE SUBSTRATE SKEW'S OWN THREE DOORS, beside the standalone bundle's.
99
+ //
100
+ // `blender.wasm` goes out through the same `:file` route below. These are
101
+ // what that skew needs and the other does not: Blender's runtime tree as one
102
+ // index and one blob (a request per file would be ~2,300 round trips), and
103
+ // the substrate's browser ESM with its bare specifiers rewritten -- which is
104
+ // the ONLY way `@volter/*` reaches the tab, and therefore the reason
105
+ // `@volter/editor-blender` can declare no dependency on browser-substrate at all.
106
+ router.get('/__editor/blender-wasm/runtime.idx', async (_req: Request, res: Response) => {
107
+ const status = await blenderWasmStatus();
108
+ if (status.skew !== 'wali' || status.dir === null) {
109
+ res.status(404).json({ error: 'This editor does not serve the substrate skew of Blender.' });
110
+ return;
111
+ }
112
+ res.json(await runtimeIndex(status.dir));
113
+ });
114
+
115
+ router.get('/__editor/blender-wasm/runtime.bin', async (_req: Request, res: Response) => {
116
+ const status = await blenderWasmStatus();
117
+ if (status.skew !== 'wali' || status.dir === null) {
118
+ res.status(404).json({ error: 'This editor does not serve the substrate skew of Blender.' });
119
+ return;
120
+ }
121
+ res.type('application/octet-stream');
122
+ await writeRuntimeBlob(status.dir, (chunk) => void res.write(chunk));
123
+ res.end();
124
+ });
125
+
126
+ router.get('/__editor/blender-wasm/wali/:segment/:file', async (req: Request, res: Response) => {
127
+ const served = await substrateModule(String(req.params['segment']), String(req.params['file']));
128
+ if (!served) {
129
+ res.status(404).json({
130
+ error:
131
+ `This editor does not serve ${req.params['segment']}/${req.params['file']}. ` +
132
+ 'The substrate skew runs Blender through @volter/browser-wali and ' +
133
+ '@volter/browser-runtime; a project that opts into it declares them.',
134
+ });
135
+ return;
136
+ }
137
+ res.type('text/javascript').send(served.text);
138
+ });
139
+
140
+ router.get('/__editor/blender-wasm/:file', async (req: Request, res: Response) => {
141
+ const file = String(req.params['file']);
142
+ // The frame's page is cross-origin-isolated and this session is not its
143
+ // origin, so every byte of the engine has to say it may be embedded — the
144
+ // glue above all, which Blender's pthreads load with `importScripts`
145
+ // (`server-utils.ts`'s allowCrossOriginFrameEmbedding says what that cost
146
+ // before it was measured).
147
+ allowCrossOriginFrameEmbedding(res);
148
+ const bundled = (BLENDER_WASM_FILES as readonly string[]).includes(file);
149
+ if (!bundled && file !== BLENDER_WALI_ARTIFACT) {
150
+ res.status(404).json({ error: `The Blender build has no ${String(file)}.` });
151
+ return;
152
+ }
153
+ const status = await blenderWasmStatus();
154
+ if (!status.available) {
155
+ res.status(404).json({
156
+ error:
157
+ 'The headless Blender WebAssembly build is not served by this editor: ' +
158
+ status.missing.join('; '),
159
+ });
160
+ return;
161
+ }
162
+ if (file === 'blender_browser.js') {
163
+ res.type('text/javascript').send(await blenderGlueText(status.dir!));
164
+ return;
165
+ }
166
+ const found = await blenderWasmOnDisk(status.dir!, file);
167
+ if (!found) {
168
+ res.status(404).json({ error: `${file} vanished from ${status.dir}.` });
169
+ return;
170
+ }
171
+ // A pre-compressed file goes out as stored, declared as brotli: the
172
+ // browser inflates it before `instantiateStreaming` / the `.data` preload
173
+ // read a byte, so `content-type` stays what the runtime requires.
174
+ res.type(file.endsWith('.wasm') ? 'application/wasm' : 'application/octet-stream');
175
+ if (found.encoding) res.setHeader('content-encoding', found.encoding);
176
+ res.setHeader('content-length', String(found.size));
177
+ blenderWasmReadStream(found).pipe(res);
178
+ });
179
+
180
+ router.post('/__editor/blender-file', async (req: Request, res: Response) => {
181
+ const id = transferId(req.query['id']);
182
+ if (id === null) {
183
+ res.status(400).json({ error: 'blender-file requires a uuid `id`.' });
184
+ return;
185
+ }
186
+ const path = transferPath(id);
187
+ await mkdir(dirname(path), { recursive: true });
188
+ const chunks: Buffer[] = [];
189
+ let bytes = 0;
190
+ for await (const value of req) {
191
+ const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
192
+ bytes += chunk.byteLength;
193
+ chunks.push(chunk);
194
+ }
195
+ await writeFile(path, new Uint8Array(Buffer.concat(chunks)));
196
+ res.json({ bytes });
197
+ });
198
+
199
+ router.get('/__editor/blender-file', async (req: Request, res: Response) => {
200
+ const id = transferId(req.query['id']);
201
+ if (id === null) {
202
+ res.status(400).json({ error: 'blender-file requires a uuid `id`.' });
203
+ return;
204
+ }
205
+ const path = transferPath(id);
206
+ const info = await stat(path).catch(() => null);
207
+ if (info === null) {
208
+ res.status(404).json({ error: 'No transfer is spooled under that id.' });
209
+ return;
210
+ }
211
+ res.setHeader('content-type', 'application/octet-stream');
212
+ res.setHeader('content-length', String(info.size));
213
+ const stream = createReadStream(path);
214
+ stream.pipe(res);
215
+ // One reader, then it is gone -- a spool that outlives its GET is a
216
+ // tmpdir leak proportional to everything Blender ever wrote.
217
+ stream.on('close', () => {
218
+ void unlink(path).catch(() => {});
219
+ });
220
+ });
221
+
222
+ // ---- The project's own files, read INTO the session.
223
+ //
224
+ // The direction `blender-file` above does not cover: the session's Python
225
+ // opening the project's own files. Scope is the project root and nothing
226
+ // else, through the estate `collaborationSourceSnapshot` already publishes
227
+ // (`isProjectOwnedRelativePath`). The client names a project-RELATIVE path
228
+ // and the server joins it to its own root -- no host path on the wire -- and
229
+ // the join is canonicalized before any byte is read.
230
+ router.get('/__editor/blender-project-index', async (_req: Request, res: Response) => {
231
+ if (ctx.projectRoot === ctx.engineRoot) {
232
+ res.status(404).json({ error: 'No project open.' });
233
+ return;
234
+ }
235
+ res.json({ root: ctx.projectRoot, files: await projectFileIndex(ctx.projectRoot) });
236
+ });
237
+
238
+ router.get('/__editor/blender-project-file', async (req: Request, res: Response) => {
239
+ const relPath = (req.query['path'] as string) ?? '';
240
+ // Saved scratch documents can be opened explicitly without indexing logs,
241
+ // recordings, credentials or the rest of the editor's private directory.
242
+ const scratchDocument =
243
+ isContainedRelativePath(relPath) &&
244
+ relPath.startsWith('.vgai/tmp/') &&
245
+ relPath.endsWith('.blend');
246
+ if (!isProjectOwnedRelativePath(relPath) && !scratchDocument) {
247
+ res.status(400).json({ error: 'Not a project-owned path.' });
248
+ return;
249
+ }
250
+ if (ctx.projectRoot === ctx.engineRoot) {
251
+ res.status(404).json({ error: 'No project open.' });
252
+ return;
253
+ }
254
+ const absolute = join(ctx.projectRoot, relPath);
255
+ if (!(await isCanonicalPathInside(ctx.projectRoot, absolute))) {
256
+ res.status(403).json({ error: 'That path leaves the project root.' });
257
+ return;
258
+ }
259
+ try {
260
+ res.type('application/octet-stream').send(await readFile(absolute));
261
+ } catch {
262
+ res.status(404).json({ error: 'Not found.' });
263
+ }
264
+ });
265
+
266
+ // ---- The session's SHIPPED OUTPUTS, written back OUT into the project
267
+ // WITH a provenance record.
268
+ //
269
+ // The third direction, and the one that must record. A Blender script's
270
+ // `export_scene.gltf` to `<project>/public/models/lantern.glb` is a
271
+ // generated binary the project SHIPS, and a shipped binary with no record in
272
+ // `.vgai/provenance.json` is the one state
273
+ // `scripts/validate-project-provenance.mjs` calls fatal.
274
+ //
275
+ // `vgai blender-mcp` used to commit those bytes itself, by importing
276
+ // `server/project-output-writer.ts` straight out of this package. That is a
277
+ // LICENCE break before it is anything else — the CLI is Apache-2.0 and this
278
+ // server is AGPL-3.0-only, and licence follows the import graph — and it put
279
+ // a second PROCESS on a ledger whose write lock had to grow a cross-process
280
+ // half to survive it. The session face is the one door the CLI rides, so it
281
+ // rides it here: the bytes arrive as a body, the writer runs in the process
282
+ // that owns the project, and there is exactly one writer.
283
+ //
284
+ // The destination shape is pinned BY THIS SERVER, like `blender-document`
285
+ // below and unlike the `blender-file` spool above: a project-relative path
286
+ // under `public/`, no traversal, joined to the server's own root. The SESSION
287
+ // facts are the caller's to state, because only it watched the frames — and
288
+ // an unnamed session is REFUSED rather than invented
289
+ // (`ProjectProvenanceSessionSchema`: a kind that can be claimed without
290
+ // evidence is the cheapest way past the gate).
291
+ router.post('/__editor/blender-output', async (req: Request, res: Response) => {
292
+ const path = (req.query['path'] as string) ?? '';
293
+ const session = (req.query['session'] as string) ?? '';
294
+ const source = (req.query['source'] as string) ?? '';
295
+ const revision = Number(req.query['revision']);
296
+ const callId = (req.query['callId'] as string) ?? '';
297
+ if (!isContainedRelativePath(path) || projectOutputRootOf(path) !== 'public') {
298
+ res.status(400).json({
299
+ error:
300
+ 'A shipped output is a project-relative path under public/ with no traversal ' +
301
+ `(public/models/model.glb); got ${JSON.stringify(path)}.`,
302
+ });
303
+ return;
304
+ }
305
+ if (session === '' || !Number.isInteger(revision) || revision < 0) {
306
+ res.status(400).json({
307
+ error:
308
+ 'A shipped output must name the session that produced it — `session` and a ' +
309
+ 'non-negative integer `revision` — so the record can say what re-derives the file.',
310
+ });
311
+ return;
312
+ }
313
+ if (source === '' || source.length > 64) {
314
+ res
315
+ .status(400)
316
+ .json({ error: '`source` names the transport that wrote the file, in 1-64 characters.' });
317
+ return;
318
+ }
319
+ // WHAT THE BYTES WERE MADE FROM — project FILES, validated the same way
320
+ // the destination is. A session-written `.glb` names the
321
+ // `src/models/<name>.blend` it was exported from, which is what lets a
322
+ // reader drill DOWN by kind from a prefab's glTF to its model
323
+ // (ARCHITECTURE-CORE §Roots). Repeatable; each value is a
324
+ // project-relative path with no traversal, and unlike the destination it
325
+ // is NOT restricted to `public/` — a source lives in `src/`.
326
+ const inputs = (
327
+ Array.isArray(req.query['inputs'])
328
+ ? (req.query['inputs'] as unknown[])
329
+ : req.query['inputs'] === undefined
330
+ ? []
331
+ : [req.query['inputs']]
332
+ ).map((value) => String(value));
333
+ const badInput = inputs.find((value) => !isContainedRelativePath(value));
334
+ if (badInput !== undefined) {
335
+ res.status(400).json({
336
+ error:
337
+ 'Each `inputs` value is a project-relative path with no traversal (the source file ' +
338
+ `these bytes were produced from); got ${JSON.stringify(badInput)}.`,
339
+ });
340
+ return;
341
+ }
342
+ if (ctx.projectRoot === ctx.engineRoot) {
343
+ res.status(404).json({ error: 'No project open, so there is nowhere to record the output.' });
344
+ return;
345
+ }
346
+ const chunks: Buffer[] = [];
347
+ let bytes = 0;
348
+ for await (const value of req) {
349
+ const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
350
+ bytes += chunk.byteLength;
351
+ chunks.push(chunk);
352
+ }
353
+ if (bytes === 0) {
354
+ res.status(400).json({ error: 'The output body was empty; nothing was written.' });
355
+ return;
356
+ }
357
+ const mediaType = projectOutputMediaType(path);
358
+ try {
359
+ const written = await createProjectOutputWriter(ctx.projectRoot, {
360
+ operationName: PROJECT_SESSION_WRITE_OPERATION,
361
+ operationSource: source,
362
+ session: {
363
+ id: session,
364
+ // The server's OWN port, read off the connection rather than taken
365
+ // from the caller: it is the port a person gets back to this tab on,
366
+ // and the one fact here nobody else should be asserting.
367
+ port: req.socket.localPort ?? 0,
368
+ revision,
369
+ ...(callId === '' ? {} : { callId }),
370
+ },
371
+ ...(inputs.length > 0 ? { inputs } : {}),
372
+ }).write([
373
+ {
374
+ path,
375
+ content: Buffer.concat(chunks),
376
+ role: 'asset',
377
+ ...(mediaType ? { mediaType } : {}),
378
+ },
379
+ ]);
380
+ res.json({ ok: true, bytes, provenanceOperationId: written.provenanceOperationId ?? null });
381
+ } catch (error) {
382
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
383
+ }
384
+ });
385
+
386
+ // ---- The session's DOCUMENT, written back OUT into the project.
387
+ //
388
+ // The one direction the two routes above do not cover. A Blender session
389
+ // holds one `.blend` (`browser/session.py`, "THE SESSION HAS A DOCUMENT");
390
+ // Blender's own `save_as_mainfile` writes it into the engine's filesystem,
391
+ // and this is how those bytes reach the disk the project actually lives on.
392
+ //
393
+ // NOT the `blender-file` spool above, and the difference is the one that
394
+ // spool exists to protect: there the page hands over bytes under an id and
395
+ // the REQUESTER names the destination, because the requester (`vgai
396
+ // blender-mcp`'s Mirror) writes anywhere a script wrote. Here the
397
+ // destination shape is pinned BY THIS SERVER — a project-relative `.blend`,
398
+ // no traversal, joined to the server's own root — so the page names a
399
+ // document, never a place on disk. Same defence-in-depth as `/__editor/
400
+ // data-file`, whose write half accepts only `src/data/**/*.data.json`, and
401
+ // the same reason: a purpose-scoped route beats widening a general one.
402
+ //
403
+ // The write is `commitProjectMutation` — the project's one attributed,
404
+ // conflict-checked transaction. A `.blend` is the project's file like any
405
+ // other, and there is deliberately no second write path.
406
+ //
407
+ // No provenance record, deliberately: the ledger is about what the project
408
+ // SHIPS and `public/` is what ships (see `vgai blender-mcp`'s Mirror). A
409
+ // document is the source the shipped artifact is exported FROM.
410
+ const isDocumentPath = (path: string): boolean => {
411
+ if (!path.endsWith('.blend') || path.startsWith('/') || path.includes('\\')) return false;
412
+ if (!isContainedRelativePath(path)) return false;
413
+ return path.split('/').every((segment) => segment !== '' && !segment.startsWith('.'));
414
+ };
415
+
416
+ router.post('/__editor/blender-document', async (req: Request, res: Response) => {
417
+ const path = (req.query['path'] as string) ?? '';
418
+ if (!isDocumentPath(path)) {
419
+ res.status(400).json({
420
+ error:
421
+ 'A Blender document is a project-relative .blend path with no traversal ' +
422
+ `(models/model.blend); got ${JSON.stringify(path)}.`,
423
+ });
424
+ return;
425
+ }
426
+ if (ctx.projectRoot === ctx.engineRoot) {
427
+ res.status(404).json({ error: 'No project open, so there is nowhere to save the document.' });
428
+ return;
429
+ }
430
+ const chunks: Buffer[] = [];
431
+ let bytes = 0;
432
+ for await (const value of req) {
433
+ const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
434
+ bytes += chunk.byteLength;
435
+ chunks.push(chunk);
436
+ }
437
+ // A zero-byte .blend is not a document; writing one would replace a good
438
+ // file with an empty one at exactly the moment something went wrong.
439
+ if (bytes === 0) {
440
+ res.status(400).json({ error: 'The Blender document body was empty; nothing was written.' });
441
+ return;
442
+ }
443
+ try {
444
+ const revision = await ctx.commitProjectMutation(req, [
445
+ { path, content: Buffer.concat(chunks) },
446
+ ]);
447
+ res.json({ ok: true, bytes, revision: revision?.revision ?? null });
448
+ } catch (error) {
449
+ ctx.projectMutationError(res, error);
450
+ }
451
+ });
452
+ }
453
+
454
+ export function registerRelayRoutes(
455
+ router: EditorServerRouter,
456
+ ctx: RouteContext,
457
+ plane: ControlPlane,
458
+ ): void {
459
+ registerBlenderFileRoutes(router, ctx);
460
+ const { consoleLedger, engineRoot, journalEvent, trustedShareIdentity } = ctx;
461
+ const {
462
+ handleCommandListener,
463
+ handleCommandReceived,
464
+ handleCommandResult,
465
+ handleConsoleEntries,
466
+ handleConsoleResolved,
467
+ handlePageError,
468
+ handleContributedCommands,
469
+ handlePlayPhase,
470
+ MAX_RECORDING_CHUNK_BYTES,
471
+ openGameplayRecording,
472
+ openGameplayRecordings,
473
+ relayCommand,
474
+ } = plane;
475
+
476
+ // small for a full QA playthrough.
477
+ router.post('/__editor/recording/start', async (req: Request, res: Response) => {
478
+ if (ctx.projectRoot === engineRoot) {
479
+ res.status(409).json({ error: 'Open a game project before recording gameplay.' });
480
+ return;
481
+ }
482
+ const startedAt = req.body?.startedAt;
483
+ const mimeType = req.body?.mimeType;
484
+ if (
485
+ typeof startedAt !== 'string' ||
486
+ !Number.isFinite(Date.parse(startedAt)) ||
487
+ typeof mimeType !== 'string' ||
488
+ !mimeType.startsWith('video/webm')
489
+ ) {
490
+ res
491
+ .status(400)
492
+ .json({ error: 'Recording start requires a WebM mimeType and ISO start time.' });
493
+ return;
494
+ }
495
+ const purpose = req.body?.purpose;
496
+ if (purpose !== undefined && purpose !== 'export') {
497
+ res.status(400).json({ error: 'Unknown recording purpose.' });
498
+ return;
499
+ }
500
+ const format = req.body?.format ?? 'composite-webm';
501
+ if (format !== 'composite-webm' && format !== 'canvas-dom') {
502
+ res.status(400).json({ error: 'Unknown gameplay recording format.' });
503
+ return;
504
+ }
505
+ if (purpose === 'export' && format !== 'composite-webm') {
506
+ res.status(400).json({ error: 'Fixed-step export requires composite-webm.' });
507
+ return;
508
+ }
509
+ const id = randomUUID();
510
+ // WHERE A RECORDING LIVES, AND HOW LONG.
511
+ //
512
+ // Two ownership rules, because the disk cost of "always record" has to be
513
+ // bounded by the DESIGN rather than by a cleanup chore nobody runs.
514
+ //
515
+ // - Inside a Gameplay Session, an unnamed clip takes the log's stem. It
516
+ // participates in the managed clip BYTE budget; the durable log has a
517
+ // separate, much larger history and survives ordinary clip eviction.
518
+ // - Outside a Gameplay Session, the unnamed fallback is the single
519
+ // rotating `play-latest.webm`.
520
+ // - Named (`vgai play --record <name>`): the caller's own keepsake, and
521
+ // NOTHING rotates it. Reusing a name overwrites that name's own file —
522
+ // an explicit instruction, unlike rotation — so a name is also how you
523
+ // keep a clip past the next play.
524
+ //
525
+ // The directory is gitignored in the scaffold (`.vgai/recordings/`), so
526
+ // neither kind is ever a commit's problem.
527
+ const slug = playRunSlug(req.body?.name);
528
+ if (purpose === 'export' && slug === null) {
529
+ res.status(400).json({ error: 'Video export requires a non-empty filename.' });
530
+ return;
531
+ }
532
+ const sessionStem = ctx.activeLogFile
533
+ ? basename(ctx.activeLogFile, extname(ctx.activeLogFile))
534
+ : null;
535
+ const path = resolve(
536
+ ctx.projectRoot,
537
+ '.vgai',
538
+ 'recordings',
539
+ slug === null && sessionStem !== null
540
+ ? `${sessionStem}.webm`
541
+ : `${slug ?? 'play-latest'}.webm`,
542
+ );
543
+ const replayPath =
544
+ format === 'canvas-dom' ? `${path.slice(0, -extname(path).length)}.replay` : null;
545
+ // Export must not replace a source capture and delete its DOM sidecar.
546
+ if (purpose === 'export' && existsSync(`${path.slice(0, -extname(path).length)}.replay`)) {
547
+ res
548
+ .status(409)
549
+ .json({ error: 'Export name belongs to a source replay. Choose a different name.' });
550
+ return;
551
+ }
552
+ await mkdir(dirname(path), { recursive: true });
553
+ // Truncate the chosen target before chunks append. For a session-derived
554
+ // name it is a fresh file; for an explicit reused name it is intentional.
555
+ await writeFile(path, new Uint8Array());
556
+ await rm(`${path.slice(0, -extname(path).length)}.replay`, { recursive: true, force: true });
557
+ if (replayPath !== null) {
558
+ await mkdir(join(replayPath, 'assets'), { recursive: true });
559
+ await writeFile(join(replayPath, 'events.json'), '[', 'utf-8');
560
+ await writeFile(
561
+ join(replayPath, 'manifest.json'),
562
+ `${JSON.stringify({ version: 1, status: 'recording', format, startedAt }, null, 2)}\n`,
563
+ 'utf-8',
564
+ );
565
+ }
566
+ openGameplayRecordings.set(id, {
567
+ path,
568
+ format,
569
+ replayPath,
570
+ nextSequence: 0,
571
+ nextDomSequence: 0,
572
+ domEventCount: 0,
573
+ domEventsStarted: false,
574
+ replayAssets: new Set(),
575
+ ...(purpose ? { purpose } : {}),
576
+ });
577
+ if (ctx.activeLogFile !== null) {
578
+ await appendFile(
579
+ ctx.activeLogFile,
580
+ `${JSON.stringify({
581
+ t: Date.now(),
582
+ level: 'info',
583
+ source: purpose === 'export' ? 'gameplay-export' : 'gameplay-recording',
584
+ msg: 'started',
585
+ meta: { file: basename(path), format, startedAt: Date.parse(startedAt) },
586
+ })}\n`,
587
+ 'utf-8',
588
+ );
589
+ }
590
+ journalEvent({
591
+ kind: 'play-recording',
592
+ action: 'started',
593
+ file: basename(path),
594
+ rotates: slug === null && sessionStem === null,
595
+ reason: null,
596
+ });
597
+ res.json({
598
+ id,
599
+ path,
600
+ format,
601
+ replayPath,
602
+ rotates: slug === null && sessionStem === null,
603
+ logFile: ctx.activeLogFile === null ? null : basename(ctx.activeLogFile),
604
+ });
605
+ });
606
+
607
+ router.post('/__editor/recording/dom', async (req: Request, res: Response) => {
608
+ const recording = openGameplayRecording(req.query['id']);
609
+ const sequence = Number(req.query['sequence']);
610
+ if (!recording) {
611
+ res.status(404).json({ error: 'No open gameplay recording has that id.' });
612
+ return;
613
+ }
614
+ if (recording.format !== 'canvas-dom' || recording.replayPath === null) {
615
+ res.status(409).json({ error: 'This recording has no DOM replay stream.' });
616
+ return;
617
+ }
618
+ if (!Number.isInteger(sequence) || sequence !== recording.nextDomSequence) {
619
+ res.status(409).json({
620
+ error: `DOM event sequence ${String(req.query['sequence'])} arrived; expected ${recording.nextDomSequence}.`,
621
+ });
622
+ return;
623
+ }
624
+ const chunks: Buffer[] = [];
625
+ let bytes = 0;
626
+ for await (const value of req) {
627
+ const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
628
+ bytes += chunk.byteLength;
629
+ if (bytes > MAX_RECORDING_CHUNK_BYTES) {
630
+ res.status(413).json({
631
+ error: `DOM event batches are limited to ${MAX_RECORDING_CHUNK_BYTES} bytes.`,
632
+ });
633
+ return;
634
+ }
635
+ chunks.push(chunk);
636
+ }
637
+ const text = Buffer.concat(chunks, bytes).toString('utf-8').trim();
638
+ let events: unknown;
639
+ try {
640
+ events = JSON.parse(text);
641
+ } catch {
642
+ res.status(400).json({ error: 'DOM event batch was not valid JSON.' });
643
+ return;
644
+ }
645
+ if (!Array.isArray(events) || events.length === 0) {
646
+ res.status(400).json({ error: 'DOM event batch must be a non-empty array.' });
647
+ return;
648
+ }
649
+ const serialized = JSON.stringify(events).slice(1, -1);
650
+ await appendFile(
651
+ join(recording.replayPath, 'events.json'),
652
+ `${recording.domEventsStarted ? ',' : ''}${serialized}`,
653
+ 'utf-8',
654
+ );
655
+ recording.domEventsStarted = true;
656
+ recording.domEventCount += events.length;
657
+ recording.nextDomSequence += 1;
658
+ res.json({ ok: true });
659
+ });
660
+
661
+ router.post('/__editor/recording/asset', async (req: Request, res: Response) => {
662
+ const recording = openGameplayRecording(req.query['id']);
663
+ const name = req.query['name'];
664
+ if (!recording) {
665
+ res.status(404).json({ error: 'No open gameplay recording has that id.' });
666
+ return;
667
+ }
668
+ if (recording.format !== 'canvas-dom' || recording.replayPath === null) {
669
+ res.status(409).json({ error: 'This recording has no replay asset store.' });
670
+ return;
671
+ }
672
+ if (typeof name !== 'string' || !/^\d{6}\.(?:png|jpg|webp|gif|svg)$/.test(name)) {
673
+ res.status(400).json({ error: 'Replay asset name is invalid.' });
674
+ return;
675
+ }
676
+ if (recording.replayAssets.has(name)) {
677
+ res.status(409).json({ error: 'Replay asset already exists.' });
678
+ return;
679
+ }
680
+ const chunks: Buffer[] = [];
681
+ let bytes = 0;
682
+ for await (const value of req) {
683
+ const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
684
+ bytes += chunk.byteLength;
685
+ if (bytes > MAX_RECORDING_CHUNK_BYTES) {
686
+ res.status(413).json({
687
+ error: `Replay assets are limited to ${MAX_RECORDING_CHUNK_BYTES} bytes.`,
688
+ });
689
+ return;
690
+ }
691
+ chunks.push(chunk);
692
+ }
693
+ if (bytes === 0) {
694
+ res.status(400).json({ error: 'Replay asset was empty.' });
695
+ return;
696
+ }
697
+ await writeFile(join(recording.replayPath, 'assets', name), Buffer.concat(chunks, bytes));
698
+ recording.replayAssets.add(name);
699
+ res.json({ ok: true });
700
+ });
701
+
702
+ router.post('/__editor/recording/chunk', async (req: Request, res: Response) => {
703
+ const recording = openGameplayRecording(req.query['id']);
704
+ const sequence = Number(req.query['sequence']);
705
+ if (!recording) {
706
+ res.status(404).json({ error: 'No open gameplay recording has that id.' });
707
+ return;
708
+ }
709
+ if (!Number.isInteger(sequence) || sequence !== recording.nextSequence) {
710
+ res.status(409).json({
711
+ error: `Recording chunk sequence ${String(req.query['sequence'])} arrived; expected ${recording.nextSequence}.`,
712
+ });
713
+ return;
714
+ }
715
+ const chunks: Buffer[] = [];
716
+ let bytes = 0;
717
+ for await (const value of req) {
718
+ const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
719
+ bytes += chunk.byteLength;
720
+ if (bytes > MAX_RECORDING_CHUNK_BYTES) {
721
+ res.status(413).json({
722
+ error: `Recording chunks are limited to ${MAX_RECORDING_CHUNK_BYTES} bytes.`,
723
+ });
724
+ return;
725
+ }
726
+ chunks.push(chunk);
727
+ }
728
+ if (bytes === 0) {
729
+ res.status(400).json({ error: 'Recording chunk was empty.' });
730
+ return;
731
+ }
732
+ await appendFile(recording.path, Buffer.concat(chunks, bytes));
733
+ recording.nextSequence += 1;
734
+ res.json({ ok: true });
735
+ });
736
+
737
+ router.post('/__editor/recording/finish', async (req: Request, res: Response) => {
738
+ const id = req.body?.id;
739
+ const recording = openGameplayRecording(id);
740
+ if (!recording || typeof id !== 'string') {
741
+ res.status(404).json({ error: 'No open gameplay recording has that id.' });
742
+ return;
743
+ }
744
+ const file = await stat(recording.path);
745
+ if (file.size === 0) {
746
+ res.status(409).json({ error: 'The browser finalized an empty gameplay recording.' });
747
+ return;
748
+ }
749
+ if (recording.format === 'canvas-dom') {
750
+ const replay = req.body?.replay;
751
+ if (
752
+ recording.replayPath === null ||
753
+ !replay ||
754
+ typeof replay !== 'object' ||
755
+ replay.version !== 1 ||
756
+ typeof replay.eventCount !== 'number' ||
757
+ replay.eventCount !== recording.domEventCount ||
758
+ typeof replay.assetCount !== 'number' ||
759
+ replay.assetCount !== recording.replayAssets.size
760
+ ) {
761
+ res.status(409).json({
762
+ error: 'The canvas-dom replay manifest does not match its uploaded events and assets.',
763
+ });
764
+ return;
765
+ }
766
+ await appendFile(join(recording.replayPath, 'events.json'), ']', 'utf-8');
767
+ await writeFile(
768
+ join(recording.replayPath, 'manifest.json'),
769
+ `${JSON.stringify(
770
+ {
771
+ ...replay,
772
+ video: basename(recording.path),
773
+ status: 'complete',
774
+ format: recording.format,
775
+ completedAt: new Date().toISOString(),
776
+ },
777
+ null,
778
+ 2,
779
+ )}\n`,
780
+ 'utf-8',
781
+ );
782
+ }
783
+ openGameplayRecordings.delete(id);
784
+ // The finalized clip lands in the journal so "where is the evidence for
785
+ // that run" is answerable from the session record alone — including for a
786
+ // run nobody was watching, which is the case this whole path exists for.
787
+ const reason = typeof req.body?.reason === 'string' ? req.body.reason : null;
788
+ if (ctx.activeLogFile !== null) {
789
+ await appendFile(
790
+ ctx.activeLogFile,
791
+ `${JSON.stringify({
792
+ t: Date.now(),
793
+ level: 'info',
794
+ source: recording.purpose === 'export' ? 'gameplay-export' : 'gameplay-recording',
795
+ msg: 'finalized',
796
+ meta: {
797
+ file: basename(recording.path),
798
+ bytes: file.size,
799
+ format: recording.format,
800
+ replay: recording.replayPath === null ? null : basename(recording.replayPath),
801
+ reason,
802
+ },
803
+ })}\n`,
804
+ 'utf-8',
805
+ );
806
+ }
807
+ journalEvent({
808
+ kind: 'play-recording',
809
+ action: 'finalized',
810
+ file: basename(recording.path),
811
+ rotates: basename(recording.path) === 'play-latest.webm',
812
+ reason,
813
+ });
814
+ await pruneGameplaySessionClips(ctx.projectRoot);
815
+ res.json({ ok: true, path: recording.path, bytes: file.size });
816
+ });
817
+
818
+ router.post('/__editor/recording/abort', async (req: Request, res: Response) => {
819
+ const id = req.body?.id;
820
+ const recording = openGameplayRecording(id);
821
+ if (recording && typeof id === 'string') {
822
+ openGameplayRecordings.delete(id);
823
+ if (ctx.activeLogFile !== null) {
824
+ await appendFile(
825
+ ctx.activeLogFile,
826
+ `${JSON.stringify({
827
+ t: Date.now(),
828
+ level: 'info',
829
+ source: recording.purpose === 'export' ? 'gameplay-export' : 'gameplay-recording',
830
+ msg: 'aborted',
831
+ meta: { file: basename(recording.path) },
832
+ })}\n`,
833
+ 'utf-8',
834
+ );
835
+ }
836
+ await unlink(recording.path).catch(() => undefined);
837
+ if (recording.replayPath !== null) {
838
+ await rm(recording.replayPath, { recursive: true, force: true }).catch(() => undefined);
839
+ }
840
+ }
841
+ res.json({ ok: true });
842
+ });
843
+
844
+ /** Serve one completed local replay artifact to the editor's in-page viewer.
845
+ * The caller names a project recording or its absolute path; containment rejects
846
+ * symlinks and every path outside this project's recording directory. */
847
+ router.get(
848
+ '/__editor/recording/replay-file',
849
+ async (req: Request, res: Response, next: NextFunction) => {
850
+ const replay = req.query['replay'];
851
+ const file = req.query['file'];
852
+ if (
853
+ typeof replay !== 'string' ||
854
+ !/^[a-zA-Z0-9._-]+\.replay$/.test(basename(replay)) ||
855
+ typeof file !== 'string'
856
+ ) {
857
+ res.status(400).json({ error: 'Replay file request is invalid.' });
858
+ return;
859
+ }
860
+ const recordingsRoot = resolve(ctx.projectRoot, '.vgai', 'recordings');
861
+ const replayRoot =
862
+ replay === basename(replay)
863
+ ? resolve(recordingsRoot, replay)
864
+ : resolve(ctx.projectRoot, replay);
865
+ if (
866
+ dirname(replayRoot) !== recordingsRoot ||
867
+ !(await isCanonicalPathInside(recordingsRoot, replayRoot))
868
+ ) {
869
+ res.status(400).json({
870
+ error:
871
+ 'Replay must be inside this project’s .vgai/recordings directory; use the original recording, not an external copy.',
872
+ });
873
+ return;
874
+ }
875
+ const manifestPath = join(replayRoot, 'manifest.json');
876
+ const manifest = await readFile(manifestPath, 'utf-8')
877
+ .then((text) => JSON.parse(text) as { status?: unknown; video?: unknown })
878
+ .catch(() => null);
879
+ if (manifest?.status !== 'complete' || typeof manifest.video !== 'string') {
880
+ res.status(404).json({ error: 'Completed replay not found.' });
881
+ return;
882
+ }
883
+ let path: string | null = null;
884
+ if (file === 'manifest.json') path = manifestPath;
885
+ else if (file === 'events.json') path = join(replayRoot, 'events.json');
886
+ else if (file === 'video') path = join(recordingsRoot, manifest.video);
887
+ else if (/^assets\/\d{6}\.(?:png|jpg|webp|gif|svg)$/.test(file)) {
888
+ path = join(replayRoot, file);
889
+ }
890
+ if (path === null || !(await isCanonicalPathInside(recordingsRoot, path))) {
891
+ res.status(404).json({ error: 'Replay file not found.' });
892
+ return;
893
+ }
894
+ res.sendFile(path, { dotfiles: 'allow' }, (error: Error | undefined) => {
895
+ if (!error) return;
896
+ if (!res.headersSent) res.status(404).json({ error: 'Replay file not found.' });
897
+ else next(error);
898
+ });
899
+ },
900
+ );
901
+
902
+ router.post('/__editor/command', (req: Request, res: Response) => {
903
+ relayCommand(
904
+ req.body as Record<string, unknown>,
905
+ trustedShareIdentity(req)?.participantId,
906
+ ).then(({ result, callerReceipt }) => {
907
+ if (callerReceipt) res.once('finish', callerReceipt);
908
+ const { status, body } = commandResponseFor(result, consoleLedger.summary());
909
+ res.status(status).json(body);
910
+ });
911
+ });
912
+
913
+ // ---- Editor command result (browser reports back) ----
914
+ router.post('/__editor/command-result', (req: Request, res: Response) => {
915
+ const outcome = handleCommandResult(req.body as Record<string, unknown>);
916
+ if (outcome.status === 200) res.json({ ok: true });
917
+ else res.status(outcome.status).json({ error: outcome.error });
918
+ });
919
+
920
+ // ---- Delivery receipt (browser reports it PICKED THE COMMAND UP) ----
921
+ //
922
+ // The other half of `relayCommandAckDeadlineMs`: the tab sends this the
923
+ // instant its command handler sees the event, before it starts the work. It
924
+ // only cancels the receipt window — the command's own budget keeps running,
925
+ // so a genuinely slow game boot still gets its full 120s.
926
+ router.post('/__editor/command-received', (req: Request, res: Response) => {
927
+ const outcome = handleCommandReceived((req.body ?? {}) as Record<string, unknown>);
928
+ if (outcome.status === 200) res.json({ ok: true });
929
+ else res.status(outcome.status).json({ error: outcome.error });
930
+ });
931
+
932
+ // ---- Command-listener presence (browser reports it CAN run commands) ----
933
+ //
934
+ // The POST fallback for pages with no upstream socket (the share tunnel's
935
+ // one-way SSE bridge). See `src/editor-api.ts`'s `reportCommandListener`.
936
+ router.post('/__editor/command-listener', (req: Request, res: Response) => {
937
+ const outcome = handleCommandListener((req.body ?? {}) as Record<string, unknown>);
938
+ if (outcome.status === 200) res.json({ ok: true });
939
+ else res.status(outcome.status).json({ error: outcome.error });
940
+ });
941
+
942
+ // The POST twin of the `page-error` control frame, for the same reason the
943
+ // one above has one: a page whose control socket never came up (the share
944
+ // tunnel's one-way SSE bridge, or a boot that died before the upgrade) must
945
+ // still be able to say what killed it. `sendBeacon`-friendly.
946
+ router.post('/__editor/contributed-commands', (req: Request, res: Response) => {
947
+ const outcome = handleContributedCommands((req.body ?? {}) as Record<string, unknown>);
948
+ if (outcome.status === 200) res.json({ ok: true });
949
+ else res.status(outcome.status).json({ error: outcome.error });
950
+ });
951
+
952
+ router.post('/__editor/page-error', (req: Request, res: Response) => {
953
+ const outcome = handlePageError((req.body ?? {}) as Record<string, unknown>);
954
+ if (outcome.status === 200) res.json({ ok: true });
955
+ else res.status(outcome.status).json({ error: outcome.error });
956
+ });
957
+
958
+ // The POST twin of the `play-phase` frame — same reason as the ones above.
959
+ router.post('/__editor/play-phase', (req: Request, res: Response) => {
960
+ const outcome = handlePlayPhase((req.body ?? {}) as Record<string, unknown>);
961
+ if (outcome.status === 200) res.json({ ok: true });
962
+ else res.status(outcome.status).json({ error: outcome.error });
963
+ });
964
+
965
+ // The POST twin of the `console-entries` frame — same reason as the two
966
+ // above (a page with no upstream socket must still be able to report).
967
+ router.post('/__editor/console-entries', (req: Request, res: Response) => {
968
+ const outcome = handleConsoleEntries((req.body ?? {}) as Record<string, unknown>);
969
+ if (outcome.status === 200) res.json({ ok: true });
970
+ else res.status(outcome.status).json({ error: outcome.error });
971
+ });
972
+
973
+ // Clearing rule (c): the owner of a condition reporting it resolved. The POST
974
+ // twin exists for the same reason as the frame above.
975
+ router.post('/__editor/console-resolved', (req: Request, res: Response) => {
976
+ const outcome = handleConsoleResolved((req.body ?? {}) as Record<string, unknown>);
977
+ if (outcome.status === 200) res.json({ ok: true });
978
+ else res.status(outcome.status).json({ error: outcome.error });
979
+ });
980
+
981
+ // ---- The unresolved console set (CLI reads; `vgai status` prints in full) ----
982
+ //
983
+ // The counts ride every command envelope (`commandResponseFor`), so this
984
+ // route exists for the DETAIL: the complete list of distinct conditions with
985
+ // their occurrence counts, which is what `vgai status` / `vgai console`
986
+ // print. Deliberately a plain GET with no tab cooperation — the whole point
987
+ // is that it answers when the tab is dead.
988
+ router.get('/__editor/console', (req: Request, res: Response) => {
989
+ const all = req.query['all'] === '1' || req.query['all'] === 'true';
990
+ res.json({
991
+ unresolvedConsole: consoleLedger.summary(),
992
+ entries: all ? consoleLedger.all() : consoleLedger.unresolved(),
993
+ });
994
+ });
995
+
996
+ // Clearing rule (b): a NAMED acknowledgment, with who and why. It never
997
+ // deletes the record — the entry stays, marked, and a `console-ack` row goes
998
+ // into the session ctx.journal — because the point of an acknowledgment is that
999
+ // somebody can later read what was waved through and on whose say-so.
1000
+ router.post('/__editor/console/ack', (req: Request, res: Response) => {
1001
+ const body = (req.body ?? {}) as Record<string, unknown>;
1002
+ const id = typeof body['id'] === 'string' ? body['id'].trim() : '';
1003
+ const reason = typeof body['reason'] === 'string' ? body['reason'].trim() : '';
1004
+ const by =
1005
+ typeof body['by'] === 'string' && body['by'].trim() !== '' ? body['by'].trim() : null;
1006
+ if (id === '') {
1007
+ res.status(400).json({ ok: false, error: 'console ack requires { id }.' });
1008
+ return;
1009
+ }
1010
+ if (reason === '') {
1011
+ res.status(400).json({
1012
+ ok: false,
1013
+ error:
1014
+ 'console ack requires { reason } — an acknowledgment with no stated reason is a silent reset, ' +
1015
+ 'which is the exact failure this ledger exists to prevent.',
1016
+ });
1017
+ return;
1018
+ }
1019
+ const acked = consoleLedger.ack(id, { by: by ?? 'unknown', reason });
1020
+ if (acked === null) {
1021
+ res.status(404).json({
1022
+ ok: false,
1023
+ error: `No unresolved console entry with id "${id}". Run \`volter-editor status\` for the current set.`,
1024
+ });
1025
+ return;
1026
+ }
1027
+ res.json({ unresolvedConsole: consoleLedger.summary(), ok: true, entry: acked });
1028
+ });
1029
+ }