@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/bin/apifuse-record.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
|
|
2
|
+
import { readDiagnosticEnv } from "../src/runtime/diagnostic-env.js";
|
|
3
3
|
|
|
4
4
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
@@ -8,20 +8,72 @@ import { pathToFileURL } from "node:url";
|
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
createBypassProviderCache,
|
|
11
|
+
createCredentialContext,
|
|
11
12
|
createHttpClient,
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
createInProcessProviderEngine,
|
|
14
|
+
createOcrClientFromEnv,
|
|
15
|
+
createHandleContext,
|
|
16
|
+
createProviderEnvironment,
|
|
14
17
|
createSttClientFromEnv,
|
|
15
18
|
executeOperation,
|
|
16
19
|
type HttpClient,
|
|
20
|
+
type HttpResponse,
|
|
21
|
+
type HttpStreamResponse,
|
|
17
22
|
type ProviderContext,
|
|
18
23
|
type ProviderDefinition,
|
|
24
|
+
type ProviderEngineBindingCandidates,
|
|
19
25
|
ProviderError,
|
|
26
|
+
type RequestOptions,
|
|
20
27
|
type StealthClient,
|
|
21
28
|
TransportError,
|
|
22
29
|
ValidationError,
|
|
23
30
|
} from "../src/index.js";
|
|
31
|
+
import {
|
|
32
|
+
type LocalConnection,
|
|
33
|
+
LOCAL_CONNECTION_ID,
|
|
34
|
+
LOCAL_CONNECTION_SECRETS_ENV,
|
|
35
|
+
providerRequiresConnection,
|
|
36
|
+
resolveLocalConnection,
|
|
37
|
+
} from "../src/cli/local-connection.js";
|
|
38
|
+
import { createCliResolverRuntime } from "../src/cli/resolver-runtime.js";
|
|
39
|
+
import { isAbsoluteUrl } from "../src/runtime/http.js";
|
|
40
|
+
import { getProviderBaseUrl, getProviderStealthBaseUrl } from "../src/runtime/provider.js";
|
|
41
|
+
import { assertProcessEngineModeSupported } from "../src/runtime/engine-mode.js";
|
|
42
|
+
import type { JsonValue } from "../src/contract-json.js";
|
|
43
|
+
import {
|
|
44
|
+
isSensitiveFixtureKey,
|
|
45
|
+
requestPathForFixture,
|
|
46
|
+
sanitizeDiagnosticText,
|
|
47
|
+
sanitizeFixtureString,
|
|
48
|
+
} from "../src/fixture-sanitization.js";
|
|
49
|
+
import {
|
|
50
|
+
isSensitiveKey,
|
|
51
|
+
normalizeSensitiveParams,
|
|
52
|
+
parseHttpRequestInvocation,
|
|
53
|
+
REDACTED_QUERY_VALUE,
|
|
54
|
+
redactSensitiveError,
|
|
55
|
+
redactSensitiveText,
|
|
56
|
+
redactUrlQueryParams,
|
|
57
|
+
replaceRequestOptionsInHttpInvocation,
|
|
58
|
+
requestOptionsFromHttpInvocation,
|
|
59
|
+
serializeRequestUrl,
|
|
60
|
+
} from "../src/runtime/request-options.js";
|
|
61
|
+
import type { ResolverTelemetryCollector } from "../src/runtime/resolver-telemetry.js";
|
|
24
62
|
import { createMemoryProviderRuntimeState } from "../src/runtime/state.js";
|
|
63
|
+
import { createStealthClient } from "../src/runtime/stealth.js";
|
|
64
|
+
import { parseSchema } from "../src/schema.js";
|
|
65
|
+
import {
|
|
66
|
+
captureStreamEvidence,
|
|
67
|
+
createStreamCaptureEnvelope,
|
|
68
|
+
findStreamCaptureGroup,
|
|
69
|
+
findStreamEvidenceRecords,
|
|
70
|
+
hasStreamEvidenceMarker,
|
|
71
|
+
parseStreamEvidenceRecord,
|
|
72
|
+
STREAM_PREVIEW_BYTES,
|
|
73
|
+
type StreamCaptureGroupItem,
|
|
74
|
+
type StreamEvidenceCapture,
|
|
75
|
+
type StreamEvidenceRequest,
|
|
76
|
+
} from "../src/stream-evidence.js";
|
|
25
77
|
|
|
26
78
|
type CliArgs = {
|
|
27
79
|
append: boolean;
|
|
@@ -29,6 +81,10 @@ type CliArgs = {
|
|
|
29
81
|
operation?: string;
|
|
30
82
|
params: string;
|
|
31
83
|
sanitize: boolean;
|
|
84
|
+
/** Repeated `--credential key=value`, in order. */
|
|
85
|
+
credentials: string[];
|
|
86
|
+
/** `--no-credential`: run without a connection even when auth declares one. */
|
|
87
|
+
noCredential: boolean;
|
|
32
88
|
};
|
|
33
89
|
|
|
34
90
|
type ProviderRuntime = ProviderDefinition;
|
|
@@ -39,60 +95,163 @@ const HELP_TEXT = `Usage: apifuse record [path] --operation <operation> --params
|
|
|
39
95
|
|
|
40
96
|
Calls a real upstream-backed operation through ctx.http or ctx.stealth and writes __fixtures__/raw.json.
|
|
41
97
|
|
|
98
|
+
Absolute request URLs need no declaration. A relative URL resolves the way serve resolves it: against
|
|
99
|
+
the operation's upstream.baseUrl, else the provider's first declared one (ctx.stealth then falls back
|
|
100
|
+
to https://<allowedHosts[0]>). A relative ctx.http URL with no base anywhere is refused and names
|
|
101
|
+
the upstream.baseUrl to declare.
|
|
102
|
+
|
|
103
|
+
Streaming responses are recorded as evidence (status, selected headers, full-body SHA-256 and byte
|
|
104
|
+
count, plus a ${STREAM_PREVIEW_BYTES}-byte base64 preview). Test replay is evidence-only: ctx.http.stream exposes the
|
|
105
|
+
preview as its body and the original body_sha256/body_bytes as response metadata.
|
|
106
|
+
When an operation opens multiple streams, all evidence records are saved in stream call order.
|
|
107
|
+
Mixed JSON/stream operations save a tagged call-ordered envelope so snapshot replay can route each response.
|
|
108
|
+
ctx.http.sse() recording is unsupported and fails explicitly.
|
|
109
|
+
|
|
42
110
|
Options:
|
|
43
111
|
--operation, -o <name> operation to call
|
|
44
112
|
--params, -p <json> JSON input passed to the operation (default: {})
|
|
45
|
-
--append
|
|
113
|
+
--append preserve the existing fixture and append this capture
|
|
46
114
|
--sanitize redact common token/header fields (default)
|
|
47
|
-
--no-sanitize
|
|
115
|
+
--no-sanitize disable common-field redaction (sensitiveParams are always redacted)
|
|
116
|
+
--credential <k>=<v> credential the gateway would inject (repeatable)
|
|
117
|
+
--no-credential record without a credential even when auth declares one
|
|
48
118
|
--help, -h show this help
|
|
49
119
|
|
|
120
|
+
Credentials:
|
|
121
|
+
A provider whose auth.mode is not "none" reads a credential the gateway
|
|
122
|
+
resolves and injects at invocation time -- for "platform-managed" that is the
|
|
123
|
+
platform-owned key, which is not a provider secret and is not in the
|
|
124
|
+
checkout. Supply it with --credential, or with
|
|
125
|
+
${LOCAL_CONNECTION_SECRETS_ENV}='{"<key>":"<value>"}'. The keys are the ones
|
|
126
|
+
the provider passes to ctx.credential.get(...).
|
|
127
|
+
|
|
50
128
|
Example:
|
|
51
129
|
apifuse record providers/korea-air-quality --operation realtime --params '{"stationName":"jongno"}'`;
|
|
52
130
|
|
|
53
131
|
export async function main() {
|
|
132
|
+
let capture: ReturnType<typeof createCaptureContext> | undefined;
|
|
54
133
|
try {
|
|
55
134
|
const args = parseArgs(normalizeArgs(process.argv.slice(2)));
|
|
56
135
|
const location = resolveProviderLocation(args.providerPath);
|
|
57
136
|
const provider = await loadProvider(location.rootDir);
|
|
137
|
+
// The capture context attaches in-process; refuse an explicit remote request instead of ignoring it.
|
|
138
|
+
assertProcessEngineModeSupported();
|
|
58
139
|
const operationName = resolveOperationName(provider, args.operation);
|
|
59
140
|
const operation = provider.operations[operationName];
|
|
60
|
-
const parsedParams = parseParams(operation, args.params);
|
|
141
|
+
const parsedParams = await parseParams(operation, args.params);
|
|
142
|
+
|
|
143
|
+
const connection = resolveRecordConnection(provider, args);
|
|
61
144
|
|
|
62
|
-
|
|
145
|
+
capture = createCaptureContext(
|
|
63
146
|
provider,
|
|
64
|
-
|
|
147
|
+
resolveRecordBaseUrls(provider, operationName),
|
|
148
|
+
args.sanitize,
|
|
149
|
+
connection,
|
|
65
150
|
);
|
|
66
151
|
|
|
67
152
|
console.log(`[apifuse record] Calling ${operationName} on ${provider.id}...`);
|
|
68
153
|
|
|
69
|
-
|
|
70
|
-
|
|
154
|
+
let result: unknown;
|
|
155
|
+
try {
|
|
156
|
+
result = await executeOperation(provider, operationName, capture.ctx, parsedParams);
|
|
157
|
+
} catch (operationError) {
|
|
158
|
+
capture.throwIfRefused();
|
|
159
|
+
let partial: unknown;
|
|
160
|
+
try {
|
|
161
|
+
partial = await capture.getCapturedRaw();
|
|
162
|
+
} catch (finalizationError) {
|
|
163
|
+
throw new StreamRecorderError("Operation and stream finalization both failed.", [
|
|
164
|
+
operationError,
|
|
165
|
+
finalizationError,
|
|
166
|
+
]);
|
|
167
|
+
}
|
|
168
|
+
const streamCount = findStreamEvidenceRecords(partial).length;
|
|
169
|
+
if (streamCount > 0) {
|
|
170
|
+
throw new StreamRecorderError(
|
|
171
|
+
`Operation failed after finalizing ${streamCount} stream capture${streamCount === 1 ? "" : "s"}.`,
|
|
172
|
+
[operationError],
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
throw operationError;
|
|
176
|
+
} finally {
|
|
177
|
+
const telemetryLine = formatResolverTelemetry(
|
|
178
|
+
capture.resolverTelemetry,
|
|
179
|
+
capture.getCapturedSensitiveParams().values,
|
|
180
|
+
);
|
|
181
|
+
if (telemetryLine) console.log(telemetryLine);
|
|
182
|
+
}
|
|
183
|
+
// Checked on success too: an operation that caught the refusal and carried
|
|
184
|
+
// on would otherwise save a fixture missing the request it never made.
|
|
185
|
+
capture.throwIfRefused();
|
|
186
|
+
const captured = await capture.getCapturedRaw();
|
|
71
187
|
|
|
72
188
|
if (captured === undefined) {
|
|
73
189
|
throw new Error(`No upstream response was captured for ${provider.id}.${operationName}.`);
|
|
74
190
|
}
|
|
75
191
|
|
|
76
|
-
const
|
|
192
|
+
const sensitiveParams = capture.getCapturedSensitiveParams();
|
|
193
|
+
const rawPayload = jsonFixtureValue(captured);
|
|
77
194
|
const fixturePath = resolve(location.rootDir, "__fixtures__", "raw.json");
|
|
78
|
-
const
|
|
195
|
+
const redactedCapture = redactFixture(rawPayload, sensitiveParams, args.sanitize);
|
|
196
|
+
const mergedPayload = await prepareFixturePayload(fixturePath, redactedCapture, args.append);
|
|
197
|
+
// Mandatory query-secret redaction applies to the merged history, including
|
|
198
|
+
// values discovered in older declared-key URL positions. Optional common-
|
|
199
|
+
// field sanitization applies only to this run's new capture so --append does
|
|
200
|
+
// not rewrite deliberately preserved historical fields.
|
|
201
|
+
const historicalSensitiveParams = discoverSensitiveQueryValues(mergedPayload, sensitiveParams);
|
|
202
|
+
const nextPayload = redactFixture(mergedPayload, historicalSensitiveParams, false);
|
|
79
203
|
|
|
80
204
|
await mkdir(dirname(fixturePath), { recursive: true });
|
|
81
205
|
await writeFile(fixturePath, `${JSON.stringify(nextPayload, null, 2)}\n`);
|
|
82
206
|
|
|
83
207
|
console.log(
|
|
84
208
|
`[apifuse record] Captured response (${formatBytes(
|
|
85
|
-
Buffer.byteLength(JSON.stringify(
|
|
209
|
+
Buffer.byteLength(JSON.stringify(redactedCapture)),
|
|
86
210
|
)})`,
|
|
87
211
|
);
|
|
88
212
|
console.log(`[apifuse record] Saved to ${relative(process.cwd(), fixturePath)}`);
|
|
89
213
|
|
|
90
214
|
void result;
|
|
91
215
|
} catch (error) {
|
|
92
|
-
handleCliError(error);
|
|
216
|
+
handleCliError(error, capture?.getCapturedSensitiveParams().values);
|
|
93
217
|
}
|
|
94
218
|
}
|
|
95
219
|
|
|
220
|
+
/**
|
|
221
|
+
* The connection this run attaches, or a hard failure when the provider needs
|
|
222
|
+
* one and none is configured.
|
|
223
|
+
*
|
|
224
|
+
* Failing here rather than at the handler is the whole point. Before this, a
|
|
225
|
+
* platform-managed provider recorded with `credential: { mode: "none" }`, so
|
|
226
|
+
* `ctx.credential.get(...)` returned `undefined` and the run died on the
|
|
227
|
+
* provider's own `MISSING_SECRET` — indistinguishable, to the author, from an
|
|
228
|
+
* upstream outage or a bad key. It reported as a capability the recorder
|
|
229
|
+
* lacked only if you already knew to look.
|
|
230
|
+
*/
|
|
231
|
+
function resolveRecordConnection(
|
|
232
|
+
provider: ProviderRuntime,
|
|
233
|
+
args: CliArgs,
|
|
234
|
+
): LocalConnection | undefined {
|
|
235
|
+
if (args.noCredential) {
|
|
236
|
+
if (providerRequiresConnection(provider)) {
|
|
237
|
+
console.warn(
|
|
238
|
+
`[apifuse record] --no-credential: recording ${provider.id} without the credential its auth.mode "${provider.auth?.mode}" declares.`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const resolution = resolveLocalConnection(provider, {
|
|
245
|
+
overrides: args.credentials,
|
|
246
|
+
});
|
|
247
|
+
if (!resolution.ok)
|
|
248
|
+
throw new ProviderError(resolution.reason, {
|
|
249
|
+
code: "CREDENTIAL_UNAVAILABLE",
|
|
250
|
+
fix: "Supply the credential with --credential or the environment variable named above.",
|
|
251
|
+
});
|
|
252
|
+
return resolution.connection;
|
|
253
|
+
}
|
|
254
|
+
|
|
96
255
|
function normalizeArgs(argv: string[]): string[] {
|
|
97
256
|
return argv[0] === "record" ? argv.slice(1) : argv;
|
|
98
257
|
}
|
|
@@ -103,6 +262,8 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
103
262
|
let params = "{}";
|
|
104
263
|
let sanitize = true;
|
|
105
264
|
let append = false;
|
|
265
|
+
const credentials: string[] = [];
|
|
266
|
+
let noCredential = false;
|
|
106
267
|
|
|
107
268
|
for (let index = 0; index < argv.length; index += 1) {
|
|
108
269
|
const arg = argv[index];
|
|
@@ -159,6 +320,26 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
159
320
|
continue;
|
|
160
321
|
}
|
|
161
322
|
|
|
323
|
+
if (arg === "--credential") {
|
|
324
|
+
const value = argv[index + 1];
|
|
325
|
+
if (!value) {
|
|
326
|
+
throw new Error("Missing value for --credential.");
|
|
327
|
+
}
|
|
328
|
+
credentials.push(value);
|
|
329
|
+
index += 1;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (arg.startsWith("--credential=")) {
|
|
334
|
+
credentials.push(arg.slice("--credential=".length));
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (arg === "--no-credential") {
|
|
339
|
+
noCredential = true;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
162
343
|
if (arg.startsWith("-")) {
|
|
163
344
|
throw new Error(`Unknown option: ${arg}`);
|
|
164
345
|
}
|
|
@@ -171,16 +352,59 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
171
352
|
throw new Error(`Unexpected argument: ${arg}`);
|
|
172
353
|
}
|
|
173
354
|
|
|
174
|
-
return {
|
|
355
|
+
return {
|
|
356
|
+
append,
|
|
357
|
+
providerPath,
|
|
358
|
+
operation,
|
|
359
|
+
params,
|
|
360
|
+
sanitize,
|
|
361
|
+
credentials,
|
|
362
|
+
noCredential,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The `resolver` sibling of the server request log, printed once per recorded invocation. The
|
|
368
|
+
* collector redacts at record time with the values registered so far; the mandatory query-secret
|
|
369
|
+
* pass over the final line also covers values captured after a solve and short low-entropy values
|
|
370
|
+
* the diagnostic redactor does not match, the same way handleCliError treats CLI errors.
|
|
371
|
+
*/
|
|
372
|
+
export function formatResolverTelemetry(
|
|
373
|
+
collector: ResolverTelemetryCollector,
|
|
374
|
+
sensitiveValues: readonly string[] = [],
|
|
375
|
+
): string | undefined {
|
|
376
|
+
const resolver = collector.toLogPayload();
|
|
377
|
+
return resolver
|
|
378
|
+
? `[apifuse record] Resolver telemetry ${redactSensitiveText(
|
|
379
|
+
JSON.stringify({ resolver }),
|
|
380
|
+
sensitiveValues,
|
|
381
|
+
)}`
|
|
382
|
+
: undefined;
|
|
175
383
|
}
|
|
176
384
|
|
|
177
|
-
function handleCliError(error: unknown): never {
|
|
178
|
-
const message = formatCliError(error);
|
|
385
|
+
function handleCliError(error: unknown, sensitiveValues: readonly string[] = []): never {
|
|
386
|
+
const message = redactSensitiveText(formatCliError(error), sensitiveValues);
|
|
179
387
|
console.error(`[apifuse record] ${message}`);
|
|
180
388
|
process.exit(1);
|
|
181
389
|
}
|
|
182
390
|
|
|
183
|
-
|
|
391
|
+
class StreamRecorderError extends Error {
|
|
392
|
+
readonly diagnosticCauses: readonly unknown[];
|
|
393
|
+
|
|
394
|
+
constructor(message: string, diagnosticCauses: readonly unknown[]) {
|
|
395
|
+
super(message, { cause: diagnosticCauses[0] });
|
|
396
|
+
this.name = "StreamRecorderError";
|
|
397
|
+
this.diagnosticCauses = diagnosticCauses;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function formatCliError(error: unknown): string {
|
|
402
|
+
if (error instanceof StreamRecorderError) {
|
|
403
|
+
return [
|
|
404
|
+
sanitizeDiagnosticText(error.message),
|
|
405
|
+
...error.diagnosticCauses.map((cause) => `cause=${formatDiagnosticCause(cause)}`),
|
|
406
|
+
].join(" ");
|
|
407
|
+
}
|
|
184
408
|
if (error instanceof TransportError) {
|
|
185
409
|
return [
|
|
186
410
|
error.message,
|
|
@@ -201,12 +425,32 @@ function formatCliError(error: unknown): string {
|
|
|
201
425
|
}
|
|
202
426
|
|
|
203
427
|
if (error instanceof Error) {
|
|
428
|
+
if (/^Stream capture\b/.test(error.message) && error.cause !== undefined) {
|
|
429
|
+
return `${sanitizeDiagnosticText(error.message)} cause=${formatDiagnosticCause(error.cause)}`;
|
|
430
|
+
}
|
|
204
431
|
return error.message;
|
|
205
432
|
}
|
|
206
433
|
|
|
207
434
|
return String(error);
|
|
208
435
|
}
|
|
209
436
|
|
|
437
|
+
function formatDiagnosticCause(cause: unknown): string {
|
|
438
|
+
if (cause instanceof StreamRecorderError) {
|
|
439
|
+
return [
|
|
440
|
+
sanitizeDiagnosticText(cause.message),
|
|
441
|
+
...cause.diagnosticCauses.map((nested) => `cause=${formatDiagnosticCause(nested)}`),
|
|
442
|
+
].join(" ");
|
|
443
|
+
}
|
|
444
|
+
if (!(cause instanceof Error)) return sanitizeDiagnosticText(String(cause));
|
|
445
|
+
const code =
|
|
446
|
+
"code" in cause && typeof cause.code === "string"
|
|
447
|
+
? ` code=${sanitizeDiagnosticText(cause.code)}`
|
|
448
|
+
: "";
|
|
449
|
+
const nestedCause =
|
|
450
|
+
cause.cause === undefined ? "" : ` cause=${formatDiagnosticCause(cause.cause)}`;
|
|
451
|
+
return `${sanitizeDiagnosticText(cause.message)}${code}${nestedCause}`;
|
|
452
|
+
}
|
|
453
|
+
|
|
210
454
|
function resolveProviderLocation(inputPath?: string) {
|
|
211
455
|
const originalInput = inputPath ?? process.cwd();
|
|
212
456
|
const resolvedInput = resolve(process.cwd(), originalInput);
|
|
@@ -284,7 +528,10 @@ function resolveOperationName(provider: ProviderRuntime, operationName?: string)
|
|
|
284
528
|
return firstOperation;
|
|
285
529
|
}
|
|
286
530
|
|
|
287
|
-
function parseParams(
|
|
531
|
+
async function parseParams(
|
|
532
|
+
operation: ProviderRuntime["operations"][string],
|
|
533
|
+
value: string,
|
|
534
|
+
): Promise<unknown> {
|
|
288
535
|
let parsed: unknown;
|
|
289
536
|
|
|
290
537
|
try {
|
|
@@ -295,47 +542,195 @@ function parseParams(operation: ProviderRuntime["operations"][string], value: st
|
|
|
295
542
|
);
|
|
296
543
|
}
|
|
297
544
|
|
|
298
|
-
return operation.input ? operation.input.
|
|
545
|
+
return operation.input ? parseSchema(operation.input, parsed, "record.params") : parsed;
|
|
299
546
|
}
|
|
300
547
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
548
|
+
/**
|
|
549
|
+
* The bases the recorder's `ctx.http` and `ctx.stealth` resolve relative
|
|
550
|
+
* request URLs against. An absent base means only absolute URLs resolve.
|
|
551
|
+
*/
|
|
552
|
+
export type RecordBaseUrls = {
|
|
553
|
+
readonly http?: string;
|
|
554
|
+
readonly stealth?: string;
|
|
555
|
+
};
|
|
308
556
|
|
|
309
|
-
|
|
557
|
+
/**
|
|
558
|
+
* `upstream.baseUrl` only resolves relative request URLs (and seeds the stealth
|
|
559
|
+
* cookie jar's default origin); the recorder uses it for nothing else, so an
|
|
560
|
+
* operation that requests absolute URLs records without one. The operation's
|
|
561
|
+
* own base wins; otherwise the recorder binds the bases `serve` binds, so a
|
|
562
|
+
* relative URL resolves in a recording exactly as it does in production.
|
|
563
|
+
*/
|
|
564
|
+
export function resolveRecordBaseUrls(
|
|
565
|
+
provider: ProviderRuntime,
|
|
566
|
+
operationName: string,
|
|
567
|
+
): RecordBaseUrls {
|
|
568
|
+
const declared = provider.operations[operationName]?.upstream?.baseUrl;
|
|
569
|
+
const own = typeof declared === "string" && declared.length > 0 ? declared : undefined;
|
|
570
|
+
const http = own ?? getProviderBaseUrl(provider);
|
|
571
|
+
const stealth = own ?? getProviderStealthBaseUrl(provider);
|
|
572
|
+
return { ...(http ? { http } : {}), ...(stealth ? { stealth } : {}) };
|
|
310
573
|
}
|
|
311
574
|
|
|
312
|
-
|
|
313
|
-
|
|
575
|
+
/** A request the recorder cannot resolve without a declaration; fails the run even if the operation catches it. */
|
|
576
|
+
class RecordRefusalError extends Error {
|
|
577
|
+
constructor(message: string) {
|
|
578
|
+
super(message);
|
|
579
|
+
this.name = "RecordRefusalError";
|
|
580
|
+
}
|
|
581
|
+
}
|
|
314
582
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
583
|
+
export function createCaptureContext(
|
|
584
|
+
provider: ProviderRuntime,
|
|
585
|
+
baseUrls: RecordBaseUrls,
|
|
586
|
+
sanitize: boolean,
|
|
587
|
+
connection?: LocalConnection,
|
|
588
|
+
) {
|
|
589
|
+
let refusal: RecordRefusalError | undefined;
|
|
590
|
+
const refuse = (message: string): never => {
|
|
591
|
+
refusal ??= new RecordRefusalError(message);
|
|
592
|
+
throw refusal;
|
|
593
|
+
};
|
|
594
|
+
const assertResolvableHttpUrl = (url: string) => {
|
|
595
|
+
if (baseUrls.http || isAbsoluteUrl(url)) return;
|
|
596
|
+
refuse(
|
|
597
|
+
`ctx.http cannot resolve the relative URL "${requestPathForFixture(url)}": no operation of provider "${provider.id}" declares upstream.baseUrl. Declare upstream.baseUrl on the recorded operation, or request an absolute URL.`,
|
|
598
|
+
);
|
|
599
|
+
};
|
|
600
|
+
let nextCaptureOrder = 0;
|
|
601
|
+
let nextStreamOrdinal = 0;
|
|
602
|
+
let capturedRaw: JsonValue | undefined;
|
|
603
|
+
const rawCaptures: Array<{ order: number; value: JsonValue }> = [];
|
|
604
|
+
const streamCaptures: Array<{
|
|
605
|
+
order: number;
|
|
606
|
+
request: StreamEvidenceRequest;
|
|
607
|
+
capture: StreamEvidenceCapture;
|
|
608
|
+
}> = [];
|
|
609
|
+
let capturedSse: { order: number; method: string; path: string } | undefined;
|
|
610
|
+
const sensitiveParamNames = new Set<string>();
|
|
611
|
+
const sensitiveParamValues = new Set<string>();
|
|
612
|
+
const cache = createBypassProviderCache({ providerId: provider.id });
|
|
613
|
+
const { resolver, resolverTelemetry, registerSensitiveValues } = createCliResolverRuntime(
|
|
614
|
+
provider,
|
|
615
|
+
cache,
|
|
616
|
+
);
|
|
617
|
+
// The injected credential is the most sensitive value in the run, and the
|
|
618
|
+
// recorder prints provider errors and writes fixtures. Seed its VALUES into
|
|
619
|
+
// the redaction set before anything can execute, so an upstream error that
|
|
620
|
+
// echoes the key, and any fixture that captured it, are redacted by the same
|
|
621
|
+
// machinery that already covers declared query secrets.
|
|
622
|
+
//
|
|
623
|
+
// Values only, never the key names: `sensitiveParamNames` means "declared
|
|
624
|
+
// query-key position", and `redactFixture` deliberately skips common-field
|
|
625
|
+
// sanitization for a key that appears in it. Seeding `access_token` there
|
|
626
|
+
// would therefore write a *newly issued* upstream `access_token` — which
|
|
627
|
+
// does not equal the injected value — to raw.json in plaintext. A credential
|
|
628
|
+
// key that really is a query parameter is added to the name set by
|
|
629
|
+
// `captureSensitiveRequestValues` when the provider sends it, which is the
|
|
630
|
+
// only place that claim is true.
|
|
631
|
+
for (const value of Object.values(connection?.secrets ?? {})) {
|
|
632
|
+
if (value.length > 0) sensitiveParamValues.add(value);
|
|
633
|
+
}
|
|
634
|
+
registerSensitiveValues([...sensitiveParamValues]);
|
|
635
|
+
|
|
636
|
+
const captureSensitiveParams = (url: string, options?: SensitiveRequestOptions) => {
|
|
637
|
+
try {
|
|
638
|
+
captureSensitiveRequestValues(url, options, sensitiveParamNames, sensitiveParamValues);
|
|
639
|
+
} finally {
|
|
640
|
+
// Resolver telemetry redacts at record time, so the vendor diagnostics of a later solve
|
|
641
|
+
// must not echo a query secret captured here (same guarantee as handleCliError).
|
|
642
|
+
registerSensitiveValues([...sensitiveParamValues]);
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
const getCapturedSensitiveParams = (): CapturedSensitiveParams => ({
|
|
646
|
+
names: [...sensitiveParamNames],
|
|
647
|
+
values: [...sensitiveParamValues],
|
|
320
648
|
});
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
649
|
+
const reserveCaptureOrder = () => {
|
|
650
|
+
nextCaptureOrder += 1;
|
|
651
|
+
return nextCaptureOrder;
|
|
324
652
|
};
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
getAccessToken: () => undefined,
|
|
330
|
-
getScopes: () => [],
|
|
653
|
+
const retainRawCapture = (order: number, value: unknown) => {
|
|
654
|
+
const json = jsonFixtureValue(value);
|
|
655
|
+
capturedRaw = json;
|
|
656
|
+
rawCaptures.push({ order, value: json });
|
|
331
657
|
};
|
|
658
|
+
|
|
659
|
+
const http = captureHttpClient(createHttpClient(baseUrls.http), {
|
|
660
|
+
assertResolvableUrl: assertResolvableHttpUrl,
|
|
661
|
+
reserveOrder: reserveCaptureOrder,
|
|
662
|
+
reserveStreamOrdinal: () => {
|
|
663
|
+
nextStreamOrdinal += 1;
|
|
664
|
+
return nextStreamOrdinal;
|
|
665
|
+
},
|
|
666
|
+
onSensitiveParams: captureSensitiveParams,
|
|
667
|
+
onResponse: (order, response) => retainRawCapture(order, response.data),
|
|
668
|
+
onStreamResponse: (order, ordinal, requestUrl, method, response) => {
|
|
669
|
+
const resolvedRequestUrl = new URL(requestUrl, baseUrls.http).toString();
|
|
670
|
+
const request = {
|
|
671
|
+
ordinal,
|
|
672
|
+
method,
|
|
673
|
+
path: requestPathForFixture(resolvedRequestUrl),
|
|
674
|
+
};
|
|
675
|
+
const capture = captureStreamEvidence(response, {
|
|
676
|
+
requestUrl: resolvedRequestUrl,
|
|
677
|
+
request,
|
|
678
|
+
...(sanitize
|
|
679
|
+
? {
|
|
680
|
+
sanitizeFixture: (value: JsonValue) =>
|
|
681
|
+
jsonFixtureValue(sanitizeStreamFixture(value, getCapturedSensitiveParams())),
|
|
682
|
+
}
|
|
683
|
+
: {}),
|
|
684
|
+
});
|
|
685
|
+
streamCaptures.push({ order, request, capture });
|
|
686
|
+
return capture.response;
|
|
687
|
+
},
|
|
688
|
+
onSseResponse: (order, requestUrl, method) => {
|
|
689
|
+
capturedSse = {
|
|
690
|
+
order,
|
|
691
|
+
method,
|
|
692
|
+
path: requestPathForFixture(new URL(requestUrl, baseUrls.http).toString()),
|
|
693
|
+
};
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
// Without a base `serve` binds no stealth transport either, so every call is refused.
|
|
697
|
+
const refuseStealth = (): never =>
|
|
698
|
+
refuse(
|
|
699
|
+
`ctx.stealth has no base URL: provider "${provider.id}" declares neither upstream.baseUrl nor allowedHosts. Declare allowedHosts on the provider (serve binds ctx.stealth to https://<allowedHosts[0]>) or upstream.baseUrl on the recorded operation.`,
|
|
700
|
+
);
|
|
701
|
+
const stealth: StealthClient = baseUrls.stealth
|
|
702
|
+
? proxyStealthClient(
|
|
703
|
+
createStealthClient(baseUrls.stealth, {
|
|
704
|
+
...(provider.stealth ? { stealth: provider.stealth } : {}),
|
|
705
|
+
}),
|
|
706
|
+
captureSensitiveParams,
|
|
707
|
+
(order, response) => retainRawCapture(order, normalizeCapturedStealthResponse(response)),
|
|
708
|
+
reserveCaptureOrder,
|
|
709
|
+
)
|
|
710
|
+
: { fetch: async () => refuseStealth(), createSession: refuseStealth };
|
|
711
|
+
|
|
712
|
+
const providerEnvironment = createProviderEnvironment(
|
|
713
|
+
process.env,
|
|
714
|
+
provider.secrets?.map((secret) => secret.name) ?? [],
|
|
715
|
+
);
|
|
716
|
+
const env = { get: (key: string) => readDiagnosticEnv(key, providerEnvironment) };
|
|
717
|
+
// Built exactly the way `serve` builds it from a gateway-supplied
|
|
718
|
+
// `request.connection`, so the provider takes its production branch rather
|
|
719
|
+
// than the always-undefined one a stub credential forces.
|
|
720
|
+
const credential = createCredentialContext({
|
|
721
|
+
...(provider.credential?.keys ? { allowedKeys: provider.credential.keys } : {}),
|
|
722
|
+
...(connection ? { mode: connection.mode, values: connection.secrets } : {}),
|
|
723
|
+
});
|
|
332
724
|
const state = createMemoryProviderRuntimeState();
|
|
333
|
-
const
|
|
725
|
+
const candidates: ProviderEngineBindingCandidates = {
|
|
334
726
|
env,
|
|
335
727
|
credential,
|
|
336
|
-
request: {
|
|
728
|
+
request: {
|
|
729
|
+
headers: {},
|
|
730
|
+
connectionId: connection ? LOCAL_CONNECTION_ID : "local-record",
|
|
731
|
+
},
|
|
337
732
|
http,
|
|
338
|
-
cache
|
|
733
|
+
cache,
|
|
339
734
|
state,
|
|
340
735
|
stealth,
|
|
341
736
|
browser: {
|
|
@@ -362,70 +757,292 @@ function createCaptureContext(provider: ProviderRuntime, baseUrl: string) {
|
|
|
362
757
|
throw new Error("Auth prompts are not available in apifuse record.");
|
|
363
758
|
},
|
|
364
759
|
},
|
|
760
|
+
ocr: createOcrClientFromEnv(provider.ocr),
|
|
365
761
|
stt: createSttClientFromEnv(provider.stt),
|
|
366
|
-
|
|
762
|
+
resolver,
|
|
763
|
+
handle: createHandleContext({
|
|
367
764
|
providerId: provider.id,
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
765
|
+
request: {
|
|
766
|
+
headers: {},
|
|
767
|
+
connectionId: connection ? LOCAL_CONNECTION_ID : "local-record",
|
|
768
|
+
},
|
|
371
769
|
state,
|
|
372
770
|
}),
|
|
373
771
|
};
|
|
772
|
+
const ctx = createInProcessProviderEngine().attach({
|
|
773
|
+
provider,
|
|
774
|
+
bindings: candidates,
|
|
775
|
+
}) as ProviderContext;
|
|
374
776
|
|
|
375
777
|
return {
|
|
376
778
|
ctx,
|
|
377
|
-
|
|
779
|
+
resolverTelemetry,
|
|
780
|
+
throwIfRefused: () => {
|
|
781
|
+
if (refusal) throw refusal;
|
|
782
|
+
},
|
|
783
|
+
getCapturedRaw: async () => {
|
|
784
|
+
if (streamCaptures.length === 0) {
|
|
785
|
+
if (capturedSse) throw unsupportedSseCaptureError(capturedSse);
|
|
786
|
+
return capturedRaw;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const settled = await Promise.allSettled(
|
|
790
|
+
streamCaptures.map(async ({ order, request, capture }) => ({
|
|
791
|
+
order,
|
|
792
|
+
request,
|
|
793
|
+
value: redactStreamEvidence(await capture.getEvidence(), getCapturedSensitiveParams()),
|
|
794
|
+
})),
|
|
795
|
+
);
|
|
796
|
+
const failures = settled.flatMap((result, index) =>
|
|
797
|
+
result.status === "rejected"
|
|
798
|
+
? [
|
|
799
|
+
new StreamRecorderError(
|
|
800
|
+
`Stream finalization failed: method=${streamCaptures[index]!.request.method} path=${streamCaptures[index]!.request.path} ordinal=${streamCaptures[index]!.request.ordinal}.`,
|
|
801
|
+
[result.reason],
|
|
802
|
+
),
|
|
803
|
+
]
|
|
804
|
+
: [],
|
|
805
|
+
);
|
|
806
|
+
if (failures.length > 0) {
|
|
807
|
+
throw new StreamRecorderError(
|
|
808
|
+
`${failures.length} stream capture${failures.length === 1 ? "" : "s"} failed to finalize.`,
|
|
809
|
+
failures,
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
const evidence = settled.flatMap((result) =>
|
|
813
|
+
result.status === "fulfilled" ? [result.value] : [],
|
|
814
|
+
);
|
|
815
|
+
if (capturedSse) throw unsupportedSseCaptureError(capturedSse);
|
|
816
|
+
const timeline: StreamCaptureGroupItem[] = [...rawCaptures, ...evidence]
|
|
817
|
+
.sort((left, right) => left.order - right.order)
|
|
818
|
+
.map((item) =>
|
|
819
|
+
"request" in item
|
|
820
|
+
? { kind: "stream" as const, evidence: item.value }
|
|
821
|
+
: { kind: "response" as const, value: item.value },
|
|
822
|
+
);
|
|
823
|
+
return createStreamCaptureEnvelope(timeline);
|
|
824
|
+
},
|
|
825
|
+
getCapturedSensitiveParams,
|
|
378
826
|
};
|
|
379
827
|
}
|
|
380
828
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
return new Proxy(client, {
|
|
386
|
-
get(target, prop, receiver) {
|
|
387
|
-
const value = Reflect.get(target, prop, receiver);
|
|
829
|
+
type CapturedSensitiveParams = {
|
|
830
|
+
names: readonly string[];
|
|
831
|
+
values: readonly string[];
|
|
832
|
+
};
|
|
388
833
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
834
|
+
type SensitiveRequestOptions = Pick<RequestOptions, "params" | "sensitiveParams">;
|
|
835
|
+
|
|
836
|
+
function captureSensitiveRequestValues(
|
|
837
|
+
url: string,
|
|
838
|
+
options: SensitiveRequestOptions | undefined,
|
|
839
|
+
names: Set<string>,
|
|
840
|
+
values: Set<string>,
|
|
841
|
+
): void {
|
|
842
|
+
const sensitiveParams = normalizeSensitiveParams(options?.sensitiveParams);
|
|
843
|
+
if (sensitiveParams === undefined) return;
|
|
844
|
+
if (!sensitiveParams || typeof sensitiveParams !== "object" || Array.isArray(sensitiveParams)) {
|
|
845
|
+
throw new TypeError("sensitiveParams must be an object whose values are strings.");
|
|
846
|
+
}
|
|
392
847
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
848
|
+
const entries = Object.entries(sensitiveParams);
|
|
849
|
+
for (const [key, value] of entries) {
|
|
850
|
+
if (typeof value !== "string") {
|
|
851
|
+
throw new TypeError(`sensitiveParams.${key} must be a string.`);
|
|
852
|
+
}
|
|
853
|
+
names.add(key);
|
|
854
|
+
if (value !== "") values.add(value);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
let serializedUrl: ReturnType<typeof serializeRequestUrl>;
|
|
858
|
+
try {
|
|
859
|
+
const absoluteUrl = new URL(String(url), "http://apifuse.invalid").toString();
|
|
860
|
+
serializedUrl = serializeRequestUrl(absoluteUrl, options?.params, sensitiveParams);
|
|
861
|
+
} catch (error) {
|
|
862
|
+
const structural = redactUrlQueryParams(String(url), [...names]);
|
|
863
|
+
const safeUrl = redactSensitiveText(structural.redactedUrl, [
|
|
864
|
+
...values,
|
|
865
|
+
...structural.sensitiveValues,
|
|
866
|
+
]);
|
|
867
|
+
const causeKind = error instanceof Error ? error.name : typeof error;
|
|
868
|
+
throw new TypeError(`Cannot securely record sensitiveParams for "${safeUrl}" (${causeKind}).`, {
|
|
869
|
+
cause: redactSensitiveError(
|
|
870
|
+
error,
|
|
871
|
+
[...values, ...structural.sensitiveValues],
|
|
872
|
+
String(url),
|
|
873
|
+
structural.redactedUrl,
|
|
874
|
+
),
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
for (const value of serializedUrl.sensitiveValues) {
|
|
879
|
+
if (value !== "") values.add(value);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function snapshotRequestOptions<T extends SensitiveRequestOptions>(options: T): T {
|
|
884
|
+
return {
|
|
885
|
+
...options,
|
|
886
|
+
...(options.params
|
|
887
|
+
? {
|
|
888
|
+
params: Object.fromEntries(
|
|
889
|
+
Object.entries(options.params).map(([key, value]) => [
|
|
890
|
+
key,
|
|
891
|
+
Array.isArray(value) ? [...value] : value,
|
|
892
|
+
]),
|
|
893
|
+
),
|
|
894
|
+
}
|
|
895
|
+
: {}),
|
|
896
|
+
...(normalizeSensitiveParams(options.sensitiveParams)
|
|
897
|
+
? { sensitiveParams: { ...options.sensitiveParams } }
|
|
898
|
+
: {}),
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
type HttpCaptureCallbacks = {
|
|
903
|
+
/** Throws before the request when the recorder cannot resolve `url`. */
|
|
904
|
+
assertResolvableUrl(url: string): void;
|
|
905
|
+
reserveOrder(): number;
|
|
906
|
+
reserveStreamOrdinal(): number;
|
|
907
|
+
onSensitiveParams(url: string, options?: RequestOptions): void;
|
|
908
|
+
onResponse(order: number, response: HttpResponse): void;
|
|
909
|
+
onStreamResponse(
|
|
910
|
+
order: number,
|
|
911
|
+
ordinal: number,
|
|
912
|
+
requestUrl: string,
|
|
913
|
+
method: string,
|
|
914
|
+
response: HttpStreamResponse,
|
|
915
|
+
): HttpStreamResponse;
|
|
916
|
+
onSseResponse(order: number, requestUrl: string, method: string): void;
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
function captureHttpClient(client: HttpClient, callbacks: HttpCaptureCallbacks): HttpClient {
|
|
920
|
+
const captureResponse = async (
|
|
921
|
+
order: number,
|
|
922
|
+
responsePromise: Promise<HttpResponse>,
|
|
923
|
+
): Promise<HttpResponse> => {
|
|
924
|
+
const response = await responsePromise;
|
|
925
|
+
callbacks.onResponse(order, response);
|
|
926
|
+
return response;
|
|
927
|
+
};
|
|
928
|
+
const captureRequestOptions = (method: PropertyKey, args: unknown[]) => {
|
|
929
|
+
callbacks.assertResolvableUrl(String(args[0]));
|
|
930
|
+
const invocation = parseHttpRequestInvocation(method, args);
|
|
931
|
+
const options = invocation ? requestOptionsFromHttpInvocation(invocation) : undefined;
|
|
932
|
+
if (!invocation || !options) return;
|
|
933
|
+
const snapshot = snapshotRequestOptions(options);
|
|
934
|
+
callbacks.onSensitiveParams(String(args[0]), snapshot);
|
|
935
|
+
replaceRequestOptionsInHttpInvocation(invocation, snapshot);
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
return {
|
|
939
|
+
request: (...args: Parameters<HttpClient["request"]>) => {
|
|
940
|
+
captureRequestOptions("request", args);
|
|
941
|
+
return captureResponse(callbacks.reserveOrder(), client.request(...args));
|
|
942
|
+
},
|
|
943
|
+
get: (...args: Parameters<HttpClient["get"]>) => {
|
|
944
|
+
captureRequestOptions("get", args);
|
|
945
|
+
return captureResponse(callbacks.reserveOrder(), client.get(...args));
|
|
946
|
+
},
|
|
947
|
+
post: (...args: Parameters<HttpClient["post"]>) => {
|
|
948
|
+
captureRequestOptions("post", args);
|
|
949
|
+
return captureResponse(callbacks.reserveOrder(), client.post(...args));
|
|
950
|
+
},
|
|
951
|
+
put: (...args: Parameters<HttpClient["put"]>) => {
|
|
952
|
+
captureRequestOptions("put", args);
|
|
953
|
+
return captureResponse(callbacks.reserveOrder(), client.put(...args));
|
|
954
|
+
},
|
|
955
|
+
delete: (...args: Parameters<HttpClient["delete"]>) => {
|
|
956
|
+
captureRequestOptions("delete", args);
|
|
957
|
+
return captureResponse(callbacks.reserveOrder(), client.delete(...args));
|
|
958
|
+
},
|
|
959
|
+
stream: async (...args: Parameters<HttpClient["stream"]>) => {
|
|
960
|
+
captureRequestOptions("stream", args);
|
|
961
|
+
const order = callbacks.reserveOrder();
|
|
962
|
+
const ordinal = callbacks.reserveStreamOrdinal();
|
|
963
|
+
const method = (args[1]?.method ?? "GET").toUpperCase();
|
|
964
|
+
const response = await client.stream(...args);
|
|
965
|
+
return callbacks.onStreamResponse(order, ordinal, args[0], method, response);
|
|
398
966
|
},
|
|
399
|
-
|
|
967
|
+
sse: async (...args: Parameters<HttpClient["sse"]>) => {
|
|
968
|
+
captureRequestOptions("sse", args);
|
|
969
|
+
const order = callbacks.reserveOrder();
|
|
970
|
+
const response = await client.sse(...args);
|
|
971
|
+
callbacks.onSseResponse(order, args[0], (args[1]?.method ?? "GET").toUpperCase());
|
|
972
|
+
return response;
|
|
973
|
+
},
|
|
974
|
+
};
|
|
400
975
|
}
|
|
401
976
|
|
|
402
977
|
type StealthSession = ReturnType<StealthClient["createSession"]>;
|
|
403
978
|
|
|
404
979
|
function proxyStealthClient(
|
|
405
980
|
client: StealthClient,
|
|
406
|
-
|
|
981
|
+
onSensitiveParams: (url: string, options?: SensitiveRequestOptions) => void,
|
|
982
|
+
onResponse: (order: number, response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
|
|
983
|
+
reserveOrder: () => number,
|
|
407
984
|
): StealthClient {
|
|
408
985
|
return {
|
|
409
986
|
fetch: async (...args: Parameters<StealthClient["fetch"]>) => {
|
|
987
|
+
const order = reserveOrder();
|
|
988
|
+
if (args[1]) args[1] = snapshotRequestOptions(args[1]);
|
|
989
|
+
onSensitiveParams(args[0], args[1]);
|
|
410
990
|
const response = await client.fetch(...args);
|
|
411
|
-
|
|
991
|
+
if (response.url) onSensitiveParams(response.url, args[1]);
|
|
992
|
+
onResponse(order, response);
|
|
412
993
|
return response;
|
|
413
994
|
},
|
|
414
995
|
createSession: (...args: Parameters<StealthClient["createSession"]>) =>
|
|
415
|
-
proxyStealthSession(
|
|
996
|
+
proxyStealthSession(
|
|
997
|
+
client.createSession(...args),
|
|
998
|
+
onSensitiveParams,
|
|
999
|
+
onResponse,
|
|
1000
|
+
reserveOrder,
|
|
1001
|
+
),
|
|
416
1002
|
};
|
|
417
1003
|
}
|
|
418
1004
|
|
|
419
1005
|
function proxyStealthSession(
|
|
420
1006
|
session: StealthSession,
|
|
421
|
-
|
|
1007
|
+
onSensitiveParams: (url: string, options?: SensitiveRequestOptions) => void,
|
|
1008
|
+
onResponse: (order: number, response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
|
|
1009
|
+
reserveOrder: () => number,
|
|
422
1010
|
): StealthSession {
|
|
423
1011
|
return {
|
|
424
1012
|
fetch: async (...args: Parameters<StealthSession["fetch"]>) => {
|
|
1013
|
+
const order = reserveOrder();
|
|
1014
|
+
if (args[1]) args[1] = snapshotRequestOptions(args[1]);
|
|
1015
|
+
onSensitiveParams(args[0], args[1]);
|
|
425
1016
|
const response = await session.fetch(...args);
|
|
426
|
-
|
|
1017
|
+
if (response.url) onSensitiveParams(response.url, args[1]);
|
|
1018
|
+
onResponse(order, response);
|
|
1019
|
+
return response;
|
|
1020
|
+
},
|
|
1021
|
+
replayChallenged: async (...args: Parameters<StealthSession["replayChallenged"]>) => {
|
|
1022
|
+
const order = reserveOrder();
|
|
1023
|
+
const response = await session.replayChallenged(...args);
|
|
1024
|
+
onResponse(order, response);
|
|
427
1025
|
return response;
|
|
428
1026
|
},
|
|
1027
|
+
cookies: session.cookies,
|
|
1028
|
+
redirects: {
|
|
1029
|
+
run: async (...args: Parameters<StealthSession["redirects"]["run"]>) => {
|
|
1030
|
+
const order = reserveOrder();
|
|
1031
|
+
args[0] = snapshotRequestOptions(args[0]);
|
|
1032
|
+
onSensitiveParams(args[0].url, args[0]);
|
|
1033
|
+
const callerStopWhen = args[0].stopWhen;
|
|
1034
|
+
args[0].stopWhen = async (hop) => {
|
|
1035
|
+
for (const url of [hop.url, hop.location, hop.nextUrl]) {
|
|
1036
|
+
if (url) onSensitiveParams(url, args[0]);
|
|
1037
|
+
}
|
|
1038
|
+
return callerStopWhen ? await callerStopWhen(hop) : false;
|
|
1039
|
+
};
|
|
1040
|
+
const result = await session.redirects.run(...args);
|
|
1041
|
+
if (result.final.url) onSensitiveParams(result.final.url, args[0]);
|
|
1042
|
+
onResponse(order, result.final);
|
|
1043
|
+
return result;
|
|
1044
|
+
},
|
|
1045
|
+
},
|
|
429
1046
|
close: () => session.close(),
|
|
430
1047
|
};
|
|
431
1048
|
}
|
|
@@ -438,31 +1055,193 @@ function normalizeCapturedStealthResponse(response: Awaited<ReturnType<StealthCl
|
|
|
438
1055
|
}
|
|
439
1056
|
}
|
|
440
1057
|
|
|
441
|
-
function
|
|
1058
|
+
function redactStreamEvidence(
|
|
1059
|
+
evidence: Awaited<ReturnType<StreamEvidenceCapture["getEvidence"]>>,
|
|
1060
|
+
sensitiveParams: CapturedSensitiveParams,
|
|
1061
|
+
): Awaited<ReturnType<StreamEvidenceCapture["getEvidence"]>> {
|
|
1062
|
+
return parseStreamEvidenceRecord(redactFixture(evidence, sensitiveParams, false));
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function sanitizeStreamFixture(value: unknown, sensitiveParams: CapturedSensitiveParams): unknown {
|
|
1066
|
+
if (typeof value === "string") {
|
|
1067
|
+
if (value !== "" && sensitiveParams.values.includes(value)) return REDACTED_QUERY_VALUE;
|
|
1068
|
+
return sanitizeFixtureString(redactFixtureText(value, sensitiveParams));
|
|
1069
|
+
}
|
|
442
1070
|
if (Array.isArray(value)) {
|
|
443
|
-
return value.map((item) =>
|
|
1071
|
+
return value.map((item) => sanitizeStreamFixture(item, sensitiveParams));
|
|
444
1072
|
}
|
|
445
|
-
|
|
446
1073
|
if (!value || typeof value !== "object") {
|
|
447
|
-
return value;
|
|
1074
|
+
return redactFixture(value, sensitiveParams, false);
|
|
448
1075
|
}
|
|
449
1076
|
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
1077
|
+
const result: MutableRecord = Object.create(null) as MutableRecord;
|
|
1078
|
+
for (const [key, entryValue] of Object.entries(value as MutableRecord)) {
|
|
1079
|
+
const redactedKey = sensitiveParams.values.includes(key)
|
|
1080
|
+
? REDACTED_QUERY_VALUE
|
|
1081
|
+
: redactFixtureText(key, sensitiveParams);
|
|
1082
|
+
const uniqueKey = collisionSafeKey(result, redactedKey);
|
|
1083
|
+
result[uniqueKey] =
|
|
1084
|
+
isSensitiveFixtureKey(key) && !sensitiveParams.names.includes(key)
|
|
1085
|
+
? REDACTED_QUERY_VALUE
|
|
1086
|
+
: sanitizeStreamFixture(entryValue, sensitiveParams);
|
|
1087
|
+
}
|
|
1088
|
+
return result;
|
|
1089
|
+
}
|
|
454
1090
|
|
|
455
|
-
|
|
456
|
-
|
|
1091
|
+
function redactStreamPreview(
|
|
1092
|
+
bodyPreviewBase64: string,
|
|
1093
|
+
sensitiveParams: CapturedSensitiveParams,
|
|
1094
|
+
): { bodyPreviewBase64: string; changed: boolean } {
|
|
1095
|
+
const preview = Buffer.from(bodyPreviewBase64, "base64");
|
|
1096
|
+
if (preview.byteLength === 0) return { bodyPreviewBase64, changed: false };
|
|
1097
|
+
|
|
1098
|
+
const text = new TextDecoder().decode(preview);
|
|
1099
|
+
let redactedText = redactFixtureText(text, sensitiveParams);
|
|
1100
|
+
const trimmed = text.trimEnd();
|
|
1101
|
+
try {
|
|
1102
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
1103
|
+
const structurallyRedacted = JSON.stringify(redactFixture(parsed, sensitiveParams, false));
|
|
1104
|
+
if (structurallyRedacted !== JSON.stringify(parsed)) redactedText = structurallyRedacted;
|
|
1105
|
+
} catch {
|
|
1106
|
+
// Non-JSON previews still use the shared free-text sensitive-value policy.
|
|
1107
|
+
}
|
|
1108
|
+
if (redactedText === text) return { bodyPreviewBase64, changed: false };
|
|
457
1109
|
|
|
458
|
-
|
|
1110
|
+
const fitted = Buffer.alloc(preview.byteLength, 0x20);
|
|
1111
|
+
let redactedBytes = Buffer.from(redactedText);
|
|
1112
|
+
if (redactedBytes.byteLength > fitted.byteLength) {
|
|
1113
|
+
redactedBytes = Buffer.from(REDACTED_QUERY_VALUE);
|
|
1114
|
+
}
|
|
1115
|
+
redactedBytes.copy(fitted, 0, 0, Math.min(redactedBytes.byteLength, fitted.byteLength));
|
|
1116
|
+
return { bodyPreviewBase64: fitted.toString("base64"), changed: true };
|
|
459
1117
|
}
|
|
460
1118
|
|
|
461
|
-
function
|
|
462
|
-
|
|
1119
|
+
function redactFixture(
|
|
1120
|
+
value: unknown,
|
|
1121
|
+
sensitiveParams: CapturedSensitiveParams,
|
|
1122
|
+
sanitizeCommonFields: boolean,
|
|
1123
|
+
): unknown {
|
|
1124
|
+
if (typeof value === "string") {
|
|
1125
|
+
if (value !== "" && sensitiveParams.values.includes(value)) return REDACTED_QUERY_VALUE;
|
|
1126
|
+
return redactFixtureText(value, sensitiveParams);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
1130
|
+
return sensitiveParams.values.includes(String(value)) ? REDACTED_QUERY_VALUE : value;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (Array.isArray(value)) {
|
|
1134
|
+
return value.map((item) => redactFixture(item, sensitiveParams, sanitizeCommonFields));
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
if (!value || typeof value !== "object") return value;
|
|
1138
|
+
|
|
1139
|
+
const result: MutableRecord = Object.create(null) as MutableRecord;
|
|
1140
|
+
for (const [key, entryValue] of Object.entries(value as MutableRecord)) {
|
|
1141
|
+
const redactedKey = sensitiveParams.values.includes(key)
|
|
1142
|
+
? REDACTED_QUERY_VALUE
|
|
1143
|
+
: redactFixtureText(key, sensitiveParams);
|
|
1144
|
+
const uniqueKey = collisionSafeKey(result, redactedKey);
|
|
1145
|
+
result[uniqueKey] =
|
|
1146
|
+
sanitizeCommonFields &&
|
|
1147
|
+
(isSensitiveKey(key) || isSensitiveFixtureKey(key)) &&
|
|
1148
|
+
!sensitiveParams.names.includes(key)
|
|
1149
|
+
? REDACTED_QUERY_VALUE
|
|
1150
|
+
: redactFixture(entryValue, sensitiveParams, sanitizeCommonFields);
|
|
1151
|
+
}
|
|
1152
|
+
const record = value as MutableRecord;
|
|
1153
|
+
if (record.__apifuse_stream__ === true && typeof record.body_preview_base64 === "string") {
|
|
1154
|
+
const preview = redactStreamPreview(record.body_preview_base64, sensitiveParams);
|
|
1155
|
+
result.body_preview_base64 = preview.bodyPreviewBase64;
|
|
1156
|
+
if (preview.changed) result.preview_sanitized = true;
|
|
1157
|
+
}
|
|
1158
|
+
return result;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function redactFixtureText(text: string, sensitiveParams: CapturedSensitiveParams): string {
|
|
1162
|
+
// Shared free-text policy: long values are unconditional substrings; values
|
|
1163
|
+
// shorter than four characters require token boundaries. Exact scalar echoes
|
|
1164
|
+
// and declared query-key positions are structurally redacted for every length.
|
|
1165
|
+
let redacted = redactSensitiveText(text, sensitiveParams.values);
|
|
1166
|
+
for (const name of sensitiveParams.names) {
|
|
1167
|
+
let componentEncodedName = name;
|
|
1168
|
+
try {
|
|
1169
|
+
componentEncodedName = encodeURIComponent(name);
|
|
1170
|
+
} catch {
|
|
1171
|
+
// Lone surrogates remain covered by the raw and form-encoded variants.
|
|
1172
|
+
}
|
|
1173
|
+
const keyVariants = new Set([
|
|
1174
|
+
name,
|
|
1175
|
+
componentEncodedName,
|
|
1176
|
+
new URLSearchParams({ [name]: "" }).toString().slice(0, -1),
|
|
1177
|
+
]);
|
|
1178
|
+
for (const key of keyVariants) {
|
|
1179
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1180
|
+
const queryValue = new RegExp(`(^|[?&])(${escapedKey}=)[^&#\\s"]*`, "g");
|
|
1181
|
+
redacted = redacted.replace(queryValue, (_match, prefix, assignment) => {
|
|
1182
|
+
return `${prefix}${assignment}${REDACTED_QUERY_VALUE}`;
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return redacted;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function discoverSensitiveQueryValues(
|
|
1190
|
+
value: unknown,
|
|
1191
|
+
sensitiveParams: CapturedSensitiveParams,
|
|
1192
|
+
): CapturedSensitiveParams {
|
|
1193
|
+
const values = new Set(sensitiveParams.values);
|
|
1194
|
+
const absoluteUrl = /\b[A-Za-z][A-Za-z\d+.-]*:\/\/[^\s<>"']+/g;
|
|
1195
|
+
const discoverFromUrl = (url: string) => {
|
|
1196
|
+
for (const discovered of redactUrlQueryParams(url, sensitiveParams.names).sensitiveValues) {
|
|
1197
|
+
if (discovered !== "" && discovered !== REDACTED_QUERY_VALUE) values.add(discovered);
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
const visitText = (text: string) => {
|
|
1201
|
+
if (!/\s/.test(text)) {
|
|
1202
|
+
try {
|
|
1203
|
+
new URL(text, "http://apifuse.invalid");
|
|
1204
|
+
discoverFromUrl(text);
|
|
1205
|
+
return;
|
|
1206
|
+
} catch {
|
|
1207
|
+
// Fall through to extracting absolute URL spans from prose.
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
for (const match of text.matchAll(absoluteUrl)) {
|
|
1211
|
+
try {
|
|
1212
|
+
new URL(match[0]);
|
|
1213
|
+
discoverFromUrl(match[0]);
|
|
1214
|
+
} catch {
|
|
1215
|
+
// Ignore URI-like prose that is not a parseable URL.
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
const visit = (current: unknown): void => {
|
|
1220
|
+
if (typeof current === "string") {
|
|
1221
|
+
visitText(current);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (Array.isArray(current)) {
|
|
1225
|
+
for (const item of current) visit(item);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
if (!current || typeof current !== "object") return;
|
|
1229
|
+
for (const [key, entryValue] of Object.entries(current as MutableRecord)) {
|
|
1230
|
+
visitText(key);
|
|
1231
|
+
visit(entryValue);
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
visit(value);
|
|
1235
|
+
return { names: sensitiveParams.names, values: [...values] };
|
|
463
1236
|
}
|
|
464
1237
|
|
|
465
|
-
|
|
1238
|
+
function collisionSafeKey(record: MutableRecord, preferredKey: string): string {
|
|
1239
|
+
if (!(preferredKey in record)) return preferredKey;
|
|
1240
|
+
let suffix = 2;
|
|
1241
|
+
while (`${preferredKey}#${suffix}` in record) suffix += 1;
|
|
1242
|
+
return `${preferredKey}#${suffix}`;
|
|
1243
|
+
}
|
|
1244
|
+
export async function prepareFixturePayload(
|
|
466
1245
|
fixturePath: string,
|
|
467
1246
|
payload: unknown,
|
|
468
1247
|
append: boolean,
|
|
@@ -471,15 +1250,45 @@ async function prepareFixturePayload(
|
|
|
471
1250
|
return payload;
|
|
472
1251
|
}
|
|
473
1252
|
|
|
1253
|
+
let fixtureSource: string;
|
|
474
1254
|
try {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
1255
|
+
fixtureSource = readFileSync(fixturePath, "utf8");
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
throw new Error(
|
|
1258
|
+
`Cannot append to existing fixture "${fixturePath}" because it could not be read: ${
|
|
1259
|
+
error instanceof Error ? error.message : String(error)
|
|
1260
|
+
}. Fix its permissions or delete it, then run apifuse record --append again.`,
|
|
1261
|
+
);
|
|
481
1262
|
}
|
|
482
1263
|
|
|
1264
|
+
let existing: unknown;
|
|
1265
|
+
try {
|
|
1266
|
+
existing = JSON.parse(fixtureSource) as unknown;
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
throw new Error(
|
|
1269
|
+
`Cannot append to corrupt fixture "${fixturePath}" because it is not valid JSON: ${
|
|
1270
|
+
error instanceof Error ? error.message : String(error)
|
|
1271
|
+
}. Fix or delete the fixture, then run apifuse record --append again.`,
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
if (hasStreamEvidenceMarker(existing)) {
|
|
1276
|
+
const evidence = parseStreamEvidenceRecord(existing);
|
|
1277
|
+
return [createStreamCaptureEnvelope([{ kind: "stream", evidence }]), payload];
|
|
1278
|
+
}
|
|
1279
|
+
if (Array.isArray(existing)) {
|
|
1280
|
+
if (existing.some((item) => hasStreamEvidenceMarker(item))) {
|
|
1281
|
+
const legacyGroup = findStreamCaptureGroup(existing);
|
|
1282
|
+
if (legacyGroup) {
|
|
1283
|
+
const prefix = existing.slice(0, -legacyGroup.items.length);
|
|
1284
|
+
return [...prefix, createStreamCaptureEnvelope(legacyGroup.items), payload];
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
return [...existing, payload];
|
|
1288
|
+
}
|
|
1289
|
+
if (existing !== null) {
|
|
1290
|
+
return [existing, payload];
|
|
1291
|
+
}
|
|
483
1292
|
return payload;
|
|
484
1293
|
}
|
|
485
1294
|
|
|
@@ -491,4 +1300,24 @@ function formatBytes(bytes: number): string {
|
|
|
491
1300
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
492
1301
|
}
|
|
493
1302
|
|
|
494
|
-
|
|
1303
|
+
function jsonFixtureValue(value: unknown): JsonValue {
|
|
1304
|
+
const serialized = JSON.stringify(value);
|
|
1305
|
+
if (serialized === undefined) {
|
|
1306
|
+
throw new Error("Captured upstream response is not JSON-serializable.");
|
|
1307
|
+
}
|
|
1308
|
+
return JSON.parse(serialized) as JsonValue;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function unsupportedSseCaptureError(request: {
|
|
1312
|
+
order: number;
|
|
1313
|
+
method: string;
|
|
1314
|
+
path: string;
|
|
1315
|
+
}): Error {
|
|
1316
|
+
return new Error(
|
|
1317
|
+
`apifuse record does not support ctx.http.sse(): method=${request.method} path=${request.path} call=${request.order}.`,
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
if (import.meta.main) {
|
|
1322
|
+
await main();
|
|
1323
|
+
}
|