@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/stealth.ts
CHANGED
|
@@ -1,26 +1,49 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import { Cookie } from "tough-cookie";
|
|
3
|
+
import type {
|
|
4
|
+
BrowserProfile,
|
|
5
|
+
EmulationOS,
|
|
6
|
+
RequestInit as WreqRequestInit,
|
|
7
|
+
Session as WreqSession,
|
|
8
|
+
} from "wreq-js";
|
|
4
9
|
|
|
5
|
-
import type { ProxyResolutionOptions } from "../config/loader.js";
|
|
10
|
+
import type { ProxyResolutionOptions, ProxyVendorName } from "../config/loader.js";
|
|
6
11
|
import {
|
|
7
12
|
DEFAULT_SMARTPROXY_POOL_SIZE,
|
|
8
13
|
invalidateProxyResolutionCacheAsync,
|
|
9
14
|
ProxyResolutionError,
|
|
15
|
+
policyResolvesRegistryVendorChain,
|
|
16
|
+
resolvePolicyProxyPoolSpan,
|
|
17
|
+
resolvePolicyTransportAttemptCap,
|
|
10
18
|
resolveProxyConfigAsync,
|
|
11
|
-
|
|
19
|
+
vendorFromResolvedSource,
|
|
12
20
|
} from "../config/loader.js";
|
|
13
21
|
import { SDKError, TransportError } from "../errors.js";
|
|
14
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_STEALTH_BROWSER,
|
|
24
|
+
DEFAULT_STEALTH_OS,
|
|
25
|
+
getStealthProfile,
|
|
26
|
+
resolverClientProfileFamily,
|
|
27
|
+
resolveStealthProfileSelection,
|
|
28
|
+
} from "../stealth/profiles.js";
|
|
15
29
|
import type {
|
|
16
|
-
CookieJar,
|
|
17
30
|
HttpMethod,
|
|
18
31
|
StealthClient,
|
|
19
32
|
StealthFetchOptions,
|
|
33
|
+
StealthProfileDescriptor,
|
|
34
|
+
StealthProfileSelection,
|
|
20
35
|
StealthRedirectHop,
|
|
21
36
|
StealthResponse,
|
|
22
37
|
StealthSession,
|
|
23
38
|
} from "../types.js";
|
|
39
|
+
import { chrome149HeaderOrder } from "./chrome149-header-order.js";
|
|
40
|
+
import { registerDiagnosticValue } from "./diagnostic-env.js";
|
|
41
|
+
import { assertCallerHeadersSupported } from "./stealth-owned-headers.js";
|
|
42
|
+
import {
|
|
43
|
+
ENGINE_CEREMONY_EGRESS_LEASE,
|
|
44
|
+
type CeremonyEgressBinding,
|
|
45
|
+
type CeremonyEgressLeaseRuntime,
|
|
46
|
+
} from "./egress-lease.js";
|
|
24
47
|
import {
|
|
25
48
|
createProxyAuthIpDeniedError,
|
|
26
49
|
createProxyEdgeAuthRejectedError,
|
|
@@ -40,18 +63,64 @@ import {
|
|
|
40
63
|
computeProxyAttemptIndex,
|
|
41
64
|
computeProxyTransportRetryDelayMs,
|
|
42
65
|
createDefaultProxyTransportRetryOptions,
|
|
66
|
+
isProxyTransportRetryMethod,
|
|
43
67
|
normalizeProxyTransportRetryOptions,
|
|
44
68
|
shouldRetryProxyTransportAttempt,
|
|
45
69
|
validateUnsafeProxyTransportRetryMethods,
|
|
46
70
|
} from "./proxy-retry-policy.js";
|
|
47
|
-
import {
|
|
71
|
+
import { redirectHopHeaders } from "./redirect-hop-headers.js";
|
|
72
|
+
import {
|
|
73
|
+
evaluateRedirectHop,
|
|
74
|
+
invalidRedirectPolicy,
|
|
75
|
+
isRedirectStatus,
|
|
76
|
+
nextRedirectMethod,
|
|
77
|
+
nextSameOriginRedirectHop,
|
|
78
|
+
normalizeRedirectPolicy,
|
|
79
|
+
type RedirectPolicyTransport,
|
|
80
|
+
resolveRedirectUrl,
|
|
81
|
+
} from "./redirects.js";
|
|
82
|
+
import {
|
|
83
|
+
isSensitiveKey,
|
|
84
|
+
normalizeSensitiveParams,
|
|
85
|
+
redactSensitiveError,
|
|
86
|
+
redactSensitiveRequestError,
|
|
87
|
+
redactSensitiveText,
|
|
88
|
+
redactUrlQueryParams,
|
|
89
|
+
serializeRequestUrl,
|
|
90
|
+
} from "./request-options.js";
|
|
91
|
+
import type { ResolverVendorTransport } from "./resolver-vendors/types.js";
|
|
92
|
+
import {
|
|
93
|
+
type AkamaiSbsdSessionState,
|
|
94
|
+
akamaiSbsdChallengeKey,
|
|
95
|
+
detectAkamaiSbsdChallenge,
|
|
96
|
+
type StealthChallengeRuntime,
|
|
97
|
+
} from "./stealth-akamai-sbsd.js";
|
|
98
|
+
import { StealthCookieJar } from "./stealth-cookies.js";
|
|
99
|
+
|
|
100
|
+
export type {
|
|
101
|
+
StealthTelemetryAttemptEvent,
|
|
102
|
+
StealthTelemetryAttemptKind,
|
|
103
|
+
StealthTelemetryDiagnostics,
|
|
104
|
+
StealthTelemetryErrorCode,
|
|
105
|
+
StealthTelemetryRequestClass,
|
|
106
|
+
StealthTelemetrySbsdEvent,
|
|
107
|
+
StealthTelemetrySbsdOutcome,
|
|
108
|
+
StealthTelemetrySink,
|
|
109
|
+
} from "./stealth-telemetry.js";
|
|
110
|
+
import type {
|
|
111
|
+
StealthTelemetryAttemptKind,
|
|
112
|
+
StealthTelemetryErrorCode,
|
|
113
|
+
StealthTelemetrySink,
|
|
114
|
+
} from "./stealth-telemetry.js";
|
|
48
115
|
|
|
49
|
-
const
|
|
116
|
+
export const DEFAULT_STEALTH_PROFILE: StealthProfileDescriptor = Object.freeze({
|
|
117
|
+
browser: DEFAULT_STEALTH_BROWSER,
|
|
118
|
+
os: DEFAULT_STEALTH_OS,
|
|
119
|
+
});
|
|
50
120
|
|
|
51
121
|
const MISSING_PROXY_WARNING =
|
|
52
122
|
"[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
|
|
53
123
|
|
|
54
|
-
const MAX_POLICY_PROXY_RETRY_ATTEMPTS = SMARTPROXY_MAX_POOL_SIZE;
|
|
55
124
|
const MAX_POLICY_PROXY_POOL_REFRESHES = 1;
|
|
56
125
|
const PROXY_CONNECT_FAILURE_CODE = "proxy_connect_failed";
|
|
57
126
|
const PROXY_CONNECT_FAILURE_BODY_PATTERN =
|
|
@@ -59,9 +128,54 @@ const PROXY_CONNECT_FAILURE_BODY_PATTERN =
|
|
|
59
128
|
const PROXY_AUTH_DIAGNOSTIC_URL = "http://example.com/";
|
|
60
129
|
const PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS = 5_000;
|
|
61
130
|
const STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES = [PROXY_CONNECT_FAILURE_CODE] as const;
|
|
131
|
+
// Failures that mean the bound ceremony endpoint did not carry the request to the origin.
|
|
132
|
+
// Proxy-level refusals are covered by isProxyPoolRefreshableError; PROXY_AUTH_IP_DENIED is
|
|
133
|
+
// deliberately absent because another endpoint of the same vendor is denied just the same.
|
|
134
|
+
const BOUND_EGRESS_TRANSPORT_FAILURE_CODES: ReadonlySet<string> = new Set([
|
|
135
|
+
PROXY_CONNECT_FAILURE_CODE,
|
|
136
|
+
"transport_network_error",
|
|
137
|
+
"transport_timeout",
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
function isBoundEgressTransportFailure(error: TransportError): boolean {
|
|
141
|
+
return (
|
|
142
|
+
isProxyPoolRefreshableError(error) ||
|
|
143
|
+
(error.code !== undefined && BOUND_EGRESS_TRANSPORT_FAILURE_CODES.has(error.code))
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const MAX_STEALTH_REDIRECT_HOPS = 10;
|
|
147
|
+
const STEALTH_REDIRECT_POLICY_TRANSPORT: RedirectPolicyTransport = {
|
|
148
|
+
label: "ctx.stealth",
|
|
149
|
+
maxHops: MAX_STEALTH_REDIRECT_HOPS,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
function isRecord(value: unknown): value is Record<PropertyKey, unknown> {
|
|
153
|
+
return typeof value === "object" && value !== null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function sensitiveQueryParamNames(url: string): string[] {
|
|
157
|
+
const queryStart = url.indexOf("?");
|
|
158
|
+
if (queryStart === -1) return [];
|
|
159
|
+
const fragmentStart = url.indexOf("#", queryStart);
|
|
160
|
+
const query = url.slice(queryStart + 1, fragmentStart === -1 ? undefined : fragmentStart);
|
|
161
|
+
return [...new URLSearchParams(query).keys()].filter(isSensitiveKey);
|
|
162
|
+
}
|
|
62
163
|
|
|
63
164
|
export type StealthClientOptions = ProxyResolutionOptions & {
|
|
165
|
+
/** Request-scoped stealth transport observations. */
|
|
166
|
+
stealthTelemetry?: StealthTelemetrySink;
|
|
64
167
|
warn?: (message: string) => void;
|
|
168
|
+
/** Abort all requests issued by this client. */
|
|
169
|
+
signal?: AbortSignal;
|
|
170
|
+
/** Browser identity and declaration-wide HTTP language defaults. */
|
|
171
|
+
stealth?: StealthProfileSelection & {
|
|
172
|
+
/**
|
|
173
|
+
* Default Accept-Language value, emitted where Chrome's network layer appends
|
|
174
|
+
* it (after Accept-Encoding). Requests may override it through `headers`, in
|
|
175
|
+
* which case it is placed like any other caller header.
|
|
176
|
+
*/
|
|
177
|
+
acceptLanguage?: string;
|
|
178
|
+
};
|
|
65
179
|
/**
|
|
66
180
|
* Proxy-only stealth transport overrides. Use only for upstream proxy products
|
|
67
181
|
* that terminate CONNECT with a private CA instead of tunneling the origin
|
|
@@ -70,225 +184,561 @@ export type StealthClientOptions = ProxyResolutionOptions & {
|
|
|
70
184
|
proxyStealth?: { insecureSkipVerify?: boolean };
|
|
71
185
|
};
|
|
72
186
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
187
|
+
/** Server-attached challenge wiring and engine lease; kept off the public StealthClientOptions type. */
|
|
188
|
+
type StealthSessionClientOptions = StealthClientOptions & {
|
|
189
|
+
stealth?: StealthClientOptions["stealth"] & {
|
|
190
|
+
readonly challengeRuntime?: StealthChallengeRuntime;
|
|
191
|
+
};
|
|
192
|
+
readonly [ENGINE_CEREMONY_EGRESS_LEASE]?: CeremonyEgressLeaseRuntime;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
type ChallengedReplayRecord = {
|
|
196
|
+
consumed: boolean;
|
|
197
|
+
/** Ceremony egress generation the challenged request ran on; a rebind retires the record. */
|
|
198
|
+
readonly generation: number;
|
|
199
|
+
replay(): Promise<StealthResponse>;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
function assertAkamaiSbsdClientProfile(
|
|
203
|
+
declared: string | undefined,
|
|
204
|
+
session: StealthProfileDescriptor,
|
|
205
|
+
): void {
|
|
206
|
+
if (declared === undefined || resolverClientProfileFamily(declared) === session.browser) return;
|
|
207
|
+
throw new SDKError(
|
|
208
|
+
`Resolver client profile "${declared}" does not match the initiating stealth session browser "${session.browser}"`,
|
|
209
|
+
{
|
|
210
|
+
code: "RESOLVER_CLIENT_PROFILE_MISMATCH",
|
|
211
|
+
fix: "Make resolver.clientProfile name the provider stealth browser family (chrome, firefox, or safari).",
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
}
|
|
86
215
|
|
|
87
|
-
type
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
100: "chrome100",
|
|
92
|
-
101: "chrome101",
|
|
93
|
-
104: "chrome104",
|
|
94
|
-
107: "chrome107",
|
|
95
|
-
110: "chrome110",
|
|
96
|
-
116: "chrome116",
|
|
97
|
-
124: "chrome124",
|
|
98
|
-
125: "chrome125",
|
|
99
|
-
131: "chrome131",
|
|
100
|
-
136: "chrome136",
|
|
101
|
-
142: "chrome142",
|
|
216
|
+
type StealthTransportHeaders = {
|
|
217
|
+
entries(): IterableIterator<[string, string]>;
|
|
218
|
+
get(name: string): string | null;
|
|
219
|
+
getSetCookie?: () => string[];
|
|
102
220
|
};
|
|
103
221
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
222
|
+
type StealthTransportBody = {
|
|
223
|
+
cancel(): Promise<void>;
|
|
224
|
+
getReader(): {
|
|
225
|
+
read(): Promise<{ done: boolean; value?: Uint8Array }>;
|
|
226
|
+
cancel(): Promise<void>;
|
|
227
|
+
releaseLock(): void;
|
|
228
|
+
};
|
|
109
229
|
};
|
|
110
230
|
|
|
111
|
-
type StealthTransportResponse =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
231
|
+
type StealthTransportResponse = {
|
|
232
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
233
|
+
headers: StealthTransportHeaders;
|
|
234
|
+
status: number;
|
|
235
|
+
body?: StealthTransportBody | null;
|
|
115
236
|
url?: string;
|
|
116
237
|
redirected?: boolean;
|
|
117
238
|
};
|
|
118
239
|
|
|
119
|
-
type StealthMethod =
|
|
120
|
-
type StealthRequestInit =
|
|
240
|
+
type StealthMethod = HttpMethod | "TRACE";
|
|
241
|
+
type StealthRequestInit = WreqRequestInit & {
|
|
121
242
|
redirect?: NonNullable<StealthFetchOptions["redirect"]>;
|
|
122
243
|
};
|
|
123
244
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
245
|
+
type WreqSessionCacheEntry = {
|
|
246
|
+
session: Promise<WreqSession>;
|
|
247
|
+
tail: Promise<void>;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
type WreqModule = typeof import("wreq-js");
|
|
127
251
|
|
|
128
|
-
|
|
129
|
-
private readonly cookies: Record<string, string>;
|
|
252
|
+
let wreqModulePromise: Promise<WreqModule> | undefined;
|
|
130
253
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
254
|
+
function getWreqModule(): Promise<WreqModule> {
|
|
255
|
+
if (!wreqModulePromise) {
|
|
256
|
+
wreqModulePromise = import("wreq-js").catch((error: unknown) => {
|
|
257
|
+
throw new SDKError(
|
|
258
|
+
`Stealth transport is unavailable on ${process.platform}-${process.arch}: the wreq-js native binary could not be loaded.`,
|
|
259
|
+
{
|
|
260
|
+
code: "stealth_transport_unavailable",
|
|
261
|
+
cause: error instanceof Error ? error : undefined,
|
|
262
|
+
},
|
|
263
|
+
);
|
|
264
|
+
});
|
|
134
265
|
}
|
|
135
266
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const [nameValue] = cookieString.split(";");
|
|
139
|
-
if (!nameValue) {
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
267
|
+
return wreqModulePromise;
|
|
268
|
+
}
|
|
142
269
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
270
|
+
function parseProfileIdentifier(identifier: string): {
|
|
271
|
+
family: string;
|
|
272
|
+
version: number[];
|
|
273
|
+
} | null {
|
|
274
|
+
const match =
|
|
275
|
+
/^(safari_ios|safari_ipad|firefox_android|firefox_private|chrome|edge|firefox|opera|safari|okhttp)_(\d+(?:[._]\d+)*)$/.exec(
|
|
276
|
+
identifier.toLowerCase(),
|
|
277
|
+
);
|
|
278
|
+
if (!match?.[1] || !match[2]) return null;
|
|
279
|
+
return {
|
|
280
|
+
family: match[1],
|
|
281
|
+
version: match[2].split(/[._]/).map(Number),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
147
284
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
285
|
+
function compareVersionDistance(target: number[], left: number[], right: number[]): number {
|
|
286
|
+
const width = Math.max(target.length, left.length, right.length);
|
|
287
|
+
for (let index = 0; index < width; index += 1) {
|
|
288
|
+
const targetPart = target[index] ?? 0;
|
|
289
|
+
const leftDistance = Math.abs((left[index] ?? 0) - targetPart);
|
|
290
|
+
const rightDistance = Math.abs((right[index] ?? 0) - targetPart);
|
|
291
|
+
if (leftDistance !== rightDistance) return leftDistance - rightDistance;
|
|
152
292
|
}
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
153
295
|
|
|
154
|
-
|
|
155
|
-
|
|
296
|
+
function closestWreqProfile(
|
|
297
|
+
identifier: string,
|
|
298
|
+
wreqProfiles: readonly BrowserProfile[],
|
|
299
|
+
): BrowserProfile | undefined {
|
|
300
|
+
const requested = parseProfileIdentifier(identifier);
|
|
301
|
+
if (!requested) return undefined;
|
|
302
|
+
|
|
303
|
+
let closest: { name: BrowserProfile; version: number[] } | undefined;
|
|
304
|
+
for (const candidateName of wreqProfiles) {
|
|
305
|
+
const candidate = parseProfileIdentifier(candidateName);
|
|
306
|
+
if (!candidate || candidate.family !== requested.family) continue;
|
|
307
|
+
if (
|
|
308
|
+
!closest ||
|
|
309
|
+
compareVersionDistance(requested.version, candidate.version, closest.version) < 0
|
|
310
|
+
) {
|
|
311
|
+
closest = { name: candidateName, version: candidate.version };
|
|
312
|
+
}
|
|
156
313
|
}
|
|
314
|
+
return closest?.name;
|
|
315
|
+
}
|
|
157
316
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
317
|
+
export function resolveWreqProfile(
|
|
318
|
+
selection: StealthProfileSelection,
|
|
319
|
+
wreqProfiles: readonly BrowserProfile[],
|
|
320
|
+
): {
|
|
321
|
+
browser: BrowserProfile;
|
|
322
|
+
os: EmulationOS;
|
|
323
|
+
} {
|
|
324
|
+
const profile = getStealthProfile(selection);
|
|
325
|
+
const identifier = profile.tlsClientIdentifier?.toLowerCase() ?? "";
|
|
326
|
+
const os = profile.os;
|
|
161
327
|
|
|
162
|
-
|
|
163
|
-
|
|
328
|
+
const browser = closestWreqProfile(identifier, wreqProfiles);
|
|
329
|
+
if (!browser) {
|
|
330
|
+
throw new SDKError(
|
|
331
|
+
`Stealth profile ${profile.browser}/${profile.os} cannot be mapped to a wreq-js browser profile.`,
|
|
332
|
+
);
|
|
164
333
|
}
|
|
334
|
+
return { browser, os };
|
|
335
|
+
}
|
|
165
336
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
.join("; ");
|
|
170
|
-
}
|
|
337
|
+
function resolveUrl(baseUrl: string, url: string): string {
|
|
338
|
+
return new URL(url, baseUrl).toString();
|
|
339
|
+
}
|
|
171
340
|
|
|
172
|
-
|
|
173
|
-
|
|
341
|
+
function resolveRequestUrl(baseUrl: string, url: string): string {
|
|
342
|
+
try {
|
|
343
|
+
return resolveUrl(baseUrl, url);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
// Both inputs are caller-owned, so an unparsable URL is a programming error, never a
|
|
346
|
+
// transport condition: not retryable, not the upstream's fault. Classify it here rather
|
|
347
|
+
// than in normalizeStealthTransportError, which also sees ERR_INVALID_URL from malformed
|
|
348
|
+
// upstream redirect targets. The fixed message keeps the URL (and any inline query
|
|
349
|
+
// values) out of the diagnostic surface; the runtime TypeError stays in `cause`.
|
|
350
|
+
throw new TransportError("Invalid request URL", {
|
|
351
|
+
code: "transport_invalid_url",
|
|
352
|
+
status: 0,
|
|
353
|
+
category: "provider_error",
|
|
354
|
+
retryable: false,
|
|
355
|
+
...(error instanceof Error ? { cause: error } : {}),
|
|
356
|
+
});
|
|
174
357
|
}
|
|
358
|
+
}
|
|
175
359
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
360
|
+
function headerEntriesFromHeaders(headers: StealthTransportHeaders): [string, string][] {
|
|
361
|
+
return Array.from(headers.entries());
|
|
362
|
+
}
|
|
179
363
|
|
|
180
|
-
|
|
181
|
-
this.clear();
|
|
182
|
-
for (const [name, value] of Object.entries(cookies)) {
|
|
183
|
-
if (name) this.cookies[name] = value;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
364
|
+
type HeaderTuple = [string, string];
|
|
186
365
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
366
|
+
function normalizeHeaders(
|
|
367
|
+
headers: Record<string, string | string[] | undefined>,
|
|
368
|
+
): Record<string, string> {
|
|
369
|
+
return Object.fromEntries(normalizedHeaderEntries(headers));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function trimOuterHttpWhitespace(value: string): string {
|
|
373
|
+
return value.replace(/^[\t\n\r ]+|[\t\n\r ]+$/g, "");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function normalizedHeaderEntries(
|
|
377
|
+
headers: Record<string, string | string[] | undefined>,
|
|
378
|
+
): HeaderTuple[] {
|
|
379
|
+
const entries: HeaderTuple[] = [];
|
|
380
|
+
const indices = new Map<string, number>();
|
|
381
|
+
for (const [originalName, originalValue] of Object.entries(headers)) {
|
|
382
|
+
if (originalValue === undefined) continue;
|
|
383
|
+
const name = originalName.toLowerCase();
|
|
384
|
+
const value = (Array.isArray(originalValue) ? originalValue : [originalValue])
|
|
385
|
+
.map(trimOuterHttpWhitespace)
|
|
386
|
+
.join(", ");
|
|
387
|
+
const existingIndex = indices.get(name);
|
|
388
|
+
if (existingIndex === undefined) {
|
|
389
|
+
indices.set(name, entries.length);
|
|
390
|
+
entries.push([name, value]);
|
|
391
|
+
continue;
|
|
190
392
|
}
|
|
393
|
+
const existing = entries[existingIndex];
|
|
394
|
+
if (existing) existing[1] = `${existing[1]}, ${value}`;
|
|
191
395
|
}
|
|
396
|
+
return entries;
|
|
397
|
+
}
|
|
192
398
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
399
|
+
// Non-pseudo-header order of real Chrome 149. The fixture captures
|
|
400
|
+
// (chrome-ground-truth-capture.json, chrome-extended-capture.json,
|
|
401
|
+
// h1-casing-capture.json) were taken through Playwright's `locale` option, which
|
|
402
|
+
// installs Accept-Language via DevTools next to User-Agent; real Chrome only
|
|
403
|
+
// receives it from //net (URLRequestHttpJob::AddExtraHeaders), after
|
|
404
|
+
// Accept-Encoding and before Cookie, as al-placement-capture.json B/C show.
|
|
405
|
+
// chrome-form-post-capture.json was taken without that option and needs no
|
|
406
|
+
// adjustment.
|
|
407
|
+
const CHROME_HEADER_ORDERS = {
|
|
408
|
+
navigation: [
|
|
409
|
+
"sec-ch-ua",
|
|
410
|
+
"sec-ch-ua-mobile",
|
|
411
|
+
"sec-ch-ua-platform",
|
|
412
|
+
"upgrade-insecure-requests",
|
|
413
|
+
"user-agent",
|
|
414
|
+
"accept",
|
|
415
|
+
"sec-fetch-site",
|
|
416
|
+
"sec-fetch-mode",
|
|
417
|
+
"sec-fetch-user",
|
|
418
|
+
"sec-fetch-dest",
|
|
419
|
+
"accept-encoding",
|
|
420
|
+
"accept-language",
|
|
421
|
+
"cookie",
|
|
422
|
+
"priority",
|
|
423
|
+
],
|
|
424
|
+
// xhr has no fixed table: it is emulated by chrome149HeaderOrder.
|
|
425
|
+
post: [
|
|
426
|
+
"content-length",
|
|
427
|
+
"sec-ch-ua-platform",
|
|
428
|
+
"user-agent",
|
|
429
|
+
"sec-ch-ua",
|
|
430
|
+
"content-type",
|
|
431
|
+
"sec-ch-ua-mobile",
|
|
432
|
+
"accept",
|
|
433
|
+
"origin",
|
|
434
|
+
"sec-fetch-site",
|
|
435
|
+
"sec-fetch-mode",
|
|
436
|
+
"sec-fetch-dest",
|
|
437
|
+
"referer",
|
|
438
|
+
"accept-encoding",
|
|
439
|
+
"accept-language",
|
|
440
|
+
"cookie",
|
|
441
|
+
"priority",
|
|
442
|
+
],
|
|
443
|
+
"form-post": [
|
|
444
|
+
"content-length",
|
|
445
|
+
"cache-control",
|
|
446
|
+
"sec-ch-ua",
|
|
447
|
+
"sec-ch-ua-mobile",
|
|
448
|
+
"sec-ch-ua-platform",
|
|
449
|
+
"upgrade-insecure-requests",
|
|
450
|
+
"content-type",
|
|
451
|
+
"user-agent",
|
|
452
|
+
"origin",
|
|
453
|
+
"accept",
|
|
454
|
+
"sec-fetch-site",
|
|
455
|
+
"sec-fetch-mode",
|
|
456
|
+
"sec-fetch-user",
|
|
457
|
+
"sec-fetch-dest",
|
|
458
|
+
"referer",
|
|
459
|
+
"accept-encoding",
|
|
460
|
+
"accept-language",
|
|
461
|
+
"cookie",
|
|
462
|
+
"priority",
|
|
463
|
+
],
|
|
464
|
+
} as const satisfies Record<Exclude<ChromeRequestClass, "xhr">, readonly string[]>;
|
|
465
|
+
|
|
466
|
+
const CHROME_H1_HEADER_ORDERS = {
|
|
467
|
+
navigation: [
|
|
468
|
+
"host",
|
|
469
|
+
"connection",
|
|
470
|
+
"sec-ch-ua",
|
|
471
|
+
"sec-ch-ua-mobile",
|
|
472
|
+
"sec-ch-ua-platform",
|
|
473
|
+
"upgrade-insecure-requests",
|
|
474
|
+
"user-agent",
|
|
475
|
+
"accept",
|
|
476
|
+
"sec-fetch-site",
|
|
477
|
+
"sec-fetch-mode",
|
|
478
|
+
"sec-fetch-user",
|
|
479
|
+
"sec-fetch-dest",
|
|
480
|
+
"accept-encoding",
|
|
481
|
+
"accept-language",
|
|
482
|
+
"cookie",
|
|
483
|
+
],
|
|
484
|
+
xhr: [
|
|
485
|
+
"host",
|
|
486
|
+
"connection",
|
|
487
|
+
"sec-ch-ua-platform",
|
|
488
|
+
"cache-control",
|
|
489
|
+
"x-requested-with",
|
|
490
|
+
"user-agent",
|
|
491
|
+
"sec-ch-ua",
|
|
492
|
+
"sec-ch-ua-mobile",
|
|
493
|
+
"accept",
|
|
494
|
+
"sec-fetch-site",
|
|
495
|
+
"sec-fetch-mode",
|
|
496
|
+
"sec-fetch-dest",
|
|
497
|
+
"referer",
|
|
498
|
+
"accept-encoding",
|
|
499
|
+
"accept-language",
|
|
500
|
+
"cookie",
|
|
501
|
+
],
|
|
502
|
+
post: [
|
|
503
|
+
"host",
|
|
504
|
+
"connection",
|
|
505
|
+
"content-length",
|
|
506
|
+
"sec-ch-ua-platform",
|
|
507
|
+
"user-agent",
|
|
508
|
+
"sec-ch-ua",
|
|
509
|
+
"content-type",
|
|
510
|
+
"sec-ch-ua-mobile",
|
|
511
|
+
"accept",
|
|
512
|
+
"origin",
|
|
513
|
+
"sec-fetch-site",
|
|
514
|
+
"sec-fetch-mode",
|
|
515
|
+
"sec-fetch-dest",
|
|
516
|
+
"referer",
|
|
517
|
+
"accept-encoding",
|
|
518
|
+
"accept-language",
|
|
519
|
+
"cookie",
|
|
520
|
+
],
|
|
521
|
+
"form-post": [
|
|
522
|
+
"host",
|
|
523
|
+
"connection",
|
|
524
|
+
"content-length",
|
|
525
|
+
"cache-control",
|
|
526
|
+
"sec-ch-ua",
|
|
527
|
+
"sec-ch-ua-mobile",
|
|
528
|
+
"sec-ch-ua-platform",
|
|
529
|
+
"upgrade-insecure-requests",
|
|
530
|
+
"content-type",
|
|
531
|
+
"user-agent",
|
|
532
|
+
"origin",
|
|
533
|
+
"accept",
|
|
534
|
+
"sec-fetch-site",
|
|
535
|
+
"sec-fetch-mode",
|
|
536
|
+
"sec-fetch-user",
|
|
537
|
+
"sec-fetch-dest",
|
|
538
|
+
"referer",
|
|
539
|
+
"accept-encoding",
|
|
540
|
+
"accept-language",
|
|
541
|
+
"cookie",
|
|
542
|
+
],
|
|
543
|
+
} as const satisfies Record<ChromeRequestClass, readonly string[]>;
|
|
544
|
+
|
|
545
|
+
const CHROME_H1_HEADER_NAMES: Record<string, string> = {
|
|
546
|
+
host: "Host",
|
|
547
|
+
connection: "Connection",
|
|
548
|
+
"upgrade-insecure-requests": "Upgrade-Insecure-Requests",
|
|
549
|
+
"user-agent": "User-Agent",
|
|
550
|
+
"accept-language": "Accept-Language",
|
|
551
|
+
accept: "Accept",
|
|
552
|
+
"sec-fetch-site": "Sec-Fetch-Site",
|
|
553
|
+
"sec-fetch-mode": "Sec-Fetch-Mode",
|
|
554
|
+
"sec-fetch-user": "Sec-Fetch-User",
|
|
555
|
+
"sec-fetch-dest": "Sec-Fetch-Dest",
|
|
556
|
+
"accept-encoding": "Accept-Encoding",
|
|
557
|
+
cookie: "Cookie",
|
|
558
|
+
"cache-control": "Cache-Control",
|
|
559
|
+
"x-requested-with": "X-Requested-With",
|
|
560
|
+
referer: "Referer",
|
|
561
|
+
"content-length": "Content-Length",
|
|
562
|
+
"content-type": "Content-Type",
|
|
563
|
+
origin: "Origin",
|
|
564
|
+
range: "Range",
|
|
565
|
+
};
|
|
200
566
|
|
|
201
|
-
|
|
567
|
+
type ChromeRequestClass = NonNullable<NonNullable<StealthFetchOptions["stealth"]>["requestClass"]>;
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* What each request class sends besides its header order, one row per public
|
|
571
|
+
* `stealth.requestClass`:
|
|
572
|
+
*
|
|
573
|
+
* - `fetchMetadata: "navigate"` is a document navigation: `Sec-Fetch-Mode:
|
|
574
|
+
* navigate`, `Sec-Fetch-Dest: document`, `Sec-Fetch-User: ?1` unless
|
|
575
|
+
* `userActivation` is false, `Upgrade-Insecure-Requests: 1`, and the
|
|
576
|
+
* profile's `Accept` and `Priority`. `"cors"` is a script fetch: `cors` /
|
|
577
|
+
* `empty`, `Accept: *\/*`, `Priority: u=1, i`.
|
|
578
|
+
* - `bodyful` sends `Content-Length`.
|
|
579
|
+
* - `validateCache` sends `Cache-Control: max-age=0`, which Chrome adds to a
|
|
580
|
+
* form submission (chrome-form-post-capture.json).
|
|
581
|
+
*/
|
|
582
|
+
const CHROME_REQUEST_CLASSES = {
|
|
583
|
+
navigation: { fetchMetadata: "navigate", bodyful: false, validateCache: false },
|
|
584
|
+
"form-post": { fetchMetadata: "navigate", bodyful: true, validateCache: true },
|
|
585
|
+
xhr: { fetchMetadata: "cors", bodyful: false, validateCache: false },
|
|
586
|
+
post: { fetchMetadata: "cors", bodyful: true, validateCache: false },
|
|
587
|
+
} as const satisfies Record<
|
|
588
|
+
ChromeRequestClass,
|
|
589
|
+
{ fetchMetadata: "navigate" | "cors"; bodyful: boolean; validateCache: boolean }
|
|
590
|
+
>;
|
|
591
|
+
|
|
592
|
+
function chromeHeaderOrder(
|
|
593
|
+
requestClass: ChromeRequestClass,
|
|
594
|
+
isHttp1: boolean,
|
|
595
|
+
caller: ReadonlyMap<string, string>,
|
|
596
|
+
): readonly string[] {
|
|
597
|
+
if (isHttp1) return CHROME_H1_HEADER_ORDERS[requestClass];
|
|
598
|
+
if (requestClass !== "xhr") return CHROME_HEADER_ORDERS[requestClass];
|
|
599
|
+
// Cookie and Referer are forbidden Fetch headers, so they never enter the page
|
|
600
|
+
// Fetch Headers map the emulator models. Range does occupy a map bucket
|
|
601
|
+
// (m1-capture.json range_*), but HttpCache::Transaction removes it from the
|
|
602
|
+
// request headers and PartialData re-adds it at the tail.
|
|
603
|
+
const order = chrome149HeaderOrder(
|
|
604
|
+
[...caller.keys()].filter((name) => name !== "cookie" && name !== "referer"),
|
|
605
|
+
);
|
|
606
|
+
if (caller.has("range")) order.splice(order.indexOf("range"), 1);
|
|
607
|
+
// //net appends Accept-Encoding and, unless the caller supplied one,
|
|
608
|
+
// Accept-Language (URLRequestHttpJob::AddExtraHeaders), then writes the Cookie
|
|
609
|
+
// header (SetCookieHeaderAndStart); the HTTP cache re-adds a caller Range after
|
|
610
|
+
// that, and the HTTP/2 Priority header stays last.
|
|
611
|
+
let insertAt = order.indexOf("accept-encoding") + 1;
|
|
612
|
+
if (order[insertAt] === "accept-language") insertAt += 1;
|
|
613
|
+
for (const name of ["cookie", "range"] as const) {
|
|
614
|
+
if (!caller.has(name)) continue;
|
|
615
|
+
order.splice(insertAt, 0, name);
|
|
616
|
+
insertAt += 1;
|
|
202
617
|
}
|
|
618
|
+
return order;
|
|
203
619
|
}
|
|
204
620
|
|
|
205
|
-
function
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
let closestBrowser: ImpitBrowser | undefined;
|
|
211
|
-
for (const [candidateMajorText, browser] of Object.entries(candidates)) {
|
|
212
|
-
const candidateMajor = Number(candidateMajorText);
|
|
213
|
-
if (
|
|
214
|
-
closestMajor === undefined ||
|
|
215
|
-
Math.abs(candidateMajor - major) < Math.abs(closestMajor - major)
|
|
216
|
-
) {
|
|
217
|
-
closestMajor = candidateMajor;
|
|
218
|
-
closestBrowser = browser;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return closestBrowser ?? "chrome142";
|
|
621
|
+
function normalizedCallerHeaderEntriesFromRecord(headers: Record<string, string>): HeaderTuple[] {
|
|
622
|
+
// The record has been through normalizeHeaders (lowercase, merged names); the
|
|
623
|
+
// lowercase here only guards later insertions such as the cookie jar's header.
|
|
624
|
+
assertCallerHeadersSupported(headers);
|
|
625
|
+
return Object.entries(headers).map(([name, value]) => [name.toLowerCase(), value]);
|
|
222
626
|
}
|
|
223
627
|
|
|
224
|
-
function
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
628
|
+
function chromeRequestClass(
|
|
629
|
+
method: StealthMethod,
|
|
630
|
+
requestedClass?: ChromeRequestClass,
|
|
631
|
+
): ChromeRequestClass {
|
|
632
|
+
if (requestedClass) return requestedClass;
|
|
633
|
+
if (method === "POST") return "post";
|
|
634
|
+
return "navigation";
|
|
635
|
+
}
|
|
228
636
|
|
|
229
|
-
|
|
637
|
+
function secFetchSite(requestUrl: string, referer: string | undefined): string {
|
|
638
|
+
if (!referer) return "none";
|
|
230
639
|
try {
|
|
231
|
-
|
|
640
|
+
return new URL(referer).origin === new URL(requestUrl).origin ? "same-origin" : "cross-site";
|
|
232
641
|
} catch {
|
|
233
|
-
|
|
234
|
-
// profile strings still run with the transport default instead of failing
|
|
235
|
-
// before the request starts. Removed built-in profile aliases above remain
|
|
236
|
-
// explicit errors so callers do not accidentally pin retired fingerprints.
|
|
237
|
-
return "chrome142";
|
|
642
|
+
return "cross-site";
|
|
238
643
|
}
|
|
644
|
+
}
|
|
239
645
|
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
if (
|
|
243
|
-
|
|
646
|
+
function requiredEmulationHeader(headers: ReadonlyMap<string, string>, name: string): string {
|
|
647
|
+
const value = headers.get(name);
|
|
648
|
+
if (value !== undefined) return value;
|
|
649
|
+
throw new SDKError(`wreq-js Chrome emulation exposes no ${name} header.`, {
|
|
650
|
+
code: "STEALTH_PROFILE_UNAVAILABLE",
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function buildChromeHeaderTuples(options: {
|
|
655
|
+
emulationHeaders: Iterable<[string, string]>;
|
|
656
|
+
method: StealthMethod;
|
|
657
|
+
body?: string | Buffer;
|
|
658
|
+
headers: Record<string, string>;
|
|
659
|
+
requestUrl: string;
|
|
660
|
+
acceptLanguage?: string;
|
|
661
|
+
requestClass?: ChromeRequestClass;
|
|
662
|
+
userActivation?: boolean;
|
|
663
|
+
}): HeaderTuple[] {
|
|
664
|
+
const callerEntries = normalizedCallerHeaderEntriesFromRecord(options.headers);
|
|
665
|
+
const caller = new Map(callerEntries);
|
|
666
|
+
const emulation = new Map(
|
|
667
|
+
Array.from(
|
|
668
|
+
options.emulationHeaders,
|
|
669
|
+
([name, value]) => [name.toLowerCase(), value] as HeaderTuple,
|
|
670
|
+
),
|
|
671
|
+
);
|
|
672
|
+
const requestClass = chromeRequestClass(options.method, options.requestClass);
|
|
673
|
+
const shape = CHROME_REQUEST_CLASSES[requestClass];
|
|
674
|
+
const referer = caller.get("referer");
|
|
675
|
+
const fetchSite = secFetchSite(options.requestUrl, referer);
|
|
676
|
+
const isNavigation = shape.fetchMetadata === "navigate";
|
|
677
|
+
const values = new Map<string, string>([
|
|
678
|
+
["sec-ch-ua", requiredEmulationHeader(emulation, "sec-ch-ua")],
|
|
679
|
+
["sec-ch-ua-mobile", requiredEmulationHeader(emulation, "sec-ch-ua-mobile")],
|
|
680
|
+
["sec-ch-ua-platform", requiredEmulationHeader(emulation, "sec-ch-ua-platform")],
|
|
681
|
+
["user-agent", requiredEmulationHeader(emulation, "user-agent")],
|
|
682
|
+
[
|
|
683
|
+
"accept-language",
|
|
684
|
+
caller.get("accept-language") ??
|
|
685
|
+
options.acceptLanguage ??
|
|
686
|
+
requiredEmulationHeader(emulation, "accept-language"),
|
|
687
|
+
],
|
|
688
|
+
["accept", isNavigation ? requiredEmulationHeader(emulation, "accept") : "*/*"],
|
|
689
|
+
[
|
|
690
|
+
"accept-encoding",
|
|
691
|
+
caller.has("range") ? "identity" : requiredEmulationHeader(emulation, "accept-encoding"),
|
|
692
|
+
],
|
|
693
|
+
["priority", isNavigation ? requiredEmulationHeader(emulation, "priority") : "u=1, i"],
|
|
694
|
+
["sec-fetch-site", fetchSite],
|
|
695
|
+
["sec-fetch-mode", isNavigation ? "navigate" : "cors"],
|
|
696
|
+
["sec-fetch-dest", isNavigation ? "document" : "empty"],
|
|
697
|
+
]);
|
|
698
|
+
if (isNavigation) {
|
|
699
|
+
values.set("upgrade-insecure-requests", "1");
|
|
700
|
+
if (options.userActivation !== false) values.set("sec-fetch-user", "?1");
|
|
244
701
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
702
|
+
if (shape.validateCache) values.set("cache-control", "max-age=0");
|
|
703
|
+
for (const [name, value] of callerEntries) values.set(name, value);
|
|
704
|
+
for (const name of ["content-type", "origin", "referer"] as const) {
|
|
705
|
+
const value = caller.get(name);
|
|
706
|
+
if (value !== undefined) values.set(name, value);
|
|
248
707
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
708
|
+
// A bodyful class keeps its class across a 301/302/303 that rewrites the method
|
|
709
|
+
// to GET (a form submission's redirect stays a validated navigation), but the
|
|
710
|
+
// rewritten request has no body and Chrome sends no Content-Length for it.
|
|
711
|
+
if (shape.bodyful && options.method !== "GET" && options.method !== "HEAD") {
|
|
712
|
+
values.set(
|
|
713
|
+
"content-length",
|
|
714
|
+
caller.get("content-length") ?? String(Buffer.byteLength(options.body ?? "")),
|
|
252
715
|
);
|
|
253
716
|
}
|
|
254
|
-
throw new SDKError(
|
|
255
|
-
`Stealth profile "${profileName}" cannot be mapped to an impit browser profile.`,
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function resolveUrl(baseUrl: string, url: string): string {
|
|
260
|
-
return new URL(url, baseUrl).toString();
|
|
261
|
-
}
|
|
262
717
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
for (const
|
|
718
|
+
const isHttp1 = new URL(options.requestUrl).protocol === "http:";
|
|
719
|
+
if (isHttp1) {
|
|
720
|
+
values.set("host", new URL(options.requestUrl).host);
|
|
721
|
+
values.set("connection", "keep-alive");
|
|
722
|
+
}
|
|
723
|
+
const order = chromeHeaderOrder(requestClass, isHttp1, caller);
|
|
724
|
+
const tuples: HeaderTuple[] = [];
|
|
725
|
+
const placed = new Set<string>();
|
|
726
|
+
for (const name of order) {
|
|
727
|
+
const value = values.get(name);
|
|
272
728
|
if (value === undefined) continue;
|
|
273
|
-
|
|
729
|
+
tuples.push([isHttp1 ? (CHROME_H1_HEADER_NAMES[name] ?? name) : name, value]);
|
|
730
|
+
placed.add(name);
|
|
274
731
|
}
|
|
275
|
-
|
|
732
|
+
for (const [name, value] of callerEntries) {
|
|
733
|
+
if (placed.has(name)) continue;
|
|
734
|
+
tuples.push([isHttp1 ? (CHROME_H1_HEADER_NAMES[name] ?? name) : name, value]);
|
|
735
|
+
}
|
|
736
|
+
return tuples;
|
|
276
737
|
}
|
|
277
738
|
|
|
278
739
|
function hasOwn(object: object, key: string): boolean {
|
|
279
740
|
return Object.hasOwn(object, key);
|
|
280
741
|
}
|
|
281
|
-
function toImpitCookieJar(cookieJar: CookieJarImpl): NonNullable<ImpitOptions["cookieJar"]> {
|
|
282
|
-
return {
|
|
283
|
-
setCookie(cookie: string, _url: string, cb?: (error?: unknown) => void) {
|
|
284
|
-
cookieJar.setFromCookieStrings([cookie]);
|
|
285
|
-
if (typeof cb === "function") cb();
|
|
286
|
-
},
|
|
287
|
-
getCookieString(_url: string) {
|
|
288
|
-
return cookieJar.toString();
|
|
289
|
-
},
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
742
|
|
|
293
743
|
function assertNoUnsupportedFingerprintOverrides(options: unknown): void {
|
|
294
744
|
if (!isRecord(options)) return;
|
|
@@ -300,23 +750,52 @@ function assertNoUnsupportedFingerprintOverrides(options: unknown): void {
|
|
|
300
750
|
if (unsupported.length === 0) return;
|
|
301
751
|
|
|
302
752
|
throw new SDKError(
|
|
303
|
-
`ctx.stealth.fetch uses
|
|
753
|
+
`ctx.stealth.fetch uses transport-managed browser fingerprints and no longer accepts low-level stealth overrides: ${unsupported.join(", ")}. Use stealth.browser and stealth.os instead.`,
|
|
304
754
|
);
|
|
305
755
|
}
|
|
306
756
|
|
|
307
|
-
function responseHeadersToRecord(
|
|
757
|
+
function responseHeadersToRecord(
|
|
758
|
+
headers: StealthTransportHeaders,
|
|
759
|
+
): Record<string, string | string[] | undefined> {
|
|
308
760
|
const record: Record<string, string> = {};
|
|
309
761
|
for (const [name, value] of headers.entries()) record[name] = value;
|
|
310
762
|
return record;
|
|
311
763
|
}
|
|
312
764
|
|
|
313
|
-
function setCookieHeadersFromResponse(headers:
|
|
765
|
+
function setCookieHeadersFromResponse(headers: StealthTransportHeaders): string[] {
|
|
314
766
|
const getSetCookie = headers.getSetCookie;
|
|
315
767
|
if (typeof getSetCookie === "function") return getSetCookie.call(headers);
|
|
316
768
|
const setCookie = headers.get("set-cookie");
|
|
317
769
|
return setCookie ? splitCombinedSetCookieHeader(setCookie) : [];
|
|
318
770
|
}
|
|
319
771
|
|
|
772
|
+
function resolverCookiesFromResponse(
|
|
773
|
+
response: StealthTransportResponse,
|
|
774
|
+
requestUrl: string,
|
|
775
|
+
): Awaited<ReturnType<ResolverVendorTransport["fetch"]>>["cookies"] {
|
|
776
|
+
const responseUrl = response.url ?? requestUrl;
|
|
777
|
+
return setCookieHeadersFromResponse(response.headers).flatMap((cookieString) => {
|
|
778
|
+
const cookie = Cookie.parse(cookieString);
|
|
779
|
+
if (!cookie?.key) return [];
|
|
780
|
+
const expires =
|
|
781
|
+
cookie.expires instanceof Date && Number.isFinite(cookie.expires.getTime())
|
|
782
|
+
? cookie.expires.getTime() / 1_000
|
|
783
|
+
: undefined;
|
|
784
|
+
return [
|
|
785
|
+
{
|
|
786
|
+
name: cookie.key,
|
|
787
|
+
value: cookie.value,
|
|
788
|
+
...(expires === undefined ? {} : { expires }),
|
|
789
|
+
httpOnly: cookie.httpOnly,
|
|
790
|
+
secure: cookie.secure,
|
|
791
|
+
...(cookie.domain ? { domain: cookie.domain } : { domain: new URL(responseUrl).hostname }),
|
|
792
|
+
...(cookie.path ? { path: cookie.path } : {}),
|
|
793
|
+
...(cookie.sameSite ? { sameSite: cookie.sameSite } : {}),
|
|
794
|
+
},
|
|
795
|
+
];
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
320
799
|
function splitCombinedSetCookieHeader(headerValue: string): string[] {
|
|
321
800
|
const cookieNamePattern = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+=/;
|
|
322
801
|
const cookieStrings: string[] = [];
|
|
@@ -337,10 +816,26 @@ function splitCombinedSetCookieHeader(headerValue: string): string[] {
|
|
|
337
816
|
export async function normalizeResponse(
|
|
338
817
|
response: StealthTransportResponse,
|
|
339
818
|
requestUrl?: string,
|
|
819
|
+
maxBodyBytes?: number,
|
|
820
|
+
): Promise<StealthResponse> {
|
|
821
|
+
return normalizeResponseWithSignal(response, requestUrl, maxBodyBytes);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
async function normalizeResponseWithSignal(
|
|
825
|
+
response: StealthTransportResponse,
|
|
826
|
+
requestUrl?: string,
|
|
827
|
+
maxBodyBytes?: number,
|
|
828
|
+
signal?: AbortSignal,
|
|
340
829
|
): Promise<StealthResponse> {
|
|
341
830
|
const headers = Object.fromEntries(response.headers.entries());
|
|
342
|
-
const cookies = new
|
|
343
|
-
|
|
831
|
+
const cookies = new StealthCookieJar(
|
|
832
|
+
setCookieHeadersFromResponse(response.headers),
|
|
833
|
+
response.url ?? requestUrl,
|
|
834
|
+
);
|
|
835
|
+
const bodyBytes =
|
|
836
|
+
maxBodyBytes === undefined
|
|
837
|
+
? await readResponseArrayBuffer(response, signal)
|
|
838
|
+
: await readResponseBodyWithLimit(response, maxBodyBytes, signal);
|
|
344
839
|
const body = new TextDecoder().decode(bodyBytes);
|
|
345
840
|
|
|
346
841
|
return {
|
|
@@ -368,9 +863,147 @@ export async function normalizeResponse(
|
|
|
368
863
|
};
|
|
369
864
|
}
|
|
370
865
|
|
|
371
|
-
function
|
|
866
|
+
async function readResponseArrayBuffer(
|
|
867
|
+
response: StealthTransportResponse,
|
|
868
|
+
signal?: AbortSignal,
|
|
869
|
+
): Promise<ArrayBuffer> {
|
|
870
|
+
if (!signal) return response.arrayBuffer();
|
|
871
|
+
throwIfAmbientAborted(signal);
|
|
872
|
+
return new Promise((resolve, reject) => {
|
|
873
|
+
let settled = false;
|
|
874
|
+
const settle = (operation: () => void) => {
|
|
875
|
+
if (settled) return;
|
|
876
|
+
settled = true;
|
|
877
|
+
signal.removeEventListener("abort", onAbort);
|
|
878
|
+
operation();
|
|
879
|
+
};
|
|
880
|
+
const onAbort = () => {
|
|
881
|
+
const error = toAmbientCancellationError(signal);
|
|
882
|
+
try {
|
|
883
|
+
void response.body?.cancel().catch(() => undefined);
|
|
884
|
+
} catch {
|
|
885
|
+
// Preserve the cancellation error if accessing or cancelling the body fails.
|
|
886
|
+
}
|
|
887
|
+
settle(() => reject(error));
|
|
888
|
+
};
|
|
889
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
890
|
+
try {
|
|
891
|
+
void response.arrayBuffer().then(
|
|
892
|
+
(body) => settle(() => resolve(body)),
|
|
893
|
+
(error) => settle(() => reject(error)),
|
|
894
|
+
);
|
|
895
|
+
} catch (error) {
|
|
896
|
+
settle(() => reject(error));
|
|
897
|
+
}
|
|
898
|
+
if (signal.aborted) onAbort();
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function responseTooLargeError(maxBodyBytes: number, observedBytes: number): TransportError {
|
|
903
|
+
return new TransportError(
|
|
904
|
+
`Response body exceeded maxBodyBytes limit of ${maxBodyBytes} bytes (observed ${observedBytes} bytes)`,
|
|
905
|
+
{
|
|
906
|
+
code: "response_too_large",
|
|
907
|
+
category: "upstream_http",
|
|
908
|
+
retryable: false,
|
|
909
|
+
status: 0,
|
|
910
|
+
},
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function declaredContentLength(headers: StealthTransportHeaders): number | undefined {
|
|
915
|
+
const contentLength = headers.get("content-length")?.trim();
|
|
916
|
+
if (!contentLength || !/^\d+$/.test(contentLength)) return undefined;
|
|
917
|
+
const parsed = Number(contentLength);
|
|
918
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
async function readResponseBodyWithLimit(
|
|
922
|
+
response: StealthTransportResponse,
|
|
923
|
+
maxBodyBytes: number,
|
|
924
|
+
signal?: AbortSignal,
|
|
925
|
+
): Promise<ArrayBuffer> {
|
|
926
|
+
throwIfAmbientAborted(signal);
|
|
927
|
+
const contentLength = declaredContentLength(response.headers);
|
|
928
|
+
if (contentLength !== undefined && contentLength > maxBodyBytes) {
|
|
929
|
+
await response.body?.cancel().catch(() => undefined);
|
|
930
|
+
throw responseTooLargeError(maxBodyBytes, contentLength);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (!response.body) {
|
|
934
|
+
throw new TransportError("Response body stream is unavailable", {
|
|
935
|
+
code: "transport_stream_unavailable",
|
|
936
|
+
category: "upstream_http",
|
|
937
|
+
status: 0,
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const reader = response.body.getReader();
|
|
942
|
+
const chunks: Uint8Array[] = [];
|
|
943
|
+
let receivedBytes = 0;
|
|
944
|
+
try {
|
|
945
|
+
while (true) {
|
|
946
|
+
const { done, value } = await readResponseBodyChunk(reader, signal);
|
|
947
|
+
if (done) break;
|
|
948
|
+
if (!value) continue;
|
|
949
|
+
receivedBytes += value.byteLength;
|
|
950
|
+
if (receivedBytes > maxBodyBytes) {
|
|
951
|
+
await reader.cancel().catch(() => undefined);
|
|
952
|
+
throw responseTooLargeError(maxBodyBytes, receivedBytes);
|
|
953
|
+
}
|
|
954
|
+
chunks.push(value);
|
|
955
|
+
}
|
|
956
|
+
} finally {
|
|
957
|
+
reader.releaseLock();
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
return concatenateResponseBodyChunks(chunks, receivedBytes);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function concatenateResponseBodyChunks(
|
|
964
|
+
chunks: readonly Uint8Array[],
|
|
965
|
+
receivedBytes: number,
|
|
966
|
+
): ArrayBuffer {
|
|
967
|
+
const bodyBytes = new Uint8Array(receivedBytes);
|
|
968
|
+
let offset = 0;
|
|
969
|
+
for (const chunk of chunks) {
|
|
970
|
+
bodyBytes.set(chunk, offset);
|
|
971
|
+
offset += chunk.byteLength;
|
|
972
|
+
}
|
|
973
|
+
return bodyBytes.buffer;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function readResponseBodyChunk(
|
|
977
|
+
reader: ReturnType<StealthTransportBody["getReader"]>,
|
|
978
|
+
signal?: AbortSignal,
|
|
979
|
+
): Promise<{ done: boolean; value?: Uint8Array }> {
|
|
980
|
+
if (!signal) return reader.read();
|
|
981
|
+
throwIfAmbientAborted(signal);
|
|
982
|
+
return new Promise((resolve, reject) => {
|
|
983
|
+
let settled = false;
|
|
984
|
+
const settle = (operation: () => void) => {
|
|
985
|
+
if (settled) return;
|
|
986
|
+
settled = true;
|
|
987
|
+
signal.removeEventListener("abort", onAbort);
|
|
988
|
+
operation();
|
|
989
|
+
};
|
|
990
|
+
const onAbort = () => {
|
|
991
|
+
const error = toAmbientCancellationError(signal);
|
|
992
|
+
void reader.cancel().catch(() => undefined);
|
|
993
|
+
settle(() => reject(error));
|
|
994
|
+
};
|
|
995
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
996
|
+
void reader.read().then(
|
|
997
|
+
(chunk) => settle(() => resolve(chunk)),
|
|
998
|
+
(error) => settle(() => reject(error)),
|
|
999
|
+
);
|
|
1000
|
+
if (signal.aborted) onAbort();
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function normalizeBody(body: unknown): string | Buffer | undefined {
|
|
372
1005
|
if (body === undefined) {
|
|
373
|
-
return
|
|
1006
|
+
return undefined;
|
|
374
1007
|
}
|
|
375
1008
|
|
|
376
1009
|
if (typeof body === "string") {
|
|
@@ -378,10 +1011,15 @@ function normalizeBody(body: StealthFetchOptions["body"]): string {
|
|
|
378
1011
|
}
|
|
379
1012
|
|
|
380
1013
|
if (Buffer.isBuffer(body)) {
|
|
381
|
-
return
|
|
1014
|
+
return Buffer.from(body);
|
|
382
1015
|
}
|
|
383
1016
|
|
|
384
|
-
|
|
1017
|
+
// `StealthFetchOptions.body` is `string | Buffer`; anything else is a provider fault,
|
|
1018
|
+
// caught before the first request so a later replay can send the same bytes.
|
|
1019
|
+
throw new SDKError("Stealth request bodies must be a string or Buffer", {
|
|
1020
|
+
code: "STEALTH_BODY_UNSUPPORTED",
|
|
1021
|
+
fix: "Supply a string or Buffer body; streams and typed arrays are not snapshotted.",
|
|
1022
|
+
});
|
|
385
1023
|
}
|
|
386
1024
|
|
|
387
1025
|
function isPolicyManagedProxy(options: StealthClientOptions): boolean {
|
|
@@ -426,6 +1064,9 @@ type ResolvedAttemptProxy = {
|
|
|
426
1064
|
url?: string;
|
|
427
1065
|
poolIndex?: number;
|
|
428
1066
|
proxyHash?: string;
|
|
1067
|
+
vendor?: ProxyVendorName;
|
|
1068
|
+
refreshEpoch?: number;
|
|
1069
|
+
lifetimeMinutes?: number;
|
|
429
1070
|
};
|
|
430
1071
|
|
|
431
1072
|
function proxyPoolIndexFromDiagnostics(
|
|
@@ -438,6 +1079,14 @@ function proxyPoolIndexFromDiagnostics(
|
|
|
438
1079
|
return Math.floor(value);
|
|
439
1080
|
}
|
|
440
1081
|
|
|
1082
|
+
function positiveDiagnosticNumber(
|
|
1083
|
+
diagnostics: Record<string, string | number | boolean> | undefined,
|
|
1084
|
+
name: string,
|
|
1085
|
+
): number | undefined {
|
|
1086
|
+
const value = diagnostics?.[name];
|
|
1087
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
441
1090
|
function proxyEndpointHash(proxyUrl: string | undefined): string | undefined {
|
|
442
1091
|
if (!proxyUrl) return undefined;
|
|
443
1092
|
try {
|
|
@@ -532,8 +1181,41 @@ function normalizeStealthTransportError(error: unknown): TransportError {
|
|
|
532
1181
|
});
|
|
533
1182
|
}
|
|
534
1183
|
|
|
535
|
-
function
|
|
536
|
-
|
|
1184
|
+
function toAmbientCancellationError(
|
|
1185
|
+
signal: AbortSignal,
|
|
1186
|
+
error: unknown = signal.reason,
|
|
1187
|
+
): TransportError {
|
|
1188
|
+
if (error instanceof TransportError && error.code === "transport_cancelled") {
|
|
1189
|
+
return error;
|
|
1190
|
+
}
|
|
1191
|
+
return new TransportError("Request cancelled", {
|
|
1192
|
+
code: "transport_cancelled",
|
|
1193
|
+
status: 0,
|
|
1194
|
+
retryable: false,
|
|
1195
|
+
...(error !== undefined
|
|
1196
|
+
? { cause: error instanceof Error ? error : new Error(String(error)) }
|
|
1197
|
+
: {}),
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function throwIfAmbientAborted(signal: AbortSignal | undefined): void {
|
|
1202
|
+
if (signal?.aborted) throw toAmbientCancellationError(signal);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
1206
|
+
if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1207
|
+
throwIfAmbientAborted(signal);
|
|
1208
|
+
return new Promise((resolve, reject) => {
|
|
1209
|
+
const onAbort = () => {
|
|
1210
|
+
clearTimeout(timer);
|
|
1211
|
+
reject(toAmbientCancellationError(signal));
|
|
1212
|
+
};
|
|
1213
|
+
const timer = setTimeout(() => {
|
|
1214
|
+
signal.removeEventListener("abort", onAbort);
|
|
1215
|
+
resolve();
|
|
1216
|
+
}, ms);
|
|
1217
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1218
|
+
});
|
|
537
1219
|
}
|
|
538
1220
|
|
|
539
1221
|
function normalizeMethod(method: HttpMethod | string): StealthMethod {
|
|
@@ -559,16 +1241,6 @@ function normalizeMethod(method: HttpMethod | string): StealthMethod {
|
|
|
559
1241
|
}
|
|
560
1242
|
}
|
|
561
1243
|
|
|
562
|
-
function isRedirectStatus(status: number): boolean {
|
|
563
|
-
return [301, 302, 303, 307, 308].includes(status);
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function nextRedirectMethod(status: number, method: StealthMethod): StealthMethod {
|
|
567
|
-
if (status === 303 && method !== "HEAD") return "GET";
|
|
568
|
-
if ((status === 301 || status === 302) && method === "POST") return "GET";
|
|
569
|
-
return method;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
1244
|
function locationHeader(headers: Record<string, string>): string | undefined {
|
|
573
1245
|
for (const [name, value] of Object.entries(headers)) {
|
|
574
1246
|
if (name.toLowerCase() === "location") return value;
|
|
@@ -576,57 +1248,535 @@ function locationHeader(headers: Record<string, string>): string | undefined {
|
|
|
576
1248
|
return undefined;
|
|
577
1249
|
}
|
|
578
1250
|
|
|
1251
|
+
/**
|
|
1252
|
+
* Snapshot a caller's `redirectPolicy` before the first send. The policy acts
|
|
1253
|
+
* on redirects the transport follows, so it requires the default
|
|
1254
|
+
* `redirect: "follow"`.
|
|
1255
|
+
*/
|
|
1256
|
+
function snapshotStealthRedirectPolicy(options: StealthFetchOptions) {
|
|
1257
|
+
let policy: ReturnType<typeof normalizeRedirectPolicy>;
|
|
1258
|
+
try {
|
|
1259
|
+
policy = normalizeRedirectPolicy(options.redirectPolicy, STEALTH_REDIRECT_POLICY_TRANSPORT);
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
if (error instanceof TransportError) throw error;
|
|
1262
|
+
throw invalidRedirectPolicy(
|
|
1263
|
+
STEALTH_REDIRECT_POLICY_TRANSPORT,
|
|
1264
|
+
"could not be read safely",
|
|
1265
|
+
error instanceof Error ? error : undefined,
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
if (policy && options.redirect !== undefined && options.redirect !== "follow") {
|
|
1269
|
+
throw invalidRedirectPolicy(
|
|
1270
|
+
STEALTH_REDIRECT_POLICY_TRANSPORT,
|
|
1271
|
+
`it applies to followed redirects and cannot be combined with redirect: "${options.redirect}"`,
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
return policy;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function assertStealthRedirectUrl(url: string): void {
|
|
1278
|
+
const protocol = new URL(url).protocol;
|
|
1279
|
+
if (protocol !== "http:" && protocol !== "https:") {
|
|
1280
|
+
throw new TransportError(`Stealth redirect target scheme "${protocol}" is not allowed`, {
|
|
1281
|
+
code: "transport_invalid_url",
|
|
1282
|
+
status: 0,
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function discardStealthRedirectBody(response: StealthTransportResponse): void {
|
|
1288
|
+
try {
|
|
1289
|
+
const cancellation = response.body?.cancel();
|
|
1290
|
+
if (cancellation) void cancellation.catch(() => undefined);
|
|
1291
|
+
} catch {
|
|
1292
|
+
// Redirect handling is decided from status and headers. A cancellation
|
|
1293
|
+
// failure must not replace or delay that decision.
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/** Telemetry must never replace a transport result, including with caller-provided sinks. */
|
|
1298
|
+
function emitStealthTelemetry(
|
|
1299
|
+
sink: StealthTelemetrySink | undefined,
|
|
1300
|
+
record: (sink: StealthTelemetrySink) => void,
|
|
1301
|
+
): void {
|
|
1302
|
+
if (!sink) return;
|
|
1303
|
+
try {
|
|
1304
|
+
record(sink);
|
|
1305
|
+
} catch {
|
|
1306
|
+
/* The request owns its result. */
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
type StealthExchangeTelemetry = {
|
|
1311
|
+
sink: StealthTelemetrySink;
|
|
1312
|
+
profileId: StealthProfileDescriptor;
|
|
1313
|
+
proxyUsed: boolean;
|
|
1314
|
+
kind: StealthTelemetryAttemptKind;
|
|
1315
|
+
cookieJar?: StealthCookieJar;
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
function stripStealthCookieMaterial(text: string, cookieNames: readonly string[]): string {
|
|
1319
|
+
// Header-shaped Cookie/Set-Cookie material is redacted wholesale, including
|
|
1320
|
+
// foreign names. Bare pairs outside a header are limited to jar-known names:
|
|
1321
|
+
// a foreign bare name is indistinguishable from benign key=value prose and is
|
|
1322
|
+
// therefore covered only when layer 1 registered its value.
|
|
1323
|
+
let result = text.replace(
|
|
1324
|
+
/(^|[^\p{L}\p{N}_-])[\t ]*(?:cookie|set-cookie)[\t ]*(?::|=)[^\r\n]*/giu,
|
|
1325
|
+
"$1[REDACTED]",
|
|
1326
|
+
);
|
|
1327
|
+
result = result.replace(
|
|
1328
|
+
/"(?:cookie|set-cookie)"[\t ]*:[\t ]*"(?:\\.|[^"\\])*"/giu,
|
|
1329
|
+
"[REDACTED]",
|
|
1330
|
+
);
|
|
1331
|
+
for (const name of cookieNames) {
|
|
1332
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1333
|
+
result = result.replace(
|
|
1334
|
+
new RegExp(`(^|[;,&?\\s])${escaped}\\s*=\\s*[^;,\\s&\\r\\n]+`, "giu"),
|
|
1335
|
+
"$1[REDACTED]",
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
return result;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function startStealthExchange(
|
|
1342
|
+
telemetry: StealthExchangeTelemetry | undefined,
|
|
1343
|
+
method: StealthMethod,
|
|
1344
|
+
options: StealthFetchOptions,
|
|
1345
|
+
signal?: AbortSignal,
|
|
1346
|
+
): (status?: number, error?: unknown) => void {
|
|
1347
|
+
if (!telemetry) return () => {};
|
|
1348
|
+
const startedAt = Date.now();
|
|
1349
|
+
const requestClass = chromeRequestClass(method, options.stealth?.requestClass);
|
|
1350
|
+
let recorded = false;
|
|
1351
|
+
return (status, error) => {
|
|
1352
|
+
if (recorded) return;
|
|
1353
|
+
recorded = true;
|
|
1354
|
+
emitStealthTelemetry(telemetry?.sink, (sink) => {
|
|
1355
|
+
let code: string | undefined;
|
|
1356
|
+
if (error !== undefined) {
|
|
1357
|
+
try {
|
|
1358
|
+
code = signal?.aborted
|
|
1359
|
+
? "transport_cancelled"
|
|
1360
|
+
: normalizeStealthTransportError(error).code;
|
|
1361
|
+
} catch {
|
|
1362
|
+
code = "other";
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
const knownCodes: readonly string[] = [
|
|
1366
|
+
"transport_network_error",
|
|
1367
|
+
"transport_timeout",
|
|
1368
|
+
"transport_cancelled",
|
|
1369
|
+
"upstream_http_error",
|
|
1370
|
+
"response_too_large",
|
|
1371
|
+
"proxy_connect_failed",
|
|
1372
|
+
"PROXY_POOL_STALE",
|
|
1373
|
+
"PROXY_EDGE_AUTH_REJECTED",
|
|
1374
|
+
"PROXY_AUTH_IP_DENIED",
|
|
1375
|
+
"PROXY_EDGE_TLS_REJECTED",
|
|
1376
|
+
"PROXY_REQUIRED",
|
|
1377
|
+
];
|
|
1378
|
+
const errorCode = code
|
|
1379
|
+
? knownCodes.includes(code)
|
|
1380
|
+
? (code as StealthTelemetryErrorCode)
|
|
1381
|
+
: "other"
|
|
1382
|
+
: status !== undefined && status >= 400
|
|
1383
|
+
? "upstream_http_error"
|
|
1384
|
+
: undefined;
|
|
1385
|
+
const strip = (text: string) =>
|
|
1386
|
+
stripStealthCookieMaterial(text, telemetry.cookieJar?.names() ?? []);
|
|
1387
|
+
sink.recordAttempt({
|
|
1388
|
+
ms: Date.now() - startedAt,
|
|
1389
|
+
profileId: telemetry.profileId,
|
|
1390
|
+
proxyUsed: telemetry.proxyUsed,
|
|
1391
|
+
kind: telemetry.kind,
|
|
1392
|
+
requestClass:
|
|
1393
|
+
requestClass === "navigation" && options.stealth?.userActivation === false
|
|
1394
|
+
? "script_navigation"
|
|
1395
|
+
: requestClass === "form-post"
|
|
1396
|
+
? "form_post"
|
|
1397
|
+
: requestClass,
|
|
1398
|
+
...(status === undefined ? {} : { status }),
|
|
1399
|
+
...(errorCode ? { errorCode } : {}),
|
|
1400
|
+
...(error === undefined
|
|
1401
|
+
? {}
|
|
1402
|
+
: {
|
|
1403
|
+
diagnostics: {
|
|
1404
|
+
name: strip(error instanceof Error ? error.name : "Error"),
|
|
1405
|
+
message: strip(error instanceof Error ? error.message : String(error)),
|
|
1406
|
+
...(error instanceof Error && "code" in error && typeof error.code === "string"
|
|
1407
|
+
? { code: strip(error.code) }
|
|
1408
|
+
: {}),
|
|
1409
|
+
},
|
|
1410
|
+
}),
|
|
1411
|
+
});
|
|
1412
|
+
});
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
async function fetchStealthRedirectChain(
|
|
1417
|
+
transport: WreqSession,
|
|
1418
|
+
cookieJar: StealthCookieJar,
|
|
1419
|
+
requestUrl: string,
|
|
1420
|
+
method: StealthMethod,
|
|
1421
|
+
options: StealthFetchOptions,
|
|
1422
|
+
signal?: AbortSignal,
|
|
1423
|
+
buildHeaders?: (
|
|
1424
|
+
url: string,
|
|
1425
|
+
method: StealthMethod,
|
|
1426
|
+
body: string | Buffer | undefined,
|
|
1427
|
+
headers: Record<string, string>,
|
|
1428
|
+
) => HeaderTuple[],
|
|
1429
|
+
telemetry?: StealthExchangeTelemetry,
|
|
1430
|
+
): Promise<{ normalized: StealthResponse; response: StealthTransportResponse }> {
|
|
1431
|
+
let currentUrl = requestUrl;
|
|
1432
|
+
let currentMethod = method;
|
|
1433
|
+
let currentBody = options.body === undefined ? undefined : normalizeBody(options.body);
|
|
1434
|
+
let currentHeaders = normalizeHeaders({ ...(options.headers ?? {}) });
|
|
1435
|
+
let followedHops = 0;
|
|
1436
|
+
let response: StealthTransportResponse;
|
|
1437
|
+
const deadline = options.timeout ? performance.now() + options.timeout : undefined;
|
|
1438
|
+
// Snapshotted and validated by session.fetch before the first send.
|
|
1439
|
+
const redirectPolicy = options.redirectPolicy;
|
|
1440
|
+
const initialOrigin = new URL(requestUrl).origin;
|
|
1441
|
+
const visitedRequests = new Set([`${method} ${requestUrl}`]);
|
|
1442
|
+
|
|
1443
|
+
let finishExchange: ReturnType<typeof startStealthExchange> | undefined;
|
|
1444
|
+
let status: number | undefined;
|
|
1445
|
+
let exchangeError: unknown;
|
|
1446
|
+
try {
|
|
1447
|
+
while (true) {
|
|
1448
|
+
throwIfAmbientAborted(signal);
|
|
1449
|
+
const headers = { ...currentHeaders };
|
|
1450
|
+
if (!hasHeader(headers, "cookie")) {
|
|
1451
|
+
const cookieHeader = cookieJar.toHeader(currentUrl);
|
|
1452
|
+
if (cookieHeader) headers.cookie = cookieHeader;
|
|
1453
|
+
}
|
|
1454
|
+
const requestInit: StealthRequestInit = {
|
|
1455
|
+
headers: buildHeaders
|
|
1456
|
+
? buildHeaders(currentUrl, currentMethod, currentBody, headers)
|
|
1457
|
+
: headers,
|
|
1458
|
+
method: currentMethod,
|
|
1459
|
+
redirect: "manual",
|
|
1460
|
+
...(new URL(currentUrl).protocol === "http:" ? { disableDefaultHeaders: true } : {}),
|
|
1461
|
+
...(signal ? { signal } : {}),
|
|
1462
|
+
};
|
|
1463
|
+
if (currentBody !== undefined) requestInit.body = currentBody;
|
|
1464
|
+
|
|
1465
|
+
await transport.clearCookies();
|
|
1466
|
+
throwIfAmbientAborted(signal);
|
|
1467
|
+
const remainingTimeout =
|
|
1468
|
+
deadline === undefined ? undefined : Math.ceil(deadline - performance.now());
|
|
1469
|
+
if (remainingTimeout !== undefined && remainingTimeout <= 0) {
|
|
1470
|
+
throw new TransportError("Request timed out", {
|
|
1471
|
+
code: "transport_timeout",
|
|
1472
|
+
status: 0,
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
if (remainingTimeout !== undefined) requestInit.timeout = remainingTimeout;
|
|
1476
|
+
status = undefined;
|
|
1477
|
+
finishExchange = startStealthExchange(telemetry, currentMethod, options, signal);
|
|
1478
|
+
response = await transport.fetch(currentUrl, requestInit);
|
|
1479
|
+
status = response.status;
|
|
1480
|
+
if (signal?.aborted) {
|
|
1481
|
+
discardStealthRedirectBody(response);
|
|
1482
|
+
throw toAmbientCancellationError(signal);
|
|
1483
|
+
}
|
|
1484
|
+
cookieJar.setFromCookieStrings(
|
|
1485
|
+
setCookieHeadersFromResponse(response.headers),
|
|
1486
|
+
response.url ?? currentUrl,
|
|
1487
|
+
);
|
|
1488
|
+
|
|
1489
|
+
if (!isRedirectStatus(response.status) || options.redirect === "manual") break;
|
|
1490
|
+
if (options.redirect === "error") {
|
|
1491
|
+
discardStealthRedirectBody(response);
|
|
1492
|
+
throw new TransportError("Stealth request encountered a redirect", {
|
|
1493
|
+
code: "transport_network_error",
|
|
1494
|
+
status: 0,
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
let nextUrl: string;
|
|
1499
|
+
let nextMethod: StealthMethod;
|
|
1500
|
+
if (redirectPolicy) {
|
|
1501
|
+
discardStealthRedirectBody(response);
|
|
1502
|
+
({ nextUrl, nextMethod } = nextSameOriginRedirectHop({
|
|
1503
|
+
policy: redirectPolicy,
|
|
1504
|
+
initialOrigin,
|
|
1505
|
+
currentUrl,
|
|
1506
|
+
responseUrl: response.url ?? currentUrl,
|
|
1507
|
+
status: response.status,
|
|
1508
|
+
method: currentMethod,
|
|
1509
|
+
location: response.headers.get("location") ?? undefined,
|
|
1510
|
+
followedHops,
|
|
1511
|
+
visitedRequests,
|
|
1512
|
+
}));
|
|
1513
|
+
} else {
|
|
1514
|
+
const location = resolveRedirectUrl(
|
|
1515
|
+
response.headers.get("location") ?? undefined,
|
|
1516
|
+
response.url ?? currentUrl,
|
|
1517
|
+
);
|
|
1518
|
+
if (!location) break;
|
|
1519
|
+
if (followedHops >= MAX_STEALTH_REDIRECT_HOPS) {
|
|
1520
|
+
discardStealthRedirectBody(response);
|
|
1521
|
+
throw new TransportError(
|
|
1522
|
+
`Stealth request exceeded the ${MAX_STEALTH_REDIRECT_HOPS}-redirect limit`,
|
|
1523
|
+
{ code: "transport_network_error", status: 0 },
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
discardStealthRedirectBody(response);
|
|
1527
|
+
nextUrl = location;
|
|
1528
|
+
nextMethod = nextRedirectMethod(response.status, currentMethod);
|
|
1529
|
+
}
|
|
1530
|
+
assertStealthRedirectUrl(nextUrl);
|
|
1531
|
+
currentHeaders = redirectHopHeaders(
|
|
1532
|
+
{ from: currentUrl, to: nextUrl, methodChanged: nextMethod !== currentMethod },
|
|
1533
|
+
currentHeaders,
|
|
1534
|
+
);
|
|
1535
|
+
if (nextMethod !== currentMethod) currentBody = undefined;
|
|
1536
|
+
currentMethod = nextMethod;
|
|
1537
|
+
currentUrl = nextUrl;
|
|
1538
|
+
followedHops += 1;
|
|
1539
|
+
visitedRequests.add(`${currentMethod} ${currentUrl}`);
|
|
1540
|
+
emitStealthTelemetry(telemetry?.sink, (sink) => sink.recordRedirectHop());
|
|
1541
|
+
finishExchange(status);
|
|
1542
|
+
finishExchange = undefined;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
const normalized = await normalizeResponseWithSignal(
|
|
1546
|
+
response,
|
|
1547
|
+
currentUrl,
|
|
1548
|
+
options.maxBodyBytes,
|
|
1549
|
+
signal,
|
|
1550
|
+
);
|
|
1551
|
+
if (followedHops > 0) normalized.redirected = true;
|
|
1552
|
+
return { normalized, response };
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
exchangeError = error;
|
|
1555
|
+
throw error;
|
|
1556
|
+
} finally {
|
|
1557
|
+
finishExchange?.(status, exchangeError);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
579
1561
|
function createSessionFetcher(
|
|
580
1562
|
baseUrl: string,
|
|
581
|
-
defaultProfile:
|
|
582
|
-
clientOptions:
|
|
1563
|
+
defaultProfile: StealthProfileDescriptor,
|
|
1564
|
+
clientOptions: StealthSessionClientOptions,
|
|
583
1565
|
): StealthSession {
|
|
584
|
-
const clients = new Map<string,
|
|
1566
|
+
const clients = new Map<string, WreqSessionCacheEntry>();
|
|
585
1567
|
let closed = false;
|
|
586
1568
|
let hasWarnedMissingProxy = false;
|
|
587
1569
|
const warn = clientOptions.warn ?? console.warn;
|
|
588
|
-
const cookieJar = new
|
|
589
|
-
const
|
|
1570
|
+
const cookieJar = new StealthCookieJar([], baseUrl, registerDiagnosticValue);
|
|
1571
|
+
const akamaiSbsdState: AkamaiSbsdSessionState = { transactions: new Map() };
|
|
1572
|
+
const challengedReplays = new WeakMap<StealthResponse, ChallengedReplayRecord>();
|
|
1573
|
+
const ceremonyEgressLease = clientOptions[ENGINE_CEREMONY_EGRESS_LEASE];
|
|
1574
|
+
// The lease is shared by every session of this client; this is the lease generation this
|
|
1575
|
+
// session's challenge state and replay records were built against.
|
|
1576
|
+
let observedEgressGeneration = ceremonyEgressLease?.generation ?? 0;
|
|
1577
|
+
const automaticChallengeRefetchPolicy = {
|
|
1578
|
+
...createDefaultProxyTransportRetryOptions({ label: "Stealth" }),
|
|
1579
|
+
methods: ["GET"],
|
|
1580
|
+
};
|
|
1581
|
+
|
|
1582
|
+
function clearAkamaiSbsdState(): void {
|
|
1583
|
+
akamaiSbsdState.rememberedScript = undefined;
|
|
1584
|
+
akamaiSbsdState.transactions.clear();
|
|
1585
|
+
akamaiSbsdState.completedSuccessKey = undefined;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
function currentEgressGeneration(): number {
|
|
1589
|
+
return ceremonyEgressLease?.generation ?? 0;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/**
|
|
1593
|
+
* Whoever dropped the binding (this session by expiry or release, or another session of
|
|
1594
|
+
* the same client), the challenge state that was only valid on the retired endpoint goes
|
|
1595
|
+
* with it, and replay records made against the old generation stop matching.
|
|
1596
|
+
*/
|
|
1597
|
+
function syncCeremonyEgress(): void {
|
|
1598
|
+
const generation = currentEgressGeneration();
|
|
1599
|
+
if (generation === observedEgressGeneration) return;
|
|
1600
|
+
observedEgressGeneration = generation;
|
|
1601
|
+
clearAkamaiSbsdState();
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
/**
|
|
1605
|
+
* A binding past its vendor session lifetime is dropped, together with the challenge
|
|
1606
|
+
* state that was only valid on that endpoint, so this request selects and binds afresh.
|
|
1607
|
+
*/
|
|
1608
|
+
function expireCeremonyEgressLease(): void {
|
|
1609
|
+
ceremonyEgressLease?.dropExpiredBinding();
|
|
1610
|
+
syncCeremonyEgress();
|
|
1611
|
+
}
|
|
590
1612
|
|
|
591
|
-
|
|
592
|
-
|
|
1613
|
+
/**
|
|
1614
|
+
* The lease is best effort (ADR-0010: a raw Smartproxy endpoint is not a vendor-guaranteed
|
|
1615
|
+
* lease). When the attempt that ran on the bound endpoint fails at the transport (the
|
|
1616
|
+
* proxy refused it, the tunnel never opened, the socket died or timed out) the binding is
|
|
1617
|
+
* released so the ceremony can leave a dead endpoint before its lifetime elapses. Origin
|
|
1618
|
+
* responses, however bad, keep the binding: the endpoint is alive. Returns true when a
|
|
1619
|
+
* binding was released.
|
|
1620
|
+
*/
|
|
1621
|
+
function releaseFailedCeremonyEgress(
|
|
1622
|
+
attemptProxyUrl: string | undefined,
|
|
1623
|
+
error: TransportError,
|
|
1624
|
+
): boolean {
|
|
1625
|
+
const bound = ceremonyEgressLease?.binding;
|
|
1626
|
+
if (
|
|
1627
|
+
!ceremonyEgressLease ||
|
|
1628
|
+
!bound ||
|
|
1629
|
+
!attemptProxyUrl ||
|
|
1630
|
+
attemptProxyUrl !== bound.proxyUrl ||
|
|
1631
|
+
!isBoundEgressTransportFailure(error)
|
|
1632
|
+
) {
|
|
1633
|
+
return false;
|
|
1634
|
+
}
|
|
1635
|
+
ceremonyEgressLease.dropBinding();
|
|
1636
|
+
syncCeremonyEgress();
|
|
1637
|
+
return true;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
async function getClientEntry(
|
|
1641
|
+
profile: StealthProfileDescriptor,
|
|
593
1642
|
proxyUrl: string | undefined,
|
|
594
1643
|
ignoreTlsErrors: boolean,
|
|
595
|
-
|
|
1644
|
+
defaultHeaders?: HeaderTuple[],
|
|
1645
|
+
): Promise<WreqSessionCacheEntry> {
|
|
596
1646
|
if (closed) {
|
|
597
1647
|
throw new TransportError("Stealth session is closed", { status: 0 });
|
|
598
1648
|
}
|
|
599
|
-
const
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
1649
|
+
const wreq = await getWreqModule();
|
|
1650
|
+
const { browser, os } = resolveWreqProfile(profile, wreq.getProfiles());
|
|
1651
|
+
const cacheKey = JSON.stringify({
|
|
1652
|
+
browser,
|
|
1653
|
+
os,
|
|
1654
|
+
proxyUrl,
|
|
1655
|
+
ignoreTlsErrors,
|
|
1656
|
+
headerOrder: defaultHeaders?.map(([name]) => name),
|
|
1657
|
+
});
|
|
1658
|
+
let entry = clients.get(cacheKey);
|
|
1659
|
+
if (!entry) {
|
|
1660
|
+
entry = {
|
|
1661
|
+
// The header shape only partitions sessions; it is never handed to wreq as
|
|
1662
|
+
// session `defaultHeaders`. wreq merges those into every request that lacks
|
|
1663
|
+
// one of their names, so a header the SDK left off on purpose — a credential
|
|
1664
|
+
// dropped on a cross-origin redirect hop, the body headers after POST -> GET,
|
|
1665
|
+
// a cookie for another host — would come back with the first request's value.
|
|
1666
|
+
// Every request already sends its complete ordered header list.
|
|
1667
|
+
session: wreq.createSession({
|
|
1668
|
+
browser,
|
|
1669
|
+
os,
|
|
1670
|
+
...(proxyUrl ? { proxy: proxyUrl } : {}),
|
|
1671
|
+
...(ignoreTlsErrors ? { insecure: true } : {}),
|
|
1672
|
+
timeout: 30_000,
|
|
1673
|
+
}),
|
|
1674
|
+
tail: Promise.resolve(),
|
|
1675
|
+
};
|
|
1676
|
+
clients.set(cacheKey, entry);
|
|
1677
|
+
}
|
|
1678
|
+
return entry;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
async function withClient<T>(
|
|
1682
|
+
profile: StealthProfileDescriptor,
|
|
1683
|
+
proxyUrl: string | undefined,
|
|
1684
|
+
ignoreTlsErrors: boolean,
|
|
1685
|
+
operation: (client: WreqSession) => Promise<T>,
|
|
1686
|
+
signal?: AbortSignal,
|
|
1687
|
+
defaultHeaders?: HeaderTuple[],
|
|
1688
|
+
): Promise<T> {
|
|
1689
|
+
throwIfAmbientAborted(signal);
|
|
1690
|
+
const entry = await getClientEntry(profile, proxyUrl, ignoreTlsErrors, defaultHeaders);
|
|
1691
|
+
throwIfAmbientAborted(signal);
|
|
1692
|
+
const previous = entry.tail;
|
|
1693
|
+
let release!: () => void;
|
|
1694
|
+
entry.tail = new Promise<void>((resolve) => {
|
|
1695
|
+
release = resolve;
|
|
1696
|
+
});
|
|
1697
|
+
let acquired = false;
|
|
1698
|
+
try {
|
|
1699
|
+
await waitForClientTurn(previous, signal);
|
|
1700
|
+
acquired = true;
|
|
1701
|
+
throwIfAmbientAborted(signal);
|
|
1702
|
+
const client = await entry.session;
|
|
1703
|
+
throwIfAmbientAborted(signal);
|
|
1704
|
+
const result = await operation(client);
|
|
1705
|
+
throwIfAmbientAborted(signal);
|
|
1706
|
+
return result;
|
|
1707
|
+
} finally {
|
|
1708
|
+
if (acquired) {
|
|
1709
|
+
release();
|
|
1710
|
+
} else {
|
|
1711
|
+
void previous.then(release, release);
|
|
1712
|
+
}
|
|
611
1713
|
}
|
|
612
|
-
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
async function waitForClientTurn(previous: Promise<void>, signal?: AbortSignal): Promise<void> {
|
|
1717
|
+
if (!signal) {
|
|
1718
|
+
await previous;
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
throwIfAmbientAborted(signal);
|
|
1722
|
+
await new Promise<void>((resolve, reject) => {
|
|
1723
|
+
let settled = false;
|
|
1724
|
+
const settle = (operation: () => void) => {
|
|
1725
|
+
if (settled) return;
|
|
1726
|
+
settled = true;
|
|
1727
|
+
signal.removeEventListener("abort", onAbort);
|
|
1728
|
+
operation();
|
|
1729
|
+
};
|
|
1730
|
+
const onAbort = () => settle(() => reject(toAmbientCancellationError(signal)));
|
|
1731
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1732
|
+
void previous.then(
|
|
1733
|
+
() => settle(resolve),
|
|
1734
|
+
(error) => settle(() => reject(error)),
|
|
1735
|
+
);
|
|
1736
|
+
if (signal.aborted) onAbort();
|
|
1737
|
+
});
|
|
613
1738
|
}
|
|
614
1739
|
|
|
615
1740
|
async function resolveRequestProxy(
|
|
616
1741
|
options?: StealthFetchOptions,
|
|
617
1742
|
proxyAttempt?: number,
|
|
1743
|
+
refreshEpoch?: number,
|
|
618
1744
|
): Promise<ResolvedAttemptProxy> {
|
|
1745
|
+
const bound = ceremonyEgressLease?.binding;
|
|
1746
|
+
if (bound) {
|
|
1747
|
+
if (options?.proxy !== undefined || options?.proxyAttemptOffset !== undefined) {
|
|
1748
|
+
throw new SDKError(
|
|
1749
|
+
"A ceremony-bound egress cannot be overridden by provider request options",
|
|
1750
|
+
{
|
|
1751
|
+
code: "EGRESS_LEASE_BINDING_INVALID",
|
|
1752
|
+
fix: "Remove `proxy` and `proxyAttemptOffset` from stealth requests made inside an auth ceremony.",
|
|
1753
|
+
},
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1756
|
+
return {
|
|
1757
|
+
url: bound.proxyUrl,
|
|
1758
|
+
poolIndex: bound.poolIndex,
|
|
1759
|
+
proxyHash: proxyEndpointHash(bound.proxyUrl),
|
|
1760
|
+
vendor: bound.vendor,
|
|
1761
|
+
refreshEpoch: bound.refreshEpoch,
|
|
1762
|
+
lifetimeMinutes: bound.lifetimeMinutes,
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
619
1765
|
const resolvedProxy = await resolveProxyConfigAsync({
|
|
620
1766
|
proxy: options?.proxy ?? clientOptions.proxy,
|
|
621
1767
|
upstream: clientOptions.upstream,
|
|
622
|
-
apifuseConfig: clientOptions.apifuseConfig,
|
|
623
1768
|
affinityKey: clientOptions.affinityKey,
|
|
624
1769
|
proxyAttempt: computeProxyAttemptIndex({
|
|
625
1770
|
baseProxyAttempt: clientOptions.proxyAttempt,
|
|
626
1771
|
proxyAttemptOffset: options?.proxyAttemptOffset,
|
|
627
1772
|
retryAttemptOffset: proxyAttempt,
|
|
628
1773
|
}),
|
|
1774
|
+
// The stealth transport tunnels both HTTP CONNECT and SOCKS5,
|
|
1775
|
+
// preserving the client TLS fingerprint end-to-end.
|
|
1776
|
+
transportProtocols: ["http", "socks5"],
|
|
1777
|
+
...(refreshEpoch === undefined ? {} : { proxyRefreshEpoch: refreshEpoch }),
|
|
629
1778
|
telemetry: clientOptions.telemetry,
|
|
1779
|
+
engineCredentials: clientOptions.engineCredentials,
|
|
630
1780
|
});
|
|
631
1781
|
|
|
632
1782
|
if (resolvedProxy.shouldWarn && !hasWarnedMissingProxy) {
|
|
@@ -638,42 +1788,141 @@ function createSessionFetcher(
|
|
|
638
1788
|
url: resolvedProxy.url,
|
|
639
1789
|
poolIndex: proxyPoolIndexFromDiagnostics(resolvedProxy.diagnostics),
|
|
640
1790
|
proxyHash: proxyEndpointHash(resolvedProxy.url),
|
|
1791
|
+
vendor: vendorFromResolvedSource(resolvedProxy.source),
|
|
1792
|
+
refreshEpoch: refreshEpoch ?? 0,
|
|
1793
|
+
...(positiveDiagnosticNumber(resolvedProxy.diagnostics, "lifetimeMinutes") === undefined
|
|
1794
|
+
? {}
|
|
1795
|
+
: {
|
|
1796
|
+
lifetimeMinutes: positiveDiagnosticNumber(resolvedProxy.diagnostics, "lifetimeMinutes"),
|
|
1797
|
+
}),
|
|
641
1798
|
};
|
|
642
1799
|
}
|
|
643
1800
|
|
|
1801
|
+
function bindCeremonyEgress(attemptProxy: ResolvedAttemptProxy | undefined): void {
|
|
1802
|
+
const lease = ceremonyEgressLease;
|
|
1803
|
+
if (!lease || !attemptProxy?.url || !attemptProxy.vendor) return;
|
|
1804
|
+
if (lease.binding) {
|
|
1805
|
+
// A concurrent request on this session may have bound the ceremony while this one
|
|
1806
|
+
// was in flight on an endpoint it selected before the binding existed (pool
|
|
1807
|
+
// rotation after a transport failure). Its response reached the origin from an
|
|
1808
|
+
// egress the handle does not name; accepting it would split the ceremony identity.
|
|
1809
|
+
if (attemptProxy.url !== lease.binding.proxyUrl) {
|
|
1810
|
+
throw new SDKError(
|
|
1811
|
+
"The response arrived on an egress other than the ceremony's bound endpoint",
|
|
1812
|
+
{
|
|
1813
|
+
code: "EGRESS_LEASE_BINDING_INVALID",
|
|
1814
|
+
},
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
if (
|
|
1820
|
+
attemptProxy.poolIndex === undefined ||
|
|
1821
|
+
attemptProxy.lifetimeMinutes === undefined ||
|
|
1822
|
+
!clientOptions.affinityKey
|
|
1823
|
+
) {
|
|
1824
|
+
// Registry vendors always report pool index and session lifetime; missing either
|
|
1825
|
+
// is an SDK fault, not something the caller can correct.
|
|
1826
|
+
throw new SDKError("The selected egress cannot be represented by a ceremony lease", {
|
|
1827
|
+
code: "EGRESS_LEASE_BINDING_INVALID",
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
const binding: CeremonyEgressBinding = {
|
|
1831
|
+
vendor: attemptProxy.vendor,
|
|
1832
|
+
proxyUrl: attemptProxy.url,
|
|
1833
|
+
poolIndex: attemptProxy.poolIndex,
|
|
1834
|
+
affinityKey: clientOptions.affinityKey,
|
|
1835
|
+
refreshEpoch: attemptProxy.refreshEpoch ?? 0,
|
|
1836
|
+
lifetimeMinutes: attemptProxy.lifetimeMinutes,
|
|
1837
|
+
};
|
|
1838
|
+
lease.bind(binding);
|
|
1839
|
+
}
|
|
1840
|
+
|
|
644
1841
|
const session: StealthSession = {
|
|
645
|
-
async fetch(url,
|
|
646
|
-
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
(
|
|
654
|
-
?
|
|
655
|
-
|
|
1842
|
+
async fetch(url, callerOptions: StealthFetchOptions = {}) {
|
|
1843
|
+
expireCeremonyEgressLease();
|
|
1844
|
+
const requestBody = normalizeBody(callerOptions.body);
|
|
1845
|
+
// Snapshot the caller's headers with the body before the first send: the first
|
|
1846
|
+
// request and an explicit replay both use this copy, so a caller mutating its own
|
|
1847
|
+
// objects afterwards cannot make the two sends differ.
|
|
1848
|
+
const options: StealthFetchOptions = {
|
|
1849
|
+
...callerOptions,
|
|
1850
|
+
...(callerOptions.headers
|
|
1851
|
+
? {
|
|
1852
|
+
headers: Object.fromEntries(
|
|
1853
|
+
Object.entries(callerOptions.headers).map(([name, value]) => [
|
|
1854
|
+
name,
|
|
1855
|
+
Array.isArray(value) ? [...value] : value,
|
|
1856
|
+
]),
|
|
1857
|
+
),
|
|
1858
|
+
}
|
|
1859
|
+
: {}),
|
|
1860
|
+
...(requestBody === undefined ? {} : { body: requestBody }),
|
|
1861
|
+
};
|
|
1862
|
+
const requestProfile = resolveStealthProfileSelection(options.stealth, defaultProfile);
|
|
1863
|
+
let challengeSolveAttempted = false;
|
|
1864
|
+
let challengeRefetchAttempted = false;
|
|
1865
|
+
let challengeSolveFailure: { readonly error: unknown } | undefined;
|
|
1866
|
+
const { hasExplicitRetryPolicy, method, stealthRetryOptions } = (() => {
|
|
1867
|
+
try {
|
|
1868
|
+
const method = normalizeMethod(options.method ?? "GET");
|
|
1869
|
+
assertCallerHeadersSupported(options.headers ?? {});
|
|
1870
|
+
// Replace the caller's object with the validated copy: the walker, a
|
|
1871
|
+
// retry and an explicit replay all read this snapshot.
|
|
1872
|
+
const redirectPolicy = snapshotStealthRedirectPolicy(options);
|
|
1873
|
+
if (redirectPolicy) options.redirectPolicy = redirectPolicy;
|
|
1874
|
+
const hasExplicitRetryPolicy = options.retry !== undefined;
|
|
1875
|
+
const stealthRetryOptions =
|
|
1876
|
+
normalizeProxyTransportRetryOptions(options.retry, {
|
|
656
1877
|
extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
|
|
657
1878
|
label: "Stealth",
|
|
658
|
-
})
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
1879
|
+
}) ??
|
|
1880
|
+
(hasExplicitRetryPolicy
|
|
1881
|
+
? undefined
|
|
1882
|
+
: createDefaultProxyTransportRetryOptions({
|
|
1883
|
+
extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
|
|
1884
|
+
label: "Stealth",
|
|
1885
|
+
}));
|
|
1886
|
+
if (stealthRetryOptions) {
|
|
1887
|
+
validateUnsafeProxyTransportRetryMethods(stealthRetryOptions, "Stealth");
|
|
1888
|
+
}
|
|
1889
|
+
return { hasExplicitRetryPolicy, method, stealthRetryOptions };
|
|
1890
|
+
} catch (error) {
|
|
1891
|
+
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
1892
|
+
}
|
|
1893
|
+
})();
|
|
1894
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
662
1895
|
const hasPolicyProxy = isPolicyManagedProxy(clientOptions);
|
|
663
1896
|
const usesPolicyAllocator = hasPolicyProxy && !options.proxy && !clientOptions.proxy;
|
|
664
1897
|
const retryAttemptCap = Math.max(1, stealthRetryOptions?.attempts ?? 1);
|
|
1898
|
+
// Span the whole vendor chain: successive attempts rotate one vendor's
|
|
1899
|
+
// pool, then fail over to the next vendor via the flat attempt index.
|
|
1900
|
+
const policyProxy =
|
|
1901
|
+
clientOptions.proxyPolicy ??
|
|
1902
|
+
(typeof clientOptions.upstream?.proxy === "object"
|
|
1903
|
+
? clientOptions.upstream.proxy
|
|
1904
|
+
: undefined);
|
|
1905
|
+
// The pool span is already bounded by each vendor's max pool size
|
|
1906
|
+
// (smartproxy ≤20, nodemaven ≤50), so the configured span never exceeds
|
|
1907
|
+
// the chain's true maximum — a large NodeMaven pool stays fully
|
|
1908
|
+
// reachable rather than being truncated at an arbitrary ceiling.
|
|
665
1909
|
const policyProxyAttemptCap = Math.max(
|
|
666
1910
|
1,
|
|
667
|
-
|
|
668
|
-
MAX_POLICY_PROXY_RETRY_ATTEMPTS,
|
|
669
|
-
clientOptions.proxyPolicy?.session?.poolSize ??
|
|
670
|
-
(typeof clientOptions.upstream?.proxy === "object"
|
|
671
|
-
? clientOptions.upstream.proxy.session?.poolSize
|
|
672
|
-
: undefined) ??
|
|
673
|
-
DEFAULT_SMARTPROXY_POOL_SIZE,
|
|
674
|
-
),
|
|
1911
|
+
policyProxy ? resolvePolicyProxyPoolSpan(policyProxy) : DEFAULT_SMARTPROXY_POOL_SIZE,
|
|
675
1912
|
);
|
|
676
|
-
|
|
1913
|
+
// A registry vendor chain (smartproxy/nodemaven) is the only policy whose
|
|
1914
|
+
// successive attempts resolve a *different* endpoint, so it is the only one
|
|
1915
|
+
// that may widen the attempt cap to the pool span, de-duplicate endpoints,
|
|
1916
|
+
// and drive allocator stale-pool refresh. Deprecated custom/decodo policies
|
|
1917
|
+
// have no managed endpoint to rotate or refresh, so they follow the ordinary
|
|
1918
|
+
// transport-retry budget instead.
|
|
1919
|
+
const rotatesRegistryChain =
|
|
1920
|
+
usesPolicyAllocator && policyResolvesRegistryVendorChain(policyProxy);
|
|
1921
|
+
const unboundMaxAttempts = rotatesRegistryChain ? policyProxyAttemptCap : retryAttemptCap;
|
|
1922
|
+
// A bound ceremony has exactly one endpoint to try; releasing the binding after a
|
|
1923
|
+
// transport failure widens the budget back to the ordinary rotation span.
|
|
1924
|
+
let maxAttempts = ceremonyEgressLease?.binding ? 1 : unboundMaxAttempts;
|
|
1925
|
+
const dedupeAllocatorEndpoints = rotatesRegistryChain;
|
|
677
1926
|
let lastError: unknown;
|
|
678
1927
|
|
|
679
1928
|
for (
|
|
@@ -686,10 +1935,17 @@ function createSessionFetcher(
|
|
|
686
1935
|
const attemptedProxies = new Set<string>();
|
|
687
1936
|
|
|
688
1937
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
1938
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
689
1939
|
let proxy: string | undefined;
|
|
690
1940
|
let attemptProxy: ResolvedAttemptProxy | undefined;
|
|
691
|
-
|
|
1941
|
+
// Reuse the exact serialization used by this outbound attempt in its catch path.
|
|
1942
|
+
let serializedUrl: ReturnType<typeof serializeRequestUrl> | undefined;
|
|
1943
|
+
let fallbackSensitiveValues: readonly string[] = [];
|
|
1944
|
+
let fallbackRequestUrl: string | undefined;
|
|
1945
|
+
let fallbackRedactedUrl: string | undefined;
|
|
1946
|
+
let attemptStartedAt = Date.now();
|
|
692
1947
|
let attemptRecorded = false;
|
|
1948
|
+
const attemptEgressGeneration = currentEgressGeneration();
|
|
693
1949
|
const recordProxyAttempt = (
|
|
694
1950
|
outcome: "ok" | "error",
|
|
695
1951
|
errorCode?: string,
|
|
@@ -698,7 +1954,7 @@ function createSessionFetcher(
|
|
|
698
1954
|
if (attemptRecorded || !proxy) return;
|
|
699
1955
|
attemptRecorded = true;
|
|
700
1956
|
clientOptions.telemetry?.recordProxyAttempt?.({
|
|
701
|
-
provider: "smartproxy",
|
|
1957
|
+
provider: attemptProxy?.vendor ?? "smartproxy",
|
|
702
1958
|
attempt: attempt + 1,
|
|
703
1959
|
...(attemptProxy?.poolIndex === undefined
|
|
704
1960
|
? {}
|
|
@@ -710,13 +1966,96 @@ function createSessionFetcher(
|
|
|
710
1966
|
durationMs: Date.now() - attemptStartedAt,
|
|
711
1967
|
});
|
|
712
1968
|
};
|
|
1969
|
+
// Shared tail of the first fetch, the automatic refetch, and an explicit replay: a
|
|
1970
|
+
// classified challenge is returned whatever `throwOnHttpError` says (documented on
|
|
1971
|
+
// StealthFetchOptions), any other non-2xx honours it, and the proxy attempt is
|
|
1972
|
+
// recorded once per delivered response.
|
|
1973
|
+
const finalizeAttempt = (
|
|
1974
|
+
attemptResponse: StealthTransportResponse,
|
|
1975
|
+
attemptNormalized: StealthResponse,
|
|
1976
|
+
): StealthResponse => {
|
|
1977
|
+
if (attemptNormalized.challenge)
|
|
1978
|
+
emitStealthTelemetry(clientOptions.stealthTelemetry, (sink) =>
|
|
1979
|
+
sink.recordSbsd(attemptNormalized.challenge!.outcome),
|
|
1980
|
+
);
|
|
1981
|
+
if (
|
|
1982
|
+
!attemptNormalized.challenge &&
|
|
1983
|
+
attemptResponse.status >= 400 &&
|
|
1984
|
+
options.throwOnHttpError !== false
|
|
1985
|
+
) {
|
|
1986
|
+
throw new TransportError(
|
|
1987
|
+
`Upstream request failed with status ${attemptResponse.status}`,
|
|
1988
|
+
{ code: "upstream_http_error", status: attemptResponse.status },
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
recordProxyAttempt("ok", undefined, attemptResponse.status);
|
|
1992
|
+
return attemptNormalized;
|
|
1993
|
+
};
|
|
1994
|
+
const redactAttemptError = <T>(error: T): T =>
|
|
1995
|
+
redactSensitiveError(
|
|
1996
|
+
error,
|
|
1997
|
+
serializedUrl?.sensitiveValues ?? fallbackSensitiveValues,
|
|
1998
|
+
serializedUrl?.requestUrl ?? fallbackRequestUrl,
|
|
1999
|
+
serializedUrl?.redactedUrl ?? fallbackRedactedUrl,
|
|
2000
|
+
);
|
|
2001
|
+
// Cancellation and resolver failures leave the transport path here, redacted; any
|
|
2002
|
+
// other failure becomes the TransportError the retry decision reasons about.
|
|
2003
|
+
const normalizeAttemptError = (error: unknown): TransportError => {
|
|
2004
|
+
let normalizedError: TransportError;
|
|
2005
|
+
try {
|
|
2006
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
2007
|
+
normalizedError = normalizeStealthTransportError(error);
|
|
2008
|
+
} catch (normalizationError) {
|
|
2009
|
+
if (
|
|
2010
|
+
normalizationError instanceof TransportError &&
|
|
2011
|
+
normalizationError.code === "transport_cancelled"
|
|
2012
|
+
) {
|
|
2013
|
+
recordProxyAttempt(
|
|
2014
|
+
"error",
|
|
2015
|
+
proxyAttemptErrorCode(normalizationError),
|
|
2016
|
+
proxyAttemptStatus(normalizationError),
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
throw redactAttemptError(normalizationError);
|
|
2020
|
+
}
|
|
2021
|
+
if (challengeSolveFailure && error === challengeSolveFailure) {
|
|
2022
|
+
throw redactAttemptError(challengeSolveFailure.error);
|
|
2023
|
+
}
|
|
2024
|
+
return normalizedError;
|
|
2025
|
+
};
|
|
2026
|
+
const recordAttemptFailure = (normalizedError: TransportError): TransportError => {
|
|
2027
|
+
const redactedError = redactAttemptError(normalizedError);
|
|
2028
|
+
recordProxyAttempt(
|
|
2029
|
+
"error",
|
|
2030
|
+
proxyAttemptErrorCode(redactedError),
|
|
2031
|
+
proxyAttemptStatus(redactedError),
|
|
2032
|
+
);
|
|
2033
|
+
return redactedError;
|
|
2034
|
+
};
|
|
713
2035
|
try {
|
|
2036
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
2037
|
+
const sensitiveParams = normalizeSensitiveParams(options.sensitiveParams);
|
|
2038
|
+
const structural = redactUrlQueryParams(url, Object.keys(sensitiveParams ?? {}));
|
|
2039
|
+
fallbackSensitiveValues = [
|
|
2040
|
+
...new Set([
|
|
2041
|
+
...Object.values(sensitiveParams ?? {}).map(String),
|
|
2042
|
+
...structural.sensitiveValues,
|
|
2043
|
+
]),
|
|
2044
|
+
].filter((value) => value !== "");
|
|
2045
|
+
fallbackRequestUrl = url;
|
|
2046
|
+
fallbackRedactedUrl = structural.redactedUrl;
|
|
714
2047
|
assertNoUnsupportedFingerprintOverrides(options);
|
|
715
|
-
attemptProxy = await resolveRequestProxy(options, attempt);
|
|
2048
|
+
attemptProxy = await resolveRequestProxy(options, attempt, refreshAttempt);
|
|
2049
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
716
2050
|
proxy = attemptProxy.url;
|
|
717
|
-
if (proxy &&
|
|
2051
|
+
if (proxy && dedupeAllocatorEndpoints) {
|
|
2052
|
+
// An under-filled allocation repeats endpoints (via the modulo
|
|
2053
|
+
// pool mapping) before the flat offset crosses into the next
|
|
2054
|
+
// vendor. Skip an already-tried endpoint and advance the offset
|
|
2055
|
+
// rather than breaking — breaking here would strand the request on
|
|
2056
|
+
// the primary vendor and never reach the fallback leg.
|
|
718
2057
|
if (attemptedProxies.has(proxy)) {
|
|
719
|
-
|
|
2058
|
+
continue;
|
|
720
2059
|
}
|
|
721
2060
|
attemptedProxies.add(proxy);
|
|
722
2061
|
}
|
|
@@ -724,81 +2063,428 @@ function createSessionFetcher(
|
|
|
724
2063
|
options.stealth?.insecureSkipVerify ??
|
|
725
2064
|
(!hasPolicyProxy && proxy && clientOptions.proxyStealth?.insecureSkipVerify),
|
|
726
2065
|
);
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
2066
|
+
serializedUrl = serializeRequestUrl(
|
|
2067
|
+
resolveRequestUrl(baseUrl, url),
|
|
2068
|
+
options.params,
|
|
2069
|
+
sensitiveParams,
|
|
2070
|
+
);
|
|
2071
|
+
const { requestUrl } = serializedUrl;
|
|
2072
|
+
const wreq = await getWreqModule();
|
|
2073
|
+
const mapping = resolveWreqProfile(requestProfile, wreq.getProfiles());
|
|
2074
|
+
const emulationHeaders = Array.from(
|
|
2075
|
+
wreq.getEmulationHeaders(mapping.browser, mapping.os),
|
|
2076
|
+
([name, value]) => [String(name), String(value)] as HeaderTuple,
|
|
2077
|
+
);
|
|
2078
|
+
const chromeEmulationHeaders = mapping.browser.startsWith("chrome_")
|
|
2079
|
+
? emulationHeaders
|
|
2080
|
+
: undefined;
|
|
2081
|
+
const buildOrderedHeaders = chromeEmulationHeaders
|
|
2082
|
+
? (
|
|
2083
|
+
currentUrl: string,
|
|
2084
|
+
currentMethod: StealthMethod,
|
|
2085
|
+
currentBody: string | Buffer | undefined,
|
|
2086
|
+
currentHeaders: Record<string, string>,
|
|
2087
|
+
) =>
|
|
2088
|
+
buildChromeHeaderTuples({
|
|
2089
|
+
emulationHeaders: chromeEmulationHeaders,
|
|
2090
|
+
method: currentMethod,
|
|
2091
|
+
body: currentBody,
|
|
2092
|
+
headers: currentHeaders,
|
|
2093
|
+
requestUrl: currentUrl,
|
|
2094
|
+
acceptLanguage: clientOptions.stealth?.acceptLanguage,
|
|
2095
|
+
requestClass: options.stealth?.requestClass,
|
|
2096
|
+
userActivation: options.stealth?.userActivation,
|
|
2097
|
+
})
|
|
2098
|
+
: undefined;
|
|
2099
|
+
const initialHeaders = normalizeHeaders({ ...(options.headers ?? {}) });
|
|
2100
|
+
if (!hasHeader(initialHeaders, "cookie")) {
|
|
2101
|
+
const cookieHeader = cookieJar.toHeader(requestUrl);
|
|
2102
|
+
if (cookieHeader) initialHeaders.cookie = cookieHeader;
|
|
742
2103
|
}
|
|
743
|
-
const
|
|
2104
|
+
const defaultHeaders = buildOrderedHeaders?.(
|
|
744
2105
|
requestUrl,
|
|
745
|
-
|
|
2106
|
+
method,
|
|
2107
|
+
requestBody,
|
|
2108
|
+
initialHeaders,
|
|
746
2109
|
);
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
2110
|
+
const fetchOnBoundSession = (
|
|
2111
|
+
fetchUrl: string,
|
|
2112
|
+
fetchMethod: StealthMethod,
|
|
2113
|
+
fetchOptions: StealthFetchOptions,
|
|
2114
|
+
fetchSignal: AbortSignal | undefined,
|
|
2115
|
+
orderedHeaders = buildOrderedHeaders,
|
|
2116
|
+
sessionDefaultHeaders = defaultHeaders,
|
|
2117
|
+
kind: StealthTelemetryAttemptKind = "request",
|
|
2118
|
+
) =>
|
|
2119
|
+
withClient(
|
|
2120
|
+
requestProfile,
|
|
2121
|
+
proxy,
|
|
2122
|
+
ignoreTlsErrors,
|
|
2123
|
+
(transport) =>
|
|
2124
|
+
fetchStealthRedirectChain(
|
|
2125
|
+
transport,
|
|
2126
|
+
cookieJar,
|
|
2127
|
+
fetchUrl,
|
|
2128
|
+
fetchMethod,
|
|
2129
|
+
fetchOptions,
|
|
2130
|
+
fetchSignal,
|
|
2131
|
+
orderedHeaders,
|
|
2132
|
+
clientOptions.stealthTelemetry
|
|
2133
|
+
? {
|
|
2134
|
+
sink: clientOptions.stealthTelemetry,
|
|
2135
|
+
profileId: requestProfile,
|
|
2136
|
+
proxyUsed: Boolean(proxy),
|
|
2137
|
+
kind,
|
|
2138
|
+
cookieJar,
|
|
2139
|
+
}
|
|
2140
|
+
: undefined,
|
|
2141
|
+
),
|
|
2142
|
+
fetchSignal,
|
|
2143
|
+
sessionDefaultHeaders,
|
|
2144
|
+
);
|
|
2145
|
+
const throwProxyTransportFault = (
|
|
2146
|
+
faultResponse: StealthTransportResponse,
|
|
2147
|
+
faultBody: string,
|
|
2148
|
+
) => {
|
|
2149
|
+
if (!proxy) return;
|
|
2150
|
+
if (isProxyConnectFailureResponse(faultResponse, faultBody)) {
|
|
2151
|
+
throw createProxyConnectFailureError(faultBody);
|
|
2152
|
+
}
|
|
2153
|
+
if (faultResponse.status < 400) return;
|
|
755
2154
|
if (
|
|
756
|
-
proxy &&
|
|
757
2155
|
usesPolicyAllocator &&
|
|
758
2156
|
isProxyEdgeTlsRejectedResponse(
|
|
759
|
-
|
|
760
|
-
[JSON.stringify(responseHeadersToRecord(
|
|
2157
|
+
faultResponse.status,
|
|
2158
|
+
[JSON.stringify(responseHeadersToRecord(faultResponse.headers)), faultBody].join(
|
|
761
2159
|
"\n",
|
|
762
2160
|
),
|
|
763
2161
|
)
|
|
764
2162
|
) {
|
|
765
|
-
throw createProxyEdgeTlsRejectedError(
|
|
2163
|
+
throw createProxyEdgeTlsRejectedError(faultResponse.status);
|
|
766
2164
|
}
|
|
767
|
-
if (
|
|
2165
|
+
if (isProxyAuthIpDeniedMessage(faultBody)) {
|
|
768
2166
|
throw createProxyAuthIpDeniedError();
|
|
769
2167
|
}
|
|
770
|
-
if (
|
|
2168
|
+
if (isProxyEdgeAuthRejectedMessage(faultBody)) {
|
|
771
2169
|
throw createProxyEdgeAuthRejectedError();
|
|
772
2170
|
}
|
|
773
2171
|
if (
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
isProxyPoolStaleMessage(normalized.body)
|
|
2172
|
+
isProxyPoolStaleStatus(faultResponse.status) &&
|
|
2173
|
+
isProxyPoolStaleMessage(faultBody)
|
|
777
2174
|
) {
|
|
778
|
-
throw createProxyPoolStaleError(
|
|
2175
|
+
throw createProxyPoolStaleError(faultResponse.status);
|
|
2176
|
+
}
|
|
2177
|
+
};
|
|
2178
|
+
let { normalized, response } = await fetchOnBoundSession(
|
|
2179
|
+
requestUrl,
|
|
2180
|
+
method,
|
|
2181
|
+
options,
|
|
2182
|
+
clientOptions.signal,
|
|
2183
|
+
);
|
|
2184
|
+
throwProxyTransportFault(response, normalized.body);
|
|
2185
|
+
|
|
2186
|
+
// Commit only after the selected endpoint reached the origin. A failed
|
|
2187
|
+
// proxy attempt may rotate pool index; the successful exact endpoint is
|
|
2188
|
+
// the one sealed into the ceremony handle.
|
|
2189
|
+
bindCeremonyEgress(attemptProxy);
|
|
2190
|
+
|
|
2191
|
+
const akamaiSbsd = clientOptions.stealth?.challengeRuntime?.akamaiSbsd;
|
|
2192
|
+
const detected = akamaiSbsd
|
|
2193
|
+
? detectAkamaiSbsdChallenge(
|
|
2194
|
+
normalized,
|
|
2195
|
+
requestUrl,
|
|
2196
|
+
cookieJar,
|
|
2197
|
+
akamaiSbsd.allowedHosts,
|
|
2198
|
+
akamaiSbsdState,
|
|
2199
|
+
)
|
|
2200
|
+
: undefined;
|
|
2201
|
+
if (akamaiSbsd && !detected)
|
|
2202
|
+
emitStealthTelemetry(clientOptions.stealthTelemetry, (sink) =>
|
|
2203
|
+
sink.recordSbsd({ detected: false }),
|
|
2204
|
+
);
|
|
2205
|
+
if (detected && akamaiSbsd) {
|
|
2206
|
+
emitStealthTelemetry(clientOptions.stealthTelemetry, (sink) =>
|
|
2207
|
+
sink.recordSbsd("detected"),
|
|
2208
|
+
);
|
|
2209
|
+
if (challengeSolveAttempted || challengeRefetchAttempted) {
|
|
2210
|
+
normalized.challenge = {
|
|
2211
|
+
challenge: detected,
|
|
2212
|
+
outcome: "challenge_persisted",
|
|
2213
|
+
};
|
|
2214
|
+
return finalizeAttempt(response, normalized);
|
|
2215
|
+
}
|
|
2216
|
+
const automaticReplayEligible = isProxyTransportRetryMethod(
|
|
2217
|
+
method,
|
|
2218
|
+
automaticChallengeRefetchPolicy,
|
|
2219
|
+
{ body: requestBody, headers: options.headers },
|
|
2220
|
+
);
|
|
2221
|
+
if (automaticReplayEligible && !akamaiSbsd.solve) {
|
|
2222
|
+
normalized.challenge = {
|
|
2223
|
+
challenge: detected,
|
|
2224
|
+
outcome: "resolver_unavailable",
|
|
2225
|
+
};
|
|
2226
|
+
return finalizeAttempt(response, normalized);
|
|
779
2227
|
}
|
|
780
|
-
}
|
|
781
2228
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
2229
|
+
const emulationHeaderMap = new Map(
|
|
2230
|
+
emulationHeaders.map(([name, value]) => [name.toLowerCase(), value] as const),
|
|
2231
|
+
);
|
|
2232
|
+
const initiatingHeaders = normalizeHeaders({ ...(options.headers ?? {}) });
|
|
2233
|
+
// Mirror what the protected request actually sent: `stealth.acceptLanguage` is
|
|
2234
|
+
// applied by the Chrome header builder only; Safari/Firefox sessions send the
|
|
2235
|
+
// emulation default.
|
|
2236
|
+
const sessionHeaders = {
|
|
2237
|
+
"User-Agent": requiredEmulationHeader(emulationHeaderMap, "user-agent"),
|
|
2238
|
+
"Accept-Language":
|
|
2239
|
+
initiatingHeaders["accept-language"] ??
|
|
2240
|
+
(chromeEmulationHeaders ? clientOptions.stealth?.acceptLanguage : undefined) ??
|
|
2241
|
+
requiredEmulationHeader(emulationHeaderMap, "accept-language"),
|
|
2242
|
+
};
|
|
2243
|
+
const resolverBuildHeaders = chromeEmulationHeaders
|
|
2244
|
+
? (
|
|
2245
|
+
currentUrl: string,
|
|
2246
|
+
currentMethod: StealthMethod,
|
|
2247
|
+
currentBody: string | Buffer | undefined,
|
|
2248
|
+
currentHeaders: Record<string, string>,
|
|
2249
|
+
) =>
|
|
2250
|
+
buildChromeHeaderTuples({
|
|
2251
|
+
emulationHeaders: chromeEmulationHeaders,
|
|
2252
|
+
method: currentMethod,
|
|
2253
|
+
body: currentBody,
|
|
2254
|
+
headers: currentHeaders,
|
|
2255
|
+
requestUrl: currentUrl,
|
|
2256
|
+
acceptLanguage: clientOptions.stealth?.acceptLanguage,
|
|
2257
|
+
})
|
|
2258
|
+
: undefined;
|
|
2259
|
+
const resolverTransport: ResolverVendorTransport = {
|
|
2260
|
+
sessionHeaders,
|
|
2261
|
+
getCookie(name, cookieUrl) {
|
|
2262
|
+
return cookieJar.get(name, cookieUrl);
|
|
2263
|
+
},
|
|
2264
|
+
async fetch(transportUrl, init) {
|
|
2265
|
+
const boundSignal = clientOptions.signal
|
|
2266
|
+
? AbortSignal.any([clientOptions.signal, init.signal])
|
|
2267
|
+
: init.signal;
|
|
2268
|
+
const transportHeaders = { ...(init.headers ?? {}) };
|
|
2269
|
+
if (resolverBuildHeaders) {
|
|
2270
|
+
for (const name of Object.keys(transportHeaders)) {
|
|
2271
|
+
if (name.toLowerCase() === "user-agent") {
|
|
2272
|
+
delete transportHeaders[name];
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
// Session defaults come from this request's own shape, never from the
|
|
2277
|
+
// initiating request: its defaults carry the upstream Cookie header,
|
|
2278
|
+
// which wreq would otherwise merge into the Hyper /ip call.
|
|
2279
|
+
const result = await fetchOnBoundSession(
|
|
2280
|
+
transportUrl,
|
|
2281
|
+
init.method,
|
|
2282
|
+
{
|
|
2283
|
+
method: init.method,
|
|
2284
|
+
headers: transportHeaders,
|
|
2285
|
+
...(init.body === undefined ? {} : { body: init.body }),
|
|
2286
|
+
redirect: "manual",
|
|
2287
|
+
throwOnHttpError: false,
|
|
2288
|
+
...(init.maxBodyBytes === undefined
|
|
2289
|
+
? {}
|
|
2290
|
+
: { maxBodyBytes: init.maxBodyBytes }),
|
|
2291
|
+
},
|
|
2292
|
+
boundSignal,
|
|
2293
|
+
resolverBuildHeaders,
|
|
2294
|
+
resolverBuildHeaders?.(
|
|
2295
|
+
transportUrl,
|
|
2296
|
+
init.method,
|
|
2297
|
+
init.body,
|
|
2298
|
+
normalizeHeaders(transportHeaders),
|
|
2299
|
+
),
|
|
2300
|
+
"resolver",
|
|
2301
|
+
);
|
|
2302
|
+
return {
|
|
2303
|
+
status: result.normalized.status,
|
|
2304
|
+
headers: result.normalized.headers,
|
|
2305
|
+
body: result.normalized.body,
|
|
2306
|
+
cookies: resolverCookiesFromResponse(result.response, transportUrl),
|
|
2307
|
+
};
|
|
2308
|
+
},
|
|
2309
|
+
};
|
|
2310
|
+
// `options` is already the fetch-start snapshot; the replay gets its own copy
|
|
2311
|
+
// of the body bytes so the transport cannot alias the first send's buffer.
|
|
2312
|
+
const replayOptions: StealthFetchOptions = {
|
|
2313
|
+
...options,
|
|
2314
|
+
...(Buffer.isBuffer(requestBody) ? { body: Buffer.from(requestBody) } : {}),
|
|
2315
|
+
};
|
|
2316
|
+
const transactionKey = akamaiSbsdChallengeKey(detected, {
|
|
2317
|
+
wreqBrowser: mapping.browser,
|
|
2318
|
+
wreqOs: mapping.os,
|
|
2319
|
+
proxyUrl: proxy,
|
|
786
2320
|
});
|
|
2321
|
+
const challenged = { normalized, response };
|
|
2322
|
+
const solveAndReplay = async (
|
|
2323
|
+
explicitReplay: boolean,
|
|
2324
|
+
): Promise<{ normalized: StealthResponse; response: StealthTransportResponse }> => {
|
|
2325
|
+
if (!akamaiSbsd.solve) {
|
|
2326
|
+
throw new SDKError("No resolver is available for the challenged request", {
|
|
2327
|
+
code: "RESOLVER_UNAVAILABLE",
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
// Only a solve that will actually run on this session needs the profile match.
|
|
2331
|
+
assertAkamaiSbsdClientProfile(akamaiSbsd.clientProfile, requestProfile);
|
|
2332
|
+
// A safe request that already solved this exact challenge on this identity
|
|
2333
|
+
// left the session cookies valid: the explicit replay reuses them once
|
|
2334
|
+
// instead of paying for a second solve.
|
|
2335
|
+
const reuseCompletedSuccess =
|
|
2336
|
+
explicitReplay && akamaiSbsdState.completedSuccessKey === transactionKey;
|
|
2337
|
+
if (reuseCompletedSuccess) akamaiSbsdState.completedSuccessKey = undefined;
|
|
2338
|
+
let ownsTransaction = false;
|
|
2339
|
+
if (!reuseCompletedSuccess) {
|
|
2340
|
+
let transaction = akamaiSbsdState.transactions.get(transactionKey);
|
|
2341
|
+
if (!transaction) {
|
|
2342
|
+
akamaiSbsdState.completedSuccessKey = undefined;
|
|
2343
|
+
challengeSolveAttempted = true;
|
|
2344
|
+
ownsTransaction = true;
|
|
2345
|
+
// The solve spans several round trips, so it runs under the client's ambient
|
|
2346
|
+
// signal and the resolver's own timeouts, not this fetch's per-request `timeout`.
|
|
2347
|
+
transaction = {
|
|
2348
|
+
result: akamaiSbsd
|
|
2349
|
+
.solve(
|
|
2350
|
+
detected,
|
|
2351
|
+
resolverTransport,
|
|
2352
|
+
clientOptions.signal ?? new AbortController().signal,
|
|
2353
|
+
)
|
|
2354
|
+
.then(
|
|
2355
|
+
() => ({ solved: true }) as const,
|
|
2356
|
+
(error: unknown) => ({ solved: false, error }) as const,
|
|
2357
|
+
)
|
|
2358
|
+
.finally(() => {
|
|
2359
|
+
akamaiSbsdState.transactions.delete(transactionKey);
|
|
2360
|
+
}),
|
|
2361
|
+
};
|
|
2362
|
+
akamaiSbsdState.transactions.set(transactionKey, transaction);
|
|
2363
|
+
}
|
|
2364
|
+
const transactionResult = await transaction.result;
|
|
2365
|
+
if (!transactionResult.solved) {
|
|
2366
|
+
emitStealthTelemetry(clientOptions.stealthTelemetry, (sink) =>
|
|
2367
|
+
sink.recordSbsd("solve_failed"),
|
|
2368
|
+
);
|
|
2369
|
+
if (ownsTransaction) {
|
|
2370
|
+
// The proxy delivered the challenged response; the resolver failed.
|
|
2371
|
+
// Surface that failure as-is: it is not a transport fault to normalize
|
|
2372
|
+
// or retry.
|
|
2373
|
+
if (!explicitReplay) {
|
|
2374
|
+
recordProxyAttempt("ok", undefined, challenged.response.status);
|
|
2375
|
+
}
|
|
2376
|
+
challengeSolveFailure = { error: transactionResult.error };
|
|
2377
|
+
throw challengeSolveFailure;
|
|
2378
|
+
}
|
|
2379
|
+
return {
|
|
2380
|
+
normalized: {
|
|
2381
|
+
...challenged.normalized,
|
|
2382
|
+
challenge: { challenge: detected, outcome: "solve_failed" },
|
|
2383
|
+
},
|
|
2384
|
+
response: challenged.response,
|
|
2385
|
+
};
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
challengeRefetchAttempted = true;
|
|
2389
|
+
// An explicit replay is a new transport exchange on the bound endpoint and
|
|
2390
|
+
// gets its own proxy-attempt record timed from here, not from the initiating
|
|
2391
|
+
// fetch (the caller's think time in between is not transport duration); the
|
|
2392
|
+
// automatic refetch stays within the initiating attempt's record.
|
|
2393
|
+
if (explicitReplay) {
|
|
2394
|
+
// The solve above may have taken seconds; if another session of this client
|
|
2395
|
+
// moved the ceremony meanwhile, the mutation must not go out on the retired
|
|
2396
|
+
// endpoint.
|
|
2397
|
+
if (currentEgressGeneration() !== attemptEgressGeneration) {
|
|
2398
|
+
throw new SDKError(
|
|
2399
|
+
"The ceremony's egress binding changed while the replay was being solved",
|
|
2400
|
+
{ code: "REPLAY_SESSION_MISMATCH" },
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
attemptRecorded = false;
|
|
2404
|
+
attemptStartedAt = Date.now();
|
|
2405
|
+
}
|
|
2406
|
+
if (!explicitReplay)
|
|
2407
|
+
emitStealthTelemetry(clientOptions.stealthTelemetry, (sink) =>
|
|
2408
|
+
sink.recordSafeRefetch(),
|
|
2409
|
+
);
|
|
2410
|
+
const replayed = await fetchOnBoundSession(
|
|
2411
|
+
requestUrl,
|
|
2412
|
+
method,
|
|
2413
|
+
replayOptions,
|
|
2414
|
+
clientOptions.signal,
|
|
2415
|
+
);
|
|
2416
|
+
throwProxyTransportFault(replayed.response, replayed.normalized.body);
|
|
2417
|
+
const persisted = detectAkamaiSbsdChallenge(
|
|
2418
|
+
replayed.normalized,
|
|
2419
|
+
requestUrl,
|
|
2420
|
+
cookieJar,
|
|
2421
|
+
akamaiSbsd.allowedHosts,
|
|
2422
|
+
akamaiSbsdState,
|
|
2423
|
+
);
|
|
2424
|
+
emitStealthTelemetry(clientOptions.stealthTelemetry, (sink) =>
|
|
2425
|
+
sink.recordSbsd(persisted ? "challenge_persisted" : "refetch_clear"),
|
|
2426
|
+
);
|
|
2427
|
+
if (persisted) {
|
|
2428
|
+
replayed.normalized.challenge = {
|
|
2429
|
+
challenge: persisted,
|
|
2430
|
+
outcome: "challenge_persisted",
|
|
2431
|
+
};
|
|
2432
|
+
} else if (ownsTransaction && !explicitReplay) {
|
|
2433
|
+
akamaiSbsdState.completedSuccessKey = transactionKey;
|
|
2434
|
+
}
|
|
2435
|
+
return replayed;
|
|
2436
|
+
};
|
|
2437
|
+
|
|
2438
|
+
if (!automaticReplayEligible) {
|
|
2439
|
+
normalized.challenge = {
|
|
2440
|
+
challenge: detected,
|
|
2441
|
+
outcome: "replay_required",
|
|
2442
|
+
};
|
|
2443
|
+
challengedReplays.set(normalized, {
|
|
2444
|
+
consumed: false,
|
|
2445
|
+
generation: attemptEgressGeneration,
|
|
2446
|
+
async replay() {
|
|
2447
|
+
try {
|
|
2448
|
+
const replayed = await solveAndReplay(true);
|
|
2449
|
+
return finalizeAttempt(replayed.response, replayed.normalized);
|
|
2450
|
+
} catch (error) {
|
|
2451
|
+
const normalizedError = normalizeAttemptError(error);
|
|
2452
|
+
releaseFailedCeremonyEgress(proxy, normalizedError);
|
|
2453
|
+
throw recordAttemptFailure(normalizedError);
|
|
2454
|
+
}
|
|
2455
|
+
},
|
|
2456
|
+
});
|
|
2457
|
+
return finalizeAttempt(response, normalized);
|
|
2458
|
+
}
|
|
2459
|
+
({ normalized, response } = await solveAndReplay(false));
|
|
787
2460
|
}
|
|
788
2461
|
|
|
789
|
-
|
|
790
|
-
return normalized;
|
|
2462
|
+
return finalizeAttempt(response, normalized);
|
|
791
2463
|
} catch (error) {
|
|
792
|
-
const normalizedError =
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
if (
|
|
800
|
-
|
|
801
|
-
|
|
2464
|
+
const normalizedError = normalizeAttemptError(error);
|
|
2465
|
+
const retryErrorCode = proxyAttemptErrorCode(normalizedError);
|
|
2466
|
+
const refreshableProxyError = isProxyPoolRefreshableError(normalizedError);
|
|
2467
|
+
const runProxyAuthDiagnostic = shouldRunProxyAuthDiagnostic(normalizedError);
|
|
2468
|
+
const redactedError = recordAttemptFailure(normalizedError);
|
|
2469
|
+
lastError = redactedError;
|
|
2470
|
+
const releasedBoundEgress = releaseFailedCeremonyEgress(proxy, normalizedError);
|
|
2471
|
+
if (releasedBoundEgress) {
|
|
2472
|
+
// The request now proceeds as an unbound one with the full rotation budget;
|
|
2473
|
+
// the attempt spent on the dead endpoint does not count against it.
|
|
2474
|
+
maxAttempts = attempt + 1 + unboundMaxAttempts;
|
|
2475
|
+
}
|
|
2476
|
+
if (challengeSolveAttempted || challengeRefetchAttempted) {
|
|
2477
|
+
// The single refetch is spent (owner or waiter of a shared solve): another
|
|
2478
|
+
// transport attempt would replay the original request and may rotate the
|
|
2479
|
+
// proxy the solved jar is bound to.
|
|
2480
|
+
throw redactedError;
|
|
2481
|
+
}
|
|
2482
|
+
if (proxy && rotatesRegistryChain && refreshableProxyError) {
|
|
2483
|
+
// Also the path a released bound endpoint takes after a proxy-level refusal:
|
|
2484
|
+
// the stale-pool bookkeeping here is what drives allocator invalidation and the
|
|
2485
|
+
// refresh pass when the fresh selection lands on the same endpoint again.
|
|
2486
|
+
stalePoolError = redactedError;
|
|
2487
|
+
if (runProxyAuthDiagnostic) {
|
|
802
2488
|
stalePoolDiagnosticProxy = proxy;
|
|
803
2489
|
}
|
|
804
2490
|
if (attempt + 1 < maxAttempts) {
|
|
@@ -807,12 +2493,40 @@ function createSessionFetcher(
|
|
|
807
2493
|
break;
|
|
808
2494
|
}
|
|
809
2495
|
if (
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
2496
|
+
releasedBoundEgress &&
|
|
2497
|
+
attempt + 1 < maxAttempts &&
|
|
2498
|
+
retryErrorCode === PROXY_CONNECT_FAILURE_CODE
|
|
2499
|
+
) {
|
|
2500
|
+
// The tunnel to the released endpoint never opened, so the origin never saw the
|
|
2501
|
+
// request: selecting and binding a fresh endpoint in this request is safe for
|
|
2502
|
+
// any method. Ambiguous failures (reset, timeout) fall through to the ordinary
|
|
2503
|
+
// method-aware retry decision below with the binding already released.
|
|
2504
|
+
continue;
|
|
2505
|
+
}
|
|
2506
|
+
// Cap the number of transport retries. For a policy-allocator chain,
|
|
2507
|
+
// every attempt resolves a *different* endpoint/vendor (poolIndex
|
|
2508
|
+
// rotates across the concatenated vendor pool spans), so a transport
|
|
2509
|
+
// failure is a signal to advance to the next endpoint — potentially
|
|
2510
|
+
// crossing into the fallback vendor — not to retry the same endpoint.
|
|
2511
|
+
// Truncating that rotation at the per-endpoint retry budget would
|
|
2512
|
+
// strand the request on the primary vendor and never reach the
|
|
2513
|
+
// fallback, since the crossover only happens once the flat attempt
|
|
2514
|
+
// index exceeds the primary vendor's pool size (~10-20).
|
|
2515
|
+
// resolvePolicyTransportAttemptCap widens to the full chain span only
|
|
2516
|
+
// for implicit, safe-method allocator requests; explicit retry
|
|
2517
|
+
// policies (their documented `attempts` ceiling), unsafe methods, and
|
|
2518
|
+
// static/non-registry vendors keep the per-endpoint retry budget.
|
|
2519
|
+
const transportRetryCap = resolvePolicyTransportAttemptCap({
|
|
2520
|
+
policy: policyProxy,
|
|
2521
|
+
usesPolicyAllocator,
|
|
2522
|
+
retryAttempts: stealthRetryOptions?.attempts ?? 1,
|
|
2523
|
+
explicitRetry: hasExplicitRetryPolicy,
|
|
2524
|
+
method,
|
|
2525
|
+
});
|
|
2526
|
+
if (
|
|
2527
|
+
attempt + 1 < transportRetryCap &&
|
|
814
2528
|
shouldRetryProxyTransportAttempt({
|
|
815
|
-
error:
|
|
2529
|
+
error: { code: retryErrorCode },
|
|
816
2530
|
explicitRetry: hasExplicitRetryPolicy,
|
|
817
2531
|
method,
|
|
818
2532
|
options: stealthRetryOptions,
|
|
@@ -820,33 +2534,37 @@ function createSessionFetcher(
|
|
|
820
2534
|
})
|
|
821
2535
|
) {
|
|
822
2536
|
if (stealthRetryOptions) {
|
|
823
|
-
await sleep(
|
|
2537
|
+
await sleep(
|
|
2538
|
+
computeProxyTransportRetryDelayMs(stealthRetryOptions, attempt + 1),
|
|
2539
|
+
clientOptions.signal,
|
|
2540
|
+
);
|
|
824
2541
|
}
|
|
2542
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
825
2543
|
continue;
|
|
826
2544
|
}
|
|
827
|
-
throw
|
|
2545
|
+
throw redactedError;
|
|
828
2546
|
}
|
|
829
2547
|
}
|
|
830
2548
|
|
|
831
2549
|
if (
|
|
832
|
-
|
|
2550
|
+
rotatesRegistryChain &&
|
|
833
2551
|
stalePoolError &&
|
|
834
2552
|
refreshAttempt < MAX_POLICY_PROXY_POOL_REFRESHES
|
|
835
2553
|
) {
|
|
2554
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
2555
|
+
emitStealthTelemetry(clientOptions.stealthTelemetry, (sink) => sink.recordPoolRefresh());
|
|
836
2556
|
await invalidateProxyResolutionCacheAsync({
|
|
837
2557
|
proxyPolicy: clientOptions.proxyPolicy,
|
|
838
2558
|
upstream: clientOptions.upstream,
|
|
839
2559
|
affinityKey: clientOptions.affinityKey,
|
|
840
2560
|
});
|
|
2561
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
841
2562
|
continue;
|
|
842
2563
|
}
|
|
843
2564
|
|
|
844
2565
|
const proxyAuthDiagnostic =
|
|
845
2566
|
stalePoolError && stalePoolDiagnosticProxy
|
|
846
|
-
? await classifyProxyAuthDiagnostic(
|
|
847
|
-
options.profile ?? defaultProfile,
|
|
848
|
-
stalePoolDiagnosticProxy,
|
|
849
|
-
)
|
|
2567
|
+
? await classifyProxyAuthDiagnostic(requestProfile, stalePoolDiagnosticProxy)
|
|
850
2568
|
: undefined;
|
|
851
2569
|
if (proxyAuthDiagnostic === "source_ip_denied") {
|
|
852
2570
|
throw createProxyAuthIpDeniedError(
|
|
@@ -873,8 +2591,38 @@ function createSessionFetcher(
|
|
|
873
2591
|
break;
|
|
874
2592
|
}
|
|
875
2593
|
|
|
2594
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
876
2595
|
throw normalizeStealthTransportError(lastError);
|
|
877
2596
|
},
|
|
2597
|
+
async replayChallenged(challengedResponse) {
|
|
2598
|
+
// Lease expiry is evaluated when a fetch starts, not here: the replay belongs to the
|
|
2599
|
+
// exchange that was challenged on the bound endpoint (Akamai state is IP-bound), so a
|
|
2600
|
+
// lifetime boundary crossed during the solve does not move it elsewhere. The next
|
|
2601
|
+
// fetch drops the expired binding and rebinds.
|
|
2602
|
+
const replay = challengedReplays.get(challengedResponse);
|
|
2603
|
+
if (!replay || challengedResponse.challenge?.outcome !== "replay_required") {
|
|
2604
|
+
throw new SDKError("The challenged response does not belong to this stealth session", {
|
|
2605
|
+
code: "REPLAY_SESSION_MISMATCH",
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2608
|
+
syncCeremonyEgress();
|
|
2609
|
+
if (replay.generation !== currentEgressGeneration()) {
|
|
2610
|
+
// The ceremony rebound to another egress after this request ran (in this session or
|
|
2611
|
+
// another of the same client); solving and replaying on the retired endpoint would
|
|
2612
|
+
// split the identity the handle names.
|
|
2613
|
+
throw new SDKError(
|
|
2614
|
+
"The challenged response predates the ceremony's current egress binding",
|
|
2615
|
+
{ code: "REPLAY_SESSION_MISMATCH" },
|
|
2616
|
+
);
|
|
2617
|
+
}
|
|
2618
|
+
if (replay.consumed) {
|
|
2619
|
+
throw new SDKError("The challenged request has already used its one replay budget", {
|
|
2620
|
+
code: "REPLAY_ALREADY_ATTEMPTED",
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
replay.consumed = true;
|
|
2624
|
+
return replay.replay();
|
|
2625
|
+
},
|
|
878
2626
|
cookies: cookieJar,
|
|
879
2627
|
redirects: {
|
|
880
2628
|
async run(options) {
|
|
@@ -882,25 +2630,102 @@ function createSessionFetcher(
|
|
|
882
2630
|
options.maxHops === undefined || !Number.isFinite(options.maxHops)
|
|
883
2631
|
? 10
|
|
884
2632
|
: Math.max(0, Math.floor(options.maxHops));
|
|
2633
|
+
const {
|
|
2634
|
+
url: _url,
|
|
2635
|
+
maxHops: _maxHops,
|
|
2636
|
+
stopWhen,
|
|
2637
|
+
params,
|
|
2638
|
+
sensitiveParams,
|
|
2639
|
+
headers: initialHeaders,
|
|
2640
|
+
...fetchOptions
|
|
2641
|
+
} = options;
|
|
885
2642
|
const hops: StealthRedirectHop[] = [];
|
|
886
|
-
let currentUrl = resolveUrl(baseUrl, options.url);
|
|
887
2643
|
let method = normalizeMethod(options.method ?? "GET");
|
|
888
2644
|
let body = options.body;
|
|
2645
|
+
let headers = initialHeaders;
|
|
889
2646
|
let response: StealthResponse | undefined;
|
|
890
2647
|
const visitedRequests = new Set<string>();
|
|
891
|
-
|
|
892
|
-
|
|
2648
|
+
const initialParams = params
|
|
2649
|
+
? Object.fromEntries(
|
|
2650
|
+
Object.entries(params).map(([key, value]) => [
|
|
2651
|
+
key,
|
|
2652
|
+
Array.isArray(value) ? [...value] : value,
|
|
2653
|
+
]),
|
|
2654
|
+
)
|
|
2655
|
+
: undefined;
|
|
2656
|
+
const normalizedSensitiveParams = normalizeSensitiveParams(sensitiveParams);
|
|
2657
|
+
const initialSensitiveParams = normalizedSensitiveParams
|
|
2658
|
+
? { ...normalizedSensitiveParams }
|
|
2659
|
+
: undefined;
|
|
2660
|
+
const sensitiveParamNames = initialSensitiveParams
|
|
2661
|
+
? Object.keys(initialSensitiveParams)
|
|
2662
|
+
: [];
|
|
2663
|
+
const callerStructural = redactUrlQueryParams(options.url, sensitiveParamNames);
|
|
2664
|
+
const sensitiveValues = new Set(
|
|
2665
|
+
[
|
|
2666
|
+
...Object.values(initialSensitiveParams ?? {}),
|
|
2667
|
+
...callerStructural.sensitiveValues,
|
|
2668
|
+
].filter((value) => value !== ""),
|
|
2669
|
+
);
|
|
2670
|
+
const redactRedirectUrl = (value: string): string => {
|
|
2671
|
+
const structural = redactUrlQueryParams(value, [
|
|
2672
|
+
...new Set([...sensitiveParamNames, ...sensitiveQueryParamNames(value)]),
|
|
2673
|
+
]);
|
|
2674
|
+
for (const sensitiveValue of structural.sensitiveValues) {
|
|
2675
|
+
sensitiveValues.add(sensitiveValue);
|
|
2676
|
+
}
|
|
2677
|
+
return redactSensitiveText(structural.redactedUrl, [...sensitiveValues]);
|
|
2678
|
+
};
|
|
2679
|
+
let currentUrl: string;
|
|
2680
|
+
let initialUrl: ReturnType<typeof serializeRequestUrl>;
|
|
2681
|
+
try {
|
|
2682
|
+
currentUrl = resolveUrl(baseUrl, options.url);
|
|
2683
|
+
redactRedirectUrl(currentUrl);
|
|
2684
|
+
initialUrl = serializeRequestUrl(currentUrl, initialParams, initialSensitiveParams);
|
|
2685
|
+
for (const value of initialUrl.sensitiveValues) {
|
|
2686
|
+
if (value !== "") sensitiveValues.add(value);
|
|
2687
|
+
}
|
|
2688
|
+
} catch (error) {
|
|
2689
|
+
throw redactSensitiveError(
|
|
2690
|
+
error,
|
|
2691
|
+
[...sensitiveValues],
|
|
2692
|
+
options.url,
|
|
2693
|
+
redactRedirectUrl(options.url),
|
|
2694
|
+
);
|
|
2695
|
+
}
|
|
893
2696
|
|
|
894
2697
|
for (let hopIndex = 0; hopIndex <= maxHops; hopIndex += 1) {
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
2698
|
+
const outboundUrl =
|
|
2699
|
+
hopIndex === 0 ? initialUrl.requestUrl : serializeRequestUrl(currentUrl).requestUrl;
|
|
2700
|
+
// Preserve params-only loop bookkeeping from before sensitiveParams:
|
|
2701
|
+
// the first visited key is the caller's resolved URL, not its expanded query.
|
|
2702
|
+
const visitedUrl = hopIndex === 0 && !initialSensitiveParams ? currentUrl : outboundUrl;
|
|
2703
|
+
visitedRequests.add(`${method} ${visitedUrl}`);
|
|
2704
|
+
try {
|
|
2705
|
+
response = await session.fetch(currentUrl, {
|
|
2706
|
+
...fetchOptions,
|
|
2707
|
+
...(headers ? { headers } : {}),
|
|
2708
|
+
body,
|
|
2709
|
+
method,
|
|
2710
|
+
...(hopIndex === 0 && initialParams ? { params: initialParams } : {}),
|
|
2711
|
+
...(hopIndex === 0 && initialSensitiveParams
|
|
2712
|
+
? { sensitiveParams: initialSensitiveParams }
|
|
2713
|
+
: {}),
|
|
2714
|
+
redirect: "manual",
|
|
2715
|
+
throwOnHttpError: false,
|
|
2716
|
+
});
|
|
2717
|
+
} catch (error) {
|
|
2718
|
+
throw redactSensitiveError(
|
|
2719
|
+
error,
|
|
2720
|
+
[...sensitiveValues],
|
|
2721
|
+
outboundUrl,
|
|
2722
|
+
redactRedirectUrl(outboundUrl),
|
|
2723
|
+
);
|
|
2724
|
+
}
|
|
2725
|
+
// StealthResponse.url is programmatic metadata and remains raw. Only the
|
|
2726
|
+
// redirect hop emitted below is a diagnostic surface.
|
|
2727
|
+
const responseUrl =
|
|
2728
|
+
response.url ?? (hopIndex === 0 && initialSensitiveParams ? outboundUrl : currentUrl);
|
|
904
2729
|
|
|
905
2730
|
if (!isRedirectStatus(response.status)) {
|
|
906
2731
|
return {
|
|
@@ -908,63 +2733,90 @@ function createSessionFetcher(
|
|
|
908
2733
|
hops,
|
|
909
2734
|
reason: "completed",
|
|
910
2735
|
cookies: cookieJar.snapshot(),
|
|
2736
|
+
cookieStore: cookieJar.serialize(),
|
|
911
2737
|
};
|
|
912
2738
|
}
|
|
913
2739
|
|
|
914
2740
|
const location = locationHeader(response.headers);
|
|
915
|
-
const
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
2741
|
+
const redactedResponseUrl = redactRedirectUrl(responseUrl);
|
|
2742
|
+
const redactedLocation = location ? redactRedirectUrl(location) : undefined;
|
|
2743
|
+
let nextUrl: string | undefined;
|
|
2744
|
+
try {
|
|
2745
|
+
nextUrl = resolveRedirectUrl(location, responseUrl);
|
|
2746
|
+
} catch (error) {
|
|
2747
|
+
throw redactSensitiveError(error, [...sensitiveValues], location, redactedLocation);
|
|
2748
|
+
}
|
|
2749
|
+
const realHop: StealthRedirectHop = {
|
|
2750
|
+
url: responseUrl,
|
|
920
2751
|
status: response.status,
|
|
921
2752
|
method,
|
|
922
2753
|
...(location ? { location } : {}),
|
|
923
2754
|
...(nextUrl ? { nextUrl } : {}),
|
|
924
2755
|
};
|
|
2756
|
+
const hop: StealthRedirectHop = {
|
|
2757
|
+
...realHop,
|
|
2758
|
+
url: redactedResponseUrl,
|
|
2759
|
+
...(redactedLocation ? { location: redactedLocation } : {}),
|
|
2760
|
+
...(nextUrl ? { nextUrl: redactRedirectUrl(nextUrl) } : {}),
|
|
2761
|
+
};
|
|
925
2762
|
hops.push(hop);
|
|
926
2763
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
2764
|
+
let shouldStop = false;
|
|
2765
|
+
if (stopWhen) {
|
|
2766
|
+
try {
|
|
2767
|
+
shouldStop = await stopWhen(realHop);
|
|
2768
|
+
} catch (error) {
|
|
2769
|
+
let sanitizedError: unknown = error;
|
|
2770
|
+
for (const [rawUrl, safeUrl] of [
|
|
2771
|
+
[responseUrl, redactedResponseUrl],
|
|
2772
|
+
[location, redactedLocation],
|
|
2773
|
+
[nextUrl, nextUrl ? redactRedirectUrl(nextUrl) : undefined],
|
|
2774
|
+
] as const) {
|
|
2775
|
+
if (!rawUrl || !safeUrl) continue;
|
|
2776
|
+
sanitizedError = redactSensitiveError(
|
|
2777
|
+
sanitizedError,
|
|
2778
|
+
[...sensitiveValues],
|
|
2779
|
+
rawUrl,
|
|
2780
|
+
safeUrl,
|
|
2781
|
+
);
|
|
2782
|
+
}
|
|
2783
|
+
throw sanitizedError;
|
|
2784
|
+
}
|
|
934
2785
|
}
|
|
935
|
-
|
|
936
|
-
|
|
2786
|
+
const decision = evaluateRedirectHop({
|
|
2787
|
+
status: response.status,
|
|
2788
|
+
method,
|
|
2789
|
+
nextUrl,
|
|
2790
|
+
shouldStop,
|
|
2791
|
+
redirectCount: hops.length,
|
|
2792
|
+
maxHops,
|
|
2793
|
+
visitedRequests,
|
|
2794
|
+
});
|
|
2795
|
+
if (decision.kind === "stop") {
|
|
937
2796
|
return {
|
|
938
2797
|
final: response,
|
|
939
2798
|
hops,
|
|
940
|
-
reason:
|
|
2799
|
+
reason: decision.reason,
|
|
941
2800
|
cookies: cookieJar.snapshot(),
|
|
2801
|
+
cookieStore: cookieJar.serialize(),
|
|
942
2802
|
};
|
|
943
2803
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
2804
|
+
if (headers) {
|
|
2805
|
+
headers = redirectHopHeaders(
|
|
2806
|
+
{
|
|
2807
|
+
from: currentUrl,
|
|
2808
|
+
to: decision.nextUrl,
|
|
2809
|
+
methodChanged: decision.nextMethod !== method,
|
|
2810
|
+
},
|
|
2811
|
+
headers,
|
|
2812
|
+
);
|
|
952
2813
|
}
|
|
953
|
-
|
|
954
|
-
const nextMethod = nextRedirectMethod(response.status, method);
|
|
955
|
-
if (nextMethod !== method) {
|
|
2814
|
+
if (decision.nextMethod !== method) {
|
|
956
2815
|
body = undefined;
|
|
957
2816
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
hops,
|
|
962
|
-
reason: "loop",
|
|
963
|
-
cookies: cookieJar.snapshot(),
|
|
964
|
-
};
|
|
965
|
-
}
|
|
966
|
-
method = nextMethod;
|
|
967
|
-
currentUrl = nextUrl;
|
|
2817
|
+
emitStealthTelemetry(clientOptions.stealthTelemetry, (sink) => sink.recordRedirectHop());
|
|
2818
|
+
method = decision.nextMethod;
|
|
2819
|
+
currentUrl = decision.nextUrl;
|
|
968
2820
|
}
|
|
969
2821
|
|
|
970
2822
|
if (!response) {
|
|
@@ -982,28 +2834,80 @@ function createSessionFetcher(
|
|
|
982
2834
|
hops,
|
|
983
2835
|
reason: "max_hops",
|
|
984
2836
|
cookies: cookieJar.snapshot(),
|
|
2837
|
+
cookieStore: cookieJar.serialize(),
|
|
985
2838
|
};
|
|
986
2839
|
},
|
|
987
2840
|
},
|
|
988
2841
|
close() {
|
|
989
2842
|
closed = true;
|
|
2843
|
+
clearAkamaiSbsdState();
|
|
2844
|
+
for (const client of clients.values()) {
|
|
2845
|
+
void client.session
|
|
2846
|
+
.then((session) => session.close())
|
|
2847
|
+
.catch((error: unknown) => {
|
|
2848
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2849
|
+
warn(`[provider-sdk] Failed to close stealth transport session: ${message}`);
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
990
2852
|
clients.clear();
|
|
991
2853
|
},
|
|
992
2854
|
};
|
|
993
2855
|
return session;
|
|
994
2856
|
|
|
995
2857
|
async function classifyProxyAuthDiagnostic(
|
|
996
|
-
|
|
2858
|
+
profile: StealthProfileDescriptor,
|
|
997
2859
|
proxy: string,
|
|
998
2860
|
): Promise<"source_ip_denied" | "edge_auth_rejected" | undefined> {
|
|
999
2861
|
try {
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
2862
|
+
return await withClient(
|
|
2863
|
+
profile,
|
|
2864
|
+
proxy,
|
|
2865
|
+
false,
|
|
2866
|
+
async (client) => {
|
|
2867
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
2868
|
+
await client.clearCookies();
|
|
2869
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
2870
|
+
const finishExchange = startStealthExchange(
|
|
2871
|
+
clientOptions.stealthTelemetry
|
|
2872
|
+
? {
|
|
2873
|
+
sink: clientOptions.stealthTelemetry,
|
|
2874
|
+
profileId: profile,
|
|
2875
|
+
proxyUsed: true,
|
|
2876
|
+
kind: "proxy_diagnostic",
|
|
2877
|
+
cookieJar,
|
|
2878
|
+
}
|
|
2879
|
+
: undefined,
|
|
2880
|
+
"GET",
|
|
2881
|
+
{},
|
|
2882
|
+
clientOptions.signal,
|
|
2883
|
+
);
|
|
2884
|
+
let status: number | undefined;
|
|
2885
|
+
let exchangeError: unknown;
|
|
2886
|
+
try {
|
|
2887
|
+
const response = await client.fetch(PROXY_AUTH_DIAGNOSTIC_URL, {
|
|
2888
|
+
method: "GET",
|
|
2889
|
+
timeout: PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS,
|
|
2890
|
+
...(clientOptions.signal ? { signal: clientOptions.signal } : {}),
|
|
2891
|
+
});
|
|
2892
|
+
status = response.status;
|
|
2893
|
+
const normalized = await normalizeResponseWithSignal(
|
|
2894
|
+
response,
|
|
2895
|
+
undefined,
|
|
2896
|
+
undefined,
|
|
2897
|
+
clientOptions.signal,
|
|
2898
|
+
);
|
|
2899
|
+
return classifyProxyAuthDiagnosticMessage(normalized.body);
|
|
2900
|
+
} catch (error) {
|
|
2901
|
+
exchangeError = error;
|
|
2902
|
+
throw error;
|
|
2903
|
+
} finally {
|
|
2904
|
+
finishExchange(status, exchangeError);
|
|
2905
|
+
}
|
|
2906
|
+
},
|
|
2907
|
+
clientOptions.signal,
|
|
2908
|
+
);
|
|
1006
2909
|
} catch (error) {
|
|
2910
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
1007
2911
|
const message =
|
|
1008
2912
|
error instanceof Error
|
|
1009
2913
|
? [error.message, error.cause instanceof Error ? error.cause.message : ""]
|
|
@@ -1042,18 +2946,17 @@ function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
|
1042
2946
|
|
|
1043
2947
|
export function createStealthClient(
|
|
1044
2948
|
baseUrl: string,
|
|
1045
|
-
defaultProfileOrOptions: string | StealthClientOptions = DEFAULT_PROFILE,
|
|
1046
2949
|
clientOptions: StealthClientOptions = {},
|
|
1047
2950
|
): StealthClient {
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
2951
|
+
if (typeof clientOptions === "string") {
|
|
2952
|
+
resolveStealthProfileSelection(clientOptions as unknown as StealthProfileSelection);
|
|
2953
|
+
}
|
|
2954
|
+
const defaultProfile = resolveStealthProfileSelection(clientOptions.stealth);
|
|
1052
2955
|
let sharedSession: StealthSession | null = null;
|
|
1053
2956
|
|
|
1054
2957
|
function getSharedSession(): StealthSession {
|
|
1055
2958
|
if (!sharedSession) {
|
|
1056
|
-
sharedSession = createSessionFetcher(baseUrl, defaultProfile,
|
|
2959
|
+
sharedSession = createSessionFetcher(baseUrl, defaultProfile, clientOptions);
|
|
1057
2960
|
}
|
|
1058
2961
|
|
|
1059
2962
|
return sharedSession;
|
|
@@ -1063,9 +2966,9 @@ export function createStealthClient(
|
|
|
1063
2966
|
fetch(url: string, options?: StealthFetchOptions) {
|
|
1064
2967
|
return getSharedSession().fetch(url, options);
|
|
1065
2968
|
},
|
|
1066
|
-
createSession(opts?: {
|
|
1067
|
-
const sessionProfile = opts?.
|
|
1068
|
-
return createSessionFetcher(baseUrl, sessionProfile,
|
|
2969
|
+
createSession(opts?: { stealth?: StealthProfileSelection }) {
|
|
2970
|
+
const sessionProfile = resolveStealthProfileSelection(opts?.stealth, defaultProfile);
|
|
2971
|
+
return createSessionFetcher(baseUrl, sessionProfile, clientOptions);
|
|
1069
2972
|
},
|
|
1070
2973
|
close() {
|
|
1071
2974
|
sharedSession?.close();
|