machine-bridge-mcp 2.0.0 → 3.0.0-beta.102
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/CHANGELOG.md +1142 -0
- package/CONTRIBUTING.md +23 -25
- package/GOVERNANCE.md +2 -2
- package/README.md +58 -20
- package/SECURITY.md +161 -120
- package/SUPPORT.md +1 -1
- package/browser-extension/broker-auth.js +83 -0
- package/browser-extension/broker-liveness.js +35 -0
- package/browser-extension/browser-error-boundary.js +96 -0
- package/browser-extension/browser-operations.js +1456 -101
- package/browser-extension/devtools-input.js +195 -34
- package/browser-extension/devtools-observation.js +429 -0
- package/browser-extension/devtools-session.js +48 -0
- package/browser-extension/manifest.json +4 -3
- package/browser-extension/page-automation.js +578 -118
- package/browser-extension/pairing-bootstrap.js +42 -0
- package/browser-extension/pairing.js +28 -12
- package/browser-extension/service-worker.js +126 -106
- package/docs/AGENT_CONTEXT.md +13 -10
- package/docs/ARCHITECTURE.md +159 -67
- package/docs/AUDIT.md +1302 -4
- package/docs/CLIENTS.md +19 -6
- package/docs/COMPUTER_USE.md +297 -0
- package/docs/ENGINEERING.md +72 -13
- package/docs/GETTING_STARTED.md +18 -20
- package/docs/LOCAL_AUTHORIZATION.md +115 -72
- package/docs/LOCAL_AUTOMATION.md +27 -21
- package/docs/LOGGING.md +40 -19
- package/docs/MANAGED_JOBS.md +37 -34
- package/docs/MULTI_ACCOUNT.md +98 -37
- package/docs/OPERATIONS.md +145 -51
- package/docs/OVERVIEW.md +14 -11
- package/docs/POLICY_REFERENCE.md +71 -3
- package/docs/PRIVACY.md +41 -2
- package/docs/PROJECT_STANDARDS.md +29 -6
- package/docs/RELEASING.md +238 -64
- package/docs/TESTING.md +150 -39
- package/docs/THREAT_MODEL.md +164 -67
- package/docs/TOOL_REFERENCE.md +653 -60
- package/docs/UPGRADING.md +78 -18
- package/native/macos/MachineBridgeBackgroundInput.swift +1051 -0
- package/native/macos/MachineBridgeBackgroundInputSmokeFixture.swift +295 -0
- package/native/macos/MachineBridgeTrustBroker.swift +233 -0
- package/package.json +94 -16
- package/scripts/accepted-candidate-tarball.mjs +77 -0
- package/scripts/candidate-runtime-store.mjs +93 -0
- package/scripts/check-plan.mjs +73 -3
- package/scripts/check-runner.mjs +129 -17
- package/scripts/commit-message-check.mjs +5 -2
- package/scripts/consumer-package-security.mjs +293 -0
- package/scripts/coverage-check.mjs +251 -15
- package/scripts/coverage-generation.mjs +41 -0
- package/scripts/coverage-range-merge.mjs +41 -0
- package/scripts/foreground-daemon-recovery.mjs +88 -0
- package/scripts/generate-policy-reference.mjs +28 -5
- package/scripts/generate-tool-reference.mjs +1 -1
- package/scripts/generate-worker-types.mjs +21 -11
- package/scripts/github-backlog.mjs +11 -10
- package/scripts/github-push.mjs +62 -16
- package/scripts/github-release-asset.mjs +58 -0
- package/scripts/github-release.mjs +241 -132
- package/scripts/global-package-installation.mjs +68 -0
- package/scripts/hardened-npm-session.mjs +42 -0
- package/scripts/install-published-prerelease.mjs +198 -0
- package/scripts/local-release-acceptance.mjs +103 -45
- package/scripts/macos-background-input-smoke.mjs +515 -0
- package/scripts/network-retry.mjs +8 -1
- package/scripts/npm-global-prefix.mjs +27 -0
- package/scripts/npm-publication-policy.mjs +17 -0
- package/scripts/official-mcp-conformance.mjs +267 -0
- package/scripts/persistent-activation-process.mjs +101 -0
- package/scripts/prepare-pinned-npm.mjs +12 -69
- package/scripts/prerelease-activation.mjs +160 -0
- package/scripts/privacy-check.mjs +14 -6
- package/scripts/promotion-digest.mjs +128 -0
- package/scripts/publish-npm.mjs +260 -0
- package/scripts/published-release.mjs +132 -0
- package/scripts/release-acceptance.mjs +62 -54
- package/scripts/release-browser-extension-store.mjs +172 -0
- package/scripts/release-candidate-manifest.mjs +52 -0
- package/scripts/release-channel.mjs +106 -0
- package/scripts/release-diagnostic.mjs +41 -0
- package/scripts/release-impact-check.mjs +12 -22
- package/scripts/release-oauth-canary-core.mjs +325 -0
- package/scripts/release-oauth-canary-evidence.mjs +100 -0
- package/scripts/release-oauth-canary.mjs +127 -0
- package/scripts/release-publication-guard.mjs +67 -0
- package/scripts/release-soak.mjs +284 -0
- package/scripts/release-state.mjs +1 -1
- package/scripts/run-checks.mjs +44 -7
- package/scripts/run-worker-dry-run.mjs +21 -0
- package/scripts/sarif-security-gate.mjs +2 -1
- package/scripts/sbom-check.mjs +106 -0
- package/scripts/start-release-candidate.mjs +267 -44
- package/scripts/syntax-check.mjs +36 -14
- package/scripts/verification-generation-guard.mjs +15 -0
- package/scripts/verification-idle-sleep-guard.mjs +37 -0
- package/scripts/verification-state.mjs +108 -0
- package/scripts/wrangler-command-lifecycle.mjs +115 -0
- package/src/local/account-access.mjs +13 -1
- package/src/local/account-admin-response.mjs +92 -0
- package/src/local/account-admin.mjs +52 -19
- package/src/local/agent-context-projection.mjs +26 -7
- package/src/local/agent-context.mjs +88 -49
- package/src/local/agent-skill-discovery.mjs +16 -5
- package/src/local/app-automation-macos-jxa.mjs +350 -0
- package/src/local/app-automation.mjs +873 -163
- package/src/local/application-capability-projection.mjs +24 -0
- package/src/local/authority-context.mjs +106 -0
- package/src/local/autostart-log-maintenance.mjs +36 -0
- package/src/local/browser-bridge-http.mjs +5 -3
- package/src/local/browser-bridge.mjs +109 -31
- package/src/local/browser-broker-auth-http.mjs +67 -0
- package/src/local/browser-broker-auth.mjs +98 -0
- package/src/local/browser-broker-routes.mjs +63 -20
- package/src/local/browser-broker-server.mjs +17 -7
- package/src/local/browser-command.mjs +79 -30
- package/src/local/browser-computer-observation-service.mjs +266 -0
- package/src/local/browser-extension-identity.mjs +50 -0
- package/src/local/browser-extension-path.mjs +22 -0
- package/src/local/browser-extension-protocol.mjs +50 -11
- package/src/local/browser-operation-service.mjs +147 -117
- package/src/local/browser-pairing-grant.mjs +109 -0
- package/src/local/browser-pairing-http.mjs +36 -0
- package/src/local/browser-pairing-launch.mjs +58 -0
- package/src/local/browser-pairing-store.mjs +85 -48
- package/src/local/browser-request-registry.mjs +40 -14
- package/src/local/browser-request-settlement.mjs +78 -0
- package/src/local/browser-resource-input.mjs +74 -0
- package/src/local/browser-trusted-input-health.mjs +63 -0
- package/src/local/call-authority.mjs +21 -0
- package/src/local/call-capacity.mjs +45 -0
- package/src/local/call-registry-drain.mjs +54 -0
- package/src/local/call-registry.mjs +30 -15
- package/src/local/capability-observer.mjs +5 -0
- package/src/local/capability-ranking.mjs +3 -2
- package/src/local/child-process-settlement.mjs +132 -0
- package/src/local/cli-account-admin.mjs +40 -6
- package/src/local/cli-activate.mjs +162 -0
- package/src/local/cli-local-admin.mjs +20 -45
- package/src/local/cli-options.mjs +12 -13
- package/src/local/cli-ready-output.mjs +52 -0
- package/src/local/cli-service.mjs +91 -11
- package/src/local/cli.mjs +232 -150
- package/src/local/computer-use-application-observation.mjs +224 -0
- package/src/local/computer-use-arguments.mjs +270 -0
- package/src/local/computer-use-deadline.mjs +50 -0
- package/src/local/computer-use-dispatch-settlement.mjs +40 -0
- package/src/local/computer-use-expectation.mjs +171 -0
- package/src/local/computer-use-observation.mjs +769 -0
- package/src/local/computer-use-recovery.mjs +82 -0
- package/src/local/computer-use-result-budget.mjs +145 -0
- package/src/local/computer-use-snapshot-store.mjs +67 -0
- package/src/local/computer-use.mjs +2255 -0
- package/src/local/daemon-http-relay-auth.mjs +26 -0
- package/src/local/daemon-http-relay-connection.mjs +298 -0
- package/src/local/daemon-http-relay-request.mjs +64 -0
- package/src/local/daemon-http-relay-sequence.mjs +49 -0
- package/src/local/daemon-process.mjs +40 -7
- package/src/local/delegated-process-sandbox.mjs +148 -0
- package/src/local/device-identity.mjs +156 -49
- package/src/local/device-root-provider.mjs +81 -0
- package/src/local/directory-metadata.mjs +47 -0
- package/src/local/doctor-reporting.mjs +23 -0
- package/src/local/durable-process-spec.mjs +80 -0
- package/src/local/errors.mjs +25 -15
- package/src/local/exclusive-file.mjs +74 -26
- package/src/local/execution-limits.mjs +9 -0
- package/src/local/execution-routing.mjs +276 -0
- package/src/local/execution-surface.mjs +23 -0
- package/src/local/file-mutation-coordinator.mjs +47 -0
- package/src/local/file-snapshot-preservation.mjs +24 -0
- package/src/local/filesystem-identity.mjs +54 -0
- package/src/local/fixed-process-environment.mjs +21 -0
- package/src/local/full-access-test.mjs +31 -13
- package/src/local/git-commit.mjs +69 -0
- package/src/local/git-config-safety.mjs +55 -0
- package/src/local/git-log-parser.mjs +41 -0
- package/src/local/git-metadata-boundary.mjs +77 -0
- package/src/local/git-metadata-tree-safety.mjs +59 -0
- package/src/local/git-operation-state.mjs +19 -0
- package/src/local/git-service.mjs +165 -30
- package/src/local/hardened-npm-download-timeout.mjs +46 -0
- package/src/local/hardened-npm-download.mjs +109 -0
- package/src/local/hardened-npm-extract.mjs +20 -0
- package/src/local/hardened-npm-verification.mjs +104 -0
- package/src/local/hardened-npm.mjs +199 -0
- package/src/local/job-runner.mjs +244 -89
- package/src/local/lifecycle.mjs +6 -0
- package/src/local/log.mjs +19 -45
- package/src/local/macos-background-input.mjs +398 -0
- package/src/local/macos-idle-sleep-assertion.mjs +65 -0
- package/src/local/macos-trust-broker.mjs +404 -0
- package/src/local/managed-job-cancellation.mjs +42 -0
- package/src/local/managed-job-capacity.mjs +16 -0
- package/src/local/managed-job-directory-generation.mjs +60 -0
- package/src/local/managed-job-directory.mjs +75 -0
- package/src/local/managed-job-durable-process.mjs +49 -0
- package/src/local/managed-job-lock.mjs +46 -14
- package/src/local/managed-job-plan.mjs +38 -14
- package/src/local/managed-job-projection.mjs +23 -15
- package/src/local/managed-job-retention.mjs +128 -0
- package/src/local/managed-job-runner-claim.mjs +70 -0
- package/src/local/managed-job-runner.mjs +35 -10
- package/src/local/managed-job-storage.mjs +22 -6
- package/src/local/managed-job-terminal-maintenance.mjs +40 -0
- package/src/local/managed-job-terminal.mjs +150 -0
- package/src/local/managed-jobs.mjs +424 -287
- package/src/local/npm-cli.mjs +42 -0
- package/src/local/npm-environment.mjs +37 -0
- package/src/local/operation-authorization.mjs +68 -324
- package/src/local/operation-risk.mjs +51 -4
- package/src/local/owner-state-lock.mjs +118 -0
- package/src/local/package-identity.mjs +9 -0
- package/src/local/patch.mjs +20 -18
- package/src/local/path-inspection.mjs +23 -0
- package/src/local/policy.mjs +19 -0
- package/src/local/private-toolchain-integrity.mjs +54 -0
- package/src/local/process-execution.mjs +87 -75
- package/src/local/process-foreground-timeout.mjs +50 -0
- package/src/local/process-identity.mjs +41 -7
- package/src/local/process-nonreplayable-settlement.mjs +50 -0
- package/src/local/process-result-projection.mjs +22 -0
- package/src/local/process-session-events.mjs +47 -0
- package/src/local/process-session-remote-activity.mjs +10 -0
- package/src/local/process-session-termination.mjs +73 -0
- package/src/local/process-sessions.mjs +144 -112
- package/src/local/process-tracker.mjs +140 -13
- package/src/local/process-tree-ownership-types.d.ts +37 -0
- package/src/local/process-tree-ownership.mjs +57 -0
- package/src/local/process-tree-signal.mjs +34 -0
- package/src/local/process-tree-snapshot.mjs +77 -0
- package/src/local/process-tree-supervisor.mjs +69 -0
- package/src/local/process-tree.mjs +11 -53
- package/src/local/project-metadata.mjs +19 -6
- package/src/local/relay-call-recovery.mjs +58 -38
- package/src/local/relay-connection-classification.mjs +131 -0
- package/src/local/relay-connection-support.mjs +91 -0
- package/src/local/relay-connection.mjs +179 -272
- package/src/local/relay-diagnostics.mjs +91 -0
- package/src/local/relay-heartbeat.mjs +119 -0
- package/src/local/relay-liveness.mjs +109 -0
- package/src/local/relay-peer-diagnostics.mjs +29 -0
- package/src/local/release-runtime-lock.mjs +18 -0
- package/src/local/remote-activity-idle-sleep-guard.mjs +77 -0
- package/src/local/remote-configuration.mjs +48 -0
- package/src/local/resilient-relay-connection.mjs +178 -0
- package/src/local/resource-admission-diagnostics.mjs +52 -0
- package/src/local/resource-admission-policy.mjs +115 -0
- package/src/local/resource-admission.mjs +339 -0
- package/src/local/resource-build-root.mjs +83 -0
- package/src/local/resource-cargo-concurrency.mjs +38 -0
- package/src/local/resource-cmake-concurrency.mjs +42 -0
- package/src/local/resource-command-concurrency.mjs +37 -0
- package/src/local/resource-command-profile.mjs +176 -0
- package/src/local/resource-coordinator-accounting.mjs +21 -0
- package/src/local/resource-cpu-window.mjs +20 -0
- package/src/local/resource-disk-headroom.mjs +13 -0
- package/src/local/resource-elastic-memory.mjs +19 -0
- package/src/local/resource-elastic-request.mjs +36 -0
- package/src/local/resource-foreground-wait.mjs +29 -0
- package/src/local/resource-go-concurrency.mjs +43 -0
- package/src/local/resource-gradle-concurrency.mjs +57 -0
- package/src/local/resource-host-cache.mjs +39 -0
- package/src/local/resource-host-darwin.mjs +54 -0
- package/src/local/resource-host-linux.mjs +35 -0
- package/src/local/resource-host-sample-file.mjs +23 -0
- package/src/local/resource-host-snapshot.mjs +99 -0
- package/src/local/resource-lease-accounting.mjs +119 -0
- package/src/local/resource-light-command.mjs +6 -0
- package/src/local/resource-make-concurrency.mjs +42 -0
- package/src/local/resource-maven-concurrency.mjs +38 -0
- package/src/local/resource-ninja-command-concurrency.mjs +20 -0
- package/src/local/resource-ninja-concurrency.mjs +28 -0
- package/src/local/resource-operations.mjs +26 -10
- package/src/local/resource-pressure.mjs +67 -0
- package/src/local/resource-probe-command.mjs +35 -0
- package/src/local/resource-process-admission.mjs +59 -0
- package/src/local/resource-process-ancestry-cache.mjs +43 -0
- package/src/local/resource-process-ancestry.mjs +32 -0
- package/src/local/resource-process-priority.mjs +15 -0
- package/src/local/resource-project-key.mjs +53 -0
- package/src/local/resource-pytest-concurrency.mjs +33 -0
- package/src/local/resource-release-control-classification.mjs +14 -0
- package/src/local/resource-release-control-executable.mjs +70 -0
- package/src/local/resource-release-control-workspace.mjs +41 -0
- package/src/local/resource-request-contract.mjs +30 -0
- package/src/local/resource-script-classification.mjs +44 -0
- package/src/local/resource-shell-analysis.mjs +58 -0
- package/src/local/resource-staging-recovery.mjs +97 -0
- package/src/local/resource-swift-concurrency.mjs +31 -0
- package/src/local/resource-transaction-lock.mjs +189 -0
- package/src/local/resource-wait.mjs +48 -0
- package/src/local/resource-waiters.mjs +139 -0
- package/src/local/resource-xcode-command.mjs +7 -0
- package/src/local/resource-xcode-concurrency.mjs +34 -0
- package/src/local/resource-xcode-non-build.mjs +19 -0
- package/src/local/runtime-activation.mjs +700 -0
- package/src/local/runtime-activity-projection.mjs +48 -0
- package/src/local/runtime-capabilities.mjs +48 -15
- package/src/local/runtime-diagnostic-state.mjs +14 -0
- package/src/local/runtime-diagnostics.mjs +40 -13
- package/src/local/runtime-info-projection.mjs +60 -0
- package/src/local/runtime-info-relay-projection.mjs +21 -0
- package/src/local/runtime-paths.mjs +2 -1
- package/src/local/runtime-process-routing.mjs +33 -0
- package/src/local/runtime-relay-connection-options.mjs +44 -0
- package/src/local/runtime-relay-control.mjs +98 -0
- package/src/local/runtime-relay.mjs +26 -50
- package/src/local/runtime-reporting.mjs +37 -21
- package/src/local/runtime-resource-service.mjs +77 -0
- package/src/local/runtime-tool-handlers.mjs +13 -10
- package/src/local/runtime.mjs +244 -208
- package/src/local/secure-file.mjs +73 -20
- package/src/local/security-audit-dispatch.mjs +35 -0
- package/src/local/security-audit-log.mjs +215 -0
- package/src/local/security-audit-state.mjs +139 -0
- package/src/local/security-audit-storage.mjs +135 -0
- package/src/local/security-audit-warning.mjs +31 -0
- package/src/local/security-audit-worker.mjs +87 -0
- package/src/local/service-convergence.mjs +42 -1
- package/src/local/service-definition.mjs +24 -0
- package/src/local/service-environment.mjs +18 -10
- package/src/local/service-owner.mjs +146 -0
- package/src/local/service-ownership.mjs +17 -0
- package/src/local/service-restart-handoff.mjs +64 -0
- package/src/local/service-restart-scheduler.mjs +49 -0
- package/src/local/service-runtime-convergence.mjs +52 -0
- package/src/local/service-runtime.mjs +105 -0
- package/src/local/service-status.mjs +60 -0
- package/src/local/service.mjs +385 -92
- package/src/local/shell.mjs +39 -19
- package/src/local/ssh-key.mjs +158 -42
- package/src/local/state-inventory.mjs +50 -24
- package/src/local/state-owner-lock-inventory.mjs +23 -0
- package/src/local/state-root-owned-namespaces.mjs +83 -0
- package/src/local/state-root-retirement.mjs +89 -0
- package/src/local/state.mjs +386 -96
- package/src/local/stdio.mjs +96 -66
- package/src/local/support-state-projection.mjs +30 -0
- package/src/local/system-network-route.mjs +76 -0
- package/src/local/systemd-removal.mjs +20 -0
- package/src/local/tool-executor.mjs +85 -20
- package/src/local/tool-result-boundary.mjs +51 -0
- package/src/local/toolchain-operation-lock.mjs +41 -0
- package/src/local/tools.mjs +7 -5
- package/src/local/trusted-executable.mjs +53 -0
- package/src/local/trusted-git-executable.mjs +34 -0
- package/src/local/trusted-github-cli.mjs +24 -0
- package/src/local/windows-service-convergence.mjs +49 -0
- package/src/local/windows-service.mjs +52 -45
- package/src/local/worker-deployment-fingerprint.mjs +102 -0
- package/src/local/worker-deployment.mjs +48 -60
- package/src/local/worker-health.mjs +12 -4
- package/src/local/worker-secret-file.mjs +37 -17
- package/src/local/workspace-file-service.mjs +156 -239
- package/src/local/workspace-file-transaction.mjs +189 -0
- package/src/local/workspace-search.mjs +55 -0
- package/src/local/wrangler-toolchain/package-lock.json +1505 -0
- package/src/local/wrangler-toolchain/package.json +23 -0
- package/src/local/wrangler-toolchain-verification.mjs +135 -0
- package/src/local/wrangler-toolchain.mjs +188 -0
- package/src/shared/access-contract.json +7 -1
- package/src/shared/activation-recovery.mjs +49 -0
- package/src/shared/admin-auth.d.mts +2 -1
- package/src/shared/admin-auth.mjs +3 -3
- package/src/shared/authority-revocation.d.mts +9 -0
- package/src/shared/authority-revocation.mjs +28 -0
- package/src/shared/daemon-auth.d.mts +11 -0
- package/src/shared/daemon-auth.mjs +17 -0
- package/src/shared/device-session-auth.d.mts +20 -0
- package/src/shared/device-session-auth.mjs +51 -0
- package/src/shared/foreground-timeout.d.mts +15 -0
- package/src/shared/foreground-timeout.mjs +70 -0
- package/src/shared/log-redaction.d.mts +9 -0
- package/src/shared/log-redaction.mjs +51 -0
- package/src/shared/mcp-protocol.d.mts +27 -0
- package/src/shared/mcp-protocol.mjs +275 -0
- package/src/shared/project-overview-projection.d.mts +4 -0
- package/src/shared/project-overview-projection.mjs +96 -0
- package/src/shared/relay-contract.json +28 -0
- package/src/shared/result-projection.d.mts +2 -1
- package/src/shared/result-projection.mjs +13 -2
- package/src/shared/server-metadata.json +15 -9
- package/src/shared/tool-argument-validation.d.mts +17 -0
- package/src/shared/tool-argument-validation.mjs +325 -0
- package/src/shared/tool-call-capacity.d.mts +41 -0
- package/src/shared/tool-call-capacity.mjs +59 -0
- package/src/shared/tool-catalog.json +626 -57
- package/src/worker/access.ts +12 -0
- package/src/worker/account-admin.ts +55 -33
- package/src/worker/authority-revocations.ts +174 -0
- package/src/worker/authority.ts +52 -6
- package/src/worker/daemon-auth.ts +49 -68
- package/src/worker/daemon-channel.ts +36 -0
- package/src/worker/daemon-http-auth.ts +63 -0
- package/src/worker/daemon-http-channel.ts +120 -0
- package/src/worker/daemon-http-controller.ts +163 -0
- package/src/worker/daemon-http-protocol.ts +62 -0
- package/src/worker/daemon-http-registry.ts +82 -0
- package/src/worker/daemon-last-observation.ts +30 -0
- package/src/worker/daemon-liveness.ts +8 -6
- package/src/worker/daemon-ready-dispatch.ts +17 -0
- package/src/worker/daemon-ready-messages.ts +73 -0
- package/src/worker/daemon-ready-waiters.ts +79 -0
- package/src/worker/daemon-recovery-budget.ts +13 -0
- package/src/worker/daemon-registry.ts +58 -0
- package/src/worker/daemon-relay-diagnostics.ts +127 -0
- package/src/worker/daemon-socket-attachment.ts +55 -0
- package/src/worker/daemon-sockets.ts +72 -62
- package/src/worker/daemon-status.ts +38 -0
- package/src/worker/device-session-verifier.ts +129 -0
- package/src/worker/dpop.ts +157 -0
- package/src/worker/durable-process-timeout.ts +29 -0
- package/src/worker/errors.ts +57 -0
- package/src/worker/http.ts +112 -26
- package/src/worker/index.ts +446 -440
- package/src/worker/mcp-access.ts +57 -0
- package/src/worker/mcp-controller.ts +188 -0
- package/src/worker/mcp-http-accept.ts +11 -0
- package/src/worker/mcp-http-contract.ts +322 -0
- package/src/worker/mcp-initialization-compat.ts +141 -0
- package/src/worker/mcp-jsonrpc.ts +11 -29
- package/src/worker/mcp-removed-protocol.ts +17 -0
- package/src/worker/mcp-response-proxy.ts +135 -0
- package/src/worker/mcp-response-stream.ts +65 -0
- package/src/worker/mcp-stale-schema-compat.ts +57 -0
- package/src/worker/mcp-stream-proxy-contract.ts +48 -0
- package/src/worker/mcp-tool-call-input.ts +23 -0
- package/src/worker/nonce-store.ts +33 -35
- package/src/worker/oauth-authorization-page.ts +21 -1
- package/src/worker/oauth-client-admin.ts +52 -0
- package/src/worker/oauth-client-contract.ts +38 -0
- package/src/worker/oauth-controller.ts +107 -47
- package/src/worker/oauth-field-contract.ts +31 -0
- package/src/worker/oauth-record-contract.ts +7 -0
- package/src/worker/oauth-refresh-authority.ts +27 -0
- package/src/worker/oauth-refresh-exchange.ts +162 -0
- package/src/worker/oauth-refresh-families.ts +130 -27
- package/src/worker/oauth-refresh-persistence.ts +116 -0
- package/src/worker/oauth-state.ts +33 -18
- package/src/worker/oauth-store-validation.ts +131 -0
- package/src/worker/oauth-token-derivation.ts +33 -0
- package/src/worker/oauth-token-issuance.ts +119 -0
- package/src/worker/oauth-tokens.ts +56 -190
- package/src/worker/observability.ts +38 -5
- package/src/worker/pending-admission.ts +15 -0
- package/src/worker/pending-call-capacity.ts +108 -0
- package/src/worker/pending-call-contract.ts +42 -5
- package/src/worker/pending-call-deadlines.ts +75 -0
- package/src/worker/pending-calls.ts +131 -125
- package/src/worker/runtime-alarm-storage.ts +37 -0
- package/src/worker/runtime-alarm.ts +136 -0
- package/src/worker/server-info-activity.ts +36 -0
- package/src/worker/server-info-tool-delivery.ts +15 -0
- package/src/worker/server-info.ts +168 -0
- package/src/worker/tool-catalog.ts +47 -1
- package/src/worker/tool-timeout.ts +75 -14
- package/src/worker/websocket-protocol.ts +26 -2
- package/src/worker/worker-edge-guard.ts +90 -0
- package/src/worker/worker-edge-log.ts +65 -0
- package/src/worker/worker-entry.ts +52 -0
- package/src/worker/worker-mcp-config.ts +23 -0
- package/src/worker/worker-metadata.ts +48 -0
- package/src/worker/worker-rate-limit-key.ts +30 -0
- package/src/worker/worker-runtime-config.ts +20 -0
- package/src/worker/worker-static-routes.ts +54 -0
- package/src/worker/worker-task-lifetime.ts +18 -0
- package/tsconfig.local.json +17 -1
- package/wrangler.jsonc +13 -0
- package/src/local/cli-approval.mjs +0 -117
- package/src/local/operation-state-lock.mjs +0 -92
- package/src/worker/mcp-session.ts +0 -72
|
@@ -1,51 +1,72 @@
|
|
|
1
1
|
import {
|
|
2
|
-
clampInt, normalizeBrowserAction, normalizeBrowserSelector, normalizeBrowserWait,
|
|
3
|
-
normalizeInputMode, normalizeNavigationWait, normalizeTabCommand, optionalInteger, optionalString,
|
|
2
|
+
browserPairingLaunchUnavailable, browserPairingLaunchUnknown, clampInt, normalizeBrowserAction, normalizeBrowserSelector, normalizeBrowserWait,
|
|
3
|
+
normalizeBrowserSnapshotIdentity, normalizeImageFormat, normalizeInputMode, normalizeNavigationWait, normalizeTabCommand, optionalBoolean, optionalInteger, optionalString,
|
|
4
4
|
validateNavigationUrl,
|
|
5
5
|
} from "./browser-command.mjs";
|
|
6
|
-
|
|
6
|
+
import { BrowserComputerObservationService } from "./browser-computer-observation-service.mjs";
|
|
7
|
+
import { BrowserTrustedInputHealth, TRUSTED_INPUT_QUARANTINE_FALLBACK } from "./browser-trusted-input-health.mjs";
|
|
8
|
+
import {
|
|
9
|
+
boundedBrowserValue, normalizeMimeType, normalizeUploadFilename, optionalStringArray, prepareBrowserFormField,
|
|
10
|
+
resolveBrowserActionValue, validateBrowserResource,
|
|
11
|
+
} from "./browser-resource-input.mjs";
|
|
7
12
|
const MAX_SOURCE_BYTES = 4 * 1024 * 1024;
|
|
8
13
|
const MAX_FORM_FIELDS = 200;
|
|
9
|
-
const MAX_FIELD_VALUE_CHARS = 128 * 1024;
|
|
10
14
|
const MAX_FORM_VALUE_BYTES = 4 * 1024 * 1024;
|
|
11
|
-
const RESOURCE_NAME = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
12
|
-
|
|
13
15
|
export class BrowserOperationService {
|
|
14
16
|
constructor({
|
|
15
17
|
authorizeTool,
|
|
16
18
|
ensureStarted,
|
|
17
19
|
request,
|
|
18
20
|
bridgeStatus,
|
|
21
|
+
createPairingLaunch,
|
|
19
22
|
extensionPath,
|
|
20
23
|
expectedExtensionVersion,
|
|
24
|
+
expectedExtensionId,
|
|
21
25
|
runProcess,
|
|
22
26
|
readResourceText,
|
|
23
27
|
readResourceBinary,
|
|
28
|
+
throwIfCancelled = () => {},
|
|
29
|
+
logger = null,
|
|
24
30
|
}) {
|
|
25
31
|
this.authorizeTool = authorizeTool;
|
|
26
32
|
this.ensureStarted = ensureStarted;
|
|
27
33
|
this.request = request;
|
|
28
34
|
this.bridgeStatus = bridgeStatus;
|
|
35
|
+
this.createPairingLaunch = createPairingLaunch;
|
|
29
36
|
this.extensionPath = extensionPath;
|
|
30
37
|
this.expectedExtensionVersion = expectedExtensionVersion;
|
|
38
|
+
this.expectedExtensionId = expectedExtensionId;
|
|
31
39
|
this.runProcess = runProcess;
|
|
32
40
|
this.readResourceText = readResourceText;
|
|
33
41
|
this.readResourceBinary = readResourceBinary;
|
|
42
|
+
this.throwIfCancelled = throwIfCancelled;
|
|
43
|
+
this.trustedInputHealth = new BrowserTrustedInputHealth({ logger });
|
|
44
|
+
this.computerObservation = new BrowserComputerObservationService({
|
|
45
|
+
authorizeTool: (tool) => this.authorizeTool(tool),
|
|
46
|
+
request: (...args) => this.requestComputerObservation(...args),
|
|
47
|
+
bridgeStatus: () => this.bridgeStatus(),
|
|
48
|
+
inspectPage: (args, context) => this.inspectPage(args, context),
|
|
49
|
+
screenshot: (args, context) => this.screenshot(args, context),
|
|
50
|
+
});
|
|
34
51
|
}
|
|
35
|
-
|
|
36
52
|
async status(context = {}) {
|
|
37
53
|
this.authorizeTool("browser_status");
|
|
38
54
|
await this.ensureStarted(context);
|
|
39
55
|
const bridge = this.bridgeStatus();
|
|
40
56
|
const extension = bridge.extensionInfo;
|
|
57
|
+
const trustedHealth = this.trustedInputHealth.status(bridge);
|
|
41
58
|
return {
|
|
42
59
|
available: true,
|
|
43
60
|
connected: bridge.extensionConnected,
|
|
44
61
|
broker_role: bridge.brokerRole,
|
|
62
|
+
runtime_clients: Number(bridge.runtime_clients) || 0,
|
|
63
|
+
routed_requests: Number(bridge.routed_requests) || 0,
|
|
45
64
|
endpoint: `ws://127.0.0.1:${bridge.port}/extension`,
|
|
46
65
|
pairing_url: `http://127.0.0.1:${bridge.port}/pair`,
|
|
47
66
|
extension_path: this.extensionPath,
|
|
48
67
|
expected_extension_version: this.expectedExtensionVersion,
|
|
68
|
+
expected_extension_id: this.expectedExtensionId,
|
|
69
|
+
extension_id: extension?.extension_id || "",
|
|
49
70
|
extension_protocol: extension?.protocol || null,
|
|
50
71
|
extension_version: extension?.version || "",
|
|
51
72
|
extension_capabilities: extension?.capabilities || [],
|
|
@@ -60,53 +81,71 @@ export class BrowserOperationService {
|
|
|
60
81
|
source_access: true,
|
|
61
82
|
semantic_snapshot_refs: true,
|
|
62
83
|
actionability_waits: true,
|
|
63
|
-
trusted_input:
|
|
84
|
+
trusted_input: trustedHealth.available,
|
|
85
|
+
trusted_input_quarantined: trustedHealth.quarantined,
|
|
86
|
+
trusted_input_health: !bridge.extensionConnected ? "disconnected" : trustedHealth.quarantined ? "quarantined" : trustedHealth.supported ? "ready" : "unsupported",
|
|
64
87
|
input_modes: ["auto", "trusted", "dom"],
|
|
65
88
|
complex_form_fill: true,
|
|
66
89
|
tab_management: true,
|
|
67
90
|
explicit_waits: true,
|
|
68
91
|
screenshots: true,
|
|
92
|
+
computer_observation_v1: extension?.capabilities?.includes("computer_observation_v1") === true,
|
|
93
|
+
cdp_accessibility_snapshot: extension?.capabilities?.includes("cdp_accessibility_snapshot") === true,
|
|
94
|
+
cdp_surface_screenshot: extension?.capabilities?.includes("cdp_surface_screenshot") === true,
|
|
95
|
+
backend_node_trusted_input: extension?.capabilities?.includes("backend_node_trusted_input") === true && !trustedHealth.quarantined,
|
|
69
96
|
restricted_pages: ["browser-internal pages", "extension stores", "some PDF/plugin viewers", "pages blocked by enterprise policy"],
|
|
70
97
|
security: {
|
|
71
98
|
loopback_only: true,
|
|
72
|
-
bearer_pairing_token:
|
|
99
|
+
bearer_pairing_token: false,
|
|
100
|
+
short_lived_pairing_grant: true,
|
|
101
|
+
ephemeral_pairing_listener: true,
|
|
102
|
+
one_time_hmac_socket_auth: true,
|
|
103
|
+
long_lived_tokens_on_wire: false,
|
|
104
|
+
pinned_extension_identity: true,
|
|
73
105
|
arbitrary_extension_code_from_mcp: false,
|
|
74
106
|
resource_values_returned_to_model: false,
|
|
75
107
|
},
|
|
76
108
|
};
|
|
77
109
|
}
|
|
78
|
-
|
|
79
110
|
async pair(args = {}, context = {}) {
|
|
80
111
|
this.authorizeTool("pair_browser_extension");
|
|
81
112
|
const status = await this.status(context);
|
|
82
|
-
|
|
113
|
+
const open = optionalBoolean(args.open, "open", true);
|
|
114
|
+
if (open) {
|
|
115
|
+
const launch = await this.createPairingLaunch(this.bridgeStatus().port);
|
|
83
116
|
const command = process.platform === "darwin"
|
|
84
|
-
? { cmd: "open", argv: [
|
|
117
|
+
? { cmd: "open", argv: [launch.url] }
|
|
85
118
|
: process.platform === "win32"
|
|
86
|
-
? { cmd: "cmd.exe", argv: ["/d", "/s", "/c", "start", "",
|
|
87
|
-
: { cmd: "xdg-open", argv: [
|
|
88
|
-
|
|
119
|
+
? { cmd: "cmd.exe", argv: ["/d", "/s", "/c", "start", "", launch.url] }
|
|
120
|
+
: { cmd: "xdg-open", argv: [launch.url] };
|
|
121
|
+
this.throwIfCancelled(context);
|
|
122
|
+
try {
|
|
123
|
+
await this.runProcess(command.cmd, command.argv, 30_000, false, 128 * 1024, context, undefined, null, { nonReplayableMutation: true });
|
|
124
|
+
} catch (error) {
|
|
125
|
+
launch.close();
|
|
126
|
+
if (error?.details?.reason === "process_outcome_unknown_after_spawn") throw browserPairingLaunchUnknown();
|
|
127
|
+
if (error?.details?.reason === "process_failed_before_spawn") throw browserPairingLaunchUnavailable();
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
89
130
|
}
|
|
90
131
|
return {
|
|
91
132
|
...status,
|
|
92
|
-
opened_pairing_page:
|
|
133
|
+
opened_pairing_page: open,
|
|
93
134
|
setup_steps: [
|
|
94
135
|
"Open the browser extensions page and enable developer mode.",
|
|
95
136
|
"Load the unpacked extension from extension_path once.",
|
|
96
|
-
"
|
|
97
|
-
"After upgrades, reload the unpacked extension and accept any newly requested browser permission.",
|
|
137
|
+
"Run pair_browser_extension with opening enabled; the sanitized pairing_url alone contains no pairing grant.",
|
|
138
|
+
"After upgrades, reload the same unpacked extension path and accept any newly requested browser permission; older local-candidate installs may need one Load unpacked migration to the current stable extension_path.",
|
|
98
139
|
],
|
|
99
140
|
};
|
|
100
141
|
}
|
|
101
|
-
|
|
102
142
|
async listTabs(args = {}, context = {}) {
|
|
103
143
|
this.authorizeTool("browser_list_tabs");
|
|
104
144
|
return this.request("list_tabs", {
|
|
105
|
-
currentWindow: args.current_window
|
|
106
|
-
includePinned: args.include_pinned
|
|
145
|
+
currentWindow: optionalBoolean(args.current_window, "current_window", false),
|
|
146
|
+
includePinned: optionalBoolean(args.include_pinned, "include_pinned", true),
|
|
107
147
|
}, clampInt(args.timeout_seconds, 30, 1, 120), context);
|
|
108
148
|
}
|
|
109
|
-
|
|
110
149
|
async manageTabs(args = {}, context = {}) {
|
|
111
150
|
this.authorizeTool("browser_manage_tabs");
|
|
112
151
|
return this.request("manage_tabs", normalizeTabCommand(args), clampInt(args.timeout_seconds, 30, 1, 120), context);
|
|
@@ -124,7 +163,7 @@ export class BrowserOperationService {
|
|
|
124
163
|
return this.request("get_source", {
|
|
125
164
|
tabId: optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER),
|
|
126
165
|
frameId: optionalInteger(args.frame_id, "frame_id", 0, Number.MAX_SAFE_INTEGER),
|
|
127
|
-
allFrames: args.all_frames
|
|
166
|
+
allFrames: optionalBoolean(args.all_frames, "all_frames", false),
|
|
128
167
|
maxBytes: clampInt(args.max_bytes, 1024 * 1024, 1, MAX_SOURCE_BYTES),
|
|
129
168
|
}, clampInt(args.timeout_seconds, 30, 1, 120), context);
|
|
130
169
|
}
|
|
@@ -134,15 +173,36 @@ export class BrowserOperationService {
|
|
|
134
173
|
return this.request("inspect_page", {
|
|
135
174
|
tabId: optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER),
|
|
136
175
|
frameId: optionalInteger(args.frame_id, "frame_id", 0, Number.MAX_SAFE_INTEGER),
|
|
137
|
-
allFrames: args.all_frames
|
|
176
|
+
allFrames: optionalBoolean(args.all_frames, "all_frames", true),
|
|
138
177
|
maxElements: clampInt(args.max_elements, 300, 1, 1000),
|
|
139
|
-
includeValues: args.include_values
|
|
178
|
+
includeValues: optionalBoolean(args.include_values, "include_values", false),
|
|
179
|
+
focusQuery: optionalString(args.focus_query, "focus_query", 1000),
|
|
140
180
|
}, clampInt(args.timeout_seconds, 30, 1, 120), context);
|
|
141
181
|
}
|
|
142
182
|
|
|
183
|
+
documentState(args = {}, context = {}) { return this.computerObservation.documentState(args, context); }
|
|
184
|
+
observeComputer(args = {}, context = {}) { return this.computerObservation.observe(args, context); }
|
|
185
|
+
pointAction(args = {}, context = {}) { return this.computerObservation.pointAction(args, context); }
|
|
186
|
+
async backendNodeAction(args = {}, context = {}) {
|
|
187
|
+
const action = args.action;
|
|
188
|
+
if (typeof action !== "string" || !["click", "double_click", "hover", "drag", "scroll", "press", "type_text", "fill", "check", "uncheck", "submit"].includes(action)) throw new Error("snapshot backend action must be click, double_click, hover, drag, scroll, press, type_text, fill, check, uncheck, or submit");
|
|
189
|
+
if (!["fill", "type_text"].includes(action) && (args.value !== undefined || args.value_resource !== undefined)) throw new Error(`value and value_resource are not valid for snapshot backend ${action}`);
|
|
190
|
+
if (action !== "press" && args.key !== undefined) throw new Error(`key is not valid for snapshot backend ${action}`);
|
|
191
|
+
this.computerObservation.preflightBackendNodeAction(args);
|
|
192
|
+
this.trustedInputHealth.assertTrustedAvailable(this.bridgeStatus());
|
|
193
|
+
const resolved = await resolveBrowserActionValue(args, this.readResourceText);
|
|
194
|
+
return this.computerObservation.backendNodeAction({ ...args, value: resolved.value ?? undefined }, context);
|
|
195
|
+
}
|
|
143
196
|
async act(args = {}, context = {}) {
|
|
144
197
|
this.authorizeTool("browser_action");
|
|
145
198
|
const action = normalizeBrowserAction(args.action);
|
|
199
|
+
const valueActions = new Set(["fill", "type_text", "select"]);
|
|
200
|
+
if (!valueActions.has(action) && (args.value !== undefined || args.value_resource !== undefined)) {
|
|
201
|
+
throw new Error(`value and value_resource are not valid for browser action ${action}`);
|
|
202
|
+
}
|
|
203
|
+
if (valueActions.has(action) && args.value === undefined && args.value_resource === undefined) {
|
|
204
|
+
throw new Error(`browser action ${action} requires value or value_resource`);
|
|
205
|
+
}
|
|
146
206
|
const rawUrl = optionalString(args.url, "url", 32768);
|
|
147
207
|
const payload = {
|
|
148
208
|
tabId: optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER),
|
|
@@ -155,70 +215,83 @@ export class BrowserOperationService {
|
|
|
155
215
|
waitFor: normalizeNavigationWait(args.wait_for),
|
|
156
216
|
inputMode: normalizeInputMode(args.input_mode),
|
|
157
217
|
elementTimeoutMs: clampInt(args.element_timeout_seconds, 10, 1, 60) * 1000,
|
|
218
|
+
expectedIdentity: normalizeBrowserSnapshotIdentity(args.expected_ref_identity),
|
|
219
|
+
expectedTabUrl: optionalString(args.expected_tab_url, "expected_tab_url", 32768),
|
|
220
|
+
expectedDocumentEpoch: optionalString(args.expected_document_epoch, "expected_document_epoch", 9000), expectedHistoryEntryKey: optionalString(args.expected_history_entry_key, "expected_history_entry_key", 512),
|
|
158
221
|
};
|
|
222
|
+
if (!["navigate", "reload", "back", "forward"].includes(action) && payload.expectedTabUrl) throw new Error("expected_tab_url is only valid for snapshot-bound navigation actions");
|
|
223
|
+
if (!["reload", "back", "forward"].includes(action) && payload.expectedDocumentEpoch) throw new Error("expected_document_epoch is only valid for snapshot-bound reload, back, or forward");
|
|
224
|
+
if (!["reload", "back", "forward"].includes(action) && payload.expectedHistoryEntryKey) throw new Error("expected_history_entry_key is only valid for snapshot-bound reload, back, or forward");
|
|
159
225
|
if (action !== "navigate" && rawUrl) throw new Error("url is only valid for navigate");
|
|
160
226
|
if (action !== "press" && payload.key) throw new Error("key is only valid for press");
|
|
161
227
|
if (payload.inputMode === "trusted" && !["click", "double_click", "hover", "press", "type_text"].includes(action)) {
|
|
162
228
|
throw new Error("input_mode=trusted supports click, double_click, hover, press, and type_text only");
|
|
163
229
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
230
|
+
const trustedDecision = this.trustedInputHealth.pageInputMode({ inputMode: payload.inputMode, action, bridge: this.bridgeStatus() });
|
|
231
|
+
payload.inputMode = trustedDecision.inputMode;
|
|
232
|
+
const resolvedValue = await resolveBrowserActionValue(args, this.readResourceText);
|
|
233
|
+
payload.value = resolvedValue.value;
|
|
169
234
|
if (payload.value !== null && !["fill", "select", "press", "type_text"].includes(action)) {
|
|
170
235
|
throw new Error(`value is not valid for browser action '${action}'`);
|
|
171
236
|
}
|
|
172
237
|
const pageAction = !["navigate", "reload", "back", "forward"].includes(action);
|
|
173
238
|
const defaultTimeout = pageAction ? Math.min(120, Math.max(30, payload.elementTimeoutMs / 1000 + 5)) : 30;
|
|
174
|
-
|
|
239
|
+
let response;
|
|
240
|
+
try {
|
|
241
|
+
response = await this.request("action", payload, clampInt(args.timeout_seconds, defaultTimeout, 1, 120), context);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
this.trustedInputHealth.noteAmbiguousFailure(error, this.bridgeStatus());
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
175
246
|
return {
|
|
176
247
|
...response,
|
|
177
|
-
|
|
248
|
+
...(trustedDecision.fallback ? {
|
|
249
|
+
input_mode: "dom",
|
|
250
|
+
trusted_input_fallback: true,
|
|
251
|
+
fallback_reason: TRUSTED_INPUT_QUARANTINE_FALLBACK,
|
|
252
|
+
} : {}),
|
|
253
|
+
value_source: resolvedValue.source,
|
|
178
254
|
value_exposed: false,
|
|
179
255
|
};
|
|
180
256
|
}
|
|
181
257
|
|
|
258
|
+
async requestComputerObservation(method, params, timeoutSeconds, context) {
|
|
259
|
+
const trustedMethod = method === "point_action" || method === "backend_node_action";
|
|
260
|
+
if (trustedMethod) this.trustedInputHealth.assertTrustedAvailable(this.bridgeStatus());
|
|
261
|
+
try {
|
|
262
|
+
return await this.request(method, params, timeoutSeconds, context);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (trustedMethod) this.trustedInputHealth.noteAmbiguousFailure(error, this.bridgeStatus());
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
182
269
|
async fillForm(args = {}, context = {}) {
|
|
183
270
|
this.authorizeTool("browser_fill_form");
|
|
184
271
|
if (!Array.isArray(args.fields) || !args.fields.length) throw new Error("fields must be a non-empty array");
|
|
185
272
|
if (args.fields.length > MAX_FORM_FIELDS) throw new Error(`fields contains more than ${MAX_FORM_FIELDS} entries`);
|
|
273
|
+
const tabId = optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER);
|
|
274
|
+
const frameId = optionalInteger(args.frame_id, "frame_id", 0, Number.MAX_SAFE_INTEGER);
|
|
275
|
+
const submit = optionalBoolean(args.submit, "submit", false);
|
|
276
|
+
const submitSelector = args.submit_selector === undefined ? null : normalizeBrowserSelector(args.submit_selector, "click");
|
|
277
|
+
const waitFor = normalizeNavigationWait(args.wait_for);
|
|
278
|
+
const elementTimeoutSeconds = clampInt(args.element_timeout_seconds, 10, 1, 60);
|
|
279
|
+
const timeoutSeconds = clampInt(args.timeout_seconds, Math.max(60, elementTimeoutSeconds + 5), 1, 180);
|
|
280
|
+
const prepared = args.fields.map((input, index) => prepareBrowserFormField(input, index));
|
|
186
281
|
const fields = [];
|
|
187
282
|
let totalValueBytes = 0;
|
|
188
|
-
for (
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
const allowed = new Set(["selector", "value", "value_resource", "action", "sensitive"]);
|
|
192
|
-
for (const key of Object.keys(input)) if (!allowed.has(key)) throw new Error(`unknown fields[${index}] property: ${key}`);
|
|
193
|
-
let value = input.value === undefined ? null : boundedValue(input.value, `fields[${index}].value`);
|
|
194
|
-
if (input.value_resource !== undefined) {
|
|
195
|
-
if (value !== null) throw new Error(`fields[${index}] value and value_resource are mutually exclusive`);
|
|
196
|
-
value = boundedValue(await this.readResourceText(validateResource(input.value_resource)), `fields[${index}].value_resource`);
|
|
197
|
-
}
|
|
283
|
+
for (const field of prepared) {
|
|
284
|
+
let value = field.value;
|
|
285
|
+
if (field.resourceName) value = boundedBrowserValue(await this.readResourceText(field.resourceName), `fields[${field.index}].value_resource`);
|
|
198
286
|
if (value !== null) {
|
|
199
287
|
totalValueBytes += Buffer.byteLength(value);
|
|
200
288
|
if (totalValueBytes > MAX_FORM_VALUE_BYTES) throw new Error("form field values exceed 4 MiB total");
|
|
201
289
|
}
|
|
202
|
-
|
|
203
|
-
if (value === null && !["check", "uncheck", "click"].includes(action)) throw new Error(`fields[${index}] requires value or value_resource`);
|
|
204
|
-
if (value !== null && !["fill", "select"].includes(action)) throw new Error(`fields[${index}] value is not valid for action '${action}'`);
|
|
205
|
-
fields.push({
|
|
206
|
-
selector: normalizeBrowserSelector(input.selector, action),
|
|
207
|
-
value,
|
|
208
|
-
action,
|
|
209
|
-
sensitive: input.sensitive === true || input.value_resource !== undefined,
|
|
210
|
-
});
|
|
290
|
+
fields.push({ selector: field.selector, value, action: field.action, sensitive: field.sensitive || Boolean(field.resourceName) });
|
|
211
291
|
}
|
|
212
|
-
const elementTimeoutSeconds = clampInt(args.element_timeout_seconds, 10, 1, 60);
|
|
213
292
|
return this.request("fill_form", {
|
|
214
|
-
tabId
|
|
215
|
-
|
|
216
|
-
fields,
|
|
217
|
-
submit: args.submit === true,
|
|
218
|
-
submitSelector: args.submit_selector ? normalizeBrowserSelector(args.submit_selector, "click") : null,
|
|
219
|
-
waitFor: normalizeNavigationWait(args.wait_for),
|
|
220
|
-
elementTimeoutMs: elementTimeoutSeconds * 1000,
|
|
221
|
-
}, clampInt(args.timeout_seconds, Math.max(60, elementTimeoutSeconds + 5), 1, 180), context);
|
|
293
|
+
tabId, frameId, fields, submit, submitSelector, waitFor, elementTimeoutMs: elementTimeoutSeconds * 1000,
|
|
294
|
+
}, timeoutSeconds, context);
|
|
222
295
|
}
|
|
223
296
|
|
|
224
297
|
async uploadFiles(args = {}, context = {}) {
|
|
@@ -226,15 +299,20 @@ export class BrowserOperationService {
|
|
|
226
299
|
if (!Array.isArray(args.resources) || !args.resources.length || args.resources.length > 8) {
|
|
227
300
|
throw new Error("resources must contain 1 to 8 registered resource names");
|
|
228
301
|
}
|
|
302
|
+
const tabId = optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER);
|
|
303
|
+
const frameId = optionalInteger(args.frame_id, "frame_id", 0, Number.MAX_SAFE_INTEGER);
|
|
304
|
+
const selector = normalizeBrowserSelector(args.selector, "fill");
|
|
305
|
+
const elementTimeoutSeconds = clampInt(args.element_timeout_seconds, 10, 1, 60);
|
|
306
|
+
const timeoutSeconds = clampInt(args.timeout_seconds, Math.max(60, elementTimeoutSeconds + 5), 1, 180);
|
|
307
|
+
const resourceNames = args.resources.map(validateBrowserResource);
|
|
229
308
|
const filenames = optionalStringArray(args.filenames, "filenames", 8, 255);
|
|
230
309
|
const mimeTypes = optionalStringArray(args.mime_types, "mime_types", 8, 200);
|
|
231
|
-
if (filenames.length >
|
|
310
|
+
if (filenames.length > resourceNames.length || mimeTypes.length > resourceNames.length) {
|
|
232
311
|
throw new Error("filenames and mime_types cannot contain more entries than resources");
|
|
233
312
|
}
|
|
234
313
|
const files = [];
|
|
235
314
|
let total = 0;
|
|
236
|
-
for (const
|
|
237
|
-
const name = validateResource(raw);
|
|
315
|
+
for (const name of resourceNames) {
|
|
238
316
|
const resource = this.readResourceBinary(name);
|
|
239
317
|
total += resource.buffer.length;
|
|
240
318
|
if (total > 5 * 1024 * 1024) throw new Error("browser upload resources exceed 5 MiB total");
|
|
@@ -246,25 +324,20 @@ export class BrowserOperationService {
|
|
|
246
324
|
data: resource.buffer.toString("base64"),
|
|
247
325
|
});
|
|
248
326
|
}
|
|
249
|
-
const elementTimeoutSeconds = clampInt(args.element_timeout_seconds, 10, 1, 60);
|
|
250
327
|
const result = await this.request("upload_files", {
|
|
251
|
-
tabId
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
files,
|
|
255
|
-
elementTimeoutMs: elementTimeoutSeconds * 1000,
|
|
256
|
-
}, clampInt(args.timeout_seconds, Math.max(60, elementTimeoutSeconds + 5), 1, 180), context);
|
|
257
|
-
return { ...result, resource_names: args.resources.map(String), resource_contents_exposed: false };
|
|
328
|
+
tabId, frameId, selector, files, elementTimeoutMs: elementTimeoutSeconds * 1000,
|
|
329
|
+
}, timeoutSeconds, context);
|
|
330
|
+
return { ...result, resource_names: resourceNames, resource_contents_exposed: false };
|
|
258
331
|
}
|
|
259
332
|
|
|
260
333
|
async screenshot(args = {}, context = {}) {
|
|
261
334
|
this.authorizeTool("browser_screenshot");
|
|
262
335
|
const result = await this.request("screenshot", {
|
|
263
336
|
tabId: optionalInteger(args.tab_id, "tab_id", 1, Number.MAX_SAFE_INTEGER),
|
|
264
|
-
format: args.format
|
|
337
|
+
format: normalizeImageFormat(args.format, "format"),
|
|
265
338
|
quality: clampInt(args.quality, 90, 1, 100),
|
|
266
339
|
}, clampInt(args.timeout_seconds, 30, 1, 120), context);
|
|
267
|
-
const data =
|
|
340
|
+
const data = typeof result.data === "string" ? result.data : "";
|
|
268
341
|
const match = /^data:(image\/(?:png|jpeg));base64,([A-Za-z0-9+/=]+)$/.exec(data);
|
|
269
342
|
if (!match) throw new Error("browser extension returned an invalid screenshot");
|
|
270
343
|
return {
|
|
@@ -273,53 +346,10 @@ export class BrowserOperationService {
|
|
|
273
346
|
structuredContent: {
|
|
274
347
|
tab_id: result.tab_id,
|
|
275
348
|
url: result.url,
|
|
276
|
-
title: result.title,
|
|
349
|
+
title: result.title, tab_metadata_verified: result.tab_metadata_verified === true,
|
|
277
350
|
mime_type: match[1],
|
|
278
351
|
},
|
|
279
352
|
},
|
|
280
353
|
};
|
|
281
354
|
}
|
|
282
355
|
}
|
|
283
|
-
|
|
284
|
-
function normalizeUploadFilename(value, { derived = false } = {}) {
|
|
285
|
-
let name = String(value || "");
|
|
286
|
-
if (derived) name = name.replace(/[\u0000-\u001f\u007f/\\]+/g, "_").trim();
|
|
287
|
-
if (!name || name === "." || name === ".." || name.length > 255 || /[\u0000-\u001f\u007f/\\]/.test(name)) {
|
|
288
|
-
if (derived) return "upload.bin";
|
|
289
|
-
throw new Error("filenames entries must be safe single-component filenames of at most 255 characters");
|
|
290
|
-
}
|
|
291
|
-
return name;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function normalizeMimeType(value) {
|
|
295
|
-
const mime = String(value || "").trim().toLowerCase();
|
|
296
|
-
if (!/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mime) || mime.length > 200) {
|
|
297
|
-
throw new Error("mime_types entries must be valid media types");
|
|
298
|
-
}
|
|
299
|
-
return mime;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function optionalStringArray(value, label, maxItems, maxLength) {
|
|
303
|
-
if (value === undefined || value === null) return [];
|
|
304
|
-
if (!Array.isArray(value) || value.length > maxItems) throw new Error(`${label} must be an array with at most ${maxItems} entries`);
|
|
305
|
-
return value.map((item, index) => {
|
|
306
|
-
if (typeof item !== "string" || item.includes("\0") || !item.length || item.length > maxLength) {
|
|
307
|
-
throw new Error(`${label}[${index}] must be a non-empty string of at most ${maxLength} characters`);
|
|
308
|
-
}
|
|
309
|
-
return item;
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function boundedValue(value, label) {
|
|
314
|
-
const string = String(value);
|
|
315
|
-
if (string.includes("\0") || string.length > MAX_FIELD_VALUE_CHARS) {
|
|
316
|
-
throw new Error(`${label} exceeds the maximum length or contains a NUL byte`);
|
|
317
|
-
}
|
|
318
|
-
return string;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function validateResource(value) {
|
|
322
|
-
const name = String(value || "").trim();
|
|
323
|
-
if (!RESOURCE_NAME.test(name)) throw new Error("value_resource is invalid");
|
|
324
|
-
return name;
|
|
325
|
-
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { createMonotonicDeadline } from "./monotonic-deadline.mjs";
|
|
3
|
+
import { createBrokerAuthChallenge } from "./browser-broker-auth.mjs";
|
|
4
|
+
|
|
5
|
+
const GRANT = /^(\d{13})\.([A-Za-z0-9_-]{22})\.([A-Za-z0-9_-]{43})$/;
|
|
6
|
+
const GRANT_ID = /^(\d{13})\.([A-Za-z0-9_-]{22})$/;
|
|
7
|
+
const CHALLENGE = /^[A-Za-z0-9_-]{32}$/;
|
|
8
|
+
const PROOF = /^[A-Za-z0-9_-]{43}$/;
|
|
9
|
+
const TOKEN = /^[A-Za-z0-9_-]{32,100}$/;
|
|
10
|
+
const PAIRING_GRANT_TTL_MS = 30_000;
|
|
11
|
+
const EXCHANGE_TTL_MS = 5_000;
|
|
12
|
+
const MAX_PENDING = 32;
|
|
13
|
+
|
|
14
|
+
export function createBrowserPairingGrant(extensionToken, port, now = Date.now()) {
|
|
15
|
+
assertToken(extensionToken);
|
|
16
|
+
const expiresAt = Number(now) + PAIRING_GRANT_TTL_MS;
|
|
17
|
+
if (!Number.isSafeInteger(expiresAt) || expiresAt <= 0) throw new Error("browser pairing grant time is invalid");
|
|
18
|
+
const nonce = randomBytes(16).toString("base64url");
|
|
19
|
+
const proof = grantSecret(extensionToken, port, expiresAt, nonce);
|
|
20
|
+
return `${expiresAt}.${nonce}.${proof}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseBrowserPairingGrant(grant, now = Date.now()) {
|
|
24
|
+
const match = GRANT.exec(String(grant || ""));
|
|
25
|
+
if (!match) return null;
|
|
26
|
+
const expiresAt = Number(match[1]);
|
|
27
|
+
const current = Number(now);
|
|
28
|
+
if (!Number.isFinite(current) || expiresAt < current || expiresAt - current > PAIRING_GRANT_TTL_MS) return null;
|
|
29
|
+
return { id: `${match[1]}.${match[2]}`, secret: match[3], expiresAt };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createPairingBootstrapRegistry(extensionToken, port, options = {}) {
|
|
33
|
+
assertToken(extensionToken);
|
|
34
|
+
const wallNow = typeof options.wallNow === "function" ? options.wallNow : Date.now;
|
|
35
|
+
const monotonicNow = typeof options.monotonicNow === "function" ? options.monotonicNow : undefined;
|
|
36
|
+
const pending = new Map();
|
|
37
|
+
const used = new Map();
|
|
38
|
+
return {
|
|
39
|
+
issue(grantId, clientChallenge, initProof) {
|
|
40
|
+
prune(used, pending, wallNow());
|
|
41
|
+
const grant = grantFromId(extensionToken, port, grantId, wallNow());
|
|
42
|
+
if (!grant || used.has(grant.id) || !CHALLENGE.test(String(clientChallenge || "")) || !PROOF.test(String(initProof || ""))) return null;
|
|
43
|
+
if (!safeEqual(bootstrapInitProof(grant.secret, grant.id, clientChallenge), initProof)) return null;
|
|
44
|
+
const existing = pending.get(grant.id);
|
|
45
|
+
if (existing && !existing.deadline.expired()) return existing.clientChallenge === clientChallenge ? { serverNonce: existing.serverNonce, serverProof: bootstrapProof(grant.secret, "server", grant.id, clientChallenge, existing.serverNonce) } : null;
|
|
46
|
+
if (used.size + pending.size >= MAX_PENDING) return null;
|
|
47
|
+
const serverNonce = createBrokerAuthChallenge();
|
|
48
|
+
pending.set(grant.id, { clientChallenge, serverNonce, deadline: createMonotonicDeadline(EXCHANGE_TTL_MS, monotonicNow) });
|
|
49
|
+
return { serverNonce, serverProof: bootstrapProof(grant.secret, "server", grant.id, clientChallenge, serverNonce) };
|
|
50
|
+
},
|
|
51
|
+
consume(grantId, clientChallenge, serverNonce, clientProof) {
|
|
52
|
+
prune(used, pending, wallNow());
|
|
53
|
+
const grant = grantFromId(extensionToken, port, grantId, wallNow());
|
|
54
|
+
if (!grant || used.has(grant.id) || !PROOF.test(String(clientProof || ""))) return false;
|
|
55
|
+
const current = pending.get(grant.id);
|
|
56
|
+
pending.delete(grant.id);
|
|
57
|
+
if (!current || current.deadline.expired()
|
|
58
|
+
|| current.clientChallenge !== clientChallenge || current.serverNonce !== serverNonce) return false;
|
|
59
|
+
const expected = bootstrapProof(grant.secret, "client", grant.id, clientChallenge, serverNonce);
|
|
60
|
+
if (!safeEqual(expected, clientProof)) return false;
|
|
61
|
+
used.set(grant.id, grant.expiresAt);
|
|
62
|
+
return true;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createPairingBootstrapInitProof(grantSecret, grantId, clientChallenge) {
|
|
68
|
+
if (!PROOF.test(String(grantSecret || "")) || !GRANT_ID.test(String(grantId || "")) || !CHALLENGE.test(String(clientChallenge || ""))) throw new Error("browser pairing bootstrap init proof input is invalid");
|
|
69
|
+
return bootstrapInitProof(grantSecret, grantId, clientChallenge);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createPairingBootstrapProof(grantSecret, direction, grantId, clientChallenge, serverNonce) {
|
|
73
|
+
if (!PROOF.test(String(grantSecret || "")) || (direction !== "server" && direction !== "client")
|
|
74
|
+
|| !GRANT_ID.test(String(grantId || "")) || !CHALLENGE.test(String(clientChallenge || ""))
|
|
75
|
+
|| !CHALLENGE.test(String(serverNonce || ""))) {
|
|
76
|
+
throw new Error("browser pairing bootstrap proof input is invalid");
|
|
77
|
+
}
|
|
78
|
+
return bootstrapProof(grantSecret, direction, grantId, clientChallenge, serverNonce);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function grantFromId(token, port, grantId, now) {
|
|
82
|
+
const match = GRANT_ID.exec(String(grantId || ""));
|
|
83
|
+
if (!match) return null;
|
|
84
|
+
const expiresAt = Number(match[1]);
|
|
85
|
+
if (!Number.isFinite(now) || expiresAt < now || expiresAt - now > PAIRING_GRANT_TTL_MS) return null;
|
|
86
|
+
return { id: grantId, secret: grantSecret(token, port, expiresAt, match[2]), expiresAt };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function grantSecret(token, port, expiresAt, nonce) {
|
|
90
|
+
const normalizedPort = Number(port);
|
|
91
|
+
if (!Number.isInteger(normalizedPort) || normalizedPort < 1024 || normalizedPort > 65535) throw new Error("browser pairing grant port is invalid");
|
|
92
|
+
return hmac(token, `machine-bridge-browser-pair-v2\0${normalizedPort}\0${expiresAt}\0${nonce}`);
|
|
93
|
+
}
|
|
94
|
+
function bootstrapInitProof(secret, grantId, clientChallenge) {
|
|
95
|
+
return hmac(secret, `machine-bridge-browser-pair-init-v2\0${grantId}\0${clientChallenge}`);
|
|
96
|
+
}
|
|
97
|
+
function bootstrapProof(secret, direction, grantId, clientChallenge, serverNonce) {
|
|
98
|
+
return hmac(secret, `machine-bridge-browser-pair-${direction}-v2\0${grantId}\0${clientChallenge}\0${serverNonce}`);
|
|
99
|
+
}
|
|
100
|
+
function hmac(key, message) { return createHmac("sha256", key).update(message).digest("base64url"); }
|
|
101
|
+
function assertToken(value) { if (!TOKEN.test(String(value || ""))) throw new Error("browser broker credential is invalid"); }
|
|
102
|
+
function safeEqual(left, right) {
|
|
103
|
+
const a = Buffer.from(String(left)); const b = Buffer.from(String(right));
|
|
104
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
105
|
+
}
|
|
106
|
+
function prune(used, pending, now) {
|
|
107
|
+
for (const [id, expiresAt] of used) if (expiresAt < now) used.delete(id);
|
|
108
|
+
for (const [id, entry] of pending) if (entry.deadline.expired()) pending.delete(id);
|
|
109
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { EXPECTED_EXTENSION_ID, normalizeExtensionId } from "./browser-extension-identity.mjs";
|
|
2
|
+
import { EXPECTED_EXTENSION_VERSION } from "./browser-extension-protocol.mjs";
|
|
3
|
+
|
|
4
|
+
export function pairingHtml(port) {
|
|
5
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="machine-bridge-browser-pair" content="1"><meta name="machine-bridge-browser-port" content="${port}"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Machine Bridge browser pairing</title></head><body><h1>Machine Bridge browser pairing</h1><p>Expected extension build: <strong>${EXPECTED_EXTENSION_VERSION}</strong>.</p><p>Expected extension ID: <code>${EXPECTED_EXTENSION_ID}</code>.</p><p>Reload the unpacked extension after every Machine Bridge upgrade.</p><p>Pairing credentials are never embedded in this HTTP page. Run pair_browser_extension with opening enabled to create a short-lived extension bootstrap.</p><p id="status">Waiting for the Machine Bridge extension bootstrap.</p></body></html>`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isAllowedExtensionOrigin(origin, expectedExtensionId = EXPECTED_EXTENSION_ID) {
|
|
9
|
+
const expected = normalizeExtensionId(expectedExtensionId);
|
|
10
|
+
if (!expected) return false;
|
|
11
|
+
let parsed;
|
|
12
|
+
try { parsed = new URL(String(origin || "")); } catch { return false; }
|
|
13
|
+
return parsed.protocol === "chrome-extension:" && parsed.hostname === expected
|
|
14
|
+
&& !parsed.username && !parsed.password && !parsed.port
|
|
15
|
+
&& (parsed.pathname === "" || parsed.pathname === "/") && !parsed.search && !parsed.hash;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isAllowedLoopbackHost(host, port) {
|
|
19
|
+
const normalized = String(host || "").toLowerCase();
|
|
20
|
+
return normalized === `127.0.0.1:${port}` || normalized === `localhost:${port}` || normalized === `[::1]:${port}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function securityHeaders(contentType) {
|
|
24
|
+
return {
|
|
25
|
+
"content-type": contentType,
|
|
26
|
+
"cache-control": "no-store",
|
|
27
|
+
"content-security-policy": "default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
|
|
28
|
+
"x-content-type-options": "nosniff",
|
|
29
|
+
"referrer-policy": "no-referrer",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function sendJson(response, value) {
|
|
34
|
+
response.writeHead(200, securityHeaders("application/json; charset=utf-8"));
|
|
35
|
+
response.end(`${JSON.stringify(value)}\n`);
|
|
36
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { pairingHtml, securityHeaders } from "./browser-pairing-http.mjs";
|
|
3
|
+
import { createBrowserPairingGrant } from "./browser-pairing-grant.mjs";
|
|
4
|
+
|
|
5
|
+
const LAUNCH_TTL_MS = 30_000;
|
|
6
|
+
|
|
7
|
+
export async function startBrowserPairingLaunch({ brokerPort, extensionToken, timeoutMs = LAUNCH_TTL_MS } = {}) {
|
|
8
|
+
const targetPort = Number(brokerPort);
|
|
9
|
+
if (!Number.isInteger(targetPort) || targetPort < 1024 || targetPort > 65535) throw new Error("browser pairing broker port is invalid");
|
|
10
|
+
const grant = createBrowserPairingGrant(extensionToken, targetPort);
|
|
11
|
+
const ttl = Number(timeoutMs);
|
|
12
|
+
if (!Number.isInteger(ttl) || ttl < 1 || ttl > LAUNCH_TTL_MS) throw new Error("browser pairing launch timeout is invalid");
|
|
13
|
+
|
|
14
|
+
let listenerPort = 0;
|
|
15
|
+
let closed = false;
|
|
16
|
+
let timer;
|
|
17
|
+
let resolveClosed;
|
|
18
|
+
const closedPromise = new Promise((resolvePromise) => { resolveClosed = resolvePromise; });
|
|
19
|
+
const server = createServer((request, response) => {
|
|
20
|
+
const host = String(request.headers.host || "");
|
|
21
|
+
if (!listenerPort || host.toLowerCase() !== `127.0.0.1:${listenerPort}`) {
|
|
22
|
+
response.writeHead(403, securityHeaders("text/plain; charset=utf-8")).end("forbidden\n");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
let url;
|
|
26
|
+
try { url = new URL(request.url || "/", `http://${host}`); }
|
|
27
|
+
catch { response.writeHead(400, securityHeaders("text/plain; charset=utf-8")).end("bad request\n"); return; }
|
|
28
|
+
if (request.method !== "GET" || url.pathname !== "/pair" || url.search) {
|
|
29
|
+
response.writeHead(404, securityHeaders("text/plain; charset=utf-8")).end("not found\n");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const html = pairingHtml(targetPort);
|
|
33
|
+
response.writeHead(200, { ...securityHeaders("text/html; charset=utf-8"), "content-length": Buffer.byteLength(html) });
|
|
34
|
+
response.end(html, () => close());
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const close = () => {
|
|
38
|
+
if (closed) return;
|
|
39
|
+
closed = true;
|
|
40
|
+
if (timer) clearTimeout(timer);
|
|
41
|
+
try { server.close(() => resolveClosed()); } catch { resolveClosed(); }
|
|
42
|
+
};
|
|
43
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
44
|
+
const onError = (error) => { server.off("listening", onListening); rejectPromise(error); };
|
|
45
|
+
const onListening = () => { server.off("error", onError); resolvePromise(); };
|
|
46
|
+
server.once("error", onError);
|
|
47
|
+
server.once("listening", onListening);
|
|
48
|
+
server.listen(0, "127.0.0.1");
|
|
49
|
+
});
|
|
50
|
+
server.on("error", close);
|
|
51
|
+
const address = server.address();
|
|
52
|
+
if (!address || typeof address === "string") { close(); throw new Error("browser pairing launch listener address is unavailable"); }
|
|
53
|
+
listenerPort = address.port;
|
|
54
|
+
timer = setTimeout(close, ttl);
|
|
55
|
+
timer.unref?.();
|
|
56
|
+
const fragment = new URLSearchParams({ broker_port: String(targetPort), grant: String(grant) }).toString();
|
|
57
|
+
return { url: `http://127.0.0.1:${listenerPort}/pair#${fragment}`, close, closed: closedPromise, listenerPort, brokerPort: targetPort };
|
|
58
|
+
}
|