@apifuse/provider-sdk 2.2.0-beta.5 → 2.2.0-beta.51
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 +537 -33
- package/CHANGELOG.md +205 -1
- package/README.md +83 -22
- package/SUBMISSION.md +2 -2
- package/bin/apifuse-check.ts +165 -11
- package/bin/apifuse-dev.ts +57 -13
- package/bin/apifuse-migrate-operation-declaration.ts +55 -0
- package/bin/apifuse-migrate-shape.ts +202 -0
- package/bin/apifuse-pack-check.ts +22 -2
- package/bin/apifuse-pack-smoke.ts +57 -2
- package/bin/apifuse-pack-types.ts +357 -38
- package/bin/apifuse-perf.ts +14 -13
- package/bin/apifuse-record.ts +709 -72
- package/bin/apifuse-submit-check.ts +2344 -324
- package/bin/apifuse-sync-assets.ts +117 -0
- 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 +27 -0
- package/dist/cli/create.d.ts +4 -1
- package/dist/cli/create.js +42 -37
- package/dist/cli/migrate-operation-declaration.d.ts +59 -0
- package/dist/cli/migrate-operation-declaration.js +1249 -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 +52 -0
- package/dist/cli/migrate-provider-shape.js +578 -0
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
- package/dist/cli/templates/provider/README.md.tpl +5 -5
- 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 +179 -16
- package/dist/config/loader.js +435 -127
- 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 +33 -0
- package/dist/declaration-validation.js +266 -0
- package/dist/define.d.ts +66 -29
- package/dist/define.js +778 -69
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +1 -1
- package/dist/engine.d.ts +78 -0
- package/dist/engine.js +133 -0
- package/dist/error-observability.d.ts +7 -0
- package/dist/error-observability.js +61 -0
- package/dist/error-resolution.d.ts +4 -0
- package/dist/error-resolution.js +122 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.js +40 -0
- package/dist/fixture-sanitization.d.ts +28 -0
- package/dist/fixture-sanitization.js +227 -0
- package/dist/health-scenario.d.ts +1842 -0
- package/dist/health-scenario.js +624 -0
- package/dist/index.d.ts +21 -10
- package/dist/index.js +12 -7
- package/dist/lint.d.ts +23 -11
- package/dist/lint.js +572 -69
- 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 +10 -2
- package/dist/provider.js +4 -1
- package/dist/runtime/auth-flow.d.ts +5 -1
- package/dist/runtime/auth-flow.js +6 -0
- package/dist/runtime/browser.d.ts +1 -0
- package/dist/runtime/browser.js +492 -49
- package/dist/runtime/cache.d.ts +1 -0
- package/dist/runtime/cache.js +169 -15
- package/dist/runtime/choice-wordlist.d.ts +9 -0
- package/dist/runtime/choice-wordlist.js +138 -0
- package/dist/runtime/choice.d.ts +13 -1
- package/dist/runtime/choice.js +490 -102
- package/dist/runtime/chrome149-header-order.d.ts +58 -0
- package/dist/runtime/chrome149-header-order.js +289 -0
- package/dist/runtime/env.js +12 -0
- package/dist/runtime/executor.d.ts +3 -2
- package/dist/runtime/executor.js +26 -25
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.js +515 -53
- package/dist/runtime/insights.js +2 -2
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +366 -8
- 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 +96 -0
- package/dist/runtime/native-network.js +1232 -0
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +440 -0
- package/dist/runtime/otlp.d.ts +71 -2
- package/dist/runtime/otlp.js +397 -16
- package/dist/runtime/proxy-errors.js +6 -2
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-telemetry.d.ts +80 -1
- package/dist/runtime/proxy-telemetry.js +154 -47
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +4 -2
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/resolver-config.d.ts +6 -0
- package/dist/runtime/resolver-config.js +6 -0
- package/dist/runtime/resolver-public.d.ts +1 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-shared.d.ts +14 -0
- package/dist/runtime/resolver-shared.js +12 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
- package/dist/runtime/resolver-vendors/bindings.js +40 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
- package/dist/runtime/resolver-vendors/browser.js +377 -0
- package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
- package/dist/runtime/resolver-vendors/capsolver.js +531 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
- package/dist/runtime/resolver-vendors/hosts.js +33 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +408 -0
- package/dist/runtime/resolver-vendors/types.d.ts +94 -0
- package/dist/runtime/resolver-vendors/types.js +96 -0
- package/dist/runtime/resolver.d.ts +72 -0
- package/dist/runtime/resolver.js +771 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +277 -71
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.d.ts +41 -5
- package/dist/runtime/stealth.js +997 -291
- package/dist/runtime/stt.js +1 -12
- 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 +5 -0
- package/dist/runtime/trace.js +43 -10
- 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/index.d.ts +5 -2
- package/dist/server/index.js +2 -2
- 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.d.ts +14 -3
- package/dist/server/self-test.js +129 -58
- package/dist/server/serve-implementation.d.ts +230 -0
- package/dist/server/serve-implementation.js +2265 -0
- package/dist/server/serve.d.ts +1 -70
- package/dist/server/serve.js +1 -1131
- package/dist/server/trace-output.d.ts +6 -0
- package/dist/server/trace-output.js +79 -0
- package/dist/server/types.d.ts +30 -5
- package/dist/server/types.js +13 -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 +215 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -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 +9 -4
- package/dist/stealth/profiles.js +160 -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 +489 -21
- package/dist/trace-sanitization.d.ts +5 -0
- package/dist/trace-sanitization.js +45 -0
- package/dist/types.d.ts +673 -131
- package/dist/types.js +1 -0
- package/package.json +45 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +118 -0
- package/src/ceremonies/index.ts +189 -46
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +10 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +8 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-hoisted-array-spread.ts.txt +15 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-imported-array-spread.ts.txt +9 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +5 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-mixed-array-spread.ts.txt +24 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +16 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +31 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +21 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +14 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +15 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +4 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +12 -0
- package/src/cli/commands.ts +33 -0
- package/src/cli/create.ts +50 -37
- package/src/cli/migrate-operation-declaration.ts +1751 -0
- package/src/cli/migrate-operation-shape.ts +184 -0
- package/src/cli/migrate-provider-shape.ts +772 -0
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/cli/templates/provider/Dockerfile.tpl +1 -1
- package/src/cli/templates/provider/README.md.tpl +5 -5
- 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 +690 -163
- 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 +330 -0
- package/src/define.ts +1048 -131
- package/src/dev.ts +4 -1
- package/src/engine.ts +279 -0
- package/src/error-observability.ts +64 -0
- package/src/error-resolution.ts +127 -0
- package/src/errors.ts +68 -0
- package/src/fixture-sanitization.ts +264 -0
- package/src/health-scenario.ts +875 -0
- package/src/index.ts +220 -13
- package/src/lint.ts +698 -90
- 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 +161 -3
- package/src/runtime/auth-flow.ts +12 -0
- package/src/runtime/browser.ts +661 -63
- package/src/runtime/cache.ts +189 -14
- package/src/runtime/choice-wordlist.ts +145 -0
- package/src/runtime/choice.ts +631 -120
- package/src/runtime/chrome149-header-order.ts +330 -0
- package/src/runtime/env.ts +13 -0
- package/src/runtime/executor.ts +43 -31
- package/src/runtime/http.ts +641 -61
- package/src/runtime/insights.ts +2 -2
- package/src/runtime/instrumentation.ts +520 -15
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +1605 -0
- package/src/runtime/ocr.ts +523 -0
- package/src/runtime/otlp.ts +467 -21
- package/src/runtime/proxy-errors.ts +12 -4
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-telemetry.ts +244 -75
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +7 -2
- package/src/runtime/request-options.ts +680 -1
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-public.ts +23 -0
- package/src/runtime/resolver-shared.ts +33 -0
- package/src/runtime/resolver-vendors/bindings.ts +56 -0
- package/src/runtime/resolver-vendors/browser.ts +533 -0
- package/src/runtime/resolver-vendors/capsolver.ts +707 -0
- package/src/runtime/resolver-vendors/hosts.ts +38 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +540 -0
- package/src/runtime/resolver-vendors/types.ts +212 -0
- package/src/runtime/resolver.ts +1157 -0
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +394 -77
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +1280 -336
- package/src/runtime/stt.ts +1 -19
- package/src/runtime/timeout.ts +18 -0
- package/src/runtime/trace-config.ts +78 -0
- package/src/runtime/trace.ts +57 -17
- package/src/serve.ts +6 -1
- package/src/server/error-observability.ts +1 -0
- package/src/server/index.ts +34 -2
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test.ts +191 -68
- package/src/server/serve-implementation.ts +3378 -0
- package/src/server/serve.ts +1 -1632
- package/src/server/trace-output.ts +129 -0
- package/src/server/types.ts +13 -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 +287 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -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 +202 -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 +658 -15
- package/src/trace-sanitization.ts +63 -0
- package/src/types.ts +788 -174
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/bin/apifuse-record.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
// @ts-nocheck
|
|
3
2
|
|
|
4
3
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
5
4
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
@@ -9,19 +8,63 @@ import { pathToFileURL } from "node:url";
|
|
|
9
8
|
import {
|
|
10
9
|
createBypassProviderCache,
|
|
11
10
|
createHttpClient,
|
|
11
|
+
createInProcessProviderEngine,
|
|
12
|
+
createOcrClientFromEnv,
|
|
12
13
|
createProviderChoiceContext,
|
|
13
|
-
|
|
14
|
+
createProviderEnvironment,
|
|
14
15
|
createSttClientFromEnv,
|
|
16
|
+
createUnsupportedResolverClient,
|
|
15
17
|
executeOperation,
|
|
16
18
|
type HttpClient,
|
|
19
|
+
type HttpResponse,
|
|
20
|
+
type HttpStreamResponse,
|
|
17
21
|
type ProviderContext,
|
|
18
22
|
type ProviderDefinition,
|
|
23
|
+
type ProviderEngineBindingCandidates,
|
|
19
24
|
ProviderError,
|
|
25
|
+
readEngineProxyCredentials,
|
|
26
|
+
type ProviderProxyPolicy,
|
|
27
|
+
type RequestOptions,
|
|
20
28
|
type StealthClient,
|
|
21
29
|
TransportError,
|
|
22
30
|
ValidationError,
|
|
23
31
|
} from "../src/index.js";
|
|
32
|
+
import type { JsonValue } from "../src/contract-json.js";
|
|
33
|
+
import {
|
|
34
|
+
isSensitiveFixtureKey,
|
|
35
|
+
requestPathForFixture,
|
|
36
|
+
sanitizeDiagnosticText,
|
|
37
|
+
sanitizeFixtureString,
|
|
38
|
+
} from "../src/fixture-sanitization.js";
|
|
39
|
+
import { createResolverClientFromEnv } from "../src/runtime/resolver.js";
|
|
40
|
+
import {
|
|
41
|
+
isSensitiveKey,
|
|
42
|
+
normalizeSensitiveParams,
|
|
43
|
+
parseHttpRequestInvocation,
|
|
44
|
+
REDACTED_QUERY_VALUE,
|
|
45
|
+
redactSensitiveError,
|
|
46
|
+
redactSensitiveText,
|
|
47
|
+
redactUrlQueryParams,
|
|
48
|
+
replaceRequestOptionsInHttpInvocation,
|
|
49
|
+
requestOptionsFromHttpInvocation,
|
|
50
|
+
serializeRequestUrl,
|
|
51
|
+
} from "../src/runtime/request-options.js";
|
|
24
52
|
import { createMemoryProviderRuntimeState } from "../src/runtime/state.js";
|
|
53
|
+
import { createStealthClient } from "../src/runtime/stealth.js";
|
|
54
|
+
import { parseSchema } from "../src/schema.js";
|
|
55
|
+
import { getStealthProfile } from "../src/stealth/profiles.js";
|
|
56
|
+
import {
|
|
57
|
+
captureStreamEvidence,
|
|
58
|
+
createStreamCaptureEnvelope,
|
|
59
|
+
findStreamCaptureGroup,
|
|
60
|
+
findStreamEvidenceRecords,
|
|
61
|
+
hasStreamEvidenceMarker,
|
|
62
|
+
parseStreamEvidenceRecord,
|
|
63
|
+
STREAM_PREVIEW_BYTES,
|
|
64
|
+
type StreamCaptureGroupItem,
|
|
65
|
+
type StreamEvidenceCapture,
|
|
66
|
+
type StreamEvidenceRequest,
|
|
67
|
+
} from "../src/stream-evidence.js";
|
|
25
68
|
|
|
26
69
|
type CliArgs = {
|
|
27
70
|
append: boolean;
|
|
@@ -39,57 +82,95 @@ const HELP_TEXT = `Usage: apifuse record [path] --operation <operation> --params
|
|
|
39
82
|
|
|
40
83
|
Calls a real upstream-backed operation through ctx.http or ctx.stealth and writes __fixtures__/raw.json.
|
|
41
84
|
|
|
85
|
+
Streaming responses are recorded as evidence (status, selected headers, full-body SHA-256 and byte
|
|
86
|
+
count, plus a ${STREAM_PREVIEW_BYTES}-byte base64 preview). Test replay is evidence-only: ctx.http.stream exposes the
|
|
87
|
+
preview as its body and the original body_sha256/body_bytes as response metadata.
|
|
88
|
+
When an operation opens multiple streams, all evidence records are saved in stream call order.
|
|
89
|
+
Mixed JSON/stream operations save a tagged call-ordered envelope so snapshot replay can route each response.
|
|
90
|
+
ctx.http.sse() recording is unsupported and fails explicitly.
|
|
91
|
+
|
|
42
92
|
Options:
|
|
43
93
|
--operation, -o <name> operation to call
|
|
44
94
|
--params, -p <json> JSON input passed to the operation (default: {})
|
|
45
|
-
--append
|
|
95
|
+
--append preserve the existing fixture and append this capture
|
|
46
96
|
--sanitize redact common token/header fields (default)
|
|
47
|
-
--no-sanitize
|
|
97
|
+
--no-sanitize disable common-field redaction (sensitiveParams are always redacted)
|
|
48
98
|
--help, -h show this help
|
|
49
99
|
|
|
50
100
|
Example:
|
|
51
101
|
apifuse record providers/korea-air-quality --operation realtime --params '{"stationName":"jongno"}'`;
|
|
52
102
|
|
|
53
103
|
export async function main() {
|
|
104
|
+
let capture: ReturnType<typeof createCaptureContext> | undefined;
|
|
54
105
|
try {
|
|
55
106
|
const args = parseArgs(normalizeArgs(process.argv.slice(2)));
|
|
56
107
|
const location = resolveProviderLocation(args.providerPath);
|
|
57
108
|
const provider = await loadProvider(location.rootDir);
|
|
58
109
|
const operationName = resolveOperationName(provider, args.operation);
|
|
59
110
|
const operation = provider.operations[operationName];
|
|
60
|
-
const parsedParams = parseParams(operation, args.params);
|
|
111
|
+
const parsedParams = await parseParams(operation, args.params);
|
|
61
112
|
|
|
62
|
-
|
|
113
|
+
capture = createCaptureContext(
|
|
63
114
|
provider,
|
|
64
115
|
resolveOperationBaseUrl(provider, operationName),
|
|
116
|
+
args.sanitize,
|
|
65
117
|
);
|
|
66
118
|
|
|
67
119
|
console.log(`[apifuse record] Calling ${operationName} on ${provider.id}...`);
|
|
68
120
|
|
|
69
|
-
|
|
70
|
-
|
|
121
|
+
let result: unknown;
|
|
122
|
+
try {
|
|
123
|
+
result = await executeOperation(provider, operationName, capture.ctx, parsedParams);
|
|
124
|
+
} catch (operationError) {
|
|
125
|
+
let partial: unknown;
|
|
126
|
+
try {
|
|
127
|
+
partial = await capture.getCapturedRaw();
|
|
128
|
+
} catch (finalizationError) {
|
|
129
|
+
throw new StreamRecorderError("Operation and stream finalization both failed.", [
|
|
130
|
+
operationError,
|
|
131
|
+
finalizationError,
|
|
132
|
+
]);
|
|
133
|
+
}
|
|
134
|
+
const streamCount = findStreamEvidenceRecords(partial).length;
|
|
135
|
+
if (streamCount > 0) {
|
|
136
|
+
throw new StreamRecorderError(
|
|
137
|
+
`Operation failed after finalizing ${streamCount} stream capture${streamCount === 1 ? "" : "s"}.`,
|
|
138
|
+
[operationError],
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
throw operationError;
|
|
142
|
+
}
|
|
143
|
+
const captured = await capture.getCapturedRaw();
|
|
71
144
|
|
|
72
145
|
if (captured === undefined) {
|
|
73
146
|
throw new Error(`No upstream response was captured for ${provider.id}.${operationName}.`);
|
|
74
147
|
}
|
|
75
148
|
|
|
76
|
-
const
|
|
149
|
+
const sensitiveParams = capture.getCapturedSensitiveParams();
|
|
150
|
+
const rawPayload = jsonFixtureValue(captured);
|
|
77
151
|
const fixturePath = resolve(location.rootDir, "__fixtures__", "raw.json");
|
|
78
|
-
const
|
|
152
|
+
const redactedCapture = redactFixture(rawPayload, sensitiveParams, args.sanitize);
|
|
153
|
+
const mergedPayload = await prepareFixturePayload(fixturePath, redactedCapture, args.append);
|
|
154
|
+
// Mandatory query-secret redaction applies to the merged history, including
|
|
155
|
+
// values discovered in older declared-key URL positions. Optional common-
|
|
156
|
+
// field sanitization applies only to this run's new capture so --append does
|
|
157
|
+
// not rewrite deliberately preserved historical fields.
|
|
158
|
+
const historicalSensitiveParams = discoverSensitiveQueryValues(mergedPayload, sensitiveParams);
|
|
159
|
+
const nextPayload = redactFixture(mergedPayload, historicalSensitiveParams, false);
|
|
79
160
|
|
|
80
161
|
await mkdir(dirname(fixturePath), { recursive: true });
|
|
81
162
|
await writeFile(fixturePath, `${JSON.stringify(nextPayload, null, 2)}\n`);
|
|
82
163
|
|
|
83
164
|
console.log(
|
|
84
165
|
`[apifuse record] Captured response (${formatBytes(
|
|
85
|
-
Buffer.byteLength(JSON.stringify(
|
|
166
|
+
Buffer.byteLength(JSON.stringify(redactedCapture)),
|
|
86
167
|
)})`,
|
|
87
168
|
);
|
|
88
169
|
console.log(`[apifuse record] Saved to ${relative(process.cwd(), fixturePath)}`);
|
|
89
170
|
|
|
90
171
|
void result;
|
|
91
172
|
} catch (error) {
|
|
92
|
-
handleCliError(error);
|
|
173
|
+
handleCliError(error, capture?.getCapturedSensitiveParams().values);
|
|
93
174
|
}
|
|
94
175
|
}
|
|
95
176
|
|
|
@@ -174,13 +255,29 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
174
255
|
return { append, providerPath, operation, params, sanitize };
|
|
175
256
|
}
|
|
176
257
|
|
|
177
|
-
function handleCliError(error: unknown): never {
|
|
178
|
-
const message = formatCliError(error);
|
|
258
|
+
function handleCliError(error: unknown, sensitiveValues: readonly string[] = []): never {
|
|
259
|
+
const message = redactSensitiveText(formatCliError(error), sensitiveValues);
|
|
179
260
|
console.error(`[apifuse record] ${message}`);
|
|
180
261
|
process.exit(1);
|
|
181
262
|
}
|
|
182
263
|
|
|
183
|
-
|
|
264
|
+
class StreamRecorderError extends Error {
|
|
265
|
+
readonly diagnosticCauses: readonly unknown[];
|
|
266
|
+
|
|
267
|
+
constructor(message: string, diagnosticCauses: readonly unknown[]) {
|
|
268
|
+
super(message, { cause: diagnosticCauses[0] });
|
|
269
|
+
this.name = "StreamRecorderError";
|
|
270
|
+
this.diagnosticCauses = diagnosticCauses;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function formatCliError(error: unknown): string {
|
|
275
|
+
if (error instanceof StreamRecorderError) {
|
|
276
|
+
return [
|
|
277
|
+
sanitizeDiagnosticText(error.message),
|
|
278
|
+
...error.diagnosticCauses.map((cause) => `cause=${formatDiagnosticCause(cause)}`),
|
|
279
|
+
].join(" ");
|
|
280
|
+
}
|
|
184
281
|
if (error instanceof TransportError) {
|
|
185
282
|
return [
|
|
186
283
|
error.message,
|
|
@@ -201,12 +298,32 @@ function formatCliError(error: unknown): string {
|
|
|
201
298
|
}
|
|
202
299
|
|
|
203
300
|
if (error instanceof Error) {
|
|
301
|
+
if (/^Stream capture\b/.test(error.message) && error.cause !== undefined) {
|
|
302
|
+
return `${sanitizeDiagnosticText(error.message)} cause=${formatDiagnosticCause(error.cause)}`;
|
|
303
|
+
}
|
|
204
304
|
return error.message;
|
|
205
305
|
}
|
|
206
306
|
|
|
207
307
|
return String(error);
|
|
208
308
|
}
|
|
209
309
|
|
|
310
|
+
function formatDiagnosticCause(cause: unknown): string {
|
|
311
|
+
if (cause instanceof StreamRecorderError) {
|
|
312
|
+
return [
|
|
313
|
+
sanitizeDiagnosticText(cause.message),
|
|
314
|
+
...cause.diagnosticCauses.map((nested) => `cause=${formatDiagnosticCause(nested)}`),
|
|
315
|
+
].join(" ");
|
|
316
|
+
}
|
|
317
|
+
if (!(cause instanceof Error)) return sanitizeDiagnosticText(String(cause));
|
|
318
|
+
const code =
|
|
319
|
+
"code" in cause && typeof cause.code === "string"
|
|
320
|
+
? ` code=${sanitizeDiagnosticText(cause.code)}`
|
|
321
|
+
: "";
|
|
322
|
+
const nestedCause =
|
|
323
|
+
cause.cause === undefined ? "" : ` cause=${formatDiagnosticCause(cause.cause)}`;
|
|
324
|
+
return `${sanitizeDiagnosticText(cause.message)}${code}${nestedCause}`;
|
|
325
|
+
}
|
|
326
|
+
|
|
210
327
|
function resolveProviderLocation(inputPath?: string) {
|
|
211
328
|
const originalInput = inputPath ?? process.cwd();
|
|
212
329
|
const resolvedInput = resolve(process.cwd(), originalInput);
|
|
@@ -284,7 +401,10 @@ function resolveOperationName(provider: ProviderRuntime, operationName?: string)
|
|
|
284
401
|
return firstOperation;
|
|
285
402
|
}
|
|
286
403
|
|
|
287
|
-
function parseParams(
|
|
404
|
+
async function parseParams(
|
|
405
|
+
operation: ProviderRuntime["operations"][string],
|
|
406
|
+
value: string,
|
|
407
|
+
): Promise<unknown> {
|
|
288
408
|
let parsed: unknown;
|
|
289
409
|
|
|
290
410
|
try {
|
|
@@ -295,7 +415,7 @@ function parseParams(operation: ProviderRuntime["operations"][string], value: st
|
|
|
295
415
|
);
|
|
296
416
|
}
|
|
297
417
|
|
|
298
|
-
return operation.input ? operation.input.
|
|
418
|
+
return operation.input ? parseSchema(operation.input, parsed, "record.params") : parsed;
|
|
299
419
|
}
|
|
300
420
|
|
|
301
421
|
function resolveOperationBaseUrl(provider: ProviderRuntime, operationName: string): string {
|
|
@@ -309,19 +429,99 @@ function resolveOperationBaseUrl(provider: ProviderRuntime, operationName: strin
|
|
|
309
429
|
return baseUrl;
|
|
310
430
|
}
|
|
311
431
|
|
|
312
|
-
function
|
|
313
|
-
|
|
432
|
+
function resolveNativeProxyPolicy(provider: ProviderDefinition): ProviderProxyPolicy | undefined {
|
|
433
|
+
if (typeof provider.proxy === "object") return provider.proxy;
|
|
434
|
+
if (provider.proxy === true) return { mode: "optional" };
|
|
435
|
+
if (provider.proxy === false) return { mode: "disabled" };
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
314
438
|
|
|
315
|
-
|
|
316
|
-
|
|
439
|
+
export function createCaptureContext(
|
|
440
|
+
provider: ProviderRuntime,
|
|
441
|
+
baseUrl: string,
|
|
442
|
+
sanitize: boolean,
|
|
443
|
+
) {
|
|
444
|
+
let nextCaptureOrder = 0;
|
|
445
|
+
let nextStreamOrdinal = 0;
|
|
446
|
+
let capturedRaw: JsonValue | undefined;
|
|
447
|
+
const rawCaptures: Array<{ order: number; value: JsonValue }> = [];
|
|
448
|
+
const streamCaptures: Array<{
|
|
449
|
+
order: number;
|
|
450
|
+
request: StreamEvidenceRequest;
|
|
451
|
+
capture: StreamEvidenceCapture;
|
|
452
|
+
}> = [];
|
|
453
|
+
let capturedSse: { order: number; method: string; path: string } | undefined;
|
|
454
|
+
const sensitiveParamNames = new Set<string>();
|
|
455
|
+
const sensitiveParamValues = new Set<string>();
|
|
456
|
+
const captureSensitiveParams = (url: string, options?: SensitiveRequestOptions) => {
|
|
457
|
+
captureSensitiveRequestValues(url, options, sensitiveParamNames, sensitiveParamValues);
|
|
458
|
+
};
|
|
459
|
+
const getCapturedSensitiveParams = (): CapturedSensitiveParams => ({
|
|
460
|
+
names: [...sensitiveParamNames],
|
|
461
|
+
values: [...sensitiveParamValues],
|
|
317
462
|
});
|
|
318
|
-
const
|
|
319
|
-
|
|
463
|
+
const reserveCaptureOrder = () => {
|
|
464
|
+
nextCaptureOrder += 1;
|
|
465
|
+
return nextCaptureOrder;
|
|
466
|
+
};
|
|
467
|
+
const retainRawCapture = (order: number, value: unknown) => {
|
|
468
|
+
const json = jsonFixtureValue(value);
|
|
469
|
+
capturedRaw = json;
|
|
470
|
+
rawCaptures.push({ order, value: json });
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const http = captureHttpClient(createHttpClient(baseUrl), {
|
|
474
|
+
reserveOrder: reserveCaptureOrder,
|
|
475
|
+
reserveStreamOrdinal: () => {
|
|
476
|
+
nextStreamOrdinal += 1;
|
|
477
|
+
return nextStreamOrdinal;
|
|
478
|
+
},
|
|
479
|
+
onSensitiveParams: captureSensitiveParams,
|
|
480
|
+
onResponse: (order, response) => retainRawCapture(order, response.data),
|
|
481
|
+
onStreamResponse: (order, ordinal, requestUrl, method, response) => {
|
|
482
|
+
const resolvedRequestUrl = new URL(requestUrl, baseUrl).toString();
|
|
483
|
+
const request = {
|
|
484
|
+
ordinal,
|
|
485
|
+
method,
|
|
486
|
+
path: requestPathForFixture(resolvedRequestUrl),
|
|
487
|
+
};
|
|
488
|
+
const capture = captureStreamEvidence(response, {
|
|
489
|
+
requestUrl: resolvedRequestUrl,
|
|
490
|
+
request,
|
|
491
|
+
...(sanitize
|
|
492
|
+
? {
|
|
493
|
+
sanitizeFixture: (value: JsonValue) =>
|
|
494
|
+
jsonFixtureValue(sanitizeStreamFixture(value, getCapturedSensitiveParams())),
|
|
495
|
+
}
|
|
496
|
+
: {}),
|
|
497
|
+
});
|
|
498
|
+
streamCaptures.push({ order, request, capture });
|
|
499
|
+
return capture.response;
|
|
500
|
+
},
|
|
501
|
+
onSseResponse: (order, requestUrl, method) => {
|
|
502
|
+
capturedSse = {
|
|
503
|
+
order,
|
|
504
|
+
method,
|
|
505
|
+
path: requestPathForFixture(new URL(requestUrl, baseUrl).toString()),
|
|
506
|
+
};
|
|
507
|
+
},
|
|
320
508
|
});
|
|
509
|
+
const stealth = proxyStealthClient(
|
|
510
|
+
createStealthClient(baseUrl, {
|
|
511
|
+
...(provider.stealth ? { stealth: provider.stealth } : {}),
|
|
512
|
+
}),
|
|
513
|
+
captureSensitiveParams,
|
|
514
|
+
(order, response) => retainRawCapture(order, normalizeCapturedStealthResponse(response)),
|
|
515
|
+
reserveCaptureOrder,
|
|
516
|
+
);
|
|
321
517
|
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
518
|
+
const providerEnvironment = createProviderEnvironment(
|
|
519
|
+
process.env,
|
|
520
|
+
provider.secrets?.map((secret) => secret.name) ?? [],
|
|
521
|
+
);
|
|
522
|
+
const env = { get: (key: string) => providerEnvironment[key] };
|
|
523
|
+
const engineEnv = { get: (key: string) => process.env[key] };
|
|
524
|
+
const engineCredentials = readEngineProxyCredentials();
|
|
325
525
|
const credential = {
|
|
326
526
|
mode: "none" as const,
|
|
327
527
|
get: () => undefined,
|
|
@@ -330,12 +530,15 @@ function createCaptureContext(provider: ProviderRuntime, baseUrl: string) {
|
|
|
330
530
|
getScopes: () => [],
|
|
331
531
|
};
|
|
332
532
|
const state = createMemoryProviderRuntimeState();
|
|
333
|
-
const
|
|
533
|
+
const cache = createBypassProviderCache({ providerId: provider.id });
|
|
534
|
+
const proxyPolicy = resolveNativeProxyPolicy(provider);
|
|
535
|
+
const stealthProfile = provider.stealth ? getStealthProfile(provider.stealth) : undefined;
|
|
536
|
+
const candidates: ProviderEngineBindingCandidates = {
|
|
334
537
|
env,
|
|
335
538
|
credential,
|
|
336
539
|
request: { headers: {} },
|
|
337
540
|
http,
|
|
338
|
-
cache
|
|
541
|
+
cache,
|
|
339
542
|
state,
|
|
340
543
|
stealth,
|
|
341
544
|
browser: {
|
|
@@ -362,70 +565,292 @@ function createCaptureContext(provider: ProviderRuntime, baseUrl: string) {
|
|
|
362
565
|
throw new Error("Auth prompts are not available in apifuse record.");
|
|
363
566
|
},
|
|
364
567
|
},
|
|
568
|
+
ocr: createOcrClientFromEnv(provider.ocr),
|
|
365
569
|
stt: createSttClientFromEnv(provider.stt),
|
|
570
|
+
resolver: provider.resolver
|
|
571
|
+
? createResolverClientFromEnv(provider.resolver, engineCredentials, {
|
|
572
|
+
allowedHosts: provider.allowedHosts,
|
|
573
|
+
cache,
|
|
574
|
+
...(proxyPolicy
|
|
575
|
+
? {
|
|
576
|
+
proxyIntent: {
|
|
577
|
+
mode: proxyPolicy.mode,
|
|
578
|
+
upstream: { proxy: provider.proxy },
|
|
579
|
+
...(stealthProfile ? { userAgent: stealthProfile.userAgent } : {}),
|
|
580
|
+
},
|
|
581
|
+
}
|
|
582
|
+
: {}),
|
|
583
|
+
})
|
|
584
|
+
: createUnsupportedResolverClient("Provider does not declare resolver capability"),
|
|
366
585
|
choice: createProviderChoiceContext({
|
|
367
586
|
providerId: provider.id,
|
|
368
|
-
env,
|
|
587
|
+
env: engineEnv,
|
|
369
588
|
request: { headers: {} },
|
|
370
589
|
credential,
|
|
371
590
|
state,
|
|
372
591
|
}),
|
|
373
592
|
};
|
|
593
|
+
const ctx = createInProcessProviderEngine().attach({
|
|
594
|
+
provider,
|
|
595
|
+
bindings: candidates,
|
|
596
|
+
}) as ProviderContext;
|
|
374
597
|
|
|
375
598
|
return {
|
|
376
599
|
ctx,
|
|
377
|
-
getCapturedRaw: () =>
|
|
600
|
+
getCapturedRaw: async () => {
|
|
601
|
+
if (streamCaptures.length === 0) {
|
|
602
|
+
if (capturedSse) throw unsupportedSseCaptureError(capturedSse);
|
|
603
|
+
return capturedRaw;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const settled = await Promise.allSettled(
|
|
607
|
+
streamCaptures.map(async ({ order, request, capture }) => ({
|
|
608
|
+
order,
|
|
609
|
+
request,
|
|
610
|
+
value: redactStreamEvidence(await capture.getEvidence(), getCapturedSensitiveParams()),
|
|
611
|
+
})),
|
|
612
|
+
);
|
|
613
|
+
const failures = settled.flatMap((result, index) =>
|
|
614
|
+
result.status === "rejected"
|
|
615
|
+
? [
|
|
616
|
+
new StreamRecorderError(
|
|
617
|
+
`Stream finalization failed: method=${streamCaptures[index]!.request.method} path=${streamCaptures[index]!.request.path} ordinal=${streamCaptures[index]!.request.ordinal}.`,
|
|
618
|
+
[result.reason],
|
|
619
|
+
),
|
|
620
|
+
]
|
|
621
|
+
: [],
|
|
622
|
+
);
|
|
623
|
+
if (failures.length > 0) {
|
|
624
|
+
throw new StreamRecorderError(
|
|
625
|
+
`${failures.length} stream capture${failures.length === 1 ? "" : "s"} failed to finalize.`,
|
|
626
|
+
failures,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
const evidence = settled.flatMap((result) =>
|
|
630
|
+
result.status === "fulfilled" ? [result.value] : [],
|
|
631
|
+
);
|
|
632
|
+
if (capturedSse) throw unsupportedSseCaptureError(capturedSse);
|
|
633
|
+
const timeline: StreamCaptureGroupItem[] = [...rawCaptures, ...evidence]
|
|
634
|
+
.sort((left, right) => left.order - right.order)
|
|
635
|
+
.map((item) =>
|
|
636
|
+
"request" in item
|
|
637
|
+
? { kind: "stream" as const, evidence: item.value }
|
|
638
|
+
: { kind: "response" as const, value: item.value },
|
|
639
|
+
);
|
|
640
|
+
return createStreamCaptureEnvelope(timeline);
|
|
641
|
+
},
|
|
642
|
+
getCapturedSensitiveParams,
|
|
378
643
|
};
|
|
379
644
|
}
|
|
380
645
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
return new Proxy(client, {
|
|
386
|
-
get(target, prop, receiver) {
|
|
387
|
-
const value = Reflect.get(target, prop, receiver);
|
|
646
|
+
type CapturedSensitiveParams = {
|
|
647
|
+
names: readonly string[];
|
|
648
|
+
values: readonly string[];
|
|
649
|
+
};
|
|
388
650
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
651
|
+
type SensitiveRequestOptions = Pick<RequestOptions, "params" | "sensitiveParams">;
|
|
652
|
+
|
|
653
|
+
function captureSensitiveRequestValues(
|
|
654
|
+
url: string,
|
|
655
|
+
options: SensitiveRequestOptions | undefined,
|
|
656
|
+
names: Set<string>,
|
|
657
|
+
values: Set<string>,
|
|
658
|
+
): void {
|
|
659
|
+
const sensitiveParams = normalizeSensitiveParams(options?.sensitiveParams);
|
|
660
|
+
if (sensitiveParams === undefined) return;
|
|
661
|
+
if (!sensitiveParams || typeof sensitiveParams !== "object" || Array.isArray(sensitiveParams)) {
|
|
662
|
+
throw new TypeError("sensitiveParams must be an object whose values are strings.");
|
|
663
|
+
}
|
|
392
664
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
665
|
+
const entries = Object.entries(sensitiveParams);
|
|
666
|
+
for (const [key, value] of entries) {
|
|
667
|
+
if (typeof value !== "string") {
|
|
668
|
+
throw new TypeError(`sensitiveParams.${key} must be a string.`);
|
|
669
|
+
}
|
|
670
|
+
names.add(key);
|
|
671
|
+
if (value !== "") values.add(value);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
let serializedUrl: ReturnType<typeof serializeRequestUrl>;
|
|
675
|
+
try {
|
|
676
|
+
const absoluteUrl = new URL(String(url), "http://apifuse.invalid").toString();
|
|
677
|
+
serializedUrl = serializeRequestUrl(absoluteUrl, options?.params, sensitiveParams);
|
|
678
|
+
} catch (error) {
|
|
679
|
+
const structural = redactUrlQueryParams(String(url), [...names]);
|
|
680
|
+
const safeUrl = redactSensitiveText(structural.redactedUrl, [
|
|
681
|
+
...values,
|
|
682
|
+
...structural.sensitiveValues,
|
|
683
|
+
]);
|
|
684
|
+
const causeKind = error instanceof Error ? error.name : typeof error;
|
|
685
|
+
throw new TypeError(`Cannot securely record sensitiveParams for "${safeUrl}" (${causeKind}).`, {
|
|
686
|
+
cause: redactSensitiveError(
|
|
687
|
+
error,
|
|
688
|
+
[...values, ...structural.sensitiveValues],
|
|
689
|
+
String(url),
|
|
690
|
+
structural.redactedUrl,
|
|
691
|
+
),
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
for (const value of serializedUrl.sensitiveValues) {
|
|
696
|
+
if (value !== "") values.add(value);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function snapshotRequestOptions<T extends SensitiveRequestOptions>(options: T): T {
|
|
701
|
+
return {
|
|
702
|
+
...options,
|
|
703
|
+
...(options.params
|
|
704
|
+
? {
|
|
705
|
+
params: Object.fromEntries(
|
|
706
|
+
Object.entries(options.params).map(([key, value]) => [
|
|
707
|
+
key,
|
|
708
|
+
Array.isArray(value) ? [...value] : value,
|
|
709
|
+
]),
|
|
710
|
+
),
|
|
711
|
+
}
|
|
712
|
+
: {}),
|
|
713
|
+
...(normalizeSensitiveParams(options.sensitiveParams)
|
|
714
|
+
? { sensitiveParams: { ...options.sensitiveParams } }
|
|
715
|
+
: {}),
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
type HttpCaptureCallbacks = {
|
|
720
|
+
reserveOrder(): number;
|
|
721
|
+
reserveStreamOrdinal(): number;
|
|
722
|
+
onSensitiveParams(url: string, options?: RequestOptions): void;
|
|
723
|
+
onResponse(order: number, response: HttpResponse): void;
|
|
724
|
+
onStreamResponse(
|
|
725
|
+
order: number,
|
|
726
|
+
ordinal: number,
|
|
727
|
+
requestUrl: string,
|
|
728
|
+
method: string,
|
|
729
|
+
response: HttpStreamResponse,
|
|
730
|
+
): HttpStreamResponse;
|
|
731
|
+
onSseResponse(order: number, requestUrl: string, method: string): void;
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
function captureHttpClient(client: HttpClient, callbacks: HttpCaptureCallbacks): HttpClient {
|
|
735
|
+
const captureResponse = async (
|
|
736
|
+
order: number,
|
|
737
|
+
responsePromise: Promise<HttpResponse>,
|
|
738
|
+
): Promise<HttpResponse> => {
|
|
739
|
+
const response = await responsePromise;
|
|
740
|
+
callbacks.onResponse(order, response);
|
|
741
|
+
return response;
|
|
742
|
+
};
|
|
743
|
+
const captureRequestOptions = (method: PropertyKey, args: unknown[]) => {
|
|
744
|
+
const invocation = parseHttpRequestInvocation(method, args);
|
|
745
|
+
const options = invocation ? requestOptionsFromHttpInvocation(invocation) : undefined;
|
|
746
|
+
if (!invocation || !options) return;
|
|
747
|
+
const snapshot = snapshotRequestOptions(options);
|
|
748
|
+
callbacks.onSensitiveParams(String(args[0]), snapshot);
|
|
749
|
+
replaceRequestOptionsInHttpInvocation(invocation, snapshot);
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
return {
|
|
753
|
+
request: (...args: Parameters<HttpClient["request"]>) => {
|
|
754
|
+
captureRequestOptions("request", args);
|
|
755
|
+
return captureResponse(callbacks.reserveOrder(), client.request(...args));
|
|
756
|
+
},
|
|
757
|
+
get: (...args: Parameters<HttpClient["get"]>) => {
|
|
758
|
+
captureRequestOptions("get", args);
|
|
759
|
+
return captureResponse(callbacks.reserveOrder(), client.get(...args));
|
|
760
|
+
},
|
|
761
|
+
post: (...args: Parameters<HttpClient["post"]>) => {
|
|
762
|
+
captureRequestOptions("post", args);
|
|
763
|
+
return captureResponse(callbacks.reserveOrder(), client.post(...args));
|
|
764
|
+
},
|
|
765
|
+
put: (...args: Parameters<HttpClient["put"]>) => {
|
|
766
|
+
captureRequestOptions("put", args);
|
|
767
|
+
return captureResponse(callbacks.reserveOrder(), client.put(...args));
|
|
768
|
+
},
|
|
769
|
+
delete: (...args: Parameters<HttpClient["delete"]>) => {
|
|
770
|
+
captureRequestOptions("delete", args);
|
|
771
|
+
return captureResponse(callbacks.reserveOrder(), client.delete(...args));
|
|
772
|
+
},
|
|
773
|
+
stream: async (...args: Parameters<HttpClient["stream"]>) => {
|
|
774
|
+
captureRequestOptions("stream", args);
|
|
775
|
+
const order = callbacks.reserveOrder();
|
|
776
|
+
const ordinal = callbacks.reserveStreamOrdinal();
|
|
777
|
+
const method = (args[1]?.method ?? "GET").toUpperCase();
|
|
778
|
+
const response = await client.stream(...args);
|
|
779
|
+
return callbacks.onStreamResponse(order, ordinal, args[0], method, response);
|
|
780
|
+
},
|
|
781
|
+
sse: async (...args: Parameters<HttpClient["sse"]>) => {
|
|
782
|
+
captureRequestOptions("sse", args);
|
|
783
|
+
const order = callbacks.reserveOrder();
|
|
784
|
+
const response = await client.sse(...args);
|
|
785
|
+
callbacks.onSseResponse(order, args[0], (args[1]?.method ?? "GET").toUpperCase());
|
|
786
|
+
return response;
|
|
398
787
|
},
|
|
399
|
-
}
|
|
788
|
+
};
|
|
400
789
|
}
|
|
401
790
|
|
|
402
791
|
type StealthSession = ReturnType<StealthClient["createSession"]>;
|
|
403
792
|
|
|
404
793
|
function proxyStealthClient(
|
|
405
794
|
client: StealthClient,
|
|
406
|
-
|
|
795
|
+
onSensitiveParams: (url: string, options?: SensitiveRequestOptions) => void,
|
|
796
|
+
onResponse: (order: number, response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
|
|
797
|
+
reserveOrder: () => number,
|
|
407
798
|
): StealthClient {
|
|
408
799
|
return {
|
|
409
800
|
fetch: async (...args: Parameters<StealthClient["fetch"]>) => {
|
|
801
|
+
const order = reserveOrder();
|
|
802
|
+
if (args[1]) args[1] = snapshotRequestOptions(args[1]);
|
|
803
|
+
onSensitiveParams(args[0], args[1]);
|
|
410
804
|
const response = await client.fetch(...args);
|
|
411
|
-
|
|
805
|
+
if (response.url) onSensitiveParams(response.url, args[1]);
|
|
806
|
+
onResponse(order, response);
|
|
412
807
|
return response;
|
|
413
808
|
},
|
|
414
809
|
createSession: (...args: Parameters<StealthClient["createSession"]>) =>
|
|
415
|
-
proxyStealthSession(
|
|
810
|
+
proxyStealthSession(
|
|
811
|
+
client.createSession(...args),
|
|
812
|
+
onSensitiveParams,
|
|
813
|
+
onResponse,
|
|
814
|
+
reserveOrder,
|
|
815
|
+
),
|
|
416
816
|
};
|
|
417
817
|
}
|
|
418
818
|
|
|
419
819
|
function proxyStealthSession(
|
|
420
820
|
session: StealthSession,
|
|
421
|
-
|
|
821
|
+
onSensitiveParams: (url: string, options?: SensitiveRequestOptions) => void,
|
|
822
|
+
onResponse: (order: number, response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
|
|
823
|
+
reserveOrder: () => number,
|
|
422
824
|
): StealthSession {
|
|
423
825
|
return {
|
|
424
826
|
fetch: async (...args: Parameters<StealthSession["fetch"]>) => {
|
|
827
|
+
const order = reserveOrder();
|
|
828
|
+
if (args[1]) args[1] = snapshotRequestOptions(args[1]);
|
|
829
|
+
onSensitiveParams(args[0], args[1]);
|
|
425
830
|
const response = await session.fetch(...args);
|
|
426
|
-
|
|
831
|
+
if (response.url) onSensitiveParams(response.url, args[1]);
|
|
832
|
+
onResponse(order, response);
|
|
427
833
|
return response;
|
|
428
834
|
},
|
|
835
|
+
cookies: session.cookies,
|
|
836
|
+
redirects: {
|
|
837
|
+
run: async (...args: Parameters<StealthSession["redirects"]["run"]>) => {
|
|
838
|
+
const order = reserveOrder();
|
|
839
|
+
args[0] = snapshotRequestOptions(args[0]);
|
|
840
|
+
onSensitiveParams(args[0].url, args[0]);
|
|
841
|
+
const callerStopWhen = args[0].stopWhen;
|
|
842
|
+
args[0].stopWhen = async (hop) => {
|
|
843
|
+
for (const url of [hop.url, hop.location, hop.nextUrl]) {
|
|
844
|
+
if (url) onSensitiveParams(url, args[0]);
|
|
845
|
+
}
|
|
846
|
+
return callerStopWhen ? await callerStopWhen(hop) : false;
|
|
847
|
+
};
|
|
848
|
+
const result = await session.redirects.run(...args);
|
|
849
|
+
if (result.final.url) onSensitiveParams(result.final.url, args[0]);
|
|
850
|
+
onResponse(order, result.final);
|
|
851
|
+
return result;
|
|
852
|
+
},
|
|
853
|
+
},
|
|
429
854
|
close: () => session.close(),
|
|
430
855
|
};
|
|
431
856
|
}
|
|
@@ -438,31 +863,193 @@ function normalizeCapturedStealthResponse(response: Awaited<ReturnType<StealthCl
|
|
|
438
863
|
}
|
|
439
864
|
}
|
|
440
865
|
|
|
441
|
-
function
|
|
866
|
+
function redactStreamEvidence(
|
|
867
|
+
evidence: Awaited<ReturnType<StreamEvidenceCapture["getEvidence"]>>,
|
|
868
|
+
sensitiveParams: CapturedSensitiveParams,
|
|
869
|
+
): Awaited<ReturnType<StreamEvidenceCapture["getEvidence"]>> {
|
|
870
|
+
return parseStreamEvidenceRecord(redactFixture(evidence, sensitiveParams, false));
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function sanitizeStreamFixture(value: unknown, sensitiveParams: CapturedSensitiveParams): unknown {
|
|
874
|
+
if (typeof value === "string") {
|
|
875
|
+
if (value !== "" && sensitiveParams.values.includes(value)) return REDACTED_QUERY_VALUE;
|
|
876
|
+
return sanitizeFixtureString(redactFixtureText(value, sensitiveParams));
|
|
877
|
+
}
|
|
442
878
|
if (Array.isArray(value)) {
|
|
443
|
-
return value.map((item) =>
|
|
879
|
+
return value.map((item) => sanitizeStreamFixture(item, sensitiveParams));
|
|
444
880
|
}
|
|
445
|
-
|
|
446
881
|
if (!value || typeof value !== "object") {
|
|
447
|
-
return value;
|
|
882
|
+
return redactFixture(value, sensitiveParams, false);
|
|
448
883
|
}
|
|
449
884
|
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
885
|
+
const result: MutableRecord = Object.create(null) as MutableRecord;
|
|
886
|
+
for (const [key, entryValue] of Object.entries(value as MutableRecord)) {
|
|
887
|
+
const redactedKey = sensitiveParams.values.includes(key)
|
|
888
|
+
? REDACTED_QUERY_VALUE
|
|
889
|
+
: redactFixtureText(key, sensitiveParams);
|
|
890
|
+
const uniqueKey = collisionSafeKey(result, redactedKey);
|
|
891
|
+
result[uniqueKey] =
|
|
892
|
+
isSensitiveFixtureKey(key) && !sensitiveParams.names.includes(key)
|
|
893
|
+
? REDACTED_QUERY_VALUE
|
|
894
|
+
: sanitizeStreamFixture(entryValue, sensitiveParams);
|
|
895
|
+
}
|
|
896
|
+
return result;
|
|
897
|
+
}
|
|
454
898
|
|
|
455
|
-
|
|
456
|
-
|
|
899
|
+
function redactStreamPreview(
|
|
900
|
+
bodyPreviewBase64: string,
|
|
901
|
+
sensitiveParams: CapturedSensitiveParams,
|
|
902
|
+
): { bodyPreviewBase64: string; changed: boolean } {
|
|
903
|
+
const preview = Buffer.from(bodyPreviewBase64, "base64");
|
|
904
|
+
if (preview.byteLength === 0) return { bodyPreviewBase64, changed: false };
|
|
905
|
+
|
|
906
|
+
const text = new TextDecoder().decode(preview);
|
|
907
|
+
let redactedText = redactFixtureText(text, sensitiveParams);
|
|
908
|
+
const trimmed = text.trimEnd();
|
|
909
|
+
try {
|
|
910
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
911
|
+
const structurallyRedacted = JSON.stringify(redactFixture(parsed, sensitiveParams, false));
|
|
912
|
+
if (structurallyRedacted !== JSON.stringify(parsed)) redactedText = structurallyRedacted;
|
|
913
|
+
} catch {
|
|
914
|
+
// Non-JSON previews still use the shared free-text sensitive-value policy.
|
|
915
|
+
}
|
|
916
|
+
if (redactedText === text) return { bodyPreviewBase64, changed: false };
|
|
917
|
+
|
|
918
|
+
const fitted = Buffer.alloc(preview.byteLength, 0x20);
|
|
919
|
+
let redactedBytes = Buffer.from(redactedText);
|
|
920
|
+
if (redactedBytes.byteLength > fitted.byteLength) {
|
|
921
|
+
redactedBytes = Buffer.from(REDACTED_QUERY_VALUE);
|
|
922
|
+
}
|
|
923
|
+
redactedBytes.copy(fitted, 0, 0, Math.min(redactedBytes.byteLength, fitted.byteLength));
|
|
924
|
+
return { bodyPreviewBase64: fitted.toString("base64"), changed: true };
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function redactFixture(
|
|
928
|
+
value: unknown,
|
|
929
|
+
sensitiveParams: CapturedSensitiveParams,
|
|
930
|
+
sanitizeCommonFields: boolean,
|
|
931
|
+
): unknown {
|
|
932
|
+
if (typeof value === "string") {
|
|
933
|
+
if (value !== "" && sensitiveParams.values.includes(value)) return REDACTED_QUERY_VALUE;
|
|
934
|
+
return redactFixtureText(value, sensitiveParams);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
938
|
+
return sensitiveParams.values.includes(String(value)) ? REDACTED_QUERY_VALUE : value;
|
|
939
|
+
}
|
|
457
940
|
|
|
458
|
-
|
|
941
|
+
if (Array.isArray(value)) {
|
|
942
|
+
return value.map((item) => redactFixture(item, sensitiveParams, sanitizeCommonFields));
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (!value || typeof value !== "object") return value;
|
|
946
|
+
|
|
947
|
+
const result: MutableRecord = Object.create(null) as MutableRecord;
|
|
948
|
+
for (const [key, entryValue] of Object.entries(value as MutableRecord)) {
|
|
949
|
+
const redactedKey = sensitiveParams.values.includes(key)
|
|
950
|
+
? REDACTED_QUERY_VALUE
|
|
951
|
+
: redactFixtureText(key, sensitiveParams);
|
|
952
|
+
const uniqueKey = collisionSafeKey(result, redactedKey);
|
|
953
|
+
result[uniqueKey] =
|
|
954
|
+
sanitizeCommonFields &&
|
|
955
|
+
(isSensitiveKey(key) || isSensitiveFixtureKey(key)) &&
|
|
956
|
+
!sensitiveParams.names.includes(key)
|
|
957
|
+
? REDACTED_QUERY_VALUE
|
|
958
|
+
: redactFixture(entryValue, sensitiveParams, sanitizeCommonFields);
|
|
959
|
+
}
|
|
960
|
+
const record = value as MutableRecord;
|
|
961
|
+
if (record.__apifuse_stream__ === true && typeof record.body_preview_base64 === "string") {
|
|
962
|
+
const preview = redactStreamPreview(record.body_preview_base64, sensitiveParams);
|
|
963
|
+
result.body_preview_base64 = preview.bodyPreviewBase64;
|
|
964
|
+
if (preview.changed) result.preview_sanitized = true;
|
|
965
|
+
}
|
|
966
|
+
return result;
|
|
459
967
|
}
|
|
460
968
|
|
|
461
|
-
function
|
|
462
|
-
|
|
969
|
+
function redactFixtureText(text: string, sensitiveParams: CapturedSensitiveParams): string {
|
|
970
|
+
// Shared free-text policy: long values are unconditional substrings; values
|
|
971
|
+
// shorter than four characters require token boundaries. Exact scalar echoes
|
|
972
|
+
// and declared query-key positions are structurally redacted for every length.
|
|
973
|
+
let redacted = redactSensitiveText(text, sensitiveParams.values);
|
|
974
|
+
for (const name of sensitiveParams.names) {
|
|
975
|
+
let componentEncodedName = name;
|
|
976
|
+
try {
|
|
977
|
+
componentEncodedName = encodeURIComponent(name);
|
|
978
|
+
} catch {
|
|
979
|
+
// Lone surrogates remain covered by the raw and form-encoded variants.
|
|
980
|
+
}
|
|
981
|
+
const keyVariants = new Set([
|
|
982
|
+
name,
|
|
983
|
+
componentEncodedName,
|
|
984
|
+
new URLSearchParams({ [name]: "" }).toString().slice(0, -1),
|
|
985
|
+
]);
|
|
986
|
+
for (const key of keyVariants) {
|
|
987
|
+
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
988
|
+
const queryValue = new RegExp(`(^|[?&])(${escapedKey}=)[^&#\\s"]*`, "g");
|
|
989
|
+
redacted = redacted.replace(queryValue, (_match, prefix, assignment) => {
|
|
990
|
+
return `${prefix}${assignment}${REDACTED_QUERY_VALUE}`;
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return redacted;
|
|
463
995
|
}
|
|
464
996
|
|
|
465
|
-
|
|
997
|
+
function discoverSensitiveQueryValues(
|
|
998
|
+
value: unknown,
|
|
999
|
+
sensitiveParams: CapturedSensitiveParams,
|
|
1000
|
+
): CapturedSensitiveParams {
|
|
1001
|
+
const values = new Set(sensitiveParams.values);
|
|
1002
|
+
const absoluteUrl = /\b[A-Za-z][A-Za-z\d+.-]*:\/\/[^\s<>"']+/g;
|
|
1003
|
+
const discoverFromUrl = (url: string) => {
|
|
1004
|
+
for (const discovered of redactUrlQueryParams(url, sensitiveParams.names).sensitiveValues) {
|
|
1005
|
+
if (discovered !== "" && discovered !== REDACTED_QUERY_VALUE) values.add(discovered);
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
const visitText = (text: string) => {
|
|
1009
|
+
if (!/\s/.test(text)) {
|
|
1010
|
+
try {
|
|
1011
|
+
new URL(text, "http://apifuse.invalid");
|
|
1012
|
+
discoverFromUrl(text);
|
|
1013
|
+
return;
|
|
1014
|
+
} catch {
|
|
1015
|
+
// Fall through to extracting absolute URL spans from prose.
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
for (const match of text.matchAll(absoluteUrl)) {
|
|
1019
|
+
try {
|
|
1020
|
+
new URL(match[0]);
|
|
1021
|
+
discoverFromUrl(match[0]);
|
|
1022
|
+
} catch {
|
|
1023
|
+
// Ignore URI-like prose that is not a parseable URL.
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
const visit = (current: unknown): void => {
|
|
1028
|
+
if (typeof current === "string") {
|
|
1029
|
+
visitText(current);
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
if (Array.isArray(current)) {
|
|
1033
|
+
for (const item of current) visit(item);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (!current || typeof current !== "object") return;
|
|
1037
|
+
for (const [key, entryValue] of Object.entries(current as MutableRecord)) {
|
|
1038
|
+
visitText(key);
|
|
1039
|
+
visit(entryValue);
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
visit(value);
|
|
1043
|
+
return { names: sensitiveParams.names, values: [...values] };
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function collisionSafeKey(record: MutableRecord, preferredKey: string): string {
|
|
1047
|
+
if (!(preferredKey in record)) return preferredKey;
|
|
1048
|
+
let suffix = 2;
|
|
1049
|
+
while (`${preferredKey}#${suffix}` in record) suffix += 1;
|
|
1050
|
+
return `${preferredKey}#${suffix}`;
|
|
1051
|
+
}
|
|
1052
|
+
export async function prepareFixturePayload(
|
|
466
1053
|
fixturePath: string,
|
|
467
1054
|
payload: unknown,
|
|
468
1055
|
append: boolean,
|
|
@@ -471,15 +1058,45 @@ async function prepareFixturePayload(
|
|
|
471
1058
|
return payload;
|
|
472
1059
|
}
|
|
473
1060
|
|
|
1061
|
+
let fixtureSource: string;
|
|
474
1062
|
try {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
1063
|
+
fixtureSource = readFileSync(fixturePath, "utf8");
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
throw new Error(
|
|
1066
|
+
`Cannot append to existing fixture "${fixturePath}" because it could not be read: ${
|
|
1067
|
+
error instanceof Error ? error.message : String(error)
|
|
1068
|
+
}. Fix its permissions or delete it, then run apifuse record --append again.`,
|
|
1069
|
+
);
|
|
481
1070
|
}
|
|
482
1071
|
|
|
1072
|
+
let existing: unknown;
|
|
1073
|
+
try {
|
|
1074
|
+
existing = JSON.parse(fixtureSource) as unknown;
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
throw new Error(
|
|
1077
|
+
`Cannot append to corrupt fixture "${fixturePath}" because it is not valid JSON: ${
|
|
1078
|
+
error instanceof Error ? error.message : String(error)
|
|
1079
|
+
}. Fix or delete the fixture, then run apifuse record --append again.`,
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (hasStreamEvidenceMarker(existing)) {
|
|
1084
|
+
const evidence = parseStreamEvidenceRecord(existing);
|
|
1085
|
+
return [createStreamCaptureEnvelope([{ kind: "stream", evidence }]), payload];
|
|
1086
|
+
}
|
|
1087
|
+
if (Array.isArray(existing)) {
|
|
1088
|
+
if (existing.some((item) => hasStreamEvidenceMarker(item))) {
|
|
1089
|
+
const legacyGroup = findStreamCaptureGroup(existing);
|
|
1090
|
+
if (legacyGroup) {
|
|
1091
|
+
const prefix = existing.slice(0, -legacyGroup.items.length);
|
|
1092
|
+
return [...prefix, createStreamCaptureEnvelope(legacyGroup.items), payload];
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return [...existing, payload];
|
|
1096
|
+
}
|
|
1097
|
+
if (existing !== null) {
|
|
1098
|
+
return [existing, payload];
|
|
1099
|
+
}
|
|
483
1100
|
return payload;
|
|
484
1101
|
}
|
|
485
1102
|
|
|
@@ -491,4 +1108,24 @@ function formatBytes(bytes: number): string {
|
|
|
491
1108
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
492
1109
|
}
|
|
493
1110
|
|
|
494
|
-
|
|
1111
|
+
function jsonFixtureValue(value: unknown): JsonValue {
|
|
1112
|
+
const serialized = JSON.stringify(value);
|
|
1113
|
+
if (serialized === undefined) {
|
|
1114
|
+
throw new Error("Captured upstream response is not JSON-serializable.");
|
|
1115
|
+
}
|
|
1116
|
+
return JSON.parse(serialized) as JsonValue;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function unsupportedSseCaptureError(request: {
|
|
1120
|
+
order: number;
|
|
1121
|
+
method: string;
|
|
1122
|
+
path: string;
|
|
1123
|
+
}): Error {
|
|
1124
|
+
return new Error(
|
|
1125
|
+
`apifuse record does not support ctx.http.sse(): method=${request.method} path=${request.path} call=${request.order}.`,
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
if (import.meta.main) {
|
|
1130
|
+
await main();
|
|
1131
|
+
}
|