@opengeni/api-router 3.0.1 → 3.1.2

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 (546) hide show
  1. package/dist/access-grant-rls.d.ts +8 -1
  2. package/dist/admission-trace.d.ts +8 -0
  3. package/dist/app.d.ts +50 -3
  4. package/dist/app.js +10 -1
  5. package/dist/artifact-catalog-cursor.d.ts +1 -0
  6. package/dist/attached-browser-viewer.d.ts +11 -0
  7. package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
  8. package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
  9. package/dist/auth/managed-auth-session-adapter.d.ts +9 -1
  10. package/dist/auth/managed-auth.d.ts +72 -5
  11. package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
  12. package/dist/auth/new-signups-gate.d.ts +23 -0
  13. package/dist/auth/signup-funnel-metrics.d.ts +44 -0
  14. package/dist/azure-live.d.ts +28 -0
  15. package/dist/browser-action-fences.d.ts +13 -0
  16. package/dist/browser-controller-authority.d.ts +1 -61
  17. package/dist/browser-ephemeral.d.ts +5 -0
  18. package/dist/browser-lost-runtime-end.d.ts +24 -0
  19. package/dist/browser-state-authority.d.ts +1 -27
  20. package/dist/chunk-OGQDXVXY.js +18 -0
  21. package/dist/chunk-OGQDXVXY.js.map +1 -0
  22. package/dist/{chunk-AJ62T5QN.js → chunk-OT56OY4V.js} +80503 -39869
  23. package/dist/chunk-OT56OY4V.js.map +1 -0
  24. package/dist/claude-default-models.d.ts +19 -0
  25. package/dist/claude-subscription-core.d.ts +50 -0
  26. package/dist/claude-subscription-oauth.d.ts +36 -0
  27. package/dist/claude-subscription-profile.d.ts +9 -0
  28. package/dist/claude-subscription-usage.d.ts +6 -0
  29. package/dist/claude-workspace-connection.d.ts +15 -0
  30. package/dist/codemode.d.ts +40 -5
  31. package/dist/codex-realtime.d.ts +29 -3
  32. package/dist/codex-session-pointers.d.ts +19 -0
  33. package/dist/connection-ownership.d.ts +6 -3
  34. package/dist/controller-data-plane.d.ts +16 -0
  35. package/dist/credit-promotion-snapshot.d.ts +6 -0
  36. package/dist/editable-artifact-source-session.d.ts +3 -0
  37. package/dist/editable-artifact-websocket.d.ts +5 -3
  38. package/dist/fatal-process-boundary.d.ts +9 -1
  39. package/dist/github-access.d.ts +7 -1
  40. package/dist/github-browser-flow.d.ts +1 -1
  41. package/dist/http/acting-person.d.ts +36 -0
  42. package/dist/http/api-contract-compat.d.ts +19 -0
  43. package/dist/http/api-error.d.ts +50 -1
  44. package/dist/http/auth.d.ts +8 -1
  45. package/dist/http/cors.d.ts +12 -0
  46. package/dist/http/deprecation.d.ts +42 -0
  47. package/dist/http/keyed-admission.d.ts +12 -0
  48. package/dist/http/local-browser-boundary.d.ts +72 -0
  49. package/dist/http/metrics-listener.d.ts +22 -0
  50. package/dist/http/path-identifier.d.ts +17 -0
  51. package/dist/http/registered-route-label.d.ts +16 -0
  52. package/dist/http/rejection-telemetry.d.ts +34 -0
  53. package/dist/http/request-body.d.ts +27 -0
  54. package/dist/http/request-source.d.ts +41 -3
  55. package/dist/http/sse.d.ts +12 -1
  56. package/dist/http/unmatched-route.d.ts +18 -0
  57. package/dist/http/user-content.d.ts +36 -0
  58. package/dist/http/variable-sets-removed.d.ts +41 -0
  59. package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
  60. package/dist/index.js +1279 -108
  61. package/dist/index.js.map +1 -1
  62. package/dist/integration-connect-metrics.d.ts +35 -0
  63. package/dist/integration-endpoint-test.d.ts +20 -0
  64. package/dist/integrations/api-integrations.d.ts +2 -2
  65. package/dist/integrations/atlassian.d.ts +8 -3
  66. package/dist/integrations/fiken.d.ts +3 -1
  67. package/dist/integrations/github-lens-connect.d.ts +37 -66
  68. package/dist/integrations/google-drive.d.ts +2 -0
  69. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  70. package/dist/integrations/marketplace-plugin.d.ts +43 -43
  71. package/dist/integrations/oauth-client.d.ts +171 -12
  72. package/dist/integrations/oauth-profiles.d.ts +42 -13
  73. package/dist/integrations/onepassword-vault-provider.d.ts +9 -0
  74. package/dist/integrations/slack-bot.d.ts +285 -7
  75. package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
  76. package/dist/integrations/slack-interactions.d.ts +89 -10
  77. package/dist/integrations/slack-mrkdwn.d.ts +41 -0
  78. package/dist/integrations/slack-routing.d.ts +1 -1
  79. package/dist/integrations/slack-session-defaults.d.ts +58 -0
  80. package/dist/integrations/slack-task-file-upload.d.ts +36 -0
  81. package/dist/integrations/slack-tool-review.d.ts +5 -0
  82. package/dist/interaction-agent-access.d.ts +13 -8
  83. package/dist/interaction-agent-version.d.ts +21 -0
  84. package/dist/interaction-holder-heartbeat.d.ts +5 -0
  85. package/dist/interaction-metrics.d.ts +27 -2
  86. package/dist/interaction-route-stages.d.ts +25 -0
  87. package/dist/interaction-secrets-vault.d.ts +20 -0
  88. package/dist/interaction-secrets.d.ts +103 -0
  89. package/dist/mcp/action-catalog-types.d.ts +14 -0
  90. package/dist/mcp/action-catalog.gen.d.ts +2 -0
  91. package/dist/mcp/action-descriptions.d.ts +4 -0
  92. package/dist/mcp/action-schema-imports.gen.d.ts +1 -0
  93. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  94. package/dist/mcp/child-read-evidence.d.ts +9 -0
  95. package/dist/mcp/company-profile-agent-admin.d.ts +12 -0
  96. package/dist/mcp/contract-input.d.ts +13 -0
  97. package/dist/mcp/documents.d.ts +2 -0
  98. package/dist/mcp/first-party-tool-permissions.d.ts +3 -606
  99. package/dist/mcp/notification-tools.d.ts +26 -0
  100. package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
  101. package/dist/mcp/scheduled-task-input.d.ts +1309 -0
  102. package/dist/mcp/scheduled-task-view.d.ts +217 -3
  103. package/dist/mcp/secret-tools.d.ts +14 -0
  104. package/dist/mcp/server.d.ts +108 -5
  105. package/dist/mcp/session-admin-access-tools.d.ts +33 -0
  106. package/dist/mcp/session-event-errors.d.ts +28 -0
  107. package/dist/mcp/session-event-view.d.ts +62 -0
  108. package/dist/mcp/session-view.d.ts +15 -0
  109. package/dist/mcp/session-wait.d.ts +18 -0
  110. package/dist/mcp/site-agent-clamp-tools.d.ts +38 -0
  111. package/dist/mcp/site-data-tools.d.ts +22 -0
  112. package/dist/mcp/site-publication-tools.d.ts +19 -0
  113. package/dist/mcp/site-sql-tools.d.ts +27 -0
  114. package/dist/mcp-oauth.d.ts +37 -4
  115. package/dist/model-calls/chat-completions.d.ts +87 -0
  116. package/dist/model-calls/pending-usage-settlements.d.ts +57 -0
  117. package/dist/model-calls/service.d.ts +36 -0
  118. package/dist/model-catalog.d.ts +9 -3
  119. package/dist/native-push-dispatch.d.ts +69 -0
  120. package/dist/observability.d.ts +3 -1
  121. package/dist/organization-mcp.d.ts +82 -0
  122. package/dist/prepared-mcp-actor.d.ts +77 -0
  123. package/dist/prepared-mcp-permissions.d.ts +8 -0
  124. package/dist/routes/analytics-consent.d.ts +26 -0
  125. package/dist/routes/api-integrations.d.ts +11 -0
  126. package/dist/routes/api-keys.d.ts +7 -7
  127. package/dist/routes/automations.d.ts +2 -0
  128. package/dist/routes/billing.d.ts +59 -0
  129. package/dist/routes/browser-sessions.d.ts +65 -2
  130. package/dist/routes/chat-completions.d.ts +11 -0
  131. package/dist/routes/claude-core-usage.d.ts +20 -0
  132. package/dist/routes/claude-subscription-accounts.d.ts +3 -0
  133. package/dist/routes/claude-subscription-oauth.d.ts +3 -0
  134. package/dist/routes/client-errors.d.ts +182 -0
  135. package/dist/routes/codex-core.d.ts +401 -0
  136. package/dist/routes/codex.d.ts +64 -2
  137. package/dist/routes/computer-sessions.d.ts +6 -1
  138. package/dist/routes/desktop-distribution.d.ts +32 -0
  139. package/dist/routes/github-browser-fonts.gen.d.ts +2 -0
  140. package/dist/routes/github-browser-pages.d.ts +27 -0
  141. package/dist/routes/inbox.d.ts +13 -0
  142. package/dist/routes/insights-response-cache.d.ts +10 -0
  143. package/dist/routes/insights-usage.d.ts +12 -0
  144. package/dist/routes/insights.d.ts +28 -0
  145. package/dist/routes/interaction-placement-message.d.ts +7 -0
  146. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  147. package/dist/routes/model-account-choice.d.ts +13 -0
  148. package/dist/routes/model-account-settings.d.ts +19 -0
  149. package/dist/routes/native-app-auth.d.ts +12 -0
  150. package/dist/routes/organization-integration-policy.d.ts +8 -0
  151. package/dist/routes/organization-integrations.d.ts +6 -0
  152. package/dist/routes/organization-mcp-connections.d.ts +3 -0
  153. package/dist/routes/organization-model-defaults.d.ts +9 -0
  154. package/dist/routes/organization-service-accounts.d.ts +3 -0
  155. package/dist/routes/personal-api-keys.d.ts +3 -0
  156. package/dist/routes/session-admin-access.d.ts +3 -0
  157. package/dist/routes/session-artifact-associations.d.ts +4 -0
  158. package/dist/routes/session-history-imports.d.ts +20 -0
  159. package/dist/routes/sessions.d.ts +38 -2
  160. package/dist/routes/site-data.d.ts +18 -0
  161. package/dist/routes/site-publishing.d.ts +5 -0
  162. package/dist/routes/site-runtime.d.ts +31 -0
  163. package/dist/routes/site-sql.d.ts +16 -0
  164. package/dist/routes/slack-bot-access.d.ts +3 -0
  165. package/dist/routes/subscription-account-pools.d.ts +21 -0
  166. package/dist/routes/subscription-core-http.d.ts +5 -0
  167. package/dist/routes/subscription-pool-access.d.ts +37 -0
  168. package/dist/routes/subscription-pool-core.d.ts +153 -0
  169. package/dist/routes/supergrok.d.ts +6 -6
  170. package/dist/routes/usage-allowances.d.ts +12 -0
  171. package/dist/routes/variable-sets-removed.d.ts +2 -0
  172. package/dist/routes/vault-providers.d.ts +34 -0
  173. package/dist/routes/vaults.d.ts +3 -0
  174. package/dist/routes/workspace-integrations.d.ts +35 -0
  175. package/dist/routes/workspace-model-providers.d.ts +4 -0
  176. package/dist/routes/workspaces.d.ts +11 -2
  177. package/dist/sandbox/agent-update.d.ts +64 -0
  178. package/dist/sandbox/auth-callout.d.ts +8 -0
  179. package/dist/sandbox/channel-a.d.ts +15 -1
  180. package/dist/sandbox/enrollment.d.ts +26 -4
  181. package/dist/sandbox/machine-audience.d.ts +29 -0
  182. package/dist/sandbox/machines.d.ts +15 -2
  183. package/dist/sandbox/metrics-ingestion.d.ts +6 -2
  184. package/dist/sandbox/screen-control.d.ts +85 -0
  185. package/dist/sandbox/viewer.d.ts +5 -1
  186. package/dist/sandbox-file-artifacts.d.ts +17 -1
  187. package/dist/scheduled-task-blockers.d.ts +39 -0
  188. package/dist/scheduled-task-deletion.d.ts +3 -4
  189. package/dist/scheduled-task-lifecycle.d.ts +14 -0
  190. package/dist/secret-answers.d.ts +40 -0
  191. package/dist/session-create-observability.d.ts +5 -0
  192. package/dist/session-event-view-36GO3IL7.js +528 -0
  193. package/dist/session-event-view-36GO3IL7.js.map +1 -0
  194. package/dist/signed-state-secret.d.ts +18 -0
  195. package/dist/site-agent-guard.d.ts +53 -0
  196. package/dist/site-data/operations.d.ts +103 -0
  197. package/dist/site-data/service.d.ts +203 -0
  198. package/dist/site-frame-guard.d.ts +32 -0
  199. package/dist/site-publishing/agent-data-tools.d.ts +84 -0
  200. package/dist/site-publishing/agent-model.d.ts +58 -0
  201. package/dist/site-publishing/agent-session.d.ts +16 -0
  202. package/dist/site-publishing/content-scanner.d.ts +31 -0
  203. package/dist/site-publishing/edge-sync.d.ts +48 -0
  204. package/dist/site-publishing/gateway-signature.d.ts +56 -0
  205. package/dist/site-publishing/limits.d.ts +63 -0
  206. package/dist/site-publishing/page-tool-authority.d.ts +30 -0
  207. package/dist/site-publishing/platform-usage.d.ts +113 -0
  208. package/dist/site-publishing/platform.d.ts +21 -0
  209. package/dist/site-publishing/readiness.d.ts +62 -0
  210. package/dist/site-publishing/runtime.d.ts +179 -0
  211. package/dist/site-publishing/service.d.ts +264 -0
  212. package/dist/site-publishing/session-events.d.ts +22 -0
  213. package/dist/site-publishing/session-views.d.ts +36 -0
  214. package/dist/site-publishing/viewer.d.ts +40 -0
  215. package/dist/site-session-origin.d.ts +31 -6
  216. package/dist/site-sql/format.d.ts +47 -0
  217. package/dist/site-sql/gateway.d.ts +50 -0
  218. package/dist/site-sql/service.d.ts +213 -0
  219. package/dist/temporal-schedule-sync.d.ts +26 -0
  220. package/dist/transcription/billing-refusal.d.ts +8 -0
  221. package/dist/transcription/normalize.d.ts +23 -0
  222. package/dist/transcription/providers/azure-mai.d.ts +12 -0
  223. package/dist/transcription/providers/azure-openai.d.ts +4 -0
  224. package/dist/transcription/providers/openai.d.ts +16 -0
  225. package/dist/transcription/providers/subscription-core.d.ts +39 -0
  226. package/dist/transcription/service.d.ts +10 -1
  227. package/dist/transcription/wav-duration.d.ts +8 -0
  228. package/dist/workspace-tool-gateway-attestations.d.ts +37 -0
  229. package/dist/workspace-tool-gateway-observability.d.ts +16 -0
  230. package/dist/workspace-tool-gateway-secrets.d.ts +27 -0
  231. package/dist/workspace-tool-gateway.d.ts +102 -6
  232. package/dist/workspace-tool-target-fence.d.ts +116 -0
  233. package/dist/workspace-tool-target.d.ts +225 -0
  234. package/dist/workspace-webhook-dispatch.d.ts +26 -0
  235. package/dist/xai-subscription-core.d.ts +60 -0
  236. package/package.json +25 -20
  237. package/src/access-grant-rls.ts +64 -14
  238. package/src/admission-trace.ts +14 -0
  239. package/src/app.ts +1684 -252
  240. package/src/artifact-catalog-cursor.ts +1 -0
  241. package/src/attached-browser-viewer.ts +26 -0
  242. package/src/auth/managed-auth-attempt-context.ts +9 -0
  243. package/src/auth/managed-auth-rate-limits.ts +252 -0
  244. package/src/auth/managed-auth-session-adapter.ts +64 -29
  245. package/src/auth/managed-auth.ts +303 -35
  246. package/src/auth/managed-email.ts +18 -1
  247. package/src/auth/managed-sign-in-notifications.ts +81 -0
  248. package/src/auth/new-signups-gate.ts +49 -0
  249. package/src/auth/organization-user-setup.ts +3 -3
  250. package/src/auth/signup-funnel-metrics.ts +164 -0
  251. package/src/azure-live.ts +103 -0
  252. package/src/browser-action-fences.ts +45 -0
  253. package/src/browser-controller-authority.ts +1 -186
  254. package/src/browser-ephemeral.ts +39 -0
  255. package/src/browser-lost-runtime-end.ts +71 -0
  256. package/src/browser-state-authority.ts +1 -236
  257. package/src/claude-default-models.ts +37 -0
  258. package/src/claude-subscription-core.ts +154 -0
  259. package/src/claude-subscription-oauth.ts +352 -0
  260. package/src/claude-subscription-profile.ts +73 -0
  261. package/src/claude-subscription-usage.ts +51 -0
  262. package/src/claude-workspace-connection.ts +82 -0
  263. package/src/codemode.ts +236 -39
  264. package/src/codex-realtime.ts +181 -53
  265. package/src/codex-session-pointers.ts +80 -0
  266. package/src/company-brain-okf.ts +1 -1
  267. package/src/connected-machine-computer-access.ts +2 -1
  268. package/src/connection-ownership.ts +19 -13
  269. package/src/controller-data-plane.ts +34 -0
  270. package/src/credit-promotion-snapshot.ts +36 -0
  271. package/src/editable-artifact-production.ts +10 -0
  272. package/src/editable-artifact-source-session.ts +123 -0
  273. package/src/editable-artifact-websocket.ts +16 -3
  274. package/src/fatal-process-boundary.ts +67 -3
  275. package/src/gateway-realtime.ts +12 -8
  276. package/src/github-access.ts +31 -5
  277. package/src/github-browser-flow.ts +1 -1
  278. package/src/http/acting-person.ts +105 -0
  279. package/src/http/api-contract-compat.ts +49 -0
  280. package/src/http/api-error.ts +210 -2
  281. package/src/http/auth.ts +68 -4
  282. package/src/http/cors.ts +37 -5
  283. package/src/http/deprecation.ts +118 -0
  284. package/src/http/keyed-admission.ts +29 -0
  285. package/src/http/local-browser-boundary.ts +343 -0
  286. package/src/http/metrics-listener.ts +57 -0
  287. package/src/http/path-identifier.ts +54 -0
  288. package/src/http/registered-route-label.ts +34 -0
  289. package/src/http/rejection-telemetry.ts +120 -0
  290. package/src/http/request-body.ts +127 -0
  291. package/src/http/request-source.ts +154 -13
  292. package/src/http/sse.ts +88 -28
  293. package/src/http/unmatched-route.ts +60 -0
  294. package/src/http/user-content.ts +158 -0
  295. package/src/http/variable-sets-removed.ts +122 -0
  296. package/src/http/websocket-upgrade-dispatch.ts +29 -0
  297. package/src/index.ts +107 -57
  298. package/src/integration-connect-metrics.ts +228 -0
  299. package/src/integration-endpoint-test.ts +291 -0
  300. package/src/integrations/api-integrations.ts +81 -7
  301. package/src/integrations/atlassian.ts +202 -83
  302. package/src/integrations/fiken.ts +66 -38
  303. package/src/integrations/github-app-connect.ts +64 -18
  304. package/src/integrations/github-lens-connect.ts +8 -29
  305. package/src/integrations/google-drive.ts +448 -152
  306. package/src/integrations/integration-commit-authority.ts +69 -0
  307. package/src/integrations/marketplace-plugin.ts +1 -1
  308. package/src/integrations/oauth-client.ts +733 -193
  309. package/src/integrations/oauth-profiles.ts +116 -45
  310. package/src/integrations/onepassword-vault-provider.ts +16 -0
  311. package/src/integrations/personal-github-repositories.ts +1 -1
  312. package/src/integrations/personal-github.ts +61 -35
  313. package/src/integrations/provider-oauth.ts +67 -85
  314. package/src/integrations/slack-app-home.ts +5 -5
  315. package/src/integrations/slack-bot.ts +893 -201
  316. package/src/integrations/slack-file-upload-flow.ts +147 -0
  317. package/src/integrations/slack-install.ts +7 -7
  318. package/src/integrations/slack-interactions.ts +1589 -299
  319. package/src/integrations/slack-mrkdwn.ts +193 -0
  320. package/src/integrations/slack-routing.ts +2 -2
  321. package/src/integrations/slack-session-defaults.ts +224 -0
  322. package/src/integrations/slack-task-file-upload.ts +323 -0
  323. package/src/integrations/slack-tool-review.ts +65 -0
  324. package/src/integrations/social-oauth.ts +90 -58
  325. package/src/interaction-agent-access.ts +40 -20
  326. package/src/interaction-agent-version.ts +85 -0
  327. package/src/interaction-holder-heartbeat.ts +24 -8
  328. package/src/interaction-metrics.ts +164 -2
  329. package/src/interaction-route-stages.ts +94 -0
  330. package/src/interaction-secrets-vault.ts +213 -0
  331. package/src/interaction-secrets.ts +426 -0
  332. package/src/mcp/action-catalog-types.ts +14 -0
  333. package/src/mcp/action-catalog.gen.ts +8014 -0
  334. package/src/mcp/action-descriptions.ts +299 -0
  335. package/src/mcp/action-schema-imports.gen.ts +304 -0
  336. package/src/mcp/capability-account-readiness.ts +17 -0
  337. package/src/mcp/child-read-evidence.ts +19 -0
  338. package/src/mcp/company-profile-agent-admin.ts +13 -0
  339. package/src/mcp/contract-input.ts +165 -0
  340. package/src/mcp/documents.ts +9 -2
  341. package/src/mcp/editable-artifacts.ts +29 -11
  342. package/src/mcp/files.ts +5 -1
  343. package/src/mcp/first-party-tool-permissions.ts +7 -285
  344. package/src/mcp/knowledge-entries.ts +108 -34
  345. package/src/mcp/notification-tools.ts +480 -0
  346. package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
  347. package/src/mcp/scheduled-task-input.ts +89 -0
  348. package/src/mcp/scheduled-task-view.ts +63 -3
  349. package/src/mcp/secret-tools.ts +185 -0
  350. package/src/mcp/server.ts +2213 -1116
  351. package/src/mcp/session-admin-access-tools.ts +163 -0
  352. package/src/mcp/session-event-errors.ts +123 -0
  353. package/src/mcp/session-event-view.ts +395 -13
  354. package/src/mcp/session-view.ts +3 -0
  355. package/src/mcp/session-wait.ts +67 -7
  356. package/src/mcp/site-agent-clamp-tools.ts +140 -0
  357. package/src/mcp/site-data-tools.ts +339 -0
  358. package/src/mcp/site-publication-tools.ts +201 -0
  359. package/src/mcp/site-sql-tools.ts +427 -0
  360. package/src/mcp-oauth.ts +635 -45
  361. package/src/model-calls/chat-completions.ts +352 -0
  362. package/src/model-calls/pending-usage-settlements.ts +125 -0
  363. package/src/model-calls/service.ts +694 -0
  364. package/src/model-catalog.ts +68 -17
  365. package/src/native-push-dispatch.ts +457 -0
  366. package/src/observability.ts +15 -1
  367. package/src/organization-mcp.ts +485 -0
  368. package/src/prepared-mcp-actor.ts +154 -0
  369. package/src/prepared-mcp-permissions.ts +38 -0
  370. package/src/routes/analytics-consent.ts +123 -0
  371. package/src/routes/api-integrations.ts +268 -9
  372. package/src/routes/api-keys.ts +164 -25
  373. package/src/routes/artifact-catalog.ts +111 -0
  374. package/src/routes/automations.ts +163 -90
  375. package/src/routes/billing.ts +535 -59
  376. package/src/routes/browser-sessions.ts +966 -342
  377. package/src/routes/canonical-human-identities.ts +61 -0
  378. package/src/routes/capabilities.ts +112 -5
  379. package/src/routes/channels.ts +4 -3
  380. package/src/routes/chat-completions.ts +235 -0
  381. package/src/routes/claude-core-usage.ts +147 -0
  382. package/src/routes/claude-subscription-accounts.ts +233 -0
  383. package/src/routes/claude-subscription-oauth.ts +175 -0
  384. package/src/routes/client-errors.ts +394 -0
  385. package/src/routes/codex-core.ts +687 -0
  386. package/src/routes/codex.ts +1277 -1119
  387. package/src/routes/company-brain.ts +4 -0
  388. package/src/routes/computer-sessions.ts +319 -85
  389. package/src/routes/connect.ts +277 -175
  390. package/src/routes/connections.ts +690 -363
  391. package/src/routes/desktop-distribution.ts +217 -0
  392. package/src/routes/documents.ts +13 -7
  393. package/src/routes/editable-artifacts.ts +74 -4
  394. package/src/routes/enrollments.ts +110 -16
  395. package/src/routes/external-identity-links.ts +13 -12
  396. package/src/routes/files.ts +41 -16
  397. package/src/routes/github-browser-fonts.gen.ts +12 -0
  398. package/src/routes/github-browser-pages.ts +258 -0
  399. package/src/routes/github.ts +200 -75
  400. package/src/routes/inbox.ts +353 -0
  401. package/src/routes/insights-response-cache.ts +83 -0
  402. package/src/routes/insights-usage.ts +345 -0
  403. package/src/routes/insights.ts +103 -8
  404. package/src/routes/install.ts +51 -10
  405. package/src/routes/integration-facets.ts +70 -28
  406. package/src/routes/interaction-placement-message.ts +12 -0
  407. package/src/routes/knowledge.ts +136 -27
  408. package/src/routes/machines.ts +189 -135
  409. package/src/routes/managed-auth-session-sets.ts +43 -10
  410. package/src/routes/managed-onboarding.ts +39 -19
  411. package/src/routes/managed-sign-in-methods.ts +433 -0
  412. package/src/routes/memory-slack-publications.ts +3 -2
  413. package/src/routes/model-account-choice.ts +194 -0
  414. package/src/routes/model-account-settings.ts +235 -0
  415. package/src/routes/model-connection-access.ts +210 -48
  416. package/src/routes/native-app-auth.ts +296 -0
  417. package/src/routes/organization-integration-policy.ts +116 -0
  418. package/src/routes/organization-integrations.ts +326 -0
  419. package/src/routes/organization-mcp-connections.ts +183 -0
  420. package/src/routes/organization-memberships.ts +205 -19
  421. package/src/routes/organization-model-defaults.ts +110 -0
  422. package/src/routes/organization-model-providers.ts +74 -3
  423. package/src/routes/organization-service-accounts.ts +128 -0
  424. package/src/routes/organization-sessions.ts +21 -5
  425. package/src/routes/personal-api-keys.ts +184 -0
  426. package/src/routes/personal-github-git-broker.ts +2 -2
  427. package/src/routes/personal-github.ts +9 -4
  428. package/src/routes/plugins.ts +46 -7
  429. package/src/routes/pr-review-github.ts +105 -118
  430. package/src/routes/pr-review.ts +88 -82
  431. package/src/routes/preference-registry.ts +18 -0
  432. package/src/routes/rigs.ts +48 -13
  433. package/src/routes/scheduled-tasks.ts +347 -27
  434. package/src/routes/session-admin-access.ts +271 -0
  435. package/src/routes/session-artifact-associations.ts +164 -0
  436. package/src/routes/session-history-imports.ts +305 -0
  437. package/src/routes/sessions.ts +1682 -363
  438. package/src/routes/site-data.ts +340 -0
  439. package/src/routes/site-publishing.ts +584 -0
  440. package/src/routes/site-runtime.ts +1944 -0
  441. package/src/routes/site-sql.ts +263 -0
  442. package/src/routes/skill-content.ts +72 -7
  443. package/src/routes/skills.ts +5 -4
  444. package/src/routes/slack-bot-access.ts +108 -0
  445. package/src/routes/social.ts +27 -15
  446. package/src/routes/subscription-account-pools.ts +209 -0
  447. package/src/routes/subscription-core-http.ts +18 -0
  448. package/src/routes/subscription-pool-access.ts +180 -0
  449. package/src/routes/subscription-pool-core.ts +552 -0
  450. package/src/routes/supergrok.ts +174 -655
  451. package/src/routes/transcription-recordings.ts +71 -4
  452. package/src/routes/transcriptions.ts +26 -2
  453. package/src/routes/usage-allowances.ts +381 -0
  454. package/src/routes/variable-sets-removed.ts +19 -0
  455. package/src/routes/vault-providers.ts +782 -0
  456. package/src/routes/vaults.ts +421 -0
  457. package/src/routes/video-generation.ts +30 -12
  458. package/src/routes/workspace-artifacts.ts +165 -53
  459. package/src/routes/workspace-integrations.ts +576 -0
  460. package/src/routes/workspace-model-providers.ts +148 -0
  461. package/src/routes/workspace-state.ts +4 -0
  462. package/src/routes/workspaces.ts +625 -117
  463. package/src/sandbox/agent-update.ts +315 -0
  464. package/src/sandbox/auth-callout.ts +104 -21
  465. package/src/sandbox/channel-a.ts +165 -55
  466. package/src/sandbox/enrollment.ts +131 -22
  467. package/src/sandbox/machine-audience.ts +192 -0
  468. package/src/sandbox/machines.ts +75 -31
  469. package/src/sandbox/metrics-ingestion.ts +133 -2
  470. package/src/sandbox/rematerialize.ts +51 -21
  471. package/src/sandbox/screen-control.ts +289 -0
  472. package/src/sandbox/viewer.ts +33 -58
  473. package/src/sandbox-file-artifacts.ts +53 -27
  474. package/src/scheduled-task-blockers.ts +293 -0
  475. package/src/scheduled-task-deletion.ts +13 -3
  476. package/src/scheduled-task-lifecycle.ts +84 -0
  477. package/src/secret-answers.ts +119 -0
  478. package/src/session-create-observability.ts +46 -0
  479. package/src/session-realtime-context.ts +26 -4
  480. package/src/signed-state-secret.ts +58 -0
  481. package/src/site-agent-guard.ts +261 -0
  482. package/src/site-data/operations.ts +777 -0
  483. package/src/site-data/service.ts +1043 -0
  484. package/src/site-frame-guard.ts +107 -0
  485. package/src/site-publishing/agent-data-tools.ts +250 -0
  486. package/src/site-publishing/agent-model.ts +127 -0
  487. package/src/site-publishing/agent-session.ts +148 -0
  488. package/src/site-publishing/content-scanner.ts +93 -0
  489. package/src/site-publishing/edge-sync.ts +148 -0
  490. package/src/site-publishing/gateway-signature.ts +198 -0
  491. package/src/site-publishing/limits.ts +315 -0
  492. package/src/site-publishing/page-tool-authority.ts +111 -0
  493. package/src/site-publishing/platform-usage.ts +479 -0
  494. package/src/site-publishing/platform.ts +98 -0
  495. package/src/site-publishing/readiness.ts +330 -0
  496. package/src/site-publishing/runtime.ts +831 -0
  497. package/src/site-publishing/service.ts +1282 -0
  498. package/src/site-publishing/session-events.ts +262 -0
  499. package/src/site-publishing/session-views.ts +262 -0
  500. package/src/site-publishing/viewer.ts +75 -0
  501. package/src/site-session-origin.ts +91 -6
  502. package/src/site-sql/format.ts +134 -0
  503. package/src/site-sql/gateway.ts +168 -0
  504. package/src/site-sql/service.ts +1636 -0
  505. package/src/site-uploads.ts +27 -5
  506. package/src/temporal-schedule-sync.ts +74 -0
  507. package/src/transcription/billing-refusal.ts +14 -0
  508. package/src/transcription/normalize.ts +77 -0
  509. package/src/transcription/providers/azure-mai.ts +98 -0
  510. package/src/transcription/providers/azure-openai.ts +16 -7
  511. package/src/transcription/providers/codex-subscription.ts +190 -67
  512. package/src/transcription/providers/openai.ts +79 -5
  513. package/src/transcription/providers/subscription-core.ts +125 -0
  514. package/src/transcription/providers/xai-subscription.ts +81 -83
  515. package/src/transcription/segmenter.ts +7 -1
  516. package/src/transcription/service.ts +239 -10
  517. package/src/transcription/wav-duration.ts +39 -0
  518. package/src/workspace-deletion.ts +25 -1
  519. package/src/workspace-tool-gateway-attestations.ts +133 -0
  520. package/src/workspace-tool-gateway-observability.ts +64 -0
  521. package/src/workspace-tool-gateway-secrets.ts +104 -0
  522. package/src/workspace-tool-gateway.ts +921 -103
  523. package/src/workspace-tool-target-fence.ts +380 -0
  524. package/src/workspace-tool-target.ts +554 -0
  525. package/src/workspace-webhook-dispatch.ts +225 -0
  526. package/src/xai-realtime.ts +85 -81
  527. package/src/xai-subscription-core.ts +172 -0
  528. package/dist/browser-auth-broker.d.ts +0 -20
  529. package/dist/chunk-AJ62T5QN.js.map +0 -1
  530. package/dist/connection-authority-owner.d.ts +0 -59
  531. package/dist/routes/connection-authorities.d.ts +0 -3
  532. package/dist/routes/environments.d.ts +0 -5
  533. package/dist/routes/host-mcp-bindings.d.ts +0 -5
  534. package/dist/routes/host-mcp-resolvers.d.ts +0 -3
  535. package/dist/routes/packs.d.ts +0 -3
  536. package/dist/session-event-view-MU3HMNV3.js +0 -264
  537. package/dist/session-event-view-MU3HMNV3.js.map +0 -1
  538. package/dist/xai-subscription-auth.d.ts +0 -23
  539. package/src/browser-auth-broker.ts +0 -155
  540. package/src/connection-authority-owner.ts +0 -61
  541. package/src/routes/connection-authorities.ts +0 -139
  542. package/src/routes/environments.ts +0 -388
  543. package/src/routes/host-mcp-bindings.ts +0 -122
  544. package/src/routes/host-mcp-resolvers.ts +0 -41
  545. package/src/routes/packs.ts +0 -784
  546. package/src/xai-subscription-auth.ts +0 -139
@@ -0,0 +1,1636 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ compileSiteSql,
4
+ isSiteSqlPrincipalParam,
5
+ isSiteSqlTableName,
6
+ SITE_SQL_HARD_LIMITS,
7
+ SITE_SQL_IDENTIFIER,
8
+ SITE_SQL_NAME,
9
+ type SiteDbExportResponse,
10
+ type SiteDbInspectResponse,
11
+ type SiteDbMeter,
12
+ type SiteDbMigrateResponse,
13
+ type SiteDbRunResponse,
14
+ type SiteDbSchemaEntry,
15
+ type SiteSqlDescribeResponse,
16
+ type SiteSqlErrorCode,
17
+ type SiteSqlExportFormat,
18
+ type SiteSqlImportRequest,
19
+ type SiteSqlImportResponse,
20
+ type SiteSqlInspectResponse,
21
+ type SiteSqlLimits,
22
+ type SiteSqlMigrateResponse,
23
+ type SiteSqlMigrationResponse,
24
+ type SiteSqlQueryAudience,
25
+ type SiteSqlQueryDefinitionRequest,
26
+ type SiteSqlQueryResponse,
27
+ type SiteSqlResetResponse,
28
+ type SiteSqlRunResponse,
29
+ type SiteSqlValue,
30
+ type SiteLimitProfileKind,
31
+ } from "@opengeni/contracts";
32
+ import type { ApiRouteDeps } from "@opengeni/core";
33
+ import {
34
+ appendSiteSqlMigration,
35
+ clearSiteSqlDefinitions,
36
+ consumeSiteSqlRate,
37
+ deleteSiteSqlQuery,
38
+ getSiteSqlDatabase,
39
+ getSiteSqlQuery,
40
+ getSitePublication,
41
+ getWorkspaceSiteLimitProfile,
42
+ listSiteSqlMigrations,
43
+ listSiteSqlQueries,
44
+ listSiteSqlSiteIds,
45
+ markSiteSqlRebuilt,
46
+ recordSitePlatformUsageInTransaction,
47
+ recordSiteSqlUsage,
48
+ recordUsageEvent,
49
+ setSiteSqlEnabled,
50
+ SiteDataError,
51
+ siteSqlMigrationBytes,
52
+ siteSqlMigrationQuotaError,
53
+ SiteSqlStoreError,
54
+ upsertSiteSqlQuery,
55
+ withSiteSqlDatabaseLock,
56
+ type Database,
57
+ type SitePlatformUsageRecord,
58
+ type SiteSqlDatabase,
59
+ type SiteSqlScope,
60
+ type SiteSqlStoredMigration,
61
+ type SiteSqlStoredQuery,
62
+ } from "@opengeni/db";
63
+ import { parseSiteDataCsv } from "../site-data/operations";
64
+ import { sourceSiteLimitProfile } from "../site-publishing/limits";
65
+ import {
66
+ afterSitePlatformUsage,
67
+ requireSitePlatformOpen,
68
+ requireSitePlatformQuota,
69
+ } from "../site-publishing/platform-usage";
70
+ import {
71
+ resolveSiteDataAccess,
72
+ type SiteDataAccess,
73
+ type SiteDataSurface,
74
+ } from "../site-data/service";
75
+ import {
76
+ callSiteDb,
77
+ SiteDbCallError,
78
+ siteSqlGatewayConfig,
79
+ siteSqlOffered,
80
+ type SiteDbFetch,
81
+ type SiteSqlGatewayConfig,
82
+ } from "./gateway";
83
+ import {
84
+ siteSqlCsvLine,
85
+ siteSqlImportRows,
86
+ siteSqlInsertStatements,
87
+ siteSqlRowObjects,
88
+ siteSqlSchemaEntries,
89
+ siteSqlSchemaStatements,
90
+ } from "./format";
91
+
92
+ /**
93
+ * Site SQL: optional per-Site Cloudflare storage. One SQLite Durable Object
94
+ * per Site holds the rows; Postgres holds whether the owner turned it on, the
95
+ * ordered migrations and the named queries (so the database can always be
96
+ * rebuilt), usage counters and call windows. Every surface (Site pages through
97
+ * the workspace API or site-runtime, the console and first-party agent tools)
98
+ * goes through here: it resolves the caller with Site Data's rules (a Site
99
+ * frame is always a viewer; managers manage), checks audiences, binds the
100
+ * viewer's principal into `:viewer` parameters, applies limits, meters usage
101
+ * in `usage_only` mode, and only then calls the gateway, which re-checks every
102
+ * statement in the Durable Object.
103
+ */
104
+
105
+ export type SiteSqlDeps = Pick<ApiRouteDeps, "db" | "settings">;
106
+
107
+ /** Who is calling: whatever Site Data accepts (an access grant, or a published Site viewer). */
108
+ export type SiteSqlAuthority = Parameters<typeof resolveSiteDataAccess>[1];
109
+
110
+ export class SiteSqlError extends Error {
111
+ constructor(
112
+ readonly code: SiteSqlErrorCode,
113
+ message: string,
114
+ readonly details: Record<string, unknown> = {},
115
+ ) {
116
+ super(message);
117
+ this.name = "SiteSqlError";
118
+ }
119
+ }
120
+
121
+ /** The transport to the gateway; tests replace `fetch`. */
122
+ export const siteSqlTransport: { fetch: SiteDbFetch } = {
123
+ fetch: (input, init) => fetch(input, init),
124
+ };
125
+
126
+ /** Calls per minute all of a Site's managers may make together (console and agents). */
127
+ export const SITE_SQL_MANAGER_CALLS_PER_MINUTE = 600;
128
+ /** One export response stops adding rows past this many bytes and returns a next cursor. */
129
+ export const SITE_SQL_EXPORT_MAX_BYTES = SITE_SQL_HARD_LIMITS.exportBytesPerResponse;
130
+ /**
131
+ * Default size of one SQL export piece: well under the 2 MB a SQL import
132
+ * takes per call, so every piece of a large dump re-imports in one call.
133
+ */
134
+ export const SITE_SQL_EXPORT_SQL_DEFAULT_BYTES = 1024 * 1024;
135
+ /** Rows per export page the API asks the Durable Object for. */
136
+ const EXPORT_PAGE_ROWS = 1_000;
137
+ /** Most JSON bytes one insert call carries (the gateway accepts 8 MiB). */
138
+ const IMPORT_CHUNK_BYTES = 2 * 1024 * 1024;
139
+ /** Most characters and statements of one data script call of a SQL import. */
140
+ const IMPORT_SCRIPT_CHARS = 1_000_000;
141
+ const IMPORT_SCRIPT_STATEMENTS = 1_000;
142
+ /** Most JSON bytes of migrations one migrate call carries (the gateway accepts 8 MiB). */
143
+ const MIGRATE_CALL_BYTES = 4 * 1024 * 1024;
144
+ /** CSV imports accept at most this many columns. */
145
+ const IMPORT_MAX_COLUMNS = 1_000;
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Errors
149
+ // ---------------------------------------------------------------------------
150
+
151
+ const DB_CODES: Record<string, SiteSqlErrorCode> = {
152
+ validation: "validation",
153
+ sql_error: "sql_error",
154
+ quota_exceeded: "quota_exceeded",
155
+ conflict: "conflict",
156
+ too_large: "validation",
157
+ };
158
+
159
+ /** Typed errors from every layer as SiteSqlError. */
160
+ export function toSiteSqlError(error: unknown): unknown {
161
+ if (error instanceof SiteSqlError) return error;
162
+ if (error instanceof SiteDataError)
163
+ return new SiteSqlError(
164
+ error.code === "quota_exceeded" ? "quota_exceeded" : error.code,
165
+ error.message,
166
+ error.details,
167
+ );
168
+ if (error instanceof SiteSqlStoreError)
169
+ return new SiteSqlError(error.code, error.message, error.details);
170
+ if (error instanceof SiteDbCallError) {
171
+ if (error.code === "replayed")
172
+ return new SiteSqlError("unavailable", "The Site database saw this request already", {
173
+ retryable: true,
174
+ });
175
+ const code = DB_CODES[error.code] ?? "unavailable";
176
+ return new SiteSqlError(code, error.message, {
177
+ ...(code === "unavailable" ? { retryable: !error.outcomeUnknown } : {}),
178
+ ...(error.outcomeUnknown ? { outcomeUnknown: true } : {}),
179
+ });
180
+ }
181
+ return error;
182
+ }
183
+
184
+ async function guarded<T>(fn: () => Promise<T>): Promise<T> {
185
+ try {
186
+ return await fn();
187
+ } catch (error) {
188
+ throw toSiteSqlError(error);
189
+ }
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // Limits
194
+ // ---------------------------------------------------------------------------
195
+
196
+ export type SiteSqlLimitSettings = Pick<
197
+ SiteSqlDeps["settings"],
198
+ | "sitesSqlFreeStorageBytes"
199
+ | "sitesSqlFreeRowsReadPerMonth"
200
+ | "sitesSqlFreeRowsWrittenPerMonth"
201
+ | "sitesSqlFreeCallsPerMinutePerViewer"
202
+ | "sitesSqlFreeCallsPerMinutePerSite"
203
+ | "sitesSqlMaxRowsPerCall"
204
+ | "sitesSqlMaxResponseBytes"
205
+ | "sitesCreditsLimitMultiplier"
206
+ >;
207
+
208
+ /** The limit profile kinds of published Sites (docs/published-sites.md). */
209
+ export type SiteSqlProfileKind = SiteLimitProfileKind;
210
+
211
+ /** Flood brakes that hold under every profile, including unlimited. */
212
+ export const SITE_SQL_FLOOD_BRAKES = Object.freeze({
213
+ callsPerMinutePerViewer: 600,
214
+ callsPerMinutePerSite: 6_000,
215
+ });
216
+
217
+ /**
218
+ * Site SQL limits for a limit profile: standard and custom use the settings
219
+ * (custom overrides carry no SQL fields yet), credits multiplies the storage,
220
+ * monthly row and per-viewer caps, and unlimited removes the monthly caps but
221
+ * keeps the hard storage ceiling and the flood brakes. Rows per call and
222
+ * response size are the same everywhere. The per-Site monthly row quotas are
223
+ * off unless configured (platform usage is metered per workspace instead),
224
+ * and storage per Site defaults to the hard ceiling.
225
+ */
226
+ export function siteSqlLimits(
227
+ settings: SiteSqlLimitSettings,
228
+ kind: SiteSqlProfileKind = "standard",
229
+ ): SiteSqlLimits {
230
+ const rowsRead = settings.sitesSqlFreeRowsReadPerMonth ?? null;
231
+ const rowsWritten = settings.sitesSqlFreeRowsWrittenPerMonth ?? null;
232
+ const brake = (value: number, ceiling: number) => Math.min(value, ceiling);
233
+ const common = {
234
+ maxRowsPerCall: brake(settings.sitesSqlMaxRowsPerCall, SITE_SQL_HARD_LIMITS.maxRowsPerCall),
235
+ maxResponseBytes: brake(
236
+ settings.sitesSqlMaxResponseBytes,
237
+ SITE_SQL_HARD_LIMITS.maxResponseBytes,
238
+ ),
239
+ };
240
+ const free: SiteSqlLimits = {
241
+ storageBytesPerSite: brake(
242
+ settings.sitesSqlFreeStorageBytes,
243
+ SITE_SQL_HARD_LIMITS.storageBytesPerSite,
244
+ ),
245
+ rowsReadPerSitePerMonth: rowsRead,
246
+ rowsWrittenPerSitePerMonth: rowsWritten,
247
+ callsPerMinutePerViewer: brake(
248
+ settings.sitesSqlFreeCallsPerMinutePerViewer,
249
+ SITE_SQL_FLOOD_BRAKES.callsPerMinutePerViewer,
250
+ ),
251
+ callsPerMinutePerSite: brake(
252
+ settings.sitesSqlFreeCallsPerMinutePerSite,
253
+ SITE_SQL_FLOOD_BRAKES.callsPerMinutePerSite,
254
+ ),
255
+ ...common,
256
+ };
257
+ if (kind === "credits") {
258
+ const factor = settings.sitesCreditsLimitMultiplier;
259
+ return {
260
+ ...free,
261
+ storageBytesPerSite: brake(
262
+ settings.sitesSqlFreeStorageBytes * factor,
263
+ SITE_SQL_HARD_LIMITS.storageBytesPerSite,
264
+ ),
265
+ rowsReadPerSitePerMonth: rowsRead === null ? null : rowsRead * factor,
266
+ rowsWrittenPerSitePerMonth: rowsWritten === null ? null : rowsWritten * factor,
267
+ callsPerMinutePerViewer: brake(
268
+ settings.sitesSqlFreeCallsPerMinutePerViewer * factor,
269
+ SITE_SQL_FLOOD_BRAKES.callsPerMinutePerViewer,
270
+ ),
271
+ callsPerMinutePerSite: brake(
272
+ settings.sitesSqlFreeCallsPerMinutePerSite * factor,
273
+ SITE_SQL_FLOOD_BRAKES.callsPerMinutePerSite,
274
+ ),
275
+ };
276
+ }
277
+ if (kind === "unlimited")
278
+ return {
279
+ ...free,
280
+ storageBytesPerSite: SITE_SQL_HARD_LIMITS.storageBytesPerSite,
281
+ rowsReadPerSitePerMonth: null,
282
+ rowsWrittenPerSitePerMonth: null,
283
+ callsPerMinutePerViewer: SITE_SQL_FLOOD_BRAKES.callsPerMinutePerViewer,
284
+ callsPerMinutePerSite: SITE_SQL_FLOOD_BRAKES.callsPerMinutePerSite,
285
+ };
286
+ return free;
287
+ }
288
+
289
+ /**
290
+ * The limit profile in force for a Site, the same one published Sites use:
291
+ * the Site's own profile, else the workspace default, else standard. Sites that
292
+ * were never published have no profile of their own.
293
+ */
294
+ export async function siteSqlProfileKind(
295
+ deps: Pick<SiteSqlDeps, "db">,
296
+ scope: SiteSqlScope,
297
+ ): Promise<SiteSqlProfileKind> {
298
+ const publishing = { accountId: scope.accountId, workspaceId: scope.workspaceId };
299
+ const publication = await getSitePublication(deps.db, publishing, scope.siteId);
300
+ const site = publication?.limitProfile ?? null;
301
+ const workspace = site ? null : await getWorkspaceSiteLimitProfile(deps.db, publishing);
302
+ return sourceSiteLimitProfile(site, workspace).kind;
303
+ }
304
+
305
+ // ---------------------------------------------------------------------------
306
+ // Access
307
+ // ---------------------------------------------------------------------------
308
+
309
+ type Resolved = {
310
+ access: SiteDataAccess;
311
+ scope: SiteSqlScope;
312
+ /** The gateway, when its origin and secret are configured (reaching existing databases). */
313
+ gateway: SiteSqlGatewayConfig | null;
314
+ /** Whether this deployment offers Site SQL (the flag and the gateway). */
315
+ offered: boolean;
316
+ guest: boolean;
317
+ };
318
+
319
+ async function resolve(
320
+ deps: SiteSqlDeps,
321
+ authority: SiteSqlAuthority,
322
+ siteId: string,
323
+ surface: SiteDataSurface,
324
+ ): Promise<Resolved> {
325
+ const access = await resolveSiteDataAccess(deps, authority, siteId, surface);
326
+ return {
327
+ access,
328
+ scope: {
329
+ accountId: access.caller.accountId,
330
+ workspaceId: access.caller.workspaceId,
331
+ siteId,
332
+ },
333
+ gateway: siteSqlGatewayConfig(deps.settings),
334
+ offered: siteSqlOffered(deps.settings),
335
+ guest: (access.caller as { guest?: boolean }).guest === true,
336
+ };
337
+ }
338
+
339
+ function unavailable(): SiteSqlError {
340
+ return new SiteSqlError("unavailable", "Site SQL is not available on this deployment");
341
+ }
342
+
343
+ /**
344
+ * The gateway for an operation. `offered` (turning on, migrating, rebuilding,
345
+ * importing and running SQL) needs Site SQL offered; `configured` (export,
346
+ * inspect, dropping the data) only needs the gateway, so data can always
347
+ * leave even after the flag is turned off.
348
+ */
349
+ function requireGateway(
350
+ resolved: Resolved,
351
+ need: "offered" | "configured" = "offered",
352
+ ): SiteSqlGatewayConfig {
353
+ if (!resolved.gateway || (need === "offered" && !resolved.offered)) throw unavailable();
354
+ return resolved.gateway;
355
+ }
356
+
357
+ /** The Site's database row; `enabled` requires the owner to have turned it on. */
358
+ async function requireDatabase(
359
+ deps: SiteSqlDeps,
360
+ resolved: Resolved,
361
+ options: { enabled: boolean },
362
+ ): Promise<SiteSqlDatabase> {
363
+ const database = await getSiteSqlDatabase(deps.db, resolved.scope);
364
+ if (!database || (options.enabled && !database.enabled))
365
+ throw new SiteSqlError(
366
+ "disabled",
367
+ database
368
+ ? "Site SQL is turned off for this Site"
369
+ : "Site SQL is not turned on for this Site; turn it on first",
370
+ );
371
+ return database;
372
+ }
373
+
374
+ async function limitsFor(
375
+ deps: SiteSqlDeps,
376
+ scope: SiteSqlScope,
377
+ kind?: SiteSqlProfileKind,
378
+ ): Promise<SiteSqlLimits> {
379
+ return siteSqlLimits(deps.settings, kind ?? (await siteSqlProfileKind(deps, scope)));
380
+ }
381
+
382
+ /** Count a call against the caller's windows before any work. */
383
+ async function consume(deps: SiteSqlDeps, resolved: Resolved, limits: SiteSqlLimits) {
384
+ const { caller } = resolved.access;
385
+ const windows: Array<readonly [string, number | null, string]> =
386
+ caller.role === "manager" && !caller.framed
387
+ ? [
388
+ [
389
+ "manager:*",
390
+ SITE_SQL_MANAGER_CALLS_PER_MINUTE,
391
+ "This Site's managers are calling too fast.",
392
+ ],
393
+ ]
394
+ : [
395
+ [caller.principal, limits.callsPerMinutePerViewer, "Too many database calls."],
396
+ ["*", limits.callsPerMinutePerSite, "This Site is receiving too many database calls."],
397
+ ];
398
+ await consumeSiteSqlRate(deps.db, resolved.scope, windows);
399
+ }
400
+
401
+ function checkMonthly(
402
+ database: SiteSqlDatabase,
403
+ limits: SiteSqlLimits,
404
+ needs: { read: boolean; write: boolean },
405
+ ) {
406
+ if (
407
+ needs.read &&
408
+ limits.rowsReadPerSitePerMonth !== null &&
409
+ database.rowsReadThisMonth >= limits.rowsReadPerSitePerMonth
410
+ )
411
+ throw new SiteSqlError("quota_exceeded", "This Site has read all the rows it may this month", {
412
+ quota: "rows_read_per_month",
413
+ limit: limits.rowsReadPerSitePerMonth,
414
+ });
415
+ if (
416
+ needs.write &&
417
+ limits.rowsWrittenPerSitePerMonth !== null &&
418
+ database.rowsWrittenThisMonth >= limits.rowsWrittenPerSitePerMonth
419
+ )
420
+ throw new SiteSqlError(
421
+ "quota_exceeded",
422
+ "This Site has written all the rows it may this month",
423
+ { quota: "rows_written_per_month", limit: limits.rowsWrittenPerSitePerMonth },
424
+ );
425
+ }
426
+
427
+ /** What the Durable Object calls of one operation did together. */
428
+ type Usage = SiteDbMeter & { calls: number };
429
+
430
+ function newUsage(): Usage {
431
+ return { rowsRead: 0, rowsWritten: 0, databaseBytes: 0, calls: 0 };
432
+ }
433
+
434
+ function addUsage(usage: Usage, call: SiteDbMeter): void {
435
+ usage.rowsRead += call.rowsRead;
436
+ usage.rowsWritten += call.rowsWritten;
437
+ usage.databaseBytes = call.databaseBytes;
438
+ usage.calls += 1;
439
+ }
440
+
441
+ /**
442
+ * Usage metering (docs/credit-boundaries-rollout.md): the workspace pays for
443
+ * viewer-triggered usage; events are recorded with the Site's usage row.
444
+ * Only Site page calls (`countTowardCaps`) add to the monthly counters the
445
+ * optional per-Site caps read; management (export, inspect, migrations,
446
+ * resets, imports and ad hoc statements) is recorded as events only. Every
447
+ * call's rows and the storage level also go to the workspace's platform
448
+ * usage in the same transaction (site_platform_usage; charged only in the
449
+ * credits platform billing mode). Metering runs after the Durable Object
450
+ * answered, so a failure here is logged and never turns a call that
451
+ * happened into an error.
452
+ */
453
+ async function meter(
454
+ deps: SiteSqlDeps,
455
+ resolved: Resolved,
456
+ usage: Usage,
457
+ options: { countTowardCaps: boolean },
458
+ ): Promise<void> {
459
+ if (usage.calls === 0) return;
460
+ const usageId = crypto.randomUUID();
461
+ const common = {
462
+ accountId: resolved.scope.accountId,
463
+ workspaceId: resolved.scope.workspaceId,
464
+ subjectId: resolved.access.subjectId,
465
+ sourceResourceType: "site",
466
+ sourceResourceId: resolved.scope.siteId,
467
+ };
468
+ const workspace = {
469
+ accountId: resolved.scope.accountId,
470
+ workspaceId: resolved.scope.workspaceId,
471
+ };
472
+ let platform: SitePlatformUsageRecord | null = null;
473
+ try {
474
+ await recordSiteSqlUsage(deps.db, resolved.scope, usage, {
475
+ countTowardCaps: options.countTowardCaps,
476
+ meter: async (tx: Database, change) => {
477
+ platform = await recordSitePlatformUsageInTransaction(tx, workspace, {
478
+ sqlRowsRead: usage.rowsRead,
479
+ sqlRowsWritten: usage.rowsWritten,
480
+ refreshStorage: true,
481
+ });
482
+ const events: Array<[string, number, string]> = [
483
+ ["site_sql.rows_read", usage.rowsRead, "row"],
484
+ ["site_sql.rows_written", usage.rowsWritten, "row"],
485
+ ["site_sql.storage_growth_bytes", change.storageGrowthBytes, "byte"],
486
+ ];
487
+ for (const [eventType, quantity, unit] of events)
488
+ if (quantity > 0)
489
+ await recordUsageEvent(tx, {
490
+ ...common,
491
+ eventType,
492
+ quantity,
493
+ unit,
494
+ idempotencyKey: `${eventType}:${usageId}`,
495
+ });
496
+ },
497
+ });
498
+ } catch (error) {
499
+ console.warn("[site-sql] could not meter usage", {
500
+ workspaceId: resolved.scope.workspaceId,
501
+ siteId: resolved.scope.siteId,
502
+ rowsRead: usage.rowsRead,
503
+ rowsWritten: usage.rowsWritten,
504
+ error: error instanceof Error ? error.message : String(error),
505
+ });
506
+ }
507
+ if (platform) await afterSitePlatformUsage(deps, workspace, platform);
508
+ }
509
+
510
+ /**
511
+ * Credits platform billing only: refuse a call whose platform meters this
512
+ * workspace can no longer pay for (402 site_platform_quota_exhausted).
513
+ */
514
+ async function requirePlatformQuota(
515
+ deps: SiteSqlDeps,
516
+ resolved: Resolved,
517
+ write: boolean,
518
+ ): Promise<void> {
519
+ await requireSitePlatformQuota(
520
+ deps,
521
+ { accountId: resolved.scope.accountId, workspaceId: resolved.scope.workspaceId },
522
+ write ? ["sql_rows_read", "sql_rows_written", "storage_bytes"] : ["sql_rows_read"],
523
+ );
524
+ }
525
+
526
+ /**
527
+ * Why a caller may not run a named query of this audience, or null. Managers
528
+ * (outside a Site frame) run every query; `managers` admits nobody else;
529
+ * `signed_in` needs a person (not a guest, and not a service key, agent or
530
+ * schedule acting on its own); `everyone` admits all.
531
+ */
532
+ export function siteSqlAudienceRefusal(
533
+ audience: SiteSqlQueryAudience,
534
+ caller: { manager: boolean; guest: boolean; person: boolean },
535
+ ): SiteSqlError | null {
536
+ if (caller.manager) return null;
537
+ if (audience === "managers")
538
+ return new SiteSqlError("forbidden", "Only the Site's managers can run this query");
539
+ if (audience === "signed_in" && (caller.guest || !caller.person))
540
+ return new SiteSqlError("forbidden", "Sign in to use this", { reason: "sign_in_required" });
541
+ return null;
542
+ }
543
+
544
+ // ---------------------------------------------------------------------------
545
+ // Responses
546
+ // ---------------------------------------------------------------------------
547
+
548
+ function publicQuery(query: SiteSqlStoredQuery): SiteSqlQueryResponse {
549
+ return {
550
+ name: query.name,
551
+ sql: query.sql,
552
+ mode: query.mode,
553
+ params: query.params,
554
+ audience: query.audience,
555
+ description: query.description,
556
+ maxRows: query.maxRows,
557
+ createdAt: query.createdAt.toISOString(),
558
+ updatedAt: query.updatedAt.toISOString(),
559
+ };
560
+ }
561
+
562
+ function publicMigration(migration: SiteSqlStoredMigration): SiteSqlMigrationResponse {
563
+ return {
564
+ ordinal: migration.ordinal,
565
+ name: migration.name,
566
+ sql: migration.sql,
567
+ checksum: migration.checksum,
568
+ createdAt: migration.createdAt.toISOString(),
569
+ };
570
+ }
571
+
572
+ export function siteSqlChecksum(sql: string): string {
573
+ return createHash("sha256").update(sql).digest("hex");
574
+ }
575
+
576
+ function compileOrThrow(sql: string, profile: "named" | "agent" | "script") {
577
+ const compiled = compileSiteSql(sql, profile);
578
+ if (!compiled.ok)
579
+ throw new SiteSqlError("validation", compiled.message, { problem: compiled.problem });
580
+ return compiled;
581
+ }
582
+
583
+ // ---------------------------------------------------------------------------
584
+ // Management
585
+ // ---------------------------------------------------------------------------
586
+
587
+ export async function describeSiteSql(
588
+ deps: SiteSqlDeps,
589
+ authority: SiteSqlAuthority,
590
+ siteId: string,
591
+ ): Promise<SiteSqlDescribeResponse> {
592
+ return guarded(async () => {
593
+ const resolved = await resolve(deps, authority, siteId, { management: true });
594
+ const [database, migrations, queries, limits] = await Promise.all([
595
+ getSiteSqlDatabase(deps.db, resolved.scope),
596
+ listSiteSqlMigrations(deps.db, resolved.scope),
597
+ listSiteSqlQueries(deps.db, resolved.scope),
598
+ limitsFor(deps, resolved.scope),
599
+ ]);
600
+ return {
601
+ siteId,
602
+ available: resolved.offered,
603
+ enabled: database?.enabled ?? false,
604
+ migrations: migrations.map(publicMigration),
605
+ queries: queries.map(publicQuery),
606
+ usage: {
607
+ storageBytes: database?.storageBytes ?? 0,
608
+ rowsReadThisMonth: database?.rowsReadThisMonth ?? 0,
609
+ rowsWrittenThisMonth: database?.rowsWrittenThisMonth ?? 0,
610
+ },
611
+ limits,
612
+ };
613
+ });
614
+ }
615
+
616
+ /** Turn Site SQL on or off for one Site. Turning it off keeps the data and definitions. */
617
+ export async function setSiteSqlSettings(
618
+ deps: SiteSqlDeps,
619
+ authority: SiteSqlAuthority,
620
+ siteId: string,
621
+ request: { enabled: boolean },
622
+ ): Promise<SiteSqlDescribeResponse> {
623
+ await guarded(async () => {
624
+ const resolved = await resolve(deps, authority, siteId, { management: true });
625
+ if (request.enabled) requireGateway(resolved);
626
+ await setSiteSqlEnabled(deps.db, resolved.scope, request.enabled, resolved.access.caller.actor);
627
+ });
628
+ return describeSiteSql(deps, authority, siteId);
629
+ }
630
+
631
+ /** Create, replace or delete (`request` null) a named query. */
632
+ export async function defineSiteSqlQuery(
633
+ deps: SiteSqlDeps,
634
+ authority: SiteSqlAuthority,
635
+ siteId: string,
636
+ name: string,
637
+ request: SiteSqlQueryDefinitionRequest | null,
638
+ ): Promise<{ query: SiteSqlQueryResponse | null; created: boolean; deleted: boolean }> {
639
+ return guarded(async () => {
640
+ const resolved = await resolve(deps, authority, siteId, { management: true });
641
+ if (!SITE_SQL_NAME.test(name))
642
+ throw new SiteSqlError(
643
+ "validation",
644
+ "A query name is a letter, then letters, digits, - or _",
645
+ );
646
+ await requireDatabase(deps, resolved, { enabled: false });
647
+ if (request === null) {
648
+ const deleted = await deleteSiteSqlQuery(deps.db, resolved.scope, name);
649
+ if (!deleted) throw new SiteSqlError("not_found", `No query named ${name}`);
650
+ return { query: null, created: false, deleted: true };
651
+ }
652
+ const compiled = compileOrThrow(request.sql, "named");
653
+ const statement = compiled.statements[0]!;
654
+ const stored = await upsertSiteSqlQuery(
655
+ deps.db,
656
+ resolved.scope,
657
+ {
658
+ name,
659
+ sql: request.sql,
660
+ mode: statement.kind === "write" ? "write" : "read",
661
+ params: compiled.paramNames,
662
+ audience: request.audience ?? "signed_in",
663
+ description: request.description ?? null,
664
+ maxRows: request.maxRows ?? null,
665
+ },
666
+ resolved.access.caller.actor,
667
+ );
668
+ return { query: publicQuery(stored.query), created: stored.created, deleted: false };
669
+ });
670
+ }
671
+
672
+ type MigrationEntry = { name: string; sql: string; checksum: string; position: number };
673
+
674
+ function migrationEntry(migration: SiteSqlStoredMigration): MigrationEntry {
675
+ return {
676
+ name: migration.name,
677
+ sql: migration.sql,
678
+ checksum: migration.checksum,
679
+ position: migration.ordinal,
680
+ };
681
+ }
682
+
683
+ /** Split migrations into calls of at most MIGRATE_CALL_BYTES of JSON each. */
684
+ function migrationBatches(entries: readonly MigrationEntry[]): MigrationEntry[][] {
685
+ const batches: MigrationEntry[][] = [];
686
+ let current: MigrationEntry[] = [];
687
+ let bytes = 0;
688
+ for (const entry of entries) {
689
+ const size = Buffer.byteLength(JSON.stringify(entry), "utf8");
690
+ if (size > MIGRATE_CALL_BYTES)
691
+ throw new SiteSqlError(
692
+ "validation",
693
+ `Migration ${entry.name} is too large to apply in one call; split it into smaller migrations`,
694
+ { migration: entry.name, limit: MIGRATE_CALL_BYTES },
695
+ );
696
+ if (current.length > 0 && bytes + size > MIGRATE_CALL_BYTES) {
697
+ batches.push(current);
698
+ current = [];
699
+ bytes = 0;
700
+ }
701
+ current.push(entry);
702
+ bytes += size;
703
+ }
704
+ if (current.length > 0) batches.push(current);
705
+ return batches;
706
+ }
707
+
708
+ /**
709
+ * Apply migrations in the Durable Object in bounded calls, in order.
710
+ * `rebuild` empties the database on the first call only (later calls add the
711
+ * rest), and still makes one call when there is nothing to apply.
712
+ */
713
+ async function applyMigrations(
714
+ gateway: SiteSqlGatewayConfig,
715
+ siteId: string,
716
+ entries: readonly MigrationEntry[],
717
+ options: { rebuild: boolean; maxDatabaseBytes: number },
718
+ usage: Usage,
719
+ ): Promise<string[]> {
720
+ const batches = migrationBatches(entries);
721
+ if (batches.length === 0 && options.rebuild) batches.push([]);
722
+ const applied: string[] = [];
723
+ for (const [index, batch] of batches.entries()) {
724
+ const response = await callSiteDb<SiteDbMigrateResponse>(
725
+ gateway,
726
+ siteId,
727
+ "migrate",
728
+ {
729
+ migrations: batch,
730
+ ...(options.rebuild && index === 0 ? { rebuild: true } : {}),
731
+ maxDatabaseBytes: options.maxDatabaseBytes,
732
+ },
733
+ siteSqlTransport.fetch,
734
+ );
735
+ addUsage(usage, response);
736
+ applied.push(...response.applied);
737
+ }
738
+ return applied;
739
+ }
740
+
741
+ /**
742
+ * Add a migration: checked against the Site's migration SQL cap, applied in
743
+ * the Durable Object together with any earlier migration it is missing (only
744
+ * those are sent, in bounded calls), then recorded in Postgres, all under the
745
+ * Site's database lock.
746
+ */
747
+ export async function migrateSiteSql(
748
+ deps: SiteSqlDeps,
749
+ authority: SiteSqlAuthority,
750
+ siteId: string,
751
+ request: { name: string; sql: string },
752
+ options: { profileKind?: SiteSqlProfileKind } = {},
753
+ ): Promise<SiteSqlMigrateResponse> {
754
+ return guarded(async () => {
755
+ const resolved = await resolve(deps, authority, siteId, { management: true });
756
+ const gateway = requireGateway(resolved);
757
+ await requireDatabase(deps, resolved, { enabled: true });
758
+ if (!SITE_SQL_NAME.test(request.name))
759
+ throw new SiteSqlError(
760
+ "validation",
761
+ "A migration name is a letter, then letters, digits, - or _",
762
+ );
763
+ const compiled = compileOrThrow(request.sql, "script");
764
+ if (compiled.paramNames.length > 0)
765
+ throw new SiteSqlError("validation", "Migrations cannot use :parameters");
766
+ await requireSitePlatformOpen(deps, "sql_write");
767
+ await requirePlatformQuota(deps, resolved, true);
768
+ const limits = await limitsFor(deps, resolved.scope, options.profileKind);
769
+ await consume(deps, resolved, limits);
770
+ const checksum = siteSqlChecksum(request.sql);
771
+ const actor = resolved.access.caller.actor;
772
+ const usage = newUsage();
773
+ try {
774
+ const { migration, applied } = await withSiteSqlDatabaseLock(
775
+ deps.db,
776
+ resolved.scope,
777
+ async (tx, locked) => {
778
+ const existing = await listSiteSqlMigrations(tx, resolved.scope);
779
+ if (existing.some((entry) => entry.name === request.name))
780
+ throw new SiteSqlError("conflict", `A migration named ${request.name} already exists`);
781
+ if (
782
+ (await siteSqlMigrationBytes(tx, resolved.scope)) +
783
+ Buffer.byteLength(request.sql, "utf8") >
784
+ SITE_SQL_HARD_LIMITS.migrationBytesPerSite
785
+ )
786
+ throw siteSqlMigrationQuotaError();
787
+ const inspected = await callSiteDb<SiteDbInspectResponse>(
788
+ gateway,
789
+ siteId,
790
+ "inspect",
791
+ { migrationsOnly: true },
792
+ siteSqlTransport.fetch,
793
+ );
794
+ addUsage(usage, inspected);
795
+ const inDatabase = new Map(
796
+ inspected.migrations.map((entry) => [entry.name, entry.checksum]),
797
+ );
798
+ const pending = existing
799
+ .filter((entry) => inDatabase.get(entry.name) !== entry.checksum)
800
+ .map(migrationEntry);
801
+ pending.push({
802
+ name: request.name,
803
+ sql: request.sql,
804
+ checksum,
805
+ position: locked.migrationCount + 1,
806
+ });
807
+ const appliedNow = await applyMigrations(
808
+ gateway,
809
+ siteId,
810
+ pending,
811
+ { rebuild: false, maxDatabaseBytes: limits.storageBytesPerSite },
812
+ usage,
813
+ );
814
+ const appended = await appendSiteSqlMigration(
815
+ tx,
816
+ resolved.scope,
817
+ { name: request.name, sql: request.sql, checksum },
818
+ actor,
819
+ );
820
+ return { migration: appended, applied: appliedNow };
821
+ },
822
+ );
823
+ return { migration: publicMigration(migration), applied };
824
+ } finally {
825
+ await meter(deps, resolved, usage, { countTowardCaps: false });
826
+ }
827
+ });
828
+ }
829
+
830
+ /**
831
+ * Reset the Site's database. `schema: "keep"` (default) empties it and applies
832
+ * every migration again in bounded calls (a rebuild from Postgres); `schema:
833
+ * "drop"` deletes the database and forgets every migration and named query,
834
+ * and works whenever the gateway is configured.
835
+ */
836
+ export async function resetSiteSql(
837
+ deps: SiteSqlDeps,
838
+ authority: SiteSqlAuthority,
839
+ siteId: string,
840
+ request: { schema?: "keep" | "drop" | undefined },
841
+ ): Promise<SiteSqlResetResponse> {
842
+ return guarded(async () => {
843
+ const resolved = await resolve(deps, authority, siteId, { management: true });
844
+ const drop = request.schema === "drop";
845
+ const gateway = requireGateway(resolved, drop ? "configured" : "offered");
846
+ await requireDatabase(deps, resolved, { enabled: false });
847
+ const limits = await limitsFor(deps, resolved.scope);
848
+ await consume(deps, resolved, limits);
849
+ const actor = resolved.access.caller.actor;
850
+ const usage = newUsage();
851
+ try {
852
+ const applied = await withSiteSqlDatabaseLock(deps.db, resolved.scope, async (tx) => {
853
+ if (drop) {
854
+ const reset = await callSiteDb<SiteDbMeter>(
855
+ gateway,
856
+ siteId,
857
+ "reset",
858
+ {},
859
+ siteSqlTransport.fetch,
860
+ );
861
+ addUsage(usage, reset);
862
+ await clearSiteSqlDefinitions(tx, resolved.scope, actor);
863
+ await markSiteSqlRebuilt(tx, resolved.scope, reset.databaseBytes);
864
+ return [] as string[];
865
+ }
866
+ const migrations = await listSiteSqlMigrations(tx, resolved.scope);
867
+ let rebuilt: string[];
868
+ try {
869
+ rebuilt = await applyMigrations(
870
+ gateway,
871
+ siteId,
872
+ migrations.map(migrationEntry),
873
+ { rebuild: true, maxDatabaseBytes: limits.storageBytesPerSite },
874
+ usage,
875
+ );
876
+ } catch (error) {
877
+ // The first call empties the database, so a failure on the way
878
+ // leaves it partly rebuilt: say so, and how to finish.
879
+ const failure = toSiteSqlError(error);
880
+ if (!(failure instanceof SiteSqlError)) throw error;
881
+ throw new SiteSqlError(
882
+ failure.code,
883
+ `${failure.message.replace(/\.?$/u, ".")} The database may be emptied and only partly rebuilt; run the reset again to finish rebuilding it from the migrations.`,
884
+ { ...failure.details, partialRebuild: true },
885
+ );
886
+ }
887
+ await markSiteSqlRebuilt(tx, resolved.scope, usage.databaseBytes);
888
+ return rebuilt;
889
+ });
890
+ return { applied, droppedSchema: drop };
891
+ } finally {
892
+ await meter(deps, resolved, usage, { countTowardCaps: false });
893
+ }
894
+ });
895
+ }
896
+
897
+ /**
898
+ * The Durable Object's tables, columns, row counts and applied migrations.
899
+ * Works whenever the gateway is configured.
900
+ */
901
+ export async function inspectSiteSql(
902
+ deps: SiteSqlDeps,
903
+ authority: SiteSqlAuthority,
904
+ siteId: string,
905
+ ): Promise<SiteSqlInspectResponse> {
906
+ return guarded(async () => {
907
+ const resolved = await resolve(deps, authority, siteId, { management: true });
908
+ const gateway = requireGateway(resolved, "configured");
909
+ await requireDatabase(deps, resolved, { enabled: false });
910
+ const limits = await limitsFor(deps, resolved.scope);
911
+ await consume(deps, resolved, limits);
912
+ const inspected = await callSiteDb<SiteDbInspectResponse>(
913
+ gateway,
914
+ siteId,
915
+ "inspect",
916
+ {},
917
+ siteSqlTransport.fetch,
918
+ );
919
+ const usage = newUsage();
920
+ addUsage(usage, inspected);
921
+ await meter(deps, resolved, usage, { countTowardCaps: false });
922
+ const applied = new Set(inspected.migrations.map((migration) => migration.name));
923
+ const migrations = await listSiteSqlMigrations(deps.db, resolved.scope);
924
+ const { rowsRead: _read, rowsWritten: _written, ...rest } = inspected;
925
+ return {
926
+ ...rest,
927
+ pendingMigrations: migrations
928
+ .filter((migration) => !applied.has(migration.name))
929
+ .map((migration) => migration.name),
930
+ };
931
+ });
932
+ }
933
+
934
+ // ---------------------------------------------------------------------------
935
+ // Running SQL
936
+ // ---------------------------------------------------------------------------
937
+
938
+ /** The values Opengeni binds for `:viewer`, `:viewer_kind` and `:viewer_signed_in`. */
939
+ export function siteSqlPrincipalParams(
940
+ access: Pick<SiteDataAccess, "caller" | "subjectId">,
941
+ guest: boolean,
942
+ ): Record<string, SiteSqlValue> {
943
+ const principal = access.caller.principal;
944
+ const kind = principal.includes(":") ? principal.slice(0, principal.indexOf(":")) : principal;
945
+ return {
946
+ viewer: principal,
947
+ viewer_kind: kind,
948
+ viewer_signed_in: !guest && access.subjectId !== null ? 1 : 0,
949
+ };
950
+ }
951
+
952
+ /** Check caller parameters against a statement's names and add the principal ones. */
953
+ function bindParams(
954
+ names: readonly string[],
955
+ given: Readonly<Record<string, SiteSqlValue>>,
956
+ principal: Record<string, SiteSqlValue>,
957
+ options: { requireAll: boolean },
958
+ ): Record<string, SiteSqlValue> {
959
+ const known = new Set(names);
960
+ for (const key of Object.keys(given)) {
961
+ if (isSiteSqlPrincipalParam(key))
962
+ throw new SiteSqlError(
963
+ "validation",
964
+ `:${key} is set by Opengeni from the signed-in viewer; do not send it`,
965
+ { param: key },
966
+ );
967
+ if (!known.has(key))
968
+ throw new SiteSqlError("validation", `This query has no parameter :${key}`, { param: key });
969
+ }
970
+ const bound: Record<string, SiteSqlValue> = {};
971
+ for (const name of names) {
972
+ if (isSiteSqlPrincipalParam(name)) {
973
+ if (!Object.hasOwn(principal, name))
974
+ throw new SiteSqlError("validation", `:${name} is reserved for Opengeni`, { param: name });
975
+ bound[name] = principal[name]!;
976
+ } else if (Object.hasOwn(given, name)) bound[name] = given[name]!;
977
+ else if (options.requireAll)
978
+ throw new SiteSqlError("validation", `Missing parameter :${name}`, { param: name });
979
+ else bound[name] = null;
980
+ }
981
+ return bound;
982
+ }
983
+
984
+ function runResponse(response: SiteDbRunResponse): SiteSqlRunResponse {
985
+ return {
986
+ columns: response.columns,
987
+ rows: response.rows,
988
+ truncated: response.truncated,
989
+ rowsRead: response.rowsRead,
990
+ rowsWritten: response.rowsWritten,
991
+ };
992
+ }
993
+
994
+ /**
995
+ * Run a named query: what Site pages call with `site.sql.run(name, params)`.
996
+ * Site frames and published viewers are viewers; the query's audience decides
997
+ * who may run it (siteSqlAudienceRefusal). These calls count toward the
998
+ * Site's monthly caps. `profileKind` skips reading the limit profile again
999
+ * when the caller has it; `beforeWrite` runs for write queries before the
1000
+ * call is counted (the published runtime applies its guest write limit).
1001
+ */
1002
+ export async function runSiteSqlQuery(
1003
+ deps: SiteSqlDeps,
1004
+ authority: SiteSqlAuthority,
1005
+ siteId: string,
1006
+ name: string,
1007
+ params: Readonly<Record<string, SiteSqlValue>>,
1008
+ surface: SiteDataSurface = {},
1009
+ options: {
1010
+ profileKind?: SiteSqlProfileKind;
1011
+ beforeWrite?: () => Promise<void>;
1012
+ } = {},
1013
+ ): Promise<SiteSqlRunResponse> {
1014
+ return guarded(async () => {
1015
+ const resolved = await resolve(deps, authority, siteId, { ...surface, management: false });
1016
+ const gateway = requireGateway(resolved);
1017
+ const database = await requireDatabase(deps, resolved, { enabled: true });
1018
+ const query = SITE_SQL_NAME.test(name)
1019
+ ? await getSiteSqlQuery(deps.db, resolved.scope, name)
1020
+ : null;
1021
+ if (!query) throw new SiteSqlError("not_found", `No query named ${name}`);
1022
+ const { caller } = resolved.access;
1023
+ const refusal = siteSqlAudienceRefusal(query.audience, {
1024
+ manager: caller.role === "manager" && !caller.framed,
1025
+ guest: resolved.guest,
1026
+ person: !resolved.guest && resolved.access.subjectId !== null,
1027
+ });
1028
+ if (refusal) throw refusal;
1029
+ if (query.mode === "write") {
1030
+ // The Opengeni-wide platform brake stops new writes (reads go on).
1031
+ await requireSitePlatformOpen(deps, "sql_write");
1032
+ if (options.beforeWrite) await options.beforeWrite();
1033
+ }
1034
+ await requirePlatformQuota(deps, resolved, query.mode === "write");
1035
+ const limits = await limitsFor(deps, resolved.scope, options.profileKind);
1036
+ await consume(deps, resolved, limits);
1037
+ checkMonthly(database, limits, { read: true, write: query.mode === "write" });
1038
+ const bound = bindParams(
1039
+ query.params,
1040
+ params,
1041
+ siteSqlPrincipalParams(resolved.access, resolved.guest),
1042
+ { requireAll: true },
1043
+ );
1044
+ const maxRows = Math.min(query.maxRows ?? limits.maxRowsPerCall, limits.maxRowsPerCall);
1045
+ const response = await callSiteDb<SiteDbRunResponse>(
1046
+ gateway,
1047
+ siteId,
1048
+ "run",
1049
+ {
1050
+ sql: query.sql,
1051
+ profile: "named",
1052
+ params: bound,
1053
+ expect: query.mode,
1054
+ maxRows,
1055
+ maxResponseBytes: limits.maxResponseBytes,
1056
+ maxDatabaseBytes: limits.storageBytesPerSite,
1057
+ },
1058
+ siteSqlTransport.fetch,
1059
+ );
1060
+ const usage = newUsage();
1061
+ addUsage(usage, response);
1062
+ await meter(deps, resolved, usage, { countTowardCaps: true });
1063
+ return runResponse(response);
1064
+ });
1065
+ }
1066
+
1067
+ /**
1068
+ * One statement from a manager's agent or the console: `read` refuses writes
1069
+ * (rolled back in the Durable Object), `write` allows them. Schema changes
1070
+ * go through migrations so the database can be rebuilt. Management calls
1071
+ * are metered but do not count toward (or stop at) the monthly caps of the
1072
+ * Site's pages; the managers' call rate and the storage cap bound them.
1073
+ *
1074
+ * `asSiteMember`: a published Site's agent in a workspace member's
1075
+ * conversation reads (never writes) the Site's database as that member,
1076
+ * framed to that one Site. The caller checks the viewer is a member; this
1077
+ * refuses guests and anyone without a person, and counts like page calls.
1078
+ */
1079
+ export async function runSiteSqlStatement(
1080
+ deps: SiteSqlDeps,
1081
+ authority: SiteSqlAuthority,
1082
+ siteId: string,
1083
+ request: {
1084
+ sql: string;
1085
+ params?: Record<string, SiteSqlValue> | undefined;
1086
+ maxRows?: number | undefined;
1087
+ },
1088
+ mode: "read" | "write",
1089
+ options: { asSiteMember?: boolean } = {},
1090
+ ): Promise<SiteSqlRunResponse> {
1091
+ return guarded(async () => {
1092
+ const asSiteMember = options.asSiteMember === true;
1093
+ if (asSiteMember && mode !== "read")
1094
+ throw new SiteSqlError("forbidden", "Only the Site's managers can change its data with SQL");
1095
+ const resolved = await resolve(
1096
+ deps,
1097
+ authority,
1098
+ siteId,
1099
+ asSiteMember ? { frameSiteId: siteId, management: false } : { management: true },
1100
+ );
1101
+ if (asSiteMember && (resolved.guest || resolved.access.subjectId === null))
1102
+ throw new SiteSqlError("forbidden", "Only workspace members can query this Site's database");
1103
+ const gateway = requireGateway(resolved);
1104
+ const database = await requireDatabase(deps, resolved, { enabled: true });
1105
+ const compiled = compileOrThrow(request.sql, "agent");
1106
+ const statement = compiled.statements[0]!;
1107
+ if (statement.kind === "schema")
1108
+ throw new SiteSqlError(
1109
+ "validation",
1110
+ "Change the schema with a migration (site_sql_migrate) so the database can be rebuilt",
1111
+ { problem: "statement_not_allowed" },
1112
+ );
1113
+ if (mode === "read" && statement.kind === "write")
1114
+ throw new SiteSqlError(
1115
+ "validation",
1116
+ asSiteMember
1117
+ ? "This statement writes; this Site's assistant can only read with SQL"
1118
+ : "This statement writes; use site_sql_execute",
1119
+ { problem: "statement_not_allowed" },
1120
+ );
1121
+ if (statement.kind === "write") await requireSitePlatformOpen(deps, "sql_write");
1122
+ await requirePlatformQuota(deps, resolved, statement.kind === "write");
1123
+ const limits = await limitsFor(deps, resolved.scope);
1124
+ await consume(deps, resolved, limits);
1125
+ // A member's reads through the Site's agent count like the page's calls.
1126
+ if (asSiteMember) checkMonthly(database, limits, { read: true, write: false });
1127
+ const bound = bindParams(
1128
+ compiled.paramNames,
1129
+ request.params ?? {},
1130
+ siteSqlPrincipalParams(resolved.access, false),
1131
+ { requireAll: true },
1132
+ );
1133
+ const response = await callSiteDb<SiteDbRunResponse>(
1134
+ gateway,
1135
+ siteId,
1136
+ "run",
1137
+ {
1138
+ sql: request.sql,
1139
+ profile: "agent",
1140
+ params: bound,
1141
+ expect: mode,
1142
+ maxRows: Math.min(request.maxRows ?? limits.maxRowsPerCall, limits.maxRowsPerCall),
1143
+ maxResponseBytes: limits.maxResponseBytes,
1144
+ maxDatabaseBytes: limits.storageBytesPerSite,
1145
+ },
1146
+ siteSqlTransport.fetch,
1147
+ );
1148
+ const usage = newUsage();
1149
+ addUsage(usage, response);
1150
+ await meter(deps, resolved, usage, { countTowardCaps: asSiteMember });
1151
+ return runResponse(response);
1152
+ });
1153
+ }
1154
+
1155
+ // ---------------------------------------------------------------------------
1156
+ // Export and import
1157
+ // ---------------------------------------------------------------------------
1158
+
1159
+ export type SiteSqlExport = {
1160
+ content: string;
1161
+ contentType: string;
1162
+ filename: string;
1163
+ truncated: boolean;
1164
+ /** Pass back as `cursor` for the next piece; null when this piece ends the export. */
1165
+ nextCursor: string | null;
1166
+ };
1167
+
1168
+ const TRIGGER = /^\s*CREATE\s+(?:TEMP(?:ORARY)?\s+)?TRIGGER\b/iu;
1169
+
1170
+ /**
1171
+ * Export the Site's data so it can always leave Cloudflare: `sql` (schema and
1172
+ * INSERT statements), `csv` (one table) or `json` (schema and rows per table).
1173
+ * SQL and JSON keep every value; CSV turns values into text. Exports work
1174
+ * whenever the gateway is configured, while Site SQL is off and past the
1175
+ * monthly row caps (they are metered as management). One response holds
1176
+ * about `maxBytes` (16 MiB at most; by default 1 MiB for SQL, so each piece
1177
+ * re-imports in one call, and 16 MiB otherwise); a larger export comes in
1178
+ * pieces: pass
1179
+ * `nextCursor` back as `cursor` and append the pieces in order. Only the
1180
+ * first piece has the header (SQL schema, CSV header row, JSON schema) and
1181
+ * only the last SQL piece has the triggers, so the pieces together re-import
1182
+ * as one dump.
1183
+ */
1184
+ export async function exportSiteSql(
1185
+ deps: SiteSqlDeps,
1186
+ authority: SiteSqlAuthority,
1187
+ siteId: string,
1188
+ request: {
1189
+ format: SiteSqlExportFormat;
1190
+ table?: string | undefined;
1191
+ cursor?: string | null | undefined;
1192
+ maxBytes?: number | undefined;
1193
+ },
1194
+ ): Promise<SiteSqlExport> {
1195
+ return guarded(async () => {
1196
+ const resolved = await resolve(deps, authority, siteId, { management: true });
1197
+ const gateway = requireGateway(resolved, "configured");
1198
+ await requireDatabase(deps, resolved, { enabled: false });
1199
+ if (request.format === "csv" && !request.table)
1200
+ throw new SiteSqlError("validation", "A CSV export needs a table");
1201
+ if (request.table !== undefined && !isSiteSqlTableName(request.table))
1202
+ throw new SiteSqlError("validation", `Invalid table name: ${request.table}`);
1203
+ const maxBytes = Math.max(
1204
+ 1,
1205
+ Math.min(
1206
+ request.maxBytes ??
1207
+ (request.format === "sql"
1208
+ ? SITE_SQL_EXPORT_SQL_DEFAULT_BYTES
1209
+ : SITE_SQL_EXPORT_MAX_BYTES),
1210
+ SITE_SQL_EXPORT_MAX_BYTES,
1211
+ ),
1212
+ );
1213
+ const resumed = typeof request.cursor === "string" && request.cursor !== "";
1214
+ const limits = await limitsFor(deps, resolved.scope);
1215
+ await consume(deps, resolved, limits);
1216
+
1217
+ const lines: string[] = [];
1218
+ let bytes = 0;
1219
+ const push = (line: string) => {
1220
+ lines.push(line);
1221
+ bytes += Buffer.byteLength(line, "utf8") + 1;
1222
+ };
1223
+ const json: {
1224
+ siteId: string;
1225
+ exportedAt: string;
1226
+ schema: string[];
1227
+ tables: Record<string, { columns: string[]; rows: Array<Record<string, unknown>> }>;
1228
+ } = { siteId, exportedAt: new Date().toISOString(), schema: [], tables: {} };
1229
+ let cursor: string | null = resumed ? (request.cursor as string) : null;
1230
+ let nextCursor: string | null = null;
1231
+ let first = true;
1232
+ let csvHeader = resumed;
1233
+ let schema: SiteDbSchemaEntry[] = [];
1234
+ const usage = newUsage();
1235
+ try {
1236
+ do {
1237
+ const page: SiteDbExportResponse = await callSiteDb<SiteDbExportResponse>(
1238
+ gateway,
1239
+ siteId,
1240
+ "export",
1241
+ {
1242
+ ...(request.table ? { tables: [request.table] } : {}),
1243
+ cursor,
1244
+ maxRows: EXPORT_PAGE_ROWS,
1245
+ maxBytes: Math.min(
1246
+ SITE_SQL_HARD_LIMITS.exportBytesPerCall,
1247
+ Math.max(1, maxBytes - bytes),
1248
+ ),
1249
+ },
1250
+ siteSqlTransport.fetch,
1251
+ );
1252
+ addUsage(usage, page);
1253
+ if (first) {
1254
+ first = false;
1255
+ if (request.table && !page.schema.some((entry) => entry.name === request.table))
1256
+ throw new SiteSqlError("not_found", `No table named ${request.table}`);
1257
+ schema = siteSqlSchemaEntries(page.schema, request.table);
1258
+ if (!resumed) {
1259
+ if (request.format === "sql") {
1260
+ push(`-- Opengeni Site SQL export of Site ${siteId}, ${json.exportedAt}`);
1261
+ push("-- Schema");
1262
+ for (const statement of siteSqlSchemaStatements(
1263
+ schema.filter((entry) => entry.type !== "trigger"),
1264
+ ))
1265
+ push(statement);
1266
+ push("-- Data");
1267
+ } else if (request.format === "json") json.schema = siteSqlSchemaStatements(schema);
1268
+ }
1269
+ }
1270
+ for (const table of page.tables) {
1271
+ if (request.format === "sql") {
1272
+ for (const statement of siteSqlInsertStatements(table)) push(statement);
1273
+ } else if (request.format === "csv") {
1274
+ if (!csvHeader) {
1275
+ push(siteSqlCsvLine(table.columns));
1276
+ csvHeader = true;
1277
+ }
1278
+ for (const row of table.rows) push(siteSqlCsvLine(row));
1279
+ } else {
1280
+ const entry = (json.tables[table.name] ??= { columns: table.columns, rows: [] });
1281
+ for (const row of siteSqlRowObjects(table)) {
1282
+ bytes += Buffer.byteLength(JSON.stringify(row), "utf8") + 1;
1283
+ entry.rows.push(row);
1284
+ }
1285
+ }
1286
+ }
1287
+ cursor = page.nextCursor;
1288
+ if (cursor && bytes >= maxBytes) {
1289
+ nextCursor = cursor;
1290
+ break;
1291
+ }
1292
+ } while (cursor);
1293
+ if (request.format === "sql" && nextCursor === null) {
1294
+ const triggers = siteSqlSchemaStatements(
1295
+ schema.filter((entry) => entry.type === "trigger"),
1296
+ );
1297
+ if (triggers.length > 0) {
1298
+ push("-- Triggers (after the data, so they do not fire on imported rows)");
1299
+ for (const statement of triggers) push(statement);
1300
+ }
1301
+ }
1302
+ if (request.format === "csv" && !csvHeader) {
1303
+ // An empty table still exports its header.
1304
+ const inspected = await callSiteDb<SiteDbInspectResponse>(
1305
+ gateway,
1306
+ siteId,
1307
+ "inspect",
1308
+ {},
1309
+ siteSqlTransport.fetch,
1310
+ );
1311
+ addUsage(usage, inspected);
1312
+ const table = inspected.tables.find((entry) => entry.name === request.table);
1313
+ if (table) push(siteSqlCsvLine(table.columns.map((column) => column.name)));
1314
+ }
1315
+ } finally {
1316
+ await meter(deps, resolved, usage, { countTowardCaps: false });
1317
+ }
1318
+ const truncated = nextCursor !== null;
1319
+ const base = request.table ?? `site-${siteId}`;
1320
+ if (request.format === "json")
1321
+ return {
1322
+ content: JSON.stringify({ ...json, truncated, nextCursor }),
1323
+ contentType: "application/json",
1324
+ filename: `${base}.json`,
1325
+ truncated,
1326
+ nextCursor,
1327
+ };
1328
+ return {
1329
+ content: lines.length > 0 ? `${lines.join("\n")}\n` : "",
1330
+ contentType: request.format === "csv" ? "text/csv; charset=utf-8" : "application/sql",
1331
+ filename: `${base}.${request.format}`,
1332
+ truncated,
1333
+ nextCursor,
1334
+ };
1335
+ });
1336
+ }
1337
+
1338
+ function importMigrationName(): string {
1339
+ const stamp = new Date().toISOString().replace(/[-:T]/gu, "").slice(0, 14);
1340
+ return `import_${stamp}_${crypto.randomUUID().slice(0, 4)}`;
1341
+ }
1342
+
1343
+ function chunkRows(rows: SiteSqlValue[][]): SiteSqlValue[][][] {
1344
+ const chunks: SiteSqlValue[][][] = [];
1345
+ let current: SiteSqlValue[][] = [];
1346
+ let bytes = 0;
1347
+ for (const row of rows) {
1348
+ const size = JSON.stringify(row).length;
1349
+ if (current.length > 0 && bytes + size > IMPORT_CHUNK_BYTES) {
1350
+ chunks.push(current);
1351
+ current = [];
1352
+ bytes = 0;
1353
+ }
1354
+ current.push(row);
1355
+ bytes += size;
1356
+ }
1357
+ if (current.length > 0) chunks.push(current);
1358
+ return chunks;
1359
+ }
1360
+
1361
+ /** Split statements into script calls of bounded size and count. */
1362
+ function chunkStatements(statements: readonly string[]): string[][] {
1363
+ const chunks: string[][] = [];
1364
+ let current: string[] = [];
1365
+ let chars = 0;
1366
+ for (const statement of statements) {
1367
+ const size = statement.length + 2;
1368
+ if (
1369
+ current.length > 0 &&
1370
+ (current.length >= IMPORT_SCRIPT_STATEMENTS || chars + size > IMPORT_SCRIPT_CHARS)
1371
+ ) {
1372
+ chunks.push(current);
1373
+ current = [];
1374
+ chars = 0;
1375
+ }
1376
+ current.push(statement);
1377
+ chars += size;
1378
+ }
1379
+ if (current.length > 0) chunks.push(current);
1380
+ return chunks;
1381
+ }
1382
+
1383
+ /**
1384
+ * Import data. `sql`: schema statements (tables, views, indexes) become a
1385
+ * migration recorded in Postgres (so a rebuild keeps them), the data
1386
+ * statements run in batches, then triggers become a second migration (so
1387
+ * they do not fire on imported rows); `csv` and `json` insert rows into an
1388
+ * existing table (a CSV header names the columns; an empty cell is NULL).
1389
+ * Batches that ran stay when a later one fails; the error says how much
1390
+ * landed and which migration was recorded. A dump larger than one call
1391
+ * imports piece by piece, in order (the first piece holds the schema, the
1392
+ * last the triggers). Imports are metered as management.
1393
+ */
1394
+ export async function importSiteSql(
1395
+ deps: SiteSqlDeps,
1396
+ authority: SiteSqlAuthority,
1397
+ siteId: string,
1398
+ request: SiteSqlImportRequest,
1399
+ ): Promise<SiteSqlImportResponse> {
1400
+ const resolved = await guarded(() => resolve(deps, authority, siteId, { management: true }));
1401
+ // An import writes: refused up front while the platform brake is engaged.
1402
+ await requireSitePlatformOpen(deps, "sql_write");
1403
+ if (request.format === "sql") {
1404
+ const compiled = await guarded(async () => {
1405
+ requireGateway(resolved);
1406
+ const result = compileOrThrow(request.sql ?? "", "script");
1407
+ if (result.paramNames.length > 0)
1408
+ throw new SiteSqlError("validation", "SQL imports cannot use :parameters");
1409
+ return result;
1410
+ });
1411
+ const schema = compiled.statements.filter(
1412
+ (statement) => statement.kind === "schema" && !TRIGGER.test(statement.sql),
1413
+ );
1414
+ const triggers = compiled.statements.filter(
1415
+ (statement) => statement.kind === "schema" && TRIGGER.test(statement.sql),
1416
+ );
1417
+ const data = compiled.statements.filter((statement) => statement.kind !== "schema");
1418
+ const base = importMigrationName();
1419
+ const migrations: string[] = [];
1420
+ let migration: string | null = null;
1421
+ if (schema.length > 0) {
1422
+ migration = base;
1423
+ await migrateSiteSql(deps, authority, siteId, {
1424
+ name: migration,
1425
+ sql: `${schema.map((statement) => statement.sql).join(";\n")};`,
1426
+ });
1427
+ migrations.push(migration);
1428
+ }
1429
+ let imported = 0;
1430
+ let rowsWritten = 0;
1431
+ const partial = (error: unknown) => {
1432
+ const typed = toSiteSqlError(error);
1433
+ if (!(typed instanceof SiteSqlError) || (imported === 0 && migrations.length === 0))
1434
+ return typed;
1435
+ const recorded =
1436
+ migrations.length > 0
1437
+ ? ` Migration ${migrations.join(" and ")} stays recorded and applied; do not import the schema again.`
1438
+ : "";
1439
+ return new SiteSqlError(
1440
+ typed.code,
1441
+ `${typed.message} (${imported} data statements were imported before this.${recorded})`,
1442
+ { ...typed.details, imported, migration, migrations: [...migrations] },
1443
+ );
1444
+ };
1445
+ if (data.length > 0) {
1446
+ const usage = newUsage();
1447
+ try {
1448
+ const gateway = requireGateway(resolved);
1449
+ await requireDatabase(deps, resolved, { enabled: true });
1450
+ const limits = await limitsFor(deps, resolved.scope);
1451
+ await consume(deps, resolved, limits);
1452
+ for (const chunk of chunkStatements(data.map((statement) => statement.sql))) {
1453
+ const response = await callSiteDb<SiteDbMeter>(
1454
+ gateway,
1455
+ siteId,
1456
+ "script",
1457
+ { sql: `${chunk.join(";\n")};`, maxDatabaseBytes: limits.storageBytesPerSite },
1458
+ siteSqlTransport.fetch,
1459
+ );
1460
+ addUsage(usage, response);
1461
+ imported += chunk.length;
1462
+ rowsWritten += response.rowsWritten;
1463
+ }
1464
+ } catch (error) {
1465
+ throw partial(error);
1466
+ } finally {
1467
+ await meter(deps, resolved, usage, { countTowardCaps: false });
1468
+ }
1469
+ }
1470
+ if (triggers.length > 0) {
1471
+ const name = `${base}_triggers`;
1472
+ try {
1473
+ await migrateSiteSql(deps, authority, siteId, {
1474
+ name,
1475
+ sql: `${triggers.map((statement) => statement.sql).join(";\n")};`,
1476
+ });
1477
+ } catch (error) {
1478
+ throw partial(error);
1479
+ }
1480
+ migrations.push(name);
1481
+ }
1482
+ return { imported, migration, migrations, rowsWritten };
1483
+ }
1484
+ return guarded(async () => {
1485
+ const gateway = requireGateway(resolved);
1486
+ await requireDatabase(deps, resolved, { enabled: true });
1487
+ const table = request.table ?? "";
1488
+ if (!isSiteSqlTableName(table))
1489
+ throw new SiteSqlError("validation", `Invalid table name: ${table}`);
1490
+ let columns: string[];
1491
+ let rows: SiteSqlValue[][];
1492
+ if (request.format === "csv") {
1493
+ let parsed: string[][];
1494
+ try {
1495
+ parsed = parseSiteDataCsv(request.csv ?? "", {
1496
+ maxColumns: IMPORT_MAX_COLUMNS,
1497
+ maxRecords: SITE_SQL_HARD_LIMITS.importRowsPerCall,
1498
+ });
1499
+ } catch (error) {
1500
+ if (error instanceof SiteDataError) throw new SiteSqlError("validation", error.message);
1501
+ throw error;
1502
+ }
1503
+ const [header, ...body] = parsed;
1504
+ if (!header || header.length === 0)
1505
+ throw new SiteSqlError("validation", "The CSV needs a header row of column names");
1506
+ columns = header.map((name) => name.trim());
1507
+ rows = body.map((cells) => columns.map((_, index) => cells[index] || null));
1508
+ } else {
1509
+ const shaped = siteSqlImportRows(request.rows ?? []);
1510
+ columns = shaped.columns;
1511
+ rows = shaped.values;
1512
+ }
1513
+ if (columns.length === 0) throw new SiteSqlError("validation", "Give at least one column");
1514
+ for (const column of columns)
1515
+ if (!SITE_SQL_IDENTIFIER.test(column))
1516
+ throw new SiteSqlError("validation", `Invalid column name: ${column}`);
1517
+ if (new Set(columns).size !== columns.length)
1518
+ throw new SiteSqlError("validation", "A column is named twice");
1519
+ if (rows.length === 0) return { imported: 0, migration: null, migrations: [], rowsWritten: 0 };
1520
+ const limits = await limitsFor(deps, resolved.scope);
1521
+ await consume(deps, resolved, limits);
1522
+ let imported = 0;
1523
+ let rowsWritten = 0;
1524
+ const usage = newUsage();
1525
+ try {
1526
+ for (const chunk of chunkRows(rows)) {
1527
+ let response: SiteDbMeter;
1528
+ try {
1529
+ response = await callSiteDb<SiteDbMeter>(
1530
+ gateway,
1531
+ siteId,
1532
+ "insert",
1533
+ {
1534
+ table,
1535
+ columns,
1536
+ rows: chunk,
1537
+ onConflict: request.onConflict ?? "fail",
1538
+ maxDatabaseBytes: limits.storageBytesPerSite,
1539
+ },
1540
+ siteSqlTransport.fetch,
1541
+ );
1542
+ } catch (error) {
1543
+ const typed = toSiteSqlError(error);
1544
+ if (typed instanceof SiteSqlError && imported > 0)
1545
+ throw new SiteSqlError(typed.code, typed.message, { ...typed.details, imported });
1546
+ throw typed;
1547
+ }
1548
+ addUsage(usage, response);
1549
+ imported += chunk.length;
1550
+ rowsWritten += response.rowsWritten;
1551
+ }
1552
+ } finally {
1553
+ await meter(deps, resolved, usage, { countTowardCaps: false });
1554
+ }
1555
+ return { imported, migration: null, migrations: [], rowsWritten };
1556
+ });
1557
+ }
1558
+
1559
+ /**
1560
+ * The Sites of a workspace that have a Site SQL database, read before the
1561
+ * workspace is deleted (its Postgres rows cascade with it; the Durable
1562
+ * Objects do not). Empty when no gateway is configured or the read fails.
1563
+ */
1564
+ export async function siteSqlSitesOfWorkspace(
1565
+ deps: Pick<ApiRouteDeps, "db"> & { settings?: SiteSqlDeps["settings"] | undefined },
1566
+ scope: { accountId: string; workspaceId: string },
1567
+ ): Promise<string[]> {
1568
+ if (!deps.settings || !siteSqlGatewayConfig(deps.settings)) return [];
1569
+ try {
1570
+ return await listSiteSqlSiteIds(deps.db, scope);
1571
+ } catch (error) {
1572
+ console.warn("[site-sql] could not list a workspace's Site databases before deleting it", {
1573
+ workspaceId: scope.workspaceId,
1574
+ error: error instanceof Error ? error.message : String(error),
1575
+ });
1576
+ return [];
1577
+ }
1578
+ }
1579
+
1580
+ /** Workspace deletion drops this many Site databases at a time. */
1581
+ const DROP_CONCURRENCY = 4;
1582
+ /** Each drop call gets at most this long. */
1583
+ const DROP_CALL_TIMEOUT_MS = 5_000;
1584
+ /** No drop starts after this long, so a large workspace cannot hold the request. */
1585
+ const DROP_DEADLINE_MS = 20_000;
1586
+
1587
+ export type SiteSqlDropResult = { dropped: string[]; failed: string[]; skipped: string[] };
1588
+
1589
+ /**
1590
+ * Drop the data of Site databases whose workspace was deleted, best effort:
1591
+ * a few at a time, each call bounded, none started after a deadline. Ids
1592
+ * that failed or were not tried are logged in one structured line (the data
1593
+ * stays in its Durable Object until a later sweep; see docs/site-data.md).
1594
+ */
1595
+ export async function dropSiteSqlDatabases(
1596
+ deps: { settings?: SiteSqlDeps["settings"] | undefined },
1597
+ siteIds: readonly string[],
1598
+ context: { workspaceId?: string } = {},
1599
+ ): Promise<SiteSqlDropResult> {
1600
+ const result: SiteSqlDropResult = { dropped: [], failed: [], skipped: [] };
1601
+ const configured = deps.settings ? siteSqlGatewayConfig(deps.settings) : null;
1602
+ if (!configured || siteIds.length === 0) return result;
1603
+ const gateway = {
1604
+ ...configured,
1605
+ timeoutMs: Math.min(configured.timeoutMs, DROP_CALL_TIMEOUT_MS),
1606
+ };
1607
+ const deadline = Date.now() + DROP_DEADLINE_MS;
1608
+ const errors: Record<string, string> = {};
1609
+ let next = 0;
1610
+ const worker = async () => {
1611
+ while (next < siteIds.length) {
1612
+ const siteId = siteIds[next]!;
1613
+ next += 1;
1614
+ if (Date.now() >= deadline) {
1615
+ result.skipped.push(siteId);
1616
+ continue;
1617
+ }
1618
+ try {
1619
+ await callSiteDb<SiteDbMeter>(gateway, siteId, "reset", {}, siteSqlTransport.fetch);
1620
+ result.dropped.push(siteId);
1621
+ } catch (error) {
1622
+ result.failed.push(siteId);
1623
+ errors[siteId] = (error instanceof Error ? error.message : String(error)).slice(0, 200);
1624
+ }
1625
+ }
1626
+ };
1627
+ await Promise.all(Array.from({ length: Math.min(DROP_CONCURRENCY, siteIds.length) }, worker));
1628
+ if (result.failed.length > 0 || result.skipped.length > 0)
1629
+ console.warn("[site-sql] Site databases left in Durable Objects after workspace deletion", {
1630
+ workspaceId: context.workspaceId ?? null,
1631
+ failedSiteIds: result.failed,
1632
+ skippedSiteIds: result.skipped,
1633
+ errors,
1634
+ });
1635
+ return result;
1636
+ }