@opengeni/api-router 3.0.1 → 3.1.2
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 +8 -1
- package/dist/admission-trace.d.ts +8 -0
- package/dist/app.d.ts +50 -3
- package/dist/app.js +10 -1
- package/dist/artifact-catalog-cursor.d.ts +1 -0
- package/dist/attached-browser-viewer.d.ts +11 -0
- package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
- package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +9 -1
- package/dist/auth/managed-auth.d.ts +72 -5
- package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
- package/dist/auth/new-signups-gate.d.ts +23 -0
- package/dist/auth/signup-funnel-metrics.d.ts +44 -0
- package/dist/azure-live.d.ts +28 -0
- package/dist/browser-action-fences.d.ts +13 -0
- package/dist/browser-controller-authority.d.ts +1 -61
- package/dist/browser-ephemeral.d.ts +5 -0
- package/dist/browser-lost-runtime-end.d.ts +24 -0
- package/dist/browser-state-authority.d.ts +1 -27
- package/dist/chunk-OGQDXVXY.js +18 -0
- package/dist/chunk-OGQDXVXY.js.map +1 -0
- package/dist/{chunk-AJ62T5QN.js → chunk-OT56OY4V.js} +80503 -39869
- package/dist/chunk-OT56OY4V.js.map +1 -0
- package/dist/claude-default-models.d.ts +19 -0
- package/dist/claude-subscription-core.d.ts +50 -0
- package/dist/claude-subscription-oauth.d.ts +36 -0
- package/dist/claude-subscription-profile.d.ts +9 -0
- package/dist/claude-subscription-usage.d.ts +6 -0
- package/dist/claude-workspace-connection.d.ts +15 -0
- package/dist/codemode.d.ts +40 -5
- package/dist/codex-realtime.d.ts +29 -3
- package/dist/codex-session-pointers.d.ts +19 -0
- package/dist/connection-ownership.d.ts +6 -3
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/credit-promotion-snapshot.d.ts +6 -0
- package/dist/editable-artifact-source-session.d.ts +3 -0
- package/dist/editable-artifact-websocket.d.ts +5 -3
- package/dist/fatal-process-boundary.d.ts +9 -1
- package/dist/github-access.d.ts +7 -1
- package/dist/github-browser-flow.d.ts +1 -1
- package/dist/http/acting-person.d.ts +36 -0
- package/dist/http/api-contract-compat.d.ts +19 -0
- package/dist/http/api-error.d.ts +50 -1
- package/dist/http/auth.d.ts +8 -1
- package/dist/http/cors.d.ts +12 -0
- package/dist/http/deprecation.d.ts +42 -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/rejection-telemetry.d.ts +34 -0
- package/dist/http/request-body.d.ts +27 -0
- package/dist/http/request-source.d.ts +41 -3
- package/dist/http/sse.d.ts +12 -1
- package/dist/http/unmatched-route.d.ts +18 -0
- package/dist/http/user-content.d.ts +36 -0
- package/dist/http/variable-sets-removed.d.ts +41 -0
- package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
- package/dist/index.js +1279 -108
- package/dist/index.js.map +1 -1
- package/dist/integration-connect-metrics.d.ts +35 -0
- package/dist/integration-endpoint-test.d.ts +20 -0
- package/dist/integrations/api-integrations.d.ts +2 -2
- package/dist/integrations/atlassian.d.ts +8 -3
- package/dist/integrations/fiken.d.ts +3 -1
- package/dist/integrations/github-lens-connect.d.ts +37 -66
- package/dist/integrations/google-drive.d.ts +2 -0
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/marketplace-plugin.d.ts +43 -43
- package/dist/integrations/oauth-client.d.ts +171 -12
- package/dist/integrations/oauth-profiles.d.ts +42 -13
- package/dist/integrations/onepassword-vault-provider.d.ts +9 -0
- package/dist/integrations/slack-bot.d.ts +285 -7
- package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
- package/dist/integrations/slack-interactions.d.ts +89 -10
- package/dist/integrations/slack-mrkdwn.d.ts +41 -0
- package/dist/integrations/slack-routing.d.ts +1 -1
- package/dist/integrations/slack-session-defaults.d.ts +58 -0
- package/dist/integrations/slack-task-file-upload.d.ts +36 -0
- package/dist/integrations/slack-tool-review.d.ts +5 -0
- package/dist/interaction-agent-access.d.ts +13 -8
- package/dist/interaction-agent-version.d.ts +21 -0
- package/dist/interaction-holder-heartbeat.d.ts +5 -0
- package/dist/interaction-metrics.d.ts +27 -2
- package/dist/interaction-route-stages.d.ts +25 -0
- package/dist/interaction-secrets-vault.d.ts +20 -0
- package/dist/interaction-secrets.d.ts +103 -0
- package/dist/mcp/action-catalog-types.d.ts +14 -0
- package/dist/mcp/action-catalog.gen.d.ts +2 -0
- package/dist/mcp/action-descriptions.d.ts +4 -0
- package/dist/mcp/action-schema-imports.gen.d.ts +1 -0
- package/dist/mcp/capability-account-readiness.d.ts +7 -0
- package/dist/mcp/child-read-evidence.d.ts +9 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +12 -0
- package/dist/mcp/contract-input.d.ts +13 -0
- package/dist/mcp/documents.d.ts +2 -0
- package/dist/mcp/first-party-tool-permissions.d.ts +3 -606
- package/dist/mcp/notification-tools.d.ts +26 -0
- package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
- package/dist/mcp/scheduled-task-input.d.ts +1309 -0
- package/dist/mcp/scheduled-task-view.d.ts +217 -3
- package/dist/mcp/secret-tools.d.ts +14 -0
- package/dist/mcp/server.d.ts +108 -5
- package/dist/mcp/session-admin-access-tools.d.ts +33 -0
- package/dist/mcp/session-event-errors.d.ts +28 -0
- package/dist/mcp/session-event-view.d.ts +62 -0
- package/dist/mcp/session-view.d.ts +15 -0
- package/dist/mcp/session-wait.d.ts +18 -0
- package/dist/mcp/site-agent-clamp-tools.d.ts +38 -0
- package/dist/mcp/site-data-tools.d.ts +22 -0
- package/dist/mcp/site-publication-tools.d.ts +19 -0
- package/dist/mcp/site-sql-tools.d.ts +27 -0
- package/dist/mcp-oauth.d.ts +37 -4
- package/dist/model-calls/chat-completions.d.ts +87 -0
- package/dist/model-calls/pending-usage-settlements.d.ts +57 -0
- package/dist/model-calls/service.d.ts +36 -0
- package/dist/model-catalog.d.ts +9 -3
- package/dist/native-push-dispatch.d.ts +69 -0
- package/dist/observability.d.ts +3 -1
- package/dist/organization-mcp.d.ts +82 -0
- package/dist/prepared-mcp-actor.d.ts +77 -0
- package/dist/prepared-mcp-permissions.d.ts +8 -0
- package/dist/routes/analytics-consent.d.ts +26 -0
- package/dist/routes/api-integrations.d.ts +11 -0
- package/dist/routes/api-keys.d.ts +7 -7
- package/dist/routes/automations.d.ts +2 -0
- package/dist/routes/billing.d.ts +59 -0
- package/dist/routes/browser-sessions.d.ts +65 -2
- package/dist/routes/chat-completions.d.ts +11 -0
- package/dist/routes/claude-core-usage.d.ts +20 -0
- 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 +182 -0
- package/dist/routes/codex-core.d.ts +401 -0
- package/dist/routes/codex.d.ts +64 -2
- package/dist/routes/computer-sessions.d.ts +6 -1
- package/dist/routes/desktop-distribution.d.ts +32 -0
- package/dist/routes/github-browser-fonts.gen.d.ts +2 -0
- package/dist/routes/github-browser-pages.d.ts +27 -0
- package/dist/routes/inbox.d.ts +13 -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 +28 -0
- package/dist/routes/interaction-placement-message.d.ts +7 -0
- package/dist/routes/managed-sign-in-methods.d.ts +6 -0
- package/dist/routes/model-account-choice.d.ts +13 -0
- package/dist/routes/model-account-settings.d.ts +19 -0
- package/dist/routes/native-app-auth.d.ts +12 -0
- package/dist/routes/organization-integration-policy.d.ts +8 -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-defaults.d.ts +9 -0
- package/dist/routes/organization-service-accounts.d.ts +3 -0
- package/dist/routes/personal-api-keys.d.ts +3 -0
- package/dist/routes/session-admin-access.d.ts +3 -0
- package/dist/routes/session-artifact-associations.d.ts +4 -0
- package/dist/routes/session-history-imports.d.ts +20 -0
- package/dist/routes/sessions.d.ts +38 -2
- package/dist/routes/site-data.d.ts +18 -0
- package/dist/routes/site-publishing.d.ts +5 -0
- package/dist/routes/site-runtime.d.ts +31 -0
- package/dist/routes/site-sql.d.ts +16 -0
- package/dist/routes/slack-bot-access.d.ts +3 -0
- package/dist/routes/subscription-account-pools.d.ts +21 -0
- package/dist/routes/subscription-core-http.d.ts +5 -0
- package/dist/routes/subscription-pool-access.d.ts +37 -0
- package/dist/routes/subscription-pool-core.d.ts +153 -0
- package/dist/routes/supergrok.d.ts +6 -6
- package/dist/routes/usage-allowances.d.ts +12 -0
- package/dist/routes/variable-sets-removed.d.ts +2 -0
- package/dist/routes/vault-providers.d.ts +34 -0
- package/dist/routes/vaults.d.ts +3 -0
- package/dist/routes/workspace-integrations.d.ts +35 -0
- package/dist/routes/workspace-model-providers.d.ts +4 -0
- package/dist/routes/workspaces.d.ts +11 -2
- package/dist/sandbox/agent-update.d.ts +64 -0
- package/dist/sandbox/auth-callout.d.ts +8 -0
- package/dist/sandbox/channel-a.d.ts +15 -1
- package/dist/sandbox/enrollment.d.ts +26 -4
- package/dist/sandbox/machine-audience.d.ts +29 -0
- package/dist/sandbox/machines.d.ts +15 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -2
- package/dist/sandbox/screen-control.d.ts +85 -0
- package/dist/sandbox/viewer.d.ts +5 -1
- package/dist/sandbox-file-artifacts.d.ts +17 -1
- package/dist/scheduled-task-blockers.d.ts +39 -0
- package/dist/scheduled-task-deletion.d.ts +3 -4
- package/dist/scheduled-task-lifecycle.d.ts +14 -0
- package/dist/secret-answers.d.ts +40 -0
- package/dist/session-create-observability.d.ts +5 -0
- package/dist/session-event-view-36GO3IL7.js +528 -0
- package/dist/session-event-view-36GO3IL7.js.map +1 -0
- package/dist/signed-state-secret.d.ts +18 -0
- package/dist/site-agent-guard.d.ts +53 -0
- package/dist/site-data/operations.d.ts +103 -0
- package/dist/site-data/service.d.ts +203 -0
- package/dist/site-frame-guard.d.ts +32 -0
- package/dist/site-publishing/agent-data-tools.d.ts +84 -0
- package/dist/site-publishing/agent-model.d.ts +58 -0
- package/dist/site-publishing/agent-session.d.ts +16 -0
- package/dist/site-publishing/content-scanner.d.ts +31 -0
- package/dist/site-publishing/edge-sync.d.ts +48 -0
- package/dist/site-publishing/gateway-signature.d.ts +56 -0
- package/dist/site-publishing/limits.d.ts +63 -0
- package/dist/site-publishing/page-tool-authority.d.ts +30 -0
- package/dist/site-publishing/platform-usage.d.ts +113 -0
- package/dist/site-publishing/platform.d.ts +21 -0
- package/dist/site-publishing/readiness.d.ts +62 -0
- package/dist/site-publishing/runtime.d.ts +179 -0
- package/dist/site-publishing/service.d.ts +264 -0
- package/dist/site-publishing/session-events.d.ts +22 -0
- package/dist/site-publishing/session-views.d.ts +36 -0
- package/dist/site-publishing/viewer.d.ts +40 -0
- package/dist/site-session-origin.d.ts +31 -6
- package/dist/site-sql/format.d.ts +47 -0
- package/dist/site-sql/gateway.d.ts +50 -0
- package/dist/site-sql/service.d.ts +213 -0
- package/dist/temporal-schedule-sync.d.ts +26 -0
- package/dist/transcription/billing-refusal.d.ts +8 -0
- package/dist/transcription/normalize.d.ts +23 -0
- package/dist/transcription/providers/azure-mai.d.ts +12 -0
- package/dist/transcription/providers/azure-openai.d.ts +4 -0
- package/dist/transcription/providers/openai.d.ts +16 -0
- package/dist/transcription/providers/subscription-core.d.ts +39 -0
- package/dist/transcription/service.d.ts +10 -1
- package/dist/transcription/wav-duration.d.ts +8 -0
- package/dist/workspace-tool-gateway-attestations.d.ts +37 -0
- package/dist/workspace-tool-gateway-observability.d.ts +16 -0
- package/dist/workspace-tool-gateway-secrets.d.ts +27 -0
- package/dist/workspace-tool-gateway.d.ts +102 -6
- package/dist/workspace-tool-target-fence.d.ts +116 -0
- package/dist/workspace-tool-target.d.ts +225 -0
- package/dist/workspace-webhook-dispatch.d.ts +26 -0
- package/dist/xai-subscription-core.d.ts +60 -0
- package/package.json +25 -20
- package/src/access-grant-rls.ts +64 -14
- package/src/admission-trace.ts +14 -0
- package/src/app.ts +1684 -252
- package/src/artifact-catalog-cursor.ts +1 -0
- package/src/attached-browser-viewer.ts +26 -0
- package/src/auth/managed-auth-attempt-context.ts +9 -0
- package/src/auth/managed-auth-rate-limits.ts +252 -0
- package/src/auth/managed-auth-session-adapter.ts +64 -29
- package/src/auth/managed-auth.ts +303 -35
- package/src/auth/managed-email.ts +18 -1
- 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 +3 -3
- package/src/auth/signup-funnel-metrics.ts +164 -0
- package/src/azure-live.ts +103 -0
- package/src/browser-action-fences.ts +45 -0
- package/src/browser-controller-authority.ts +1 -186
- package/src/browser-ephemeral.ts +39 -0
- package/src/browser-lost-runtime-end.ts +71 -0
- package/src/browser-state-authority.ts +1 -236
- package/src/claude-default-models.ts +37 -0
- package/src/claude-subscription-core.ts +154 -0
- package/src/claude-subscription-oauth.ts +352 -0
- package/src/claude-subscription-profile.ts +73 -0
- package/src/claude-subscription-usage.ts +51 -0
- package/src/claude-workspace-connection.ts +82 -0
- package/src/codemode.ts +236 -39
- package/src/codex-realtime.ts +181 -53
- package/src/codex-session-pointers.ts +80 -0
- package/src/company-brain-okf.ts +1 -1
- package/src/connected-machine-computer-access.ts +2 -1
- package/src/connection-ownership.ts +19 -13
- package/src/controller-data-plane.ts +34 -0
- package/src/credit-promotion-snapshot.ts +36 -0
- package/src/editable-artifact-production.ts +10 -0
- package/src/editable-artifact-source-session.ts +123 -0
- package/src/editable-artifact-websocket.ts +16 -3
- package/src/fatal-process-boundary.ts +67 -3
- package/src/gateway-realtime.ts +12 -8
- package/src/github-access.ts +31 -5
- package/src/github-browser-flow.ts +1 -1
- package/src/http/acting-person.ts +105 -0
- package/src/http/api-contract-compat.ts +49 -0
- package/src/http/api-error.ts +210 -2
- package/src/http/auth.ts +68 -4
- package/src/http/cors.ts +37 -5
- package/src/http/deprecation.ts +118 -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/rejection-telemetry.ts +120 -0
- package/src/http/request-body.ts +127 -0
- package/src/http/request-source.ts +154 -13
- package/src/http/sse.ts +88 -28
- package/src/http/unmatched-route.ts +60 -0
- package/src/http/user-content.ts +158 -0
- package/src/http/variable-sets-removed.ts +122 -0
- package/src/http/websocket-upgrade-dispatch.ts +29 -0
- package/src/index.ts +107 -57
- package/src/integration-connect-metrics.ts +228 -0
- package/src/integration-endpoint-test.ts +291 -0
- package/src/integrations/api-integrations.ts +81 -7
- package/src/integrations/atlassian.ts +202 -83
- package/src/integrations/fiken.ts +66 -38
- package/src/integrations/github-app-connect.ts +64 -18
- package/src/integrations/github-lens-connect.ts +8 -29
- package/src/integrations/google-drive.ts +448 -152
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/marketplace-plugin.ts +1 -1
- package/src/integrations/oauth-client.ts +733 -193
- package/src/integrations/oauth-profiles.ts +116 -45
- package/src/integrations/onepassword-vault-provider.ts +16 -0
- package/src/integrations/personal-github-repositories.ts +1 -1
- package/src/integrations/personal-github.ts +61 -35
- package/src/integrations/provider-oauth.ts +67 -85
- package/src/integrations/slack-app-home.ts +5 -5
- package/src/integrations/slack-bot.ts +893 -201
- package/src/integrations/slack-file-upload-flow.ts +147 -0
- package/src/integrations/slack-install.ts +7 -7
- package/src/integrations/slack-interactions.ts +1589 -299
- package/src/integrations/slack-mrkdwn.ts +193 -0
- package/src/integrations/slack-routing.ts +2 -2
- package/src/integrations/slack-session-defaults.ts +224 -0
- package/src/integrations/slack-task-file-upload.ts +323 -0
- package/src/integrations/slack-tool-review.ts +65 -0
- package/src/integrations/social-oauth.ts +90 -58
- package/src/interaction-agent-access.ts +40 -20
- package/src/interaction-agent-version.ts +85 -0
- package/src/interaction-holder-heartbeat.ts +24 -8
- package/src/interaction-metrics.ts +164 -2
- package/src/interaction-route-stages.ts +94 -0
- package/src/interaction-secrets-vault.ts +213 -0
- package/src/interaction-secrets.ts +426 -0
- package/src/mcp/action-catalog-types.ts +14 -0
- package/src/mcp/action-catalog.gen.ts +8014 -0
- package/src/mcp/action-descriptions.ts +299 -0
- package/src/mcp/action-schema-imports.gen.ts +304 -0
- package/src/mcp/capability-account-readiness.ts +17 -0
- package/src/mcp/child-read-evidence.ts +19 -0
- package/src/mcp/company-profile-agent-admin.ts +13 -0
- package/src/mcp/contract-input.ts +165 -0
- package/src/mcp/documents.ts +9 -2
- package/src/mcp/editable-artifacts.ts +29 -11
- package/src/mcp/files.ts +5 -1
- package/src/mcp/first-party-tool-permissions.ts +7 -285
- package/src/mcp/knowledge-entries.ts +108 -34
- package/src/mcp/notification-tools.ts +480 -0
- package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
- package/src/mcp/scheduled-task-input.ts +89 -0
- package/src/mcp/scheduled-task-view.ts +63 -3
- package/src/mcp/secret-tools.ts +185 -0
- package/src/mcp/server.ts +2213 -1116
- package/src/mcp/session-admin-access-tools.ts +163 -0
- package/src/mcp/session-event-errors.ts +123 -0
- package/src/mcp/session-event-view.ts +395 -13
- package/src/mcp/session-view.ts +3 -0
- package/src/mcp/session-wait.ts +67 -7
- package/src/mcp/site-agent-clamp-tools.ts +140 -0
- package/src/mcp/site-data-tools.ts +339 -0
- package/src/mcp/site-publication-tools.ts +201 -0
- package/src/mcp/site-sql-tools.ts +427 -0
- package/src/mcp-oauth.ts +635 -45
- package/src/model-calls/chat-completions.ts +352 -0
- package/src/model-calls/pending-usage-settlements.ts +125 -0
- package/src/model-calls/service.ts +694 -0
- package/src/model-catalog.ts +68 -17
- package/src/native-push-dispatch.ts +457 -0
- package/src/observability.ts +15 -1
- package/src/organization-mcp.ts +485 -0
- package/src/prepared-mcp-actor.ts +154 -0
- package/src/prepared-mcp-permissions.ts +38 -0
- package/src/routes/analytics-consent.ts +123 -0
- package/src/routes/api-integrations.ts +268 -9
- package/src/routes/api-keys.ts +164 -25
- package/src/routes/artifact-catalog.ts +111 -0
- package/src/routes/automations.ts +163 -90
- package/src/routes/billing.ts +535 -59
- package/src/routes/browser-sessions.ts +966 -342
- package/src/routes/canonical-human-identities.ts +61 -0
- package/src/routes/capabilities.ts +112 -5
- package/src/routes/channels.ts +4 -3
- package/src/routes/chat-completions.ts +235 -0
- package/src/routes/claude-core-usage.ts +147 -0
- package/src/routes/claude-subscription-accounts.ts +233 -0
- package/src/routes/claude-subscription-oauth.ts +175 -0
- package/src/routes/client-errors.ts +394 -0
- package/src/routes/codex-core.ts +687 -0
- package/src/routes/codex.ts +1277 -1119
- package/src/routes/company-brain.ts +4 -0
- package/src/routes/computer-sessions.ts +319 -85
- package/src/routes/connect.ts +277 -175
- package/src/routes/connections.ts +690 -363
- package/src/routes/desktop-distribution.ts +217 -0
- package/src/routes/documents.ts +13 -7
- package/src/routes/editable-artifacts.ts +74 -4
- package/src/routes/enrollments.ts +110 -16
- package/src/routes/external-identity-links.ts +13 -12
- package/src/routes/files.ts +41 -16
- package/src/routes/github-browser-fonts.gen.ts +12 -0
- package/src/routes/github-browser-pages.ts +258 -0
- package/src/routes/github.ts +200 -75
- package/src/routes/inbox.ts +353 -0
- package/src/routes/insights-response-cache.ts +83 -0
- package/src/routes/insights-usage.ts +345 -0
- package/src/routes/insights.ts +103 -8
- package/src/routes/install.ts +51 -10
- package/src/routes/integration-facets.ts +70 -28
- package/src/routes/interaction-placement-message.ts +12 -0
- package/src/routes/knowledge.ts +136 -27
- package/src/routes/machines.ts +189 -135
- package/src/routes/managed-auth-session-sets.ts +43 -10
- package/src/routes/managed-onboarding.ts +39 -19
- package/src/routes/managed-sign-in-methods.ts +433 -0
- package/src/routes/memory-slack-publications.ts +3 -2
- package/src/routes/model-account-choice.ts +194 -0
- package/src/routes/model-account-settings.ts +235 -0
- package/src/routes/model-connection-access.ts +210 -48
- package/src/routes/native-app-auth.ts +296 -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 +183 -0
- package/src/routes/organization-memberships.ts +205 -19
- package/src/routes/organization-model-defaults.ts +110 -0
- package/src/routes/organization-model-providers.ts +74 -3
- package/src/routes/organization-service-accounts.ts +128 -0
- package/src/routes/organization-sessions.ts +21 -5
- package/src/routes/personal-api-keys.ts +184 -0
- package/src/routes/personal-github-git-broker.ts +2 -2
- package/src/routes/personal-github.ts +9 -4
- package/src/routes/plugins.ts +46 -7
- package/src/routes/pr-review-github.ts +105 -118
- package/src/routes/pr-review.ts +88 -82
- package/src/routes/preference-registry.ts +18 -0
- package/src/routes/rigs.ts +48 -13
- package/src/routes/scheduled-tasks.ts +347 -27
- package/src/routes/session-admin-access.ts +271 -0
- package/src/routes/session-artifact-associations.ts +164 -0
- package/src/routes/session-history-imports.ts +305 -0
- package/src/routes/sessions.ts +1682 -363
- package/src/routes/site-data.ts +340 -0
- package/src/routes/site-publishing.ts +584 -0
- package/src/routes/site-runtime.ts +1944 -0
- package/src/routes/site-sql.ts +263 -0
- package/src/routes/skill-content.ts +72 -7
- package/src/routes/skills.ts +5 -4
- package/src/routes/slack-bot-access.ts +108 -0
- package/src/routes/social.ts +27 -15
- package/src/routes/subscription-account-pools.ts +209 -0
- package/src/routes/subscription-core-http.ts +18 -0
- package/src/routes/subscription-pool-access.ts +180 -0
- package/src/routes/subscription-pool-core.ts +552 -0
- package/src/routes/supergrok.ts +174 -655
- package/src/routes/transcription-recordings.ts +71 -4
- package/src/routes/transcriptions.ts +26 -2
- package/src/routes/usage-allowances.ts +381 -0
- package/src/routes/variable-sets-removed.ts +19 -0
- package/src/routes/vault-providers.ts +782 -0
- package/src/routes/vaults.ts +421 -0
- package/src/routes/video-generation.ts +30 -12
- package/src/routes/workspace-artifacts.ts +165 -53
- package/src/routes/workspace-integrations.ts +576 -0
- package/src/routes/workspace-model-providers.ts +148 -0
- package/src/routes/workspace-state.ts +4 -0
- package/src/routes/workspaces.ts +625 -117
- package/src/sandbox/agent-update.ts +315 -0
- package/src/sandbox/auth-callout.ts +104 -21
- package/src/sandbox/channel-a.ts +165 -55
- package/src/sandbox/enrollment.ts +131 -22
- package/src/sandbox/machine-audience.ts +192 -0
- package/src/sandbox/machines.ts +75 -31
- package/src/sandbox/metrics-ingestion.ts +133 -2
- package/src/sandbox/rematerialize.ts +51 -21
- package/src/sandbox/screen-control.ts +289 -0
- package/src/sandbox/viewer.ts +33 -58
- package/src/sandbox-file-artifacts.ts +53 -27
- package/src/scheduled-task-blockers.ts +293 -0
- package/src/scheduled-task-deletion.ts +13 -3
- package/src/scheduled-task-lifecycle.ts +84 -0
- package/src/secret-answers.ts +119 -0
- package/src/session-create-observability.ts +46 -0
- package/src/session-realtime-context.ts +26 -4
- package/src/signed-state-secret.ts +58 -0
- package/src/site-agent-guard.ts +261 -0
- package/src/site-data/operations.ts +777 -0
- package/src/site-data/service.ts +1043 -0
- package/src/site-frame-guard.ts +107 -0
- package/src/site-publishing/agent-data-tools.ts +250 -0
- package/src/site-publishing/agent-model.ts +127 -0
- package/src/site-publishing/agent-session.ts +148 -0
- package/src/site-publishing/content-scanner.ts +93 -0
- package/src/site-publishing/edge-sync.ts +148 -0
- package/src/site-publishing/gateway-signature.ts +198 -0
- package/src/site-publishing/limits.ts +315 -0
- package/src/site-publishing/page-tool-authority.ts +111 -0
- package/src/site-publishing/platform-usage.ts +479 -0
- package/src/site-publishing/platform.ts +98 -0
- package/src/site-publishing/readiness.ts +330 -0
- package/src/site-publishing/runtime.ts +831 -0
- package/src/site-publishing/service.ts +1282 -0
- package/src/site-publishing/session-events.ts +262 -0
- package/src/site-publishing/session-views.ts +262 -0
- package/src/site-publishing/viewer.ts +75 -0
- package/src/site-session-origin.ts +91 -6
- package/src/site-sql/format.ts +134 -0
- package/src/site-sql/gateway.ts +168 -0
- package/src/site-sql/service.ts +1636 -0
- package/src/site-uploads.ts +27 -5
- package/src/temporal-schedule-sync.ts +74 -0
- package/src/transcription/billing-refusal.ts +14 -0
- package/src/transcription/normalize.ts +77 -0
- package/src/transcription/providers/azure-mai.ts +98 -0
- package/src/transcription/providers/azure-openai.ts +16 -7
- package/src/transcription/providers/codex-subscription.ts +190 -67
- package/src/transcription/providers/openai.ts +79 -5
- package/src/transcription/providers/subscription-core.ts +125 -0
- package/src/transcription/providers/xai-subscription.ts +81 -83
- package/src/transcription/segmenter.ts +7 -1
- package/src/transcription/service.ts +239 -10
- package/src/transcription/wav-duration.ts +39 -0
- package/src/workspace-deletion.ts +25 -1
- package/src/workspace-tool-gateway-attestations.ts +133 -0
- package/src/workspace-tool-gateway-observability.ts +64 -0
- package/src/workspace-tool-gateway-secrets.ts +104 -0
- package/src/workspace-tool-gateway.ts +921 -103
- package/src/workspace-tool-target-fence.ts +380 -0
- package/src/workspace-tool-target.ts +554 -0
- package/src/workspace-webhook-dispatch.ts +225 -0
- package/src/xai-realtime.ts +85 -81
- package/src/xai-subscription-core.ts +172 -0
- package/dist/browser-auth-broker.d.ts +0 -20
- package/dist/chunk-AJ62T5QN.js.map +0 -1
- package/dist/connection-authority-owner.d.ts +0 -59
- package/dist/routes/connection-authorities.d.ts +0 -3
- package/dist/routes/environments.d.ts +0 -5
- package/dist/routes/host-mcp-bindings.d.ts +0 -5
- package/dist/routes/host-mcp-resolvers.d.ts +0 -3
- package/dist/routes/packs.d.ts +0 -3
- package/dist/session-event-view-MU3HMNV3.js +0 -264
- package/dist/session-event-view-MU3HMNV3.js.map +0 -1
- package/dist/xai-subscription-auth.d.ts +0 -23
- package/src/browser-auth-broker.ts +0 -155
- package/src/connection-authority-owner.ts +0 -61
- package/src/routes/connection-authorities.ts +0 -139
- package/src/routes/environments.ts +0 -388
- package/src/routes/host-mcp-bindings.ts +0 -122
- package/src/routes/host-mcp-resolvers.ts +0 -41
- package/src/routes/packs.ts +0 -784
- package/src/xai-subscription-auth.ts +0 -139
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
import { attachedBrowserViewer } from "../attached-browser-viewer";
|
|
2
|
+
import {
|
|
3
|
+
assertEphemeralBrowserCreateEnabled,
|
|
4
|
+
ephemeralBrowserPartition,
|
|
5
|
+
} from "../browser-ephemeral";
|
|
1
6
|
import { randomUUID } from "node:crypto";
|
|
2
7
|
import {
|
|
3
8
|
environmentsEncryptionKeyBytes,
|
|
4
9
|
resolveFirstPartyDelegationSecret,
|
|
5
10
|
resolveStreamTokenSecret,
|
|
11
|
+
sandboxWarmRateMicrosPerSecond,
|
|
6
12
|
} from "@opengeni/config";
|
|
7
13
|
import {
|
|
8
14
|
BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX,
|
|
15
|
+
EPHEMERAL_CHROMIUM_DRIVER_ID,
|
|
16
|
+
browserSessionStorageMode,
|
|
9
17
|
BROWSER_CONTROL_WEBSOCKET_PROTOCOL,
|
|
10
18
|
BROWSER_CONTROL_PORT,
|
|
11
19
|
BROWSER_CONTROL_PROTOCOL_VERSION,
|
|
@@ -14,16 +22,18 @@ import {
|
|
|
14
22
|
AttachedBrowserDevice,
|
|
15
23
|
AttachedBrowserDeviceListResponse,
|
|
16
24
|
BrowserActionCommand,
|
|
25
|
+
browserActionSecretTexts,
|
|
17
26
|
BrowserActionRequest,
|
|
18
27
|
BrowserDiagnosticKind,
|
|
19
28
|
BrowserDownload,
|
|
20
29
|
BrowserDownloadSaveRequest,
|
|
21
30
|
BrowserDownloadSaveResponse,
|
|
22
31
|
BrowserDownloadListResponse,
|
|
32
|
+
BrowserDomReadRequest,
|
|
33
|
+
BrowserDomReadResponse,
|
|
23
34
|
BrowserExternalAuthCommand,
|
|
24
35
|
BrowserOpenTargetRequest,
|
|
25
36
|
BrowserObservation,
|
|
26
|
-
BrowserProtectedAuthFillCommand,
|
|
27
37
|
BrowserSessionAttachment,
|
|
28
38
|
BrowserSessionAttachmentRequest,
|
|
29
39
|
BrowserSessionHeartbeatResponse,
|
|
@@ -31,21 +41,19 @@ import {
|
|
|
31
41
|
BrowserSessionListResponse,
|
|
32
42
|
BrowserSessionMutationResponse,
|
|
33
43
|
BrowserTargetListResponse,
|
|
44
|
+
BrowserTargetState,
|
|
34
45
|
CreateBrowserSessionRequest,
|
|
35
46
|
ExternalAuthInteractiveRequest,
|
|
36
47
|
ExternalAuthInteractiveResponse,
|
|
37
48
|
ExternalAuthRunRequest,
|
|
38
49
|
ExternalAuthRunResponse,
|
|
39
50
|
InteractionActor,
|
|
40
|
-
ProtectedAuthFillRequest,
|
|
41
|
-
ProtectedAuthFillResponse,
|
|
42
51
|
ReportAuthRunRequest,
|
|
43
52
|
StartAuthRunRequest,
|
|
44
53
|
VerifyAuthRunRequest,
|
|
45
54
|
PublishBrowserRevisionRequest,
|
|
46
55
|
PublishBrowserRevisionResponse,
|
|
47
56
|
type AccessGrant,
|
|
48
|
-
type BrowserProtectedAuthFillReceipt as BrowserProtectedAuthFillReceiptValue,
|
|
49
57
|
type BrowserActionRequest as BrowserActionRequestValue,
|
|
50
58
|
type BrowserDownload as BrowserDownloadValue,
|
|
51
59
|
type BrowserSession as BrowserSessionValue,
|
|
@@ -56,7 +64,6 @@ import {
|
|
|
56
64
|
type BrowserRevisionMaterialization as BrowserRevisionMaterializationValue,
|
|
57
65
|
type Session,
|
|
58
66
|
type SessionAuthorizationOperation,
|
|
59
|
-
type SiteAuthAuthority,
|
|
60
67
|
} from "@opengeni/contracts";
|
|
61
68
|
import {
|
|
62
69
|
getSessionAuthorityEpoch,
|
|
@@ -75,21 +82,23 @@ import {
|
|
|
75
82
|
BrowserSessionNotFoundError,
|
|
76
83
|
BrowserSessionOperationConflictError,
|
|
77
84
|
BrowserSessionStateError,
|
|
85
|
+
SandboxPaidComputeAdmissionError,
|
|
78
86
|
completeBrowserSessionEnd,
|
|
79
87
|
completeExternalAuth,
|
|
80
88
|
completeBrowserDownloadSave,
|
|
81
89
|
completeFileUpload,
|
|
82
|
-
completeProtectedAuthFill,
|
|
83
90
|
commitBrowserSessionSuspension,
|
|
84
91
|
commitBrowserRevisionPublication,
|
|
85
92
|
dispatchBrowserRevisionPublication,
|
|
86
93
|
dispatchBrowserDownloadSave,
|
|
87
94
|
dispatchBrowserSessionOperation,
|
|
88
95
|
dispatchExternalAuth,
|
|
89
|
-
dispatchProtectedAuthFill,
|
|
90
96
|
failBrowserSessionOperation,
|
|
97
|
+
failPreparedBrowserSessionEnd,
|
|
98
|
+
failPreparedBrowserSessionSuspend,
|
|
91
99
|
failBrowserSessionResume,
|
|
92
100
|
failBrowserSessionResumePreparation,
|
|
101
|
+
getExpiredBrowserSessionResume,
|
|
93
102
|
failBrowserSessionSuspension,
|
|
94
103
|
failBrowserRevisionPublication,
|
|
95
104
|
findBrowserSessionControlRecordByOperation,
|
|
@@ -106,8 +115,6 @@ import {
|
|
|
106
115
|
getFileUpload,
|
|
107
116
|
getAuthRun,
|
|
108
117
|
getExternalAuthPreparation,
|
|
109
|
-
getProtectedAuthFillPreparation,
|
|
110
|
-
getSiteAuthConnection,
|
|
111
118
|
getSession,
|
|
112
119
|
InteractionResourceConflictError,
|
|
113
120
|
InteractionResourceNotFoundError,
|
|
@@ -115,20 +122,22 @@ import {
|
|
|
115
122
|
listAuthRuns,
|
|
116
123
|
listBrowserSessions,
|
|
117
124
|
listAttachedBrowserDevices,
|
|
125
|
+
interactionControllerGone,
|
|
118
126
|
prepareBrowserSessionCreate,
|
|
127
|
+
markEphemeralBrowserSessionLost,
|
|
119
128
|
prepareExternalAuth,
|
|
120
129
|
prepareBrowserDownloadSave,
|
|
121
130
|
prepareBrowserSessionEnd,
|
|
122
131
|
prepareBrowserSessionResume,
|
|
132
|
+
settleExpiredBrowserSessionResume,
|
|
133
|
+
continueExpiredBrowserSessionResume,
|
|
123
134
|
prepareBrowserSessionSuspend,
|
|
124
135
|
prepareBrowserRevisionPublication,
|
|
125
|
-
prepareProtectedAuthFill,
|
|
126
136
|
reportAuthRun,
|
|
127
137
|
releaseLeaseHolder,
|
|
128
138
|
readLease,
|
|
129
139
|
recordLeaseControllerDataPlaneUrl,
|
|
130
140
|
loadConnectionCredentialForBroker,
|
|
131
|
-
markProtectedAuthFillOutcomeUnknown,
|
|
132
141
|
startAuthRun,
|
|
133
142
|
touchBrowserSessionController,
|
|
134
143
|
verifyAuthRun,
|
|
@@ -169,6 +178,8 @@ import {
|
|
|
169
178
|
provisionBrowserControlClient,
|
|
170
179
|
renewSandboxProviderExpiration,
|
|
171
180
|
type BrowserControlPlacementSession,
|
|
181
|
+
type BrowserControlScreenshotOptions,
|
|
182
|
+
type BrowserControlFrame,
|
|
172
183
|
type PlacementBrowserStateCaptureReceipt,
|
|
173
184
|
type PlacementBrowserNetworkRoute,
|
|
174
185
|
type PlacementBrowserTransport,
|
|
@@ -189,9 +200,14 @@ import {
|
|
|
189
200
|
controllerCacheAllowsHostFetch,
|
|
190
201
|
controllerCachedUrlIsUsable,
|
|
191
202
|
shouldPersistControllerDataPlaneUrl,
|
|
203
|
+
isRetryableControllerTransport,
|
|
192
204
|
withCachedController,
|
|
205
|
+
withControllerTransportRecovery,
|
|
193
206
|
} from "../controller-data-plane";
|
|
194
|
-
import {
|
|
207
|
+
import {
|
|
208
|
+
filterInteractionSessionsForGrant,
|
|
209
|
+
parseInteractionSessionListQuery,
|
|
210
|
+
} from "../interaction-agent-access";
|
|
195
211
|
import { withInteractionHolderHeartbeat } from "../interaction-holder-heartbeat";
|
|
196
212
|
import {
|
|
197
213
|
browserStateArtifactAad,
|
|
@@ -201,11 +217,13 @@ import {
|
|
|
201
217
|
unwrapBrowserStateDataKey,
|
|
202
218
|
wrapBrowserStateDataKey,
|
|
203
219
|
} from "../browser-state-authority";
|
|
204
|
-
import {
|
|
205
|
-
BrowserAuthCredentialError,
|
|
206
|
-
resolveProtectedAuthFieldValues,
|
|
207
|
-
} from "../browser-auth-broker";
|
|
208
220
|
import { managedNetworkRouteForPlacement } from "../browser-network-route";
|
|
221
|
+
import {
|
|
222
|
+
endBrowserDiscardingLostRuntime,
|
|
223
|
+
endExpiredResumeSettingAsideInterruptedLaunch,
|
|
224
|
+
} from "../browser-lost-runtime-end";
|
|
225
|
+
import { resolveBrowserActionFences, type BrowserActionFences } from "../browser-action-fences";
|
|
226
|
+
import { InteractionRouteStages, reportInteractionRouteStages } from "../interaction-route-stages";
|
|
209
227
|
import { validateInteractionRequestOrigin } from "../http/cors";
|
|
210
228
|
import { ApiHttpError } from "../http/api-error";
|
|
211
229
|
import { interactionControlApiError } from "../http/interaction-control-error";
|
|
@@ -221,6 +239,15 @@ import {
|
|
|
221
239
|
} from "../interaction-metrics";
|
|
222
240
|
import { withChannelA, withChannelARead, type ChannelAOperation } from "../sandbox/channel-a";
|
|
223
241
|
import { sanitizeFilename } from "./files";
|
|
242
|
+
import { USER_CONTENT_SECURITY_HEADERS } from "../http/user-content";
|
|
243
|
+
import { wrongInteractionPlacementMessage } from "./interaction-placement-message";
|
|
244
|
+
import {
|
|
245
|
+
releaseBrowserPasskeys,
|
|
246
|
+
resolveInteractionSecretValues,
|
|
247
|
+
syncBrowserPasskeys,
|
|
248
|
+
type InteractionSecretsPort,
|
|
249
|
+
} from "../interaction-secrets";
|
|
250
|
+
import { requirePlacementAcceptsSecretValues } from "../interaction-agent-version";
|
|
224
251
|
|
|
225
252
|
const BROWSER_DRIVER_ID = "opengeni.cdp.v1";
|
|
226
253
|
const LIGHTPANDA_DRIVER_ID = "opengeni.lightpanda.cdp.v1";
|
|
@@ -237,7 +264,12 @@ type BrowserPlacement = {
|
|
|
237
264
|
transport: PlacementBrowserTransport;
|
|
238
265
|
};
|
|
239
266
|
|
|
240
|
-
export function registerBrowserSessionRoutes(
|
|
267
|
+
export function registerBrowserSessionRoutes(
|
|
268
|
+
app: Hono,
|
|
269
|
+
deps: ApiRouteDeps,
|
|
270
|
+
options: { interactionSecrets?: InteractionSecretsPort | null } = {},
|
|
271
|
+
): void {
|
|
272
|
+
const interactionSecrets = options.interactionSecrets ?? null;
|
|
241
273
|
const channelServices = {
|
|
242
274
|
db: deps.db,
|
|
243
275
|
settings: deps.settings,
|
|
@@ -252,8 +284,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
252
284
|
return context.json(
|
|
253
285
|
AttachedBrowserDeviceListResponse.parse(
|
|
254
286
|
await listAttachedBrowserDevices(deps.db, {
|
|
255
|
-
|
|
256
|
-
workspaceId,
|
|
287
|
+
...(await attachedBrowserViewer(deps, grant, workspaceId)),
|
|
257
288
|
includeDisconnected,
|
|
258
289
|
}),
|
|
259
290
|
),
|
|
@@ -268,8 +299,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
268
299
|
return context.json(
|
|
269
300
|
AttachedBrowserDevice.parse(
|
|
270
301
|
await getAttachedBrowserDevice(deps.db, {
|
|
271
|
-
|
|
272
|
-
workspaceId,
|
|
302
|
+
...(await attachedBrowserViewer(deps, grant, workspaceId)),
|
|
273
303
|
deviceId,
|
|
274
304
|
}),
|
|
275
305
|
),
|
|
@@ -288,6 +318,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
288
318
|
const listed = await listBrowserSessions(deps.db, {
|
|
289
319
|
accountId: grant.accountId,
|
|
290
320
|
workspaceId,
|
|
321
|
+
...parseInteractionSessionListQuery(context.req.query()),
|
|
291
322
|
});
|
|
292
323
|
return context.json(
|
|
293
324
|
BrowserSessionListResponse.parse({
|
|
@@ -319,11 +350,18 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
319
350
|
const grant = await requireAccessGrant(context, deps, workspaceId, "sessions:control");
|
|
320
351
|
const request = await parseJsonBody(context, CreateBrowserSessionRequest);
|
|
321
352
|
const startedAtMs = performance.now();
|
|
353
|
+
const stages = new InteractionRouteStages("create");
|
|
322
354
|
await authorizeSourceSession(deps, grant, request.sessionId, "session.control");
|
|
355
|
+
stages.mark("authorize");
|
|
323
356
|
const origin = requestOrigin(context, deps.settings);
|
|
324
357
|
const authority = browserAuthorityRoot(deps);
|
|
358
|
+
let prepared: Awaited<ReturnType<typeof prepareBrowserSessionCreate>> | null = null;
|
|
325
359
|
|
|
326
360
|
try {
|
|
361
|
+
assertEphemeralBrowserCreateEnabled(
|
|
362
|
+
request,
|
|
363
|
+
deps.settings.experimentalBrowserContextPoolEnabled,
|
|
364
|
+
);
|
|
327
365
|
const existing = await findBrowserSessionControlRecordByOperation(deps.db, {
|
|
328
366
|
accountId: grant.accountId,
|
|
329
367
|
workspaceId,
|
|
@@ -335,7 +373,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
335
373
|
);
|
|
336
374
|
}
|
|
337
375
|
if (existing) assertCreateReplay(request, existing.session);
|
|
338
|
-
|
|
376
|
+
stages.mark("replay_check");
|
|
377
|
+
prepared = existing
|
|
339
378
|
? await prepareBrowserSessionCreate(
|
|
340
379
|
deps.db,
|
|
341
380
|
browserCreateInput(grant, workspaceId, request, existing.session.placement),
|
|
@@ -362,6 +401,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
362
401
|
}
|
|
363
402
|
|
|
364
403
|
const sourceSession = await requireSourceSession(deps, workspaceId, request.sessionId);
|
|
404
|
+
stages.mark("source_session");
|
|
365
405
|
const response = await withBrowserPlacement(
|
|
366
406
|
sourceSession,
|
|
367
407
|
grant,
|
|
@@ -370,6 +410,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
370
410
|
"browser.create",
|
|
371
411
|
context.req.raw.signal,
|
|
372
412
|
async (placement) => {
|
|
413
|
+
stages.mark("placement_resolve");
|
|
373
414
|
let revisionAuthority: BrowserRevisionArtifactAuthority | null = null;
|
|
374
415
|
if (!prepared) {
|
|
375
416
|
prepared = await prepareBrowserSessionCreate(
|
|
@@ -377,6 +418,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
377
418
|
browserCreateInput(grant, workspaceId, request, placement.placement),
|
|
378
419
|
);
|
|
379
420
|
}
|
|
421
|
+
stages.mark("prepare");
|
|
380
422
|
if (isTerminalOperation(prepared.operation.state)) return prepared;
|
|
381
423
|
|
|
382
424
|
if (prepared.session.baseRevisionId && !revisionAuthority) {
|
|
@@ -397,6 +439,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
397
439
|
placement.placement,
|
|
398
440
|
revisionAuthority,
|
|
399
441
|
);
|
|
442
|
+
stages.mark("restore");
|
|
400
443
|
|
|
401
444
|
try {
|
|
402
445
|
const networkRoute = await resolveBrowserNetworkRouteLaunch({
|
|
@@ -408,6 +451,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
408
451
|
rootSecret: authority,
|
|
409
452
|
placement: placement.placement,
|
|
410
453
|
});
|
|
454
|
+
stages.mark("network_route");
|
|
411
455
|
const interactionHeld = await ensureInteractionHolder(
|
|
412
456
|
deps,
|
|
413
457
|
grant,
|
|
@@ -416,6 +460,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
416
460
|
placement,
|
|
417
461
|
context.req.raw.signal,
|
|
418
462
|
);
|
|
463
|
+
stages.mark("holder");
|
|
419
464
|
const record = await ensureDispatchedGeneration(
|
|
420
465
|
deps,
|
|
421
466
|
grant,
|
|
@@ -435,6 +480,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
435
480
|
}
|
|
436
481
|
throw error;
|
|
437
482
|
});
|
|
483
|
+
stages.mark("generation");
|
|
438
484
|
const preparedSession = prepared.session;
|
|
439
485
|
const controllerGeneration = requireOperationGeneration(record);
|
|
440
486
|
const adminToken = deriveBrowserControllerAdminToken({
|
|
@@ -482,18 +528,25 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
482
528
|
controllerGeneration,
|
|
483
529
|
},
|
|
484
530
|
async () => {
|
|
531
|
+
stages.mark("provision");
|
|
485
532
|
await ensureHeadedBrowserDisplayStack(
|
|
486
533
|
placement.session,
|
|
487
534
|
preparedSession.headless,
|
|
488
535
|
linkedComputer !== null,
|
|
489
536
|
);
|
|
537
|
+
stages.mark("display");
|
|
490
538
|
return await client.createSession({
|
|
491
539
|
browserSessionId: preparedSession.id,
|
|
492
540
|
controllerGeneration,
|
|
493
541
|
tokenGeneration: record.tokenGeneration,
|
|
494
542
|
...tokens,
|
|
495
543
|
headed: !preparedSession.headless,
|
|
496
|
-
transport: browserRuntimeTransport(
|
|
544
|
+
transport: browserRuntimeTransport(
|
|
545
|
+
preparedSession,
|
|
546
|
+
placement.transport,
|
|
547
|
+
placement.placementInstanceId,
|
|
548
|
+
authority,
|
|
549
|
+
),
|
|
497
550
|
...(linkedComputer ? { linkedComputer } : {}),
|
|
498
551
|
...(networkRoute ? { networkRoute } : {}),
|
|
499
552
|
...(request.initialUrl ? { initialUrl: request.initialUrl } : {}),
|
|
@@ -503,9 +556,11 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
503
556
|
);
|
|
504
557
|
},
|
|
505
558
|
);
|
|
559
|
+
stages.mark("launch");
|
|
506
560
|
await cacheBrowserControllerPlacement(grant, workspaceId, placement).catch(
|
|
507
561
|
() => placement,
|
|
508
562
|
);
|
|
563
|
+
stages.mark("placement_cache_write");
|
|
509
564
|
} catch (error) {
|
|
510
565
|
if (
|
|
511
566
|
error instanceof BrowserControlTransportError ||
|
|
@@ -538,7 +593,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
538
593
|
// Once the controller has accepted the exact binding, a persistence
|
|
539
594
|
// failure is retried against that same idempotent physical session.
|
|
540
595
|
// Never terminalize or release its placement authority here.
|
|
541
|
-
|
|
596
|
+
const activated = await activateBrowserSession(deps.db, {
|
|
542
597
|
accountId: grant.accountId,
|
|
543
598
|
workspaceId,
|
|
544
599
|
operationId: request.operationId,
|
|
@@ -550,6 +605,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
550
605
|
},
|
|
551
606
|
engineVersion: null,
|
|
552
607
|
});
|
|
608
|
+
stages.mark("activate");
|
|
609
|
+
return activated;
|
|
553
610
|
} finally {
|
|
554
611
|
restore?.dataKey.fill(0);
|
|
555
612
|
restore?.aad.fill(0);
|
|
@@ -558,11 +615,24 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
558
615
|
);
|
|
559
616
|
const parsed = BrowserSessionMutationResponse.parse(response);
|
|
560
617
|
observeLifecycleResult(deps.observability, startedAtMs, parsed);
|
|
618
|
+
reportInteractionRouteStages(context, deps.observability, stages);
|
|
561
619
|
return context.json(
|
|
562
620
|
parsed,
|
|
563
621
|
parsed.operation.state === "completed" && !parsed.operation.replayed ? 201 : 200,
|
|
564
622
|
);
|
|
565
623
|
} catch (error) {
|
|
624
|
+
if (prepared) {
|
|
625
|
+
// The operation lock fences a concurrent dispatch. Once a controller
|
|
626
|
+
// binding exists, retain its exact receipt for physical reconciliation.
|
|
627
|
+
await failBrowserSessionOperation(deps.db, {
|
|
628
|
+
accountId: grant.accountId,
|
|
629
|
+
workspaceId,
|
|
630
|
+
operationId: request.operationId,
|
|
631
|
+
browserSessionId: prepared.session.id,
|
|
632
|
+
onlyIfPreparedCreate: true,
|
|
633
|
+
error: interactionFailure(error),
|
|
634
|
+
}).catch(() => undefined);
|
|
635
|
+
}
|
|
566
636
|
throw browserRouteError(error);
|
|
567
637
|
}
|
|
568
638
|
});
|
|
@@ -614,6 +684,28 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
614
684
|
},
|
|
615
685
|
);
|
|
616
686
|
|
|
687
|
+
app.post(
|
|
688
|
+
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/targets/open-with-inventory",
|
|
689
|
+
async (context) => {
|
|
690
|
+
const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
|
|
691
|
+
context,
|
|
692
|
+
"sessions:control",
|
|
693
|
+
);
|
|
694
|
+
const request = await parseJsonBody(context, BrowserOpenTargetRequest);
|
|
695
|
+
const result = await withActiveBrowserController(
|
|
696
|
+
context,
|
|
697
|
+
grant,
|
|
698
|
+
workspaceId,
|
|
699
|
+
browserSessionId,
|
|
700
|
+
"session.control",
|
|
701
|
+
"browser.control",
|
|
702
|
+
async ({ sessionClient }) =>
|
|
703
|
+
BrowserTargetListResponse.parse(await sessionClient.openTargetWithInventory(request.url)),
|
|
704
|
+
);
|
|
705
|
+
return context.json(result, 201);
|
|
706
|
+
},
|
|
707
|
+
);
|
|
708
|
+
|
|
617
709
|
app.post(
|
|
618
710
|
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/targets/:targetId/select",
|
|
619
711
|
async (context) => {
|
|
@@ -889,6 +981,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
889
981
|
workspaceId,
|
|
890
982
|
session: sourceSession,
|
|
891
983
|
subjectId: grant.subjectId,
|
|
984
|
+
grant,
|
|
892
985
|
waitSignal: context.req.raw.signal,
|
|
893
986
|
operation: "browser.download.save",
|
|
894
987
|
},
|
|
@@ -898,6 +991,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
898
991
|
destinationPath: save.destinationPath,
|
|
899
992
|
overwrite: save.overwrite,
|
|
900
993
|
mayReplaceExisting: save.overwrite && dispatched.dispatchedNow,
|
|
994
|
+
// Missing folders on the relative destination are created; the
|
|
995
|
+
// import still confines every existing ancestor to the workspace.
|
|
996
|
+
createParents: true,
|
|
901
997
|
sizeBytes: save.download.receivedBytes,
|
|
902
998
|
sha256: save.download.sha256!,
|
|
903
999
|
source: {
|
|
@@ -934,11 +1030,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
934
1030
|
app.get(
|
|
935
1031
|
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/targets/:targetId/observation",
|
|
936
1032
|
async (context) => {
|
|
1033
|
+
const stages = new InteractionRouteStages("observe");
|
|
937
1034
|
const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
|
|
938
1035
|
context,
|
|
939
1036
|
"sessions:read",
|
|
940
1037
|
);
|
|
941
1038
|
const targetId = requireOpaqueParam(context, "targetId");
|
|
1039
|
+
stages.mark("auth");
|
|
942
1040
|
const result = await withActiveBrowserController(
|
|
943
1041
|
context,
|
|
944
1042
|
grant,
|
|
@@ -946,15 +1044,114 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
946
1044
|
browserSessionId,
|
|
947
1045
|
"session.read",
|
|
948
1046
|
"browser.read",
|
|
949
|
-
async ({ sessionClient }) =>
|
|
1047
|
+
async ({ sessionClient }) => {
|
|
1048
|
+
stages.mark("placement");
|
|
1049
|
+
try {
|
|
1050
|
+
return await sessionClient.observe(targetId);
|
|
1051
|
+
} finally {
|
|
1052
|
+
stages.mark("dispatch");
|
|
1053
|
+
}
|
|
1054
|
+
},
|
|
1055
|
+
true,
|
|
1056
|
+
stages,
|
|
950
1057
|
);
|
|
1058
|
+
reportInteractionRouteStages(context, deps.observability, stages);
|
|
951
1059
|
return context.json(result);
|
|
952
1060
|
},
|
|
953
1061
|
);
|
|
954
1062
|
|
|
1063
|
+
app.get(
|
|
1064
|
+
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/targets/:targetId/state",
|
|
1065
|
+
async (context) => {
|
|
1066
|
+
const stages = new InteractionRouteStages("target_state");
|
|
1067
|
+
const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
|
|
1068
|
+
context,
|
|
1069
|
+
"sessions:read",
|
|
1070
|
+
);
|
|
1071
|
+
const targetId = requireOpaqueParam(context, "targetId");
|
|
1072
|
+
stages.mark("auth");
|
|
1073
|
+
const result = await withActiveBrowserController(
|
|
1074
|
+
context,
|
|
1075
|
+
grant,
|
|
1076
|
+
workspaceId,
|
|
1077
|
+
browserSessionId,
|
|
1078
|
+
"session.read",
|
|
1079
|
+
"browser.read",
|
|
1080
|
+
async ({ sessionClient }) => {
|
|
1081
|
+
stages.mark("placement");
|
|
1082
|
+
try {
|
|
1083
|
+
return await sessionClient.targetState(targetId);
|
|
1084
|
+
} finally {
|
|
1085
|
+
stages.mark("dispatch");
|
|
1086
|
+
}
|
|
1087
|
+
},
|
|
1088
|
+
true,
|
|
1089
|
+
stages,
|
|
1090
|
+
);
|
|
1091
|
+
reportInteractionRouteStages(context, deps.observability, stages);
|
|
1092
|
+
return context.json(BrowserTargetState.parse(result));
|
|
1093
|
+
},
|
|
1094
|
+
);
|
|
1095
|
+
|
|
1096
|
+
app.post(
|
|
1097
|
+
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/targets/:targetId/dom-read",
|
|
1098
|
+
async (context) => {
|
|
1099
|
+
const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
|
|
1100
|
+
context,
|
|
1101
|
+
"sessions:read",
|
|
1102
|
+
);
|
|
1103
|
+
const targetId = requireOpaqueParam(context, "targetId");
|
|
1104
|
+
const request = await parseJsonBody(context, BrowserDomReadRequest);
|
|
1105
|
+
const result = await withActiveBrowserController(
|
|
1106
|
+
context,
|
|
1107
|
+
grant,
|
|
1108
|
+
workspaceId,
|
|
1109
|
+
browserSessionId,
|
|
1110
|
+
"session.read",
|
|
1111
|
+
"browser.read",
|
|
1112
|
+
async ({ sessionClient }) => await sessionClient.readDom(targetId, request),
|
|
1113
|
+
);
|
|
1114
|
+
return context.json(BrowserDomReadResponse.parse(result));
|
|
1115
|
+
},
|
|
1116
|
+
);
|
|
1117
|
+
|
|
1118
|
+
app.get(
|
|
1119
|
+
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/targets/:targetId/screenshot",
|
|
1120
|
+
async (context) => {
|
|
1121
|
+
const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
|
|
1122
|
+
context,
|
|
1123
|
+
"sessions:read",
|
|
1124
|
+
);
|
|
1125
|
+
const targetId = requireOpaqueParam(context, "targetId");
|
|
1126
|
+
const captureOptions = parseBrowserScreenshotOptions(new URL(context.req.url).searchParams);
|
|
1127
|
+
const frame = await withActiveBrowserController(
|
|
1128
|
+
context,
|
|
1129
|
+
grant,
|
|
1130
|
+
workspaceId,
|
|
1131
|
+
browserSessionId,
|
|
1132
|
+
"session.read",
|
|
1133
|
+
"browser.read",
|
|
1134
|
+
async ({ sessionClient, record }) => {
|
|
1135
|
+
if (!record.session.capabilities.screenshots) {
|
|
1136
|
+
throw new BrowserControlUnsupportedError(
|
|
1137
|
+
"This browser engine does not render page screenshots; use semantic observation",
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
try {
|
|
1141
|
+
return await sessionClient.capture(targetId, captureOptions);
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
throw browserScreenshotError(error);
|
|
1144
|
+
}
|
|
1145
|
+
},
|
|
1146
|
+
);
|
|
1147
|
+
return browserScreenshotResponse(frame);
|
|
1148
|
+
},
|
|
1149
|
+
);
|
|
1150
|
+
|
|
955
1151
|
app.post(
|
|
956
1152
|
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/actions",
|
|
957
1153
|
async (context) => {
|
|
1154
|
+
const stages = new InteractionRouteStages("act");
|
|
958
1155
|
const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
|
|
959
1156
|
context,
|
|
960
1157
|
"sessions:control",
|
|
@@ -964,7 +1161,12 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
964
1161
|
if (workspaceFileIds.length > 0) {
|
|
965
1162
|
await requireAccessGrant(context, deps, workspaceId, "files:read");
|
|
966
1163
|
}
|
|
1164
|
+
stages.mark("auth");
|
|
967
1165
|
const startedAtMs = performance.now();
|
|
1166
|
+
// Omitted fences mean the target's current state. Read them once from the
|
|
1167
|
+
// live controller in this request (a recovery retry of the callback must
|
|
1168
|
+
// replay the same command digest, so it reuses the first read).
|
|
1169
|
+
let resolvedFences: BrowserActionFences | null = null;
|
|
968
1170
|
const result = await withActiveBrowserController(
|
|
969
1171
|
context,
|
|
970
1172
|
grant,
|
|
@@ -973,19 +1175,38 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
973
1175
|
"session.control",
|
|
974
1176
|
"browser.action",
|
|
975
1177
|
async ({ sessionClient, binding, record, sourceAuthorization }) => {
|
|
976
|
-
|
|
1178
|
+
stages.mark("placement");
|
|
1179
|
+
const passkeySync = (refresh: boolean) =>
|
|
1180
|
+
syncBrowserPasskeys({
|
|
1181
|
+
port: interactionSecrets,
|
|
1182
|
+
grant,
|
|
1183
|
+
browserSessionId,
|
|
1184
|
+
controllerGeneration: binding.controllerGeneration,
|
|
1185
|
+
client: sessionClient,
|
|
1186
|
+
refresh,
|
|
1187
|
+
log: (event, fields) => console.warn(event, { browserSessionId, ...fields }),
|
|
1188
|
+
});
|
|
1189
|
+
// Before the action this browser takes over the principal's passkeys.
|
|
1190
|
+
// It is independent of the fence read, so both run together; the
|
|
1191
|
+
// action is dispatched only after the takeover settles.
|
|
1192
|
+
const passkeyTakeover = passkeySync(true);
|
|
1193
|
+
resolvedFences ??= await resolveBrowserActionFences(request, () =>
|
|
1194
|
+
sessionClient.targetState(request.targetId),
|
|
1195
|
+
);
|
|
1196
|
+
stages.mark("fences");
|
|
1197
|
+
const commandFields = {
|
|
977
1198
|
protocolVersion: BROWSER_CONTROL_PROTOCOL_VERSION,
|
|
978
1199
|
operationId: request.operationId,
|
|
979
1200
|
browserSessionId,
|
|
980
1201
|
controllerGeneration: binding.controllerGeneration,
|
|
981
1202
|
targetId: request.targetId,
|
|
982
|
-
|
|
983
|
-
expectedDocumentGeneration: request.expectedDocumentGeneration,
|
|
984
|
-
expectedFrameId: request.expectedFrameId,
|
|
1203
|
+
...resolvedFences,
|
|
985
1204
|
actor: interactionActorForGrant(grant),
|
|
986
1205
|
observationMode: request.observationMode,
|
|
987
1206
|
action: request.action,
|
|
988
|
-
}
|
|
1207
|
+
};
|
|
1208
|
+
// Validate the public command before resolving any reference.
|
|
1209
|
+
BrowserActionCommand.parse(commandFields);
|
|
989
1210
|
if (workspaceFileIds.length > 0) {
|
|
990
1211
|
let alreadyAdmitted = false;
|
|
991
1212
|
try {
|
|
@@ -1034,10 +1255,46 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1034
1255
|
});
|
|
1035
1256
|
}
|
|
1036
1257
|
}
|
|
1037
|
-
|
|
1258
|
+
await passkeyTakeover;
|
|
1259
|
+
stages.mark("passkey_pre");
|
|
1260
|
+
// Resolved last, right before dispatch, so TOTP codes are fresh (Q-33).
|
|
1261
|
+
const secretValues = await resolveInteractionSecretValues({
|
|
1262
|
+
port: interactionSecrets,
|
|
1263
|
+
grant,
|
|
1264
|
+
texts: browserActionSecretTexts(request.action),
|
|
1265
|
+
use: {
|
|
1266
|
+
surface: "browser",
|
|
1267
|
+
toolName: "browser_act",
|
|
1268
|
+
operationId: request.operationId,
|
|
1269
|
+
},
|
|
1270
|
+
beforeResolve: requirePlacementAcceptsSecretValues(
|
|
1271
|
+
deps.db,
|
|
1272
|
+
grant,
|
|
1273
|
+
workspaceId,
|
|
1274
|
+
record.session.placement,
|
|
1275
|
+
),
|
|
1276
|
+
});
|
|
1277
|
+
stages.mark("secrets");
|
|
1278
|
+
try {
|
|
1279
|
+
return await sessionClient.action(
|
|
1280
|
+
BrowserActionCommand.parse({
|
|
1281
|
+
...commandFields,
|
|
1282
|
+
...(secretValues === undefined ? {} : { secretValues }),
|
|
1283
|
+
}),
|
|
1284
|
+
);
|
|
1285
|
+
} finally {
|
|
1286
|
+
stages.mark("dispatch");
|
|
1287
|
+
// Write back what the action did (new registrations, sign counts)
|
|
1288
|
+
// without holding the response: unacknowledged events stay in
|
|
1289
|
+
// browserd, and the session's next sync waits for this one.
|
|
1290
|
+
void passkeySync(false);
|
|
1291
|
+
}
|
|
1038
1292
|
},
|
|
1293
|
+
true,
|
|
1294
|
+
stages,
|
|
1039
1295
|
);
|
|
1040
1296
|
observeBrowserActionResult(deps.observability, startedAtMs, request, result);
|
|
1297
|
+
reportInteractionRouteStages(context, deps.observability, stages);
|
|
1041
1298
|
return context.json(result);
|
|
1042
1299
|
},
|
|
1043
1300
|
);
|
|
@@ -1200,209 +1457,6 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1200
1457
|
},
|
|
1201
1458
|
);
|
|
1202
1459
|
|
|
1203
|
-
app.post(
|
|
1204
|
-
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/auth-runs/:authRunId/protected-fill",
|
|
1205
|
-
async (context) => {
|
|
1206
|
-
const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
|
|
1207
|
-
context,
|
|
1208
|
-
"sessions:control",
|
|
1209
|
-
);
|
|
1210
|
-
const authRunId = requireUuidParam(context, "authRunId");
|
|
1211
|
-
const request = await parseJsonBody(context, ProtectedAuthFillRequest);
|
|
1212
|
-
const startedAtMs = performance.now();
|
|
1213
|
-
try {
|
|
1214
|
-
const replay = await getProtectedAuthFillPreparation(deps.db, {
|
|
1215
|
-
accountId: grant.accountId,
|
|
1216
|
-
workspaceId,
|
|
1217
|
-
actorSubjectId: grant.subjectId,
|
|
1218
|
-
authRunId,
|
|
1219
|
-
...request,
|
|
1220
|
-
});
|
|
1221
|
-
if (replay?.response) {
|
|
1222
|
-
const record = await getBrowserSessionControlRecord(deps.db, {
|
|
1223
|
-
accountId: grant.accountId,
|
|
1224
|
-
workspaceId,
|
|
1225
|
-
browserSessionId,
|
|
1226
|
-
});
|
|
1227
|
-
await authorizeSourceSession(deps, grant, record.sourceSessionId, "session.control");
|
|
1228
|
-
assertAuthRunBrowser(replay.run.browserSessionId, browserSessionId);
|
|
1229
|
-
const response = ProtectedAuthFillResponse.parse(replay.response);
|
|
1230
|
-
observeAuthMutation(deps.observability, startedAtMs, response);
|
|
1231
|
-
return context.json(response);
|
|
1232
|
-
}
|
|
1233
|
-
} catch (error) {
|
|
1234
|
-
throw browserRouteError(error);
|
|
1235
|
-
}
|
|
1236
|
-
const result = await withActiveBrowserController(
|
|
1237
|
-
context,
|
|
1238
|
-
grant,
|
|
1239
|
-
workspaceId,
|
|
1240
|
-
browserSessionId,
|
|
1241
|
-
"session.control",
|
|
1242
|
-
"browser.control",
|
|
1243
|
-
async ({ sessionClient, binding, record }) => {
|
|
1244
|
-
const actor = interactionActorForGrant(grant);
|
|
1245
|
-
const scope = {
|
|
1246
|
-
accountId: grant.accountId,
|
|
1247
|
-
workspaceId,
|
|
1248
|
-
actorSubjectId: grant.subjectId,
|
|
1249
|
-
authRunId,
|
|
1250
|
-
};
|
|
1251
|
-
let preparation = await getProtectedAuthFillPreparation(deps.db, {
|
|
1252
|
-
...scope,
|
|
1253
|
-
...request,
|
|
1254
|
-
});
|
|
1255
|
-
let credential: Awaited<ReturnType<typeof loadConnectionCredentialForBroker>> = null;
|
|
1256
|
-
if (!preparation) {
|
|
1257
|
-
const run = await getAuthRun(deps.db, {
|
|
1258
|
-
accountId: grant.accountId,
|
|
1259
|
-
workspaceId,
|
|
1260
|
-
authRunId,
|
|
1261
|
-
});
|
|
1262
|
-
assertAuthRunBrowser(run.browserSessionId, browserSessionId);
|
|
1263
|
-
const connection = await getSiteAuthConnection(deps.db, {
|
|
1264
|
-
accountId: grant.accountId,
|
|
1265
|
-
workspaceId,
|
|
1266
|
-
siteAuthConnectionId: run.siteAuthConnectionId,
|
|
1267
|
-
});
|
|
1268
|
-
const authority = connection.authorities.find(
|
|
1269
|
-
(candidate) => candidate.id === request.authorityId,
|
|
1270
|
-
);
|
|
1271
|
-
if (!authority) {
|
|
1272
|
-
throw new InteractionResourceStateError("Auth authority is not configured");
|
|
1273
|
-
}
|
|
1274
|
-
if (authority.kind === "external_provider") {
|
|
1275
|
-
throw new InteractionResourceStateError(
|
|
1276
|
-
"External auth providers cannot use protected field fill",
|
|
1277
|
-
);
|
|
1278
|
-
}
|
|
1279
|
-
if (authority.kind === "connection_fields") {
|
|
1280
|
-
credential = await loadBoundBrowserCredential(deps, grant, workspaceId, authority);
|
|
1281
|
-
}
|
|
1282
|
-
preparation = await prepareProtectedAuthFill(deps.db, {
|
|
1283
|
-
...scope,
|
|
1284
|
-
...request,
|
|
1285
|
-
credentialVersion: credential?.version ?? null,
|
|
1286
|
-
});
|
|
1287
|
-
}
|
|
1288
|
-
assertAuthRunBrowser(preparation.run.browserSessionId, browserSessionId);
|
|
1289
|
-
if (preparation.run.controllerGeneration !== binding.controllerGeneration) {
|
|
1290
|
-
throw new InteractionResourceConflictError(
|
|
1291
|
-
"Auth run belongs to a stale browser controller",
|
|
1292
|
-
);
|
|
1293
|
-
}
|
|
1294
|
-
if (preparation.response) return preparation.response;
|
|
1295
|
-
if (
|
|
1296
|
-
preparation.operationState === "failed" ||
|
|
1297
|
-
preparation.operationState === "outcome_unknown"
|
|
1298
|
-
) {
|
|
1299
|
-
throw new InteractionResourceStateError(
|
|
1300
|
-
`Protected-fill operation is ${preparation.operationState.replace("_", " ")}`,
|
|
1301
|
-
);
|
|
1302
|
-
}
|
|
1303
|
-
if (preparation.authority.kind === "human") {
|
|
1304
|
-
if (preparation.operationState !== "prepared") {
|
|
1305
|
-
throw new InteractionResourceStateError(
|
|
1306
|
-
"Human protected-fill operation entered an invalid dispatch state",
|
|
1307
|
-
);
|
|
1308
|
-
}
|
|
1309
|
-
return await completeProtectedAuthFill(deps.db, {
|
|
1310
|
-
...scope,
|
|
1311
|
-
operationId: request.operationId,
|
|
1312
|
-
status: "needs_human",
|
|
1313
|
-
intervention: {
|
|
1314
|
-
originatingSessionId: record.sourceSessionId,
|
|
1315
|
-
originatingTurnId: actor.kind === "agent" ? (actor.turnId ?? null) : null,
|
|
1316
|
-
originatingAttemptId: actor.kind === "agent" ? (actor.attemptId ?? null) : null,
|
|
1317
|
-
originatingToolOperationId:
|
|
1318
|
-
actor.kind === "agent" && actor.attemptId ? request.operationId : null,
|
|
1319
|
-
kind: preparation.authority.fields.some((field) => field.purpose === "totp")
|
|
1320
|
-
? "mfa"
|
|
1321
|
-
: "manual_login",
|
|
1322
|
-
reason: "Sign in or complete authentication in this browser tab.",
|
|
1323
|
-
expiresInSeconds: 900,
|
|
1324
|
-
},
|
|
1325
|
-
});
|
|
1326
|
-
}
|
|
1327
|
-
if (preparation.authority.kind === "external_provider") {
|
|
1328
|
-
throw new InteractionResourceStateError(
|
|
1329
|
-
"External auth providers cannot use protected field fill",
|
|
1330
|
-
);
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
if (preparation.operationState === "dispatched") {
|
|
1334
|
-
try {
|
|
1335
|
-
const receipt = await sessionClient.protectedAuthReceipt(request.operationId);
|
|
1336
|
-
return await settleProtectedAuthReceipt({
|
|
1337
|
-
deps,
|
|
1338
|
-
scope,
|
|
1339
|
-
receipt,
|
|
1340
|
-
});
|
|
1341
|
-
} catch (error) {
|
|
1342
|
-
if (
|
|
1343
|
-
error instanceof BrowserControlRequestError &&
|
|
1344
|
-
error.error.code === "resource_not_found"
|
|
1345
|
-
) {
|
|
1346
|
-
await markProtectedAuthFillOutcomeUnknown(deps.db, {
|
|
1347
|
-
...scope,
|
|
1348
|
-
operationId: request.operationId,
|
|
1349
|
-
errorCode: "controller_receipt_missing",
|
|
1350
|
-
});
|
|
1351
|
-
throw new InteractionResourceStateError(
|
|
1352
|
-
"Protected-fill outcome is unknown after controller recovery",
|
|
1353
|
-
);
|
|
1354
|
-
}
|
|
1355
|
-
throw error;
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
credential ??= await loadBoundBrowserCredential(
|
|
1360
|
-
deps,
|
|
1361
|
-
grant,
|
|
1362
|
-
workspaceId,
|
|
1363
|
-
preparation.authority,
|
|
1364
|
-
);
|
|
1365
|
-
if (credential.version !== preparation.credentialVersion) {
|
|
1366
|
-
throw new InteractionResourceConflictError(
|
|
1367
|
-
"Credential changed after protected fill was prepared; use a new operation id",
|
|
1368
|
-
);
|
|
1369
|
-
}
|
|
1370
|
-
const fields = resolveProtectedAuthFieldValues({
|
|
1371
|
-
authority: preparation.authority,
|
|
1372
|
-
requestedFields: request.fields,
|
|
1373
|
-
credential: credential.credential,
|
|
1374
|
-
});
|
|
1375
|
-
await dispatchProtectedAuthFill(deps.db, {
|
|
1376
|
-
...scope,
|
|
1377
|
-
operationId: request.operationId,
|
|
1378
|
-
});
|
|
1379
|
-
const receipt = await sessionClient.protectedAuthFill(
|
|
1380
|
-
BrowserProtectedAuthFillCommand.parse({
|
|
1381
|
-
protocolVersion: BROWSER_CONTROL_PROTOCOL_VERSION,
|
|
1382
|
-
operationId: request.operationId,
|
|
1383
|
-
browserSessionId,
|
|
1384
|
-
controllerGeneration: binding.controllerGeneration,
|
|
1385
|
-
targetId: preparation.run.targetId,
|
|
1386
|
-
expectedTargetGeneration: request.expectedTargetGeneration,
|
|
1387
|
-
expectedDocumentGeneration: request.expectedDocumentGeneration,
|
|
1388
|
-
expectedFrameId: request.expectedFrameId,
|
|
1389
|
-
actor,
|
|
1390
|
-
authorityId: preparation.authority.id,
|
|
1391
|
-
credentialVersion: preparation.credentialVersion,
|
|
1392
|
-
allowedOrigins: preparation.origins,
|
|
1393
|
-
fields,
|
|
1394
|
-
submit: request.submit,
|
|
1395
|
-
}),
|
|
1396
|
-
);
|
|
1397
|
-
return await settleProtectedAuthReceipt({ deps, scope, receipt });
|
|
1398
|
-
},
|
|
1399
|
-
);
|
|
1400
|
-
const response = ProtectedAuthFillResponse.parse(result);
|
|
1401
|
-
observeAuthMutation(deps.observability, startedAtMs, response);
|
|
1402
|
-
return context.json(response);
|
|
1403
|
-
},
|
|
1404
|
-
);
|
|
1405
|
-
|
|
1406
1460
|
app.post(
|
|
1407
1461
|
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/auth-runs/:authRunId/external-auth",
|
|
1408
1462
|
async (context) => {
|
|
@@ -1749,7 +1803,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1749
1803
|
browserSessionId,
|
|
1750
1804
|
controllerGeneration: binding.controllerGeneration,
|
|
1751
1805
|
};
|
|
1752
|
-
await client.createViewGrant(reference, {
|
|
1806
|
+
const viewGrant = await client.createViewGrant(reference, {
|
|
1753
1807
|
grantId,
|
|
1754
1808
|
token,
|
|
1755
1809
|
expiresAt,
|
|
@@ -1841,6 +1895,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
1841
1895
|
controllerGeneration: binding.controllerGeneration,
|
|
1842
1896
|
targetId: request.targetId,
|
|
1843
1897
|
stream,
|
|
1898
|
+
...(viewGrant.fencedInputBatches === true ? { fencedInputBatches: true } : {}),
|
|
1899
|
+
...(viewGrant.focusedInputObservations === true
|
|
1900
|
+
? { focusedInputObservations: true }
|
|
1901
|
+
: {}),
|
|
1844
1902
|
expiresAt,
|
|
1845
1903
|
});
|
|
1846
1904
|
},
|
|
@@ -2047,6 +2105,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2047
2105
|
const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
|
|
2048
2106
|
const startedAtMs = performance.now();
|
|
2049
2107
|
const origin = requestOrigin(context, deps.settings);
|
|
2108
|
+
let preparedForDispatch = false;
|
|
2050
2109
|
try {
|
|
2051
2110
|
const before = await getBrowserSessionControlRecord(deps.db, {
|
|
2052
2111
|
accountId: grant.accountId,
|
|
@@ -2098,6 +2157,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2098
2157
|
return context.json(parsed, 200);
|
|
2099
2158
|
}
|
|
2100
2159
|
|
|
2160
|
+
preparedForDispatch = true;
|
|
2161
|
+
|
|
2101
2162
|
const record = await getBrowserSessionControlRecord(deps.db, {
|
|
2102
2163
|
accountId: grant.accountId,
|
|
2103
2164
|
workspaceId,
|
|
@@ -2259,10 +2320,19 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2259
2320
|
observeLifecycleResult(deps.observability, startedAtMs, parsed);
|
|
2260
2321
|
return context.json(parsed, 200);
|
|
2261
2322
|
} catch (error) {
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2323
|
+
if (preparedForDispatch) {
|
|
2324
|
+
await failPreparedBrowserSessionSuspend(deps.db, {
|
|
2325
|
+
accountId: grant.accountId,
|
|
2326
|
+
workspaceId,
|
|
2327
|
+
browserSessionId,
|
|
2328
|
+
operationId: request.operationId,
|
|
2329
|
+
error: interactionFailure(error),
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2332
|
+
throw browserRouteError(error);
|
|
2333
|
+
}
|
|
2334
|
+
},
|
|
2335
|
+
);
|
|
2266
2336
|
|
|
2267
2337
|
app.post(
|
|
2268
2338
|
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/resume",
|
|
@@ -2281,6 +2351,34 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2281
2351
|
browserSessionId,
|
|
2282
2352
|
});
|
|
2283
2353
|
await authorizeSourceSession(deps, grant, before.sourceSessionId, "session.control");
|
|
2354
|
+
if (
|
|
2355
|
+
before.session.lifecycle === "lost" &&
|
|
2356
|
+
before.session.failureCode === "controller_transition_expired"
|
|
2357
|
+
) {
|
|
2358
|
+
// A resume whose transition expired never activated. When its
|
|
2359
|
+
// controller still holds that generation's working copy, continue the
|
|
2360
|
+
// same browser from it; nothing is restored over it or discarded.
|
|
2361
|
+
const continued = await continueExpiredBrowserResume(
|
|
2362
|
+
context,
|
|
2363
|
+
grant,
|
|
2364
|
+
workspaceId,
|
|
2365
|
+
browserSessionId,
|
|
2366
|
+
request.operationId,
|
|
2367
|
+
);
|
|
2368
|
+
if (continued) {
|
|
2369
|
+
const parsed = BrowserSessionMutationResponse.parse(continued);
|
|
2370
|
+
observeLifecycleResult(deps.observability, startedAtMs, parsed);
|
|
2371
|
+
return context.json(parsed, 200);
|
|
2372
|
+
}
|
|
2373
|
+
// Otherwise end its retained controller generation, then resume
|
|
2374
|
+
// normally from the same durable state.
|
|
2375
|
+
await settleExpiredBrowserResume(context, grant, workspaceId, browserSessionId);
|
|
2376
|
+
before = await getBrowserSessionControlRecord(deps.db, {
|
|
2377
|
+
accountId: grant.accountId,
|
|
2378
|
+
workspaceId,
|
|
2379
|
+
browserSessionId,
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2284
2382
|
if (before.session.lifecycle === "suspended" && before.session.controller) {
|
|
2285
2383
|
await finishSuspendedBrowserControllerCleanup(
|
|
2286
2384
|
context,
|
|
@@ -2522,6 +2620,11 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2522
2620
|
const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
|
|
2523
2621
|
const startedAtMs = performance.now();
|
|
2524
2622
|
const origin = requestOrigin(context, deps.settings);
|
|
2623
|
+
let endPreparation: {
|
|
2624
|
+
restoreLifecycle: BrowserSessionValue["lifecycle"];
|
|
2625
|
+
restoreFailureCode: string | null;
|
|
2626
|
+
expectedControllerGeneration: string | null;
|
|
2627
|
+
} | null = null;
|
|
2525
2628
|
try {
|
|
2526
2629
|
const before = await getBrowserSessionControlRecord(deps.db, {
|
|
2527
2630
|
accountId: grant.accountId,
|
|
@@ -2536,6 +2639,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2536
2639
|
browserSessionId,
|
|
2537
2640
|
operationId: request.operationId,
|
|
2538
2641
|
actorSubjectId: grant.subjectId,
|
|
2642
|
+
expectedLifecycle: before.session.lifecycle,
|
|
2643
|
+
expectedFailureCode: before.session.failureCode,
|
|
2644
|
+
expectedControllerGeneration: before.session.controller?.controllerGeneration ?? null,
|
|
2539
2645
|
});
|
|
2540
2646
|
if (isTerminalOperation(prepared.operation.state)) {
|
|
2541
2647
|
if (prepared.operation.state === "completed") {
|
|
@@ -2546,11 +2652,19 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2546
2652
|
browserSessionId,
|
|
2547
2653
|
before.controllerHostSandboxGroupId,
|
|
2548
2654
|
).catch(() => undefined);
|
|
2655
|
+
await releaseBrowserPasskeys(interactionSecrets, browserSessionId);
|
|
2549
2656
|
}
|
|
2550
2657
|
const parsed = BrowserSessionMutationResponse.parse(prepared);
|
|
2551
2658
|
observeLifecycleResult(deps.observability, startedAtMs, parsed);
|
|
2552
2659
|
return context.json(parsed, 200);
|
|
2553
2660
|
}
|
|
2661
|
+
if (before.session.lifecycle !== "ending") {
|
|
2662
|
+
endPreparation = {
|
|
2663
|
+
restoreLifecycle: before.session.lifecycle,
|
|
2664
|
+
restoreFailureCode: before.session.failureCode,
|
|
2665
|
+
expectedControllerGeneration: before.session.controller?.controllerGeneration ?? null,
|
|
2666
|
+
};
|
|
2667
|
+
}
|
|
2554
2668
|
|
|
2555
2669
|
const record = await getBrowserSessionControlRecord(deps.db, {
|
|
2556
2670
|
accountId: grant.accountId,
|
|
@@ -2559,13 +2673,19 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2559
2673
|
operationId: request.operationId,
|
|
2560
2674
|
});
|
|
2561
2675
|
const binding = record.session.controller;
|
|
2562
|
-
|
|
2676
|
+
// A browser with no controller, or whose controller can never be
|
|
2677
|
+
// reached again, ends here: nothing physical remains to clean up, and
|
|
2678
|
+
// completing under the bound generation fences that controller off.
|
|
2679
|
+
if (
|
|
2680
|
+
!binding ||
|
|
2681
|
+
(await browserControllerGone(grant, workspaceId, record.session.placement))
|
|
2682
|
+
) {
|
|
2563
2683
|
const completed = await completeBrowserSessionEnd(deps.db, {
|
|
2564
2684
|
accountId: grant.accountId,
|
|
2565
2685
|
workspaceId,
|
|
2566
2686
|
operationId: request.operationId,
|
|
2567
2687
|
browserSessionId,
|
|
2568
|
-
expectedControllerGeneration: null,
|
|
2688
|
+
expectedControllerGeneration: binding?.controllerGeneration ?? null,
|
|
2569
2689
|
});
|
|
2570
2690
|
await releaseInteractionHolder(
|
|
2571
2691
|
deps,
|
|
@@ -2574,6 +2694,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2574
2694
|
browserSessionId,
|
|
2575
2695
|
record.controllerHostSandboxGroupId,
|
|
2576
2696
|
).catch(() => undefined);
|
|
2697
|
+
await releaseBrowserPasskeys(interactionSecrets, browserSessionId);
|
|
2577
2698
|
const parsed = BrowserSessionMutationResponse.parse(completed);
|
|
2578
2699
|
observeLifecycleResult(deps.observability, startedAtMs, parsed);
|
|
2579
2700
|
return context.json(parsed, 200);
|
|
@@ -2608,13 +2729,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2608
2729
|
controllerGeneration: binding.controllerGeneration,
|
|
2609
2730
|
},
|
|
2610
2731
|
async () =>
|
|
2611
|
-
await client
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
},
|
|
2616
|
-
{ removeState: true },
|
|
2617
|
-
),
|
|
2732
|
+
await endBrowserDiscardingLostRuntime(client, {
|
|
2733
|
+
browserSessionId,
|
|
2734
|
+
controllerGeneration: binding.controllerGeneration,
|
|
2735
|
+
}),
|
|
2618
2736
|
);
|
|
2619
2737
|
} catch (error) {
|
|
2620
2738
|
// Physical absence already satisfies an exact end. A stale live
|
|
@@ -2637,6 +2755,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2637
2755
|
browserSessionId,
|
|
2638
2756
|
placement.controllerHostSandboxGroupId,
|
|
2639
2757
|
).catch(() => undefined);
|
|
2758
|
+
await releaseBrowserPasskeys(interactionSecrets, browserSessionId);
|
|
2640
2759
|
return completed;
|
|
2641
2760
|
},
|
|
2642
2761
|
);
|
|
@@ -2644,6 +2763,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2644
2763
|
observeLifecycleResult(deps.observability, startedAtMs, parsed);
|
|
2645
2764
|
return context.json(parsed, 200);
|
|
2646
2765
|
} catch (error) {
|
|
2766
|
+
if (endPreparation) {
|
|
2767
|
+
await failPreparedBrowserSessionEnd(deps.db, {
|
|
2768
|
+
accountId: grant.accountId,
|
|
2769
|
+
workspaceId,
|
|
2770
|
+
browserSessionId,
|
|
2771
|
+
operationId: request.operationId,
|
|
2772
|
+
...endPreparation,
|
|
2773
|
+
error: interactionFailure(error),
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2647
2776
|
throw browserRouteError(error);
|
|
2648
2777
|
}
|
|
2649
2778
|
},
|
|
@@ -2657,26 +2786,27 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2657
2786
|
operation: ChannelAOperation,
|
|
2658
2787
|
waitSignal: AbortSignal,
|
|
2659
2788
|
callback: (placement: BrowserPlacement) => Promise<T>,
|
|
2789
|
+
allowOperationReplay = true,
|
|
2660
2790
|
): Promise<T> {
|
|
2661
2791
|
if (expectedPlacement?.kind === "attached_device") {
|
|
2662
2792
|
waitSignal.throwIfAborted();
|
|
2793
|
+
// Personal machines are controlled only for their owner, as everywhere else.
|
|
2794
|
+
const viewer = await attachedBrowserViewer(deps, grant, sourceSession.workspaceId);
|
|
2663
2795
|
const device = await getAttachedBrowserDevice(deps.db, {
|
|
2664
|
-
|
|
2665
|
-
workspaceId: sourceSession.workspaceId,
|
|
2796
|
+
...viewer,
|
|
2666
2797
|
deviceId: expectedPlacement.deviceId,
|
|
2667
2798
|
});
|
|
2668
2799
|
if (device.state !== "connected") {
|
|
2800
|
+
recordAttachedBrowserUnavailable(deps.observability, "disconnected");
|
|
2669
2801
|
throw new BrowserSessionStateError("Attached browser is disconnected");
|
|
2670
2802
|
}
|
|
2671
|
-
const enrollment = await getLiveEnrollmentConnection(
|
|
2672
|
-
deps.db,
|
|
2673
|
-
sourceSession.workspaceId,
|
|
2674
|
-
device.enrollmentId,
|
|
2675
|
-
);
|
|
2803
|
+
const enrollment = await getLiveEnrollmentConnection(deps.db, viewer, device.enrollmentId);
|
|
2676
2804
|
if (!enrollment || enrollment.status !== "active" || !enrollment.connectionInstanceId) {
|
|
2805
|
+
recordAttachedBrowserUnavailable(deps.observability, "machine_unavailable");
|
|
2677
2806
|
throw new BrowserSessionStateError("Attached browser machine is unavailable");
|
|
2678
2807
|
}
|
|
2679
2808
|
if (!enrollment.workspaceRoot) {
|
|
2809
|
+
recordAttachedBrowserUnavailable(deps.observability, "no_workspace_root");
|
|
2680
2810
|
throw new BrowserSessionStateError(
|
|
2681
2811
|
"Attached browser machine has not reported an absolute workspace root",
|
|
2682
2812
|
);
|
|
@@ -2746,9 +2876,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2746
2876
|
workspaceId: sourceSession.workspaceId,
|
|
2747
2877
|
session: sourceSession,
|
|
2748
2878
|
subjectId: grant.subjectId,
|
|
2879
|
+
grant,
|
|
2749
2880
|
waitSignal,
|
|
2750
2881
|
operation,
|
|
2751
2882
|
retryControllerTransport: operation === "browser.read" || operation === "browser.action",
|
|
2883
|
+
allowOperationReplay,
|
|
2884
|
+
...(expectedPlacementInstanceId &&
|
|
2885
|
+
(expectedPlacement?.kind === "sandbox_group" ||
|
|
2886
|
+
expectedPlacement?.kind === "external_provider")
|
|
2887
|
+
? { retainedInstanceId: expectedPlacementInstanceId }
|
|
2888
|
+
: {}),
|
|
2752
2889
|
},
|
|
2753
2890
|
async (handle) => {
|
|
2754
2891
|
if (expectedPlacement?.kind === "sandbox_group") {
|
|
@@ -2792,6 +2929,11 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
2792
2929
|
resolved.kind !== "selfhosted" ||
|
|
2793
2930
|
resolved.sandboxId !== expectedPlacement.sandboxId
|
|
2794
2931
|
) {
|
|
2932
|
+
if (operation === "browser.create") {
|
|
2933
|
+
throw new HTTPException(422, {
|
|
2934
|
+
message: wrongInteractionPlacementMessage(expectedPlacement.sandboxId),
|
|
2935
|
+
});
|
|
2936
|
+
}
|
|
2795
2937
|
return await throwBrowserSourcePlacementChanged(
|
|
2796
2938
|
grant,
|
|
2797
2939
|
sourceSession.id,
|
|
@@ -3024,6 +3166,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3024
3166
|
sourceAuthorization: ResolvedSessionAuthorization | null;
|
|
3025
3167
|
}) => Promise<T>,
|
|
3026
3168
|
recoverMissing = true,
|
|
3169
|
+
stages?: InteractionRouteStages,
|
|
3027
3170
|
): Promise<T> {
|
|
3028
3171
|
try {
|
|
3029
3172
|
const record = await getBrowserSessionControlRecord(deps.db, {
|
|
@@ -3031,12 +3174,14 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3031
3174
|
workspaceId,
|
|
3032
3175
|
browserSessionId,
|
|
3033
3176
|
});
|
|
3177
|
+
stages?.mark("record");
|
|
3034
3178
|
const sourceAuthorization = await authorizeSourceSession(
|
|
3035
3179
|
deps,
|
|
3036
3180
|
grant,
|
|
3037
3181
|
record.sourceSessionId,
|
|
3038
3182
|
authorizationOperation,
|
|
3039
3183
|
);
|
|
3184
|
+
stages?.mark("authorize");
|
|
3040
3185
|
let sourceSession: Session | null = null;
|
|
3041
3186
|
if (record.session.placement?.kind === "connected_machine") {
|
|
3042
3187
|
sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
|
|
@@ -3048,6 +3193,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3048
3193
|
);
|
|
3049
3194
|
}
|
|
3050
3195
|
}
|
|
3196
|
+
stages?.mark("source_session");
|
|
3051
3197
|
if (record.session.lifecycle !== "active" || !record.session.controller) {
|
|
3052
3198
|
throw new BrowserSessionStateError("BrowserSession is not active");
|
|
3053
3199
|
}
|
|
@@ -3061,10 +3207,19 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3061
3207
|
if (!admitted) {
|
|
3062
3208
|
throw new BrowserSessionStateError("BrowserSession controller authority changed");
|
|
3063
3209
|
}
|
|
3064
|
-
|
|
3210
|
+
stages?.mark("control");
|
|
3211
|
+
const safelyReplayable =
|
|
3212
|
+
browserSessionStorageMode(record.session) !== "ephemeral_context" &&
|
|
3213
|
+
(channelOperation === "browser.read" || channelOperation === "browser.action");
|
|
3214
|
+
let controllerTransportFailed = false;
|
|
3215
|
+
const controllerRecoveryState = { attempted: false };
|
|
3216
|
+
const run = async (
|
|
3217
|
+
placement: BrowserPlacement,
|
|
3218
|
+
provisionedClient?: BrowserControlClient,
|
|
3219
|
+
): Promise<T> => {
|
|
3065
3220
|
// An active binding already proves browserd was provisioned. Reusing
|
|
3066
3221
|
// it avoids restarting/checking the sidecar on every live input.
|
|
3067
|
-
const client = connectController(deps, grant, record, placement);
|
|
3222
|
+
const client = provisionedClient ?? connectController(deps, grant, record, placement);
|
|
3068
3223
|
const tokens = deriveBrowserSessionControllerTokens({
|
|
3069
3224
|
rootSecret: browserAuthorityRoot(deps),
|
|
3070
3225
|
accountId: grant.accountId,
|
|
@@ -3093,6 +3248,30 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3093
3248
|
try {
|
|
3094
3249
|
result = await callback(controller);
|
|
3095
3250
|
} catch (error) {
|
|
3251
|
+
if (
|
|
3252
|
+
browserSessionStorageMode(record.session) === "ephemeral_context" &&
|
|
3253
|
+
(isMissingBrowserControllerSession(error) ||
|
|
3254
|
+
(error instanceof BrowserControlRequestError &&
|
|
3255
|
+
error.error.code === "controller_lost"))
|
|
3256
|
+
) {
|
|
3257
|
+
const retired = await markEphemeralBrowserSessionLost(deps.db, {
|
|
3258
|
+
accountId: grant.accountId,
|
|
3259
|
+
workspaceId,
|
|
3260
|
+
browserSessionId,
|
|
3261
|
+
controllerGeneration: binding.controllerGeneration,
|
|
3262
|
+
});
|
|
3263
|
+
if (retired)
|
|
3264
|
+
await releaseInteractionHolder(
|
|
3265
|
+
deps,
|
|
3266
|
+
grant,
|
|
3267
|
+
workspaceId,
|
|
3268
|
+
browserSessionId,
|
|
3269
|
+
record.controllerHostSandboxGroupId,
|
|
3270
|
+
);
|
|
3271
|
+
throw new BrowserSessionStateError(
|
|
3272
|
+
"Ephemeral browser context was lost; create a new BrowserSession. No identity or operation was replayed.",
|
|
3273
|
+
);
|
|
3274
|
+
}
|
|
3096
3275
|
if (!recoverMissing || !isMissingBrowserControllerSession(error)) throw error;
|
|
3097
3276
|
await recoverActiveBrowserController(
|
|
3098
3277
|
deps,
|
|
@@ -3102,25 +3281,22 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3102
3281
|
placement,
|
|
3103
3282
|
client,
|
|
3104
3283
|
tokens,
|
|
3284
|
+
authorizationOperation,
|
|
3105
3285
|
);
|
|
3106
3286
|
result = await callback(controller);
|
|
3107
3287
|
}
|
|
3108
3288
|
return result;
|
|
3109
3289
|
};
|
|
3110
3290
|
const cached = await cachedBrowserPlacement(record);
|
|
3291
|
+
stages?.mark("placement_cache");
|
|
3111
3292
|
if (cached) {
|
|
3112
3293
|
try {
|
|
3113
3294
|
return await run(cached);
|
|
3114
3295
|
} catch (error) {
|
|
3115
|
-
|
|
3116
|
-
channelOperation === "browser.read" || channelOperation === "browser.action";
|
|
3117
|
-
if (
|
|
3118
|
-
!safelyReplayable ||
|
|
3119
|
-
(!(error instanceof BrowserControlTransportError) &&
|
|
3120
|
-
!(error instanceof BrowserControlRequestError && error.retryable))
|
|
3121
|
-
) {
|
|
3296
|
+
if (!safelyReplayable || !isRetryableControllerTransport(error)) {
|
|
3122
3297
|
throw error;
|
|
3123
3298
|
}
|
|
3299
|
+
controllerTransportFailed = true;
|
|
3124
3300
|
await recordLeaseControllerDataPlaneUrl(deps.db, {
|
|
3125
3301
|
accountId: grant.accountId,
|
|
3126
3302
|
workspaceId,
|
|
@@ -3140,12 +3316,54 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3140
3316
|
binding.placementInstanceId,
|
|
3141
3317
|
channelOperation,
|
|
3142
3318
|
context.req.raw.signal,
|
|
3143
|
-
async (placement) =>
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3319
|
+
async (placement) => {
|
|
3320
|
+
stages?.mark("placement_resolve");
|
|
3321
|
+
const use = async () => {
|
|
3322
|
+
const cachedPlacement = await cacheBrowserControllerPlacement(
|
|
3323
|
+
grant,
|
|
3324
|
+
workspaceId,
|
|
3325
|
+
placement,
|
|
3326
|
+
).catch(() => placement);
|
|
3327
|
+
stages?.mark("placement_cache_write");
|
|
3328
|
+
return await run(cachedPlacement);
|
|
3329
|
+
};
|
|
3330
|
+
if (browserSessionStorageMode(record.session) === "ephemeral_context") return await use();
|
|
3331
|
+
return await withControllerTransportRecovery({
|
|
3332
|
+
channelOperation,
|
|
3333
|
+
placementKind: placement.placement.kind,
|
|
3334
|
+
recoveryState: controllerRecoveryState,
|
|
3335
|
+
transportAlreadyFailed: controllerTransportFailed,
|
|
3336
|
+
use,
|
|
3337
|
+
admitRecovery: async () => {
|
|
3338
|
+
if (
|
|
3339
|
+
!(await touchBrowserSessionController(deps.db, {
|
|
3340
|
+
accountId: grant.accountId,
|
|
3341
|
+
workspaceId,
|
|
3342
|
+
browserSessionId,
|
|
3343
|
+
controllerGeneration: binding.controllerGeneration,
|
|
3344
|
+
}))
|
|
3345
|
+
) {
|
|
3346
|
+
throw new BrowserSessionStateError("BrowserSession controller authority changed");
|
|
3347
|
+
}
|
|
3348
|
+
},
|
|
3349
|
+
recover: async () => {
|
|
3350
|
+
// Provision and restore through the original exec-capable session;
|
|
3351
|
+
// the cached tunnel alone cannot start browserd or its display.
|
|
3352
|
+
const client = await provisionController(
|
|
3353
|
+
deps,
|
|
3354
|
+
grant,
|
|
3355
|
+
record,
|
|
3356
|
+
placement,
|
|
3357
|
+
requestOrigin(context, deps.settings),
|
|
3358
|
+
);
|
|
3359
|
+
await cacheBrowserControllerPlacement(grant, workspaceId, placement).catch(
|
|
3360
|
+
() => placement,
|
|
3361
|
+
);
|
|
3362
|
+
return await run(placement, client);
|
|
3363
|
+
},
|
|
3364
|
+
});
|
|
3365
|
+
},
|
|
3366
|
+
browserSessionStorageMode(record.session) !== "ephemeral_context",
|
|
3149
3367
|
);
|
|
3150
3368
|
} catch (error) {
|
|
3151
3369
|
throw browserRouteError(error);
|
|
@@ -3171,6 +3389,23 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3171
3389
|
);
|
|
3172
3390
|
}
|
|
3173
3391
|
|
|
3392
|
+
/** See `interactionControllerGone`: a removed machine or a disconnected
|
|
3393
|
+
* Chrome device; a managed browser on a machine that is only offline waits. */
|
|
3394
|
+
async function browserControllerGone(
|
|
3395
|
+
grant: AccessGrant,
|
|
3396
|
+
workspaceId: string,
|
|
3397
|
+
placement: InteractionPlacement | null,
|
|
3398
|
+
): Promise<boolean> {
|
|
3399
|
+
if (placement?.kind !== "connected_machine" && placement?.kind !== "attached_device") {
|
|
3400
|
+
return false;
|
|
3401
|
+
}
|
|
3402
|
+
return await interactionControllerGone(
|
|
3403
|
+
deps.db,
|
|
3404
|
+
await attachedBrowserViewer(deps, grant, workspaceId),
|
|
3405
|
+
placement,
|
|
3406
|
+
);
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3174
3409
|
async function recoverActiveBrowserController(
|
|
3175
3410
|
routeDeps: ApiRouteDeps,
|
|
3176
3411
|
grant: AccessGrant,
|
|
@@ -3179,7 +3414,66 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3179
3414
|
placement: BrowserPlacement,
|
|
3180
3415
|
client: BrowserControlClient,
|
|
3181
3416
|
tokens: ReturnType<typeof deriveBrowserSessionControllerTokens>,
|
|
3417
|
+
authorizationOperation: SessionAuthorizationOperation,
|
|
3182
3418
|
): Promise<void> {
|
|
3419
|
+
if (browserSessionStorageMode(record.session) === "ephemeral_context") {
|
|
3420
|
+
throw new BrowserSessionStateError(
|
|
3421
|
+
"Ephemeral browser context was lost; create a new BrowserSession. Its identity and operations cannot be restored or replayed.",
|
|
3422
|
+
);
|
|
3423
|
+
}
|
|
3424
|
+
const transport = browserRuntimeTransport(record.session, placement.transport);
|
|
3425
|
+
const recoverWorkingDirectory =
|
|
3426
|
+
transport.kind === "managed" &&
|
|
3427
|
+
transport.engine === "chromium" &&
|
|
3428
|
+
!transport.ephemeralPartition &&
|
|
3429
|
+
browserSessionStorageMode(record.session) === "private_profile";
|
|
3430
|
+
if (recoverWorkingDirectory) {
|
|
3431
|
+
// Missing controller memory cannot admit a stale source, token, placement
|
|
3432
|
+
// or route snapshot. Reuse the existing durable authority before recovery.
|
|
3433
|
+
await authorizeSourceSession(
|
|
3434
|
+
routeDeps,
|
|
3435
|
+
grant,
|
|
3436
|
+
record.sourceSessionId,
|
|
3437
|
+
authorizationOperation,
|
|
3438
|
+
);
|
|
3439
|
+
const current = await getBrowserSessionControlRecord(routeDeps.db, {
|
|
3440
|
+
accountId: grant.accountId,
|
|
3441
|
+
workspaceId: record.session.workspaceId,
|
|
3442
|
+
browserSessionId: record.session.id,
|
|
3443
|
+
});
|
|
3444
|
+
if (
|
|
3445
|
+
current.sourceSessionId !== record.sourceSessionId ||
|
|
3446
|
+
current.session.lifecycle !== "active" ||
|
|
3447
|
+
current.session.controller?.controllerGeneration !== binding.controllerGeneration ||
|
|
3448
|
+
current.session.controller.placementInstanceId !== placement.placementInstanceId ||
|
|
3449
|
+
current.session.controller.controllerId !== binding.controllerId ||
|
|
3450
|
+
current.tokenGeneration !== record.tokenGeneration ||
|
|
3451
|
+
!sameInteractionPlacement(current.session.placement, record.session.placement) ||
|
|
3452
|
+
current.session.driverId !== record.session.driverId ||
|
|
3453
|
+
current.session.engine !== record.session.engine ||
|
|
3454
|
+
current.session.headless !== record.session.headless ||
|
|
3455
|
+
current.session.linkedComputerSessionId !== record.session.linkedComputerSessionId ||
|
|
3456
|
+
current.session.networkRouteId !== record.session.networkRouteId ||
|
|
3457
|
+
JSON.stringify(current.networkRouteAuthority) !==
|
|
3458
|
+
JSON.stringify(record.networkRouteAuthority)
|
|
3459
|
+
) {
|
|
3460
|
+
throw new BrowserSessionStateError("BrowserSession controller authority changed");
|
|
3461
|
+
}
|
|
3462
|
+
if (record.session.placement.kind === "connected_machine") {
|
|
3463
|
+
const source = await requireSourceSession(
|
|
3464
|
+
routeDeps,
|
|
3465
|
+
record.session.workspaceId,
|
|
3466
|
+
record.sourceSessionId,
|
|
3467
|
+
);
|
|
3468
|
+
if (source.activeSandboxId !== record.session.placement.sandboxId) {
|
|
3469
|
+
return await throwBrowserSourcePlacementChanged(
|
|
3470
|
+
grant,
|
|
3471
|
+
record.sourceSessionId,
|
|
3472
|
+
record.session.placement.sandboxId,
|
|
3473
|
+
);
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3183
3477
|
const linkedComputer = await ensureLinkedComputerController(
|
|
3184
3478
|
routeDeps,
|
|
3185
3479
|
grant,
|
|
@@ -3199,13 +3493,25 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3199
3493
|
record.session.headless,
|
|
3200
3494
|
linkedComputer !== null,
|
|
3201
3495
|
);
|
|
3496
|
+
if (
|
|
3497
|
+
recoverWorkingDirectory &&
|
|
3498
|
+
!(await touchBrowserSessionController(routeDeps.db, {
|
|
3499
|
+
accountId: grant.accountId,
|
|
3500
|
+
workspaceId: record.session.workspaceId,
|
|
3501
|
+
browserSessionId: record.session.id,
|
|
3502
|
+
controllerGeneration: binding.controllerGeneration,
|
|
3503
|
+
}))
|
|
3504
|
+
) {
|
|
3505
|
+
throw new BrowserSessionStateError("BrowserSession controller authority changed");
|
|
3506
|
+
}
|
|
3202
3507
|
await client.createSession({
|
|
3203
3508
|
browserSessionId: record.session.id,
|
|
3204
3509
|
controllerGeneration: binding.controllerGeneration,
|
|
3205
3510
|
tokenGeneration: record.tokenGeneration,
|
|
3206
3511
|
...tokens,
|
|
3207
3512
|
headed: !record.session.headless,
|
|
3208
|
-
transport
|
|
3513
|
+
transport,
|
|
3514
|
+
...(recoverWorkingDirectory ? { recoverExistingWorkingDirectory: true } : {}),
|
|
3209
3515
|
...(linkedComputer ? { linkedComputer } : {}),
|
|
3210
3516
|
...(networkRoute ? { networkRoute } : {}),
|
|
3211
3517
|
});
|
|
@@ -3219,6 +3525,245 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
|
|
|
3219
3525
|
}
|
|
3220
3526
|
}
|
|
3221
3527
|
|
|
3528
|
+
/** Continue an expired resume on its retained controller generation when the
|
|
3529
|
+
* controller still holds that generation's own working copy (a resident
|
|
3530
|
+
* browser, or its preserved profile and launch receipt after the controller
|
|
3531
|
+
* restarted). The controller's existing same-generation recovery adopts or
|
|
3532
|
+
* relaunches it from that directory, so the record, its history and the
|
|
3533
|
+
* possibly newer working profile all survive. Returns null when the browser
|
|
3534
|
+
* cannot be continued this way, leaving the record unchanged for the
|
|
3535
|
+
* settle-and-restore path. */
|
|
3536
|
+
async function continueExpiredBrowserResume(
|
|
3537
|
+
context: Context,
|
|
3538
|
+
grant: AccessGrant,
|
|
3539
|
+
workspaceId: string,
|
|
3540
|
+
browserSessionId: string,
|
|
3541
|
+
operationId: string,
|
|
3542
|
+
): Promise<BrowserSessionMutationResponse | null> {
|
|
3543
|
+
const expired = await getExpiredBrowserSessionResume(deps.db, {
|
|
3544
|
+
accountId: grant.accountId,
|
|
3545
|
+
workspaceId,
|
|
3546
|
+
browserSessionId,
|
|
3547
|
+
});
|
|
3548
|
+
if (!expired?.controllerGeneration) return null;
|
|
3549
|
+
const record = await getBrowserSessionControlRecord(deps.db, {
|
|
3550
|
+
accountId: grant.accountId,
|
|
3551
|
+
workspaceId,
|
|
3552
|
+
browserSessionId,
|
|
3553
|
+
});
|
|
3554
|
+
const binding = record.session.controller;
|
|
3555
|
+
if (binding?.controllerGeneration !== expired.controllerGeneration) {
|
|
3556
|
+
throw new BrowserSessionStateError("BrowserSession controller authority changed");
|
|
3557
|
+
}
|
|
3558
|
+
// Only a private profile has a working copy the controller can adopt.
|
|
3559
|
+
if (browserSessionStorageMode(record.session) !== "private_profile") return null;
|
|
3560
|
+
const rootSecret = browserAuthorityRoot(deps);
|
|
3561
|
+
const sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
|
|
3562
|
+
if (
|
|
3563
|
+
record.session.placement.kind === "connected_machine" &&
|
|
3564
|
+
sourceSession.activeSandboxId !== record.session.placement.sandboxId
|
|
3565
|
+
) {
|
|
3566
|
+
return null;
|
|
3567
|
+
}
|
|
3568
|
+
return await withBrowserPlacement(
|
|
3569
|
+
sourceSession,
|
|
3570
|
+
grant,
|
|
3571
|
+
record.session.placement,
|
|
3572
|
+
binding.placementInstanceId,
|
|
3573
|
+
"browser.resume",
|
|
3574
|
+
context.req.raw.signal,
|
|
3575
|
+
async (placement) => {
|
|
3576
|
+
const transport = browserRuntimeTransport(record.session, placement.transport);
|
|
3577
|
+
if (
|
|
3578
|
+
transport.kind !== "managed" ||
|
|
3579
|
+
transport.engine !== "chromium" ||
|
|
3580
|
+
transport.ephemeralPartition
|
|
3581
|
+
) {
|
|
3582
|
+
return null;
|
|
3583
|
+
}
|
|
3584
|
+
const client = await provisionController(
|
|
3585
|
+
deps,
|
|
3586
|
+
grant,
|
|
3587
|
+
record,
|
|
3588
|
+
placement,
|
|
3589
|
+
requestOrigin(context, deps.settings),
|
|
3590
|
+
);
|
|
3591
|
+
let linkedComputer: Awaited<ReturnType<typeof ensureLinkedComputerController>>;
|
|
3592
|
+
try {
|
|
3593
|
+
linkedComputer = await ensureLinkedComputerController(
|
|
3594
|
+
deps,
|
|
3595
|
+
grant,
|
|
3596
|
+
record.session,
|
|
3597
|
+
placement,
|
|
3598
|
+
client,
|
|
3599
|
+
);
|
|
3600
|
+
} catch (error) {
|
|
3601
|
+
// Its working copy was launched on a desktop that is gone.
|
|
3602
|
+
if (error instanceof BrowserSessionStateError) return null;
|
|
3603
|
+
throw error;
|
|
3604
|
+
}
|
|
3605
|
+
const networkRoute = await resolveActiveBrowserNetworkRouteLaunch({
|
|
3606
|
+
deps,
|
|
3607
|
+
grant,
|
|
3608
|
+
record,
|
|
3609
|
+
placement: placement.placement,
|
|
3610
|
+
});
|
|
3611
|
+
const tokens = deriveBrowserSessionControllerTokens({
|
|
3612
|
+
rootSecret,
|
|
3613
|
+
accountId: grant.accountId,
|
|
3614
|
+
workspaceId,
|
|
3615
|
+
browserSessionId,
|
|
3616
|
+
placement: placement.placement,
|
|
3617
|
+
placementInstanceId: placement.placementInstanceId,
|
|
3618
|
+
controllerGeneration: binding.controllerGeneration,
|
|
3619
|
+
tokenGeneration: record.tokenGeneration,
|
|
3620
|
+
});
|
|
3621
|
+
const interactionHeld = await ensureInteractionHolder(
|
|
3622
|
+
deps,
|
|
3623
|
+
grant,
|
|
3624
|
+
sourceSession,
|
|
3625
|
+
browserSessionId,
|
|
3626
|
+
placement,
|
|
3627
|
+
context.req.raw.signal,
|
|
3628
|
+
);
|
|
3629
|
+
const release = async () => {
|
|
3630
|
+
if (!interactionHeld) return;
|
|
3631
|
+
await releaseInteractionHolder(
|
|
3632
|
+
deps,
|
|
3633
|
+
grant,
|
|
3634
|
+
workspaceId,
|
|
3635
|
+
browserSessionId,
|
|
3636
|
+
placement.controllerHostSandboxGroupId,
|
|
3637
|
+
).catch(() => undefined);
|
|
3638
|
+
};
|
|
3639
|
+
try {
|
|
3640
|
+
await withInteractionHolderHeartbeat(
|
|
3641
|
+
deps,
|
|
3642
|
+
{
|
|
3643
|
+
grant,
|
|
3644
|
+
workspaceId,
|
|
3645
|
+
sandboxGroupId: placement.controllerHostSandboxGroupId,
|
|
3646
|
+
holderId: interactionHolderId(browserSessionId),
|
|
3647
|
+
operationId,
|
|
3648
|
+
// This operation is recorded only after the controller adopts
|
|
3649
|
+
// the working copy; until then the expired resume holds authority.
|
|
3650
|
+
continuesExpiredBrowserResume: true,
|
|
3651
|
+
resourceId: browserSessionId,
|
|
3652
|
+
controllerGeneration: binding.controllerGeneration,
|
|
3653
|
+
},
|
|
3654
|
+
async () => {
|
|
3655
|
+
await ensureHeadedBrowserDisplayStack(
|
|
3656
|
+
placement.session,
|
|
3657
|
+
record.session.headless,
|
|
3658
|
+
linkedComputer !== null,
|
|
3659
|
+
);
|
|
3660
|
+
await client.createSession({
|
|
3661
|
+
browserSessionId,
|
|
3662
|
+
controllerGeneration: binding.controllerGeneration,
|
|
3663
|
+
tokenGeneration: record.tokenGeneration,
|
|
3664
|
+
...tokens,
|
|
3665
|
+
headed: !record.session.headless,
|
|
3666
|
+
transport,
|
|
3667
|
+
recoverExistingWorkingDirectory: true,
|
|
3668
|
+
...(linkedComputer ? { linkedComputer } : {}),
|
|
3669
|
+
...(networkRoute ? { networkRoute } : {}),
|
|
3670
|
+
});
|
|
3671
|
+
},
|
|
3672
|
+
);
|
|
3673
|
+
} catch (error) {
|
|
3674
|
+
// The controller proves no adoptable working copy for this generation
|
|
3675
|
+
// (none was completed, or its authority or profile changed): leave
|
|
3676
|
+
// the record for the settle-and-restore path.
|
|
3677
|
+
if (
|
|
3678
|
+
isDefiniteBrowserControllerFailure(error) &&
|
|
3679
|
+
error instanceof BrowserControlRequestError &&
|
|
3680
|
+
error.error.code === "resource_unavailable"
|
|
3681
|
+
) {
|
|
3682
|
+
await release();
|
|
3683
|
+
return null;
|
|
3684
|
+
}
|
|
3685
|
+
throw error;
|
|
3686
|
+
}
|
|
3687
|
+
return await continueExpiredBrowserSessionResume(deps.db, {
|
|
3688
|
+
accountId: grant.accountId,
|
|
3689
|
+
workspaceId,
|
|
3690
|
+
browserSessionId,
|
|
3691
|
+
operationId,
|
|
3692
|
+
actorSubjectId: grant.subjectId,
|
|
3693
|
+
controller: binding,
|
|
3694
|
+
});
|
|
3695
|
+
},
|
|
3696
|
+
);
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
/** End the retained controller generation of an expired resume and return
|
|
3700
|
+
* the browser to suspended. Other lost browsers are left unchanged so the
|
|
3701
|
+
* normal resume preparation refuses them. */
|
|
3702
|
+
async function settleExpiredBrowserResume(
|
|
3703
|
+
context: Context,
|
|
3704
|
+
grant: AccessGrant,
|
|
3705
|
+
workspaceId: string,
|
|
3706
|
+
browserSessionId: string,
|
|
3707
|
+
): Promise<void> {
|
|
3708
|
+
const expired = await getExpiredBrowserSessionResume(deps.db, {
|
|
3709
|
+
accountId: grant.accountId,
|
|
3710
|
+
workspaceId,
|
|
3711
|
+
browserSessionId,
|
|
3712
|
+
});
|
|
3713
|
+
if (!expired) return;
|
|
3714
|
+
const record = await getBrowserSessionControlRecord(deps.db, {
|
|
3715
|
+
accountId: grant.accountId,
|
|
3716
|
+
workspaceId,
|
|
3717
|
+
browserSessionId,
|
|
3718
|
+
});
|
|
3719
|
+
const binding = record.session.controller;
|
|
3720
|
+
if ((binding?.controllerGeneration ?? null) !== expired.controllerGeneration) {
|
|
3721
|
+
throw new BrowserSessionStateError("BrowserSession controller authority changed");
|
|
3722
|
+
}
|
|
3723
|
+
if (binding) {
|
|
3724
|
+
browserAuthorityRoot(deps);
|
|
3725
|
+
const sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
|
|
3726
|
+
await withBrowserPlacement(
|
|
3727
|
+
sourceSession,
|
|
3728
|
+
grant,
|
|
3729
|
+
record.session.placement,
|
|
3730
|
+
binding.placementInstanceId,
|
|
3731
|
+
"browser.resume",
|
|
3732
|
+
context.req.raw.signal,
|
|
3733
|
+
async (placement) => {
|
|
3734
|
+
const client = await provisionController(
|
|
3735
|
+
deps,
|
|
3736
|
+
grant,
|
|
3737
|
+
record,
|
|
3738
|
+
placement,
|
|
3739
|
+
requestOrigin(context, deps.settings),
|
|
3740
|
+
);
|
|
3741
|
+
// A resident or provably never-completed launch is ended exactly; an
|
|
3742
|
+
// interrupted launch whose profile no browser runs is set aside
|
|
3743
|
+
// intact; a launch that completed (adoptable) or whose profile may
|
|
3744
|
+
// be in use stays preserved and refuses this resume.
|
|
3745
|
+
await endExpiredResumeSettingAsideInterruptedLaunch(client, {
|
|
3746
|
+
browserSessionId,
|
|
3747
|
+
controllerGeneration: binding.controllerGeneration,
|
|
3748
|
+
});
|
|
3749
|
+
},
|
|
3750
|
+
);
|
|
3751
|
+
}
|
|
3752
|
+
await settleExpiredBrowserSessionResume(deps.db, {
|
|
3753
|
+
accountId: grant.accountId,
|
|
3754
|
+
workspaceId,
|
|
3755
|
+
browserSessionId,
|
|
3756
|
+
expectedControllerGeneration: expired.controllerGeneration,
|
|
3757
|
+
});
|
|
3758
|
+
await releaseInteractionHolder(
|
|
3759
|
+
deps,
|
|
3760
|
+
grant,
|
|
3761
|
+
workspaceId,
|
|
3762
|
+
browserSessionId,
|
|
3763
|
+
record.controllerHostSandboxGroupId,
|
|
3764
|
+
).catch(() => undefined);
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3222
3767
|
async function finishSuspendedBrowserControllerCleanup(
|
|
3223
3768
|
context: Context,
|
|
3224
3769
|
grant: AccessGrant,
|
|
@@ -3286,18 +3831,27 @@ function sourcePlacementChangedApiError(interactionResource: "browser_session"):
|
|
|
3286
3831
|
});
|
|
3287
3832
|
}
|
|
3288
3833
|
|
|
3289
|
-
function browserCreateInput(
|
|
3834
|
+
export function browserCreateInput(
|
|
3290
3835
|
grant: AccessGrant,
|
|
3291
3836
|
workspaceId: string,
|
|
3292
3837
|
request: CreateBrowserSessionRequestValue,
|
|
3293
3838
|
placement: InteractionPlacement,
|
|
3294
3839
|
) {
|
|
3840
|
+
if (request.storageMode === "ephemeral_context" && placement.kind !== "sandbox_group") {
|
|
3841
|
+
throw new BrowserControlUnsupportedError(
|
|
3842
|
+
"Ephemeral contexts require a managed sandbox placement",
|
|
3843
|
+
);
|
|
3844
|
+
}
|
|
3295
3845
|
const attached = placement.kind === "attached_device";
|
|
3296
3846
|
const external = placement.kind === "external_provider";
|
|
3297
3847
|
const engine = attached ? ("chrome" as const) : external ? ("external" as const) : request.engine;
|
|
3298
|
-
if (
|
|
3848
|
+
if (
|
|
3849
|
+
request.engine === "lightpanda" &&
|
|
3850
|
+
placement.kind !== "sandbox_group" &&
|
|
3851
|
+
placement.kind !== "connected_machine"
|
|
3852
|
+
) {
|
|
3299
3853
|
throw new BrowserControlUnsupportedError(
|
|
3300
|
-
"Lightpanda
|
|
3854
|
+
"Lightpanda requires a managed sandbox or Connected Machine browser placement",
|
|
3301
3855
|
);
|
|
3302
3856
|
}
|
|
3303
3857
|
return {
|
|
@@ -3310,11 +3864,13 @@ function browserCreateInput(
|
|
|
3310
3864
|
initialUrl: request.initialUrl ?? null,
|
|
3311
3865
|
placement,
|
|
3312
3866
|
driverId:
|
|
3313
|
-
|
|
3314
|
-
?
|
|
3315
|
-
:
|
|
3316
|
-
?
|
|
3317
|
-
:
|
|
3867
|
+
request.storageMode === "ephemeral_context"
|
|
3868
|
+
? EPHEMERAL_CHROMIUM_DRIVER_ID
|
|
3869
|
+
: engine === "lightpanda"
|
|
3870
|
+
? LIGHTPANDA_DRIVER_ID
|
|
3871
|
+
: external
|
|
3872
|
+
? EXTERNAL_BROWSER_DRIVER_ID
|
|
3873
|
+
: BROWSER_DRIVER_ID,
|
|
3318
3874
|
engine,
|
|
3319
3875
|
headless: attached ? false : request.headless,
|
|
3320
3876
|
identityId: request.identityId ?? null,
|
|
@@ -3339,7 +3895,21 @@ function browserCreateInput(
|
|
|
3339
3895
|
function browserRuntimeTransport(
|
|
3340
3896
|
session: BrowserSessionValue,
|
|
3341
3897
|
transport: PlacementBrowserTransport,
|
|
3898
|
+
placementInstanceId?: string,
|
|
3899
|
+
rootSecret?: string,
|
|
3342
3900
|
): PlacementBrowserTransport {
|
|
3901
|
+
if (browserSessionStorageMode(session) === "ephemeral_context") {
|
|
3902
|
+
if (transport.kind !== "managed" || !placementInstanceId || !rootSecret) {
|
|
3903
|
+
throw new BrowserControlUnsupportedError(
|
|
3904
|
+
"Ephemeral contexts cannot recover or change their placement",
|
|
3905
|
+
);
|
|
3906
|
+
}
|
|
3907
|
+
return {
|
|
3908
|
+
kind: "managed",
|
|
3909
|
+
engine: "chromium",
|
|
3910
|
+
ephemeralPartition: ephemeralBrowserPartition(session, placementInstanceId, rootSecret),
|
|
3911
|
+
};
|
|
3912
|
+
}
|
|
3343
3913
|
if (transport.kind !== "managed") return transport;
|
|
3344
3914
|
return {
|
|
3345
3915
|
kind: "managed",
|
|
@@ -3401,6 +3971,11 @@ function assertCreateReplay(
|
|
|
3401
3971
|
"BrowserSession create operation is bound to another placement",
|
|
3402
3972
|
);
|
|
3403
3973
|
}
|
|
3974
|
+
if (request.storageMode !== browserSessionStorageMode(session)) {
|
|
3975
|
+
throw new BrowserSessionOperationConflictError(
|
|
3976
|
+
"BrowserSession create operation is bound to another storage mode",
|
|
3977
|
+
);
|
|
3978
|
+
}
|
|
3404
3979
|
const expectedEngine =
|
|
3405
3980
|
session.placement.kind === "attached_device"
|
|
3406
3981
|
? "chrome"
|
|
@@ -3739,7 +4314,9 @@ async function ensureInteractionHolder(
|
|
|
3739
4314
|
if (!placement.lease?.instanceId) {
|
|
3740
4315
|
throw new BrowserSessionStateError("BrowserSession lease placement is unavailable");
|
|
3741
4316
|
}
|
|
3742
|
-
const sandboxRuntime = await resolveSessionSandboxRuntime(deps.db, deps.settings, sourceSession
|
|
4317
|
+
const sandboxRuntime = await resolveSessionSandboxRuntime(deps.db, deps.settings, sourceSession, {
|
|
4318
|
+
grant,
|
|
4319
|
+
});
|
|
3743
4320
|
const acquired = await acquireLease(deps.db, {
|
|
3744
4321
|
accountId: grant.accountId,
|
|
3745
4322
|
workspaceId: sourceSession.workspaceId,
|
|
@@ -3748,8 +4325,13 @@ async function ensureInteractionHolder(
|
|
|
3748
4325
|
holderId: interactionHolderId(browserSessionId),
|
|
3749
4326
|
subjectId: sourceSession.id,
|
|
3750
4327
|
backend: placement.lease.backend,
|
|
4328
|
+
warmBilling: {
|
|
4329
|
+
mode: deps.settings.sandboxWarmBillingMode,
|
|
4330
|
+
rateMicrosPerSecond: sandboxWarmRateMicrosPerSecond(deps.settings, placement.lease.backend),
|
|
4331
|
+
},
|
|
3751
4332
|
os: placement.lease.os,
|
|
3752
4333
|
image: sandboxRuntime.image,
|
|
4334
|
+
imagePolicy: "new_creates_only",
|
|
3753
4335
|
rigVersionId: sourceSession.rigVersionId,
|
|
3754
4336
|
leaseTtlMs: deps.settings.sandboxLeaseTtlMs,
|
|
3755
4337
|
expectedEpoch: placement.lease.leaseEpoch,
|
|
@@ -4023,6 +4605,7 @@ async function settleBrowserSessionSuspensionCaptureFailure(
|
|
|
4023
4605
|
return await failBrowserSessionSuspension(deps.db, {
|
|
4024
4606
|
...input,
|
|
4025
4607
|
...(outcomeUnknown ? { state: "outcome_unknown" as const } : {}),
|
|
4608
|
+
missingControllerSession: error.status === 404 && isMissingBrowserControllerSession(error),
|
|
4026
4609
|
error: error.error,
|
|
4027
4610
|
});
|
|
4028
4611
|
}
|
|
@@ -4160,6 +4743,86 @@ function optionalBoundedInteger(
|
|
|
4160
4743
|
return parsed;
|
|
4161
4744
|
}
|
|
4162
4745
|
|
|
4746
|
+
export function parseBrowserScreenshotOptions(
|
|
4747
|
+
query: URLSearchParams,
|
|
4748
|
+
): BrowserControlScreenshotOptions {
|
|
4749
|
+
for (const key of ["fullPage", "format", "quality"]) {
|
|
4750
|
+
if (query.getAll(key).length > 1) {
|
|
4751
|
+
throw new HTTPException(400, { message: `duplicate browser screenshot ${key}` });
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
const fullPage = query.get("fullPage");
|
|
4755
|
+
if (fullPage !== null && fullPage !== "true" && fullPage !== "false") {
|
|
4756
|
+
throw new HTTPException(400, { message: "invalid browser screenshot fullPage" });
|
|
4757
|
+
}
|
|
4758
|
+
const format = query.get("format");
|
|
4759
|
+
if (format !== null && format !== "jpeg" && format !== "png") {
|
|
4760
|
+
throw new HTTPException(400, { message: "invalid browser screenshot format" });
|
|
4761
|
+
}
|
|
4762
|
+
const quality = query.get("quality");
|
|
4763
|
+
const parsedQuality = quality === null ? null : optionalBoundedInteger(quality, 1, 100);
|
|
4764
|
+
if (quality !== null && parsedQuality === null) {
|
|
4765
|
+
throw new HTTPException(400, { message: "invalid browser screenshot quality" });
|
|
4766
|
+
}
|
|
4767
|
+
return {
|
|
4768
|
+
...(fullPage === null ? {} : { fullPage: fullPage === "true" }),
|
|
4769
|
+
...(format === null ? {} : { format }),
|
|
4770
|
+
...(parsedQuality === null ? {} : { quality: parsedQuality }),
|
|
4771
|
+
};
|
|
4772
|
+
}
|
|
4773
|
+
|
|
4774
|
+
/** Preserve the controller's definite tab diagnosis (a tab that stopped
|
|
4775
|
+
* answering, or one whose renderer crashed) through the public API with its
|
|
4776
|
+
* own code and guidance. Any other controller failure keeps the ordinary
|
|
4777
|
+
* envelope, which never publishes driver diagnostics. This only handles typed
|
|
4778
|
+
* read/pre-dispatch failures. Dispatched action uncertainty stays in the
|
|
4779
|
+
* action receipt, never relabeled as a safe retry. */
|
|
4780
|
+
export function browserTabDiagnosisError(error: unknown): unknown {
|
|
4781
|
+
if (!(error instanceof BrowserControlRequestError)) return error;
|
|
4782
|
+
const code = error.error.code;
|
|
4783
|
+
if (code !== "tab_not_responding" && code !== "tab_crashed") return error;
|
|
4784
|
+
const failure = new ApiHttpError(409, {
|
|
4785
|
+
code,
|
|
4786
|
+
message: error.error.message,
|
|
4787
|
+
retryable: false,
|
|
4788
|
+
outcomeUnknown: false,
|
|
4789
|
+
});
|
|
4790
|
+
failure.cause = error;
|
|
4791
|
+
return failure;
|
|
4792
|
+
}
|
|
4793
|
+
|
|
4794
|
+
/** Keep a definite read timeout useful without publishing driver diagnostics. */
|
|
4795
|
+
export function browserScreenshotError(error: unknown): unknown {
|
|
4796
|
+
if (error instanceof BrowserControlRequestError && error.error.code === "timeout") {
|
|
4797
|
+
const failure = new ApiHttpError(504, {
|
|
4798
|
+
code: "upstream_unavailable",
|
|
4799
|
+
message:
|
|
4800
|
+
"This tab did not produce a screenshot in time. Other browser operations may still work.",
|
|
4801
|
+
retryable: error.retryable,
|
|
4802
|
+
outcomeUnknown: false,
|
|
4803
|
+
});
|
|
4804
|
+
failure.cause = error;
|
|
4805
|
+
return failure;
|
|
4806
|
+
}
|
|
4807
|
+
return error;
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4810
|
+
export function browserScreenshotResponse(
|
|
4811
|
+
frame: Pick<BrowserControlFrame, "data" | "mediaType" | "metadataHeader">,
|
|
4812
|
+
): Response {
|
|
4813
|
+
// Node Buffers can be views into a larger backing allocation. Copy exactly
|
|
4814
|
+
// the image bytes before constructing the Response.
|
|
4815
|
+
return new Response(Uint8Array.from(frame.data).buffer, {
|
|
4816
|
+
status: 200,
|
|
4817
|
+
headers: {
|
|
4818
|
+
"cache-control": "no-store",
|
|
4819
|
+
"content-type": frame.mediaType,
|
|
4820
|
+
"x-opengeni-browser-frame": frame.metadataHeader,
|
|
4821
|
+
...USER_CONTENT_SECURITY_HEADERS,
|
|
4822
|
+
},
|
|
4823
|
+
});
|
|
4824
|
+
}
|
|
4825
|
+
|
|
4163
4826
|
function isUuid(value: unknown): value is string {
|
|
4164
4827
|
return (
|
|
4165
4828
|
typeof value === "string" &&
|
|
@@ -4214,13 +4877,9 @@ function isTerminalOperation(state: string): boolean {
|
|
|
4214
4877
|
return state === "completed" || state === "failed" || state === "outcome_unknown";
|
|
4215
4878
|
}
|
|
4216
4879
|
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
workspaceId: string,
|
|
4221
|
-
authority: Extract<SiteAuthAuthority, { kind: "connection_fields" }>,
|
|
4222
|
-
) {
|
|
4223
|
-
return await loadExactInteractionCredential(deps, grant, workspaceId, authority.credential);
|
|
4880
|
+
/** A configured browser credential (proxy or provider authority) is unusable. */
|
|
4881
|
+
class BrowserAuthCredentialError extends Error {
|
|
4882
|
+
override readonly name = "BrowserAuthCredentialError";
|
|
4224
4883
|
}
|
|
4225
4884
|
|
|
4226
4885
|
async function loadExactInteractionCredential(
|
|
@@ -4478,61 +5137,6 @@ function browserProxyUrl(
|
|
|
4478
5137
|
return url.toString();
|
|
4479
5138
|
}
|
|
4480
5139
|
|
|
4481
|
-
async function settleProtectedAuthReceipt(input: {
|
|
4482
|
-
deps: ApiRouteDeps;
|
|
4483
|
-
scope: {
|
|
4484
|
-
accountId: string;
|
|
4485
|
-
workspaceId: string;
|
|
4486
|
-
actorSubjectId: string;
|
|
4487
|
-
authRunId: string;
|
|
4488
|
-
};
|
|
4489
|
-
receipt: BrowserProtectedAuthFillReceiptValue;
|
|
4490
|
-
}) {
|
|
4491
|
-
const { deps, scope, receipt } = input;
|
|
4492
|
-
if (receipt.state === "completed") {
|
|
4493
|
-
if (!receipt.observation) {
|
|
4494
|
-
throw new BrowserControlProtocolError("protected-fill receipt omitted its observation");
|
|
4495
|
-
}
|
|
4496
|
-
return await completeProtectedAuthFill(deps.db, {
|
|
4497
|
-
...scope,
|
|
4498
|
-
operationId: receipt.operationId,
|
|
4499
|
-
status: receipt.observation.status,
|
|
4500
|
-
targetGeneration: receipt.observation.target.targetGeneration,
|
|
4501
|
-
documentGeneration: receipt.observation.target.documentGeneration,
|
|
4502
|
-
});
|
|
4503
|
-
}
|
|
4504
|
-
if (receipt.state === "failed") {
|
|
4505
|
-
if (!receipt.error) {
|
|
4506
|
-
throw new BrowserControlProtocolError("protected-fill failure omitted its error");
|
|
4507
|
-
}
|
|
4508
|
-
const staleCodes = new Set([
|
|
4509
|
-
"controller_stale",
|
|
4510
|
-
"target_not_found",
|
|
4511
|
-
"target_stale",
|
|
4512
|
-
"observation_stale",
|
|
4513
|
-
"document_stale",
|
|
4514
|
-
"frame_stale",
|
|
4515
|
-
"locator_not_found",
|
|
4516
|
-
"locator_ambiguous",
|
|
4517
|
-
]);
|
|
4518
|
-
return await completeProtectedAuthFill(deps.db, {
|
|
4519
|
-
...scope,
|
|
4520
|
-
operationId: receipt.operationId,
|
|
4521
|
-
status: staleCodes.has(receipt.error.code) ? "stale" : "failed",
|
|
4522
|
-
failureCode: receipt.error.code,
|
|
4523
|
-
});
|
|
4524
|
-
}
|
|
4525
|
-
if (receipt.state === "outcome_unknown") {
|
|
4526
|
-
await markProtectedAuthFillOutcomeUnknown(deps.db, {
|
|
4527
|
-
...scope,
|
|
4528
|
-
operationId: receipt.operationId,
|
|
4529
|
-
errorCode: receipt.error?.code ?? "outcome_unknown",
|
|
4530
|
-
});
|
|
4531
|
-
throw new InteractionResourceStateError("Protected-fill outcome is unknown");
|
|
4532
|
-
}
|
|
4533
|
-
throw new InteractionResourceStateError(`Protected-fill operation is still ${receipt.state}`);
|
|
4534
|
-
}
|
|
4535
|
-
|
|
4536
5140
|
function assertAuthRunBrowser(actual: string, expected: string): void {
|
|
4537
5141
|
if (actual !== expected) {
|
|
4538
5142
|
throw new InteractionResourceConflictError("Auth run belongs to another browser session");
|
|
@@ -4712,10 +5316,30 @@ async function recordBrowserDownloadFileUsage(
|
|
|
4712
5316
|
});
|
|
4713
5317
|
}
|
|
4714
5318
|
|
|
5319
|
+
/** A user's own attached browser could not be reached for an operation. */
|
|
5320
|
+
function recordAttachedBrowserUnavailable(
|
|
5321
|
+
observability: ApiRouteDeps["observability"],
|
|
5322
|
+
reason: "disconnected" | "machine_unavailable" | "no_workspace_root",
|
|
5323
|
+
): void {
|
|
5324
|
+
try {
|
|
5325
|
+
observability?.incrementCounter({
|
|
5326
|
+
name: "opengeni_attached_browser_unavailable_total",
|
|
5327
|
+
help: "Browser operations refused because the user's attached browser (or its machine) was unreachable, by closed reason.",
|
|
5328
|
+
labels: { reason },
|
|
5329
|
+
});
|
|
5330
|
+
} catch {
|
|
5331
|
+
// Telemetry never changes the refusal.
|
|
5332
|
+
}
|
|
5333
|
+
}
|
|
5334
|
+
|
|
4715
5335
|
function browserRouteError(error: unknown): HTTPException {
|
|
5336
|
+
const tabDiagnosis = browserTabDiagnosisError(error);
|
|
5337
|
+
if (tabDiagnosis instanceof ApiHttpError) return tabDiagnosis;
|
|
4716
5338
|
const connectedMachineError = interactionControlApiError(error, "browser");
|
|
4717
5339
|
if (connectedMachineError) return connectedMachineError;
|
|
4718
5340
|
if (error instanceof HTTPException) return error;
|
|
5341
|
+
if (error instanceof SandboxPaidComputeAdmissionError)
|
|
5342
|
+
return new HTTPException(402, { message: error.message, cause: error });
|
|
4719
5343
|
if (error instanceof BrowserSessionNotFoundError) {
|
|
4720
5344
|
return new HTTPException(404, { message: error.message, cause: error });
|
|
4721
5345
|
}
|