@opengeni/api-router 3.0.1 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-OGQDXVXY.js +18 -0
- package/dist/chunk-OGQDXVXY.js.map +1 -0
- package/dist/{chunk-AJ62T5QN.js → chunk-OT56OY4V.js} +80503 -39869
- package/dist/chunk-OT56OY4V.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 +165 -55
- 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/codex.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { CodexRateLimitResetCreditsAccountResult } from "@opengeni/db";
|
|
2
|
+
import type { CodexResetRedemptionRecovery } from "@opengeni/db";
|
|
1
3
|
// Codex (ChatGPT) subscription connect / status / usage routes.
|
|
2
4
|
//
|
|
3
5
|
// Connect uses the device-code flow split into two stateless calls: `start`
|
|
@@ -7,20 +9,26 @@
|
|
|
7
9
|
// is added to isAuthExempt. Secrets never leave the server: status/usage read the
|
|
8
10
|
// decrypted token only to call the codex backend; the token is never returned.
|
|
9
11
|
|
|
10
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
environmentsEncryptionKeyBytes,
|
|
14
|
+
configuredModels,
|
|
15
|
+
getSettings,
|
|
16
|
+
productLabelForModelId,
|
|
17
|
+
withCodexCatalogProvider,
|
|
18
|
+
type Settings,
|
|
19
|
+
} from "@opengeni/config";
|
|
11
20
|
import {
|
|
12
21
|
accessTokenExpiry,
|
|
13
22
|
buildCodexUsageWindowFromCache,
|
|
14
23
|
CODEX_CLIENT_VERSION,
|
|
15
|
-
CODEX_FALLBACK_MODEL_SLUGS,
|
|
16
24
|
CODEX_FIVE_HOUR_WINDOW_SECONDS,
|
|
17
|
-
CODEX_MODEL_ID_PREFIX,
|
|
18
25
|
CODEX_PROVIDER_ID,
|
|
19
26
|
CODEX_WEEKLY_WINDOW_SECONDS,
|
|
20
27
|
CodexDeviceError,
|
|
28
|
+
CodexReloginRequired,
|
|
21
29
|
consumeCodexRateLimitResetCredit,
|
|
22
30
|
exchangeDeviceCode,
|
|
23
|
-
|
|
31
|
+
fetchCodexRateLimitResetCredits,
|
|
24
32
|
parseIdToken,
|
|
25
33
|
pollDeviceCode,
|
|
26
34
|
startDeviceCode,
|
|
@@ -32,49 +40,34 @@ import {
|
|
|
32
40
|
import {
|
|
33
41
|
abandonCodexResetRedemptionBeforeProvider,
|
|
34
42
|
adoptCodexResetRedemptionAttempt,
|
|
35
|
-
|
|
43
|
+
buildSubscriptionCoreCodexConnectionTokenResolver,
|
|
44
|
+
buildSubscriptionCoreCodexOperationFetch,
|
|
45
|
+
completeSubscriptionCoreCodexResetRedemption,
|
|
46
|
+
deliverSubscriptionCoreCodexWake,
|
|
47
|
+
fenceSubscriptionCoreCodexResetCredit,
|
|
48
|
+
fetchSubscriptionCoreCodexUsage,
|
|
49
|
+
getSubscriptionCoreCodexWorkspaceProjection,
|
|
50
|
+
listSubscriptionCoreCodexResetRedemptionRecoveries,
|
|
51
|
+
readSubscriptionCoreCodexResetAuthority,
|
|
52
|
+
resolveSubscriptionCoreCodexConnectionId,
|
|
53
|
+
subscriptionCoreCodexResetAuthority,
|
|
54
|
+
subscriptionCoreCodexResetCreditFence,
|
|
55
|
+
SubscriptionCoreCodexOperationUnavailableError,
|
|
56
|
+
withSessionRlsActorContext,
|
|
36
57
|
claimCodexResetRedemption,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
designateCodexAppsCredential,
|
|
40
|
-
disconnectAllCodexAccounts,
|
|
41
|
-
disconnectCodexAccount,
|
|
42
|
-
disconnectOrganizationCodexAccount,
|
|
58
|
+
assertOrganizationCodexAdministrator,
|
|
59
|
+
buildOrganizationCodexAdministratorTokenResolver,
|
|
43
60
|
encryptEnvironmentValue,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
fetchCodexUsageForAccount,
|
|
47
|
-
fetchCodexRateLimitResetCreditsForAccount,
|
|
61
|
+
findOrganizationCodexConnectionForAdministrator,
|
|
62
|
+
fetchOrganizationCodexUsageForAccount,
|
|
48
63
|
fenceCodexResetRedemptionSend,
|
|
49
64
|
getCodexResetRedemptionAttempt,
|
|
50
|
-
|
|
51
|
-
getCodexAppsSettings,
|
|
52
|
-
getCodexRotationSettings,
|
|
53
|
-
getOrganizationCodexRotationSettings,
|
|
54
|
-
getWorkspaceCodexSubscriptionSource,
|
|
55
|
-
listPendingCodexCapacityWakeTargets,
|
|
56
|
-
listCodexAccountStatuses,
|
|
57
|
-
listOrganizationCodexAccountStatuses,
|
|
58
|
-
listCodexResetRedemptionRecoveries,
|
|
65
|
+
getSubscriptionCoreOrganizationCodexProjection,
|
|
59
66
|
nestedPostgresSqlState,
|
|
60
67
|
releaseCodexResetRedemptionClaim,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
renameCodexAccount,
|
|
64
|
-
renameOrganizationCodexAccount,
|
|
65
|
-
setActiveCodexCredential,
|
|
66
|
-
setActiveOrganizationCodexCredential,
|
|
67
|
-
setInitialActiveCodexCredential,
|
|
68
|
-
setWorkspaceCodexSubscriptionMode,
|
|
69
|
-
setWorkspaceCodexSubscriptionModeInTransaction,
|
|
70
|
-
updateOrganizationCodexRotationSettings,
|
|
71
|
-
updateCodexRotationSettings,
|
|
72
|
-
upsertOrganizationCodexSubscriptionCredential,
|
|
73
|
-
upsertCodexSubscriptionCredential,
|
|
74
|
-
withCodexCapacityMutation,
|
|
75
|
-
withSessionCodexCapacityMutation,
|
|
68
|
+
resolveOrganizationCodexRedemptionWorkspace,
|
|
69
|
+
activeCodexPlanExclusions,
|
|
76
70
|
type CodexAccountStatus,
|
|
77
|
-
type CodexCapacityWakeTarget,
|
|
78
71
|
} from "@opengeni/db";
|
|
79
72
|
|
|
80
73
|
// The picker surfaces codex models under their own "no credits" provider group so
|
|
@@ -84,7 +77,7 @@ const CODEX_PROVIDER_LABEL = "Codex subscription · no credits";
|
|
|
84
77
|
// The wire shape for one Codex account (metadata only; never the secret column).
|
|
85
78
|
// P2: fiveHour/weekly ride along, built from the CACHED usage columns (zero
|
|
86
79
|
// provider calls, zero decrypts) so the bars render instantly off this read.
|
|
87
|
-
function codexAccountJson(
|
|
80
|
+
export function codexAccountJson(
|
|
88
81
|
row: CodexAccountStatus,
|
|
89
82
|
options: {
|
|
90
83
|
appsCredentialId?: string | null;
|
|
@@ -99,6 +92,20 @@ function codexAccountJson(
|
|
|
99
92
|
label: row.label,
|
|
100
93
|
email: row.accountEmail,
|
|
101
94
|
plan: row.planType,
|
|
95
|
+
planCheckedAt: row.planCheckedAt ?? null,
|
|
96
|
+
// The most recent observed plan change (for example "pro" before a move to
|
|
97
|
+
// "free"), kept as evidence until the plan changes again.
|
|
98
|
+
planChangedFrom: row.planPreviousType ?? null,
|
|
99
|
+
planChangedAt: row.planChangedAt ?? null,
|
|
100
|
+
// Models the CURRENT plan was proven not to include. Each leaves automatic
|
|
101
|
+
// selection until `retryAfter` (one request then re-checks it) or until a
|
|
102
|
+
// different plan is observed (refresh usage after an upgrade).
|
|
103
|
+
planExcludedModels: activeCodexPlanExclusions(row, new Date()).map((entry) => ({
|
|
104
|
+
model: entry.modelId,
|
|
105
|
+
label: productLabelForModelId(entry.modelId),
|
|
106
|
+
excludedAt: entry.excludedAt,
|
|
107
|
+
retryAfter: entry.expiresAt,
|
|
108
|
+
})),
|
|
102
109
|
status: row.status,
|
|
103
110
|
active: row.isActive,
|
|
104
111
|
expiresAt: row.expiresAt,
|
|
@@ -115,6 +122,9 @@ function codexAccountJson(
|
|
|
115
122
|
CODEX_WEEKLY_WINDOW_SECONDS,
|
|
116
123
|
),
|
|
117
124
|
usageCheckedAt: row.usageCheckedAt,
|
|
125
|
+
extraCreditsEnabled: row.extraCreditsEnabled ?? false,
|
|
126
|
+
extraCreditsVersion: row.extraCreditsVersion ?? 1,
|
|
127
|
+
extraCreditsUpdatedAt: row.extraCreditsUpdatedAt ?? null,
|
|
118
128
|
allocatorEnabled: row.allocatorEnabled,
|
|
119
129
|
allocatorVersion: row.allocatorVersion,
|
|
120
130
|
allocatorUpdatedAt: row.allocatorUpdatedAt,
|
|
@@ -122,6 +132,7 @@ function codexAccountJson(
|
|
|
122
132
|
resetCreditsCheckedAt: row.resetCreditsCheckedAt,
|
|
123
133
|
// P3 rotation cooldown: when set and in the future, this account is cooling-down.
|
|
124
134
|
exhaustedUntil: row.exhaustedUntil,
|
|
135
|
+
...(row.ownInWorkspaceIds ? { ownInWorkspaceIds: row.ownInWorkspaceIds } : {}),
|
|
125
136
|
appsDesignated: options.appsCredentialId === row.id,
|
|
126
137
|
canEnableApps:
|
|
127
138
|
row.source === "workspace" &&
|
|
@@ -186,27 +197,36 @@ export function codexWorkerReadiness(input: {
|
|
|
186
197
|
// The /codex/usage{,/refresh,/:id} wire wrapper: the rich normalized payload
|
|
187
198
|
// carries its own `status`, surfaced at the top level for back-compat with the
|
|
188
199
|
// existing CodexUsage = { status; usage } shape.
|
|
189
|
-
function codexUsageJson(payload: CodexUsagePayload): {
|
|
200
|
+
export function codexUsageJson(payload: CodexUsagePayload): {
|
|
190
201
|
status: CodexUsagePayload["status"];
|
|
191
202
|
usage: CodexUsagePayload;
|
|
192
203
|
} {
|
|
193
204
|
return { status: payload.status, usage: payload };
|
|
194
205
|
}
|
|
195
206
|
|
|
196
|
-
export function codexModelsForPicker(
|
|
207
|
+
export function codexModelsForPicker(
|
|
208
|
+
settings: Settings = getSettings(),
|
|
209
|
+
supportedSlugs?: readonly string[],
|
|
210
|
+
): Array<{
|
|
197
211
|
id: string;
|
|
198
212
|
label: string;
|
|
199
213
|
provider: string;
|
|
200
214
|
providerLabel: string;
|
|
201
215
|
api: "responses";
|
|
202
216
|
}> {
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
217
|
+
return configuredModels(withCodexCatalogProvider(settings))
|
|
218
|
+
.filter(
|
|
219
|
+
(model) =>
|
|
220
|
+
model.providerId === CODEX_PROVIDER_ID &&
|
|
221
|
+
(supportedSlugs === undefined || supportedSlugs.includes(model.upstreamModelId)),
|
|
222
|
+
)
|
|
223
|
+
.map((model) => ({
|
|
224
|
+
id: model.id,
|
|
225
|
+
label: model.label,
|
|
226
|
+
provider: CODEX_PROVIDER_ID,
|
|
227
|
+
providerLabel: CODEX_PROVIDER_LABEL,
|
|
228
|
+
api: "responses" as const,
|
|
229
|
+
}));
|
|
210
230
|
}
|
|
211
231
|
import { createSignedState, readSignedState } from "@opengeni/github";
|
|
212
232
|
import {
|
|
@@ -214,11 +234,44 @@ import {
|
|
|
214
234
|
hasPermission,
|
|
215
235
|
requireAccessGrant,
|
|
216
236
|
requireCanonicalLocalAccountAdministrator,
|
|
237
|
+
resolveCatalogSettings,
|
|
217
238
|
type ApiRouteDeps,
|
|
218
239
|
} from "@opengeni/core";
|
|
219
240
|
import type { Context, Hono } from "hono";
|
|
241
|
+
import {
|
|
242
|
+
codexRouteDisposition,
|
|
243
|
+
coreCodexUsage,
|
|
244
|
+
coreCodexUsageRefresh,
|
|
245
|
+
coreCodexAccounts,
|
|
246
|
+
coreCodexActivate,
|
|
247
|
+
coreCodexAllocator,
|
|
248
|
+
coreCodexExtraCredits,
|
|
249
|
+
coreCodexClearApps,
|
|
250
|
+
coreCodexConnected,
|
|
251
|
+
coreCodexDesignateApps,
|
|
252
|
+
coreCodexDisconnect,
|
|
253
|
+
coreCodexDisconnectAll,
|
|
254
|
+
coreCodexRename,
|
|
255
|
+
coreCodexSetRotation,
|
|
256
|
+
coreCodexSetSource,
|
|
257
|
+
coreCodexSource,
|
|
258
|
+
coreCodexStatus,
|
|
259
|
+
coreOrganizationCodexAccounts,
|
|
260
|
+
} from "./codex-core";
|
|
220
261
|
import { HTTPException } from "hono/http-exception";
|
|
262
|
+
import {
|
|
263
|
+
agentActingAsPerson,
|
|
264
|
+
agentActingAsPersonBeforeGrantCheck,
|
|
265
|
+
isAgentActingAsPerson,
|
|
266
|
+
organizationSettingsPermission,
|
|
267
|
+
requireNotAgent,
|
|
268
|
+
} from "../http/acting-person";
|
|
221
269
|
import * as z from "zod/v4";
|
|
270
|
+
import {
|
|
271
|
+
CodexSourceRequest,
|
|
272
|
+
SubscriptionAccountToggleRequest,
|
|
273
|
+
SubscriptionRotationSettingsRequest,
|
|
274
|
+
} from "@opengeni/contracts";
|
|
222
275
|
import {
|
|
223
276
|
hashCodexBrowserSession,
|
|
224
277
|
signCodexRedemptionConfirmation,
|
|
@@ -243,7 +296,16 @@ type ManagedCookieHuman = {
|
|
|
243
296
|
browserSessionHash: string;
|
|
244
297
|
};
|
|
245
298
|
|
|
246
|
-
|
|
299
|
+
/**
|
|
300
|
+
* The stand-in browser session for an agent acting as a person: stable per
|
|
301
|
+
* person, so the agent can confirm a usage limit reset it prepared, and never
|
|
302
|
+
* equal to a real browser session's hash.
|
|
303
|
+
*/
|
|
304
|
+
async function agentSessionHash(subjectId: string): Promise<string> {
|
|
305
|
+
return await hashCodexBrowserSession(`agent:${subjectId}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function managedCookieHuman(
|
|
247
309
|
c: Context,
|
|
248
310
|
deps: ApiRouteDeps,
|
|
249
311
|
): Promise<ManagedCookieHuman | null> {
|
|
@@ -267,14 +329,50 @@ async function managedCookieHuman(
|
|
|
267
329
|
};
|
|
268
330
|
}
|
|
269
331
|
|
|
332
|
+
/**
|
|
333
|
+
* The person in this browser, or an agent acting as them. Only for routes that
|
|
334
|
+
* then require the same person's workspace grant; provider sign-in steps keep
|
|
335
|
+
* managedCookieHuman. An agent has no browser session, so its hash never
|
|
336
|
+
* matches a sign-in started in one.
|
|
337
|
+
*/
|
|
338
|
+
export async function managedHumanOrAgent(
|
|
339
|
+
c: Context,
|
|
340
|
+
deps: ApiRouteDeps,
|
|
341
|
+
): Promise<ManagedCookieHuman | null> {
|
|
342
|
+
const human = await managedCookieHuman(c, deps);
|
|
343
|
+
if (human) return human;
|
|
344
|
+
const agent = agentActingAsPersonBeforeGrantCheck(c);
|
|
345
|
+
return agent
|
|
346
|
+
? {
|
|
347
|
+
subjectId: agent.subjectId,
|
|
348
|
+
browserSessionHash: await agentSessionHash(agent.subjectId),
|
|
349
|
+
}
|
|
350
|
+
: null;
|
|
351
|
+
}
|
|
352
|
+
|
|
270
353
|
export async function requireOrganizationCodexHuman(
|
|
271
354
|
c: Context,
|
|
272
355
|
deps: ApiRouteDeps,
|
|
273
356
|
organizationId: string,
|
|
357
|
+
options: {
|
|
358
|
+
/** A provider sign-in step: the person does it in the browser, never an agent. */
|
|
359
|
+
providerConsent?: boolean;
|
|
360
|
+
} = {},
|
|
274
361
|
): Promise<ManagedCookieHuman> {
|
|
362
|
+
if (options.providerConsent) requireNotAgent(c, "Signing in to a provider");
|
|
275
363
|
const parsed = z.string().uuid().safeParse(organizationId);
|
|
276
364
|
if (!parsed.success) throw new HTTPException(422, { message: "invalid organization id" });
|
|
277
365
|
let human = await managedCookieHuman(c, deps);
|
|
366
|
+
if (!human) {
|
|
367
|
+
const agent = agentActingAsPerson(c, parsed.data, organizationSettingsPermission(c));
|
|
368
|
+
// An agent has no browser session, so it never matches a sign-in started in one.
|
|
369
|
+
if (agent) {
|
|
370
|
+
human = {
|
|
371
|
+
subjectId: agent.subjectId,
|
|
372
|
+
browserSessionHash: await agentSessionHash(agent.subjectId),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
278
376
|
if (!human && deps.settings.productAccessMode === "local") {
|
|
279
377
|
const local = await requireCanonicalLocalAccountAdministrator(c, deps, organizationId);
|
|
280
378
|
human = {
|
|
@@ -288,7 +386,9 @@ export async function requireOrganizationCodexHuman(
|
|
|
288
386
|
});
|
|
289
387
|
}
|
|
290
388
|
try {
|
|
291
|
-
|
|
389
|
+
// The generic organization administrator check only: after 0680 the
|
|
390
|
+
// legacy organization Codex tables must not be read on this path.
|
|
391
|
+
await assertOrganizationCodexAdministrator(deps.db, {
|
|
292
392
|
organizationId,
|
|
293
393
|
actorSubjectId: human.subjectId,
|
|
294
394
|
});
|
|
@@ -307,24 +407,10 @@ export async function requireOrganizationCodexHuman(
|
|
|
307
407
|
return human;
|
|
308
408
|
}
|
|
309
409
|
|
|
310
|
-
async function requireWorkspaceCodexManagementSource(
|
|
311
|
-
deps: ApiRouteDeps,
|
|
312
|
-
workspaceId: string,
|
|
313
|
-
): Promise<void> {
|
|
314
|
-
const source = await getWorkspaceCodexSubscriptionSource(deps.db, workspaceId);
|
|
315
|
-
if (source.effectiveSource === "organization") {
|
|
316
|
-
throw new HTTPException(409, {
|
|
317
|
-
message: "this Codex subscription is managed in Organization settings",
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
if (source.effectiveSource === "disabled") {
|
|
321
|
-
throw new HTTPException(409, {
|
|
322
|
-
message: "Codex is disabled for this workspace",
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
410
|
export function requireSameOriginBrowserMutation(c: Context, deps: ApiRouteDeps): void {
|
|
411
|
+
// Built in process for an agent acting as a person: no browser credentials
|
|
412
|
+
// ride along, so there is no cross-site request to guard against.
|
|
413
|
+
if (isAgentActingAsPerson(c)) return;
|
|
328
414
|
const contentType = c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
329
415
|
if (contentType !== "application/json") {
|
|
330
416
|
throw new HTTPException(403, {
|
|
@@ -434,17 +520,29 @@ async function requireRedemptionHuman(
|
|
|
434
520
|
message: "reset redemption requires managed product mode",
|
|
435
521
|
});
|
|
436
522
|
}
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
523
|
+
// An agent the person signed in (organization MCP) acts as that person and
|
|
524
|
+
// may redeem, within the same exact-person grant check below. Its
|
|
525
|
+
// confirmation binds to a stable per-person agent hash, which can never equal
|
|
526
|
+
// a browser session hash.
|
|
527
|
+
const agent = isAgentActingAsPerson(c) ? agentActingAsPersonBeforeGrantCheck(c) : null;
|
|
528
|
+
if (!agent) {
|
|
529
|
+
// Normal managed auth prefers a bearer over a cookie. This irreversible
|
|
530
|
+
// route rejects the header before grant resolution so an API key or
|
|
531
|
+
// delegated token can never borrow a browser cookie that happens to ride
|
|
532
|
+
// along. Exact JSON content type plus Origin and Fetch Metadata fail closed.
|
|
533
|
+
if (c.req.header("authorization")) {
|
|
534
|
+
throw new HTTPException(403, {
|
|
535
|
+
message: "authorization bearer is not allowed for redemption",
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
requireSameOriginBrowserMutation(c, deps);
|
|
445
539
|
}
|
|
446
|
-
|
|
447
|
-
|
|
540
|
+
const human: ManagedCookieHuman | null = agent
|
|
541
|
+
? {
|
|
542
|
+
subjectId: agent.subjectId,
|
|
543
|
+
browserSessionHash: await agentSessionHash(agent.subjectId),
|
|
544
|
+
}
|
|
545
|
+
: await managedCookieHuman(c, deps);
|
|
448
546
|
if (!human) {
|
|
449
547
|
throw new HTTPException(401, {
|
|
450
548
|
message: "managed browser session required",
|
|
@@ -459,7 +557,7 @@ async function requireRedemptionHuman(
|
|
|
459
557
|
return { human, accountId: grant.accountId };
|
|
460
558
|
}
|
|
461
559
|
|
|
462
|
-
async function requireCodexAppsHuman(
|
|
560
|
+
export async function requireCodexAppsHuman(
|
|
463
561
|
c: Context,
|
|
464
562
|
deps: ApiRouteDeps,
|
|
465
563
|
workspaceId: string,
|
|
@@ -470,7 +568,7 @@ async function requireCodexAppsHuman(
|
|
|
470
568
|
});
|
|
471
569
|
}
|
|
472
570
|
requireSameOriginBrowserMutation(c, deps);
|
|
473
|
-
const human = await
|
|
571
|
+
const human = await managedHumanOrAgent(c, deps);
|
|
474
572
|
if (!human) {
|
|
475
573
|
throw new HTTPException(401, {
|
|
476
574
|
message: "managed browser session required",
|
|
@@ -591,35 +689,11 @@ type CodexRedemptionAccess = {
|
|
|
591
689
|
canClaimUnownedViaReconnect: boolean;
|
|
592
690
|
};
|
|
593
691
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}): CodexRedemptionAccess {
|
|
600
|
-
if (
|
|
601
|
-
input.managedHumanSubjectId === null ||
|
|
602
|
-
input.managedHumanSubjectId !== input.grantSubjectId
|
|
603
|
-
) {
|
|
604
|
-
return {
|
|
605
|
-
ownership: "managed_human_unavailable",
|
|
606
|
-
canClaimUnownedViaReconnect: false,
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
if (input.connectedBySubjectId === null) {
|
|
610
|
-
return {
|
|
611
|
-
ownership: "unowned",
|
|
612
|
-
canClaimUnownedViaReconnect: input.canManage,
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
return {
|
|
616
|
-
ownership:
|
|
617
|
-
input.connectedBySubjectId === input.managedHumanSubjectId
|
|
618
|
-
? "current_human"
|
|
619
|
-
: "different_human",
|
|
620
|
-
canClaimUnownedViaReconnect: false,
|
|
621
|
-
};
|
|
622
|
-
}
|
|
692
|
+
/** Provider reads for one overview account (shared-core organizations). */
|
|
693
|
+
type CodexOverviewSources = {
|
|
694
|
+
usage(connectionId: string): Promise<CodexUsagePayload>;
|
|
695
|
+
details(connectionId: string): Promise<CodexRateLimitResetCreditsAccountResult>;
|
|
696
|
+
};
|
|
623
697
|
|
|
624
698
|
async function fetchCodexAccountOverview(
|
|
625
699
|
deps: ApiRouteDeps,
|
|
@@ -628,25 +702,13 @@ async function fetchCodexAccountOverview(
|
|
|
628
702
|
redemptionAccess: CodexRedemptionAccess,
|
|
629
703
|
canRedeem: boolean,
|
|
630
704
|
canResumeRedemption: boolean,
|
|
631
|
-
redemptions:
|
|
632
|
-
providerCall: CodexProviderCall
|
|
705
|
+
redemptions: CodexResetRedemptionRecovery[],
|
|
706
|
+
providerCall: CodexProviderCall,
|
|
707
|
+
sources: CodexOverviewSources,
|
|
633
708
|
) {
|
|
634
|
-
const fetchImpl = (deps.codexFetch ?? fetch) as CodexFetch;
|
|
635
709
|
const [usageSettled, detailsSettled] = await Promise.allSettled([
|
|
636
|
-
providerCall(
|
|
637
|
-
|
|
638
|
-
await fetchCodexUsageForAccount(deps.db, deps.settings, workspaceId, row.id, fetchImpl),
|
|
639
|
-
),
|
|
640
|
-
providerCall(
|
|
641
|
-
async () =>
|
|
642
|
-
await fetchCodexRateLimitResetCreditsForAccount(
|
|
643
|
-
deps.db,
|
|
644
|
-
deps.settings,
|
|
645
|
-
workspaceId,
|
|
646
|
-
row.id,
|
|
647
|
-
fetchImpl,
|
|
648
|
-
),
|
|
649
|
-
),
|
|
710
|
+
providerCall(() => sources.usage(row.id)),
|
|
711
|
+
providerCall(() => sources.details(row.id)),
|
|
650
712
|
]);
|
|
651
713
|
const liveUsage = usageSettled.status === "fulfilled" ? usageSettled.value : null;
|
|
652
714
|
const cached = cachedUsage(row);
|
|
@@ -756,40 +818,6 @@ type CodexConnectState = {
|
|
|
756
818
|
iat?: number;
|
|
757
819
|
};
|
|
758
820
|
|
|
759
|
-
async function signalCodexCapacityTargets(
|
|
760
|
-
deps: ApiRouteDeps,
|
|
761
|
-
targets: CodexCapacityWakeTarget[],
|
|
762
|
-
): Promise<void> {
|
|
763
|
-
await Promise.allSettled(
|
|
764
|
-
targets.map((target) =>
|
|
765
|
-
deps.workflowClient.signalCodexCapacity
|
|
766
|
-
? deps.workflowClient.signalCodexCapacity({
|
|
767
|
-
accountId: target.accountId,
|
|
768
|
-
workspaceId: target.workspaceId,
|
|
769
|
-
sessionId: target.sessionId,
|
|
770
|
-
workflowId: target.workflowId,
|
|
771
|
-
wakeRevision: target.wakeRevision,
|
|
772
|
-
workflowWakeRevision: target.workflowWakeRevision,
|
|
773
|
-
})
|
|
774
|
-
: deps.workflowClient.wakeSessionWorkflow({
|
|
775
|
-
accountId: target.accountId,
|
|
776
|
-
workspaceId: target.workspaceId,
|
|
777
|
-
sessionId: target.sessionId,
|
|
778
|
-
workflowId: target.workflowId,
|
|
779
|
-
wakeRevision: target.workflowWakeRevision,
|
|
780
|
-
}),
|
|
781
|
-
),
|
|
782
|
-
);
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
async function signalPendingCodexCapacityTargets(
|
|
786
|
-
deps: ApiRouteDeps,
|
|
787
|
-
workspaceId: string,
|
|
788
|
-
): Promise<void> {
|
|
789
|
-
const targets = await listPendingCodexCapacityWakeTargets(deps.db, workspaceId).catch(() => []);
|
|
790
|
-
await signalCodexCapacityTargets(deps, targets);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
821
|
const CODEX_DEVICE_EXPIRY_SECONDS = 15 * 60; // the device code expires 15 min after start (spec §1.1)
|
|
794
822
|
|
|
795
823
|
export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
@@ -797,72 +825,139 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
797
825
|
|
|
798
826
|
app.get("/v1/workspaces/:workspaceId/codex/source", async (c) => {
|
|
799
827
|
const workspaceId = c.req.param("workspaceId");
|
|
800
|
-
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
801
|
-
|
|
828
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
829
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
830
|
+
|
|
831
|
+
return await coreCodexSource(c, deps, grant.accountId, workspaceId);
|
|
802
832
|
});
|
|
803
833
|
|
|
804
834
|
app.patch("/v1/workspaces/:workspaceId/codex/source", async (c) => {
|
|
805
835
|
const workspaceId = c.req.param("workspaceId");
|
|
806
836
|
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
807
|
-
const parsed =
|
|
808
|
-
.object({
|
|
809
|
-
mode: z.enum(["automatic", "workspace", "organization", "disabled"]),
|
|
810
|
-
})
|
|
811
|
-
.safeParse(await c.req.json().catch(() => null));
|
|
837
|
+
const parsed = CodexSourceRequest.safeParse(await c.req.json().catch(() => null));
|
|
812
838
|
if (!parsed.success) {
|
|
813
839
|
throw new HTTPException(400, {
|
|
814
840
|
message: "a valid Codex source mode is required",
|
|
815
841
|
});
|
|
816
842
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
);
|
|
826
|
-
} catch (error) {
|
|
827
|
-
if (
|
|
828
|
-
error instanceof Error &&
|
|
829
|
-
(error.message.includes("personal workspaces") ||
|
|
830
|
-
error.message.includes("active turns are using it"))
|
|
831
|
-
) {
|
|
832
|
-
throw new HTTPException(409, { message: error.message });
|
|
833
|
-
}
|
|
834
|
-
throw error;
|
|
835
|
-
}
|
|
843
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
844
|
+
|
|
845
|
+
return await coreCodexSetSource(c, deps, {
|
|
846
|
+
accountId: grant.accountId,
|
|
847
|
+
workspaceId,
|
|
848
|
+
subjectId: grant.subjectId,
|
|
849
|
+
mode: parsed.data.mode,
|
|
850
|
+
});
|
|
836
851
|
});
|
|
837
852
|
|
|
838
853
|
app.get("/v1/organizations/:organizationId/codex/accounts", async (c) => {
|
|
839
854
|
const organizationId = c.req.param("organizationId");
|
|
840
855
|
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
856
|
+
await codexRouteDisposition(deps, organizationId);
|
|
857
|
+
|
|
858
|
+
return await coreOrganizationCodexAccounts(c, deps, organizationId, human.subjectId);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
app.get("/v1/organizations/:organizationId/codex/accounts/:accountId/usage", async (c) => {
|
|
862
|
+
const organizationId = c.req.param("organizationId");
|
|
863
|
+
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
864
|
+
const mode = await codexRouteDisposition(deps, organizationId);
|
|
865
|
+
const credentialId = c.req.param("accountId");
|
|
866
|
+
if (!z.string().uuid().safeParse(credentialId).success) {
|
|
867
|
+
throw new HTTPException(404, { message: "codex account not found" });
|
|
868
|
+
}
|
|
869
|
+
const usage = await fetchOrganizationCodexUsageForAccount(
|
|
870
|
+
db,
|
|
871
|
+
settings,
|
|
872
|
+
{
|
|
847
873
|
organizationId,
|
|
848
874
|
actorSubjectId: human.subjectId,
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
return c.json({
|
|
852
|
-
accounts: accounts.map((account) => codexAccountJson(account)),
|
|
853
|
-
activeAccountId: rotation?.activeCredentialId ?? null,
|
|
854
|
-
settings: {
|
|
855
|
-
rotationEnabled: rotation?.rotationEnabled ?? false,
|
|
856
|
-
rotationStrategy: "sharded",
|
|
857
|
-
activeCredentialId: rotation?.activeCredentialId ?? null,
|
|
875
|
+
credentialId,
|
|
876
|
+
mode,
|
|
858
877
|
},
|
|
859
|
-
|
|
878
|
+
deps.codexFetch ?? fetch,
|
|
879
|
+
);
|
|
880
|
+
return c.json(codexUsageJson(usage));
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
// One organization account's usage and usage limit resets, for its page on
|
|
884
|
+
// the organization's Models page (any workspace use not required).
|
|
885
|
+
app.get("/v1/organizations/:organizationId/codex/accounts/:accountId/overview", async (c) => {
|
|
886
|
+
const organizationId = c.req.param("organizationId");
|
|
887
|
+
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
888
|
+
return await coreOrganizationCodexOverview(
|
|
889
|
+
c,
|
|
890
|
+
deps,
|
|
891
|
+
organizationId,
|
|
892
|
+
human,
|
|
893
|
+
c.req.param("accountId"),
|
|
894
|
+
);
|
|
860
895
|
});
|
|
861
896
|
|
|
897
|
+
// Redeem an organization account's usage limit reset from the organization's
|
|
898
|
+
// Models page: the same HMAC confirmation, single-use ledger fences and
|
|
899
|
+
// ambiguous-outcome recovery as a workspace page, for an organization
|
|
900
|
+
// administrator in their own browser or an agent acting as them.
|
|
901
|
+
app.post(
|
|
902
|
+
"/v1/organizations/:organizationId/codex/accounts/:accountId/reset-credits/prepare",
|
|
903
|
+
async (c) => {
|
|
904
|
+
const organizationId = c.req.param("organizationId");
|
|
905
|
+
const credentialId = c.req.param("accountId");
|
|
906
|
+
const { human, workspaceId } = await requireOrganizationRedemptionHuman(
|
|
907
|
+
c,
|
|
908
|
+
deps,
|
|
909
|
+
organizationId,
|
|
910
|
+
);
|
|
911
|
+
await codexRouteDisposition(deps, organizationId);
|
|
912
|
+
return await coreCodexResetPrepare(
|
|
913
|
+
c,
|
|
914
|
+
deps,
|
|
915
|
+
{ human, accountId: organizationId, workspaceId, credentialId },
|
|
916
|
+
() =>
|
|
917
|
+
organizationRedemptionTarget(deps, {
|
|
918
|
+
organizationId,
|
|
919
|
+
subjectId: human.subjectId,
|
|
920
|
+
credentialId,
|
|
921
|
+
}),
|
|
922
|
+
);
|
|
923
|
+
},
|
|
924
|
+
);
|
|
925
|
+
|
|
926
|
+
app.post(
|
|
927
|
+
"/v1/organizations/:organizationId/codex/accounts/:accountId/reset-credits/redeem",
|
|
928
|
+
async (c) => {
|
|
929
|
+
const organizationId = c.req.param("organizationId");
|
|
930
|
+
const credentialId = c.req.param("accountId");
|
|
931
|
+
const { human, workspaceId } = await requireOrganizationRedemptionHuman(
|
|
932
|
+
c,
|
|
933
|
+
deps,
|
|
934
|
+
organizationId,
|
|
935
|
+
);
|
|
936
|
+
await codexRouteDisposition(deps, organizationId);
|
|
937
|
+
return await coreCodexResetRedeem(
|
|
938
|
+
c,
|
|
939
|
+
deps,
|
|
940
|
+
{ human, accountId: organizationId, workspaceId, credentialId },
|
|
941
|
+
() =>
|
|
942
|
+
organizationRedemptionTarget(deps, {
|
|
943
|
+
organizationId,
|
|
944
|
+
subjectId: human.subjectId,
|
|
945
|
+
credentialId,
|
|
946
|
+
}),
|
|
947
|
+
);
|
|
948
|
+
},
|
|
949
|
+
);
|
|
950
|
+
|
|
862
951
|
app.post("/v1/organizations/:organizationId/codex/connect/start", async (c) => {
|
|
863
952
|
const organizationId = c.req.param("organizationId");
|
|
864
953
|
requireSameOriginBrowserMutation(c, deps);
|
|
954
|
+
// Device sign-in accepts an agent acting as an admin, as the workspace
|
|
955
|
+
// route does: the agent relays the short code and the person still
|
|
956
|
+
// approves at OpenAI themselves. The state binds the same person.
|
|
865
957
|
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
958
|
+
// The device-code start touches no account state: public compatibility
|
|
959
|
+
// (a disabled cutover still fails closed).
|
|
960
|
+
await codexRouteDisposition(deps, organizationId);
|
|
866
961
|
let start: Awaited<ReturnType<typeof startDeviceCode>>;
|
|
867
962
|
try {
|
|
868
963
|
start = await startDeviceCode();
|
|
@@ -889,6 +984,7 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
889
984
|
const organizationId = c.req.param("organizationId");
|
|
890
985
|
requireSameOriginBrowserMutation(c, deps);
|
|
891
986
|
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
987
|
+
await codexRouteDisposition(deps, organizationId);
|
|
892
988
|
const { state } = (await c.req.json().catch(() => null)) as {
|
|
893
989
|
state?: string;
|
|
894
990
|
};
|
|
@@ -943,53 +1039,32 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
943
1039
|
});
|
|
944
1040
|
}
|
|
945
1041
|
const id = parseIdToken(tokens.idToken);
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
scopes: null,
|
|
1042
|
+
const credentialEncrypted = encryptEnvironmentValue(
|
|
1043
|
+
key,
|
|
1044
|
+
JSON.stringify({
|
|
1045
|
+
access_token: tokens.accessToken,
|
|
1046
|
+
refresh_token: tokens.refreshToken,
|
|
1047
|
+
id_token: tokens.idToken,
|
|
1048
|
+
}),
|
|
1049
|
+
);
|
|
1050
|
+
await codexRouteDisposition(deps, organizationId);
|
|
1051
|
+
|
|
1052
|
+
return await coreCodexConnected(c, deps, {
|
|
1053
|
+
accountId: organizationId,
|
|
1054
|
+
workspaceId: null,
|
|
1055
|
+
subjectId: human.subjectId,
|
|
1056
|
+
credential: {
|
|
1057
|
+
credentialEncrypted,
|
|
1058
|
+
providerAccountId: id.chatgptAccountId,
|
|
1059
|
+
providerSubjectId: id.chatgptUserId,
|
|
965
1060
|
planType: id.planType,
|
|
966
1061
|
isFedramp: id.isFedramp,
|
|
967
1062
|
expiresAt: accessTokenExpiry(tokens.accessToken),
|
|
968
1063
|
lastRefreshAt: new Date(),
|
|
969
1064
|
accountEmail: id.email ?? null,
|
|
970
1065
|
label: id.email ?? id.chatgptAccountId ?? null,
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
const cause = (error as { cause?: unknown } | null)?.cause;
|
|
974
|
-
const message =
|
|
975
|
-
cause instanceof Error ? cause.message : error instanceof Error ? error.message : "";
|
|
976
|
-
if (message.includes("active turns are using it")) {
|
|
977
|
-
throw new HTTPException(409, {
|
|
978
|
-
message: "Codex subscription source cannot change while active turns are using it",
|
|
979
|
-
});
|
|
980
|
-
}
|
|
981
|
-
throw error;
|
|
982
|
-
}
|
|
983
|
-
await signalCodexCapacityTargets(deps, upserted.wakeTargets);
|
|
984
|
-
const rotation = await getOrganizationCodexRotationSettings(db, {
|
|
985
|
-
organizationId,
|
|
986
|
-
actorSubjectId: human.subjectId,
|
|
987
|
-
});
|
|
988
|
-
return c.json({
|
|
989
|
-
status: "connected",
|
|
990
|
-
plan: id.planType,
|
|
991
|
-
accountId: upserted.id,
|
|
992
|
-
isActive: rotation?.activeCredentialId === upserted.id,
|
|
1066
|
+
connectedBySubjectId: (await managedCookieHuman(c, deps))?.subjectId ?? null,
|
|
1067
|
+
},
|
|
993
1068
|
});
|
|
994
1069
|
});
|
|
995
1070
|
|
|
@@ -998,42 +1073,69 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
998
1073
|
requireSameOriginBrowserMutation(c, deps);
|
|
999
1074
|
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
1000
1075
|
const credentialId = c.req.param("accountId");
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1076
|
+
await codexRouteDisposition(deps, organizationId);
|
|
1077
|
+
|
|
1078
|
+
return await coreCodexActivate(
|
|
1079
|
+
c,
|
|
1080
|
+
deps,
|
|
1081
|
+
{ accountId: organizationId, workspaceId: null, subjectId: human.subjectId },
|
|
1004
1082
|
credentialId,
|
|
1005
|
-
|
|
1006
|
-
if (!activation.activated) {
|
|
1007
|
-
throw new HTTPException(404, { message: "codex account not found" });
|
|
1008
|
-
}
|
|
1009
|
-
await signalCodexCapacityTargets(deps, activation.wakeTargets);
|
|
1010
|
-
return c.json({ activated: true, accountId: credentialId });
|
|
1083
|
+
);
|
|
1011
1084
|
});
|
|
1012
1085
|
|
|
1013
1086
|
app.patch("/v1/organizations/:organizationId/codex/settings", async (c) => {
|
|
1014
1087
|
const organizationId = c.req.param("organizationId");
|
|
1015
1088
|
requireSameOriginBrowserMutation(c, deps);
|
|
1016
1089
|
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
1017
|
-
const parsed =
|
|
1018
|
-
.
|
|
1019
|
-
|
|
1090
|
+
const parsed = SubscriptionRotationSettingsRequest.safeParse(
|
|
1091
|
+
await c.req.json().catch(() => null),
|
|
1092
|
+
);
|
|
1020
1093
|
if (!parsed.success) {
|
|
1021
1094
|
throw new HTTPException(400, { message: "rotationEnabled is required" });
|
|
1022
1095
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1096
|
+
await codexRouteDisposition(deps, organizationId);
|
|
1097
|
+
|
|
1098
|
+
return await coreCodexSetRotation(
|
|
1099
|
+
c,
|
|
1100
|
+
deps,
|
|
1101
|
+
{ accountId: organizationId, workspaceId: null, subjectId: human.subjectId },
|
|
1102
|
+
parsed.data.rotationEnabled,
|
|
1103
|
+
);
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
app.patch("/v1/organizations/:organizationId/codex/accounts/:accountId/allocator", async (c) => {
|
|
1107
|
+
const organizationId = c.req.param("organizationId");
|
|
1108
|
+
requireSameOriginBrowserMutation(c, deps);
|
|
1109
|
+
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
1110
|
+
const parsed = SubscriptionAccountToggleRequest.safeParse(await c.req.json().catch(() => null));
|
|
1111
|
+
if (!parsed.success) {
|
|
1112
|
+
throw new HTTPException(400, { message: "enabled and expectedVersion are required" });
|
|
1113
|
+
}
|
|
1114
|
+
const admin = { accountId: organizationId, workspaceId: null, subjectId: human.subjectId };
|
|
1115
|
+
await codexRouteDisposition(deps, organizationId);
|
|
1116
|
+
|
|
1117
|
+
return await coreCodexAllocator(c, deps, admin, c.req.param("accountId"), parsed.data);
|
|
1035
1118
|
});
|
|
1036
1119
|
|
|
1120
|
+
app.patch(
|
|
1121
|
+
"/v1/organizations/:organizationId/codex/accounts/:accountId/extra-credits",
|
|
1122
|
+
async (c) => {
|
|
1123
|
+
const organizationId = c.req.param("organizationId");
|
|
1124
|
+
requireSameOriginBrowserMutation(c, deps);
|
|
1125
|
+
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
1126
|
+
const parsed = SubscriptionAccountToggleRequest.safeParse(
|
|
1127
|
+
await c.req.json().catch(() => null),
|
|
1128
|
+
);
|
|
1129
|
+
if (!parsed.success) {
|
|
1130
|
+
throw new HTTPException(400, { message: "enabled and expectedVersion are required" });
|
|
1131
|
+
}
|
|
1132
|
+
const admin = { accountId: organizationId, workspaceId: null, subjectId: human.subjectId };
|
|
1133
|
+
await codexRouteDisposition(deps, organizationId);
|
|
1134
|
+
|
|
1135
|
+
return await coreCodexExtraCredits(c, deps, admin, c.req.param("accountId"), parsed.data);
|
|
1136
|
+
},
|
|
1137
|
+
);
|
|
1138
|
+
|
|
1037
1139
|
app.patch("/v1/organizations/:organizationId/codex/accounts/:accountId", async (c) => {
|
|
1038
1140
|
const organizationId = c.req.param("organizationId");
|
|
1039
1141
|
requireSameOriginBrowserMutation(c, deps);
|
|
@@ -1041,48 +1143,28 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1041
1143
|
const body = (await c.req.json().catch(() => null)) as {
|
|
1042
1144
|
label?: unknown;
|
|
1043
1145
|
} | null;
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
actorSubjectId: human.subjectId,
|
|
1054
|
-
});
|
|
1055
|
-
const row = accounts.find((account) => account.id === c.req.param("accountId"));
|
|
1056
|
-
if (!row) throw new HTTPException(404, { message: "codex account not found" });
|
|
1057
|
-
return c.json(codexAccountJson(row));
|
|
1146
|
+
await codexRouteDisposition(deps, organizationId);
|
|
1147
|
+
|
|
1148
|
+
return await coreCodexRename(
|
|
1149
|
+
c,
|
|
1150
|
+
deps,
|
|
1151
|
+
{ accountId: organizationId, workspaceId: null, subjectId: human.subjectId },
|
|
1152
|
+
c.req.param("accountId"),
|
|
1153
|
+
typeof body?.label === "string" ? body.label : null,
|
|
1154
|
+
);
|
|
1058
1155
|
});
|
|
1059
1156
|
|
|
1060
1157
|
app.delete("/v1/organizations/:organizationId/codex/accounts/:accountId", async (c) => {
|
|
1061
1158
|
const organizationId = c.req.param("organizationId");
|
|
1062
1159
|
requireSameOriginBrowserMutation(c, deps);
|
|
1063
1160
|
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
} catch (error) {
|
|
1072
|
-
const cause = (error as { cause?: unknown } | null)?.cause;
|
|
1073
|
-
const message =
|
|
1074
|
-
cause instanceof Error ? cause.message : error instanceof Error ? error.message : "";
|
|
1075
|
-
if (message.includes("active turns are using it")) {
|
|
1076
|
-
throw new HTTPException(409, {
|
|
1077
|
-
message: "Codex subscription cannot disconnect while active turns are using it",
|
|
1078
|
-
});
|
|
1079
|
-
}
|
|
1080
|
-
throw error;
|
|
1081
|
-
}
|
|
1082
|
-
await signalCodexCapacityTargets(deps, result.wakeTargets);
|
|
1083
|
-
return c.json({
|
|
1084
|
-
disconnected: result.removed,
|
|
1085
|
-
newActiveId: result.newActiveCredentialId,
|
|
1161
|
+
await codexRouteDisposition(deps, organizationId);
|
|
1162
|
+
|
|
1163
|
+
return await coreCodexDisconnect(c, deps, {
|
|
1164
|
+
accountId: organizationId,
|
|
1165
|
+
workspaceId: null,
|
|
1166
|
+
subjectId: human.subjectId,
|
|
1167
|
+
connectionId: c.req.param("accountId"),
|
|
1086
1168
|
});
|
|
1087
1169
|
});
|
|
1088
1170
|
|
|
@@ -1090,7 +1172,10 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1090
1172
|
// signed state that carries the device_auth_id back to `poll`.
|
|
1091
1173
|
app.post("/v1/workspaces/:workspaceId/codex/connect/start", async (c) => {
|
|
1092
1174
|
const workspaceId = c.req.param("workspaceId");
|
|
1093
|
-
await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
1175
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
1176
|
+
if (await managedCookieHuman(c, deps)) requireSameOriginBrowserMutation(c, deps);
|
|
1177
|
+
// The device-code start touches no account state.
|
|
1178
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
1094
1179
|
let start: Awaited<ReturnType<typeof startDeviceCode>>;
|
|
1095
1180
|
try {
|
|
1096
1181
|
start = await startDeviceCode();
|
|
@@ -1102,6 +1187,7 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1102
1187
|
}
|
|
1103
1188
|
const state = createSignedState(githubStateSecret, {
|
|
1104
1189
|
workspaceId,
|
|
1190
|
+
actorSubjectId: grant.subjectId,
|
|
1105
1191
|
deviceAuthId: start.deviceAuthId,
|
|
1106
1192
|
userCode: start.userCode,
|
|
1107
1193
|
});
|
|
@@ -1117,6 +1203,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1117
1203
|
app.post("/v1/workspaces/:workspaceId/codex/connect/poll", async (c) => {
|
|
1118
1204
|
const workspaceId = c.req.param("workspaceId");
|
|
1119
1205
|
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
1206
|
+
const connectingHuman = await managedCookieHuman(c, deps);
|
|
1207
|
+
if (connectingHuman) requireSameOriginBrowserMutation(c, deps);
|
|
1208
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
1120
1209
|
const { state } = (await c.req.json()) as { state?: string };
|
|
1121
1210
|
const payload = (state
|
|
1122
1211
|
? readSignedState(state, githubStateSecret)
|
|
@@ -1124,6 +1213,7 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1124
1213
|
if (
|
|
1125
1214
|
!payload ||
|
|
1126
1215
|
payload.workspaceId !== workspaceId ||
|
|
1216
|
+
payload.actorSubjectId !== grant.subjectId ||
|
|
1127
1217
|
!payload.deviceAuthId ||
|
|
1128
1218
|
!payload.userCode
|
|
1129
1219
|
) {
|
|
@@ -1170,22 +1260,23 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1170
1260
|
});
|
|
1171
1261
|
}
|
|
1172
1262
|
const id = parseIdToken(tokens.idToken);
|
|
1173
|
-
const connectingHuman = await managedCookieHuman(c, deps);
|
|
1174
1263
|
const key = environmentsEncryptionKeyBytes(settings);
|
|
1175
1264
|
if (!key) {
|
|
1176
1265
|
throw new HTTPException(500, {
|
|
1177
1266
|
message: "OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured",
|
|
1178
1267
|
});
|
|
1179
1268
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1269
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
1270
|
+
|
|
1271
|
+
// The core writer decides authority from the grant's subject: an
|
|
1272
|
+
// organization administrator (new or reconnected shared account), a
|
|
1273
|
+
// workspace administrator (reconnect of an account this workspace
|
|
1274
|
+
// manages), or the person in their own Personal workspace.
|
|
1275
|
+
return await coreCodexConnected(c, deps, {
|
|
1276
|
+
accountId: grant.accountId,
|
|
1277
|
+
workspaceId,
|
|
1278
|
+
subjectId: grant.subjectId,
|
|
1279
|
+
credential: {
|
|
1189
1280
|
credentialEncrypted: encryptEnvironmentValue(
|
|
1190
1281
|
key,
|
|
1191
1282
|
JSON.stringify({
|
|
@@ -1194,59 +1285,16 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1194
1285
|
id_token: tokens.idToken,
|
|
1195
1286
|
}),
|
|
1196
1287
|
),
|
|
1197
|
-
|
|
1198
|
-
|
|
1288
|
+
providerAccountId: id.chatgptAccountId,
|
|
1289
|
+
providerSubjectId: id.chatgptUserId,
|
|
1199
1290
|
planType: id.planType,
|
|
1200
1291
|
isFedramp: id.isFedramp,
|
|
1201
1292
|
expiresAt: accessTokenExpiry(tokens.accessToken),
|
|
1202
1293
|
lastRefreshAt: new Date(),
|
|
1203
1294
|
accountEmail: id.email ?? null,
|
|
1204
1295
|
label: id.email ?? id.chatgptAccountId ?? null,
|
|
1205
|
-
connectedBySubjectId:
|
|
1206
|
-
|
|
1207
|
-
});
|
|
1208
|
-
if (upserted.kind === "unresolved_redemption") {
|
|
1209
|
-
return { result: { upserted, isActive: false }, changed: false };
|
|
1210
|
-
}
|
|
1211
|
-
await ensureCodexRotationSettings(tx, grant.accountId, workspaceId);
|
|
1212
|
-
await setInitialActiveCodexCredential(tx, workspaceId, upserted.id);
|
|
1213
|
-
await setWorkspaceCodexSubscriptionModeInTransaction(tx, {
|
|
1214
|
-
accountId: grant.accountId,
|
|
1215
|
-
workspaceId,
|
|
1216
|
-
subjectId: grant.subjectId,
|
|
1217
|
-
mode: "workspace",
|
|
1218
|
-
effectiveSourceBeforeMutation: sourceBeforeConnect.effectiveSource,
|
|
1219
|
-
});
|
|
1220
|
-
const rotation = await getCodexRotationSettings(tx, workspaceId);
|
|
1221
|
-
return {
|
|
1222
|
-
result: {
|
|
1223
|
-
upserted,
|
|
1224
|
-
isActive: rotation?.activeCredentialId === upserted.id,
|
|
1225
|
-
},
|
|
1226
|
-
changed: true,
|
|
1227
|
-
};
|
|
1228
|
-
}).catch((error: unknown) => {
|
|
1229
|
-
const cause = (error as { cause?: unknown } | null)?.cause;
|
|
1230
|
-
const message =
|
|
1231
|
-
cause instanceof Error ? cause.message : error instanceof Error ? error.message : "";
|
|
1232
|
-
if (message.includes("active turns are using it")) {
|
|
1233
|
-
throw new HTTPException(409, { message });
|
|
1234
|
-
}
|
|
1235
|
-
throw error;
|
|
1236
|
-
});
|
|
1237
|
-
const { upserted, isActive } = mutation.result;
|
|
1238
|
-
if (upserted.kind === "unresolved_redemption") {
|
|
1239
|
-
throw new HTTPException(409, {
|
|
1240
|
-
message:
|
|
1241
|
-
"this subscription has an unresolved reset redemption; recover it before changing ownership",
|
|
1242
|
-
});
|
|
1243
|
-
}
|
|
1244
|
-
await signalCodexCapacityTargets(deps, mutation.wakeTargets);
|
|
1245
|
-
return c.json({
|
|
1246
|
-
status: "connected",
|
|
1247
|
-
plan: id.planType,
|
|
1248
|
-
accountId: upserted.id,
|
|
1249
|
-
isActive,
|
|
1296
|
+
connectedBySubjectId: connectingHuman?.subjectId ?? null,
|
|
1297
|
+
},
|
|
1250
1298
|
});
|
|
1251
1299
|
});
|
|
1252
1300
|
|
|
@@ -1254,232 +1302,77 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1254
1302
|
// the token is accepted). Never runs a generation. Never returns the token.
|
|
1255
1303
|
app.get("/v1/workspaces/:workspaceId/codex/status", async (c) => {
|
|
1256
1304
|
const workspaceId = c.req.param("workspaceId");
|
|
1257
|
-
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
1258
|
-
|
|
1259
|
-
getCodexCredentialStatus(db, workspaceId),
|
|
1260
|
-
listCodexAccountStatuses(db, workspaceId),
|
|
1261
|
-
getWorkspaceCodexSubscriptionSource(db, workspaceId),
|
|
1262
|
-
getCodexRotationSettings(db, workspaceId),
|
|
1263
|
-
]);
|
|
1264
|
-
const activeRow = accounts.find((account) => account.id === status?.credentialId) ?? null;
|
|
1265
|
-
const activeAccount = activeRow
|
|
1266
|
-
? {
|
|
1267
|
-
id: activeRow.id,
|
|
1268
|
-
label:
|
|
1269
|
-
activeRow.label ??
|
|
1270
|
-
activeRow.accountEmail ??
|
|
1271
|
-
activeRow.planType ??
|
|
1272
|
-
activeRow.chatgptAccountId,
|
|
1273
|
-
chatgptAccountId: activeRow.chatgptAccountId,
|
|
1274
|
-
}
|
|
1275
|
-
: null;
|
|
1276
|
-
const activeCredentialId = status?.credentialId ?? rotation?.activeCredentialId ?? null;
|
|
1277
|
-
const readiness = codexWorkerReadiness({
|
|
1278
|
-
effectiveSource: source.effectiveSource,
|
|
1279
|
-
rotationEnabled: rotation?.rotationEnabled ?? false,
|
|
1280
|
-
activeCredentialId,
|
|
1281
|
-
accounts,
|
|
1282
|
-
now: new Date(),
|
|
1283
|
-
});
|
|
1284
|
-
let valid = false;
|
|
1285
|
-
const models = codexModelsForPicker();
|
|
1286
|
-
let catalogError: string | null = null;
|
|
1287
|
-
try {
|
|
1288
|
-
const cred = status?.credentialId
|
|
1289
|
-
? await loadCodexCredentialForRun(db, settings, workspaceId, status.credentialId)
|
|
1290
|
-
: null;
|
|
1291
|
-
if (cred) {
|
|
1292
|
-
const live = await fetchCodexModels({
|
|
1293
|
-
accessToken: cred.tokens.accessToken,
|
|
1294
|
-
chatgptAccountId: cred.chatgptAccountId,
|
|
1295
|
-
isFedramp: cred.isFedramp,
|
|
1296
|
-
clientVersion: CODEX_CLIENT_VERSION,
|
|
1297
|
-
});
|
|
1298
|
-
if (live.ok) {
|
|
1299
|
-
valid = true;
|
|
1300
|
-
} else {
|
|
1301
|
-
catalogError = `Codex models request failed with status ${live.status}`;
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
} catch (error) {
|
|
1305
|
-
valid = false;
|
|
1306
|
-
catalogError = error instanceof Error ? error.message : String(error);
|
|
1307
|
-
}
|
|
1308
|
-
return c.json({
|
|
1309
|
-
connected: status?.connected ?? false,
|
|
1310
|
-
plan: status?.planType ?? null,
|
|
1311
|
-
valid,
|
|
1312
|
-
activeAccountValid: valid,
|
|
1313
|
-
poolReady: readiness.poolReady,
|
|
1314
|
-
workerRoutable: readiness.workerRoutable,
|
|
1315
|
-
expiresAt: status?.expiresAt ?? null,
|
|
1316
|
-
lastError: catalogError ?? status?.lastError ?? null,
|
|
1317
|
-
models, // ClientModel[] the picker surfaces under the "no credits" group
|
|
1318
|
-
activeAccount, // the account a session runs on when unpinned (label for the indicator)
|
|
1319
|
-
accountCount: accounts.length,
|
|
1320
|
-
source,
|
|
1321
|
-
});
|
|
1322
|
-
});
|
|
1305
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
1306
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
1323
1307
|
|
|
1324
|
-
|
|
1308
|
+
return await coreCodexStatus(
|
|
1309
|
+
c,
|
|
1310
|
+
deps,
|
|
1311
|
+
grant.accountId,
|
|
1312
|
+
workspaceId,
|
|
1313
|
+
(await resolveCatalogSettings(db, settings)).settings,
|
|
1314
|
+
grant.subjectId,
|
|
1315
|
+
);
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
// List every connected Codex account (metadata only, never decrypts) + the
|
|
1325
1319
|
// workspace active pointer + rotation settings. Read access.
|
|
1326
1320
|
app.get("/v1/workspaces/:workspaceId/codex/accounts", async (c) => {
|
|
1327
1321
|
const workspaceId = c.req.param("workspaceId");
|
|
1328
1322
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
getCodexAppsSettings(db, workspaceId),
|
|
1333
|
-
managedCookieHuman(c, deps),
|
|
1334
|
-
getWorkspaceCodexSubscriptionSource(db, workspaceId),
|
|
1335
|
-
]);
|
|
1336
|
-
const activeAccountId = rotation?.activeCredentialId ?? null;
|
|
1337
|
-
const humanSubjectId = human?.subjectId === grant.subjectId ? human.subjectId : null;
|
|
1338
|
-
const canManageApps =
|
|
1339
|
-
humanSubjectId !== null && hasPermission(grant.permissions, "connections:write");
|
|
1340
|
-
return c.json({
|
|
1341
|
-
accounts: accounts.map((account) =>
|
|
1342
|
-
codexAccountJson(account, {
|
|
1343
|
-
appsCredentialId: apps.credentialId,
|
|
1344
|
-
canManageApps: settings.codexConnectedAppsEnabled && canManageApps,
|
|
1345
|
-
humanSubjectId,
|
|
1346
|
-
}),
|
|
1347
|
-
),
|
|
1348
|
-
activeAccountId,
|
|
1349
|
-
source,
|
|
1350
|
-
apps: {
|
|
1351
|
-
available: settings.codexConnectedAppsEnabled,
|
|
1352
|
-
credentialId: apps.credentialId,
|
|
1353
|
-
version: apps.version,
|
|
1354
|
-
designatedAt: apps.designatedAt,
|
|
1355
|
-
canDisable: canManageApps && apps.credentialId !== null,
|
|
1356
|
-
},
|
|
1357
|
-
settings: {
|
|
1358
|
-
rotationEnabled: rotation?.rotationEnabled ?? false,
|
|
1359
|
-
// sharded-rotation policy: rotation-enabled always behaves as sticky-sharded; report the
|
|
1360
|
-
// effective truth, never the stored legacy residue.
|
|
1361
|
-
rotationStrategy: "sharded",
|
|
1362
|
-
activeCredentialId: activeAccountId,
|
|
1363
|
-
},
|
|
1364
|
-
});
|
|
1323
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
1324
|
+
|
|
1325
|
+
return await coreCodexAccounts(c, deps, grant, workspaceId);
|
|
1365
1326
|
});
|
|
1366
1327
|
|
|
1367
1328
|
app.post("/v1/workspaces/:workspaceId/codex/apps", async (c) => {
|
|
1368
1329
|
const workspaceId = c.req.param("workspaceId");
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
message: "Codex Apps is disabled for this deployment",
|
|
1373
|
-
});
|
|
1374
|
-
}
|
|
1330
|
+
// Authenticate the designating human before any organization state is
|
|
1331
|
+
// read, so an unauthenticated caller learns nothing about the cutover row.
|
|
1332
|
+
// The core gate fails closed without consulting the historical source.
|
|
1375
1333
|
const { human, accountId } = await requireCodexAppsHuman(c, deps, workspaceId);
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
expectedVersion: z.number().int().nonnegative(),
|
|
1380
|
-
})
|
|
1381
|
-
.safeParse(await c.req.json().catch(() => null));
|
|
1382
|
-
if (!parsed.success) {
|
|
1383
|
-
throw new HTTPException(400, {
|
|
1384
|
-
message: "accountId and expectedVersion are required",
|
|
1385
|
-
});
|
|
1386
|
-
}
|
|
1387
|
-
const result = await designateCodexAppsCredential(db, {
|
|
1388
|
-
accountId,
|
|
1389
|
-
workspaceId,
|
|
1390
|
-
credentialId: parsed.data.accountId,
|
|
1391
|
-
subjectId: human.subjectId,
|
|
1392
|
-
expectedVersion: parsed.data.expectedVersion,
|
|
1393
|
-
});
|
|
1394
|
-
if (result.kind === "not_found") {
|
|
1395
|
-
throw new HTTPException(404, { message: "codex account not found" });
|
|
1396
|
-
}
|
|
1397
|
-
if (result.kind === "not_owner") {
|
|
1398
|
-
throw new HTTPException(403, {
|
|
1399
|
-
message: "only the managed human who connected this subscription may designate it",
|
|
1400
|
-
});
|
|
1401
|
-
}
|
|
1402
|
-
if (result.kind === "forbidden") {
|
|
1403
|
-
throw new HTTPException(403, {
|
|
1404
|
-
message: "missing permission: connections:write",
|
|
1405
|
-
});
|
|
1406
|
-
}
|
|
1407
|
-
if (result.kind === "unavailable") {
|
|
1408
|
-
throw new HTTPException(409, {
|
|
1409
|
-
message: "codex account requires relogin",
|
|
1410
|
-
});
|
|
1411
|
-
}
|
|
1412
|
-
const response = {
|
|
1413
|
-
credentialId: result.credentialId,
|
|
1414
|
-
version: result.version,
|
|
1415
|
-
designatedAt: result.designatedAt,
|
|
1416
|
-
changed: result.kind === "updated",
|
|
1417
|
-
};
|
|
1418
|
-
return result.kind === "updated" ? c.json(response) : c.json(response, 409);
|
|
1334
|
+
await codexRouteDisposition(deps, accountId);
|
|
1335
|
+
|
|
1336
|
+
return await coreCodexDesignateApps(c, deps, workspaceId, { human, accountId });
|
|
1419
1337
|
});
|
|
1420
1338
|
|
|
1421
1339
|
app.delete("/v1/workspaces/:workspaceId/codex/apps", async (c) => {
|
|
1422
1340
|
const workspaceId = c.req.param("workspaceId");
|
|
1423
|
-
|
|
1341
|
+
// The Apps designation is its own setting, not part of inference-account
|
|
1342
|
+
// routing: a workspace that switched routing to organization accounts (or
|
|
1343
|
+
// disabled subscriptions) must still be able to turn Apps off. Clearing
|
|
1344
|
+
// only removes authority; the owner/permission checks below still apply.
|
|
1424
1345
|
const { human, accountId } = await requireCodexAppsHuman(c, deps, workspaceId);
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
if (!parsed.success) {
|
|
1429
|
-
throw new HTTPException(400, { message: "expectedVersion is required" });
|
|
1430
|
-
}
|
|
1431
|
-
const result = await clearCodexAppsCredential(db, {
|
|
1432
|
-
accountId,
|
|
1433
|
-
workspaceId,
|
|
1434
|
-
subjectId: human.subjectId,
|
|
1435
|
-
expectedVersion: parsed.data.expectedVersion,
|
|
1436
|
-
});
|
|
1437
|
-
if (result.kind === "forbidden") {
|
|
1438
|
-
throw new HTTPException(403, {
|
|
1439
|
-
message: "missing permission: connections:write",
|
|
1440
|
-
});
|
|
1441
|
-
}
|
|
1442
|
-
const response = {
|
|
1443
|
-
credentialId: result.credentialId,
|
|
1444
|
-
version: result.version,
|
|
1445
|
-
designatedAt: result.designatedAt,
|
|
1446
|
-
changed: result.kind === "updated",
|
|
1447
|
-
};
|
|
1448
|
-
return result.kind === "conflict" ? c.json(response, 409) : c.json(response);
|
|
1346
|
+
await codexRouteDisposition(deps, accountId);
|
|
1347
|
+
|
|
1348
|
+
return await coreCodexClearApps(c, deps, workspaceId, { human, accountId });
|
|
1449
1349
|
});
|
|
1450
1350
|
|
|
1451
1351
|
// Manually switch the workspace ACTIVE account (the one unpinned sessions use).
|
|
1452
1352
|
// Pure pointer flip; in-flight turns pick it up on their next token fetch.
|
|
1453
1353
|
app.post("/v1/workspaces/:workspaceId/codex/accounts/:accountId/activate", async (c) => {
|
|
1454
1354
|
const workspaceId = c.req.param("workspaceId");
|
|
1455
|
-
await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
1456
|
-
await
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
return { result: activated, changed: activated };
|
|
1464
|
-
},
|
|
1355
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
1356
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
1357
|
+
|
|
1358
|
+
return await coreCodexActivate(
|
|
1359
|
+
c,
|
|
1360
|
+
deps,
|
|
1361
|
+
{ accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
|
|
1362
|
+
c.req.param("accountId"),
|
|
1465
1363
|
);
|
|
1466
|
-
const activated = mutation.result;
|
|
1467
|
-
if (!activated) {
|
|
1468
|
-
throw new HTTPException(404, { message: "codex account not found" });
|
|
1469
|
-
}
|
|
1470
|
-
await signalCodexCapacityTargets(deps, mutation.wakeTargets);
|
|
1471
|
-
return c.json({ activated: true, accountId });
|
|
1472
1364
|
});
|
|
1473
1365
|
|
|
1474
1366
|
// Update rotation settings. Connection-management access. Sharded-rotation policy: the strategy picker is GONE —
|
|
1475
1367
|
// rotation-enabled always behaves as sticky-sharded (worker-side
|
|
1476
1368
|
// effectiveRotationStrategy normalization). `rotationStrategy` in the body is
|
|
1477
1369
|
// ACCEPTED-BUT-IGNORED so no existing SDK/UI caller breaks (deprecation), and
|
|
1478
|
-
// the stored column is
|
|
1370
|
+
// the stored column is historical residue, not authority to restart old binaries.
|
|
1479
1371
|
app.patch("/v1/workspaces/:workspaceId/codex/settings", async (c) => {
|
|
1480
1372
|
const workspaceId = c.req.param("workspaceId");
|
|
1481
1373
|
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
1482
|
-
await
|
|
1374
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
1375
|
+
|
|
1483
1376
|
const body = (await c.req.json().catch(() => ({}))) as {
|
|
1484
1377
|
rotationEnabled?: unknown;
|
|
1485
1378
|
rotationStrategy?: unknown;
|
|
@@ -1499,48 +1392,32 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1499
1392
|
rotationStrategyDeprecated: true,
|
|
1500
1393
|
});
|
|
1501
1394
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
return { result: updated, changed: updated !== null };
|
|
1509
|
-
},
|
|
1395
|
+
|
|
1396
|
+
return await coreCodexSetRotation(
|
|
1397
|
+
c,
|
|
1398
|
+
deps,
|
|
1399
|
+
{ accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
|
|
1400
|
+
patch.rotationEnabled,
|
|
1510
1401
|
);
|
|
1511
|
-
const updated = mutation.result;
|
|
1512
|
-
if (!updated) {
|
|
1513
|
-
throw new HTTPException(404, {
|
|
1514
|
-
message: "codex rotation settings not found",
|
|
1515
|
-
});
|
|
1516
|
-
}
|
|
1517
|
-
await signalCodexCapacityTargets(deps, mutation.wakeTargets);
|
|
1518
|
-
return c.json({
|
|
1519
|
-
rotationEnabled: updated.rotationEnabled,
|
|
1520
|
-
// sharded-rotation policy: sharded is the only behavior; the stored column is residue.
|
|
1521
|
-
rotationStrategy: "sharded",
|
|
1522
|
-
activeCredentialId: updated.activeCredentialId,
|
|
1523
|
-
});
|
|
1524
1402
|
});
|
|
1525
1403
|
|
|
1526
1404
|
// Rename an account (label only in P1).
|
|
1527
1405
|
app.patch("/v1/workspaces/:workspaceId/codex/accounts/:accountId", async (c) => {
|
|
1528
1406
|
const workspaceId = c.req.param("workspaceId");
|
|
1529
|
-
await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
1530
|
-
await
|
|
1407
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
1408
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
1409
|
+
|
|
1531
1410
|
const accountId = c.req.param("accountId");
|
|
1532
1411
|
const body = (await c.req.json()) as { label?: string | null };
|
|
1533
1412
|
const label = typeof body.label === "string" ? body.label : null;
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
}
|
|
1543
|
-
return c.json(codexAccountJson(row));
|
|
1413
|
+
|
|
1414
|
+
return await coreCodexRename(
|
|
1415
|
+
c,
|
|
1416
|
+
deps,
|
|
1417
|
+
{ accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
|
|
1418
|
+
accountId,
|
|
1419
|
+
label,
|
|
1420
|
+
);
|
|
1544
1421
|
});
|
|
1545
1422
|
|
|
1546
1423
|
// Codex quota: independent OCC for new-turn allocator eligibility. Same-state is
|
|
@@ -1549,38 +1426,43 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1549
1426
|
app.patch("/v1/workspaces/:workspaceId/codex/accounts/:accountId/allocator", async (c) => {
|
|
1550
1427
|
const workspaceId = c.req.param("workspaceId");
|
|
1551
1428
|
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
1552
|
-
await
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
enabled: z.boolean(),
|
|
1556
|
-
expectedVersion: z.number().int().positive(),
|
|
1557
|
-
})
|
|
1558
|
-
.safeParse(await c.req.json().catch(() => null));
|
|
1429
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
1430
|
+
|
|
1431
|
+
const parsed = SubscriptionAccountToggleRequest.safeParse(await c.req.json().catch(() => null));
|
|
1559
1432
|
if (!parsed.success) {
|
|
1560
1433
|
throw new HTTPException(400, {
|
|
1561
1434
|
message: "enabled and expectedVersion are required",
|
|
1562
1435
|
});
|
|
1563
1436
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
subjectId: grant.subjectId,
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1437
|
+
|
|
1438
|
+
return await coreCodexAllocator(
|
|
1439
|
+
c,
|
|
1440
|
+
deps,
|
|
1441
|
+
{ accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
|
|
1442
|
+
c.req.param("accountId"),
|
|
1443
|
+
parsed.data,
|
|
1444
|
+
);
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
app.patch("/v1/workspaces/:workspaceId/codex/accounts/:accountId/extra-credits", async (c) => {
|
|
1448
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1449
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
1450
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
1451
|
+
|
|
1452
|
+
const parsed = SubscriptionAccountToggleRequest.safeParse(await c.req.json().catch(() => null));
|
|
1453
|
+
if (!parsed.success) {
|
|
1454
|
+
throw new HTTPException(400, {
|
|
1455
|
+
message: "enabled and expectedVersion are required",
|
|
1456
|
+
});
|
|
1575
1457
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1458
|
+
|
|
1459
|
+
return await coreCodexExtraCredits(
|
|
1460
|
+
c,
|
|
1461
|
+
deps,
|
|
1462
|
+
{ accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
|
|
1463
|
+
c.req.param("accountId"),
|
|
1464
|
+
parsed.data,
|
|
1465
|
+
);
|
|
1584
1466
|
});
|
|
1585
1467
|
|
|
1586
1468
|
// Disconnect ONE account by id. The accessor re-picks active when the removed
|
|
@@ -1588,27 +1470,13 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1588
1470
|
app.delete("/v1/workspaces/:workspaceId/codex/accounts/:accountId", async (c) => {
|
|
1589
1471
|
const workspaceId = c.req.param("workspaceId");
|
|
1590
1472
|
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
1591
|
-
await
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
return { result, changed: result.removed };
|
|
1599
|
-
},
|
|
1600
|
-
);
|
|
1601
|
-
const result = mutation.result;
|
|
1602
|
-
if (result.blockedByUnresolvedRedemption) {
|
|
1603
|
-
throw new HTTPException(409, {
|
|
1604
|
-
message:
|
|
1605
|
-
"this subscription has an unresolved reset redemption; recover it before disconnecting",
|
|
1606
|
-
});
|
|
1607
|
-
}
|
|
1608
|
-
await signalCodexCapacityTargets(deps, mutation.wakeTargets);
|
|
1609
|
-
return c.json({
|
|
1610
|
-
disconnected: result.removed,
|
|
1611
|
-
newActiveId: result.newActiveCredentialId,
|
|
1473
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
1474
|
+
|
|
1475
|
+
return await coreCodexDisconnect(c, deps, {
|
|
1476
|
+
accountId: grant.accountId,
|
|
1477
|
+
workspaceId,
|
|
1478
|
+
subjectId: grant.subjectId,
|
|
1479
|
+
connectionId: c.req.param("accountId"),
|
|
1612
1480
|
});
|
|
1613
1481
|
});
|
|
1614
1482
|
|
|
@@ -1617,24 +1485,13 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1617
1485
|
app.delete("/v1/workspaces/:workspaceId/codex", async (c) => {
|
|
1618
1486
|
const workspaceId = c.req.param("workspaceId");
|
|
1619
1487
|
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
1620
|
-
await
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
},
|
|
1628
|
-
);
|
|
1629
|
-
const result = mutation.result;
|
|
1630
|
-
if (result.blockedCredentialIds.length > 0) {
|
|
1631
|
-
throw new HTTPException(409, {
|
|
1632
|
-
message:
|
|
1633
|
-
"one or more subscriptions have unresolved reset redemptions; recover them before disconnecting",
|
|
1634
|
-
});
|
|
1635
|
-
}
|
|
1636
|
-
await signalCodexCapacityTargets(deps, mutation.wakeTargets);
|
|
1637
|
-
return c.json({ disconnected: result.removed > 0 });
|
|
1488
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
1489
|
+
|
|
1490
|
+
return await coreCodexDisconnectAll(c, deps, {
|
|
1491
|
+
accountId: grant.accountId,
|
|
1492
|
+
workspaceId,
|
|
1493
|
+
subjectId: grant.subjectId,
|
|
1494
|
+
});
|
|
1638
1495
|
});
|
|
1639
1496
|
|
|
1640
1497
|
// Back-compat: remaining usage / limits for the ACTIVE account only. Repointed
|
|
@@ -1642,33 +1499,20 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1642
1499
|
// stale access token. Deprecated in favor of the /accounts + /usage/refresh pair.
|
|
1643
1500
|
app.get("/v1/workspaces/:workspaceId/codex/usage", async (c) => {
|
|
1644
1501
|
const workspaceId = c.req.param("workspaceId");
|
|
1645
|
-
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
message: "codex subscription is not connected",
|
|
1650
|
-
});
|
|
1651
|
-
}
|
|
1652
|
-
const payload = await fetchCodexUsageForAccount(db, settings, workspaceId, status.credentialId);
|
|
1653
|
-
await signalPendingCodexCapacityTargets(deps, workspaceId);
|
|
1654
|
-
return c.json(codexUsageJson(payload));
|
|
1502
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
1503
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
1504
|
+
|
|
1505
|
+
return await coreCodexUsage(c, deps, grant, workspaceId, null);
|
|
1655
1506
|
});
|
|
1656
1507
|
|
|
1657
1508
|
// Single-account LIVE usage read (per-row manual refresh): refresh THIS account's
|
|
1658
1509
|
// bearer, hit /wham/usage, write the cache columns, return the normalized payload.
|
|
1659
1510
|
app.get("/v1/workspaces/:workspaceId/codex/accounts/:accountId/usage", async (c) => {
|
|
1660
1511
|
const workspaceId = c.req.param("workspaceId");
|
|
1661
|
-
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
const accounts = await listCodexAccountStatuses(db, workspaceId);
|
|
1666
|
-
if (!accounts.some((account) => account.id === accountId)) {
|
|
1667
|
-
throw new HTTPException(404, { message: "codex account not found" });
|
|
1668
|
-
}
|
|
1669
|
-
const payload = await fetchCodexUsageForAccount(db, settings, workspaceId, accountId);
|
|
1670
|
-
await signalPendingCodexCapacityTargets(deps, workspaceId);
|
|
1671
|
-
return c.json(codexUsageJson(payload));
|
|
1512
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
1513
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
1514
|
+
|
|
1515
|
+
return await coreCodexUsage(c, deps, grant, workspaceId, c.req.param("accountId"));
|
|
1672
1516
|
});
|
|
1673
1517
|
|
|
1674
1518
|
// Batched LIVE refresh across every connected account, keyed by credential id.
|
|
@@ -1678,42 +1522,10 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1678
1522
|
// staleness check call — NEVER a browser interval.
|
|
1679
1523
|
app.post("/v1/workspaces/:workspaceId/codex/usage/refresh", async (c) => {
|
|
1680
1524
|
const workspaceId = c.req.param("workspaceId");
|
|
1681
|
-
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
const queue = [...accounts];
|
|
1686
|
-
const CONCURRENCY = 4;
|
|
1687
|
-
const worker = async (): Promise<void> => {
|
|
1688
|
-
for (;;) {
|
|
1689
|
-
const account = queue.shift();
|
|
1690
|
-
if (!account) return;
|
|
1691
|
-
const settled = await Promise.allSettled([
|
|
1692
|
-
fetchCodexUsageForAccount(db, settings, workspaceId, account.id),
|
|
1693
|
-
]);
|
|
1694
|
-
const result = settled[0];
|
|
1695
|
-
usage[account.id] =
|
|
1696
|
-
result.status === "fulfilled"
|
|
1697
|
-
? codexUsageJson(result.value)
|
|
1698
|
-
: {
|
|
1699
|
-
status: "error",
|
|
1700
|
-
usage: {
|
|
1701
|
-
status: "error",
|
|
1702
|
-
planType: null,
|
|
1703
|
-
fiveHour: null,
|
|
1704
|
-
weekly: null,
|
|
1705
|
-
limitReached: false,
|
|
1706
|
-
fetchedAt: new Date().toISOString(),
|
|
1707
|
-
rateLimitResetCredits: null,
|
|
1708
|
-
},
|
|
1709
|
-
};
|
|
1710
|
-
}
|
|
1711
|
-
};
|
|
1712
|
-
await Promise.all(
|
|
1713
|
-
Array.from({ length: Math.min(CONCURRENCY, Math.max(1, accounts.length)) }, () => worker()),
|
|
1714
|
-
);
|
|
1715
|
-
await signalPendingCodexCapacityTargets(deps, workspaceId);
|
|
1716
|
-
return c.json({ usage });
|
|
1525
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
1526
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
1527
|
+
|
|
1528
|
+
return await coreCodexUsageRefresh(c, deps, grant, workspaceId);
|
|
1717
1529
|
});
|
|
1718
1530
|
|
|
1719
1531
|
// Trustworthy live overview: usage and reset details settle independently per
|
|
@@ -1722,132 +1534,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1722
1534
|
app.get("/v1/workspaces/:workspaceId/codex/overview", async (c) => {
|
|
1723
1535
|
const workspaceId = c.req.param("workspaceId");
|
|
1724
1536
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
human &&
|
|
1729
|
-
human.subjectId === grant.subjectId &&
|
|
1730
|
-
hasPermission(grant.permissions, "connections:write")
|
|
1731
|
-
? await listCodexResetRedemptionRecoveries(db, {
|
|
1732
|
-
accountId: grant.accountId,
|
|
1733
|
-
workspaceId,
|
|
1734
|
-
subjectId: human.subjectId,
|
|
1735
|
-
})
|
|
1736
|
-
: [];
|
|
1737
|
-
const overview: Record<string, Awaited<ReturnType<typeof fetchCodexAccountOverview>>> = {};
|
|
1738
|
-
const queue = [...accounts];
|
|
1739
|
-
// Usage + detailed inventory are two independent calls per account. Limit
|
|
1740
|
-
// the actual provider calls, not merely account workers, so aggregate
|
|
1741
|
-
// concurrency never exceeds four.
|
|
1742
|
-
const providerCall = createProviderCallLimiter(4);
|
|
1743
|
-
let routeTimedOut = false;
|
|
1744
|
-
const worker = async (): Promise<void> => {
|
|
1745
|
-
for (;;) {
|
|
1746
|
-
if (routeTimedOut) return;
|
|
1747
|
-
const account = queue.shift();
|
|
1748
|
-
if (!account) return;
|
|
1749
|
-
const canResumeRedemption = Boolean(
|
|
1750
|
-
account.source === "workspace" &&
|
|
1751
|
-
human &&
|
|
1752
|
-
human.subjectId === grant.subjectId &&
|
|
1753
|
-
human.subjectId === account.connectedBySubjectId &&
|
|
1754
|
-
hasPermission(grant.permissions, "connections:write"),
|
|
1755
|
-
);
|
|
1756
|
-
const canRedeem = canResumeRedemption && account.status === "active";
|
|
1757
|
-
const redemptionAccess: CodexRedemptionAccess =
|
|
1758
|
-
account.source === "organization"
|
|
1759
|
-
? {
|
|
1760
|
-
ownership: "managed_human_unavailable",
|
|
1761
|
-
canClaimUnownedViaReconnect: false,
|
|
1762
|
-
}
|
|
1763
|
-
: codexRedemptionAccess({
|
|
1764
|
-
connectedBySubjectId: account.connectedBySubjectId,
|
|
1765
|
-
grantSubjectId: grant.subjectId,
|
|
1766
|
-
managedHumanSubjectId: human?.subjectId ?? null,
|
|
1767
|
-
canManage: hasPermission(grant.permissions, "connections:write"),
|
|
1768
|
-
});
|
|
1769
|
-
overview[account.id] = await fetchCodexAccountOverview(
|
|
1770
|
-
deps,
|
|
1771
|
-
workspaceId,
|
|
1772
|
-
account,
|
|
1773
|
-
redemptionAccess,
|
|
1774
|
-
canRedeem,
|
|
1775
|
-
canResumeRedemption,
|
|
1776
|
-
canResumeRedemption
|
|
1777
|
-
? ownerRecoveries.filter((recovery) => recovery.credentialId === account.id)
|
|
1778
|
-
: [],
|
|
1779
|
-
providerCall,
|
|
1780
|
-
);
|
|
1781
|
-
}
|
|
1782
|
-
};
|
|
1783
|
-
const workers = Promise.all(
|
|
1784
|
-
Array.from({ length: Math.min(4, Math.max(1, accounts.length)) }, () => worker()),
|
|
1785
|
-
);
|
|
1786
|
-
let deadline: ReturnType<typeof setTimeout> | undefined;
|
|
1787
|
-
await Promise.race([
|
|
1788
|
-
workers,
|
|
1789
|
-
new Promise<void>((resolve) => {
|
|
1790
|
-
deadline = setTimeout(() => {
|
|
1791
|
-
routeTimedOut = true;
|
|
1792
|
-
queue.length = 0;
|
|
1793
|
-
resolve();
|
|
1794
|
-
}, CODEX_OVERVIEW_ROUTE_TIMEOUT_MS);
|
|
1795
|
-
}),
|
|
1796
|
-
]);
|
|
1797
|
-
if (deadline) clearTimeout(deadline);
|
|
1798
|
-
if (routeTimedOut) {
|
|
1799
|
-
// Fill every unscheduled account from persisted cache without performing
|
|
1800
|
-
// more provider work. In-flight account operations remain rejection-
|
|
1801
|
-
// handled and are themselves bounded; they cannot delay this response or
|
|
1802
|
-
// leave limiter waiters permanently queued.
|
|
1803
|
-
const unavailableProviderCall: CodexProviderCall = async () => {
|
|
1804
|
-
throw new Error("Codex overview route deadline reached");
|
|
1805
|
-
};
|
|
1806
|
-
await Promise.all(
|
|
1807
|
-
accounts
|
|
1808
|
-
.filter((account) => overview[account.id] == null)
|
|
1809
|
-
.map(async (account) => {
|
|
1810
|
-
const canResumeRedemption = Boolean(
|
|
1811
|
-
account.source === "workspace" &&
|
|
1812
|
-
human &&
|
|
1813
|
-
human.subjectId === grant.subjectId &&
|
|
1814
|
-
human.subjectId === account.connectedBySubjectId &&
|
|
1815
|
-
hasPermission(grant.permissions, "connections:write"),
|
|
1816
|
-
);
|
|
1817
|
-
const fallback = await fetchCodexAccountOverview(
|
|
1818
|
-
deps,
|
|
1819
|
-
workspaceId,
|
|
1820
|
-
account,
|
|
1821
|
-
account.source === "organization"
|
|
1822
|
-
? {
|
|
1823
|
-
ownership: "managed_human_unavailable",
|
|
1824
|
-
canClaimUnownedViaReconnect: false,
|
|
1825
|
-
}
|
|
1826
|
-
: codexRedemptionAccess({
|
|
1827
|
-
connectedBySubjectId: account.connectedBySubjectId,
|
|
1828
|
-
grantSubjectId: grant.subjectId,
|
|
1829
|
-
managedHumanSubjectId: human?.subjectId ?? null,
|
|
1830
|
-
canManage: hasPermission(grant.permissions, "connections:write"),
|
|
1831
|
-
}),
|
|
1832
|
-
false,
|
|
1833
|
-
canResumeRedemption,
|
|
1834
|
-
canResumeRedemption
|
|
1835
|
-
? ownerRecoveries.filter((recovery) => recovery.credentialId === account.id)
|
|
1836
|
-
: [],
|
|
1837
|
-
unavailableProviderCall,
|
|
1838
|
-
);
|
|
1839
|
-
// A bounded in-flight worker may have completed while fallback was
|
|
1840
|
-
// assembled; prefer that fresh truth when present.
|
|
1841
|
-
overview[account.id] ??= fallback;
|
|
1842
|
-
}),
|
|
1843
|
-
);
|
|
1844
|
-
void workers.catch(() => undefined);
|
|
1845
|
-
}
|
|
1846
|
-
// Overview writes the same authoritative usage snapshots as the explicit
|
|
1847
|
-
// refresh routes. Deliver any committed capacity outbox entries instead of
|
|
1848
|
-
// leaving quota-recovered waiters dormant until a later unrelated refresh.
|
|
1849
|
-
void signalPendingCodexCapacityTargets(deps, workspaceId).catch(() => undefined);
|
|
1850
|
-
return c.json({ accounts: overview });
|
|
1537
|
+
await codexRouteDisposition(deps, grant.accountId);
|
|
1538
|
+
|
|
1539
|
+
return await coreCodexOverview(c, deps, grant, workspaceId);
|
|
1851
1540
|
});
|
|
1852
1541
|
|
|
1853
1542
|
// Mint a five-minute HMAC confirmation bound to the actual Better Auth
|
|
@@ -1860,335 +1549,804 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1860
1549
|
const workspaceId = c.req.param("workspaceId");
|
|
1861
1550
|
const credentialId = c.req.param("accountId");
|
|
1862
1551
|
const { human, accountId } = await requireRedemptionHuman(c, deps, workspaceId);
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
});
|
|
1869
|
-
}
|
|
1870
|
-
const accounts = await listCodexAccountStatuses(db, workspaceId);
|
|
1871
|
-
const account = accounts.find((candidate) => candidate.id === credentialId);
|
|
1872
|
-
if (!account) throw new HTTPException(404, { message: "codex account not found" });
|
|
1873
|
-
if (account.source === "organization") {
|
|
1874
|
-
throw new HTTPException(409, {
|
|
1875
|
-
message: "organization Codex subscriptions are managed in Organization settings",
|
|
1876
|
-
});
|
|
1877
|
-
}
|
|
1878
|
-
let existing = await getCodexResetRedemptionAttempt(db, workspaceId, parsed.data.attemptId);
|
|
1879
|
-
if (account.connectedBySubjectId !== human.subjectId) {
|
|
1880
|
-
throw new HTTPException(403, {
|
|
1881
|
-
message: "only the human who connected this subscription may redeem its reset credits",
|
|
1882
|
-
});
|
|
1883
|
-
}
|
|
1884
|
-
if (
|
|
1885
|
-
existing &&
|
|
1886
|
-
(existing.credentialId !== credentialId ||
|
|
1887
|
-
existing.creditId !== parsed.data.creditId ||
|
|
1888
|
-
existing.subjectId !== human.subjectId)
|
|
1889
|
-
) {
|
|
1890
|
-
throw new HTTPException(409, {
|
|
1891
|
-
message: "logical redemption attempt identity mismatch",
|
|
1892
|
-
});
|
|
1893
|
-
}
|
|
1894
|
-
if (existing) {
|
|
1895
|
-
const adoption = await adoptCodexResetRedemptionAttempt(db, {
|
|
1896
|
-
accountId,
|
|
1897
|
-
workspaceId,
|
|
1898
|
-
attemptId: existing.id,
|
|
1899
|
-
credentialId,
|
|
1900
|
-
creditId: existing.creditId,
|
|
1901
|
-
subjectId: human.subjectId,
|
|
1902
|
-
browserSessionHash: human.browserSessionHash,
|
|
1903
|
-
});
|
|
1904
|
-
if (adoption.kind === "in_progress") {
|
|
1905
|
-
throw new HTTPException(409, {
|
|
1906
|
-
message: "this redemption is still in progress in another browser request",
|
|
1907
|
-
});
|
|
1908
|
-
}
|
|
1909
|
-
if (adoption.kind === "not_found") {
|
|
1910
|
-
throw new HTTPException(409, {
|
|
1911
|
-
message: "redemption recovery state changed",
|
|
1912
|
-
});
|
|
1913
|
-
}
|
|
1914
|
-
if (adoption.kind === "forbidden") {
|
|
1915
|
-
throw new HTTPException(403, {
|
|
1916
|
-
message: "redemption owner is unavailable",
|
|
1917
|
-
});
|
|
1918
|
-
}
|
|
1919
|
-
if (adoption.kind === "conflict") {
|
|
1920
|
-
throw new HTTPException(409, {
|
|
1921
|
-
message: "logical redemption attempt identity mismatch",
|
|
1922
|
-
});
|
|
1923
|
-
}
|
|
1924
|
-
existing = adoption.attempt;
|
|
1925
|
-
}
|
|
1926
|
-
// Starting or retrying provider work requires a healthy credential. A
|
|
1927
|
-
// completed attempt is different: its provider outcome is durable truth,
|
|
1928
|
-
// and a lost HTTP response must remain replayable after a later health
|
|
1929
|
-
// transition without another consume call.
|
|
1930
|
-
if (account.status !== "active" && existing?.status !== "completed") {
|
|
1931
|
-
throw new HTTPException(403, {
|
|
1932
|
-
message: "redemption credential is unavailable",
|
|
1933
|
-
});
|
|
1934
|
-
}
|
|
1935
|
-
const secret = settings.betterAuthSecret;
|
|
1936
|
-
if (!secret) {
|
|
1937
|
-
throw new HTTPException(503, {
|
|
1938
|
-
message: "managed browser confirmation is unavailable",
|
|
1939
|
-
});
|
|
1940
|
-
}
|
|
1941
|
-
const expiresAt = Math.floor(Date.now() / 1000) + CODEX_REDEMPTION_CONFIRMATION_SECONDS;
|
|
1942
|
-
const confirmationToken = await signCodexRedemptionConfirmation(secret, {
|
|
1943
|
-
version: 1,
|
|
1944
|
-
attemptId: parsed.data.attemptId,
|
|
1552
|
+
await codexRouteDisposition(deps, accountId);
|
|
1553
|
+
|
|
1554
|
+
return await coreCodexResetPrepare(c, deps, {
|
|
1555
|
+
human,
|
|
1556
|
+
accountId,
|
|
1945
1557
|
workspaceId,
|
|
1946
1558
|
credentialId,
|
|
1947
|
-
creditId: parsed.data.creditId,
|
|
1948
|
-
subjectId: human.subjectId,
|
|
1949
|
-
browserSessionHash: human.browserSessionHash,
|
|
1950
|
-
expiresAt,
|
|
1951
|
-
});
|
|
1952
|
-
return c.json({
|
|
1953
|
-
attemptId: parsed.data.attemptId,
|
|
1954
|
-
confirmationToken,
|
|
1955
|
-
expiresAt: new Date(expiresAt * 1000).toISOString(),
|
|
1956
|
-
// A completed attempt may have lost its HTTP response after its outcome
|
|
1957
|
-
// committed. Keep that exact logical id replayable without another
|
|
1958
|
-
// provider consume call, just like an ambiguous provider_started attempt.
|
|
1959
|
-
resumable: existing?.status === "provider_started" || existing?.status === "completed",
|
|
1960
|
-
recoveryStatus:
|
|
1961
|
-
existing?.status === "provider_started" || existing?.status === "completed"
|
|
1962
|
-
? existing.status
|
|
1963
|
-
: null,
|
|
1964
1559
|
});
|
|
1965
1560
|
},
|
|
1966
1561
|
);
|
|
1967
1562
|
|
|
1968
|
-
// The only
|
|
1969
|
-
//
|
|
1563
|
+
// The only Opengeni reset-credit mutation route: the person in the web app,
|
|
1564
|
+
// or an agent they signed in acting as them. Nothing redeems automatically
|
|
1565
|
+
// (no worker, scheduled, allocator or rotation path).
|
|
1970
1566
|
app.post(
|
|
1971
1567
|
"/v1/workspaces/:workspaceId/codex/accounts/:accountId/reset-credits/redeem",
|
|
1972
1568
|
async (c) => {
|
|
1973
1569
|
const workspaceId = c.req.param("workspaceId");
|
|
1974
1570
|
const credentialId = c.req.param("accountId");
|
|
1975
1571
|
const { human, accountId } = await requireRedemptionHuman(c, deps, workspaceId);
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
1572
|
+
await codexRouteDisposition(deps, accountId);
|
|
1573
|
+
|
|
1574
|
+
return await coreCodexResetRedeem(c, deps, { human, accountId, workspaceId, credentialId });
|
|
1575
|
+
},
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// ---------------------------------------------------------------------------
|
|
1580
|
+
// Reset credits and the overview on the shared subscription core (M3 PR 2c).
|
|
1581
|
+
// Same routes, payloads, HMAC confirmation, single-use ledger fences and
|
|
1582
|
+
// ambiguous-outcome recovery as the historical handlers; authority is the
|
|
1583
|
+
// design 6.3 rule enforced by `subscription_codex_reset_authority` (an
|
|
1584
|
+
// organization administrator or an administrator of the managing workspace,
|
|
1585
|
+
// from a same-origin managed browser or an agent acting as them), and the
|
|
1586
|
+
// credential is read and refreshed through the core connection seam.
|
|
1587
|
+
|
|
1588
|
+
type CoreRedemptionInput = {
|
|
1589
|
+
human: ManagedCookieHuman;
|
|
1590
|
+
accountId: string;
|
|
1591
|
+
/** The workspace whose redemption ledger files the attempt. */
|
|
1592
|
+
workspaceId: string;
|
|
1593
|
+
credentialId: string;
|
|
1594
|
+
};
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* The account a redemption targets, and how its provider calls are made. A
|
|
1598
|
+
* workspace page reaches it through the workspace's pool; the organization
|
|
1599
|
+
* page reaches an organization-managed account directly, as an organization
|
|
1600
|
+
* administrator, whether or not any workspace uses it.
|
|
1601
|
+
*/
|
|
1602
|
+
type RedemptionTarget = {
|
|
1603
|
+
credentialId: string;
|
|
1604
|
+
organizationManaged: boolean;
|
|
1605
|
+
provider: () => {
|
|
1606
|
+
getToken: () => Promise<{
|
|
1607
|
+
accessToken: string;
|
|
1608
|
+
chatgptAccountId: string | null;
|
|
1609
|
+
isFedramp: boolean;
|
|
1610
|
+
}>;
|
|
1611
|
+
fetch: CodexFetch;
|
|
1612
|
+
};
|
|
1613
|
+
};
|
|
1614
|
+
|
|
1615
|
+
/** Ledger and authority reads run as the browser human, never the ambient actor. */
|
|
1616
|
+
function asRedemptionHuman<T>(human: ManagedCookieHuman, fn: () => Promise<T>): Promise<T> {
|
|
1617
|
+
return withSessionRlsActorContext({ subjectId: human.subjectId }, fn);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
/**
|
|
1621
|
+
* Canonical core connection for a route id (canonical or legacy alias) in the
|
|
1622
|
+
* workspace's pool. An organization account is redeemable only by an
|
|
1623
|
+
* organization administrator (M3 PR 3b); everyone else gets the legacy 409.
|
|
1624
|
+
*/
|
|
1625
|
+
async function coreRedemptionTarget(deps: ApiRouteDeps, input: CoreRedemptionInput) {
|
|
1626
|
+
const canonical = z.uuid().safeParse(input.credentialId).success
|
|
1627
|
+
? await resolveSubscriptionCoreCodexConnectionId(deps.db, {
|
|
1628
|
+
accountId: input.accountId,
|
|
1629
|
+
workspaceId: input.workspaceId,
|
|
1630
|
+
connectionId: input.credentialId,
|
|
1631
|
+
})
|
|
1632
|
+
: null;
|
|
1633
|
+
const { accounts } = await getSubscriptionCoreCodexWorkspaceProjection(deps.db, input);
|
|
1634
|
+
const account = canonical ? accounts.find((candidate) => candidate.id === canonical) : undefined;
|
|
1635
|
+
if (!canonical || !account) throw new HTTPException(404, { message: "codex account not found" });
|
|
1636
|
+
return { credentialId: canonical, account };
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
async function workspaceRedemptionTarget(
|
|
1640
|
+
deps: ApiRouteDeps,
|
|
1641
|
+
input: CoreRedemptionInput,
|
|
1642
|
+
): Promise<RedemptionTarget> {
|
|
1643
|
+
const { credentialId, account } = await coreRedemptionTarget(deps, input);
|
|
1644
|
+
const scope = coreCodexConnectionScope(input, input.human.subjectId);
|
|
1645
|
+
return {
|
|
1646
|
+
credentialId,
|
|
1647
|
+
organizationManaged: account.source === "organization",
|
|
1648
|
+
provider: () => ({
|
|
1649
|
+
getToken: () =>
|
|
1650
|
+
buildSubscriptionCoreCodexConnectionTokenResolver(
|
|
1651
|
+
deps.db,
|
|
1652
|
+
deps.settings,
|
|
1653
|
+
scope,
|
|
1654
|
+
credentialId,
|
|
1655
|
+
null,
|
|
1656
|
+
).getToken(),
|
|
1657
|
+
fetch: buildSubscriptionCoreCodexOperationFetch(
|
|
1658
|
+
deps.db,
|
|
1659
|
+
scope,
|
|
1660
|
+
null,
|
|
1661
|
+
credentialId,
|
|
1662
|
+
(deps.codexFetch ?? fetch) as CodexFetch,
|
|
1663
|
+
),
|
|
1664
|
+
}),
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
/** An organization-managed account, read by an organization administrator. */
|
|
1669
|
+
async function organizationRedemptionTarget(
|
|
1670
|
+
deps: ApiRouteDeps,
|
|
1671
|
+
input: { organizationId: string; subjectId: string; credentialId: string },
|
|
1672
|
+
): Promise<RedemptionTarget> {
|
|
1673
|
+
const found = z.uuid().safeParse(input.credentialId).success
|
|
1674
|
+
? await findOrganizationCodexConnectionForAdministrator(deps.db, deps.settings, {
|
|
1675
|
+
organizationId: input.organizationId,
|
|
1676
|
+
actorSubjectId: input.subjectId,
|
|
1677
|
+
credentialId: input.credentialId,
|
|
1678
|
+
})
|
|
1679
|
+
: null;
|
|
1680
|
+
if (!found) throw new HTTPException(404, { message: "codex account not found" });
|
|
1681
|
+
return {
|
|
1682
|
+
credentialId: found.credentialId,
|
|
1683
|
+
organizationManaged: true,
|
|
1684
|
+
provider: () => ({
|
|
1685
|
+
getToken: () =>
|
|
1686
|
+
buildOrganizationCodexAdministratorTokenResolver(deps.db, deps.settings, {
|
|
1687
|
+
organizationId: input.organizationId,
|
|
1688
|
+
actorSubjectId: input.subjectId,
|
|
1689
|
+
credentialId: found.credentialId,
|
|
1690
|
+
}).getToken(),
|
|
1691
|
+
fetch: (deps.codexFetch ?? fetch) as CodexFetch,
|
|
1692
|
+
}),
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function organizationManagedRedemption(): never {
|
|
1697
|
+
throw new HTTPException(409, {
|
|
1698
|
+
message: "organization Codex subscriptions are managed in Organization settings",
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
/**
|
|
1703
|
+
* An organization administrator redeeming from the organization's Models
|
|
1704
|
+
* page: the person in their own managed browser, or an agent acting as them
|
|
1705
|
+
* (never an API key or a delegated bearer), and the workspace whose ledger
|
|
1706
|
+
* files the attempt. An agent's confirmation binds to its stable per-person
|
|
1707
|
+
* stand-in session, which never equals a browser session.
|
|
1708
|
+
*/
|
|
1709
|
+
async function requireOrganizationRedemptionHuman(
|
|
1710
|
+
c: Context,
|
|
1711
|
+
deps: ApiRouteDeps,
|
|
1712
|
+
organizationId: string,
|
|
1713
|
+
): Promise<{ human: ManagedCookieHuman; workspaceId: string }> {
|
|
1714
|
+
if (deps.settings.productAccessMode !== "managed") {
|
|
1715
|
+
throw new HTTPException(403, { message: "reset redemption requires managed product mode" });
|
|
1716
|
+
}
|
|
1717
|
+
const agent = isAgentActingAsPerson(c);
|
|
1718
|
+
if (!agent && c.req.header("authorization")) {
|
|
1719
|
+
throw new HTTPException(403, {
|
|
1720
|
+
message: "authorization bearer is not allowed for redemption",
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
requireSameOriginBrowserMutation(c, deps);
|
|
1724
|
+
const browser = agent ? null : await managedCookieHuman(c, deps);
|
|
1725
|
+
if (!agent && !browser) {
|
|
1726
|
+
throw new HTTPException(401, { message: "managed browser session required" });
|
|
1727
|
+
}
|
|
1728
|
+
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
1729
|
+
if (browser && human.subjectId !== browser.subjectId) {
|
|
1730
|
+
throw new HTTPException(403, { message: "managed browser identity mismatch" });
|
|
1731
|
+
}
|
|
1732
|
+
const workspaceId = await resolveOrganizationCodexRedemptionWorkspace(deps.db, {
|
|
1733
|
+
organizationId,
|
|
1734
|
+
actorSubjectId: human.subjectId,
|
|
1735
|
+
});
|
|
1736
|
+
if (!workspaceId) {
|
|
1737
|
+
throw new HTTPException(409, {
|
|
1738
|
+
message: "create a workspace first: usage limit reset redemptions are recorded in one",
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
return { human, workspaceId };
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/**
|
|
1745
|
+
* One organization-managed account's usage and usage limit resets on the
|
|
1746
|
+
* organization's Models page, whether or not any workspace uses it. Anyone
|
|
1747
|
+
* who may manage the organization's Codex accounts can read it (an agent
|
|
1748
|
+
* acting as an administrator included); an organization administrator, or an
|
|
1749
|
+
* agent acting as them, gets redemption flags and their recoverable attempts.
|
|
1750
|
+
*/
|
|
1751
|
+
async function coreOrganizationCodexOverview(
|
|
1752
|
+
c: Context,
|
|
1753
|
+
deps: ApiRouteDeps,
|
|
1754
|
+
organizationId: string,
|
|
1755
|
+
human: ManagedCookieHuman,
|
|
1756
|
+
rawCredentialId: string,
|
|
1757
|
+
) {
|
|
1758
|
+
c.header("cache-control", "no-store");
|
|
1759
|
+
const mode = await codexRouteDisposition(deps, organizationId);
|
|
1760
|
+
const target = await organizationRedemptionTarget(deps, {
|
|
1761
|
+
organizationId,
|
|
1762
|
+
subjectId: human.subjectId,
|
|
1763
|
+
credentialId: rawCredentialId,
|
|
1764
|
+
});
|
|
1765
|
+
const credentialId = target.credentialId;
|
|
1766
|
+
const { accounts } = await getSubscriptionCoreOrganizationCodexProjection(deps.db, {
|
|
1767
|
+
organizationId,
|
|
1768
|
+
subjectId: human.subjectId,
|
|
1769
|
+
});
|
|
1770
|
+
const row = accounts.find((account) => account.id === credentialId);
|
|
1771
|
+
if (!row) throw new HTTPException(404, { message: "codex account not found" });
|
|
1772
|
+
const browser = isAgentActingAsPerson(c) ? human : await managedCookieHuman(c, deps);
|
|
1773
|
+
const browserHuman =
|
|
1774
|
+
deps.settings.productAccessMode === "managed" && browser?.subjectId === human.subjectId
|
|
1775
|
+
? browser
|
|
1776
|
+
: null;
|
|
1777
|
+
const workspaceId = browserHuman
|
|
1778
|
+
? await resolveOrganizationCodexRedemptionWorkspace(deps.db, {
|
|
1779
|
+
organizationId,
|
|
1780
|
+
actorSubjectId: browserHuman.subjectId,
|
|
1781
|
+
})
|
|
1782
|
+
: null;
|
|
1783
|
+
const authority =
|
|
1784
|
+
browserHuman && workspaceId
|
|
1785
|
+
? await asRedemptionHuman(browserHuman, () =>
|
|
1786
|
+
readSubscriptionCoreCodexResetAuthority(deps.db, {
|
|
1787
|
+
accountId: organizationId,
|
|
1788
|
+
workspaceId,
|
|
1789
|
+
credentialId,
|
|
1790
|
+
subjectId: browserHuman.subjectId,
|
|
1791
|
+
}),
|
|
1792
|
+
).catch(() => null)
|
|
1793
|
+
: null;
|
|
1794
|
+
const canResumeRedemption = authority?.owned === true;
|
|
1795
|
+
const redemptions =
|
|
1796
|
+
canResumeRedemption && browserHuman && workspaceId
|
|
1797
|
+
? (
|
|
1798
|
+
await asRedemptionHuman(browserHuman, () =>
|
|
1799
|
+
listSubscriptionCoreCodexResetRedemptionRecoveries(deps.db, {
|
|
1800
|
+
accountId: organizationId,
|
|
1801
|
+
workspaceId,
|
|
1802
|
+
subjectId: browserHuman.subjectId,
|
|
1803
|
+
}),
|
|
1804
|
+
)
|
|
1805
|
+
).filter((recovery) => recovery.credentialId === credentialId)
|
|
1806
|
+
: [];
|
|
1807
|
+
const redemptionAccess: CodexRedemptionAccess = {
|
|
1808
|
+
ownership: !browserHuman
|
|
1809
|
+
? "managed_human_unavailable"
|
|
1810
|
+
: canResumeRedemption
|
|
1811
|
+
? "current_human"
|
|
1812
|
+
: "different_human",
|
|
1813
|
+
canClaimUnownedViaReconnect: false,
|
|
1814
|
+
};
|
|
1815
|
+
const sources: CodexOverviewSources = {
|
|
1816
|
+
usage: () =>
|
|
1817
|
+
fetchOrganizationCodexUsageForAccount(
|
|
1818
|
+
deps.db,
|
|
1819
|
+
deps.settings,
|
|
1820
|
+
{ organizationId, actorSubjectId: human.subjectId, credentialId, mode },
|
|
1821
|
+
deps.codexFetch ?? fetch,
|
|
1822
|
+
),
|
|
1823
|
+
details: async () => {
|
|
1824
|
+
const provider = target.provider();
|
|
1825
|
+
let token;
|
|
1826
|
+
try {
|
|
1827
|
+
token = await provider.getToken();
|
|
1828
|
+
} catch (error) {
|
|
1829
|
+
return {
|
|
1830
|
+
ok: false as const,
|
|
1831
|
+
status: 0,
|
|
1832
|
+
reason:
|
|
1833
|
+
error instanceof CodexReloginRequired
|
|
1834
|
+
? ("needs_relogin" as const)
|
|
1835
|
+
: ("network_error" as const),
|
|
1836
|
+
};
|
|
2002
1837
|
}
|
|
1838
|
+
return await fetchCodexRateLimitResetCredits(
|
|
1839
|
+
{
|
|
1840
|
+
accessToken: token.accessToken,
|
|
1841
|
+
chatgptAccountId: token.chatgptAccountId,
|
|
1842
|
+
isFedramp: token.isFedramp,
|
|
1843
|
+
clientVersion: CODEX_CLIENT_VERSION,
|
|
1844
|
+
},
|
|
1845
|
+
provider.fetch,
|
|
1846
|
+
);
|
|
1847
|
+
},
|
|
1848
|
+
};
|
|
1849
|
+
return c.json(
|
|
1850
|
+
await fetchCodexAccountOverview(
|
|
1851
|
+
deps,
|
|
1852
|
+
workspaceId ?? organizationId,
|
|
1853
|
+
row,
|
|
1854
|
+
redemptionAccess,
|
|
1855
|
+
canResumeRedemption && row.status === "active",
|
|
1856
|
+
canResumeRedemption,
|
|
1857
|
+
redemptions,
|
|
1858
|
+
createProviderCallLimiter(2),
|
|
1859
|
+
sources,
|
|
1860
|
+
),
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
2003
1863
|
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
1864
|
+
function coreCodexConnectionScope(
|
|
1865
|
+
input: { accountId: string; workspaceId: string },
|
|
1866
|
+
subjectId: string,
|
|
1867
|
+
) {
|
|
1868
|
+
return {
|
|
1869
|
+
kind: "workspace" as const,
|
|
1870
|
+
accountId: input.accountId,
|
|
1871
|
+
workspaceId: input.workspaceId,
|
|
1872
|
+
subjectId,
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
async function coreCodexResetPrepare(
|
|
1877
|
+
c: Context,
|
|
1878
|
+
deps: ApiRouteDeps,
|
|
1879
|
+
input: CoreRedemptionInput,
|
|
1880
|
+
resolveTarget: () => Promise<RedemptionTarget> = () => workspaceRedemptionTarget(deps, input),
|
|
1881
|
+
) {
|
|
1882
|
+
c.header("cache-control", "no-store");
|
|
1883
|
+
const parsed = redemptionPrepareBody.safeParse(await c.req.json().catch(() => null));
|
|
1884
|
+
if (!parsed.success) {
|
|
1885
|
+
throw new HTTPException(400, { message: "attemptId and creditId are required" });
|
|
1886
|
+
}
|
|
1887
|
+
const { human, accountId, workspaceId } = input;
|
|
1888
|
+
const { credentialId, organizationManaged } = await resolveTarget();
|
|
1889
|
+
return await asRedemptionHuman(human, async () => {
|
|
1890
|
+
const authority = await readSubscriptionCoreCodexResetAuthority(deps.db, {
|
|
1891
|
+
accountId,
|
|
1892
|
+
workspaceId,
|
|
1893
|
+
credentialId,
|
|
1894
|
+
subjectId: human.subjectId,
|
|
1895
|
+
});
|
|
1896
|
+
if (organizationManaged && !authority?.owned) organizationManagedRedemption();
|
|
1897
|
+
if (!authority) throw new HTTPException(404, { message: "codex account not found" });
|
|
1898
|
+
let existing = await getCodexResetRedemptionAttempt(
|
|
1899
|
+
deps.db,
|
|
1900
|
+
workspaceId,
|
|
1901
|
+
parsed.data.attemptId,
|
|
1902
|
+
);
|
|
1903
|
+
if (!existing && authority.owned) {
|
|
1904
|
+
// The same person's attempt filed in another workspace (an
|
|
1905
|
+
// organization account redeemed from several workspaces, or a ledger
|
|
1906
|
+
// row the cutover kept in its legacy workspace) is moved here so it is
|
|
1907
|
+
// recovered on its one upstream key; another workspace's hold on this
|
|
1908
|
+
// credit refuses a second logical redemption.
|
|
1909
|
+
const fence = await fenceSubscriptionCoreCodexResetCredit(deps.db, {
|
|
2007
1910
|
accountId,
|
|
2008
1911
|
workspaceId,
|
|
2009
1912
|
credentialId,
|
|
2010
1913
|
subjectId: human.subjectId,
|
|
2011
|
-
browserSessionHash: human.browserSessionHash,
|
|
2012
1914
|
creditId: parsed.data.creditId,
|
|
2013
|
-
|
|
2014
|
-
claimHolderId,
|
|
1915
|
+
attemptId: parsed.data.attemptId,
|
|
2015
1916
|
});
|
|
2016
|
-
if (
|
|
2017
|
-
throw new HTTPException(
|
|
2018
|
-
|
|
2019
|
-
if (claimed.kind === "forbidden") {
|
|
2020
|
-
throw new HTTPException(403, {
|
|
2021
|
-
message: "redemption owner or credential is unavailable",
|
|
1917
|
+
if (fence === "held_elsewhere") {
|
|
1918
|
+
throw new HTTPException(409, {
|
|
1919
|
+
message: "this reset credit is already being redeemed from another workspace",
|
|
2022
1920
|
});
|
|
2023
1921
|
}
|
|
2024
|
-
if (
|
|
1922
|
+
if (fence === "refiled") {
|
|
1923
|
+
existing = await getCodexResetRedemptionAttempt(
|
|
1924
|
+
deps.db,
|
|
1925
|
+
workspaceId,
|
|
1926
|
+
parsed.data.attemptId,
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
if (!authority.owned) {
|
|
1931
|
+
throw new HTTPException(403, {
|
|
1932
|
+
message:
|
|
1933
|
+
"only an organization administrator or an administrator of the workspace that manages this subscription may redeem its reset credits",
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
if (
|
|
1937
|
+
existing &&
|
|
1938
|
+
(existing.credentialId !== credentialId ||
|
|
1939
|
+
existing.creditId !== parsed.data.creditId ||
|
|
1940
|
+
existing.subjectId !== human.subjectId)
|
|
1941
|
+
) {
|
|
1942
|
+
throw new HTTPException(409, { message: "logical redemption attempt identity mismatch" });
|
|
1943
|
+
}
|
|
1944
|
+
if (existing) {
|
|
1945
|
+
const adoption = await adoptCodexResetRedemptionAttempt(
|
|
1946
|
+
deps.db,
|
|
1947
|
+
{
|
|
1948
|
+
accountId,
|
|
1949
|
+
workspaceId,
|
|
1950
|
+
attemptId: existing.id,
|
|
1951
|
+
credentialId,
|
|
1952
|
+
creditId: existing.creditId,
|
|
1953
|
+
subjectId: human.subjectId,
|
|
1954
|
+
browserSessionHash: human.browserSessionHash,
|
|
1955
|
+
},
|
|
1956
|
+
subscriptionCoreCodexResetAuthority,
|
|
1957
|
+
);
|
|
1958
|
+
if (adoption.kind === "in_progress") {
|
|
2025
1959
|
throw new HTTPException(409, {
|
|
2026
|
-
message: "
|
|
1960
|
+
message: "this redemption is still in progress in another browser request",
|
|
2027
1961
|
});
|
|
2028
1962
|
}
|
|
2029
|
-
if (
|
|
2030
|
-
|
|
1963
|
+
if (adoption.kind === "not_found") {
|
|
1964
|
+
throw new HTTPException(409, { message: "redemption recovery state changed" });
|
|
2031
1965
|
}
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
outcome,
|
|
2038
|
-
// Durable provider truth must never wait for best-effort provider
|
|
2039
|
-
// readback. The browser refreshes overview independently after this
|
|
2040
|
-
// response; a hung account cannot suppress a completed outcome.
|
|
2041
|
-
overview: null,
|
|
2042
|
-
});
|
|
2043
|
-
if (claimed.kind === "completed") {
|
|
2044
|
-
return finishResponse(claimed.attempt.outcome!);
|
|
1966
|
+
if (adoption.kind === "forbidden") {
|
|
1967
|
+
throw new HTTPException(403, { message: "redemption owner is unavailable" });
|
|
1968
|
+
}
|
|
1969
|
+
if (adoption.kind === "conflict") {
|
|
1970
|
+
throw new HTTPException(409, { message: "logical redemption attempt identity mismatch" });
|
|
2045
1971
|
}
|
|
1972
|
+
existing = adoption.attempt;
|
|
1973
|
+
}
|
|
1974
|
+
if (authority.status !== "active" && existing?.status !== "completed") {
|
|
1975
|
+
throw new HTTPException(403, { message: "redemption credential is unavailable" });
|
|
1976
|
+
}
|
|
1977
|
+
const secret = deps.settings.betterAuthSecret;
|
|
1978
|
+
if (!secret) {
|
|
1979
|
+
throw new HTTPException(503, { message: "managed browser confirmation is unavailable" });
|
|
1980
|
+
}
|
|
1981
|
+
const expiresAt = Math.floor(Date.now() / 1000) + CODEX_REDEMPTION_CONFIRMATION_SECONDS;
|
|
1982
|
+
const confirmationToken = await signCodexRedemptionConfirmation(secret, {
|
|
1983
|
+
version: 1,
|
|
1984
|
+
attemptId: parsed.data.attemptId,
|
|
1985
|
+
workspaceId,
|
|
1986
|
+
credentialId,
|
|
1987
|
+
creditId: parsed.data.creditId,
|
|
1988
|
+
subjectId: human.subjectId,
|
|
1989
|
+
browserSessionHash: human.browserSessionHash,
|
|
1990
|
+
expiresAt,
|
|
1991
|
+
});
|
|
1992
|
+
return c.json({
|
|
1993
|
+
attemptId: parsed.data.attemptId,
|
|
1994
|
+
confirmationToken,
|
|
1995
|
+
expiresAt: new Date(expiresAt * 1000).toISOString(),
|
|
1996
|
+
resumable: existing?.status === "provider_started" || existing?.status === "completed",
|
|
1997
|
+
recoveryStatus:
|
|
1998
|
+
existing?.status === "provider_started" || existing?.status === "completed"
|
|
1999
|
+
? existing.status
|
|
2000
|
+
: null,
|
|
2001
|
+
});
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2046
2004
|
|
|
2047
|
-
|
|
2048
|
-
|
|
2005
|
+
async function coreCodexResetRedeem(
|
|
2006
|
+
c: Context,
|
|
2007
|
+
deps: ApiRouteDeps,
|
|
2008
|
+
input: CoreRedemptionInput,
|
|
2009
|
+
resolveTarget: () => Promise<RedemptionTarget> = () => workspaceRedemptionTarget(deps, input),
|
|
2010
|
+
) {
|
|
2011
|
+
c.header("cache-control", "no-store");
|
|
2012
|
+
const parsed = redemptionBody.safeParse(await c.req.json().catch(() => null));
|
|
2013
|
+
if (!parsed.success) {
|
|
2014
|
+
throw new HTTPException(400, { message: "explicit redemption confirmation is required" });
|
|
2015
|
+
}
|
|
2016
|
+
const secret = deps.settings.betterAuthSecret;
|
|
2017
|
+
if (!secret) {
|
|
2018
|
+
throw new HTTPException(503, { message: "managed browser confirmation is unavailable" });
|
|
2019
|
+
}
|
|
2020
|
+
const { human, accountId, workspaceId } = input;
|
|
2021
|
+
const target = await resolveTarget();
|
|
2022
|
+
const { credentialId } = target;
|
|
2023
|
+
const claims = await verifyCodexRedemptionConfirmation(secret, parsed.data.confirmationToken);
|
|
2024
|
+
if (
|
|
2025
|
+
!claims ||
|
|
2026
|
+
claims.attemptId !== parsed.data.attemptId ||
|
|
2027
|
+
claims.workspaceId !== workspaceId ||
|
|
2028
|
+
claims.credentialId !== credentialId ||
|
|
2029
|
+
claims.creditId !== parsed.data.creditId ||
|
|
2030
|
+
claims.subjectId !== human.subjectId ||
|
|
2031
|
+
claims.browserSessionHash !== human.browserSessionHash
|
|
2032
|
+
) {
|
|
2033
|
+
throw new HTTPException(403, { message: "redemption confirmation is invalid or expired" });
|
|
2034
|
+
}
|
|
2035
|
+
const db = deps.db;
|
|
2036
|
+
return await asRedemptionHuman(human, async () => {
|
|
2037
|
+
const claimHolderId = crypto.randomUUID();
|
|
2038
|
+
const claimed = await claimCodexResetRedemption(
|
|
2039
|
+
db,
|
|
2040
|
+
{
|
|
2041
|
+
id: parsed.data.attemptId,
|
|
2042
|
+
accountId,
|
|
2043
|
+
workspaceId,
|
|
2044
|
+
credentialId,
|
|
2045
|
+
subjectId: human.subjectId,
|
|
2046
|
+
browserSessionHash: human.browserSessionHash,
|
|
2047
|
+
creditId: parsed.data.creditId,
|
|
2048
|
+
confirmationExpiresAt: new Date(claims.expiresAt * 1000),
|
|
2049
|
+
claimHolderId,
|
|
2050
|
+
},
|
|
2051
|
+
subscriptionCoreCodexResetAuthority,
|
|
2052
|
+
subscriptionCoreCodexResetCreditFence,
|
|
2053
|
+
);
|
|
2054
|
+
if (claimed.kind === "not_found") {
|
|
2055
|
+
throw new HTTPException(404, { message: "codex account not found" });
|
|
2056
|
+
}
|
|
2057
|
+
if (claimed.kind === "forbidden") {
|
|
2058
|
+
if (target.organizationManaged) organizationManagedRedemption();
|
|
2059
|
+
throw new HTTPException(403, { message: "redemption owner or credential is unavailable" });
|
|
2060
|
+
}
|
|
2061
|
+
if (claimed.kind === "conflict") {
|
|
2062
|
+
throw new HTTPException(409, { message: "logical redemption attempt identity mismatch" });
|
|
2063
|
+
}
|
|
2064
|
+
if (claimed.kind === "in_progress") {
|
|
2065
|
+
return c.json({ status: "in_progress", attemptId: parsed.data.attemptId }, 409);
|
|
2066
|
+
}
|
|
2067
|
+
const finishResponse = (outcome: string) =>
|
|
2068
|
+
c.json({ status: "completed", attemptId: parsed.data.attemptId, outcome, overview: null });
|
|
2069
|
+
if (claimed.kind === "completed") return finishResponse(claimed.attempt.outcome!);
|
|
2070
|
+
|
|
2071
|
+
const attempt = claimed.attempt;
|
|
2072
|
+
const ledger = { accountId, workspaceId, attemptId: attempt.id, claimHolderId };
|
|
2073
|
+
const provider = target.provider();
|
|
2074
|
+
let token: Awaited<ReturnType<typeof provider.getToken>>;
|
|
2075
|
+
try {
|
|
2076
|
+
token = await provider.getToken();
|
|
2077
|
+
} catch {
|
|
2049
2078
|
if (attempt.status === "processing") {
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
credentialId,
|
|
2055
|
-
fetchImpl,
|
|
2079
|
+
await abandonCodexResetRedemptionBeforeProvider(db, ledger);
|
|
2080
|
+
return c.json(
|
|
2081
|
+
{ status: "preflight_unavailable", attemptId: attempt.id, retryable: true },
|
|
2082
|
+
503,
|
|
2056
2083
|
);
|
|
2057
|
-
if (!details.ok) {
|
|
2058
|
-
await abandonCodexResetRedemptionBeforeProvider(db, {
|
|
2059
|
-
accountId,
|
|
2060
|
-
workspaceId,
|
|
2061
|
-
attemptId: attempt.id,
|
|
2062
|
-
claimHolderId,
|
|
2063
|
-
});
|
|
2064
|
-
return c.json(
|
|
2065
|
-
{
|
|
2066
|
-
status: "preflight_unavailable",
|
|
2067
|
-
attemptId: attempt.id,
|
|
2068
|
-
retryable: true,
|
|
2069
|
-
},
|
|
2070
|
-
503,
|
|
2071
|
-
);
|
|
2072
|
-
}
|
|
2073
|
-
if (!freshActionableCredit(details.details, attempt.creditId)) {
|
|
2074
|
-
await abandonCodexResetRedemptionBeforeProvider(db, {
|
|
2075
|
-
accountId,
|
|
2076
|
-
workspaceId,
|
|
2077
|
-
attemptId: attempt.id,
|
|
2078
|
-
claimHolderId,
|
|
2079
|
-
});
|
|
2080
|
-
return c.json(
|
|
2081
|
-
{
|
|
2082
|
-
status: "not_actionable",
|
|
2083
|
-
attemptId: attempt.id,
|
|
2084
|
-
retryable: false,
|
|
2085
|
-
},
|
|
2086
|
-
409,
|
|
2087
|
-
);
|
|
2088
|
-
}
|
|
2089
2084
|
}
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
}
|
|
2085
|
+
await releaseCodexResetRedemptionClaim(db, {
|
|
2086
|
+
...ledger,
|
|
2087
|
+
failureKind: "provider_auth_unavailable",
|
|
2088
|
+
});
|
|
2089
|
+
return c.json(
|
|
2090
|
+
{ status: "provider_unavailable", attemptId: attempt.id, retryable: true },
|
|
2091
|
+
503,
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2094
|
+
const auth = {
|
|
2095
|
+
accessToken: token.accessToken,
|
|
2096
|
+
chatgptAccountId: token.chatgptAccountId,
|
|
2097
|
+
isFedramp: token.isFedramp,
|
|
2098
|
+
clientVersion: CODEX_CLIENT_VERSION,
|
|
2099
|
+
};
|
|
2100
|
+
const requestFetch = provider.fetch;
|
|
2101
|
+
if (attempt.status === "processing") {
|
|
2102
|
+
const details = await fetchCodexRateLimitResetCredits(auth, requestFetch);
|
|
2103
|
+
if (!details.ok) {
|
|
2104
|
+
await abandonCodexResetRedemptionBeforeProvider(db, ledger);
|
|
2111
2105
|
return c.json(
|
|
2112
|
-
{
|
|
2113
|
-
status: "provider_unavailable",
|
|
2114
|
-
attemptId: attempt.id,
|
|
2115
|
-
retryable: true,
|
|
2116
|
-
},
|
|
2106
|
+
{ status: "preflight_unavailable", attemptId: attempt.id, retryable: true },
|
|
2117
2107
|
503,
|
|
2118
2108
|
);
|
|
2119
2109
|
}
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2110
|
+
if (!freshActionableCredit(details.details, attempt.creditId)) {
|
|
2111
|
+
await abandonCodexResetRedemptionBeforeProvider(db, ledger);
|
|
2112
|
+
return c.json({ status: "not_actionable", attemptId: attempt.id, retryable: false }, 409);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
const fenced = await fenceCodexResetRedemptionSend(
|
|
2116
|
+
db,
|
|
2117
|
+
{
|
|
2118
|
+
...ledger,
|
|
2125
2119
|
credentialId,
|
|
2126
2120
|
subjectId: human.subjectId,
|
|
2127
2121
|
browserSessionHash: human.browserSessionHash,
|
|
2122
|
+
},
|
|
2123
|
+
subscriptionCoreCodexResetAuthority,
|
|
2124
|
+
);
|
|
2125
|
+
if (fenced.kind !== "ready") {
|
|
2126
|
+
if (fenced.reason === "confirmation_expired") {
|
|
2127
|
+
return c.json(
|
|
2128
|
+
{ status: "confirmation_expired", attemptId: attempt.id, retryable: true },
|
|
2129
|
+
403,
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
if (fenced.reason === "credential_unavailable") {
|
|
2133
|
+
return c.json(
|
|
2134
|
+
{ status: "provider_unavailable", attemptId: attempt.id, retryable: true },
|
|
2135
|
+
503,
|
|
2136
|
+
);
|
|
2137
|
+
}
|
|
2138
|
+
return c.json({ status: "in_progress", attemptId: attempt.id }, 409);
|
|
2139
|
+
}
|
|
2140
|
+
const sendAttempt = fenced.attempt;
|
|
2141
|
+
// The one upstream idempotency key of this logical attempt: an ambiguous
|
|
2142
|
+
// send is recovered with the same key, never reissued with a new one.
|
|
2143
|
+
const consumed = await consumeCodexRateLimitResetCredit(
|
|
2144
|
+
auth,
|
|
2145
|
+
{ idempotencyKey: sendAttempt.upstreamIdempotencyKey, creditId: sendAttempt.creditId },
|
|
2146
|
+
requestFetch,
|
|
2147
|
+
);
|
|
2148
|
+
if (!consumed.ok) {
|
|
2149
|
+
await releaseCodexResetRedemptionClaim(db, {
|
|
2150
|
+
...ledger,
|
|
2151
|
+
failureKind: `provider_${consumed.reason}`,
|
|
2128
2152
|
});
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2153
|
+
return c.json({ status: "ambiguous", attemptId: attempt.id, retryable: true }, 503);
|
|
2154
|
+
}
|
|
2155
|
+
const completion = await completeSubscriptionCoreCodexResetRedemption(db, {
|
|
2156
|
+
...ledger,
|
|
2157
|
+
outcome: consumed.result.outcome,
|
|
2158
|
+
});
|
|
2159
|
+
if (!completion.attempt) {
|
|
2160
|
+
return c.json({ status: "in_progress", attemptId: attempt.id }, 409);
|
|
2161
|
+
}
|
|
2162
|
+
void deliverSubscriptionCoreCodexWake(db, completion.wake).catch(() => undefined);
|
|
2163
|
+
return finishResponse(completion.attempt.outcome!);
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
/**
|
|
2168
|
+
* The overview on the core: per-account usage and reset details settle
|
|
2169
|
+
* independently through the connection seam, with at most four provider
|
|
2170
|
+
* calls at a time and the legacy route deadline. Redemption flags follow the
|
|
2171
|
+
* 6.3 authority for the person in their browser or an agent acting as them.
|
|
2172
|
+
*/
|
|
2173
|
+
async function coreCodexOverview(
|
|
2174
|
+
c: Context,
|
|
2175
|
+
deps: ApiRouteDeps,
|
|
2176
|
+
grant: Awaited<ReturnType<typeof requireAccessGrant>>,
|
|
2177
|
+
workspaceId: string,
|
|
2178
|
+
) {
|
|
2179
|
+
const human = await managedHumanOrAgent(c, deps);
|
|
2180
|
+
const browserHuman =
|
|
2181
|
+
human &&
|
|
2182
|
+
human.subjectId === grant.subjectId &&
|
|
2183
|
+
hasPermission(grant.permissions, "connections:write")
|
|
2184
|
+
? human
|
|
2185
|
+
: null;
|
|
2186
|
+
const scope = coreCodexConnectionScope(
|
|
2187
|
+
{ accountId: grant.accountId, workspaceId },
|
|
2188
|
+
grant.subjectId,
|
|
2189
|
+
);
|
|
2190
|
+
const { accounts } = await getSubscriptionCoreCodexWorkspaceProjection(deps.db, {
|
|
2191
|
+
accountId: grant.accountId,
|
|
2192
|
+
workspaceId,
|
|
2193
|
+
});
|
|
2194
|
+
const recoveries = browserHuman
|
|
2195
|
+
? await asRedemptionHuman(browserHuman, () =>
|
|
2196
|
+
listSubscriptionCoreCodexResetRedemptionRecoveries(deps.db, {
|
|
2197
|
+
accountId: grant.accountId,
|
|
2198
|
+
workspaceId,
|
|
2199
|
+
subjectId: browserHuman.subjectId,
|
|
2200
|
+
}),
|
|
2201
|
+
)
|
|
2202
|
+
: [];
|
|
2203
|
+
const fetchImpl = (deps.codexFetch ?? fetch) as CodexFetch;
|
|
2204
|
+
const sources: CodexOverviewSources = {
|
|
2205
|
+
usage: async (connectionId) => {
|
|
2206
|
+
const { usage, recovered } = await fetchSubscriptionCoreCodexUsage(
|
|
2207
|
+
deps.db,
|
|
2208
|
+
deps.settings,
|
|
2209
|
+
scope,
|
|
2210
|
+
connectionId,
|
|
2211
|
+
fetchImpl,
|
|
2212
|
+
);
|
|
2213
|
+
if (recovered) {
|
|
2214
|
+
void deliverSubscriptionCoreCodexWake(deps.db, {
|
|
2215
|
+
accountId: grant.accountId,
|
|
2216
|
+
reason: "usage_recovered",
|
|
2217
|
+
}).catch(() => undefined);
|
|
2218
|
+
}
|
|
2219
|
+
return usage;
|
|
2220
|
+
},
|
|
2221
|
+
details: async (connectionId) => {
|
|
2222
|
+
let token;
|
|
2223
|
+
try {
|
|
2224
|
+
token = await buildSubscriptionCoreCodexConnectionTokenResolver(
|
|
2225
|
+
deps.db,
|
|
2226
|
+
deps.settings,
|
|
2227
|
+
scope,
|
|
2228
|
+
connectionId,
|
|
2229
|
+
null,
|
|
2230
|
+
).getToken();
|
|
2231
|
+
} catch (error) {
|
|
2232
|
+
// Not readable in this workspace context: reset details are not
|
|
2233
|
+
// available here (reported as unsupported, not as a failure).
|
|
2234
|
+
if (error instanceof SubscriptionCoreCodexOperationUnavailableError) {
|
|
2235
|
+
return { ok: false as const, status: 404, reason: "http_error" as const };
|
|
2149
2236
|
}
|
|
2150
|
-
return
|
|
2237
|
+
return {
|
|
2238
|
+
ok: false as const,
|
|
2239
|
+
status: 0,
|
|
2240
|
+
reason:
|
|
2241
|
+
error instanceof CodexReloginRequired
|
|
2242
|
+
? ("needs_relogin" as const)
|
|
2243
|
+
: ("network_error" as const),
|
|
2244
|
+
};
|
|
2151
2245
|
}
|
|
2152
|
-
|
|
2153
|
-
const sendAttempt = fenced.attempt;
|
|
2154
|
-
const consumed = await consumeCodexRateLimitResetCredit(
|
|
2246
|
+
return await fetchCodexRateLimitResetCredits(
|
|
2155
2247
|
{
|
|
2156
2248
|
accessToken: token.accessToken,
|
|
2157
2249
|
chatgptAccountId: token.chatgptAccountId,
|
|
2158
2250
|
isFedramp: token.isFedramp,
|
|
2159
2251
|
clientVersion: CODEX_CLIENT_VERSION,
|
|
2160
2252
|
},
|
|
2161
|
-
|
|
2162
|
-
idempotencyKey: sendAttempt.upstreamIdempotencyKey,
|
|
2163
|
-
creditId: sendAttempt.creditId,
|
|
2164
|
-
},
|
|
2165
|
-
fetchImpl,
|
|
2253
|
+
buildSubscriptionCoreCodexOperationFetch(deps.db, scope, null, connectionId, fetchImpl),
|
|
2166
2254
|
);
|
|
2167
|
-
if (!consumed.ok) {
|
|
2168
|
-
await releaseCodexResetRedemptionClaim(db, {
|
|
2169
|
-
accountId,
|
|
2170
|
-
workspaceId,
|
|
2171
|
-
attemptId: attempt.id,
|
|
2172
|
-
claimHolderId,
|
|
2173
|
-
failureKind: `provider_${consumed.reason}`,
|
|
2174
|
-
});
|
|
2175
|
-
return c.json({ status: "ambiguous", attemptId: attempt.id, retryable: true }, 503);
|
|
2176
|
-
}
|
|
2177
|
-
const completion = await completeCodexResetRedemption(db, {
|
|
2178
|
-
accountId,
|
|
2179
|
-
workspaceId,
|
|
2180
|
-
attemptId: attempt.id,
|
|
2181
|
-
claimHolderId,
|
|
2182
|
-
outcome: consumed.result.outcome,
|
|
2183
|
-
});
|
|
2184
|
-
const completed = completion.result;
|
|
2185
|
-
if (!completed) {
|
|
2186
|
-
return c.json({ status: "in_progress", attemptId: attempt.id }, 409);
|
|
2187
|
-
}
|
|
2188
|
-
// The outbox is durable; signaling is best-effort and must not hold the
|
|
2189
|
-
// owning human's already-completed provider outcome hostage.
|
|
2190
|
-
void signalCodexCapacityTargets(deps, completion.wakeTargets).catch(() => undefined);
|
|
2191
|
-
return finishResponse(completed.outcome!);
|
|
2192
2255
|
},
|
|
2256
|
+
};
|
|
2257
|
+
const accessFor = async (account: CodexAccountStatus) => {
|
|
2258
|
+
// Organization accounts are redeemable by an organization administrator
|
|
2259
|
+
// (M3 PR 3b); the SQL authority decides for both sources.
|
|
2260
|
+
const authority = browserHuman
|
|
2261
|
+
? await asRedemptionHuman(browserHuman, () =>
|
|
2262
|
+
readSubscriptionCoreCodexResetAuthority(deps.db, {
|
|
2263
|
+
accountId: grant.accountId,
|
|
2264
|
+
workspaceId,
|
|
2265
|
+
credentialId: account.id,
|
|
2266
|
+
subjectId: browserHuman.subjectId,
|
|
2267
|
+
}),
|
|
2268
|
+
).catch(() => null)
|
|
2269
|
+
: null;
|
|
2270
|
+
const canResumeRedemption = authority?.owned === true;
|
|
2271
|
+
const redemptionAccess: CodexRedemptionAccess =
|
|
2272
|
+
!browserHuman || (account.source === "organization" && !canResumeRedemption)
|
|
2273
|
+
? { ownership: "managed_human_unavailable", canClaimUnownedViaReconnect: false }
|
|
2274
|
+
: {
|
|
2275
|
+
ownership: canResumeRedemption ? "current_human" : "different_human",
|
|
2276
|
+
canClaimUnownedViaReconnect: false,
|
|
2277
|
+
};
|
|
2278
|
+
return {
|
|
2279
|
+
redemptionAccess,
|
|
2280
|
+
canRedeem: canResumeRedemption && account.status === "active",
|
|
2281
|
+
canResumeRedemption,
|
|
2282
|
+
redemptions: canResumeRedemption
|
|
2283
|
+
? recoveries.filter((recovery) => recovery.credentialId === account.id)
|
|
2284
|
+
: [],
|
|
2285
|
+
};
|
|
2286
|
+
};
|
|
2287
|
+
const overview: Record<string, Awaited<ReturnType<typeof fetchCodexAccountOverview>>> = {};
|
|
2288
|
+
const queue = [...accounts];
|
|
2289
|
+
const providerCall = createProviderCallLimiter(4);
|
|
2290
|
+
let routeTimedOut = false;
|
|
2291
|
+
const worker = async (): Promise<void> => {
|
|
2292
|
+
for (;;) {
|
|
2293
|
+
if (routeTimedOut) return;
|
|
2294
|
+
const account = queue.shift();
|
|
2295
|
+
if (!account) return;
|
|
2296
|
+
const access = await accessFor(account);
|
|
2297
|
+
overview[account.id] = await fetchCodexAccountOverview(
|
|
2298
|
+
deps,
|
|
2299
|
+
workspaceId,
|
|
2300
|
+
account,
|
|
2301
|
+
access.redemptionAccess,
|
|
2302
|
+
access.canRedeem,
|
|
2303
|
+
access.canResumeRedemption,
|
|
2304
|
+
access.redemptions,
|
|
2305
|
+
providerCall,
|
|
2306
|
+
sources,
|
|
2307
|
+
);
|
|
2308
|
+
}
|
|
2309
|
+
};
|
|
2310
|
+
const workers = Promise.all(
|
|
2311
|
+
Array.from({ length: Math.min(4, Math.max(1, accounts.length)) }, () => worker()),
|
|
2193
2312
|
);
|
|
2313
|
+
let deadline: ReturnType<typeof setTimeout> | undefined;
|
|
2314
|
+
await Promise.race([
|
|
2315
|
+
workers,
|
|
2316
|
+
new Promise<void>((resolve) => {
|
|
2317
|
+
deadline = setTimeout(() => {
|
|
2318
|
+
routeTimedOut = true;
|
|
2319
|
+
queue.length = 0;
|
|
2320
|
+
resolve();
|
|
2321
|
+
}, CODEX_OVERVIEW_ROUTE_TIMEOUT_MS);
|
|
2322
|
+
}),
|
|
2323
|
+
]);
|
|
2324
|
+
if (deadline) clearTimeout(deadline);
|
|
2325
|
+
if (routeTimedOut) {
|
|
2326
|
+
// Unscheduled accounts come from the core's persisted quota only.
|
|
2327
|
+
const unavailableProviderCall: CodexProviderCall = async () => {
|
|
2328
|
+
throw new Error("Codex overview route deadline reached");
|
|
2329
|
+
};
|
|
2330
|
+
await Promise.all(
|
|
2331
|
+
accounts
|
|
2332
|
+
.filter((account) => overview[account.id] == null)
|
|
2333
|
+
.map(async (account) => {
|
|
2334
|
+
const access = await accessFor(account);
|
|
2335
|
+
const fallback = await fetchCodexAccountOverview(
|
|
2336
|
+
deps,
|
|
2337
|
+
workspaceId,
|
|
2338
|
+
account,
|
|
2339
|
+
access.redemptionAccess,
|
|
2340
|
+
false,
|
|
2341
|
+
access.canResumeRedemption,
|
|
2342
|
+
access.redemptions,
|
|
2343
|
+
unavailableProviderCall,
|
|
2344
|
+
sources,
|
|
2345
|
+
);
|
|
2346
|
+
overview[account.id] ??= fallback;
|
|
2347
|
+
}),
|
|
2348
|
+
);
|
|
2349
|
+
void workers.catch(() => undefined);
|
|
2350
|
+
}
|
|
2351
|
+
return c.json({ accounts: overview });
|
|
2194
2352
|
}
|