@opengeni/api-router 0.30.3 → 1.0.0

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 (434) hide show
  1. package/dist/access-grant-rls.d.ts +3 -0
  2. package/dist/admission-trace.d.ts +8 -0
  3. package/dist/api-websocket.d.ts +18 -0
  4. package/dist/app.d.ts +50 -2
  5. package/dist/app.js +12 -1
  6. package/dist/artifact-catalog-cursor.d.ts +27 -0
  7. package/dist/auth/managed-auth-attempt-context.d.ts +10 -0
  8. package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
  9. package/dist/auth/managed-auth-session-adapter.d.ts +6 -0
  10. package/dist/auth/managed-auth.d.ts +102 -3
  11. package/dist/auth/managed-email.d.ts +29 -0
  12. package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
  13. package/dist/auth/new-signups-gate.d.ts +23 -0
  14. package/dist/auth/organization-user-setup.d.ts +80 -0
  15. package/dist/auth/signup-funnel-metrics.d.ts +44 -0
  16. package/dist/browser-controller-authority.d.ts +1 -61
  17. package/dist/browser-ephemeral.d.ts +5 -0
  18. package/dist/browser-state-authority.d.ts +1 -27
  19. package/dist/chunk-6TZ5LG3O.js +123948 -0
  20. package/dist/chunk-6TZ5LG3O.js.map +1 -0
  21. package/dist/chunk-OGQDXVXY.js +18 -0
  22. package/dist/chunk-OGQDXVXY.js.map +1 -0
  23. package/dist/claude-subscription-account-conflicts.d.ts +1 -0
  24. package/dist/claude-subscription-account-usage.d.ts +39 -0
  25. package/dist/claude-subscription-oauth.d.ts +25 -0
  26. package/dist/claude-subscription-profile.d.ts +9 -0
  27. package/dist/claude-subscription-usage.d.ts +44 -0
  28. package/dist/claude-workspace-connection.d.ts +15 -0
  29. package/dist/codemode.d.ts +32 -1
  30. package/dist/company-brain-okf.d.ts +24 -0
  31. package/dist/connected-machine-computer-access.d.ts +11 -0
  32. package/dist/connection-ownership.d.ts +54 -0
  33. package/dist/controller-data-plane.d.ts +32 -0
  34. package/dist/editable-artifact-source-session.d.ts +3 -0
  35. package/dist/editable-artifact-websocket.d.ts +10 -18
  36. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  37. package/dist/fatal-process-boundary.d.ts +25 -0
  38. package/dist/github-access.d.ts +31 -2
  39. package/dist/http/acting-person.d.ts +24 -0
  40. package/dist/http/api-contract-compat.d.ts +19 -0
  41. package/dist/http/api-error.d.ts +27 -1
  42. package/dist/http/cors.d.ts +17 -0
  43. package/dist/http/deprecation.d.ts +42 -0
  44. package/dist/http/interaction-control-error.d.ts +13 -0
  45. package/dist/http/keyed-admission.d.ts +12 -0
  46. package/dist/http/local-browser-boundary.d.ts +72 -0
  47. package/dist/http/metrics-listener.d.ts +22 -0
  48. package/dist/http/path-identifier.d.ts +17 -0
  49. package/dist/http/registered-route-label.d.ts +16 -0
  50. package/dist/http/request-body.d.ts +27 -0
  51. package/dist/http/request-source.d.ts +52 -0
  52. package/dist/http/sse.d.ts +17 -6
  53. package/dist/http/unmatched-route.d.ts +18 -0
  54. package/dist/http/user-content.d.ts +36 -0
  55. package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
  56. package/dist/index.d.ts +10 -3
  57. package/dist/index.js +1091 -162
  58. package/dist/index.js.map +1 -1
  59. package/dist/integration-endpoint-test.d.ts +20 -0
  60. package/dist/integrations/api-integrations.d.ts +2 -2
  61. package/dist/integrations/atlassian.d.ts +29 -3
  62. package/dist/integrations/connect-authority.d.ts +1 -0
  63. package/dist/integrations/connect-callback-return.d.ts +6 -0
  64. package/dist/integrations/fiken.d.ts +16 -2
  65. package/dist/integrations/github-app-connect.d.ts +47 -0
  66. package/dist/integrations/github-installation-proof.d.ts +3 -0
  67. package/dist/integrations/github-lens-connect.d.ts +116 -0
  68. package/dist/integrations/github-skill-source.d.ts +1 -5
  69. package/dist/integrations/google-drive.d.ts +24 -0
  70. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  71. package/dist/integrations/marketplace-plugin.d.ts +2691 -0
  72. package/dist/integrations/oauth-client.d.ts +178 -18
  73. package/dist/integrations/oauth-profiles.d.ts +222 -0
  74. package/dist/integrations/oauth-return-path.d.ts +1 -0
  75. package/dist/integrations/personal-github-repositories.d.ts +84 -0
  76. package/dist/integrations/personal-github.d.ts +42 -0
  77. package/dist/integrations/pr-review-provider.d.ts +22 -0
  78. package/dist/integrations/provider-oauth.d.ts +19 -1
  79. package/dist/integrations/slack-app-home.d.ts +24 -0
  80. package/dist/integrations/slack-bot.d.ts +367 -12
  81. package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
  82. package/dist/integrations/slack-install.d.ts +20 -0
  83. package/dist/integrations/slack-interactions.d.ts +107 -9
  84. package/dist/integrations/slack-mrkdwn.d.ts +41 -0
  85. package/dist/integrations/slack-routing.d.ts +120 -0
  86. package/dist/integrations/slack-session-defaults.d.ts +58 -0
  87. package/dist/integrations/slack-task-file-upload.d.ts +36 -0
  88. package/dist/integrations/social-oauth.d.ts +7 -0
  89. package/dist/interaction-agent-access.d.ts +15 -0
  90. package/dist/interaction-frame-proxy.d.ts +68 -0
  91. package/dist/interaction-metrics.d.ts +2 -0
  92. package/dist/mcp/action-catalog-types.d.ts +9 -0
  93. package/dist/mcp/action-catalog.gen.d.ts +2 -0
  94. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  95. package/dist/mcp/child-read-evidence.d.ts +9 -0
  96. package/dist/mcp/command-read.d.ts +24 -0
  97. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  98. package/dist/mcp/contract-input.d.ts +11 -0
  99. package/dist/mcp/documents.d.ts +7 -5
  100. package/dist/mcp/files.d.ts +2 -2
  101. package/dist/mcp/first-party-tool-permissions.d.ts +650 -0
  102. package/dist/mcp/knowledge-entries.d.ts +5 -0
  103. package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
  104. package/dist/mcp/receipts.d.ts +9 -0
  105. package/dist/mcp/request-abort.d.ts +19 -0
  106. package/dist/mcp/scheduled-task-input.d.ts +1189 -0
  107. package/dist/mcp/scheduled-task-view.d.ts +22 -4
  108. package/dist/mcp/server.d.ts +255 -3
  109. package/dist/mcp/session-event-view.d.ts +94 -0
  110. package/dist/mcp/session-view.d.ts +66 -2
  111. package/dist/mcp/session-wait.d.ts +174 -0
  112. package/dist/mcp-oauth.d.ts +52 -0
  113. package/dist/model-catalog.d.ts +11 -31
  114. package/dist/observability.d.ts +3 -1
  115. package/dist/organization-mcp.d.ts +52 -0
  116. package/dist/organization-recovery-notifications.d.ts +41 -0
  117. package/dist/routes/analytics-consent.d.ts +26 -0
  118. package/dist/routes/api-integrations.d.ts +25 -0
  119. package/dist/routes/api-keys.d.ts +17 -0
  120. package/dist/routes/artifact-catalog.d.ts +6 -0
  121. package/dist/routes/automations.d.ts +19 -0
  122. package/dist/routes/billing.d.ts +49 -0
  123. package/dist/routes/browser-sessions.d.ts +59 -2
  124. package/dist/routes/claude-subscription-accounts.d.ts +3 -0
  125. package/dist/routes/claude-subscription-oauth.d.ts +3 -0
  126. package/dist/routes/client-errors.d.ts +146 -0
  127. package/dist/routes/codex.d.ts +76 -2
  128. package/dist/routes/company-brain.d.ts +3 -0
  129. package/dist/routes/company-profile.d.ts +2 -1
  130. package/dist/routes/computer-sessions.d.ts +14 -0
  131. package/dist/routes/connect.d.ts +5 -0
  132. package/dist/routes/editable-artifacts.d.ts +2 -0
  133. package/dist/routes/external-identity-links.d.ts +6 -0
  134. package/dist/routes/feedback.d.ts +3 -0
  135. package/dist/routes/github-browser-pages.d.ts +12 -0
  136. package/dist/routes/github.d.ts +2 -0
  137. package/dist/routes/insights-response-cache.d.ts +10 -0
  138. package/dist/routes/insights-usage.d.ts +12 -0
  139. package/dist/routes/insights.d.ts +30 -1
  140. package/dist/routes/knowledge.d.ts +3 -0
  141. package/dist/routes/managed-auth-session-sets.d.ts +26 -0
  142. package/dist/routes/managed-onboarding.d.ts +29 -0
  143. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  144. package/dist/routes/model-connection-access.d.ts +3 -0
  145. package/dist/routes/organization-integration-policy.d.ts +3 -0
  146. package/dist/routes/organization-integrations.d.ts +6 -0
  147. package/dist/routes/organization-mcp-connections.d.ts +3 -0
  148. package/dist/routes/organization-model-providers.d.ts +3 -0
  149. package/dist/routes/organization-recovery.d.ts +19 -0
  150. package/dist/routes/organization-service-accounts.d.ts +3 -0
  151. package/dist/routes/organization-sessions.d.ts +26 -0
  152. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  153. package/dist/routes/personal-github.d.ts +3 -0
  154. package/dist/routes/pr-review-github.d.ts +3 -0
  155. package/dist/routes/pr-review.d.ts +3 -0
  156. package/dist/routes/session-artifact-associations.d.ts +4 -0
  157. package/dist/routes/session-history-imports.d.ts +9 -0
  158. package/dist/routes/sessions.d.ts +44 -6
  159. package/dist/routes/skill-content.d.ts +4 -0
  160. package/dist/routes/skill-install-authority.d.ts +6 -0
  161. package/dist/routes/subscription-account-pools.d.ts +34 -0
  162. package/dist/routes/subscription-pool-access.d.ts +16 -0
  163. package/dist/routes/supergrok.d.ts +6 -1
  164. package/dist/routes/usage-allowances.d.ts +12 -0
  165. package/dist/routes/user-resource-authorities.d.ts +3 -0
  166. package/dist/routes/workspace-artifacts.d.ts +2 -1
  167. package/dist/routes/workspace-capture.d.ts +10 -2
  168. package/dist/routes/workspace-integrations.d.ts +35 -0
  169. package/dist/routes/workspace-learning.d.ts +3 -0
  170. package/dist/routes/workspace-model-providers.d.ts +4 -0
  171. package/dist/routes/workspaces.d.ts +29 -0
  172. package/dist/sandbox/auth-callout.d.ts +8 -0
  173. package/dist/sandbox/channel-a.d.ts +19 -1
  174. package/dist/sandbox/connection-authority.d.ts +3 -0
  175. package/dist/sandbox/enrollment.d.ts +9 -1
  176. package/dist/sandbox/machines.d.ts +5 -2
  177. package/dist/sandbox/metrics-ingestion.d.ts +28 -20
  178. package/dist/sandbox/rematerialize.d.ts +2 -0
  179. package/dist/sandbox/session-attach-variable-sets.d.ts +30 -0
  180. package/dist/sandbox/viewer.d.ts +26 -1
  181. package/dist/sandbox-file-artifacts.d.ts +50 -0
  182. package/dist/scheduled-task-deletion.d.ts +13 -0
  183. package/dist/session-create-observability.d.ts +5 -0
  184. package/dist/session-event-view-MPS2OZD3.js +292 -0
  185. package/dist/session-event-view-MPS2OZD3.js.map +1 -0
  186. package/dist/site-session-origin.d.ts +13 -0
  187. package/dist/site-uploads.d.ts +41 -0
  188. package/dist/slack-reaction-files.d.ts +1 -1
  189. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  190. package/dist/temporal-schedule-sync.d.ts +26 -0
  191. package/dist/transcription/providers/xai-subscription.d.ts +1 -0
  192. package/dist/transcription/service.d.ts +2 -1
  193. package/dist/work-discovery-observability.d.ts +33 -0
  194. package/dist/workspace-artifact-content.d.ts +28 -0
  195. package/dist/workspace-artifact-provenance.d.ts +7 -0
  196. package/dist/workspace-delete-observability.d.ts +10 -0
  197. package/dist/workspace-deletion.d.ts +6 -0
  198. package/dist/workspace-state-projection.d.ts +3 -5
  199. package/dist/workspace-tool-gateway-observability.d.ts +17 -0
  200. package/dist/workspace-tool-gateway.d.ts +123 -0
  201. package/dist/workspace-webhook-dispatch.d.ts +26 -0
  202. package/package.json +23 -19
  203. package/src/access-grant-rls.ts +75 -0
  204. package/src/admission-trace.ts +14 -0
  205. package/src/api-websocket.ts +23 -0
  206. package/src/app.ts +1967 -252
  207. package/src/artifact-catalog-cursor.ts +60 -0
  208. package/src/auth/managed-auth-attempt-context.ts +70 -0
  209. package/src/auth/managed-auth-rate-limits.ts +252 -0
  210. package/src/auth/managed-auth-session-adapter.ts +222 -0
  211. package/src/auth/managed-auth.ts +536 -60
  212. package/src/auth/managed-email.ts +174 -0
  213. package/src/auth/managed-sign-in-notifications.ts +81 -0
  214. package/src/auth/new-signups-gate.ts +49 -0
  215. package/src/auth/organization-user-setup.ts +307 -0
  216. package/src/auth/signup-funnel-metrics.ts +164 -0
  217. package/src/browser-controller-authority.ts +1 -186
  218. package/src/browser-ephemeral.ts +39 -0
  219. package/src/browser-state-authority.ts +1 -236
  220. package/src/claude-subscription-account-conflicts.ts +12 -0
  221. package/src/claude-subscription-account-usage.ts +48 -0
  222. package/src/claude-subscription-oauth.ts +362 -0
  223. package/src/claude-subscription-profile.ts +73 -0
  224. package/src/claude-subscription-usage.ts +97 -0
  225. package/src/claude-workspace-connection.ts +82 -0
  226. package/src/codemode.ts +194 -30
  227. package/src/codex-realtime.ts +8 -2
  228. package/src/company-brain-okf.ts +340 -0
  229. package/src/connected-machine-computer-access.ts +33 -0
  230. package/src/connection-ownership.ts +199 -0
  231. package/src/controller-data-plane.ts +81 -0
  232. package/src/editable-artifact-native-kernel.ts +26 -15
  233. package/src/editable-artifact-office-import.ts +28 -3
  234. package/src/editable-artifact-production.ts +23 -7
  235. package/src/editable-artifact-source-session.ts +123 -0
  236. package/src/editable-artifact-websocket.ts +27 -21
  237. package/src/editable-artifact-workspace-files.ts +31 -6
  238. package/src/fatal-process-boundary.ts +246 -0
  239. package/src/gateway-realtime.ts +1 -1
  240. package/src/github-access.ts +147 -5
  241. package/src/http/acting-person.ts +71 -0
  242. package/src/http/api-contract-compat.ts +49 -0
  243. package/src/http/api-error.ts +115 -1
  244. package/src/http/auth.ts +48 -0
  245. package/src/http/cors.ts +67 -0
  246. package/src/http/deprecation.ts +118 -0
  247. package/src/http/interaction-control-error.ts +164 -0
  248. package/src/http/keyed-admission.ts +29 -0
  249. package/src/http/local-browser-boundary.ts +343 -0
  250. package/src/http/metrics-listener.ts +57 -0
  251. package/src/http/path-identifier.ts +54 -0
  252. package/src/http/registered-route-label.ts +34 -0
  253. package/src/http/request-body.ts +127 -0
  254. package/src/http/request-source.ts +178 -0
  255. package/src/http/sse.ts +486 -132
  256. package/src/http/unmatched-route.ts +60 -0
  257. package/src/http/user-content.ts +158 -0
  258. package/src/http/websocket-upgrade-dispatch.ts +29 -0
  259. package/src/index.ts +157 -52
  260. package/src/integration-endpoint-test.ts +291 -0
  261. package/src/integrations/api-integrations.ts +98 -11
  262. package/src/integrations/atlassian.ts +474 -136
  263. package/src/integrations/connect-authority.ts +2 -0
  264. package/src/integrations/connect-callback-return.ts +86 -0
  265. package/src/integrations/fiken.ts +354 -64
  266. package/src/integrations/github-app-connect.ts +435 -0
  267. package/src/integrations/github-installation-proof.ts +60 -0
  268. package/src/integrations/github-lens-connect.ts +76 -0
  269. package/src/integrations/github-skill-source.ts +2 -142
  270. package/src/integrations/google-drive.ts +717 -225
  271. package/src/integrations/integration-commit-authority.ts +69 -0
  272. package/src/integrations/marketplace-plugin.ts +48 -0
  273. package/src/integrations/oauth-client.ts +1356 -459
  274. package/src/integrations/oauth-profiles.ts +537 -0
  275. package/src/integrations/oauth-return-path.ts +18 -0
  276. package/src/integrations/personal-github-repositories.ts +674 -0
  277. package/src/integrations/personal-github.ts +825 -0
  278. package/src/integrations/pr-review-provider.ts +246 -0
  279. package/src/integrations/provider-oauth.ts +250 -22
  280. package/src/integrations/slack-app-home.ts +300 -0
  281. package/src/integrations/slack-bot.ts +963 -239
  282. package/src/integrations/slack-file-upload-flow.ts +147 -0
  283. package/src/integrations/slack-install.ts +83 -0
  284. package/src/integrations/slack-interactions.ts +3167 -474
  285. package/src/integrations/slack-mrkdwn.ts +193 -0
  286. package/src/integrations/slack-routing.ts +337 -0
  287. package/src/integrations/slack-session-defaults.ts +224 -0
  288. package/src/integrations/slack-task-file-upload.ts +323 -0
  289. package/src/integrations/social-oauth.ts +213 -15
  290. package/src/interaction-agent-access.ts +48 -0
  291. package/src/interaction-frame-proxy.ts +409 -0
  292. package/src/interaction-metrics.ts +30 -0
  293. package/src/mcp/action-catalog-types.ts +9 -0
  294. package/src/mcp/action-catalog.gen.ts +6696 -0
  295. package/src/mcp/capability-account-readiness.ts +17 -0
  296. package/src/mcp/child-read-evidence.ts +19 -0
  297. package/src/mcp/command-read.ts +109 -0
  298. package/src/mcp/company-profile-agent-admin.ts +198 -0
  299. package/src/mcp/contract-input.ts +141 -0
  300. package/src/mcp/documents.ts +65 -377
  301. package/src/mcp/editable-artifacts.ts +12 -6
  302. package/src/mcp/files.ts +80 -43
  303. package/src/mcp/first-party-tool-permissions.ts +305 -0
  304. package/src/mcp/knowledge-entries.ts +322 -0
  305. package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
  306. package/src/mcp/receipts.ts +34 -0
  307. package/src/mcp/request-abort.ts +44 -0
  308. package/src/mcp/scheduled-task-input.ts +76 -0
  309. package/src/mcp/scheduled-task-view.ts +44 -3
  310. package/src/mcp/server.ts +3030 -1267
  311. package/src/mcp/session-event-view.ts +383 -0
  312. package/src/mcp/session-view.ts +84 -3
  313. package/src/mcp/session-wait.ts +665 -0
  314. package/src/mcp-oauth.ts +1123 -0
  315. package/src/model-catalog.ts +91 -333
  316. package/src/observability.ts +15 -1
  317. package/src/organization-mcp.ts +397 -0
  318. package/src/organization-recovery-notifications.ts +103 -0
  319. package/src/routes/analytics-consent.ts +123 -0
  320. package/src/routes/api-integrations.ts +364 -74
  321. package/src/routes/api-keys.ts +336 -10
  322. package/src/routes/artifact-catalog.ts +279 -0
  323. package/src/routes/automations.ts +768 -0
  324. package/src/routes/billing.ts +461 -46
  325. package/src/routes/browser-sessions.ts +780 -112
  326. package/src/routes/canonical-human-identities.ts +90 -14
  327. package/src/routes/capabilities.ts +107 -6
  328. package/src/routes/channels.ts +20 -3
  329. package/src/routes/claude-subscription-accounts.ts +334 -0
  330. package/src/routes/claude-subscription-oauth.ts +142 -0
  331. package/src/routes/client-errors.ts +362 -0
  332. package/src/routes/codex.ts +833 -116
  333. package/src/routes/company-brain.ts +322 -0
  334. package/src/routes/company-profile.ts +65 -1
  335. package/src/routes/computer-sessions.ts +557 -106
  336. package/src/routes/connect.ts +1938 -0
  337. package/src/routes/connections.ts +1039 -349
  338. package/src/routes/documents.ts +488 -287
  339. package/src/routes/editable-artifacts.ts +79 -9
  340. package/src/routes/enrollments.ts +135 -39
  341. package/src/routes/environments.ts +154 -58
  342. package/src/routes/external-identity-links.ts +201 -0
  343. package/src/routes/feedback.ts +113 -0
  344. package/src/routes/files.ts +267 -33
  345. package/src/routes/github-browser-pages.ts +162 -0
  346. package/src/routes/github.ts +446 -144
  347. package/src/routes/insights-response-cache.ts +83 -0
  348. package/src/routes/insights-usage.ts +345 -0
  349. package/src/routes/insights.ts +153 -19
  350. package/src/routes/install.ts +133 -38
  351. package/src/routes/integration-facets.ts +107 -33
  352. package/src/routes/interaction-resources.ts +30 -3
  353. package/src/routes/knowledge.ts +488 -0
  354. package/src/routes/machines.ts +313 -15
  355. package/src/routes/managed-auth-session-sets.ts +1218 -0
  356. package/src/routes/managed-onboarding.ts +337 -0
  357. package/src/routes/managed-sign-in-methods.ts +433 -0
  358. package/src/routes/memory-slack-publications.ts +3 -2
  359. package/src/routes/model-connection-access.ts +208 -0
  360. package/src/routes/organization-integration-policy.ts +116 -0
  361. package/src/routes/organization-integrations.ts +326 -0
  362. package/src/routes/organization-mcp-connections.ts +178 -0
  363. package/src/routes/organization-memberships.ts +1019 -27
  364. package/src/routes/organization-model-providers.ts +293 -0
  365. package/src/routes/organization-recovery.ts +325 -0
  366. package/src/routes/organization-service-accounts.ts +128 -0
  367. package/src/routes/organization-sessions.ts +350 -0
  368. package/src/routes/personal-github-git-broker.ts +785 -0
  369. package/src/routes/personal-github.ts +370 -0
  370. package/src/routes/plugins.ts +251 -22
  371. package/src/routes/pr-review-github.ts +881 -0
  372. package/src/routes/pr-review.ts +622 -0
  373. package/src/routes/preference-registry.ts +12 -93
  374. package/src/routes/rigs.ts +167 -50
  375. package/src/routes/scheduled-tasks.ts +346 -41
  376. package/src/routes/session-artifact-associations.ts +87 -0
  377. package/src/routes/session-history-imports.ts +280 -0
  378. package/src/routes/sessions.ts +2306 -264
  379. package/src/routes/skill-content.ts +297 -0
  380. package/src/routes/skill-install-authority.ts +64 -0
  381. package/src/routes/skills.ts +62 -9
  382. package/src/routes/social.ts +46 -18
  383. package/src/routes/subscription-account-pools.ts +436 -0
  384. package/src/routes/subscription-pool-access.ts +126 -0
  385. package/src/routes/supergrok.ts +219 -303
  386. package/src/routes/transcription-recordings.ts +38 -3
  387. package/src/routes/transcriptions.ts +4 -0
  388. package/src/routes/usage-allowances.ts +378 -0
  389. package/src/routes/user-resource-authorities.ts +138 -0
  390. package/src/routes/video-generation.ts +14 -10
  391. package/src/routes/workspace-artifacts.ts +275 -69
  392. package/src/routes/workspace-capture.ts +21 -0
  393. package/src/routes/workspace-integrations.ts +576 -0
  394. package/src/routes/workspace-learning.ts +159 -0
  395. package/src/routes/workspace-model-providers.ts +148 -0
  396. package/src/routes/workspace-state.ts +38 -25
  397. package/src/routes/workspaces.ts +1004 -127
  398. package/src/sandbox/auth-callout.ts +96 -19
  399. package/src/sandbox/channel-a.ts +323 -24
  400. package/src/sandbox/connection-authority.ts +3 -0
  401. package/src/sandbox/enrollment.ts +81 -6
  402. package/src/sandbox/machines.ts +218 -76
  403. package/src/sandbox/metrics-ingestion.ts +411 -62
  404. package/src/sandbox/rematerialize.ts +121 -19
  405. package/src/sandbox/session-attach-variable-sets.ts +131 -0
  406. package/src/sandbox/viewer.ts +280 -25
  407. package/src/sandbox-file-artifacts.ts +413 -0
  408. package/src/scheduled-task-deletion.ts +138 -0
  409. package/src/session-create-observability.ts +46 -0
  410. package/src/session-realtime-context.ts +6 -3
  411. package/src/site-session-origin.ts +48 -0
  412. package/src/site-uploads.ts +224 -0
  413. package/src/slack-reaction-files.ts +16 -5
  414. package/src/temporal-schedule-cleanup.ts +13 -0
  415. package/src/temporal-schedule-sync.ts +74 -0
  416. package/src/transcription/providers/codex-subscription.ts +2 -0
  417. package/src/transcription/providers/openai.ts +1 -0
  418. package/src/transcription/providers/xai-subscription.ts +49 -3
  419. package/src/transcription/segmenter.ts +8 -2
  420. package/src/transcription/service.ts +43 -6
  421. package/src/work-discovery-observability.ts +121 -0
  422. package/src/workspace-artifact-content.ts +144 -0
  423. package/src/workspace-artifact-provenance.ts +104 -0
  424. package/src/workspace-delete-observability.ts +75 -0
  425. package/src/workspace-deletion.ts +73 -0
  426. package/src/workspace-state-projection.ts +18 -148
  427. package/src/workspace-tool-gateway-observability.ts +100 -0
  428. package/src/workspace-tool-gateway.ts +870 -0
  429. package/src/workspace-webhook-dispatch.ts +190 -0
  430. package/src/xai-subscription-auth.ts +9 -2
  431. package/dist/chunk-WCXHI3FF.js +0 -49920
  432. package/dist/chunk-WCXHI3FF.js.map +0 -1
  433. package/dist/routes/packs.d.ts +0 -3
  434. package/src/routes/packs.ts +0 -729
@@ -1,17 +1,28 @@
1
1
  import {
2
+ BillingCheckoutStatus,
3
+ CreateBillingPortalRequest,
4
+ CreateBillingPortalResponse,
2
5
  CreateCheckoutRequest,
3
6
  CreateCheckoutResponse,
7
+ OrganizationUsageQuery,
8
+ OrganizationUsageWorkspacePageQuery,
4
9
  type AccessContext,
5
10
  type Permission,
6
11
  } from "@opengeni/contracts";
12
+ import { OrganizationModelUsageQuery } from "@opengeni/contracts/organization-model-usage";
7
13
  import { configuredEntitlements } from "@opengeni/config";
8
14
  import {
9
15
  applyCreditLedgerEntry,
10
16
  getBillingBalance,
11
17
  getBillingCustomer,
18
+ getCreditLedgerEntry,
12
19
  hasCreditLedgerEntry,
13
20
  isStripeWebhookProcessed,
14
21
  listUsageEvents,
22
+ getOrganizationModelUsage,
23
+ getOrganizationUsageSummary,
24
+ getOrganizationUsageWorkspacePage,
25
+ withSessionRlsActorContext,
15
26
  getManagedAccount,
16
27
  markStripeWebhookProcessed,
17
28
  recordStripeWebhookEvent,
@@ -22,6 +33,7 @@ import { HTTPException } from "hono/http-exception";
22
33
  import Stripe from "stripe";
23
34
  import { requireAccessContext } from "@opengeni/core";
24
35
  import type { ApiRouteDeps } from "@opengeni/core";
36
+ import { withAccessGrantSessionRlsContext } from "../access-grant-rls";
25
37
 
26
38
  export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
27
39
  app.get("/v1/billing", async (c) => {
@@ -55,6 +67,46 @@ export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
55
67
  });
56
68
  });
57
69
 
70
+ app.get("/v1/billing/usage-summary", async (c) => {
71
+ const context = await requireAccessContext(c, deps);
72
+ const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
73
+ const parsed = OrganizationUsageQuery.safeParse(c.req.query());
74
+ if (!parsed.success) {
75
+ throw new HTTPException(400, {
76
+ message: parsed.error.issues[0]?.message ?? "invalid usage query",
77
+ });
78
+ }
79
+ return await withBillingUsageActor(deps, context, accountId, async () =>
80
+ c.json(await getOrganizationUsageSummary(deps.db, { accountId, ...parsed.data })),
81
+ );
82
+ });
83
+
84
+ app.get("/v1/billing/usage-workspaces", async (c) => {
85
+ const context = await requireAccessContext(c, deps);
86
+ const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
87
+ const parsed = OrganizationUsageWorkspacePageQuery.safeParse(c.req.query());
88
+ if (!parsed.success)
89
+ throw new HTTPException(400, {
90
+ message: parsed.error.issues[0]?.message ?? "invalid usage page query",
91
+ });
92
+ return await withBillingUsageActor(deps, context, accountId, async () =>
93
+ c.json(await getOrganizationUsageWorkspacePage(deps.db, { accountId, ...parsed.data })),
94
+ );
95
+ });
96
+
97
+ app.get("/v1/billing/usage-models", async (c) => {
98
+ const context = await requireAccessContext(c, deps);
99
+ const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
100
+ const parsed = OrganizationModelUsageQuery.safeParse(c.req.query());
101
+ if (!parsed.success)
102
+ throw new HTTPException(400, {
103
+ message: parsed.error.issues[0]?.message ?? "invalid model usage query",
104
+ });
105
+ return await withBillingUsageActor(deps, context, accountId, async () =>
106
+ c.json(await getOrganizationModelUsage(deps.db, { accountId, ...parsed.data })),
107
+ );
108
+ });
109
+
58
110
  app.get("/v1/billing/entitlements", async (c) => {
59
111
  const context = await requireAccessContext(c, deps);
60
112
  const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
@@ -78,31 +130,154 @@ export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
78
130
  }
79
131
  const body = parsed.data;
80
132
  const accountId = requireSelectedAccount(context, body.accountId, "billing:manage");
81
- const amountCents = usdToCents(body.amountUsd);
82
- const amountMicros = centsToMicros(amountCents);
83
133
  const stripe = stripeClient(deps);
134
+ const promotion = body.promotionCode
135
+ ? await resolveCheckoutPromotionCode(stripe, body.promotionCode)
136
+ : null;
137
+ const amountCents =
138
+ body.amountUsd !== undefined
139
+ ? usdToCents(body.amountUsd)
140
+ : (promotion?.amountOffCents ?? null);
141
+ if (amountCents === null) {
142
+ throw new HTTPException(422, {
143
+ message: promotion
144
+ ? "Choose how many credits to buy with this code."
145
+ : "amountUsd is required",
146
+ });
147
+ }
148
+ if (amountCents < 500 || amountCents > 1_000_000) {
149
+ throw new HTTPException(422, {
150
+ message: "Credits must be between $5 and $10,000.",
151
+ });
152
+ }
153
+ const amountMicros = centsToMicros(amountCents);
84
154
  const customerId = await getOrCreateStripeCustomer(deps, stripe, context, accountId);
85
155
  const idempotencyKey = `checkout:${accountId}:${amountMicros}:${crypto.randomUUID()}`;
86
- const session = await stripe.checkout.sessions.create(
87
- stripeCheckoutSessionCreateParams({
88
- accountId,
156
+ let session: Stripe.Checkout.Session;
157
+ try {
158
+ session = await stripe.checkout.sessions.create(
159
+ stripeCheckoutSessionCreateParams({
160
+ accountId,
161
+ customerId,
162
+ amountCents,
163
+ amountMicros,
164
+ creditsProductId: deps.settings.stripeCreditsProductId,
165
+ publicBaseUrl: deps.settings.publicBaseUrl,
166
+ webBaseUrl: deps.settings.webBaseUrl,
167
+ successUrl: body.successUrl,
168
+ cancelUrl: body.cancelUrl,
169
+ idempotencyKey,
170
+ ...(promotion
171
+ ? {
172
+ promotionCodeId: promotion.id,
173
+ fullyDiscounted:
174
+ promotion.percentOff === 100 || (promotion.amountOffCents ?? 0) >= amountCents,
175
+ }
176
+ : {}),
177
+ }),
178
+ { idempotencyKey },
179
+ );
180
+ } catch (error) {
181
+ // Stripe refuses a code whose restrictions this purchase doesn't meet
182
+ // (first purchase only, a minimum amount, another customer's code).
183
+ if (promotion && error instanceof Stripe.errors.StripeInvalidRequestError) {
184
+ throw new HTTPException(422, { message: "This code can't be used for this purchase." });
185
+ }
186
+ throw error;
187
+ }
188
+ if (!session.url) {
189
+ throw new HTTPException(502, { message: "Stripe did not return a checkout URL" });
190
+ }
191
+ return c.json(
192
+ CreateCheckoutResponse.parse({
193
+ checkoutSessionId: session.id,
194
+ url: session.url,
195
+ amountUsd: amountCents / 100,
196
+ }),
197
+ );
198
+ });
199
+
200
+ // Where one checkout stands, for the page the customer returns to. Credits
201
+ // normally post from the webhook; a completed checkout whose webhook has not
202
+ // arrived yet is settled here from Stripe's own record of the session, under
203
+ // the same ledger idempotency key, so it grants at most once.
204
+ app.get("/v1/billing/checkout/:checkoutSessionId", async (c) => {
205
+ if (deps.settings.billingMode !== "stripe") {
206
+ throw new HTTPException(404, { message: "stripe billing is not enabled" });
207
+ }
208
+ const context = await requireAccessContext(c, deps);
209
+ const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
210
+ const checkoutSessionId = c.req.param("checkoutSessionId");
211
+ if (!/^cs_[A-Za-z0-9_]{1,250}$/.test(checkoutSessionId)) {
212
+ throw new HTTPException(404, { message: "checkout not found" });
213
+ }
214
+ const stripe = stripeClient(deps);
215
+ let session: Stripe.Checkout.Session;
216
+ try {
217
+ session = await stripe.checkout.sessions.retrieve(checkoutSessionId);
218
+ } catch (error) {
219
+ if (error instanceof Stripe.errors.StripeInvalidRequestError) {
220
+ throw new HTTPException(404, { message: "checkout not found" });
221
+ }
222
+ throw error;
223
+ }
224
+ if (session.metadata?.opengeni_account_id !== accountId) {
225
+ throw new HTTPException(404, { message: "checkout not found" });
226
+ }
227
+ const credit = creditMetadata(session.metadata, `Stripe checkout session ${session.id}`);
228
+ let entry = await getCreditLedgerEntry(deps.db, accountId, credit.idempotencyKey);
229
+ if (!entry && session.status === "complete") {
230
+ await grantCheckoutSessionCredits(deps, session, {
231
+ stripeEventId: null,
232
+ livemode: session.livemode,
233
+ });
234
+ entry = await getCreditLedgerEntry(deps.db, accountId, credit.idempotencyKey);
235
+ }
236
+ return c.json(
237
+ BillingCheckoutStatus.parse({
238
+ checkoutSessionId: session.id,
239
+ status: session.status ?? "open",
240
+ credit: {
241
+ state: entry ? "granted" : "pending",
242
+ amountMicros: entry?.amountMicros ?? credit.amountMicros,
243
+ currency: "usd",
244
+ free: entry
245
+ ? entry.sourceType === "stripe_checkout_coupon"
246
+ : isFreeCouponCheckout(session),
247
+ },
248
+ balance: entry ? await getBillingBalance(deps.db, accountId) : null,
249
+ }),
250
+ );
251
+ });
252
+
253
+ app.post("/v1/billing/portal", async (c) => {
254
+ if (deps.settings.billingMode !== "stripe") {
255
+ throw new HTTPException(404, { message: "stripe billing is not enabled" });
256
+ }
257
+ const context = await requireAccessContext(c, deps);
258
+ const parsed = CreateBillingPortalRequest.safeParse(await c.req.json());
259
+ if (!parsed.success) {
260
+ throw new HTTPException(400, {
261
+ message: parsed.error.issues[0]?.message ?? "invalid billing portal request",
262
+ });
263
+ }
264
+ const accountId = requireSelectedAccount(context, parsed.data.accountId, "billing:manage");
265
+ const stripe = stripeClient(deps);
266
+ const customerId = await getOrCreateStripeCustomer(deps, stripe, context, accountId);
267
+ const session = await stripe.billingPortal.sessions.create(
268
+ stripeBillingPortalSessionCreateParams({
89
269
  customerId,
90
- amountCents,
91
- amountMicros,
92
- creditsProductId: deps.settings.stripeCreditsProductId,
93
270
  publicBaseUrl: deps.settings.publicBaseUrl,
94
- successUrl: body.successUrl,
95
- cancelUrl: body.cancelUrl,
96
- idempotencyKey,
271
+ webBaseUrl: deps.settings.webBaseUrl,
272
+ returnUrl: parsed.data.returnUrl,
97
273
  }),
98
- { idempotencyKey },
99
274
  );
100
275
  if (!session.url) {
101
- throw new HTTPException(502, { message: "Stripe did not return a checkout URL" });
276
+ throw new HTTPException(502, { message: "Stripe did not return a billing portal URL" });
102
277
  }
103
278
  return c.json(
104
- CreateCheckoutResponse.parse({
105
- checkoutSessionId: session.id,
279
+ CreateBillingPortalResponse.parse({
280
+ portalSessionId: session.id,
106
281
  url: session.url,
107
282
  }),
108
283
  );
@@ -159,24 +334,39 @@ export function stripeCheckoutSessionCreateParams(input: {
159
334
  amountMicros: number;
160
335
  creditsProductId?: string | undefined;
161
336
  publicBaseUrl?: string | undefined;
337
+ webBaseUrl?: string | undefined;
162
338
  successUrl?: string | undefined;
163
339
  cancelUrl?: string | undefined;
164
340
  idempotencyKey: string;
341
+ /** Apply this promotion code up front instead of letting Stripe ask for one. */
342
+ promotionCodeId?: string | undefined;
343
+ /**
344
+ * The applied code covers the whole package, so the total is $0. Nothing is
345
+ * taxable, so Checkout skips tax and with it the billing address: the
346
+ * customer only confirms.
347
+ */
348
+ fullyDiscounted?: boolean | undefined;
165
349
  }): Stripe.Checkout.SessionCreateParams {
166
350
  const successUrl = checkoutReturnUrl(
167
351
  input.publicBaseUrl,
352
+ input.webBaseUrl,
168
353
  input.successUrl,
169
354
  "/billing?checkout=success",
170
355
  "successUrl",
171
356
  );
172
357
  const cancelUrl = checkoutReturnUrl(
173
358
  input.publicBaseUrl,
359
+ input.webBaseUrl,
174
360
  input.cancelUrl,
175
361
  "/billing?checkout=cancelled",
176
362
  "cancelUrl",
177
363
  );
178
364
  return {
179
365
  mode: "payment",
366
+ // Stripe takes either a code field on its page or one discount up front.
367
+ ...(input.promotionCodeId
368
+ ? { discounts: [{ promotion_code: input.promotionCodeId }] }
369
+ : { allow_promotion_codes: true }),
180
370
  customer: input.customerId,
181
371
  customer_update: {
182
372
  address: "auto",
@@ -184,7 +374,7 @@ export function stripeCheckoutSessionCreateParams(input: {
184
374
  },
185
375
  success_url: successUrl,
186
376
  cancel_url: cancelUrl,
187
- automatic_tax: { enabled: true },
377
+ automatic_tax: { enabled: !(input.promotionCodeId && input.fullyDiscounted) },
188
378
  billing_address_collection: "auto",
189
379
  line_items: [
190
380
  {
@@ -196,7 +386,7 @@ export function stripeCheckoutSessionCreateParams(input: {
196
386
  ? { product: input.creditsProductId }
197
387
  : {
198
388
  product_data: {
199
- name: "OpenGeni credits",
389
+ name: "Opengeni credits",
200
390
  metadata: {
201
391
  app: "opengeni",
202
392
  billing_model: "prepaid_credits",
@@ -211,6 +401,7 @@ export function stripeCheckoutSessionCreateParams(input: {
211
401
  opengeni_credit_amount_usd: (input.amountCents / 100).toFixed(2),
212
402
  opengeni_credit_micros: String(input.amountMicros),
213
403
  opengeni_credit_idempotency_key: input.idempotencyKey,
404
+ opengeni_credit_coupon_v1: "1",
214
405
  },
215
406
  payment_intent_data: {
216
407
  metadata: {
@@ -218,30 +409,65 @@ export function stripeCheckoutSessionCreateParams(input: {
218
409
  opengeni_credit_amount_usd: (input.amountCents / 100).toFixed(2),
219
410
  opengeni_credit_micros: String(input.amountMicros),
220
411
  opengeni_credit_idempotency_key: input.idempotencyKey,
412
+ opengeni_credit_coupon_v1: "1",
413
+ },
414
+ },
415
+ invoice_creation: {
416
+ enabled: true,
417
+ invoice_data: {
418
+ metadata: {
419
+ opengeni_account_id: input.accountId,
420
+ opengeni_credit_amount_usd: (input.amountCents / 100).toFixed(2),
421
+ opengeni_credit_micros: String(input.amountMicros),
422
+ opengeni_credit_idempotency_key: input.idempotencyKey,
423
+ opengeni_credit_coupon_v1: "1",
424
+ },
221
425
  },
222
426
  },
223
427
  };
224
428
  }
225
429
 
430
+ export function stripeBillingPortalSessionCreateParams(input: {
431
+ customerId: string;
432
+ publicBaseUrl?: string | undefined;
433
+ webBaseUrl?: string | undefined;
434
+ returnUrl?: string | undefined;
435
+ }): Stripe.BillingPortal.SessionCreateParams {
436
+ return {
437
+ customer: input.customerId,
438
+ return_url: checkoutReturnUrl(
439
+ input.publicBaseUrl,
440
+ input.webBaseUrl,
441
+ input.returnUrl,
442
+ "/billing",
443
+ "returnUrl",
444
+ ),
445
+ };
446
+ }
447
+
226
448
  function checkoutReturnUrl(
227
449
  publicBaseUrl: string | undefined,
450
+ webBaseUrl: string | undefined,
228
451
  candidate: string | undefined,
229
452
  fallbackPath: string,
230
453
  field: string,
231
454
  ): string {
232
455
  if (!publicBaseUrl) {
233
456
  throw new HTTPException(500, {
234
- message: "OPENGENI_PUBLIC_BASE_URL is required for Stripe checkout",
457
+ message: "OPENGENI_PUBLIC_BASE_URL is required for Stripe redirects",
235
458
  });
236
459
  }
237
- const base = new URL(publicBaseUrl);
460
+ const base = new URL(webBaseUrl ?? publicBaseUrl);
238
461
  const fallback = new URL(fallbackPath, base).toString();
239
462
  if (!candidate) {
240
463
  return fallback;
241
464
  }
242
465
  const parsed = new URL(candidate);
243
- if (parsed.origin !== base.origin) {
244
- throw new HTTPException(400, { message: `${field} must use the OpenGeni public origin` });
466
+ const allowedOrigins = new Set([new URL(publicBaseUrl).origin, base.origin]);
467
+ if (!allowedOrigins.has(parsed.origin)) {
468
+ throw new HTTPException(400, {
469
+ message: `${field} must use the Opengeni public${allowedOrigins.size > 1 ? " or web" : ""} origin`,
470
+ });
245
471
  }
246
472
  return parsed.toString();
247
473
  }
@@ -253,7 +479,12 @@ async function handleStripeWebhookEvent(
253
479
  ): Promise<void> {
254
480
  switch (event.type) {
255
481
  case "checkout.session.completed":
256
- await handleCheckoutSessionCompleted(deps, event);
482
+ case "checkout.session.async_payment_succeeded":
483
+ await handleCheckoutSessionPayment(deps, event);
484
+ return;
485
+ case "checkout.session.async_payment_failed":
486
+ // A delayed payment method failed. Completion reported it `unpaid`, so
487
+ // no credit was granted and there is nothing to reverse.
257
488
  return;
258
489
  case "checkout.session.expired":
259
490
  case "payment_intent.succeeded":
@@ -289,42 +520,171 @@ async function handleStripeWebhookEvent(
289
520
  }
290
521
  }
291
522
 
292
- async function handleCheckoutSessionCompleted(
523
+ export type StripeCheckoutCreditDecision =
524
+ | { action: "grant"; credit: CheckoutCreditMetadata }
525
+ | { action: "ignore"; reason: "foreign_checkout" | "not_payment_mode" | "payment_not_paid" };
526
+
527
+ /**
528
+ * Decides whether a Checkout Session webhook grants OpenGeni credits.
529
+ *
530
+ * Sessions created outside OpenGeni on the same Stripe account carry no
531
+ * `opengeni_` metadata and are acknowledged without effect. An OpenGeni
532
+ * session with malformed credit metadata still throws so the failure stays
533
+ * visible. Credits are granted once Stripe reports the payment `paid` or a
534
+ * completed checkout fully covered by a coupon:
535
+ * on `checkout.session.completed` for immediate methods, or on
536
+ * `checkout.session.async_payment_succeeded` for delayed methods, whose
537
+ * completion event arrives `unpaid`. Both events carry the same session
538
+ * metadata, so the ledger idempotency key grants at most once.
539
+ */
540
+ export function stripeCheckoutCreditDecision(
541
+ session: Stripe.Checkout.Session,
542
+ ): StripeCheckoutCreditDecision {
543
+ if (!Object.keys(session.metadata ?? {}).some((key) => key.startsWith("opengeni_"))) {
544
+ return { action: "ignore", reason: "foreign_checkout" };
545
+ }
546
+ if (session.mode !== "payment") {
547
+ return { action: "ignore", reason: "not_payment_mode" };
548
+ }
549
+ if (session.payment_status !== "paid" && !isFreeCouponCheckout(session)) {
550
+ return { action: "ignore", reason: "payment_not_paid" };
551
+ }
552
+ const credit = creditMetadata(session.metadata, `Stripe checkout session ${session.id}`);
553
+ if (
554
+ session.metadata?.opengeni_credit_coupon_v1 === "1" &&
555
+ (session.currency !== "usd" ||
556
+ session.amount_subtotal !== credit.amountMicros / 10_000 ||
557
+ typeof session.amount_total !== "number" ||
558
+ !Number.isSafeInteger(session.amount_total) ||
559
+ session.amount_total < 0)
560
+ ) {
561
+ throw new Error(`Stripe checkout session ${session.id} has invalid credit package totals`);
562
+ }
563
+ return { action: "grant", credit };
564
+ }
565
+
566
+ function isFreeCouponCheckout(session: Stripe.Checkout.Session): boolean {
567
+ return (
568
+ (session.payment_status === "paid" || session.payment_status === "no_payment_required") &&
569
+ session.status === "complete" &&
570
+ session.amount_total === 0 &&
571
+ typeof session.amount_subtotal === "number" &&
572
+ session.amount_subtotal > 0 &&
573
+ (session.total_details?.amount_discount ?? 0) >= session.amount_subtotal
574
+ );
575
+ }
576
+
577
+ async function handleCheckoutSessionPayment(
293
578
  deps: ApiRouteDeps,
294
579
  event: Stripe.Event,
295
580
  ): Promise<void> {
296
- const session = event.data.object as Stripe.Checkout.Session;
297
- if (session.mode !== "payment" || session.payment_status !== "paid") {
581
+ await grantCheckoutSessionCredits(deps, event.data.object as Stripe.Checkout.Session, {
582
+ stripeEventId: event.id,
583
+ stripeEventType: event.type,
584
+ livemode: event.livemode,
585
+ });
586
+ }
587
+
588
+ /**
589
+ * Grants a paid (or fully coupon-covered) OpenGeni checkout's credits once.
590
+ * The webhook calls this with its event; the checkout status read calls it
591
+ * with the session it retrieved from Stripe when the webhook is late.
592
+ */
593
+ async function grantCheckoutSessionCredits(
594
+ deps: ApiRouteDeps,
595
+ session: Stripe.Checkout.Session,
596
+ source: { stripeEventId: string | null; stripeEventType?: string; livemode: boolean },
597
+ ): Promise<void> {
598
+ const decision = stripeCheckoutCreditDecision(session);
599
+ if (decision.action === "ignore") {
600
+ if (decision.reason === "foreign_checkout") {
601
+ console.info("[api] stripe webhook ignored a checkout session not created by OpenGeni", {
602
+ stripeEventType: source.stripeEventType ?? null,
603
+ livemode: source.livemode,
604
+ });
605
+ }
606
+ return;
607
+ }
608
+ const credit = decision.credit;
609
+ const freeCouponCheckout = isFreeCouponCheckout(session);
610
+ if (!(await getManagedAccount(deps.db, credit.accountId))) {
611
+ // Another OpenGeni deployment sharing the Stripe account (or an account
612
+ // removed before a delayed payment settled). Retrying cannot succeed, so
613
+ // acknowledge instead of failing the delivery for days.
614
+ console.info(
615
+ "[api] stripe webhook ignored a checkout session for an account not in this deployment",
616
+ {
617
+ stripeEventType: source.stripeEventType ?? null,
618
+ livemode: source.livemode,
619
+ },
620
+ );
298
621
  return;
299
622
  }
300
- const credit = creditMetadata(session.metadata, `Stripe checkout session ${session.id}`);
301
623
  const customerId = typeof session.customer === "string" ? session.customer : session.customer?.id;
302
624
  if (customerId) {
303
625
  await upsertBillingCustomer(deps.db, {
304
626
  accountId: credit.accountId,
305
- provider: stripeCustomerProvider(event),
627
+ provider: source.livemode ? "stripe:live" : "stripe:test",
306
628
  providerCustomerId: customerId,
307
629
  email: session.customer_details?.email ?? session.customer_email ?? null,
308
630
  });
309
631
  }
632
+ if (await hasCreditLedgerEntry(deps.db, credit.accountId, credit.idempotencyKey)) {
633
+ return;
634
+ }
310
635
  await applyCreditLedgerEntry(deps.db, {
311
636
  accountId: credit.accountId,
312
- type: "credit_topup",
637
+ type: freeCouponCheckout ? "grant" : "credit_topup",
313
638
  amountMicros: credit.amountMicros,
314
- sourceType: "stripe_checkout_session",
639
+ sourceType: freeCouponCheckout ? "stripe_checkout_coupon" : "stripe_checkout_session",
315
640
  sourceId: session.id,
316
641
  idempotencyKey: credit.idempotencyKey,
317
642
  metadata: {
318
- stripeEventId: event.id,
643
+ stripeEventId: source.stripeEventId,
319
644
  stripePaymentIntentId:
320
645
  typeof session.payment_intent === "string"
321
646
  ? session.payment_intent
322
647
  : (session.payment_intent?.id ?? null),
323
648
  stripePackageId: credit.packageId,
324
649
  stripeCreditAmountUsd: credit.amountUsd,
650
+ ...(session.metadata?.opengeni_credit_coupon_v1 === "1"
651
+ ? {
652
+ stripeAmountTotalCents: session.amount_total,
653
+ stripeDiscountCents: session.total_details?.amount_discount ?? 0,
654
+ }
655
+ : {}),
325
656
  },
326
657
  });
327
- recordCreditMicrosMetric(deps, "topup", credit.amountMicros);
658
+ recordCreditMicrosMetric(deps, freeCouponCheckout ? "grant" : "topup", credit.amountMicros);
659
+ }
660
+
661
+ /**
662
+ * Looks up a customer-typed promotion code. Stripe matches codes without
663
+ * regard to case. A fixed USD amount off sets the credits a checkout buys.
664
+ */
665
+ async function resolveCheckoutPromotionCode(
666
+ stripe: Stripe,
667
+ code: string,
668
+ ): Promise<{ id: string; amountOffCents: number | null; percentOff: number | null }> {
669
+ const listed = await stripe.promotionCodes.list({
670
+ code,
671
+ active: true,
672
+ limit: 1,
673
+ expand: ["data.promotion.coupon"],
674
+ });
675
+ const promotionCode = listed.data[0];
676
+ const coupon =
677
+ promotionCode && typeof promotionCode.promotion?.coupon === "object"
678
+ ? promotionCode.promotion.coupon
679
+ : null;
680
+ if (!promotionCode || !coupon?.valid) {
681
+ throw new HTTPException(422, { message: "That code isn't valid or has expired." });
682
+ }
683
+ const amountOffCents =
684
+ coupon.amount_off && coupon.currency === "usd"
685
+ ? coupon.amount_off
686
+ : (coupon.currency_options?.usd?.amount_off ?? null);
687
+ return { id: promotionCode.id, amountOffCents, percentOff: coupon.percent_off ?? null };
328
688
  }
329
689
 
330
690
  async function mirrorPaymentIntentCustomer(deps: ApiRouteDeps, event: Stripe.Event): Promise<void> {
@@ -377,19 +737,21 @@ async function applyRefundDebit(
377
737
  if (await hasCreditLedgerEntry(deps.db, accountId, idempotencyKey)) {
378
738
  return;
379
739
  }
740
+ const debitMicros = await creditAdjustmentMicros(deps, metadata, refund.amount);
380
741
  await applyCreditLedgerEntry(deps.db, {
381
742
  accountId,
382
743
  type: "credit_refund",
383
- amountMicros: -centsToMicros(refund.amount),
744
+ amountMicros: -debitMicros,
384
745
  sourceType: "stripe_refund",
385
746
  sourceId: refund.id,
386
747
  idempotencyKey,
387
748
  metadata: {
388
749
  stripeRefundId: refund.id,
389
750
  stripePaymentIntentId: paymentIntentId(refund.payment_intent),
751
+ stripeRefundAmountCents: refund.amount,
390
752
  },
391
753
  });
392
- recordCreditMicrosMetric(deps, "refund", centsToMicros(refund.amount));
754
+ recordCreditMicrosMetric(deps, "refund", debitMicros);
393
755
  }
394
756
 
395
757
  async function holdDisputedCredits(
@@ -403,10 +765,11 @@ async function holdDisputedCredits(
403
765
  if (!accountId) {
404
766
  return;
405
767
  }
768
+ const debitMicros = await creditAdjustmentMicros(deps, metadata, dispute.amount);
406
769
  await applyCreditLedgerEntry(deps.db, {
407
770
  accountId,
408
771
  type: "credit_dispute_hold",
409
- amountMicros: -centsToMicros(dispute.amount),
772
+ amountMicros: -debitMicros,
410
773
  sourceType: "stripe_dispute",
411
774
  sourceId: dispute.id,
412
775
  idempotencyKey: `stripe:dispute_hold:${dispute.id}`,
@@ -432,10 +795,11 @@ async function releaseDisputedCredits(
432
795
  if (!(await hasCreditLedgerEntry(deps.db, accountId, holdIdempotencyKey))) {
433
796
  return;
434
797
  }
798
+ const debitMicros = await creditAdjustmentMicros(deps, metadata, dispute.amount);
435
799
  await applyCreditLedgerEntry(deps.db, {
436
800
  accountId,
437
801
  type: "credit_dispute_release",
438
- amountMicros: centsToMicros(dispute.amount),
802
+ amountMicros: debitMicros,
439
803
  sourceType: "stripe_dispute",
440
804
  sourceId: dispute.id,
441
805
  idempotencyKey: `stripe:dispute_release:${dispute.id}`,
@@ -443,6 +807,34 @@ async function releaseDisputedCredits(
443
807
  });
444
808
  }
445
809
 
810
+ async function creditAdjustmentMicros(
811
+ deps: ApiRouteDeps,
812
+ metadata: Stripe.Metadata | null,
813
+ amountCents: number,
814
+ ): Promise<number> {
815
+ if (metadata?.opengeni_credit_coupon_v1 !== "1") return centsToMicros(amountCents);
816
+ const credit = creditMetadata(metadata, "Stripe adjustment");
817
+ const entry = await getCreditLedgerEntry(deps.db, credit.accountId, credit.idempotencyKey);
818
+ const paidCents = entry?.metadata.stripeAmountTotalCents;
819
+ if (
820
+ entry?.sourceType !== "stripe_checkout_session" ||
821
+ entry.amountMicros !== credit.amountMicros ||
822
+ typeof paidCents !== "number" ||
823
+ !Number.isSafeInteger(paidCents) ||
824
+ paidCents <= 0 ||
825
+ !Number.isSafeInteger(amountCents) ||
826
+ amountCents <= 0
827
+ ) {
828
+ throw new Error("Stripe credit adjustment is missing a settled checkout");
829
+ }
830
+ const proportionalMicros =
831
+ (BigInt(entry.amountMicros) * BigInt(amountCents) + BigInt(Math.floor(paidCents / 2))) /
832
+ BigInt(paidCents);
833
+ return Number(
834
+ proportionalMicros > BigInt(entry.amountMicros) ? entry.amountMicros : proportionalMicros,
835
+ );
836
+ }
837
+
446
838
  async function mirrorCustomer(
447
839
  deps: ApiRouteDeps,
448
840
  event: Stripe.Event,
@@ -480,18 +872,20 @@ async function metadataForRefund(
480
872
  stripe: Stripe,
481
873
  refund: Stripe.Refund,
482
874
  ): Promise<Stripe.Metadata | null> {
483
- if (Object.keys(refund.metadata ?? {}).length > 0) {
875
+ if (refund.metadata?.opengeni_credit_idempotency_key) {
484
876
  return refund.metadata;
485
877
  }
486
878
  const paymentIntent = paymentIntentId(refund.payment_intent);
487
- return paymentIntent ? (await stripe.paymentIntents.retrieve(paymentIntent)).metadata : null;
879
+ return paymentIntent
880
+ ? (await stripe.paymentIntents.retrieve(paymentIntent)).metadata
881
+ : refund.metadata;
488
882
  }
489
883
 
490
884
  async function metadataForDispute(
491
885
  stripe: Stripe,
492
886
  dispute: Stripe.Dispute,
493
887
  ): Promise<Stripe.Metadata | null> {
494
- if (Object.keys(dispute.metadata ?? {}).length > 0) {
888
+ if (dispute.metadata?.opengeni_credit_idempotency_key) {
495
889
  return dispute.metadata;
496
890
  }
497
891
  const paymentIntent = paymentIntentId(
@@ -503,7 +897,7 @@ async function metadataForDispute(
503
897
  }
504
898
  const chargeId = typeof dispute.charge === "string" ? dispute.charge : dispute.charge?.id;
505
899
  if (!chargeId) {
506
- return null;
900
+ return dispute.metadata;
507
901
  }
508
902
  const charge = await stripe.charges.retrieve(chargeId);
509
903
  const chargePaymentIntent = paymentIntentId(charge.payment_intent);
@@ -512,21 +906,23 @@ async function metadataForDispute(
512
906
  : charge.metadata;
513
907
  }
514
908
 
515
- function creditMetadata(
516
- metadata: Stripe.Metadata | null | undefined,
517
- label: string,
518
- ): {
909
+ export type CheckoutCreditMetadata = {
519
910
  accountId: string;
520
911
  amountMicros: number;
521
912
  idempotencyKey: string;
522
913
  amountUsd?: string;
523
914
  packageId?: string;
524
- } {
915
+ };
916
+
917
+ function creditMetadata(
918
+ metadata: Stripe.Metadata | null | undefined,
919
+ label: string,
920
+ ): CheckoutCreditMetadata {
525
921
  const accountId = metadata?.opengeni_account_id;
526
922
  const amountMicros = Number(metadata?.opengeni_credit_micros);
527
923
  const idempotencyKey = metadata?.opengeni_credit_idempotency_key;
528
924
  if (!accountId || !Number.isSafeInteger(amountMicros) || amountMicros <= 0 || !idempotencyKey) {
529
- throw new Error(`${label} is missing OpenGeni credit metadata`);
925
+ throw new Error(`${label} is missing Opengeni credit metadata`);
530
926
  }
531
927
  return {
532
928
  accountId,
@@ -582,7 +978,26 @@ export function stripeCustomerProvider(
582
978
  : "stripe:test";
583
979
  }
584
980
 
585
- function requireSelectedAccount(
981
+ /** Billing routes do not traverse the workspace actor middleware. Always bind
982
+ * these reads explicitly; only a revalidated live attempt may supply a human
983
+ * initiator. Neither query parameters nor serviceInitiator claims are proof. */
984
+ export async function withBillingUsageActor<T>(
985
+ deps: ApiRouteDeps,
986
+ context: AccessContext,
987
+ accountId: string,
988
+ read: () => Promise<T>,
989
+ ): Promise<T> {
990
+ const attemptGrant = context.workspaceGrants.find(
991
+ (grant) =>
992
+ grant.accountId === accountId &&
993
+ grant.subjectId === context.subjectId &&
994
+ grant.principalKind === "agent_attempt",
995
+ );
996
+ if (attemptGrant) return await withAccessGrantSessionRlsContext(deps, attemptGrant, read);
997
+ return await withSessionRlsActorContext({ subjectId: context.subjectId }, read);
998
+ }
999
+
1000
+ export function requireSelectedAccount(
586
1001
  context: AccessContext,
587
1002
  requested: string | undefined,
588
1003
  permission: Permission,