@opengeni/api-router 3.0.1 → 3.0.2-canary.38894290271001

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-AJ62T5QN.js → chunk-GBUJ2DYZ.js} +80507 -39878
  21. package/dist/chunk-GBUJ2DYZ.js.map +1 -0
  22. package/dist/chunk-OGQDXVXY.js +18 -0
  23. package/dist/chunk-OGQDXVXY.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 +150 -49
  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
@@ -1,19 +1,33 @@
1
1
  import {
2
+ BillingCheckoutStatus,
2
3
  CreateBillingPortalRequest,
3
4
  CreateBillingPortalResponse,
4
5
  CreateCheckoutRequest,
5
6
  CreateCheckoutResponse,
7
+ OrganizationUsageQuery,
8
+ OrganizationUsageWorkspacePageQuery,
6
9
  type AccessContext,
7
10
  type Permission,
11
+ type PromotionalCreditScope,
8
12
  } from "@opengeni/contracts";
9
- import { configuredEntitlements } from "@opengeni/config";
13
+ import { OrganizationModelUsageQuery } from "@opengeni/contracts/organization-model-usage";
14
+ import { configuredEntitlements, promotionalCreditScope } from "@opengeni/config";
15
+ import { creditScopeMetadata, creditScopeFromMetadata } from "../credit-promotion-snapshot";
10
16
  import {
11
17
  applyCreditLedgerEntry,
18
+ applyCreditLedgerEntryOnce,
12
19
  getBillingBalance,
20
+ readCreditPromotionPolicy,
13
21
  getBillingCustomer,
22
+ getCreditLedgerEntry,
14
23
  hasCreditLedgerEntry,
15
24
  isStripeWebhookProcessed,
16
25
  listUsageEvents,
26
+ getOrganizationModelUsage,
27
+ getOrganizationUsageSummary,
28
+ getOrganizationUsageWorkspacePage,
29
+ withSessionRlsActorContext,
30
+ withRlsContext,
17
31
  getManagedAccount,
18
32
  markStripeWebhookProcessed,
19
33
  recordStripeWebhookEvent,
@@ -24,6 +38,7 @@ import { HTTPException } from "hono/http-exception";
24
38
  import Stripe from "stripe";
25
39
  import { requireAccessContext } from "@opengeni/core";
26
40
  import type { ApiRouteDeps } from "@opengeni/core";
41
+ import { withAccessGrantSessionRlsContext } from "../access-grant-rls";
27
42
 
28
43
  export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
29
44
  app.get("/v1/billing", async (c) => {
@@ -57,6 +72,46 @@ export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
57
72
  });
58
73
  });
59
74
 
75
+ app.get("/v1/billing/usage-summary", async (c) => {
76
+ const context = await requireAccessContext(c, deps);
77
+ const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
78
+ const parsed = OrganizationUsageQuery.safeParse(c.req.query());
79
+ if (!parsed.success) {
80
+ throw new HTTPException(400, {
81
+ message: parsed.error.issues[0]?.message ?? "invalid usage query",
82
+ });
83
+ }
84
+ return await withBillingUsageActor(deps, context, accountId, async () =>
85
+ c.json(await getOrganizationUsageSummary(deps.db, { accountId, ...parsed.data })),
86
+ );
87
+ });
88
+
89
+ app.get("/v1/billing/usage-workspaces", async (c) => {
90
+ const context = await requireAccessContext(c, deps);
91
+ const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
92
+ const parsed = OrganizationUsageWorkspacePageQuery.safeParse(c.req.query());
93
+ if (!parsed.success)
94
+ throw new HTTPException(400, {
95
+ message: parsed.error.issues[0]?.message ?? "invalid usage page query",
96
+ });
97
+ return await withBillingUsageActor(deps, context, accountId, async () =>
98
+ c.json(await getOrganizationUsageWorkspacePage(deps.db, { accountId, ...parsed.data })),
99
+ );
100
+ });
101
+
102
+ app.get("/v1/billing/usage-models", async (c) => {
103
+ const context = await requireAccessContext(c, deps);
104
+ const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
105
+ const parsed = OrganizationModelUsageQuery.safeParse(c.req.query());
106
+ if (!parsed.success)
107
+ throw new HTTPException(400, {
108
+ message: parsed.error.issues[0]?.message ?? "invalid model usage query",
109
+ });
110
+ return await withBillingUsageActor(deps, context, accountId, async () =>
111
+ c.json(await getOrganizationModelUsage(deps.db, { accountId, ...parsed.data })),
112
+ );
113
+ });
114
+
60
115
  app.get("/v1/billing/entitlements", async (c) => {
61
116
  const context = await requireAccessContext(c, deps);
62
117
  const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
@@ -80,25 +135,81 @@ export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
80
135
  }
81
136
  const body = parsed.data;
82
137
  const accountId = requireSelectedAccount(context, body.accountId, "billing:manage");
83
- const amountCents = usdToCents(body.amountUsd);
84
- const amountMicros = centsToMicros(amountCents);
85
138
  const stripe = stripeClient(deps);
139
+ const promotion = body.promotionCode
140
+ ? await resolveCheckoutPromotionCode(stripe, body.promotionCode)
141
+ : null;
142
+ const promotionalScope = promotion
143
+ ? promotionalCreditScope(
144
+ {
145
+ creditPromotionPolicy:
146
+ (await readCreditPromotionPolicy(deps.db)) ?? deps.settings.creditPromotionPolicy,
147
+ },
148
+ promotion.couponId,
149
+ )
150
+ : undefined;
151
+ const amountCents =
152
+ body.amountUsd !== undefined
153
+ ? usdToCents(body.amountUsd)
154
+ : (promotion?.amountOffCents ?? null);
155
+ if (amountCents === null) {
156
+ throw new HTTPException(422, {
157
+ message: promotion
158
+ ? "Choose how many credits to buy with this code."
159
+ : "amountUsd is required",
160
+ });
161
+ }
162
+ if (amountCents < 500 || amountCents > 1_000_000) {
163
+ throw new HTTPException(422, {
164
+ message: "Credits must be between $5 and $10,000.",
165
+ });
166
+ }
167
+ if (
168
+ promotionalScope &&
169
+ (!promotion?.amountOffCents || amountCents !== promotion.amountOffCents)
170
+ ) {
171
+ throw new HTTPException(422, {
172
+ message:
173
+ "Redeem this offer for its exact credit amount. Buy additional credits separately.",
174
+ });
175
+ }
176
+ const amountMicros = centsToMicros(amountCents);
86
177
  const customerId = await getOrCreateStripeCustomer(deps, stripe, context, accountId);
87
178
  const idempotencyKey = `checkout:${accountId}:${amountMicros}:${crypto.randomUUID()}`;
88
- const session = await stripe.checkout.sessions.create(
89
- stripeCheckoutSessionCreateParams({
90
- accountId,
91
- customerId,
92
- amountCents,
93
- amountMicros,
94
- creditsProductId: deps.settings.stripeCreditsProductId,
95
- publicBaseUrl: deps.settings.publicBaseUrl,
96
- successUrl: body.successUrl,
97
- cancelUrl: body.cancelUrl,
98
- idempotencyKey,
99
- }),
100
- { idempotencyKey },
101
- );
179
+ let session: Stripe.Checkout.Session;
180
+ try {
181
+ session = await stripe.checkout.sessions.create(
182
+ stripeCheckoutSessionCreateParams({
183
+ accountId,
184
+ customerId,
185
+ amountCents,
186
+ amountMicros,
187
+ creditsProductId: deps.settings.stripeCreditsProductId,
188
+ publicBaseUrl: deps.settings.publicBaseUrl,
189
+ webBaseUrl: deps.settings.webBaseUrl,
190
+ successUrl: body.successUrl,
191
+ cancelUrl: body.cancelUrl,
192
+ idempotencyKey,
193
+ promotionalScope,
194
+ ...(promotion
195
+ ? {
196
+ promotionCodeId: promotion.id,
197
+ couponId: promotion.couponId,
198
+ fullyDiscounted:
199
+ promotion.percentOff === 100 || (promotion.amountOffCents ?? 0) >= amountCents,
200
+ }
201
+ : {}),
202
+ }),
203
+ { idempotencyKey },
204
+ );
205
+ } catch (error) {
206
+ // Stripe refuses a code whose restrictions this purchase doesn't meet
207
+ // (first purchase only, a minimum amount, another customer's code).
208
+ if (promotion && error instanceof Stripe.errors.StripeInvalidRequestError) {
209
+ throw new HTTPException(422, { message: "This code can't be used for this purchase." });
210
+ }
211
+ throw error;
212
+ }
102
213
  if (!session.url) {
103
214
  throw new HTTPException(502, { message: "Stripe did not return a checkout URL" });
104
215
  }
@@ -106,6 +217,62 @@ export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
106
217
  CreateCheckoutResponse.parse({
107
218
  checkoutSessionId: session.id,
108
219
  url: session.url,
220
+ amountUsd: amountCents / 100,
221
+ promotionalScope,
222
+ }),
223
+ );
224
+ });
225
+
226
+ // Where one checkout stands, for the page the customer returns to. Credits
227
+ // normally post from the webhook; a completed checkout whose webhook has not
228
+ // arrived yet is settled here from Stripe's own record of the session, under
229
+ // the same ledger idempotency key, so it grants at most once.
230
+ app.get("/v1/billing/checkout/:checkoutSessionId", async (c) => {
231
+ if (deps.settings.billingMode !== "stripe") {
232
+ throw new HTTPException(404, { message: "stripe billing is not enabled" });
233
+ }
234
+ const context = await requireAccessContext(c, deps);
235
+ const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
236
+ const checkoutSessionId = c.req.param("checkoutSessionId");
237
+ if (!/^cs_[A-Za-z0-9_]{1,250}$/.test(checkoutSessionId)) {
238
+ throw new HTTPException(404, { message: "checkout not found" });
239
+ }
240
+ const stripe = stripeClient(deps);
241
+ let session: Stripe.Checkout.Session;
242
+ try {
243
+ session = await stripe.checkout.sessions.retrieve(checkoutSessionId);
244
+ } catch (error) {
245
+ if (error instanceof Stripe.errors.StripeInvalidRequestError) {
246
+ throw new HTTPException(404, { message: "checkout not found" });
247
+ }
248
+ throw error;
249
+ }
250
+ if (session.metadata?.opengeni_account_id !== accountId) {
251
+ throw new HTTPException(404, { message: "checkout not found" });
252
+ }
253
+ const credit = creditMetadata(session.metadata, `Stripe checkout session ${session.id}`);
254
+ let entry = await getCreditLedgerEntry(deps.db, accountId, credit.idempotencyKey);
255
+ if (!entry && session.status === "complete") {
256
+ await grantCheckoutSessionCredits(deps, session, {
257
+ stripeEventId: null,
258
+ livemode: session.livemode,
259
+ });
260
+ entry = await getCreditLedgerEntry(deps.db, accountId, credit.idempotencyKey);
261
+ }
262
+ return c.json(
263
+ BillingCheckoutStatus.parse({
264
+ checkoutSessionId: session.id,
265
+ status: session.status ?? "open",
266
+ credit: {
267
+ state: entry ? "granted" : "pending",
268
+ amountMicros: entry?.amountMicros ?? credit.amountMicros,
269
+ currency: "usd",
270
+ promotionalScope: creditScopeFromMetadata(session.metadata),
271
+ free: entry
272
+ ? entry.sourceType === "stripe_checkout_coupon"
273
+ : isFreeCouponCheckout(session),
274
+ },
275
+ balance: entry ? await getBillingBalance(deps.db, accountId) : null,
109
276
  }),
110
277
  );
111
278
  });
@@ -128,6 +295,7 @@ export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
128
295
  stripeBillingPortalSessionCreateParams({
129
296
  customerId,
130
297
  publicBaseUrl: deps.settings.publicBaseUrl,
298
+ webBaseUrl: deps.settings.webBaseUrl,
131
299
  returnUrl: parsed.data.returnUrl,
132
300
  }),
133
301
  );
@@ -193,24 +361,41 @@ export function stripeCheckoutSessionCreateParams(input: {
193
361
  amountMicros: number;
194
362
  creditsProductId?: string | undefined;
195
363
  publicBaseUrl?: string | undefined;
364
+ webBaseUrl?: string | undefined;
196
365
  successUrl?: string | undefined;
197
366
  cancelUrl?: string | undefined;
198
367
  idempotencyKey: string;
368
+ /** Apply this promotion code up front instead of letting Stripe ask for one. */
369
+ promotionCodeId?: string | undefined;
370
+ couponId?: string | undefined;
371
+ promotionalScope?: PromotionalCreditScope | undefined;
372
+ /**
373
+ * The applied code covers the whole package, so the total is $0. Nothing is
374
+ * taxable, so Checkout skips tax and with it the billing address: the
375
+ * customer only confirms.
376
+ */
377
+ fullyDiscounted?: boolean | undefined;
199
378
  }): Stripe.Checkout.SessionCreateParams {
200
379
  const successUrl = checkoutReturnUrl(
201
380
  input.publicBaseUrl,
381
+ input.webBaseUrl,
202
382
  input.successUrl,
203
383
  "/billing?checkout=success",
204
384
  "successUrl",
205
385
  );
206
386
  const cancelUrl = checkoutReturnUrl(
207
387
  input.publicBaseUrl,
388
+ input.webBaseUrl,
208
389
  input.cancelUrl,
209
390
  "/billing?checkout=cancelled",
210
391
  "cancelUrl",
211
392
  );
212
393
  return {
213
394
  mode: "payment",
395
+ // Stripe takes either a code field on its page or one discount up front.
396
+ ...(input.promotionCodeId
397
+ ? { discounts: [{ promotion_code: input.promotionCodeId }] }
398
+ : { allow_promotion_codes: false }),
214
399
  customer: input.customerId,
215
400
  customer_update: {
216
401
  address: "auto",
@@ -218,7 +403,7 @@ export function stripeCheckoutSessionCreateParams(input: {
218
403
  },
219
404
  success_url: successUrl,
220
405
  cancel_url: cancelUrl,
221
- automatic_tax: { enabled: true },
406
+ automatic_tax: { enabled: !(input.promotionCodeId && input.fullyDiscounted) },
222
407
  billing_address_collection: "auto",
223
408
  line_items: [
224
409
  {
@@ -230,7 +415,7 @@ export function stripeCheckoutSessionCreateParams(input: {
230
415
  ? { product: input.creditsProductId }
231
416
  : {
232
417
  product_data: {
233
- name: "OpenGeni credits",
418
+ name: "Opengeni credits",
234
419
  metadata: {
235
420
  app: "opengeni",
236
421
  billing_model: "prepaid_credits",
@@ -241,10 +426,13 @@ export function stripeCheckoutSessionCreateParams(input: {
241
426
  },
242
427
  ],
243
428
  metadata: {
429
+ ...creditScopeMetadata(input.promotionalScope),
430
+ ...(input.couponId ? { opengeni_credit_offer_id: input.couponId } : {}),
244
431
  opengeni_account_id: input.accountId,
245
432
  opengeni_credit_amount_usd: (input.amountCents / 100).toFixed(2),
246
433
  opengeni_credit_micros: String(input.amountMicros),
247
434
  opengeni_credit_idempotency_key: input.idempotencyKey,
435
+ opengeni_credit_coupon_v1: "1",
248
436
  },
249
437
  payment_intent_data: {
250
438
  metadata: {
@@ -252,6 +440,7 @@ export function stripeCheckoutSessionCreateParams(input: {
252
440
  opengeni_credit_amount_usd: (input.amountCents / 100).toFixed(2),
253
441
  opengeni_credit_micros: String(input.amountMicros),
254
442
  opengeni_credit_idempotency_key: input.idempotencyKey,
443
+ opengeni_credit_coupon_v1: "1",
255
444
  },
256
445
  },
257
446
  invoice_creation: {
@@ -262,6 +451,7 @@ export function stripeCheckoutSessionCreateParams(input: {
262
451
  opengeni_credit_amount_usd: (input.amountCents / 100).toFixed(2),
263
452
  opengeni_credit_micros: String(input.amountMicros),
264
453
  opengeni_credit_idempotency_key: input.idempotencyKey,
454
+ opengeni_credit_coupon_v1: "1",
265
455
  },
266
456
  },
267
457
  },
@@ -271,16 +461,24 @@ export function stripeCheckoutSessionCreateParams(input: {
271
461
  export function stripeBillingPortalSessionCreateParams(input: {
272
462
  customerId: string;
273
463
  publicBaseUrl?: string | undefined;
464
+ webBaseUrl?: string | undefined;
274
465
  returnUrl?: string | undefined;
275
466
  }): Stripe.BillingPortal.SessionCreateParams {
276
467
  return {
277
468
  customer: input.customerId,
278
- return_url: checkoutReturnUrl(input.publicBaseUrl, input.returnUrl, "/billing", "returnUrl"),
469
+ return_url: checkoutReturnUrl(
470
+ input.publicBaseUrl,
471
+ input.webBaseUrl,
472
+ input.returnUrl,
473
+ "/billing",
474
+ "returnUrl",
475
+ ),
279
476
  };
280
477
  }
281
478
 
282
479
  function checkoutReturnUrl(
283
480
  publicBaseUrl: string | undefined,
481
+ webBaseUrl: string | undefined,
284
482
  candidate: string | undefined,
285
483
  fallbackPath: string,
286
484
  field: string,
@@ -290,14 +488,17 @@ function checkoutReturnUrl(
290
488
  message: "OPENGENI_PUBLIC_BASE_URL is required for Stripe redirects",
291
489
  });
292
490
  }
293
- const base = new URL(publicBaseUrl);
491
+ const base = new URL(webBaseUrl ?? publicBaseUrl);
294
492
  const fallback = new URL(fallbackPath, base).toString();
295
493
  if (!candidate) {
296
494
  return fallback;
297
495
  }
298
496
  const parsed = new URL(candidate);
299
- if (parsed.origin !== base.origin) {
300
- throw new HTTPException(400, { message: `${field} must use the OpenGeni public origin` });
497
+ const allowedOrigins = new Set([new URL(publicBaseUrl).origin, base.origin]);
498
+ if (!allowedOrigins.has(parsed.origin)) {
499
+ throw new HTTPException(400, {
500
+ message: `${field} must use the Opengeni public${allowedOrigins.size > 1 ? " or web" : ""} origin`,
501
+ });
301
502
  }
302
503
  return parsed.toString();
303
504
  }
@@ -309,7 +510,12 @@ async function handleStripeWebhookEvent(
309
510
  ): Promise<void> {
310
511
  switch (event.type) {
311
512
  case "checkout.session.completed":
312
- await handleCheckoutSessionCompleted(deps, event);
513
+ case "checkout.session.async_payment_succeeded":
514
+ await handleCheckoutSessionPayment(deps, event);
515
+ return;
516
+ case "checkout.session.async_payment_failed":
517
+ // A delayed payment method failed. Completion reported it `unpaid`, so
518
+ // no credit was granted and there is nothing to reverse.
313
519
  return;
314
520
  case "checkout.session.expired":
315
521
  case "payment_intent.succeeded":
@@ -345,42 +551,247 @@ async function handleStripeWebhookEvent(
345
551
  }
346
552
  }
347
553
 
348
- async function handleCheckoutSessionCompleted(
554
+ export type StripeCheckoutCreditDecision =
555
+ | { action: "grant"; credit: CheckoutCreditMetadata }
556
+ | { action: "ignore"; reason: "foreign_checkout" | "not_payment_mode" | "payment_not_paid" };
557
+
558
+ /**
559
+ * Decides whether a Checkout Session webhook grants Opengeni credits.
560
+ *
561
+ * Sessions created outside Opengeni on the same Stripe account carry no
562
+ * `opengeni_` metadata and are acknowledged without effect. An Opengeni
563
+ * session with malformed credit metadata still throws so the failure stays
564
+ * visible. Credits are granted once Stripe reports the payment `paid` or a
565
+ * completed checkout fully covered by a coupon:
566
+ * on `checkout.session.completed` for immediate methods, or on
567
+ * `checkout.session.async_payment_succeeded` for delayed methods, whose
568
+ * completion event arrives `unpaid`. Both events carry the same session
569
+ * metadata, so the ledger idempotency key grants at most once.
570
+ */
571
+ export function stripeCheckoutCreditDecision(
572
+ session: Stripe.Checkout.Session,
573
+ ): StripeCheckoutCreditDecision {
574
+ if (!Object.keys(session.metadata ?? {}).some((key) => key.startsWith("opengeni_"))) {
575
+ return { action: "ignore", reason: "foreign_checkout" };
576
+ }
577
+ if (session.mode !== "payment") {
578
+ return { action: "ignore", reason: "not_payment_mode" };
579
+ }
580
+ if (session.payment_status !== "paid" && !isFreeCouponCheckout(session)) {
581
+ return { action: "ignore", reason: "payment_not_paid" };
582
+ }
583
+ const credit = creditMetadata(session.metadata, `Stripe checkout session ${session.id}`);
584
+ if (
585
+ session.metadata?.opengeni_credit_coupon_v1 === "1" &&
586
+ (session.currency !== "usd" ||
587
+ session.amount_subtotal !== credit.amountMicros / 10_000 ||
588
+ typeof session.amount_total !== "number" ||
589
+ !Number.isSafeInteger(session.amount_total) ||
590
+ session.amount_total < 0)
591
+ ) {
592
+ throw new Error(`Stripe checkout session ${session.id} has invalid credit package totals`);
593
+ }
594
+ return { action: "grant", credit };
595
+ }
596
+
597
+ function isFreeCouponCheckout(session: Stripe.Checkout.Session): boolean {
598
+ return (
599
+ (session.payment_status === "paid" || session.payment_status === "no_payment_required") &&
600
+ session.status === "complete" &&
601
+ session.amount_total === 0 &&
602
+ typeof session.amount_subtotal === "number" &&
603
+ session.amount_subtotal > 0 &&
604
+ (session.total_details?.amount_discount ?? 0) >= session.amount_subtotal
605
+ );
606
+ }
607
+
608
+ async function handleCheckoutSessionPayment(
349
609
  deps: ApiRouteDeps,
350
610
  event: Stripe.Event,
351
611
  ): Promise<void> {
352
- const session = event.data.object as Stripe.Checkout.Session;
353
- if (session.mode !== "payment" || session.payment_status !== "paid") {
612
+ await grantCheckoutSessionCredits(deps, event.data.object as Stripe.Checkout.Session, {
613
+ stripeEventId: event.id,
614
+ stripeEventType: event.type,
615
+ livemode: event.livemode,
616
+ });
617
+ }
618
+
619
+ /**
620
+ * Grants a paid (or fully coupon-covered) Opengeni checkout's credits once.
621
+ * The webhook calls this with its event; the checkout status read calls it
622
+ * with the session it retrieved from Stripe when the webhook is late.
623
+ */
624
+ async function grantCheckoutSessionCredits(
625
+ deps: ApiRouteDeps,
626
+ session: Stripe.Checkout.Session,
627
+ source: { stripeEventId: string | null; stripeEventType?: string; livemode: boolean },
628
+ ): Promise<void> {
629
+ const decision = stripeCheckoutCreditDecision(session);
630
+ if (decision.action === "ignore") {
631
+ if (decision.reason === "foreign_checkout") {
632
+ console.info("[api] stripe webhook ignored a checkout session not created by Opengeni", {
633
+ stripeEventType: source.stripeEventType ?? null,
634
+ livemode: source.livemode,
635
+ });
636
+ }
637
+ return;
638
+ }
639
+ const credit = decision.credit;
640
+ const freeCouponCheckout = isFreeCouponCheckout(session);
641
+ const promotionalScope = creditScopeFromMetadata(session.metadata);
642
+ if (promotionalScope && !freeCouponCheckout) {
643
+ throw new Error("Scoped promotional credits require a fully discounted checkout");
644
+ }
645
+ if (!(await getManagedAccount(deps.db, credit.accountId))) {
646
+ // Another Opengeni deployment sharing the Stripe account (or an account
647
+ // removed before a delayed payment settled). Retrying cannot succeed, so
648
+ // acknowledge instead of failing the delivery for days.
649
+ console.info(
650
+ "[api] stripe webhook ignored a checkout session for an account not in this deployment",
651
+ {
652
+ stripeEventType: source.stripeEventType ?? null,
653
+ livemode: source.livemode,
654
+ },
655
+ );
354
656
  return;
355
657
  }
356
- const credit = creditMetadata(session.metadata, `Stripe checkout session ${session.id}`);
357
658
  const customerId = typeof session.customer === "string" ? session.customer : session.customer?.id;
358
659
  if (customerId) {
359
660
  await upsertBillingCustomer(deps.db, {
360
661
  accountId: credit.accountId,
361
- provider: stripeCustomerProvider(event),
662
+ provider: source.livemode ? "stripe:live" : "stripe:test",
362
663
  providerCustomerId: customerId,
363
664
  email: session.customer_details?.email ?? session.customer_email ?? null,
364
665
  });
365
666
  }
366
- await applyCreditLedgerEntry(deps.db, {
667
+ if (await hasCreditLedgerEntry(deps.db, credit.accountId, credit.idempotencyKey)) {
668
+ return;
669
+ }
670
+ const { inserted } = await applyCreditLedgerEntryOnce(deps.db, {
367
671
  accountId: credit.accountId,
368
- type: "credit_topup",
672
+ type: freeCouponCheckout ? "grant" : "credit_topup",
369
673
  amountMicros: credit.amountMicros,
370
- sourceType: "stripe_checkout_session",
674
+ eligibleModelIds: promotionalScope?.eligibleModelIds,
675
+ sourceType: freeCouponCheckout ? "stripe_checkout_coupon" : "stripe_checkout_session",
371
676
  sourceId: session.id,
372
677
  idempotencyKey: credit.idempotencyKey,
373
678
  metadata: {
374
- stripeEventId: event.id,
679
+ ...(promotionalScope ? { creditOfferLabel: promotionalScope.label } : {}),
680
+ creditOfferId: session.metadata?.opengeni_credit_offer_id ?? null,
681
+ stripeEventId: source.stripeEventId,
375
682
  stripePaymentIntentId:
376
683
  typeof session.payment_intent === "string"
377
684
  ? session.payment_intent
378
685
  : (session.payment_intent?.id ?? null),
379
686
  stripePackageId: credit.packageId,
380
687
  stripeCreditAmountUsd: credit.amountUsd,
688
+ ...(session.metadata?.opengeni_credit_coupon_v1 === "1"
689
+ ? {
690
+ stripeAmountTotalCents: session.amount_total,
691
+ stripeDiscountCents: session.total_details?.amount_discount ?? 0,
692
+ }
693
+ : {}),
381
694
  },
382
695
  });
383
- recordCreditMicrosMetric(deps, "topup", credit.amountMicros);
696
+ // Two deliveries (webhook and the late-webhook status read) may race past
697
+ // the existence check; only the call that inserted the row counts.
698
+ if (!inserted) return;
699
+ recordCreditMicrosMetric(deps, freeCouponCheckout ? "grant" : "topup", credit.amountMicros);
700
+ if (!freeCouponCheckout) {
701
+ recordCreditPurchaseMetrics(deps.observability, {
702
+ livemode: source.livemode,
703
+ creditMicros: credit.amountMicros,
704
+ currency: session.currency,
705
+ amountTotal: session.amount_total,
706
+ });
707
+ }
708
+ }
709
+
710
+ /**
711
+ * One paid credit purchase (a `credit_topup` ledger row from a Stripe
712
+ * checkout). Fully discounted coupon checkouts are grants, not purchases, and
713
+ * are counted by `opengeni_credit_granted_micros_total{grant_class="coupon"}`.
714
+ * Labels are the closed `mode` (live | test) only; never an account or
715
+ * customer identifier.
716
+ */
717
+ export function recordCreditPurchaseMetrics(
718
+ observability: ApiRouteDeps["observability"],
719
+ input: {
720
+ livemode: boolean;
721
+ creditMicros: number;
722
+ currency: string | null | undefined;
723
+ amountTotal: number | null | undefined;
724
+ },
725
+ ): void {
726
+ if (!observability || !Number.isSafeInteger(input.creditMicros) || input.creditMicros <= 0) {
727
+ return;
728
+ }
729
+ const labels = { mode: input.livemode ? "live" : "test" };
730
+ observability.incrementCounter({
731
+ name: "opengeni_credit_purchases_total",
732
+ help: "Paid credit purchases (Stripe checkout credit top-ups) by mode.",
733
+ labels,
734
+ });
735
+ observability.incrementCounter({
736
+ name: "opengeni_credit_purchased_micros_total",
737
+ help: "Credit micros added by paid credit purchases, by mode.",
738
+ labels,
739
+ amount: input.creditMicros,
740
+ });
741
+ // Stripe amounts are in the currency's minor unit; credit checkouts are USD,
742
+ // where one cent is 10,000 micros. Other currencies are not converted.
743
+ if (
744
+ input.currency === "usd" &&
745
+ typeof input.amountTotal === "number" &&
746
+ Number.isSafeInteger(input.amountTotal) &&
747
+ input.amountTotal > 0
748
+ ) {
749
+ observability.incrementCounter({
750
+ name: "opengeni_credit_purchase_paid_usd_micros_total",
751
+ help: "USD micros customers paid for credit purchases (after discounts), by mode.",
752
+ labels,
753
+ amount: input.amountTotal * 10_000,
754
+ });
755
+ }
756
+ }
757
+
758
+ /**
759
+ * Looks up a customer-typed promotion code. Stripe matches codes without
760
+ * regard to case. A fixed USD amount off sets the credits a checkout buys.
761
+ */
762
+ async function resolveCheckoutPromotionCode(
763
+ stripe: Stripe,
764
+ code: string,
765
+ ): Promise<{
766
+ id: string;
767
+ couponId: string;
768
+ amountOffCents: number | null;
769
+ percentOff: number | null;
770
+ }> {
771
+ const listed = await stripe.promotionCodes.list({
772
+ code,
773
+ active: true,
774
+ limit: 1,
775
+ expand: ["data.promotion.coupon"],
776
+ });
777
+ const promotionCode = listed.data[0];
778
+ const coupon =
779
+ promotionCode && typeof promotionCode.promotion?.coupon === "object"
780
+ ? promotionCode.promotion.coupon
781
+ : null;
782
+ if (!promotionCode || !coupon?.valid) {
783
+ throw new HTTPException(422, { message: "That code isn't valid or has expired." });
784
+ }
785
+ const amountOffCents =
786
+ coupon.amount_off && coupon.currency === "usd"
787
+ ? coupon.amount_off
788
+ : (coupon.currency_options?.usd?.amount_off ?? null);
789
+ return {
790
+ id: promotionCode.id,
791
+ couponId: coupon.id,
792
+ amountOffCents,
793
+ percentOff: coupon.percent_off ?? null,
794
+ };
384
795
  }
385
796
 
386
797
  async function mirrorPaymentIntentCustomer(deps: ApiRouteDeps, event: Stripe.Event): Promise<void> {
@@ -433,19 +844,21 @@ async function applyRefundDebit(
433
844
  if (await hasCreditLedgerEntry(deps.db, accountId, idempotencyKey)) {
434
845
  return;
435
846
  }
847
+ const debitMicros = await creditAdjustmentMicros(deps, metadata, refund.amount);
436
848
  await applyCreditLedgerEntry(deps.db, {
437
849
  accountId,
438
850
  type: "credit_refund",
439
- amountMicros: -centsToMicros(refund.amount),
851
+ amountMicros: -debitMicros,
440
852
  sourceType: "stripe_refund",
441
853
  sourceId: refund.id,
442
854
  idempotencyKey,
443
855
  metadata: {
444
856
  stripeRefundId: refund.id,
445
857
  stripePaymentIntentId: paymentIntentId(refund.payment_intent),
858
+ stripeRefundAmountCents: refund.amount,
446
859
  },
447
860
  });
448
- recordCreditMicrosMetric(deps, "refund", centsToMicros(refund.amount));
861
+ recordCreditMicrosMetric(deps, "refund", debitMicros);
449
862
  }
450
863
 
451
864
  async function holdDisputedCredits(
@@ -459,10 +872,11 @@ async function holdDisputedCredits(
459
872
  if (!accountId) {
460
873
  return;
461
874
  }
875
+ const debitMicros = await creditAdjustmentMicros(deps, metadata, dispute.amount);
462
876
  await applyCreditLedgerEntry(deps.db, {
463
877
  accountId,
464
878
  type: "credit_dispute_hold",
465
- amountMicros: -centsToMicros(dispute.amount),
879
+ amountMicros: -debitMicros,
466
880
  sourceType: "stripe_dispute",
467
881
  sourceId: dispute.id,
468
882
  idempotencyKey: `stripe:dispute_hold:${dispute.id}`,
@@ -485,20 +899,59 @@ async function releaseDisputedCredits(
485
899
  return;
486
900
  }
487
901
  const holdIdempotencyKey = `stripe:dispute_hold:${dispute.id}`;
488
- if (!(await hasCreditLedgerEntry(deps.db, accountId, holdIdempotencyKey))) {
489
- return;
490
- }
491
- await applyCreditLedgerEntry(deps.db, {
492
- accountId,
493
- type: "credit_dispute_release",
494
- amountMicros: centsToMicros(dispute.amount),
495
- sourceType: "stripe_dispute",
496
- sourceId: dispute.id,
497
- idempotencyKey: `stripe:dispute_release:${dispute.id}`,
498
- metadata: { stripeDisputeId: dispute.id, stripeEventType: event.type },
902
+ const debitMicros = await creditAdjustmentMicros(deps, metadata, dispute.amount);
903
+ await withRlsContext(deps.db, { accountId }, async (tx) => {
904
+ // Stripe can deliver the resolution before the hold. Materialize both
905
+ // receipts atomically so a late hold cannot withhold restored credits.
906
+ await applyCreditLedgerEntry(tx, {
907
+ accountId,
908
+ type: "credit_dispute_hold",
909
+ amountMicros: -debitMicros,
910
+ sourceType: "stripe_dispute",
911
+ sourceId: dispute.id,
912
+ idempotencyKey: holdIdempotencyKey,
913
+ metadata: { stripeDisputeId: dispute.id, stripeEventType: event.type },
914
+ });
915
+ await applyCreditLedgerEntry(tx, {
916
+ accountId,
917
+ type: "credit_dispute_release",
918
+ amountMicros: debitMicros,
919
+ sourceType: "stripe_dispute",
920
+ sourceId: dispute.id,
921
+ idempotencyKey: `stripe:dispute_release:${dispute.id}`,
922
+ metadata: { stripeDisputeId: dispute.id, stripeEventType: event.type },
923
+ });
499
924
  });
500
925
  }
501
926
 
927
+ async function creditAdjustmentMicros(
928
+ deps: ApiRouteDeps,
929
+ metadata: Stripe.Metadata | null,
930
+ amountCents: number,
931
+ ): Promise<number> {
932
+ if (metadata?.opengeni_credit_coupon_v1 !== "1") return centsToMicros(amountCents);
933
+ const credit = creditMetadata(metadata, "Stripe adjustment");
934
+ const entry = await getCreditLedgerEntry(deps.db, credit.accountId, credit.idempotencyKey);
935
+ const paidCents = entry?.metadata.stripeAmountTotalCents;
936
+ if (
937
+ entry?.sourceType !== "stripe_checkout_session" ||
938
+ entry.amountMicros !== credit.amountMicros ||
939
+ typeof paidCents !== "number" ||
940
+ !Number.isSafeInteger(paidCents) ||
941
+ paidCents <= 0 ||
942
+ !Number.isSafeInteger(amountCents) ||
943
+ amountCents <= 0
944
+ ) {
945
+ throw new Error("Stripe credit adjustment is missing a settled checkout");
946
+ }
947
+ const proportionalMicros =
948
+ (BigInt(entry.amountMicros) * BigInt(amountCents) + BigInt(Math.floor(paidCents / 2))) /
949
+ BigInt(paidCents);
950
+ return Number(
951
+ proportionalMicros > BigInt(entry.amountMicros) ? entry.amountMicros : proportionalMicros,
952
+ );
953
+ }
954
+
502
955
  async function mirrorCustomer(
503
956
  deps: ApiRouteDeps,
504
957
  event: Stripe.Event,
@@ -536,18 +989,20 @@ async function metadataForRefund(
536
989
  stripe: Stripe,
537
990
  refund: Stripe.Refund,
538
991
  ): Promise<Stripe.Metadata | null> {
539
- if (Object.keys(refund.metadata ?? {}).length > 0) {
992
+ if (refund.metadata?.opengeni_credit_idempotency_key) {
540
993
  return refund.metadata;
541
994
  }
542
995
  const paymentIntent = paymentIntentId(refund.payment_intent);
543
- return paymentIntent ? (await stripe.paymentIntents.retrieve(paymentIntent)).metadata : null;
996
+ return paymentIntent
997
+ ? (await stripe.paymentIntents.retrieve(paymentIntent)).metadata
998
+ : refund.metadata;
544
999
  }
545
1000
 
546
1001
  async function metadataForDispute(
547
1002
  stripe: Stripe,
548
1003
  dispute: Stripe.Dispute,
549
1004
  ): Promise<Stripe.Metadata | null> {
550
- if (Object.keys(dispute.metadata ?? {}).length > 0) {
1005
+ if (dispute.metadata?.opengeni_credit_idempotency_key) {
551
1006
  return dispute.metadata;
552
1007
  }
553
1008
  const paymentIntent = paymentIntentId(
@@ -559,7 +1014,7 @@ async function metadataForDispute(
559
1014
  }
560
1015
  const chargeId = typeof dispute.charge === "string" ? dispute.charge : dispute.charge?.id;
561
1016
  if (!chargeId) {
562
- return null;
1017
+ return dispute.metadata;
563
1018
  }
564
1019
  const charge = await stripe.charges.retrieve(chargeId);
565
1020
  const chargePaymentIntent = paymentIntentId(charge.payment_intent);
@@ -568,21 +1023,23 @@ async function metadataForDispute(
568
1023
  : charge.metadata;
569
1024
  }
570
1025
 
571
- function creditMetadata(
572
- metadata: Stripe.Metadata | null | undefined,
573
- label: string,
574
- ): {
1026
+ export type CheckoutCreditMetadata = {
575
1027
  accountId: string;
576
1028
  amountMicros: number;
577
1029
  idempotencyKey: string;
578
1030
  amountUsd?: string;
579
1031
  packageId?: string;
580
- } {
1032
+ };
1033
+
1034
+ function creditMetadata(
1035
+ metadata: Stripe.Metadata | null | undefined,
1036
+ label: string,
1037
+ ): CheckoutCreditMetadata {
581
1038
  const accountId = metadata?.opengeni_account_id;
582
1039
  const amountMicros = Number(metadata?.opengeni_credit_micros);
583
1040
  const idempotencyKey = metadata?.opengeni_credit_idempotency_key;
584
1041
  if (!accountId || !Number.isSafeInteger(amountMicros) || amountMicros <= 0 || !idempotencyKey) {
585
- throw new Error(`${label} is missing OpenGeni credit metadata`);
1042
+ throw new Error(`${label} is missing Opengeni credit metadata`);
586
1043
  }
587
1044
  return {
588
1045
  accountId,
@@ -638,7 +1095,26 @@ export function stripeCustomerProvider(
638
1095
  : "stripe:test";
639
1096
  }
640
1097
 
641
- function requireSelectedAccount(
1098
+ /** Billing routes do not traverse the workspace actor middleware. Always bind
1099
+ * these reads explicitly; only a revalidated live attempt may supply a human
1100
+ * initiator. Neither query parameters nor serviceInitiator claims are proof. */
1101
+ export async function withBillingUsageActor<T>(
1102
+ deps: ApiRouteDeps,
1103
+ context: AccessContext,
1104
+ accountId: string,
1105
+ read: () => Promise<T>,
1106
+ ): Promise<T> {
1107
+ const attemptGrant = context.workspaceGrants.find(
1108
+ (grant) =>
1109
+ grant.accountId === accountId &&
1110
+ grant.subjectId === context.subjectId &&
1111
+ grant.principalKind === "agent_attempt",
1112
+ );
1113
+ if (attemptGrant) return await withAccessGrantSessionRlsContext(deps, attemptGrant, read);
1114
+ return await withSessionRlsActorContext({ subjectId: context.subjectId }, read);
1115
+ }
1116
+
1117
+ export function requireSelectedAccount(
642
1118
  context: AccessContext,
643
1119
  requested: string | undefined,
644
1120
  permission: Permission,