@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,1944 @@
1
+ import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
2
+ import {
3
+ DeleteSessionQueueItemRequest,
4
+ EditSessionQueueItemRequest,
5
+ matchSiteOperation,
6
+ MoveSessionQueueItemRequest,
7
+ SaveComposerDraftRequest,
8
+ SessionControlRequest,
9
+ SiteAbuseReportRequest,
10
+ SiteAuthExchangeRequest,
11
+ SiteDataFileScope,
12
+ SiteDataGetRequest,
13
+ SiteDataQueryRequest,
14
+ SiteDataWriteRequest,
15
+ SiteSqlRunRequest,
16
+ SiteViewAs,
17
+ type SiteLimitProfileKind,
18
+ SITE_GATEWAY_CALLBACK_PATH,
19
+ SITE_RUNTIME_API_PREFIX,
20
+ SITE_TOOL_METHODS,
21
+ SteerSessionQueueItemRequest,
22
+ SubmitComposerDraftRequest,
23
+ ToolGatewayCallRequest,
24
+ ToolGatewayResolveRequest,
25
+ ToolGatewayInvokeRequest,
26
+ type Permission,
27
+ type SiteAuthExchangeResponse,
28
+ type SiteEmbeddedAgent,
29
+ type SiteRuntimeConfig,
30
+ type ToolRef,
31
+ } from "@opengeni/contracts";
32
+ import {
33
+ withHostSessionUsageLimit,
34
+ withSiteSessionOrigin,
35
+ admissibleWorkspaceModel,
36
+ loadWorkspaceModelSelectionInput,
37
+ resolveWorkspaceModelSelection,
38
+ hasPermission,
39
+ type ApiRouteDeps,
40
+ } from "@opengeni/core";
41
+ import {
42
+ consumeSiteAuthCode,
43
+ finishSiteRuntimeAdmission,
44
+ withSiteRuntimeAdmission,
45
+ createSiteAbuseReport,
46
+ getSiteWorkspaceMemberPermissions,
47
+ readSitePlatformUsage,
48
+ recordSitePlatformRequests,
49
+ setSiteViewerSessionViewAs,
50
+ createSiteViewerSession,
51
+ getOwnedSiteRuntimeConversation,
52
+ getServableSiteVersion,
53
+ getPublishedSiteVersion,
54
+ getSession,
55
+ isSiteWorkspaceMember,
56
+ listOwnedSiteRuntimeConversations,
57
+ listSessionEvents,
58
+ releaseSiteRuntimeConversationStart,
59
+ reserveSiteRuntimeConversationStart,
60
+ reserveSiteRuntimeConversationWork,
61
+ revokeSiteViewerSession,
62
+ SiteDataError,
63
+ SITE_RUNTIME_SITE_PRINCIPAL,
64
+ type SiteRuntimeReservation,
65
+ type SiteRuntimeConversationRecord,
66
+ } from "@opengeni/db";
67
+ import {
68
+ SITE_AGENT_DATA_TOOL_PERMISSIONS,
69
+ siteAgentDataToolsForStart,
70
+ siteAgentInstructions,
71
+ sitePublishedDataViewer,
72
+ } from "../site-publishing/agent-data-tools";
73
+ import {
74
+ normalizeSiteAgentSession,
75
+ SITE_AGENT_DEFAULTS_VERSION,
76
+ } from "../site-publishing/agent-session";
77
+ import {
78
+ isSubscriptionBilledModel,
79
+ resolveSiteOutsideModel,
80
+ siteCreditModelIds,
81
+ } from "../site-publishing/agent-model";
82
+ import {
83
+ afterSitePlatformUsage,
84
+ requireSitePlatformOpen,
85
+ sitePlatformBrakeEngaged,
86
+ } from "../site-publishing/platform-usage";
87
+ import { SiteSqlError } from "../site-sql/service";
88
+ import {
89
+ authorizePublishedSiteTools,
90
+ publishedSiteToolAuthority,
91
+ } from "../site-publishing/page-tool-authority";
92
+ import {
93
+ callWorkspaceToolGateway,
94
+ prepareWorkspaceToolGateway,
95
+ requireWorkspaceSiteToolAuthorization,
96
+ workspaceToolGatewayDeclarations,
97
+ } from "../workspace-tool-gateway";
98
+ import { invokeWorkspaceToolTarget, resolveWorkspaceToolTarget } from "../workspace-tool-target";
99
+ import { withSiteSecretContext } from "../workspace-tool-gateway-secrets";
100
+ import type { Context, Hono } from "hono";
101
+ import { z } from "zod";
102
+ import { markLocalInternalDispatch } from "../http/local-browser-boundary";
103
+ import { USER_CONTENT_SECURITY_HEADERS } from "../http/user-content";
104
+ import {
105
+ getSiteDataFileUrl,
106
+ getSiteDataRecord,
107
+ listSiteDataFiles,
108
+ querySiteData,
109
+ uploadSiteDataFile,
110
+ writeSiteData,
111
+ deleteSiteDataFile,
112
+ } from "../site-data/operations";
113
+ import type { PublishedSiteDataViewer } from "../site-data/service";
114
+ import {
115
+ sameSha256Hex,
116
+ verifySiteGatewayRequest,
117
+ type SiteGatewayFields,
118
+ } from "../site-publishing/gateway-signature";
119
+ import {
120
+ capSiteConversationUsageLimit,
121
+ liftSiteConversationUsageLimit,
122
+ enforceSiteRuntimeRate,
123
+ enforceSiteRuntimeViewerRate,
124
+ findSiteRuntimeHost,
125
+ meterSiteRuntime,
126
+ requireServedSiteRuntimeHost,
127
+ requireSiteAgent,
128
+ resolveSiteRuntimeViewer,
129
+ settleSiteRuntimeSpend,
130
+ sha256Text,
131
+ siteReservationRefused,
132
+ siteClientAddressPrincipal,
133
+ siteNotAvailableForViewer,
134
+ siteRuntimeBudgetKey,
135
+ siteRuntimeBudgetLimits,
136
+ siteViewerCapabilities,
137
+ siteRuntimeForwardedResponse,
138
+ siteRuntimeLimitProfile,
139
+ siteRuntimeSessionRequest,
140
+ siteWideBudget,
141
+ sweepSiteRuntimeReservations,
142
+ type SiteRuntimeHost,
143
+ } from "../site-publishing/runtime";
144
+ import {
145
+ configuredSitesDomain,
146
+ healSiteEdgeIfStale,
147
+ notifySiteAbuse,
148
+ setSitePublishingDispatch,
149
+ siteHostMapRecord,
150
+ sitePublishingError,
151
+ } from "../site-publishing/service";
152
+ import {
153
+ siteSessionEventPage,
154
+ siteSessionEventPageResponse,
155
+ siteSessionEventStreamResponse,
156
+ SITE_VISIBLE_SESSION_EVENT_TYPES,
157
+ } from "../site-publishing/session-events";
158
+ import {
159
+ siteAcceptedEventView,
160
+ siteAcceptedMessageView,
161
+ siteControlResultView,
162
+ siteControlView,
163
+ siteDraftView,
164
+ siteHumanInputListView,
165
+ siteHumanInputRequestView,
166
+ siteQueueMutationView,
167
+ siteQueueView,
168
+ } from "../site-publishing/session-views";
169
+ import {
170
+ effectiveSiteAccess,
171
+ publicSiteViewer,
172
+ signedInSiteViewerKind,
173
+ type SiteRuntimeViewer,
174
+ } from "../site-publishing/viewer";
175
+ import { runSiteSqlQuery } from "../site-sql/service";
176
+ import { siteDataHttpError } from "./site-data";
177
+ import { runSiteSqlRoute } from "./site-sql";
178
+
179
+ /**
180
+ * `/v1/site-runtime/*`: the API a Site gateway calls for one published Site
181
+ * host. Nothing else may call it: every request must carry a fresh gateway
182
+ * signature over its method, host, path, body and viewer headers, and must
183
+ * not carry Opengeni credentials. The signature is checked before anything
184
+ * else; then the host names exactly one Site, the viewer is resolved for it,
185
+ * and only the operations in `SITE_OPERATIONS` (plus the gateway's own host,
186
+ * version, sign-in and report calls) are served.
187
+ */
188
+
189
+ /** Sign-in exchanges one client address may attempt per minute (per Site). */
190
+ const EXCHANGES_PER_MINUTE = 30;
191
+ /** Abuse reports one Site accepts per minute from everyone together. */
192
+ const SITE_REPORTS_PER_MINUTE = 60;
193
+ /** Conversations a viewer's list shows. */
194
+ const LIST_LIMIT = 20;
195
+ const MESSAGE_MAX_CHARS = 20_000;
196
+ /** Request bodies site-runtime reads at most (the gateway enforces the same caps). */
197
+ export const SITE_RUNTIME_JSON_BODY_LIMIT = 4 * 1024 * 1024;
198
+ export const SITE_RUNTIME_FILE_BODY_LIMIT = 25 * 1024 * 1024;
199
+ const FILE_UPLOAD_SUFFIX = "/published-artifacts/current/files";
200
+ /** SHA-256 of the empty body: what the gateway signs for GET and HEAD. */
201
+ const EMPTY_BODY_SHA256 = createHash("sha256").digest("hex");
202
+ /** A version a publish or rollback replaced is still served this long (open pages keep loading). */
203
+ export const SITE_VERSION_GRACE_SECONDS = 600;
204
+
205
+ /** The document policy for a published version: scripts in an opaque origin, framed only by its own shell. */
206
+ export const SITE_VERSION_CONTENT_SECURITY_POLICY =
207
+ "sandbox allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads allow-modals; frame-ancestors 'self'";
208
+
209
+ const VersionId = z.string().uuid();
210
+ const ClientEventId = z.string().min(1).max(200).optional();
211
+ const CreateConversationRequest = z
212
+ .object({
213
+ initialMessage: z.string().trim().min(1).max(MESSAGE_MAX_CHARS),
214
+ idempotencyKey: z.string().min(1).max(200).optional(),
215
+ })
216
+ .passthrough();
217
+ const SendMessageRequest = z
218
+ .object({
219
+ type: z.literal("user.message"),
220
+ clientEventId: ClientEventId,
221
+ payload: z.object({ text: z.string().trim().min(1).max(MESSAGE_MAX_CHARS) }).passthrough(),
222
+ })
223
+ .passthrough();
224
+ const SteerRequest = z
225
+ .object({
226
+ text: z.string().trim().min(1).max(MESSAGE_MAX_CHARS),
227
+ clientEventId: ClientEventId,
228
+ })
229
+ .passthrough();
230
+ const SessionMcpServerIdPattern = /^[A-Za-z0-9_-]{1,200}$/u;
231
+
232
+ function invalid(message: string) {
233
+ return sitePublishingError(400, "validation_failed", "site_invalid_request", message);
234
+ }
235
+
236
+ function unsupported() {
237
+ return sitePublishingError(
238
+ 404,
239
+ "not_found",
240
+ "site_operation_unsupported",
241
+ "This operation is not available to published Sites",
242
+ );
243
+ }
244
+
245
+ function parseJson<S extends z.ZodType>(bytes: Uint8Array, schema: S): z.infer<S> {
246
+ let value: unknown = null;
247
+ try {
248
+ value = bytes.byteLength === 0 ? null : JSON.parse(new TextDecoder().decode(bytes));
249
+ } catch {
250
+ throw invalid("The request body must be JSON");
251
+ }
252
+ const parsed = schema.safeParse(value);
253
+ if (!parsed.success)
254
+ throw invalid(
255
+ parsed.error.issues
256
+ .slice(0, 3)
257
+ .map((issue) => `${issue.path.join(".") || "request"}: ${issue.message}`)
258
+ .join("; "),
259
+ );
260
+ return parsed.data;
261
+ }
262
+
263
+ /**
264
+ * Site Data for a viewer: what the viewer's audience may not use answers
265
+ * `not_available_for_viewer`; other refusals (a record the viewer does not
266
+ * own, a manager-only write) keep their Site Data code.
267
+ */
268
+ async function runSiteData<T>(fn: () => Promise<T>): Promise<T> {
269
+ try {
270
+ return await fn();
271
+ } catch (error) {
272
+ if (
273
+ error instanceof SiteDataError &&
274
+ error.code === "forbidden" &&
275
+ error.details.reason === "guest_access"
276
+ )
277
+ throw siteNotAvailableForViewer("site_data", error.message, error.details);
278
+ if (error instanceof SiteDataError) throw siteDataHttpError(error);
279
+ throw error;
280
+ }
281
+ }
282
+
283
+ /** Site SQL for a viewer: refusals for this viewer answer `not_available_for_viewer`. */
284
+ async function runSiteSqlForViewer<T>(fn: () => Promise<T>): Promise<T> {
285
+ try {
286
+ return await fn();
287
+ } catch (error) {
288
+ if (error instanceof SiteSqlError && error.code === "forbidden")
289
+ throw siteNotAvailableForViewer("site_sql", error.message, error.details);
290
+ throw error;
291
+ }
292
+ }
293
+
294
+ /** Hosts of one gateway platform report recorded at a time. */
295
+ const SITE_PLATFORM_REPORT_CONCURRENCY = 8;
296
+
297
+ /** POST /v1/site-runtime/platform/requests (gateway only, signed on the Sites apex). */
298
+ const SitePlatformRequestReport = z.object({
299
+ v: z.literal(1),
300
+ reportId: z.string().regex(/^[A-Za-z0-9_-]{8,64}$/u),
301
+ hosts: z
302
+ .array(
303
+ z.object({
304
+ host: z.string().min(1).max(253),
305
+ requests: z.number().int().positive().max(1_000_000_000),
306
+ }),
307
+ )
308
+ .min(1)
309
+ .max(500),
310
+ });
311
+
312
+ function sameText(a: string, b: string): boolean {
313
+ const left = Buffer.from(a, "utf8");
314
+ const right = Buffer.from(b, "utf8");
315
+ return left.length === right.length && timingSafeEqual(left, right);
316
+ }
317
+
318
+ function tooLarge(limit: number) {
319
+ return sitePublishingError(
320
+ 413,
321
+ "limit_exceeded",
322
+ "site_request_too_large",
323
+ "This request body is too large",
324
+ { limitBytes: limit },
325
+ );
326
+ }
327
+
328
+ /**
329
+ * Read a request body of at most `limit` bytes, hashing it as it arrives: a
330
+ * larger declared length is refused before reading, and the stream is cut
331
+ * off once it passes the cap.
332
+ */
333
+ async function readCappedBody(
334
+ request: Request,
335
+ limit: number,
336
+ ): Promise<{ body: Uint8Array; sha256: string }> {
337
+ const hash = createHash("sha256");
338
+ const declared = request.headers.get("content-length")?.trim();
339
+ if (declared && (!/^\d{1,15}$/u.test(declared) || Number(declared) > limit))
340
+ throw tooLarge(limit);
341
+ if (!request.body) return { body: new Uint8Array(), sha256: hash.digest("hex") };
342
+ const reader = request.body.getReader();
343
+ const chunks: Uint8Array[] = [];
344
+ let total = 0;
345
+ try {
346
+ for (;;) {
347
+ const { done, value } = await reader.read();
348
+ if (done) break;
349
+ total += value.byteLength;
350
+ if (total > limit) {
351
+ await reader.cancel().catch(() => undefined);
352
+ throw tooLarge(limit);
353
+ }
354
+ hash.update(value);
355
+ chunks.push(value);
356
+ }
357
+ } finally {
358
+ reader.releaseLock();
359
+ }
360
+ const body = new Uint8Array(total);
361
+ let offset = 0;
362
+ for (const chunk of chunks) {
363
+ body.set(chunk, offset);
364
+ offset += chunk.byteLength;
365
+ }
366
+ return { body, sha256: hash.digest("hex") };
367
+ }
368
+
369
+ /** Fields of a session a Site viewer sees: the conversation, not the workspace behind it. */
370
+ const SITE_SESSION_FIELDS = [
371
+ "id",
372
+ "status",
373
+ "title",
374
+ "titleSource",
375
+ "initialMessage",
376
+ "model",
377
+ "reasoningEffort",
378
+ "latencyMode",
379
+ "parentSessionId",
380
+ "activeTurnId",
381
+ "initialTurnId",
382
+ "lastSequence",
383
+ "queueVersion",
384
+ "queueHeadPosition",
385
+ "queueTailPosition",
386
+ "effectiveControl",
387
+ "inputWait",
388
+ "activelyWorking",
389
+ "requiresActionSince",
390
+ "createdAt",
391
+ "updatedAt",
392
+ ] as const;
393
+ const SITE_USAGE_LIMIT_FIELDS = [
394
+ "basis",
395
+ "rootSessionId",
396
+ "inherited",
397
+ "status",
398
+ "limitMicros",
399
+ "usedMicros",
400
+ "coverage",
401
+ ] as const;
402
+
403
+ function pick(value: Record<string, unknown>, keys: readonly string[]): Record<string, unknown> {
404
+ const out: Record<string, unknown> = {};
405
+ for (const key of keys) if (key in value) out[key] = value[key];
406
+ return out;
407
+ }
408
+
409
+ /**
410
+ * The Site-safe projection of a session: no account or workspace ids,
411
+ * instructions, tools or policies, metadata, creator, sandbox or diagnostics.
412
+ */
413
+ export function siteSessionProjection(session: unknown): Record<string, unknown> {
414
+ if (!session || typeof session !== "object" || Array.isArray(session)) return {};
415
+ const value = session as Record<string, unknown>;
416
+ const projected = pick(value, SITE_SESSION_FIELDS);
417
+ if ("effectiveControl" in projected)
418
+ projected.effectiveControl = siteControlView(projected.effectiveControl);
419
+ const usage = value.usageLimit;
420
+ if (usage && typeof usage === "object" && !Array.isArray(usage))
421
+ projected.usageLimit = pick(usage as Record<string, unknown>, SITE_USAGE_LIMIT_FIELDS);
422
+ return projected;
423
+ }
424
+
425
+ /**
426
+ * Query parameters a Site viewer may pass to an event page (anything else is
427
+ * dropped). Class filters and `latest` are not offered: they select hidden
428
+ * event types by class, and the Site projection hides those anyway.
429
+ */
430
+ const EVENT_QUERY = ["after", "before", "direction", "excludeTypes", "compact"] as const;
431
+ /** Monitoring-read page sizes: the default, and the most one page returns. */
432
+ const SITE_EVENT_PAGE_DEFAULT = 40;
433
+ const SITE_EVENT_PAGE_MAX = 500;
434
+
435
+ function allowedQuery(url: URL, names: readonly string[], extra: Record<string, string> = {}) {
436
+ const params = new URLSearchParams();
437
+ for (const name of names) {
438
+ const value = url.searchParams.get(name);
439
+ if (value !== null) params.set(name, value);
440
+ }
441
+ for (const [name, value] of Object.entries(extra)) params.set(name, value);
442
+ const text = params.toString();
443
+ return text ? `?${text}` : "";
444
+ }
445
+
446
+ /**
447
+ * The workspace tools a Site conversation gets: the OVERLAP of what the Site
448
+ * declares (its admin-chosen MCP connections) and what the viewer may do in
449
+ * the workspace. Only workspace members who may attach MCP connections to
450
+ * their own sessions (`mcp_servers:attach`, or workspace admins) get them.
451
+ * People outside the workspace (invited people and public visitors, signed in
452
+ * or anonymous) never get workspace tools: only the Site's own data and the
453
+ * page. There is no setting that changes this; `guestTools` is ignored.
454
+ * Optional references: a connection the workspace does not have is skipped
455
+ * rather than failing the conversation.
456
+ */
457
+ export function siteAgentToolRefs(
458
+ agent: Pick<SiteEmbeddedAgent, "tools">,
459
+ viewer: Pick<SiteRuntimeViewer, "kind">,
460
+ memberPermissions: readonly string[] | null = null,
461
+ ): ToolRef[] {
462
+ const toolIds = siteViewerGetsTools(viewer, memberPermissions) ? (agent.tools ?? []) : [];
463
+ return toolIds
464
+ .filter((tool) => SessionMcpServerIdPattern.test(tool))
465
+ .map((tool) => ({ kind: "mcp", id: tool, optional: true }));
466
+ }
467
+
468
+ /** Only workspace members who may use MCP connections get the Site's tools. */
469
+ export function siteViewerGetsTools(
470
+ viewer: Pick<SiteRuntimeViewer, "kind">,
471
+ memberPermissions: readonly string[] | null,
472
+ ): boolean {
473
+ if (viewer.kind !== "member" || !memberPermissions) return false;
474
+ return (
475
+ memberPermissions.includes("mcp_servers:attach") ||
476
+ memberPermissions.includes("workspace:admin")
477
+ );
478
+ }
479
+
480
+ /** A session id derived from the viewer's create idempotency key (same key, same conversation). */
481
+ function idempotentSessionId(seed: string): string {
482
+ const hex = createHash("sha256").update(seed, "utf8").digest("hex");
483
+ const variant = ((parseInt(hex[16]!, 16) & 0x3) | 0x8).toString(16);
484
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-${variant}${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
485
+ }
486
+
487
+ type RuntimeRequest = {
488
+ context: Context;
489
+ url: URL;
490
+ method: string;
491
+ suffix: string;
492
+ body: Uint8Array;
493
+ fields: SiteGatewayFields;
494
+ domain: string;
495
+ };
496
+
497
+ export function registerSiteRuntimeRoutes(app: Hono, deps: ApiRouteDeps): void {
498
+ const dispatch = async (request: Request): Promise<Response> =>
499
+ await app.fetch(markLocalInternalDispatch(request));
500
+ // Publication changes cancel a Site's conversations through this dispatch.
501
+ setSitePublishingDispatch(deps.settings, dispatch);
502
+
503
+ const handler = async (context: Context): Promise<Response> => {
504
+ const domain = configuredSitesDomain(deps.settings);
505
+ if (!domain || !deps.settings.sitesGatewaySecret)
506
+ throw sitePublishingError(
507
+ 404,
508
+ "not_found",
509
+ "sites_publishing_disabled",
510
+ "Publishing Sites is not enabled on this deployment",
511
+ );
512
+ const url = new URL(context.req.url);
513
+ const method = context.req.method.toUpperCase();
514
+ const secrets = [deps.settings.sitesGatewaySecret, deps.settings.sitesGatewaySecretPrevious];
515
+ const refuse = (reason: string) =>
516
+ sitePublishingError(
517
+ 401,
518
+ "unauthenticated",
519
+ "site_gateway_signature_invalid",
520
+ "This request was not signed by the Site gateway",
521
+ { reason },
522
+ );
523
+ // The gateway signature first: nothing about the request is trusted before
524
+ // it. It is checked from the headers alone (the gateway signs the body
525
+ // hash and sends it as a header), so an unsigned request is refused
526
+ // before its body is read; the body is then read under the cap, hashed
527
+ // as it arrives, and must match the signed hash.
528
+ const headers = context.req.raw.headers;
529
+ const pathWithQuery = `${url.pathname}${url.search}`;
530
+ const verified = verifySiteGatewayRequest({ method, pathWithQuery, headers, secrets });
531
+ if (!verified.ok) throw refuse(verified.reason);
532
+ const suffix = url.pathname.slice(SITE_RUNTIME_API_PREFIX.length);
533
+ const read =
534
+ method === "GET" || method === "HEAD"
535
+ ? { body: new Uint8Array(), sha256: EMPTY_BODY_SHA256 }
536
+ : await readCappedBody(
537
+ context.req.raw,
538
+ method === "POST" && suffix === FILE_UPLOAD_SUFFIX
539
+ ? SITE_RUNTIME_FILE_BODY_LIMIT
540
+ : SITE_RUNTIME_JSON_BODY_LIMIT,
541
+ );
542
+ if (!sameSha256Hex(read.sha256, verified.bodySha256)) throw refuse("body_mismatch");
543
+ const body = read.body;
544
+ context.header("cache-control", "no-store");
545
+ const runtime: RuntimeRequest = {
546
+ context,
547
+ url,
548
+ method,
549
+ suffix,
550
+ body,
551
+ fields: verified.fields,
552
+ domain,
553
+ };
554
+ return await route(runtime);
555
+ };
556
+
557
+ const route = async (request: RuntimeRequest): Promise<Response> => {
558
+ const { method, suffix } = request;
559
+ if (method === "GET" && suffix === "/host") return await hostLookup(request);
560
+ const version = /^\/versions\/([^/]+)\/html$/u.exec(suffix);
561
+ if (method === "GET" && version) return await versionHtml(request, version[1]!);
562
+ if (method === "POST" && suffix === "/auth/exchange") return await exchange(request);
563
+ if (method === "POST" && suffix === "/auth/logout") return await logout(request);
564
+ if (method === "POST" && suffix === "/auth/view-as") return await viewAs(request);
565
+ if (method === "POST" && suffix === "/platform/requests")
566
+ return await platformRequests(request);
567
+ if (method === "POST" && suffix === "/site/reports") return await report(request);
568
+ const tool = /^\/tools\/([a-z]+)$/u.exec(suffix);
569
+ if (method === "POST" && tool) return await tools(request, tool[1]!);
570
+ const sitePath = suffix === "/v1/config/client" ? suffix : `/v1/workspaces/site-host${suffix}`;
571
+ const matched = matchSiteOperation(method, sitePath);
572
+ if (!matched) throw unsupported();
573
+ return await operation(request, matched.operation.id);
574
+ };
575
+
576
+ /**
577
+ * The host map record for the signed host (the gateway's fallback on a KV
578
+ * miss). The signed host header names it; an optional `host` query must agree.
579
+ */
580
+ const hostLookup = async (request: RuntimeRequest): Promise<Response> => {
581
+ const asked = request.url.searchParams.get("host");
582
+ const normalize = (host: string) => host.trim().toLowerCase().replace(/\.$/u, "");
583
+ if (asked !== null && normalize(asked) !== normalize(request.fields.host))
584
+ throw invalid("host must be the signed Site host");
585
+ const found = await findSiteRuntimeHost(deps, request.domain, request.fields.host);
586
+ if (!found)
587
+ throw sitePublishingError(404, "not_found", "site_not_found", "This Site does not exist");
588
+ // A lookup reaches the API only on an edge miss: repair a lost edge sync.
589
+ healSiteEdgeIfStale(deps, found.publication);
590
+ return request.context.json(
591
+ siteHostMapRecord(deps.settings, request.domain, found.publication, found.site),
592
+ );
593
+ };
594
+
595
+ /** A viewer whose access is checked, with per-visitor request limits applied. */
596
+ const viewing = async (request: RuntimeRequest) => {
597
+ const host = await requireServedSiteRuntimeHost(deps, request.domain, request.fields.host);
598
+ const viewer = await resolveSiteRuntimeViewer(deps, host, request.fields);
599
+ const { limits, kind: profileKind } = await siteRuntimeLimitProfile(deps, host, viewer);
600
+ await enforceSiteRuntimeViewerRate(
601
+ deps,
602
+ host,
603
+ viewer,
604
+ request.fields,
605
+ "requests",
606
+ limits.requestsPerMinutePerVisitor,
607
+ );
608
+ return { host, viewer, limits, profileKind };
609
+ };
610
+
611
+ /**
612
+ * Immutable HTML of the Site's current version, or of a version a publish
613
+ * or rollback replaced within the last ten minutes (pages already open).
614
+ * Never another Site's version, nor an older one of this Site.
615
+ */
616
+ const versionHtml = async (request: RuntimeRequest, versionId: string): Promise<Response> => {
617
+ const { host } = await viewing(request);
618
+ if (!deps.objectStorage)
619
+ throw sitePublishingError(
620
+ 503,
621
+ "upstream_unavailable",
622
+ "site_storage_unavailable",
623
+ "Storage is not configured",
624
+ );
625
+ const parsed = VersionId.safeParse(versionId);
626
+ const version = parsed.success
627
+ ? await getServableSiteVersion(deps.db, host.scope, {
628
+ siteId: host.site.id,
629
+ versionId: parsed.data,
630
+ currentVersionId: host.versionId,
631
+ graceSeconds: SITE_VERSION_GRACE_SECONDS,
632
+ })
633
+ : null;
634
+ const object = version ? await deps.objectStorage.getObjectBytes(version.contentKey) : null;
635
+ if (!version || !object)
636
+ throw sitePublishingError(404, "not_found", "site_version_not_found", "Version not found");
637
+ return new Response(object.bytes as Uint8Array<ArrayBuffer>, {
638
+ headers: {
639
+ ...USER_CONTENT_SECURITY_HEADERS,
640
+ "Content-Type": "text/html; charset=utf-8",
641
+ "Content-Security-Policy": SITE_VERSION_CONTENT_SECURITY_POLICY,
642
+ // A version's bytes never change; the gateway caches them after its own access check.
643
+ "Cache-Control": "private, max-age=31536000, immutable",
644
+ "Referrer-Policy": "no-referrer",
645
+ ...(version.contentSha256 ? { ETag: `"${version.contentSha256}"` } : {}),
646
+ },
647
+ });
648
+ };
649
+
650
+ /** Viewer sign-in, step two: the gateway trades the one-time code for a site-session. */
651
+ const exchange = async (request: RuntimeRequest): Promise<Response> => {
652
+ const host = await requireServedSiteRuntimeHost(deps, request.domain, request.fields.host);
653
+ const input = parseJson(request.body, SiteAuthExchangeRequest);
654
+ await enforceSiteRuntimeRate(
655
+ deps,
656
+ host,
657
+ siteClientAddressPrincipal(request.fields),
658
+ "sign_in",
659
+ EXCHANGES_PER_MINUTE,
660
+ );
661
+ const refused = () =>
662
+ sitePublishingError(
663
+ 400,
664
+ "validation_failed",
665
+ "site_auth_code_invalid",
666
+ "This sign-in link is no longer valid",
667
+ );
668
+ const code = await consumeSiteAuthCode(deps.db, host.scope, {
669
+ siteId: host.site.id,
670
+ codeHash: sha256Text(input.code),
671
+ hostname: host.hostname,
672
+ });
673
+ if (!code) throw refused();
674
+ const callback = `https://${host.hostname}${SITE_GATEWAY_CALLBACK_PATH}`;
675
+ const challenge = createHash("sha256").update(input.codeVerifier, "utf8").digest("base64url");
676
+ if (
677
+ input.redirectUri !== callback ||
678
+ !sameText(code.redirectUri, input.redirectUri) ||
679
+ !sameText(code.codeChallenge, challenge)
680
+ )
681
+ throw refused();
682
+ // Access is decided again now: membership or an invitation may have changed.
683
+ const kind = signedInSiteViewerKind({
684
+ access: effectiveSiteAccess(host.publication.access, deps.settings),
685
+ member: await isSiteWorkspaceMember(deps.db, host.scope, code.subjectId),
686
+ email: code.email,
687
+ invitedEmails: host.publication.invitedEmails,
688
+ });
689
+ if (!kind)
690
+ throw sitePublishingError(
691
+ 403,
692
+ "forbidden",
693
+ "site_access_denied",
694
+ "You do not have access to this Site",
695
+ );
696
+ const sessionToken = randomBytes(32).toString("base64url");
697
+ const session = await createSiteViewerSession(deps.db, host.scope, {
698
+ siteId: host.site.id,
699
+ tokenHash: sha256Text(sessionToken),
700
+ subjectId: code.subjectId,
701
+ kind,
702
+ email: code.email,
703
+ displayName: code.displayName,
704
+ absoluteSeconds: deps.settings.sitesViewerSessionAbsoluteSeconds,
705
+ });
706
+ const viewer: SiteRuntimeViewer = {
707
+ kind,
708
+ principal: `user:${code.subjectId}`,
709
+ subjectId: code.subjectId,
710
+ name: code.displayName,
711
+ email: code.email,
712
+ sessionId: session.id,
713
+ };
714
+ await meterSiteRuntime(deps, host, { type: "sign_in", viewer });
715
+ const response: SiteAuthExchangeResponse = {
716
+ sessionToken,
717
+ expiresAt: session.expiresAt.toISOString(),
718
+ viewer: publicSiteViewer(viewer),
719
+ };
720
+ return request.context.json(response);
721
+ };
722
+
723
+ const logout = async (request: RuntimeRequest): Promise<Response> => {
724
+ const host = await findSiteRuntimeHost(deps, request.domain, request.fields.host);
725
+ // Signing out always succeeds for the gateway, even for a Site that is gone.
726
+ if (!host) return request.context.json({ revoked: false });
727
+ const revoked = request.fields.session
728
+ ? await revokeSiteViewerSession(deps.db, host.scope, {
729
+ siteId: host.site.id,
730
+ tokenHash: sha256Text(request.fields.session),
731
+ })
732
+ : false;
733
+ return request.context.json({ revoked });
734
+ };
735
+
736
+ /**
737
+ * "View as visitor": a workspace member starts (`invited`, `public`) or
738
+ * ends (null) previewing this Site as that role on their own site-session.
739
+ * Refused for anyone else; the preview only ever shows less (see
740
+ * resolveSiteRuntimeViewer).
741
+ */
742
+ const viewAs = async (request: RuntimeRequest): Promise<Response> => {
743
+ const host = await requireServedSiteRuntimeHost(deps, request.domain, request.fields.host);
744
+ const input = parseJson(request.body, z.object({ viewAs: SiteViewAs.nullable() }));
745
+ const viewer = await resolveSiteRuntimeViewer(deps, host, request.fields, {
746
+ ignoreViewAs: true,
747
+ });
748
+ if (viewer.kind !== "member" || !viewer.sessionId)
749
+ throw sitePublishingError(
750
+ 403,
751
+ "forbidden",
752
+ "site_view_as_members_only",
753
+ "Only workspace members can preview a Site as a visitor",
754
+ );
755
+ await setSiteViewerSessionViewAs(deps.db, host.scope, {
756
+ siteId: host.site.id,
757
+ sessionId: viewer.sessionId,
758
+ viewAs: input.viewAs,
759
+ });
760
+ return request.context.json({ viewAs: input.viewAs });
761
+ };
762
+
763
+ /**
764
+ * Batched gateway request counts (platform usage): the gateway counts
765
+ * every request it serves for a Site host, cache hits included, and
766
+ * reports them in signed batches on the Sites domain apex. Each host's
767
+ * count is recorded once per report id, so a retried report never counts
768
+ * twice. Hosts that no longer resolve are skipped. Hosts are recorded a few
769
+ * at a time, and each workspace's charges are settled once per report.
770
+ */
771
+ const platformRequests = async (request: RuntimeRequest): Promise<Response> => {
772
+ const apex = request.domain.toLowerCase();
773
+ if (request.fields.host.toLowerCase().replace(/\.$/u, "") !== apex)
774
+ throw invalid("Platform reports are signed for the Sites domain");
775
+ const input = parseJson(request.body, SitePlatformRequestReport);
776
+ const perHost = new Map<string, number>();
777
+ for (const entry of input.hosts) {
778
+ const name = entry.host.toLowerCase();
779
+ perHost.set(name, (perHost.get(name) ?? 0) + entry.requests);
780
+ }
781
+ const entries = [...perHost];
782
+ const workspaces = new Map<string, SiteRuntimeHost["scope"]>();
783
+ let counted = 0;
784
+ const recordOne = async ([name, requests]: [string, number]) => {
785
+ const host = await findSiteRuntimeHost(deps, request.domain, name);
786
+ if (!host) return;
787
+ const recorded = await recordSitePlatformRequests(deps.db, host.scope, {
788
+ siteId: host.site.id,
789
+ requests,
790
+ idempotencyKey: `site_platform.requests:${input.reportId}:${name}`,
791
+ });
792
+ if (!recorded) return;
793
+ counted += requests;
794
+ workspaces.set(host.scope.workspaceId, host.scope);
795
+ };
796
+ for (let next = 0; next < entries.length; next += SITE_PLATFORM_REPORT_CONCURRENCY)
797
+ await Promise.all(
798
+ entries.slice(next, next + SITE_PLATFORM_REPORT_CONCURRENCY).map(recordOne),
799
+ );
800
+ for (const scope of workspaces.values()) {
801
+ const usage = await readSitePlatformUsage(deps.db, scope);
802
+ if (usage) await afterSitePlatformUsage(deps, scope, usage);
803
+ }
804
+ return request.context.json({ counted });
805
+ };
806
+
807
+ /**
808
+ * Abuse reports: anyone who reaches a served Site host, rate limited per
809
+ * reporter, per client address and per Site. A reporter and a client
810
+ * address each get the same small share (5 a minute by default) of the
811
+ * Site's 60, so no one reporter fills the Site's cap. Each report also
812
+ * reaches the platform (a structured log line, and an email when an abuse
813
+ * address is set).
814
+ */
815
+ const report = async (request: RuntimeRequest): Promise<Response> => {
816
+ const host = await requireServedSiteRuntimeHost(deps, request.domain, request.fields.host);
817
+ const input = parseJson(request.body, SiteAbuseReportRequest);
818
+ const address = siteClientAddressPrincipal(request.fields);
819
+ const reporter = request.fields.session
820
+ ? `session:${sha256Text(request.fields.session).slice(0, 32)}`
821
+ : request.fields.visitor
822
+ ? `guest:${request.fields.visitor.slice(0, 128)}`
823
+ : address;
824
+ const perVisitor = deps.settings.sitesReportsPerMinutePerVisitor;
825
+ await enforceSiteRuntimeRate(deps, host, reporter, "reports", perVisitor);
826
+ if (reporter !== address)
827
+ await enforceSiteRuntimeRate(deps, host, address, "reports", perVisitor);
828
+ await enforceSiteRuntimeRate(
829
+ deps,
830
+ host,
831
+ SITE_RUNTIME_SITE_PRINCIPAL,
832
+ "reports",
833
+ SITE_REPORTS_PER_MINUTE,
834
+ );
835
+ const created = await createSiteAbuseReport(deps.db, host.scope, {
836
+ siteId: host.site.id,
837
+ hostname: host.hostname,
838
+ category: input.category,
839
+ message: input.message,
840
+ contactEmail: input.contactEmail,
841
+ reporterPrincipal: reporter,
842
+ });
843
+ await meterSiteRuntime(deps, host, { type: "report" });
844
+ notifySiteAbuse(deps, {
845
+ scope: host.scope,
846
+ siteId: host.site.id,
847
+ hostname: host.hostname,
848
+ kind: "report",
849
+ detail: `${input.category}: ${input.message}`,
850
+ });
851
+ return request.context.json({ id: created.id, received: true }, 201);
852
+ };
853
+
854
+ /** Page tools use the signed-in member, not the Site's assistant or owner. */
855
+ const tools = async (request: RuntimeRequest, method: string): Promise<Response> => {
856
+ if (!(SITE_TOOL_METHODS as readonly string[]).includes(method)) throw unsupported();
857
+ await viewing(request);
858
+ const authorization = await authorizePublishedSiteTools(deps, request.domain, request.fields);
859
+ const authority = publishedSiteToolAuthority(authorization);
860
+ // Host-selected association overwrites any page-supplied Site context.
861
+ const site = {
862
+ siteArtifactId: authority.origin.siteId,
863
+ siteVersionId: authority.origin.versionId,
864
+ };
865
+ const authorizeSite: typeof requireWorkspaceSiteToolAuthorization = (db, grant, context) =>
866
+ requireWorkspaceSiteToolAuthorization(db, grant, context, authority.readVersion);
867
+ const options = { signal: request.context.req.raw.signal };
868
+ let result: unknown;
869
+ if (method === "resolve") {
870
+ result = await resolveWorkspaceToolTarget(
871
+ deps,
872
+ authorization,
873
+ { ...parseJson(request.body, ToolGatewayResolveRequest), ...site },
874
+ options,
875
+ );
876
+ } else if (method === "invoke") {
877
+ result = await withSiteSecretContext(
878
+ { siteId: site.siteArtifactId, siteVersion: site.siteVersionId },
879
+ () =>
880
+ invokeWorkspaceToolTarget(
881
+ deps,
882
+ authorization,
883
+ { ...parseJson(request.body, ToolGatewayInvokeRequest), ...site },
884
+ options,
885
+ ),
886
+ );
887
+ } else {
888
+ const call =
889
+ method === "call" ? { ...parseJson(request.body, ToolGatewayCallRequest), ...site } : null;
890
+ // Reject undeclared identities before any connector is prepared.
891
+ if (call)
892
+ await authorizeSite(deps.db, authorization.grant, { ...site, identity: call.identity });
893
+ const prepared = await prepareWorkspaceToolGateway(deps, authorization, options);
894
+ try {
895
+ result = call
896
+ ? await callWorkspaceToolGateway(
897
+ prepared,
898
+ authorization.grant,
899
+ call,
900
+ deps.db,
901
+ undefined,
902
+ deps.observability,
903
+ authorizeSite,
904
+ async () => authority.origin,
905
+ )
906
+ : method === "declarations"
907
+ ? workspaceToolGatewayDeclarations(prepared)
908
+ : prepared.toolGatewayCatalog;
909
+ if (!call) await authority.reauthorize();
910
+ } finally {
911
+ await prepared.close();
912
+ }
913
+ }
914
+ return Response.json(result, { headers: { "cache-control": "no-store" } });
915
+ };
916
+
917
+ /**
918
+ * The models a viewer's conversations may use. Members: the Site agent's
919
+ * model and the allowed models, subscriptions included. Everyone else
920
+ * never uses a subscription-billed model: a subscription agent model is
921
+ * replaced by the workspace's default credit-billed model, and
922
+ * `unavailable` when there is none (agent-model.ts).
923
+ */
924
+ const siteViewerModels = async (
925
+ host: SiteRuntimeHost,
926
+ viewer: SiteRuntimeViewer,
927
+ limits: { agentModels: string[] | null },
928
+ ): Promise<{ allowed: string[]; default: string | null; unavailable: boolean }> => {
929
+ const agentModel = host.publication.agent.model;
930
+ const named = (models: (string | null | undefined)[]) => [
931
+ ...new Set(
932
+ models.filter((model): model is string => typeof model === "string" && model.length > 0),
933
+ ),
934
+ ];
935
+ if (viewer.kind === "member") {
936
+ if (!viewer.subjectId)
937
+ throw siteNotAvailableForViewer("agent_model", "Sign in to use your workspace's models");
938
+ const selections = resolveWorkspaceModelSelection(
939
+ await loadWorkspaceModelSelectionInput(deps.db, deps.settings, {
940
+ ...host.scope,
941
+ subjectId: viewer.subjectId,
942
+ }),
943
+ );
944
+ return {
945
+ allowed: selections
946
+ .filter((selection) => admissibleWorkspaceModel([selection], selection.model.id))
947
+ .map((selection) => selection.model.id),
948
+ default: agentModel,
949
+ unavailable: false,
950
+ };
951
+ }
952
+ const choice = await resolveSiteOutsideModel(deps, host.scope, {
953
+ siteId: host.site.id,
954
+ agentModel,
955
+ });
956
+ const creditModels = new Set(await siteCreditModelIds(deps, host.scope, host.site.id));
957
+ const credited = (models: string[]) => models.filter((model) => creditModels.has(model));
958
+ if (choice.kind === "unavailable")
959
+ return {
960
+ allowed: credited(named(limits.agentModels ?? [])),
961
+ default: null,
962
+ unavailable: true,
963
+ };
964
+ const model = choice.model;
965
+ return {
966
+ allowed: credited(named([model, ...(limits.agentModels ?? [])])),
967
+ default: model,
968
+ unavailable: false,
969
+ };
970
+ };
971
+
972
+ /**
973
+ * What this viewer can use, from the same checks the routes enforce: the
974
+ * agent also needs the platform brake open for people outside the
975
+ * workspace who are not invited, and a credit-billed model for anyone
976
+ * outside the workspace. Page tools are independent of the assistant.
977
+ */
978
+ const viewerCapabilities = async (
979
+ host: SiteRuntimeHost,
980
+ viewer: SiteRuntimeViewer,
981
+ limits: Parameters<typeof siteViewerCapabilities>[3],
982
+ ) => {
983
+ const base = siteViewerCapabilities(deps, host, viewer, limits);
984
+ let agent = base.agent;
985
+ if (agent && (viewer.kind === "guest" || viewer.kind === "user"))
986
+ agent = !(await sitePlatformBrakeEngaged(deps).catch(() => false));
987
+ if (agent && viewer.kind !== "member") {
988
+ const choice = await resolveSiteOutsideModel(deps, host.scope, {
989
+ siteId: host.site.id,
990
+ agentModel: host.publication.agent.model,
991
+ }).catch(() => null);
992
+ agent = choice !== null && choice.kind !== "unavailable";
993
+ }
994
+ const version = host.site.currentVersionId
995
+ ? await getPublishedSiteVersion(deps.db, host.scope, host.site.id, host.site.currentVersionId)
996
+ : null;
997
+ const workspaceTools =
998
+ base.workspaceTools &&
999
+ (version?.requestedTools.length ?? 0) > 0 &&
1000
+ viewer.subjectId !== null &&
1001
+ viewer.subjectId !== undefined &&
1002
+ hasPermission(
1003
+ (await getSiteWorkspaceMemberPermissions(deps.db, host.scope, viewer.subjectId)) ?? [],
1004
+ "workspace:read",
1005
+ );
1006
+ return { ...base, agent, workspaceTools };
1007
+ };
1008
+
1009
+ const operation = async (request: RuntimeRequest, id: string): Promise<Response> => {
1010
+ const { host, viewer, limits, profileKind } = await viewing(request);
1011
+ const { context } = request;
1012
+ switch (id) {
1013
+ case "config.client":
1014
+ return siteRuntimeForwardedResponse(
1015
+ await dispatch(new Request(new URL("/v1/config/client", request.url.origin))),
1016
+ );
1017
+ case "workspace.get":
1018
+ // The Site's own view of "its workspace": no tenant data.
1019
+ return context.json({ id: host.scope.workspaceId, name: host.site.title });
1020
+ case "site.config": {
1021
+ const capabilities = await viewerCapabilities(host, viewer, limits);
1022
+ const config: SiteRuntimeConfig = {
1023
+ siteId: host.site.id,
1024
+ title: host.site.title,
1025
+ hostname: host.hostname,
1026
+ versionId: host.versionId,
1027
+ access: effectiveSiteAccess(host.publication.access, deps.settings),
1028
+ viewer: publicSiteViewer(viewer),
1029
+ capabilities,
1030
+ agent: {
1031
+ enabled: capabilities.agent,
1032
+ model: host.publication.agent.model,
1033
+ },
1034
+ siteData: true,
1035
+ noindex: host.publication.noindex || deps.settings.sitesForceNoindex,
1036
+ abuseEmail: deps.settings.sitesAbuseEmail ?? `abuse@${request.domain}`,
1037
+ };
1038
+ return context.json(config);
1039
+ }
1040
+ case "models.list": {
1041
+ const models = await siteViewerModels(host, viewer, limits);
1042
+ return context.json({
1043
+ models: models.allowed.map((model) => ({ id: model })),
1044
+ defaultModel: models.default,
1045
+ });
1046
+ }
1047
+ case "site.report":
1048
+ return await report(request);
1049
+ case "data.read":
1050
+ case "data.write":
1051
+ case "data.put":
1052
+ case "data.patch":
1053
+ case "data.delete":
1054
+ return await siteData(request, host, viewer, limits.guestWritesPerMinute);
1055
+ case "sql.run":
1056
+ return await siteSql(request, host, viewer, limits.guestWritesPerMinute, profileKind);
1057
+ default:
1058
+ if (id.startsWith("sessions.")) return await sessions(request, id, host, viewer, limits);
1059
+ throw unsupported();
1060
+ }
1061
+ };
1062
+
1063
+ /** This Site's viewer (`user:<subject>` or `guest:<visitor>`) as Site Data and Site SQL see it. */
1064
+ const publishedViewer = (
1065
+ host: SiteRuntimeHost & { versionId: string },
1066
+ viewer: SiteRuntimeViewer,
1067
+ ): PublishedSiteDataViewer =>
1068
+ sitePublishedDataViewer(host, viewer.principal, viewer.kind === "guest");
1069
+
1070
+ /**
1071
+ * Site SQL: run one named query of this Site as the viewer. The query's
1072
+ * audience decides who may run it (guests only `everyone`), and `:viewer`
1073
+ * is bound from this viewer; the Site never sends SQL. Guests' write
1074
+ * queries also count against the guest write limit per visitor and per
1075
+ * client address, like Site Data.
1076
+ */
1077
+ const siteSql = async (
1078
+ request: RuntimeRequest,
1079
+ host: SiteRuntimeHost & { versionId: string },
1080
+ viewer: SiteRuntimeViewer,
1081
+ guestWritesPerMinute: number | null,
1082
+ profileKind: SiteLimitProfileKind,
1083
+ ): Promise<Response> => {
1084
+ const name = /^\/published-artifacts\/current\/sql\/queries\/([^/]+)$/u.exec(
1085
+ request.suffix,
1086
+ )?.[1];
1087
+ if (request.method !== "POST" || !name) throw unsupported();
1088
+ const input = parseJson(request.body, SiteSqlRunRequest);
1089
+ const siteId = host.site.id;
1090
+ return request.context.json(
1091
+ await runSiteSqlRoute(() =>
1092
+ runSiteSqlForViewer(() =>
1093
+ runSiteSqlQuery(
1094
+ deps,
1095
+ publishedViewer(host, viewer),
1096
+ siteId,
1097
+ name,
1098
+ input.params ?? {},
1099
+ { frameSiteId: siteId },
1100
+ {
1101
+ profileKind,
1102
+ beforeWrite: async () => {
1103
+ if (viewer.kind === "guest")
1104
+ await enforceSiteRuntimeViewerRate(
1105
+ deps,
1106
+ host,
1107
+ viewer,
1108
+ request.fields,
1109
+ "guest_writes",
1110
+ guestWritesPerMinute,
1111
+ );
1112
+ },
1113
+ },
1114
+ ),
1115
+ ),
1116
+ ),
1117
+ );
1118
+ };
1119
+
1120
+ /** Site Data and files of this Site, as the viewer (`user:<subject>` or `guest:<visitor>`). */
1121
+ const siteData = async (
1122
+ request: RuntimeRequest,
1123
+ host: SiteRuntimeHost & { versionId: string },
1124
+ viewer: SiteRuntimeViewer,
1125
+ guestWritesPerMinute: number | null,
1126
+ ): Promise<Response> => {
1127
+ const { context, method, url } = request;
1128
+ const path = request.suffix.slice("/published-artifacts/current".length);
1129
+ const authority = publishedViewer(host, viewer);
1130
+ const siteId = host.site.id;
1131
+ const surface = { frameSiteId: siteId };
1132
+ // Guest writes count per visitor and per client address; both must pass.
1133
+ const guestWrite = async () => {
1134
+ if (viewer.kind === "guest")
1135
+ await enforceSiteRuntimeViewerRate(
1136
+ deps,
1137
+ host,
1138
+ viewer,
1139
+ request.fields,
1140
+ "guest_writes",
1141
+ guestWritesPerMinute,
1142
+ );
1143
+ };
1144
+ const file = /^\/files\/([^/]+)$/u.exec(path);
1145
+ if (method === "POST" && path === "/data/query")
1146
+ return context.json(
1147
+ await runSiteData(() =>
1148
+ querySiteData(
1149
+ deps,
1150
+ authority,
1151
+ siteId,
1152
+ parseJson(request.body, SiteDataQueryRequest),
1153
+ surface,
1154
+ ),
1155
+ ),
1156
+ );
1157
+ if (method === "POST" && path === "/data/get") {
1158
+ const record = await runSiteData(() =>
1159
+ getSiteDataRecord(
1160
+ deps,
1161
+ authority,
1162
+ siteId,
1163
+ parseJson(request.body, SiteDataGetRequest),
1164
+ surface,
1165
+ ),
1166
+ );
1167
+ return context.json({ record });
1168
+ }
1169
+ if (method === "POST" && path === "/data/write") {
1170
+ const input = parseJson(request.body, SiteDataWriteRequest);
1171
+ await guestWrite();
1172
+ const written = await runSiteData(() =>
1173
+ writeSiteData(deps, authority, siteId, input.operations, surface),
1174
+ );
1175
+ return context.json({ results: written.results });
1176
+ }
1177
+ if (method === "GET" && path === "/files") {
1178
+ const limit = Number(url.searchParams.get("limit") ?? "100");
1179
+ return context.json(
1180
+ await runSiteData(() =>
1181
+ listSiteDataFiles(
1182
+ deps,
1183
+ authority,
1184
+ siteId,
1185
+ { limit: Number.isFinite(limit) ? limit : 100, cursor: url.searchParams.get("cursor") },
1186
+ surface,
1187
+ ),
1188
+ ),
1189
+ );
1190
+ }
1191
+ if (method === "POST" && path === "/files") {
1192
+ const scope = SiteDataFileScope.safeParse(url.searchParams.get("scope") ?? "shared");
1193
+ if (!scope.success) throw invalid("scope must be shared or private");
1194
+ await guestWrite();
1195
+ const bytes = request.body;
1196
+ const created = await runSiteData(() =>
1197
+ uploadSiteDataFile(
1198
+ deps,
1199
+ authority,
1200
+ siteId,
1201
+ {
1202
+ name: url.searchParams.get("name"),
1203
+ contentType: context.req.header("content-type"),
1204
+ body: {
1205
+ declaredBytes: bytes.byteLength,
1206
+ read: async () => {
1207
+ if (bytes.byteLength > deps.settings.siteDataMaxFileBytes)
1208
+ throw new SiteDataError("quota_exceeded", "This file is too large", {
1209
+ quota: "file_bytes",
1210
+ limit: deps.settings.siteDataMaxFileBytes,
1211
+ });
1212
+ return bytes;
1213
+ },
1214
+ },
1215
+ scope: scope.data,
1216
+ },
1217
+ surface,
1218
+ ),
1219
+ );
1220
+ return context.json(created, 201);
1221
+ }
1222
+ if (method === "GET" && file)
1223
+ return context.json(
1224
+ await runSiteData(() => getSiteDataFileUrl(deps, authority, siteId, file[1]!, surface)),
1225
+ );
1226
+ if (method === "DELETE" && file) {
1227
+ await guestWrite();
1228
+ return context.json(
1229
+ await runSiteData(() => deleteSiteDataFile(deps, authority, siteId, file[1]!, surface)),
1230
+ );
1231
+ }
1232
+ throw unsupported();
1233
+ };
1234
+
1235
+ /** The viewer's own agent conversations on this Site, run as `site:<siteId>`. */
1236
+ const sessions = async (
1237
+ request: RuntimeRequest,
1238
+ id: string,
1239
+ host: SiteRuntimeHost & { versionId: string },
1240
+ viewer: SiteRuntimeViewer,
1241
+ limits: Awaited<ReturnType<typeof siteRuntimeLimitProfile>>["limits"],
1242
+ ): Promise<Response> => {
1243
+ const { context, url } = request;
1244
+ const base = `/v1/workspaces/${host.scope.workspaceId}/sessions`;
1245
+ // Every forwarded call runs inside the Site's origin, and also
1246
+ // carries the Site and version headers the API's Site frame guard
1247
+ // validates; sessions and turns it starts record this Site and version.
1248
+ const siteOrigin = { siteId: host.site.id, title: host.site.title, versionId: host.versionId };
1249
+ let pendingAdmission: { sessionId: string; admissionId: string } | undefined;
1250
+ const send = async (
1251
+ method: string,
1252
+ path: string,
1253
+ body?: unknown,
1254
+ extraPermissions?: readonly Permission[],
1255
+ ) => {
1256
+ const execute = () =>
1257
+ withSiteSessionOrigin(siteOrigin, async () =>
1258
+ dispatch(
1259
+ await siteRuntimeSessionRequest(deps, host, viewer, {
1260
+ origin: url.origin,
1261
+ method,
1262
+ path,
1263
+ ...(body === undefined ? {} : { body }),
1264
+ ...(extraPermissions ? { extraPermissions } : {}),
1265
+ }),
1266
+ ),
1267
+ );
1268
+ const pending = pendingAdmission;
1269
+ if (!pending) return await execute();
1270
+ // The trusted context fences every session write through enqueue commit.
1271
+ // A thrown/unknown dispatch retains its marker; recovery reconciles it.
1272
+ const response = await withSiteRuntimeAdmission(
1273
+ { ...pending, workspaceId: host.scope.workspaceId },
1274
+ execute,
1275
+ );
1276
+ await finishSiteRuntimeAdmission(deps.db, host.scope, pending);
1277
+ pendingAdmission = undefined;
1278
+ if (!response.ok) {
1279
+ const current = await getOwnedSiteRuntimeConversation(deps.db, host.scope, {
1280
+ sessionId: pending.sessionId,
1281
+ siteId: host.site.id,
1282
+ viewerPrincipal: viewer.principal,
1283
+ });
1284
+ if (current) await settleSiteRuntimeSpend(deps, host, current, { release: true });
1285
+ }
1286
+ return response;
1287
+ };
1288
+ /** A forwarded session read, answered with the Site-safe projection. */
1289
+ const projected = async (response: Response, status = response.status) => {
1290
+ if (!response.ok) return siteRuntimeForwardedResponse(response);
1291
+ return context.json(siteSessionProjection(await response.json()), status as 200);
1292
+ };
1293
+ /** Forward, and answer a success with a Site-safe view of its body. */
1294
+ const forwardView = async (
1295
+ method: string,
1296
+ path: string,
1297
+ view: (value: unknown) => unknown,
1298
+ body?: unknown,
1299
+ ) => {
1300
+ const response = await send(method, path, body);
1301
+ if (!response.ok) return siteRuntimeForwardedResponse(response);
1302
+ return context.json(view(await response.json()) as object, response.status as 200);
1303
+ };
1304
+ const notFound = () =>
1305
+ sitePublishingError(
1306
+ 404,
1307
+ "not_found",
1308
+ "site_conversation_not_found",
1309
+ "Conversation not found",
1310
+ );
1311
+ const budgets = siteRuntimeBudgetLimits(limits, deps.settings);
1312
+ /**
1313
+ * Reserve, and when a Site-wide budget refuses, release reservations
1314
+ * finished conversations no longer need and retry once (unless the Site
1315
+ * was swept moments ago).
1316
+ */
1317
+ const reserve = async (run: () => Promise<SiteRuntimeReservation>) => {
1318
+ let reservation = await run();
1319
+ if (siteWideBudget(reservation) && (await sweepSiteRuntimeReservations(deps, host)))
1320
+ reservation = await run();
1321
+ if (!reservation.ok) throw siteReservationRefused(reservation);
1322
+ return reservation;
1323
+ };
1324
+
1325
+ if (id === "sessions.list") {
1326
+ const owned = await listOwnedSiteRuntimeConversations(deps.db, host.scope, {
1327
+ siteId: host.site.id,
1328
+ viewerPrincipal: viewer.principal,
1329
+ limit: LIST_LIMIT,
1330
+ });
1331
+ const listed: unknown[] = [];
1332
+ for (const conversation of owned) {
1333
+ const response = await send("GET", `${base}/${conversation.sessionId}`);
1334
+ if (response.ok) listed.push(siteSessionProjection(await response.json()));
1335
+ else await response.body?.cancel();
1336
+ }
1337
+ return context.json({ pinned: [], sessions: listed, nextCursor: null });
1338
+ }
1339
+
1340
+ if (id === "sessions.create") {
1341
+ const input = parseJson(request.body, CreateConversationRequest);
1342
+ requireSiteAgent(deps, host, viewer, limits);
1343
+ // The Opengeni-wide platform brake stops public visitors' agent turns.
1344
+ if (viewer.kind === "guest" || viewer.kind === "user")
1345
+ await requireSitePlatformOpen(deps, "guest_agent");
1346
+ const models = await siteViewerModels(host, viewer, limits);
1347
+ if (models.unavailable)
1348
+ throw siteNotAvailableForViewer(
1349
+ "agent_model",
1350
+ "This Site's assistant is only available to workspace members right now",
1351
+ { reason: "no_credit_billed_model" },
1352
+ );
1353
+ if (
1354
+ viewer.kind === "guest" &&
1355
+ deps.settings.sitesGuestAgentHumanCheck &&
1356
+ !request.fields.human
1357
+ )
1358
+ throw sitePublishingError(
1359
+ 403,
1360
+ "forbidden",
1361
+ "site_human_check_required",
1362
+ "Confirm you are human to use this Site's assistant",
1363
+ );
1364
+ // A replayed idempotency key names the same conversation: answer it again.
1365
+ const requestedSessionId = input.idempotencyKey
1366
+ ? idempotentSessionId(`${host.site.id}\n${viewer.principal}\n${input.idempotencyKey}`)
1367
+ : randomUUID();
1368
+ let repair: { conversation: SiteRuntimeConversationRecord; model: string } | undefined;
1369
+ if (input.idempotencyKey) {
1370
+ const replay = await getOwnedSiteRuntimeConversation(deps.db, host.scope, {
1371
+ sessionId: requestedSessionId,
1372
+ siteId: host.site.id,
1373
+ viewerPrincipal: viewer.principal,
1374
+ });
1375
+ if (replay) {
1376
+ // A lost start response may have left only a reservation. Reconcile
1377
+ // before replay so this key cannot remain stuck on a missing session.
1378
+ await settleSiteRuntimeSpend(deps, host, replay);
1379
+ const retained = await getOwnedSiteRuntimeConversation(deps.db, host.scope, {
1380
+ sessionId: requestedSessionId,
1381
+ siteId: host.site.id,
1382
+ viewerPrincipal: viewer.principal,
1383
+ });
1384
+ if (retained) {
1385
+ const shell = await getSession(deps.db, host.scope.workspaceId, requestedSessionId);
1386
+ const started = await listSessionEvents(
1387
+ deps.db,
1388
+ host.scope.workspaceId,
1389
+ requestedSessionId,
1390
+ {
1391
+ includeTypes: ["session.created"],
1392
+ payloadMode: "none",
1393
+ limit: 1,
1394
+ },
1395
+ );
1396
+ if (!shell || started.length > 0)
1397
+ return await projected(await send("GET", `${base}/${replay.sessionId}`), 202);
1398
+ // Core deliberately retains a keyed shell when initialization fails.
1399
+ // Re-enter its keyed Start repair, never insert/count a new conversation.
1400
+ repair = { conversation: retained, model: shell.model };
1401
+ }
1402
+ }
1403
+ }
1404
+ // Settle the viewer's recent spend so the day budget sees it.
1405
+ for (const conversation of await listOwnedSiteRuntimeConversations(deps.db, host.scope, {
1406
+ siteId: host.site.id,
1407
+ viewerPrincipal: viewer.principal,
1408
+ limit: 5,
1409
+ }))
1410
+ await settleSiteRuntimeSpend(deps, host, conversation);
1411
+ const agent = host.publication.agent;
1412
+ const memberPermissions =
1413
+ viewer.kind === "member" && viewer.subjectId
1414
+ ? await getSiteWorkspaceMemberPermissions(deps.db, host.scope, viewer.subjectId)
1415
+ : null;
1416
+ const mcpTools = siteAgentToolRefs(agent, viewer, memberPermissions);
1417
+ if (repair) {
1418
+ const previous = repair.conversation;
1419
+ const legacyMember =
1420
+ viewer.kind === "member" &&
1421
+ previous.viewerKind === "member" &&
1422
+ previous.budgetKey === SITE_RUNTIME_SITE_PRINCIPAL;
1423
+ if (
1424
+ (previous.budgetKey !== siteRuntimeBudgetKey(viewer) && !legacyMember) ||
1425
+ previous.toolIds.some((tool) => !mcpTools.some((allowed) => allowed.id === tool))
1426
+ )
1427
+ throw sitePublishingError(
1428
+ 409,
1429
+ "conflict",
1430
+ "site_conversation_outdated",
1431
+ "This Site's assistant changed since this conversation started; start a new one",
1432
+ );
1433
+ if (
1434
+ viewer.kind !== "member" &&
1435
+ !(await siteCreditModelIds(deps, host.scope, host.site.id)).includes(repair.model)
1436
+ )
1437
+ throw siteNotAvailableForViewer(
1438
+ "agent_model",
1439
+ "This Site's assistant cannot use that model for you",
1440
+ { reason: "subscription_model" },
1441
+ );
1442
+ if (viewer.kind === "member" && previous.limitMicros !== null)
1443
+ await liftSiteConversationUsageLimit(
1444
+ deps,
1445
+ host,
1446
+ requestedSessionId,
1447
+ previous.limitMicros,
1448
+ null,
1449
+ );
1450
+ }
1451
+ // Every conversation gets this Site's own data tools, pinned to this
1452
+ // Site and acting as this viewer (site-publishing/agent-data-tools.ts).
1453
+ const dataTools = await siteAgentDataToolsForStart(
1454
+ deps,
1455
+ host.scope,
1456
+ host.site.id,
1457
+ viewer.kind,
1458
+ );
1459
+ // One transaction counts the start and reserves a per-turn estimate
1460
+ // against the visitor's and the Site's windows; only the funded allowance
1461
+ // becomes the session's usage limit, set in the create transaction. The
1462
+ // row also records the tools the conversation gets, so narrowing them
1463
+ // later stops it.
1464
+ const admissionId = randomUUID();
1465
+ const reservation = await reserve(() =>
1466
+ repair
1467
+ ? reserveSiteRuntimeConversationWork(deps.db, host.scope, {
1468
+ admissionId,
1469
+ sessionId: requestedSessionId,
1470
+ limits: budgets,
1471
+ newTurn: false,
1472
+ uncapped: viewer.kind === "member",
1473
+ repairStart: true,
1474
+ })
1475
+ : reserveSiteRuntimeConversationStart(deps.db, host.scope, {
1476
+ admissionId,
1477
+ sessionId: requestedSessionId,
1478
+ siteId: host.site.id,
1479
+ viewerPrincipal: viewer.principal,
1480
+ viewerKind: viewer.kind,
1481
+ viewerEmail: viewer.email,
1482
+ toolIds: mcpTools.map((tool) => tool.id),
1483
+ versionId: host.versionId,
1484
+ limits: budgets,
1485
+ budgetKey: siteRuntimeBudgetKey(viewer),
1486
+ }),
1487
+ );
1488
+ if (reservation.alreadyStarted)
1489
+ return await projected(await send("GET", `${base}/${requestedSessionId}`), 202);
1490
+ pendingAdmission = { sessionId: requestedSessionId, admissionId };
1491
+ const body = {
1492
+ initialMessage: input.initialMessage,
1493
+ requestedSessionId,
1494
+ startMode: "standard",
1495
+ agentAccess: "session",
1496
+ memoryScope: "off",
1497
+ rigId: null,
1498
+ // Only this Site's data tools; never management or workspace tools.
1499
+ firstPartyMcpTools: dataTools,
1500
+ ...(dataTools.length > 0
1501
+ ? { firstPartyMcpPermissions: [...SITE_AGENT_DATA_TOOL_PERMISSIONS] }
1502
+ : {}),
1503
+ agent: {
1504
+ capabilities: {
1505
+ from: "none",
1506
+ humanInput: false,
1507
+ skills: false,
1508
+ ...(dataTools.length > 0 ? { artifacts: true } : {}),
1509
+ },
1510
+ },
1511
+ // No sandbox: a Site's agent answers from the page and the Site's
1512
+ // data, with no shell or workspace files.
1513
+ sandboxBackend: "none",
1514
+ ...(repair ? { model: repair.model } : models.default ? { model: models.default } : {}),
1515
+ instructions: siteAgentInstructions(host, viewer, dataTools, agent.instructions),
1516
+ // Always explicit: an empty list means no workspace tools at all.
1517
+ tools: mcpTools,
1518
+ metadata: {
1519
+ opengeniPublishedSite: {
1520
+ siteId: host.site.id,
1521
+ versionId: host.versionId,
1522
+ viewerKind: viewer.kind,
1523
+ agentDefaultsVersion: SITE_AGENT_DEFAULTS_VERSION,
1524
+ },
1525
+ },
1526
+ ...(input.idempotencyKey ? { idempotencyKey: `site-runtime:${requestedSessionId}` } : {}),
1527
+ };
1528
+ const start = () =>
1529
+ send("POST", base, body, dataTools.length > 0 ? SITE_AGENT_DATA_TOOL_PERMISSIONS : []);
1530
+ // Workspace members get no Site budget at all: their conversation is an
1531
+ // ordinary session under the workspace's normal limits, so no session
1532
+ // usage limit is stamped on it, whatever the Site's limits say.
1533
+ const usageLimitMicros =
1534
+ viewer.kind === "member" || repair ? null : reservation.executionLimitMicros;
1535
+ let created: Response;
1536
+ try {
1537
+ // Initial create's limit command is idempotent, not a top-up. A retained
1538
+ // shell must receive its newly funded ceiling before core repairs it.
1539
+ if (repair && viewer.kind !== "member" && reservation.executionLimitMicros !== null) {
1540
+ try {
1541
+ await capSiteConversationUsageLimit(deps, host, requestedSessionId, null);
1542
+ } catch (error) {
1543
+ // Dispatch has not begun: this failure is definitive, unlike a
1544
+ // lost response from core's initialization transaction.
1545
+ await finishSiteRuntimeAdmission(deps.db, host.scope, pendingAdmission);
1546
+ pendingAdmission = undefined;
1547
+ const retained = await getOwnedSiteRuntimeConversation(deps.db, host.scope, {
1548
+ sessionId: requestedSessionId,
1549
+ siteId: host.site.id,
1550
+ viewerPrincipal: viewer.principal,
1551
+ });
1552
+ if (retained) await settleSiteRuntimeSpend(deps, host, retained, { release: true });
1553
+ throw error;
1554
+ }
1555
+ }
1556
+ created =
1557
+ usageLimitMicros === null
1558
+ ? await start()
1559
+ : await withHostSessionUsageLimit(
1560
+ { subjectId: `site:${host.site.id}`, limitMicros: usageLimitMicros },
1561
+ start,
1562
+ );
1563
+ } catch (error) {
1564
+ await releaseUnstarted(host.scope, requestedSessionId);
1565
+ throw error;
1566
+ }
1567
+ if (created.status !== 202) {
1568
+ await releaseUnstarted(host.scope, requestedSessionId);
1569
+ return siteRuntimeForwardedResponse(created);
1570
+ }
1571
+ const session = (await created.json().catch(() => null)) as { id?: unknown } | null;
1572
+ if (session?.id !== requestedSessionId) {
1573
+ // Never leave a conversation running that its owner record does not track.
1574
+ if (typeof session?.id === "string")
1575
+ await send("POST", `${base}/${session.id}/control`, {
1576
+ action: "cancel",
1577
+ clientEventId: `site-runtime-untracked:${session.id}`,
1578
+ reason: "The conversation could not be tracked",
1579
+ }).then((response) => response.body?.cancel());
1580
+ await releaseUnstarted(host.scope, requestedSessionId);
1581
+ throw sitePublishingError(
1582
+ 502,
1583
+ "upstream_unavailable",
1584
+ "site_conversation_failed",
1585
+ "The conversation could not be started",
1586
+ );
1587
+ }
1588
+ await meterSiteRuntime(deps, host, { type: "conversation", viewer });
1589
+ return context.json(siteSessionProjection(session), 202);
1590
+ }
1591
+
1592
+ const sessionId = /^\/sessions\/([^/]+)/u.exec(request.suffix)?.[1] ?? "";
1593
+ const conversation = await getOwnedSiteRuntimeConversation(deps.db, host.scope, {
1594
+ sessionId,
1595
+ siteId: host.site.id,
1596
+ viewerPrincipal: viewer.principal,
1597
+ });
1598
+ if (!conversation) throw notFound();
1599
+ const target = `${base}/${conversation.sessionId}`;
1600
+ // An EventSource reconnect resumes from Last-Event-ID (forwarded by the
1601
+ // gateway); in-process dispatch carries it as the stream's `after` cursor.
1602
+ const streamQuery = () => {
1603
+ const extra: Record<string, string> = {};
1604
+ const lastEventId = request.context.req.header("last-event-id")?.trim();
1605
+ if (!url.searchParams.has("after") && lastEventId && /^\d{1,15}$/u.test(lastEventId))
1606
+ extra.after = lastEventId;
1607
+ return allowedQuery(url, ["after", "transport"], extra);
1608
+ };
1609
+ /**
1610
+ * Event pages and the stream deliver the same Site-safe projection
1611
+ * (site-publishing/session-events.ts): allowlisted event types and
1612
+ * payload fields, with hidden events as placeholders so sequences stay
1613
+ * contiguous. The page is read whole from the API (every type, stored
1614
+ * payloads) because the projection, not the read mode, decides what a
1615
+ * viewer sees, and message text must not be cut to a preview. Page sizes
1616
+ * and the default newest-first order are those of a monitoring read;
1617
+ * `payloadMode=none` drops payloads.
1618
+ */
1619
+ const payloadNone = url.searchParams.get("payloadMode") === "none";
1620
+ const eventQuery = (includeTypes: string | null) => {
1621
+ const requested = Number(url.searchParams.get("limit") ?? SITE_EVENT_PAGE_DEFAULT);
1622
+ const limit = Number.isFinite(requested)
1623
+ ? Math.min(SITE_EVENT_PAGE_MAX, Math.max(1, Math.floor(requested)))
1624
+ : SITE_EVENT_PAGE_DEFAULT;
1625
+ const ordered = ["after", "before", "direction"].some((name) => url.searchParams.has(name));
1626
+ return allowedQuery(url, EVENT_QUERY, {
1627
+ mode: "forensic",
1628
+ payloadMode: payloadNone ? "none" : "full",
1629
+ limit: String(limit),
1630
+ ...(ordered ? {} : { direction: "before" }),
1631
+ ...(includeTypes ? { includeTypes } : {}),
1632
+ });
1633
+ };
1634
+ /** Requested types a viewer can see; null when none of them is visible. */
1635
+ const visibleIncludeTypes = (): string | null | undefined => {
1636
+ const raw = url.searchParams.get("includeTypes");
1637
+ if (raw === null) return undefined;
1638
+ const visible = raw
1639
+ .split(",")
1640
+ .map((type) => type.trim())
1641
+ .filter((type) => SITE_VISIBLE_SESSION_EVENT_TYPES.has(type));
1642
+ return visible.length > 0 ? [...new Set(visible)].join(",") : null;
1643
+ };
1644
+ // The agent and budget checks every message-bearing operation passes.
1645
+ const admitAgentWork = async (newTurn: boolean) => {
1646
+ requireSiteAgent(deps, host, viewer, limits);
1647
+ if (viewer.kind === "guest" || viewer.kind === "user")
1648
+ await requireSitePlatformOpen(deps, "guest_agent");
1649
+ // Tools and budgets are bound when a conversation starts. One holding a
1650
+ // tool the Site no longer gives this viewer (removed, or the viewer left
1651
+ // the workspace or lost the permission), or counted as a member's while
1652
+ // the viewer no longer is one (or the reverse), takes no more work.
1653
+ const memberPermissions =
1654
+ viewer.kind === "member" && viewer.subjectId
1655
+ ? await getSiteWorkspaceMemberPermissions(deps.db, host.scope, viewer.subjectId)
1656
+ : null;
1657
+ const allowedTools = new Set(
1658
+ siteAgentToolRefs(host.publication.agent, viewer, memberPermissions).map((t) => t.id),
1659
+ );
1660
+ // A member's conversation started before members were kept out of the
1661
+ // Site's windows counts in `*` and stays usable; its reservations stay
1662
+ // in the windows they were made in.
1663
+ const legacyMember =
1664
+ viewer.kind === "member" &&
1665
+ conversation.viewerKind === "member" &&
1666
+ conversation.budgetKey === SITE_RUNTIME_SITE_PRINCIPAL;
1667
+ if (
1668
+ (conversation.budgetKey !== siteRuntimeBudgetKey(viewer) && !legacyMember) ||
1669
+ conversation.toolIds.some((tool) => !allowedTools.has(tool))
1670
+ )
1671
+ throw sitePublishingError(
1672
+ 409,
1673
+ "conflict",
1674
+ "site_conversation_outdated",
1675
+ "This Site's assistant changed since this conversation started; start a new one",
1676
+ );
1677
+ // People outside the workspace never use a subscription-billed model,
1678
+ // whatever model the conversation holds.
1679
+ if (viewer.kind !== "member") {
1680
+ const session = await getSession(deps.db, host.scope.workspaceId, conversation.sessionId);
1681
+ if (
1682
+ session &&
1683
+ !(await siteCreditModelIds(deps, host.scope, host.site.id)).includes(session.model)
1684
+ )
1685
+ throw siteNotAvailableForViewer(
1686
+ "agent_model",
1687
+ "This Site's assistant cannot use that model for you",
1688
+ { reason: "subscription_model" },
1689
+ );
1690
+ }
1691
+ await normalizeSiteAgentSession(
1692
+ deps,
1693
+ host,
1694
+ viewer,
1695
+ conversation.sessionId,
1696
+ conversation.toolIds,
1697
+ );
1698
+ const usage = await settleSiteRuntimeSpend(deps, host, conversation);
1699
+ // Clear the stamped session cap before dropping the durable conversation
1700
+ // marker. Failure refuses admission and preserves the marker for retry.
1701
+ if (viewer.kind === "member" && conversation.limitMicros !== null)
1702
+ await liftSiteConversationUsageLimit(
1703
+ deps,
1704
+ host,
1705
+ conversation.sessionId,
1706
+ conversation.limitMicros,
1707
+ usage,
1708
+ );
1709
+ const admissionId = randomUUID();
1710
+ const admitted = await reserve(() =>
1711
+ reserveSiteRuntimeConversationWork(deps.db, host.scope, {
1712
+ admissionId,
1713
+ sessionId: conversation.sessionId,
1714
+ limits: budgets,
1715
+ newTurn,
1716
+ uncapped: viewer.kind === "member",
1717
+ }),
1718
+ );
1719
+ // Stamp the funded ceiling before work, including atomic top-ups and
1720
+ // reduced caps. Members never carry a Site-imposed usage limit.
1721
+ pendingAdmission = { sessionId: conversation.sessionId, admissionId };
1722
+ try {
1723
+ if (admitted.executionLimitMicros !== null && viewer.kind !== "member")
1724
+ await capSiteConversationUsageLimit(deps, host, conversation.sessionId, usage);
1725
+ } catch (error) {
1726
+ // No dispatch has begun, so failure here is definitive. Reconcile the
1727
+ // queue before refunding: a previous turn may still be executing.
1728
+ await finishSiteRuntimeAdmission(deps.db, host.scope, pendingAdmission);
1729
+ pendingAdmission = undefined;
1730
+ await settleSiteRuntimeSpend(
1731
+ deps,
1732
+ host,
1733
+ { sessionId: conversation.sessionId, admissions: conversation.admissions + 1 },
1734
+ { release: true },
1735
+ );
1736
+ throw error;
1737
+ }
1738
+ };
1739
+ /**
1740
+ * A Site's composer drafts are text only: no workspace resources,
1741
+ * timeline annotations, credentials or connection accounts, and only a
1742
+ * model the Site allows (or the conversation's own).
1743
+ */
1744
+ const siteDraft = async (
1745
+ draft: Pick<
1746
+ z.infer<typeof SaveComposerDraftRequest>,
1747
+ "text" | "resources" | "annotations" | "model" | "reasoningEffort" | "latencyMode"
1748
+ >,
1749
+ ) => {
1750
+ if (draft.text.length > MESSAGE_MAX_CHARS)
1751
+ throw invalid(`A message is at most ${MESSAGE_MAX_CHARS} characters`);
1752
+ if (draft.resources.length > 0 || (draft.annotations?.length ?? 0) > 0)
1753
+ throw invalid("Messages on a published Site are text only");
1754
+ const allowed = new Set((await siteViewerModels(host, viewer, limits)).allowed);
1755
+ // People outside the workspace never use a subscription-billed model.
1756
+ if (viewer.kind !== "member" && isSubscriptionBilledModel([], draft.model))
1757
+ throw siteNotAvailableForViewer(
1758
+ "agent_model",
1759
+ "This Site's assistant cannot use that model for you",
1760
+ { reason: "subscription_model" },
1761
+ );
1762
+ if (!allowed.has(draft.model)) {
1763
+ const current = await send("GET", target);
1764
+ const session = current.ok
1765
+ ? ((await current.json().catch(() => null)) as { model?: unknown } | null)
1766
+ : (await current.body?.cancel(), null);
1767
+ if (session?.model !== draft.model)
1768
+ throw sitePublishingError(
1769
+ 400,
1770
+ "validation_failed",
1771
+ "site_model_not_allowed",
1772
+ "This Site's assistant cannot use that model",
1773
+ );
1774
+ }
1775
+ return {
1776
+ text: draft.text,
1777
+ annotations: [],
1778
+ resources: [],
1779
+ model: draft.model,
1780
+ reasoningEffort: draft.reasoningEffort,
1781
+ latencyMode: draft.latencyMode,
1782
+ };
1783
+ };
1784
+ switch (id) {
1785
+ case "sessions.get":
1786
+ await settleSiteRuntimeSpend(deps, host, conversation);
1787
+ return await projected(await send("GET", target));
1788
+ case "sessions.events": {
1789
+ const payload = payloadNone ? ("none" as const) : ("summary" as const);
1790
+ const includeTypes = visibleIncludeTypes();
1791
+ // Asking only for hidden types yields nothing rather than everything.
1792
+ if (includeTypes === null) return siteSessionEventPage([], { payload });
1793
+ return await siteSessionEventPageResponse(
1794
+ await send("GET", `${target}/events${eventQuery(includeTypes ?? null)}`),
1795
+ { payload },
1796
+ );
1797
+ }
1798
+ case "sessions.stream":
1799
+ return siteSessionEventStreamResponse(
1800
+ await send("GET", `${target}/events/stream${streamQuery()}`),
1801
+ );
1802
+ case "sessions.queue":
1803
+ return await forwardView("GET", `${target}/queue`, siteQueueView);
1804
+ case "sessions.humanInput":
1805
+ return await forwardView(
1806
+ "GET",
1807
+ `${target}/human-input-requests${allowedQuery(url, ["status"])}`,
1808
+ siteHumanInputListView,
1809
+ );
1810
+ case "sessions.humanInput.get": {
1811
+ const requestId = /\/human-input-requests\/([^/]+)$/u.exec(request.suffix)?.[1];
1812
+ if (!requestId) throw unsupported();
1813
+ return await forwardView(
1814
+ "GET",
1815
+ `${target}/human-input-requests/${requestId}`,
1816
+ siteHumanInputRequestView,
1817
+ );
1818
+ }
1819
+ case "sessions.queue.item": {
1820
+ const item = /\/queue\/([^/]+)\/(move|edit|steer|delete)$/u.exec(request.suffix);
1821
+ if (!item) throw unsupported();
1822
+ const [, turnId, action] = item;
1823
+ const path = `${target}/queue/${turnId}/${action}`;
1824
+ switch (action) {
1825
+ case "move": {
1826
+ const move = parseJson(request.body, MoveSessionQueueItemRequest);
1827
+ requireSiteAgent(deps, host, viewer, limits);
1828
+ return await forwardView("POST", path, siteQueueMutationView, move);
1829
+ }
1830
+ case "edit": {
1831
+ const edit = parseJson(request.body, EditSessionQueueItemRequest);
1832
+ requireSiteAgent(deps, host, viewer, limits);
1833
+ return await forwardView("POST", path, siteQueueMutationView, edit);
1834
+ }
1835
+ case "delete": {
1836
+ const remove = parseJson(request.body, DeleteSessionQueueItemRequest);
1837
+ requireSiteAgent(deps, host, viewer, limits);
1838
+ return await forwardView("POST", path, siteQueueMutationView, remove);
1839
+ }
1840
+ default: {
1841
+ // Steering a queued message runs it now: the message was counted
1842
+ // when it was sent, but the agent and spend checks still apply.
1843
+ const steer = parseJson(request.body, SteerSessionQueueItemRequest);
1844
+ await admitAgentWork(false);
1845
+ return await forwardView("POST", path, siteQueueMutationView, steer);
1846
+ }
1847
+ }
1848
+ }
1849
+ case "sessions.composerDraft.get":
1850
+ return await forwardView("GET", `${target}/composer-draft`, siteDraftView);
1851
+ case "sessions.composerDraft.put": {
1852
+ const draft = parseJson(request.body, SaveComposerDraftRequest);
1853
+ requireSiteAgent(deps, host, viewer, limits);
1854
+ return await forwardView("PUT", `${target}/composer-draft`, siteDraftView, {
1855
+ ...(await siteDraft(draft)),
1856
+ expectedRevision: draft.expectedRevision,
1857
+ });
1858
+ }
1859
+ case "sessions.composerDraft.submit": {
1860
+ const submit = parseJson(request.body, SubmitComposerDraftRequest);
1861
+ if (
1862
+ submit.modelContext !== undefined ||
1863
+ (submit.mcpCredentialUpdates?.length ?? 0) > 0 ||
1864
+ submit.connectionAccounts.length > 0 ||
1865
+ submit.personalResourceAttachment !== undefined
1866
+ )
1867
+ throw invalid("Messages on a published Site are text only");
1868
+ const draft = await siteDraft(submit);
1869
+ await admitAgentWork(true);
1870
+ return await forwardView(
1871
+ "POST",
1872
+ `${target}/composer-draft/submit`,
1873
+ siteAcceptedMessageView,
1874
+ {
1875
+ ...draft,
1876
+ expectedDraftRevision: submit.expectedDraftRevision,
1877
+ clientEventId: submit.clientEventId,
1878
+ delivery: submit.delivery,
1879
+ ...(submit.controlEtag ? { controlEtag: submit.controlEtag } : {}),
1880
+ },
1881
+ );
1882
+ }
1883
+ case "sessions.send": {
1884
+ const message = parseJson(request.body, SendMessageRequest);
1885
+ await admitAgentWork(true);
1886
+ return await forwardView("POST", `${target}/events`, siteAcceptedEventView, {
1887
+ type: "user.message",
1888
+ ...(message.clientEventId ? { clientEventId: message.clientEventId } : {}),
1889
+ payload: { text: message.payload.text },
1890
+ });
1891
+ }
1892
+ case "sessions.steer": {
1893
+ const steer = parseJson(request.body, SteerRequest);
1894
+ await admitAgentWork(false);
1895
+ return await forwardView("POST", `${target}/steer`, siteAcceptedMessageView, {
1896
+ text: steer.text,
1897
+ ...(steer.clientEventId ? { clientEventId: steer.clientEventId } : {}),
1898
+ });
1899
+ }
1900
+ case "sessions.control": {
1901
+ const control = parseJson(request.body, SessionControlRequest);
1902
+ // Pausing or cancelling is always allowed; resuming starts work again.
1903
+ if (control.action === "resume") await admitAgentWork(false);
1904
+ const controlled = await send("POST", `${target}/control`, {
1905
+ action: control.action,
1906
+ clientEventId: control.clientEventId,
1907
+ ...(control.reason ? { reason: control.reason } : {}),
1908
+ });
1909
+ // Settled after the control applied: a cancelled conversation releases
1910
+ // its reservation at once.
1911
+ if (control.action !== "resume" && controlled.ok)
1912
+ await settleSiteRuntimeSpend(deps, host, conversation);
1913
+ if (!controlled.ok) return siteRuntimeForwardedResponse(controlled);
1914
+ return context.json(
1915
+ siteControlResultView(await controlled.json()) as object,
1916
+ controlled.status as 200,
1917
+ );
1918
+ }
1919
+ default:
1920
+ throw unsupported();
1921
+ }
1922
+ };
1923
+
1924
+ /**
1925
+ * Undo a conversation start whose session does not exist (the create was
1926
+ * refused or failed before commit). A session that does exist stays
1927
+ * tracked: its usage limit caps it and settling releases what it does not use.
1928
+ */
1929
+ const releaseUnstarted = async (
1930
+ scope: { accountId: string; workspaceId: string },
1931
+ sessionId: string,
1932
+ ) => {
1933
+ // A failed read keeps the reservation: settling releases it once the
1934
+ // session is known to be missing or idle.
1935
+ const exists = await getSession(deps.db, scope.workspaceId, sessionId).then(
1936
+ (session) => session !== null,
1937
+ () => true,
1938
+ );
1939
+ if (!exists) await releaseSiteRuntimeConversationStart(deps.db, scope, { sessionId });
1940
+ };
1941
+
1942
+ app.all(SITE_RUNTIME_API_PREFIX, handler);
1943
+ app.all(`${SITE_RUNTIME_API_PREFIX}/*`, handler);
1944
+ }