@opengeni/api-router 0.30.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-grant-rls.d.ts +3 -0
- package/dist/admission-trace.d.ts +8 -0
- package/dist/api-websocket.d.ts +18 -0
- package/dist/app.d.ts +50 -2
- package/dist/app.js +12 -1
- package/dist/artifact-catalog-cursor.d.ts +27 -0
- package/dist/auth/managed-auth-attempt-context.d.ts +10 -0
- package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +6 -0
- package/dist/auth/managed-auth.d.ts +102 -3
- package/dist/auth/managed-email.d.ts +29 -0
- package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
- package/dist/auth/new-signups-gate.d.ts +23 -0
- package/dist/auth/organization-user-setup.d.ts +80 -0
- package/dist/auth/signup-funnel-metrics.d.ts +44 -0
- package/dist/browser-controller-authority.d.ts +1 -61
- package/dist/browser-ephemeral.d.ts +5 -0
- package/dist/browser-state-authority.d.ts +1 -27
- package/dist/chunk-6TZ5LG3O.js +123948 -0
- package/dist/chunk-6TZ5LG3O.js.map +1 -0
- package/dist/chunk-OGQDXVXY.js +18 -0
- package/dist/chunk-OGQDXVXY.js.map +1 -0
- package/dist/claude-subscription-account-conflicts.d.ts +1 -0
- package/dist/claude-subscription-account-usage.d.ts +39 -0
- package/dist/claude-subscription-oauth.d.ts +25 -0
- package/dist/claude-subscription-profile.d.ts +9 -0
- package/dist/claude-subscription-usage.d.ts +44 -0
- package/dist/claude-workspace-connection.d.ts +15 -0
- package/dist/codemode.d.ts +32 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-ownership.d.ts +54 -0
- package/dist/controller-data-plane.d.ts +32 -0
- package/dist/editable-artifact-source-session.d.ts +3 -0
- package/dist/editable-artifact-websocket.d.ts +10 -18
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/fatal-process-boundary.d.ts +25 -0
- package/dist/github-access.d.ts +31 -2
- package/dist/http/acting-person.d.ts +24 -0
- package/dist/http/api-contract-compat.d.ts +19 -0
- package/dist/http/api-error.d.ts +27 -1
- package/dist/http/cors.d.ts +17 -0
- package/dist/http/deprecation.d.ts +42 -0
- package/dist/http/interaction-control-error.d.ts +13 -0
- package/dist/http/keyed-admission.d.ts +12 -0
- package/dist/http/local-browser-boundary.d.ts +72 -0
- package/dist/http/metrics-listener.d.ts +22 -0
- package/dist/http/path-identifier.d.ts +17 -0
- package/dist/http/registered-route-label.d.ts +16 -0
- package/dist/http/request-body.d.ts +27 -0
- package/dist/http/request-source.d.ts +52 -0
- package/dist/http/sse.d.ts +17 -6
- package/dist/http/unmatched-route.d.ts +18 -0
- package/dist/http/user-content.d.ts +36 -0
- package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.js +1091 -162
- package/dist/index.js.map +1 -1
- package/dist/integration-endpoint-test.d.ts +20 -0
- package/dist/integrations/api-integrations.d.ts +2 -2
- package/dist/integrations/atlassian.d.ts +29 -3
- package/dist/integrations/connect-authority.d.ts +1 -0
- package/dist/integrations/connect-callback-return.d.ts +6 -0
- package/dist/integrations/fiken.d.ts +16 -2
- package/dist/integrations/github-app-connect.d.ts +47 -0
- package/dist/integrations/github-installation-proof.d.ts +3 -0
- package/dist/integrations/github-lens-connect.d.ts +116 -0
- package/dist/integrations/github-skill-source.d.ts +1 -5
- package/dist/integrations/google-drive.d.ts +24 -0
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/marketplace-plugin.d.ts +2691 -0
- package/dist/integrations/oauth-client.d.ts +178 -18
- package/dist/integrations/oauth-profiles.d.ts +222 -0
- package/dist/integrations/oauth-return-path.d.ts +1 -0
- package/dist/integrations/personal-github-repositories.d.ts +84 -0
- package/dist/integrations/personal-github.d.ts +42 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +19 -1
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +367 -12
- package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
- package/dist/integrations/slack-install.d.ts +20 -0
- package/dist/integrations/slack-interactions.d.ts +107 -9
- package/dist/integrations/slack-mrkdwn.d.ts +41 -0
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/slack-session-defaults.d.ts +58 -0
- package/dist/integrations/slack-task-file-upload.d.ts +36 -0
- package/dist/integrations/social-oauth.d.ts +7 -0
- package/dist/interaction-agent-access.d.ts +15 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/interaction-metrics.d.ts +2 -0
- package/dist/mcp/action-catalog-types.d.ts +9 -0
- package/dist/mcp/action-catalog.gen.d.ts +2 -0
- package/dist/mcp/capability-account-readiness.d.ts +7 -0
- package/dist/mcp/child-read-evidence.d.ts +9 -0
- package/dist/mcp/command-read.d.ts +24 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/contract-input.d.ts +11 -0
- package/dist/mcp/documents.d.ts +7 -5
- package/dist/mcp/files.d.ts +2 -2
- package/dist/mcp/first-party-tool-permissions.d.ts +650 -0
- package/dist/mcp/knowledge-entries.d.ts +5 -0
- package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
- package/dist/mcp/receipts.d.ts +9 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-input.d.ts +1189 -0
- package/dist/mcp/scheduled-task-view.d.ts +22 -4
- package/dist/mcp/server.d.ts +255 -3
- package/dist/mcp/session-event-view.d.ts +94 -0
- package/dist/mcp/session-view.d.ts +66 -2
- package/dist/mcp/session-wait.d.ts +174 -0
- package/dist/mcp-oauth.d.ts +52 -0
- package/dist/model-catalog.d.ts +11 -31
- package/dist/observability.d.ts +3 -1
- package/dist/organization-mcp.d.ts +52 -0
- package/dist/organization-recovery-notifications.d.ts +41 -0
- package/dist/routes/analytics-consent.d.ts +26 -0
- package/dist/routes/api-integrations.d.ts +25 -0
- package/dist/routes/api-keys.d.ts +17 -0
- package/dist/routes/artifact-catalog.d.ts +6 -0
- package/dist/routes/automations.d.ts +19 -0
- package/dist/routes/billing.d.ts +49 -0
- package/dist/routes/browser-sessions.d.ts +59 -2
- package/dist/routes/claude-subscription-accounts.d.ts +3 -0
- package/dist/routes/claude-subscription-oauth.d.ts +3 -0
- package/dist/routes/client-errors.d.ts +146 -0
- package/dist/routes/codex.d.ts +76 -2
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/computer-sessions.d.ts +14 -0
- package/dist/routes/connect.d.ts +5 -0
- package/dist/routes/editable-artifacts.d.ts +2 -0
- package/dist/routes/external-identity-links.d.ts +6 -0
- package/dist/routes/feedback.d.ts +3 -0
- package/dist/routes/github-browser-pages.d.ts +12 -0
- package/dist/routes/github.d.ts +2 -0
- package/dist/routes/insights-response-cache.d.ts +10 -0
- package/dist/routes/insights-usage.d.ts +12 -0
- package/dist/routes/insights.d.ts +30 -1
- package/dist/routes/knowledge.d.ts +3 -0
- package/dist/routes/managed-auth-session-sets.d.ts +26 -0
- package/dist/routes/managed-onboarding.d.ts +29 -0
- package/dist/routes/managed-sign-in-methods.d.ts +6 -0
- package/dist/routes/model-connection-access.d.ts +3 -0
- package/dist/routes/organization-integration-policy.d.ts +3 -0
- package/dist/routes/organization-integrations.d.ts +6 -0
- package/dist/routes/organization-mcp-connections.d.ts +3 -0
- package/dist/routes/organization-model-providers.d.ts +3 -0
- package/dist/routes/organization-recovery.d.ts +19 -0
- package/dist/routes/organization-service-accounts.d.ts +3 -0
- package/dist/routes/organization-sessions.d.ts +26 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/session-artifact-associations.d.ts +4 -0
- package/dist/routes/session-history-imports.d.ts +9 -0
- package/dist/routes/sessions.d.ts +44 -6
- package/dist/routes/skill-content.d.ts +4 -0
- package/dist/routes/skill-install-authority.d.ts +6 -0
- package/dist/routes/subscription-account-pools.d.ts +34 -0
- package/dist/routes/subscription-pool-access.d.ts +16 -0
- package/dist/routes/supergrok.d.ts +6 -1
- package/dist/routes/usage-allowances.d.ts +12 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-artifacts.d.ts +2 -1
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-integrations.d.ts +35 -0
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspace-model-providers.d.ts +4 -0
- package/dist/routes/workspaces.d.ts +29 -0
- package/dist/sandbox/auth-callout.d.ts +8 -0
- package/dist/sandbox/channel-a.d.ts +19 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +9 -1
- package/dist/sandbox/machines.d.ts +5 -2
- package/dist/sandbox/metrics-ingestion.d.ts +28 -20
- package/dist/sandbox/rematerialize.d.ts +2 -0
- package/dist/sandbox/session-attach-variable-sets.d.ts +30 -0
- package/dist/sandbox/viewer.d.ts +26 -1
- package/dist/sandbox-file-artifacts.d.ts +50 -0
- package/dist/scheduled-task-deletion.d.ts +13 -0
- package/dist/session-create-observability.d.ts +5 -0
- package/dist/session-event-view-MPS2OZD3.js +292 -0
- package/dist/session-event-view-MPS2OZD3.js.map +1 -0
- package/dist/site-session-origin.d.ts +13 -0
- package/dist/site-uploads.d.ts +41 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/dist/temporal-schedule-sync.d.ts +26 -0
- package/dist/transcription/providers/xai-subscription.d.ts +1 -0
- package/dist/transcription/service.d.ts +2 -1
- package/dist/work-discovery-observability.d.ts +33 -0
- package/dist/workspace-artifact-content.d.ts +28 -0
- package/dist/workspace-artifact-provenance.d.ts +7 -0
- package/dist/workspace-delete-observability.d.ts +10 -0
- package/dist/workspace-deletion.d.ts +6 -0
- package/dist/workspace-state-projection.d.ts +3 -5
- package/dist/workspace-tool-gateway-observability.d.ts +17 -0
- package/dist/workspace-tool-gateway.d.ts +123 -0
- package/dist/workspace-webhook-dispatch.d.ts +26 -0
- package/package.json +23 -19
- package/src/access-grant-rls.ts +75 -0
- package/src/admission-trace.ts +14 -0
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +1967 -252
- package/src/artifact-catalog-cursor.ts +60 -0
- package/src/auth/managed-auth-attempt-context.ts +70 -0
- package/src/auth/managed-auth-rate-limits.ts +252 -0
- package/src/auth/managed-auth-session-adapter.ts +222 -0
- package/src/auth/managed-auth.ts +536 -60
- package/src/auth/managed-email.ts +174 -0
- package/src/auth/managed-sign-in-notifications.ts +81 -0
- package/src/auth/new-signups-gate.ts +49 -0
- package/src/auth/organization-user-setup.ts +307 -0
- package/src/auth/signup-funnel-metrics.ts +164 -0
- package/src/browser-controller-authority.ts +1 -186
- package/src/browser-ephemeral.ts +39 -0
- package/src/browser-state-authority.ts +1 -236
- package/src/claude-subscription-account-conflicts.ts +12 -0
- package/src/claude-subscription-account-usage.ts +48 -0
- package/src/claude-subscription-oauth.ts +362 -0
- package/src/claude-subscription-profile.ts +73 -0
- package/src/claude-subscription-usage.ts +97 -0
- package/src/claude-workspace-connection.ts +82 -0
- package/src/codemode.ts +194 -30
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-ownership.ts +199 -0
- package/src/controller-data-plane.ts +81 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +28 -3
- package/src/editable-artifact-production.ts +23 -7
- package/src/editable-artifact-source-session.ts +123 -0
- package/src/editable-artifact-websocket.ts +27 -21
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/fatal-process-boundary.ts +246 -0
- package/src/gateway-realtime.ts +1 -1
- package/src/github-access.ts +147 -5
- package/src/http/acting-person.ts +71 -0
- package/src/http/api-contract-compat.ts +49 -0
- package/src/http/api-error.ts +115 -1
- package/src/http/auth.ts +48 -0
- package/src/http/cors.ts +67 -0
- package/src/http/deprecation.ts +118 -0
- package/src/http/interaction-control-error.ts +164 -0
- package/src/http/keyed-admission.ts +29 -0
- package/src/http/local-browser-boundary.ts +343 -0
- package/src/http/metrics-listener.ts +57 -0
- package/src/http/path-identifier.ts +54 -0
- package/src/http/registered-route-label.ts +34 -0
- package/src/http/request-body.ts +127 -0
- package/src/http/request-source.ts +178 -0
- package/src/http/sse.ts +486 -132
- package/src/http/unmatched-route.ts +60 -0
- package/src/http/user-content.ts +158 -0
- package/src/http/websocket-upgrade-dispatch.ts +29 -0
- package/src/index.ts +157 -52
- package/src/integration-endpoint-test.ts +291 -0
- package/src/integrations/api-integrations.ts +98 -11
- package/src/integrations/atlassian.ts +474 -136
- package/src/integrations/connect-authority.ts +2 -0
- package/src/integrations/connect-callback-return.ts +86 -0
- package/src/integrations/fiken.ts +354 -64
- package/src/integrations/github-app-connect.ts +435 -0
- package/src/integrations/github-installation-proof.ts +60 -0
- package/src/integrations/github-lens-connect.ts +76 -0
- package/src/integrations/github-skill-source.ts +2 -142
- package/src/integrations/google-drive.ts +717 -225
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/marketplace-plugin.ts +48 -0
- package/src/integrations/oauth-client.ts +1356 -459
- package/src/integrations/oauth-profiles.ts +537 -0
- package/src/integrations/oauth-return-path.ts +18 -0
- package/src/integrations/personal-github-repositories.ts +674 -0
- package/src/integrations/personal-github.ts +825 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +250 -22
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +963 -239
- package/src/integrations/slack-file-upload-flow.ts +147 -0
- package/src/integrations/slack-install.ts +83 -0
- package/src/integrations/slack-interactions.ts +3167 -474
- package/src/integrations/slack-mrkdwn.ts +193 -0
- package/src/integrations/slack-routing.ts +337 -0
- package/src/integrations/slack-session-defaults.ts +224 -0
- package/src/integrations/slack-task-file-upload.ts +323 -0
- package/src/integrations/social-oauth.ts +213 -15
- package/src/interaction-agent-access.ts +48 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/interaction-metrics.ts +30 -0
- package/src/mcp/action-catalog-types.ts +9 -0
- package/src/mcp/action-catalog.gen.ts +6696 -0
- package/src/mcp/capability-account-readiness.ts +17 -0
- package/src/mcp/child-read-evidence.ts +19 -0
- package/src/mcp/command-read.ts +109 -0
- package/src/mcp/company-profile-agent-admin.ts +198 -0
- package/src/mcp/contract-input.ts +141 -0
- package/src/mcp/documents.ts +65 -377
- package/src/mcp/editable-artifacts.ts +12 -6
- package/src/mcp/files.ts +80 -43
- package/src/mcp/first-party-tool-permissions.ts +305 -0
- package/src/mcp/knowledge-entries.ts +322 -0
- package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
- package/src/mcp/receipts.ts +34 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-input.ts +76 -0
- package/src/mcp/scheduled-task-view.ts +44 -3
- package/src/mcp/server.ts +3030 -1267
- package/src/mcp/session-event-view.ts +383 -0
- package/src/mcp/session-view.ts +84 -3
- package/src/mcp/session-wait.ts +665 -0
- package/src/mcp-oauth.ts +1123 -0
- package/src/model-catalog.ts +91 -333
- package/src/observability.ts +15 -1
- package/src/organization-mcp.ts +397 -0
- package/src/organization-recovery-notifications.ts +103 -0
- package/src/routes/analytics-consent.ts +123 -0
- package/src/routes/api-integrations.ts +364 -74
- package/src/routes/api-keys.ts +336 -10
- package/src/routes/artifact-catalog.ts +279 -0
- package/src/routes/automations.ts +768 -0
- package/src/routes/billing.ts +461 -46
- package/src/routes/browser-sessions.ts +780 -112
- package/src/routes/canonical-human-identities.ts +90 -14
- package/src/routes/capabilities.ts +107 -6
- package/src/routes/channels.ts +20 -3
- package/src/routes/claude-subscription-accounts.ts +334 -0
- package/src/routes/claude-subscription-oauth.ts +142 -0
- package/src/routes/client-errors.ts +362 -0
- package/src/routes/codex.ts +833 -116
- package/src/routes/company-brain.ts +322 -0
- package/src/routes/company-profile.ts +65 -1
- package/src/routes/computer-sessions.ts +557 -106
- package/src/routes/connect.ts +1938 -0
- package/src/routes/connections.ts +1039 -349
- package/src/routes/documents.ts +488 -287
- package/src/routes/editable-artifacts.ts +79 -9
- package/src/routes/enrollments.ts +135 -39
- package/src/routes/environments.ts +154 -58
- package/src/routes/external-identity-links.ts +201 -0
- package/src/routes/feedback.ts +113 -0
- package/src/routes/files.ts +267 -33
- package/src/routes/github-browser-pages.ts +162 -0
- package/src/routes/github.ts +446 -144
- package/src/routes/insights-response-cache.ts +83 -0
- package/src/routes/insights-usage.ts +345 -0
- package/src/routes/insights.ts +153 -19
- package/src/routes/install.ts +133 -38
- package/src/routes/integration-facets.ts +107 -33
- package/src/routes/interaction-resources.ts +30 -3
- package/src/routes/knowledge.ts +488 -0
- package/src/routes/machines.ts +313 -15
- package/src/routes/managed-auth-session-sets.ts +1218 -0
- package/src/routes/managed-onboarding.ts +337 -0
- package/src/routes/managed-sign-in-methods.ts +433 -0
- package/src/routes/memory-slack-publications.ts +3 -2
- package/src/routes/model-connection-access.ts +208 -0
- package/src/routes/organization-integration-policy.ts +116 -0
- package/src/routes/organization-integrations.ts +326 -0
- package/src/routes/organization-mcp-connections.ts +178 -0
- package/src/routes/organization-memberships.ts +1019 -27
- package/src/routes/organization-model-providers.ts +293 -0
- package/src/routes/organization-recovery.ts +325 -0
- package/src/routes/organization-service-accounts.ts +128 -0
- package/src/routes/organization-sessions.ts +350 -0
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +370 -0
- package/src/routes/plugins.ts +251 -22
- package/src/routes/pr-review-github.ts +881 -0
- package/src/routes/pr-review.ts +622 -0
- package/src/routes/preference-registry.ts +12 -93
- package/src/routes/rigs.ts +167 -50
- package/src/routes/scheduled-tasks.ts +346 -41
- package/src/routes/session-artifact-associations.ts +87 -0
- package/src/routes/session-history-imports.ts +280 -0
- package/src/routes/sessions.ts +2306 -264
- package/src/routes/skill-content.ts +297 -0
- package/src/routes/skill-install-authority.ts +64 -0
- package/src/routes/skills.ts +62 -9
- package/src/routes/social.ts +46 -18
- package/src/routes/subscription-account-pools.ts +436 -0
- package/src/routes/subscription-pool-access.ts +126 -0
- package/src/routes/supergrok.ts +219 -303
- package/src/routes/transcription-recordings.ts +38 -3
- package/src/routes/transcriptions.ts +4 -0
- package/src/routes/usage-allowances.ts +378 -0
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/video-generation.ts +14 -10
- package/src/routes/workspace-artifacts.ts +275 -69
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-integrations.ts +576 -0
- package/src/routes/workspace-learning.ts +159 -0
- package/src/routes/workspace-model-providers.ts +148 -0
- package/src/routes/workspace-state.ts +38 -25
- package/src/routes/workspaces.ts +1004 -127
- package/src/sandbox/auth-callout.ts +96 -19
- package/src/sandbox/channel-a.ts +323 -24
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +81 -6
- package/src/sandbox/machines.ts +218 -76
- package/src/sandbox/metrics-ingestion.ts +411 -62
- package/src/sandbox/rematerialize.ts +121 -19
- package/src/sandbox/session-attach-variable-sets.ts +131 -0
- package/src/sandbox/viewer.ts +280 -25
- package/src/sandbox-file-artifacts.ts +413 -0
- package/src/scheduled-task-deletion.ts +138 -0
- package/src/session-create-observability.ts +46 -0
- package/src/session-realtime-context.ts +6 -3
- package/src/site-session-origin.ts +48 -0
- package/src/site-uploads.ts +224 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- package/src/temporal-schedule-sync.ts +74 -0
- package/src/transcription/providers/codex-subscription.ts +2 -0
- package/src/transcription/providers/openai.ts +1 -0
- package/src/transcription/providers/xai-subscription.ts +49 -3
- package/src/transcription/segmenter.ts +8 -2
- package/src/transcription/service.ts +43 -6
- package/src/work-discovery-observability.ts +121 -0
- package/src/workspace-artifact-content.ts +144 -0
- package/src/workspace-artifact-provenance.ts +104 -0
- package/src/workspace-delete-observability.ts +75 -0
- package/src/workspace-deletion.ts +73 -0
- package/src/workspace-state-projection.ts +18 -148
- package/src/workspace-tool-gateway-observability.ts +100 -0
- package/src/workspace-tool-gateway.ts +870 -0
- package/src/workspace-webhook-dispatch.ts +190 -0
- package/src/xai-subscription-auth.ts +9 -2
- package/dist/chunk-WCXHI3FF.js +0 -49920
- package/dist/chunk-WCXHI3FF.js.map +0 -1
- package/dist/routes/packs.d.ts +0 -3
- package/src/routes/packs.ts +0 -729
|
@@ -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,10 +109,11 @@ 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,
|
|
@@ -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,6 +159,7 @@ import {
|
|
|
142
159
|
recordWorkspaceUsage,
|
|
143
160
|
requireLimit,
|
|
144
161
|
relayConfigFromSettings,
|
|
162
|
+
resolveSessionSandboxRuntime,
|
|
145
163
|
SessionAuthorizationDeniedError,
|
|
146
164
|
SessionAuthorizationUnavailableError,
|
|
147
165
|
type ApiRouteDeps,
|
|
@@ -158,16 +176,21 @@ import {
|
|
|
158
176
|
buildStreamUrl,
|
|
159
177
|
exposedPortEndpointFromUrl,
|
|
160
178
|
buildSelfhostedBackendSession,
|
|
179
|
+
ensureDisplayStack,
|
|
161
180
|
mintStreamToken,
|
|
162
181
|
NatsControlRpc,
|
|
163
182
|
NatsOpStreamTransport,
|
|
164
183
|
provisionBrowserControlClient,
|
|
184
|
+
renewSandboxProviderExpiration,
|
|
165
185
|
type BrowserControlPlacementSession,
|
|
186
|
+
type BrowserControlScreenshotOptions,
|
|
187
|
+
type BrowserControlFrame,
|
|
166
188
|
type PlacementBrowserStateCaptureReceipt,
|
|
167
189
|
type PlacementBrowserNetworkRoute,
|
|
168
190
|
type PlacementBrowserTransport,
|
|
169
191
|
type RestorePlacementBrowserStateInput,
|
|
170
192
|
} from "@opengeni/runtime/sandbox";
|
|
193
|
+
import { retryWhileMissing } from "@opengeni/storage";
|
|
171
194
|
import type { Context, Hono } from "hono";
|
|
172
195
|
import { HTTPException } from "hono/http-exception";
|
|
173
196
|
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
|
@@ -176,8 +199,17 @@ import {
|
|
|
176
199
|
deriveBrowserNetworkRouteAuthorityDigest,
|
|
177
200
|
deriveBrowserSessionControllerTokens,
|
|
178
201
|
deriveBrowserViewGrantToken,
|
|
202
|
+
deriveComputerSessionControllerTokens,
|
|
179
203
|
} from "../browser-controller-authority";
|
|
180
|
-
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";
|
|
181
213
|
import { withInteractionHolderHeartbeat } from "../interaction-holder-heartbeat";
|
|
182
214
|
import {
|
|
183
215
|
browserStateArtifactAad,
|
|
@@ -192,7 +224,13 @@ import {
|
|
|
192
224
|
resolveProtectedAuthFieldValues,
|
|
193
225
|
} from "../browser-auth-broker";
|
|
194
226
|
import { managedNetworkRouteForPlacement } from "../browser-network-route";
|
|
195
|
-
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";
|
|
196
234
|
import {
|
|
197
235
|
observeAuthMutation,
|
|
198
236
|
observeBrowserActionResult,
|
|
@@ -201,6 +239,7 @@ import {
|
|
|
201
239
|
} from "../interaction-metrics";
|
|
202
240
|
import { withChannelA, withChannelARead, type ChannelAOperation } from "../sandbox/channel-a";
|
|
203
241
|
import { sanitizeFilename } from "./files";
|
|
242
|
+
import { USER_CONTENT_SECURITY_HEADERS } from "../http/user-content";
|
|
204
243
|
|
|
205
244
|
const BROWSER_DRIVER_ID = "opengeni.cdp.v1";
|
|
206
245
|
const LIGHTPANDA_DRIVER_ID = "opengeni.lightpanda.cdp.v1";
|
|
@@ -265,13 +304,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
265
304
|
app.get("/v1/workspaces/:workspaceId/browser-sessions", async (context) => {
|
|
266
305
|
const workspaceId = context.req.param("workspaceId") ?? "";
|
|
267
306
|
const grant = await requireAccessGrant(context, deps, workspaceId, "sessions:read");
|
|
307
|
+
const listed = await listBrowserSessions(deps.db, {
|
|
308
|
+
accountId: grant.accountId,
|
|
309
|
+
workspaceId,
|
|
310
|
+
});
|
|
268
311
|
return context.json(
|
|
269
|
-
BrowserSessionListResponse.parse(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}),
|
|
274
|
-
),
|
|
312
|
+
BrowserSessionListResponse.parse({
|
|
313
|
+
...listed,
|
|
314
|
+
sessions: await filterInteractionSessionsForGrant(deps, grant, listed.sessions),
|
|
315
|
+
}),
|
|
275
316
|
);
|
|
276
317
|
});
|
|
277
318
|
|
|
@@ -298,10 +339,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
298
339
|
const request = await parseJsonBody(context, CreateBrowserSessionRequest);
|
|
299
340
|
const startedAtMs = performance.now();
|
|
300
341
|
await authorizeSourceSession(deps, grant, request.sessionId, "session.control");
|
|
301
|
-
const origin = requestOrigin(context, deps.settings
|
|
342
|
+
const origin = requestOrigin(context, deps.settings);
|
|
302
343
|
const authority = browserAuthorityRoot(deps);
|
|
344
|
+
let prepared: Awaited<ReturnType<typeof prepareBrowserSessionCreate>> | null = null;
|
|
303
345
|
|
|
304
346
|
try {
|
|
347
|
+
assertEphemeralBrowserCreateEnabled(
|
|
348
|
+
request,
|
|
349
|
+
deps.settings.experimentalBrowserContextPoolEnabled,
|
|
350
|
+
);
|
|
305
351
|
const existing = await findBrowserSessionControlRecordByOperation(deps.db, {
|
|
306
352
|
accountId: grant.accountId,
|
|
307
353
|
workspaceId,
|
|
@@ -313,7 +359,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
313
359
|
);
|
|
314
360
|
}
|
|
315
361
|
if (existing) assertCreateReplay(request, existing.session);
|
|
316
|
-
|
|
362
|
+
prepared = existing
|
|
317
363
|
? await prepareBrowserSessionCreate(
|
|
318
364
|
deps.db,
|
|
319
365
|
browserCreateInput(grant, workspaceId, request, existing.session.placement),
|
|
@@ -325,6 +371,20 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
325
371
|
return context.json(parsed, 200);
|
|
326
372
|
}
|
|
327
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
|
+
|
|
328
388
|
const sourceSession = await requireSourceSession(deps, workspaceId, request.sessionId);
|
|
329
389
|
const response = await withBrowserPlacement(
|
|
330
390
|
sourceSession,
|
|
@@ -401,12 +461,6 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
401
461
|
});
|
|
402
462
|
const preparedSession = prepared.session;
|
|
403
463
|
const controllerGeneration = requireOperationGeneration(record);
|
|
404
|
-
const linkedComputer = await requireLinkedComputerBinding(
|
|
405
|
-
deps,
|
|
406
|
-
grant,
|
|
407
|
-
prepared.session,
|
|
408
|
-
placement,
|
|
409
|
-
);
|
|
410
464
|
const adminToken = deriveBrowserControllerAdminToken({
|
|
411
465
|
rootSecret: authority,
|
|
412
466
|
accountId: grant.accountId,
|
|
@@ -432,8 +486,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
432
486
|
placement,
|
|
433
487
|
adminToken,
|
|
434
488
|
origin,
|
|
435
|
-
async (client) =>
|
|
436
|
-
await
|
|
489
|
+
async (client) => {
|
|
490
|
+
const linkedComputer = await ensureLinkedComputerController(
|
|
491
|
+
deps,
|
|
492
|
+
grant,
|
|
493
|
+
preparedSession,
|
|
494
|
+
placement,
|
|
495
|
+
client,
|
|
496
|
+
);
|
|
497
|
+
return await withInteractionHolderHeartbeat(
|
|
437
498
|
deps,
|
|
438
499
|
{
|
|
439
500
|
grant,
|
|
@@ -444,20 +505,32 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
444
505
|
resourceId: preparedSession.id,
|
|
445
506
|
controllerGeneration,
|
|
446
507
|
},
|
|
447
|
-
async () =>
|
|
448
|
-
await
|
|
508
|
+
async () => {
|
|
509
|
+
await ensureHeadedBrowserDisplayStack(
|
|
510
|
+
placement.session,
|
|
511
|
+
preparedSession.headless,
|
|
512
|
+
linkedComputer !== null,
|
|
513
|
+
);
|
|
514
|
+
return await client.createSession({
|
|
449
515
|
browserSessionId: preparedSession.id,
|
|
450
516
|
controllerGeneration,
|
|
451
517
|
tokenGeneration: record.tokenGeneration,
|
|
452
518
|
...tokens,
|
|
453
519
|
headed: !preparedSession.headless,
|
|
454
|
-
transport: browserRuntimeTransport(
|
|
520
|
+
transport: browserRuntimeTransport(
|
|
521
|
+
preparedSession,
|
|
522
|
+
placement.transport,
|
|
523
|
+
placement.placementInstanceId,
|
|
524
|
+
authority,
|
|
525
|
+
),
|
|
455
526
|
...(linkedComputer ? { linkedComputer } : {}),
|
|
456
527
|
...(networkRoute ? { networkRoute } : {}),
|
|
457
528
|
...(request.initialUrl ? { initialUrl: request.initialUrl } : {}),
|
|
458
529
|
...(restore ? { restore } : {}),
|
|
459
|
-
})
|
|
460
|
-
|
|
530
|
+
});
|
|
531
|
+
},
|
|
532
|
+
);
|
|
533
|
+
},
|
|
461
534
|
);
|
|
462
535
|
await cacheBrowserControllerPlacement(grant, workspaceId, placement).catch(
|
|
463
536
|
() => placement,
|
|
@@ -519,6 +592,18 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
519
592
|
parsed.operation.state === "completed" && !parsed.operation.replayed ? 201 : 200,
|
|
520
593
|
);
|
|
521
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
|
+
}
|
|
522
607
|
throw browserRouteError(error);
|
|
523
608
|
}
|
|
524
609
|
});
|
|
@@ -908,6 +993,82 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
908
993
|
},
|
|
909
994
|
);
|
|
910
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
|
+
|
|
911
1072
|
app.post(
|
|
912
1073
|
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/actions",
|
|
913
1074
|
async (context) => {
|
|
@@ -1663,7 +1824,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1663
1824
|
context,
|
|
1664
1825
|
"stream:view",
|
|
1665
1826
|
);
|
|
1666
|
-
const origin = requestOrigin(context, deps.settings
|
|
1827
|
+
const origin = requestOrigin(context, deps.settings);
|
|
1667
1828
|
const request = await parseJsonBody(context, BrowserSessionAttachmentRequest);
|
|
1668
1829
|
const result = await withActiveBrowserController(
|
|
1669
1830
|
context,
|
|
@@ -1705,7 +1866,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1705
1866
|
browserSessionId,
|
|
1706
1867
|
controllerGeneration: binding.controllerGeneration,
|
|
1707
1868
|
};
|
|
1708
|
-
await client.createViewGrant(reference, {
|
|
1869
|
+
const viewGrant = await client.createViewGrant(reference, {
|
|
1709
1870
|
grantId,
|
|
1710
1871
|
token,
|
|
1711
1872
|
expiresAt,
|
|
@@ -1719,6 +1880,18 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1719
1880
|
});
|
|
1720
1881
|
const stream = relayed
|
|
1721
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
|
+
}
|
|
1722
1895
|
const relayToken = await mintStreamToken(relaySecret!, {
|
|
1723
1896
|
workspaceId,
|
|
1724
1897
|
sessionId: record.sourceSessionId,
|
|
@@ -1726,6 +1899,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1726
1899
|
leaseEpoch: record.tokenGeneration,
|
|
1727
1900
|
port: relayed.channel.port,
|
|
1728
1901
|
ttlSeconds: request.expiresInSeconds,
|
|
1902
|
+
subjectId: grant.subjectId,
|
|
1903
|
+
authorityEpoch: relayAuthorityEpoch,
|
|
1729
1904
|
});
|
|
1730
1905
|
return {
|
|
1731
1906
|
kind: "relay" as const,
|
|
@@ -1740,19 +1915,53 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1740
1915
|
},
|
|
1741
1916
|
};
|
|
1742
1917
|
})()
|
|
1743
|
-
: {
|
|
1744
|
-
|
|
1745
|
-
url: await client.frameStreamUrl(reference, request.targetId, request.stream),
|
|
1746
|
-
protocols: [
|
|
1918
|
+
: await (async () => {
|
|
1919
|
+
const protocols = [
|
|
1747
1920
|
BROWSER_CONTROL_WEBSOCKET_PROTOCOL,
|
|
1748
1921
|
`${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
|
|
1749
|
-
]
|
|
1750
|
-
|
|
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
|
+
})();
|
|
1751
1956
|
return BrowserSessionAttachment.parse({
|
|
1752
1957
|
browserSessionId,
|
|
1753
1958
|
controllerGeneration: binding.controllerGeneration,
|
|
1754
1959
|
targetId: request.targetId,
|
|
1755
1960
|
stream,
|
|
1961
|
+
...(viewGrant.fencedInputBatches === true ? { fencedInputBatches: true } : {}),
|
|
1962
|
+
...(viewGrant.focusedInputObservations === true
|
|
1963
|
+
? { focusedInputObservations: true }
|
|
1964
|
+
: {}),
|
|
1756
1965
|
expiresAt,
|
|
1757
1966
|
});
|
|
1758
1967
|
},
|
|
@@ -1958,7 +2167,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1958
2167
|
const browserSessionId = requireUuidParam(context, "browserSessionId");
|
|
1959
2168
|
const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
|
|
1960
2169
|
const startedAtMs = performance.now();
|
|
1961
|
-
const origin = requestOrigin(context, deps.settings
|
|
2170
|
+
const origin = requestOrigin(context, deps.settings);
|
|
2171
|
+
let preparedForDispatch = false;
|
|
1962
2172
|
try {
|
|
1963
2173
|
const before = await getBrowserSessionControlRecord(deps.db, {
|
|
1964
2174
|
accountId: grant.accountId,
|
|
@@ -2010,6 +2220,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2010
2220
|
return context.json(parsed, 200);
|
|
2011
2221
|
}
|
|
2012
2222
|
|
|
2223
|
+
preparedForDispatch = true;
|
|
2224
|
+
|
|
2013
2225
|
const record = await getBrowserSessionControlRecord(deps.db, {
|
|
2014
2226
|
accountId: grant.accountId,
|
|
2015
2227
|
workspaceId,
|
|
@@ -2171,6 +2383,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2171
2383
|
observeLifecycleResult(deps.observability, startedAtMs, parsed);
|
|
2172
2384
|
return context.json(parsed, 200);
|
|
2173
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
|
+
}
|
|
2174
2395
|
throw browserRouteError(error);
|
|
2175
2396
|
}
|
|
2176
2397
|
},
|
|
@@ -2184,7 +2405,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2184
2405
|
const browserSessionId = requireUuidParam(context, "browserSessionId");
|
|
2185
2406
|
const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
|
|
2186
2407
|
const startedAtMs = performance.now();
|
|
2187
|
-
const origin = requestOrigin(context, deps.settings
|
|
2408
|
+
const origin = requestOrigin(context, deps.settings);
|
|
2188
2409
|
let restore: RestorePlacementBrowserStateInput | null = null;
|
|
2189
2410
|
try {
|
|
2190
2411
|
let before = await getBrowserSessionControlRecord(deps.db, {
|
|
@@ -2303,12 +2524,6 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2303
2524
|
throw error;
|
|
2304
2525
|
});
|
|
2305
2526
|
const controllerGeneration = requireOperationGeneration(record);
|
|
2306
|
-
const linkedComputer = await requireLinkedComputerBinding(
|
|
2307
|
-
deps,
|
|
2308
|
-
grant,
|
|
2309
|
-
prepared.session,
|
|
2310
|
-
placement,
|
|
2311
|
-
);
|
|
2312
2527
|
const adminToken = deriveBrowserControllerAdminToken({
|
|
2313
2528
|
rootSecret: authority,
|
|
2314
2529
|
accountId: grant.accountId,
|
|
@@ -2332,6 +2547,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2332
2547
|
...(origin ? { allowedOrigins: [origin] } : {}),
|
|
2333
2548
|
});
|
|
2334
2549
|
try {
|
|
2550
|
+
const linkedComputer = await ensureLinkedComputerController(
|
|
2551
|
+
deps,
|
|
2552
|
+
grant,
|
|
2553
|
+
prepared.session,
|
|
2554
|
+
placement,
|
|
2555
|
+
client,
|
|
2556
|
+
);
|
|
2335
2557
|
await withInteractionHolderHeartbeat(
|
|
2336
2558
|
deps,
|
|
2337
2559
|
{
|
|
@@ -2343,8 +2565,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2343
2565
|
resourceId: browserSessionId,
|
|
2344
2566
|
controllerGeneration,
|
|
2345
2567
|
},
|
|
2346
|
-
async () =>
|
|
2347
|
-
await
|
|
2568
|
+
async () => {
|
|
2569
|
+
await ensureHeadedBrowserDisplayStack(
|
|
2570
|
+
placement.session,
|
|
2571
|
+
prepared.session.headless,
|
|
2572
|
+
linkedComputer !== null,
|
|
2573
|
+
);
|
|
2574
|
+
return await client.createSession({
|
|
2348
2575
|
browserSessionId,
|
|
2349
2576
|
controllerGeneration,
|
|
2350
2577
|
tokenGeneration: record.tokenGeneration,
|
|
@@ -2354,7 +2581,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2354
2581
|
...(linkedComputer ? { linkedComputer } : {}),
|
|
2355
2582
|
...(networkRoute ? { networkRoute } : {}),
|
|
2356
2583
|
restore: restore!,
|
|
2357
|
-
})
|
|
2584
|
+
});
|
|
2585
|
+
},
|
|
2358
2586
|
);
|
|
2359
2587
|
} catch (error) {
|
|
2360
2588
|
if (!isDefiniteBrowserControllerFailure(error)) throw error;
|
|
@@ -2426,7 +2654,12 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2426
2654
|
const browserSessionId = requireUuidParam(context, "browserSessionId");
|
|
2427
2655
|
const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
|
|
2428
2656
|
const startedAtMs = performance.now();
|
|
2429
|
-
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;
|
|
2430
2663
|
try {
|
|
2431
2664
|
const before = await getBrowserSessionControlRecord(deps.db, {
|
|
2432
2665
|
accountId: grant.accountId,
|
|
@@ -2441,6 +2674,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2441
2674
|
browserSessionId,
|
|
2442
2675
|
operationId: request.operationId,
|
|
2443
2676
|
actorSubjectId: grant.subjectId,
|
|
2677
|
+
expectedLifecycle: before.session.lifecycle,
|
|
2678
|
+
expectedFailureCode: before.session.failureCode,
|
|
2679
|
+
expectedControllerGeneration: before.session.controller?.controllerGeneration ?? null,
|
|
2444
2680
|
});
|
|
2445
2681
|
if (isTerminalOperation(prepared.operation.state)) {
|
|
2446
2682
|
if (prepared.operation.state === "completed") {
|
|
@@ -2456,6 +2692,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2456
2692
|
observeLifecycleResult(deps.observability, startedAtMs, parsed);
|
|
2457
2693
|
return context.json(parsed, 200);
|
|
2458
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
|
+
}
|
|
2459
2702
|
|
|
2460
2703
|
const record = await getBrowserSessionControlRecord(deps.db, {
|
|
2461
2704
|
accountId: grant.accountId,
|
|
@@ -2549,6 +2792,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2549
2792
|
observeLifecycleResult(deps.observability, startedAtMs, parsed);
|
|
2550
2793
|
return context.json(parsed, 200);
|
|
2551
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
|
+
}
|
|
2552
2805
|
throw browserRouteError(error);
|
|
2553
2806
|
}
|
|
2554
2807
|
},
|
|
@@ -2562,6 +2815,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2562
2815
|
operation: ChannelAOperation,
|
|
2563
2816
|
waitSignal: AbortSignal,
|
|
2564
2817
|
callback: (placement: BrowserPlacement) => Promise<T>,
|
|
2818
|
+
allowOperationReplay = true,
|
|
2565
2819
|
): Promise<T> {
|
|
2566
2820
|
if (expectedPlacement?.kind === "attached_device") {
|
|
2567
2821
|
waitSignal.throwIfAborted();
|
|
@@ -2573,18 +2827,29 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2573
2827
|
if (device.state !== "connected") {
|
|
2574
2828
|
throw new BrowserSessionStateError("Attached browser is disconnected");
|
|
2575
2829
|
}
|
|
2576
|
-
const enrollment = await
|
|
2830
|
+
const enrollment = await getLiveEnrollmentConnection(
|
|
2577
2831
|
deps.db,
|
|
2578
2832
|
sourceSession.workspaceId,
|
|
2579
2833
|
device.enrollmentId,
|
|
2580
2834
|
);
|
|
2581
|
-
if (!enrollment || enrollment.status !== "active") {
|
|
2835
|
+
if (!enrollment || enrollment.status !== "active" || !enrollment.connectionInstanceId) {
|
|
2582
2836
|
throw new BrowserSessionStateError("Attached browser machine is unavailable");
|
|
2583
2837
|
}
|
|
2584
|
-
|
|
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
|
+
);
|
|
2585
2848
|
const built = await buildSelfhostedBackendSession({
|
|
2586
2849
|
workspaceId: sourceSession.workspaceId,
|
|
2587
2850
|
agentId: device.enrollmentId,
|
|
2851
|
+
connectionInstanceId: enrollment.connectionInstanceId,
|
|
2852
|
+
workspaceRoot: enrollment.workspaceRoot,
|
|
2588
2853
|
relay: relayConfigFromSettings(deps.settings),
|
|
2589
2854
|
controlRpcFactory: () => new NatsControlRpc(async () => deps.bus.getRequestConnection()),
|
|
2590
2855
|
// Browser-profile generation is the physical controller fence. Epoch 0
|
|
@@ -2593,6 +2858,11 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2593
2858
|
epoch: 0,
|
|
2594
2859
|
timeoutMs: deps.settings.sandboxSelfhostedControlTimeoutMs,
|
|
2595
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,
|
|
2596
2866
|
...(deps.settings.agentOpStreamEnabled === true &&
|
|
2597
2867
|
enrollment.opStream === true &&
|
|
2598
2868
|
deps.bus.getOpStreamConnection
|
|
@@ -2609,13 +2879,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2609
2879
|
return await callback({
|
|
2610
2880
|
placement: expectedPlacement,
|
|
2611
2881
|
controllerHostSandboxGroupId: null,
|
|
2612
|
-
placementInstanceId
|
|
2882
|
+
placementInstanceId,
|
|
2613
2883
|
session: built.session as unknown as BrowserControlPlacementSession,
|
|
2614
2884
|
lease: null,
|
|
2615
2885
|
transport: {
|
|
2616
2886
|
kind: "attached_chrome",
|
|
2617
2887
|
deviceId: device.id,
|
|
2618
|
-
connectionGeneration:
|
|
2888
|
+
connectionGeneration: placementInstanceId,
|
|
2619
2889
|
browserName: device.browserName,
|
|
2620
2890
|
browserVersion: device.browserVersion,
|
|
2621
2891
|
},
|
|
@@ -2638,6 +2908,12 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2638
2908
|
waitSignal,
|
|
2639
2909
|
operation,
|
|
2640
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
|
+
: {}),
|
|
2641
2917
|
},
|
|
2642
2918
|
async (handle) => {
|
|
2643
2919
|
if (expectedPlacement?.kind === "sandbox_group") {
|
|
@@ -2681,8 +2957,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2681
2957
|
resolved.kind !== "selfhosted" ||
|
|
2682
2958
|
resolved.sandboxId !== expectedPlacement.sandboxId
|
|
2683
2959
|
) {
|
|
2684
|
-
|
|
2685
|
-
|
|
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,
|
|
2686
2970
|
);
|
|
2687
2971
|
}
|
|
2688
2972
|
const placementInstanceId = resolved.providerInstanceId ?? expectedPlacement.sandboxId;
|
|
@@ -2779,7 +3063,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2779
3063
|
!lease ||
|
|
2780
3064
|
(lease.liveness !== "warm" && lease.liveness !== "draining") ||
|
|
2781
3065
|
lease.instanceId !== binding.placementInstanceId ||
|
|
2782
|
-
!lease.controllerDataPlaneUrl
|
|
3066
|
+
!lease.controllerDataPlaneUrl ||
|
|
3067
|
+
(lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) ||
|
|
3068
|
+
!controllerCachedUrlIsUsable(lease.controllerDataPlaneUrl)
|
|
2783
3069
|
) {
|
|
2784
3070
|
return null;
|
|
2785
3071
|
}
|
|
@@ -2805,8 +3091,29 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2805
3091
|
if (!sandboxGroupId || !placement.lease?.instanceId || !placement.session.resolveExposedPort) {
|
|
2806
3092
|
return placement;
|
|
2807
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
|
+
}
|
|
2808
3106
|
const endpoint = await placement.session.resolveExposedPort(BROWSER_CONTROL_PORT);
|
|
2809
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
|
+
}
|
|
2810
3117
|
const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
|
|
2811
3118
|
accountId: grant.accountId,
|
|
2812
3119
|
workspaceId,
|
|
@@ -2816,6 +3123,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2816
3123
|
controllerDataPlaneUrl: url,
|
|
2817
3124
|
});
|
|
2818
3125
|
if (!lease) return placement;
|
|
3126
|
+
if (!controllerCacheAllowsHostFetch(url)) {
|
|
3127
|
+
return { ...placement, lease };
|
|
3128
|
+
}
|
|
2819
3129
|
return { ...placement, session: controllerOnlySession(url), lease };
|
|
2820
3130
|
}
|
|
2821
3131
|
|
|
@@ -2831,7 +3141,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2831
3141
|
const sandboxGroupId = placement.controllerHostSandboxGroupId;
|
|
2832
3142
|
const cachedUrl = placement.lease?.controllerDataPlaneUrl;
|
|
2833
3143
|
return await withCachedController({
|
|
2834
|
-
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,
|
|
2835
3154
|
createCachedClient: (url) =>
|
|
2836
3155
|
new BrowserControlClient(controllerOnlySession(url), { adminToken }),
|
|
2837
3156
|
prepareCachedClient: async (client) => {
|
|
@@ -2889,6 +3208,17 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2889
3208
|
record.sourceSessionId,
|
|
2890
3209
|
authorizationOperation,
|
|
2891
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
|
+
}
|
|
2892
3222
|
if (record.session.lifecycle !== "active" || !record.session.controller) {
|
|
2893
3223
|
throw new BrowserSessionStateError("BrowserSession is not active");
|
|
2894
3224
|
}
|
|
@@ -2902,10 +3232,18 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2902
3232
|
if (!admitted) {
|
|
2903
3233
|
throw new BrowserSessionStateError("BrowserSession controller authority changed");
|
|
2904
3234
|
}
|
|
2905
|
-
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> => {
|
|
2906
3244
|
// An active binding already proves browserd was provisioned. Reusing
|
|
2907
3245
|
// it avoids restarting/checking the sidecar on every live input.
|
|
2908
|
-
const client = connectController(deps, grant, record, placement);
|
|
3246
|
+
const client = provisionedClient ?? connectController(deps, grant, record, placement);
|
|
2909
3247
|
const tokens = deriveBrowserSessionControllerTokens({
|
|
2910
3248
|
rootSecret: browserAuthorityRoot(deps),
|
|
2911
3249
|
accountId: grant.accountId,
|
|
@@ -2934,6 +3272,30 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2934
3272
|
try {
|
|
2935
3273
|
result = await callback(controller);
|
|
2936
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
|
+
}
|
|
2937
3299
|
if (!recoverMissing || !isMissingBrowserControllerSession(error)) throw error;
|
|
2938
3300
|
await recoverActiveBrowserController(
|
|
2939
3301
|
deps,
|
|
@@ -2943,6 +3305,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2943
3305
|
placement,
|
|
2944
3306
|
client,
|
|
2945
3307
|
tokens,
|
|
3308
|
+
authorizationOperation,
|
|
2946
3309
|
);
|
|
2947
3310
|
result = await callback(controller);
|
|
2948
3311
|
}
|
|
@@ -2953,15 +3316,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2953
3316
|
try {
|
|
2954
3317
|
return await run(cached);
|
|
2955
3318
|
} catch (error) {
|
|
2956
|
-
|
|
2957
|
-
channelOperation === "browser.read" || channelOperation === "browser.action";
|
|
2958
|
-
if (
|
|
2959
|
-
!safelyReplayable ||
|
|
2960
|
-
(!(error instanceof BrowserControlTransportError) &&
|
|
2961
|
-
!(error instanceof BrowserControlRequestError && error.retryable))
|
|
2962
|
-
) {
|
|
3319
|
+
if (!safelyReplayable || !isRetryableControllerTransport(error)) {
|
|
2963
3320
|
throw error;
|
|
2964
3321
|
}
|
|
3322
|
+
controllerTransportFailed = true;
|
|
2965
3323
|
await recordLeaseControllerDataPlaneUrl(deps.db, {
|
|
2966
3324
|
accountId: grant.accountId,
|
|
2967
3325
|
workspaceId,
|
|
@@ -2973,7 +3331,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2973
3331
|
}
|
|
2974
3332
|
}
|
|
2975
3333
|
|
|
2976
|
-
|
|
3334
|
+
sourceSession ??= await requireSourceSession(deps, workspaceId, record.sourceSessionId);
|
|
2977
3335
|
return await withBrowserPlacement(
|
|
2978
3336
|
sourceSession,
|
|
2979
3337
|
grant,
|
|
@@ -2981,18 +3339,75 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2981
3339
|
binding.placementInstanceId,
|
|
2982
3340
|
channelOperation,
|
|
2983
3341
|
context.req.raw.signal,
|
|
2984
|
-
async (placement) =>
|
|
2985
|
-
|
|
2986
|
-
await
|
|
2987
|
-
(
|
|
2988
|
-
|
|
2989
|
-
|
|
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",
|
|
2990
3386
|
);
|
|
2991
3387
|
} catch (error) {
|
|
2992
3388
|
throw browserRouteError(error);
|
|
2993
3389
|
}
|
|
2994
3390
|
}
|
|
2995
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
|
+
|
|
2996
3411
|
async function recoverActiveBrowserController(
|
|
2997
3412
|
routeDeps: ApiRouteDeps,
|
|
2998
3413
|
grant: AccessGrant,
|
|
@@ -3001,12 +3416,72 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3001
3416
|
placement: BrowserPlacement,
|
|
3002
3417
|
client: BrowserControlClient,
|
|
3003
3418
|
tokens: ReturnType<typeof deriveBrowserSessionControllerTokens>,
|
|
3419
|
+
authorizationOperation: SessionAuthorizationOperation,
|
|
3004
3420
|
): Promise<void> {
|
|
3005
|
-
|
|
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(
|
|
3006
3480
|
routeDeps,
|
|
3007
3481
|
grant,
|
|
3008
3482
|
record.session,
|
|
3009
3483
|
placement,
|
|
3484
|
+
client,
|
|
3010
3485
|
);
|
|
3011
3486
|
const networkRoute = await resolveActiveBrowserNetworkRouteLaunch({
|
|
3012
3487
|
deps: routeDeps,
|
|
@@ -3015,13 +3490,30 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3015
3490
|
placement: placement.placement,
|
|
3016
3491
|
});
|
|
3017
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
|
+
}
|
|
3018
3509
|
await client.createSession({
|
|
3019
3510
|
browserSessionId: record.session.id,
|
|
3020
3511
|
controllerGeneration: binding.controllerGeneration,
|
|
3021
3512
|
tokenGeneration: record.tokenGeneration,
|
|
3022
3513
|
...tokens,
|
|
3023
3514
|
headed: !record.session.headless,
|
|
3024
|
-
transport
|
|
3515
|
+
transport,
|
|
3516
|
+
...(recoverWorkingDirectory ? { recoverExistingWorkingDirectory: true } : {}),
|
|
3025
3517
|
...(linkedComputer ? { linkedComputer } : {}),
|
|
3026
3518
|
...(networkRoute ? { networkRoute } : {}),
|
|
3027
3519
|
});
|
|
@@ -3065,7 +3557,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3065
3557
|
grant,
|
|
3066
3558
|
record,
|
|
3067
3559
|
placement,
|
|
3068
|
-
requestOrigin(context, deps.settings
|
|
3560
|
+
requestOrigin(context, deps.settings),
|
|
3069
3561
|
);
|
|
3070
3562
|
await endCapturedBrowserController(client, browserSessionId, binding);
|
|
3071
3563
|
await clearSuspendedBrowserSessionController(deps.db, {
|
|
@@ -3087,18 +3579,42 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3087
3579
|
}
|
|
3088
3580
|
}
|
|
3089
3581
|
|
|
3090
|
-
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(
|
|
3091
3598
|
grant: AccessGrant,
|
|
3092
3599
|
workspaceId: string,
|
|
3093
3600
|
request: CreateBrowserSessionRequestValue,
|
|
3094
3601
|
placement: InteractionPlacement,
|
|
3095
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
|
+
}
|
|
3096
3608
|
const attached = placement.kind === "attached_device";
|
|
3097
3609
|
const external = placement.kind === "external_provider";
|
|
3098
3610
|
const engine = attached ? ("chrome" as const) : external ? ("external" as const) : request.engine;
|
|
3099
|
-
if (
|
|
3611
|
+
if (
|
|
3612
|
+
request.engine === "lightpanda" &&
|
|
3613
|
+
placement.kind !== "sandbox_group" &&
|
|
3614
|
+
placement.kind !== "connected_machine"
|
|
3615
|
+
) {
|
|
3100
3616
|
throw new BrowserControlUnsupportedError(
|
|
3101
|
-
"Lightpanda
|
|
3617
|
+
"Lightpanda requires a managed sandbox or Connected Machine browser placement",
|
|
3102
3618
|
);
|
|
3103
3619
|
}
|
|
3104
3620
|
return {
|
|
@@ -3111,11 +3627,13 @@ function browserCreateInput(
|
|
|
3111
3627
|
initialUrl: request.initialUrl ?? null,
|
|
3112
3628
|
placement,
|
|
3113
3629
|
driverId:
|
|
3114
|
-
|
|
3115
|
-
?
|
|
3116
|
-
:
|
|
3117
|
-
?
|
|
3118
|
-
:
|
|
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,
|
|
3119
3637
|
engine,
|
|
3120
3638
|
headless: attached ? false : request.headless,
|
|
3121
3639
|
identityId: request.identityId ?? null,
|
|
@@ -3140,7 +3658,21 @@ function browserCreateInput(
|
|
|
3140
3658
|
function browserRuntimeTransport(
|
|
3141
3659
|
session: BrowserSessionValue,
|
|
3142
3660
|
transport: PlacementBrowserTransport,
|
|
3661
|
+
placementInstanceId?: string,
|
|
3662
|
+
rootSecret?: string,
|
|
3143
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
|
+
}
|
|
3144
3676
|
if (transport.kind !== "managed") return transport;
|
|
3145
3677
|
return {
|
|
3146
3678
|
kind: "managed",
|
|
@@ -3202,6 +3734,11 @@ function assertCreateReplay(
|
|
|
3202
3734
|
"BrowserSession create operation is bound to another placement",
|
|
3203
3735
|
);
|
|
3204
3736
|
}
|
|
3737
|
+
if (request.storageMode !== browserSessionStorageMode(session)) {
|
|
3738
|
+
throw new BrowserSessionOperationConflictError(
|
|
3739
|
+
"BrowserSession create operation is bound to another storage mode",
|
|
3740
|
+
);
|
|
3741
|
+
}
|
|
3205
3742
|
const expectedEngine =
|
|
3206
3743
|
session.placement.kind === "attached_device"
|
|
3207
3744
|
? "chrome"
|
|
@@ -3235,11 +3772,39 @@ function assertCreateReplay(
|
|
|
3235
3772
|
}
|
|
3236
3773
|
}
|
|
3237
3774
|
|
|
3238
|
-
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(
|
|
3239
3803
|
deps: ApiRouteDeps,
|
|
3240
3804
|
grant: AccessGrant,
|
|
3241
3805
|
browser: BrowserSessionValue,
|
|
3242
3806
|
placement: BrowserPlacement,
|
|
3807
|
+
client: BrowserControlClient,
|
|
3243
3808
|
): Promise<{ computerSessionId: string; controllerGeneration: string } | null> {
|
|
3244
3809
|
if (!browser.linkedComputerSessionId) return null;
|
|
3245
3810
|
const record = await getComputerSessionControlRecord(deps.db, {
|
|
@@ -3257,10 +3822,42 @@ async function requireLinkedComputerBinding(
|
|
|
3257
3822
|
"Linked ComputerSession is not active on the browser placement",
|
|
3258
3823
|
);
|
|
3259
3824
|
}
|
|
3260
|
-
|
|
3825
|
+
const reference = {
|
|
3261
3826
|
computerSessionId: record.session.id,
|
|
3262
3827
|
controllerGeneration: record.session.controller.controllerGeneration,
|
|
3263
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
|
+
);
|
|
3264
3861
|
}
|
|
3265
3862
|
|
|
3266
3863
|
async function requireBrowserRevisionRestoreAuthority(
|
|
@@ -3480,6 +4077,7 @@ async function ensureInteractionHolder(
|
|
|
3480
4077
|
if (!placement.lease?.instanceId) {
|
|
3481
4078
|
throw new BrowserSessionStateError("BrowserSession lease placement is unavailable");
|
|
3482
4079
|
}
|
|
4080
|
+
const sandboxRuntime = await resolveSessionSandboxRuntime(deps.db, deps.settings, sourceSession);
|
|
3483
4081
|
const acquired = await acquireLease(deps.db, {
|
|
3484
4082
|
accountId: grant.accountId,
|
|
3485
4083
|
workspaceId: sourceSession.workspaceId,
|
|
@@ -3488,9 +4086,14 @@ async function ensureInteractionHolder(
|
|
|
3488
4086
|
holderId: interactionHolderId(browserSessionId),
|
|
3489
4087
|
subjectId: sourceSession.id,
|
|
3490
4088
|
backend: placement.lease.backend,
|
|
4089
|
+
warmBilling: {
|
|
4090
|
+
mode: deps.settings.sandboxWarmBillingMode,
|
|
4091
|
+
rateMicrosPerSecond: sandboxWarmRateMicrosPerSecond(deps.settings, placement.lease.backend),
|
|
4092
|
+
},
|
|
3491
4093
|
os: placement.lease.os,
|
|
3492
|
-
image:
|
|
3493
|
-
|
|
4094
|
+
image: sandboxRuntime.image,
|
|
4095
|
+
imagePolicy: "new_creates_only",
|
|
4096
|
+
rigVersionId: sourceSession.rigVersionId,
|
|
3494
4097
|
leaseTtlMs: deps.settings.sandboxLeaseTtlMs,
|
|
3495
4098
|
expectedEpoch: placement.lease.leaseEpoch,
|
|
3496
4099
|
waitSignal,
|
|
@@ -3512,6 +4115,11 @@ async function ensureInteractionHolder(
|
|
|
3512
4115
|
).catch(() => undefined);
|
|
3513
4116
|
throw new BrowserSessionStateError("BrowserSession placement fence changed; retry");
|
|
3514
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);
|
|
3515
4123
|
return true;
|
|
3516
4124
|
}
|
|
3517
4125
|
|
|
@@ -3758,6 +4366,7 @@ async function settleBrowserSessionSuspensionCaptureFailure(
|
|
|
3758
4366
|
return await failBrowserSessionSuspension(deps.db, {
|
|
3759
4367
|
...input,
|
|
3760
4368
|
...(outcomeUnknown ? { state: "outcome_unknown" as const } : {}),
|
|
4369
|
+
missingControllerSession: error.status === 404 && isMissingBrowserControllerSession(error),
|
|
3761
4370
|
error: error.error,
|
|
3762
4371
|
});
|
|
3763
4372
|
}
|
|
@@ -3895,6 +4504,66 @@ function optionalBoundedInteger(
|
|
|
3895
4504
|
return parsed;
|
|
3896
4505
|
}
|
|
3897
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
|
+
|
|
3898
4567
|
function isUuid(value: unknown): value is string {
|
|
3899
4568
|
return (
|
|
3900
4569
|
typeof value === "string" &&
|
|
@@ -3902,32 +4571,8 @@ function isUuid(value: unknown): value is string {
|
|
|
3902
4571
|
);
|
|
3903
4572
|
}
|
|
3904
4573
|
|
|
3905
|
-
function requestOrigin(context: Context,
|
|
3906
|
-
return
|
|
3907
|
-
}
|
|
3908
|
-
|
|
3909
|
-
export function validateBrowserRequestOrigin(
|
|
3910
|
-
value: string | undefined,
|
|
3911
|
-
allowedPattern: string,
|
|
3912
|
-
): string | null {
|
|
3913
|
-
if (!value) return null;
|
|
3914
|
-
let url: URL;
|
|
3915
|
-
try {
|
|
3916
|
-
url = new URL(value);
|
|
3917
|
-
} catch {
|
|
3918
|
-
throw new HTTPException(400, { message: "invalid request origin" });
|
|
3919
|
-
}
|
|
3920
|
-
if (
|
|
3921
|
-
url.origin === "null" ||
|
|
3922
|
-
(url.protocol !== "http:" && url.protocol !== "https:") ||
|
|
3923
|
-
url.origin !== value
|
|
3924
|
-
) {
|
|
3925
|
-
throw new HTTPException(400, { message: "invalid request origin" });
|
|
3926
|
-
}
|
|
3927
|
-
if (!allowedCorsOrigin(allowedPattern, url.origin)) {
|
|
3928
|
-
throw new HTTPException(403, { message: "request origin is not allowed" });
|
|
3929
|
-
}
|
|
3930
|
-
return url.origin;
|
|
4574
|
+
function requestOrigin(context: Context, settings: ApiRouteDeps["settings"]): string | null {
|
|
4575
|
+
return validateInteractionRequestOrigin(context.req.header("origin"), settings);
|
|
3931
4576
|
}
|
|
3932
4577
|
|
|
3933
4578
|
export function interactionActorForGrant(
|
|
@@ -3955,6 +4600,20 @@ function assertPlacementInstance(expected: string | null, actual: string): void
|
|
|
3955
4600
|
}
|
|
3956
4601
|
}
|
|
3957
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
|
+
|
|
3958
4617
|
function isTerminalOperation(state: string): boolean {
|
|
3959
4618
|
return state === "completed" || state === "failed" || state === "outcome_unknown";
|
|
3960
4619
|
}
|
|
@@ -4410,9 +5069,14 @@ async function finalizeBrowserDownloadFile(
|
|
|
4410
5069
|
if (upload.expiresAt.getTime() <= Date.now()) {
|
|
4411
5070
|
throw new InteractionResourceStateError("Browser download file upload authority expired");
|
|
4412
5071
|
}
|
|
4413
|
-
const
|
|
4414
|
-
|
|
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);
|
|
4415
5076
|
});
|
|
5077
|
+
if (!head) {
|
|
5078
|
+
throw new HTTPException(503, { message: "browser download object is not available" });
|
|
5079
|
+
}
|
|
4416
5080
|
if (
|
|
4417
5081
|
Number(head.ContentLength ?? -1) !== upload.file.sizeBytes ||
|
|
4418
5082
|
(head.ContentType !== undefined && head.ContentType !== upload.file.contentType) ||
|
|
@@ -4453,7 +5117,11 @@ async function recordBrowserDownloadFileUsage(
|
|
|
4453
5117
|
}
|
|
4454
5118
|
|
|
4455
5119
|
function browserRouteError(error: unknown): HTTPException {
|
|
5120
|
+
const connectedMachineError = interactionControlApiError(error, "browser");
|
|
5121
|
+
if (connectedMachineError) return connectedMachineError;
|
|
4456
5122
|
if (error instanceof HTTPException) return error;
|
|
5123
|
+
if (error instanceof SandboxPaidComputeAdmissionError)
|
|
5124
|
+
return new HTTPException(402, { message: error.message, cause: error });
|
|
4457
5125
|
if (error instanceof BrowserSessionNotFoundError) {
|
|
4458
5126
|
return new HTTPException(404, { message: error.message, cause: error });
|
|
4459
5127
|
}
|