@nodetool-ai/websocket 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.nodetool-build-stamp +1 -1
- package/dist/chat-turn-registry.d.ts +2 -1
- package/dist/chat-turn-registry.d.ts.map +1 -1
- package/dist/chat-turn-registry.js +8 -1
- package/dist/chat-turn-registry.js.map +1 -1
- package/dist/collection-api.d.ts.map +1 -1
- package/dist/collection-api.js +41 -6
- package/dist/collection-api.js.map +1 -1
- package/dist/custom-providers.d.ts.map +1 -1
- package/dist/custom-providers.js +6 -1
- package/dist/custom-providers.js.map +1 -1
- package/dist/error-traces.d.ts +56 -0
- package/dist/error-traces.d.ts.map +1 -0
- package/dist/error-traces.js +179 -0
- package/dist/error-traces.js.map +1 -0
- package/dist/example-workflows.d.ts.map +1 -1
- package/dist/example-workflows.js +6 -0
- package/dist/example-workflows.js.map +1 -1
- package/dist/fake-runtime.d.ts +41 -6
- package/dist/fake-runtime.d.ts.map +1 -1
- package/dist/fake-runtime.js +247 -16
- package/dist/fake-runtime.js.map +1 -1
- package/dist/file-api.d.ts.map +1 -1
- package/dist/file-api.js +6 -8
- package/dist/file-api.js.map +1 -1
- package/dist/generation-recovery.d.ts.map +1 -1
- package/dist/generation-recovery.js +32 -7
- package/dist/generation-recovery.js.map +1 -1
- package/dist/headless-job-runner.d.ts.map +1 -1
- package/dist/headless-job-runner.js +5 -1
- package/dist/headless-job-runner.js.map +1 -1
- package/dist/http-api.d.ts +3 -1
- package/dist/http-api.d.ts.map +1 -1
- package/dist/http-api.js +66 -9
- package/dist/http-api.js.map +1 -1
- package/dist/http-body-schemas.d.ts +2 -0
- package/dist/http-body-schemas.d.ts.map +1 -1
- package/dist/http-body-schemas.js +8 -1
- package/dist/http-body-schemas.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/interrupted-jobs.d.ts +14 -0
- package/dist/interrupted-jobs.d.ts.map +1 -0
- package/dist/interrupted-jobs.js +28 -0
- package/dist/interrupted-jobs.js.map +1 -0
- package/dist/lib/admin.d.ts +7 -0
- package/dist/lib/admin.d.ts.map +1 -0
- package/dist/lib/admin.js +20 -0
- package/dist/lib/admin.js.map +1 -0
- package/dist/lib/app-debug-service.js +1 -1
- package/dist/lib/app-debug-service.js.map +1 -1
- package/dist/lib/app-instances-service.d.ts +1319 -0
- package/dist/lib/app-instances-service.d.ts.map +1 -0
- package/dist/lib/app-instances-service.js +377 -0
- package/dist/lib/app-instances-service.js.map +1 -0
- package/dist/lib/app-project-export.d.ts +12 -0
- package/dist/lib/app-project-export.d.ts.map +1 -0
- package/dist/lib/app-project-export.js +42 -0
- package/dist/lib/app-project-export.js.map +1 -0
- package/dist/lib/app-run-cancellation.d.ts +4 -0
- package/dist/lib/app-run-cancellation.d.ts.map +1 -0
- package/dist/lib/app-run-cancellation.js +28 -0
- package/dist/lib/app-run-cancellation.js.map +1 -0
- package/dist/lib/applications-service.d.ts +182 -1
- package/dist/lib/applications-service.d.ts.map +1 -1
- package/dist/lib/applications-service.js +27 -6
- package/dist/lib/applications-service.js.map +1 -1
- package/dist/lib/asset-export.d.ts +6 -2
- package/dist/lib/asset-export.d.ts.map +1 -1
- package/dist/lib/asset-export.js +15 -7
- package/dist/lib/asset-export.js.map +1 -1
- package/dist/lib/asset-paths.d.ts +7 -11
- package/dist/lib/asset-paths.d.ts.map +1 -1
- package/dist/lib/asset-paths.js +8 -56
- package/dist/lib/asset-paths.js.map +1 -1
- package/dist/lib/asset-response.d.ts.map +1 -1
- package/dist/lib/asset-response.js +3 -1
- package/dist/lib/asset-response.js.map +1 -1
- package/dist/lib/audio-peaks.d.ts.map +1 -1
- package/dist/lib/audio-peaks.js +5 -1
- package/dist/lib/audio-peaks.js.map +1 -1
- package/dist/lib/bridge.d.ts.map +1 -1
- package/dist/lib/bridge.js.map +1 -1
- package/dist/lib/cloud-profile.d.ts +10 -0
- package/dist/lib/cloud-profile.d.ts.map +1 -0
- package/dist/lib/cloud-profile.js +13 -0
- package/dist/lib/cloud-profile.js.map +1 -0
- package/dist/lib/document-model-interfaces.d.ts.map +1 -1
- package/dist/lib/document-model-interfaces.js +9 -8
- package/dist/lib/document-model-interfaces.js.map +1 -1
- package/dist/lib/example-apps.d.ts +9 -0
- package/dist/lib/example-apps.d.ts.map +1 -1
- package/dist/lib/example-apps.js +61 -14
- package/dist/lib/example-apps.js.map +1 -1
- package/dist/lib/http-tracing.d.ts +12 -0
- package/dist/lib/http-tracing.d.ts.map +1 -0
- package/dist/lib/http-tracing.js +81 -0
- package/dist/lib/http-tracing.js.map +1 -0
- package/dist/lib/http-user-id.d.ts +2 -0
- package/dist/lib/http-user-id.d.ts.map +1 -0
- package/dist/lib/http-user-id.js +4 -0
- package/dist/lib/http-user-id.js.map +1 -0
- package/dist/lib/json-body.d.ts +11 -0
- package/dist/lib/json-body.d.ts.map +1 -0
- package/dist/lib/json-body.js +13 -0
- package/dist/lib/json-body.js.map +1 -0
- package/dist/lib/media.d.ts +1 -6
- package/dist/lib/media.d.ts.map +1 -1
- package/dist/lib/media.js +8 -44
- package/dist/lib/media.js.map +1 -1
- package/dist/lib/public-routes.d.ts +22 -0
- package/dist/lib/public-routes.d.ts.map +1 -1
- package/dist/lib/public-routes.js +41 -0
- package/dist/lib/public-routes.js.map +1 -1
- package/dist/lib/public-workflow-share.d.ts +31 -0
- package/dist/lib/public-workflow-share.d.ts.map +1 -0
- package/dist/lib/public-workflow-share.js +88 -0
- package/dist/lib/public-workflow-share.js.map +1 -0
- package/dist/lib/python-metadata-roots.d.ts +27 -0
- package/dist/lib/python-metadata-roots.d.ts.map +1 -0
- package/dist/lib/python-metadata-roots.js +92 -0
- package/dist/lib/python-metadata-roots.js.map +1 -0
- package/dist/lib/run-trace-live-delivery.d.ts +25 -0
- package/dist/lib/run-trace-live-delivery.d.ts.map +1 -0
- package/dist/lib/run-trace-live-delivery.js +91 -0
- package/dist/lib/run-trace-live-delivery.js.map +1 -0
- package/dist/lib/runtime-detection.d.ts +1 -1
- package/dist/lib/runtime-detection.d.ts.map +1 -1
- package/dist/lib/runtime-detection.js +2 -1
- package/dist/lib/runtime-detection.js.map +1 -1
- package/dist/lib/server-auth-token.d.ts +18 -0
- package/dist/lib/server-auth-token.d.ts.map +1 -0
- package/dist/lib/server-auth-token.js +28 -0
- package/dist/lib/server-auth-token.js.map +1 -0
- package/dist/lib/test-ui-run-routes.d.ts +8 -0
- package/dist/lib/test-ui-run-routes.d.ts.map +1 -0
- package/dist/lib/test-ui-run-routes.js +42 -0
- package/dist/lib/test-ui-run-routes.js.map +1 -0
- package/dist/lib/thumbnail.d.ts.map +1 -1
- package/dist/lib/thumbnail.js +12 -9
- package/dist/lib/thumbnail.js.map +1 -1
- package/dist/lib/timeline-bundle.d.ts +159 -16
- package/dist/lib/timeline-bundle.d.ts.map +1 -1
- package/dist/lib/timeline-bundle.js +34 -6
- package/dist/lib/timeline-bundle.js.map +1 -1
- package/dist/lib/timeline-media-export.d.ts +18 -0
- package/dist/lib/timeline-media-export.d.ts.map +1 -0
- package/dist/lib/timeline-media-export.js +66 -0
- package/dist/lib/timeline-media-export.js.map +1 -0
- package/dist/lib/timeline-premiere-import.d.ts +24 -0
- package/dist/lib/timeline-premiere-import.d.ts.map +1 -0
- package/dist/lib/timeline-premiere-import.js +102 -0
- package/dist/lib/timeline-premiere-import.js.map +1 -0
- package/dist/lib/timeline-premiere.d.ts +38 -0
- package/dist/lib/timeline-premiere.d.ts.map +1 -0
- package/dist/lib/timeline-premiere.js +87 -0
- package/dist/lib/timeline-premiere.js.map +1 -0
- package/dist/lib/video-proxy.d.ts.map +1 -1
- package/dist/lib/video-proxy.js +3 -2
- package/dist/lib/video-proxy.js.map +1 -1
- package/dist/lib/workflow-service.d.ts.map +1 -1
- package/dist/lib/workflow-service.js +4 -1
- package/dist/lib/workflow-service.js.map +1 -1
- package/dist/mcp-agent-tools.d.ts.map +1 -1
- package/dist/mcp-agent-tools.js +95 -162
- package/dist/mcp-agent-tools.js.map +1 -1
- package/dist/mcp-client-config.d.ts +64 -0
- package/dist/mcp-client-config.d.ts.map +1 -0
- package/dist/mcp-client-config.js +353 -0
- package/dist/mcp-client-config.js.map +1 -0
- package/dist/mcp-server.d.ts +7 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +163 -16
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp-skill-prompts.d.ts +23 -0
- package/dist/mcp-skill-prompts.d.ts.map +1 -0
- package/dist/mcp-skill-prompts.js +91 -0
- package/dist/mcp-skill-prompts.js.map +1 -0
- package/dist/mcp-tool-deps.d.ts.map +1 -1
- package/dist/mcp-tool-deps.js +8 -1
- package/dist/mcp-tool-deps.js.map +1 -1
- package/dist/models-api.d.ts +10 -0
- package/dist/models-api.d.ts.map +1 -1
- package/dist/models-api.js +29 -2
- package/dist/models-api.js.map +1 -1
- package/dist/node-registry-setup.d.ts +1 -1
- package/dist/node-registry-setup.d.ts.map +1 -1
- package/dist/node-registry-setup.js +21 -114
- package/dist/node-registry-setup.js.map +1 -1
- package/dist/oauth-api.d.ts.map +1 -1
- package/dist/oauth-api.js +45 -23
- package/dist/oauth-api.js.map +1 -1
- package/dist/openai-api.d.ts.map +1 -1
- package/dist/openai-api.js +10 -2
- package/dist/openai-api.js.map +1 -1
- package/dist/plugins/websocket.d.ts.map +1 -1
- package/dist/plugins/websocket.js +27 -59
- package/dist/plugins/websocket.js.map +1 -1
- package/dist/python-bridge-gate.d.ts +28 -0
- package/dist/python-bridge-gate.d.ts.map +1 -0
- package/dist/python-bridge-gate.js +36 -0
- package/dist/python-bridge-gate.js.map +1 -0
- package/dist/routes/app-runs.d.ts +4 -0
- package/dist/routes/app-runs.d.ts.map +1 -0
- package/dist/routes/app-runs.js +173 -0
- package/dist/routes/app-runs.js.map +1 -0
- package/dist/routes/applications.d.ts +1 -0
- package/dist/routes/applications.d.ts.map +1 -1
- package/dist/routes/applications.js +24 -0
- package/dist/routes/applications.js.map +1 -1
- package/dist/routes/assets.d.ts.map +1 -1
- package/dist/routes/assets.js +31 -41
- package/dist/routes/assets.js.map +1 -1
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +6 -0
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/documents.d.ts.map +1 -1
- package/dist/routes/documents.js +24 -0
- package/dist/routes/documents.js.map +1 -1
- package/dist/routes/fal-pricing-estimate.d.ts.map +1 -1
- package/dist/routes/fal-pricing-estimate.js +2 -0
- package/dist/routes/fal-pricing-estimate.js.map +1 -1
- package/dist/routes/files.d.ts.map +1 -1
- package/dist/routes/files.js +2 -0
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/games.d.ts +38 -0
- package/dist/routes/games.d.ts.map +1 -0
- package/dist/routes/games.js +158 -0
- package/dist/routes/games.js.map +1 -0
- package/dist/routes/integrations.d.ts.map +1 -1
- package/dist/routes/integrations.js +22 -26
- package/dist/routes/integrations.js.map +1 -1
- package/dist/routes/js-scripts.d.ts +10 -4
- package/dist/routes/js-scripts.d.ts.map +1 -1
- package/dist/routes/js-scripts.js +451 -69
- package/dist/routes/js-scripts.js.map +1 -1
- package/dist/routes/nodes.d.ts.map +1 -1
- package/dist/routes/nodes.js +34 -1
- package/dist/routes/nodes.js.map +1 -1
- package/dist/routes/public-shared-workflows.d.ts +16 -0
- package/dist/routes/public-shared-workflows.d.ts.map +1 -0
- package/dist/routes/public-shared-workflows.js +55 -0
- package/dist/routes/public-shared-workflows.js.map +1 -0
- package/dist/routes/run-spans.d.ts +4 -0
- package/dist/routes/run-spans.d.ts.map +1 -0
- package/dist/routes/run-spans.js +53 -0
- package/dist/routes/run-spans.js.map +1 -0
- package/dist/routes/storage.d.ts.map +1 -1
- package/dist/routes/storage.js +2 -0
- package/dist/routes/storage.js.map +1 -1
- package/dist/routes/storyboards.d.ts.map +1 -1
- package/dist/routes/storyboards.js +6 -3
- package/dist/routes/storyboards.js.map +1 -1
- package/dist/routes/timeline-fonts.d.ts +22 -0
- package/dist/routes/timeline-fonts.d.ts.map +1 -1
- package/dist/routes/timeline-fonts.js +138 -7
- package/dist/routes/timeline-fonts.js.map +1 -1
- package/dist/routes/timelines.d.ts +8 -4
- package/dist/routes/timelines.d.ts.map +1 -1
- package/dist/routes/timelines.js +209 -16
- package/dist/routes/timelines.js.map +1 -1
- package/dist/screenshot-docs-seed.d.ts +24 -0
- package/dist/screenshot-docs-seed.d.ts.map +1 -0
- package/dist/screenshot-docs-seed.js +414 -0
- package/dist/screenshot-docs-seed.js.map +1 -0
- package/dist/screenshot-projects.d.ts.map +1 -1
- package/dist/screenshot-projects.js +9 -9
- package/dist/screenshot-projects.js.map +1 -1
- package/dist/screenshot-server.js +114 -20
- package/dist/screenshot-server.js.map +1 -1
- package/dist/sdk/sdk-model-download-service.d.ts.map +1 -1
- package/dist/sdk/sdk-model-download-service.js +5 -2
- package/dist/sdk/sdk-model-download-service.js.map +1 -1
- package/dist/server.js +173 -109
- package/dist/server.js.map +1 -1
- package/dist/session/app-run-fold.d.ts +7 -0
- package/dist/session/app-run-fold.d.ts.map +1 -0
- package/dist/session/app-run-fold.js +45 -0
- package/dist/session/app-run-fold.js.map +1 -0
- package/dist/session/chat-prompt.d.ts.map +1 -1
- package/dist/session/chat-prompt.js +37 -15
- package/dist/session/chat-prompt.js.map +1 -1
- package/dist/session/chat-turn.d.ts +11 -0
- package/dist/session/chat-turn.d.ts.map +1 -1
- package/dist/session/chat-turn.js +270 -116
- package/dist/session/chat-turn.js.map +1 -1
- package/dist/session/commands.d.ts +14 -0
- package/dist/session/commands.d.ts.map +1 -1
- package/dist/session/commands.js +119 -14
- package/dist/session/commands.js.map +1 -1
- package/dist/session/inference.d.ts +30 -2
- package/dist/session/inference.d.ts.map +1 -1
- package/dist/session/inference.js +165 -16
- package/dist/session/inference.js.map +1 -1
- package/dist/session/job-execution.d.ts +7 -1
- package/dist/session/job-execution.d.ts.map +1 -1
- package/dist/session/job-execution.js +329 -42
- package/dist/session/job-execution.js.map +1 -1
- package/dist/session/model-interfaces.d.ts +2 -1
- package/dist/session/model-interfaces.d.ts.map +1 -1
- package/dist/session/model-interfaces.js +1 -0
- package/dist/session/model-interfaces.js.map +1 -1
- package/dist/storage-api.d.ts +0 -4
- package/dist/storage-api.d.ts.map +1 -1
- package/dist/storage-api.js +3 -26
- package/dist/storage-api.js.map +1 -1
- package/dist/storage-retention.d.ts +5 -0
- package/dist/storage-retention.d.ts.map +1 -1
- package/dist/storage-retention.js +22 -1
- package/dist/storage-retention.js.map +1 -1
- package/dist/test-ui-server.d.ts +11 -0
- package/dist/test-ui-server.d.ts.map +1 -1
- package/dist/test-ui-server.js +84 -6
- package/dist/test-ui-server.js.map +1 -1
- package/dist/timeline-generation-recovery.d.ts +4 -0
- package/dist/timeline-generation-recovery.d.ts.map +1 -0
- package/dist/timeline-generation-recovery.js +72 -0
- package/dist/timeline-generation-recovery.js.map +1 -0
- package/dist/triggers/dispatcher.d.ts.map +1 -1
- package/dist/triggers/dispatcher.js +26 -6
- package/dist/triggers/dispatcher.js.map +1 -1
- package/dist/triggers/file-watch.d.ts.map +1 -1
- package/dist/triggers/file-watch.js +35 -19
- package/dist/triggers/file-watch.js.map +1 -1
- package/dist/triggers/scheduler.d.ts.map +1 -1
- package/dist/triggers/scheduler.js +7 -5
- package/dist/triggers/scheduler.js.map +1 -1
- package/dist/triggers/stores.js +8 -8
- package/dist/triggers/stores.js.map +1 -1
- package/dist/triggers/webhook-route.js +2 -2
- package/dist/triggers/webhook-route.js.map +1 -1
- package/dist/trpc/error-formatter.d.ts.map +1 -1
- package/dist/trpc/error-formatter.js +2 -1
- package/dist/trpc/error-formatter.js.map +1 -1
- package/dist/trpc/error-logging.d.ts +10 -0
- package/dist/trpc/error-logging.d.ts.map +1 -1
- package/dist/trpc/error-logging.js +21 -0
- package/dist/trpc/error-logging.js.map +1 -1
- package/dist/trpc/router.d.ts +100 -31028
- package/dist/trpc/router.d.ts.map +1 -1
- package/dist/trpc/router.js +12 -3
- package/dist/trpc/router.js.map +1 -1
- package/dist/trpc/routers/app-instances.d.ts +2332 -0
- package/dist/trpc/routers/app-instances.d.ts.map +1 -0
- package/dist/trpc/routers/app-instances.js +52 -0
- package/dist/trpc/routers/app-instances.js.map +1 -0
- package/dist/trpc/routers/app-runs.d.ts +1120 -0
- package/dist/trpc/routers/app-runs.d.ts.map +1 -0
- package/dist/trpc/routers/app-runs.js +28 -0
- package/dist/trpc/routers/app-runs.js.map +1 -0
- package/dist/trpc/routers/applications.d.ts +1102 -0
- package/dist/trpc/routers/applications.d.ts.map +1 -1
- package/dist/trpc/routers/applications.js +5 -2
- package/dist/trpc/routers/applications.js.map +1 -1
- package/dist/trpc/routers/assets.d.ts +18 -0
- package/dist/trpc/routers/assets.d.ts.map +1 -1
- package/dist/trpc/routers/assets.js +20 -5
- package/dist/trpc/routers/assets.js.map +1 -1
- package/dist/trpc/routers/bug-reports.d.ts +33 -0
- package/dist/trpc/routers/bug-reports.d.ts.map +1 -0
- package/dist/trpc/routers/bug-reports.js +69 -0
- package/dist/trpc/routers/bug-reports.js.map +1 -0
- package/dist/trpc/routers/collections.d.ts.map +1 -1
- package/dist/trpc/routers/collections.js +16 -10
- package/dist/trpc/routers/collections.js.map +1 -1
- package/dist/trpc/routers/credits.d.ts.map +1 -1
- package/dist/trpc/routers/credits.js +11 -1
- package/dist/trpc/routers/credits.js.map +1 -1
- package/dist/trpc/routers/documents.d.ts.map +1 -1
- package/dist/trpc/routers/documents.js +8 -2
- package/dist/trpc/routers/documents.js.map +1 -1
- package/dist/trpc/routers/error-traces.d.ts +132 -0
- package/dist/trpc/routers/error-traces.d.ts.map +1 -0
- package/dist/trpc/routers/error-traces.js +118 -0
- package/dist/trpc/routers/error-traces.js.map +1 -0
- package/dist/trpc/routers/extension.d.ts +3 -3
- package/dist/trpc/routers/extension.d.ts.map +1 -1
- package/dist/trpc/routers/extension.js +13 -9
- package/dist/trpc/routers/extension.js.map +1 -1
- package/dist/trpc/routers/gameDraftHistory.d.ts +7 -0
- package/dist/trpc/routers/gameDraftHistory.d.ts.map +1 -0
- package/dist/trpc/routers/gameDraftHistory.js +40 -0
- package/dist/trpc/routers/gameDraftHistory.js.map +1 -0
- package/dist/trpc/routers/games.d.ts +59974 -5414
- package/dist/trpc/routers/games.d.ts.map +1 -1
- package/dist/trpc/routers/games.js +531 -136
- package/dist/trpc/routers/games.js.map +1 -1
- package/dist/trpc/routers/jobs.d.ts +42 -0
- package/dist/trpc/routers/jobs.d.ts.map +1 -1
- package/dist/trpc/routers/jobs.js +89 -4
- package/dist/trpc/routers/jobs.js.map +1 -1
- package/dist/trpc/routers/js-scripts.d.ts.map +1 -1
- package/dist/trpc/routers/js-scripts.js +18 -2
- package/dist/trpc/routers/js-scripts.js.map +1 -1
- package/dist/trpc/routers/mcp-config.d.ts +3 -1
- package/dist/trpc/routers/mcp-config.d.ts.map +1 -1
- package/dist/trpc/routers/mcp-config.js +46 -208
- package/dist/trpc/routers/mcp-config.js.map +1 -1
- package/dist/trpc/routers/messages.d.ts +10 -0
- package/dist/trpc/routers/messages.d.ts.map +1 -1
- package/dist/trpc/routers/messages.js +16 -2
- package/dist/trpc/routers/messages.js.map +1 -1
- package/dist/trpc/routers/models.d.ts +130 -0
- package/dist/trpc/routers/models.d.ts.map +1 -1
- package/dist/trpc/routers/models.js +119 -61
- package/dist/trpc/routers/models.js.map +1 -1
- package/dist/trpc/routers/packs.d.ts.map +1 -1
- package/dist/trpc/routers/packs.js +18 -4
- package/dist/trpc/routers/packs.js.map +1 -1
- package/dist/trpc/routers/projects.d.ts +66 -2
- package/dist/trpc/routers/projects.d.ts.map +1 -1
- package/dist/trpc/routers/projects.js +19 -2
- package/dist/trpc/routers/projects.js.map +1 -1
- package/dist/trpc/routers/runs.d.ts +472 -0
- package/dist/trpc/routers/runs.d.ts.map +1 -0
- package/dist/trpc/routers/runs.js +40 -0
- package/dist/trpc/routers/runs.js.map +1 -0
- package/dist/trpc/routers/settings.d.ts +3 -0
- package/dist/trpc/routers/settings.d.ts.map +1 -1
- package/dist/trpc/routers/settings.js +32 -5
- package/dist/trpc/routers/settings.js.map +1 -1
- package/dist/trpc/routers/sketch.d.ts +44 -0
- package/dist/trpc/routers/sketch.d.ts.map +1 -1
- package/dist/trpc/routers/sketch.js +2 -2
- package/dist/trpc/routers/sketch.js.map +1 -1
- package/dist/trpc/routers/storyboards.d.ts +903 -1
- package/dist/trpc/routers/storyboards.d.ts.map +1 -1
- package/dist/trpc/routers/storyboards.js +8 -5
- package/dist/trpc/routers/storyboards.js.map +1 -1
- package/dist/trpc/routers/threads.d.ts.map +1 -1
- package/dist/trpc/routers/threads.js +8 -2
- package/dist/trpc/routers/threads.js.map +1 -1
- package/dist/trpc/routers/timeline.d.ts +647 -63
- package/dist/trpc/routers/timeline.d.ts.map +1 -1
- package/dist/trpc/routers/timeline.js +193 -4
- package/dist/trpc/routers/timeline.js.map +1 -1
- package/dist/trpc/routers/triggers.d.ts.map +1 -1
- package/dist/trpc/routers/triggers.js +18 -3
- package/dist/trpc/routers/triggers.js.map +1 -1
- package/dist/trpc/routers/users.d.ts +3 -2
- package/dist/trpc/routers/users.d.ts.map +1 -1
- package/dist/trpc/routers/users.js +25 -27
- package/dist/trpc/routers/users.js.map +1 -1
- package/dist/trpc/routers/worker.d.ts +10 -9
- package/dist/trpc/routers/worker.d.ts.map +1 -1
- package/dist/trpc/routers/worker.js +40 -16
- package/dist/trpc/routers/worker.js.map +1 -1
- package/dist/trpc/routers/workflows.d.ts +202 -3
- package/dist/trpc/routers/workflows.d.ts.map +1 -1
- package/dist/trpc/routers/workflows.js +72 -12
- package/dist/trpc/routers/workflows.js.map +1 -1
- package/dist/trpc/sandbox-coverage.d.ts.map +1 -1
- package/dist/trpc/sandbox-coverage.js +119 -0
- package/dist/trpc/sandbox-coverage.js.map +1 -1
- package/dist/websocket-client-session.d.ts +4 -2
- package/dist/websocket-client-session.d.ts.map +1 -1
- package/dist/websocket-client-session.js +23 -3
- package/dist/websocket-client-session.js.map +1 -1
- package/dist/worker-settings-env.d.ts +23 -0
- package/dist/worker-settings-env.d.ts.map +1 -0
- package/dist/worker-settings-env.js +47 -0
- package/dist/worker-settings-env.js.map +1 -0
- package/dist/workflow-interface-service.d.ts +7 -0
- package/dist/workflow-interface-service.d.ts.map +1 -1
- package/package.json +58 -41
- package/dist/extension-cdp-bridge.d.ts +0 -85
- package/dist/extension-cdp-bridge.d.ts.map +0 -1
- package/dist/extension-cdp-bridge.js +0 -163
- package/dist/extension-cdp-bridge.js.map +0 -1
|
@@ -2,18 +2,33 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import sharp from "sharp";
|
|
5
|
-
import { getManagedWorkspaceDir, managedWorkspaceKey, workspaceStorageKind } from "@nodetool-ai/config";
|
|
6
|
-
import { AmbiguousGameIdError, Asset, Game, Project, Workspace } from "@nodetool-ai/models";
|
|
7
|
-
import { exampleGameSummary, gameAssetBinding, gameDocument, installExampleGameInput } from "@nodetool-ai/protocol
|
|
8
|
-
import { createTopDownRoomGame, gameDocumentOp, GameOpError,
|
|
5
|
+
import { createLogger, getManagedWorkspaceDir, managedWorkspaceKey, workspaceStorageKind } from "@nodetool-ai/config";
|
|
6
|
+
import { AmbiguousGameIdError, InvalidGameDocumentError, MissingGameDraftSourceError, Asset, Game, Project, Workspace } from "@nodetool-ai/models";
|
|
7
|
+
import { exampleGameSummary, anyGameAssetBinding, gameAssetBinding, gameAssetBinding3D, gameAssetSiblingRebinds, gamePreparedCollider3D, gameModelImportSettings3D, parseGameDocument, anyGameDocument as gameDocument, installExampleGameInput, stagedGameCandidateRecord, stagedGameCandidateRecordPath } from "@nodetool-ai/protocol";
|
|
8
|
+
import { createTopDownRoomGame, createNative3DGame, anyGameDocumentOp as gameDocumentOp, GameOpError, decodePreparedGameCollider3D, trackGameAuthoringEdits, validateAnyGame } from "@nodetool-ai/game-runtime";
|
|
9
|
+
import { normalizeGameModel3D, prepareGameModelBinding3D } from "@nodetool-ai/game-renderer/preparation3d";
|
|
10
|
+
import { contextSecretAvailability, createCapabilityRun, gateFromContext } from "@nodetool-ai/agents";
|
|
11
|
+
import { gameAuthoringCandidate, gameAuthoringProgram, gameAuthoringConflict } from "@nodetool-ai/protocol";
|
|
12
|
+
import { PERMISSION_GATE_CONTEXT_KEY, ProcessingContext, headlessGate } from "@nodetool-ai/runtime";
|
|
9
13
|
import { getAssetAdapter } from "../../lib/storage.js";
|
|
10
14
|
import { getAssetStorageKey, retrieveAssetBytes } from "../../lib/asset-paths.js";
|
|
11
|
-
import { installExampleGameAssets, listExampleGames } from "../../lib/example-games.js";
|
|
15
|
+
import { getExampleGameBundle, installExampleGameAssets, listExampleGames } from "../../lib/example-games.js";
|
|
12
16
|
import { workspaceFromRow } from "../../lib/workflow-workspace.js";
|
|
17
|
+
import { loadConfiguredProviders } from "../../configured-providers.js";
|
|
18
|
+
import { createRuntimeContext } from "../../session/model-interfaces.js";
|
|
13
19
|
import { ApiErrorCode } from "../../error-codes.js";
|
|
14
20
|
import { router } from "../index.js";
|
|
15
21
|
import { protectedProcedure } from "../middleware.js";
|
|
16
22
|
import { throwApiError } from "../error-formatter.js";
|
|
23
|
+
import { boundedGameDraftHistory, MAX_GAME_DRAFT_OPS } from "./gameDraftHistory.js";
|
|
24
|
+
const log = createLogger("nodetool.games");
|
|
25
|
+
const gameProcedure = protectedProcedure.use(async ({ next }) => {
|
|
26
|
+
const result = await next();
|
|
27
|
+
if (!result.ok && result.error.cause instanceof MissingGameDraftSourceError) {
|
|
28
|
+
throwApiError(ApiErrorCode.SERVICE_UNAVAILABLE, result.error.cause.message, "PRECONDITION_FAILED");
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
});
|
|
17
32
|
const idInput = z.object({ id: z.string() });
|
|
18
33
|
const gameInfo = z.object({
|
|
19
34
|
id: z.string(),
|
|
@@ -27,6 +42,35 @@ const gameInfo = z.object({
|
|
|
27
42
|
updatedAt: z.string()
|
|
28
43
|
});
|
|
29
44
|
const gameWithDocument = z.object({ game: gameInfo, document: gameDocument });
|
|
45
|
+
const authoringPreview = z.object({
|
|
46
|
+
candidate: gameAuthoringCandidate,
|
|
47
|
+
document: gameDocument,
|
|
48
|
+
conflicts: z.array(gameAuthoringConflict),
|
|
49
|
+
affected_entities: z.array(z.string()),
|
|
50
|
+
changed_dependencies: z.array(z.string()),
|
|
51
|
+
restart_required: z.boolean()
|
|
52
|
+
});
|
|
53
|
+
const assetBrowserInfo = z.object({
|
|
54
|
+
dimension: z.enum(["2d", "3d"]),
|
|
55
|
+
draft_updated_at: z.string(),
|
|
56
|
+
candidate_workspace_id: z.string(),
|
|
57
|
+
candidates: z.array(z.object({
|
|
58
|
+
digest: z.string(), extension: z.string(), media_kind: z.enum(["image", "audio", "font", "model", "collider"]), path: z.string(),
|
|
59
|
+
size: z.number(), modified_at: z.string(), bound_slots: z.array(z.string()), slot: z.string().optional(), prompt: z.string().optional(),
|
|
60
|
+
source: z.string().optional(), recorded: z.boolean()
|
|
61
|
+
})),
|
|
62
|
+
slot_requests: z.record(z.string(), z.object({ kind: z.string(), prompt: z.string().optional(),
|
|
63
|
+
preparation: z.record(z.string(), z.unknown()).optional(), source: z.string() }))
|
|
64
|
+
});
|
|
65
|
+
const assetFrame = z.object({ x: z.number(), y: z.number(), width: z.number(), height: z.number() });
|
|
66
|
+
const generatedAssetResult = z.object({
|
|
67
|
+
error: z.string().optional(),
|
|
68
|
+
binding: anyGameAssetBinding.optional(),
|
|
69
|
+
frames: z.array(assetFrame).optional(),
|
|
70
|
+
tiles: z.array(z.object({ mask: z.number().int(), frame: assetFrame })).optional()
|
|
71
|
+
}).loose();
|
|
72
|
+
const stagedCandidateBinding = z.object({ error: z.string().optional(), binding: anyGameAssetBinding.optional() }).loose();
|
|
73
|
+
const generatedAsset = gameWithDocument.extend({ stagedDigest: z.string().nullable(), installed: z.boolean() });
|
|
30
74
|
const gameRevisionInfo = z.object({ revision: z.string(), modifiedAt: z.number(), current: z.boolean(), message: z.string().nullable() });
|
|
31
75
|
function info(game) {
|
|
32
76
|
return {
|
|
@@ -44,6 +88,28 @@ function info(game) {
|
|
|
44
88
|
function newRevision() {
|
|
45
89
|
return randomUUID().replace(/-/g, "");
|
|
46
90
|
}
|
|
91
|
+
function authoringRun(userId) {
|
|
92
|
+
const context = new ProcessingContext({ jobId: `game-authoring-${randomUUID()}`, userId });
|
|
93
|
+
context.set(PERMISSION_GATE_CONTEXT_KEY, headlessGate("game.authoring"));
|
|
94
|
+
return createCapabilityRun({ context, gate: gateFromContext(context, "game.authoring") });
|
|
95
|
+
}
|
|
96
|
+
/** A run that can reach the user's configured media providers, for editor-initiated asset generation. */
|
|
97
|
+
async function assetGenerationRun(userId, game, workspace) {
|
|
98
|
+
const context = createRuntimeContext({ jobId: `game-asset-${randomUUID()}`, userId, projectId: game.project_id, workspace });
|
|
99
|
+
context.set(PERMISSION_GATE_CONTEXT_KEY, headlessGate("game.assets"));
|
|
100
|
+
return createCapabilityRun({ context, gate: gateFromContext(context, "game.assets"), projectId: game.project_id,
|
|
101
|
+
availableSecrets: contextSecretAvailability(context), providers: await loadConfiguredProviders(userId) });
|
|
102
|
+
}
|
|
103
|
+
function checkAuthoringResult(result) {
|
|
104
|
+
if (result !== null && typeof result === "object" && "error" in result && typeof result.error === "string") {
|
|
105
|
+
if ("code" in result && result.code === "game_draft_source_unavailable") {
|
|
106
|
+
throwApiError(ApiErrorCode.SERVICE_UNAVAILABLE, result.error, "PRECONDITION_FAILED");
|
|
107
|
+
}
|
|
108
|
+
const code = /stale|concurrent|changed since/i.test(result.error)
|
|
109
|
+
? ApiErrorCode.ALREADY_EXISTS : ApiErrorCode.INVALID_INPUT;
|
|
110
|
+
throwApiError(code, result.error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
47
113
|
function sourcePath(game, revision) {
|
|
48
114
|
return `${game.source_root}/revisions/${revision}/game.json`;
|
|
49
115
|
}
|
|
@@ -74,6 +140,40 @@ async function gameWorkspace(userId, game) {
|
|
|
74
140
|
}
|
|
75
141
|
return workspace;
|
|
76
142
|
}
|
|
143
|
+
const CONCURRENT_DRAFT_EDIT = "Game draft was modified concurrently";
|
|
144
|
+
async function persistGameAsset(userId, game, workspace, draftUpdatedAt, slot, binding, bytes, extension, contentType, siblingBindings) {
|
|
145
|
+
await workspace.write(`${game.source_root}/assets/${binding.digest}.${extension}`, bytes, contentType);
|
|
146
|
+
const installed = await Asset.create({
|
|
147
|
+
user_id: userId, project_id: game.project_id, parent_id: userId,
|
|
148
|
+
name: `${slot}.${extension}`, content_type: contentType, size: bytes.byteLength,
|
|
149
|
+
metadata: { game_source_asset_id: binding.mediaKind === "model" ? binding.sourceAssetId ?? binding.assetId : binding.assetId,
|
|
150
|
+
game_digest: binding.digest }
|
|
151
|
+
});
|
|
152
|
+
if (!(installed instanceof Asset)) {
|
|
153
|
+
throwApiError(ApiErrorCode.INTERNAL_ERROR, "Asset creation failed");
|
|
154
|
+
}
|
|
155
|
+
const storage = getAssetAdapter();
|
|
156
|
+
let uri = null;
|
|
157
|
+
try {
|
|
158
|
+
uri = await storage.store(getAssetStorageKey(userId, installed.id, contentType), bytes, contentType);
|
|
159
|
+
const op = gameDocumentOp.parse({ op: "bind_asset", slot, binding: { ...binding, assetId: installed.id } });
|
|
160
|
+
const parent = siblingBindings ? gameAssetBinding.safeParse({ ...binding, assetId: installed.id }) : null;
|
|
161
|
+
const siblings = siblingBindings && parent?.success
|
|
162
|
+
? Object.entries(siblingBindings(parent.data)).map(([sibling, siblingBinding]) => gameDocumentOp.parse({ op: "bind_asset", slot: sibling, binding: siblingBinding })) : [];
|
|
163
|
+
const saved = await Game.updateDraft(userId, game.id, draftUpdatedAt, [op, ...siblings], workspace, { actor: "user" });
|
|
164
|
+
if (!saved) {
|
|
165
|
+
throwApiError(ApiErrorCode.ALREADY_EXISTS, CONCURRENT_DRAFT_EDIT);
|
|
166
|
+
}
|
|
167
|
+
return { game: info(saved.game), document: saved.document };
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (uri) {
|
|
171
|
+
await storage.delete(uri);
|
|
172
|
+
}
|
|
173
|
+
await installed.delete();
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
77
177
|
async function projectWorkspace(userId, projectId) {
|
|
78
178
|
const rows = await Workspace.listByProject(userId, projectId);
|
|
79
179
|
const existing = rows.find((row) => row.isAccessible() && (process.env["NODETOOL_ENV"] !== "production" || row.isManaged()));
|
|
@@ -105,10 +205,14 @@ async function projectWorkspace(userId, projectId) {
|
|
|
105
205
|
}));
|
|
106
206
|
}
|
|
107
207
|
function validatedDocument(value, id, revision) {
|
|
108
|
-
const
|
|
109
|
-
|
|
208
|
+
const parsed = parseGameDocument(value);
|
|
209
|
+
if (!parsed.ok) {
|
|
210
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, parsed.diagnostics.map((issue) => `${issue.code} ${issue.path.join(".")}: ${issue.message}`).join("; "));
|
|
211
|
+
}
|
|
212
|
+
const source = parsed.document;
|
|
213
|
+
const result = validateAnyGame({ ...source, id, revision });
|
|
110
214
|
if (!result.valid || !result.document) {
|
|
111
|
-
throwApiError(ApiErrorCode.INVALID_INPUT, result.
|
|
215
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, result.diagnostics.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; "));
|
|
112
216
|
}
|
|
113
217
|
return result.document;
|
|
114
218
|
}
|
|
@@ -129,7 +233,7 @@ async function readRevision(workspace, game, revision) {
|
|
|
129
233
|
async function writeRevision(workspace, game, document) {
|
|
130
234
|
await workspace.write(sourcePath(game, document.revision), JSON.stringify(document), "application/json");
|
|
131
235
|
}
|
|
132
|
-
async function publishDocument(userId, game, baseRevision, value, message) {
|
|
236
|
+
async function publishDocument(userId, game, baseRevision, value, message, restoreAuthoring = false, expectedDraftUpdatedAt, expectedDigest) {
|
|
133
237
|
if (baseRevision !== game.current_revision) {
|
|
134
238
|
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game was modified concurrently");
|
|
135
239
|
}
|
|
@@ -138,21 +242,52 @@ async function publishDocument(userId, game, baseRevision, value, message) {
|
|
|
138
242
|
if (!draft) {
|
|
139
243
|
throwApiError(ApiErrorCode.NOT_FOUND, "Game draft not found");
|
|
140
244
|
}
|
|
245
|
+
if ((value !== undefined || restoreAuthoring) && !expectedDraftUpdatedAt) {
|
|
246
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "A draft token is required for publish and restore");
|
|
247
|
+
}
|
|
248
|
+
if (expectedDraftUpdatedAt !== undefined && expectedDraftUpdatedAt !== draft.game.draft_updated_at) {
|
|
249
|
+
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game draft was modified concurrently");
|
|
250
|
+
}
|
|
251
|
+
const candidate = validatedDocument(value ?? draft.document, game.id, draft.game.current_revision);
|
|
252
|
+
const digest = createHash("sha256").update(JSON.stringify(candidate)).digest("hex");
|
|
253
|
+
if (expectedDigest !== undefined && digest !== expectedDigest) {
|
|
254
|
+
throwApiError(ApiErrorCode.ALREADY_EXISTS, "The validated game document changed before publish");
|
|
255
|
+
}
|
|
141
256
|
const revision = newRevision();
|
|
142
|
-
|
|
257
|
+
let document = { ...candidate, revision };
|
|
258
|
+
if (!restoreAuthoring && value !== undefined && JSON.stringify(document.authoring) !== JSON.stringify(draft.document.authoring)) {
|
|
259
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "Retained construction must be changed through preview and apply");
|
|
260
|
+
}
|
|
261
|
+
if (!restoreAuthoring && draft.document.authoring) {
|
|
262
|
+
document = trackGameAuthoringEdits(draft.document, document);
|
|
263
|
+
}
|
|
264
|
+
if ((document.schemaVersion === 3) !== (draft.document.schemaVersion === 3)) {
|
|
265
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "A game cannot change dimension");
|
|
266
|
+
}
|
|
143
267
|
await writeRevision(workspace, game, document);
|
|
144
268
|
const updated = await Game.publish(userId, game.id, baseRevision, revision, draft.game.draft_updated_at, workspace, message);
|
|
145
269
|
if (!updated) {
|
|
146
|
-
|
|
270
|
+
try {
|
|
271
|
+
await workspace.delete(sourcePath(game, revision));
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
log.error("Losing game publication revision cleanup failed", { gameId: game.id, revision, error: String(error) });
|
|
275
|
+
}
|
|
147
276
|
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game was modified concurrently");
|
|
148
277
|
}
|
|
149
|
-
|
|
150
|
-
|
|
278
|
+
try {
|
|
279
|
+
if (draft.game.draft_version_id) {
|
|
280
|
+
await workspace.delete(`${game.source_root}/drafts/${draft.game.draft_version_id}.json`);
|
|
281
|
+
}
|
|
282
|
+
await workspace.write(`${game.source_root}/draft.json`, JSON.stringify(document), "application/json");
|
|
283
|
+
await Game.pruneRevisionFiles(userId, game.id, workspace);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
log.error("Game post-publish cleanup failed", { gameId: game.id, error: String(error) });
|
|
151
287
|
}
|
|
152
|
-
await workspace.write(`${game.source_root}/draft.json`, JSON.stringify(document), "application/json");
|
|
153
288
|
return { game: info(updated), document };
|
|
154
289
|
}
|
|
155
|
-
async function createGame(userId, projectId, name, source) {
|
|
290
|
+
async function createGame(userId, projectId, name, source, dimension = "2d") {
|
|
156
291
|
if (!(await Project.findOwned(userId, projectId))) {
|
|
157
292
|
throwApiError(ApiErrorCode.NOT_FOUND, "Project not found");
|
|
158
293
|
}
|
|
@@ -163,7 +298,10 @@ async function createGame(userId, projectId, name, source) {
|
|
|
163
298
|
}
|
|
164
299
|
const id = randomUUID().replace(/-/g, "");
|
|
165
300
|
const revision = newRevision();
|
|
166
|
-
const document = validatedDocument(source ?? createTopDownRoomGame(id), id, revision);
|
|
301
|
+
const document = validatedDocument(source ?? (dimension === "3d" ? createNative3DGame(id) : createTopDownRoomGame(id)), id, revision);
|
|
302
|
+
if (document.authoring) {
|
|
303
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "Attach retained construction through preview and apply after creating the game");
|
|
304
|
+
}
|
|
167
305
|
const candidate = new Game({
|
|
168
306
|
id,
|
|
169
307
|
user_id: userId,
|
|
@@ -185,8 +323,167 @@ async function createGame(userId, projectId, name, source) {
|
|
|
185
323
|
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game already exists");
|
|
186
324
|
return { game: info(game), document };
|
|
187
325
|
}
|
|
326
|
+
const installCandidateInput = idInput.extend({
|
|
327
|
+
baseRevision: z.string().optional(),
|
|
328
|
+
baseUpdatedAt: z.string().optional(),
|
|
329
|
+
slot: z.string().min(1),
|
|
330
|
+
candidateWorkspaceId: z.string().optional(),
|
|
331
|
+
binding: anyGameAssetBinding
|
|
332
|
+
});
|
|
333
|
+
function siblingBindingsFor(slot, assets, options) {
|
|
334
|
+
const { frames, tiles } = options;
|
|
335
|
+
if (frames?.length) {
|
|
336
|
+
return (installed) => Object.fromEntries(frames.map((frame, index) => [`${slot}.frame.${index}`, { ...installed, frame }]));
|
|
337
|
+
}
|
|
338
|
+
if (tiles?.length) {
|
|
339
|
+
return (installed) => Object.fromEntries(tiles.map((tile) => [`${slot}.tile.${tile.mask}`, { ...installed, frame: tile.frame }]));
|
|
340
|
+
}
|
|
341
|
+
if (!options.replaceInPlace) {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
return (installed) => Object.fromEntries(gameAssetSiblingRebinds(assets, slot, installed).ops.map((op) => [op.slot, op.binding]));
|
|
345
|
+
}
|
|
346
|
+
/** Install a staged candidate's bytes into the draft, with the sibling bindings `options` names. */
|
|
347
|
+
async function installCandidateBinding(userId, input, options = {}) {
|
|
348
|
+
const ctx = { userId };
|
|
349
|
+
const game = await ownedGame(ctx.userId, input.id);
|
|
350
|
+
const workspace = await gameWorkspace(ctx.userId, game);
|
|
351
|
+
const candidateRow = input.candidateWorkspaceId
|
|
352
|
+
? await Workspace.find(ctx.userId, input.candidateWorkspaceId)
|
|
353
|
+
: null;
|
|
354
|
+
if (input.candidateWorkspaceId && (!candidateRow || candidateRow.project_id !== game.project_id)) {
|
|
355
|
+
throwApiError(ApiErrorCode.NOT_FOUND, "Candidate workspace not found");
|
|
356
|
+
}
|
|
357
|
+
const candidateWorkspace = candidateRow ? workspaceFromRow(candidateRow) : workspace;
|
|
358
|
+
if (!candidateWorkspace) {
|
|
359
|
+
throwApiError(ApiErrorCode.SERVICE_UNAVAILABLE, "Candidate workspace storage is unavailable");
|
|
360
|
+
}
|
|
361
|
+
const digest = input.binding.digest;
|
|
362
|
+
if (!/^[a-f0-9]{64}$/.test(digest)) {
|
|
363
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "Invalid candidate digest");
|
|
364
|
+
}
|
|
365
|
+
if (input.binding.mediaKind === "font" && !input.binding.fontFormat) {
|
|
366
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "Font binding needs a TrueType or OpenType format");
|
|
367
|
+
}
|
|
368
|
+
if (input.binding.mediaKind === "hdri") {
|
|
369
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "HDRI candidates cannot be installed until HDRI preparation verifies their bytes and dimensions");
|
|
370
|
+
}
|
|
371
|
+
const formats = input.binding.mediaKind === "model"
|
|
372
|
+
? [["glb", "model/gltf-binary"]]
|
|
373
|
+
: input.binding.mediaKind === "collider"
|
|
374
|
+
? [["json", "application/json"]]
|
|
375
|
+
: input.binding.mediaKind === "audio"
|
|
376
|
+
? [["wav", "audio/wav"], ["mp3", "audio/mpeg"], ["ogg", "audio/ogg"]]
|
|
377
|
+
: input.binding.mediaKind === "font"
|
|
378
|
+
? [[input.binding.fontFormat, `font/${input.binding.fontFormat}`]]
|
|
379
|
+
: [["png", "image/png"], ["jpg", "image/jpeg"], ["webp", "image/webp"]];
|
|
380
|
+
let candidate = null;
|
|
381
|
+
for (const [extension, contentType] of formats) {
|
|
382
|
+
if (!extension || !contentType)
|
|
383
|
+
continue;
|
|
384
|
+
const path = `${game.source_root}/assets/${digest}.${extension}`;
|
|
385
|
+
const bytes = await candidateWorkspace.read(path);
|
|
386
|
+
if (bytes) {
|
|
387
|
+
candidate = { extension, contentType, bytes };
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (!candidate || createHash("sha256").update(candidate.bytes).digest("hex") !== digest) {
|
|
392
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "Candidate asset is missing or changed");
|
|
393
|
+
}
|
|
394
|
+
const { extension, contentType, bytes } = candidate;
|
|
395
|
+
let binding = input.binding;
|
|
396
|
+
if (binding.mediaKind === "model") {
|
|
397
|
+
const prepared = await prepareGameModelBinding3D(bytes, { assetId: binding.assetId, expectedDigest: digest });
|
|
398
|
+
if (!prepared.ok) {
|
|
399
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, prepared.diagnostics.map((issue) => issue.message).join("; "));
|
|
400
|
+
}
|
|
401
|
+
const sourceAsset = binding.sourceAssetId ? await Asset.find(ctx.userId, binding.sourceAssetId) : null;
|
|
402
|
+
if (binding.sourceAssetId && !sourceAsset) {
|
|
403
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "Model source asset is not owned");
|
|
404
|
+
}
|
|
405
|
+
const canonical = { ...prepared.binding, required: binding.required };
|
|
406
|
+
if (binding.provenance) {
|
|
407
|
+
canonical.provenance = binding.provenance;
|
|
408
|
+
}
|
|
409
|
+
if (binding.importSettings) {
|
|
410
|
+
canonical.importSettings = binding.importSettings;
|
|
411
|
+
}
|
|
412
|
+
if (binding.sourceDigest) {
|
|
413
|
+
canonical.sourceDigest = binding.sourceDigest;
|
|
414
|
+
}
|
|
415
|
+
if (sourceAsset) {
|
|
416
|
+
canonical.sourceAssetId = sourceAsset.id;
|
|
417
|
+
}
|
|
418
|
+
binding = canonical;
|
|
419
|
+
}
|
|
420
|
+
else if (binding.mediaKind === "collider") {
|
|
421
|
+
let geometry;
|
|
422
|
+
try {
|
|
423
|
+
geometry = JSON.parse(new TextDecoder().decode(bytes));
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "Prepared collider is not valid JSON");
|
|
427
|
+
}
|
|
428
|
+
const parsed = gamePreparedCollider3D.safeParse(geometry);
|
|
429
|
+
if (!parsed.success || binding.shape === "triangleMesh" && !parsed.data.indices?.length) {
|
|
430
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "Prepared collider has invalid vertices or triangle indices");
|
|
431
|
+
}
|
|
432
|
+
const { vertices, indices } = parsed.data;
|
|
433
|
+
const minimum = { x: Infinity, y: Infinity, z: Infinity };
|
|
434
|
+
const maximum = { x: -Infinity, y: -Infinity, z: -Infinity };
|
|
435
|
+
for (let index = 0; index < vertices.length; index += 3) {
|
|
436
|
+
minimum.x = Math.min(minimum.x, vertices[index]);
|
|
437
|
+
maximum.x = Math.max(maximum.x, vertices[index]);
|
|
438
|
+
minimum.y = Math.min(minimum.y, vertices[index + 1]);
|
|
439
|
+
maximum.y = Math.max(maximum.y, vertices[index + 1]);
|
|
440
|
+
minimum.z = Math.min(minimum.z, vertices[index + 2]);
|
|
441
|
+
maximum.z = Math.max(maximum.z, vertices[index + 2]);
|
|
442
|
+
}
|
|
443
|
+
binding = { ...binding, preparationVersion: "1", bounds: { min: minimum, max: maximum }, vertices: vertices.length / 3, triangles: (indices?.length ?? 0) / 3 };
|
|
444
|
+
try {
|
|
445
|
+
await decodePreparedGameCollider3D(bytes, gameAssetBinding3D.options[1].parse(binding));
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, error instanceof Error ? error.message : "Prepared collider is invalid");
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (input.baseRevision && game.current_revision !== input.baseRevision) {
|
|
452
|
+
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game was modified concurrently");
|
|
453
|
+
}
|
|
454
|
+
const draft = await Game.readDraft(ctx.userId, game.id, workspace);
|
|
455
|
+
if (!draft)
|
|
456
|
+
throwApiError(ApiErrorCode.NOT_FOUND, "Game draft not found");
|
|
457
|
+
if (input.baseUpdatedAt && draft.game.draft_updated_at !== input.baseUpdatedAt) {
|
|
458
|
+
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game draft was modified concurrently");
|
|
459
|
+
}
|
|
460
|
+
const targetBinding = draft.document.schemaVersion === 3 ? gameAssetBinding3D.safeParse(binding) :
|
|
461
|
+
gameAssetBinding.safeParse(binding);
|
|
462
|
+
if (!targetBinding.success || draft.document.schemaVersion !== 3 && (binding.mediaKind === "model" || binding.mediaKind === "collider")) {
|
|
463
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "Asset binding does not match the game dimension");
|
|
464
|
+
}
|
|
465
|
+
return persistGameAsset(ctx.userId, game, workspace, draft.game.draft_updated_at, input.slot, targetBinding.data, bytes, extension, contentType, draft.document.schemaVersion !== 3 ? siblingBindingsFor(input.slot, draft.document.assets, options) : undefined);
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Install into whichever draft is current when the bytes are ready. An editor
|
|
469
|
+
* session saves while a generation runs, so a slot binding must not depend on
|
|
470
|
+
* the draft token the request started from. The write itself stays a
|
|
471
|
+
* compare-and-swap, retried when a save lands between the read and the write.
|
|
472
|
+
*/
|
|
473
|
+
async function installOnLatestDraft(userId, input, options) {
|
|
474
|
+
for (let attempt = 1;; attempt++) {
|
|
475
|
+
try {
|
|
476
|
+
return await installCandidateBinding(userId, input, options);
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
if (attempt >= 3 || !(error instanceof Error) || error.message !== CONCURRENT_DRAFT_EDIT) {
|
|
480
|
+
throw error;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
188
485
|
export const gamesRouter = router({
|
|
189
|
-
list:
|
|
486
|
+
list: gameProcedure
|
|
190
487
|
.input(z.object({ projectId: z.string() }))
|
|
191
488
|
.output(z.array(gameInfo))
|
|
192
489
|
.query(async ({ ctx, input }) => {
|
|
@@ -195,14 +492,29 @@ export const gamesRouter = router({
|
|
|
195
492
|
}
|
|
196
493
|
return (await Game.listByProject(ctx.userId, input.projectId)).map(info);
|
|
197
494
|
}),
|
|
198
|
-
create:
|
|
199
|
-
.input(z.object({ projectId: z.string(), name: z.string().min(1).max(200), document: gameDocument.optional() }))
|
|
495
|
+
create: gameProcedure
|
|
496
|
+
.input(z.object({ projectId: z.string(), name: z.string().min(1).max(200), dimension: z.enum(["2d", "3d"]).optional(), document: gameDocument.optional() }))
|
|
200
497
|
.output(gameWithDocument)
|
|
201
|
-
.mutation(({ ctx, input }) =>
|
|
202
|
-
|
|
498
|
+
.mutation(({ ctx, input }) => {
|
|
499
|
+
if (input.dimension && input.document && ((input.document.schemaVersion === 3) !== (input.dimension === "3d"))) {
|
|
500
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "Requested dimension does not match the supplied game document");
|
|
501
|
+
}
|
|
502
|
+
return createGame(ctx.userId, input.projectId, input.name, input.document, input.dimension);
|
|
503
|
+
}),
|
|
504
|
+
examples: gameProcedure
|
|
203
505
|
.output(z.array(exampleGameSummary))
|
|
204
506
|
.query(({ ctx }) => listExampleGames(ctx.apiOptions)),
|
|
205
|
-
|
|
507
|
+
example: gameProcedure
|
|
508
|
+
.input(z.object({ slug: z.string().min(1) }))
|
|
509
|
+
.output(z.object({ name: z.string(), document: gameDocument }))
|
|
510
|
+
.query(({ ctx, input }) => {
|
|
511
|
+
const bundle = getExampleGameBundle(ctx.apiOptions, input.slug);
|
|
512
|
+
if (!bundle) {
|
|
513
|
+
throwApiError(ApiErrorCode.NOT_FOUND, "Example game not found");
|
|
514
|
+
}
|
|
515
|
+
return { name: bundle.name, document: bundle.document };
|
|
516
|
+
}),
|
|
517
|
+
installExample: gameProcedure
|
|
206
518
|
.input(installExampleGameInput)
|
|
207
519
|
.output(gameWithDocument)
|
|
208
520
|
.mutation(async ({ ctx, input }) => {
|
|
@@ -222,7 +534,7 @@ export const gamesRouter = router({
|
|
|
222
534
|
throw error;
|
|
223
535
|
}
|
|
224
536
|
}),
|
|
225
|
-
get:
|
|
537
|
+
get: gameProcedure
|
|
226
538
|
.input(idInput.extend({ revision: z.string().optional() }))
|
|
227
539
|
.output(gameWithDocument)
|
|
228
540
|
.query(async ({ ctx, input }) => {
|
|
@@ -230,7 +542,7 @@ export const gamesRouter = router({
|
|
|
230
542
|
const workspace = await gameWorkspace(ctx.userId, game);
|
|
231
543
|
return { game: info(game), document: await readRevision(workspace, game, input.revision ?? game.current_revision) };
|
|
232
544
|
}),
|
|
233
|
-
getDraft:
|
|
545
|
+
getDraft: gameProcedure
|
|
234
546
|
.input(idInput)
|
|
235
547
|
.output(gameWithDocument)
|
|
236
548
|
.query(async ({ ctx, input }) => {
|
|
@@ -240,8 +552,8 @@ export const gamesRouter = router({
|
|
|
240
552
|
throwApiError(ApiErrorCode.NOT_FOUND, "Game draft not found");
|
|
241
553
|
return { game: info(draft.game), document: draft.document };
|
|
242
554
|
}),
|
|
243
|
-
saveDraft:
|
|
244
|
-
.input(idInput.extend({ baseUpdatedAt: z.string(), ops: z.array(gameDocumentOp).min(1) }))
|
|
555
|
+
saveDraft: gameProcedure
|
|
556
|
+
.input(idInput.extend({ baseUpdatedAt: z.string(), ops: z.array(gameDocumentOp).min(1).max(MAX_GAME_DRAFT_OPS) }))
|
|
245
557
|
.output(gameWithDocument)
|
|
246
558
|
.mutation(async ({ ctx, input }) => {
|
|
247
559
|
const game = await ownedGame(ctx.userId, input.id);
|
|
@@ -255,10 +567,59 @@ export const gamesRouter = router({
|
|
|
255
567
|
if (error instanceof GameOpError) {
|
|
256
568
|
throwApiError(ApiErrorCode.INVALID_INPUT, `Op ${error.opIndex}: ${error.path}: ${error.message}`);
|
|
257
569
|
}
|
|
570
|
+
if (error instanceof InvalidGameDocumentError) {
|
|
571
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, error.message);
|
|
572
|
+
}
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
}),
|
|
576
|
+
saveDraftDocument: gameProcedure
|
|
577
|
+
.input(idInput.extend({ baseUpdatedAt: z.string(), document: gameDocument }))
|
|
578
|
+
.output(gameWithDocument)
|
|
579
|
+
.mutation(async ({ ctx, input }) => {
|
|
580
|
+
const game = await ownedGame(ctx.userId, input.id);
|
|
581
|
+
try {
|
|
582
|
+
const saved = await Game.saveDraftDocument(ctx.userId, game.id, input.baseUpdatedAt, input.document, await gameWorkspace(ctx.userId, game));
|
|
583
|
+
if (!saved)
|
|
584
|
+
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game draft was modified concurrently");
|
|
585
|
+
return { game: info(saved.game), document: saved.document };
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
if (error instanceof InvalidGameDocumentError) {
|
|
589
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, error.message);
|
|
590
|
+
}
|
|
258
591
|
throw error;
|
|
259
592
|
}
|
|
260
593
|
}),
|
|
261
|
-
|
|
594
|
+
previewAuthoring: gameProcedure
|
|
595
|
+
.input(idInput.extend({ program: gameAuthoringProgram.optional() }).strict())
|
|
596
|
+
.output(authoringPreview)
|
|
597
|
+
.mutation(async ({ ctx, input }) => {
|
|
598
|
+
const game = await ownedGame(ctx.userId, input.id);
|
|
599
|
+
const args = { game_id: game.id };
|
|
600
|
+
if (input.program) {
|
|
601
|
+
args.program = input.program;
|
|
602
|
+
}
|
|
603
|
+
const result = await authoringRun(ctx.userId).invoke("preview_native_game_authoring", args);
|
|
604
|
+
checkAuthoringResult(result);
|
|
605
|
+
return authoringPreview.parse(result);
|
|
606
|
+
}),
|
|
607
|
+
applyAuthoring: gameProcedure
|
|
608
|
+
.input(idInput.extend({ candidate: gameAuthoringCandidate }).strict())
|
|
609
|
+
.output(gameWithDocument)
|
|
610
|
+
.mutation(async ({ ctx, input }) => {
|
|
611
|
+
const game = await ownedGame(ctx.userId, input.id);
|
|
612
|
+
const result = await authoringRun(ctx.userId).invoke("apply_native_game_authoring", {
|
|
613
|
+
game_id: game.id, candidate: input.candidate
|
|
614
|
+
});
|
|
615
|
+
checkAuthoringResult(result);
|
|
616
|
+
const draft = await Game.readDraft(ctx.userId, game.id, await gameWorkspace(ctx.userId, game));
|
|
617
|
+
if (!draft) {
|
|
618
|
+
throwApiError(ApiErrorCode.NOT_FOUND, "Game draft not found");
|
|
619
|
+
}
|
|
620
|
+
return { game: info(draft.game), document: draft.document };
|
|
621
|
+
}),
|
|
622
|
+
draftChanges: gameProcedure
|
|
262
623
|
.input(idInput)
|
|
263
624
|
.output(z.array(z.object({
|
|
264
625
|
id: z.string(), actor: z.enum(["agent", "user"]), threadId: z.string().nullable(),
|
|
@@ -268,9 +629,9 @@ export const gamesRouter = router({
|
|
|
268
629
|
})))
|
|
269
630
|
.query(async ({ ctx, input }) => {
|
|
270
631
|
const game = await ownedGame(ctx.userId, input.id);
|
|
271
|
-
return (await Game.listDraftChanges(ctx.userId, game.id)).map(({ gameId: _gameId, ...change }) => change);
|
|
632
|
+
return boundedGameDraftHistory((await Game.listDraftChanges(ctx.userId, game.id)).map(({ gameId: _gameId, ...change }) => change));
|
|
272
633
|
}),
|
|
273
|
-
draftBeforeChange:
|
|
634
|
+
draftBeforeChange: gameProcedure
|
|
274
635
|
.input(idInput.extend({ changeId: z.string() }))
|
|
275
636
|
.output(gameDocument)
|
|
276
637
|
.query(async ({ ctx, input }) => {
|
|
@@ -280,7 +641,7 @@ export const gamesRouter = router({
|
|
|
280
641
|
throwApiError(ApiErrorCode.NOT_FOUND, "Game draft change not found");
|
|
281
642
|
return document;
|
|
282
643
|
}),
|
|
283
|
-
revisions:
|
|
644
|
+
revisions: gameProcedure
|
|
284
645
|
.input(idInput)
|
|
285
646
|
.output(z.array(gameRevisionInfo))
|
|
286
647
|
.query(async ({ ctx, input }) => {
|
|
@@ -295,61 +656,102 @@ export const gamesRouter = router({
|
|
|
295
656
|
});
|
|
296
657
|
return revisions.sort((a, b) => b.modifiedAt - a.modifiedAt);
|
|
297
658
|
}),
|
|
298
|
-
publish:
|
|
299
|
-
.input(idInput.extend({ baseRevision: z.string(), document: gameDocument.optional(), message: z.string().trim().max(500).optional() }))
|
|
659
|
+
publish: gameProcedure
|
|
660
|
+
.input(idInput.extend({ baseRevision: z.string(), baseUpdatedAt: z.string().optional(), expectedDigest: z.string().regex(/^[a-f0-9]{64}$/).optional(), document: gameDocument.optional(), message: z.string().trim().max(500).optional() }))
|
|
300
661
|
.output(gameWithDocument)
|
|
301
|
-
.mutation(async ({ ctx, input }) => publishDocument(ctx.userId, await ownedGame(ctx.userId, input.id), input.baseRevision, input.document, input.message)),
|
|
302
|
-
restore:
|
|
303
|
-
.input(idInput.extend({ baseRevision: z.string(), revision: z.string() }))
|
|
662
|
+
.mutation(async ({ ctx, input }) => publishDocument(ctx.userId, await ownedGame(ctx.userId, input.id), input.baseRevision, input.document, input.message, false, input.baseUpdatedAt, input.expectedDigest)),
|
|
663
|
+
restore: gameProcedure
|
|
664
|
+
.input(idInput.extend({ baseRevision: z.string(), baseUpdatedAt: z.string(), revision: z.string() }))
|
|
304
665
|
.output(gameWithDocument)
|
|
305
666
|
.mutation(async ({ ctx, input }) => {
|
|
306
667
|
const game = await ownedGame(ctx.userId, input.id);
|
|
307
668
|
const workspace = await gameWorkspace(ctx.userId, game);
|
|
308
669
|
const oldDocument = await readRevision(workspace, game, input.revision);
|
|
309
|
-
return publishDocument(ctx.userId, game, input.baseRevision, oldDocument);
|
|
670
|
+
return publishDocument(ctx.userId, game, input.baseRevision, oldDocument, undefined, true, input.baseUpdatedAt);
|
|
310
671
|
}),
|
|
311
|
-
restoreDraft:
|
|
672
|
+
restoreDraft: gameProcedure
|
|
312
673
|
.input(idInput.extend({ baseUpdatedAt: z.string(), revision: z.string() }))
|
|
313
674
|
.output(gameWithDocument)
|
|
314
675
|
.mutation(async ({ ctx, input }) => {
|
|
315
676
|
const game = await ownedGame(ctx.userId, input.id);
|
|
316
677
|
const workspace = await gameWorkspace(ctx.userId, game);
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
678
|
+
try {
|
|
679
|
+
const document = await readRevision(workspace, game, input.revision);
|
|
680
|
+
const restored = await Game.replaceDraft(ctx.userId, game.id, input.baseUpdatedAt, document, workspace);
|
|
681
|
+
if (!restored)
|
|
682
|
+
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game draft was modified concurrently");
|
|
683
|
+
return { game: info(restored.game), document: restored.document };
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
if (error instanceof InvalidGameDocumentError || error instanceof z.ZodError) {
|
|
687
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, error.message);
|
|
688
|
+
}
|
|
689
|
+
throw error;
|
|
690
|
+
}
|
|
322
691
|
}),
|
|
323
|
-
installAsset:
|
|
324
|
-
.input(idInput.extend({ baseRevision: z.string().optional(), baseUpdatedAt: z.string().optional(), slot: z.string().min(1),
|
|
692
|
+
installAsset: gameProcedure
|
|
693
|
+
.input(idInput.extend({ baseRevision: z.string().optional(), baseUpdatedAt: z.string().optional(), slot: z.string().min(1),
|
|
694
|
+
assetId: z.string(), expectedDigest: z.string().optional(), importSettings: gameModelImportSettings3D.partial().optional(),
|
|
695
|
+
dependencyAssetIds: z.record(z.string(), z.string()).refine((value) => Object.keys(value).length <= 320, "Too many model dependencies").optional() }))
|
|
325
696
|
.output(gameWithDocument)
|
|
326
697
|
.mutation(async ({ ctx, input }) => {
|
|
327
698
|
const game = await ownedGame(ctx.userId, input.id);
|
|
699
|
+
const workspace = await gameWorkspace(ctx.userId, game);
|
|
700
|
+
const draft = await Game.readDraft(ctx.userId, game.id, workspace);
|
|
701
|
+
if (!draft) {
|
|
702
|
+
throwApiError(ApiErrorCode.NOT_FOUND, "Game draft not found");
|
|
703
|
+
}
|
|
704
|
+
if (input.baseRevision && game.current_revision !== input.baseRevision) {
|
|
705
|
+
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game was modified concurrently");
|
|
706
|
+
}
|
|
707
|
+
if (input.baseUpdatedAt && draft.game.draft_updated_at !== input.baseUpdatedAt) {
|
|
708
|
+
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game draft was modified concurrently");
|
|
709
|
+
}
|
|
710
|
+
const is3D = draft.document.schemaVersion === 3;
|
|
328
711
|
const asset = await Asset.find(ctx.userId, input.assetId);
|
|
329
|
-
if (!asset || !asset.content_type.startsWith("image/")) {
|
|
330
|
-
throwApiError(ApiErrorCode.NOT_FOUND, "Image asset not found");
|
|
712
|
+
if (!asset || (is3D ? !["model/gltf-binary", "model/gltf+json"].includes(asset.content_type) : !asset.content_type.startsWith("image/"))) {
|
|
713
|
+
throwApiError(ApiErrorCode.NOT_FOUND, is3D ? "Model asset not found" : "Image asset not found");
|
|
714
|
+
}
|
|
715
|
+
if (is3D && asset.size !== null && asset.size > 64 * 1024 * 1024) {
|
|
716
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "Model source exceeds import byte budget");
|
|
331
717
|
}
|
|
332
718
|
const bytes = await retrieveAssetBytes(getAssetAdapter(), ctx.userId, asset.id, asset.content_type);
|
|
333
|
-
if (!bytes)
|
|
334
|
-
throwApiError(ApiErrorCode.NOT_FOUND, "Image bytes not found");
|
|
719
|
+
if (!bytes) {
|
|
720
|
+
throwApiError(ApiErrorCode.NOT_FOUND, is3D ? "Model bytes not found" : "Image bytes not found");
|
|
721
|
+
}
|
|
335
722
|
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
336
723
|
if (input.expectedDigest && digest !== input.expectedDigest) {
|
|
337
|
-
throwApiError(ApiErrorCode.INVALID_INPUT, "Image content digest changed");
|
|
724
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, is3D ? "Model content digest changed" : "Image content digest changed");
|
|
725
|
+
}
|
|
726
|
+
if (draft.document.schemaVersion === 3) {
|
|
727
|
+
const normalized = await normalizeGameModel3D(bytes, {
|
|
728
|
+
assetId: asset.id, sourceAssetId: asset.id, importSettings: input.importSettings,
|
|
729
|
+
provenance: asset.workflow_id ?? undefined,
|
|
730
|
+
resolveDependency: async (uri) => {
|
|
731
|
+
const dependencyId = input.dependencyAssetIds && Object.hasOwn(input.dependencyAssetIds, uri)
|
|
732
|
+
? input.dependencyAssetIds[uri] : undefined;
|
|
733
|
+
if (!dependencyId) {
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
const dependency = await Asset.find(ctx.userId, dependencyId);
|
|
737
|
+
if (!dependency) {
|
|
738
|
+
throwApiError(ApiErrorCode.NOT_FOUND, "Model dependency asset not found");
|
|
739
|
+
}
|
|
740
|
+
if (dependency.size !== null && dependency.size > 64 * 1024 * 1024) {
|
|
741
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "Model dependency exceeds import byte budget");
|
|
742
|
+
}
|
|
743
|
+
return retrieveAssetBytes(getAssetAdapter(), ctx.userId, dependency.id, dependency.content_type);
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
if (!normalized.ok) {
|
|
747
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, normalized.diagnostics.map((issue) => issue.message).join("; "));
|
|
748
|
+
}
|
|
749
|
+
return persistGameAsset(ctx.userId, game, workspace, draft.game.draft_updated_at, input.slot, normalized.binding, normalized.bytes, "glb", "model/gltf-binary");
|
|
338
750
|
}
|
|
339
751
|
const metadata = await sharp(bytes).metadata();
|
|
340
752
|
if (!metadata.width || !metadata.height) {
|
|
341
753
|
throwApiError(ApiErrorCode.INVALID_INPUT, "Image dimensions are unavailable");
|
|
342
754
|
}
|
|
343
|
-
const workspace = await gameWorkspace(ctx.userId, game);
|
|
344
|
-
const draft = await Game.readDraft(ctx.userId, game.id, workspace);
|
|
345
|
-
if (!draft)
|
|
346
|
-
throwApiError(ApiErrorCode.NOT_FOUND, "Game draft not found");
|
|
347
|
-
if (input.baseRevision && game.current_revision !== input.baseRevision) {
|
|
348
|
-
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game was modified concurrently");
|
|
349
|
-
}
|
|
350
|
-
if (input.baseUpdatedAt && draft.game.draft_updated_at !== input.baseUpdatedAt) {
|
|
351
|
-
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game draft was modified concurrently");
|
|
352
|
-
}
|
|
353
755
|
const saved = await Game.updateDraft(ctx.userId, game.id, draft.game.draft_updated_at, [{
|
|
354
756
|
op: "bind_asset", slot: input.slot,
|
|
355
757
|
binding: {
|
|
@@ -359,98 +761,91 @@ export const gamesRouter = router({
|
|
|
359
761
|
provenance: asset.workflow_id ?? undefined
|
|
360
762
|
}
|
|
361
763
|
}], workspace, { actor: "user" });
|
|
362
|
-
if (!saved)
|
|
764
|
+
if (!saved) {
|
|
363
765
|
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game draft was modified concurrently");
|
|
766
|
+
}
|
|
364
767
|
return { game: info(saved.game), document: saved.document };
|
|
365
768
|
}),
|
|
366
|
-
installCandidate:
|
|
769
|
+
installCandidate: gameProcedure
|
|
770
|
+
.input(installCandidateInput)
|
|
771
|
+
.output(gameWithDocument)
|
|
772
|
+
.mutation(async ({ ctx, input }) => installCandidateBinding(ctx.userId, input)),
|
|
773
|
+
/** Staged candidates and per-slot generation requests for the asset browser. The catalog itself is computed client-side from the draft. */
|
|
774
|
+
assetBrowser: gameProcedure
|
|
775
|
+
.input(idInput)
|
|
776
|
+
.output(assetBrowserInfo)
|
|
777
|
+
.query(async ({ ctx, input }) => {
|
|
778
|
+
const game = await ownedGame(ctx.userId, input.id);
|
|
779
|
+
const result = await authoringRun(ctx.userId).invoke("browse_native_game_assets", { game_id: game.id });
|
|
780
|
+
checkAuthoringResult(result);
|
|
781
|
+
return assetBrowserInfo.parse(result);
|
|
782
|
+
}),
|
|
783
|
+
/**
|
|
784
|
+
* Generate an asset for one slot through `generate_game_asset`, record the
|
|
785
|
+
* prompt beside the staged bytes, and bind it in place on the draft that is
|
|
786
|
+
* current when the bytes are ready. The editor keeps saving while a
|
|
787
|
+
* generation runs, so the request carries no draft token: a sheet's frame
|
|
788
|
+
* bindings, or the slot's existing frame and tile bindings, are written in
|
|
789
|
+
* the same compare-and-swap as the slot, as the user who asked. A
|
|
790
|
+
* provider and model pin the generator; without them the capability picks one.
|
|
791
|
+
*/
|
|
792
|
+
generateAsset: gameProcedure
|
|
367
793
|
.input(idInput.extend({
|
|
368
|
-
baseRevision: z.string().optional(),
|
|
369
|
-
baseUpdatedAt: z.string().optional(),
|
|
370
794
|
slot: z.string().min(1),
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
.
|
|
795
|
+
kind: z.enum(["image", "audio", "music"]),
|
|
796
|
+
prompt: z.string().trim().min(1).max(4000),
|
|
797
|
+
preparation: z.record(z.string(), z.unknown()).optional(),
|
|
798
|
+
provider: z.string().min(1).optional(),
|
|
799
|
+
model: z.string().min(1).optional()
|
|
800
|
+
}).strict().refine((value) => (value.provider === undefined) === (value.model === undefined), "Pass provider and model together"))
|
|
801
|
+
.output(generatedAsset)
|
|
375
802
|
.mutation(async ({ ctx, input }) => {
|
|
376
803
|
const game = await ownedGame(ctx.userId, input.id);
|
|
377
804
|
const workspace = await gameWorkspace(ctx.userId, game);
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if (input.candidateWorkspaceId && (!candidateRow || candidateRow.project_id !== game.project_id)) {
|
|
382
|
-
throwApiError(ApiErrorCode.NOT_FOUND, "Candidate workspace not found");
|
|
383
|
-
}
|
|
384
|
-
const candidateWorkspace = candidateRow ? workspaceFromRow(candidateRow) : workspace;
|
|
385
|
-
if (!candidateWorkspace) {
|
|
386
|
-
throwApiError(ApiErrorCode.SERVICE_UNAVAILABLE, "Candidate workspace storage is unavailable");
|
|
387
|
-
}
|
|
388
|
-
const digest = input.binding.digest;
|
|
389
|
-
if (!/^[a-f0-9]{64}$/.test(digest)) {
|
|
390
|
-
throwApiError(ApiErrorCode.INVALID_INPUT, "Invalid candidate digest");
|
|
391
|
-
}
|
|
392
|
-
if (input.binding.mediaKind === "font" && !input.binding.fontFormat) {
|
|
393
|
-
throwApiError(ApiErrorCode.INVALID_INPUT, "Font binding needs a TrueType or OpenType format");
|
|
394
|
-
}
|
|
395
|
-
const formats = input.binding.mediaKind === "audio"
|
|
396
|
-
? [["wav", "audio/wav"], ["mp3", "audio/mpeg"], ["ogg", "audio/ogg"]]
|
|
397
|
-
: input.binding.mediaKind === "font"
|
|
398
|
-
? [[input.binding.fontFormat, `font/${input.binding.fontFormat}`]]
|
|
399
|
-
: [["png", "image/png"], ["jpg", "image/jpeg"], ["webp", "image/webp"]];
|
|
400
|
-
let candidate = null;
|
|
401
|
-
for (const [extension, contentType] of formats) {
|
|
402
|
-
if (!extension || !contentType)
|
|
403
|
-
continue;
|
|
404
|
-
const path = `${game.source_root}/assets/${digest}.${extension}`;
|
|
405
|
-
const bytes = await candidateWorkspace.read(path);
|
|
406
|
-
if (bytes) {
|
|
407
|
-
candidate = { path, extension, contentType, bytes };
|
|
408
|
-
break;
|
|
409
|
-
}
|
|
805
|
+
const args = { game_id: game.id, slot: input.slot, kind: input.kind, prompt: input.prompt, install: false };
|
|
806
|
+
if (input.preparation) {
|
|
807
|
+
args["preparation"] = input.preparation;
|
|
410
808
|
}
|
|
411
|
-
if (
|
|
412
|
-
|
|
809
|
+
if (input.provider && input.model) {
|
|
810
|
+
args["provider"] = input.provider;
|
|
811
|
+
args["model"] = input.model;
|
|
413
812
|
}
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
813
|
+
const run = await assetGenerationRun(ctx.userId, game, workspace);
|
|
814
|
+
const result = generatedAssetResult.safeParse(await run.invoke("generate_game_asset", args));
|
|
815
|
+
if (!result.success) {
|
|
816
|
+
throwApiError(ApiErrorCode.INTERNAL_ERROR, "Asset generation returned an invalid result");
|
|
417
817
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game draft was modified concurrently");
|
|
818
|
+
checkAuthoringResult(result.data);
|
|
819
|
+
const { binding, frames, tiles } = result.data;
|
|
820
|
+
if (!binding) {
|
|
821
|
+
throwApiError(ApiErrorCode.INTERNAL_ERROR, "Asset generation did not stage an asset");
|
|
423
822
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
const installed =
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
823
|
+
await workspace.write(stagedGameCandidateRecordPath(game.source_root, binding.digest), JSON.stringify(stagedGameCandidateRecord.parse({
|
|
824
|
+
version: 1, digest: binding.digest, slot: input.slot, prompt: input.prompt, source: "generate", stagedAt: new Date().toISOString(), binding
|
|
825
|
+
})), "application/json");
|
|
826
|
+
const installed = await installOnLatestDraft(ctx.userId, { id: game.id, slot: input.slot, binding }, { replaceInPlace: true, frames, tiles });
|
|
827
|
+
return { ...installed, stagedDigest: binding.digest, installed: true };
|
|
828
|
+
}),
|
|
829
|
+
/** Bind a staged candidate to a slot on the current draft. A 2D slot's frame and tile bindings move onto the new bytes. */
|
|
830
|
+
installStagedCandidate: gameProcedure
|
|
831
|
+
.input(idInput.extend({
|
|
832
|
+
slot: z.string().min(1),
|
|
833
|
+
digest: z.string().regex(/^[a-f0-9]{64}$/)
|
|
834
|
+
}).strict())
|
|
835
|
+
.output(gameWithDocument)
|
|
836
|
+
.mutation(async ({ ctx, input }) => {
|
|
837
|
+
const game = await ownedGame(ctx.userId, input.id);
|
|
838
|
+
const picked = stagedCandidateBinding.safeParse(await authoringRun(ctx.userId).invoke("browse_native_game_assets", {
|
|
839
|
+
game_id: game.id, digest: input.digest, slot: input.slot
|
|
435
840
|
}));
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
let uri = null;
|
|
439
|
-
try {
|
|
440
|
-
uri = await storage.store(key, bytes, contentType);
|
|
441
|
-
const saved = await Game.updateDraft(ctx.userId, game.id, draft.game.draft_updated_at, [{
|
|
442
|
-
op: "bind_asset", slot: input.slot, binding: { ...input.binding, assetId: installed.id }
|
|
443
|
-
}], workspace, { actor: "user" });
|
|
444
|
-
if (!saved)
|
|
445
|
-
throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game draft was modified concurrently");
|
|
446
|
-
return { game: info(saved.game), document: saved.document };
|
|
841
|
+
if (!picked.success) {
|
|
842
|
+
throwApiError(ApiErrorCode.INTERNAL_ERROR, "Candidate lookup returned an invalid result");
|
|
447
843
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
await installed.delete();
|
|
452
|
-
throw error;
|
|
844
|
+
checkAuthoringResult(picked.data);
|
|
845
|
+
if (!picked.data.binding) {
|
|
846
|
+
throwApiError(ApiErrorCode.NOT_FOUND, "Staged candidate not found");
|
|
453
847
|
}
|
|
848
|
+
return installOnLatestDraft(ctx.userId, { id: game.id, slot: input.slot, binding: picked.data.binding }, { replaceInPlace: true });
|
|
454
849
|
})
|
|
455
850
|
});
|
|
456
851
|
//# sourceMappingURL=games.js.map
|