@opengeni/api-router 0.30.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-grant-rls.d.ts +3 -0
- package/dist/admission-trace.d.ts +8 -0
- package/dist/api-websocket.d.ts +18 -0
- package/dist/app.d.ts +50 -2
- package/dist/app.js +12 -1
- package/dist/artifact-catalog-cursor.d.ts +27 -0
- package/dist/auth/managed-auth-attempt-context.d.ts +10 -0
- package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +6 -0
- package/dist/auth/managed-auth.d.ts +102 -3
- package/dist/auth/managed-email.d.ts +29 -0
- package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
- package/dist/auth/new-signups-gate.d.ts +23 -0
- package/dist/auth/organization-user-setup.d.ts +80 -0
- package/dist/auth/signup-funnel-metrics.d.ts +44 -0
- package/dist/browser-controller-authority.d.ts +1 -61
- package/dist/browser-ephemeral.d.ts +5 -0
- package/dist/browser-state-authority.d.ts +1 -27
- package/dist/chunk-6TZ5LG3O.js +123948 -0
- package/dist/chunk-6TZ5LG3O.js.map +1 -0
- package/dist/chunk-OGQDXVXY.js +18 -0
- package/dist/chunk-OGQDXVXY.js.map +1 -0
- package/dist/claude-subscription-account-conflicts.d.ts +1 -0
- package/dist/claude-subscription-account-usage.d.ts +39 -0
- package/dist/claude-subscription-oauth.d.ts +25 -0
- package/dist/claude-subscription-profile.d.ts +9 -0
- package/dist/claude-subscription-usage.d.ts +44 -0
- package/dist/claude-workspace-connection.d.ts +15 -0
- package/dist/codemode.d.ts +32 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-ownership.d.ts +54 -0
- package/dist/controller-data-plane.d.ts +32 -0
- package/dist/editable-artifact-source-session.d.ts +3 -0
- package/dist/editable-artifact-websocket.d.ts +10 -18
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/fatal-process-boundary.d.ts +25 -0
- package/dist/github-access.d.ts +31 -2
- package/dist/http/acting-person.d.ts +24 -0
- package/dist/http/api-contract-compat.d.ts +19 -0
- package/dist/http/api-error.d.ts +27 -1
- package/dist/http/cors.d.ts +17 -0
- package/dist/http/deprecation.d.ts +42 -0
- package/dist/http/interaction-control-error.d.ts +13 -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/request-body.d.ts +27 -0
- package/dist/http/request-source.d.ts +52 -0
- package/dist/http/sse.d.ts +17 -6
- package/dist/http/unmatched-route.d.ts +18 -0
- package/dist/http/user-content.d.ts +36 -0
- package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.js +1091 -162
- package/dist/index.js.map +1 -1
- package/dist/integration-endpoint-test.d.ts +20 -0
- package/dist/integrations/api-integrations.d.ts +2 -2
- package/dist/integrations/atlassian.d.ts +29 -3
- package/dist/integrations/connect-authority.d.ts +1 -0
- package/dist/integrations/connect-callback-return.d.ts +6 -0
- package/dist/integrations/fiken.d.ts +16 -2
- package/dist/integrations/github-app-connect.d.ts +47 -0
- package/dist/integrations/github-installation-proof.d.ts +3 -0
- package/dist/integrations/github-lens-connect.d.ts +116 -0
- package/dist/integrations/github-skill-source.d.ts +1 -5
- package/dist/integrations/google-drive.d.ts +24 -0
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/marketplace-plugin.d.ts +2691 -0
- package/dist/integrations/oauth-client.d.ts +178 -18
- package/dist/integrations/oauth-profiles.d.ts +222 -0
- package/dist/integrations/oauth-return-path.d.ts +1 -0
- package/dist/integrations/personal-github-repositories.d.ts +84 -0
- package/dist/integrations/personal-github.d.ts +42 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +19 -1
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +367 -12
- package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
- package/dist/integrations/slack-install.d.ts +20 -0
- package/dist/integrations/slack-interactions.d.ts +107 -9
- package/dist/integrations/slack-mrkdwn.d.ts +41 -0
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/slack-session-defaults.d.ts +58 -0
- package/dist/integrations/slack-task-file-upload.d.ts +36 -0
- package/dist/integrations/social-oauth.d.ts +7 -0
- package/dist/interaction-agent-access.d.ts +15 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/interaction-metrics.d.ts +2 -0
- package/dist/mcp/action-catalog-types.d.ts +9 -0
- package/dist/mcp/action-catalog.gen.d.ts +2 -0
- package/dist/mcp/capability-account-readiness.d.ts +7 -0
- package/dist/mcp/child-read-evidence.d.ts +9 -0
- package/dist/mcp/command-read.d.ts +24 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/contract-input.d.ts +11 -0
- package/dist/mcp/documents.d.ts +7 -5
- package/dist/mcp/files.d.ts +2 -2
- package/dist/mcp/first-party-tool-permissions.d.ts +650 -0
- package/dist/mcp/knowledge-entries.d.ts +5 -0
- package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
- package/dist/mcp/receipts.d.ts +9 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-input.d.ts +1189 -0
- package/dist/mcp/scheduled-task-view.d.ts +22 -4
- package/dist/mcp/server.d.ts +255 -3
- package/dist/mcp/session-event-view.d.ts +94 -0
- package/dist/mcp/session-view.d.ts +66 -2
- package/dist/mcp/session-wait.d.ts +174 -0
- package/dist/mcp-oauth.d.ts +52 -0
- package/dist/model-catalog.d.ts +11 -31
- package/dist/observability.d.ts +3 -1
- package/dist/organization-mcp.d.ts +52 -0
- package/dist/organization-recovery-notifications.d.ts +41 -0
- package/dist/routes/analytics-consent.d.ts +26 -0
- package/dist/routes/api-integrations.d.ts +25 -0
- package/dist/routes/api-keys.d.ts +17 -0
- package/dist/routes/artifact-catalog.d.ts +6 -0
- package/dist/routes/automations.d.ts +19 -0
- package/dist/routes/billing.d.ts +49 -0
- package/dist/routes/browser-sessions.d.ts +59 -2
- 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 +146 -0
- package/dist/routes/codex.d.ts +76 -2
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/computer-sessions.d.ts +14 -0
- package/dist/routes/connect.d.ts +5 -0
- package/dist/routes/editable-artifacts.d.ts +2 -0
- package/dist/routes/external-identity-links.d.ts +6 -0
- package/dist/routes/feedback.d.ts +3 -0
- package/dist/routes/github-browser-pages.d.ts +12 -0
- package/dist/routes/github.d.ts +2 -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 +30 -1
- package/dist/routes/knowledge.d.ts +3 -0
- package/dist/routes/managed-auth-session-sets.d.ts +26 -0
- package/dist/routes/managed-onboarding.d.ts +29 -0
- package/dist/routes/managed-sign-in-methods.d.ts +6 -0
- package/dist/routes/model-connection-access.d.ts +3 -0
- package/dist/routes/organization-integration-policy.d.ts +3 -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-providers.d.ts +3 -0
- package/dist/routes/organization-recovery.d.ts +19 -0
- package/dist/routes/organization-service-accounts.d.ts +3 -0
- package/dist/routes/organization-sessions.d.ts +26 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/session-artifact-associations.d.ts +4 -0
- package/dist/routes/session-history-imports.d.ts +9 -0
- package/dist/routes/sessions.d.ts +44 -6
- package/dist/routes/skill-content.d.ts +4 -0
- package/dist/routes/skill-install-authority.d.ts +6 -0
- package/dist/routes/subscription-account-pools.d.ts +34 -0
- package/dist/routes/subscription-pool-access.d.ts +16 -0
- package/dist/routes/supergrok.d.ts +6 -1
- package/dist/routes/usage-allowances.d.ts +12 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-artifacts.d.ts +2 -1
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-integrations.d.ts +35 -0
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspace-model-providers.d.ts +4 -0
- package/dist/routes/workspaces.d.ts +29 -0
- package/dist/sandbox/auth-callout.d.ts +8 -0
- package/dist/sandbox/channel-a.d.ts +19 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +9 -1
- package/dist/sandbox/machines.d.ts +5 -2
- package/dist/sandbox/metrics-ingestion.d.ts +28 -20
- package/dist/sandbox/rematerialize.d.ts +2 -0
- package/dist/sandbox/session-attach-variable-sets.d.ts +30 -0
- package/dist/sandbox/viewer.d.ts +26 -1
- package/dist/sandbox-file-artifacts.d.ts +50 -0
- package/dist/scheduled-task-deletion.d.ts +13 -0
- package/dist/session-create-observability.d.ts +5 -0
- package/dist/session-event-view-MPS2OZD3.js +292 -0
- package/dist/session-event-view-MPS2OZD3.js.map +1 -0
- package/dist/site-session-origin.d.ts +13 -0
- package/dist/site-uploads.d.ts +41 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/dist/temporal-schedule-sync.d.ts +26 -0
- package/dist/transcription/providers/xai-subscription.d.ts +1 -0
- package/dist/transcription/service.d.ts +2 -1
- package/dist/work-discovery-observability.d.ts +33 -0
- package/dist/workspace-artifact-content.d.ts +28 -0
- package/dist/workspace-artifact-provenance.d.ts +7 -0
- package/dist/workspace-delete-observability.d.ts +10 -0
- package/dist/workspace-deletion.d.ts +6 -0
- package/dist/workspace-state-projection.d.ts +3 -5
- package/dist/workspace-tool-gateway-observability.d.ts +17 -0
- package/dist/workspace-tool-gateway.d.ts +123 -0
- package/dist/workspace-webhook-dispatch.d.ts +26 -0
- package/package.json +23 -19
- package/src/access-grant-rls.ts +75 -0
- package/src/admission-trace.ts +14 -0
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +1967 -252
- package/src/artifact-catalog-cursor.ts +60 -0
- package/src/auth/managed-auth-attempt-context.ts +70 -0
- package/src/auth/managed-auth-rate-limits.ts +252 -0
- package/src/auth/managed-auth-session-adapter.ts +222 -0
- package/src/auth/managed-auth.ts +536 -60
- package/src/auth/managed-email.ts +174 -0
- 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 +307 -0
- package/src/auth/signup-funnel-metrics.ts +164 -0
- package/src/browser-controller-authority.ts +1 -186
- package/src/browser-ephemeral.ts +39 -0
- package/src/browser-state-authority.ts +1 -236
- package/src/claude-subscription-account-conflicts.ts +12 -0
- package/src/claude-subscription-account-usage.ts +48 -0
- package/src/claude-subscription-oauth.ts +362 -0
- package/src/claude-subscription-profile.ts +73 -0
- package/src/claude-subscription-usage.ts +97 -0
- package/src/claude-workspace-connection.ts +82 -0
- package/src/codemode.ts +194 -30
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-ownership.ts +199 -0
- package/src/controller-data-plane.ts +81 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +28 -3
- package/src/editable-artifact-production.ts +23 -7
- package/src/editable-artifact-source-session.ts +123 -0
- package/src/editable-artifact-websocket.ts +27 -21
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/fatal-process-boundary.ts +246 -0
- package/src/gateway-realtime.ts +1 -1
- package/src/github-access.ts +147 -5
- package/src/http/acting-person.ts +71 -0
- package/src/http/api-contract-compat.ts +49 -0
- package/src/http/api-error.ts +115 -1
- package/src/http/auth.ts +48 -0
- package/src/http/cors.ts +67 -0
- package/src/http/deprecation.ts +118 -0
- package/src/http/interaction-control-error.ts +164 -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/request-body.ts +127 -0
- package/src/http/request-source.ts +178 -0
- package/src/http/sse.ts +486 -132
- package/src/http/unmatched-route.ts +60 -0
- package/src/http/user-content.ts +158 -0
- package/src/http/websocket-upgrade-dispatch.ts +29 -0
- package/src/index.ts +157 -52
- package/src/integration-endpoint-test.ts +291 -0
- package/src/integrations/api-integrations.ts +98 -11
- package/src/integrations/atlassian.ts +474 -136
- package/src/integrations/connect-authority.ts +2 -0
- package/src/integrations/connect-callback-return.ts +86 -0
- package/src/integrations/fiken.ts +354 -64
- package/src/integrations/github-app-connect.ts +435 -0
- package/src/integrations/github-installation-proof.ts +60 -0
- package/src/integrations/github-lens-connect.ts +76 -0
- package/src/integrations/github-skill-source.ts +2 -142
- package/src/integrations/google-drive.ts +717 -225
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/marketplace-plugin.ts +48 -0
- package/src/integrations/oauth-client.ts +1356 -459
- package/src/integrations/oauth-profiles.ts +537 -0
- package/src/integrations/oauth-return-path.ts +18 -0
- package/src/integrations/personal-github-repositories.ts +674 -0
- package/src/integrations/personal-github.ts +825 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +250 -22
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +963 -239
- package/src/integrations/slack-file-upload-flow.ts +147 -0
- package/src/integrations/slack-install.ts +83 -0
- package/src/integrations/slack-interactions.ts +3167 -474
- package/src/integrations/slack-mrkdwn.ts +193 -0
- package/src/integrations/slack-routing.ts +337 -0
- package/src/integrations/slack-session-defaults.ts +224 -0
- package/src/integrations/slack-task-file-upload.ts +323 -0
- package/src/integrations/social-oauth.ts +213 -15
- package/src/interaction-agent-access.ts +48 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/interaction-metrics.ts +30 -0
- package/src/mcp/action-catalog-types.ts +9 -0
- package/src/mcp/action-catalog.gen.ts +6696 -0
- package/src/mcp/capability-account-readiness.ts +17 -0
- package/src/mcp/child-read-evidence.ts +19 -0
- package/src/mcp/command-read.ts +109 -0
- package/src/mcp/company-profile-agent-admin.ts +198 -0
- package/src/mcp/contract-input.ts +141 -0
- package/src/mcp/documents.ts +65 -377
- package/src/mcp/editable-artifacts.ts +12 -6
- package/src/mcp/files.ts +80 -43
- package/src/mcp/first-party-tool-permissions.ts +305 -0
- package/src/mcp/knowledge-entries.ts +322 -0
- package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
- package/src/mcp/receipts.ts +34 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-input.ts +76 -0
- package/src/mcp/scheduled-task-view.ts +44 -3
- package/src/mcp/server.ts +3030 -1267
- package/src/mcp/session-event-view.ts +383 -0
- package/src/mcp/session-view.ts +84 -3
- package/src/mcp/session-wait.ts +665 -0
- package/src/mcp-oauth.ts +1123 -0
- package/src/model-catalog.ts +91 -333
- package/src/observability.ts +15 -1
- package/src/organization-mcp.ts +397 -0
- package/src/organization-recovery-notifications.ts +103 -0
- package/src/routes/analytics-consent.ts +123 -0
- package/src/routes/api-integrations.ts +364 -74
- package/src/routes/api-keys.ts +336 -10
- package/src/routes/artifact-catalog.ts +279 -0
- package/src/routes/automations.ts +768 -0
- package/src/routes/billing.ts +461 -46
- package/src/routes/browser-sessions.ts +780 -112
- package/src/routes/canonical-human-identities.ts +90 -14
- package/src/routes/capabilities.ts +107 -6
- package/src/routes/channels.ts +20 -3
- package/src/routes/claude-subscription-accounts.ts +334 -0
- package/src/routes/claude-subscription-oauth.ts +142 -0
- package/src/routes/client-errors.ts +362 -0
- package/src/routes/codex.ts +833 -116
- package/src/routes/company-brain.ts +322 -0
- package/src/routes/company-profile.ts +65 -1
- package/src/routes/computer-sessions.ts +557 -106
- package/src/routes/connect.ts +1938 -0
- package/src/routes/connections.ts +1039 -349
- package/src/routes/documents.ts +488 -287
- package/src/routes/editable-artifacts.ts +79 -9
- package/src/routes/enrollments.ts +135 -39
- package/src/routes/environments.ts +154 -58
- package/src/routes/external-identity-links.ts +201 -0
- package/src/routes/feedback.ts +113 -0
- package/src/routes/files.ts +267 -33
- package/src/routes/github-browser-pages.ts +162 -0
- package/src/routes/github.ts +446 -144
- package/src/routes/insights-response-cache.ts +83 -0
- package/src/routes/insights-usage.ts +345 -0
- package/src/routes/insights.ts +153 -19
- package/src/routes/install.ts +133 -38
- package/src/routes/integration-facets.ts +107 -33
- package/src/routes/interaction-resources.ts +30 -3
- package/src/routes/knowledge.ts +488 -0
- package/src/routes/machines.ts +313 -15
- package/src/routes/managed-auth-session-sets.ts +1218 -0
- package/src/routes/managed-onboarding.ts +337 -0
- package/src/routes/managed-sign-in-methods.ts +433 -0
- package/src/routes/memory-slack-publications.ts +3 -2
- package/src/routes/model-connection-access.ts +208 -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 +178 -0
- package/src/routes/organization-memberships.ts +1019 -27
- package/src/routes/organization-model-providers.ts +293 -0
- package/src/routes/organization-recovery.ts +325 -0
- package/src/routes/organization-service-accounts.ts +128 -0
- package/src/routes/organization-sessions.ts +350 -0
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +370 -0
- package/src/routes/plugins.ts +251 -22
- package/src/routes/pr-review-github.ts +881 -0
- package/src/routes/pr-review.ts +622 -0
- package/src/routes/preference-registry.ts +12 -93
- package/src/routes/rigs.ts +167 -50
- package/src/routes/scheduled-tasks.ts +346 -41
- package/src/routes/session-artifact-associations.ts +87 -0
- package/src/routes/session-history-imports.ts +280 -0
- package/src/routes/sessions.ts +2306 -264
- package/src/routes/skill-content.ts +297 -0
- package/src/routes/skill-install-authority.ts +64 -0
- package/src/routes/skills.ts +62 -9
- package/src/routes/social.ts +46 -18
- package/src/routes/subscription-account-pools.ts +436 -0
- package/src/routes/subscription-pool-access.ts +126 -0
- package/src/routes/supergrok.ts +219 -303
- package/src/routes/transcription-recordings.ts +38 -3
- package/src/routes/transcriptions.ts +4 -0
- package/src/routes/usage-allowances.ts +378 -0
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/video-generation.ts +14 -10
- package/src/routes/workspace-artifacts.ts +275 -69
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-integrations.ts +576 -0
- package/src/routes/workspace-learning.ts +159 -0
- package/src/routes/workspace-model-providers.ts +148 -0
- package/src/routes/workspace-state.ts +38 -25
- package/src/routes/workspaces.ts +1004 -127
- package/src/sandbox/auth-callout.ts +96 -19
- package/src/sandbox/channel-a.ts +323 -24
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +81 -6
- package/src/sandbox/machines.ts +218 -76
- package/src/sandbox/metrics-ingestion.ts +411 -62
- package/src/sandbox/rematerialize.ts +121 -19
- package/src/sandbox/session-attach-variable-sets.ts +131 -0
- package/src/sandbox/viewer.ts +280 -25
- package/src/sandbox-file-artifacts.ts +413 -0
- package/src/scheduled-task-deletion.ts +138 -0
- package/src/session-create-observability.ts +46 -0
- package/src/session-realtime-context.ts +6 -3
- package/src/site-session-origin.ts +48 -0
- package/src/site-uploads.ts +224 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- package/src/temporal-schedule-sync.ts +74 -0
- package/src/transcription/providers/codex-subscription.ts +2 -0
- package/src/transcription/providers/openai.ts +1 -0
- package/src/transcription/providers/xai-subscription.ts +49 -3
- package/src/transcription/segmenter.ts +8 -2
- package/src/transcription/service.ts +43 -6
- package/src/work-discovery-observability.ts +121 -0
- package/src/workspace-artifact-content.ts +144 -0
- package/src/workspace-artifact-provenance.ts +104 -0
- package/src/workspace-delete-observability.ts +75 -0
- package/src/workspace-deletion.ts +73 -0
- package/src/workspace-state-projection.ts +18 -148
- package/src/workspace-tool-gateway-observability.ts +100 -0
- package/src/workspace-tool-gateway.ts +870 -0
- package/src/workspace-webhook-dispatch.ts +190 -0
- package/src/xai-subscription-auth.ts +9 -2
- package/dist/chunk-WCXHI3FF.js +0 -49920
- package/dist/chunk-WCXHI3FF.js.map +0 -1
- package/dist/routes/packs.d.ts +0 -3
- package/src/routes/packs.ts +0 -729
package/src/routes/codex.ts
CHANGED
|
@@ -7,14 +7,19 @@
|
|
|
7
7
|
// is added to isAuthExempt. Secrets never leave the server: status/usage read the
|
|
8
8
|
// decrypted token only to call the codex backend; the token is never returned.
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
environmentsEncryptionKeyBytes,
|
|
12
|
+
configuredModels,
|
|
13
|
+
getSettings,
|
|
14
|
+
productLabelForModelId,
|
|
15
|
+
withCodexCatalogProvider,
|
|
16
|
+
type Settings,
|
|
17
|
+
} from "@opengeni/config";
|
|
11
18
|
import {
|
|
12
19
|
accessTokenExpiry,
|
|
13
20
|
buildCodexUsageWindowFromCache,
|
|
14
21
|
CODEX_CLIENT_VERSION,
|
|
15
|
-
CODEX_FALLBACK_MODEL_SLUGS,
|
|
16
22
|
CODEX_FIVE_HOUR_WINDOW_SECONDS,
|
|
17
|
-
CODEX_MODEL_ID_PREFIX,
|
|
18
23
|
CODEX_PROVIDER_ID,
|
|
19
24
|
CODEX_WEEKLY_WINDOW_SECONDS,
|
|
20
25
|
CodexDeviceError,
|
|
@@ -39,8 +44,10 @@ import {
|
|
|
39
44
|
designateCodexAppsCredential,
|
|
40
45
|
disconnectAllCodexAccounts,
|
|
41
46
|
disconnectCodexAccount,
|
|
47
|
+
disconnectOrganizationCodexAccount,
|
|
42
48
|
encryptEnvironmentValue,
|
|
43
49
|
ensureCodexRotationSettings,
|
|
50
|
+
ensureOrganizationCodexRotationSettings,
|
|
44
51
|
fetchCodexUsageForAccount,
|
|
45
52
|
fetchCodexRateLimitResetCreditsForAccount,
|
|
46
53
|
fenceCodexResetRedemptionSend,
|
|
@@ -48,18 +55,30 @@ import {
|
|
|
48
55
|
getCodexCredentialStatus,
|
|
49
56
|
getCodexAppsSettings,
|
|
50
57
|
getCodexRotationSettings,
|
|
58
|
+
getOrganizationCodexRotationSettings,
|
|
59
|
+
getWorkspaceCodexSubscriptionSource,
|
|
51
60
|
listPendingCodexCapacityWakeTargets,
|
|
52
61
|
listCodexAccountStatuses,
|
|
62
|
+
listOrganizationCodexAccountStatuses,
|
|
53
63
|
listCodexResetRedemptionRecoveries,
|
|
64
|
+
nestedPostgresSqlState,
|
|
54
65
|
releaseCodexResetRedemptionClaim,
|
|
55
66
|
updateCodexAllocatorEligibility,
|
|
56
67
|
loadCodexCredentialForRun,
|
|
57
68
|
renameCodexAccount,
|
|
69
|
+
renameOrganizationCodexAccount,
|
|
58
70
|
setActiveCodexCredential,
|
|
71
|
+
setActiveOrganizationCodexCredential,
|
|
59
72
|
setInitialActiveCodexCredential,
|
|
73
|
+
setWorkspaceCodexSubscriptionMode,
|
|
74
|
+
setWorkspaceCodexSubscriptionModeInTransaction,
|
|
75
|
+
updateOrganizationCodexRotationSettings,
|
|
60
76
|
updateCodexRotationSettings,
|
|
77
|
+
upsertOrganizationCodexSubscriptionCredential,
|
|
61
78
|
upsertCodexSubscriptionCredential,
|
|
62
79
|
withCodexCapacityMutation,
|
|
80
|
+
withSessionCodexCapacityMutation,
|
|
81
|
+
activeCodexPlanExclusions,
|
|
63
82
|
type CodexAccountStatus,
|
|
64
83
|
type CodexCapacityWakeTarget,
|
|
65
84
|
} from "@opengeni/db";
|
|
@@ -71,7 +90,7 @@ const CODEX_PROVIDER_LABEL = "Codex subscription · no credits";
|
|
|
71
90
|
// The wire shape for one Codex account (metadata only; never the secret column).
|
|
72
91
|
// P2: fiveHour/weekly ride along, built from the CACHED usage columns (zero
|
|
73
92
|
// provider calls, zero decrypts) so the bars render instantly off this read.
|
|
74
|
-
function codexAccountJson(
|
|
93
|
+
export function codexAccountJson(
|
|
75
94
|
row: CodexAccountStatus,
|
|
76
95
|
options: {
|
|
77
96
|
appsCredentialId?: string | null;
|
|
@@ -81,10 +100,25 @@ function codexAccountJson(
|
|
|
81
100
|
) {
|
|
82
101
|
return {
|
|
83
102
|
id: row.id,
|
|
103
|
+
source: row.source,
|
|
84
104
|
chatgptAccountId: row.chatgptAccountId,
|
|
85
105
|
label: row.label,
|
|
86
106
|
email: row.accountEmail,
|
|
87
107
|
plan: row.planType,
|
|
108
|
+
planCheckedAt: row.planCheckedAt ?? null,
|
|
109
|
+
// The most recent observed plan change (for example "pro" before a move to
|
|
110
|
+
// "free"), kept as evidence until the plan changes again.
|
|
111
|
+
planChangedFrom: row.planPreviousType ?? null,
|
|
112
|
+
planChangedAt: row.planChangedAt ?? null,
|
|
113
|
+
// Models the CURRENT plan was proven not to include. Each leaves automatic
|
|
114
|
+
// selection until `retryAfter` (one request then re-checks it) or until a
|
|
115
|
+
// different plan is observed (refresh usage after an upgrade).
|
|
116
|
+
planExcludedModels: activeCodexPlanExclusions(row, new Date()).map((entry) => ({
|
|
117
|
+
model: entry.modelId,
|
|
118
|
+
label: productLabelForModelId(entry.modelId),
|
|
119
|
+
excludedAt: entry.excludedAt,
|
|
120
|
+
retryAfter: entry.expiresAt,
|
|
121
|
+
})),
|
|
88
122
|
status: row.status,
|
|
89
123
|
active: row.isActive,
|
|
90
124
|
expiresAt: row.expiresAt,
|
|
@@ -110,6 +144,7 @@ function codexAccountJson(
|
|
|
110
144
|
exhaustedUntil: row.exhaustedUntil,
|
|
111
145
|
appsDesignated: options.appsCredentialId === row.id,
|
|
112
146
|
canEnableApps:
|
|
147
|
+
row.source === "workspace" &&
|
|
113
148
|
options.appsCredentialId === null &&
|
|
114
149
|
options.canManageApps === true &&
|
|
115
150
|
options.humanSubjectId !== null &&
|
|
@@ -119,6 +154,55 @@ function codexAccountJson(
|
|
|
119
154
|
};
|
|
120
155
|
}
|
|
121
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Derive the two distinct cached worker-readiness meanings exposed by the
|
|
159
|
+
* status route. `poolReady` means at least one effective-pool account passes
|
|
160
|
+
* the worker's cached admission predicate. `workerRoutable` additionally
|
|
161
|
+
* honors rotation-off's active-pointer-only rule; it says nothing about a
|
|
162
|
+
* session-specific manual pin.
|
|
163
|
+
*/
|
|
164
|
+
export function codexWorkerReadiness(input: {
|
|
165
|
+
effectiveSource: "workspace" | "organization" | "disabled";
|
|
166
|
+
rotationEnabled: boolean;
|
|
167
|
+
activeCredentialId: string | null;
|
|
168
|
+
accounts: ReadonlyArray<
|
|
169
|
+
Pick<
|
|
170
|
+
CodexAccountStatus,
|
|
171
|
+
| "id"
|
|
172
|
+
| "status"
|
|
173
|
+
| "allocatorEnabled"
|
|
174
|
+
| "primaryUsedPercent"
|
|
175
|
+
| "primaryResetAt"
|
|
176
|
+
| "secondaryUsedPercent"
|
|
177
|
+
| "secondaryResetAt"
|
|
178
|
+
| "exhaustedUntil"
|
|
179
|
+
>
|
|
180
|
+
>;
|
|
181
|
+
now: Date;
|
|
182
|
+
}): { poolReady: boolean; workerRoutable: boolean } {
|
|
183
|
+
if (input.effectiveSource === "disabled") {
|
|
184
|
+
return { poolReady: false, workerRoutable: false };
|
|
185
|
+
}
|
|
186
|
+
const windowUsed = (used: number | null, resetAt: Date | null): number =>
|
|
187
|
+
resetAt !== null && resetAt.getTime() <= input.now.getTime() ? 0 : (used ?? 0);
|
|
188
|
+
const eligible = (account: (typeof input.accounts)[number]): boolean =>
|
|
189
|
+
account.status === "active" &&
|
|
190
|
+
account.allocatorEnabled &&
|
|
191
|
+
(account.exhaustedUntil === null || account.exhaustedUntil.getTime() <= input.now.getTime()) &&
|
|
192
|
+
Math.max(
|
|
193
|
+
windowUsed(account.primaryUsedPercent, account.primaryResetAt),
|
|
194
|
+
windowUsed(account.secondaryUsedPercent, account.secondaryResetAt),
|
|
195
|
+
) < 100;
|
|
196
|
+
const poolReady = input.accounts.some(eligible);
|
|
197
|
+
const activePointerReady = input.accounts.some(
|
|
198
|
+
(account) => account.id === input.activeCredentialId && eligible(account),
|
|
199
|
+
);
|
|
200
|
+
return {
|
|
201
|
+
poolReady,
|
|
202
|
+
workerRoutable: input.rotationEnabled ? poolReady : activePointerReady,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
122
206
|
// The /codex/usage{,/refresh,/:id} wire wrapper: the rich normalized payload
|
|
123
207
|
// carries its own `status`, surfaced at the top level for back-compat with the
|
|
124
208
|
// existing CodexUsage = { status; usage } shape.
|
|
@@ -129,35 +213,48 @@ function codexUsageJson(payload: CodexUsagePayload): {
|
|
|
129
213
|
return { status: payload.status, usage: payload };
|
|
130
214
|
}
|
|
131
215
|
|
|
132
|
-
export function codexModelsForPicker(
|
|
216
|
+
export function codexModelsForPicker(
|
|
217
|
+
settings: Settings = getSettings(),
|
|
218
|
+
supportedSlugs?: readonly string[],
|
|
219
|
+
): Array<{
|
|
133
220
|
id: string;
|
|
134
221
|
label: string;
|
|
135
222
|
provider: string;
|
|
136
223
|
providerLabel: string;
|
|
137
224
|
api: "responses";
|
|
138
225
|
}> {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
226
|
+
return configuredModels(withCodexCatalogProvider(settings))
|
|
227
|
+
.filter(
|
|
228
|
+
(model) =>
|
|
229
|
+
model.providerId === CODEX_PROVIDER_ID &&
|
|
230
|
+
(supportedSlugs === undefined || supportedSlugs.includes(model.upstreamModelId)),
|
|
231
|
+
)
|
|
232
|
+
.map((model) => ({
|
|
233
|
+
id: model.id,
|
|
234
|
+
label: model.label,
|
|
235
|
+
provider: CODEX_PROVIDER_ID,
|
|
236
|
+
providerLabel: CODEX_PROVIDER_LABEL,
|
|
237
|
+
api: "responses" as const,
|
|
238
|
+
}));
|
|
151
239
|
}
|
|
152
240
|
import { createSignedState, readSignedState } from "@opengeni/github";
|
|
153
241
|
import {
|
|
154
242
|
getManagedSession,
|
|
155
243
|
hasPermission,
|
|
156
244
|
requireAccessGrant,
|
|
245
|
+
requireCanonicalLocalAccountAdministrator,
|
|
246
|
+
resolveCatalogSettings,
|
|
157
247
|
type ApiRouteDeps,
|
|
158
248
|
} from "@opengeni/core";
|
|
159
249
|
import type { Context, Hono } from "hono";
|
|
160
250
|
import { HTTPException } from "hono/http-exception";
|
|
251
|
+
import {
|
|
252
|
+
agentActingAsPerson,
|
|
253
|
+
agentActingAsPersonBeforeGrantCheck,
|
|
254
|
+
isAgentActingAsPerson,
|
|
255
|
+
organizationSettingsPermission,
|
|
256
|
+
requireNotAgent,
|
|
257
|
+
} from "../http/acting-person";
|
|
161
258
|
import * as z from "zod/v4";
|
|
162
259
|
import {
|
|
163
260
|
hashCodexBrowserSession,
|
|
@@ -183,7 +280,7 @@ type ManagedCookieHuman = {
|
|
|
183
280
|
browserSessionHash: string;
|
|
184
281
|
};
|
|
185
282
|
|
|
186
|
-
async function managedCookieHuman(
|
|
283
|
+
export async function managedCookieHuman(
|
|
187
284
|
c: Context,
|
|
188
285
|
deps: ApiRouteDeps,
|
|
189
286
|
): Promise<ManagedCookieHuman | null> {
|
|
@@ -195,7 +292,11 @@ async function managedCookieHuman(
|
|
|
195
292
|
) {
|
|
196
293
|
return null;
|
|
197
294
|
}
|
|
198
|
-
const session = await getManagedSession(c, deps.managedAuth
|
|
295
|
+
const session = await getManagedSession(c, deps.managedAuth, {
|
|
296
|
+
db: deps.db,
|
|
297
|
+
sessionAdapter: deps.managedAuthSessionAdapter,
|
|
298
|
+
sessionSetMode: deps.settings.managedAuthSessionSetMode,
|
|
299
|
+
});
|
|
199
300
|
if (!session?.user?.id || !session.session?.id) return null;
|
|
200
301
|
return {
|
|
201
302
|
subjectId: `user:${session.user.id}`,
|
|
@@ -203,31 +304,202 @@ async function managedCookieHuman(
|
|
|
203
304
|
};
|
|
204
305
|
}
|
|
205
306
|
|
|
206
|
-
|
|
307
|
+
/**
|
|
308
|
+
* The person in this browser, or an agent acting as them. Only for routes that
|
|
309
|
+
* then require the same person's workspace grant; provider sign-in steps keep
|
|
310
|
+
* managedCookieHuman. An agent has no browser session, so its hash never
|
|
311
|
+
* matches a sign-in started in one.
|
|
312
|
+
*/
|
|
313
|
+
export async function managedHumanOrAgent(
|
|
314
|
+
c: Context,
|
|
315
|
+
deps: ApiRouteDeps,
|
|
316
|
+
): Promise<ManagedCookieHuman | null> {
|
|
317
|
+
const human = await managedCookieHuman(c, deps);
|
|
318
|
+
if (human) return human;
|
|
319
|
+
const agent = agentActingAsPersonBeforeGrantCheck(c);
|
|
320
|
+
return agent
|
|
321
|
+
? {
|
|
322
|
+
subjectId: agent.subjectId,
|
|
323
|
+
browserSessionHash: await hashCodexBrowserSession(`agent:${crypto.randomUUID()}`),
|
|
324
|
+
}
|
|
325
|
+
: null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export async function requireOrganizationCodexHuman(
|
|
329
|
+
c: Context,
|
|
330
|
+
deps: ApiRouteDeps,
|
|
331
|
+
organizationId: string,
|
|
332
|
+
options: {
|
|
333
|
+
/** A provider sign-in step: the person does it in the browser, never an agent. */
|
|
334
|
+
providerConsent?: boolean;
|
|
335
|
+
} = {},
|
|
336
|
+
): Promise<ManagedCookieHuman> {
|
|
337
|
+
if (options.providerConsent) requireNotAgent(c, "Signing in to a provider");
|
|
338
|
+
const parsed = z.string().uuid().safeParse(organizationId);
|
|
339
|
+
if (!parsed.success) throw new HTTPException(422, { message: "invalid organization id" });
|
|
340
|
+
let human = await managedCookieHuman(c, deps);
|
|
341
|
+
if (!human) {
|
|
342
|
+
const agent = agentActingAsPerson(c, parsed.data, organizationSettingsPermission(c));
|
|
343
|
+
// An agent has no browser session, so it never matches a sign-in started in one.
|
|
344
|
+
if (agent) {
|
|
345
|
+
human = {
|
|
346
|
+
subjectId: agent.subjectId,
|
|
347
|
+
browserSessionHash: await hashCodexBrowserSession(`agent:${crypto.randomUUID()}`),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (!human && deps.settings.productAccessMode === "local") {
|
|
352
|
+
const local = await requireCanonicalLocalAccountAdministrator(c, deps, organizationId);
|
|
353
|
+
human = {
|
|
354
|
+
subjectId: local.subjectId,
|
|
355
|
+
browserSessionHash: await hashCodexBrowserSession(`local:${local.subjectId}`),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
if (!human) {
|
|
359
|
+
throw new HTTPException(401, {
|
|
360
|
+
message: "organization administrator session required",
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
await getOrganizationCodexRotationSettings(deps.db, {
|
|
365
|
+
organizationId,
|
|
366
|
+
actorSubjectId: human.subjectId,
|
|
367
|
+
});
|
|
368
|
+
} catch (error) {
|
|
369
|
+
const state = nestedPostgresSqlState(error);
|
|
370
|
+
if (state === "42501") {
|
|
371
|
+
throw new HTTPException(403, {
|
|
372
|
+
message: "organization administration is not authorized",
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
if (state === "P0002") {
|
|
376
|
+
throw new HTTPException(404, { message: "organization not found" });
|
|
377
|
+
}
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
return human;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function requireWorkspaceCodexManagementSource(
|
|
384
|
+
deps: ApiRouteDeps,
|
|
385
|
+
workspaceId: string,
|
|
386
|
+
): Promise<void> {
|
|
387
|
+
const source = await getWorkspaceCodexSubscriptionSource(deps.db, workspaceId);
|
|
388
|
+
if (source.effectiveSource === "organization") {
|
|
389
|
+
throw new HTTPException(409, {
|
|
390
|
+
message: "this Codex subscription is managed in Organization settings",
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
if (source.effectiveSource === "disabled") {
|
|
394
|
+
throw new HTTPException(409, {
|
|
395
|
+
message: "Codex is disabled for this workspace",
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function requireSameOriginBrowserMutation(c: Context, deps: ApiRouteDeps): void {
|
|
401
|
+
// Built in process for an agent acting as a person: no browser credentials
|
|
402
|
+
// ride along, so there is no cross-site request to guard against.
|
|
403
|
+
if (isAgentActingAsPerson(c)) return;
|
|
207
404
|
const contentType = c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
208
405
|
if (contentType !== "application/json") {
|
|
209
406
|
throw new HTTPException(403, {
|
|
210
407
|
message: "JSON browser request required",
|
|
211
408
|
});
|
|
212
409
|
}
|
|
213
|
-
if (!deps.settings.publicBaseUrl) {
|
|
410
|
+
if (deps.settings.productAccessMode !== "local" && !deps.settings.publicBaseUrl) {
|
|
214
411
|
throw new HTTPException(503, {
|
|
215
412
|
message: "managed browser origin is not configured",
|
|
216
413
|
});
|
|
217
414
|
}
|
|
218
|
-
const
|
|
219
|
-
|
|
415
|
+
const origin = c.req.header("origin");
|
|
416
|
+
const localOriginMatches =
|
|
417
|
+
deps.settings.productAccessMode === "local" && localBrowserOriginMatchesRequest(c, origin);
|
|
418
|
+
if (
|
|
419
|
+
deps.settings.productAccessMode === "local"
|
|
420
|
+
? !localOriginMatches
|
|
421
|
+
: origin !== new URL(deps.settings.publicBaseUrl!).origin
|
|
422
|
+
) {
|
|
220
423
|
throw new HTTPException(403, {
|
|
221
424
|
message: "same-origin browser request required",
|
|
222
425
|
});
|
|
223
426
|
}
|
|
224
|
-
|
|
427
|
+
const fetchSite = c.req.header("sec-fetch-site")?.toLowerCase();
|
|
428
|
+
const localFetchSiteMatches =
|
|
429
|
+
localOriginMatches &&
|
|
430
|
+
(fetchSite === "same-origin" ||
|
|
431
|
+
fetchSite === "same-site" ||
|
|
432
|
+
(fetchSite === "cross-site" && localLoopbackOriginMatchesRequest(c, origin)));
|
|
433
|
+
if (
|
|
434
|
+
deps.settings.productAccessMode === "local"
|
|
435
|
+
? !localFetchSiteMatches
|
|
436
|
+
: fetchSite !== "same-origin"
|
|
437
|
+
) {
|
|
225
438
|
throw new HTTPException(403, {
|
|
226
439
|
message: "same-origin fetch metadata required",
|
|
227
440
|
});
|
|
228
441
|
}
|
|
229
442
|
}
|
|
230
443
|
|
|
444
|
+
function localBrowserOriginMatchesRequest(c: Context, value: string | undefined): boolean {
|
|
445
|
+
if (!value) return false;
|
|
446
|
+
let origin: URL;
|
|
447
|
+
try {
|
|
448
|
+
origin = new URL(value);
|
|
449
|
+
} catch {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
if (
|
|
453
|
+
origin.origin !== value ||
|
|
454
|
+
origin.origin === "null" ||
|
|
455
|
+
(origin.protocol !== "http:" && origin.protocol !== "https:")
|
|
456
|
+
) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const forwardedProtocol = c.req.header("x-forwarded-proto")?.trim().toLowerCase();
|
|
461
|
+
const protocol = forwardedProtocol ? `${forwardedProtocol}:` : new URL(c.req.url).protocol;
|
|
462
|
+
if (protocol !== "http:" && protocol !== "https:") return false;
|
|
463
|
+
const forwardedHost = c.req.header("x-forwarded-host") ?? c.req.header("host");
|
|
464
|
+
if (!forwardedHost || /[\s,/?#@\\]/u.test(forwardedHost)) return false;
|
|
465
|
+
|
|
466
|
+
let request: URL;
|
|
467
|
+
try {
|
|
468
|
+
request = new URL(`${protocol}//${forwardedHost}`);
|
|
469
|
+
} catch {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
return (
|
|
473
|
+
origin.protocol === request.protocol &&
|
|
474
|
+
(origin.hostname === request.hostname ||
|
|
475
|
+
(isLoopbackHostname(origin.hostname) && isLoopbackHostname(request.hostname)))
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function localLoopbackOriginMatchesRequest(c: Context, value: string | undefined): boolean {
|
|
480
|
+
if (!value) return false;
|
|
481
|
+
try {
|
|
482
|
+
const origin = new URL(value);
|
|
483
|
+
const forwardedProtocol = c.req.header("x-forwarded-proto")?.trim().toLowerCase();
|
|
484
|
+
const protocol = forwardedProtocol ? `${forwardedProtocol}:` : new URL(c.req.url).protocol;
|
|
485
|
+
const forwardedHost = c.req.header("x-forwarded-host") ?? c.req.header("host");
|
|
486
|
+
if (!forwardedHost || /[\s,/?#@\\]/u.test(forwardedHost)) return false;
|
|
487
|
+
const request = new URL(`${protocol}//${forwardedHost}`);
|
|
488
|
+
return (
|
|
489
|
+
origin.origin === value &&
|
|
490
|
+
origin.protocol === request.protocol &&
|
|
491
|
+
isLoopbackHostname(origin.hostname) &&
|
|
492
|
+
isLoopbackHostname(request.hostname)
|
|
493
|
+
);
|
|
494
|
+
} catch {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function isLoopbackHostname(value: string): boolean {
|
|
500
|
+
return value === "localhost" || value === "[::1]" || /^127(?:\.[0-9]{1,3}){3}$/u.test(value);
|
|
501
|
+
}
|
|
502
|
+
|
|
231
503
|
async function requireRedemptionHuman(
|
|
232
504
|
c: Context,
|
|
233
505
|
deps: ApiRouteDeps,
|
|
@@ -248,13 +520,13 @@ async function requireRedemptionHuman(
|
|
|
248
520
|
});
|
|
249
521
|
}
|
|
250
522
|
requireSameOriginBrowserMutation(c, deps);
|
|
251
|
-
const human = await
|
|
523
|
+
const human = await managedHumanOrAgent(c, deps);
|
|
252
524
|
if (!human) {
|
|
253
525
|
throw new HTTPException(401, {
|
|
254
526
|
message: "managed browser session required",
|
|
255
527
|
});
|
|
256
528
|
}
|
|
257
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
529
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
258
530
|
if (grant.subjectId !== human.subjectId) {
|
|
259
531
|
throw new HTTPException(403, {
|
|
260
532
|
message: "managed browser identity mismatch",
|
|
@@ -274,13 +546,17 @@ async function requireCodexAppsHuman(
|
|
|
274
546
|
});
|
|
275
547
|
}
|
|
276
548
|
requireSameOriginBrowserMutation(c, deps);
|
|
277
|
-
const human = await
|
|
549
|
+
const human = await managedHumanOrAgent(c, deps);
|
|
278
550
|
if (!human) {
|
|
279
|
-
throw new HTTPException(401, {
|
|
551
|
+
throw new HTTPException(401, {
|
|
552
|
+
message: "managed browser session required",
|
|
553
|
+
});
|
|
280
554
|
}
|
|
281
555
|
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
282
556
|
if (grant.subjectId !== human.subjectId) {
|
|
283
|
-
throw new HTTPException(403, {
|
|
557
|
+
throw new HTTPException(403, {
|
|
558
|
+
message: "managed browser identity mismatch",
|
|
559
|
+
});
|
|
284
560
|
}
|
|
285
561
|
return { human, accountId: grant.accountId };
|
|
286
562
|
}
|
|
@@ -386,10 +662,46 @@ function createProviderCallLimiter(limit: number): CodexProviderCall {
|
|
|
386
662
|
};
|
|
387
663
|
}
|
|
388
664
|
|
|
665
|
+
type CodexRedemptionAccess = {
|
|
666
|
+
ownership: "current_human" | "unowned" | "different_human" | "managed_human_unavailable";
|
|
667
|
+
canClaimUnownedViaReconnect: boolean;
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
function codexRedemptionAccess(input: {
|
|
671
|
+
connectedBySubjectId: string | null;
|
|
672
|
+
grantSubjectId: string;
|
|
673
|
+
managedHumanSubjectId: string | null;
|
|
674
|
+
canManage: boolean;
|
|
675
|
+
}): CodexRedemptionAccess {
|
|
676
|
+
if (
|
|
677
|
+
input.managedHumanSubjectId === null ||
|
|
678
|
+
input.managedHumanSubjectId !== input.grantSubjectId
|
|
679
|
+
) {
|
|
680
|
+
return {
|
|
681
|
+
ownership: "managed_human_unavailable",
|
|
682
|
+
canClaimUnownedViaReconnect: false,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
if (input.connectedBySubjectId === null) {
|
|
686
|
+
return {
|
|
687
|
+
ownership: "unowned",
|
|
688
|
+
canClaimUnownedViaReconnect: input.canManage,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
return {
|
|
692
|
+
ownership:
|
|
693
|
+
input.connectedBySubjectId === input.managedHumanSubjectId
|
|
694
|
+
? "current_human"
|
|
695
|
+
: "different_human",
|
|
696
|
+
canClaimUnownedViaReconnect: false,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
389
700
|
async function fetchCodexAccountOverview(
|
|
390
701
|
deps: ApiRouteDeps,
|
|
391
702
|
workspaceId: string,
|
|
392
703
|
row: CodexAccountStatus,
|
|
704
|
+
redemptionAccess: CodexRedemptionAccess,
|
|
393
705
|
canRedeem: boolean,
|
|
394
706
|
canResumeRedemption: boolean,
|
|
395
707
|
redemptions: Awaited<ReturnType<typeof listCodexResetRedemptionRecoveries>> = [],
|
|
@@ -496,6 +808,7 @@ async function fetchCodexAccountOverview(
|
|
|
496
808
|
})),
|
|
497
809
|
},
|
|
498
810
|
canRedeem,
|
|
811
|
+
redemptionAccess,
|
|
499
812
|
canResumeRedemption,
|
|
500
813
|
redemptions: redemptions.map((redemption) => ({
|
|
501
814
|
attemptId: redemption.attemptId,
|
|
@@ -512,6 +825,8 @@ async function fetchCodexAccountOverview(
|
|
|
512
825
|
|
|
513
826
|
type CodexConnectState = {
|
|
514
827
|
workspaceId?: string;
|
|
828
|
+
organizationId?: string;
|
|
829
|
+
actorSubjectId?: string;
|
|
515
830
|
deviceAuthId?: string;
|
|
516
831
|
userCode?: string;
|
|
517
832
|
iat?: number;
|
|
@@ -556,11 +871,306 @@ const CODEX_DEVICE_EXPIRY_SECONDS = 15 * 60; // the device code expires 15 min a
|
|
|
556
871
|
export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
557
872
|
const { db, settings, githubStateSecret } = deps;
|
|
558
873
|
|
|
874
|
+
app.get("/v1/workspaces/:workspaceId/codex/source", async (c) => {
|
|
875
|
+
const workspaceId = c.req.param("workspaceId");
|
|
876
|
+
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
877
|
+
return c.json(await getWorkspaceCodexSubscriptionSource(db, workspaceId));
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
app.patch("/v1/workspaces/:workspaceId/codex/source", async (c) => {
|
|
881
|
+
const workspaceId = c.req.param("workspaceId");
|
|
882
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
883
|
+
const parsed = z
|
|
884
|
+
.object({
|
|
885
|
+
mode: z.enum(["automatic", "workspace", "organization", "disabled"]),
|
|
886
|
+
})
|
|
887
|
+
.safeParse(await c.req.json().catch(() => null));
|
|
888
|
+
if (!parsed.success) {
|
|
889
|
+
throw new HTTPException(400, {
|
|
890
|
+
message: "a valid Codex source mode is required",
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
try {
|
|
894
|
+
return c.json(
|
|
895
|
+
await setWorkspaceCodexSubscriptionMode(db, {
|
|
896
|
+
accountId: grant.accountId,
|
|
897
|
+
workspaceId,
|
|
898
|
+
subjectId: grant.subjectId,
|
|
899
|
+
mode: parsed.data.mode,
|
|
900
|
+
}),
|
|
901
|
+
);
|
|
902
|
+
} catch (error) {
|
|
903
|
+
if (
|
|
904
|
+
error instanceof Error &&
|
|
905
|
+
(error.message.includes("personal workspaces") ||
|
|
906
|
+
error.message.includes("active turns are using it"))
|
|
907
|
+
) {
|
|
908
|
+
throw new HTTPException(409, { message: error.message });
|
|
909
|
+
}
|
|
910
|
+
throw error;
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
app.get("/v1/organizations/:organizationId/codex/accounts", async (c) => {
|
|
915
|
+
const organizationId = c.req.param("organizationId");
|
|
916
|
+
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
917
|
+
const [accounts, rotation] = await Promise.all([
|
|
918
|
+
listOrganizationCodexAccountStatuses(db, {
|
|
919
|
+
organizationId,
|
|
920
|
+
actorSubjectId: human.subjectId,
|
|
921
|
+
}),
|
|
922
|
+
getOrganizationCodexRotationSettings(db, {
|
|
923
|
+
organizationId,
|
|
924
|
+
actorSubjectId: human.subjectId,
|
|
925
|
+
}),
|
|
926
|
+
]);
|
|
927
|
+
return c.json({
|
|
928
|
+
accounts: accounts.map((account) => codexAccountJson(account)),
|
|
929
|
+
activeAccountId: rotation?.activeCredentialId ?? null,
|
|
930
|
+
settings: {
|
|
931
|
+
rotationEnabled: rotation?.rotationEnabled ?? false,
|
|
932
|
+
rotationStrategy: "sharded",
|
|
933
|
+
activeCredentialId: rotation?.activeCredentialId ?? null,
|
|
934
|
+
},
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
app.post("/v1/organizations/:organizationId/codex/connect/start", async (c) => {
|
|
939
|
+
const organizationId = c.req.param("organizationId");
|
|
940
|
+
requireSameOriginBrowserMutation(c, deps);
|
|
941
|
+
const human = await requireOrganizationCodexHuman(c, deps, organizationId, {
|
|
942
|
+
providerConsent: true,
|
|
943
|
+
});
|
|
944
|
+
let start: Awaited<ReturnType<typeof startDeviceCode>>;
|
|
945
|
+
try {
|
|
946
|
+
start = await startDeviceCode();
|
|
947
|
+
} catch (error) {
|
|
948
|
+
throw new HTTPException(502, {
|
|
949
|
+
message:
|
|
950
|
+
error instanceof CodexDeviceError ? error.message : "failed to start Codex device login",
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
return c.json({
|
|
954
|
+
userCode: start.userCode,
|
|
955
|
+
verificationUri: start.verificationUri,
|
|
956
|
+
intervalSeconds: start.intervalSeconds,
|
|
957
|
+
state: createSignedState(githubStateSecret, {
|
|
958
|
+
organizationId,
|
|
959
|
+
actorSubjectId: human.subjectId,
|
|
960
|
+
deviceAuthId: start.deviceAuthId,
|
|
961
|
+
userCode: start.userCode,
|
|
962
|
+
}),
|
|
963
|
+
});
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
app.post("/v1/organizations/:organizationId/codex/connect/poll", async (c) => {
|
|
967
|
+
const organizationId = c.req.param("organizationId");
|
|
968
|
+
requireSameOriginBrowserMutation(c, deps);
|
|
969
|
+
const human = await requireOrganizationCodexHuman(c, deps, organizationId, {
|
|
970
|
+
providerConsent: true,
|
|
971
|
+
});
|
|
972
|
+
const { state } = (await c.req.json().catch(() => null)) as {
|
|
973
|
+
state?: string;
|
|
974
|
+
};
|
|
975
|
+
const payload = (state
|
|
976
|
+
? readSignedState(state, githubStateSecret)
|
|
977
|
+
: null) as unknown as CodexConnectState | null;
|
|
978
|
+
if (
|
|
979
|
+
!payload ||
|
|
980
|
+
payload.organizationId !== organizationId ||
|
|
981
|
+
payload.actorSubjectId !== human.subjectId ||
|
|
982
|
+
!payload.deviceAuthId ||
|
|
983
|
+
!payload.userCode
|
|
984
|
+
) {
|
|
985
|
+
throw new HTTPException(400, {
|
|
986
|
+
message: "codex connect state is invalid or expired",
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
if (
|
|
990
|
+
typeof payload.iat === "number" &&
|
|
991
|
+
Date.now() / 1000 - payload.iat > CODEX_DEVICE_EXPIRY_SECONDS
|
|
992
|
+
) {
|
|
993
|
+
return c.json({ status: "expired" });
|
|
994
|
+
}
|
|
995
|
+
let poll: Awaited<ReturnType<typeof pollDeviceCode>>;
|
|
996
|
+
try {
|
|
997
|
+
poll = await pollDeviceCode({
|
|
998
|
+
deviceAuthId: payload.deviceAuthId,
|
|
999
|
+
userCode: payload.userCode,
|
|
1000
|
+
});
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
throw new HTTPException(502, {
|
|
1003
|
+
message: error instanceof CodexDeviceError ? error.message : "codex device poll failed",
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
if (poll.status === "pending") return c.json({ status: "pending" });
|
|
1007
|
+
if (poll.status === "expired") return c.json({ status: "expired" });
|
|
1008
|
+
let tokens: Awaited<ReturnType<typeof exchangeDeviceCode>>;
|
|
1009
|
+
try {
|
|
1010
|
+
tokens = await exchangeDeviceCode({
|
|
1011
|
+
authorizationCode: poll.authorizationCode,
|
|
1012
|
+
codeVerifier: poll.codeVerifier,
|
|
1013
|
+
});
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
throw new HTTPException(502, {
|
|
1016
|
+
message: error instanceof CodexDeviceError ? error.message : "codex token exchange failed",
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
const key = environmentsEncryptionKeyBytes(settings);
|
|
1020
|
+
if (!key) {
|
|
1021
|
+
throw new HTTPException(500, {
|
|
1022
|
+
message: "OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured",
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
const id = parseIdToken(tokens.idToken);
|
|
1026
|
+
await ensureOrganizationCodexRotationSettings(db, {
|
|
1027
|
+
organizationId,
|
|
1028
|
+
actorSubjectId: human.subjectId,
|
|
1029
|
+
});
|
|
1030
|
+
let upserted: Awaited<ReturnType<typeof upsertOrganizationCodexSubscriptionCredential>>;
|
|
1031
|
+
try {
|
|
1032
|
+
upserted = await upsertOrganizationCodexSubscriptionCredential(db, {
|
|
1033
|
+
organizationId,
|
|
1034
|
+
actorSubjectId: human.subjectId,
|
|
1035
|
+
credentialEncrypted: encryptEnvironmentValue(
|
|
1036
|
+
key,
|
|
1037
|
+
JSON.stringify({
|
|
1038
|
+
access_token: tokens.accessToken,
|
|
1039
|
+
refresh_token: tokens.refreshToken,
|
|
1040
|
+
id_token: tokens.idToken,
|
|
1041
|
+
}),
|
|
1042
|
+
),
|
|
1043
|
+
chatgptAccountId: id.chatgptAccountId,
|
|
1044
|
+
scopes: null,
|
|
1045
|
+
planType: id.planType,
|
|
1046
|
+
isFedramp: id.isFedramp,
|
|
1047
|
+
expiresAt: accessTokenExpiry(tokens.accessToken),
|
|
1048
|
+
lastRefreshAt: new Date(),
|
|
1049
|
+
accountEmail: id.email ?? null,
|
|
1050
|
+
label: id.email ?? id.chatgptAccountId ?? null,
|
|
1051
|
+
});
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
const cause = (error as { cause?: unknown } | null)?.cause;
|
|
1054
|
+
const message =
|
|
1055
|
+
cause instanceof Error ? cause.message : error instanceof Error ? error.message : "";
|
|
1056
|
+
if (message.includes("active turns are using it")) {
|
|
1057
|
+
throw new HTTPException(409, {
|
|
1058
|
+
message: "Codex subscription source cannot change while active turns are using it",
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
throw error;
|
|
1062
|
+
}
|
|
1063
|
+
await signalCodexCapacityTargets(deps, upserted.wakeTargets);
|
|
1064
|
+
const rotation = await getOrganizationCodexRotationSettings(db, {
|
|
1065
|
+
organizationId,
|
|
1066
|
+
actorSubjectId: human.subjectId,
|
|
1067
|
+
});
|
|
1068
|
+
return c.json({
|
|
1069
|
+
status: "connected",
|
|
1070
|
+
plan: id.planType,
|
|
1071
|
+
accountId: upserted.id,
|
|
1072
|
+
isActive: rotation?.activeCredentialId === upserted.id,
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
app.post("/v1/organizations/:organizationId/codex/accounts/:accountId/activate", async (c) => {
|
|
1077
|
+
const organizationId = c.req.param("organizationId");
|
|
1078
|
+
requireSameOriginBrowserMutation(c, deps);
|
|
1079
|
+
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
1080
|
+
const credentialId = c.req.param("accountId");
|
|
1081
|
+
const activation = await setActiveOrganizationCodexCredential(db, {
|
|
1082
|
+
organizationId,
|
|
1083
|
+
actorSubjectId: human.subjectId,
|
|
1084
|
+
credentialId,
|
|
1085
|
+
});
|
|
1086
|
+
if (!activation.activated) {
|
|
1087
|
+
throw new HTTPException(404, { message: "codex account not found" });
|
|
1088
|
+
}
|
|
1089
|
+
await signalCodexCapacityTargets(deps, activation.wakeTargets);
|
|
1090
|
+
return c.json({ activated: true, accountId: credentialId });
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
app.patch("/v1/organizations/:organizationId/codex/settings", async (c) => {
|
|
1094
|
+
const organizationId = c.req.param("organizationId");
|
|
1095
|
+
requireSameOriginBrowserMutation(c, deps);
|
|
1096
|
+
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
1097
|
+
const parsed = z
|
|
1098
|
+
.object({ rotationEnabled: z.boolean() })
|
|
1099
|
+
.safeParse(await c.req.json().catch(() => null));
|
|
1100
|
+
if (!parsed.success) {
|
|
1101
|
+
throw new HTTPException(400, { message: "rotationEnabled is required" });
|
|
1102
|
+
}
|
|
1103
|
+
const updated = await updateOrganizationCodexRotationSettings(db, {
|
|
1104
|
+
organizationId,
|
|
1105
|
+
actorSubjectId: human.subjectId,
|
|
1106
|
+
rotationEnabled: parsed.data.rotationEnabled,
|
|
1107
|
+
});
|
|
1108
|
+
if (!updated) throw new HTTPException(404, { message: "Codex settings not found" });
|
|
1109
|
+
await signalCodexCapacityTargets(deps, updated.wakeTargets);
|
|
1110
|
+
return c.json({
|
|
1111
|
+
rotationEnabled: updated.rotationEnabled,
|
|
1112
|
+
rotationStrategy: "sharded",
|
|
1113
|
+
activeCredentialId: updated.activeCredentialId,
|
|
1114
|
+
});
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
app.patch("/v1/organizations/:organizationId/codex/accounts/:accountId", async (c) => {
|
|
1118
|
+
const organizationId = c.req.param("organizationId");
|
|
1119
|
+
requireSameOriginBrowserMutation(c, deps);
|
|
1120
|
+
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
1121
|
+
const body = (await c.req.json().catch(() => null)) as {
|
|
1122
|
+
label?: unknown;
|
|
1123
|
+
} | null;
|
|
1124
|
+
const renamed = await renameOrganizationCodexAccount(db, {
|
|
1125
|
+
organizationId,
|
|
1126
|
+
actorSubjectId: human.subjectId,
|
|
1127
|
+
credentialId: c.req.param("accountId"),
|
|
1128
|
+
label: typeof body?.label === "string" ? body.label : null,
|
|
1129
|
+
});
|
|
1130
|
+
if (!renamed) throw new HTTPException(404, { message: "codex account not found" });
|
|
1131
|
+
const accounts = await listOrganizationCodexAccountStatuses(db, {
|
|
1132
|
+
organizationId,
|
|
1133
|
+
actorSubjectId: human.subjectId,
|
|
1134
|
+
});
|
|
1135
|
+
const row = accounts.find((account) => account.id === c.req.param("accountId"));
|
|
1136
|
+
if (!row) throw new HTTPException(404, { message: "codex account not found" });
|
|
1137
|
+
return c.json(codexAccountJson(row));
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
app.delete("/v1/organizations/:organizationId/codex/accounts/:accountId", async (c) => {
|
|
1141
|
+
const organizationId = c.req.param("organizationId");
|
|
1142
|
+
requireSameOriginBrowserMutation(c, deps);
|
|
1143
|
+
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
1144
|
+
let result: Awaited<ReturnType<typeof disconnectOrganizationCodexAccount>>;
|
|
1145
|
+
try {
|
|
1146
|
+
result = await disconnectOrganizationCodexAccount(db, {
|
|
1147
|
+
organizationId,
|
|
1148
|
+
actorSubjectId: human.subjectId,
|
|
1149
|
+
credentialId: c.req.param("accountId"),
|
|
1150
|
+
});
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
const cause = (error as { cause?: unknown } | null)?.cause;
|
|
1153
|
+
const message =
|
|
1154
|
+
cause instanceof Error ? cause.message : error instanceof Error ? error.message : "";
|
|
1155
|
+
if (message.includes("active turns are using it")) {
|
|
1156
|
+
throw new HTTPException(409, {
|
|
1157
|
+
message: "Codex subscription cannot disconnect while active turns are using it",
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
throw error;
|
|
1161
|
+
}
|
|
1162
|
+
await signalCodexCapacityTargets(deps, result.wakeTargets);
|
|
1163
|
+
return c.json({
|
|
1164
|
+
disconnected: result.removed,
|
|
1165
|
+
newActiveId: result.newActiveCredentialId,
|
|
1166
|
+
});
|
|
1167
|
+
});
|
|
1168
|
+
|
|
559
1169
|
// Begin device-code login: returns the user code + verification URL and a
|
|
560
1170
|
// signed state that carries the device_auth_id back to `poll`.
|
|
561
1171
|
app.post("/v1/workspaces/:workspaceId/codex/connect/start", async (c) => {
|
|
562
1172
|
const workspaceId = c.req.param("workspaceId");
|
|
563
|
-
await requireAccessGrant(c, deps, workspaceId, "
|
|
1173
|
+
await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
564
1174
|
let start: Awaited<ReturnType<typeof startDeviceCode>>;
|
|
565
1175
|
try {
|
|
566
1176
|
start = await startDeviceCode();
|
|
@@ -586,7 +1196,7 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
586
1196
|
// Poll for authorization: pending | expired | connected (persists on success).
|
|
587
1197
|
app.post("/v1/workspaces/:workspaceId/codex/connect/poll", async (c) => {
|
|
588
1198
|
const workspaceId = c.req.param("workspaceId");
|
|
589
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
1199
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
590
1200
|
const { state } = (await c.req.json()) as { state?: string };
|
|
591
1201
|
const payload = (state
|
|
592
1202
|
? readSignedState(state, githubStateSecret)
|
|
@@ -647,58 +1257,79 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
647
1257
|
message: "OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured",
|
|
648
1258
|
});
|
|
649
1259
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
),
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
1260
|
+
const mutation = await withSessionCodexCapacityMutation<{
|
|
1261
|
+
upserted: Awaited<ReturnType<typeof upsertCodexSubscriptionCredential>>;
|
|
1262
|
+
isActive: boolean;
|
|
1263
|
+
}>(db, { workspaceId, reason: "codex_credential_connected" }, async (tx) => {
|
|
1264
|
+
// The capacity mutation holds the source lock before entering this callback.
|
|
1265
|
+
const sourceBeforeConnect = await getWorkspaceCodexSubscriptionSource(tx, workspaceId);
|
|
1266
|
+
const upserted = await upsertCodexSubscriptionCredential(tx, {
|
|
1267
|
+
accountId: grant.accountId,
|
|
1268
|
+
workspaceId,
|
|
1269
|
+
credentialEncrypted: encryptEnvironmentValue(
|
|
1270
|
+
key,
|
|
1271
|
+
JSON.stringify({
|
|
1272
|
+
access_token: tokens.accessToken,
|
|
1273
|
+
refresh_token: tokens.refreshToken,
|
|
1274
|
+
id_token: tokens.idToken,
|
|
1275
|
+
}),
|
|
1276
|
+
),
|
|
1277
|
+
chatgptAccountId: id.chatgptAccountId,
|
|
1278
|
+
scopes: null, // device grant scopes are discovered at runtime, not asserted here
|
|
1279
|
+
planType: id.planType,
|
|
1280
|
+
isFedramp: id.isFedramp,
|
|
1281
|
+
expiresAt: accessTokenExpiry(tokens.accessToken),
|
|
1282
|
+
lastRefreshAt: new Date(),
|
|
1283
|
+
accountEmail: id.email ?? null,
|
|
1284
|
+
label: id.email ?? id.chatgptAccountId ?? null,
|
|
1285
|
+
connectedBySubjectId:
|
|
1286
|
+
connectingHuman?.subjectId === grant.subjectId ? connectingHuman.subjectId : null,
|
|
1287
|
+
});
|
|
1288
|
+
if (upserted.kind === "unresolved_redemption") {
|
|
1289
|
+
return { result: { upserted, isActive: false }, changed: false };
|
|
1290
|
+
}
|
|
1291
|
+
await ensureCodexRotationSettings(tx, grant.accountId, workspaceId);
|
|
1292
|
+
await setInitialActiveCodexCredential(tx, workspaceId, upserted.id);
|
|
1293
|
+
await setWorkspaceCodexSubscriptionModeInTransaction(tx, {
|
|
1294
|
+
accountId: grant.accountId,
|
|
1295
|
+
workspaceId,
|
|
1296
|
+
subjectId: grant.subjectId,
|
|
1297
|
+
// Connecting local capacity does not override an explicit source choice.
|
|
1298
|
+
// Automatic naturally prefers the newly connected workspace pool.
|
|
1299
|
+
mode: sourceBeforeConnect.mode,
|
|
1300
|
+
effectiveSourceBeforeMutation: sourceBeforeConnect.effectiveSource,
|
|
1301
|
+
});
|
|
1302
|
+
const rotation = await getCodexRotationSettings(tx, workspaceId);
|
|
1303
|
+
return {
|
|
1304
|
+
result: {
|
|
1305
|
+
upserted,
|
|
1306
|
+
isActive: rotation?.activeCredentialId === upserted.id,
|
|
1307
|
+
},
|
|
1308
|
+
changed: true,
|
|
1309
|
+
};
|
|
1310
|
+
}).catch((error: unknown) => {
|
|
1311
|
+
const cause = (error as { cause?: unknown } | null)?.cause;
|
|
1312
|
+
const message =
|
|
1313
|
+
cause instanceof Error ? cause.message : error instanceof Error ? error.message : "";
|
|
1314
|
+
if (message.includes("active turns are using it")) {
|
|
1315
|
+
throw new HTTPException(409, { message });
|
|
1316
|
+
}
|
|
1317
|
+
throw error;
|
|
1318
|
+
});
|
|
1319
|
+
const { upserted, isActive } = mutation.result;
|
|
681
1320
|
if (upserted.kind === "unresolved_redemption") {
|
|
682
1321
|
throw new HTTPException(409, {
|
|
683
1322
|
message:
|
|
684
1323
|
"this subscription has an unresolved reset redemption; recover it before changing ownership",
|
|
685
1324
|
});
|
|
686
1325
|
}
|
|
687
|
-
// Ensure the per-workspace rotation-settings row exists, then auto-activate
|
|
688
|
-
// the FIRST account only. Additional new accounts do NOT auto-activate — a
|
|
689
|
-
// manual switch is required (no auto-rotation in P1). A re-connect of the
|
|
690
|
-
// already-active account is a no-op for the pointer.
|
|
691
|
-
// Keep both rotation bits false on first connect. The deployment flag makes
|
|
692
|
-
// the compatible allocator available, but the workspace-local cutover bit
|
|
693
|
-
// is enabled only by an explicit settings write after every worker replica
|
|
694
|
-
// understands leasing.
|
|
695
|
-
const rotation = await getCodexRotationSettings(db, workspaceId);
|
|
696
|
-
let isActive = rotation?.activeCredentialId === upserted.id;
|
|
697
|
-
if (!isActive && rotation?.activeCredentialId == null) {
|
|
698
|
-
isActive = await setInitialActiveCodexCredential(db, workspaceId, upserted.id);
|
|
699
|
-
}
|
|
700
1326
|
await signalCodexCapacityTargets(deps, mutation.wakeTargets);
|
|
701
|
-
return c.json({
|
|
1327
|
+
return c.json({
|
|
1328
|
+
status: "connected",
|
|
1329
|
+
plan: id.planType,
|
|
1330
|
+
accountId: upserted.id,
|
|
1331
|
+
isActive,
|
|
1332
|
+
});
|
|
702
1333
|
});
|
|
703
1334
|
|
|
704
1335
|
// Connection health: the cheapest real call is GET /codex/models (a 200 proves
|
|
@@ -706,12 +1337,13 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
706
1337
|
app.get("/v1/workspaces/:workspaceId/codex/status", async (c) => {
|
|
707
1338
|
const workspaceId = c.req.param("workspaceId");
|
|
708
1339
|
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
709
|
-
const status = await
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1340
|
+
const [status, accounts, source, rotation] = await Promise.all([
|
|
1341
|
+
getCodexCredentialStatus(db, workspaceId),
|
|
1342
|
+
listCodexAccountStatuses(db, workspaceId),
|
|
1343
|
+
getWorkspaceCodexSubscriptionSource(db, workspaceId),
|
|
1344
|
+
getCodexRotationSettings(db, workspaceId),
|
|
1345
|
+
]);
|
|
1346
|
+
const activeRow = accounts.find((account) => account.id === status?.credentialId) ?? null;
|
|
715
1347
|
const activeAccount = activeRow
|
|
716
1348
|
? {
|
|
717
1349
|
id: activeRow.id,
|
|
@@ -723,23 +1355,33 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
723
1355
|
chatgptAccountId: activeRow.chatgptAccountId,
|
|
724
1356
|
}
|
|
725
1357
|
: null;
|
|
1358
|
+
const activeCredentialId = status?.credentialId ?? rotation?.activeCredentialId ?? null;
|
|
1359
|
+
const readiness = codexWorkerReadiness({
|
|
1360
|
+
effectiveSource: source.effectiveSource,
|
|
1361
|
+
rotationEnabled: rotation?.rotationEnabled ?? false,
|
|
1362
|
+
activeCredentialId,
|
|
1363
|
+
accounts,
|
|
1364
|
+
now: new Date(),
|
|
1365
|
+
});
|
|
726
1366
|
let valid = false;
|
|
1367
|
+
const catalogSettings = (await resolveCatalogSettings(db, settings)).settings;
|
|
727
1368
|
let models: ReturnType<typeof codexModelsForPicker> = [];
|
|
728
1369
|
let catalogError: string | null = null;
|
|
729
1370
|
try {
|
|
730
|
-
const cred = status
|
|
1371
|
+
const cred = status?.credentialId
|
|
731
1372
|
? await loadCodexCredentialForRun(db, settings, workspaceId, status.credentialId)
|
|
732
1373
|
: null;
|
|
733
1374
|
if (cred) {
|
|
1375
|
+
const token = await buildCodexTokenResolver(db, settings, workspaceId, cred.id).getToken();
|
|
734
1376
|
const live = await fetchCodexModels({
|
|
735
|
-
accessToken:
|
|
736
|
-
chatgptAccountId:
|
|
737
|
-
isFedramp:
|
|
1377
|
+
accessToken: token.accessToken,
|
|
1378
|
+
chatgptAccountId: token.chatgptAccountId,
|
|
1379
|
+
isFedramp: token.isFedramp,
|
|
738
1380
|
clientVersion: CODEX_CLIENT_VERSION,
|
|
739
1381
|
});
|
|
740
1382
|
if (live.ok) {
|
|
741
|
-
models = codexModelsForPicker(live.slugs);
|
|
742
1383
|
valid = true;
|
|
1384
|
+
models = codexModelsForPicker(catalogSettings, live.slugs);
|
|
743
1385
|
} else {
|
|
744
1386
|
catalogError = `Codex models request failed with status ${live.status}`;
|
|
745
1387
|
}
|
|
@@ -749,14 +1391,18 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
749
1391
|
catalogError = error instanceof Error ? error.message : String(error);
|
|
750
1392
|
}
|
|
751
1393
|
return c.json({
|
|
752
|
-
connected: status
|
|
753
|
-
plan: status
|
|
1394
|
+
connected: status?.connected ?? false,
|
|
1395
|
+
plan: status?.planType ?? null,
|
|
754
1396
|
valid,
|
|
755
|
-
|
|
756
|
-
|
|
1397
|
+
activeAccountValid: valid,
|
|
1398
|
+
poolReady: readiness.poolReady,
|
|
1399
|
+
workerRoutable: readiness.workerRoutable,
|
|
1400
|
+
expiresAt: status?.expiresAt ?? null,
|
|
1401
|
+
lastError: catalogError ?? status?.lastError ?? null,
|
|
757
1402
|
models, // ClientModel[] the picker surfaces under the "no credits" group
|
|
758
1403
|
activeAccount, // the account a session runs on when unpinned (label for the indicator)
|
|
759
1404
|
accountCount: accounts.length,
|
|
1405
|
+
source,
|
|
760
1406
|
});
|
|
761
1407
|
});
|
|
762
1408
|
|
|
@@ -765,11 +1411,12 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
765
1411
|
app.get("/v1/workspaces/:workspaceId/codex/accounts", async (c) => {
|
|
766
1412
|
const workspaceId = c.req.param("workspaceId");
|
|
767
1413
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
768
|
-
const [accounts, rotation, apps, human] = await Promise.all([
|
|
1414
|
+
const [accounts, rotation, apps, human, source] = await Promise.all([
|
|
769
1415
|
listCodexAccountStatuses(db, workspaceId),
|
|
770
1416
|
getCodexRotationSettings(db, workspaceId),
|
|
771
1417
|
getCodexAppsSettings(db, workspaceId),
|
|
772
|
-
|
|
1418
|
+
managedHumanOrAgent(c, deps),
|
|
1419
|
+
getWorkspaceCodexSubscriptionSource(db, workspaceId),
|
|
773
1420
|
]);
|
|
774
1421
|
const activeAccountId = rotation?.activeCredentialId ?? null;
|
|
775
1422
|
const humanSubjectId = human?.subjectId === grant.subjectId ? human.subjectId : null;
|
|
@@ -784,6 +1431,7 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
784
1431
|
}),
|
|
785
1432
|
),
|
|
786
1433
|
activeAccountId,
|
|
1434
|
+
source,
|
|
787
1435
|
apps: {
|
|
788
1436
|
available: settings.codexConnectedAppsEnabled,
|
|
789
1437
|
credentialId: apps.credentialId,
|
|
@@ -803,8 +1451,11 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
803
1451
|
|
|
804
1452
|
app.post("/v1/workspaces/:workspaceId/codex/apps", async (c) => {
|
|
805
1453
|
const workspaceId = c.req.param("workspaceId");
|
|
1454
|
+
await requireWorkspaceCodexManagementSource(deps, workspaceId);
|
|
806
1455
|
if (!settings.codexConnectedAppsEnabled) {
|
|
807
|
-
throw new HTTPException(409, {
|
|
1456
|
+
throw new HTTPException(409, {
|
|
1457
|
+
message: "Codex Apps is disabled for this deployment",
|
|
1458
|
+
});
|
|
808
1459
|
}
|
|
809
1460
|
const { human, accountId } = await requireCodexAppsHuman(c, deps, workspaceId);
|
|
810
1461
|
const parsed = z
|
|
@@ -814,7 +1465,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
814
1465
|
})
|
|
815
1466
|
.safeParse(await c.req.json().catch(() => null));
|
|
816
1467
|
if (!parsed.success) {
|
|
817
|
-
throw new HTTPException(400, {
|
|
1468
|
+
throw new HTTPException(400, {
|
|
1469
|
+
message: "accountId and expectedVersion are required",
|
|
1470
|
+
});
|
|
818
1471
|
}
|
|
819
1472
|
const result = await designateCodexAppsCredential(db, {
|
|
820
1473
|
accountId,
|
|
@@ -832,10 +1485,14 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
832
1485
|
});
|
|
833
1486
|
}
|
|
834
1487
|
if (result.kind === "forbidden") {
|
|
835
|
-
throw new HTTPException(403, {
|
|
1488
|
+
throw new HTTPException(403, {
|
|
1489
|
+
message: "missing permission: connections:write",
|
|
1490
|
+
});
|
|
836
1491
|
}
|
|
837
1492
|
if (result.kind === "unavailable") {
|
|
838
|
-
throw new HTTPException(409, {
|
|
1493
|
+
throw new HTTPException(409, {
|
|
1494
|
+
message: "codex account requires relogin",
|
|
1495
|
+
});
|
|
839
1496
|
}
|
|
840
1497
|
const response = {
|
|
841
1498
|
credentialId: result.credentialId,
|
|
@@ -848,6 +1505,7 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
848
1505
|
|
|
849
1506
|
app.delete("/v1/workspaces/:workspaceId/codex/apps", async (c) => {
|
|
850
1507
|
const workspaceId = c.req.param("workspaceId");
|
|
1508
|
+
await requireWorkspaceCodexManagementSource(deps, workspaceId);
|
|
851
1509
|
const { human, accountId } = await requireCodexAppsHuman(c, deps, workspaceId);
|
|
852
1510
|
const parsed = z
|
|
853
1511
|
.object({ expectedVersion: z.number().int().nonnegative() })
|
|
@@ -862,7 +1520,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
862
1520
|
expectedVersion: parsed.data.expectedVersion,
|
|
863
1521
|
});
|
|
864
1522
|
if (result.kind === "forbidden") {
|
|
865
|
-
throw new HTTPException(403, {
|
|
1523
|
+
throw new HTTPException(403, {
|
|
1524
|
+
message: "missing permission: connections:write",
|
|
1525
|
+
});
|
|
866
1526
|
}
|
|
867
1527
|
const response = {
|
|
868
1528
|
credentialId: result.credentialId,
|
|
@@ -877,7 +1537,8 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
877
1537
|
// Pure pointer flip; in-flight turns pick it up on their next token fetch.
|
|
878
1538
|
app.post("/v1/workspaces/:workspaceId/codex/accounts/:accountId/activate", async (c) => {
|
|
879
1539
|
const workspaceId = c.req.param("workspaceId");
|
|
880
|
-
await requireAccessGrant(c, deps, workspaceId, "
|
|
1540
|
+
await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
1541
|
+
await requireWorkspaceCodexManagementSource(deps, workspaceId);
|
|
881
1542
|
const accountId = c.req.param("accountId");
|
|
882
1543
|
const mutation = await withCodexCapacityMutation(
|
|
883
1544
|
db,
|
|
@@ -895,14 +1556,15 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
895
1556
|
return c.json({ activated: true, accountId });
|
|
896
1557
|
});
|
|
897
1558
|
|
|
898
|
-
// Update rotation settings.
|
|
1559
|
+
// Update rotation settings. Connection-management access. Sharded-rotation policy: the strategy picker is GONE —
|
|
899
1560
|
// rotation-enabled always behaves as sticky-sharded (worker-side
|
|
900
1561
|
// effectiveRotationStrategy normalization). `rotationStrategy` in the body is
|
|
901
1562
|
// ACCEPTED-BUT-IGNORED so no existing SDK/UI caller breaks (deprecation), and
|
|
902
1563
|
// the stored column is only legacy residue kept for old-binary rollback.
|
|
903
1564
|
app.patch("/v1/workspaces/:workspaceId/codex/settings", async (c) => {
|
|
904
1565
|
const workspaceId = c.req.param("workspaceId");
|
|
905
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
1566
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
1567
|
+
await requireWorkspaceCodexManagementSource(deps, workspaceId);
|
|
906
1568
|
const body = (await c.req.json().catch(() => ({}))) as {
|
|
907
1569
|
rotationEnabled?: unknown;
|
|
908
1570
|
rotationStrategy?: unknown;
|
|
@@ -917,7 +1579,10 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
917
1579
|
if (patch.rotationEnabled === undefined) {
|
|
918
1580
|
// Strategy-only writes are a deprecated no-op (no db touch): report the
|
|
919
1581
|
// (only) truth. Callers that also flip rotationEnabled fall through.
|
|
920
|
-
return c.json({
|
|
1582
|
+
return c.json({
|
|
1583
|
+
rotationStrategy: "sharded",
|
|
1584
|
+
rotationStrategyDeprecated: true,
|
|
1585
|
+
});
|
|
921
1586
|
}
|
|
922
1587
|
await ensureCodexRotationSettings(db, grant.accountId, workspaceId);
|
|
923
1588
|
const mutation = await withCodexCapacityMutation(
|
|
@@ -946,7 +1611,8 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
946
1611
|
// Rename an account (label only in P1).
|
|
947
1612
|
app.patch("/v1/workspaces/:workspaceId/codex/accounts/:accountId", async (c) => {
|
|
948
1613
|
const workspaceId = c.req.param("workspaceId");
|
|
949
|
-
await requireAccessGrant(c, deps, workspaceId, "
|
|
1614
|
+
await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
1615
|
+
await requireWorkspaceCodexManagementSource(deps, workspaceId);
|
|
950
1616
|
const accountId = c.req.param("accountId");
|
|
951
1617
|
const body = (await c.req.json()) as { label?: string | null };
|
|
952
1618
|
const label = typeof body.label === "string" ? body.label : null;
|
|
@@ -967,7 +1633,8 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
967
1633
|
// an explicit 409 carrying the current version.
|
|
968
1634
|
app.patch("/v1/workspaces/:workspaceId/codex/accounts/:accountId/allocator", async (c) => {
|
|
969
1635
|
const workspaceId = c.req.param("workspaceId");
|
|
970
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
1636
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
1637
|
+
await requireWorkspaceCodexManagementSource(deps, workspaceId);
|
|
971
1638
|
const parsed = z
|
|
972
1639
|
.object({
|
|
973
1640
|
enabled: z.boolean(),
|
|
@@ -1005,7 +1672,8 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1005
1672
|
// row was active (FK ON DELETE SET NULL + re-pick in the same RLS txn).
|
|
1006
1673
|
app.delete("/v1/workspaces/:workspaceId/codex/accounts/:accountId", async (c) => {
|
|
1007
1674
|
const workspaceId = c.req.param("workspaceId");
|
|
1008
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
1675
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
1676
|
+
await requireWorkspaceCodexManagementSource(deps, workspaceId);
|
|
1009
1677
|
const accountId = c.req.param("accountId");
|
|
1010
1678
|
const mutation = await withCodexCapacityMutation(
|
|
1011
1679
|
db,
|
|
@@ -1023,14 +1691,18 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1023
1691
|
});
|
|
1024
1692
|
}
|
|
1025
1693
|
await signalCodexCapacityTargets(deps, mutation.wakeTargets);
|
|
1026
|
-
return c.json({
|
|
1694
|
+
return c.json({
|
|
1695
|
+
disconnected: result.removed,
|
|
1696
|
+
newActiveId: result.newActiveCredentialId,
|
|
1697
|
+
});
|
|
1027
1698
|
});
|
|
1028
1699
|
|
|
1029
1700
|
// Legacy "disconnect all" (old workspace-wide behavior), deprecated in favor of
|
|
1030
1701
|
// the by-id route above.
|
|
1031
1702
|
app.delete("/v1/workspaces/:workspaceId/codex", async (c) => {
|
|
1032
1703
|
const workspaceId = c.req.param("workspaceId");
|
|
1033
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
1704
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
1705
|
+
await requireWorkspaceCodexManagementSource(deps, workspaceId);
|
|
1034
1706
|
const mutation = await withCodexCapacityMutation(
|
|
1035
1707
|
db,
|
|
1036
1708
|
{ workspaceId, reason: "codex_credentials_disconnected" },
|
|
@@ -1135,12 +1807,12 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1135
1807
|
app.get("/v1/workspaces/:workspaceId/codex/overview", async (c) => {
|
|
1136
1808
|
const workspaceId = c.req.param("workspaceId");
|
|
1137
1809
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
1138
|
-
const human = await
|
|
1810
|
+
const human = await managedHumanOrAgent(c, deps);
|
|
1139
1811
|
const accounts = await listCodexAccountStatuses(db, workspaceId);
|
|
1140
1812
|
const ownerRecoveries =
|
|
1141
1813
|
human &&
|
|
1142
1814
|
human.subjectId === grant.subjectId &&
|
|
1143
|
-
hasPermission(grant.permissions, "
|
|
1815
|
+
hasPermission(grant.permissions, "connections:write")
|
|
1144
1816
|
? await listCodexResetRedemptionRecoveries(db, {
|
|
1145
1817
|
accountId: grant.accountId,
|
|
1146
1818
|
workspaceId,
|
|
@@ -1160,16 +1832,30 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1160
1832
|
const account = queue.shift();
|
|
1161
1833
|
if (!account) return;
|
|
1162
1834
|
const canResumeRedemption = Boolean(
|
|
1835
|
+
account.source === "workspace" &&
|
|
1163
1836
|
human &&
|
|
1164
1837
|
human.subjectId === grant.subjectId &&
|
|
1165
1838
|
human.subjectId === account.connectedBySubjectId &&
|
|
1166
|
-
hasPermission(grant.permissions, "
|
|
1839
|
+
hasPermission(grant.permissions, "connections:write"),
|
|
1167
1840
|
);
|
|
1168
1841
|
const canRedeem = canResumeRedemption && account.status === "active";
|
|
1842
|
+
const redemptionAccess: CodexRedemptionAccess =
|
|
1843
|
+
account.source === "organization"
|
|
1844
|
+
? {
|
|
1845
|
+
ownership: "managed_human_unavailable",
|
|
1846
|
+
canClaimUnownedViaReconnect: false,
|
|
1847
|
+
}
|
|
1848
|
+
: codexRedemptionAccess({
|
|
1849
|
+
connectedBySubjectId: account.connectedBySubjectId,
|
|
1850
|
+
grantSubjectId: grant.subjectId,
|
|
1851
|
+
managedHumanSubjectId: human?.subjectId ?? null,
|
|
1852
|
+
canManage: hasPermission(grant.permissions, "connections:write"),
|
|
1853
|
+
});
|
|
1169
1854
|
overview[account.id] = await fetchCodexAccountOverview(
|
|
1170
1855
|
deps,
|
|
1171
1856
|
workspaceId,
|
|
1172
1857
|
account,
|
|
1858
|
+
redemptionAccess,
|
|
1173
1859
|
canRedeem,
|
|
1174
1860
|
canResumeRedemption,
|
|
1175
1861
|
canResumeRedemption
|
|
@@ -1207,15 +1893,27 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1207
1893
|
.filter((account) => overview[account.id] == null)
|
|
1208
1894
|
.map(async (account) => {
|
|
1209
1895
|
const canResumeRedemption = Boolean(
|
|
1896
|
+
account.source === "workspace" &&
|
|
1210
1897
|
human &&
|
|
1211
1898
|
human.subjectId === grant.subjectId &&
|
|
1212
1899
|
human.subjectId === account.connectedBySubjectId &&
|
|
1213
|
-
hasPermission(grant.permissions, "
|
|
1900
|
+
hasPermission(grant.permissions, "connections:write"),
|
|
1214
1901
|
);
|
|
1215
1902
|
const fallback = await fetchCodexAccountOverview(
|
|
1216
1903
|
deps,
|
|
1217
1904
|
workspaceId,
|
|
1218
1905
|
account,
|
|
1906
|
+
account.source === "organization"
|
|
1907
|
+
? {
|
|
1908
|
+
ownership: "managed_human_unavailable",
|
|
1909
|
+
canClaimUnownedViaReconnect: false,
|
|
1910
|
+
}
|
|
1911
|
+
: codexRedemptionAccess({
|
|
1912
|
+
connectedBySubjectId: account.connectedBySubjectId,
|
|
1913
|
+
grantSubjectId: grant.subjectId,
|
|
1914
|
+
managedHumanSubjectId: human?.subjectId ?? null,
|
|
1915
|
+
canManage: hasPermission(grant.permissions, "connections:write"),
|
|
1916
|
+
}),
|
|
1219
1917
|
false,
|
|
1220
1918
|
canResumeRedemption,
|
|
1221
1919
|
canResumeRedemption
|
|
@@ -1257,6 +1955,11 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1257
1955
|
const accounts = await listCodexAccountStatuses(db, workspaceId);
|
|
1258
1956
|
const account = accounts.find((candidate) => candidate.id === credentialId);
|
|
1259
1957
|
if (!account) throw new HTTPException(404, { message: "codex account not found" });
|
|
1958
|
+
if (account.source === "organization") {
|
|
1959
|
+
throw new HTTPException(409, {
|
|
1960
|
+
message: "organization Codex subscriptions are managed in Organization settings",
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1260
1963
|
let existing = await getCodexResetRedemptionAttempt(db, workspaceId, parsed.data.attemptId);
|
|
1261
1964
|
if (account.connectedBySubjectId !== human.subjectId) {
|
|
1262
1965
|
throw new HTTPException(403, {
|
|
@@ -1289,10 +1992,14 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1289
1992
|
});
|
|
1290
1993
|
}
|
|
1291
1994
|
if (adoption.kind === "not_found") {
|
|
1292
|
-
throw new HTTPException(409, {
|
|
1995
|
+
throw new HTTPException(409, {
|
|
1996
|
+
message: "redemption recovery state changed",
|
|
1997
|
+
});
|
|
1293
1998
|
}
|
|
1294
1999
|
if (adoption.kind === "forbidden") {
|
|
1295
|
-
throw new HTTPException(403, {
|
|
2000
|
+
throw new HTTPException(403, {
|
|
2001
|
+
message: "redemption owner is unavailable",
|
|
2002
|
+
});
|
|
1296
2003
|
}
|
|
1297
2004
|
if (adoption.kind === "conflict") {
|
|
1298
2005
|
throw new HTTPException(409, {
|
|
@@ -1306,7 +2013,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1306
2013
|
// and a lost HTTP response must remain replayable after a later health
|
|
1307
2014
|
// transition without another consume call.
|
|
1308
2015
|
if (account.status !== "active" && existing?.status !== "completed") {
|
|
1309
|
-
throw new HTTPException(403, {
|
|
2016
|
+
throw new HTTPException(403, {
|
|
2017
|
+
message: "redemption credential is unavailable",
|
|
2018
|
+
});
|
|
1310
2019
|
}
|
|
1311
2020
|
const secret = settings.betterAuthSecret;
|
|
1312
2021
|
if (!secret) {
|
|
@@ -1505,13 +2214,21 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1505
2214
|
if (fenced.kind !== "ready") {
|
|
1506
2215
|
if (fenced.reason === "confirmation_expired") {
|
|
1507
2216
|
return c.json(
|
|
1508
|
-
{
|
|
2217
|
+
{
|
|
2218
|
+
status: "confirmation_expired",
|
|
2219
|
+
attemptId: attempt.id,
|
|
2220
|
+
retryable: true,
|
|
2221
|
+
},
|
|
1509
2222
|
403,
|
|
1510
2223
|
);
|
|
1511
2224
|
}
|
|
1512
2225
|
if (fenced.reason === "credential_unavailable") {
|
|
1513
2226
|
return c.json(
|
|
1514
|
-
{
|
|
2227
|
+
{
|
|
2228
|
+
status: "provider_unavailable",
|
|
2229
|
+
attemptId: attempt.id,
|
|
2230
|
+
retryable: true,
|
|
2231
|
+
},
|
|
1515
2232
|
503,
|
|
1516
2233
|
);
|
|
1517
2234
|
}
|