@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
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertEphemeralBrowserCreateEnabled,
|
|
3
|
+
ephemeralBrowserPartition,
|
|
4
|
+
} from "../browser-ephemeral";
|
|
1
5
|
import { randomUUID } from "node:crypto";
|
|
2
6
|
import {
|
|
3
7
|
environmentsEncryptionKeyBytes,
|
|
4
8
|
resolveFirstPartyDelegationSecret,
|
|
5
9
|
resolveStreamTokenSecret,
|
|
10
|
+
sandboxWarmRateMicrosPerSecond,
|
|
6
11
|
} from "@opengeni/config";
|
|
7
12
|
import {
|
|
8
13
|
BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX,
|
|
14
|
+
EPHEMERAL_CHROMIUM_DRIVER_ID,
|
|
15
|
+
browserSessionStorageMode,
|
|
9
16
|
BROWSER_CONTROL_WEBSOCKET_PROTOCOL,
|
|
10
17
|
BROWSER_CONTROL_PORT,
|
|
11
18
|
BROWSER_CONTROL_PROTOCOL_VERSION,
|
|
@@ -20,6 +27,8 @@ import {
|
|
|
20
27
|
BrowserDownloadSaveRequest,
|
|
21
28
|
BrowserDownloadSaveResponse,
|
|
22
29
|
BrowserDownloadListResponse,
|
|
30
|
+
BrowserDomReadRequest,
|
|
31
|
+
BrowserDomReadResponse,
|
|
23
32
|
BrowserExternalAuthCommand,
|
|
24
33
|
BrowserOpenTargetRequest,
|
|
25
34
|
BrowserObservation,
|
|
@@ -31,6 +40,7 @@ import {
|
|
|
31
40
|
BrowserSessionListResponse,
|
|
32
41
|
BrowserSessionMutationResponse,
|
|
33
42
|
BrowserTargetListResponse,
|
|
43
|
+
BrowserTargetState,
|
|
34
44
|
CreateBrowserSessionRequest,
|
|
35
45
|
ExternalAuthInteractiveRequest,
|
|
36
46
|
ExternalAuthInteractiveResponse,
|
|
@@ -59,6 +69,7 @@ import {
|
|
|
59
69
|
type SiteAuthAuthority,
|
|
60
70
|
} from "@opengeni/contracts";
|
|
61
71
|
import {
|
|
72
|
+
getSessionAuthorityEpoch,
|
|
62
73
|
acquireLease,
|
|
63
74
|
activateBrowserSession,
|
|
64
75
|
ATTACHED_BROWSER_SESSION_CAPABILITIES,
|
|
@@ -74,6 +85,7 @@ import {
|
|
|
74
85
|
BrowserSessionNotFoundError,
|
|
75
86
|
BrowserSessionOperationConflictError,
|
|
76
87
|
BrowserSessionStateError,
|
|
88
|
+
SandboxPaidComputeAdmissionError,
|
|
77
89
|
completeBrowserSessionEnd,
|
|
78
90
|
completeExternalAuth,
|
|
79
91
|
completeBrowserDownloadSave,
|
|
@@ -87,6 +99,8 @@ import {
|
|
|
87
99
|
dispatchExternalAuth,
|
|
88
100
|
dispatchProtectedAuthFill,
|
|
89
101
|
failBrowserSessionOperation,
|
|
102
|
+
failPreparedBrowserSessionEnd,
|
|
103
|
+
failPreparedBrowserSessionSuspend,
|
|
90
104
|
failBrowserSessionResume,
|
|
91
105
|
failBrowserSessionResumePreparation,
|
|
92
106
|
failBrowserSessionSuspension,
|
|
@@ -95,12 +109,13 @@ import {
|
|
|
95
109
|
findBrowserDownloadSave,
|
|
96
110
|
getBrowserSessionControlRecord,
|
|
97
111
|
getComputerSessionControlRecord,
|
|
112
|
+
getBrowserIdentity,
|
|
98
113
|
getBrowserPrivateCheckpointAuthority,
|
|
99
114
|
getAttachedBrowserDevice,
|
|
100
115
|
getBrowserRevisionArtifactAuthority,
|
|
101
|
-
|
|
116
|
+
getLiveEnrollmentConnection,
|
|
102
117
|
getExternalAuthInteractiveContext,
|
|
103
|
-
|
|
118
|
+
getFilesForSubject,
|
|
104
119
|
getFileUpload,
|
|
105
120
|
getAuthRun,
|
|
106
121
|
getExternalAuthPreparation,
|
|
@@ -114,6 +129,7 @@ import {
|
|
|
114
129
|
listBrowserSessions,
|
|
115
130
|
listAttachedBrowserDevices,
|
|
116
131
|
prepareBrowserSessionCreate,
|
|
132
|
+
markEphemeralBrowserSessionLost,
|
|
117
133
|
prepareExternalAuth,
|
|
118
134
|
prepareBrowserDownloadSave,
|
|
119
135
|
prepareBrowserSessionEnd,
|
|
@@ -131,6 +147,7 @@ import {
|
|
|
131
147
|
touchBrowserSessionController,
|
|
132
148
|
verifyAuthRun,
|
|
133
149
|
settleBrowserDownloadSaveFailure,
|
|
150
|
+
terminalizeStaleConnectedInteractionPlacement,
|
|
134
151
|
type BrowserSessionControlRecord,
|
|
135
152
|
type BrowserPrivateCheckpointAuthority,
|
|
136
153
|
type BrowserRevisionArtifactAuthority,
|
|
@@ -142,9 +159,11 @@ import {
|
|
|
142
159
|
recordWorkspaceUsage,
|
|
143
160
|
requireLimit,
|
|
144
161
|
relayConfigFromSettings,
|
|
162
|
+
resolveSessionSandboxRuntime,
|
|
145
163
|
SessionAuthorizationDeniedError,
|
|
146
164
|
SessionAuthorizationUnavailableError,
|
|
147
165
|
type ApiRouteDeps,
|
|
166
|
+
type ResolvedSessionAuthorization,
|
|
148
167
|
} from "@opengeni/core";
|
|
149
168
|
import {
|
|
150
169
|
BrowserControlProtocolError,
|
|
@@ -157,16 +176,21 @@ import {
|
|
|
157
176
|
buildStreamUrl,
|
|
158
177
|
exposedPortEndpointFromUrl,
|
|
159
178
|
buildSelfhostedBackendSession,
|
|
179
|
+
ensureDisplayStack,
|
|
160
180
|
mintStreamToken,
|
|
161
181
|
NatsControlRpc,
|
|
162
182
|
NatsOpStreamTransport,
|
|
163
183
|
provisionBrowserControlClient,
|
|
184
|
+
renewSandboxProviderExpiration,
|
|
164
185
|
type BrowserControlPlacementSession,
|
|
186
|
+
type BrowserControlScreenshotOptions,
|
|
187
|
+
type BrowserControlFrame,
|
|
165
188
|
type PlacementBrowserStateCaptureReceipt,
|
|
166
189
|
type PlacementBrowserNetworkRoute,
|
|
167
190
|
type PlacementBrowserTransport,
|
|
168
191
|
type RestorePlacementBrowserStateInput,
|
|
169
192
|
} from "@opengeni/runtime/sandbox";
|
|
193
|
+
import { retryWhileMissing } from "@opengeni/storage";
|
|
170
194
|
import type { Context, Hono } from "hono";
|
|
171
195
|
import { HTTPException } from "hono/http-exception";
|
|
172
196
|
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
|
@@ -175,8 +199,17 @@ import {
|
|
|
175
199
|
deriveBrowserNetworkRouteAuthorityDigest,
|
|
176
200
|
deriveBrowserSessionControllerTokens,
|
|
177
201
|
deriveBrowserViewGrantToken,
|
|
202
|
+
deriveComputerSessionControllerTokens,
|
|
178
203
|
} from "../browser-controller-authority";
|
|
179
|
-
import {
|
|
204
|
+
import {
|
|
205
|
+
controllerCacheAllowsHostFetch,
|
|
206
|
+
controllerCachedUrlIsUsable,
|
|
207
|
+
shouldPersistControllerDataPlaneUrl,
|
|
208
|
+
isRetryableControllerTransport,
|
|
209
|
+
withCachedController,
|
|
210
|
+
withControllerTransportRecovery,
|
|
211
|
+
} from "../controller-data-plane";
|
|
212
|
+
import { filterInteractionSessionsForGrant } from "../interaction-agent-access";
|
|
180
213
|
import { withInteractionHolderHeartbeat } from "../interaction-holder-heartbeat";
|
|
181
214
|
import {
|
|
182
215
|
browserStateArtifactAad,
|
|
@@ -191,7 +224,13 @@ import {
|
|
|
191
224
|
resolveProtectedAuthFieldValues,
|
|
192
225
|
} from "../browser-auth-broker";
|
|
193
226
|
import { managedNetworkRouteForPlacement } from "../browser-network-route";
|
|
194
|
-
import {
|
|
227
|
+
import { validateInteractionRequestOrigin } from "../http/cors";
|
|
228
|
+
import { ApiHttpError } from "../http/api-error";
|
|
229
|
+
import { interactionControlApiError } from "../http/interaction-control-error";
|
|
230
|
+
import {
|
|
231
|
+
createInteractionFrameProxyAttachment,
|
|
232
|
+
placementUsesInteractionFrameProxy,
|
|
233
|
+
} from "../interaction-frame-proxy";
|
|
195
234
|
import {
|
|
196
235
|
observeAuthMutation,
|
|
197
236
|
observeBrowserActionResult,
|
|
@@ -200,6 +239,7 @@ import {
|
|
|
200
239
|
} from "../interaction-metrics";
|
|
201
240
|
import { withChannelA, withChannelARead, type ChannelAOperation } from "../sandbox/channel-a";
|
|
202
241
|
import { sanitizeFilename } from "./files";
|
|
242
|
+
import { USER_CONTENT_SECURITY_HEADERS } from "../http/user-content";
|
|
203
243
|
|
|
204
244
|
const BROWSER_DRIVER_ID = "opengeni.cdp.v1";
|
|
205
245
|
const LIGHTPANDA_DRIVER_ID = "opengeni.lightpanda.cdp.v1";
|
|
@@ -264,13 +304,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
264
304
|
app.get("/v1/workspaces/:workspaceId/browser-sessions", async (context) => {
|
|
265
305
|
const workspaceId = context.req.param("workspaceId") ?? "";
|
|
266
306
|
const grant = await requireAccessGrant(context, deps, workspaceId, "sessions:read");
|
|
307
|
+
const listed = await listBrowserSessions(deps.db, {
|
|
308
|
+
accountId: grant.accountId,
|
|
309
|
+
workspaceId,
|
|
310
|
+
});
|
|
267
311
|
return context.json(
|
|
268
|
-
BrowserSessionListResponse.parse(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}),
|
|
273
|
-
),
|
|
312
|
+
BrowserSessionListResponse.parse({
|
|
313
|
+
...listed,
|
|
314
|
+
sessions: await filterInteractionSessionsForGrant(deps, grant, listed.sessions),
|
|
315
|
+
}),
|
|
274
316
|
);
|
|
275
317
|
});
|
|
276
318
|
|
|
@@ -297,10 +339,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
297
339
|
const request = await parseJsonBody(context, CreateBrowserSessionRequest);
|
|
298
340
|
const startedAtMs = performance.now();
|
|
299
341
|
await authorizeSourceSession(deps, grant, request.sessionId, "session.control");
|
|
300
|
-
const origin = requestOrigin(context, deps.settings
|
|
342
|
+
const origin = requestOrigin(context, deps.settings);
|
|
301
343
|
const authority = browserAuthorityRoot(deps);
|
|
344
|
+
let prepared: Awaited<ReturnType<typeof prepareBrowserSessionCreate>> | null = null;
|
|
302
345
|
|
|
303
346
|
try {
|
|
347
|
+
assertEphemeralBrowserCreateEnabled(
|
|
348
|
+
request,
|
|
349
|
+
deps.settings.experimentalBrowserContextPoolEnabled,
|
|
350
|
+
);
|
|
304
351
|
const existing = await findBrowserSessionControlRecordByOperation(deps.db, {
|
|
305
352
|
accountId: grant.accountId,
|
|
306
353
|
workspaceId,
|
|
@@ -312,7 +359,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
312
359
|
);
|
|
313
360
|
}
|
|
314
361
|
if (existing) assertCreateReplay(request, existing.session);
|
|
315
|
-
|
|
362
|
+
prepared = existing
|
|
316
363
|
? await prepareBrowserSessionCreate(
|
|
317
364
|
deps.db,
|
|
318
365
|
browserCreateInput(grant, workspaceId, request, existing.session.placement),
|
|
@@ -324,6 +371,20 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
324
371
|
return context.json(parsed, 200);
|
|
325
372
|
}
|
|
326
373
|
|
|
374
|
+
// Reject an unavailable explicit identity before acquiring or waking the
|
|
375
|
+
// browser placement. prepareBrowserSessionCreate repeats this check in its
|
|
376
|
+
// transaction, so an archive racing this preflight is still fenced.
|
|
377
|
+
if (!existing && request.identityId) {
|
|
378
|
+
const identity = await getBrowserIdentity(deps.db, {
|
|
379
|
+
accountId: grant.accountId,
|
|
380
|
+
workspaceId,
|
|
381
|
+
identityId: request.identityId,
|
|
382
|
+
});
|
|
383
|
+
if (identity.status !== "active") {
|
|
384
|
+
throw new BrowserIdentityStateError("BrowserIdentity is archived");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
327
388
|
const sourceSession = await requireSourceSession(deps, workspaceId, request.sessionId);
|
|
328
389
|
const response = await withBrowserPlacement(
|
|
329
390
|
sourceSession,
|
|
@@ -400,12 +461,6 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
400
461
|
});
|
|
401
462
|
const preparedSession = prepared.session;
|
|
402
463
|
const controllerGeneration = requireOperationGeneration(record);
|
|
403
|
-
const linkedComputer = await requireLinkedComputerBinding(
|
|
404
|
-
deps,
|
|
405
|
-
grant,
|
|
406
|
-
prepared.session,
|
|
407
|
-
placement,
|
|
408
|
-
);
|
|
409
464
|
const adminToken = deriveBrowserControllerAdminToken({
|
|
410
465
|
rootSecret: authority,
|
|
411
466
|
accountId: grant.accountId,
|
|
@@ -431,8 +486,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
431
486
|
placement,
|
|
432
487
|
adminToken,
|
|
433
488
|
origin,
|
|
434
|
-
async (client) =>
|
|
435
|
-
await
|
|
489
|
+
async (client) => {
|
|
490
|
+
const linkedComputer = await ensureLinkedComputerController(
|
|
491
|
+
deps,
|
|
492
|
+
grant,
|
|
493
|
+
preparedSession,
|
|
494
|
+
placement,
|
|
495
|
+
client,
|
|
496
|
+
);
|
|
497
|
+
return await withInteractionHolderHeartbeat(
|
|
436
498
|
deps,
|
|
437
499
|
{
|
|
438
500
|
grant,
|
|
@@ -443,20 +505,32 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
443
505
|
resourceId: preparedSession.id,
|
|
444
506
|
controllerGeneration,
|
|
445
507
|
},
|
|
446
|
-
async () =>
|
|
447
|
-
await
|
|
508
|
+
async () => {
|
|
509
|
+
await ensureHeadedBrowserDisplayStack(
|
|
510
|
+
placement.session,
|
|
511
|
+
preparedSession.headless,
|
|
512
|
+
linkedComputer !== null,
|
|
513
|
+
);
|
|
514
|
+
return await client.createSession({
|
|
448
515
|
browserSessionId: preparedSession.id,
|
|
449
516
|
controllerGeneration,
|
|
450
517
|
tokenGeneration: record.tokenGeneration,
|
|
451
518
|
...tokens,
|
|
452
519
|
headed: !preparedSession.headless,
|
|
453
|
-
transport: browserRuntimeTransport(
|
|
520
|
+
transport: browserRuntimeTransport(
|
|
521
|
+
preparedSession,
|
|
522
|
+
placement.transport,
|
|
523
|
+
placement.placementInstanceId,
|
|
524
|
+
authority,
|
|
525
|
+
),
|
|
454
526
|
...(linkedComputer ? { linkedComputer } : {}),
|
|
455
527
|
...(networkRoute ? { networkRoute } : {}),
|
|
456
528
|
...(request.initialUrl ? { initialUrl: request.initialUrl } : {}),
|
|
457
529
|
...(restore ? { restore } : {}),
|
|
458
|
-
})
|
|
459
|
-
|
|
530
|
+
});
|
|
531
|
+
},
|
|
532
|
+
);
|
|
533
|
+
},
|
|
460
534
|
);
|
|
461
535
|
await cacheBrowserControllerPlacement(grant, workspaceId, placement).catch(
|
|
462
536
|
() => placement,
|
|
@@ -518,6 +592,18 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
518
592
|
parsed.operation.state === "completed" && !parsed.operation.replayed ? 201 : 200,
|
|
519
593
|
);
|
|
520
594
|
} catch (error) {
|
|
595
|
+
if (prepared) {
|
|
596
|
+
// The operation lock fences a concurrent dispatch. Once a controller
|
|
597
|
+
// binding exists, retain its exact receipt for physical reconciliation.
|
|
598
|
+
await failBrowserSessionOperation(deps.db, {
|
|
599
|
+
accountId: grant.accountId,
|
|
600
|
+
workspaceId,
|
|
601
|
+
operationId: request.operationId,
|
|
602
|
+
browserSessionId: prepared.session.id,
|
|
603
|
+
onlyIfPreparedCreate: true,
|
|
604
|
+
error: interactionFailure(error),
|
|
605
|
+
}).catch(() => undefined);
|
|
606
|
+
}
|
|
521
607
|
throw browserRouteError(error);
|
|
522
608
|
}
|
|
523
609
|
});
|
|
@@ -907,6 +993,82 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
907
993
|
},
|
|
908
994
|
);
|
|
909
995
|
|
|
996
|
+
app.get(
|
|
997
|
+
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/targets/:targetId/state",
|
|
998
|
+
async (context) => {
|
|
999
|
+
const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
|
|
1000
|
+
context,
|
|
1001
|
+
"sessions:read",
|
|
1002
|
+
);
|
|
1003
|
+
const targetId = requireOpaqueParam(context, "targetId");
|
|
1004
|
+
const result = await withActiveBrowserController(
|
|
1005
|
+
context,
|
|
1006
|
+
grant,
|
|
1007
|
+
workspaceId,
|
|
1008
|
+
browserSessionId,
|
|
1009
|
+
"session.read",
|
|
1010
|
+
"browser.read",
|
|
1011
|
+
async ({ sessionClient }) => await sessionClient.targetState(targetId),
|
|
1012
|
+
);
|
|
1013
|
+
return context.json(BrowserTargetState.parse(result));
|
|
1014
|
+
},
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
app.post(
|
|
1018
|
+
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/targets/:targetId/dom-read",
|
|
1019
|
+
async (context) => {
|
|
1020
|
+
const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
|
|
1021
|
+
context,
|
|
1022
|
+
"sessions:read",
|
|
1023
|
+
);
|
|
1024
|
+
const targetId = requireOpaqueParam(context, "targetId");
|
|
1025
|
+
const request = await parseJsonBody(context, BrowserDomReadRequest);
|
|
1026
|
+
const result = await withActiveBrowserController(
|
|
1027
|
+
context,
|
|
1028
|
+
grant,
|
|
1029
|
+
workspaceId,
|
|
1030
|
+
browserSessionId,
|
|
1031
|
+
"session.read",
|
|
1032
|
+
"browser.read",
|
|
1033
|
+
async ({ sessionClient }) => await sessionClient.readDom(targetId, request),
|
|
1034
|
+
);
|
|
1035
|
+
return context.json(BrowserDomReadResponse.parse(result));
|
|
1036
|
+
},
|
|
1037
|
+
);
|
|
1038
|
+
|
|
1039
|
+
app.get(
|
|
1040
|
+
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/targets/:targetId/screenshot",
|
|
1041
|
+
async (context) => {
|
|
1042
|
+
const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
|
|
1043
|
+
context,
|
|
1044
|
+
"sessions:read",
|
|
1045
|
+
);
|
|
1046
|
+
const targetId = requireOpaqueParam(context, "targetId");
|
|
1047
|
+
const captureOptions = parseBrowserScreenshotOptions(new URL(context.req.url).searchParams);
|
|
1048
|
+
const frame = await withActiveBrowserController(
|
|
1049
|
+
context,
|
|
1050
|
+
grant,
|
|
1051
|
+
workspaceId,
|
|
1052
|
+
browserSessionId,
|
|
1053
|
+
"session.read",
|
|
1054
|
+
"browser.read",
|
|
1055
|
+
async ({ sessionClient, record }) => {
|
|
1056
|
+
if (!record.session.capabilities.screenshots) {
|
|
1057
|
+
throw new BrowserControlUnsupportedError(
|
|
1058
|
+
"This browser engine does not render page screenshots; use semantic observation",
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
try {
|
|
1062
|
+
return await sessionClient.capture(targetId, captureOptions);
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
throw browserScreenshotError(error);
|
|
1065
|
+
}
|
|
1066
|
+
},
|
|
1067
|
+
);
|
|
1068
|
+
return browserScreenshotResponse(frame);
|
|
1069
|
+
},
|
|
1070
|
+
);
|
|
1071
|
+
|
|
910
1072
|
app.post(
|
|
911
1073
|
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/actions",
|
|
912
1074
|
async (context) => {
|
|
@@ -927,7 +1089,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
927
1089
|
browserSessionId,
|
|
928
1090
|
"session.control",
|
|
929
1091
|
"browser.action",
|
|
930
|
-
async ({ sessionClient, binding, record }) => {
|
|
1092
|
+
async ({ sessionClient, binding, record, sourceAuthorization }) => {
|
|
931
1093
|
const command = BrowserActionCommand.parse({
|
|
932
1094
|
protocolVersion: BROWSER_CONTROL_PROTOCOL_VERSION,
|
|
933
1095
|
operationId: request.operationId,
|
|
@@ -957,27 +1119,25 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
957
1119
|
message: "object storage is not configured",
|
|
958
1120
|
});
|
|
959
1121
|
}
|
|
960
|
-
const files = await
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
throw new HTTPException(409, { message: "workspace file is not ready" });
|
|
968
|
-
}
|
|
1122
|
+
const files = await getFilesForSubject(deps.db, {
|
|
1123
|
+
accountId: grant.accountId,
|
|
1124
|
+
workspaceId,
|
|
1125
|
+
subjectId: browserFileAuthoritySubjectId(grant, sourceAuthorization),
|
|
1126
|
+
fileIds: workspaceFileIds,
|
|
1127
|
+
});
|
|
1128
|
+
const ordered = requireAuthorizedBrowserUploadFiles(workspaceFileIds, files);
|
|
969
1129
|
const authorities = await Promise.all(
|
|
970
1130
|
ordered.map(async (file) => {
|
|
971
1131
|
const signed = await deps.objectStorage!.createGetUrl({
|
|
972
|
-
key: file
|
|
1132
|
+
key: file.objectKey,
|
|
973
1133
|
expiresInSeconds: BROWSER_WORKSPACE_FILE_AUTHORITY_TTL_SECONDS,
|
|
974
1134
|
audience: signedUrlAudienceForPlacement(record.session.placement),
|
|
975
1135
|
});
|
|
976
1136
|
return {
|
|
977
|
-
fileId: file
|
|
978
|
-
safeFilename: browserUploadFilename(file
|
|
979
|
-
sizeBytes: file
|
|
980
|
-
sha256: browserUploadSha256(file
|
|
1137
|
+
fileId: file.id,
|
|
1138
|
+
safeFilename: browserUploadFilename(file.safeFilename),
|
|
1139
|
+
sizeBytes: file.sizeBytes,
|
|
1140
|
+
sha256: browserUploadSha256(file.sha256),
|
|
981
1141
|
download: {
|
|
982
1142
|
url: signed.url,
|
|
983
1143
|
expiresAt: signed.expiresAt.toISOString(),
|
|
@@ -1664,7 +1824,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1664
1824
|
context,
|
|
1665
1825
|
"stream:view",
|
|
1666
1826
|
);
|
|
1667
|
-
const origin = requestOrigin(context, deps.settings
|
|
1827
|
+
const origin = requestOrigin(context, deps.settings);
|
|
1668
1828
|
const request = await parseJsonBody(context, BrowserSessionAttachmentRequest);
|
|
1669
1829
|
const result = await withActiveBrowserController(
|
|
1670
1830
|
context,
|
|
@@ -1706,7 +1866,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1706
1866
|
browserSessionId,
|
|
1707
1867
|
controllerGeneration: binding.controllerGeneration,
|
|
1708
1868
|
};
|
|
1709
|
-
await client.createViewGrant(reference, {
|
|
1869
|
+
const viewGrant = await client.createViewGrant(reference, {
|
|
1710
1870
|
grantId,
|
|
1711
1871
|
token,
|
|
1712
1872
|
expiresAt,
|
|
@@ -1720,6 +1880,18 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1720
1880
|
});
|
|
1721
1881
|
const stream = relayed
|
|
1722
1882
|
? await (async () => {
|
|
1883
|
+
// 0281: stamp the authenticated viewer subject and the live
|
|
1884
|
+
// session authority epoch into the relay stream token.
|
|
1885
|
+
const relayAuthorityEpoch = await getSessionAuthorityEpoch(deps.db, {
|
|
1886
|
+
accountId: grant.accountId,
|
|
1887
|
+
workspaceId,
|
|
1888
|
+
sessionId: record.sourceSessionId,
|
|
1889
|
+
});
|
|
1890
|
+
if (relayAuthorityEpoch === null) {
|
|
1891
|
+
throw new BrowserControlUnsupportedError(
|
|
1892
|
+
"stream authority is unavailable for this session",
|
|
1893
|
+
);
|
|
1894
|
+
}
|
|
1723
1895
|
const relayToken = await mintStreamToken(relaySecret!, {
|
|
1724
1896
|
workspaceId,
|
|
1725
1897
|
sessionId: record.sourceSessionId,
|
|
@@ -1727,6 +1899,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1727
1899
|
leaseEpoch: record.tokenGeneration,
|
|
1728
1900
|
port: relayed.channel.port,
|
|
1729
1901
|
ttlSeconds: request.expiresInSeconds,
|
|
1902
|
+
subjectId: grant.subjectId,
|
|
1903
|
+
authorityEpoch: relayAuthorityEpoch,
|
|
1730
1904
|
});
|
|
1731
1905
|
return {
|
|
1732
1906
|
kind: "relay" as const,
|
|
@@ -1741,19 +1915,53 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1741
1915
|
},
|
|
1742
1916
|
};
|
|
1743
1917
|
})()
|
|
1744
|
-
: {
|
|
1745
|
-
|
|
1746
|
-
url: await client.frameStreamUrl(reference, request.targetId, request.stream),
|
|
1747
|
-
protocols: [
|
|
1918
|
+
: await (async () => {
|
|
1919
|
+
const protocols = [
|
|
1748
1920
|
BROWSER_CONTROL_WEBSOCKET_PROTOCOL,
|
|
1749
1921
|
`${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
|
|
1750
|
-
]
|
|
1751
|
-
|
|
1922
|
+
] as const;
|
|
1923
|
+
const upstreamUrl = await client.frameStreamUrl(
|
|
1924
|
+
reference,
|
|
1925
|
+
request.targetId,
|
|
1926
|
+
request.stream,
|
|
1927
|
+
);
|
|
1928
|
+
const attachment = placementUsesInteractionFrameProxy(placement.lease?.backend, {
|
|
1929
|
+
openSandboxSignedEndpoints: deps.settings.openSandboxSignedEndpoints,
|
|
1930
|
+
...(typeof deps.settings.openSandboxInteractionFrameProxy === "boolean"
|
|
1931
|
+
? {
|
|
1932
|
+
openSandboxInteractionFrameProxy:
|
|
1933
|
+
deps.settings.openSandboxInteractionFrameProxy,
|
|
1934
|
+
}
|
|
1935
|
+
: {}),
|
|
1936
|
+
})
|
|
1937
|
+
? createInteractionFrameProxyAttachment({
|
|
1938
|
+
requestUrl: context.req.url,
|
|
1939
|
+
publicBaseUrl: deps.settings.publicBaseUrl,
|
|
1940
|
+
webBaseUrl: deps.settings.webBaseUrl,
|
|
1941
|
+
forwardedProto: context.req.header("x-forwarded-proto"),
|
|
1942
|
+
forwardedHost:
|
|
1943
|
+
context.req.header("x-forwarded-host") ?? context.req.header("host"),
|
|
1944
|
+
rootSecret,
|
|
1945
|
+
upstreamUrl,
|
|
1946
|
+
upstreamProtocols: protocols,
|
|
1947
|
+
origin,
|
|
1948
|
+
expiresAt,
|
|
1949
|
+
})
|
|
1950
|
+
: { url: upstreamUrl, protocols };
|
|
1951
|
+
return {
|
|
1952
|
+
kind: "direct_websocket" as const,
|
|
1953
|
+
...attachment,
|
|
1954
|
+
};
|
|
1955
|
+
})();
|
|
1752
1956
|
return BrowserSessionAttachment.parse({
|
|
1753
1957
|
browserSessionId,
|
|
1754
1958
|
controllerGeneration: binding.controllerGeneration,
|
|
1755
1959
|
targetId: request.targetId,
|
|
1756
1960
|
stream,
|
|
1961
|
+
...(viewGrant.fencedInputBatches === true ? { fencedInputBatches: true } : {}),
|
|
1962
|
+
...(viewGrant.focusedInputObservations === true
|
|
1963
|
+
? { focusedInputObservations: true }
|
|
1964
|
+
: {}),
|
|
1757
1965
|
expiresAt,
|
|
1758
1966
|
});
|
|
1759
1967
|
},
|
|
@@ -1959,7 +2167,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1959
2167
|
const browserSessionId = requireUuidParam(context, "browserSessionId");
|
|
1960
2168
|
const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
|
|
1961
2169
|
const startedAtMs = performance.now();
|
|
1962
|
-
const origin = requestOrigin(context, deps.settings
|
|
2170
|
+
const origin = requestOrigin(context, deps.settings);
|
|
2171
|
+
let preparedForDispatch = false;
|
|
1963
2172
|
try {
|
|
1964
2173
|
const before = await getBrowserSessionControlRecord(deps.db, {
|
|
1965
2174
|
accountId: grant.accountId,
|
|
@@ -2011,6 +2220,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2011
2220
|
return context.json(parsed, 200);
|
|
2012
2221
|
}
|
|
2013
2222
|
|
|
2223
|
+
preparedForDispatch = true;
|
|
2224
|
+
|
|
2014
2225
|
const record = await getBrowserSessionControlRecord(deps.db, {
|
|
2015
2226
|
accountId: grant.accountId,
|
|
2016
2227
|
workspaceId,
|
|
@@ -2172,6 +2383,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2172
2383
|
observeLifecycleResult(deps.observability, startedAtMs, parsed);
|
|
2173
2384
|
return context.json(parsed, 200);
|
|
2174
2385
|
} catch (error) {
|
|
2386
|
+
if (preparedForDispatch) {
|
|
2387
|
+
await failPreparedBrowserSessionSuspend(deps.db, {
|
|
2388
|
+
accountId: grant.accountId,
|
|
2389
|
+
workspaceId,
|
|
2390
|
+
browserSessionId,
|
|
2391
|
+
operationId: request.operationId,
|
|
2392
|
+
error: interactionFailure(error),
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2175
2395
|
throw browserRouteError(error);
|
|
2176
2396
|
}
|
|
2177
2397
|
},
|
|
@@ -2185,7 +2405,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2185
2405
|
const browserSessionId = requireUuidParam(context, "browserSessionId");
|
|
2186
2406
|
const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
|
|
2187
2407
|
const startedAtMs = performance.now();
|
|
2188
|
-
const origin = requestOrigin(context, deps.settings
|
|
2408
|
+
const origin = requestOrigin(context, deps.settings);
|
|
2189
2409
|
let restore: RestorePlacementBrowserStateInput | null = null;
|
|
2190
2410
|
try {
|
|
2191
2411
|
let before = await getBrowserSessionControlRecord(deps.db, {
|
|
@@ -2304,12 +2524,6 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2304
2524
|
throw error;
|
|
2305
2525
|
});
|
|
2306
2526
|
const controllerGeneration = requireOperationGeneration(record);
|
|
2307
|
-
const linkedComputer = await requireLinkedComputerBinding(
|
|
2308
|
-
deps,
|
|
2309
|
-
grant,
|
|
2310
|
-
prepared.session,
|
|
2311
|
-
placement,
|
|
2312
|
-
);
|
|
2313
2527
|
const adminToken = deriveBrowserControllerAdminToken({
|
|
2314
2528
|
rootSecret: authority,
|
|
2315
2529
|
accountId: grant.accountId,
|
|
@@ -2333,6 +2547,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2333
2547
|
...(origin ? { allowedOrigins: [origin] } : {}),
|
|
2334
2548
|
});
|
|
2335
2549
|
try {
|
|
2550
|
+
const linkedComputer = await ensureLinkedComputerController(
|
|
2551
|
+
deps,
|
|
2552
|
+
grant,
|
|
2553
|
+
prepared.session,
|
|
2554
|
+
placement,
|
|
2555
|
+
client,
|
|
2556
|
+
);
|
|
2336
2557
|
await withInteractionHolderHeartbeat(
|
|
2337
2558
|
deps,
|
|
2338
2559
|
{
|
|
@@ -2344,8 +2565,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2344
2565
|
resourceId: browserSessionId,
|
|
2345
2566
|
controllerGeneration,
|
|
2346
2567
|
},
|
|
2347
|
-
async () =>
|
|
2348
|
-
await
|
|
2568
|
+
async () => {
|
|
2569
|
+
await ensureHeadedBrowserDisplayStack(
|
|
2570
|
+
placement.session,
|
|
2571
|
+
prepared.session.headless,
|
|
2572
|
+
linkedComputer !== null,
|
|
2573
|
+
);
|
|
2574
|
+
return await client.createSession({
|
|
2349
2575
|
browserSessionId,
|
|
2350
2576
|
controllerGeneration,
|
|
2351
2577
|
tokenGeneration: record.tokenGeneration,
|
|
@@ -2355,7 +2581,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2355
2581
|
...(linkedComputer ? { linkedComputer } : {}),
|
|
2356
2582
|
...(networkRoute ? { networkRoute } : {}),
|
|
2357
2583
|
restore: restore!,
|
|
2358
|
-
})
|
|
2584
|
+
});
|
|
2585
|
+
},
|
|
2359
2586
|
);
|
|
2360
2587
|
} catch (error) {
|
|
2361
2588
|
if (!isDefiniteBrowserControllerFailure(error)) throw error;
|
|
@@ -2427,7 +2654,12 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2427
2654
|
const browserSessionId = requireUuidParam(context, "browserSessionId");
|
|
2428
2655
|
const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
|
|
2429
2656
|
const startedAtMs = performance.now();
|
|
2430
|
-
const origin = requestOrigin(context, deps.settings
|
|
2657
|
+
const origin = requestOrigin(context, deps.settings);
|
|
2658
|
+
let endPreparation: {
|
|
2659
|
+
restoreLifecycle: BrowserSessionValue["lifecycle"];
|
|
2660
|
+
restoreFailureCode: string | null;
|
|
2661
|
+
expectedControllerGeneration: string | null;
|
|
2662
|
+
} | null = null;
|
|
2431
2663
|
try {
|
|
2432
2664
|
const before = await getBrowserSessionControlRecord(deps.db, {
|
|
2433
2665
|
accountId: grant.accountId,
|
|
@@ -2442,6 +2674,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2442
2674
|
browserSessionId,
|
|
2443
2675
|
operationId: request.operationId,
|
|
2444
2676
|
actorSubjectId: grant.subjectId,
|
|
2677
|
+
expectedLifecycle: before.session.lifecycle,
|
|
2678
|
+
expectedFailureCode: before.session.failureCode,
|
|
2679
|
+
expectedControllerGeneration: before.session.controller?.controllerGeneration ?? null,
|
|
2445
2680
|
});
|
|
2446
2681
|
if (isTerminalOperation(prepared.operation.state)) {
|
|
2447
2682
|
if (prepared.operation.state === "completed") {
|
|
@@ -2457,6 +2692,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2457
2692
|
observeLifecycleResult(deps.observability, startedAtMs, parsed);
|
|
2458
2693
|
return context.json(parsed, 200);
|
|
2459
2694
|
}
|
|
2695
|
+
if (before.session.lifecycle !== "ending") {
|
|
2696
|
+
endPreparation = {
|
|
2697
|
+
restoreLifecycle: before.session.lifecycle,
|
|
2698
|
+
restoreFailureCode: before.session.failureCode,
|
|
2699
|
+
expectedControllerGeneration: before.session.controller?.controllerGeneration ?? null,
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2460
2702
|
|
|
2461
2703
|
const record = await getBrowserSessionControlRecord(deps.db, {
|
|
2462
2704
|
accountId: grant.accountId,
|
|
@@ -2550,6 +2792,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2550
2792
|
observeLifecycleResult(deps.observability, startedAtMs, parsed);
|
|
2551
2793
|
return context.json(parsed, 200);
|
|
2552
2794
|
} catch (error) {
|
|
2795
|
+
if (endPreparation) {
|
|
2796
|
+
await failPreparedBrowserSessionEnd(deps.db, {
|
|
2797
|
+
accountId: grant.accountId,
|
|
2798
|
+
workspaceId,
|
|
2799
|
+
browserSessionId,
|
|
2800
|
+
operationId: request.operationId,
|
|
2801
|
+
...endPreparation,
|
|
2802
|
+
error: interactionFailure(error),
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2553
2805
|
throw browserRouteError(error);
|
|
2554
2806
|
}
|
|
2555
2807
|
},
|
|
@@ -2563,6 +2815,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2563
2815
|
operation: ChannelAOperation,
|
|
2564
2816
|
waitSignal: AbortSignal,
|
|
2565
2817
|
callback: (placement: BrowserPlacement) => Promise<T>,
|
|
2818
|
+
allowOperationReplay = true,
|
|
2566
2819
|
): Promise<T> {
|
|
2567
2820
|
if (expectedPlacement?.kind === "attached_device") {
|
|
2568
2821
|
waitSignal.throwIfAborted();
|
|
@@ -2574,18 +2827,29 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2574
2827
|
if (device.state !== "connected") {
|
|
2575
2828
|
throw new BrowserSessionStateError("Attached browser is disconnected");
|
|
2576
2829
|
}
|
|
2577
|
-
const enrollment = await
|
|
2830
|
+
const enrollment = await getLiveEnrollmentConnection(
|
|
2578
2831
|
deps.db,
|
|
2579
2832
|
sourceSession.workspaceId,
|
|
2580
2833
|
device.enrollmentId,
|
|
2581
2834
|
);
|
|
2582
|
-
if (!enrollment || enrollment.status !== "active") {
|
|
2835
|
+
if (!enrollment || enrollment.status !== "active" || !enrollment.connectionInstanceId) {
|
|
2583
2836
|
throw new BrowserSessionStateError("Attached browser machine is unavailable");
|
|
2584
2837
|
}
|
|
2585
|
-
|
|
2838
|
+
if (!enrollment.workspaceRoot) {
|
|
2839
|
+
throw new BrowserSessionStateError(
|
|
2840
|
+
"Attached browser machine has not reported an absolute workspace root",
|
|
2841
|
+
);
|
|
2842
|
+
}
|
|
2843
|
+
const placementInstanceId = attachedEndPlacementInstanceId(
|
|
2844
|
+
operation,
|
|
2845
|
+
expectedPlacementInstanceId,
|
|
2846
|
+
device.connectionGeneration,
|
|
2847
|
+
);
|
|
2586
2848
|
const built = await buildSelfhostedBackendSession({
|
|
2587
2849
|
workspaceId: sourceSession.workspaceId,
|
|
2588
2850
|
agentId: device.enrollmentId,
|
|
2851
|
+
connectionInstanceId: enrollment.connectionInstanceId,
|
|
2852
|
+
workspaceRoot: enrollment.workspaceRoot,
|
|
2589
2853
|
relay: relayConfigFromSettings(deps.settings),
|
|
2590
2854
|
controlRpcFactory: () => new NatsControlRpc(async () => deps.bus.getRequestConnection()),
|
|
2591
2855
|
// Browser-profile generation is the physical controller fence. Epoch 0
|
|
@@ -2594,6 +2858,11 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2594
2858
|
epoch: 0,
|
|
2595
2859
|
timeoutMs: deps.settings.sandboxSelfhostedControlTimeoutMs,
|
|
2596
2860
|
execTimeoutMs: deps.settings.sandboxSelfhostedExecTimeoutMs,
|
|
2861
|
+
operationResourcePolicy: enrollment.operationPolicy,
|
|
2862
|
+
operationResourcePolicySupported:
|
|
2863
|
+
enrollment.agentCapabilities.operationResourcePolicy === true,
|
|
2864
|
+
operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
|
|
2865
|
+
transactionalFsWriteSupported: enrollment.agentCapabilities.transactionalFsWrite === true,
|
|
2597
2866
|
...(deps.settings.agentOpStreamEnabled === true &&
|
|
2598
2867
|
enrollment.opStream === true &&
|
|
2599
2868
|
deps.bus.getOpStreamConnection
|
|
@@ -2610,13 +2879,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2610
2879
|
return await callback({
|
|
2611
2880
|
placement: expectedPlacement,
|
|
2612
2881
|
controllerHostSandboxGroupId: null,
|
|
2613
|
-
placementInstanceId
|
|
2882
|
+
placementInstanceId,
|
|
2614
2883
|
session: built.session as unknown as BrowserControlPlacementSession,
|
|
2615
2884
|
lease: null,
|
|
2616
2885
|
transport: {
|
|
2617
2886
|
kind: "attached_chrome",
|
|
2618
2887
|
deviceId: device.id,
|
|
2619
|
-
connectionGeneration:
|
|
2888
|
+
connectionGeneration: placementInstanceId,
|
|
2620
2889
|
browserName: device.browserName,
|
|
2621
2890
|
browserVersion: device.browserVersion,
|
|
2622
2891
|
},
|
|
@@ -2639,6 +2908,12 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2639
2908
|
waitSignal,
|
|
2640
2909
|
operation,
|
|
2641
2910
|
retryControllerTransport: operation === "browser.read" || operation === "browser.action",
|
|
2911
|
+
allowOperationReplay,
|
|
2912
|
+
...(expectedPlacementInstanceId &&
|
|
2913
|
+
(expectedPlacement?.kind === "sandbox_group" ||
|
|
2914
|
+
expectedPlacement?.kind === "external_provider")
|
|
2915
|
+
? { retainedInstanceId: expectedPlacementInstanceId }
|
|
2916
|
+
: {}),
|
|
2642
2917
|
},
|
|
2643
2918
|
async (handle) => {
|
|
2644
2919
|
if (expectedPlacement?.kind === "sandbox_group") {
|
|
@@ -2682,8 +2957,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2682
2957
|
resolved.kind !== "selfhosted" ||
|
|
2683
2958
|
resolved.sandboxId !== expectedPlacement.sandboxId
|
|
2684
2959
|
) {
|
|
2685
|
-
|
|
2686
|
-
|
|
2960
|
+
if (operation === "browser.create") {
|
|
2961
|
+
throw new HTTPException(422, {
|
|
2962
|
+
message:
|
|
2963
|
+
"The requested Connected Machine is not this session's current placement. Move the session to that machine before creating an interaction resource.",
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
return await throwBrowserSourcePlacementChanged(
|
|
2967
|
+
grant,
|
|
2968
|
+
sourceSession.id,
|
|
2969
|
+
expectedPlacement.sandboxId,
|
|
2687
2970
|
);
|
|
2688
2971
|
}
|
|
2689
2972
|
const placementInstanceId = resolved.providerInstanceId ?? expectedPlacement.sandboxId;
|
|
@@ -2780,7 +3063,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2780
3063
|
!lease ||
|
|
2781
3064
|
(lease.liveness !== "warm" && lease.liveness !== "draining") ||
|
|
2782
3065
|
lease.instanceId !== binding.placementInstanceId ||
|
|
2783
|
-
!lease.controllerDataPlaneUrl
|
|
3066
|
+
!lease.controllerDataPlaneUrl ||
|
|
3067
|
+
(lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) ||
|
|
3068
|
+
!controllerCachedUrlIsUsable(lease.controllerDataPlaneUrl)
|
|
2784
3069
|
) {
|
|
2785
3070
|
return null;
|
|
2786
3071
|
}
|
|
@@ -2806,8 +3091,29 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2806
3091
|
if (!sandboxGroupId || !placement.lease?.instanceId || !placement.session.resolveExposedPort) {
|
|
2807
3092
|
return placement;
|
|
2808
3093
|
}
|
|
3094
|
+
if (placement.lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) {
|
|
3095
|
+
if (!placement.lease.controllerDataPlaneUrl) return placement;
|
|
3096
|
+
const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
|
|
3097
|
+
accountId: grant.accountId,
|
|
3098
|
+
workspaceId,
|
|
3099
|
+
sandboxGroupId,
|
|
3100
|
+
expectedEpoch: placement.lease.leaseEpoch,
|
|
3101
|
+
expectedInstanceId: placement.lease.instanceId,
|
|
3102
|
+
controllerDataPlaneUrl: null,
|
|
3103
|
+
});
|
|
3104
|
+
return lease ? { ...placement, lease } : placement;
|
|
3105
|
+
}
|
|
2809
3106
|
const endpoint = await placement.session.resolveExposedPort(BROWSER_CONTROL_PORT);
|
|
2810
3107
|
const url = buildStreamUrl(endpoint);
|
|
3108
|
+
if (
|
|
3109
|
+
!shouldPersistControllerDataPlaneUrl({
|
|
3110
|
+
backend: placement.lease.backend,
|
|
3111
|
+
signedEndpoints: deps.settings.openSandboxSignedEndpoints,
|
|
3112
|
+
url,
|
|
3113
|
+
})
|
|
3114
|
+
) {
|
|
3115
|
+
return placement;
|
|
3116
|
+
}
|
|
2811
3117
|
const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
|
|
2812
3118
|
accountId: grant.accountId,
|
|
2813
3119
|
workspaceId,
|
|
@@ -2817,6 +3123,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2817
3123
|
controllerDataPlaneUrl: url,
|
|
2818
3124
|
});
|
|
2819
3125
|
if (!lease) return placement;
|
|
3126
|
+
if (!controllerCacheAllowsHostFetch(url)) {
|
|
3127
|
+
return { ...placement, lease };
|
|
3128
|
+
}
|
|
2820
3129
|
return { ...placement, session: controllerOnlySession(url), lease };
|
|
2821
3130
|
}
|
|
2822
3131
|
|
|
@@ -2832,7 +3141,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2832
3141
|
const sandboxGroupId = placement.controllerHostSandboxGroupId;
|
|
2833
3142
|
const cachedUrl = placement.lease?.controllerDataPlaneUrl;
|
|
2834
3143
|
return await withCachedController({
|
|
2835
|
-
cachedUrl:
|
|
3144
|
+
cachedUrl:
|
|
3145
|
+
sandboxGroupId &&
|
|
3146
|
+
cachedUrl &&
|
|
3147
|
+
!(
|
|
3148
|
+
placement.lease?.backend === "opensandbox" &&
|
|
3149
|
+
routeDeps.settings.openSandboxSignedEndpoints
|
|
3150
|
+
) &&
|
|
3151
|
+
controllerCachedUrlIsUsable(cachedUrl)
|
|
3152
|
+
? cachedUrl
|
|
3153
|
+
: null,
|
|
2836
3154
|
createCachedClient: (url) =>
|
|
2837
3155
|
new BrowserControlClient(controllerOnlySession(url), { adminToken }),
|
|
2838
3156
|
prepareCachedClient: async (client) => {
|
|
@@ -2874,6 +3192,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2874
3192
|
record: BrowserSessionControlRecord;
|
|
2875
3193
|
binding: NonNullable<BrowserSessionControlRecord["session"]["controller"]>;
|
|
2876
3194
|
placement: BrowserPlacement;
|
|
3195
|
+
sourceAuthorization: ResolvedSessionAuthorization | null;
|
|
2877
3196
|
}) => Promise<T>,
|
|
2878
3197
|
recoverMissing = true,
|
|
2879
3198
|
): Promise<T> {
|
|
@@ -2883,7 +3202,23 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2883
3202
|
workspaceId,
|
|
2884
3203
|
browserSessionId,
|
|
2885
3204
|
});
|
|
2886
|
-
await authorizeSourceSession(
|
|
3205
|
+
const sourceAuthorization = await authorizeSourceSession(
|
|
3206
|
+
deps,
|
|
3207
|
+
grant,
|
|
3208
|
+
record.sourceSessionId,
|
|
3209
|
+
authorizationOperation,
|
|
3210
|
+
);
|
|
3211
|
+
let sourceSession: Session | null = null;
|
|
3212
|
+
if (record.session.placement?.kind === "connected_machine") {
|
|
3213
|
+
sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
|
|
3214
|
+
if (sourceSession.activeSandboxId !== record.session.placement.sandboxId) {
|
|
3215
|
+
return await throwBrowserSourcePlacementChanged(
|
|
3216
|
+
grant,
|
|
3217
|
+
record.sourceSessionId,
|
|
3218
|
+
record.session.placement.sandboxId,
|
|
3219
|
+
);
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
2887
3222
|
if (record.session.lifecycle !== "active" || !record.session.controller) {
|
|
2888
3223
|
throw new BrowserSessionStateError("BrowserSession is not active");
|
|
2889
3224
|
}
|
|
@@ -2897,10 +3232,18 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2897
3232
|
if (!admitted) {
|
|
2898
3233
|
throw new BrowserSessionStateError("BrowserSession controller authority changed");
|
|
2899
3234
|
}
|
|
2900
|
-
const
|
|
3235
|
+
const safelyReplayable =
|
|
3236
|
+
browserSessionStorageMode(record.session) !== "ephemeral_context" &&
|
|
3237
|
+
(channelOperation === "browser.read" || channelOperation === "browser.action");
|
|
3238
|
+
let controllerTransportFailed = false;
|
|
3239
|
+
const controllerRecoveryState = { attempted: false };
|
|
3240
|
+
const run = async (
|
|
3241
|
+
placement: BrowserPlacement,
|
|
3242
|
+
provisionedClient?: BrowserControlClient,
|
|
3243
|
+
): Promise<T> => {
|
|
2901
3244
|
// An active binding already proves browserd was provisioned. Reusing
|
|
2902
3245
|
// it avoids restarting/checking the sidecar on every live input.
|
|
2903
|
-
const client = connectController(deps, grant, record, placement);
|
|
3246
|
+
const client = provisionedClient ?? connectController(deps, grant, record, placement);
|
|
2904
3247
|
const tokens = deriveBrowserSessionControllerTokens({
|
|
2905
3248
|
rootSecret: browserAuthorityRoot(deps),
|
|
2906
3249
|
accountId: grant.accountId,
|
|
@@ -2923,11 +3266,36 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2923
3266
|
record,
|
|
2924
3267
|
binding,
|
|
2925
3268
|
placement,
|
|
3269
|
+
sourceAuthorization,
|
|
2926
3270
|
};
|
|
2927
3271
|
let result: T;
|
|
2928
3272
|
try {
|
|
2929
3273
|
result = await callback(controller);
|
|
2930
3274
|
} catch (error) {
|
|
3275
|
+
if (
|
|
3276
|
+
browserSessionStorageMode(record.session) === "ephemeral_context" &&
|
|
3277
|
+
(isMissingBrowserControllerSession(error) ||
|
|
3278
|
+
(error instanceof BrowserControlRequestError &&
|
|
3279
|
+
error.error.code === "controller_lost"))
|
|
3280
|
+
) {
|
|
3281
|
+
const retired = await markEphemeralBrowserSessionLost(deps.db, {
|
|
3282
|
+
accountId: grant.accountId,
|
|
3283
|
+
workspaceId,
|
|
3284
|
+
browserSessionId,
|
|
3285
|
+
controllerGeneration: binding.controllerGeneration,
|
|
3286
|
+
});
|
|
3287
|
+
if (retired)
|
|
3288
|
+
await releaseInteractionHolder(
|
|
3289
|
+
deps,
|
|
3290
|
+
grant,
|
|
3291
|
+
workspaceId,
|
|
3292
|
+
browserSessionId,
|
|
3293
|
+
record.controllerHostSandboxGroupId,
|
|
3294
|
+
);
|
|
3295
|
+
throw new BrowserSessionStateError(
|
|
3296
|
+
"Ephemeral browser context was lost; create a new BrowserSession. No identity or operation was replayed.",
|
|
3297
|
+
);
|
|
3298
|
+
}
|
|
2931
3299
|
if (!recoverMissing || !isMissingBrowserControllerSession(error)) throw error;
|
|
2932
3300
|
await recoverActiveBrowserController(
|
|
2933
3301
|
deps,
|
|
@@ -2937,6 +3305,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2937
3305
|
placement,
|
|
2938
3306
|
client,
|
|
2939
3307
|
tokens,
|
|
3308
|
+
authorizationOperation,
|
|
2940
3309
|
);
|
|
2941
3310
|
result = await callback(controller);
|
|
2942
3311
|
}
|
|
@@ -2947,15 +3316,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2947
3316
|
try {
|
|
2948
3317
|
return await run(cached);
|
|
2949
3318
|
} catch (error) {
|
|
2950
|
-
|
|
2951
|
-
channelOperation === "browser.read" || channelOperation === "browser.action";
|
|
2952
|
-
if (
|
|
2953
|
-
!safelyReplayable ||
|
|
2954
|
-
(!(error instanceof BrowserControlTransportError) &&
|
|
2955
|
-
!(error instanceof BrowserControlRequestError && error.retryable))
|
|
2956
|
-
) {
|
|
3319
|
+
if (!safelyReplayable || !isRetryableControllerTransport(error)) {
|
|
2957
3320
|
throw error;
|
|
2958
3321
|
}
|
|
3322
|
+
controllerTransportFailed = true;
|
|
2959
3323
|
await recordLeaseControllerDataPlaneUrl(deps.db, {
|
|
2960
3324
|
accountId: grant.accountId,
|
|
2961
3325
|
workspaceId,
|
|
@@ -2967,7 +3331,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2967
3331
|
}
|
|
2968
3332
|
}
|
|
2969
3333
|
|
|
2970
|
-
|
|
3334
|
+
sourceSession ??= await requireSourceSession(deps, workspaceId, record.sourceSessionId);
|
|
2971
3335
|
return await withBrowserPlacement(
|
|
2972
3336
|
sourceSession,
|
|
2973
3337
|
grant,
|
|
@@ -2975,18 +3339,75 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2975
3339
|
binding.placementInstanceId,
|
|
2976
3340
|
channelOperation,
|
|
2977
3341
|
context.req.raw.signal,
|
|
2978
|
-
async (placement) =>
|
|
2979
|
-
|
|
2980
|
-
await
|
|
2981
|
-
(
|
|
2982
|
-
|
|
2983
|
-
|
|
3342
|
+
async (placement) => {
|
|
3343
|
+
const use = async () =>
|
|
3344
|
+
await run(
|
|
3345
|
+
await cacheBrowserControllerPlacement(grant, workspaceId, placement).catch(
|
|
3346
|
+
() => placement,
|
|
3347
|
+
),
|
|
3348
|
+
);
|
|
3349
|
+
if (browserSessionStorageMode(record.session) === "ephemeral_context") return await use();
|
|
3350
|
+
return await withControllerTransportRecovery({
|
|
3351
|
+
channelOperation,
|
|
3352
|
+
placementKind: placement.placement.kind,
|
|
3353
|
+
recoveryState: controllerRecoveryState,
|
|
3354
|
+
transportAlreadyFailed: controllerTransportFailed,
|
|
3355
|
+
use,
|
|
3356
|
+
admitRecovery: async () => {
|
|
3357
|
+
if (
|
|
3358
|
+
!(await touchBrowserSessionController(deps.db, {
|
|
3359
|
+
accountId: grant.accountId,
|
|
3360
|
+
workspaceId,
|
|
3361
|
+
browserSessionId,
|
|
3362
|
+
controllerGeneration: binding.controllerGeneration,
|
|
3363
|
+
}))
|
|
3364
|
+
) {
|
|
3365
|
+
throw new BrowserSessionStateError("BrowserSession controller authority changed");
|
|
3366
|
+
}
|
|
3367
|
+
},
|
|
3368
|
+
recover: async () => {
|
|
3369
|
+
// Provision and restore through the original exec-capable session;
|
|
3370
|
+
// the cached tunnel alone cannot start browserd or its display.
|
|
3371
|
+
const client = await provisionController(
|
|
3372
|
+
deps,
|
|
3373
|
+
grant,
|
|
3374
|
+
record,
|
|
3375
|
+
placement,
|
|
3376
|
+
requestOrigin(context, deps.settings),
|
|
3377
|
+
);
|
|
3378
|
+
await cacheBrowserControllerPlacement(grant, workspaceId, placement).catch(
|
|
3379
|
+
() => placement,
|
|
3380
|
+
);
|
|
3381
|
+
return await run(placement, client);
|
|
3382
|
+
},
|
|
3383
|
+
});
|
|
3384
|
+
},
|
|
3385
|
+
browserSessionStorageMode(record.session) !== "ephemeral_context",
|
|
2984
3386
|
);
|
|
2985
3387
|
} catch (error) {
|
|
2986
3388
|
throw browserRouteError(error);
|
|
2987
3389
|
}
|
|
2988
3390
|
}
|
|
2989
3391
|
|
|
3392
|
+
async function throwBrowserSourcePlacementChanged(
|
|
3393
|
+
grant: AccessGrant,
|
|
3394
|
+
sourceSessionId: string,
|
|
3395
|
+
connectedSandboxId: string,
|
|
3396
|
+
): Promise<never> {
|
|
3397
|
+
const reconciliation = await terminalizeStaleConnectedInteractionPlacement(deps.db, {
|
|
3398
|
+
accountId: grant.accountId,
|
|
3399
|
+
workspaceId: grant.workspaceId,
|
|
3400
|
+
sourceSessionId,
|
|
3401
|
+
connectedSandboxId,
|
|
3402
|
+
});
|
|
3403
|
+
if (reconciliation.sourcePlacementChanged) {
|
|
3404
|
+
throw sourcePlacementChangedApiError("browser_session");
|
|
3405
|
+
}
|
|
3406
|
+
throw new BrowserSessionStateError(
|
|
3407
|
+
"BrowserSession source placement changed during the request; retry",
|
|
3408
|
+
);
|
|
3409
|
+
}
|
|
3410
|
+
|
|
2990
3411
|
async function recoverActiveBrowserController(
|
|
2991
3412
|
routeDeps: ApiRouteDeps,
|
|
2992
3413
|
grant: AccessGrant,
|
|
@@ -2995,12 +3416,72 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2995
3416
|
placement: BrowserPlacement,
|
|
2996
3417
|
client: BrowserControlClient,
|
|
2997
3418
|
tokens: ReturnType<typeof deriveBrowserSessionControllerTokens>,
|
|
3419
|
+
authorizationOperation: SessionAuthorizationOperation,
|
|
2998
3420
|
): Promise<void> {
|
|
2999
|
-
|
|
3421
|
+
if (browserSessionStorageMode(record.session) === "ephemeral_context") {
|
|
3422
|
+
throw new BrowserSessionStateError(
|
|
3423
|
+
"Ephemeral browser context was lost; create a new BrowserSession. Its identity and operations cannot be restored or replayed.",
|
|
3424
|
+
);
|
|
3425
|
+
}
|
|
3426
|
+
const transport = browserRuntimeTransport(record.session, placement.transport);
|
|
3427
|
+
const recoverWorkingDirectory =
|
|
3428
|
+
transport.kind === "managed" &&
|
|
3429
|
+
transport.engine === "chromium" &&
|
|
3430
|
+
!transport.ephemeralPartition &&
|
|
3431
|
+
browserSessionStorageMode(record.session) === "private_profile";
|
|
3432
|
+
if (recoverWorkingDirectory) {
|
|
3433
|
+
// Missing controller memory cannot admit a stale source, token, placement
|
|
3434
|
+
// or route snapshot. Reuse the existing durable authority before recovery.
|
|
3435
|
+
await authorizeSourceSession(
|
|
3436
|
+
routeDeps,
|
|
3437
|
+
grant,
|
|
3438
|
+
record.sourceSessionId,
|
|
3439
|
+
authorizationOperation,
|
|
3440
|
+
);
|
|
3441
|
+
const current = await getBrowserSessionControlRecord(routeDeps.db, {
|
|
3442
|
+
accountId: grant.accountId,
|
|
3443
|
+
workspaceId: record.session.workspaceId,
|
|
3444
|
+
browserSessionId: record.session.id,
|
|
3445
|
+
});
|
|
3446
|
+
if (
|
|
3447
|
+
current.sourceSessionId !== record.sourceSessionId ||
|
|
3448
|
+
current.session.lifecycle !== "active" ||
|
|
3449
|
+
current.session.controller?.controllerGeneration !== binding.controllerGeneration ||
|
|
3450
|
+
current.session.controller.placementInstanceId !== placement.placementInstanceId ||
|
|
3451
|
+
current.session.controller.controllerId !== binding.controllerId ||
|
|
3452
|
+
current.tokenGeneration !== record.tokenGeneration ||
|
|
3453
|
+
!sameInteractionPlacement(current.session.placement, record.session.placement) ||
|
|
3454
|
+
current.session.driverId !== record.session.driverId ||
|
|
3455
|
+
current.session.engine !== record.session.engine ||
|
|
3456
|
+
current.session.headless !== record.session.headless ||
|
|
3457
|
+
current.session.linkedComputerSessionId !== record.session.linkedComputerSessionId ||
|
|
3458
|
+
current.session.networkRouteId !== record.session.networkRouteId ||
|
|
3459
|
+
JSON.stringify(current.networkRouteAuthority) !==
|
|
3460
|
+
JSON.stringify(record.networkRouteAuthority)
|
|
3461
|
+
) {
|
|
3462
|
+
throw new BrowserSessionStateError("BrowserSession controller authority changed");
|
|
3463
|
+
}
|
|
3464
|
+
if (record.session.placement.kind === "connected_machine") {
|
|
3465
|
+
const source = await requireSourceSession(
|
|
3466
|
+
routeDeps,
|
|
3467
|
+
record.session.workspaceId,
|
|
3468
|
+
record.sourceSessionId,
|
|
3469
|
+
);
|
|
3470
|
+
if (source.activeSandboxId !== record.session.placement.sandboxId) {
|
|
3471
|
+
return await throwBrowserSourcePlacementChanged(
|
|
3472
|
+
grant,
|
|
3473
|
+
record.sourceSessionId,
|
|
3474
|
+
record.session.placement.sandboxId,
|
|
3475
|
+
);
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
const linkedComputer = await ensureLinkedComputerController(
|
|
3000
3480
|
routeDeps,
|
|
3001
3481
|
grant,
|
|
3002
3482
|
record.session,
|
|
3003
3483
|
placement,
|
|
3484
|
+
client,
|
|
3004
3485
|
);
|
|
3005
3486
|
const networkRoute = await resolveActiveBrowserNetworkRouteLaunch({
|
|
3006
3487
|
deps: routeDeps,
|
|
@@ -3009,13 +3490,30 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3009
3490
|
placement: placement.placement,
|
|
3010
3491
|
});
|
|
3011
3492
|
try {
|
|
3493
|
+
await ensureHeadedBrowserDisplayStack(
|
|
3494
|
+
placement.session,
|
|
3495
|
+
record.session.headless,
|
|
3496
|
+
linkedComputer !== null,
|
|
3497
|
+
);
|
|
3498
|
+
if (
|
|
3499
|
+
recoverWorkingDirectory &&
|
|
3500
|
+
!(await touchBrowserSessionController(routeDeps.db, {
|
|
3501
|
+
accountId: grant.accountId,
|
|
3502
|
+
workspaceId: record.session.workspaceId,
|
|
3503
|
+
browserSessionId: record.session.id,
|
|
3504
|
+
controllerGeneration: binding.controllerGeneration,
|
|
3505
|
+
}))
|
|
3506
|
+
) {
|
|
3507
|
+
throw new BrowserSessionStateError("BrowserSession controller authority changed");
|
|
3508
|
+
}
|
|
3012
3509
|
await client.createSession({
|
|
3013
3510
|
browserSessionId: record.session.id,
|
|
3014
3511
|
controllerGeneration: binding.controllerGeneration,
|
|
3015
3512
|
tokenGeneration: record.tokenGeneration,
|
|
3016
3513
|
...tokens,
|
|
3017
3514
|
headed: !record.session.headless,
|
|
3018
|
-
transport
|
|
3515
|
+
transport,
|
|
3516
|
+
...(recoverWorkingDirectory ? { recoverExistingWorkingDirectory: true } : {}),
|
|
3019
3517
|
...(linkedComputer ? { linkedComputer } : {}),
|
|
3020
3518
|
...(networkRoute ? { networkRoute } : {}),
|
|
3021
3519
|
});
|
|
@@ -3059,7 +3557,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3059
3557
|
grant,
|
|
3060
3558
|
record,
|
|
3061
3559
|
placement,
|
|
3062
|
-
requestOrigin(context, deps.settings
|
|
3560
|
+
requestOrigin(context, deps.settings),
|
|
3063
3561
|
);
|
|
3064
3562
|
await endCapturedBrowserController(client, browserSessionId, binding);
|
|
3065
3563
|
await clearSuspendedBrowserSessionController(deps.db, {
|
|
@@ -3081,18 +3579,42 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3081
3579
|
}
|
|
3082
3580
|
}
|
|
3083
3581
|
|
|
3084
|
-
function
|
|
3582
|
+
function sourcePlacementChangedApiError(interactionResource: "browser_session"): ApiHttpError {
|
|
3583
|
+
return new ApiHttpError(409, {
|
|
3584
|
+
code: "conflict",
|
|
3585
|
+
message:
|
|
3586
|
+
"This browser belonged to a previous task placement and was retired. Open a new browser.",
|
|
3587
|
+
retryable: false,
|
|
3588
|
+
outcomeUnknown: false,
|
|
3589
|
+
details: {
|
|
3590
|
+
interactionResource,
|
|
3591
|
+
interactionFailureCode: "source_placement_changed",
|
|
3592
|
+
interactionLifecycle: "lost",
|
|
3593
|
+
},
|
|
3594
|
+
});
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
export function browserCreateInput(
|
|
3085
3598
|
grant: AccessGrant,
|
|
3086
3599
|
workspaceId: string,
|
|
3087
3600
|
request: CreateBrowserSessionRequestValue,
|
|
3088
3601
|
placement: InteractionPlacement,
|
|
3089
3602
|
) {
|
|
3603
|
+
if (request.storageMode === "ephemeral_context" && placement.kind !== "sandbox_group") {
|
|
3604
|
+
throw new BrowserControlUnsupportedError(
|
|
3605
|
+
"Ephemeral contexts require a managed sandbox placement",
|
|
3606
|
+
);
|
|
3607
|
+
}
|
|
3090
3608
|
const attached = placement.kind === "attached_device";
|
|
3091
3609
|
const external = placement.kind === "external_provider";
|
|
3092
3610
|
const engine = attached ? ("chrome" as const) : external ? ("external" as const) : request.engine;
|
|
3093
|
-
if (
|
|
3611
|
+
if (
|
|
3612
|
+
request.engine === "lightpanda" &&
|
|
3613
|
+
placement.kind !== "sandbox_group" &&
|
|
3614
|
+
placement.kind !== "connected_machine"
|
|
3615
|
+
) {
|
|
3094
3616
|
throw new BrowserControlUnsupportedError(
|
|
3095
|
-
"Lightpanda
|
|
3617
|
+
"Lightpanda requires a managed sandbox or Connected Machine browser placement",
|
|
3096
3618
|
);
|
|
3097
3619
|
}
|
|
3098
3620
|
return {
|
|
@@ -3105,11 +3627,13 @@ function browserCreateInput(
|
|
|
3105
3627
|
initialUrl: request.initialUrl ?? null,
|
|
3106
3628
|
placement,
|
|
3107
3629
|
driverId:
|
|
3108
|
-
|
|
3109
|
-
?
|
|
3110
|
-
:
|
|
3111
|
-
?
|
|
3112
|
-
:
|
|
3630
|
+
request.storageMode === "ephemeral_context"
|
|
3631
|
+
? EPHEMERAL_CHROMIUM_DRIVER_ID
|
|
3632
|
+
: engine === "lightpanda"
|
|
3633
|
+
? LIGHTPANDA_DRIVER_ID
|
|
3634
|
+
: external
|
|
3635
|
+
? EXTERNAL_BROWSER_DRIVER_ID
|
|
3636
|
+
: BROWSER_DRIVER_ID,
|
|
3113
3637
|
engine,
|
|
3114
3638
|
headless: attached ? false : request.headless,
|
|
3115
3639
|
identityId: request.identityId ?? null,
|
|
@@ -3134,7 +3658,21 @@ function browserCreateInput(
|
|
|
3134
3658
|
function browserRuntimeTransport(
|
|
3135
3659
|
session: BrowserSessionValue,
|
|
3136
3660
|
transport: PlacementBrowserTransport,
|
|
3661
|
+
placementInstanceId?: string,
|
|
3662
|
+
rootSecret?: string,
|
|
3137
3663
|
): PlacementBrowserTransport {
|
|
3664
|
+
if (browserSessionStorageMode(session) === "ephemeral_context") {
|
|
3665
|
+
if (transport.kind !== "managed" || !placementInstanceId || !rootSecret) {
|
|
3666
|
+
throw new BrowserControlUnsupportedError(
|
|
3667
|
+
"Ephemeral contexts cannot recover or change their placement",
|
|
3668
|
+
);
|
|
3669
|
+
}
|
|
3670
|
+
return {
|
|
3671
|
+
kind: "managed",
|
|
3672
|
+
engine: "chromium",
|
|
3673
|
+
ephemeralPartition: ephemeralBrowserPartition(session, placementInstanceId, rootSecret),
|
|
3674
|
+
};
|
|
3675
|
+
}
|
|
3138
3676
|
if (transport.kind !== "managed") return transport;
|
|
3139
3677
|
return {
|
|
3140
3678
|
kind: "managed",
|
|
@@ -3196,6 +3734,11 @@ function assertCreateReplay(
|
|
|
3196
3734
|
"BrowserSession create operation is bound to another placement",
|
|
3197
3735
|
);
|
|
3198
3736
|
}
|
|
3737
|
+
if (request.storageMode !== browserSessionStorageMode(session)) {
|
|
3738
|
+
throw new BrowserSessionOperationConflictError(
|
|
3739
|
+
"BrowserSession create operation is bound to another storage mode",
|
|
3740
|
+
);
|
|
3741
|
+
}
|
|
3199
3742
|
const expectedEngine =
|
|
3200
3743
|
session.placement.kind === "attached_device"
|
|
3201
3744
|
? "chrome"
|
|
@@ -3229,11 +3772,39 @@ function assertCreateReplay(
|
|
|
3229
3772
|
}
|
|
3230
3773
|
}
|
|
3231
3774
|
|
|
3232
|
-
async function
|
|
3775
|
+
async function ensureHeadedBrowserDisplayStack(
|
|
3776
|
+
session: BrowserControlPlacementSession,
|
|
3777
|
+
headless: boolean,
|
|
3778
|
+
linkedComputer: boolean,
|
|
3779
|
+
): Promise<void> {
|
|
3780
|
+
if (
|
|
3781
|
+
!browserNeedsStandaloneDisplayStack({
|
|
3782
|
+
headless,
|
|
3783
|
+
linkedComputer,
|
|
3784
|
+
nativeBrowserControl: typeof session.ensureBrowserControl === "function",
|
|
3785
|
+
})
|
|
3786
|
+
)
|
|
3787
|
+
return;
|
|
3788
|
+
if (typeof session.exec !== "function" && typeof session.execCommand !== "function") return;
|
|
3789
|
+
await ensureDisplayStack(session, {
|
|
3790
|
+
telemetryContext: { callerKind: "viewer" },
|
|
3791
|
+
});
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3794
|
+
export function browserNeedsStandaloneDisplayStack(input: {
|
|
3795
|
+
headless: boolean;
|
|
3796
|
+
linkedComputer: boolean;
|
|
3797
|
+
nativeBrowserControl: boolean;
|
|
3798
|
+
}): boolean {
|
|
3799
|
+
return !input.headless && !input.linkedComputer && !input.nativeBrowserControl;
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
async function ensureLinkedComputerController(
|
|
3233
3803
|
deps: ApiRouteDeps,
|
|
3234
3804
|
grant: AccessGrant,
|
|
3235
3805
|
browser: BrowserSessionValue,
|
|
3236
3806
|
placement: BrowserPlacement,
|
|
3807
|
+
client: BrowserControlClient,
|
|
3237
3808
|
): Promise<{ computerSessionId: string; controllerGeneration: string } | null> {
|
|
3238
3809
|
if (!browser.linkedComputerSessionId) return null;
|
|
3239
3810
|
const record = await getComputerSessionControlRecord(deps.db, {
|
|
@@ -3251,10 +3822,42 @@ async function requireLinkedComputerBinding(
|
|
|
3251
3822
|
"Linked ComputerSession is not active on the browser placement",
|
|
3252
3823
|
);
|
|
3253
3824
|
}
|
|
3254
|
-
|
|
3825
|
+
const reference = {
|
|
3255
3826
|
computerSessionId: record.session.id,
|
|
3256
3827
|
controllerGeneration: record.session.controller.controllerGeneration,
|
|
3257
3828
|
};
|
|
3829
|
+
const tokens = deriveComputerSessionControllerTokens({
|
|
3830
|
+
rootSecret: browserAuthorityRoot(deps),
|
|
3831
|
+
accountId: grant.accountId,
|
|
3832
|
+
workspaceId: browser.workspaceId,
|
|
3833
|
+
placement: record.session.placement,
|
|
3834
|
+
placementInstanceId: placement.placementInstanceId,
|
|
3835
|
+
computerSessionId: record.session.id,
|
|
3836
|
+
controllerGeneration: record.session.controller.controllerGeneration,
|
|
3837
|
+
tokenGeneration: record.tokenGeneration,
|
|
3838
|
+
});
|
|
3839
|
+
const sessionClient = client.computerSessionClient({ reference, ...tokens });
|
|
3840
|
+
try {
|
|
3841
|
+
await sessionClient.heartbeat();
|
|
3842
|
+
} catch (error) {
|
|
3843
|
+
if (!isMissingLinkedComputerControllerSession(error)) throw error;
|
|
3844
|
+
await client.createComputerSession({
|
|
3845
|
+
...reference,
|
|
3846
|
+
tokenGeneration: record.tokenGeneration,
|
|
3847
|
+
...tokens,
|
|
3848
|
+
});
|
|
3849
|
+
}
|
|
3850
|
+
return reference;
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
function isMissingLinkedComputerControllerSession(error: unknown): boolean {
|
|
3854
|
+
if (!(error instanceof BrowserControlRequestError)) return false;
|
|
3855
|
+
if (error.status === 401 && error.error.code === "permission_denied") return true;
|
|
3856
|
+
return (
|
|
3857
|
+
error.status === 404 &&
|
|
3858
|
+
error.error.code === "resource_not_found" &&
|
|
3859
|
+
["computer session not found", "computer session is not active"].includes(error.error.message)
|
|
3860
|
+
);
|
|
3258
3861
|
}
|
|
3259
3862
|
|
|
3260
3863
|
async function requireBrowserRevisionRestoreAuthority(
|
|
@@ -3474,6 +4077,7 @@ async function ensureInteractionHolder(
|
|
|
3474
4077
|
if (!placement.lease?.instanceId) {
|
|
3475
4078
|
throw new BrowserSessionStateError("BrowserSession lease placement is unavailable");
|
|
3476
4079
|
}
|
|
4080
|
+
const sandboxRuntime = await resolveSessionSandboxRuntime(deps.db, deps.settings, sourceSession);
|
|
3477
4081
|
const acquired = await acquireLease(deps.db, {
|
|
3478
4082
|
accountId: grant.accountId,
|
|
3479
4083
|
workspaceId: sourceSession.workspaceId,
|
|
@@ -3482,9 +4086,14 @@ async function ensureInteractionHolder(
|
|
|
3482
4086
|
holderId: interactionHolderId(browserSessionId),
|
|
3483
4087
|
subjectId: sourceSession.id,
|
|
3484
4088
|
backend: placement.lease.backend,
|
|
4089
|
+
warmBilling: {
|
|
4090
|
+
mode: deps.settings.sandboxWarmBillingMode,
|
|
4091
|
+
rateMicrosPerSecond: sandboxWarmRateMicrosPerSecond(deps.settings, placement.lease.backend),
|
|
4092
|
+
},
|
|
3485
4093
|
os: placement.lease.os,
|
|
3486
|
-
image:
|
|
3487
|
-
|
|
4094
|
+
image: sandboxRuntime.image,
|
|
4095
|
+
imagePolicy: "new_creates_only",
|
|
4096
|
+
rigVersionId: sourceSession.rigVersionId,
|
|
3488
4097
|
leaseTtlMs: deps.settings.sandboxLeaseTtlMs,
|
|
3489
4098
|
expectedEpoch: placement.lease.leaseEpoch,
|
|
3490
4099
|
waitSignal,
|
|
@@ -3506,6 +4115,11 @@ async function ensureInteractionHolder(
|
|
|
3506
4115
|
).catch(() => undefined);
|
|
3507
4116
|
throw new BrowserSessionStateError("BrowserSession placement fence changed; retry");
|
|
3508
4117
|
}
|
|
4118
|
+
await renewSandboxProviderExpiration({
|
|
4119
|
+
backend: acquired.lease.backend as ApiRouteDeps["settings"]["sandboxBackend"],
|
|
4120
|
+
settings: deps.settings,
|
|
4121
|
+
instanceId: acquired.lease.instanceId,
|
|
4122
|
+
}).catch(() => false);
|
|
3509
4123
|
return true;
|
|
3510
4124
|
}
|
|
3511
4125
|
|
|
@@ -3752,6 +4366,7 @@ async function settleBrowserSessionSuspensionCaptureFailure(
|
|
|
3752
4366
|
return await failBrowserSessionSuspension(deps.db, {
|
|
3753
4367
|
...input,
|
|
3754
4368
|
...(outcomeUnknown ? { state: "outcome_unknown" as const } : {}),
|
|
4369
|
+
missingControllerSession: error.status === 404 && isMissingBrowserControllerSession(error),
|
|
3755
4370
|
error: error.error,
|
|
3756
4371
|
});
|
|
3757
4372
|
}
|
|
@@ -3792,9 +4407,9 @@ async function authorizeSourceSession(
|
|
|
3792
4407
|
grant: AccessGrant,
|
|
3793
4408
|
sessionId: string,
|
|
3794
4409
|
operation: SessionAuthorizationOperation,
|
|
3795
|
-
): Promise<
|
|
4410
|
+
): Promise<ResolvedSessionAuthorization | null> {
|
|
3796
4411
|
try {
|
|
3797
|
-
await requireSessionAuthorization(deps, grant, {
|
|
4412
|
+
return await requireSessionAuthorization(deps, grant, {
|
|
3798
4413
|
sessionId,
|
|
3799
4414
|
operation,
|
|
3800
4415
|
surface: "http",
|
|
@@ -3889,6 +4504,66 @@ function optionalBoundedInteger(
|
|
|
3889
4504
|
return parsed;
|
|
3890
4505
|
}
|
|
3891
4506
|
|
|
4507
|
+
export function parseBrowserScreenshotOptions(
|
|
4508
|
+
query: URLSearchParams,
|
|
4509
|
+
): BrowserControlScreenshotOptions {
|
|
4510
|
+
for (const key of ["fullPage", "format", "quality"]) {
|
|
4511
|
+
if (query.getAll(key).length > 1) {
|
|
4512
|
+
throw new HTTPException(400, { message: `duplicate browser screenshot ${key}` });
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
const fullPage = query.get("fullPage");
|
|
4516
|
+
if (fullPage !== null && fullPage !== "true" && fullPage !== "false") {
|
|
4517
|
+
throw new HTTPException(400, { message: "invalid browser screenshot fullPage" });
|
|
4518
|
+
}
|
|
4519
|
+
const format = query.get("format");
|
|
4520
|
+
if (format !== null && format !== "jpeg" && format !== "png") {
|
|
4521
|
+
throw new HTTPException(400, { message: "invalid browser screenshot format" });
|
|
4522
|
+
}
|
|
4523
|
+
const quality = query.get("quality");
|
|
4524
|
+
const parsedQuality = quality === null ? null : optionalBoundedInteger(quality, 1, 100);
|
|
4525
|
+
if (quality !== null && parsedQuality === null) {
|
|
4526
|
+
throw new HTTPException(400, { message: "invalid browser screenshot quality" });
|
|
4527
|
+
}
|
|
4528
|
+
return {
|
|
4529
|
+
...(fullPage === null ? {} : { fullPage: fullPage === "true" }),
|
|
4530
|
+
...(format === null ? {} : { format }),
|
|
4531
|
+
...(parsedQuality === null ? {} : { quality: parsedQuality }),
|
|
4532
|
+
};
|
|
4533
|
+
}
|
|
4534
|
+
|
|
4535
|
+
/** Keep a definite read timeout useful without publishing driver diagnostics. */
|
|
4536
|
+
export function browserScreenshotError(error: unknown): unknown {
|
|
4537
|
+
if (error instanceof BrowserControlRequestError && error.error.code === "timeout") {
|
|
4538
|
+
const failure = new ApiHttpError(504, {
|
|
4539
|
+
code: "upstream_unavailable",
|
|
4540
|
+
message:
|
|
4541
|
+
"This tab did not produce a screenshot in time. Other browser operations may still work.",
|
|
4542
|
+
retryable: error.retryable,
|
|
4543
|
+
outcomeUnknown: false,
|
|
4544
|
+
});
|
|
4545
|
+
failure.cause = error;
|
|
4546
|
+
return failure;
|
|
4547
|
+
}
|
|
4548
|
+
return error;
|
|
4549
|
+
}
|
|
4550
|
+
|
|
4551
|
+
export function browserScreenshotResponse(
|
|
4552
|
+
frame: Pick<BrowserControlFrame, "data" | "mediaType" | "metadataHeader">,
|
|
4553
|
+
): Response {
|
|
4554
|
+
// Node Buffers can be views into a larger backing allocation. Copy exactly
|
|
4555
|
+
// the image bytes before constructing the Response.
|
|
4556
|
+
return new Response(Uint8Array.from(frame.data).buffer, {
|
|
4557
|
+
status: 200,
|
|
4558
|
+
headers: {
|
|
4559
|
+
"cache-control": "no-store",
|
|
4560
|
+
"content-type": frame.mediaType,
|
|
4561
|
+
"x-opengeni-browser-frame": frame.metadataHeader,
|
|
4562
|
+
...USER_CONTENT_SECURITY_HEADERS,
|
|
4563
|
+
},
|
|
4564
|
+
});
|
|
4565
|
+
}
|
|
4566
|
+
|
|
3892
4567
|
function isUuid(value: unknown): value is string {
|
|
3893
4568
|
return (
|
|
3894
4569
|
typeof value === "string" &&
|
|
@@ -3896,32 +4571,8 @@ function isUuid(value: unknown): value is string {
|
|
|
3896
4571
|
);
|
|
3897
4572
|
}
|
|
3898
4573
|
|
|
3899
|
-
function requestOrigin(context: Context,
|
|
3900
|
-
return
|
|
3901
|
-
}
|
|
3902
|
-
|
|
3903
|
-
export function validateBrowserRequestOrigin(
|
|
3904
|
-
value: string | undefined,
|
|
3905
|
-
allowedPattern: string,
|
|
3906
|
-
): string | null {
|
|
3907
|
-
if (!value) return null;
|
|
3908
|
-
let url: URL;
|
|
3909
|
-
try {
|
|
3910
|
-
url = new URL(value);
|
|
3911
|
-
} catch {
|
|
3912
|
-
throw new HTTPException(400, { message: "invalid request origin" });
|
|
3913
|
-
}
|
|
3914
|
-
if (
|
|
3915
|
-
url.origin === "null" ||
|
|
3916
|
-
(url.protocol !== "http:" && url.protocol !== "https:") ||
|
|
3917
|
-
url.origin !== value
|
|
3918
|
-
) {
|
|
3919
|
-
throw new HTTPException(400, { message: "invalid request origin" });
|
|
3920
|
-
}
|
|
3921
|
-
if (!allowedCorsOrigin(allowedPattern, url.origin)) {
|
|
3922
|
-
throw new HTTPException(403, { message: "request origin is not allowed" });
|
|
3923
|
-
}
|
|
3924
|
-
return url.origin;
|
|
4574
|
+
function requestOrigin(context: Context, settings: ApiRouteDeps["settings"]): string | null {
|
|
4575
|
+
return validateInteractionRequestOrigin(context.req.header("origin"), settings);
|
|
3925
4576
|
}
|
|
3926
4577
|
|
|
3927
4578
|
export function interactionActorForGrant(
|
|
@@ -3949,6 +4600,20 @@ function assertPlacementInstance(expected: string | null, actual: string): void
|
|
|
3949
4600
|
}
|
|
3950
4601
|
}
|
|
3951
4602
|
|
|
4603
|
+
/** End keeps the original controller token fence so a later Chrome generation
|
|
4604
|
+
* can still dispose the stale browserd session. */
|
|
4605
|
+
function attachedEndPlacementInstanceId(
|
|
4606
|
+
operation: ChannelAOperation,
|
|
4607
|
+
expectedPlacementInstanceId: string | null,
|
|
4608
|
+
liveGeneration: string,
|
|
4609
|
+
): string {
|
|
4610
|
+
if (operation === "browser.end" && expectedPlacementInstanceId) {
|
|
4611
|
+
return expectedPlacementInstanceId;
|
|
4612
|
+
}
|
|
4613
|
+
assertPlacementInstance(expectedPlacementInstanceId, liveGeneration);
|
|
4614
|
+
return liveGeneration;
|
|
4615
|
+
}
|
|
4616
|
+
|
|
3952
4617
|
function isTerminalOperation(state: string): boolean {
|
|
3953
4618
|
return state === "completed" || state === "failed" || state === "outcome_unknown";
|
|
3954
4619
|
}
|
|
@@ -4285,6 +4950,40 @@ function browserUploadFileIds(action: BrowserActionRequestValue["action"]): stri
|
|
|
4285
4950
|
];
|
|
4286
4951
|
}
|
|
4287
4952
|
|
|
4953
|
+
/** Resolve Drive authority from the same immutable session authorization that
|
|
4954
|
+
* admitted the browser action. Agent-attempt subjects are technical worker
|
|
4955
|
+
* identities; their frozen initiating human is the only personal Drive
|
|
4956
|
+
* principal. Pure service attempts retain null and can read only ordinary
|
|
4957
|
+
* workspace files through the database predicate. */
|
|
4958
|
+
export function browserFileAuthoritySubjectId(
|
|
4959
|
+
grant: AccessGrant,
|
|
4960
|
+
authorization: ResolvedSessionAuthorization | null,
|
|
4961
|
+
): string | null {
|
|
4962
|
+
if (!authorization) return grant.principalKind === "agent_attempt" ? null : grant.subjectId;
|
|
4963
|
+
return authorization.actor.kind === "agent_attempt"
|
|
4964
|
+
? authorization.actor.initiatingHumanSubjectId
|
|
4965
|
+
: authorization.actor.subjectId;
|
|
4966
|
+
}
|
|
4967
|
+
|
|
4968
|
+
/** The batch authority query intentionally omits every unauthorized file. Any
|
|
4969
|
+
* omission therefore fails the whole upload before an object-storage URL is
|
|
4970
|
+
* minted, including mixed ordinary/Drive mappings and partially authorized
|
|
4971
|
+
* batches. */
|
|
4972
|
+
export function requireAuthorizedBrowserUploadFiles(
|
|
4973
|
+
workspaceFileIds: readonly string[],
|
|
4974
|
+
authorizedFiles: readonly FileAsset[],
|
|
4975
|
+
): FileAsset[] {
|
|
4976
|
+
const byId = new Map(authorizedFiles.map((file) => [file.id, file]));
|
|
4977
|
+
return workspaceFileIds.map((fileId) => {
|
|
4978
|
+
const file = byId.get(fileId);
|
|
4979
|
+
if (!file) throw new HTTPException(404, { message: "workspace file not found" });
|
|
4980
|
+
if (file.status !== "ready") {
|
|
4981
|
+
throw new HTTPException(409, { message: "workspace file is not ready" });
|
|
4982
|
+
}
|
|
4983
|
+
return file;
|
|
4984
|
+
});
|
|
4985
|
+
}
|
|
4986
|
+
|
|
4288
4987
|
function browserUploadFilename(value: string): string {
|
|
4289
4988
|
const normalized = value
|
|
4290
4989
|
.trim()
|
|
@@ -4370,9 +5069,14 @@ async function finalizeBrowserDownloadFile(
|
|
|
4370
5069
|
if (upload.expiresAt.getTime() <= Date.now()) {
|
|
4371
5070
|
throw new InteractionResourceStateError("Browser download file upload authority expired");
|
|
4372
5071
|
}
|
|
4373
|
-
const
|
|
4374
|
-
|
|
5072
|
+
const pendingFile = upload.file;
|
|
5073
|
+
const head = await retryWhileMissing(async () => {
|
|
5074
|
+
if (!(await objectStorage.fileExists(pendingFile))) return null;
|
|
5075
|
+
return await objectStorage.headFile(pendingFile);
|
|
4375
5076
|
});
|
|
5077
|
+
if (!head) {
|
|
5078
|
+
throw new HTTPException(503, { message: "browser download object is not available" });
|
|
5079
|
+
}
|
|
4376
5080
|
if (
|
|
4377
5081
|
Number(head.ContentLength ?? -1) !== upload.file.sizeBytes ||
|
|
4378
5082
|
(head.ContentType !== undefined && head.ContentType !== upload.file.contentType) ||
|
|
@@ -4413,7 +5117,11 @@ async function recordBrowserDownloadFileUsage(
|
|
|
4413
5117
|
}
|
|
4414
5118
|
|
|
4415
5119
|
function browserRouteError(error: unknown): HTTPException {
|
|
5120
|
+
const connectedMachineError = interactionControlApiError(error, "browser");
|
|
5121
|
+
if (connectedMachineError) return connectedMachineError;
|
|
4416
5122
|
if (error instanceof HTTPException) return error;
|
|
5123
|
+
if (error instanceof SandboxPaidComputeAdmissionError)
|
|
5124
|
+
return new HTTPException(402, { message: error.message, cause: error });
|
|
4417
5125
|
if (error instanceof BrowserSessionNotFoundError) {
|
|
4418
5126
|
return new HTTPException(404, { message: error.message, cause: error });
|
|
4419
5127
|
}
|