@apifuse/provider-sdk 2.2.0-beta.7 → 2.2.0-beta.70
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/AUTHORING.md +943 -51
- package/CHANGELOG.md +273 -1
- package/LICENSE +21 -0
- package/README.md +117 -29
- package/SUBMISSION.md +3 -2
- package/bin/apifuse-check.ts +293 -13
- package/bin/apifuse-dev.ts +42 -19
- package/bin/apifuse-migrate-deployment.ts +200 -0
- package/bin/apifuse-migrate-operation-declaration.ts +55 -0
- package/bin/apifuse-migrate-shape.ts +202 -0
- package/bin/apifuse-pack-check.ts +19 -6
- package/bin/apifuse-pack-smoke.ts +57 -2
- package/bin/apifuse-pack-types.ts +736 -38
- package/bin/apifuse-perf.ts +59 -23
- package/bin/apifuse-record.ts +923 -94
- package/bin/apifuse-submit-check.ts +2313 -281
- package/bin/apifuse-test.ts +3 -1
- package/bin/submit-check-delimited-text.ts +50 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth-turn/index.js +1 -1
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +67 -0
- package/dist/ceremonies/index.d.ts +16 -0
- package/dist/ceremonies/index.js +141 -36
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +30 -0
- package/dist/cli/create.d.ts +1 -1
- package/dist/cli/create.js +8 -2
- package/dist/cli/deployment-intent.d.ts +178 -0
- package/dist/cli/deployment-intent.js +277 -0
- package/dist/cli/local-connection.d.ts +111 -0
- package/dist/cli/local-connection.js +151 -0
- package/dist/cli/migrate-deployment-intent.d.ts +132 -0
- package/dist/cli/migrate-deployment-intent.js +1552 -0
- package/dist/cli/migrate-operation-declaration.d.ts +70 -0
- package/dist/cli/migrate-operation-declaration.js +2206 -0
- package/dist/cli/migrate-operation-shape.d.ts +44 -0
- package/dist/cli/migrate-operation-shape.js +113 -0
- package/dist/cli/migrate-provider-shape.d.ts +56 -0
- package/dist/cli/migrate-provider-shape.js +580 -0
- package/dist/cli/resolver-runtime.d.ts +21 -0
- package/dist/cli/resolver-runtime.js +31 -0
- package/dist/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +141 -23
- package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
- package/dist/cli/templates/provider/README.md.tpl +41 -19
- package/dist/cli/templates/provider/index.ts.tpl +6 -3
- package/dist/cli/templates/provider/operations/ping.ts.tpl +4 -1
- package/dist/cli/templates/provider/provider.json.tpl +6 -0
- package/dist/config/loader.d.ts +202 -16
- package/dist/config/loader.js +491 -141
- package/dist/contract-serialization.js +4 -8
- package/dist/contract-types.d.ts +12 -5
- package/dist/contract.js +24 -10
- package/dist/declaration-validation.d.ts +34 -0
- package/dist/declaration-validation.js +305 -0
- package/dist/define.d.ts +71 -29
- package/dist/define.js +807 -69
- package/dist/dev.d.ts +5 -0
- package/dist/dev.js +8 -1
- package/dist/engine.d.ts +129 -0
- package/dist/engine.js +209 -0
- package/dist/error-observability.d.ts +7 -0
- package/dist/error-observability.js +61 -0
- package/dist/error-resolution.d.ts +18 -0
- package/dist/error-resolution.js +243 -0
- package/dist/errors.d.ts +74 -0
- package/dist/errors.js +99 -0
- package/dist/fixture-sanitization.d.ts +28 -0
- package/dist/fixture-sanitization.js +371 -0
- package/dist/handle-meta.d.ts +90 -0
- package/dist/handle-meta.js +73 -0
- package/dist/handle.d.ts +226 -0
- package/dist/handle.js +345 -0
- package/dist/health-scenario.d.ts +1901 -0
- package/dist/health-scenario.js +653 -0
- package/dist/i18n/catalog.js +21 -1
- package/dist/i18n/error-messages.d.ts +86 -0
- package/dist/i18n/error-messages.js +192 -0
- package/dist/i18n/index.d.ts +1 -0
- package/dist/i18n/index.js +1 -0
- package/dist/i18n/keys.d.ts +12 -0
- package/dist/i18n/keys.js +76 -5
- package/dist/i18n/operation-locale-namespace.d.ts +2 -0
- package/dist/i18n/operation-locale-namespace.js +7 -0
- package/dist/index.d.ts +40 -15
- package/dist/index.js +29 -11
- package/dist/lint.d.ts +43 -11
- package/dist/lint.js +2512 -177
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +31 -0
- package/dist/native-egress-policy.js +288 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/provider.d.ts +18 -8
- package/dist/provider.js +11 -6
- package/dist/runtime/auth-flow.d.ts +6 -1
- package/dist/runtime/auth-flow.js +8 -0
- package/dist/runtime/bounded-expiring-map.d.ts +21 -0
- package/dist/runtime/bounded-expiring-map.js +64 -0
- package/dist/runtime/browser-telemetry-binding.d.ts +5 -0
- package/dist/runtime/browser-telemetry-binding.js +83 -0
- package/dist/runtime/browser-telemetry.d.ts +76 -0
- package/dist/runtime/browser-telemetry.js +165 -0
- package/dist/runtime/browser.d.ts +4 -0
- package/dist/runtime/browser.js +688 -83
- package/dist/runtime/cache-telemetry.d.ts +71 -0
- package/dist/runtime/cache-telemetry.js +177 -0
- package/dist/runtime/cache.d.ts +3 -0
- package/dist/runtime/cache.js +245 -23
- package/dist/runtime/capability-response.d.ts +3 -0
- package/dist/runtime/capability-response.js +56 -0
- package/dist/runtime/capability-telemetry.d.ts +46 -0
- package/dist/runtime/capability-telemetry.js +68 -0
- package/dist/runtime/chrome149-header-order.d.ts +58 -0
- package/dist/runtime/chrome149-header-order.js +289 -0
- package/dist/runtime/diagnostic-env-values.d.ts +5 -0
- package/dist/runtime/diagnostic-env-values.js +57 -0
- package/dist/runtime/diagnostic-env.d.ts +14 -0
- package/dist/runtime/diagnostic-env.js +44 -0
- package/dist/runtime/diagnostic-redactor.d.ts +54 -0
- package/dist/runtime/diagnostic-redactor.js +571 -0
- package/dist/runtime/egress-lease.d.ts +65 -0
- package/dist/runtime/egress-lease.js +236 -0
- package/dist/runtime/engine-mode.d.ts +74 -0
- package/dist/runtime/engine-mode.js +152 -0
- package/dist/runtime/env.js +14 -1
- package/dist/runtime/executor.d.ts +3 -2
- package/dist/runtime/executor.js +23 -26
- package/dist/runtime/handle-wordlist.d.ts +43 -0
- package/dist/runtime/handle-wordlist.js +256 -0
- package/dist/runtime/handle.d.ts +71 -0
- package/dist/runtime/handle.js +908 -0
- package/dist/runtime/http-telemetry-binding.d.ts +10 -0
- package/dist/runtime/http-telemetry-binding.js +152 -0
- package/dist/runtime/http-telemetry-guard.d.ts +16 -0
- package/dist/runtime/http-telemetry-guard.js +202 -0
- package/dist/runtime/http-telemetry.d.ts +104 -0
- package/dist/runtime/http-telemetry.js +190 -0
- package/dist/runtime/http.d.ts +34 -0
- package/dist/runtime/http.js +680 -121
- package/dist/runtime/insights.js +2 -2
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +462 -19
- package/dist/runtime/native-network-errors.d.ts +33 -0
- package/dist/runtime/native-network-errors.js +69 -0
- package/dist/runtime/native-network.d.ts +105 -0
- package/dist/runtime/native-network.js +1433 -0
- package/dist/runtime/native-telemetry.d.ts +142 -0
- package/dist/runtime/native-telemetry.js +231 -0
- package/dist/runtime/ocr-telemetry.d.ts +82 -0
- package/dist/runtime/ocr-telemetry.js +192 -0
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +468 -0
- package/dist/runtime/otlp.d.ts +71 -2
- package/dist/runtime/otlp.js +398 -16
- package/dist/runtime/provider.d.ts +7 -0
- package/dist/runtime/provider.js +14 -0
- package/dist/runtime/proxy-errors.js +6 -2
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +147 -0
- package/dist/runtime/proxy-retry-policy.d.ts +4 -1
- package/dist/runtime/proxy-retry-policy.js +11 -2
- package/dist/runtime/proxy-telemetry.d.ts +133 -3
- package/dist/runtime/proxy-telemetry.js +218 -70
- package/dist/runtime/redirect-hop-headers.d.ts +19 -0
- package/dist/runtime/redirect-hop-headers.js +58 -0
- package/dist/runtime/redirects.d.ts +61 -0
- package/dist/runtime/redirects.js +149 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +4 -2
- package/dist/runtime/request-options.d.ts +75 -1
- package/dist/runtime/request-options.js +588 -0
- package/dist/runtime/request-telemetry.d.ts +70 -0
- package/dist/runtime/request-telemetry.js +229 -0
- package/dist/runtime/resolver-config.d.ts +13 -0
- package/dist/runtime/resolver-config.js +14 -0
- package/dist/runtime/resolver-public.d.ts +3 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-runtime-options.d.ts +14 -0
- package/dist/runtime/resolver-runtime-options.js +33 -0
- package/dist/runtime/resolver-shared.d.ts +24 -0
- package/dist/runtime/resolver-shared.js +69 -0
- package/dist/runtime/resolver-span-attributes.d.ts +34 -0
- package/dist/runtime/resolver-span-attributes.js +93 -0
- package/dist/runtime/resolver-telemetry.d.ts +135 -0
- package/dist/runtime/resolver-telemetry.js +204 -0
- package/dist/runtime/resolver-usage.d.ts +32 -0
- package/dist/runtime/resolver-usage.js +54 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +53 -0
- package/dist/runtime/resolver-vendors/bindings.js +45 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +24 -0
- package/dist/runtime/resolver-vendors/browser.js +423 -0
- package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
- package/dist/runtime/resolver-vendors/capsolver.js +544 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +23 -0
- package/dist/runtime/resolver-vendors/hosts.js +68 -0
- package/dist/runtime/resolver-vendors/hypersolutions.d.ts +27 -0
- package/dist/runtime/resolver-vendors/hypersolutions.js +369 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +425 -0
- package/dist/runtime/resolver-vendors/types.d.ts +125 -0
- package/dist/runtime/resolver-vendors/types.js +106 -0
- package/dist/runtime/resolver.d.ts +79 -0
- package/dist/runtime/resolver.js +1094 -0
- package/dist/runtime/state-telemetry.d.ts +79 -0
- package/dist/runtime/state-telemetry.js +194 -0
- package/dist/runtime/state.d.ts +10 -1
- package/dist/runtime/state.js +322 -82
- package/dist/runtime/stealth-akamai-sbsd.d.ts +54 -0
- package/dist/runtime/stealth-akamai-sbsd.js +142 -0
- package/dist/runtime/stealth-cookies.d.ts +24 -0
- package/dist/runtime/stealth-cookies.js +131 -0
- package/dist/runtime/stealth-owned-headers.d.ts +39 -0
- package/dist/runtime/stealth-owned-headers.js +57 -0
- package/dist/runtime/stealth-telemetry.d.ts +92 -0
- package/dist/runtime/stealth-telemetry.js +179 -0
- package/dist/runtime/stealth.d.ts +45 -6
- package/dist/runtime/stealth.js +1775 -320
- package/dist/runtime/stt-telemetry.d.ts +77 -0
- package/dist/runtime/stt-telemetry.js +176 -0
- package/dist/runtime/stt.js +18 -19
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- package/dist/runtime/trace-config.d.ts +12 -0
- package/dist/runtime/trace-config.js +62 -0
- package/dist/runtime/trace.d.ts +17 -0
- package/dist/runtime/trace.js +176 -25
- package/dist/runtime-boundary-lint.d.ts +137 -0
- package/dist/runtime-boundary-lint.js +590 -0
- package/dist/schema.d.ts +20 -0
- package/dist/schema.js +72 -2
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/error-observability.d.ts +1 -0
- package/dist/server/error-observability.js +1 -0
- package/dist/server/error-stack-frames.d.ts +35 -0
- package/dist/server/error-stack-frames.js +219 -0
- package/dist/server/index.d.ts +21 -2
- package/dist/server/index.js +11 -2
- package/dist/server/runtime-diagnostics.d.ts +165 -0
- package/dist/server/runtime-diagnostics.js +360 -0
- package/dist/server/sdk-version.d.ts +3 -0
- package/dist/server/sdk-version.js +19 -0
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/server/self-test-redaction.js +8 -33
- package/dist/server/self-test.d.ts +18 -3
- package/dist/server/self-test.js +139 -76
- package/dist/server/sensitive-values.d.ts +26 -0
- package/dist/server/sensitive-values.js +137 -0
- package/dist/server/serve-implementation.d.ts +285 -0
- package/dist/server/serve-implementation.js +3172 -0
- package/dist/server/serve.d.ts +1 -75
- package/dist/server/serve.js +1 -1170
- package/dist/server/state-backend-report.d.ts +70 -0
- package/dist/server/state-backend-report.js +113 -0
- package/dist/server/trace-output.d.ts +7 -0
- package/dist/server/trace-output.js +111 -0
- package/dist/server/types.d.ts +37 -5
- package/dist/server/types.js +34 -1
- package/dist/stateful/errors.d.ts +19 -0
- package/dist/stateful/errors.js +24 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +237 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +35 -0
- package/dist/stateful/stateful-provider-runtime-context.js +61 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
- package/dist/stateful/stateful-provider-session-routing.js +345 -0
- package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
- package/dist/stateful/stateful-provider-session-runtime.js +245 -0
- package/dist/stateful-signing.d.ts +18 -0
- package/dist/stateful-signing.js +27 -0
- package/dist/stealth/profiles.d.ts +15 -4
- package/dist/stealth/profiles.js +169 -211
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.js +7 -1
- package/dist/testing/index.d.ts +2 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +509 -25
- package/dist/trace-sanitization.d.ts +6 -0
- package/dist/trace-sanitization.js +68 -0
- package/dist/types.d.ts +899 -210
- package/dist/types.js +1 -0
- package/dist/typescript-module.d.ts +39 -0
- package/dist/typescript-module.js +72 -0
- package/package.json +52 -6
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +118 -0
- package/src/ceremonies/index.ts +189 -46
- package/src/cli/commands.ts +37 -0
- package/src/cli/create.ts +8 -2
- package/src/cli/deployment-intent.ts +364 -0
- package/src/cli/local-connection.ts +221 -0
- package/src/cli/migrate-deployment-intent.ts +1908 -0
- package/src/cli/migrate-operation-declaration.ts +3026 -0
- package/src/cli/migrate-operation-shape.ts +184 -0
- package/src/cli/migrate-provider-shape.ts +778 -0
- package/src/cli/resolver-runtime.ts +72 -0
- package/src/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +141 -23
- package/src/cli/templates/provider/Dockerfile.tpl +1 -1
- package/src/cli/templates/provider/README.md.tpl +41 -19
- package/src/cli/templates/provider/index.ts.tpl +6 -3
- package/src/cli/templates/provider/operations/ping.ts.tpl +4 -1
- package/src/cli/templates/provider/provider.json.tpl +6 -0
- package/src/config/loader.ts +774 -181
- package/src/contract-serialization.ts +5 -7
- package/src/contract-types.ts +12 -5
- package/src/contract.ts +24 -10
- package/src/declaration-validation.ts +376 -0
- package/src/define.ts +1105 -131
- package/src/dev.ts +13 -1
- package/src/engine.ts +390 -0
- package/src/error-observability.ts +64 -0
- package/src/error-resolution.ts +269 -0
- package/src/errors.ts +157 -0
- package/src/fixture-sanitization.ts +422 -0
- package/src/handle-meta.ts +143 -0
- package/src/handle.ts +694 -0
- package/src/health-scenario.ts +960 -0
- package/src/i18n/catalog.ts +23 -1
- package/src/i18n/error-messages.ts +255 -0
- package/src/i18n/index.ts +1 -0
- package/src/i18n/keys.ts +80 -6
- package/src/i18n/operation-locale-namespace.ts +10 -0
- package/src/index.ts +390 -23
- package/src/lint.ts +2975 -234
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +358 -0
- package/src/observability.ts +51 -1
- package/src/provider.ts +207 -27
- package/src/runtime/auth-flow.ts +15 -0
- package/src/runtime/bounded-expiring-map.ts +67 -0
- package/src/runtime/browser-telemetry-binding.ts +92 -0
- package/src/runtime/browser-telemetry.ts +289 -0
- package/src/runtime/browser.ts +882 -99
- package/src/runtime/cache-telemetry.ts +247 -0
- package/src/runtime/cache.ts +258 -22
- package/src/runtime/capability-response.ts +53 -0
- package/src/runtime/capability-telemetry.ts +101 -0
- package/src/runtime/chrome149-header-order.ts +330 -0
- package/src/runtime/diagnostic-env-values.ts +56 -0
- package/src/runtime/diagnostic-env.ts +65 -0
- package/src/runtime/diagnostic-redactor.ts +668 -0
- package/src/runtime/egress-lease.ts +341 -0
- package/src/runtime/engine-mode.ts +233 -0
- package/src/runtime/env.ts +15 -1
- package/src/runtime/executor.ts +39 -32
- package/src/runtime/handle-wordlist.ts +266 -0
- package/src/runtime/handle.ts +1180 -0
- package/src/runtime/http-telemetry-binding.ts +162 -0
- package/src/runtime/http-telemetry-guard.ts +208 -0
- package/src/runtime/http-telemetry.ts +292 -0
- package/src/runtime/http.ts +896 -150
- package/src/runtime/insights.ts +2 -2
- package/src/runtime/instrumentation.ts +628 -28
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +1853 -0
- package/src/runtime/native-telemetry.ts +366 -0
- package/src/runtime/ocr-telemetry.ts +308 -0
- package/src/runtime/ocr.ts +564 -0
- package/src/runtime/otlp.ts +468 -21
- package/src/runtime/provider.ts +15 -0
- package/src/runtime/proxy-errors.ts +12 -4
- package/src/runtime/proxy-nodemaven.ts +222 -0
- package/src/runtime/proxy-retry-policy.ts +12 -1
- package/src/runtime/proxy-telemetry.ts +358 -103
- package/src/runtime/redirect-hop-headers.ts +71 -0
- package/src/runtime/redirects.ts +215 -0
- package/src/runtime/redis.ts +7 -2
- package/src/runtime/request-options.ts +728 -1
- package/src/runtime/request-telemetry.ts +375 -0
- package/src/runtime/resolver-config.ts +18 -0
- package/src/runtime/resolver-public.ts +44 -0
- package/src/runtime/resolver-runtime-options.ts +50 -0
- package/src/runtime/resolver-shared.ts +113 -0
- package/src/runtime/resolver-span-attributes.ts +139 -0
- package/src/runtime/resolver-telemetry.ts +362 -0
- package/src/runtime/resolver-usage.ts +99 -0
- package/src/runtime/resolver-vendors/bindings.ts +61 -0
- package/src/runtime/resolver-vendors/browser.ts +591 -0
- package/src/runtime/resolver-vendors/capsolver.ts +727 -0
- package/src/runtime/resolver-vendors/hosts.ts +81 -0
- package/src/runtime/resolver-vendors/hypersolutions.ts +480 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +555 -0
- package/src/runtime/resolver-vendors/types.ts +261 -0
- package/src/runtime/resolver.ts +1543 -0
- package/src/runtime/state-telemetry.ts +298 -0
- package/src/runtime/state.ts +435 -89
- package/src/runtime/stealth-akamai-sbsd.ts +207 -0
- package/src/runtime/stealth-cookies.ts +155 -0
- package/src/runtime/stealth-owned-headers.ts +67 -0
- package/src/runtime/stealth-telemetry.ts +309 -0
- package/src/runtime/stealth.ts +2275 -372
- package/src/runtime/stt-telemetry.ts +282 -0
- package/src/runtime/stt.ts +96 -91
- package/src/runtime/timeout.ts +18 -0
- package/src/runtime/trace-config.ts +78 -0
- package/src/runtime/trace.ts +257 -35
- package/src/runtime-boundary-lint.ts +707 -0
- package/src/schema.ts +80 -2
- package/src/serve.ts +6 -1
- package/src/server/error-observability.ts +1 -0
- package/src/server/error-stack-frames.ts +214 -0
- package/src/server/index.ts +184 -2
- package/src/server/runtime-diagnostics.ts +522 -0
- package/src/server/sdk-version.ts +22 -0
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test-redaction.ts +10 -40
- package/src/server/self-test.ts +205 -89
- package/src/server/sensitive-values.ts +211 -0
- package/src/server/serve-implementation.ts +4596 -0
- package/src/server/serve.ts +1 -1685
- package/src/server/state-backend-report.ts +185 -0
- package/src/server/trace-output.ts +189 -0
- package/src/server/types.ts +34 -1
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +35 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +310 -0
- package/src/stateful/stateful-provider-runtime-context.ts +96 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +546 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +212 -231
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -1
- package/src/testing/index.ts +10 -1
- package/src/testing/run.ts +685 -19
- package/src/trace-sanitization.ts +110 -0
- package/src/types.ts +1014 -273
- package/src/typescript-module.ts +100 -0
- package/dist/choice-token.d.ts +0 -24
- package/dist/choice-token.js +0 -74
- package/dist/runtime/choice.d.ts +0 -15
- package/dist/runtime/choice.js +0 -413
- package/src/choice-token.ts +0 -165
- package/src/runtime/choice.ts +0 -686
package/src/runtime/browser.ts
CHANGED
|
@@ -1,11 +1,42 @@
|
|
|
1
|
+
import { readDiagnosticEnv, registerDiagnosticValue } from "./diagnostic-env.js";
|
|
2
|
+
import { AsyncResource } from "node:async_hooks";
|
|
3
|
+
import type {
|
|
4
|
+
BrowserTelemetryPoolAcquireOutcome,
|
|
5
|
+
BrowserTelemetrySink,
|
|
6
|
+
} from "./browser-telemetry.js";
|
|
7
|
+
import { observeTelemetryCallback as observeBrowserTelemetry } from "./http-telemetry-guard.js";
|
|
8
|
+
import { cookieValuesFromHeaders } from "./stealth-cookies.js";
|
|
9
|
+
|
|
10
|
+
function browserTelemetryErrorCode(
|
|
11
|
+
error: unknown,
|
|
12
|
+
):
|
|
13
|
+
| "BROWSER_CDP_POOL_REQUIRED"
|
|
14
|
+
| "BROWSER_PROXY_INVALID"
|
|
15
|
+
| "BROWSER_RUNTIME_UNSUPPORTED"
|
|
16
|
+
| "BROWSER_CDP_POOL_ERROR"
|
|
17
|
+
| "other" {
|
|
18
|
+
const code =
|
|
19
|
+
error && typeof error === "object" && "code" in error
|
|
20
|
+
? (error as { code?: unknown }).code
|
|
21
|
+
: undefined;
|
|
22
|
+
if (
|
|
23
|
+
code === "BROWSER_CDP_POOL_REQUIRED" ||
|
|
24
|
+
code === "BROWSER_PROXY_INVALID" ||
|
|
25
|
+
code === "BROWSER_RUNTIME_UNSUPPORTED"
|
|
26
|
+
)
|
|
27
|
+
return code;
|
|
28
|
+
if (typeof code === "number") return "BROWSER_CDP_POOL_ERROR";
|
|
29
|
+
return "other";
|
|
30
|
+
}
|
|
1
31
|
import { createRequire } from "node:module";
|
|
2
|
-
import type { Frame, LaunchOptions, Locator, Page, Request, Route } from "playwright";
|
|
32
|
+
import type { CDPSession, Frame, LaunchOptions, Locator, Page, Request, Route } from "playwright";
|
|
3
33
|
|
|
4
34
|
import { ProviderError } from "../errors.js";
|
|
5
35
|
import type {
|
|
6
36
|
BrowserChallengeRequest,
|
|
7
37
|
BrowserChallengeResult,
|
|
8
38
|
BrowserClient as BrowserClientContract,
|
|
39
|
+
BrowserCookie,
|
|
9
40
|
BrowserEngine,
|
|
10
41
|
BrowserFrame,
|
|
11
42
|
BrowserLocator,
|
|
@@ -42,19 +73,35 @@ type PoolReleaseRequest = {
|
|
|
42
73
|
pageId: string;
|
|
43
74
|
};
|
|
44
75
|
|
|
45
|
-
type
|
|
76
|
+
type WebSocketCommandId = number;
|
|
46
77
|
|
|
47
|
-
type
|
|
78
|
+
type WebSocketCommandError = {
|
|
48
79
|
code?: number;
|
|
49
80
|
message?: string;
|
|
50
81
|
};
|
|
51
82
|
|
|
52
|
-
type
|
|
53
|
-
error?:
|
|
54
|
-
id?:
|
|
83
|
+
type WebSocketCommandMessage = {
|
|
84
|
+
error?: WebSocketCommandError;
|
|
85
|
+
id?: WebSocketCommandId;
|
|
86
|
+
jsonrpc?: "2.0";
|
|
55
87
|
method?: string;
|
|
56
88
|
params?: Record<string, unknown>;
|
|
57
89
|
result?: Record<string, unknown>;
|
|
90
|
+
sessionId?: string;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
type CdpCommandFrame = {
|
|
94
|
+
id: WebSocketCommandId;
|
|
95
|
+
method: string;
|
|
96
|
+
params: Record<string, unknown>;
|
|
97
|
+
sessionId?: string;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
type JsonRpcCommandFrame = {
|
|
101
|
+
id: WebSocketCommandId;
|
|
102
|
+
jsonrpc: "2.0";
|
|
103
|
+
method: string;
|
|
104
|
+
params: Record<string, unknown>;
|
|
58
105
|
};
|
|
59
106
|
|
|
60
107
|
type CdpFrameTreeNode = {
|
|
@@ -70,10 +117,25 @@ type CdpFrameTreeNode = {
|
|
|
70
117
|
type CdpFetchFulfillParams = {
|
|
71
118
|
readonly requestId: string;
|
|
72
119
|
readonly responseCode: number;
|
|
73
|
-
readonly responseHeaders?:
|
|
120
|
+
readonly responseHeaders?: {
|
|
121
|
+
readonly name: string;
|
|
122
|
+
readonly value: string;
|
|
123
|
+
}[];
|
|
124
|
+
readonly body?: string;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
type CdpPausedDocumentResponse = {
|
|
128
|
+
readonly hasBody: boolean;
|
|
129
|
+
readonly requestId: string;
|
|
130
|
+
readonly responseCode: number;
|
|
131
|
+
readonly responseHeaders: readonly {
|
|
74
132
|
readonly name: string;
|
|
75
133
|
readonly value: string;
|
|
76
134
|
}[];
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
type CdpResponseBody = {
|
|
138
|
+
readonly base64Encoded?: boolean;
|
|
77
139
|
readonly body?: string;
|
|
78
140
|
};
|
|
79
141
|
|
|
@@ -92,7 +154,7 @@ function toResourceBody(body: BrowserResourceBody | undefined): Buffer | string
|
|
|
92
154
|
}
|
|
93
155
|
|
|
94
156
|
function isResourceMethod(method: string): method is BrowserResourceMethod {
|
|
95
|
-
return method === "GET" || method === "HEAD";
|
|
157
|
+
return method === "GET" || method === "HEAD" || method === "POST";
|
|
96
158
|
}
|
|
97
159
|
|
|
98
160
|
async function toResourceRequest(request: Request): Promise<BrowserResourceRequest | null> {
|
|
@@ -147,6 +209,85 @@ function getCdpPausedRequestId(params: unknown): string | null {
|
|
|
147
209
|
return params.requestId;
|
|
148
210
|
}
|
|
149
211
|
|
|
212
|
+
function isCdpResponseStage(params: unknown): boolean {
|
|
213
|
+
return (
|
|
214
|
+
isRecord(params) &&
|
|
215
|
+
(typeof params.responseStatusCode === "number" ||
|
|
216
|
+
typeof params.responseErrorReason === "string")
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function toCdpPausedDocumentResponse(params: unknown): CdpPausedDocumentResponse | null {
|
|
221
|
+
if (
|
|
222
|
+
!isRecord(params) ||
|
|
223
|
+
typeof params.requestId !== "string" ||
|
|
224
|
+
typeof params.responseStatusCode !== "number" ||
|
|
225
|
+
params.resourceType !== "Document" ||
|
|
226
|
+
!isRecord(params.request)
|
|
227
|
+
) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const responseCode = params.responseStatusCode;
|
|
232
|
+
const responseHeaders = Array.isArray(params.responseHeaders)
|
|
233
|
+
? params.responseHeaders.flatMap((header) => {
|
|
234
|
+
if (
|
|
235
|
+
!isRecord(header) ||
|
|
236
|
+
typeof header.name !== "string" ||
|
|
237
|
+
typeof header.value !== "string"
|
|
238
|
+
) {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
return [{ name: header.name, value: header.value }];
|
|
242
|
+
})
|
|
243
|
+
: [];
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
hasBody:
|
|
247
|
+
String(params.request.method ?? "").toUpperCase() !== "HEAD" &&
|
|
248
|
+
responseCode >= 200 &&
|
|
249
|
+
(responseCode < 300 || responseCode >= 400) &&
|
|
250
|
+
responseCode !== 204 &&
|
|
251
|
+
responseCode !== 205 &&
|
|
252
|
+
responseCode !== 304,
|
|
253
|
+
requestId: params.requestId,
|
|
254
|
+
responseCode,
|
|
255
|
+
responseHeaders,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function toCdpDocumentFulfillParams(
|
|
260
|
+
response: CdpPausedDocumentResponse,
|
|
261
|
+
body: CdpResponseBody,
|
|
262
|
+
contentSecurityPolicy: string,
|
|
263
|
+
): CdpFetchFulfillParams {
|
|
264
|
+
const responseBody = typeof body.body === "string" ? body.body : "";
|
|
265
|
+
return {
|
|
266
|
+
body: body.base64Encoded ? responseBody : Buffer.from(responseBody).toString("base64"),
|
|
267
|
+
requestId: response.requestId,
|
|
268
|
+
responseCode: response.responseCode,
|
|
269
|
+
responseHeaders: [
|
|
270
|
+
...response.responseHeaders,
|
|
271
|
+
{ name: "Content-Security-Policy", value: contentSecurityPolicy },
|
|
272
|
+
],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function resourcePolicyFetchPatterns(policy: BrowserResourcePolicy) {
|
|
277
|
+
return [
|
|
278
|
+
{ requestStage: "Request" as const, urlPattern: "*" },
|
|
279
|
+
...(policy.documentContentSecurityPolicy
|
|
280
|
+
? [
|
|
281
|
+
{
|
|
282
|
+
requestStage: "Response" as const,
|
|
283
|
+
resourceType: "Document" as const,
|
|
284
|
+
urlPattern: "*",
|
|
285
|
+
},
|
|
286
|
+
]
|
|
287
|
+
: []),
|
|
288
|
+
];
|
|
289
|
+
}
|
|
290
|
+
|
|
150
291
|
function toCdpResourceHeaders(value: unknown): Record<string, string> {
|
|
151
292
|
if (!isRecord(value)) {
|
|
152
293
|
return {};
|
|
@@ -208,11 +349,172 @@ async function fulfillResourceRoute(
|
|
|
208
349
|
});
|
|
209
350
|
}
|
|
210
351
|
|
|
352
|
+
async function decideResourceRequest(
|
|
353
|
+
policy: BrowserResourcePolicy,
|
|
354
|
+
request: BrowserResourceRequest,
|
|
355
|
+
): Promise<BrowserResourceDecision> {
|
|
356
|
+
for (const resourceRoute of policy.routes) {
|
|
357
|
+
if (matchesResourceRoute(resourceRoute.match, request)) {
|
|
358
|
+
return await resourceRoute.handle(request);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return { action: "block" };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function abortResourceRoute(route: Route): Promise<void> {
|
|
366
|
+
await route.abort("blockedbyclient");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function handleCdpResourceRequest(
|
|
370
|
+
session: CDPSession,
|
|
371
|
+
policy: BrowserResourcePolicy,
|
|
372
|
+
allowedMethods: ReadonlySet<BrowserResourceMethod>,
|
|
373
|
+
params: unknown,
|
|
374
|
+
): Promise<void> {
|
|
375
|
+
const paused = toCdpResourceRequest(params);
|
|
376
|
+
const requestId = paused?.requestId ?? getCdpPausedRequestId(params);
|
|
377
|
+
if (!requestId) return;
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
if (!paused || !allowedMethods.has(paused.request.method)) {
|
|
381
|
+
await session.send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId });
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const decision = await decideResourceRequest(policy, paused.request);
|
|
386
|
+
switch (decision.action) {
|
|
387
|
+
case "continue":
|
|
388
|
+
await session.send("Fetch.continueRequest", { requestId });
|
|
389
|
+
return;
|
|
390
|
+
case "fulfill":
|
|
391
|
+
await session.send("Fetch.fulfillRequest", toCdpFulfillParams(requestId, decision));
|
|
392
|
+
return;
|
|
393
|
+
case "block":
|
|
394
|
+
await session.send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
} catch {
|
|
398
|
+
await session
|
|
399
|
+
.send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId })
|
|
400
|
+
.catch(() => undefined);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function handleCdpDocumentResponse(
|
|
405
|
+
session: CDPSession,
|
|
406
|
+
contentSecurityPolicy: string,
|
|
407
|
+
params: unknown,
|
|
408
|
+
): Promise<void> {
|
|
409
|
+
const requestId = getCdpPausedRequestId(params);
|
|
410
|
+
if (!requestId) return;
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
const response = toCdpPausedDocumentResponse(params);
|
|
414
|
+
if (!response) throw new Error("CDP document response metadata is unavailable");
|
|
415
|
+
if (!response.hasBody) {
|
|
416
|
+
await session.send("Fetch.continueResponse", {
|
|
417
|
+
requestId,
|
|
418
|
+
responseCode: response.responseCode,
|
|
419
|
+
responseHeaders: [...response.responseHeaders],
|
|
420
|
+
});
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const body = await session.send("Fetch.getResponseBody", {
|
|
425
|
+
requestId: response.requestId,
|
|
426
|
+
});
|
|
427
|
+
await session.send(
|
|
428
|
+
"Fetch.fulfillRequest",
|
|
429
|
+
toCdpDocumentFulfillParams(response, body, contentSecurityPolicy),
|
|
430
|
+
);
|
|
431
|
+
} catch {
|
|
432
|
+
await session
|
|
433
|
+
.send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId })
|
|
434
|
+
.catch(() => undefined);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function getCdpAuthRequiredRequestId(params: unknown): string | null {
|
|
439
|
+
if (!isRecord(params) || typeof params.requestId !== "string") {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return params.requestId;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function isCdpProxyAuthChallenge(params: unknown): boolean {
|
|
447
|
+
return (
|
|
448
|
+
isRecord(params) && isRecord(params.authChallenge) && params.authChallenge.source === "Proxy"
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function handleCdpAuthRequired(
|
|
453
|
+
session: CDPSession,
|
|
454
|
+
proxy: PlaywrightProxy & { username: string; password: string },
|
|
455
|
+
authAttempts: Set<string>,
|
|
456
|
+
params: unknown,
|
|
457
|
+
telemetry?: BrowserTelemetrySink,
|
|
458
|
+
): Promise<void> {
|
|
459
|
+
const requestId = getCdpAuthRequiredRequestId(params);
|
|
460
|
+
if (!requestId) return;
|
|
461
|
+
|
|
462
|
+
// Chromium can emit another challenge after rejected credentials. Cancel a
|
|
463
|
+
// retry so incorrect credentials fail promptly instead of looping forever.
|
|
464
|
+
const response =
|
|
465
|
+
isCdpProxyAuthChallenge(params) && !authAttempts.has(requestId)
|
|
466
|
+
? (() => {
|
|
467
|
+
authAttempts.add(requestId);
|
|
468
|
+
observeBrowserTelemetry(telemetry, () => telemetry?.recordProxyAuthChallenge());
|
|
469
|
+
return {
|
|
470
|
+
authChallengeResponse: {
|
|
471
|
+
password: proxy.password,
|
|
472
|
+
response: "ProvideCredentials" as const,
|
|
473
|
+
username: proxy.username,
|
|
474
|
+
},
|
|
475
|
+
requestId,
|
|
476
|
+
};
|
|
477
|
+
})()
|
|
478
|
+
: {
|
|
479
|
+
authChallengeResponse: { response: "CancelAuth" as const },
|
|
480
|
+
requestId,
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
await session.send("Fetch.continueWithAuth", response).catch(() => undefined);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function createResourcePolicyHandler(
|
|
487
|
+
policy: BrowserResourcePolicy,
|
|
488
|
+
allowedMethods: ReadonlySet<BrowserResourceMethod>,
|
|
489
|
+
): (route: Route) => Promise<void> {
|
|
490
|
+
return async (route: Route): Promise<void> => {
|
|
491
|
+
const request = await toResourceRequest(route.request());
|
|
492
|
+
if (!request || !allowedMethods.has(request.method)) {
|
|
493
|
+
await abortResourceRoute(route);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const decision = await decideResourceRequest(policy, request);
|
|
498
|
+
switch (decision.action) {
|
|
499
|
+
case "continue":
|
|
500
|
+
await route.continue();
|
|
501
|
+
return;
|
|
502
|
+
case "fulfill":
|
|
503
|
+
await fulfillResourceRoute(route, decision);
|
|
504
|
+
return;
|
|
505
|
+
case "block":
|
|
506
|
+
await abortResourceRoute(route);
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
211
511
|
export type BrowserClientOptions = BrowserOptions & {
|
|
212
512
|
allowedHosts?: string[];
|
|
213
513
|
cdpUrl?: string;
|
|
214
514
|
executablePath?: string;
|
|
215
515
|
extraArgs?: string[];
|
|
516
|
+
serviceWorkers?: "allow" | "block";
|
|
517
|
+
telemetry?: BrowserTelemetrySink;
|
|
216
518
|
};
|
|
217
519
|
|
|
218
520
|
type SupportedBrowserClient = {
|
|
@@ -224,7 +526,7 @@ type SupportedBrowserClient = {
|
|
|
224
526
|
};
|
|
225
527
|
|
|
226
528
|
function getDefaultCdpPoolUrl(env = process.env): string | undefined {
|
|
227
|
-
return env
|
|
529
|
+
return readDiagnosticEnv("APIFUSE__CDP_POOL__URL", env);
|
|
228
530
|
}
|
|
229
531
|
|
|
230
532
|
async function importOptionalModule<T extends object>(moduleName: string): Promise<T> {
|
|
@@ -271,12 +573,72 @@ function formatExpression<T>(fn: string | (() => T)): string {
|
|
|
271
573
|
return `(${fn.toString()})()`;
|
|
272
574
|
}
|
|
273
575
|
|
|
274
|
-
function toLaunchOptions(
|
|
576
|
+
function toLaunchOptions(
|
|
577
|
+
options: BrowserClientOptions,
|
|
578
|
+
proxy: LaunchOptions["proxy"],
|
|
579
|
+
): LaunchOptions {
|
|
275
580
|
return {
|
|
276
|
-
|
|
581
|
+
// `extraArgs` is optional, but playwright-extra's stealth evasions mutate
|
|
582
|
+
// `options.args` unguarded (navigator.webdriver does
|
|
583
|
+
// `options.args.findIndex(...)` in beforeLaunch). Forwarding `undefined`
|
|
584
|
+
// therefore crashes every stealth launch that omits extraArgs with
|
|
585
|
+
// "TypeError: undefined is not an object (evaluating 'options.args.findIndex')".
|
|
586
|
+
// Always hand the launcher a concrete array.
|
|
587
|
+
args: options.extraArgs ?? [],
|
|
277
588
|
executablePath: options.executablePath,
|
|
278
589
|
headless: options.headless ?? true,
|
|
279
|
-
proxy
|
|
590
|
+
proxy,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
type PlaywrightProxy = NonNullable<LaunchOptions["proxy"]>;
|
|
595
|
+
|
|
596
|
+
function hasProxyCredentials(
|
|
597
|
+
proxy: LaunchOptions["proxy"] | undefined,
|
|
598
|
+
): proxy is PlaywrightProxy & { username: string; password: string } {
|
|
599
|
+
return proxy?.username !== undefined && proxy.password !== undefined;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function toPlaywrightProxy(proxy: string | undefined): LaunchOptions["proxy"] {
|
|
603
|
+
if (!proxy) return undefined;
|
|
604
|
+
|
|
605
|
+
const schemeEnd = proxy.indexOf("://");
|
|
606
|
+
const authorityStart = schemeEnd >= 0 ? schemeEnd + 3 : 0;
|
|
607
|
+
const remainder = proxy.slice(authorityStart);
|
|
608
|
+
const authorityEnd = remainder.search(/[/?#]/);
|
|
609
|
+
const authority = remainder.slice(0, authorityEnd >= 0 ? authorityEnd : undefined);
|
|
610
|
+
if (!authority.includes("@")) {
|
|
611
|
+
return { server: proxy };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
let parsed: URL;
|
|
615
|
+
try {
|
|
616
|
+
parsed = new URL(proxy);
|
|
617
|
+
} catch {
|
|
618
|
+
throw new ProviderError("Browser proxy URL is invalid", {
|
|
619
|
+
code: "BROWSER_PROXY_INVALID",
|
|
620
|
+
fix: "Use a valid proxy URL.",
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
let username: string;
|
|
625
|
+
let password: string;
|
|
626
|
+
try {
|
|
627
|
+
username = decodeURIComponent(parsed.username);
|
|
628
|
+
password = decodeURIComponent(parsed.password);
|
|
629
|
+
} catch {
|
|
630
|
+
throw new ProviderError("Browser proxy credentials use invalid percent-encoding", {
|
|
631
|
+
code: "BROWSER_PROXY_INVALID",
|
|
632
|
+
fix: "Percent-encode the proxy username and password as URL userinfo.",
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
registerDiagnosticValue(username);
|
|
636
|
+
registerDiagnosticValue(password);
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
server: `${parsed.protocol}//${parsed.host}`,
|
|
640
|
+
username,
|
|
641
|
+
password,
|
|
280
642
|
};
|
|
281
643
|
}
|
|
282
644
|
|
|
@@ -470,10 +832,24 @@ class PlaywrightBrowserPage implements BrowserPageContract {
|
|
|
470
832
|
readonly id = "main";
|
|
471
833
|
readonly pageId?: string;
|
|
472
834
|
|
|
473
|
-
constructor(
|
|
835
|
+
constructor(
|
|
836
|
+
private readonly page: Page,
|
|
837
|
+
private readonly proxy: LaunchOptions["proxy"] = undefined,
|
|
838
|
+
private readonly telemetry?: BrowserTelemetrySink,
|
|
839
|
+
) {}
|
|
474
840
|
|
|
475
|
-
async goto(
|
|
476
|
-
|
|
841
|
+
async goto(
|
|
842
|
+
url: string,
|
|
843
|
+
options?: { readonly timeout?: number; readonly waitUntil?: "load" | "domcontentloaded" },
|
|
844
|
+
): Promise<void> {
|
|
845
|
+
const response = await this.page.goto(url, options);
|
|
846
|
+
const headers = await response?.headersArray().catch(() => []);
|
|
847
|
+
for (const header of headers ?? []) {
|
|
848
|
+
if (header.name.toLowerCase() === "set-cookie") {
|
|
849
|
+
registerDiagnosticValue(header.value);
|
|
850
|
+
for (const value of cookieValuesFromHeaders(header.value)) registerDiagnosticValue(value);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
477
853
|
}
|
|
478
854
|
|
|
479
855
|
async evaluate<T>(fn: string | (() => T)): Promise<T> {
|
|
@@ -484,6 +860,10 @@ class PlaywrightBrowserPage implements BrowserPageContract {
|
|
|
484
860
|
return await this.page.evaluate(fn);
|
|
485
861
|
}
|
|
486
862
|
|
|
863
|
+
async userAgent(): Promise<string> {
|
|
864
|
+
return await this.evaluate<string>("navigator.userAgent");
|
|
865
|
+
}
|
|
866
|
+
|
|
487
867
|
async waitForSelector(selector: string, options?: { timeout?: number }): Promise<void> {
|
|
488
868
|
await this.page.waitForSelector(selector, options);
|
|
489
869
|
}
|
|
@@ -529,45 +909,110 @@ class PlaywrightBrowserPage implements BrowserPageContract {
|
|
|
529
909
|
await this.page.close();
|
|
530
910
|
}
|
|
531
911
|
|
|
912
|
+
async cookies(): Promise<readonly BrowserCookie[]> {
|
|
913
|
+
const cookies = (await this.page.context().cookies()).map(toBrowserCookie);
|
|
914
|
+
for (const cookie of cookies) registerDiagnosticValue(cookie.value);
|
|
915
|
+
return cookies;
|
|
916
|
+
}
|
|
917
|
+
|
|
532
918
|
async withResourcePolicy<T>(policy: BrowserResourcePolicy, run: () => Promise<T>): Promise<T> {
|
|
533
919
|
const allowedMethods = new Set(policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS);
|
|
920
|
+
const policyHandler = createResourcePolicyHandler(policy, allowedMethods);
|
|
921
|
+
const context = this.page.context();
|
|
534
922
|
const handler = async (route: Route): Promise<void> => {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
923
|
+
try {
|
|
924
|
+
// Context routing sees a popup's first request before the late `page`
|
|
925
|
+
// event. Fail every secondary page closed so no redirect can escape
|
|
926
|
+
// the per-page CDP interception while that session is being attached.
|
|
927
|
+
if (route.request().frame().page() !== this.page) {
|
|
928
|
+
await abortResourceRoute(route);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
} catch {
|
|
932
|
+
await abortResourceRoute(route);
|
|
538
933
|
return;
|
|
539
934
|
}
|
|
540
935
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
936
|
+
await policyHandler(route);
|
|
937
|
+
};
|
|
938
|
+
const sessions = new Set<CDPSession>();
|
|
939
|
+
const pendingAttachments = new Set<Promise<void>>();
|
|
940
|
+
const proxy = hasProxyCredentials(this.proxy) ? this.proxy : undefined;
|
|
941
|
+
let active = true;
|
|
942
|
+
const attachPage = async (page: Page): Promise<void> => {
|
|
943
|
+
const session = await context.newCDPSession(page);
|
|
944
|
+
if (!active) {
|
|
945
|
+
await session.detach();
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
545
948
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
return;
|
|
551
|
-
case "block":
|
|
552
|
-
await route.abort("blockedbyclient");
|
|
553
|
-
return;
|
|
949
|
+
const onRequestPaused = (params: unknown) => {
|
|
950
|
+
if (isCdpResponseStage(params) && policy.documentContentSecurityPolicy) {
|
|
951
|
+
void handleCdpDocumentResponse(session, policy.documentContentSecurityPolicy, params);
|
|
952
|
+
return;
|
|
554
953
|
}
|
|
954
|
+
void handleCdpResourceRequest(session, policy, allowedMethods, params);
|
|
955
|
+
};
|
|
956
|
+
const authAttempts = new Set<string>();
|
|
957
|
+
const onAuthRequired = (params: unknown) => {
|
|
958
|
+
if (!proxy) return;
|
|
959
|
+
void handleCdpAuthRequired(session, proxy, authAttempts, params, this.telemetry);
|
|
960
|
+
};
|
|
961
|
+
session.on("Fetch.requestPaused", onRequestPaused);
|
|
962
|
+
if (proxy) {
|
|
963
|
+
session.on("Fetch.authRequired", onAuthRequired);
|
|
964
|
+
}
|
|
965
|
+
try {
|
|
966
|
+
await session.send("Fetch.enable", {
|
|
967
|
+
...(proxy ? { handleAuthRequests: true } : {}),
|
|
968
|
+
patterns: resourcePolicyFetchPatterns(policy),
|
|
969
|
+
});
|
|
970
|
+
sessions.add(session);
|
|
971
|
+
} catch (error) {
|
|
972
|
+
session.off("Fetch.requestPaused", onRequestPaused);
|
|
973
|
+
if (proxy) {
|
|
974
|
+
session.off("Fetch.authRequired", onAuthRequired);
|
|
975
|
+
}
|
|
976
|
+
await session.detach().catch(() => undefined);
|
|
977
|
+
throw error;
|
|
555
978
|
}
|
|
556
|
-
|
|
557
|
-
|
|
979
|
+
};
|
|
980
|
+
const onPage = (page: Page) => {
|
|
981
|
+
const attachment = attachPage(page)
|
|
982
|
+
.catch(async () => {
|
|
983
|
+
await page.close().catch(() => undefined);
|
|
984
|
+
})
|
|
985
|
+
.finally(() => pendingAttachments.delete(attachment));
|
|
986
|
+
pendingAttachments.add(attachment);
|
|
558
987
|
};
|
|
559
988
|
|
|
560
|
-
await
|
|
989
|
+
await context.route(RESOURCE_POLICY_ROUTE_PATTERN, handler);
|
|
561
990
|
try {
|
|
562
|
-
|
|
991
|
+
context.on("page", onPage);
|
|
992
|
+
try {
|
|
993
|
+
await attachPage(this.page);
|
|
994
|
+
return await run();
|
|
995
|
+
} finally {
|
|
996
|
+
context.off("page", onPage);
|
|
997
|
+
}
|
|
563
998
|
} finally {
|
|
564
|
-
|
|
999
|
+
active = false;
|
|
1000
|
+
await context.unroute(RESOURCE_POLICY_ROUTE_PATTERN, handler);
|
|
1001
|
+
await Promise.allSettled(pendingAttachments);
|
|
1002
|
+
await Promise.all(
|
|
1003
|
+
[...sessions].map(async (session) => {
|
|
1004
|
+
await session.send("Fetch.disable").catch(() => undefined);
|
|
1005
|
+
await session.detach().catch(() => undefined);
|
|
1006
|
+
}),
|
|
1007
|
+
);
|
|
565
1008
|
}
|
|
566
1009
|
}
|
|
567
1010
|
}
|
|
568
1011
|
|
|
569
1012
|
class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
570
1013
|
private browser: import("playwright").Browser | null = null;
|
|
1014
|
+
private parsedProxy: LaunchOptions["proxy"] | undefined;
|
|
1015
|
+
private proxyParsed = false;
|
|
571
1016
|
readonly engine = "playwright-stealth" satisfies BrowserEngine;
|
|
572
1017
|
|
|
573
1018
|
constructor(private readonly options: BrowserClientOptions = {}) {}
|
|
@@ -578,15 +1023,23 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
|
578
1023
|
}
|
|
579
1024
|
|
|
580
1025
|
const chromium = await loadChromiumLauncher(this.options);
|
|
581
|
-
|
|
1026
|
+
if (!this.proxyParsed) {
|
|
1027
|
+
this.parsedProxy = toPlaywrightProxy(this.options.proxy);
|
|
1028
|
+
this.proxyParsed = true;
|
|
1029
|
+
}
|
|
1030
|
+
this.browser = await chromium.launch(toLaunchOptions(this.options, this.parsedProxy));
|
|
582
1031
|
return this.browser;
|
|
583
1032
|
}
|
|
584
1033
|
|
|
585
1034
|
async newPage(): Promise<BrowserPageContract> {
|
|
1035
|
+
if (!this.options.cdpUrl)
|
|
1036
|
+
observeBrowserTelemetry(this.options.telemetry, () =>
|
|
1037
|
+
this.options.telemetry?.recordPoolAcquire("not_configured"),
|
|
1038
|
+
);
|
|
586
1039
|
const browser = await this.ensureBrowser();
|
|
587
1040
|
const page = await browser.newPage();
|
|
588
1041
|
|
|
589
|
-
return new PlaywrightBrowserPage(page);
|
|
1042
|
+
return new PlaywrightBrowserPage(page, this.parsedProxy, this.options.telemetry);
|
|
590
1043
|
}
|
|
591
1044
|
|
|
592
1045
|
async rawPage(): Promise<BrowserPageContract> {
|
|
@@ -597,10 +1050,14 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
|
597
1050
|
}
|
|
598
1051
|
|
|
599
1052
|
async withIsolatedContext<T>(handler: (page: BrowserPageContract) => Promise<T>): Promise<T> {
|
|
1053
|
+
if (!this.options.cdpUrl)
|
|
1054
|
+
observeBrowserTelemetry(this.options.telemetry, () =>
|
|
1055
|
+
this.options.telemetry?.recordPoolAcquire("not_configured"),
|
|
1056
|
+
);
|
|
600
1057
|
const browser = await this.ensureBrowser();
|
|
601
|
-
const context = await browser.newContext();
|
|
1058
|
+
const context = await browser.newContext({ serviceWorkers: this.options.serviceWorkers });
|
|
602
1059
|
const page = await context.newPage();
|
|
603
|
-
const browserPage = new PlaywrightBrowserPage(page);
|
|
1060
|
+
const browserPage = new PlaywrightBrowserPage(page, this.parsedProxy, this.options.telemetry);
|
|
604
1061
|
|
|
605
1062
|
try {
|
|
606
1063
|
return await handler(browserPage);
|
|
@@ -641,12 +1098,12 @@ function normalizeWebSocketEndpoint(endpoint: string): string {
|
|
|
641
1098
|
throw new Error(`Unsupported WebSocket endpoint protocol: ${url.protocol}`);
|
|
642
1099
|
}
|
|
643
1100
|
|
|
644
|
-
class
|
|
1101
|
+
abstract class WebSocketCommandClient {
|
|
645
1102
|
private nextId = 1;
|
|
646
1103
|
private readonly endpoint: string;
|
|
647
1104
|
private readonly listeners = new Map<string, Set<(params: unknown) => void>>();
|
|
648
1105
|
private readonly pending = new Map<
|
|
649
|
-
|
|
1106
|
+
WebSocketCommandId,
|
|
650
1107
|
{
|
|
651
1108
|
reject: (reason?: unknown) => void;
|
|
652
1109
|
resolve: (value: Record<string, unknown>) => void;
|
|
@@ -654,6 +1111,7 @@ class JsonRpcWebSocketClient {
|
|
|
654
1111
|
>();
|
|
655
1112
|
private socket?: WebSocket;
|
|
656
1113
|
private socketPromise?: Promise<WebSocket>;
|
|
1114
|
+
private closed = false;
|
|
657
1115
|
|
|
658
1116
|
constructor(endpoint: string) {
|
|
659
1117
|
this.endpoint = normalizeWebSocketEndpoint(endpoint);
|
|
@@ -677,23 +1135,18 @@ class JsonRpcWebSocketClient {
|
|
|
677
1135
|
params: Record<string, unknown> = {},
|
|
678
1136
|
): Promise<Record<string, unknown>> {
|
|
679
1137
|
const socket = await this.getSocket();
|
|
1138
|
+
if (this.closed) throw new Error(`WebSocket closed: ${this.endpoint}`);
|
|
680
1139
|
const id = this.nextId++;
|
|
681
1140
|
|
|
682
1141
|
return await new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
683
1142
|
this.pending.set(id, { resolve, reject });
|
|
684
1143
|
|
|
685
|
-
socket.send(
|
|
686
|
-
JSON.stringify({
|
|
687
|
-
id,
|
|
688
|
-
jsonrpc: "2.0",
|
|
689
|
-
method,
|
|
690
|
-
params,
|
|
691
|
-
}),
|
|
692
|
-
);
|
|
1144
|
+
socket.send(JSON.stringify(this.createCommandFrame(id, method, params)));
|
|
693
1145
|
});
|
|
694
1146
|
}
|
|
695
1147
|
|
|
696
1148
|
async close(): Promise<void> {
|
|
1149
|
+
this.closed = true;
|
|
697
1150
|
for (const pending of this.pending.values()) {
|
|
698
1151
|
pending.reject(new Error(`WebSocket closed: ${this.endpoint}`));
|
|
699
1152
|
}
|
|
@@ -705,7 +1158,14 @@ class JsonRpcWebSocketClient {
|
|
|
705
1158
|
this.socketPromise = undefined;
|
|
706
1159
|
}
|
|
707
1160
|
|
|
1161
|
+
protected abstract createCommandFrame(
|
|
1162
|
+
id: WebSocketCommandId,
|
|
1163
|
+
method: string,
|
|
1164
|
+
params: Record<string, unknown>,
|
|
1165
|
+
): CdpCommandFrame | JsonRpcCommandFrame;
|
|
1166
|
+
|
|
708
1167
|
private async getSocket(): Promise<WebSocket> {
|
|
1168
|
+
if (this.closed) throw new Error(`WebSocket closed: ${this.endpoint}`);
|
|
709
1169
|
if (this.socket?.readyState === WebSocket.OPEN) {
|
|
710
1170
|
return this.socket;
|
|
711
1171
|
}
|
|
@@ -718,6 +1178,11 @@ class JsonRpcWebSocketClient {
|
|
|
718
1178
|
const socket = new WebSocket(this.endpoint);
|
|
719
1179
|
|
|
720
1180
|
socket.addEventListener("open", () => {
|
|
1181
|
+
if (this.closed) {
|
|
1182
|
+
socket.close();
|
|
1183
|
+
reject(new Error(`WebSocket closed: ${this.endpoint}`));
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
721
1186
|
this.socket = socket;
|
|
722
1187
|
resolve(socket);
|
|
723
1188
|
});
|
|
@@ -727,7 +1192,7 @@ class JsonRpcWebSocketClient {
|
|
|
727
1192
|
typeof event.data === "string"
|
|
728
1193
|
? event.data
|
|
729
1194
|
: Buffer.from(event.data as ArrayBufferLike).toString("utf8");
|
|
730
|
-
const payload = JSON.parse(rawData) as
|
|
1195
|
+
const payload = JSON.parse(rawData) as WebSocketCommandMessage;
|
|
731
1196
|
|
|
732
1197
|
if (typeof payload.id === "number") {
|
|
733
1198
|
const pending = this.pending.get(payload.id);
|
|
@@ -738,7 +1203,11 @@ class JsonRpcWebSocketClient {
|
|
|
738
1203
|
this.pending.delete(payload.id);
|
|
739
1204
|
|
|
740
1205
|
if (payload.error) {
|
|
741
|
-
|
|
1206
|
+
const error = new Error(payload.error.message ?? "JSON-RPC command failed");
|
|
1207
|
+
if (typeof payload.error.code === "number") {
|
|
1208
|
+
Object.assign(error, { code: payload.error.code });
|
|
1209
|
+
}
|
|
1210
|
+
pending.reject(error);
|
|
742
1211
|
return;
|
|
743
1212
|
}
|
|
744
1213
|
|
|
@@ -774,6 +1243,38 @@ class JsonRpcWebSocketClient {
|
|
|
774
1243
|
}
|
|
775
1244
|
}
|
|
776
1245
|
|
|
1246
|
+
class CdpWebSocketClient extends WebSocketCommandClient {
|
|
1247
|
+
constructor(
|
|
1248
|
+
endpoint: string,
|
|
1249
|
+
private readonly sessionId?: string,
|
|
1250
|
+
) {
|
|
1251
|
+
super(endpoint);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
protected createCommandFrame(
|
|
1255
|
+
id: WebSocketCommandId,
|
|
1256
|
+
method: string,
|
|
1257
|
+
params: Record<string, unknown>,
|
|
1258
|
+
): CdpCommandFrame {
|
|
1259
|
+
return {
|
|
1260
|
+
id,
|
|
1261
|
+
method,
|
|
1262
|
+
params,
|
|
1263
|
+
...(this.sessionId === undefined ? {} : { sessionId: this.sessionId }),
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
class JsonRpcWebSocketClient extends WebSocketCommandClient {
|
|
1269
|
+
protected createCommandFrame(
|
|
1270
|
+
id: WebSocketCommandId,
|
|
1271
|
+
method: string,
|
|
1272
|
+
params: Record<string, unknown>,
|
|
1273
|
+
): JsonRpcCommandFrame {
|
|
1274
|
+
return { id, jsonrpc: "2.0", method, params };
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
777
1278
|
function flattenCdpFrameTree(
|
|
778
1279
|
node: CdpFrameTreeNode | undefined,
|
|
779
1280
|
out: CdpFrameTreeNode["frame"][] = [],
|
|
@@ -794,6 +1295,64 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
794
1295
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
795
1296
|
}
|
|
796
1297
|
|
|
1298
|
+
function isBrowserCookieSameSite(value: unknown): value is BrowserCookie["sameSite"] {
|
|
1299
|
+
return value === "Strict" || value === "Lax" || value === "None";
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function toBrowserCookie(cookie: {
|
|
1303
|
+
readonly name: string;
|
|
1304
|
+
readonly value: string;
|
|
1305
|
+
readonly domain: string;
|
|
1306
|
+
readonly path: string;
|
|
1307
|
+
readonly expires?: number;
|
|
1308
|
+
readonly httpOnly: boolean;
|
|
1309
|
+
readonly secure: boolean;
|
|
1310
|
+
readonly sameSite?: unknown;
|
|
1311
|
+
}): BrowserCookie {
|
|
1312
|
+
return {
|
|
1313
|
+
name: cookie.name,
|
|
1314
|
+
value: cookie.value,
|
|
1315
|
+
domain: cookie.domain,
|
|
1316
|
+
path: cookie.path,
|
|
1317
|
+
...(cookie.expires !== undefined && cookie.expires > 0 ? { expires: cookie.expires } : {}),
|
|
1318
|
+
httpOnly: cookie.httpOnly,
|
|
1319
|
+
secure: cookie.secure,
|
|
1320
|
+
...(isBrowserCookieSameSite(cookie.sameSite) ? { sameSite: cookie.sameSite } : {}),
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function parseCdpCookies(value: unknown): readonly BrowserCookie[] {
|
|
1325
|
+
if (!Array.isArray(value)) {
|
|
1326
|
+
throw new Error("CDP Network.getCookies returned an invalid cookie list");
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
return value.map((cookie) => {
|
|
1330
|
+
if (
|
|
1331
|
+
!isRecord(cookie) ||
|
|
1332
|
+
typeof cookie.name !== "string" ||
|
|
1333
|
+
typeof cookie.value !== "string" ||
|
|
1334
|
+
typeof cookie.domain !== "string" ||
|
|
1335
|
+
typeof cookie.path !== "string" ||
|
|
1336
|
+
typeof cookie.expires !== "number" ||
|
|
1337
|
+
typeof cookie.httpOnly !== "boolean" ||
|
|
1338
|
+
typeof cookie.secure !== "boolean"
|
|
1339
|
+
) {
|
|
1340
|
+
throw new Error("CDP Network.getCookies returned an invalid cookie");
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
return toBrowserCookie({
|
|
1344
|
+
name: cookie.name,
|
|
1345
|
+
value: cookie.value,
|
|
1346
|
+
domain: cookie.domain,
|
|
1347
|
+
path: cookie.path,
|
|
1348
|
+
expires: cookie.expires,
|
|
1349
|
+
httpOnly: cookie.httpOnly,
|
|
1350
|
+
secure: cookie.secure,
|
|
1351
|
+
sameSite: cookie.sameSite,
|
|
1352
|
+
});
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
|
|
797
1356
|
function parsePoolAcquireResponse(value: unknown): PoolAcquireResponse {
|
|
798
1357
|
if (
|
|
799
1358
|
!isRecord(value) ||
|
|
@@ -818,6 +1377,36 @@ function parsePoolAcquireResponse(value: unknown): PoolAcquireResponse {
|
|
|
818
1377
|
};
|
|
819
1378
|
}
|
|
820
1379
|
|
|
1380
|
+
function poolAcquireOutcome(error: unknown): BrowserTelemetryPoolAcquireOutcome {
|
|
1381
|
+
const code =
|
|
1382
|
+
error instanceof Error && typeof (error as Error & { code?: unknown }).code === "number"
|
|
1383
|
+
? (error as Error & { code: number }).code
|
|
1384
|
+
: undefined;
|
|
1385
|
+
switch (code) {
|
|
1386
|
+
case -32001:
|
|
1387
|
+
return "queue_full";
|
|
1388
|
+
case -32002:
|
|
1389
|
+
return "timed_out";
|
|
1390
|
+
case -32003:
|
|
1391
|
+
return "shutting_down";
|
|
1392
|
+
case -32004:
|
|
1393
|
+
return "unknown_lease";
|
|
1394
|
+
case -32005:
|
|
1395
|
+
return "unknown_method";
|
|
1396
|
+
case -32006:
|
|
1397
|
+
return "missing_allowed_hosts";
|
|
1398
|
+
}
|
|
1399
|
+
if (error instanceof Error && /Unable to connect|WebSocket closed/.test(error.message))
|
|
1400
|
+
return "transport_failure";
|
|
1401
|
+
return "other";
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function poolAcquireCode(error: unknown): number | undefined {
|
|
1405
|
+
return error instanceof Error && typeof (error as Error & { code?: unknown }).code === "number"
|
|
1406
|
+
? (error as Error & { code: number }).code
|
|
1407
|
+
: undefined;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
821
1410
|
function parseCdpFrameTreeNode(value: unknown): CdpFrameTreeNode | undefined {
|
|
822
1411
|
if (!isRecord(value) || !isRecord(value.frame)) {
|
|
823
1412
|
return undefined;
|
|
@@ -861,6 +1450,18 @@ function getCdpExecutionContext(params: unknown): {
|
|
|
861
1450
|
};
|
|
862
1451
|
}
|
|
863
1452
|
|
|
1453
|
+
function getCdpDestroyedExecutionContextId(params: unknown): number | undefined {
|
|
1454
|
+
if (!isRecord(params)) {
|
|
1455
|
+
return undefined;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
return typeof params.executionContextId === "number" ? params.executionContextId : undefined;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function isMissingExecutionContextError(error: unknown): boolean {
|
|
1462
|
+
return error instanceof Error && /\b(?:cannot|failed to) find context\b/i.test(error.message);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
864
1465
|
class CdpBrowserLocator implements BrowserLocator {
|
|
865
1466
|
constructor(
|
|
866
1467
|
private readonly frame: {
|
|
@@ -967,14 +1568,14 @@ class CdpBrowserFrame implements BrowserFrame {
|
|
|
967
1568
|
}
|
|
968
1569
|
|
|
969
1570
|
class CdpPoolBrowserPage implements BrowserPageContract {
|
|
970
|
-
private
|
|
1571
|
+
private closePromise?: Promise<void>;
|
|
971
1572
|
private initialized = false;
|
|
972
1573
|
private readonly frameExecutionContexts = new Map<string, number>();
|
|
973
1574
|
|
|
974
1575
|
constructor(
|
|
975
1576
|
readonly pageId: string,
|
|
976
1577
|
private readonly browserContextId: string | undefined,
|
|
977
|
-
private readonly pageClient:
|
|
1578
|
+
private readonly pageClient: CdpWebSocketClient,
|
|
978
1579
|
private readonly release: (request: PoolReleaseRequest) => Promise<void>,
|
|
979
1580
|
) {}
|
|
980
1581
|
|
|
@@ -982,17 +1583,29 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
982
1583
|
return this.pageId;
|
|
983
1584
|
}
|
|
984
1585
|
|
|
985
|
-
async goto(
|
|
1586
|
+
async goto(
|
|
1587
|
+
url: string,
|
|
1588
|
+
options?: { readonly timeout?: number; readonly waitUntil?: "load" | "domcontentloaded" },
|
|
1589
|
+
): Promise<void> {
|
|
986
1590
|
await this.initialize();
|
|
987
1591
|
const startedAt = Date.now();
|
|
988
|
-
|
|
989
|
-
const
|
|
990
|
-
|
|
1592
|
+
const timeout = options?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
1593
|
+
const waitUntil = options?.waitUntil ?? "load";
|
|
1594
|
+
let expectedEventSeen = false;
|
|
1595
|
+
const eventName =
|
|
1596
|
+
waitUntil === "domcontentloaded" ? "Page.domContentEventFired" : "Page.loadEventFired";
|
|
1597
|
+
const unsubscribe = this.pageClient.on(eventName, () => {
|
|
1598
|
+
expectedEventSeen = true;
|
|
991
1599
|
});
|
|
992
1600
|
|
|
993
1601
|
try {
|
|
994
|
-
await this.pageClient.send("Page.navigate", { url })
|
|
995
|
-
|
|
1602
|
+
const navigation = (await this.pageClient.send("Page.navigate", { url })) as {
|
|
1603
|
+
readonly errorText?: unknown;
|
|
1604
|
+
};
|
|
1605
|
+
if (typeof navigation.errorText === "string" && navigation.errorText.length > 0) {
|
|
1606
|
+
throw new Error(`Page.navigate failed: ${navigation.errorText} at ${url}`);
|
|
1607
|
+
}
|
|
1608
|
+
await this.waitForDocumentReady(startedAt + timeout, () => expectedEventSeen, waitUntil);
|
|
996
1609
|
} finally {
|
|
997
1610
|
unsubscribe();
|
|
998
1611
|
}
|
|
@@ -1003,10 +1616,27 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1003
1616
|
return await this.evaluateWithContext<T>(fn);
|
|
1004
1617
|
}
|
|
1005
1618
|
|
|
1619
|
+
async userAgent(): Promise<string> {
|
|
1620
|
+
return await this.evaluate<string>("navigator.userAgent");
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1006
1623
|
async evaluateInFrame<T>(frameId: string, fn: string | (() => T)): Promise<T> {
|
|
1007
1624
|
await this.initialize();
|
|
1008
1625
|
const contextId = await this.getFrameExecutionContextId(frameId);
|
|
1009
|
-
|
|
1626
|
+
try {
|
|
1627
|
+
return await this.evaluateWithContext<T>(fn, contextId);
|
|
1628
|
+
} catch (error) {
|
|
1629
|
+
if (!isMissingExecutionContextError(error)) {
|
|
1630
|
+
throw error;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
if (this.frameExecutionContexts.get(frameId) === contextId) {
|
|
1634
|
+
this.frameExecutionContexts.delete(frameId);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
const refreshedContextId = await this.getFrameExecutionContextId(frameId);
|
|
1638
|
+
return await this.evaluateWithContext<T>(fn, refreshedContextId);
|
|
1639
|
+
}
|
|
1010
1640
|
}
|
|
1011
1641
|
|
|
1012
1642
|
async waitForSelectorInFrame(
|
|
@@ -1149,21 +1779,34 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1149
1779
|
return Buffer.from(String(result.data ?? ""), "base64");
|
|
1150
1780
|
}
|
|
1151
1781
|
|
|
1782
|
+
async cookies(): Promise<readonly BrowserCookie[]> {
|
|
1783
|
+
await this.initialize();
|
|
1784
|
+
const result = await this.pageClient.send("Network.getCookies");
|
|
1785
|
+
const cookies = parseCdpCookies(result.cookies);
|
|
1786
|
+
for (const cookie of cookies) registerDiagnosticValue(cookie.value);
|
|
1787
|
+
return cookies;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1152
1790
|
async close(): Promise<void> {
|
|
1153
|
-
|
|
1791
|
+
// Only the first caller observes the release outcome; repeat calls stay
|
|
1792
|
+
// idempotent (as they were before the promise was memoized) instead of
|
|
1793
|
+
// replaying a cached rejection, and never re-issue a release against a
|
|
1794
|
+
// target the pool has already disposed.
|
|
1795
|
+
if (this.closePromise) {
|
|
1796
|
+
await this.closePromise.catch(() => undefined);
|
|
1154
1797
|
return;
|
|
1155
1798
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1799
|
+
this.closePromise = (async () => {
|
|
1800
|
+
try {
|
|
1801
|
+
await this.release({
|
|
1802
|
+
...(this.browserContextId ? { browserContextId: this.browserContextId } : {}),
|
|
1803
|
+
pageId: this.pageId,
|
|
1804
|
+
});
|
|
1805
|
+
} finally {
|
|
1806
|
+
await this.pageClient.close();
|
|
1807
|
+
}
|
|
1808
|
+
})();
|
|
1809
|
+
await this.closePromise;
|
|
1167
1810
|
}
|
|
1168
1811
|
|
|
1169
1812
|
async withResourcePolicy<T>(policy: BrowserResourcePolicy, run: () => Promise<T>): Promise<T> {
|
|
@@ -1176,7 +1819,7 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1176
1819
|
|
|
1177
1820
|
try {
|
|
1178
1821
|
await this.pageClient.send("Fetch.enable", {
|
|
1179
|
-
patterns:
|
|
1822
|
+
patterns: resourcePolicyFetchPatterns(policy),
|
|
1180
1823
|
});
|
|
1181
1824
|
} catch (error) {
|
|
1182
1825
|
unsubscribe();
|
|
@@ -1193,8 +1836,19 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1193
1836
|
try {
|
|
1194
1837
|
return await run();
|
|
1195
1838
|
} finally {
|
|
1196
|
-
|
|
1197
|
-
|
|
1839
|
+
try {
|
|
1840
|
+
if (this.closePromise) {
|
|
1841
|
+
// Keep interception active until release settles. The release failure
|
|
1842
|
+
// stays observable through close() itself; swallowing it here keeps
|
|
1843
|
+
// this finally from replacing run()'s own outcome, and still avoids a
|
|
1844
|
+
// reconnect or a disable against a disposed target.
|
|
1845
|
+
await this.closePromise.catch(() => undefined);
|
|
1846
|
+
} else {
|
|
1847
|
+
await this.pageClient.send("Fetch.disable");
|
|
1848
|
+
}
|
|
1849
|
+
} finally {
|
|
1850
|
+
unsubscribe();
|
|
1851
|
+
}
|
|
1198
1852
|
}
|
|
1199
1853
|
}
|
|
1200
1854
|
|
|
@@ -1209,6 +1863,28 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1209
1863
|
}
|
|
1210
1864
|
|
|
1211
1865
|
try {
|
|
1866
|
+
if (isCdpResponseStage(params) && policy.documentContentSecurityPolicy) {
|
|
1867
|
+
const response = toCdpPausedDocumentResponse(params);
|
|
1868
|
+
if (!response) throw new Error("CDP document response metadata is unavailable");
|
|
1869
|
+
if (!response.hasBody) {
|
|
1870
|
+
await this.pageClient.send("Fetch.continueResponse", {
|
|
1871
|
+
requestId,
|
|
1872
|
+
responseCode: response.responseCode,
|
|
1873
|
+
responseHeaders: [...response.responseHeaders],
|
|
1874
|
+
});
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
const body = await this.pageClient.send("Fetch.getResponseBody", {
|
|
1879
|
+
requestId: response.requestId,
|
|
1880
|
+
});
|
|
1881
|
+
await this.pageClient.send(
|
|
1882
|
+
"Fetch.fulfillRequest",
|
|
1883
|
+
toCdpDocumentFulfillParams(response, body, policy.documentContentSecurityPolicy),
|
|
1884
|
+
);
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1212
1888
|
const parsed = toCdpResourceRequest(params);
|
|
1213
1889
|
if (!parsed || !allowedMethods.has(parsed.request.method)) {
|
|
1214
1890
|
await this.failCdpResourceRequest(requestId);
|
|
@@ -1222,6 +1898,11 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1222
1898
|
|
|
1223
1899
|
const decision = await resourceRoute.handle(parsed.request);
|
|
1224
1900
|
switch (decision.action) {
|
|
1901
|
+
case "continue":
|
|
1902
|
+
await this.pageClient.send("Fetch.continueRequest", {
|
|
1903
|
+
requestId: parsed.requestId,
|
|
1904
|
+
});
|
|
1905
|
+
return;
|
|
1225
1906
|
case "fulfill":
|
|
1226
1907
|
await this.pageClient.send(
|
|
1227
1908
|
"Fetch.fulfillRequest",
|
|
@@ -1264,8 +1945,46 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1264
1945
|
this.frameExecutionContexts.set(context.frameId, context.id);
|
|
1265
1946
|
}
|
|
1266
1947
|
});
|
|
1948
|
+
this.pageClient.on("Runtime.executionContextDestroyed", (params) => {
|
|
1949
|
+
const destroyedContextId = getCdpDestroyedExecutionContextId(params);
|
|
1950
|
+
if (destroyedContextId === undefined) {
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
for (const [frameId, contextId] of this.frameExecutionContexts) {
|
|
1955
|
+
if (contextId === destroyedContextId) {
|
|
1956
|
+
this.frameExecutionContexts.delete(frameId);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
});
|
|
1960
|
+
this.pageClient.on("Runtime.executionContextsCleared", () => {
|
|
1961
|
+
this.frameExecutionContexts.clear();
|
|
1962
|
+
});
|
|
1963
|
+
const registerResponseCookies = (params: unknown) => {
|
|
1964
|
+
if (!isRecord(params)) return;
|
|
1965
|
+
const headers = Array.isArray(params.responseHeaders)
|
|
1966
|
+
? params.responseHeaders
|
|
1967
|
+
: isRecord(params.headers)
|
|
1968
|
+
? Object.entries(params.headers).map(([name, value]) => ({ name, value }))
|
|
1969
|
+
: [];
|
|
1970
|
+
for (const header of headers) {
|
|
1971
|
+
if (
|
|
1972
|
+
isRecord(header) &&
|
|
1973
|
+
typeof header.name === "string" &&
|
|
1974
|
+
header.name.toLowerCase() === "set-cookie" &&
|
|
1975
|
+
typeof header.value === "string"
|
|
1976
|
+
) {
|
|
1977
|
+
registerDiagnosticValue(header.value);
|
|
1978
|
+
for (const value of cookieValuesFromHeaders(header.value)) registerDiagnosticValue(value);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
};
|
|
1982
|
+
const scopedCookieRegistration = AsyncResource.bind(registerResponseCookies);
|
|
1983
|
+
this.pageClient.on("Network.responseReceived", scopedCookieRegistration);
|
|
1984
|
+
this.pageClient.on("Network.responseReceivedExtraInfo", scopedCookieRegistration);
|
|
1267
1985
|
await this.pageClient.send("Page.enable");
|
|
1268
1986
|
await this.pageClient.send("Runtime.enable");
|
|
1987
|
+
await this.pageClient.send("Network.enable").catch(() => undefined);
|
|
1269
1988
|
this.initialized = true;
|
|
1270
1989
|
}
|
|
1271
1990
|
|
|
@@ -1291,12 +2010,16 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1291
2010
|
|
|
1292
2011
|
private async waitForDocumentReady(
|
|
1293
2012
|
deadline: number,
|
|
1294
|
-
|
|
2013
|
+
isExpectedEventSeen: () => boolean,
|
|
2014
|
+
waitUntil: "load" | "domcontentloaded",
|
|
1295
2015
|
): Promise<void> {
|
|
1296
2016
|
while (Date.now() < deadline) {
|
|
1297
2017
|
const readyState = await this.evaluate<string>("document.readyState");
|
|
1298
|
-
|
|
1299
|
-
|
|
2018
|
+
const documentReady =
|
|
2019
|
+
readyState === "complete" ||
|
|
2020
|
+
(waitUntil === "domcontentloaded" && readyState === "interactive");
|
|
2021
|
+
if (documentReady) {
|
|
2022
|
+
if (isExpectedEventSeen() || readyState === "complete") {
|
|
1300
2023
|
return;
|
|
1301
2024
|
}
|
|
1302
2025
|
}
|
|
@@ -1311,6 +2034,7 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1311
2034
|
class CdpPoolBrowserClient implements SupportedBrowserClient {
|
|
1312
2035
|
private readonly allowedHosts: string[];
|
|
1313
2036
|
private readonly poolClient: JsonRpcWebSocketClient;
|
|
2037
|
+
private readonly telemetry?: BrowserTelemetrySink;
|
|
1314
2038
|
readonly engine = "playwright-stealth" satisfies BrowserEngine;
|
|
1315
2039
|
|
|
1316
2040
|
constructor(options: BrowserClientOptions) {
|
|
@@ -1321,6 +2045,7 @@ class CdpPoolBrowserClient implements SupportedBrowserClient {
|
|
|
1321
2045
|
}
|
|
1322
2046
|
|
|
1323
2047
|
this.allowedHosts = [...new Set(options.allowedHosts ?? [])];
|
|
2048
|
+
this.telemetry = options.telemetry;
|
|
1324
2049
|
this.poolClient = new JsonRpcWebSocketClient(options.cdpUrl);
|
|
1325
2050
|
}
|
|
1326
2051
|
|
|
@@ -1329,13 +2054,30 @@ class CdpPoolBrowserClient implements SupportedBrowserClient {
|
|
|
1329
2054
|
}
|
|
1330
2055
|
|
|
1331
2056
|
private async acquirePage(options?: { isolatedContext?: boolean }): Promise<BrowserPageContract> {
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
2057
|
+
let acquireResult: PoolAcquireResponse;
|
|
2058
|
+
try {
|
|
2059
|
+
acquireResult = parsePoolAcquireResponse(
|
|
2060
|
+
await this.poolClient.send("acquire", {
|
|
2061
|
+
...(this.allowedHosts.length > 0 ? { allowedHosts: this.allowedHosts } : {}),
|
|
2062
|
+
...(options?.isolatedContext ? { isolationMode: "browserContext" } : {}),
|
|
2063
|
+
}),
|
|
2064
|
+
);
|
|
2065
|
+
observeBrowserTelemetry(this.telemetry, () => this.telemetry?.recordPoolAcquire("ok"));
|
|
2066
|
+
} catch (error) {
|
|
2067
|
+
observeBrowserTelemetry(this.telemetry, () =>
|
|
2068
|
+
this.telemetry?.recordPoolAcquire(poolAcquireOutcome(error)),
|
|
2069
|
+
);
|
|
2070
|
+
observeBrowserTelemetry(this.telemetry, () =>
|
|
2071
|
+
this.telemetry?.recordError("BROWSER_CDP_POOL_ERROR"),
|
|
2072
|
+
);
|
|
2073
|
+
const code = poolAcquireCode(error);
|
|
2074
|
+
if (code !== undefined && ![-32001, -32002, -32003, -32004, -32005, -32006].includes(code))
|
|
2075
|
+
observeBrowserTelemetry(this.telemetry, () =>
|
|
2076
|
+
this.telemetry?.recordPoolAcquireUnknownCode?.(code),
|
|
2077
|
+
);
|
|
2078
|
+
throw error;
|
|
2079
|
+
}
|
|
2080
|
+
const pageClient = new CdpWebSocketClient(acquireResult.wsEndpoint);
|
|
1339
2081
|
const page = new CdpPoolBrowserPage(
|
|
1340
2082
|
acquireResult.pageId,
|
|
1341
2083
|
acquireResult.browserContextId,
|
|
@@ -1426,6 +2168,7 @@ export class BrowserClient implements BrowserClientContract {
|
|
|
1426
2168
|
private activePage?: BrowserPageContract;
|
|
1427
2169
|
private readonly activePages = new Set<BrowserPageContract>();
|
|
1428
2170
|
private readonly _engine: BrowserEngine;
|
|
2171
|
+
private readonly telemetry?: BrowserTelemetrySink;
|
|
1429
2172
|
|
|
1430
2173
|
constructor(options: BrowserClientOptions = {}) {
|
|
1431
2174
|
const resolvedOptions = {
|
|
@@ -1434,6 +2177,7 @@ export class BrowserClient implements BrowserClientContract {
|
|
|
1434
2177
|
};
|
|
1435
2178
|
const engine = resolvedOptions.engine ?? "playwright-stealth";
|
|
1436
2179
|
this._engine = engine;
|
|
2180
|
+
this.telemetry = resolvedOptions.telemetry;
|
|
1437
2181
|
this.cdpUrl = resolvedOptions.cdpUrl;
|
|
1438
2182
|
|
|
1439
2183
|
if (resolvedOptions.requireCdpPool && !resolvedOptions.cdpUrl) {
|
|
@@ -1462,23 +2206,46 @@ export class BrowserClient implements BrowserClientContract {
|
|
|
1462
2206
|
}
|
|
1463
2207
|
|
|
1464
2208
|
async newPage(): Promise<BrowserPageContract> {
|
|
1465
|
-
|
|
1466
|
-
|
|
2209
|
+
observeBrowserTelemetry(this.telemetry, () => this.telemetry?.recordEngine(this._engine));
|
|
2210
|
+
try {
|
|
2211
|
+
const page = await this.client.newPage();
|
|
2212
|
+
return this.activatePage(page);
|
|
2213
|
+
} catch (error) {
|
|
2214
|
+
observeBrowserTelemetry(this.telemetry, () =>
|
|
2215
|
+
this.telemetry?.recordError(browserTelemetryErrorCode(error)),
|
|
2216
|
+
);
|
|
2217
|
+
throw error;
|
|
2218
|
+
}
|
|
1467
2219
|
}
|
|
1468
2220
|
|
|
1469
2221
|
async rawPage(): Promise<BrowserPageContract> {
|
|
2222
|
+
observeBrowserTelemetry(this.telemetry, () => this.telemetry?.recordEngine(this._engine));
|
|
1470
2223
|
if (!this.cdpUrl) {
|
|
2224
|
+
observeBrowserTelemetry(this.telemetry, () =>
|
|
2225
|
+
this.telemetry?.recordPoolAcquire("not_configured"),
|
|
2226
|
+
);
|
|
2227
|
+
observeBrowserTelemetry(this.telemetry, () =>
|
|
2228
|
+
this.telemetry?.recordError("BROWSER_RUNTIME_UNSUPPORTED"),
|
|
2229
|
+
);
|
|
1471
2230
|
throw new ProviderError("ctx.browser.rawPage() requires a CDP pool", {
|
|
1472
2231
|
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
1473
2232
|
fix: "Set APIFUSE__CDP_POOL__URL. The SDK escape hatch is CDP pool-backed only and never launches local Chromium.",
|
|
1474
2233
|
});
|
|
1475
2234
|
}
|
|
1476
2235
|
|
|
1477
|
-
|
|
1478
|
-
|
|
2236
|
+
try {
|
|
2237
|
+
const page = await this.client.rawPage();
|
|
2238
|
+
return this.activatePage(page);
|
|
2239
|
+
} catch (error) {
|
|
2240
|
+
observeBrowserTelemetry(this.telemetry, () =>
|
|
2241
|
+
this.telemetry?.recordError(browserTelemetryErrorCode(error)),
|
|
2242
|
+
);
|
|
2243
|
+
throw error;
|
|
2244
|
+
}
|
|
1479
2245
|
}
|
|
1480
2246
|
|
|
1481
2247
|
async withIsolatedContext<T>(handler: (page: BrowserPageContract) => Promise<T>): Promise<T> {
|
|
2248
|
+
observeBrowserTelemetry(this.telemetry, () => this.telemetry?.recordEngine(this._engine));
|
|
1482
2249
|
const previousActivePage = this.activePage;
|
|
1483
2250
|
let trackedPage: BrowserPageContract | undefined;
|
|
1484
2251
|
|
|
@@ -1496,22 +2263,34 @@ export class BrowserClient implements BrowserClientContract {
|
|
|
1496
2263
|
}
|
|
1497
2264
|
|
|
1498
2265
|
private activatePage(page: BrowserPageContract): BrowserPageContract {
|
|
1499
|
-
let
|
|
2266
|
+
let closePromise: Promise<void> | undefined;
|
|
1500
2267
|
const originalClose = page.close.bind(page);
|
|
1501
2268
|
const trackedPage = new Proxy(page, {
|
|
1502
2269
|
get: (target, property, receiver) => {
|
|
1503
2270
|
if (property === "close") {
|
|
1504
2271
|
return async () => {
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
this.activePage = undefined;
|
|
1513
|
-
}
|
|
2272
|
+
// Repeat calls resolve on the first outcome instead of replaying a
|
|
2273
|
+
// cached rejection, so a failed release cannot make close() — or
|
|
2274
|
+
// BrowserClient.close(), which closes every tracked page — throw
|
|
2275
|
+
// forever.
|
|
2276
|
+
if (closePromise) {
|
|
2277
|
+
await closePromise.catch(() => undefined);
|
|
2278
|
+
return;
|
|
1514
2279
|
}
|
|
2280
|
+
closePromise = (async () => {
|
|
2281
|
+
// Bookkeeping runs even when the release fails: the page is gone
|
|
2282
|
+
// either way, so leaving it in activePages would leak it and let
|
|
2283
|
+
// solveChallenge reuse a disposed target.
|
|
2284
|
+
try {
|
|
2285
|
+
await originalClose();
|
|
2286
|
+
} finally {
|
|
2287
|
+
this.activePages.delete(trackedPage);
|
|
2288
|
+
if (this.activePage === trackedPage) {
|
|
2289
|
+
this.activePage = undefined;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
})();
|
|
2293
|
+
await closePromise;
|
|
1515
2294
|
};
|
|
1516
2295
|
}
|
|
1517
2296
|
const value = Reflect.get(target, property, receiver);
|
|
@@ -1524,7 +2303,11 @@ export class BrowserClient implements BrowserClientContract {
|
|
|
1524
2303
|
}
|
|
1525
2304
|
|
|
1526
2305
|
async solveChallenge(request: BrowserChallengeRequest): Promise<BrowserChallengeResult> {
|
|
2306
|
+
observeBrowserTelemetry(this.telemetry, () => this.telemetry?.recordEngine(this._engine));
|
|
1527
2307
|
if (request.type !== "recaptcha") {
|
|
2308
|
+
observeBrowserTelemetry(this.telemetry, () =>
|
|
2309
|
+
this.telemetry?.recordError("BROWSER_RUNTIME_UNSUPPORTED"),
|
|
2310
|
+
);
|
|
1528
2311
|
throw new ProviderError(`Unsupported browser challenge: ${request.type}`, {
|
|
1529
2312
|
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
1530
2313
|
});
|