@nodetool-ai/websocket 0.8.2 → 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.
Files changed (316) hide show
  1. package/dist/.nodetool-build-stamp +1 -1
  2. package/dist/chat-turn-registry.d.ts +2 -1
  3. package/dist/chat-turn-registry.d.ts.map +1 -1
  4. package/dist/chat-turn-registry.js +8 -1
  5. package/dist/chat-turn-registry.js.map +1 -1
  6. package/dist/collection-api.d.ts.map +1 -1
  7. package/dist/collection-api.js +3 -5
  8. package/dist/collection-api.js.map +1 -1
  9. package/dist/custom-providers.d.ts.map +1 -1
  10. package/dist/custom-providers.js +6 -1
  11. package/dist/custom-providers.js.map +1 -1
  12. package/dist/example-workflows.d.ts.map +1 -1
  13. package/dist/example-workflows.js +6 -0
  14. package/dist/example-workflows.js.map +1 -1
  15. package/dist/fake-runtime.d.ts +41 -6
  16. package/dist/fake-runtime.d.ts.map +1 -1
  17. package/dist/fake-runtime.js +207 -13
  18. package/dist/fake-runtime.js.map +1 -1
  19. package/dist/file-api.d.ts.map +1 -1
  20. package/dist/file-api.js +6 -8
  21. package/dist/file-api.js.map +1 -1
  22. package/dist/generation-recovery.d.ts.map +1 -1
  23. package/dist/generation-recovery.js +7 -4
  24. package/dist/generation-recovery.js.map +1 -1
  25. package/dist/headless-job-runner.d.ts.map +1 -1
  26. package/dist/headless-job-runner.js +5 -1
  27. package/dist/headless-job-runner.js.map +1 -1
  28. package/dist/http-api.d.ts +3 -1
  29. package/dist/http-api.d.ts.map +1 -1
  30. package/dist/http-api.js +51 -9
  31. package/dist/http-api.js.map +1 -1
  32. package/dist/http-body-schemas.d.ts +2 -0
  33. package/dist/http-body-schemas.d.ts.map +1 -1
  34. package/dist/http-body-schemas.js +8 -1
  35. package/dist/http-body-schemas.js.map +1 -1
  36. package/dist/lib/admin.d.ts +7 -0
  37. package/dist/lib/admin.d.ts.map +1 -0
  38. package/dist/lib/admin.js +20 -0
  39. package/dist/lib/admin.js.map +1 -0
  40. package/dist/lib/app-instances-service.d.ts +35 -0
  41. package/dist/lib/app-instances-service.d.ts.map +1 -1
  42. package/dist/lib/app-instances-service.js +3 -4
  43. package/dist/lib/app-instances-service.js.map +1 -1
  44. package/dist/lib/app-project-export.d.ts +12 -0
  45. package/dist/lib/app-project-export.d.ts.map +1 -0
  46. package/dist/lib/app-project-export.js +42 -0
  47. package/dist/lib/app-project-export.js.map +1 -0
  48. package/dist/lib/applications-service.d.ts.map +1 -1
  49. package/dist/lib/applications-service.js +7 -2
  50. package/dist/lib/applications-service.js.map +1 -1
  51. package/dist/lib/asset-export.d.ts +6 -2
  52. package/dist/lib/asset-export.d.ts.map +1 -1
  53. package/dist/lib/asset-export.js +15 -7
  54. package/dist/lib/asset-export.js.map +1 -1
  55. package/dist/lib/asset-response.d.ts.map +1 -1
  56. package/dist/lib/asset-response.js +1 -0
  57. package/dist/lib/asset-response.js.map +1 -1
  58. package/dist/lib/cloud-profile.d.ts +10 -0
  59. package/dist/lib/cloud-profile.d.ts.map +1 -0
  60. package/dist/lib/cloud-profile.js +13 -0
  61. package/dist/lib/cloud-profile.js.map +1 -0
  62. package/dist/lib/http-user-id.d.ts +2 -0
  63. package/dist/lib/http-user-id.d.ts.map +1 -0
  64. package/dist/lib/http-user-id.js +4 -0
  65. package/dist/lib/http-user-id.js.map +1 -0
  66. package/dist/lib/json-body.d.ts +11 -0
  67. package/dist/lib/json-body.d.ts.map +1 -0
  68. package/dist/lib/json-body.js +13 -0
  69. package/dist/lib/json-body.js.map +1 -0
  70. package/dist/lib/public-routes.d.ts +15 -0
  71. package/dist/lib/public-routes.d.ts.map +1 -1
  72. package/dist/lib/public-routes.js +27 -0
  73. package/dist/lib/public-routes.js.map +1 -1
  74. package/dist/lib/public-workflow-share.d.ts +12 -1
  75. package/dist/lib/public-workflow-share.d.ts.map +1 -1
  76. package/dist/lib/public-workflow-share.js +45 -2
  77. package/dist/lib/public-workflow-share.js.map +1 -1
  78. package/dist/lib/python-metadata-roots.d.ts +27 -0
  79. package/dist/lib/python-metadata-roots.d.ts.map +1 -0
  80. package/dist/lib/python-metadata-roots.js +92 -0
  81. package/dist/lib/python-metadata-roots.js.map +1 -0
  82. package/dist/lib/runtime-detection.d.ts +1 -1
  83. package/dist/lib/runtime-detection.d.ts.map +1 -1
  84. package/dist/lib/runtime-detection.js +2 -1
  85. package/dist/lib/runtime-detection.js.map +1 -1
  86. package/dist/lib/server-auth-token.d.ts +18 -0
  87. package/dist/lib/server-auth-token.d.ts.map +1 -0
  88. package/dist/lib/server-auth-token.js +28 -0
  89. package/dist/lib/server-auth-token.js.map +1 -0
  90. package/dist/lib/timeline-bundle.d.ts +11 -0
  91. package/dist/lib/timeline-bundle.d.ts.map +1 -1
  92. package/dist/lib/timeline-bundle.js +22 -6
  93. package/dist/lib/timeline-bundle.js.map +1 -1
  94. package/dist/lib/timeline-media-export.d.ts +18 -0
  95. package/dist/lib/timeline-media-export.d.ts.map +1 -0
  96. package/dist/lib/timeline-media-export.js +66 -0
  97. package/dist/lib/timeline-media-export.js.map +1 -0
  98. package/dist/lib/timeline-premiere-import.d.ts +24 -0
  99. package/dist/lib/timeline-premiere-import.d.ts.map +1 -0
  100. package/dist/lib/timeline-premiere-import.js +102 -0
  101. package/dist/lib/timeline-premiere-import.js.map +1 -0
  102. package/dist/lib/timeline-premiere.d.ts +38 -0
  103. package/dist/lib/timeline-premiere.d.ts.map +1 -0
  104. package/dist/lib/timeline-premiere.js +87 -0
  105. package/dist/lib/timeline-premiere.js.map +1 -0
  106. package/dist/mcp-agent-tools.d.ts.map +1 -1
  107. package/dist/mcp-agent-tools.js +2 -0
  108. package/dist/mcp-agent-tools.js.map +1 -1
  109. package/dist/mcp-server.d.ts +7 -0
  110. package/dist/mcp-server.d.ts.map +1 -1
  111. package/dist/mcp-server.js +163 -16
  112. package/dist/mcp-server.js.map +1 -1
  113. package/dist/mcp-skill-prompts.d.ts +23 -0
  114. package/dist/mcp-skill-prompts.d.ts.map +1 -0
  115. package/dist/mcp-skill-prompts.js +91 -0
  116. package/dist/mcp-skill-prompts.js.map +1 -0
  117. package/dist/mcp-tool-deps.d.ts.map +1 -1
  118. package/dist/mcp-tool-deps.js +8 -1
  119. package/dist/mcp-tool-deps.js.map +1 -1
  120. package/dist/models-api.d.ts.map +1 -1
  121. package/dist/models-api.js +6 -2
  122. package/dist/models-api.js.map +1 -1
  123. package/dist/oauth-api.d.ts.map +1 -1
  124. package/dist/oauth-api.js +45 -23
  125. package/dist/oauth-api.js.map +1 -1
  126. package/dist/openai-api.d.ts.map +1 -1
  127. package/dist/openai-api.js +10 -2
  128. package/dist/openai-api.js.map +1 -1
  129. package/dist/plugins/websocket.d.ts.map +1 -1
  130. package/dist/plugins/websocket.js +7 -2
  131. package/dist/plugins/websocket.js.map +1 -1
  132. package/dist/routes/applications.d.ts +1 -0
  133. package/dist/routes/applications.d.ts.map +1 -1
  134. package/dist/routes/applications.js +24 -0
  135. package/dist/routes/applications.js.map +1 -1
  136. package/dist/routes/assets.d.ts.map +1 -1
  137. package/dist/routes/assets.js +31 -41
  138. package/dist/routes/assets.js.map +1 -1
  139. package/dist/routes/config.d.ts.map +1 -1
  140. package/dist/routes/config.js +6 -0
  141. package/dist/routes/config.js.map +1 -1
  142. package/dist/routes/documents.d.ts.map +1 -1
  143. package/dist/routes/documents.js +24 -0
  144. package/dist/routes/documents.js.map +1 -1
  145. package/dist/routes/fal-pricing-estimate.d.ts.map +1 -1
  146. package/dist/routes/fal-pricing-estimate.js +2 -0
  147. package/dist/routes/fal-pricing-estimate.js.map +1 -1
  148. package/dist/routes/files.d.ts.map +1 -1
  149. package/dist/routes/files.js +2 -0
  150. package/dist/routes/files.js.map +1 -1
  151. package/dist/routes/games.d.ts +38 -0
  152. package/dist/routes/games.d.ts.map +1 -0
  153. package/dist/routes/games.js +158 -0
  154. package/dist/routes/games.js.map +1 -0
  155. package/dist/routes/integrations.d.ts.map +1 -1
  156. package/dist/routes/integrations.js +22 -26
  157. package/dist/routes/integrations.js.map +1 -1
  158. package/dist/routes/js-scripts.d.ts +1 -1
  159. package/dist/routes/js-scripts.d.ts.map +1 -1
  160. package/dist/routes/js-scripts.js +1 -1
  161. package/dist/routes/js-scripts.js.map +1 -1
  162. package/dist/routes/nodes.d.ts.map +1 -1
  163. package/dist/routes/nodes.js +2 -0
  164. package/dist/routes/nodes.js.map +1 -1
  165. package/dist/routes/storage.d.ts.map +1 -1
  166. package/dist/routes/storage.js +2 -0
  167. package/dist/routes/storage.js.map +1 -1
  168. package/dist/routes/storyboards.d.ts.map +1 -1
  169. package/dist/routes/storyboards.js +6 -3
  170. package/dist/routes/storyboards.js.map +1 -1
  171. package/dist/routes/timelines.d.ts +8 -4
  172. package/dist/routes/timelines.d.ts.map +1 -1
  173. package/dist/routes/timelines.js +209 -16
  174. package/dist/routes/timelines.js.map +1 -1
  175. package/dist/screenshot-docs-seed.d.ts +24 -0
  176. package/dist/screenshot-docs-seed.d.ts.map +1 -0
  177. package/dist/screenshot-docs-seed.js +414 -0
  178. package/dist/screenshot-docs-seed.js.map +1 -0
  179. package/dist/screenshot-projects.d.ts.map +1 -1
  180. package/dist/screenshot-projects.js +9 -9
  181. package/dist/screenshot-projects.js.map +1 -1
  182. package/dist/screenshot-server.js +111 -16
  183. package/dist/screenshot-server.js.map +1 -1
  184. package/dist/server.js +87 -90
  185. package/dist/server.js.map +1 -1
  186. package/dist/session/chat-turn.d.ts +10 -0
  187. package/dist/session/chat-turn.d.ts.map +1 -1
  188. package/dist/session/chat-turn.js +68 -10
  189. package/dist/session/chat-turn.js.map +1 -1
  190. package/dist/session/commands.d.ts +14 -0
  191. package/dist/session/commands.d.ts.map +1 -1
  192. package/dist/session/commands.js +113 -14
  193. package/dist/session/commands.js.map +1 -1
  194. package/dist/session/inference.d.ts +25 -2
  195. package/dist/session/inference.d.ts.map +1 -1
  196. package/dist/session/inference.js +86 -8
  197. package/dist/session/inference.js.map +1 -1
  198. package/dist/session/job-execution.d.ts +0 -1
  199. package/dist/session/job-execution.d.ts.map +1 -1
  200. package/dist/session/job-execution.js +34 -37
  201. package/dist/session/job-execution.js.map +1 -1
  202. package/dist/storage-api.d.ts +0 -4
  203. package/dist/storage-api.d.ts.map +1 -1
  204. package/dist/storage-api.js +3 -26
  205. package/dist/storage-api.js.map +1 -1
  206. package/dist/test-ui-server.d.ts +11 -0
  207. package/dist/test-ui-server.d.ts.map +1 -1
  208. package/dist/test-ui-server.js +63 -3
  209. package/dist/test-ui-server.js.map +1 -1
  210. package/dist/triggers/dispatcher.d.ts.map +1 -1
  211. package/dist/triggers/dispatcher.js +24 -4
  212. package/dist/triggers/dispatcher.js.map +1 -1
  213. package/dist/triggers/file-watch.d.ts.map +1 -1
  214. package/dist/triggers/file-watch.js +35 -19
  215. package/dist/triggers/file-watch.js.map +1 -1
  216. package/dist/triggers/scheduler.d.ts.map +1 -1
  217. package/dist/triggers/scheduler.js +7 -5
  218. package/dist/triggers/scheduler.js.map +1 -1
  219. package/dist/trpc/error-formatter.d.ts.map +1 -1
  220. package/dist/trpc/error-formatter.js +2 -1
  221. package/dist/trpc/error-formatter.js.map +1 -1
  222. package/dist/trpc/routers/app-instances.d.ts +63 -0
  223. package/dist/trpc/routers/app-instances.d.ts.map +1 -1
  224. package/dist/trpc/routers/app-runs.d.ts +28 -0
  225. package/dist/trpc/routers/app-runs.d.ts.map +1 -1
  226. package/dist/trpc/routers/applications.d.ts +7 -0
  227. package/dist/trpc/routers/applications.d.ts.map +1 -1
  228. package/dist/trpc/routers/assets.d.ts +8 -0
  229. package/dist/trpc/routers/assets.d.ts.map +1 -1
  230. package/dist/trpc/routers/assets.js +16 -5
  231. package/dist/trpc/routers/assets.js.map +1 -1
  232. package/dist/trpc/routers/collections.d.ts.map +1 -1
  233. package/dist/trpc/routers/collections.js +16 -10
  234. package/dist/trpc/routers/collections.js.map +1 -1
  235. package/dist/trpc/routers/credits.d.ts.map +1 -1
  236. package/dist/trpc/routers/credits.js +11 -1
  237. package/dist/trpc/routers/credits.js.map +1 -1
  238. package/dist/trpc/routers/extension.d.ts +1 -1
  239. package/dist/trpc/routers/extension.d.ts.map +1 -1
  240. package/dist/trpc/routers/extension.js +8 -4
  241. package/dist/trpc/routers/extension.js.map +1 -1
  242. package/dist/trpc/routers/gameDraftHistory.d.ts +1 -2
  243. package/dist/trpc/routers/gameDraftHistory.d.ts.map +1 -1
  244. package/dist/trpc/routers/gameDraftHistory.js +1 -1
  245. package/dist/trpc/routers/gameDraftHistory.js.map +1 -1
  246. package/dist/trpc/routers/games.d.ts +40822 -5154
  247. package/dist/trpc/routers/games.d.ts.map +1 -1
  248. package/dist/trpc/routers/games.js +317 -140
  249. package/dist/trpc/routers/games.js.map +1 -1
  250. package/dist/trpc/routers/jobs.d.ts +7 -0
  251. package/dist/trpc/routers/jobs.d.ts.map +1 -1
  252. package/dist/trpc/routers/jobs.js +5 -3
  253. package/dist/trpc/routers/jobs.js.map +1 -1
  254. package/dist/trpc/routers/js-scripts.d.ts.map +1 -1
  255. package/dist/trpc/routers/js-scripts.js +18 -2
  256. package/dist/trpc/routers/js-scripts.js.map +1 -1
  257. package/dist/trpc/routers/messages.d.ts +10 -0
  258. package/dist/trpc/routers/messages.d.ts.map +1 -1
  259. package/dist/trpc/routers/messages.js +16 -2
  260. package/dist/trpc/routers/messages.js.map +1 -1
  261. package/dist/trpc/routers/models.d.ts.map +1 -1
  262. package/dist/trpc/routers/models.js +20 -30
  263. package/dist/trpc/routers/models.js.map +1 -1
  264. package/dist/trpc/routers/packs.d.ts.map +1 -1
  265. package/dist/trpc/routers/packs.js +18 -4
  266. package/dist/trpc/routers/packs.js.map +1 -1
  267. package/dist/trpc/routers/projects.d.ts +66 -2
  268. package/dist/trpc/routers/projects.d.ts.map +1 -1
  269. package/dist/trpc/routers/projects.js +19 -2
  270. package/dist/trpc/routers/projects.js.map +1 -1
  271. package/dist/trpc/routers/settings.d.ts.map +1 -1
  272. package/dist/trpc/routers/settings.js +30 -5
  273. package/dist/trpc/routers/settings.js.map +1 -1
  274. package/dist/trpc/routers/sketch.d.ts +30 -0
  275. package/dist/trpc/routers/sketch.d.ts.map +1 -1
  276. package/dist/trpc/routers/sketch.js +2 -2
  277. package/dist/trpc/routers/sketch.js.map +1 -1
  278. package/dist/trpc/routers/storyboards.d.ts +171 -1
  279. package/dist/trpc/routers/storyboards.d.ts.map +1 -1
  280. package/dist/trpc/routers/storyboards.js +8 -5
  281. package/dist/trpc/routers/storyboards.js.map +1 -1
  282. package/dist/trpc/routers/threads.d.ts.map +1 -1
  283. package/dist/trpc/routers/threads.js +8 -2
  284. package/dist/trpc/routers/threads.js.map +1 -1
  285. package/dist/trpc/routers/timeline.d.ts +15 -0
  286. package/dist/trpc/routers/timeline.d.ts.map +1 -1
  287. package/dist/trpc/routers/timeline.js +15 -2
  288. package/dist/trpc/routers/timeline.js.map +1 -1
  289. package/dist/trpc/routers/triggers.d.ts.map +1 -1
  290. package/dist/trpc/routers/triggers.js +18 -3
  291. package/dist/trpc/routers/triggers.js.map +1 -1
  292. package/dist/trpc/routers/users.d.ts +3 -2
  293. package/dist/trpc/routers/users.d.ts.map +1 -1
  294. package/dist/trpc/routers/users.js +25 -27
  295. package/dist/trpc/routers/users.js.map +1 -1
  296. package/dist/trpc/routers/worker.d.ts.map +1 -1
  297. package/dist/trpc/routers/worker.js +37 -14
  298. package/dist/trpc/routers/worker.js.map +1 -1
  299. package/dist/trpc/routers/workflows.d.ts +146 -0
  300. package/dist/trpc/routers/workflows.d.ts.map +1 -1
  301. package/dist/trpc/routers/workflows.js +52 -13
  302. package/dist/trpc/routers/workflows.js.map +1 -1
  303. package/dist/trpc/sandbox-coverage.d.ts.map +1 -1
  304. package/dist/trpc/sandbox-coverage.js +29 -0
  305. package/dist/trpc/sandbox-coverage.js.map +1 -1
  306. package/dist/websocket-client-session.d.ts +2 -2
  307. package/dist/websocket-client-session.d.ts.map +1 -1
  308. package/dist/websocket-client-session.js +9 -3
  309. package/dist/websocket-client-session.js.map +1 -1
  310. package/dist/worker-settings-env.d.ts +23 -0
  311. package/dist/worker-settings-env.d.ts.map +1 -0
  312. package/dist/worker-settings-env.js +47 -0
  313. package/dist/worker-settings-env.js.map +1 -0
  314. package/dist/workflow-interface-service.d.ts +7 -0
  315. package/dist/workflow-interface-service.d.ts.map +1 -1
  316. package/package.json +44 -42
@@ -3,23 +3,32 @@ import { join } from "node:path";
3
3
  import { z } from "zod";
4
4
  import sharp from "sharp";
5
5
  import { createLogger, getManagedWorkspaceDir, managedWorkspaceKey, workspaceStorageKind } from "@nodetool-ai/config";
6
- import { AmbiguousGameIdError, InvalidGameDocumentError, Asset, Game, Project, Workspace } from "@nodetool-ai/models";
7
- import { exampleGameSummary, anyGameAssetBinding, gameAssetBinding, gameAssetBinding3D, gamePreparedCollider3D, gameModelImportSettings3D, parseGameDocument, anyGameDocument as gameDocument, installExampleGameInput } from "@nodetool-ai/protocol";
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
8
  import { createTopDownRoomGame, createNative3DGame, anyGameDocumentOp as gameDocumentOp, GameOpError, decodePreparedGameCollider3D, trackGameAuthoringEdits, validateAnyGame } from "@nodetool-ai/game-runtime";
9
9
  import { normalizeGameModel3D, prepareGameModelBinding3D } from "@nodetool-ai/game-renderer/preparation3d";
10
- import { createCapabilityRun, gateFromContext } from "@nodetool-ai/agents";
10
+ import { contextSecretAvailability, createCapabilityRun, gateFromContext } from "@nodetool-ai/agents";
11
11
  import { gameAuthoringCandidate, gameAuthoringProgram, gameAuthoringConflict } from "@nodetool-ai/protocol";
12
12
  import { PERMISSION_GATE_CONTEXT_KEY, ProcessingContext, headlessGate } from "@nodetool-ai/runtime";
13
13
  import { getAssetAdapter } from "../../lib/storage.js";
14
14
  import { getAssetStorageKey, retrieveAssetBytes } from "../../lib/asset-paths.js";
15
15
  import { getExampleGameBundle, installExampleGameAssets, listExampleGames } from "../../lib/example-games.js";
16
16
  import { workspaceFromRow } from "../../lib/workflow-workspace.js";
17
+ import { loadConfiguredProviders } from "../../configured-providers.js";
18
+ import { createRuntimeContext } from "../../session/model-interfaces.js";
17
19
  import { ApiErrorCode } from "../../error-codes.js";
18
20
  import { router } from "../index.js";
19
21
  import { protectedProcedure } from "../middleware.js";
20
22
  import { throwApiError } from "../error-formatter.js";
21
23
  import { boundedGameDraftHistory, MAX_GAME_DRAFT_OPS } from "./gameDraftHistory.js";
22
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
+ });
23
32
  const idInput = z.object({ id: z.string() });
24
33
  const gameInfo = z.object({
25
34
  id: z.string(),
@@ -41,6 +50,27 @@ const authoringPreview = z.object({
41
50
  changed_dependencies: z.array(z.string()),
42
51
  restart_required: z.boolean()
43
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() });
44
74
  const gameRevisionInfo = z.object({ revision: z.string(), modifiedAt: z.number(), current: z.boolean(), message: z.string().nullable() });
45
75
  function info(game) {
46
76
  return {
@@ -63,8 +93,18 @@ function authoringRun(userId) {
63
93
  context.set(PERMISSION_GATE_CONTEXT_KEY, headlessGate("game.authoring"));
64
94
  return createCapabilityRun({ context, gate: gateFromContext(context, "game.authoring") });
65
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
+ }
66
103
  function checkAuthoringResult(result) {
67
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
+ }
68
108
  const code = /stale|concurrent|changed since/i.test(result.error)
69
109
  ? ApiErrorCode.ALREADY_EXISTS : ApiErrorCode.INVALID_INPUT;
70
110
  throwApiError(code, result.error);
@@ -100,7 +140,8 @@ async function gameWorkspace(userId, game) {
100
140
  }
101
141
  return workspace;
102
142
  }
103
- async function persistGameAsset(userId, game, workspace, draftUpdatedAt, slot, binding, bytes, extension, contentType) {
143
+ const CONCURRENT_DRAFT_EDIT = "Game draft was modified concurrently";
144
+ async function persistGameAsset(userId, game, workspace, draftUpdatedAt, slot, binding, bytes, extension, contentType, siblingBindings) {
104
145
  await workspace.write(`${game.source_root}/assets/${binding.digest}.${extension}`, bytes, contentType);
105
146
  const installed = await Asset.create({
106
147
  user_id: userId, project_id: game.project_id, parent_id: userId,
@@ -116,9 +157,12 @@ async function persistGameAsset(userId, game, workspace, draftUpdatedAt, slot, b
116
157
  try {
117
158
  uri = await storage.store(getAssetStorageKey(userId, installed.id, contentType), bytes, contentType);
118
159
  const op = gameDocumentOp.parse({ op: "bind_asset", slot, binding: { ...binding, assetId: installed.id } });
119
- const saved = await Game.updateDraft(userId, game.id, draftUpdatedAt, [op], workspace, { actor: "user" });
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" });
120
164
  if (!saved) {
121
- throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game draft was modified concurrently");
165
+ throwApiError(ApiErrorCode.ALREADY_EXISTS, CONCURRENT_DRAFT_EDIT);
122
166
  }
123
167
  return { game: info(saved.game), document: saved.document };
124
168
  }
@@ -279,8 +323,167 @@ async function createGame(userId, projectId, name, source, dimension = "2d") {
279
323
  throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game already exists");
280
324
  return { game: info(game), document };
281
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
+ }
282
485
  export const gamesRouter = router({
283
- list: protectedProcedure
486
+ list: gameProcedure
284
487
  .input(z.object({ projectId: z.string() }))
285
488
  .output(z.array(gameInfo))
286
489
  .query(async ({ ctx, input }) => {
@@ -289,7 +492,7 @@ export const gamesRouter = router({
289
492
  }
290
493
  return (await Game.listByProject(ctx.userId, input.projectId)).map(info);
291
494
  }),
292
- create: protectedProcedure
495
+ create: gameProcedure
293
496
  .input(z.object({ projectId: z.string(), name: z.string().min(1).max(200), dimension: z.enum(["2d", "3d"]).optional(), document: gameDocument.optional() }))
294
497
  .output(gameWithDocument)
295
498
  .mutation(({ ctx, input }) => {
@@ -298,10 +501,10 @@ export const gamesRouter = router({
298
501
  }
299
502
  return createGame(ctx.userId, input.projectId, input.name, input.document, input.dimension);
300
503
  }),
301
- examples: protectedProcedure
504
+ examples: gameProcedure
302
505
  .output(z.array(exampleGameSummary))
303
506
  .query(({ ctx }) => listExampleGames(ctx.apiOptions)),
304
- example: protectedProcedure
507
+ example: gameProcedure
305
508
  .input(z.object({ slug: z.string().min(1) }))
306
509
  .output(z.object({ name: z.string(), document: gameDocument }))
307
510
  .query(({ ctx, input }) => {
@@ -311,7 +514,7 @@ export const gamesRouter = router({
311
514
  }
312
515
  return { name: bundle.name, document: bundle.document };
313
516
  }),
314
- installExample: protectedProcedure
517
+ installExample: gameProcedure
315
518
  .input(installExampleGameInput)
316
519
  .output(gameWithDocument)
317
520
  .mutation(async ({ ctx, input }) => {
@@ -331,7 +534,7 @@ export const gamesRouter = router({
331
534
  throw error;
332
535
  }
333
536
  }),
334
- get: protectedProcedure
537
+ get: gameProcedure
335
538
  .input(idInput.extend({ revision: z.string().optional() }))
336
539
  .output(gameWithDocument)
337
540
  .query(async ({ ctx, input }) => {
@@ -339,7 +542,7 @@ export const gamesRouter = router({
339
542
  const workspace = await gameWorkspace(ctx.userId, game);
340
543
  return { game: info(game), document: await readRevision(workspace, game, input.revision ?? game.current_revision) };
341
544
  }),
342
- getDraft: protectedProcedure
545
+ getDraft: gameProcedure
343
546
  .input(idInput)
344
547
  .output(gameWithDocument)
345
548
  .query(async ({ ctx, input }) => {
@@ -349,7 +552,7 @@ export const gamesRouter = router({
349
552
  throwApiError(ApiErrorCode.NOT_FOUND, "Game draft not found");
350
553
  return { game: info(draft.game), document: draft.document };
351
554
  }),
352
- saveDraft: protectedProcedure
555
+ saveDraft: gameProcedure
353
556
  .input(idInput.extend({ baseUpdatedAt: z.string(), ops: z.array(gameDocumentOp).min(1).max(MAX_GAME_DRAFT_OPS) }))
354
557
  .output(gameWithDocument)
355
558
  .mutation(async ({ ctx, input }) => {
@@ -370,7 +573,25 @@ export const gamesRouter = router({
370
573
  throw error;
371
574
  }
372
575
  }),
373
- previewAuthoring: protectedProcedure
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
+ }
591
+ throw error;
592
+ }
593
+ }),
594
+ previewAuthoring: gameProcedure
374
595
  .input(idInput.extend({ program: gameAuthoringProgram.optional() }).strict())
375
596
  .output(authoringPreview)
376
597
  .mutation(async ({ ctx, input }) => {
@@ -383,7 +604,7 @@ export const gamesRouter = router({
383
604
  checkAuthoringResult(result);
384
605
  return authoringPreview.parse(result);
385
606
  }),
386
- applyAuthoring: protectedProcedure
607
+ applyAuthoring: gameProcedure
387
608
  .input(idInput.extend({ candidate: gameAuthoringCandidate }).strict())
388
609
  .output(gameWithDocument)
389
610
  .mutation(async ({ ctx, input }) => {
@@ -398,7 +619,7 @@ export const gamesRouter = router({
398
619
  }
399
620
  return { game: info(draft.game), document: draft.document };
400
621
  }),
401
- draftChanges: protectedProcedure
622
+ draftChanges: gameProcedure
402
623
  .input(idInput)
403
624
  .output(z.array(z.object({
404
625
  id: z.string(), actor: z.enum(["agent", "user"]), threadId: z.string().nullable(),
@@ -410,7 +631,7 @@ export const gamesRouter = router({
410
631
  const game = await ownedGame(ctx.userId, input.id);
411
632
  return boundedGameDraftHistory((await Game.listDraftChanges(ctx.userId, game.id)).map(({ gameId: _gameId, ...change }) => change));
412
633
  }),
413
- draftBeforeChange: protectedProcedure
634
+ draftBeforeChange: gameProcedure
414
635
  .input(idInput.extend({ changeId: z.string() }))
415
636
  .output(gameDocument)
416
637
  .query(async ({ ctx, input }) => {
@@ -420,7 +641,7 @@ export const gamesRouter = router({
420
641
  throwApiError(ApiErrorCode.NOT_FOUND, "Game draft change not found");
421
642
  return document;
422
643
  }),
423
- revisions: protectedProcedure
644
+ revisions: gameProcedure
424
645
  .input(idInput)
425
646
  .output(z.array(gameRevisionInfo))
426
647
  .query(async ({ ctx, input }) => {
@@ -435,11 +656,11 @@ export const gamesRouter = router({
435
656
  });
436
657
  return revisions.sort((a, b) => b.modifiedAt - a.modifiedAt);
437
658
  }),
438
- publish: protectedProcedure
659
+ publish: gameProcedure
439
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() }))
440
661
  .output(gameWithDocument)
441
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)),
442
- restore: protectedProcedure
663
+ restore: gameProcedure
443
664
  .input(idInput.extend({ baseRevision: z.string(), baseUpdatedAt: z.string(), revision: z.string() }))
444
665
  .output(gameWithDocument)
445
666
  .mutation(async ({ ctx, input }) => {
@@ -448,7 +669,7 @@ export const gamesRouter = router({
448
669
  const oldDocument = await readRevision(workspace, game, input.revision);
449
670
  return publishDocument(ctx.userId, game, input.baseRevision, oldDocument, undefined, true, input.baseUpdatedAt);
450
671
  }),
451
- restoreDraft: protectedProcedure
672
+ restoreDraft: gameProcedure
452
673
  .input(idInput.extend({ baseUpdatedAt: z.string(), revision: z.string() }))
453
674
  .output(gameWithDocument)
454
675
  .mutation(async ({ ctx, input }) => {
@@ -468,7 +689,7 @@ export const gamesRouter = router({
468
689
  throw error;
469
690
  }
470
691
  }),
471
- installAsset: protectedProcedure
692
+ installAsset: gameProcedure
472
693
  .input(idInput.extend({ baseRevision: z.string().optional(), baseUpdatedAt: z.string().optional(), slot: z.string().min(1),
473
694
  assetId: z.string(), expectedDigest: z.string().optional(), importSettings: gameModelImportSettings3D.partial().optional(),
474
695
  dependencyAssetIds: z.record(z.string(), z.string()).refine((value) => Object.keys(value).length <= 320, "Too many model dependencies").optional() }))
@@ -545,130 +766,86 @@ export const gamesRouter = router({
545
766
  }
546
767
  return { game: info(saved.game), document: saved.document };
547
768
  }),
548
- installCandidate: protectedProcedure
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
549
793
  .input(idInput.extend({
550
- baseRevision: z.string().optional(),
551
- baseUpdatedAt: z.string().optional(),
552
794
  slot: z.string().min(1),
553
- candidateWorkspaceId: z.string().optional(),
554
- binding: anyGameAssetBinding
555
- }))
556
- .output(gameWithDocument)
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)
557
802
  .mutation(async ({ ctx, input }) => {
558
803
  const game = await ownedGame(ctx.userId, input.id);
559
804
  const workspace = await gameWorkspace(ctx.userId, game);
560
- const candidateRow = input.candidateWorkspaceId
561
- ? await Workspace.find(ctx.userId, input.candidateWorkspaceId)
562
- : null;
563
- if (input.candidateWorkspaceId && (!candidateRow || candidateRow.project_id !== game.project_id)) {
564
- throwApiError(ApiErrorCode.NOT_FOUND, "Candidate workspace not found");
565
- }
566
- const candidateWorkspace = candidateRow ? workspaceFromRow(candidateRow) : workspace;
567
- if (!candidateWorkspace) {
568
- throwApiError(ApiErrorCode.SERVICE_UNAVAILABLE, "Candidate workspace storage is unavailable");
569
- }
570
- const digest = input.binding.digest;
571
- if (!/^[a-f0-9]{64}$/.test(digest)) {
572
- throwApiError(ApiErrorCode.INVALID_INPUT, "Invalid candidate digest");
573
- }
574
- if (input.binding.mediaKind === "font" && !input.binding.fontFormat) {
575
- throwApiError(ApiErrorCode.INVALID_INPUT, "Font binding needs a TrueType or OpenType format");
576
- }
577
- const formats = input.binding.mediaKind === "model"
578
- ? [["glb", "model/gltf-binary"]]
579
- : input.binding.mediaKind === "collider"
580
- ? [["json", "application/json"]]
581
- : input.binding.mediaKind === "audio"
582
- ? [["wav", "audio/wav"], ["mp3", "audio/mpeg"], ["ogg", "audio/ogg"]]
583
- : input.binding.mediaKind === "font"
584
- ? [[input.binding.fontFormat, `font/${input.binding.fontFormat}`]]
585
- : [["png", "image/png"], ["jpg", "image/jpeg"], ["webp", "image/webp"]];
586
- let candidate = null;
587
- for (const [extension, contentType] of formats) {
588
- if (!extension || !contentType)
589
- continue;
590
- const path = `${game.source_root}/assets/${digest}.${extension}`;
591
- const bytes = await candidateWorkspace.read(path);
592
- if (bytes) {
593
- candidate = { extension, contentType, bytes };
594
- break;
595
- }
596
- }
597
- if (!candidate || createHash("sha256").update(candidate.bytes).digest("hex") !== digest) {
598
- throwApiError(ApiErrorCode.INVALID_INPUT, "Candidate asset is missing or changed");
599
- }
600
- const { extension, contentType, bytes } = candidate;
601
- let binding = input.binding;
602
- if (binding.mediaKind === "model") {
603
- const prepared = await prepareGameModelBinding3D(bytes, { assetId: binding.assetId, expectedDigest: digest });
604
- if (!prepared.ok) {
605
- throwApiError(ApiErrorCode.INVALID_INPUT, prepared.diagnostics.map((issue) => issue.message).join("; "));
606
- }
607
- const sourceAsset = binding.sourceAssetId ? await Asset.find(ctx.userId, binding.sourceAssetId) : null;
608
- if (binding.sourceAssetId && !sourceAsset) {
609
- throwApiError(ApiErrorCode.INVALID_INPUT, "Model source asset is not owned");
610
- }
611
- const canonical = { ...prepared.binding, required: binding.required };
612
- if (binding.provenance) {
613
- canonical.provenance = binding.provenance;
614
- }
615
- if (binding.importSettings) {
616
- canonical.importSettings = binding.importSettings;
617
- }
618
- if (binding.sourceDigest) {
619
- canonical.sourceDigest = binding.sourceDigest;
620
- }
621
- if (sourceAsset) {
622
- canonical.sourceAssetId = sourceAsset.id;
623
- }
624
- binding = canonical;
625
- }
626
- else if (binding.mediaKind === "collider") {
627
- let geometry;
628
- try {
629
- geometry = JSON.parse(new TextDecoder().decode(bytes));
630
- }
631
- catch {
632
- throwApiError(ApiErrorCode.INVALID_INPUT, "Prepared collider is not valid JSON");
633
- }
634
- const parsed = gamePreparedCollider3D.safeParse(geometry);
635
- if (!parsed.success || binding.shape === "triangleMesh" && !parsed.data.indices?.length) {
636
- throwApiError(ApiErrorCode.INVALID_INPUT, "Prepared collider has invalid vertices or triangle indices");
637
- }
638
- const { vertices, indices } = parsed.data;
639
- const minimum = { x: Infinity, y: Infinity, z: Infinity };
640
- const maximum = { x: -Infinity, y: -Infinity, z: -Infinity };
641
- for (let index = 0; index < vertices.length; index += 3) {
642
- minimum.x = Math.min(minimum.x, vertices[index]);
643
- maximum.x = Math.max(maximum.x, vertices[index]);
644
- minimum.y = Math.min(minimum.y, vertices[index + 1]);
645
- maximum.y = Math.max(maximum.y, vertices[index + 1]);
646
- minimum.z = Math.min(minimum.z, vertices[index + 2]);
647
- maximum.z = Math.max(maximum.z, vertices[index + 2]);
648
- }
649
- binding = { ...binding, preparationVersion: "1", bounds: { min: minimum, max: maximum }, vertices: vertices.length / 3, triangles: (indices?.length ?? 0) / 3 };
650
- try {
651
- await decodePreparedGameCollider3D(bytes, gameAssetBinding3D.options[1].parse(binding));
652
- }
653
- catch (error) {
654
- throwApiError(ApiErrorCode.INVALID_INPUT, error instanceof Error ? error.message : "Prepared collider is invalid");
655
- }
656
- }
657
- if (input.baseRevision && game.current_revision !== input.baseRevision) {
658
- throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game was modified concurrently");
659
- }
660
- const draft = await Game.readDraft(ctx.userId, game.id, workspace);
661
- if (!draft)
662
- throwApiError(ApiErrorCode.NOT_FOUND, "Game draft not found");
663
- if (input.baseUpdatedAt && draft.game.draft_updated_at !== input.baseUpdatedAt) {
664
- throwApiError(ApiErrorCode.ALREADY_EXISTS, "Game draft was modified concurrently");
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;
808
+ }
809
+ if (input.provider && input.model) {
810
+ args["provider"] = input.provider;
811
+ args["model"] = input.model;
812
+ }
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");
817
+ }
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");
822
+ }
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
840
+ }));
841
+ if (!picked.success) {
842
+ throwApiError(ApiErrorCode.INTERNAL_ERROR, "Candidate lookup returned an invalid result");
665
843
  }
666
- const targetBinding = draft.document.schemaVersion === 3 ? gameAssetBinding3D.safeParse(binding) :
667
- gameAssetBinding.safeParse(binding);
668
- if (!targetBinding.success || draft.document.schemaVersion !== 3 && (binding.mediaKind === "model" || binding.mediaKind === "collider")) {
669
- throwApiError(ApiErrorCode.INVALID_INPUT, "Asset binding does not match the game dimension");
844
+ checkAuthoringResult(picked.data);
845
+ if (!picked.data.binding) {
846
+ throwApiError(ApiErrorCode.NOT_FOUND, "Staged candidate not found");
670
847
  }
671
- return persistGameAsset(ctx.userId, game, workspace, draft.game.draft_updated_at, input.slot, targetBinding.data, bytes, extension, contentType);
848
+ return installOnLatestDraft(ctx.userId, { id: game.id, slot: input.slot, binding: picked.data.binding }, { replaceInPlace: true });
672
849
  })
673
850
  });
674
851
  //# sourceMappingURL=games.js.map