@opengeni/api-router 0.30.1 → 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-office-import.d.ts +7 -4
- 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 +1129 -166
- 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 +25 -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 +113 -10
- 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 +71 -3
- 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/files.d.ts +4 -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 +1974 -256
- 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 +77 -9
- 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 +820 -222
- 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 +3170 -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 -38
- 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 +839 -131
- 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 +1040 -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 +337 -39
- 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-2PQMSRCB.js +0 -49759
- package/dist/chunk-2PQMSRCB.js.map +0 -1
- package/dist/routes/packs.d.ts +0 -3
- package/src/routes/packs.ts +0 -729
package/src/routes/workspaces.ts
CHANGED
|
@@ -1,65 +1,148 @@
|
|
|
1
|
+
import { withDirectModelProviders } from "@opengeni/config";
|
|
2
|
+
import { listConnectionsMetadata } from "@opengeni/db";
|
|
3
|
+
import {
|
|
4
|
+
workspaceProviderApiKeyConnectionMetadataFromConnections,
|
|
5
|
+
listWorkspaceProviderCustomModelsByKind,
|
|
6
|
+
getOrganizationModelProviderCatalogForWorkspace,
|
|
7
|
+
type WorkspaceCustomModelProviderKind,
|
|
8
|
+
} from "@opengeni/db";
|
|
9
|
+
import { CLAUDE_CONNECTION_KINDS, type ClaudeConnectionCatalog } from "@opengeni/config";
|
|
10
|
+
import { SessionControlConflictError, WorkspacePauseTimerInputError } from "@opengeni/db";
|
|
11
|
+
import { updateWorkspaceSettingsWithToolDefaults } from "@opengeni/db/workspace-tool-defaults";
|
|
12
|
+
import { WorkspacePauseTimerRequest } from "@opengeni/contracts";
|
|
13
|
+
import { getWorkspaceConnectionModelRestrictions } from "@opengeni/db";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
1
15
|
import {
|
|
2
16
|
AddWorkspaceMemberRequest,
|
|
3
17
|
CreateWorkspaceRequest,
|
|
18
|
+
EnsureWorkspaceRequest,
|
|
19
|
+
EnsureWorkspaceResponse,
|
|
20
|
+
ListWorkspaceMemberCandidatesResponse,
|
|
4
21
|
ListWorkspaceMembersResponse,
|
|
5
22
|
SetWorkspaceDefaultRigRequest,
|
|
6
23
|
UpdateWorkspaceMemberRequest,
|
|
24
|
+
CreateWorkspaceGatewayCustomModelRequest,
|
|
25
|
+
CreateWorkspaceOpenRouterCustomModelRequest,
|
|
26
|
+
DeleteWorkspaceGatewayCustomModelRequest,
|
|
27
|
+
DeleteWorkspaceOpenRouterCustomModelRequest,
|
|
7
28
|
UpdateWorkspaceModelPolicyRequest,
|
|
8
29
|
UpdateWorkspaceRequest,
|
|
9
30
|
UpdateWorkspaceSettingsRequest,
|
|
10
31
|
WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
|
|
11
32
|
WorkspaceModelCatalogResponse,
|
|
33
|
+
WorkspaceGatewayCustomModel,
|
|
34
|
+
WorkspaceGatewayCustomModelsResponse,
|
|
35
|
+
WorkspaceOpenRouterCustomModel,
|
|
36
|
+
WorkspaceOpenRouterCustomModelsResponse,
|
|
12
37
|
WorkspaceRealtimeModelCatalogResponse,
|
|
13
38
|
WorkspaceInferenceControlRequest,
|
|
14
39
|
Workspace,
|
|
15
40
|
WorkspaceMember,
|
|
16
41
|
workspaceControlUtf8Bytes,
|
|
42
|
+
stableJson,
|
|
17
43
|
type AccessContext,
|
|
18
44
|
type Permission,
|
|
45
|
+
type WorkspaceMemberCandidate,
|
|
46
|
+
type WorkspaceMember as WorkspaceMemberValue,
|
|
19
47
|
} from "@opengeni/contracts";
|
|
48
|
+
import { loadWorkspaceCodexModelAvailability } from "@opengeni/core";
|
|
20
49
|
import {
|
|
21
50
|
allWorkspacePermissions,
|
|
22
51
|
createWorkspace,
|
|
23
|
-
|
|
24
|
-
|
|
52
|
+
ensureWorkspaceByExternalIdentity,
|
|
53
|
+
findWorkspaceByExternalIdentity,
|
|
54
|
+
getManagedUserProfilesByIds,
|
|
55
|
+
getWorkspace,
|
|
25
56
|
getWorkspaceModelPolicy,
|
|
26
57
|
grantWorkspaceAccess,
|
|
27
58
|
listWorkspaceMembers,
|
|
59
|
+
listWorkspaceMemberManagementCandidates,
|
|
60
|
+
normalizeWorkspaceMembershipPermissions,
|
|
28
61
|
listWorkspaceControlEvents,
|
|
62
|
+
listSharedWorkspacesForAccount,
|
|
63
|
+
listWorkspaceGatewayCustomModels,
|
|
64
|
+
listWorkspaceOpenRouterCustomModels,
|
|
65
|
+
createWorkspaceGatewayCustomModel,
|
|
66
|
+
createWorkspaceOpenRouterCustomModel,
|
|
67
|
+
deleteWorkspaceGatewayCustomModel,
|
|
68
|
+
deleteWorkspaceOpenRouterCustomModel,
|
|
69
|
+
replayWorkspaceGatewayCustomModelCreate,
|
|
70
|
+
replayWorkspaceOpenRouterCustomModelCreate,
|
|
29
71
|
listWorkspacesForSubject,
|
|
72
|
+
nestedPostgresSqlState,
|
|
30
73
|
removeWorkspaceMember,
|
|
31
74
|
requireWorkspace,
|
|
75
|
+
updateWorkspaceSettings,
|
|
32
76
|
getRig,
|
|
33
77
|
setWorkspaceDefaultRig,
|
|
34
78
|
updateWorkspace,
|
|
35
|
-
|
|
79
|
+
upsertWorkspaceMemberAsWorkspaceManager,
|
|
36
80
|
upsertWorkspaceModelPolicy,
|
|
37
81
|
workspaceCodexSubscriptionActive,
|
|
82
|
+
workspaceControlRequestLockTimeoutMs,
|
|
38
83
|
workspaceXaiSubscriptionActive,
|
|
39
84
|
workspaceVercelAiGatewayConnectionActive,
|
|
85
|
+
WorkspaceGatewayCustomModelHistoryLimitError,
|
|
86
|
+
WorkspaceOpenRouterCustomModelHistoryLimitError,
|
|
87
|
+
WorkspaceExternalIdentityConflictError,
|
|
88
|
+
WorkspaceGatewayCustomModelLimitError,
|
|
89
|
+
WorkspaceOpenRouterCustomModelLimitError,
|
|
90
|
+
WorkspaceLimitExceededError,
|
|
40
91
|
} from "@opengeni/db";
|
|
41
92
|
import { boundWorkspaceControlHttpPage } from "@opengeni/events";
|
|
42
93
|
import type { Hono } from "hono";
|
|
43
94
|
import { HTTPException } from "hono/http-exception";
|
|
44
|
-
import {
|
|
95
|
+
import {
|
|
96
|
+
getManagedAuthRequestActorEpoch,
|
|
97
|
+
accountScopedApiKeyWorkspaceAuthority,
|
|
98
|
+
organizationWorkspaceInScope,
|
|
99
|
+
hasPermission,
|
|
100
|
+
requireAccessContext,
|
|
101
|
+
requireApiKeyDelegationContext,
|
|
102
|
+
requireExplicitPermissionDelegation,
|
|
103
|
+
isDeveloperSetupApiKeyContext,
|
|
104
|
+
listExternalActorWorkspaces,
|
|
105
|
+
addExternalWorkspaceMemberForRequest,
|
|
106
|
+
requireAccessGrant,
|
|
107
|
+
requireWorkspaceSettingsGrant,
|
|
108
|
+
requireFreshAccessGrant,
|
|
109
|
+
resolveWorkspaceCatalogSettings,
|
|
110
|
+
creditsDefaultSessionModel,
|
|
111
|
+
loadWorkspaceClaudeSubscriptionReadiness,
|
|
112
|
+
resolveDefaultSessionModelForSelections,
|
|
113
|
+
resolveWorkspaceModelSelection,
|
|
114
|
+
} from "@opengeni/core";
|
|
45
115
|
import { requireLimit } from "@opengeni/core";
|
|
46
116
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
47
117
|
import {
|
|
48
118
|
assertWorkspaceMemberRemovable,
|
|
119
|
+
assertWorkspaceMemberUpdateAllowed,
|
|
49
120
|
controlHumanWorkspace,
|
|
50
|
-
|
|
121
|
+
controlHumanWorkspaceTimer,
|
|
51
122
|
} from "@opengeni/core";
|
|
52
123
|
import { boundedLimit } from "../http/common";
|
|
53
|
-
import {
|
|
54
|
-
import {
|
|
55
|
-
import {
|
|
124
|
+
import { ApiHttpError } from "../http/api-error";
|
|
125
|
+
import { browserSseDeliveryOptions, sseWorkspaceControlStream } from "../http/sse";
|
|
126
|
+
import { projectWorkspaceModelCatalog } from "../model-catalog";
|
|
127
|
+
import { deleteWorkspaceForRequest } from "../workspace-deletion";
|
|
56
128
|
import {
|
|
57
129
|
AI_GATEWAY_REALTIME_MODELS,
|
|
58
130
|
CODEX_REALTIME_MODEL_ID,
|
|
59
131
|
SUPERGROK_REALTIME_MODEL_ID,
|
|
60
132
|
canonicalizeConfiguredModelId,
|
|
133
|
+
configuredStaticUsageLimits,
|
|
134
|
+
configuredGatewayUpstreamModelIds,
|
|
135
|
+
configuredGatewayWorkspaceProductModelIds,
|
|
136
|
+
configuredModelInputIdentities,
|
|
137
|
+
configuredOpenRouterUpstreamModelIds,
|
|
138
|
+
configuredOpenRouterWorkspaceProductModelIds,
|
|
139
|
+
WORKSPACE_GATEWAY_MODEL_ID_PREFIX,
|
|
140
|
+
WORKSPACE_OPENROUTER_MODEL_ID_PREFIX,
|
|
141
|
+
sandboxImageAllowlist,
|
|
61
142
|
type Settings,
|
|
62
143
|
} from "@opengeni/config";
|
|
144
|
+
import { AddExternalWorkspaceMemberRequest } from "@opengeni/contracts/external-identities";
|
|
145
|
+
import { parseRequestBody, parseRequestJson, readRequestJson } from "../http/request-body";
|
|
63
146
|
|
|
64
147
|
export function canonicalWorkspacePolicyModelIds(
|
|
65
148
|
settings: Settings,
|
|
@@ -71,13 +154,145 @@ export function canonicalWorkspacePolicyModelIds(
|
|
|
71
154
|
return [...new Set(modelIds.map((modelId) => canonicalizeConfiguredModelId(settings, modelId)))];
|
|
72
155
|
}
|
|
73
156
|
|
|
157
|
+
function projectWorkspaceGatewayCustomModel(model: {
|
|
158
|
+
id: string;
|
|
159
|
+
upstreamModelId: string;
|
|
160
|
+
label: string | null;
|
|
161
|
+
version: number;
|
|
162
|
+
createdAt: Date;
|
|
163
|
+
updatedAt: Date;
|
|
164
|
+
}) {
|
|
165
|
+
return WorkspaceGatewayCustomModel.parse({
|
|
166
|
+
id: model.id,
|
|
167
|
+
upstreamModelId: model.upstreamModelId,
|
|
168
|
+
label: model.label,
|
|
169
|
+
version: model.version,
|
|
170
|
+
createdAt: model.createdAt.toISOString(),
|
|
171
|
+
updatedAt: model.updatedAt.toISOString(),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function projectWorkspaceOpenRouterCustomModel(model: {
|
|
176
|
+
id: string;
|
|
177
|
+
upstreamModelId: string;
|
|
178
|
+
label: string | null;
|
|
179
|
+
version: number;
|
|
180
|
+
createdAt: Date;
|
|
181
|
+
updatedAt: Date;
|
|
182
|
+
}) {
|
|
183
|
+
return WorkspaceOpenRouterCustomModel.parse({
|
|
184
|
+
id: model.id,
|
|
185
|
+
upstreamModelId: model.upstreamModelId,
|
|
186
|
+
label: model.label,
|
|
187
|
+
version: model.version,
|
|
188
|
+
createdAt: model.createdAt.toISOString(),
|
|
189
|
+
updatedAt: model.updatedAt.toISOString(),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function workspaceCustomModelRequestHash(value: unknown): string {
|
|
194
|
+
return createHash("sha256").update(stableJson(value), "utf8").digest("hex");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
type WorkspaceMemberProjectionInput = Omit<WorkspaceMemberValue, "permissions"> & {
|
|
198
|
+
permissions: unknown;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Keep the roster readable across stored-permission contract changes. Unknown
|
|
203
|
+
* values are never re-authorized: they are omitted from the public projection,
|
|
204
|
+
* while every currently recognized permission is preserved in stored order.
|
|
205
|
+
*/
|
|
206
|
+
export function workspaceMembersResponse(members: readonly WorkspaceMemberProjectionInput[]) {
|
|
207
|
+
return ListWorkspaceMembersResponse.parse({
|
|
208
|
+
members: members.map((member) => ({
|
|
209
|
+
...member,
|
|
210
|
+
permissions: normalizeWorkspaceMembershipPermissions(member.permissions),
|
|
211
|
+
})),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function workspaceUpdateRequestsSettings(value: unknown): boolean {
|
|
216
|
+
return (
|
|
217
|
+
typeof value === "object" &&
|
|
218
|
+
value !== null &&
|
|
219
|
+
Object.prototype.hasOwnProperty.call(value, "settings")
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function workspaceUpdateRequestsAccountTransfer(value: unknown): boolean {
|
|
224
|
+
return (
|
|
225
|
+
typeof value === "object" &&
|
|
226
|
+
value !== null &&
|
|
227
|
+
Object.prototype.hasOwnProperty.call(value, "accountId")
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Owning organization for `PUT /v1/workspaces/external`. An organization API
|
|
233
|
+
* key can only ever create workspaces in its own organization, so an omitted
|
|
234
|
+
* `accountId` resolves to that organization. Every other caller - notably a
|
|
235
|
+
* human who may belong to several organizations - must name it explicitly;
|
|
236
|
+
* guessing a default there could create the workspace in the wrong tenant.
|
|
237
|
+
*/
|
|
238
|
+
export function externalWorkspaceAccountId(
|
|
239
|
+
context: AccessContext,
|
|
240
|
+
requested: string | undefined,
|
|
241
|
+
): string {
|
|
242
|
+
if (requested !== undefined) return requested;
|
|
243
|
+
const organizationKey = accountScopedApiKeyWorkspaceAuthority(context);
|
|
244
|
+
if (organizationKey) return organizationKey.accountId;
|
|
245
|
+
throw new ApiHttpError(400, {
|
|
246
|
+
code: "validation_failed",
|
|
247
|
+
message:
|
|
248
|
+
"Invalid request body: accountId: required - pass the id of the organization that will own the workspace (only an organization API key may omit it).",
|
|
249
|
+
retryable: false,
|
|
250
|
+
details: {
|
|
251
|
+
code: "invalid_request_body",
|
|
252
|
+
issues: [{ path: "accountId", message: "Required" }],
|
|
253
|
+
omittedIssueCount: 0,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
74
258
|
export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
259
|
+
app.post("/v1/workspaces/:workspaceId/external-members", async (c) => {
|
|
260
|
+
// Validate at the HTTP boundary so a malformed body is a 400, not a raw
|
|
261
|
+
// schema error from the application layer.
|
|
262
|
+
const body = await readRequestJson(c);
|
|
263
|
+
parseRequestBody(AddExternalWorkspaceMemberRequest, body);
|
|
264
|
+
return c.json(
|
|
265
|
+
await addExternalWorkspaceMemberForRequest(c, deps, c.req.param("workspaceId"), body),
|
|
266
|
+
);
|
|
267
|
+
});
|
|
75
268
|
app.get("/v1/access/me", async (c) => {
|
|
76
269
|
return c.json(await requireAccessContext(c, deps));
|
|
77
270
|
});
|
|
78
271
|
|
|
272
|
+
app.get("/v1/workspaces/:workspaceId/access/grant", async (c) => {
|
|
273
|
+
// Inventory may be empty for an external actor. Resolve the selected
|
|
274
|
+
// workspace through the canonical membership/key-ceiling boundary.
|
|
275
|
+
const grant = await requireAccessGrant(c, deps, c.req.param("workspaceId"));
|
|
276
|
+
c.header("cache-control", "private, no-store");
|
|
277
|
+
return c.json(grant);
|
|
278
|
+
});
|
|
279
|
+
|
|
79
280
|
app.get("/v1/workspaces", async (c) => {
|
|
80
281
|
const context = await requireAccessContext(c, deps);
|
|
282
|
+
const externalWorkspaces = await listExternalActorWorkspaces(context, deps);
|
|
283
|
+
if (externalWorkspaces !== null)
|
|
284
|
+
return c.json(externalWorkspaces.map((workspace) => Workspace.parse(workspace)));
|
|
285
|
+
const accountScopedAuthority = accountScopedApiKeyWorkspaceAuthority(context);
|
|
286
|
+
if (accountScopedAuthority) {
|
|
287
|
+
if (!hasPermission(accountScopedAuthority.permissions, "workspace:read")) return c.json([]);
|
|
288
|
+
return c.json(
|
|
289
|
+
(await listSharedWorkspacesForAccount(deps.db, accountScopedAuthority.accountId))
|
|
290
|
+
.filter((workspace) =>
|
|
291
|
+
organizationWorkspaceInScope(accountScopedAuthority.workspaceScope, workspace.id),
|
|
292
|
+
)
|
|
293
|
+
.map((workspace) => Workspace.parse(workspace)),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
81
296
|
const readableWorkspaceIds = [
|
|
82
297
|
...new Set(
|
|
83
298
|
context.workspaceGrants
|
|
@@ -98,34 +313,123 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
98
313
|
);
|
|
99
314
|
});
|
|
100
315
|
|
|
316
|
+
app.put("/v1/workspaces/external", async (c) => {
|
|
317
|
+
const context = await requireAccessContext(c, deps);
|
|
318
|
+
const request = await parseRequestJson(c, EnsureWorkspaceRequest);
|
|
319
|
+
const accountId = externalWorkspaceAccountId(context, request.accountId);
|
|
320
|
+
const payload = { ...request, accountId };
|
|
321
|
+
requireAccountPermission(context, accountId, "workspace:create");
|
|
322
|
+
try {
|
|
323
|
+
const existing = await findWorkspaceByExternalIdentity(deps.db, {
|
|
324
|
+
accountId: payload.accountId,
|
|
325
|
+
externalSource: payload.externalSource,
|
|
326
|
+
externalId: payload.externalId,
|
|
327
|
+
});
|
|
328
|
+
if (existing) {
|
|
329
|
+
if (existing.accountId !== payload.accountId || existing.kind !== "shared") {
|
|
330
|
+
throw new WorkspaceExternalIdentityConflictError();
|
|
331
|
+
}
|
|
332
|
+
const authority = accountScopedApiKeyWorkspaceAuthority(context);
|
|
333
|
+
if (authority && !organizationWorkspaceInScope(authority.workspaceScope, existing.id))
|
|
334
|
+
throw new HTTPException(403, { message: "workspace is outside organization key scope" });
|
|
335
|
+
return c.json(
|
|
336
|
+
EnsureWorkspaceResponse.parse({
|
|
337
|
+
workspace: existing,
|
|
338
|
+
created: false,
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
await requireLimit(deps, {
|
|
343
|
+
accountId: payload.accountId,
|
|
344
|
+
action: "workspace:create",
|
|
345
|
+
quantity: 1,
|
|
346
|
+
});
|
|
347
|
+
const result = await ensureWorkspaceByExternalIdentity(deps.db, {
|
|
348
|
+
accountId: payload.accountId,
|
|
349
|
+
externalSource: payload.externalSource,
|
|
350
|
+
externalId: payload.externalId,
|
|
351
|
+
name: payload.name,
|
|
352
|
+
slug: payload.slug ?? null,
|
|
353
|
+
...(payload.agentInstructions !== undefined
|
|
354
|
+
? {
|
|
355
|
+
agentInstructions: normalizeAgentInstructions(payload.agentInstructions),
|
|
356
|
+
}
|
|
357
|
+
: {}),
|
|
358
|
+
maxWorkspacesPerAccount: workspaceLimit(deps),
|
|
359
|
+
});
|
|
360
|
+
const response = EnsureWorkspaceResponse.parse(result);
|
|
361
|
+
const authority = accountScopedApiKeyWorkspaceAuthority(context);
|
|
362
|
+
if (
|
|
363
|
+
!result.created &&
|
|
364
|
+
authority &&
|
|
365
|
+
!organizationWorkspaceInScope(authority.workspaceScope, result.workspace.id)
|
|
366
|
+
)
|
|
367
|
+
throw new HTTPException(403, { message: "workspace is outside organization key scope" });
|
|
368
|
+
return result.created ? c.json(response, 201) : c.json(response);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
if (error instanceof WorkspaceExternalIdentityConflictError) {
|
|
371
|
+
throw new HTTPException(409, {
|
|
372
|
+
message: "external workspace identity is already in use",
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
if (error instanceof WorkspaceLimitExceededError) {
|
|
376
|
+
throw new HTTPException(429, { message: error.message });
|
|
377
|
+
}
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
101
382
|
app.post("/v1/workspaces", async (c) => {
|
|
102
383
|
const context = await requireAccessContext(c, deps);
|
|
103
|
-
const payload =
|
|
384
|
+
const payload = await parseRequestJson(c, CreateWorkspaceRequest);
|
|
104
385
|
const accountId = payload.accountId ?? context.defaultAccountId;
|
|
105
386
|
if (!accountId) {
|
|
106
|
-
throw new HTTPException(409, {
|
|
387
|
+
throw new HTTPException(409, {
|
|
388
|
+
message: "account selection is required",
|
|
389
|
+
});
|
|
107
390
|
}
|
|
108
391
|
requireAccountPermission(context, accountId, "workspace:create");
|
|
109
|
-
await requireLimit(deps, {
|
|
110
|
-
const workspace = await createWorkspace(deps.db, {
|
|
392
|
+
await requireLimit(deps, {
|
|
111
393
|
accountId,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
externalSource: payload.externalSource ?? null,
|
|
115
|
-
externalId: payload.externalId ?? null,
|
|
116
|
-
...(payload.agentInstructions !== undefined
|
|
117
|
-
? { agentInstructions: normalizeAgentInstructions(payload.agentInstructions) }
|
|
118
|
-
: {}),
|
|
394
|
+
action: "workspace:create",
|
|
395
|
+
quantity: 1,
|
|
119
396
|
});
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
397
|
+
try {
|
|
398
|
+
const workspace = await createWorkspace(deps.db, {
|
|
399
|
+
accountId,
|
|
400
|
+
name: payload.name.trim(),
|
|
401
|
+
slug: payload.slug?.trim() || null,
|
|
402
|
+
externalSource: payload.externalSource ?? null,
|
|
403
|
+
externalId: payload.externalId ?? null,
|
|
404
|
+
...(payload.agentInstructions !== undefined
|
|
405
|
+
? {
|
|
406
|
+
agentInstructions: normalizeAgentInstructions(payload.agentInstructions),
|
|
407
|
+
}
|
|
408
|
+
: {}),
|
|
409
|
+
maxWorkspacesPerAccount: workspaceLimit(deps),
|
|
410
|
+
});
|
|
411
|
+
// Setup keys already have canonical same-organization workspace access.
|
|
412
|
+
// Do not widen that ceiling with an all-permissions creator membership.
|
|
413
|
+
if (
|
|
414
|
+
!isDeveloperSetupApiKeyContext(context) &&
|
|
415
|
+
accountScopedApiKeyWorkspaceAuthority(context)?.permissionMode !== "explicit"
|
|
416
|
+
) {
|
|
417
|
+
await grantWorkspaceAccess(deps.db, {
|
|
418
|
+
accountId,
|
|
419
|
+
workspaceId: workspace.id,
|
|
420
|
+
subjectId: context.subjectId,
|
|
421
|
+
role: "owner",
|
|
422
|
+
permissions: allWorkspacePermissions,
|
|
423
|
+
...(context.subjectLabel ? { subjectLabel: context.subjectLabel } : {}),
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
return c.json(Workspace.parse(workspace), 201);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
if (error instanceof WorkspaceLimitExceededError) {
|
|
429
|
+
throw new HTTPException(429, { message: error.message });
|
|
430
|
+
}
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
129
433
|
});
|
|
130
434
|
|
|
131
435
|
app.get("/v1/workspaces/:workspaceId", async (c) => {
|
|
@@ -137,12 +441,43 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
137
441
|
app.patch("/v1/workspaces/:workspaceId", async (c) => {
|
|
138
442
|
const workspaceId = c.req.param("workspaceId");
|
|
139
443
|
await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
140
|
-
const
|
|
444
|
+
const body = await readRequestJson(c);
|
|
445
|
+
if (workspaceUpdateRequestsSettings(body)) {
|
|
446
|
+
throw new ApiHttpError(400, {
|
|
447
|
+
code: "validation_failed",
|
|
448
|
+
message:
|
|
449
|
+
"Invalid request body: settings: workspace settings are not accepted here; send them to PATCH /v1/workspaces/:workspaceId/settings (SDK: updateWorkspaceSettings).",
|
|
450
|
+
retryable: false,
|
|
451
|
+
details: {
|
|
452
|
+
code: "invalid_request_body",
|
|
453
|
+
issues: [
|
|
454
|
+
{
|
|
455
|
+
path: "settings",
|
|
456
|
+
message: "Use PATCH /v1/workspaces/:workspaceId/settings",
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
omittedIssueCount: 0,
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
if (workspaceUpdateRequestsAccountTransfer(body)) {
|
|
464
|
+
throw new ApiHttpError(409, {
|
|
465
|
+
code: "conflict",
|
|
466
|
+
message:
|
|
467
|
+
"A workspace is permanently owned by one organization; use workspace grants for same-organization access handoff.",
|
|
468
|
+
retryable: false,
|
|
469
|
+
outcomeUnknown: false,
|
|
470
|
+
details: { code: "workspace_transfer_unsupported" },
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
const payload = parseRequestBody(UpdateWorkspaceRequest, body);
|
|
141
474
|
const workspace = await updateWorkspace(deps.db, workspaceId, {
|
|
142
475
|
...(payload.name !== undefined ? { name: payload.name.trim() } : {}),
|
|
143
476
|
...(payload.slug !== undefined ? { slug: payload.slug?.trim() || null } : {}),
|
|
144
477
|
...(payload.agentInstructions !== undefined
|
|
145
|
-
? {
|
|
478
|
+
? {
|
|
479
|
+
agentInstructions: normalizeAgentInstructions(payload.agentInstructions),
|
|
480
|
+
}
|
|
146
481
|
: {}),
|
|
147
482
|
});
|
|
148
483
|
return c.json(Workspace.parse(workspace));
|
|
@@ -152,13 +487,42 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
152
487
|
// deep-merges (top-level) a settings patch, preserving unknown/future keys.
|
|
153
488
|
app.patch("/v1/workspaces/:workspaceId/settings", async (c) => {
|
|
154
489
|
const workspaceId = c.req.param("workspaceId");
|
|
155
|
-
await
|
|
490
|
+
await requireWorkspaceSettingsGrant(c, deps, workspaceId);
|
|
156
491
|
const parsed = UpdateWorkspaceSettingsRequest.safeParse(await c.req.json());
|
|
157
492
|
if (!parsed.success) {
|
|
158
|
-
throw new HTTPException(400, {
|
|
493
|
+
throw new HTTPException(400, {
|
|
494
|
+
message: "invalid workspace settings patch",
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
const requestedImage = parsed.data.defaultSandboxImage;
|
|
498
|
+
if (requestedImage && !sandboxImageAllowlist(deps.settings).includes(requestedImage)) {
|
|
499
|
+
throw new HTTPException(422, {
|
|
500
|
+
message:
|
|
501
|
+
"defaultSandboxImage must be one of the images in this deployment's OPENGENI_SANDBOX_IMAGE_ALLOWLIST",
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
// Request-scoped: bound the exclusive control-prefix wait so a busy
|
|
505
|
+
// workspace yields the retryable 503 instead of parking this request.
|
|
506
|
+
try {
|
|
507
|
+
const workspace = await updateWorkspaceSettingsWithToolDefaults(
|
|
508
|
+
deps.db,
|
|
509
|
+
workspaceId,
|
|
510
|
+
parsed.data,
|
|
511
|
+
{ requireWorkspace, updateWorkspaceSettings },
|
|
512
|
+
{
|
|
513
|
+
controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
|
|
514
|
+
},
|
|
515
|
+
);
|
|
516
|
+
return c.json(Workspace.parse(workspace));
|
|
517
|
+
} catch (error) {
|
|
518
|
+
if (nestedPostgresSqlState(error) === "0A000") {
|
|
519
|
+
throw new HTTPException(409, {
|
|
520
|
+
message:
|
|
521
|
+
"Memory settings moved to Settings > Agent learning. Refresh the app to change them.",
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
throw error;
|
|
159
525
|
}
|
|
160
|
-
const workspace = await updateWorkspaceSettings(deps.db, workspaceId, parsed.data);
|
|
161
|
-
return c.json(Workspace.parse(workspace));
|
|
162
526
|
});
|
|
163
527
|
|
|
164
528
|
// Per-workspace model/provider availability policy (the HARD blocker over
|
|
@@ -168,31 +532,392 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
168
532
|
app.get("/v1/workspaces/:workspaceId/model-catalog", async (c) => {
|
|
169
533
|
const workspaceId = c.req.param("workspaceId");
|
|
170
534
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
535
|
+
const resolvedCatalog = await deps.resolveCatalogSettings();
|
|
536
|
+
const providerKinds: WorkspaceCustomModelProviderKind[] = [
|
|
537
|
+
"vercel_gateway",
|
|
538
|
+
"openrouter",
|
|
539
|
+
...CLAUDE_CONNECTION_KINDS.filter(
|
|
540
|
+
(kind) =>
|
|
541
|
+
kind !== "claude_subscription" || resolvedCatalog.settings.claudeSubscriptionEnabled,
|
|
542
|
+
),
|
|
543
|
+
];
|
|
171
544
|
const [
|
|
545
|
+
connectionModelRestrictions,
|
|
172
546
|
policy,
|
|
173
547
|
codexSubscriptionActive,
|
|
548
|
+
codexModelAvailability,
|
|
174
549
|
xaiSubscriptionActive,
|
|
175
|
-
|
|
550
|
+
claudePool,
|
|
551
|
+
workspaceConnections,
|
|
552
|
+
workspaceCustomModels,
|
|
553
|
+
organizationProviders,
|
|
554
|
+
workspace,
|
|
176
555
|
] = await Promise.all([
|
|
556
|
+
getWorkspaceConnectionModelRestrictions(deps.db, workspaceId, grant.subjectId),
|
|
177
557
|
getWorkspaceModelPolicy(deps.db, workspaceId),
|
|
178
558
|
workspaceCodexSubscriptionActive(deps.db, deps.settings, workspaceId),
|
|
559
|
+
loadWorkspaceCodexModelAvailability(deps.db, resolvedCatalog.settings, workspaceId),
|
|
179
560
|
workspaceXaiSubscriptionActive(deps.db, deps.settings, workspaceId, grant.subjectId),
|
|
180
|
-
|
|
561
|
+
loadWorkspaceClaudeSubscriptionReadiness(deps.db, resolvedCatalog.settings, {
|
|
562
|
+
accountId: grant.accountId,
|
|
563
|
+
workspaceId,
|
|
564
|
+
subjectId: grant.subjectId,
|
|
565
|
+
}),
|
|
566
|
+
listConnectionsMetadata(deps.db, workspaceId, null),
|
|
567
|
+
listWorkspaceProviderCustomModelsByKind(deps.db, {
|
|
568
|
+
accountId: grant.accountId,
|
|
569
|
+
workspaceId,
|
|
570
|
+
providerKinds,
|
|
571
|
+
}),
|
|
572
|
+
getOrganizationModelProviderCatalogForWorkspace(deps.db, {
|
|
573
|
+
accountId: grant.accountId,
|
|
574
|
+
workspaceId,
|
|
575
|
+
providerKinds,
|
|
576
|
+
}),
|
|
577
|
+
getWorkspace(deps.db, workspaceId),
|
|
181
578
|
]);
|
|
579
|
+
const claudeConnections: ClaudeConnectionCatalog = {};
|
|
580
|
+
const workspaceClaudeConnections: ClaudeConnectionCatalog = {};
|
|
581
|
+
const workspaceConnectionActive = (kind: WorkspaceCustomModelProviderKind) =>
|
|
582
|
+
workspaceProviderApiKeyConnectionMetadataFromConnections(workspaceConnections, kind) !== null;
|
|
583
|
+
for (const kind of CLAUDE_CONNECTION_KINDS) {
|
|
584
|
+
if (kind === "claude_subscription" && !resolvedCatalog.settings.claudeSubscriptionEnabled)
|
|
585
|
+
continue;
|
|
586
|
+
claudeConnections[kind] = {
|
|
587
|
+
active:
|
|
588
|
+
kind === "claude_subscription"
|
|
589
|
+
? claudePool.organization
|
|
590
|
+
: organizationProviders[kind].active,
|
|
591
|
+
models: organizationProviders[kind].models,
|
|
592
|
+
};
|
|
593
|
+
workspaceClaudeConnections[kind] = {
|
|
594
|
+
active:
|
|
595
|
+
kind === "claude_subscription" ? claudePool.workspace : workspaceConnectionActive(kind),
|
|
596
|
+
models: workspaceCustomModels[kind],
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
const workspaceCatalogSettings = withDirectModelProviders(
|
|
600
|
+
resolvedCatalog.settings,
|
|
601
|
+
workspaceConnections,
|
|
602
|
+
);
|
|
603
|
+
const selections = resolveWorkspaceModelSelection({
|
|
604
|
+
claudeConnections,
|
|
605
|
+
workspaceClaudeConnections,
|
|
606
|
+
connectionModelRestrictions,
|
|
607
|
+
settings: workspaceCatalogSettings,
|
|
608
|
+
policy,
|
|
609
|
+
observations: codexModelAvailability,
|
|
610
|
+
codexSubscriptionActive,
|
|
611
|
+
xaiSubscriptionActive,
|
|
612
|
+
workspaceGatewayConnectionActive: workspaceConnectionActive("vercel_gateway"),
|
|
613
|
+
workspaceGatewayCustomModels: workspaceCustomModels.vercel_gateway,
|
|
614
|
+
workspaceOpenRouterConnectionActive: workspaceConnectionActive("openrouter"),
|
|
615
|
+
workspaceOpenRouterCustomModels: workspaceCustomModels.openrouter,
|
|
616
|
+
organizationGatewayConnectionActive: organizationProviders.vercel_gateway.active,
|
|
617
|
+
organizationOpenRouterConnectionActive: organizationProviders.openrouter.active,
|
|
618
|
+
organizationGatewayCustomModels: organizationProviders.vercel_gateway.models,
|
|
619
|
+
organizationOpenRouterCustomModels: organizationProviders.openrouter.models,
|
|
620
|
+
});
|
|
621
|
+
// The same precedence the server applies when a new chat, API create, or
|
|
622
|
+
// scheduled occurrence names no model; published so pickers show it.
|
|
623
|
+
const workspaceSettings = workspace?.settings ?? {};
|
|
624
|
+
const defaultSelection = await resolveDefaultSessionModelForSelections(deps.db, {
|
|
625
|
+
settings: workspaceCatalogSettings,
|
|
626
|
+
accountId: grant.accountId,
|
|
627
|
+
workspaceSettings,
|
|
628
|
+
selections,
|
|
629
|
+
});
|
|
182
630
|
c.header("cache-control", "private, no-store");
|
|
183
631
|
return c.json(
|
|
184
632
|
WorkspaceModelCatalogResponse.parse(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
633
|
+
projectWorkspaceModelCatalog(selections, {
|
|
634
|
+
defaultSelection,
|
|
635
|
+
creditsSelection: creditsDefaultSessionModel({
|
|
636
|
+
settings: workspaceCatalogSettings,
|
|
637
|
+
selections,
|
|
638
|
+
workspaceSettings,
|
|
639
|
+
}),
|
|
191
640
|
}),
|
|
192
641
|
),
|
|
193
642
|
);
|
|
194
643
|
});
|
|
195
644
|
|
|
645
|
+
app.get("/v1/workspaces/:workspaceId/gateway-custom-models", async (c) => {
|
|
646
|
+
const workspaceId = c.req.param("workspaceId");
|
|
647
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
648
|
+
const models = await listWorkspaceGatewayCustomModels(deps.db, {
|
|
649
|
+
accountId: grant.accountId,
|
|
650
|
+
workspaceId,
|
|
651
|
+
});
|
|
652
|
+
c.header("cache-control", "private, no-store");
|
|
653
|
+
return c.json(
|
|
654
|
+
WorkspaceGatewayCustomModelsResponse.parse({
|
|
655
|
+
models: models.map(projectWorkspaceGatewayCustomModel),
|
|
656
|
+
}),
|
|
657
|
+
);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
app.post("/v1/workspaces/:workspaceId/gateway-custom-models", async (c) => {
|
|
661
|
+
const workspaceId = c.req.param("workspaceId");
|
|
662
|
+
const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId);
|
|
663
|
+
const parsed = CreateWorkspaceGatewayCustomModelRequest.safeParse(
|
|
664
|
+
await c.req.json().catch(() => null),
|
|
665
|
+
);
|
|
666
|
+
if (!parsed.success) {
|
|
667
|
+
throw new HTTPException(422, { message: "invalid Gateway custom model" });
|
|
668
|
+
}
|
|
669
|
+
const requestHash = workspaceCustomModelRequestHash({
|
|
670
|
+
action: "create",
|
|
671
|
+
upstreamModelId: parsed.data.upstreamModelId,
|
|
672
|
+
label: parsed.data.label ?? null,
|
|
673
|
+
});
|
|
674
|
+
const replay = await replayWorkspaceGatewayCustomModelCreate(deps.db, {
|
|
675
|
+
accountId: grant.accountId,
|
|
676
|
+
workspaceId,
|
|
677
|
+
operationId: parsed.data.operationId,
|
|
678
|
+
requestHash,
|
|
679
|
+
});
|
|
680
|
+
if (replay.outcome === "conflict") {
|
|
681
|
+
throw new HTTPException(409, {
|
|
682
|
+
message: "Gateway custom model operation conflicts with current state",
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
if (replay.outcome === "success") {
|
|
686
|
+
return c.json(projectWorkspaceGatewayCustomModel(replay.model), 201);
|
|
687
|
+
}
|
|
688
|
+
const catalog = await deps.resolveCatalogSettings();
|
|
689
|
+
if (configuredGatewayUpstreamModelIds(catalog.settings).includes(parsed.data.upstreamModelId)) {
|
|
690
|
+
throw new HTTPException(422, {
|
|
691
|
+
message: "Gateway model is already included in the deployment catalog",
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
const customProductId = `${WORKSPACE_GATEWAY_MODEL_ID_PREFIX}${parsed.data.upstreamModelId}`;
|
|
695
|
+
const deploymentProductIds = new Set([
|
|
696
|
+
...configuredModelInputIdentities(catalog.settings),
|
|
697
|
+
...configuredGatewayWorkspaceProductModelIds(catalog.settings),
|
|
698
|
+
]);
|
|
699
|
+
if (deploymentProductIds.has(customProductId)) {
|
|
700
|
+
throw new HTTPException(422, {
|
|
701
|
+
message: "Gateway model product id conflicts with the deployment catalog",
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
try {
|
|
705
|
+
const model = await createWorkspaceGatewayCustomModel(deps.db, {
|
|
706
|
+
accountId: grant.accountId,
|
|
707
|
+
workspaceId,
|
|
708
|
+
upstreamModelId: parsed.data.upstreamModelId,
|
|
709
|
+
label: parsed.data.label ?? null,
|
|
710
|
+
operationId: parsed.data.operationId,
|
|
711
|
+
requestHash,
|
|
712
|
+
createdBySubjectId: grant.subjectId,
|
|
713
|
+
});
|
|
714
|
+
if (!model || model.retiredAt) {
|
|
715
|
+
throw new HTTPException(409, {
|
|
716
|
+
message: "Gateway custom model operation conflicts with current state",
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
return c.json(projectWorkspaceGatewayCustomModel(model), 201);
|
|
720
|
+
} catch (error) {
|
|
721
|
+
if (
|
|
722
|
+
error instanceof WorkspaceGatewayCustomModelLimitError ||
|
|
723
|
+
error instanceof WorkspaceGatewayCustomModelHistoryLimitError
|
|
724
|
+
) {
|
|
725
|
+
throw new HTTPException(422, { message: error.message });
|
|
726
|
+
}
|
|
727
|
+
if (nestedPostgresSqlState(error) === "23505") {
|
|
728
|
+
throw new HTTPException(422, {
|
|
729
|
+
message: "Gateway custom model already exists",
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
throw error;
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
app.delete("/v1/workspaces/:workspaceId/gateway-custom-models/:customModelId", async (c) => {
|
|
737
|
+
const workspaceId = c.req.param("workspaceId");
|
|
738
|
+
const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId);
|
|
739
|
+
const customModelId = c.req.param("customModelId");
|
|
740
|
+
if (
|
|
741
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(
|
|
742
|
+
customModelId,
|
|
743
|
+
)
|
|
744
|
+
) {
|
|
745
|
+
throw new HTTPException(422, {
|
|
746
|
+
message: "invalid Gateway custom model id",
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
const parsed = DeleteWorkspaceGatewayCustomModelRequest.safeParse(
|
|
750
|
+
await c.req.json().catch(() => null),
|
|
751
|
+
);
|
|
752
|
+
if (!parsed.success) {
|
|
753
|
+
throw new HTTPException(422, {
|
|
754
|
+
message: "invalid Gateway custom model deletion",
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
const removed = await deleteWorkspaceGatewayCustomModel(deps.db, {
|
|
758
|
+
accountId: grant.accountId,
|
|
759
|
+
workspaceId,
|
|
760
|
+
customModelId,
|
|
761
|
+
expectedVersion: parsed.data.expectedVersion,
|
|
762
|
+
operationId: parsed.data.operationId,
|
|
763
|
+
requestHash: workspaceCustomModelRequestHash({
|
|
764
|
+
action: "delete",
|
|
765
|
+
customModelId,
|
|
766
|
+
expectedVersion: parsed.data.expectedVersion,
|
|
767
|
+
}),
|
|
768
|
+
});
|
|
769
|
+
if (removed.outcome === "not_found") {
|
|
770
|
+
throw new HTTPException(404, {
|
|
771
|
+
message: "Gateway custom model not found",
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
if (removed.outcome === "conflict") {
|
|
775
|
+
throw new HTTPException(409, {
|
|
776
|
+
message: "Gateway custom model changed; reload and retry",
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
return c.body(null, 204);
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
app.get("/v1/workspaces/:workspaceId/openrouter-custom-models", async (c) => {
|
|
783
|
+
const workspaceId = c.req.param("workspaceId");
|
|
784
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
785
|
+
const models = await listWorkspaceOpenRouterCustomModels(deps.db, {
|
|
786
|
+
accountId: grant.accountId,
|
|
787
|
+
workspaceId,
|
|
788
|
+
});
|
|
789
|
+
c.header("cache-control", "private, no-store");
|
|
790
|
+
return c.json(
|
|
791
|
+
WorkspaceOpenRouterCustomModelsResponse.parse({
|
|
792
|
+
models: models.map(projectWorkspaceOpenRouterCustomModel),
|
|
793
|
+
}),
|
|
794
|
+
);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
app.post("/v1/workspaces/:workspaceId/openrouter-custom-models", async (c) => {
|
|
798
|
+
const workspaceId = c.req.param("workspaceId");
|
|
799
|
+
const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId);
|
|
800
|
+
const parsed = CreateWorkspaceOpenRouterCustomModelRequest.safeParse(
|
|
801
|
+
await c.req.json().catch(() => null),
|
|
802
|
+
);
|
|
803
|
+
if (!parsed.success) {
|
|
804
|
+
throw new HTTPException(422, { message: "invalid OpenRouter custom model" });
|
|
805
|
+
}
|
|
806
|
+
const requestHash = workspaceCustomModelRequestHash({
|
|
807
|
+
action: "create",
|
|
808
|
+
upstreamModelId: parsed.data.upstreamModelId,
|
|
809
|
+
label: parsed.data.label ?? null,
|
|
810
|
+
});
|
|
811
|
+
const replay = await replayWorkspaceOpenRouterCustomModelCreate(deps.db, {
|
|
812
|
+
accountId: grant.accountId,
|
|
813
|
+
workspaceId,
|
|
814
|
+
operationId: parsed.data.operationId,
|
|
815
|
+
requestHash,
|
|
816
|
+
});
|
|
817
|
+
if (replay.outcome === "conflict") {
|
|
818
|
+
throw new HTTPException(409, {
|
|
819
|
+
message: "OpenRouter custom model operation conflicts with current state",
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
if (replay.outcome === "success") {
|
|
823
|
+
return c.json(projectWorkspaceOpenRouterCustomModel(replay.model), 201);
|
|
824
|
+
}
|
|
825
|
+
const catalog = await deps.resolveCatalogSettings();
|
|
826
|
+
if (
|
|
827
|
+
configuredOpenRouterUpstreamModelIds(catalog.settings).includes(parsed.data.upstreamModelId)
|
|
828
|
+
) {
|
|
829
|
+
throw new HTTPException(422, {
|
|
830
|
+
message: "OpenRouter model is already included in the deployment catalog",
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
const customProductId = `${WORKSPACE_OPENROUTER_MODEL_ID_PREFIX}${parsed.data.upstreamModelId}`;
|
|
834
|
+
const deploymentProductIds = new Set([
|
|
835
|
+
...configuredModelInputIdentities(catalog.settings),
|
|
836
|
+
...configuredOpenRouterWorkspaceProductModelIds(catalog.settings),
|
|
837
|
+
]);
|
|
838
|
+
if (deploymentProductIds.has(customProductId)) {
|
|
839
|
+
throw new HTTPException(422, {
|
|
840
|
+
message: "OpenRouter model product id conflicts with the deployment catalog",
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
try {
|
|
844
|
+
const model = await createWorkspaceOpenRouterCustomModel(deps.db, {
|
|
845
|
+
accountId: grant.accountId,
|
|
846
|
+
workspaceId,
|
|
847
|
+
upstreamModelId: parsed.data.upstreamModelId,
|
|
848
|
+
label: parsed.data.label ?? null,
|
|
849
|
+
operationId: parsed.data.operationId,
|
|
850
|
+
requestHash,
|
|
851
|
+
createdBySubjectId: grant.subjectId,
|
|
852
|
+
});
|
|
853
|
+
if (!model || model.retiredAt) {
|
|
854
|
+
throw new HTTPException(409, {
|
|
855
|
+
message: "OpenRouter custom model operation conflicts with current state",
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
return c.json(projectWorkspaceOpenRouterCustomModel(model), 201);
|
|
859
|
+
} catch (error) {
|
|
860
|
+
if (
|
|
861
|
+
error instanceof WorkspaceOpenRouterCustomModelLimitError ||
|
|
862
|
+
error instanceof WorkspaceOpenRouterCustomModelHistoryLimitError
|
|
863
|
+
) {
|
|
864
|
+
throw new HTTPException(422, { message: error.message });
|
|
865
|
+
}
|
|
866
|
+
if (nestedPostgresSqlState(error) === "23505") {
|
|
867
|
+
throw new HTTPException(422, {
|
|
868
|
+
message: "OpenRouter custom model already exists",
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
throw error;
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
app.delete("/v1/workspaces/:workspaceId/openrouter-custom-models/:customModelId", async (c) => {
|
|
876
|
+
const workspaceId = c.req.param("workspaceId");
|
|
877
|
+
const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId);
|
|
878
|
+
const customModelId = c.req.param("customModelId");
|
|
879
|
+
if (
|
|
880
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(
|
|
881
|
+
customModelId,
|
|
882
|
+
)
|
|
883
|
+
) {
|
|
884
|
+
throw new HTTPException(422, {
|
|
885
|
+
message: "invalid OpenRouter custom model id",
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
const parsed = DeleteWorkspaceOpenRouterCustomModelRequest.safeParse(
|
|
889
|
+
await c.req.json().catch(() => null),
|
|
890
|
+
);
|
|
891
|
+
if (!parsed.success) {
|
|
892
|
+
throw new HTTPException(422, {
|
|
893
|
+
message: "invalid OpenRouter custom model deletion",
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
const removed = await deleteWorkspaceOpenRouterCustomModel(deps.db, {
|
|
897
|
+
accountId: grant.accountId,
|
|
898
|
+
workspaceId,
|
|
899
|
+
customModelId,
|
|
900
|
+
expectedVersion: parsed.data.expectedVersion,
|
|
901
|
+
operationId: parsed.data.operationId,
|
|
902
|
+
requestHash: workspaceCustomModelRequestHash({
|
|
903
|
+
action: "delete",
|
|
904
|
+
customModelId,
|
|
905
|
+
expectedVersion: parsed.data.expectedVersion,
|
|
906
|
+
}),
|
|
907
|
+
});
|
|
908
|
+
if (removed.outcome === "not_found") {
|
|
909
|
+
throw new HTTPException(404, {
|
|
910
|
+
message: "OpenRouter custom model not found",
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
if (removed.outcome === "conflict") {
|
|
914
|
+
throw new HTTPException(409, {
|
|
915
|
+
message: "OpenRouter custom model changed; reload and retry",
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
return c.body(null, 204);
|
|
919
|
+
});
|
|
920
|
+
|
|
196
921
|
app.get("/v1/workspaces/:workspaceId/realtime-model-catalog", async (c) => {
|
|
197
922
|
const workspaceId = c.req.param("workspaceId");
|
|
198
923
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
@@ -218,7 +943,7 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
218
943
|
description: model.description,
|
|
219
944
|
...availability(
|
|
220
945
|
Boolean(deps.settings.vercelAiGatewayApiKey),
|
|
221
|
-
"
|
|
946
|
+
"Opengeni Gateway voice is not configured",
|
|
222
947
|
),
|
|
223
948
|
recommended: index === 0,
|
|
224
949
|
})),
|
|
@@ -262,35 +987,83 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
262
987
|
});
|
|
263
988
|
|
|
264
989
|
// Full replace (PUT, not merge): null/omitted = unrestricted for that
|
|
265
|
-
// dimension; an empty array is a valid explicit total block.
|
|
266
|
-
//
|
|
267
|
-
//
|
|
990
|
+
// dimension; an empty array is a valid explicit total block. Settings access
|
|
991
|
+
// admits workspace administrators and the verified Personal owner, without
|
|
992
|
+
// widening membership or API-key delegation authority.
|
|
268
993
|
app.put("/v1/workspaces/:workspaceId/model-policy", async (c) => {
|
|
269
994
|
const workspaceId = c.req.param("workspaceId");
|
|
270
|
-
const grant = await
|
|
271
|
-
const payload =
|
|
995
|
+
const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId);
|
|
996
|
+
const payload = await parseRequestJson(c, UpdateWorkspaceModelPolicyRequest);
|
|
997
|
+
const catalog = await resolveWorkspaceCatalogSettings(deps.db, deps.settings, {
|
|
998
|
+
accountId: grant.accountId,
|
|
999
|
+
workspaceId,
|
|
1000
|
+
});
|
|
272
1001
|
const policy = await upsertWorkspaceModelPolicy(deps.db, {
|
|
273
1002
|
accountId: grant.accountId,
|
|
274
1003
|
workspaceId,
|
|
275
1004
|
allowedProviders: payload.allowedProviders ?? null,
|
|
276
|
-
allowedModels: canonicalWorkspacePolicyModelIds(
|
|
1005
|
+
allowedModels: canonicalWorkspacePolicyModelIds(catalog.settings, payload.allowedModels),
|
|
277
1006
|
});
|
|
278
1007
|
return c.json(policy);
|
|
279
1008
|
});
|
|
280
1009
|
|
|
281
|
-
app.post("/v1/workspaces/:workspaceId/
|
|
1010
|
+
app.post("/v1/workspaces/:workspaceId/pause-timer", async (c) => {
|
|
282
1011
|
const workspaceId = c.req.param("workspaceId");
|
|
283
|
-
const grant = await
|
|
1012
|
+
const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId);
|
|
284
1013
|
if (workspaceControlUtf8Bytes(grant.subjectId) > WORKSPACE_CONTROL_ACTOR_MAX_BYTES) {
|
|
285
1014
|
throw new HTTPException(400, { message: "workspace-control actor is too large" });
|
|
286
1015
|
}
|
|
1016
|
+
const parsed = WorkspacePauseTimerRequest.safeParse(await c.req.json().catch(() => null));
|
|
1017
|
+
if (!parsed.success) throw new HTTPException(400, { message: "Invalid pause timer" });
|
|
1018
|
+
try {
|
|
1019
|
+
await controlHumanWorkspaceTimer(
|
|
1020
|
+
{
|
|
1021
|
+
db: deps.db,
|
|
1022
|
+
bus: deps.bus,
|
|
1023
|
+
workflowClient: deps.workflowClient,
|
|
1024
|
+
...(deps.schedulePromptPostCommit
|
|
1025
|
+
? { schedulePromptPostCommit: deps.schedulePromptPostCommit }
|
|
1026
|
+
: {}),
|
|
1027
|
+
},
|
|
1028
|
+
{ accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
|
|
1029
|
+
parsed.data,
|
|
1030
|
+
);
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
if (error instanceof SessionControlConflictError)
|
|
1033
|
+
throw new HTTPException(409, {
|
|
1034
|
+
message: "Workspace changed. Reopen the timer and try again.",
|
|
1035
|
+
});
|
|
1036
|
+
if (error instanceof WorkspacePauseTimerInputError)
|
|
1037
|
+
throw new HTTPException(400, { message: error.message });
|
|
1038
|
+
throw error;
|
|
1039
|
+
}
|
|
1040
|
+
return c.json({ ok: true });
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
app.post("/v1/workspaces/:workspaceId/inference-control", async (c) => {
|
|
1044
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1045
|
+
const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId);
|
|
1046
|
+
if (workspaceControlUtf8Bytes(grant.subjectId) > WORKSPACE_CONTROL_ACTOR_MAX_BYTES) {
|
|
1047
|
+
throw new HTTPException(400, {
|
|
1048
|
+
message: "workspace-control actor is too large",
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
287
1051
|
const parsed = WorkspaceInferenceControlRequest.safeParse(await c.req.json().catch(() => null));
|
|
288
1052
|
if (!parsed.success) {
|
|
289
|
-
throw new HTTPException(400, {
|
|
1053
|
+
throw new HTTPException(400, {
|
|
1054
|
+
message: "invalid workspace inference-control request",
|
|
1055
|
+
});
|
|
290
1056
|
}
|
|
291
1057
|
return c.json(
|
|
292
1058
|
await controlHumanWorkspace(
|
|
293
|
-
{
|
|
1059
|
+
{
|
|
1060
|
+
db: deps.db,
|
|
1061
|
+
bus: deps.bus,
|
|
1062
|
+
workflowClient: deps.workflowClient,
|
|
1063
|
+
...(deps.schedulePromptPostCommit
|
|
1064
|
+
? { schedulePromptPostCommit: deps.schedulePromptPostCommit }
|
|
1065
|
+
: {}),
|
|
1066
|
+
},
|
|
294
1067
|
{ accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
|
|
295
1068
|
parsed.data,
|
|
296
1069
|
),
|
|
@@ -324,18 +1097,27 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
324
1097
|
workspaceId,
|
|
325
1098
|
after,
|
|
326
1099
|
c.req.raw.signal,
|
|
327
|
-
{
|
|
1100
|
+
{
|
|
1101
|
+
...browserSseDeliveryOptions(c.req.query("transport")),
|
|
1102
|
+
observability: deps.observability,
|
|
1103
|
+
actorEpoch: getManagedAuthRequestActorEpoch(c.req.raw) ?? undefined,
|
|
1104
|
+
reauthorize: async () => {
|
|
1105
|
+
await requireFreshAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
1106
|
+
},
|
|
1107
|
+
},
|
|
328
1108
|
);
|
|
329
1109
|
});
|
|
330
1110
|
|
|
331
1111
|
app.put("/v1/workspaces/:workspaceId/default-rig", async (c) => {
|
|
332
1112
|
const workspaceId = c.req.param("workspaceId");
|
|
333
1113
|
await requireAccessGrant(c, deps, workspaceId, "rigs:manage");
|
|
334
|
-
const payload =
|
|
1114
|
+
const payload = await parseRequestJson(c, SetWorkspaceDefaultRigRequest);
|
|
335
1115
|
if (payload.rigId) {
|
|
336
1116
|
const rig = await getRig(deps.db, workspaceId, payload.rigId);
|
|
337
1117
|
if (!rig) {
|
|
338
|
-
throw new HTTPException(422, {
|
|
1118
|
+
throw new HTTPException(422, {
|
|
1119
|
+
message: `unknown rigId: ${payload.rigId}`,
|
|
1120
|
+
});
|
|
339
1121
|
}
|
|
340
1122
|
}
|
|
341
1123
|
const workspace = await setWorkspaceDefaultRig(deps.db, workspaceId, payload.rigId);
|
|
@@ -345,51 +1127,10 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
345
1127
|
app.delete("/v1/workspaces/:workspaceId", async (c) => {
|
|
346
1128
|
const workspaceId = c.req.param("workspaceId");
|
|
347
1129
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
348
|
-
|
|
349
|
-
// session/lease before checking runtime quiescence, then returns the exact
|
|
350
|
-
// external schedules removed by the cascade. A racing cold->warm transition
|
|
351
|
-
// can therefore never erase the only provider/capture ownership receipt.
|
|
352
|
-
const deleted = await deleteWorkspaceIfQuiescent(deps.db, {
|
|
1130
|
+
await deleteWorkspaceForRequest(deps, {
|
|
353
1131
|
accountId: grant.accountId,
|
|
354
1132
|
workspaceId,
|
|
355
1133
|
});
|
|
356
|
-
if (deleted.status === "not_found") {
|
|
357
|
-
throw new HTTPException(404, { message: "workspace not found" });
|
|
358
|
-
}
|
|
359
|
-
if (deleted.status === "only_workspace") {
|
|
360
|
-
throw new HTTPException(409, { message: "cannot delete the account's only workspace" });
|
|
361
|
-
}
|
|
362
|
-
if (deleted.status === "active_sessions") {
|
|
363
|
-
throw new HTTPException(409, {
|
|
364
|
-
message: "stop the workspace's running sessions before deleting it",
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
if (deleted.status === "active_video_generations") {
|
|
368
|
-
throw new HTTPException(409, {
|
|
369
|
-
message: "wait for the workspace's active video generations to finish before deleting it",
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
if (deleted.status === "live_sandboxes") {
|
|
373
|
-
throw new HTTPException(409, {
|
|
374
|
-
message: "wait for the workspace's active sandboxes to finish draining before deleting it",
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
if (deleted.status !== "deleted") {
|
|
378
|
-
throw new Error(`Unhandled workspace deletion outcome: ${deleted.status}`);
|
|
379
|
-
}
|
|
380
|
-
// The cleanup claims were inserted in the same transaction as the cascade.
|
|
381
|
-
// Try them immediately; failures are released to the replica-safe outbox
|
|
382
|
-
// pump, so a process crash or Temporal outage cannot orphan the schedules.
|
|
383
|
-
await processTemporalScheduleCleanupClaims(
|
|
384
|
-
{
|
|
385
|
-
db: deps.db,
|
|
386
|
-
deleteSchedule: async (temporalScheduleId) => {
|
|
387
|
-
await deps.workflowClient.deleteScheduledTaskSchedule({ temporalScheduleId });
|
|
388
|
-
},
|
|
389
|
-
...(deps.observability ? { observability: deps.observability } : {}),
|
|
390
|
-
},
|
|
391
|
-
deleted.temporalScheduleCleanups,
|
|
392
|
-
);
|
|
393
1134
|
return c.body(null, 204);
|
|
394
1135
|
});
|
|
395
1136
|
|
|
@@ -398,28 +1139,75 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
398
1139
|
app.get("/v1/workspaces/:workspaceId/members", async (c) => {
|
|
399
1140
|
const workspaceId = c.req.param("workspaceId");
|
|
400
1141
|
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
401
|
-
const members = await
|
|
402
|
-
return c.json(
|
|
1142
|
+
const members = await listWorkspacePeople(deps, workspaceId);
|
|
1143
|
+
return c.json(workspaceMembersResponse(members));
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
app.get("/v1/workspaces/:workspaceId/member-candidates", async (c) => {
|
|
1147
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1148
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "members:manage");
|
|
1149
|
+
try {
|
|
1150
|
+
return c.json(
|
|
1151
|
+
ListWorkspaceMemberCandidatesResponse.parse({
|
|
1152
|
+
members: await listWorkspaceMemberManagementCandidates(deps.db, {
|
|
1153
|
+
accountId: grant.accountId,
|
|
1154
|
+
workspaceId,
|
|
1155
|
+
actorSubjectId: grant.subjectId,
|
|
1156
|
+
}),
|
|
1157
|
+
}),
|
|
1158
|
+
);
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
rethrowWorkspaceMemberCandidateError(error);
|
|
1161
|
+
}
|
|
403
1162
|
});
|
|
404
1163
|
|
|
405
1164
|
app.post("/v1/workspaces/:workspaceId/members", async (c) => {
|
|
406
1165
|
const workspaceId = c.req.param("workspaceId");
|
|
407
1166
|
const grant = await requireAccessGrant(c, deps, workspaceId, "members:manage");
|
|
408
|
-
const payload =
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
const
|
|
422
|
-
|
|
1167
|
+
const payload = await parseRequestJson(c, AddWorkspaceMemberRequest);
|
|
1168
|
+
requireApiKeyDelegationContext(await requireAccessContext(c, deps), payload.permissions);
|
|
1169
|
+
requireExplicitPermissionDelegation(grant, payload.permissions);
|
|
1170
|
+
let candidates: WorkspaceMemberCandidate[];
|
|
1171
|
+
try {
|
|
1172
|
+
candidates = await listWorkspaceMemberManagementCandidates(deps.db, {
|
|
1173
|
+
accountId: grant.accountId,
|
|
1174
|
+
workspaceId,
|
|
1175
|
+
actorSubjectId: grant.subjectId,
|
|
1176
|
+
});
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
rethrowWorkspaceMemberCandidateError(error);
|
|
1179
|
+
}
|
|
1180
|
+
const candidate = candidates.find(
|
|
1181
|
+
(entry) => entry.organizationMembershipId === payload.organizationMembershipId,
|
|
1182
|
+
);
|
|
1183
|
+
if (!candidate) {
|
|
1184
|
+
throw new HTTPException(404, {
|
|
1185
|
+
message: "this organization member is not available to add",
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
const subjectId = candidate.subjectId;
|
|
1189
|
+
const existing = await listWorkspaceMembers(deps.db, workspaceId);
|
|
1190
|
+
if (existing.some((member) => member.subjectId === subjectId)) {
|
|
1191
|
+
throw new HTTPException(409, {
|
|
1192
|
+
message: "this person already has access to the workspace",
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
try {
|
|
1196
|
+
await upsertWorkspaceMemberAsWorkspaceManager(deps.db, {
|
|
1197
|
+
accountId: grant.accountId,
|
|
1198
|
+
workspaceId,
|
|
1199
|
+
actorSubjectId: grant.subjectId,
|
|
1200
|
+
targetSubjectId: subjectId,
|
|
1201
|
+
mode: "add",
|
|
1202
|
+
subjectLabel: candidate.name?.trim() || candidate.email || subjectId,
|
|
1203
|
+
role: payload.role ?? "member",
|
|
1204
|
+
permissions: payload.permissions,
|
|
1205
|
+
});
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
rethrowWorkspaceMemberCandidateError(error);
|
|
1208
|
+
}
|
|
1209
|
+
const members = await listWorkspacePeople(deps, workspaceId);
|
|
1210
|
+
const member = members.find((addedMember) => addedMember.subjectId === subjectId);
|
|
423
1211
|
if (!member) {
|
|
424
1212
|
throw new HTTPException(500, { message: "failed to add member" });
|
|
425
1213
|
}
|
|
@@ -430,21 +1218,35 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
430
1218
|
const workspaceId = c.req.param("workspaceId");
|
|
431
1219
|
const grant = await requireAccessGrant(c, deps, workspaceId, "members:manage");
|
|
432
1220
|
const subjectId = decodeURIComponent(c.req.param("subjectId"));
|
|
433
|
-
const payload =
|
|
434
|
-
|
|
1221
|
+
const payload = await parseRequestJson(c, UpdateWorkspaceMemberRequest);
|
|
1222
|
+
requireApiKeyDelegationContext(await requireAccessContext(c, deps), payload.permissions);
|
|
1223
|
+
requireExplicitPermissionDelegation(grant, payload.permissions);
|
|
1224
|
+
const existing = await listWorkspacePeople(deps, workspaceId);
|
|
435
1225
|
const current = existing.find((member) => member.subjectId === subjectId);
|
|
436
1226
|
if (!current) {
|
|
437
1227
|
throw new HTTPException(404, { message: "member not found" });
|
|
438
1228
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
workspaceId,
|
|
1229
|
+
assertWorkspaceMemberUpdateAllowed({
|
|
1230
|
+
members: existing,
|
|
442
1231
|
subjectId,
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
permissions: payload.permissions,
|
|
1232
|
+
callerSubjectId: grant.subjectId,
|
|
1233
|
+
nextPermissions: payload.permissions,
|
|
446
1234
|
});
|
|
447
|
-
|
|
1235
|
+
try {
|
|
1236
|
+
await upsertWorkspaceMemberAsWorkspaceManager(deps.db, {
|
|
1237
|
+
accountId: grant.accountId,
|
|
1238
|
+
workspaceId,
|
|
1239
|
+
actorSubjectId: grant.subjectId,
|
|
1240
|
+
targetSubjectId: subjectId,
|
|
1241
|
+
mode: "update",
|
|
1242
|
+
...(current.subjectLabel ? { subjectLabel: current.subjectLabel } : {}),
|
|
1243
|
+
role: payload.role ?? current.role,
|
|
1244
|
+
permissions: payload.permissions,
|
|
1245
|
+
});
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
rethrowWorkspaceMemberCandidateError(error);
|
|
1248
|
+
}
|
|
1249
|
+
const members = await listWorkspacePeople(deps, workspaceId);
|
|
448
1250
|
const member = members.find((candidate) => candidate.subjectId === subjectId);
|
|
449
1251
|
if (!member) {
|
|
450
1252
|
throw new HTTPException(500, { message: "failed to update member" });
|
|
@@ -458,12 +1260,85 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
458
1260
|
const subjectId = decodeURIComponent(c.req.param("subjectId"));
|
|
459
1261
|
const members = await listWorkspaceMembers(deps.db, workspaceId);
|
|
460
1262
|
// Never remove yourself, and never remove the last administering member.
|
|
461
|
-
|
|
462
|
-
|
|
1263
|
+
// The fenced removal command re-enforces both guards fail-closed.
|
|
1264
|
+
assertWorkspaceMemberRemovable({
|
|
1265
|
+
members,
|
|
1266
|
+
subjectId,
|
|
1267
|
+
callerSubjectId: grant.subjectId,
|
|
1268
|
+
});
|
|
1269
|
+
await removeWorkspaceMember(deps.db, {
|
|
1270
|
+
accountId: grant.accountId,
|
|
1271
|
+
workspaceId,
|
|
1272
|
+
actorSubjectId: grant.subjectId,
|
|
1273
|
+
targetSubjectId: subjectId,
|
|
1274
|
+
});
|
|
463
1275
|
return c.body(null, 204);
|
|
464
1276
|
});
|
|
465
1277
|
}
|
|
466
1278
|
|
|
1279
|
+
function workspaceLimit(deps: ApiRouteDeps): number | null {
|
|
1280
|
+
if (deps.settings.usageLimitsMode !== "static" && deps.settings.usageLimitsMode !== "managed") {
|
|
1281
|
+
return null;
|
|
1282
|
+
}
|
|
1283
|
+
return configuredStaticUsageLimits(deps.settings).maxWorkspacesPerAccount ?? null;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
async function listWorkspacePeople(
|
|
1287
|
+
deps: ApiRouteDeps,
|
|
1288
|
+
workspaceId: string,
|
|
1289
|
+
): Promise<WorkspaceMemberValue[]> {
|
|
1290
|
+
const members = await listWorkspaceMembers(deps.db, workspaceId);
|
|
1291
|
+
const userIds = members.flatMap((member) =>
|
|
1292
|
+
member.subjectId.startsWith("user:") ? [member.subjectId.slice("user:".length)] : [],
|
|
1293
|
+
);
|
|
1294
|
+
const profiles = await getManagedUserProfilesByIds(deps.db, userIds);
|
|
1295
|
+
const profileBySubject = new Map(
|
|
1296
|
+
profiles.map((profile) => [`user:${profile.id}`, profile.name?.trim() || profile.email]),
|
|
1297
|
+
);
|
|
1298
|
+
return members.map((member) => ({
|
|
1299
|
+
...member,
|
|
1300
|
+
subjectLabel: profileBySubject.get(member.subjectId) ?? member.subjectLabel,
|
|
1301
|
+
}));
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function rethrowWorkspaceMemberCandidateError(error: unknown): never {
|
|
1305
|
+
const managementCode =
|
|
1306
|
+
error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
1307
|
+
if (managementCode === "WORKSPACE_MEMBER_ALREADY_EXISTS") {
|
|
1308
|
+
throw new HTTPException(409, {
|
|
1309
|
+
message: "this person already has access to the workspace",
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
if (managementCode === "WORKSPACE_MEMBER_NOT_FOUND") {
|
|
1313
|
+
throw new HTTPException(404, { message: "member not found" });
|
|
1314
|
+
}
|
|
1315
|
+
if (managementCode === "WORKSPACE_MEMBER_SELF_UPDATE") {
|
|
1316
|
+
throw new HTTPException(409, {
|
|
1317
|
+
message: "you cannot change your own workspace access",
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
if (managementCode === "WORKSPACE_MEMBER_LAST_ADMIN") {
|
|
1321
|
+
throw new HTTPException(409, {
|
|
1322
|
+
message: "the workspace must keep at least one administrator",
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
const sqlState = nestedPostgresSqlState(error);
|
|
1326
|
+
if (sqlState === "P0002") {
|
|
1327
|
+
throw new HTTPException(404, { message: "workspace not found" });
|
|
1328
|
+
}
|
|
1329
|
+
if (sqlState === "42501") {
|
|
1330
|
+
throw new HTTPException(403, {
|
|
1331
|
+
message: "workspace member management is not allowed",
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
if (sqlState === "54000") {
|
|
1335
|
+
throw new HTTPException(409, {
|
|
1336
|
+
message: "this organization has too many members to show here",
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
throw error;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
467
1342
|
// A persona override that is null or trims to empty collapses to null (use the
|
|
468
1343
|
// deployment default). Otherwise the template is stored verbatim so the runtime
|
|
469
1344
|
// can substitute the non-bypassable CORE at its {{core}} marker.
|
|
@@ -485,6 +1360,8 @@ function requireAccountPermission(
|
|
|
485
1360
|
!grant ||
|
|
486
1361
|
(!grant.permissions.includes(permission) && !grant.permissions.includes("account:admin"))
|
|
487
1362
|
) {
|
|
488
|
-
throw new HTTPException(403, {
|
|
1363
|
+
throw new HTTPException(403, {
|
|
1364
|
+
message: `missing permission: ${permission}`,
|
|
1365
|
+
});
|
|
489
1366
|
}
|
|
490
1367
|
}
|