@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,1282 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import {
3
+ normalizeSitesDomain,
4
+ OPENGENI_API_CONTRACT_HEADER,
5
+ OPENGENI_API_CONTRACT_REVISION,
6
+ signDelegatedAccessToken,
7
+ siteHostname,
8
+ siteHostnameLabelProblem,
9
+ SITE_HOSTNAME_PROBLEM_MESSAGES,
10
+ suggestSiteHostnameLabel,
11
+ type ErrorCode,
12
+ type Permission,
13
+ type SiteHostMapRecord,
14
+ type SiteHostnameAvailability,
15
+ type SitePublication,
16
+ type UpsertSitePublicationRequest,
17
+ } from "@opengeni/contracts";
18
+ import type { AccessGrant } from "@opengeni/contracts";
19
+ import {
20
+ assertNotSiteClampedAgent,
21
+ currentSiteSessionOrigin,
22
+ hasLiteralPermission,
23
+ siteAgentClamp,
24
+ siteViewerForGrant,
25
+ type ApiRouteDeps,
26
+ } from "@opengeni/core";
27
+ import {
28
+ bumpSitePublicationGeneration,
29
+ checkSiteHostnameClaim,
30
+ countOpenSiteAbuseReports,
31
+ getPublishedSiteVersion,
32
+ getSiteDataSite,
33
+ artifactCreatedInSessionTree,
34
+ getSitePublication,
35
+ isSitePublishingUuid,
36
+ getWorkspaceSiteLimitProfile,
37
+ markSitePublicationEdgeSynced,
38
+ readSiteRuntimeUsage,
39
+ recordSitePublicationScan,
40
+ getPublishedSite,
41
+ listActiveSiteRuntimeConversations,
42
+ revokeSiteViewerSessions,
43
+ SitePublishingError,
44
+ SITE_RUNTIME_SITE_PRINCIPAL,
45
+ upsertSitePublication,
46
+ readSiteHostnameEdgeState,
47
+ readSiteHostnameLiveGeneration,
48
+ type PublishedSiteRecord,
49
+ type SiteDataSite,
50
+ type SiteHostnameEdgeState,
51
+ type SitePublicationRecord,
52
+ type SitePublishingScope,
53
+ type StoredSiteLimitProfile,
54
+ } from "@opengeni/db";
55
+ import { resolveFirstPartyDelegationSecret } from "@opengeni/config";
56
+ import { HTTPException } from "hono/http-exception";
57
+ import { ApiHttpError } from "../http/api-error";
58
+ import { siteDataRole } from "../site-data/service";
59
+ import { createSiteContentScanner, type SiteContentScanner } from "./content-scanner";
60
+ import { createSiteEdgeSync, type SiteEdgeSync } from "./edge-sync";
61
+ import { effectiveSiteLimits, sourceSiteLimitProfile, storedSiteLimitProfileKind } from "./limits";
62
+ import { requireSitePlatformOpen, requireSitePlatformQuota } from "./platform-usage";
63
+ import { readSitePublicationReadiness } from "./readiness";
64
+
65
+ /**
66
+ * Published Sites: publication management, the host map, and the work that
67
+ * follows a change (edge sync, content scan). Postgres is the source of truth;
68
+ * the edge copies are rebuildable and written after commit, never blocking it.
69
+ */
70
+
71
+ export type SitePublishingDeps = Pick<ApiRouteDeps, "db" | "settings" | "objectStorage"> &
72
+ Partial<Pick<ApiRouteDeps, "managedEmailTransport">>;
73
+
74
+ // --- configuration -------------------------------------------------------------
75
+
76
+ /** The configured Sites domain, or null when publishing is off (unset or invalid). */
77
+ export function configuredSitesDomain(settings: ApiRouteDeps["settings"]): string | null {
78
+ if (!settings.sitesDomain) return null;
79
+ try {
80
+ return normalizeSitesDomain(settings.sitesDomain);
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+
86
+ export function sitePublishingError(
87
+ status: number,
88
+ code: ErrorCode,
89
+ detail: string,
90
+ message: string,
91
+ extra: Record<string, unknown> = {},
92
+ ): ApiHttpError {
93
+ return new ApiHttpError(status, {
94
+ code,
95
+ message,
96
+ retryable: false,
97
+ outcomeUnknown: false,
98
+ details: { ...extra, code: detail },
99
+ });
100
+ }
101
+
102
+ /** The Sites domain, or 404 when this deployment does not publish Sites. */
103
+ export function requireSitesDomain(settings: ApiRouteDeps["settings"]): string {
104
+ const domain = configuredSitesDomain(settings);
105
+ if (!domain)
106
+ throw sitePublishingError(
107
+ 404,
108
+ "not_found",
109
+ "sites_publishing_disabled",
110
+ "Publishing Sites is not enabled on this deployment",
111
+ );
112
+ return domain;
113
+ }
114
+
115
+ const PUBLIC_CODE: Record<SitePublishingError["code"], ErrorCode> = {
116
+ not_found: "not_found",
117
+ taken: "conflict",
118
+ held: "conflict",
119
+ reserved: "conflict",
120
+ conflict: "conflict",
121
+ forbidden: "forbidden",
122
+ validation: "validation_failed",
123
+ };
124
+ const STATUS: Record<SitePublishingError["code"], number> = {
125
+ not_found: 404,
126
+ taken: 409,
127
+ held: 409,
128
+ reserved: 409,
129
+ conflict: 409,
130
+ forbidden: 403,
131
+ validation: 400,
132
+ };
133
+
134
+ export function sitePublishingHttpError(error: SitePublishingError): ApiHttpError {
135
+ return sitePublishingError(
136
+ STATUS[error.code],
137
+ PUBLIC_CODE[error.code],
138
+ `site_${error.code}`,
139
+ error.message,
140
+ );
141
+ }
142
+
143
+ export async function runSitePublishing<T>(fn: () => Promise<T>): Promise<T> {
144
+ try {
145
+ return await fn();
146
+ } catch (error) {
147
+ if (error instanceof SitePublishingError) throw sitePublishingHttpError(error);
148
+ throw error;
149
+ }
150
+ }
151
+
152
+ // --- after-commit effects --------------------------------------------------------
153
+
154
+ export type SitePublishingEffects = {
155
+ edgeSync: SiteEdgeSync;
156
+ scanner: SiteContentScanner;
157
+ /** Wait for every scheduled effect (tests and graceful shutdown). */
158
+ flush(): Promise<void>;
159
+ /**
160
+ * Run `run` in the background, at most `SITE_EFFECT_CONCURRENCY` at a time;
161
+ * the rest wait in a bounded queue (beyond it a task is dropped and logged:
162
+ * edge copies self-heal and expire, scans and notices are best effort).
163
+ * Returns false when the task was dropped, so callers can release what
164
+ * they reserved for it.
165
+ */
166
+ schedule(name: string, run: () => Promise<void>): boolean;
167
+ /** Hostnames with an edge sync running or queued, and what it still owes. */
168
+ edgeLabels: Map<string, { again: boolean; purge: boolean; siteIdHint: string | null }>;
169
+ };
170
+
171
+ /** Background effects running at once per process. */
172
+ export const SITE_EFFECT_CONCURRENCY = 4;
173
+ /** Background effects waiting per process before new ones are dropped. */
174
+ const SITE_EFFECT_QUEUE_LIMIT = 2_000;
175
+
176
+ const effectsBySettings = new WeakMap<object, SitePublishingEffects>();
177
+ const dispatchBySettings = new WeakMap<object, (request: Request) => Promise<Response>>();
178
+
179
+ /**
180
+ * The app's in-process dispatch, registered with the site-runtime routes, so
181
+ * publication changes can cancel a Site's conversations through the ordinary
182
+ * session routes. Without it (the operator script) cancellation is skipped.
183
+ */
184
+ export function setSitePublishingDispatch(
185
+ settings: ApiRouteDeps["settings"],
186
+ dispatch: (request: Request) => Promise<Response>,
187
+ ): void {
188
+ dispatchBySettings.set(settings, dispatch);
189
+ }
190
+
191
+ export function sitePublishingDispatch(
192
+ settings: ApiRouteDeps["settings"],
193
+ ): ((request: Request) => Promise<Response>) | null {
194
+ return dispatchBySettings.get(settings) ?? null;
195
+ }
196
+
197
+ type SitePublishingEffectsOptions = {
198
+ fetch?: typeof fetch;
199
+ backoffMs?: number;
200
+ scanner?: SiteContentScanner;
201
+ /** Waiting tasks before new ones are dropped (tests fill a small queue). */
202
+ queueLimit?: number;
203
+ };
204
+
205
+ function createEffects(
206
+ settings: ApiRouteDeps["settings"],
207
+ options: SitePublishingEffectsOptions = {},
208
+ ): SitePublishingEffects {
209
+ const queueLimit = options.queueLimit ?? SITE_EFFECT_QUEUE_LIMIT;
210
+ const pending = new Set<Promise<void>>();
211
+ const waiting: Array<() => void> = [];
212
+ let running = 0;
213
+ return {
214
+ edgeLabels: new Map(),
215
+ edgeSync: createSiteEdgeSync(settings, {
216
+ ...(options.fetch ? { fetch: options.fetch } : {}),
217
+ ...(options.backoffMs !== undefined ? { backoffMs: options.backoffMs } : {}),
218
+ }),
219
+ scanner:
220
+ options.scanner ??
221
+ createSiteContentScanner(settings, options.fetch ? { fetch: options.fetch } : {}),
222
+ async flush() {
223
+ while (pending.size > 0) await Promise.allSettled([...pending]);
224
+ },
225
+ schedule(name, run) {
226
+ if (running >= SITE_EFFECT_CONCURRENCY && waiting.length >= queueLimit) {
227
+ console.warn(`[site-publishing] ${name} dropped: too many pending effects`);
228
+ return false;
229
+ }
230
+ const task = new Promise<void>((resolve) => {
231
+ const start = async () => {
232
+ running += 1;
233
+ try {
234
+ await run();
235
+ } catch (error) {
236
+ console.warn(`[site-publishing] ${name} failed`, {
237
+ error: error instanceof Error ? error.message : String(error),
238
+ });
239
+ } finally {
240
+ running -= 1;
241
+ resolve();
242
+ waiting.shift()?.();
243
+ }
244
+ };
245
+ if (running < SITE_EFFECT_CONCURRENCY) void start();
246
+ else waiting.push(() => void start());
247
+ });
248
+ pending.add(task);
249
+ void task.finally(() => pending.delete(task));
250
+ return true;
251
+ },
252
+ };
253
+ }
254
+
255
+ /** The effects for this deployment's settings (one set per settings object). */
256
+ export function sitePublishingEffects(settings: ApiRouteDeps["settings"]): SitePublishingEffects {
257
+ let effects = effectsBySettings.get(settings);
258
+ if (!effects) {
259
+ effects = createEffects(settings);
260
+ effectsBySettings.set(settings, effects);
261
+ }
262
+ return effects;
263
+ }
264
+
265
+ /** Replace the effects for a settings object (tests: a fake fetch or scanner). */
266
+ export function configureSitePublishingEffects(
267
+ settings: ApiRouteDeps["settings"],
268
+ options: SitePublishingEffectsOptions,
269
+ ): SitePublishingEffects {
270
+ const effects = createEffects(settings, options);
271
+ effectsBySettings.set(settings, effects);
272
+ return effects;
273
+ }
274
+
275
+ /** Whether a Site may be served at all: active and visible to its workspace. */
276
+ export function siteServable(site: Pick<PublishedSiteRecord, "status" | "visibility">): boolean {
277
+ return site.status === "active" && site.visibility === "workspace_shared";
278
+ }
279
+
280
+ export function siteHostMapRecord(
281
+ settings: ApiRouteDeps["settings"],
282
+ domain: string,
283
+ publication: SitePublicationRecord,
284
+ site: PublishedSiteRecord,
285
+ ): SiteHostMapRecord {
286
+ const status =
287
+ publication.status === "active" && !siteServable(site) ? "unpublished" : publication.status;
288
+ return {
289
+ v: 1,
290
+ hostname: siteHostname(publication.label, domain),
291
+ siteId: publication.siteId,
292
+ status,
293
+ access: publication.access,
294
+ versionId: site.currentVersionId,
295
+ title: site.title,
296
+ noindex: publication.noindex || settings.sitesForceNoindex,
297
+ generation: publication.generation,
298
+ updatedAt: publication.updatedAt.toISOString(),
299
+ };
300
+ }
301
+
302
+ /**
303
+ * The host-map record a hostname should have now: the live Site's, else a
304
+ * tombstone (unpublished, no version) with a fresh generation, so it replaces
305
+ * whatever the edge holds for that hostname. Null when no Site is known.
306
+ */
307
+ export function siteHostnameEdgeRecord(
308
+ settings: ApiRouteDeps["settings"],
309
+ domain: string,
310
+ state: SiteHostnameEdgeState,
311
+ siteIdHint: string | null,
312
+ ): SiteHostMapRecord | null {
313
+ if (state.live)
314
+ return siteHostMapRecord(settings, domain, state.live.publication, state.live.site);
315
+ const siteId = state.hostname?.siteId ?? state.hold?.siteId ?? siteIdHint;
316
+ if (!siteId || state.tombstoneGeneration === null) return null;
317
+ return {
318
+ v: 1,
319
+ hostname: siteHostname(state.label, domain),
320
+ siteId,
321
+ status: state.hold?.kind === "platform" ? "suspended" : "unpublished",
322
+ access: "members",
323
+ versionId: null,
324
+ title: "",
325
+ noindex: true,
326
+ generation: state.tombstoneGeneration,
327
+ updatedAt: new Date().toISOString(),
328
+ };
329
+ }
330
+
331
+ /** When each label last asked for a self-heal sync (per process). */
332
+ const recentHeals = new Map<string, number>();
333
+ /** Self-heal syncs started in the current minute (per process). */
334
+ const healWindow = { startedAt: 0, count: 0 };
335
+ /** At most this many self-heal syncs per minute per process. */
336
+ const SITE_EDGE_HEALS_PER_MINUTE = 60;
337
+ /** Re-reads after an edge write before leaving the rest to the next sync. */
338
+ const SITE_EDGE_SYNC_ROUNDS = 3;
339
+
340
+ /**
341
+ * Sync one hostname to the edge: read what it should hold (under the
342
+ * hostname's lock, in a short transaction that commits before any HTTP),
343
+ * write the live Site's record or a tombstone, purge the edge cache when
344
+ * asked, and record the generation the edge holds. Then re-read the label's
345
+ * live generation; if it moved while the HTTP calls ran, write again (up to
346
+ * `SITE_EDGE_SYNC_ROUNDS` times). No database transaction or lock is held
347
+ * during the Cloudflare calls. A no-op without edge settings.
348
+ */
349
+ export async function syncSiteHostnameEdge(
350
+ deps: SitePublishingDeps,
351
+ label: string,
352
+ options: { purge?: boolean; siteIdHint?: string | null } = {},
353
+ ): Promise<"synced" | "skipped" | "disabled"> {
354
+ const domain = configuredSitesDomain(deps.settings);
355
+ const effects = sitePublishingEffects(deps.settings);
356
+ if (!domain || (!effects.edgeSync.kvEnabled && !effects.edgeSync.purgeEnabled)) return "disabled";
357
+ let outcome: "synced" | "skipped" = "skipped";
358
+ let purge = options.purge === true;
359
+ for (let round = 0; round < SITE_EDGE_SYNC_ROUNDS; round += 1) {
360
+ const state = await readSiteHostnameEdgeState(deps.db, label);
361
+ const record = siteHostnameEdgeRecord(deps.settings, domain, state, options.siteIdHint ?? null);
362
+ if (!record) return outcome;
363
+ const put = await effects.edgeSync.putHostRecord(record);
364
+ if (purge) {
365
+ await effects.edgeSync.purgeHost(record.hostname);
366
+ purge = false;
367
+ }
368
+ if (state.live && put !== "disabled") {
369
+ const { publication } = state.live;
370
+ await markSitePublicationEdgeSynced(
371
+ deps.db,
372
+ { accountId: publication.accountId, workspaceId: publication.workspaceId },
373
+ publication.siteId,
374
+ publication.generation,
375
+ );
376
+ outcome = "synced";
377
+ }
378
+ const now = await readSiteHostnameLiveGeneration(deps.db, label);
379
+ const unchanged = state.live
380
+ ? now?.siteId === state.live.publication.siteId &&
381
+ now.generation === state.live.publication.generation
382
+ : now === null;
383
+ if (unchanged) return outcome;
384
+ }
385
+ return outcome;
386
+ }
387
+
388
+ /**
389
+ * After commit: rewrite the edge copy of each hostname (the Site's label and
390
+ * a released previous one), and purge their edge cache when asked
391
+ * (takedown, unpublish). A no-op without edge settings; failures are retried
392
+ * and logged, and a missed sync heals on the next host lookup. Lost if the
393
+ * process stops first: the KV TTL then bounds how long the edge stays stale.
394
+ * One sync per hostname at a time; background effects run with bounded
395
+ * concurrency (`SITE_EFFECT_CONCURRENCY`).
396
+ */
397
+ export function scheduleSiteEdgeSync(
398
+ deps: SitePublishingDeps,
399
+ labels: ReadonlyArray<string | null | undefined>,
400
+ options: { purge?: boolean; siteIdHint?: string | null } = {},
401
+ ): void {
402
+ const domain = configuredSitesDomain(deps.settings);
403
+ const effects = sitePublishingEffects(deps.settings);
404
+ if (!domain || (!effects.edgeSync.kvEnabled && !effects.edgeSync.purgeEnabled)) return;
405
+ const unique = [...new Set(labels.filter((label): label is string => Boolean(label)))];
406
+ for (const label of unique) {
407
+ // At most one sync in flight per hostname: a change while one runs makes
408
+ // it run again (with the purge it now owes) instead of starting another.
409
+ const owed = effects.edgeLabels.get(label);
410
+ if (owed) {
411
+ owed.again = true;
412
+ owed.purge ||= options.purge === true;
413
+ owed.siteIdHint ??= options.siteIdHint ?? null;
414
+ continue;
415
+ }
416
+ const entry = {
417
+ again: false,
418
+ purge: options.purge === true,
419
+ siteIdHint: options.siteIdHint ?? null,
420
+ };
421
+ effects.edgeLabels.set(label, entry);
422
+ const queued = effects.schedule("edge sync", async () => {
423
+ try {
424
+ for (let pass = 0; pass < SITE_EDGE_SYNC_ROUNDS; pass += 1) {
425
+ const purge = entry.purge;
426
+ entry.again = false;
427
+ entry.purge = false;
428
+ await syncSiteHostnameEdge(deps, label, { purge, siteIdHint: entry.siteIdHint });
429
+ if (!entry.again) return;
430
+ }
431
+ } finally {
432
+ effects.edgeLabels.delete(label);
433
+ // Still owed after the last pass: queue one more sync for it.
434
+ if (entry.again)
435
+ scheduleSiteEdgeSync(deps, [label], {
436
+ purge: entry.purge,
437
+ siteIdHint: entry.siteIdHint,
438
+ });
439
+ }
440
+ });
441
+ // Dropped (the queue is full): release the hostname, so a later change
442
+ // or the self-heal can sync it again.
443
+ if (!queued) effects.edgeLabels.delete(label);
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Self-heal: the edge copy of a live hostname is behind its publication
449
+ * (a sync was lost or failed). Schedules one sync per hostname at most every
450
+ * 30 seconds, and at most `SITE_EDGE_HEALS_PER_MINUTE` in all, per process;
451
+ * none while a sync for that hostname is already running.
452
+ */
453
+ export function healSiteEdgeIfStale(
454
+ deps: SitePublishingDeps,
455
+ publication: Pick<
456
+ SitePublicationRecord,
457
+ "label" | "generation" | "edgeSyncedGeneration" | "siteId"
458
+ >,
459
+ ): void {
460
+ if (publication.edgeSyncedGeneration >= publication.generation) return;
461
+ const effects = sitePublishingEffects(deps.settings);
462
+ if (!effects.edgeSync.kvEnabled || effects.edgeLabels.has(publication.label)) return;
463
+ const now = Date.now();
464
+ if ((recentHeals.get(publication.label) ?? 0) > now - 30_000) return;
465
+ if (healWindow.startedAt <= now - 60_000) {
466
+ healWindow.startedAt = now;
467
+ healWindow.count = 0;
468
+ }
469
+ if (healWindow.count >= SITE_EDGE_HEALS_PER_MINUTE) return;
470
+ healWindow.count += 1;
471
+ if (recentHeals.size > 10_000) recentHeals.clear();
472
+ recentHeals.set(publication.label, now);
473
+ scheduleSiteEdgeSync(deps, [publication.label], { siteIdHint: publication.siteId });
474
+ }
475
+
476
+ /**
477
+ * After commit: scan the URLs in the published version's HTML with the
478
+ * configured scanner and record the findings. Never blocks publishing; the
479
+ * `none` scanner records nothing.
480
+ */
481
+ export function scheduleSiteContentScan(
482
+ deps: SitePublishingDeps,
483
+ scope: SitePublishingScope,
484
+ siteId: string,
485
+ versionId: string | null,
486
+ ): void {
487
+ const effects = sitePublishingEffects(deps.settings);
488
+ if (effects.scanner.name === "none" || !versionId || !deps.objectStorage) return;
489
+ const storage = deps.objectStorage;
490
+ effects.schedule("content scan", async () => {
491
+ const version = await getPublishedSiteVersion(deps.db, scope, siteId, versionId);
492
+ if (!version) return;
493
+ const object = await storage.getObjectBytes(version.contentKey);
494
+ if (!object) return;
495
+ const html = new TextDecoder().decode(object.bytes.slice(0, 2 * 1024 * 1024));
496
+ const result = await effects.scanner.scanHtml(html);
497
+ await recordSitePublicationScan(deps.db, scope, siteId, {
498
+ versionId,
499
+ status: result.status,
500
+ findings: result.findings,
501
+ });
502
+ const domain = configuredSitesDomain(deps.settings);
503
+ const publication =
504
+ result.status === "flagged" && domain
505
+ ? await getSitePublication(deps.db, scope, siteId)
506
+ : null;
507
+ if (publication && domain)
508
+ notifySiteAbuse(deps, {
509
+ scope,
510
+ siteId,
511
+ hostname: siteHostname(publication.label, domain),
512
+ kind: "flagged_scan",
513
+ detail: `${result.findings.length} finding(s) in version ${versionId}`,
514
+ });
515
+ });
516
+ }
517
+
518
+ /**
519
+ * A Site changed outside publishing (new version, rollback, archive, audience):
520
+ * advance its publication generation and resync the edge, and scan a newly
521
+ * current version. A no-op when the Site is not published. Never throws.
522
+ */
523
+ export async function notifySitePublicationChanged(
524
+ deps: SitePublishingDeps,
525
+ scope: SitePublishingScope,
526
+ siteId: string,
527
+ options: { scanVersionId?: string | null } = {},
528
+ ): Promise<void> {
529
+ if (!configuredSitesDomain(deps.settings)) return;
530
+ try {
531
+ const publication = await bumpSitePublicationGeneration(deps.db, scope, siteId);
532
+ if (!publication) return;
533
+ // Archived or made private: the Site is no longer served, so its viewers stop too.
534
+ if (publication.status === "active") {
535
+ const site = await getPublishedSite(deps.db, scope, siteId);
536
+ if (!site || !siteServable(site))
537
+ await stopSiteViewers(deps, scope, siteId, { reason: "unpublished" });
538
+ }
539
+ scheduleSiteEdgeSync(deps, [publication.label], { siteIdHint: siteId });
540
+ if (options.scanVersionId && publication.status === "active")
541
+ scheduleSiteContentScan(deps, scope, siteId, options.scanVersionId);
542
+ } catch (error) {
543
+ console.warn("[site-publishing] could not record a Site change", {
544
+ siteId,
545
+ error: error instanceof Error ? error.message : String(error),
546
+ });
547
+ }
548
+ }
549
+
550
+ /**
551
+ * The one hook every Site artifact mutation runs, from the API routes and
552
+ * the agent tools alike (publish, rollback, archive, restore): a published
553
+ * Site follows its Site. `scan` scans the version that became current
554
+ * (publish and rollback). Replays changed nothing and do nothing.
555
+ */
556
+ export async function afterSiteArtifactMutation<
557
+ T extends {
558
+ replayed?: boolean;
559
+ artifact: { id: string; accountId: string; workspaceId: string };
560
+ version?: { id: string } | null;
561
+ },
562
+ >(deps: SitePublishingDeps, result: T, options: { scan?: boolean } = {}): Promise<T> {
563
+ if (!result.replayed)
564
+ await notifySitePublicationChanged(
565
+ deps,
566
+ { accountId: result.artifact.accountId, workspaceId: result.artifact.workspaceId },
567
+ result.artifact.id,
568
+ options.scan && result.version ? { scanVersionId: result.version.id } : {},
569
+ );
570
+ return result;
571
+ }
572
+
573
+ /**
574
+ * Refuse a new version of a published (active) Site while the Opengeni-wide
575
+ * platform brake is engaged. Sites that are not published, rollbacks and
576
+ * unpublishing are never braked.
577
+ */
578
+ export async function requireSiteVersionPublishOpen(
579
+ deps: SitePublishingDeps,
580
+ scope: SitePublishingScope,
581
+ siteId: string,
582
+ ): Promise<void> {
583
+ if (!isSitePublishingUuid(siteId)) return;
584
+ const publication = await getSitePublication(deps.db, scope, siteId);
585
+ if (publication?.status === "active") await requireSitePlatformOpen(deps, "publish");
586
+ }
587
+
588
+ // --- the Site's own principal ---------------------------------------------------------
589
+
590
+ /** The authority Site agent sessions run with: sessions only, nothing else. */
591
+ export const SITE_RUNTIME_AGENT_PERMISSIONS: readonly Permission[] = Object.freeze([
592
+ "workspace:read",
593
+ "sessions:read",
594
+ "sessions:create",
595
+ "sessions:control",
596
+ ]);
597
+
598
+ /**
599
+ * A request to an ordinary session route as the Site's service principal
600
+ * (`site:<siteId>`), with a 60-second delegated token carrying only session
601
+ * permissions, for in-process dispatch. The Site headers ride along so the
602
+ * API's Site frame guard and provenance see which Site acts.
603
+ * `extraPermissions` adds what one request must delegate onward (a
604
+ * conversation start delegates its Site data tools' permissions).
605
+ */
606
+ export async function siteServiceSessionRequest(
607
+ deps: Pick<SitePublishingDeps, "settings">,
608
+ scope: SitePublishingScope,
609
+ site: { id: string; versionId: string | null },
610
+ input: {
611
+ origin: string;
612
+ method: string;
613
+ path: string;
614
+ body?: unknown;
615
+ label: string;
616
+ extraPermissions?: readonly Permission[];
617
+ },
618
+ ): Promise<Request> {
619
+ const secret = resolveFirstPartyDelegationSecret(deps.settings);
620
+ if (!secret)
621
+ throw sitePublishingError(
622
+ 503,
623
+ "upstream_unavailable",
624
+ "site_agents_unavailable",
625
+ "Site assistants are not configured on this deployment",
626
+ { reason: "delegation_unconfigured" },
627
+ );
628
+ const token = await signDelegatedAccessToken(secret, {
629
+ accountId: scope.accountId,
630
+ workspaceId: scope.workspaceId,
631
+ subjectId: `site:${site.id}`,
632
+ subjectLabel: input.label.slice(0, 250),
633
+ permissions: [
634
+ ...new Set([...SITE_RUNTIME_AGENT_PERMISSIONS, ...(input.extraPermissions ?? [])]),
635
+ ],
636
+ principalKind: "service",
637
+ exp: Math.floor(Date.now() / 1000) + 60,
638
+ });
639
+ const headers = new Headers({
640
+ authorization: `Bearer ${token}`,
641
+ accept: "application/json",
642
+ [OPENGENI_API_CONTRACT_HEADER]: OPENGENI_API_CONTRACT_REVISION,
643
+ "x-opengeni-site-id": site.id,
644
+ });
645
+ if (site.versionId) headers.set("x-opengeni-site-version", site.versionId);
646
+ if (input.body !== undefined) headers.set("content-type", "application/json");
647
+ return new Request(new URL(input.path, input.origin), {
648
+ method: input.method,
649
+ headers,
650
+ ...(input.body === undefined ? {} : { body: JSON.stringify(input.body) }),
651
+ });
652
+ }
653
+
654
+ /** Conversations cancelled per stop (newest activity first); the rest end on their own caps. */
655
+ const STOP_CONVERSATIONS_LIMIT = 200;
656
+
657
+ /**
658
+ * Stop a Site's viewers after a takedown, unpublish or narrower access:
659
+ * revoke every site-session now, and after commit cancel the Site's agent
660
+ * conversations that may still be working (in-flight turns and queued
661
+ * messages), as the Site itself through the ordinary control route. The
662
+ * gateway refuses new work at once regardless: every site-runtime call
663
+ * re-checks the publication. Without an in-process dispatch (the operator
664
+ * script) the cancellation is skipped and logged; per-conversation caps
665
+ * still bound what running turns can spend.
666
+ */
667
+ export async function stopSiteViewers(
668
+ deps: SitePublishingDeps,
669
+ scope: SitePublishingScope,
670
+ siteId: string,
671
+ options: {
672
+ revokeSessions?: boolean;
673
+ cancelConversations?: boolean;
674
+ /** Cancel only conversations holding tools outside these (see `siteAgentToolsNarrowed`). */
675
+ toolsNarrowedTo?: { tools: readonly string[]; everyoneTools: boolean };
676
+ reason: string;
677
+ },
678
+ ): Promise<{ revokedSessions: number }> {
679
+ let revokedSessions = 0;
680
+ if (options.revokeSessions !== false) {
681
+ try {
682
+ revokedSessions = await revokeSiteViewerSessions(deps.db, scope, siteId);
683
+ } catch (error) {
684
+ console.warn("[site-publishing] could not revoke viewer sessions", {
685
+ siteId,
686
+ error: error instanceof Error ? error.message : String(error),
687
+ });
688
+ }
689
+ }
690
+ if (options.cancelConversations === false) return { revokedSessions };
691
+ const dispatch = sitePublishingDispatch(deps.settings);
692
+ if (!dispatch) {
693
+ console.warn("[site-publishing] conversation cancel skipped: no in-process dispatch", {
694
+ siteId,
695
+ });
696
+ return { revokedSessions };
697
+ }
698
+ const effects = sitePublishingEffects(deps.settings);
699
+ const origin = deps.settings.publicBaseUrl ?? "http://localhost";
700
+ effects.schedule("cancel conversations", async () => {
701
+ const sessionIds = await listActiveSiteRuntimeConversations(deps.db, scope, {
702
+ siteId,
703
+ limit: STOP_CONVERSATIONS_LIMIT,
704
+ ...(options.toolsNarrowedTo ? { toolsNarrowedTo: options.toolsNarrowedTo } : {}),
705
+ });
706
+ if (sessionIds.length === 0) return;
707
+ // The Site acts with its own origin (a Site and one of its versions).
708
+ const versionId = (await getPublishedSite(deps.db, scope, siteId))?.currentVersionId ?? null;
709
+ if (!versionId) {
710
+ console.warn("[site-publishing] conversation cancel skipped: the Site has no version", {
711
+ siteId,
712
+ });
713
+ return;
714
+ }
715
+ const operation = crypto.randomUUID();
716
+ for (const sessionId of sessionIds) {
717
+ try {
718
+ const response = await dispatch(
719
+ await siteServiceSessionRequest(
720
+ deps,
721
+ scope,
722
+ { id: siteId, versionId },
723
+ {
724
+ origin,
725
+ method: "POST",
726
+ path: `/v1/workspaces/${scope.workspaceId}/sessions/${sessionId}/control`,
727
+ body: {
728
+ action: "cancel",
729
+ clientEventId: `site-stop:${operation}:${sessionId}`.slice(0, 200),
730
+ reason: options.reason.slice(0, 500),
731
+ },
732
+ label: "Published Site",
733
+ },
734
+ ),
735
+ );
736
+ if (!response.ok)
737
+ console.warn("[site-publishing] could not cancel a Site conversation", {
738
+ siteId,
739
+ sessionId,
740
+ status: response.status,
741
+ body: (await response.text().catch(() => "")).slice(0, 300),
742
+ });
743
+ else await response.body?.cancel().catch(() => undefined);
744
+ } catch (error) {
745
+ console.warn("[site-publishing] could not cancel a Site conversation", {
746
+ siteId,
747
+ sessionId,
748
+ error: error instanceof Error ? error.message : String(error),
749
+ });
750
+ }
751
+ }
752
+ });
753
+ return { revokedSessions };
754
+ }
755
+
756
+ // --- reaching the platform -----------------------------------------------------------
757
+
758
+ const abuseNoticeHours = new Map<string, number>();
759
+
760
+ /**
761
+ * Tell the platform about an abuse report or a flagged content scan: always a
762
+ * structured log line (`site_abuse_notice`), and, when OPENGENI_SITES_ABUSE_EMAIL
763
+ * is set, an email through the deployment's managed email transport (at most
764
+ * one per Site per hour, best effort, after the request). The operator's
765
+ * cross-workspace queue (`scripts/operator/site-moderation-queue.ts`) lists
766
+ * everything open.
767
+ */
768
+ export function notifySiteAbuse(
769
+ deps: SitePublishingDeps,
770
+ input: {
771
+ scope: SitePublishingScope;
772
+ siteId: string;
773
+ hostname: string;
774
+ kind: "report" | "flagged_scan";
775
+ detail: string;
776
+ },
777
+ ): void {
778
+ console.warn(
779
+ JSON.stringify({
780
+ event: "site_abuse_notice",
781
+ kind: input.kind,
782
+ accountId: input.scope.accountId,
783
+ workspaceId: input.scope.workspaceId,
784
+ siteId: input.siteId,
785
+ hostname: input.hostname,
786
+ detail: input.detail.slice(0, 200),
787
+ }),
788
+ );
789
+ const to = deps.settings.sitesAbuseEmail;
790
+ const transport = deps.managedEmailTransport;
791
+ if (!to || !transport) return;
792
+ const hour = Math.floor(Date.now() / 3_600_000);
793
+ const key = `${input.siteId}:${input.kind}`;
794
+ if (abuseNoticeHours.get(key) === hour) return;
795
+ if (abuseNoticeHours.size > 10_000) abuseNoticeHours.clear();
796
+ abuseNoticeHours.set(key, hour);
797
+ const what = input.kind === "report" ? "An abuse report" : "A flagged content scan";
798
+ const text = [
799
+ `${what} for the published Site https://${input.hostname}/`,
800
+ `Site: ${input.siteId}`,
801
+ `Workspace: ${input.scope.workspaceId}`,
802
+ `Detail: ${input.detail.slice(0, 200)}`,
803
+ "",
804
+ "List open items with scripts/operator/site-moderation-queue.ts; take a Site down with scripts/operator/site-takedown.ts.",
805
+ ].join("\n");
806
+ sitePublishingEffects(deps.settings).schedule("abuse notice", async () => {
807
+ const result = await transport.send({
808
+ kind: "site_abuse_notice",
809
+ from: transport.sender,
810
+ to,
811
+ subject: `[Opengeni Sites] ${what}: ${input.hostname}`.slice(0, 200),
812
+ text,
813
+ html: `<pre>${text.replace(/[&<>]/gu, (c) => (c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;"))}</pre>`,
814
+ idempotencyKey: `site-abuse-notice:${key}:${hour}`,
815
+ });
816
+ if (result.status !== "sent")
817
+ console.warn("[site-publishing] abuse notice email not sent", {
818
+ siteId: input.siteId,
819
+ status: result.status,
820
+ });
821
+ });
822
+ }
823
+
824
+ // --- management ------------------------------------------------------------------
825
+
826
+ export type SiteManagement = {
827
+ scope: SitePublishingScope;
828
+ site: SiteDataSite;
829
+ record: PublishedSiteRecord;
830
+ manager: boolean;
831
+ admin: boolean;
832
+ owner: boolean;
833
+ subjectId: string | null;
834
+ };
835
+
836
+ /**
837
+ * Publication changes come from people and their own agents, never from a
838
+ * Site page: refuse a request that carries a validated Site origin (a Site or
839
+ * frame page reaching the API through a host) and an agent started from a
840
+ * Site page, or acting in a turn one sent (the Site clamp).
841
+ */
842
+ export async function requireSitePublicationWriter(
843
+ deps: SitePublishingDeps,
844
+ grant: AccessGrant,
845
+ ): Promise<void> {
846
+ if (currentSiteSessionOrigin())
847
+ throw sitePublishingError(
848
+ 403,
849
+ "forbidden",
850
+ "site_page_forbidden",
851
+ "A Site page cannot change Site publications",
852
+ );
853
+ try {
854
+ await assertNotSiteClampedAgent(deps.db, grant, "Site publications");
855
+ } catch (error) {
856
+ if (error instanceof HTTPException && error.status === 403)
857
+ throw sitePublishingError(
858
+ 403,
859
+ "forbidden",
860
+ "site_agent_forbidden",
861
+ "An agent started from a Site page cannot change Site publications",
862
+ );
863
+ throw error;
864
+ }
865
+ }
866
+
867
+ export const SITE_AGENT_OTHER_SITE_MESSAGE =
868
+ "An agent started from a Site page can change only its own Site and Sites it created";
869
+
870
+ /**
871
+ * Whether `grant` may change a Site's content (new versions, rollback,
872
+ * archive, restore, audience). An agent started from a Site page may only
873
+ * for a Site it was started from, or one an agent in its own session tree
874
+ * created with the artifact tools (the Site's first version records that
875
+ * session). It never rewrites another Site the person can edit, published
876
+ * or not, so a page cannot spread itself through the person's drafts. Other
877
+ * callers are not limited here.
878
+ */
879
+ export async function siteAgentMayChangeArtifact(
880
+ deps: SitePublishingDeps,
881
+ grant: AccessGrant,
882
+ artifactId: string,
883
+ ): Promise<boolean> {
884
+ const clamp = await siteAgentClamp(deps.db, grant);
885
+ if (!clamp?.clamped || clamp.originSiteIds.includes(artifactId)) return true;
886
+ const sessionId = grant.metadata?.sessionId;
887
+ if (typeof sessionId !== "string") return false;
888
+ return await artifactCreatedInSessionTree(
889
+ deps.db,
890
+ { accountId: grant.accountId, workspaceId: grant.workspaceId },
891
+ artifactId,
892
+ sessionId,
893
+ );
894
+ }
895
+
896
+ /** `siteAgentMayChangeArtifact`, refusing with 403 `site_agent_forbidden`. */
897
+ export async function requireSiteAgentMayChangeArtifact(
898
+ deps: SitePublishingDeps,
899
+ grant: AccessGrant,
900
+ artifactId: string,
901
+ ): Promise<void> {
902
+ if (!(await siteAgentMayChangeArtifact(deps, grant, artifactId)))
903
+ throw sitePublishingError(
904
+ 403,
905
+ "forbidden",
906
+ "site_agent_forbidden",
907
+ SITE_AGENT_OTHER_SITE_MESSAGE,
908
+ );
909
+ }
910
+
911
+ /**
912
+ * Resolve a Site for publication management. The caller must see the Site
913
+ * (its audience); managers are the Site Data managers (owner, workspace admin,
914
+ * an agent acting for one, or a person-less key with artifacts:publish).
915
+ * `mutate` callers must also pass `requireSitePublicationWriter`.
916
+ */
917
+ export async function resolveSiteManagement(
918
+ deps: SitePublishingDeps,
919
+ grant: AccessGrant,
920
+ siteId: string,
921
+ options: { mutate?: boolean } = {},
922
+ ): Promise<SiteManagement> {
923
+ if (options.mutate) await requireSitePublicationWriter(deps, grant);
924
+ const scope = { accountId: grant.accountId, workspaceId: grant.workspaceId };
925
+ const viewer = await siteViewerForGrant(deps.db, grant);
926
+ const site = await getSiteDataSite(deps.db, scope, viewer, siteId);
927
+ const record = site ? await getPublishedSite(deps.db, scope, siteId) : null;
928
+ if (!site || !record)
929
+ throw sitePublishingError(404, "not_found", "site_not_found", "Site not found");
930
+ const owner = site.ownerSubjectId ?? site.createdBySubjectId ?? null;
931
+ return {
932
+ scope,
933
+ site,
934
+ record,
935
+ manager: siteDataRole(grant, site, viewer.subjectId) === "manager",
936
+ admin: hasLiteralPermission(grant.permissions, "workspace:admin"),
937
+ owner: viewer.subjectId !== null && owner === viewer.subjectId,
938
+ subjectId: viewer.subjectId,
939
+ };
940
+ }
941
+
942
+ export function requireSiteManager(management: SiteManagement): void {
943
+ if (!management.manager)
944
+ throw sitePublishingError(
945
+ 403,
946
+ "forbidden",
947
+ "site_manager_required",
948
+ "Only the Site's owner or a workspace admin can manage its publication",
949
+ );
950
+ }
951
+
952
+ function randomSuffix(): string {
953
+ const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
954
+ const bytes = randomBytes(4);
955
+ return [...bytes].map((byte) => alphabet[byte % alphabet.length]).join("");
956
+ }
957
+
958
+ function normalizeEmails(emails: readonly string[]): string[] {
959
+ return [...new Set(emails.map((email) => email.trim().toLowerCase()).filter(Boolean))].sort();
960
+ }
961
+
962
+ export async function siteLimitInputs(
963
+ deps: SitePublishingDeps,
964
+ scope: SitePublishingScope,
965
+ publication: Pick<SitePublicationRecord, "limitProfile"> | null,
966
+ ): Promise<{ site: StoredSiteLimitProfile | null; workspace: StoredSiteLimitProfile | null }> {
967
+ const site = publication?.limitProfile ?? null;
968
+ return { site, workspace: site ? null : await getWorkspaceSiteLimitProfile(deps.db, scope) };
969
+ }
970
+
971
+ /** The publication as the management API returns it. */
972
+ export async function sitePublicationView(
973
+ deps: SitePublishingDeps,
974
+ scope: SitePublishingScope,
975
+ publication: SitePublicationRecord,
976
+ ): Promise<SitePublication> {
977
+ const domain = requireSitesDomain(deps.settings);
978
+ const profiles = await siteLimitInputs(deps, scope, publication);
979
+ const [openReports, usage, readiness] = await Promise.all([
980
+ countOpenSiteAbuseReports(deps.db, scope, publication.siteId),
981
+ readSiteRuntimeUsage(deps.db, scope, {
982
+ siteId: publication.siteId,
983
+ viewerPrincipal: SITE_RUNTIME_SITE_PRINCIPAL,
984
+ }),
985
+ readSitePublicationReadiness(deps, scope, publication),
986
+ ]);
987
+ const hostname = siteHostname(publication.label, domain);
988
+ return {
989
+ siteId: publication.siteId,
990
+ workspaceId: publication.workspaceId,
991
+ label: publication.label,
992
+ hostname,
993
+ url: `https://${hostname}/`,
994
+ access: publication.access,
995
+ status: publication.status,
996
+ noindex: publication.noindex || deps.settings.sitesForceNoindex,
997
+ agent: { ...publication.agent, guestTools: false },
998
+ limitProfile: sourceSiteLimitProfile(profiles.site, profiles.workspace),
999
+ effectiveLimits: effectiveSiteLimits(deps.settings, profiles.site, profiles.workspace),
1000
+ invitedEmails: publication.invitedEmails,
1001
+ moderation: {
1002
+ openReports,
1003
+ suspendedAt: publication.suspendedAt?.toISOString() ?? null,
1004
+ suspendedBy: publication.suspendedBy,
1005
+ suspendedReason: publication.suspendedReason,
1006
+ },
1007
+ usage: {
1008
+ agentMicrosThisMonth: usage.siteMonth.agentMicros,
1009
+ agentMicrosToday: usage.siteDay.agentMicros,
1010
+ conversationsToday: usage.siteDay.conversations,
1011
+ },
1012
+ warnings: readiness.warnings,
1013
+ outsideAgentModel: readiness.outsideAgentModel,
1014
+ createdAt: publication.createdAt.toISOString(),
1015
+ updatedAt: publication.updatedAt.toISOString(),
1016
+ };
1017
+ }
1018
+
1019
+ /** Hostname availability for this Site (the label rules, then global uniqueness and the hold). */
1020
+ export async function siteHostnameAvailability(
1021
+ deps: SitePublishingDeps,
1022
+ scope: SitePublishingScope,
1023
+ label: string,
1024
+ siteId: string | null,
1025
+ ): Promise<SiteHostnameAvailability> {
1026
+ const domain = requireSitesDomain(deps.settings);
1027
+ const problem = siteHostnameLabelProblem(label);
1028
+ if (problem)
1029
+ return {
1030
+ label,
1031
+ hostname: null,
1032
+ available: false,
1033
+ problem,
1034
+ message: SITE_HOSTNAME_PROBLEM_MESSAGES[problem],
1035
+ };
1036
+ const check = await checkSiteHostnameClaim(
1037
+ deps.db,
1038
+ scope,
1039
+ label,
1040
+ siteId,
1041
+ deps.settings.sitesHostnameHoldDays,
1042
+ );
1043
+ return check.available
1044
+ ? {
1045
+ label,
1046
+ hostname: siteHostname(label, domain),
1047
+ available: true,
1048
+ problem: null,
1049
+ message: null,
1050
+ }
1051
+ : {
1052
+ label,
1053
+ hostname: siteHostname(label, domain),
1054
+ available: false,
1055
+ problem: check.problem,
1056
+ message:
1057
+ check.problem === "held"
1058
+ ? "This name was released recently and is held for its previous owner."
1059
+ : check.problem === "reserved"
1060
+ ? "This name is reserved."
1061
+ : "This name is already taken.",
1062
+ };
1063
+ }
1064
+
1065
+ /**
1066
+ * Create or change a Site's publication. The caller is a manager; a Site must
1067
+ * be active and visible to its workspace to be served; public access needs the
1068
+ * deployment to allow it; changing the limit profile needs a workspace admin.
1069
+ */
1070
+ export async function upsertSitePublicationForManager(
1071
+ deps: SitePublishingDeps,
1072
+ grant: AccessGrant,
1073
+ management: SiteManagement,
1074
+ request: UpsertSitePublicationRequest,
1075
+ ): Promise<{ publication: SitePublication; created: boolean }> {
1076
+ requireSitesDomain(deps.settings);
1077
+ requireSiteManager(management);
1078
+ const { scope, record } = management;
1079
+ const existing = await getSitePublication(deps.db, scope, record.id);
1080
+ if (
1081
+ existing?.status === "suspended" &&
1082
+ (request.status !== undefined ||
1083
+ (request.label !== undefined && request.label !== existing.label))
1084
+ )
1085
+ throw sitePublishingError(
1086
+ 409,
1087
+ "conflict",
1088
+ "site_suspended",
1089
+ "This Site was taken down; restore it first",
1090
+ );
1091
+ const activating =
1092
+ (request.status ?? existing?.status ?? "active") === "active" &&
1093
+ existing?.status !== "suspended";
1094
+ // The Opengeni-wide platform brake stops new publications (already
1095
+ // published Sites keep serving, and can still be changed or unpublished).
1096
+ // In credits mode, so does a used-up monthly allowance the workspace
1097
+ // cannot pay for (402 site_platform_quota_exhausted).
1098
+ if (activating && existing?.status !== "active") {
1099
+ await requireSitePlatformOpen(deps, "publish");
1100
+ await requireSitePlatformQuota(deps, scope, ["requests", "storage_bytes"]);
1101
+ }
1102
+ if (activating && !siteServable(record))
1103
+ throw sitePublishingError(
1104
+ 409,
1105
+ "conflict",
1106
+ record.status !== "active" ? "site_archived" : "site_not_shared",
1107
+ record.status !== "active"
1108
+ ? "Restore this Site before publishing it"
1109
+ : "Share this Site with the workspace before publishing it; Only me Sites cannot be published",
1110
+ );
1111
+ if (request.access === "public" && !deps.settings.sitesPublicEnabled)
1112
+ throw sitePublishingError(
1113
+ 403,
1114
+ "forbidden",
1115
+ "site_public_disabled",
1116
+ "Public Sites are turned off on this deployment",
1117
+ );
1118
+ if (request.limitProfile !== undefined && !management.admin)
1119
+ throw sitePublishingError(
1120
+ 403,
1121
+ "forbidden",
1122
+ "site_admin_required",
1123
+ "Only a workspace admin can change a Site's limits",
1124
+ );
1125
+ // Workspace tools act with the workspace's shared connections for the
1126
+ // workspace members who use the Site's agent (people outside the workspace
1127
+ // never get them): only a workspace admin may give them to the Site's
1128
+ // agent. Anyone who manages the Site may keep or remove them. `guestTools`
1129
+ // is retired: accepted and ignored.
1130
+ const tools = request.agent?.tools;
1131
+ const currentTools = new Set(existing?.agent.tools ?? []);
1132
+ const addsTools = (tools ?? []).some((tool) => !currentTools.has(tool));
1133
+ if (addsTools && !management.admin)
1134
+ throw sitePublishingError(
1135
+ 403,
1136
+ "forbidden",
1137
+ "site_admin_required",
1138
+ "Only a workspace admin can give a Site's assistant workspace tools",
1139
+ );
1140
+ for (const tool of tools ?? [])
1141
+ if (!SITE_AGENT_TOOL_ID.test(tool) || RESERVED_SITE_AGENT_TOOL_IDS.has(tool))
1142
+ throw sitePublishingError(
1143
+ 400,
1144
+ "validation_failed",
1145
+ "site_tool_invalid",
1146
+ `"${tool.slice(0, 80)}" is not a workspace tool connection a Site's assistant can use`,
1147
+ );
1148
+ const limitProfile =
1149
+ request.limitProfile === undefined
1150
+ ? undefined
1151
+ : {
1152
+ kind: storedSiteLimitProfileKind(request.limitProfile.kind),
1153
+ // Overrides adjust any kind's limits (the console edits only the
1154
+ // spend limits and keeps the kind).
1155
+ overrides: request.limitProfile.overrides
1156
+ ? { ...request.limitProfile.overrides, __siteLimitsVersion: 2 }
1157
+ : null,
1158
+ };
1159
+ const model = request.agent?.model;
1160
+ if (model) {
1161
+ const profiles = await siteLimitInputs(deps, scope, {
1162
+ limitProfile: limitProfile === undefined ? (existing?.limitProfile ?? null) : limitProfile,
1163
+ });
1164
+ const allowed = effectiveSiteLimits(
1165
+ deps.settings,
1166
+ profiles.site,
1167
+ profiles.workspace,
1168
+ ).agentModels;
1169
+ if (allowed && !allowed.includes(model))
1170
+ throw sitePublishingError(
1171
+ 400,
1172
+ "validation_failed",
1173
+ "site_model_not_allowed",
1174
+ "This model is not allowed for this Site's agent",
1175
+ );
1176
+ }
1177
+ const changes = {
1178
+ ...(request.label !== undefined ? { label: request.label } : {}),
1179
+ ...(request.access !== undefined ? { access: request.access } : {}),
1180
+ ...(request.status !== undefined ? { status: request.status } : {}),
1181
+ ...(request.noindex !== undefined ? { noindex: request.noindex } : {}),
1182
+ ...(request.agent !== undefined
1183
+ ? {
1184
+ agent: {
1185
+ ...(request.agent.enabled !== undefined ? { enabled: request.agent.enabled } : {}),
1186
+ ...(request.agent.model !== undefined ? { model: request.agent.model } : {}),
1187
+ ...(request.agent.instructions !== undefined
1188
+ ? { instructions: request.agent.instructions }
1189
+ : {}),
1190
+ ...(request.agent.tools !== undefined
1191
+ ? { tools: request.agent.tools ? [...new Set(request.agent.tools)] : null }
1192
+ : {}),
1193
+ guestTools: false,
1194
+ },
1195
+ }
1196
+ : {}),
1197
+ ...(limitProfile !== undefined ? { limitProfile } : {}),
1198
+ ...(request.invitedEmails !== undefined
1199
+ ? { invitedEmails: normalizeEmails(request.invitedEmails) }
1200
+ : {}),
1201
+ };
1202
+ // A new publication without a label takes a suggested one; a random suffix
1203
+ // collides rarely, so retry a few times.
1204
+ let result: Awaited<ReturnType<typeof upsertSitePublication>> | null = null;
1205
+ for (let attempt = 0; attempt < 5 && result === null; attempt += 1) {
1206
+ try {
1207
+ result = await upsertSitePublication(deps.db, scope, {
1208
+ siteId: record.id,
1209
+ defaultLabel: suggestSiteHostnameLabel(record.title, randomSuffix()),
1210
+ changes,
1211
+ actor: grant.subjectId,
1212
+ holdDays: deps.settings.sitesHostnameHoldDays,
1213
+ defaults: { access: "members", noindex: true },
1214
+ });
1215
+ } catch (error) {
1216
+ const retry =
1217
+ error instanceof SitePublishingError &&
1218
+ (error.code === "taken" || error.code === "held") &&
1219
+ request.label === undefined &&
1220
+ existing === null &&
1221
+ attempt < 4;
1222
+ if (!retry) throw error;
1223
+ }
1224
+ }
1225
+ const { publication, created, previousLabel } = result!;
1226
+ const stopping = existing ? siteAccessNarrowed(existing, publication) : null;
1227
+ if (stopping)
1228
+ await stopSiteViewers(deps, scope, record.id, {
1229
+ reason: stopping,
1230
+ revokeSessions: stopping !== "agent_disabled",
1231
+ });
1232
+ else if (existing && siteAgentToolsNarrowed(existing.agent, publication.agent))
1233
+ // Fewer tools, or no longer for everyone: stop the conversations that hold
1234
+ // tools they would not get now. Viewers stay signed in.
1235
+ await stopSiteViewers(deps, scope, record.id, {
1236
+ reason: "agent_tools_narrowed",
1237
+ revokeSessions: false,
1238
+ toolsNarrowedTo: { tools: publication.agent.tools ?? [], everyoneTools: false },
1239
+ });
1240
+ scheduleSiteEdgeSync(deps, [publication.label, previousLabel], {
1241
+ siteIdHint: record.id,
1242
+ purge: stopping === "unpublished",
1243
+ });
1244
+ if (publication.status === "active" && (created || existing?.status !== "active"))
1245
+ scheduleSiteContentScan(deps, scope, record.id, record.currentVersionId);
1246
+ return { publication: await sitePublicationView(deps, scope, publication), created };
1247
+ }
1248
+
1249
+ const ACCESS_RANK = { members: 0, invited: 1, public: 2 } as const;
1250
+
1251
+ /**
1252
+ * Why a publication change must stop current viewers, or null: unpublished,
1253
+ * a narrower access mode, a removed invitation, or the assistant turned off
1254
+ * (conversations only).
1255
+ */
1256
+ export function siteAccessNarrowed(
1257
+ before: Pick<SitePublicationRecord, "status" | "access" | "invitedEmails" | "agent">,
1258
+ after: Pick<SitePublicationRecord, "status" | "access" | "invitedEmails" | "agent">,
1259
+ ): "unpublished" | "access_narrowed" | "agent_disabled" | null {
1260
+ if (before.status === "active" && after.status !== "active") return "unpublished";
1261
+ if (ACCESS_RANK[after.access] < ACCESS_RANK[before.access]) return "access_narrowed";
1262
+ if (after.access === "invited") {
1263
+ const kept = new Set(after.invitedEmails);
1264
+ if (before.invitedEmails.some((email) => !kept.has(email))) return "access_narrowed";
1265
+ }
1266
+ if (before.agent.enabled && !after.agent.enabled) return "agent_disabled";
1267
+ return null;
1268
+ }
1269
+
1270
+ /** Whether a Site's workspace tools narrowed: a tool removed. */
1271
+ export function siteAgentToolsNarrowed(
1272
+ before: Pick<SitePublicationRecord["agent"], "tools">,
1273
+ after: Pick<SitePublicationRecord["agent"], "tools">,
1274
+ ): boolean {
1275
+ const kept = new Set(after.tools ?? []);
1276
+ return (before.tools ?? []).some((tool) => !kept.has(tool));
1277
+ }
1278
+
1279
+ /** MCP server ids (the session tool reference format). */
1280
+ const SITE_AGENT_TOOL_ID = /^[A-Za-z0-9_-]{1,200}$/u;
1281
+ /** The first-party server is never a Site agent's tool: it is the workspace itself. */
1282
+ const RESERVED_SITE_AGENT_TOOL_IDS = new Set(["opengeni"]);