@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.
- package/dist/access-grant-rls.d.ts +8 -1
- package/dist/admission-trace.d.ts +8 -0
- package/dist/app.d.ts +50 -3
- package/dist/app.js +10 -1
- package/dist/artifact-catalog-cursor.d.ts +1 -0
- package/dist/attached-browser-viewer.d.ts +11 -0
- package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
- package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +9 -1
- package/dist/auth/managed-auth.d.ts +72 -5
- package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
- package/dist/auth/new-signups-gate.d.ts +23 -0
- package/dist/auth/signup-funnel-metrics.d.ts +44 -0
- package/dist/azure-live.d.ts +28 -0
- package/dist/browser-action-fences.d.ts +13 -0
- package/dist/browser-controller-authority.d.ts +1 -61
- package/dist/browser-ephemeral.d.ts +5 -0
- package/dist/browser-lost-runtime-end.d.ts +24 -0
- package/dist/browser-state-authority.d.ts +1 -27
- package/dist/{chunk-AJ62T5QN.js → chunk-GBUJ2DYZ.js} +80507 -39878
- package/dist/chunk-GBUJ2DYZ.js.map +1 -0
- package/dist/chunk-OGQDXVXY.js +18 -0
- package/dist/chunk-OGQDXVXY.js.map +1 -0
- package/dist/claude-default-models.d.ts +19 -0
- package/dist/claude-subscription-core.d.ts +50 -0
- package/dist/claude-subscription-oauth.d.ts +36 -0
- package/dist/claude-subscription-profile.d.ts +9 -0
- package/dist/claude-subscription-usage.d.ts +6 -0
- package/dist/claude-workspace-connection.d.ts +15 -0
- package/dist/codemode.d.ts +40 -5
- package/dist/codex-realtime.d.ts +29 -3
- package/dist/codex-session-pointers.d.ts +19 -0
- package/dist/connection-ownership.d.ts +6 -3
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/credit-promotion-snapshot.d.ts +6 -0
- package/dist/editable-artifact-source-session.d.ts +3 -0
- package/dist/editable-artifact-websocket.d.ts +5 -3
- package/dist/fatal-process-boundary.d.ts +9 -1
- package/dist/github-access.d.ts +7 -1
- package/dist/github-browser-flow.d.ts +1 -1
- package/dist/http/acting-person.d.ts +36 -0
- package/dist/http/api-contract-compat.d.ts +19 -0
- package/dist/http/api-error.d.ts +50 -1
- package/dist/http/auth.d.ts +8 -1
- package/dist/http/cors.d.ts +12 -0
- package/dist/http/deprecation.d.ts +42 -0
- package/dist/http/keyed-admission.d.ts +12 -0
- package/dist/http/local-browser-boundary.d.ts +72 -0
- package/dist/http/metrics-listener.d.ts +22 -0
- package/dist/http/path-identifier.d.ts +17 -0
- package/dist/http/registered-route-label.d.ts +16 -0
- package/dist/http/rejection-telemetry.d.ts +34 -0
- package/dist/http/request-body.d.ts +27 -0
- package/dist/http/request-source.d.ts +41 -3
- package/dist/http/sse.d.ts +12 -1
- package/dist/http/unmatched-route.d.ts +18 -0
- package/dist/http/user-content.d.ts +36 -0
- package/dist/http/variable-sets-removed.d.ts +41 -0
- package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
- package/dist/index.js +1279 -108
- package/dist/index.js.map +1 -1
- package/dist/integration-connect-metrics.d.ts +35 -0
- package/dist/integration-endpoint-test.d.ts +20 -0
- package/dist/integrations/api-integrations.d.ts +2 -2
- package/dist/integrations/atlassian.d.ts +8 -3
- package/dist/integrations/fiken.d.ts +3 -1
- package/dist/integrations/github-lens-connect.d.ts +37 -66
- package/dist/integrations/google-drive.d.ts +2 -0
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/marketplace-plugin.d.ts +43 -43
- package/dist/integrations/oauth-client.d.ts +171 -12
- package/dist/integrations/oauth-profiles.d.ts +42 -13
- package/dist/integrations/onepassword-vault-provider.d.ts +9 -0
- package/dist/integrations/slack-bot.d.ts +285 -7
- package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
- package/dist/integrations/slack-interactions.d.ts +89 -10
- package/dist/integrations/slack-mrkdwn.d.ts +41 -0
- package/dist/integrations/slack-routing.d.ts +1 -1
- package/dist/integrations/slack-session-defaults.d.ts +58 -0
- package/dist/integrations/slack-task-file-upload.d.ts +36 -0
- package/dist/integrations/slack-tool-review.d.ts +5 -0
- package/dist/interaction-agent-access.d.ts +13 -8
- package/dist/interaction-agent-version.d.ts +21 -0
- package/dist/interaction-holder-heartbeat.d.ts +5 -0
- package/dist/interaction-metrics.d.ts +27 -2
- package/dist/interaction-route-stages.d.ts +25 -0
- package/dist/interaction-secrets-vault.d.ts +20 -0
- package/dist/interaction-secrets.d.ts +103 -0
- package/dist/mcp/action-catalog-types.d.ts +14 -0
- package/dist/mcp/action-catalog.gen.d.ts +2 -0
- package/dist/mcp/action-descriptions.d.ts +4 -0
- package/dist/mcp/action-schema-imports.gen.d.ts +1 -0
- package/dist/mcp/capability-account-readiness.d.ts +7 -0
- package/dist/mcp/child-read-evidence.d.ts +9 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +12 -0
- package/dist/mcp/contract-input.d.ts +13 -0
- package/dist/mcp/documents.d.ts +2 -0
- package/dist/mcp/first-party-tool-permissions.d.ts +3 -606
- package/dist/mcp/notification-tools.d.ts +26 -0
- package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
- package/dist/mcp/scheduled-task-input.d.ts +1309 -0
- package/dist/mcp/scheduled-task-view.d.ts +217 -3
- package/dist/mcp/secret-tools.d.ts +14 -0
- package/dist/mcp/server.d.ts +108 -5
- package/dist/mcp/session-admin-access-tools.d.ts +33 -0
- package/dist/mcp/session-event-errors.d.ts +28 -0
- package/dist/mcp/session-event-view.d.ts +62 -0
- package/dist/mcp/session-view.d.ts +15 -0
- package/dist/mcp/session-wait.d.ts +18 -0
- package/dist/mcp/site-agent-clamp-tools.d.ts +38 -0
- package/dist/mcp/site-data-tools.d.ts +22 -0
- package/dist/mcp/site-publication-tools.d.ts +19 -0
- package/dist/mcp/site-sql-tools.d.ts +27 -0
- package/dist/mcp-oauth.d.ts +37 -4
- package/dist/model-calls/chat-completions.d.ts +87 -0
- package/dist/model-calls/pending-usage-settlements.d.ts +57 -0
- package/dist/model-calls/service.d.ts +36 -0
- package/dist/model-catalog.d.ts +9 -3
- package/dist/native-push-dispatch.d.ts +69 -0
- package/dist/observability.d.ts +3 -1
- package/dist/organization-mcp.d.ts +82 -0
- package/dist/prepared-mcp-actor.d.ts +77 -0
- package/dist/prepared-mcp-permissions.d.ts +8 -0
- package/dist/routes/analytics-consent.d.ts +26 -0
- package/dist/routes/api-integrations.d.ts +11 -0
- package/dist/routes/api-keys.d.ts +7 -7
- package/dist/routes/automations.d.ts +2 -0
- package/dist/routes/billing.d.ts +59 -0
- package/dist/routes/browser-sessions.d.ts +65 -2
- package/dist/routes/chat-completions.d.ts +11 -0
- package/dist/routes/claude-core-usage.d.ts +20 -0
- package/dist/routes/claude-subscription-accounts.d.ts +3 -0
- package/dist/routes/claude-subscription-oauth.d.ts +3 -0
- package/dist/routes/client-errors.d.ts +182 -0
- package/dist/routes/codex-core.d.ts +401 -0
- package/dist/routes/codex.d.ts +64 -2
- package/dist/routes/computer-sessions.d.ts +6 -1
- package/dist/routes/desktop-distribution.d.ts +32 -0
- package/dist/routes/github-browser-fonts.gen.d.ts +2 -0
- package/dist/routes/github-browser-pages.d.ts +27 -0
- package/dist/routes/inbox.d.ts +13 -0
- package/dist/routes/insights-response-cache.d.ts +10 -0
- package/dist/routes/insights-usage.d.ts +12 -0
- package/dist/routes/insights.d.ts +28 -0
- package/dist/routes/interaction-placement-message.d.ts +7 -0
- package/dist/routes/managed-sign-in-methods.d.ts +6 -0
- package/dist/routes/model-account-choice.d.ts +13 -0
- package/dist/routes/model-account-settings.d.ts +19 -0
- package/dist/routes/native-app-auth.d.ts +12 -0
- package/dist/routes/organization-integration-policy.d.ts +8 -0
- package/dist/routes/organization-integrations.d.ts +6 -0
- package/dist/routes/organization-mcp-connections.d.ts +3 -0
- package/dist/routes/organization-model-defaults.d.ts +9 -0
- package/dist/routes/organization-service-accounts.d.ts +3 -0
- package/dist/routes/personal-api-keys.d.ts +3 -0
- package/dist/routes/session-admin-access.d.ts +3 -0
- package/dist/routes/session-artifact-associations.d.ts +4 -0
- package/dist/routes/session-history-imports.d.ts +20 -0
- package/dist/routes/sessions.d.ts +38 -2
- package/dist/routes/site-data.d.ts +18 -0
- package/dist/routes/site-publishing.d.ts +5 -0
- package/dist/routes/site-runtime.d.ts +31 -0
- package/dist/routes/site-sql.d.ts +16 -0
- package/dist/routes/slack-bot-access.d.ts +3 -0
- package/dist/routes/subscription-account-pools.d.ts +21 -0
- package/dist/routes/subscription-core-http.d.ts +5 -0
- package/dist/routes/subscription-pool-access.d.ts +37 -0
- package/dist/routes/subscription-pool-core.d.ts +153 -0
- package/dist/routes/supergrok.d.ts +6 -6
- package/dist/routes/usage-allowances.d.ts +12 -0
- package/dist/routes/variable-sets-removed.d.ts +2 -0
- package/dist/routes/vault-providers.d.ts +34 -0
- package/dist/routes/vaults.d.ts +3 -0
- package/dist/routes/workspace-integrations.d.ts +35 -0
- package/dist/routes/workspace-model-providers.d.ts +4 -0
- package/dist/routes/workspaces.d.ts +11 -2
- package/dist/sandbox/agent-update.d.ts +64 -0
- package/dist/sandbox/auth-callout.d.ts +8 -0
- package/dist/sandbox/channel-a.d.ts +15 -1
- package/dist/sandbox/enrollment.d.ts +26 -4
- package/dist/sandbox/machine-audience.d.ts +29 -0
- package/dist/sandbox/machines.d.ts +15 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -2
- package/dist/sandbox/screen-control.d.ts +85 -0
- package/dist/sandbox/viewer.d.ts +5 -1
- package/dist/sandbox-file-artifacts.d.ts +17 -1
- package/dist/scheduled-task-blockers.d.ts +39 -0
- package/dist/scheduled-task-deletion.d.ts +3 -4
- package/dist/scheduled-task-lifecycle.d.ts +14 -0
- package/dist/secret-answers.d.ts +40 -0
- package/dist/session-create-observability.d.ts +5 -0
- package/dist/session-event-view-36GO3IL7.js +528 -0
- package/dist/session-event-view-36GO3IL7.js.map +1 -0
- package/dist/signed-state-secret.d.ts +18 -0
- package/dist/site-agent-guard.d.ts +53 -0
- package/dist/site-data/operations.d.ts +103 -0
- package/dist/site-data/service.d.ts +203 -0
- package/dist/site-frame-guard.d.ts +32 -0
- package/dist/site-publishing/agent-data-tools.d.ts +84 -0
- package/dist/site-publishing/agent-model.d.ts +58 -0
- package/dist/site-publishing/agent-session.d.ts +16 -0
- package/dist/site-publishing/content-scanner.d.ts +31 -0
- package/dist/site-publishing/edge-sync.d.ts +48 -0
- package/dist/site-publishing/gateway-signature.d.ts +56 -0
- package/dist/site-publishing/limits.d.ts +63 -0
- package/dist/site-publishing/page-tool-authority.d.ts +30 -0
- package/dist/site-publishing/platform-usage.d.ts +113 -0
- package/dist/site-publishing/platform.d.ts +21 -0
- package/dist/site-publishing/readiness.d.ts +62 -0
- package/dist/site-publishing/runtime.d.ts +179 -0
- package/dist/site-publishing/service.d.ts +264 -0
- package/dist/site-publishing/session-events.d.ts +22 -0
- package/dist/site-publishing/session-views.d.ts +36 -0
- package/dist/site-publishing/viewer.d.ts +40 -0
- package/dist/site-session-origin.d.ts +31 -6
- package/dist/site-sql/format.d.ts +47 -0
- package/dist/site-sql/gateway.d.ts +50 -0
- package/dist/site-sql/service.d.ts +213 -0
- package/dist/temporal-schedule-sync.d.ts +26 -0
- package/dist/transcription/billing-refusal.d.ts +8 -0
- package/dist/transcription/normalize.d.ts +23 -0
- package/dist/transcription/providers/azure-mai.d.ts +12 -0
- package/dist/transcription/providers/azure-openai.d.ts +4 -0
- package/dist/transcription/providers/openai.d.ts +16 -0
- package/dist/transcription/providers/subscription-core.d.ts +39 -0
- package/dist/transcription/service.d.ts +10 -1
- package/dist/transcription/wav-duration.d.ts +8 -0
- package/dist/workspace-tool-gateway-attestations.d.ts +37 -0
- package/dist/workspace-tool-gateway-observability.d.ts +16 -0
- package/dist/workspace-tool-gateway-secrets.d.ts +27 -0
- package/dist/workspace-tool-gateway.d.ts +102 -6
- package/dist/workspace-tool-target-fence.d.ts +116 -0
- package/dist/workspace-tool-target.d.ts +225 -0
- package/dist/workspace-webhook-dispatch.d.ts +26 -0
- package/dist/xai-subscription-core.d.ts +60 -0
- package/package.json +25 -20
- package/src/access-grant-rls.ts +64 -14
- package/src/admission-trace.ts +14 -0
- package/src/app.ts +1684 -252
- package/src/artifact-catalog-cursor.ts +1 -0
- package/src/attached-browser-viewer.ts +26 -0
- package/src/auth/managed-auth-attempt-context.ts +9 -0
- package/src/auth/managed-auth-rate-limits.ts +252 -0
- package/src/auth/managed-auth-session-adapter.ts +64 -29
- package/src/auth/managed-auth.ts +303 -35
- package/src/auth/managed-email.ts +18 -1
- package/src/auth/managed-sign-in-notifications.ts +81 -0
- package/src/auth/new-signups-gate.ts +49 -0
- package/src/auth/organization-user-setup.ts +3 -3
- package/src/auth/signup-funnel-metrics.ts +164 -0
- package/src/azure-live.ts +103 -0
- package/src/browser-action-fences.ts +45 -0
- package/src/browser-controller-authority.ts +1 -186
- package/src/browser-ephemeral.ts +39 -0
- package/src/browser-lost-runtime-end.ts +71 -0
- package/src/browser-state-authority.ts +1 -236
- package/src/claude-default-models.ts +37 -0
- package/src/claude-subscription-core.ts +154 -0
- package/src/claude-subscription-oauth.ts +352 -0
- package/src/claude-subscription-profile.ts +73 -0
- package/src/claude-subscription-usage.ts +51 -0
- package/src/claude-workspace-connection.ts +82 -0
- package/src/codemode.ts +236 -39
- package/src/codex-realtime.ts +181 -53
- package/src/codex-session-pointers.ts +80 -0
- package/src/company-brain-okf.ts +1 -1
- package/src/connected-machine-computer-access.ts +2 -1
- package/src/connection-ownership.ts +19 -13
- package/src/controller-data-plane.ts +34 -0
- package/src/credit-promotion-snapshot.ts +36 -0
- package/src/editable-artifact-production.ts +10 -0
- package/src/editable-artifact-source-session.ts +123 -0
- package/src/editable-artifact-websocket.ts +16 -3
- package/src/fatal-process-boundary.ts +67 -3
- package/src/gateway-realtime.ts +12 -8
- package/src/github-access.ts +31 -5
- package/src/github-browser-flow.ts +1 -1
- package/src/http/acting-person.ts +105 -0
- package/src/http/api-contract-compat.ts +49 -0
- package/src/http/api-error.ts +210 -2
- package/src/http/auth.ts +68 -4
- package/src/http/cors.ts +37 -5
- package/src/http/deprecation.ts +118 -0
- package/src/http/keyed-admission.ts +29 -0
- package/src/http/local-browser-boundary.ts +343 -0
- package/src/http/metrics-listener.ts +57 -0
- package/src/http/path-identifier.ts +54 -0
- package/src/http/registered-route-label.ts +34 -0
- package/src/http/rejection-telemetry.ts +120 -0
- package/src/http/request-body.ts +127 -0
- package/src/http/request-source.ts +154 -13
- package/src/http/sse.ts +88 -28
- package/src/http/unmatched-route.ts +60 -0
- package/src/http/user-content.ts +158 -0
- package/src/http/variable-sets-removed.ts +122 -0
- package/src/http/websocket-upgrade-dispatch.ts +29 -0
- package/src/index.ts +107 -57
- package/src/integration-connect-metrics.ts +228 -0
- package/src/integration-endpoint-test.ts +291 -0
- package/src/integrations/api-integrations.ts +81 -7
- package/src/integrations/atlassian.ts +202 -83
- package/src/integrations/fiken.ts +66 -38
- package/src/integrations/github-app-connect.ts +64 -18
- package/src/integrations/github-lens-connect.ts +8 -29
- package/src/integrations/google-drive.ts +448 -152
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/marketplace-plugin.ts +1 -1
- package/src/integrations/oauth-client.ts +733 -193
- package/src/integrations/oauth-profiles.ts +116 -45
- package/src/integrations/onepassword-vault-provider.ts +16 -0
- package/src/integrations/personal-github-repositories.ts +1 -1
- package/src/integrations/personal-github.ts +61 -35
- package/src/integrations/provider-oauth.ts +67 -85
- package/src/integrations/slack-app-home.ts +5 -5
- package/src/integrations/slack-bot.ts +893 -201
- package/src/integrations/slack-file-upload-flow.ts +147 -0
- package/src/integrations/slack-install.ts +7 -7
- package/src/integrations/slack-interactions.ts +1589 -299
- package/src/integrations/slack-mrkdwn.ts +193 -0
- package/src/integrations/slack-routing.ts +2 -2
- package/src/integrations/slack-session-defaults.ts +224 -0
- package/src/integrations/slack-task-file-upload.ts +323 -0
- package/src/integrations/slack-tool-review.ts +65 -0
- package/src/integrations/social-oauth.ts +90 -58
- package/src/interaction-agent-access.ts +40 -20
- package/src/interaction-agent-version.ts +85 -0
- package/src/interaction-holder-heartbeat.ts +24 -8
- package/src/interaction-metrics.ts +164 -2
- package/src/interaction-route-stages.ts +94 -0
- package/src/interaction-secrets-vault.ts +213 -0
- package/src/interaction-secrets.ts +426 -0
- package/src/mcp/action-catalog-types.ts +14 -0
- package/src/mcp/action-catalog.gen.ts +8014 -0
- package/src/mcp/action-descriptions.ts +299 -0
- package/src/mcp/action-schema-imports.gen.ts +304 -0
- package/src/mcp/capability-account-readiness.ts +17 -0
- package/src/mcp/child-read-evidence.ts +19 -0
- package/src/mcp/company-profile-agent-admin.ts +13 -0
- package/src/mcp/contract-input.ts +165 -0
- package/src/mcp/documents.ts +9 -2
- package/src/mcp/editable-artifacts.ts +29 -11
- package/src/mcp/files.ts +5 -1
- package/src/mcp/first-party-tool-permissions.ts +7 -285
- package/src/mcp/knowledge-entries.ts +108 -34
- package/src/mcp/notification-tools.ts +480 -0
- package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
- package/src/mcp/scheduled-task-input.ts +89 -0
- package/src/mcp/scheduled-task-view.ts +63 -3
- package/src/mcp/secret-tools.ts +185 -0
- package/src/mcp/server.ts +2213 -1116
- package/src/mcp/session-admin-access-tools.ts +163 -0
- package/src/mcp/session-event-errors.ts +123 -0
- package/src/mcp/session-event-view.ts +395 -13
- package/src/mcp/session-view.ts +3 -0
- package/src/mcp/session-wait.ts +67 -7
- package/src/mcp/site-agent-clamp-tools.ts +140 -0
- package/src/mcp/site-data-tools.ts +339 -0
- package/src/mcp/site-publication-tools.ts +201 -0
- package/src/mcp/site-sql-tools.ts +427 -0
- package/src/mcp-oauth.ts +635 -45
- package/src/model-calls/chat-completions.ts +352 -0
- package/src/model-calls/pending-usage-settlements.ts +125 -0
- package/src/model-calls/service.ts +694 -0
- package/src/model-catalog.ts +68 -17
- package/src/native-push-dispatch.ts +457 -0
- package/src/observability.ts +15 -1
- package/src/organization-mcp.ts +485 -0
- package/src/prepared-mcp-actor.ts +154 -0
- package/src/prepared-mcp-permissions.ts +38 -0
- package/src/routes/analytics-consent.ts +123 -0
- package/src/routes/api-integrations.ts +268 -9
- package/src/routes/api-keys.ts +164 -25
- package/src/routes/artifact-catalog.ts +111 -0
- package/src/routes/automations.ts +163 -90
- package/src/routes/billing.ts +535 -59
- package/src/routes/browser-sessions.ts +966 -342
- package/src/routes/canonical-human-identities.ts +61 -0
- package/src/routes/capabilities.ts +112 -5
- package/src/routes/channels.ts +4 -3
- package/src/routes/chat-completions.ts +235 -0
- package/src/routes/claude-core-usage.ts +147 -0
- package/src/routes/claude-subscription-accounts.ts +233 -0
- package/src/routes/claude-subscription-oauth.ts +175 -0
- package/src/routes/client-errors.ts +394 -0
- package/src/routes/codex-core.ts +687 -0
- package/src/routes/codex.ts +1277 -1119
- package/src/routes/company-brain.ts +4 -0
- package/src/routes/computer-sessions.ts +319 -85
- package/src/routes/connect.ts +277 -175
- package/src/routes/connections.ts +690 -363
- package/src/routes/desktop-distribution.ts +217 -0
- package/src/routes/documents.ts +13 -7
- package/src/routes/editable-artifacts.ts +74 -4
- package/src/routes/enrollments.ts +110 -16
- package/src/routes/external-identity-links.ts +13 -12
- package/src/routes/files.ts +41 -16
- package/src/routes/github-browser-fonts.gen.ts +12 -0
- package/src/routes/github-browser-pages.ts +258 -0
- package/src/routes/github.ts +200 -75
- package/src/routes/inbox.ts +353 -0
- package/src/routes/insights-response-cache.ts +83 -0
- package/src/routes/insights-usage.ts +345 -0
- package/src/routes/insights.ts +103 -8
- package/src/routes/install.ts +51 -10
- package/src/routes/integration-facets.ts +70 -28
- package/src/routes/interaction-placement-message.ts +12 -0
- package/src/routes/knowledge.ts +136 -27
- package/src/routes/machines.ts +189 -135
- package/src/routes/managed-auth-session-sets.ts +43 -10
- package/src/routes/managed-onboarding.ts +39 -19
- package/src/routes/managed-sign-in-methods.ts +433 -0
- package/src/routes/memory-slack-publications.ts +3 -2
- package/src/routes/model-account-choice.ts +194 -0
- package/src/routes/model-account-settings.ts +235 -0
- package/src/routes/model-connection-access.ts +210 -48
- package/src/routes/native-app-auth.ts +296 -0
- package/src/routes/organization-integration-policy.ts +116 -0
- package/src/routes/organization-integrations.ts +326 -0
- package/src/routes/organization-mcp-connections.ts +183 -0
- package/src/routes/organization-memberships.ts +205 -19
- package/src/routes/organization-model-defaults.ts +110 -0
- package/src/routes/organization-model-providers.ts +74 -3
- package/src/routes/organization-service-accounts.ts +128 -0
- package/src/routes/organization-sessions.ts +21 -5
- package/src/routes/personal-api-keys.ts +184 -0
- package/src/routes/personal-github-git-broker.ts +2 -2
- package/src/routes/personal-github.ts +9 -4
- package/src/routes/plugins.ts +46 -7
- package/src/routes/pr-review-github.ts +105 -118
- package/src/routes/pr-review.ts +88 -82
- package/src/routes/preference-registry.ts +18 -0
- package/src/routes/rigs.ts +48 -13
- package/src/routes/scheduled-tasks.ts +347 -27
- package/src/routes/session-admin-access.ts +271 -0
- package/src/routes/session-artifact-associations.ts +164 -0
- package/src/routes/session-history-imports.ts +305 -0
- package/src/routes/sessions.ts +1682 -363
- package/src/routes/site-data.ts +340 -0
- package/src/routes/site-publishing.ts +584 -0
- package/src/routes/site-runtime.ts +1944 -0
- package/src/routes/site-sql.ts +263 -0
- package/src/routes/skill-content.ts +72 -7
- package/src/routes/skills.ts +5 -4
- package/src/routes/slack-bot-access.ts +108 -0
- package/src/routes/social.ts +27 -15
- package/src/routes/subscription-account-pools.ts +209 -0
- package/src/routes/subscription-core-http.ts +18 -0
- package/src/routes/subscription-pool-access.ts +180 -0
- package/src/routes/subscription-pool-core.ts +552 -0
- package/src/routes/supergrok.ts +174 -655
- package/src/routes/transcription-recordings.ts +71 -4
- package/src/routes/transcriptions.ts +26 -2
- package/src/routes/usage-allowances.ts +381 -0
- package/src/routes/variable-sets-removed.ts +19 -0
- package/src/routes/vault-providers.ts +782 -0
- package/src/routes/vaults.ts +421 -0
- package/src/routes/video-generation.ts +30 -12
- package/src/routes/workspace-artifacts.ts +165 -53
- package/src/routes/workspace-integrations.ts +576 -0
- package/src/routes/workspace-model-providers.ts +148 -0
- package/src/routes/workspace-state.ts +4 -0
- package/src/routes/workspaces.ts +625 -117
- package/src/sandbox/agent-update.ts +315 -0
- package/src/sandbox/auth-callout.ts +104 -21
- package/src/sandbox/channel-a.ts +150 -49
- package/src/sandbox/enrollment.ts +131 -22
- package/src/sandbox/machine-audience.ts +192 -0
- package/src/sandbox/machines.ts +75 -31
- package/src/sandbox/metrics-ingestion.ts +133 -2
- package/src/sandbox/rematerialize.ts +51 -21
- package/src/sandbox/screen-control.ts +289 -0
- package/src/sandbox/viewer.ts +33 -58
- package/src/sandbox-file-artifacts.ts +53 -27
- package/src/scheduled-task-blockers.ts +293 -0
- package/src/scheduled-task-deletion.ts +13 -3
- package/src/scheduled-task-lifecycle.ts +84 -0
- package/src/secret-answers.ts +119 -0
- package/src/session-create-observability.ts +46 -0
- package/src/session-realtime-context.ts +26 -4
- package/src/signed-state-secret.ts +58 -0
- package/src/site-agent-guard.ts +261 -0
- package/src/site-data/operations.ts +777 -0
- package/src/site-data/service.ts +1043 -0
- package/src/site-frame-guard.ts +107 -0
- package/src/site-publishing/agent-data-tools.ts +250 -0
- package/src/site-publishing/agent-model.ts +127 -0
- package/src/site-publishing/agent-session.ts +148 -0
- package/src/site-publishing/content-scanner.ts +93 -0
- package/src/site-publishing/edge-sync.ts +148 -0
- package/src/site-publishing/gateway-signature.ts +198 -0
- package/src/site-publishing/limits.ts +315 -0
- package/src/site-publishing/page-tool-authority.ts +111 -0
- package/src/site-publishing/platform-usage.ts +479 -0
- package/src/site-publishing/platform.ts +98 -0
- package/src/site-publishing/readiness.ts +330 -0
- package/src/site-publishing/runtime.ts +831 -0
- package/src/site-publishing/service.ts +1282 -0
- package/src/site-publishing/session-events.ts +262 -0
- package/src/site-publishing/session-views.ts +262 -0
- package/src/site-publishing/viewer.ts +75 -0
- package/src/site-session-origin.ts +91 -6
- package/src/site-sql/format.ts +134 -0
- package/src/site-sql/gateway.ts +168 -0
- package/src/site-sql/service.ts +1636 -0
- package/src/site-uploads.ts +27 -5
- package/src/temporal-schedule-sync.ts +74 -0
- package/src/transcription/billing-refusal.ts +14 -0
- package/src/transcription/normalize.ts +77 -0
- package/src/transcription/providers/azure-mai.ts +98 -0
- package/src/transcription/providers/azure-openai.ts +16 -7
- package/src/transcription/providers/codex-subscription.ts +190 -67
- package/src/transcription/providers/openai.ts +79 -5
- package/src/transcription/providers/subscription-core.ts +125 -0
- package/src/transcription/providers/xai-subscription.ts +81 -83
- package/src/transcription/segmenter.ts +7 -1
- package/src/transcription/service.ts +239 -10
- package/src/transcription/wav-duration.ts +39 -0
- package/src/workspace-deletion.ts +25 -1
- package/src/workspace-tool-gateway-attestations.ts +133 -0
- package/src/workspace-tool-gateway-observability.ts +64 -0
- package/src/workspace-tool-gateway-secrets.ts +104 -0
- package/src/workspace-tool-gateway.ts +921 -103
- package/src/workspace-tool-target-fence.ts +380 -0
- package/src/workspace-tool-target.ts +554 -0
- package/src/workspace-webhook-dispatch.ts +225 -0
- package/src/xai-realtime.ts +85 -81
- package/src/xai-subscription-core.ts +172 -0
- package/dist/browser-auth-broker.d.ts +0 -20
- package/dist/chunk-AJ62T5QN.js.map +0 -1
- package/dist/connection-authority-owner.d.ts +0 -59
- package/dist/routes/connection-authorities.d.ts +0 -3
- package/dist/routes/environments.d.ts +0 -5
- package/dist/routes/host-mcp-bindings.d.ts +0 -5
- package/dist/routes/host-mcp-resolvers.d.ts +0 -3
- package/dist/routes/packs.d.ts +0 -3
- package/dist/session-event-view-MU3HMNV3.js +0 -264
- package/dist/session-event-view-MU3HMNV3.js.map +0 -1
- package/dist/xai-subscription-auth.d.ts +0 -23
- package/src/browser-auth-broker.ts +0 -155
- package/src/connection-authority-owner.ts +0 -61
- package/src/routes/connection-authorities.ts +0 -139
- package/src/routes/environments.ts +0 -388
- package/src/routes/host-mcp-bindings.ts +0 -122
- package/src/routes/host-mcp-resolvers.ts +0 -41
- package/src/routes/packs.ts +0 -784
- package/src/xai-subscription-auth.ts +0 -139
package/src/routes/billing.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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:
|
|
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: "
|
|
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(
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
353
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: -
|
|
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",
|
|
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: -
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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 (
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|