@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
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { Context, MiddlewareHandler } from "hono";
|
|
2
|
+
import { registeredHandlerRoutePath } from "./registered-route-label";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A public `/v1` route whose current behaviour is scheduled for removal.
|
|
6
|
+
*
|
|
7
|
+
* See docs/design/api-compatibility-policy.md: a breaking change to the public
|
|
8
|
+
* surface is announced, advertised on every affected response with the
|
|
9
|
+
* standard `Deprecation` (RFC 9745) and `Sunset` (RFC 8594) headers plus a
|
|
10
|
+
* `Link: rel="deprecation"` to the migration notes, and the old behaviour is
|
|
11
|
+
* kept for at least 90 days on the managed service or until the next major,
|
|
12
|
+
* whichever is later. Published SDKs surface these headers to the integrator.
|
|
13
|
+
*/
|
|
14
|
+
export type RouteDeprecation = {
|
|
15
|
+
/** Upper-case HTTP method. */
|
|
16
|
+
method: string;
|
|
17
|
+
/** The exact registered Hono template, e.g. `/v1/workspaces/:workspaceId/packs`. */
|
|
18
|
+
path: string;
|
|
19
|
+
/** When the deprecation was announced (ISO 8601). */
|
|
20
|
+
deprecatedAt: string;
|
|
21
|
+
/** Earliest removal on the managed service (ISO 8601). */
|
|
22
|
+
sunset: string;
|
|
23
|
+
/** Public migration notes (changelog/docs URL). */
|
|
24
|
+
link: string;
|
|
25
|
+
/** Optional successor route or document. */
|
|
26
|
+
successor?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Every currently deprecated public route. Adding an entry here is the
|
|
31
|
+
* server-side half of a deprecation; the breaking change itself still needs an
|
|
32
|
+
* entry in scripts/public-api-breaking-changes.json and a new major.
|
|
33
|
+
*/
|
|
34
|
+
export const DEPRECATED_ROUTES: readonly RouteDeprecation[] = [];
|
|
35
|
+
|
|
36
|
+
const MINIMUM_NOTICE_MS = 90 * 24 * 60 * 60 * 1000;
|
|
37
|
+
|
|
38
|
+
function isoMillis(value: string, field: string, deprecation: RouteDeprecation): number {
|
|
39
|
+
const millis = Date.parse(value);
|
|
40
|
+
if (Number.isNaN(millis)) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`deprecation ${deprecation.method} ${deprecation.path}: ${field} is not an ISO date`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return millis;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Validate one registry entry; the policy's 90-day minimum is enforced here. */
|
|
49
|
+
export function assertValidRouteDeprecation(deprecation: RouteDeprecation): void {
|
|
50
|
+
if (!/^(GET|POST|PUT|PATCH|DELETE)$/.test(deprecation.method)) {
|
|
51
|
+
throw new Error(`deprecation method must be an upper-case HTTP verb: ${deprecation.method}`);
|
|
52
|
+
}
|
|
53
|
+
if (!deprecation.path.startsWith("/v1/")) {
|
|
54
|
+
throw new Error(`deprecation path must be a /v1 route template: ${deprecation.path}`);
|
|
55
|
+
}
|
|
56
|
+
const deprecatedAt = isoMillis(deprecation.deprecatedAt, "deprecatedAt", deprecation);
|
|
57
|
+
const sunset = isoMillis(deprecation.sunset, "sunset", deprecation);
|
|
58
|
+
if (sunset - deprecatedAt < MINIMUM_NOTICE_MS) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`deprecation ${deprecation.method} ${deprecation.path}: sunset must be at least 90 days after deprecatedAt`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (!/^https:\/\//.test(deprecation.link)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`deprecation ${deprecation.method} ${deprecation.path}: link must be an https URL`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The response headers that advertise one deprecation. */
|
|
71
|
+
export function deprecationHeaders(deprecation: RouteDeprecation): Record<string, string> {
|
|
72
|
+
const deprecatedAt = Math.floor(Date.parse(deprecation.deprecatedAt) / 1000);
|
|
73
|
+
const links = [`<${deprecation.link}>; rel="deprecation"; type="text/html"`];
|
|
74
|
+
if (deprecation.successor) links.push(`<${deprecation.successor}>; rel="successor-version"`);
|
|
75
|
+
return {
|
|
76
|
+
// RFC 9745: a structured-field date, `@<unix seconds>`.
|
|
77
|
+
Deprecation: `@${deprecatedAt}`,
|
|
78
|
+
// RFC 8594: an HTTP-date.
|
|
79
|
+
Sunset: new Date(deprecation.sunset).toUTCString(),
|
|
80
|
+
Link: links.join(", "),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function appendHeader(context: Context, name: string, value: string): void {
|
|
85
|
+
const existing = context.res.headers.get(name);
|
|
86
|
+
context.res.headers.set(name, existing ? `${existing}, ${value}` : value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Stamp deprecation headers on every response from a registered deprecated
|
|
91
|
+
* route, including error responses, so an integrator sees the notice whatever
|
|
92
|
+
* the call's outcome. Matching uses the registered route template, never
|
|
93
|
+
* request data.
|
|
94
|
+
*/
|
|
95
|
+
export function deprecationHeadersMiddleware(
|
|
96
|
+
registry: readonly RouteDeprecation[] = DEPRECATED_ROUTES,
|
|
97
|
+
): MiddlewareHandler {
|
|
98
|
+
for (const deprecation of registry) assertValidRouteDeprecation(deprecation);
|
|
99
|
+
const byRoute = new Map(registry.map((entry) => [`${entry.method} ${entry.path}`, entry]));
|
|
100
|
+
return async (context, next) => {
|
|
101
|
+
await next();
|
|
102
|
+
if (byRoute.size === 0) return;
|
|
103
|
+
const path = registeredHandlerRoutePath(context);
|
|
104
|
+
if (!path) return;
|
|
105
|
+
const deprecation = byRoute.get(`${context.req.method} ${path}`);
|
|
106
|
+
if (!deprecation) return;
|
|
107
|
+
const headers = deprecationHeaders(deprecation);
|
|
108
|
+
try {
|
|
109
|
+
context.res.headers.set("Deprecation", headers.Deprecation!);
|
|
110
|
+
} catch {
|
|
111
|
+
// Immutable headers (a proxied or redirect response): copy once.
|
|
112
|
+
context.res = new Response(context.res.body, context.res);
|
|
113
|
+
context.res.headers.set("Deprecation", headers.Deprecation!);
|
|
114
|
+
}
|
|
115
|
+
context.res.headers.set("Sunset", headers.Sunset!);
|
|
116
|
+
appendHeader(context, "Link", headers.Link!);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { ErrorCode as AgentErrorCode } from "@opengeni/agent-proto";
|
|
2
|
+
import {
|
|
3
|
+
InteractionControlFailureDetails,
|
|
4
|
+
type ErrorCode,
|
|
5
|
+
type InteractionControlFailureCode,
|
|
6
|
+
} from "@opengeni/contracts";
|
|
7
|
+
import { SelfhostedControlError } from "@opengeni/runtime/sandbox";
|
|
8
|
+
import { HTTPException } from "hono/http-exception";
|
|
9
|
+
import { ApiHttpError } from "./api-error";
|
|
10
|
+
|
|
11
|
+
export type InteractionControlSurface = "browser" | "computer";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Projects an internal connected-machine control fault into the public API
|
|
15
|
+
* vocabulary. This is deliberately an allowlist: the agent's free-form message
|
|
16
|
+
* and detail map may contain paths, process output, or provider diagnostics and
|
|
17
|
+
* never cross this boundary. The exact inner request id is safe opaque
|
|
18
|
+
* correlation and links API logs back to the machine operation.
|
|
19
|
+
*/
|
|
20
|
+
export function interactionControlApiError(
|
|
21
|
+
error: unknown,
|
|
22
|
+
surface: InteractionControlSurface,
|
|
23
|
+
): ApiHttpError | null {
|
|
24
|
+
const controlError =
|
|
25
|
+
error instanceof SelfhostedControlError
|
|
26
|
+
? error
|
|
27
|
+
: error instanceof HTTPException && error.cause instanceof SelfhostedControlError
|
|
28
|
+
? error.cause
|
|
29
|
+
: null;
|
|
30
|
+
if (!controlError) return null;
|
|
31
|
+
const controlFailureCode = publicControlFailureCode(controlError.code);
|
|
32
|
+
const details = InteractionControlFailureDetails.parse({
|
|
33
|
+
interactionLayer: "connected_machine",
|
|
34
|
+
interactionSurface: surface,
|
|
35
|
+
controlFailureCode,
|
|
36
|
+
...(safeControlRequestId(controlError.controlRequestId)
|
|
37
|
+
? { controlRequestId: controlError.controlRequestId }
|
|
38
|
+
: {}),
|
|
39
|
+
});
|
|
40
|
+
const ruling = publicControlFailureRuling(controlError, surface, controlFailureCode);
|
|
41
|
+
return new ApiHttpError(ruling.status, {
|
|
42
|
+
code: ruling.code,
|
|
43
|
+
message: ruling.message,
|
|
44
|
+
retryable: controlError.retryable,
|
|
45
|
+
outcomeUnknown:
|
|
46
|
+
controlError.code === AgentErrorCode.ERROR_CODE_TIMEOUT && !controlError.neverSent,
|
|
47
|
+
details,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function publicControlFailureCode(code: AgentErrorCode): InteractionControlFailureCode {
|
|
52
|
+
switch (code) {
|
|
53
|
+
case AgentErrorCode.ERROR_CODE_UNSUPPORTED:
|
|
54
|
+
return "unsupported";
|
|
55
|
+
case AgentErrorCode.ERROR_CODE_OS:
|
|
56
|
+
return "os";
|
|
57
|
+
case AgentErrorCode.ERROR_CODE_NOT_FOUND:
|
|
58
|
+
return "not_found";
|
|
59
|
+
case AgentErrorCode.ERROR_CODE_CONSENT_REQUIRED:
|
|
60
|
+
return "consent_required";
|
|
61
|
+
case AgentErrorCode.ERROR_CODE_TIMEOUT:
|
|
62
|
+
return "timeout";
|
|
63
|
+
case AgentErrorCode.ERROR_CODE_DRAINING:
|
|
64
|
+
return "draining";
|
|
65
|
+
case AgentErrorCode.ERROR_CODE_PROTOCOL:
|
|
66
|
+
return "protocol";
|
|
67
|
+
case AgentErrorCode.ERROR_CODE_STREAM:
|
|
68
|
+
return "stream";
|
|
69
|
+
case AgentErrorCode.ERROR_CODE_AGENT_OFFLINE:
|
|
70
|
+
return "agent_offline";
|
|
71
|
+
case AgentErrorCode.ERROR_CODE_FENCED:
|
|
72
|
+
return "fenced";
|
|
73
|
+
case AgentErrorCode.ERROR_CODE_PAYLOAD_TOO_LARGE:
|
|
74
|
+
return "payload_too_large";
|
|
75
|
+
default:
|
|
76
|
+
return "unknown";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function publicControlFailureRuling(
|
|
81
|
+
error: SelfhostedControlError,
|
|
82
|
+
surface: InteractionControlSurface,
|
|
83
|
+
code: InteractionControlFailureCode,
|
|
84
|
+
): { status: number; code: ErrorCode; message: string } {
|
|
85
|
+
const liveView = surface === "browser" ? "browser live view" : "computer live view";
|
|
86
|
+
switch (code) {
|
|
87
|
+
case "consent_required":
|
|
88
|
+
return {
|
|
89
|
+
status: 403,
|
|
90
|
+
code: "forbidden",
|
|
91
|
+
message: `Screen access is not enabled for this connected machine's ${liveView}.`,
|
|
92
|
+
};
|
|
93
|
+
case "unsupported":
|
|
94
|
+
return {
|
|
95
|
+
status: 409,
|
|
96
|
+
code: "conflict",
|
|
97
|
+
message: `This connected machine does not support the ${liveView}.`,
|
|
98
|
+
};
|
|
99
|
+
case "not_found":
|
|
100
|
+
return {
|
|
101
|
+
status: 404,
|
|
102
|
+
code: "not_found",
|
|
103
|
+
message: `The connected-machine resource for this ${liveView} no longer exists.`,
|
|
104
|
+
};
|
|
105
|
+
case "fenced":
|
|
106
|
+
return {
|
|
107
|
+
status: 409,
|
|
108
|
+
code: "conflict",
|
|
109
|
+
message: `The connected-machine ${liveView} changed while it was opening.`,
|
|
110
|
+
};
|
|
111
|
+
case "agent_offline":
|
|
112
|
+
return {
|
|
113
|
+
status: 503,
|
|
114
|
+
code: "upstream_unavailable",
|
|
115
|
+
message: `The connected machine is offline, so its ${liveView} is unavailable.`,
|
|
116
|
+
};
|
|
117
|
+
case "draining":
|
|
118
|
+
return {
|
|
119
|
+
status: 503,
|
|
120
|
+
code: "upstream_unavailable",
|
|
121
|
+
message: `The connected machine is busy and could not open the ${liveView}.`,
|
|
122
|
+
};
|
|
123
|
+
case "timeout":
|
|
124
|
+
return {
|
|
125
|
+
status: 504,
|
|
126
|
+
code: "upstream_unavailable",
|
|
127
|
+
message: `The connected machine did not finish opening the ${liveView} in time.`,
|
|
128
|
+
};
|
|
129
|
+
case "payload_too_large":
|
|
130
|
+
return {
|
|
131
|
+
status: 502,
|
|
132
|
+
code: "upstream_unavailable",
|
|
133
|
+
message: `The connected machine could not deliver the ${liveView} within its transport limit.`,
|
|
134
|
+
};
|
|
135
|
+
case "stream":
|
|
136
|
+
return {
|
|
137
|
+
status: error.retryable ? 503 : 502,
|
|
138
|
+
code: "upstream_unavailable",
|
|
139
|
+
message: `The connected machine could not open the ${liveView} stream.`,
|
|
140
|
+
};
|
|
141
|
+
case "protocol":
|
|
142
|
+
return {
|
|
143
|
+
status: 502,
|
|
144
|
+
code: "upstream_unavailable",
|
|
145
|
+
message: `The connected machine returned an invalid ${liveView} response.`,
|
|
146
|
+
};
|
|
147
|
+
case "os":
|
|
148
|
+
return {
|
|
149
|
+
status: 502,
|
|
150
|
+
code: "upstream_unavailable",
|
|
151
|
+
message: `The connected machine could not prepare the ${liveView}.`,
|
|
152
|
+
};
|
|
153
|
+
case "unknown":
|
|
154
|
+
return {
|
|
155
|
+
status: 502,
|
|
156
|
+
code: "upstream_unavailable",
|
|
157
|
+
message: `The connected machine could not open the ${liveView}.`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function safeControlRequestId(value: string | null): value is string {
|
|
163
|
+
return value !== null && value.length <= 128 && /^[A-Za-z0-9._:-]+$/u.test(value);
|
|
164
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One token bucket per closed key, for anonymous beacon routes. It bounds
|
|
3
|
+
* counter inflation and log volume from any caller without per-client state.
|
|
4
|
+
*/
|
|
5
|
+
export type KeyedAdmission<Key extends string> = { admit(key: Key): boolean };
|
|
6
|
+
|
|
7
|
+
export function createKeyedAdmission<Key extends string>(
|
|
8
|
+
options: { capacity?: number; refillPerSecond?: number; now?: () => number } = {},
|
|
9
|
+
): KeyedAdmission<Key> {
|
|
10
|
+
const capacity = options.capacity ?? 30;
|
|
11
|
+
const refillPerMs = (options.refillPerSecond ?? 0.5) / 1_000;
|
|
12
|
+
const now = options.now ?? Date.now;
|
|
13
|
+
const buckets = new Map<Key, { tokens: number; updatedAt: number }>();
|
|
14
|
+
return {
|
|
15
|
+
admit(key) {
|
|
16
|
+
const at = now();
|
|
17
|
+
const bucket = buckets.get(key) ?? { tokens: capacity, updatedAt: at };
|
|
18
|
+
bucket.tokens = Math.min(
|
|
19
|
+
capacity,
|
|
20
|
+
bucket.tokens + Math.max(0, at - bucket.updatedAt) * refillPerMs,
|
|
21
|
+
);
|
|
22
|
+
bucket.updatedAt = at;
|
|
23
|
+
buckets.set(key, bucket);
|
|
24
|
+
if (bucket.tokens < 1) return false;
|
|
25
|
+
bucket.tokens -= 1;
|
|
26
|
+
return true;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import {
|
|
2
|
+
localAllowedOriginEntries,
|
|
3
|
+
localSandboxApiRouteAllowed,
|
|
4
|
+
type Settings,
|
|
5
|
+
} from "@opengeni/config";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Browser boundary for the unauthenticated local product mode.
|
|
9
|
+
*
|
|
10
|
+
* In `local` access mode every request without credentials acts as the local
|
|
11
|
+
* `dev` user, and the default local sandbox runs agent commands on the host.
|
|
12
|
+
* Binding the API to loopback keeps other devices out, but a web page open in
|
|
13
|
+
* the developer's browser can still reach `http://127.0.0.1:<port>`:
|
|
14
|
+
*
|
|
15
|
+
* - any site can send cross-origin requests to it (CORS only hides the
|
|
16
|
+
* response; a simple POST still runs), and
|
|
17
|
+
* - a DNS-rebinding site can make its own hostname resolve to 127.0.0.1 and
|
|
18
|
+
* read responses as a same-origin page.
|
|
19
|
+
*
|
|
20
|
+
* The boundary therefore sorts the `Host` a request is addressed to into two
|
|
21
|
+
* classes:
|
|
22
|
+
*
|
|
23
|
+
* - Browser hosts: loopback, the hosts of the configured web, public, and
|
|
24
|
+
* extra origins, the GitHub App callback tunnel, and a specific API bind
|
|
25
|
+
* address. A present `Origin` must be this stack's web app, an explicitly
|
|
26
|
+
* configured origin, or the API's own address on one of these hosts.
|
|
27
|
+
* - Sandbox hosts: names only sandboxes and the worker use to reach the API
|
|
28
|
+
* (`host.docker.internal` with the Docker sandbox, and the hosts of
|
|
29
|
+
* `OPENGENI_MCP_URL` and `OPENGENI_MCP_INTERNAL_URL`). They serve only the
|
|
30
|
+
* sandbox routes (Codemode, first-party MCP, and the Git broker) and refuse
|
|
31
|
+
* any request that carries browser metadata. A browser cannot tell a
|
|
32
|
+
* sandbox name from an attacker's, so a rebinding page on one of these
|
|
33
|
+
* names can neither send an accepted `Origin` nor reach the rest of the API.
|
|
34
|
+
*
|
|
35
|
+
* Every other `Host` is refused. Non-browser clients (the SDK, curl, sandbox
|
|
36
|
+
* callbacks, host-app servers) send no `Origin` and are unaffected. The `Host`
|
|
37
|
+
* port is not compared: a rebinding page controls only the hostname, while
|
|
38
|
+
* local proxies and forwarders legitimately present their own port.
|
|
39
|
+
*
|
|
40
|
+
* It applies only to `local` access mode in the `local` environment (the
|
|
41
|
+
* `bun run dev` stack and manual local runs). Managed and configured access
|
|
42
|
+
* modes, and deployments that set another `OPENGENI_ENVIRONMENT`, keep their
|
|
43
|
+
* existing CORS policy.
|
|
44
|
+
*/
|
|
45
|
+
export type LocalBrowserBoundarySettings = Pick<
|
|
46
|
+
Settings,
|
|
47
|
+
| "productAccessMode"
|
|
48
|
+
| "environment"
|
|
49
|
+
| "apiHost"
|
|
50
|
+
| "webBaseUrl"
|
|
51
|
+
| "publicBaseUrl"
|
|
52
|
+
| "opengeniMcpUrl"
|
|
53
|
+
| "opengeniMcpInternalUrl"
|
|
54
|
+
| "githubAppManifestBaseUrl"
|
|
55
|
+
| "localAllowedOrigins"
|
|
56
|
+
| "sandboxBackend"
|
|
57
|
+
>;
|
|
58
|
+
|
|
59
|
+
export type LocalBrowserBoundaryRejectionCode =
|
|
60
|
+
| "LOCAL_HOST_NOT_ALLOWED"
|
|
61
|
+
| "LOCAL_ORIGIN_NOT_ALLOWED"
|
|
62
|
+
| "LOCAL_SANDBOX_ROUTE_ONLY";
|
|
63
|
+
|
|
64
|
+
export type LocalBrowserBoundaryRejection = {
|
|
65
|
+
status: 403;
|
|
66
|
+
code: LocalBrowserBoundaryRejectionCode;
|
|
67
|
+
message: string;
|
|
68
|
+
/** The refused `Host` or `Origin`, for the API log only; never sent back. */
|
|
69
|
+
refused: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Called once per distinct refused value, so an operator can see why. */
|
|
73
|
+
export type LocalBrowserBoundaryWarn = (
|
|
74
|
+
message: string,
|
|
75
|
+
attributes: Record<string, string>,
|
|
76
|
+
) => void;
|
|
77
|
+
|
|
78
|
+
export type LocalBrowserBoundary = {
|
|
79
|
+
readonly allowedOrigins: ReadonlySet<string>;
|
|
80
|
+
/** Hostnames a browser may address. */
|
|
81
|
+
readonly browserHostnames: ReadonlySet<string>;
|
|
82
|
+
/** Hostnames only sandboxes and the worker use; sandbox routes only, no browsers. */
|
|
83
|
+
readonly sandboxHostnames: ReadonlySet<string>;
|
|
84
|
+
/** Whether a present `Origin` may call the API for a request with this `Host`. */
|
|
85
|
+
originAllowed(origin: string, host: string | null): boolean;
|
|
86
|
+
/** The rejection for this request, or null when the boundary admits it. */
|
|
87
|
+
rejection(request: Request): LocalBrowserBoundaryRejection | null;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const LOOPBACK_HOSTNAMES = ["127.0.0.1", "localhost", "[::1]"] as const;
|
|
91
|
+
/** Docker Desktop's name for the host; Linux sandboxes use the configured bridge route. */
|
|
92
|
+
const DOCKER_HOST_ALIAS = "host.docker.internal";
|
|
93
|
+
/** `apps/web` dev server default when no web base URL is configured. */
|
|
94
|
+
const DEFAULT_LOCAL_WEB_ORIGIN = "http://127.0.0.1:3000";
|
|
95
|
+
const WILDCARD_BIND_HOSTS = new Set(["", "0.0.0.0", "::", "[::]"]);
|
|
96
|
+
/**
|
|
97
|
+
* Only browsers send these. (Node's fetch sends `Sec-Fetch-Mode: cors`, so that
|
|
98
|
+
* header does not identify a browser.)
|
|
99
|
+
*/
|
|
100
|
+
const BROWSER_REQUEST_HEADERS = ["origin", "sec-fetch-site"];
|
|
101
|
+
/** Distinct refused values logged per boundary, so a rebinding page cannot flood the log. */
|
|
102
|
+
const MAX_REPORTED_REJECTIONS = 32;
|
|
103
|
+
const MAX_REPORTED_VALUE_LENGTH = 200;
|
|
104
|
+
|
|
105
|
+
const HOST_NOT_ALLOWED_MESSAGE =
|
|
106
|
+
"The local Opengeni API only answers requests addressed to this computer. " +
|
|
107
|
+
"To use another address, set it in OPENGENI_WEB_BASE_URL, OPENGENI_PUBLIC_BASE_URL, " +
|
|
108
|
+
"or OPENGENI_LOCAL_ALLOWED_ORIGINS.";
|
|
109
|
+
const ORIGIN_NOT_ALLOWED_MESSAGE =
|
|
110
|
+
"The local Opengeni API has no authentication, so browser requests are accepted only " +
|
|
111
|
+
"from this stack's web app. Add other trusted origins to OPENGENI_LOCAL_ALLOWED_ORIGINS.";
|
|
112
|
+
const SANDBOX_ROUTE_ONLY_MESSAGE =
|
|
113
|
+
"This address is the local Opengeni API's sandbox route. It serves only sandbox calls " +
|
|
114
|
+
"(Codemode, first-party MCP, and the Git broker), never browsers. Open the web app at " +
|
|
115
|
+
"its own address, or list this address in OPENGENI_LOCAL_ALLOWED_ORIGINS.";
|
|
116
|
+
|
|
117
|
+
const WARNINGS: Record<LocalBrowserBoundaryRejectionCode, { message: string; setting: string }> = {
|
|
118
|
+
LOCAL_HOST_NOT_ALLOWED: {
|
|
119
|
+
message: "Local API refused a request addressed to a name that is not this computer's",
|
|
120
|
+
setting: "OPENGENI_WEB_BASE_URL, OPENGENI_PUBLIC_BASE_URL, or OPENGENI_LOCAL_ALLOWED_ORIGINS",
|
|
121
|
+
},
|
|
122
|
+
LOCAL_ORIGIN_NOT_ALLOWED: {
|
|
123
|
+
message: "Local API refused a browser request from an origin that is not this stack's web app",
|
|
124
|
+
setting: "OPENGENI_WEB_BASE_URL or OPENGENI_LOCAL_ALLOWED_ORIGINS",
|
|
125
|
+
},
|
|
126
|
+
LOCAL_SANDBOX_ROUTE_ONLY: {
|
|
127
|
+
message: "Local API refused a browser or non-sandbox request on an address only sandboxes use",
|
|
128
|
+
setting: "OPENGENI_WEB_BASE_URL or OPENGENI_LOCAL_ALLOWED_ORIGINS",
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Requests the API builds and dispatches to itself, such as the Codemode SDK
|
|
134
|
+
* proxy's re-dispatch. They never come from the network, and the request they
|
|
135
|
+
* were built from already passed the boundary.
|
|
136
|
+
*/
|
|
137
|
+
const internalDispatches = new WeakSet<Request>();
|
|
138
|
+
|
|
139
|
+
/** Mark a request the API dispatches to itself so the boundary admits it. */
|
|
140
|
+
export function markLocalInternalDispatch(request: Request): Request {
|
|
141
|
+
internalDispatches.add(request);
|
|
142
|
+
return request;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function localBrowserBoundaryApplies(
|
|
146
|
+
settings: Pick<Settings, "productAccessMode" | "environment">,
|
|
147
|
+
): boolean {
|
|
148
|
+
return settings.productAccessMode === "local" && settings.environment === "local";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Build the boundary, or null when the deployment is not the local dev product. */
|
|
152
|
+
export function createLocalBrowserBoundary(
|
|
153
|
+
settings: LocalBrowserBoundarySettings,
|
|
154
|
+
options: { warn?: LocalBrowserBoundaryWarn } = {},
|
|
155
|
+
): LocalBrowserBoundary | null {
|
|
156
|
+
if (!localBrowserBoundaryApplies(settings)) return null;
|
|
157
|
+
|
|
158
|
+
const webOrigins = [settings.webBaseUrl ?? DEFAULT_LOCAL_WEB_ORIGIN, settings.publicBaseUrl]
|
|
159
|
+
.map((value) => parseHttpUrl(value))
|
|
160
|
+
.filter((url): url is URL => url !== null);
|
|
161
|
+
const extraOrigins = localAllowedOriginEntries(settings.localAllowedOrigins).map(
|
|
162
|
+
(origin) => new URL(origin),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const allowedOrigins = new Set<string>();
|
|
166
|
+
for (const url of [...webOrigins, ...extraOrigins]) {
|
|
167
|
+
allowedOrigins.add(url.origin);
|
|
168
|
+
// 127.0.0.1, localhost, and [::1] are the same machine and port; the
|
|
169
|
+
// developer may open the printed URL under any of those names.
|
|
170
|
+
if (isLoopbackHostname(url.hostname)) {
|
|
171
|
+
for (const hostname of LOOPBACK_HOSTNAMES) {
|
|
172
|
+
const alias = new URL(url.origin);
|
|
173
|
+
alias.hostname = hostname;
|
|
174
|
+
allowedOrigins.add(alias.origin);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const browserHostnames = new Set<string>(LOOPBACK_HOSTNAMES);
|
|
180
|
+
for (const url of [...webOrigins, ...extraOrigins]) browserHostnames.add(url.hostname);
|
|
181
|
+
// A tunnel configured for GitHub App callbacks: GitHub redirects the browser there.
|
|
182
|
+
const githubAppTunnel = parseHttpUrl(settings.githubAppManifestBaseUrl);
|
|
183
|
+
if (githubAppTunnel) browserHostnames.add(githubAppTunnel.hostname);
|
|
184
|
+
const apiHost = settings.apiHost.trim().toLowerCase();
|
|
185
|
+
if (!WILDCARD_BIND_HOSTS.has(apiHost)) {
|
|
186
|
+
const url = parseHostHeader(
|
|
187
|
+
apiHost.includes(":") && !apiHost.startsWith("[") ? `[${apiHost}]` : apiHost,
|
|
188
|
+
);
|
|
189
|
+
if (url) browserHostnames.add(url.hostname);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// The sandbox-visible API route (Docker Desktop's host alias, the Linux
|
|
193
|
+
// Docker bridge address, or a tunnel for a remote sandbox) and the worker's
|
|
194
|
+
// internal route arrive with their own Host.
|
|
195
|
+
const sandboxHostnames = new Set<string>();
|
|
196
|
+
if (settings.sandboxBackend === "docker") sandboxHostnames.add(DOCKER_HOST_ALIAS);
|
|
197
|
+
for (const value of [settings.opengeniMcpUrl, settings.opengeniMcpInternalUrl]) {
|
|
198
|
+
const url = parseHttpUrl(value?.replaceAll("{workspaceId}", "workspace"));
|
|
199
|
+
if (url) sandboxHostnames.add(url.hostname);
|
|
200
|
+
}
|
|
201
|
+
// A name the developer also uses in the browser stays a browser host.
|
|
202
|
+
for (const hostname of browserHostnames) sandboxHostnames.delete(hostname);
|
|
203
|
+
|
|
204
|
+
const browserHost = (host: string | null): URL | null => {
|
|
205
|
+
const url = host === null ? null : parseHostHeader(host);
|
|
206
|
+
return url !== null && browserHostnames.has(url.hostname) ? url : null;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const originAllowed = (origin: string, host: string | null): boolean => {
|
|
210
|
+
const url = parseHttpUrl(origin);
|
|
211
|
+
if (!url || url.origin !== origin.toLowerCase().replace(/\/$/u, "")) return false;
|
|
212
|
+
if (allowedOrigins.has(url.origin)) return true;
|
|
213
|
+
// Pages served by the API itself (for example the MCP OAuth consent form)
|
|
214
|
+
// and same-origin dev proxies present the API's own address. Only a
|
|
215
|
+
// browser host qualifies: sandbox names and unknown names never do.
|
|
216
|
+
const requestHost = browserHost(host);
|
|
217
|
+
return requestHost !== null && requestHost.host === url.host;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const reported = new Set<string>();
|
|
221
|
+
const report = (rejection: LocalBrowserBoundaryRejection, host: string | null) => {
|
|
222
|
+
if (!options.warn) return;
|
|
223
|
+
const key = `${rejection.code}\u0000${rejection.refused}`;
|
|
224
|
+
if (reported.has(key) || reported.size >= MAX_REPORTED_REJECTIONS) return;
|
|
225
|
+
reported.add(key);
|
|
226
|
+
const warning = WARNINGS[rejection.code];
|
|
227
|
+
options.warn(warning.message, {
|
|
228
|
+
code: rejection.code,
|
|
229
|
+
refused: rejection.refused.slice(0, MAX_REPORTED_VALUE_LENGTH),
|
|
230
|
+
host: (host ?? "").slice(0, MAX_REPORTED_VALUE_LENGTH),
|
|
231
|
+
setting: warning.setting,
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const refuse = (
|
|
236
|
+
code: LocalBrowserBoundaryRejectionCode,
|
|
237
|
+
refused: string,
|
|
238
|
+
host: string | null,
|
|
239
|
+
): LocalBrowserBoundaryRejection => {
|
|
240
|
+
const message =
|
|
241
|
+
code === "LOCAL_HOST_NOT_ALLOWED"
|
|
242
|
+
? HOST_NOT_ALLOWED_MESSAGE
|
|
243
|
+
: code === "LOCAL_ORIGIN_NOT_ALLOWED"
|
|
244
|
+
? ORIGIN_NOT_ALLOWED_MESSAGE
|
|
245
|
+
: SANDBOX_ROUTE_ONLY_MESSAGE;
|
|
246
|
+
const rejection: LocalBrowserBoundaryRejection = { status: 403, code, message, refused };
|
|
247
|
+
report(rejection, host);
|
|
248
|
+
return rejection;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
allowedOrigins,
|
|
253
|
+
browserHostnames,
|
|
254
|
+
sandboxHostnames,
|
|
255
|
+
originAllowed,
|
|
256
|
+
rejection(request) {
|
|
257
|
+
if (internalDispatches.has(request)) return null;
|
|
258
|
+
const host = localBrowserRequestHost(request);
|
|
259
|
+
const origin = request.headers.get("origin");
|
|
260
|
+
if (browserHost(host)) {
|
|
261
|
+
if (origin !== null && !originAllowed(origin, host)) {
|
|
262
|
+
return refuse("LOCAL_ORIGIN_NOT_ALLOWED", origin, host);
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
const parsedHost = host === null ? null : parseHostHeader(host);
|
|
267
|
+
if (parsedHost === null || !sandboxHostnames.has(parsedHost.hostname)) {
|
|
268
|
+
return refuse("LOCAL_HOST_NOT_ALLOWED", host ?? "", host);
|
|
269
|
+
}
|
|
270
|
+
if (
|
|
271
|
+
BROWSER_REQUEST_HEADERS.some((name) => request.headers.has(name)) ||
|
|
272
|
+
!localSandboxApiRouteAllowed(requestPathname(request))
|
|
273
|
+
) {
|
|
274
|
+
return refuse("LOCAL_SANDBOX_ROUTE_ONLY", host ?? "", host);
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The standard API error envelope for a boundary rejection outside Hono. */
|
|
282
|
+
export function localBrowserBoundaryResponse(rejection: LocalBrowserBoundaryRejection): Response {
|
|
283
|
+
return Response.json(
|
|
284
|
+
{
|
|
285
|
+
error: {
|
|
286
|
+
status: rejection.status,
|
|
287
|
+
code: "forbidden",
|
|
288
|
+
message: rejection.message,
|
|
289
|
+
retryable: false,
|
|
290
|
+
details: { code: rejection.code },
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
{ status: rejection.status },
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** The `Host` the boundary checks: the header, else the request URL's authority. */
|
|
298
|
+
export function localBrowserRequestHost(request: Request): string | null {
|
|
299
|
+
const header = request.headers.get("host");
|
|
300
|
+
if (header !== null) return header;
|
|
301
|
+
try {
|
|
302
|
+
return new URL(request.url).host || null;
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** The normalized path the router sees (URL parsing resolves dot segments). */
|
|
309
|
+
function requestPathname(request: Request): string {
|
|
310
|
+
try {
|
|
311
|
+
return new URL(request.url).pathname;
|
|
312
|
+
} catch {
|
|
313
|
+
return "";
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function parseHostHeader(value: string): URL | null {
|
|
318
|
+
const host = value.trim();
|
|
319
|
+
// A Host is exactly `hostname[:port]`; anything else is not an address.
|
|
320
|
+
if (!host || /[\s/?#@\\,]/u.test(host)) return null;
|
|
321
|
+
try {
|
|
322
|
+
const url = new URL(`http://${host}`);
|
|
323
|
+
return url.host === host.toLowerCase() || url.host === host.toLowerCase().replace(/:80$/u, "")
|
|
324
|
+
? url
|
|
325
|
+
: null;
|
|
326
|
+
} catch {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function parseHttpUrl(value: string | undefined): URL | null {
|
|
332
|
+
if (!value) return null;
|
|
333
|
+
try {
|
|
334
|
+
const url = new URL(value);
|
|
335
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url : null;
|
|
336
|
+
} catch {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function isLoopbackHostname(hostname: string): boolean {
|
|
342
|
+
return (LOOPBACK_HOSTNAMES as readonly string[]).includes(hostname);
|
|
343
|
+
}
|