@apifuse/provider-sdk 2.2.0-beta.12 → 2.2.0-beta.14

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.
Files changed (65) hide show
  1. package/AUTHORING.md +271 -6
  2. package/CHANGELOG.md +16 -0
  3. package/README.md +26 -2
  4. package/bin/apifuse-pack-types.ts +30 -1
  5. package/bin/apifuse-record.ts +622 -57
  6. package/bin/apifuse-submit-check.ts +43 -10
  7. package/dist/define.d.ts +2 -1
  8. package/dist/define.js +61 -3
  9. package/dist/errors.d.ts +1 -0
  10. package/dist/errors.js +6 -0
  11. package/dist/fixture-sanitization.d.ts +26 -0
  12. package/dist/fixture-sanitization.js +216 -0
  13. package/dist/index.d.ts +3 -2
  14. package/dist/index.js +2 -1
  15. package/dist/provider.d.ts +2 -1
  16. package/dist/provider.js +1 -0
  17. package/dist/runtime/executor.js +17 -2
  18. package/dist/runtime/http.js +86 -32
  19. package/dist/runtime/instrumentation.js +295 -9
  20. package/dist/runtime/native-network.d.ts +53 -0
  21. package/dist/runtime/native-network.js +477 -0
  22. package/dist/runtime/proxy-nodemaven.d.ts +14 -0
  23. package/dist/runtime/proxy-nodemaven.js +20 -2
  24. package/dist/runtime/request-options.d.ts +68 -1
  25. package/dist/runtime/request-options.js +548 -0
  26. package/dist/runtime/stealth.d.ts +3 -1
  27. package/dist/runtime/stealth.js +239 -39
  28. package/dist/server/index.d.ts +2 -2
  29. package/dist/server/index.js +2 -2
  30. package/dist/server/self-test-input-tokens.d.ts +2 -1
  31. package/dist/server/self-test-input-tokens.js +18 -14
  32. package/dist/server/serve.d.ts +9 -0
  33. package/dist/server/serve.js +153 -51
  34. package/dist/server/types.d.ts +3 -0
  35. package/dist/server/types.js +1 -0
  36. package/dist/stateful/stateful-provider-owner-forwarder.js +9 -1
  37. package/dist/stream-evidence.d.ts +74 -0
  38. package/dist/stream-evidence.js +785 -0
  39. package/dist/testing/index.d.ts +1 -1
  40. package/dist/testing/index.js +1 -1
  41. package/dist/testing/run.d.ts +32 -2
  42. package/dist/testing/run.js +451 -19
  43. package/dist/types.d.ts +162 -0
  44. package/package.json +2 -1
  45. package/src/define.ts +81 -3
  46. package/src/errors.ts +9 -0
  47. package/src/fixture-sanitization.ts +247 -0
  48. package/src/index.ts +43 -1
  49. package/src/provider.ts +37 -0
  50. package/src/runtime/executor.ts +22 -2
  51. package/src/runtime/http.ts +144 -38
  52. package/src/runtime/instrumentation.ts +424 -8
  53. package/src/runtime/native-network.ts +600 -0
  54. package/src/runtime/proxy-nodemaven.ts +37 -2
  55. package/src/runtime/request-options.ts +680 -1
  56. package/src/runtime/stealth.ts +293 -40
  57. package/src/server/index.ts +6 -1
  58. package/src/server/self-test-input-tokens.ts +29 -14
  59. package/src/server/serve.ts +190 -68
  60. package/src/server/types.ts +1 -0
  61. package/src/stateful/stateful-provider-owner-forwarder.ts +9 -1
  62. package/src/stream-evidence.ts +988 -0
  63. package/src/testing/index.ts +9 -1
  64. package/src/testing/run.ts +608 -12
  65. package/src/types.ts +194 -0
package/AUTHORING.md CHANGED
@@ -106,6 +106,90 @@ description:
106
106
 
107
107
  Use `defineOperation()` when an operation is large enough to live beside helper functions or in a separate module. It preserves the same type inference as inline `defineProvider()` operations and can be placed directly in the provider `operations` map. `defineProvider()` accepts Zod and Standard Schema v1-compatible schemas. If config validation fails, the SDK names the field to fix, for example `runtime`, `auth.mode`, `operations.<id>.handler`, or `operations.<id>.fixtures.response`.
108
108
 
109
+ ### Replay-safe fixtures
110
+
111
+ Keep public operation schemas strict: date fields should accept absolute dates,
112
+ not relative tokens. Inside `fixtures.request` only, the SDK resolves `+Nd` and
113
+ `+Nd:YYYYMMDD` (1–365 days ahead) before import-time schema validation and
114
+ stores the resolved request in provider metadata. Health-check case inputs use
115
+ the same resolver when a probe runs. The default calendar is **KST**, including
116
+ the 15:00–23:59 UTC window when KST is already on the next day.
117
+
118
+ `fixtures.recordedAt` is the KST `YYYY-MM-DD` date when the response evidence
119
+ was captured. It must be a real, non-future calendar date. Response date fields
120
+ are expected to align with `recordedAt`, not with the newly resolved request;
121
+ this permits stable recorded evidence alongside a replay-safe request.
122
+
123
+ ```ts
124
+ const FlightInput = z.object({
125
+ departureDate: z.string().date(), // public calls remain absolute-date only
126
+ });
127
+
128
+ const searchFlights = {
129
+ input: FlightInput,
130
+ output: FlightOutput,
131
+ async handler(ctx, input) {
132
+ return fetchAndNormalizeFlights(ctx, input);
133
+ },
134
+ fixtures: {
135
+ request: { departureDate: "+45d" },
136
+ response: recordedFlightResponse, // dates reflect the capture below
137
+ recordedAt: "2026-07-15",
138
+ },
139
+ healthCheckUnsupported: { reason: "Upstream search is cost-bearing." },
140
+ };
141
+ ```
142
+
143
+ For code that explicitly calls the shared resolver, omit the third argument to
144
+ use KST or pass `"UTC"` deliberately:
145
+
146
+ ```ts
147
+ import { resolveHealthCheckInputDateTokens } from "@apifuse/provider-sdk/server";
148
+
149
+ const kstInput = resolveHealthCheckInputDateTokens({ date: "+45d" });
150
+ const utcInput = resolveHealthCheckInputDateTokens({ date: "+45d" }, new Date(), "UTC");
151
+ ```
152
+
153
+ Do not re-resolve a health assertion's dates in UTC when its case input used the
154
+ default KST calendar.
155
+
156
+ ### Real-handler E2E in standard tests
157
+
158
+ `runStandardTests(provider)` validates declarations and fixtures but reports a
159
+ per-operation warning because it has no handler E2E coverage. Opt in with an
160
+ `upstreamStub`: the runner calls each fixture-backed real handler with its
161
+ already-resolved fixture request, routes ProviderContext upstream transports to
162
+ the stub, and validates the result against the output schema. It never compares
163
+ the result to the recorded response because that evidence belongs to
164
+ `recordedAt`.
165
+
166
+ ```ts
167
+ import { runStandardTests } from "@apifuse/provider-sdk/testing";
168
+ import provider from "../index.js";
169
+
170
+ runStandardTests(provider, {
171
+ upstreamStub: ({ transport, method, url }) => {
172
+ if (
173
+ transport === "http" &&
174
+ method === "GET" &&
175
+ url === "https://api.example.test/flights"
176
+ ) {
177
+ return Response.json({ flights: [{ id: "fixture-flight" }] });
178
+ }
179
+ return undefined; // fails the test: live-network passthrough is forbidden
180
+ },
181
+ });
182
+ ```
183
+
184
+ The stub also identifies `stealth`, `browser`, and `native` interactions. Return
185
+ a Web `Response` or `{ status, headers, body }`; an unmatched call fails with
186
+ the operation, transport, and method named in the error. Browser handlers expose
187
+ method-level calls such as `goto`, `evaluate`, and `locator.click`, so provide a
188
+ canned result for each method the handler uses. Native connections similarly
189
+ identify `connectTcp`/`connectTls` and subsequent `write` calls. Direct global
190
+ `fetch` or socket usage is outside this ProviderContext seam and should not be
191
+ used by provider handlers.
192
+
109
193
  ### Health assertion context
110
194
 
111
195
  `healthCheck.cases[].assertions` receives a `HealthCheckAssertionContext` with
@@ -309,6 +393,70 @@ External contributors are expected to submit standalone Provider source plus:
309
393
  Maintainers own monorepo import under `providers/<id>/`, registry generation,
310
394
  deployment projection checks, and release workflows.
311
395
 
396
+ ### Error responses
397
+
398
+ Provider-server failures use a stable public envelope:
399
+
400
+ ```json
401
+ {
402
+ "error": {
403
+ "code": "UPSTREAM_ERROR",
404
+ "message": "The upstream service failed",
405
+ "requestId": "req_123",
406
+ "retryable": true,
407
+ "details": { "providerReason": "temporarily_unavailable" }
408
+ }
409
+ }
410
+ ```
411
+
412
+ `retryable` is always present on responses emitted by the current SDK. Set
413
+ `retryable` in the `ProviderError` options when the provider knows the answer;
414
+ an explicit `true` or `false` wins over SDK derivation. When it is omitted, the
415
+ SDK derives the value for its known error classes and otherwise defaults to
416
+ `false`. During stateful rolling upgrades, the forwarding client also accepts
417
+ an older owner response that omits `retryable` and treats it as `false` without
418
+ loosening the emitted response contract. Existing optional `fix` guidance is
419
+ also preserved when a `ProviderError` supplies it.
420
+
421
+ `details` belongs exclusively to the provider. The server passes
422
+ `ProviderError.options.details` through verbatim, including strings and arrays,
423
+ and never merges, overwrites, or wraps it. Do not put SDK taxonomy fields there.
424
+ SDK-owned validation and masked-internal-error paths retain their own diagnostic
425
+ details.
426
+
427
+ SDK observability is emitted separately in the
428
+ `X-ApiFuse-Error-Observability` response header as compact, single-line JSON:
429
+
430
+ ```json
431
+ {"category":"upstream_http","taxonomyVersion":"2026-05-26","retryable":true,"upstreamStatus":502}
432
+ ```
433
+
434
+ Treat this header as telemetry, not as provider-controlled public error detail.
435
+ Its category, taxonomy version, retryability, and optional upstream status match
436
+ the structured `provider_request_failed` log event.
437
+
438
+ Registered error-code mappings take precedence for every `ProviderError`,
439
+ including `ValidationError`:
440
+
441
+ | Error code or fallback | HTTP status |
442
+ | --- | ---: |
443
+ | `AUTH_REQUIRED`, `reauth_required` | 401 |
444
+ | `MISSING_SECRET` | 400 |
445
+ | `NOT_FOUND`, `not_found`, `NO_DATA` | 404 |
446
+ | `RATE_LIMITED`, `UPSTREAM_RATE_LIMIT`, `LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR` | 429 |
447
+ | `UPSTREAM_ERROR`, `BLOCKED` | 502 |
448
+ | `STT_UNAVAILABLE`, `UNSUPPORTED_STT_BACKEND`, `STATEFUL_FORWARDING_REPLAY_CACHE_FULL` | 503 |
449
+ | Unregistered input `ValidationError` code | 400 |
450
+ | Other unregistered `ProviderError` code | 500 |
451
+
452
+ An unregistered non-validation `ProviderError` code returns HTTP 500 and emits
453
+ the greppable `unregistered_provider_error_code` signal with the code in the
454
+ structured failure log. Before publishing a new code, register its status in
455
+ the SDK mapping and add it to the mapping tests; declaring it only in operation
456
+ documentation does not change runtime status selection. The HTTP 400
457
+ `ValidationError` behavior is only the fallback for unregistered input
458
+ validation codes; a registered code such as `NOT_FOUND` retains its mapped 404.
459
+
312
460
  ### Declared secrets are SDK-enforced
313
461
 
314
462
  Environment/secret presence validation is single-sourced in the SDK. Declare
@@ -327,12 +475,12 @@ secrets: [
327
475
  The runtime validates every `required: true` declaration before any operation
328
476
  handler or auth-flow handler (except `abort`) runs. When a required secret is
329
477
  unset or whitespace-only, the invocation fails with the canonical structured
330
- error — code `MISSING_SECRET`, HTTP 400, `details.category:
331
- "credential_unavailable"`, `retryable: false`, and a `fix` naming every missing
332
- secret across `/v1/{operation}`, self-test probes, `apifuse perf`, and
333
- `apifuse record`. The server also emits a `provider_secrets_missing` warn log
334
- at boot so unprovisioned deployments are visible immediately without crashing
335
- the pod.
478
+ error — code `MISSING_SECRET`, HTTP 400, top-level `retryable: false`, and a
479
+ `fix` naming every missing secret — across `/v1/{operation}`, self-test probes,
480
+ `apifuse perf`, and `apifuse record`. Its error-observability header carries the
481
+ `credential_unavailable` category. The server also emits a
482
+ `provider_secrets_missing` warn log at boot so unprovisioned deployments are
483
+ visible immediately without crashing the pod.
336
484
 
337
485
  Provider-local presence re-validation is **deprecated**: do not write
338
486
  `requireServiceKey`/`requireApiKey`-style guards that re-check `ctx.env.get()`
@@ -362,6 +510,44 @@ Note the asymmetry: the gate treats whitespace-only values as missing, but
362
510
  `ctx.env.get()` still returns the raw value to handlers — trim at the point of
363
511
  use if the upstream is whitespace-sensitive.
364
512
 
513
+ ### Credentials forced into query parameters
514
+
515
+ Prefer an authorization header or request body whenever the upstream supports
516
+ one. When the upstream requires a credential in the URL query (for example
517
+ `serviceKey`, `confmKey`, or `crtfc_key`), use `sensitiveParams`:
518
+
519
+ ```ts
520
+ const response = await ctx.http.get("/openapi/lookup", {
521
+ params: { pageNo: 1, numOfRows: 100 },
522
+ sensitiveParams: {
523
+ serviceKey: ctx.env.get("APIFUSE__PROVIDER__EXAMPLE__SERVICE_KEY")!,
524
+ },
525
+ });
526
+ ```
527
+
528
+ `sensitiveParams` is merged into the outgoing query like `params`, while its
529
+ values are redacted from SDK transport errors, traces, and `apifuse record`
530
+ fixtures. Do not put query credentials in `params`, and do not hand-build a URL
531
+ containing a key; those paths cannot declare which query values are secret.
532
+
533
+ #### Residual risks
534
+
535
+ Redaction is unconditional in structural positions (declared query keys and
536
+ exact scalar fixture/error fields) for values of every length. In unstructured
537
+ free text, values of four or more characters are replaced as substrings; shorter
538
+ values are replaced only at token boundaries to avoid corrupting unrelated text
539
+ (for example, a secret `api` must not rewrite `rapid`). The residual risk is that
540
+ a sub-four-character secret embedded directly inside a larger alphanumeric token
541
+ can remain in free text. Prefer a higher-entropy credential, or a header/body
542
+ credential channel, whenever the upstream permits it. An empty
543
+ `sensitiveParams: {}` is treated exactly as if the option were omitted.
544
+
545
+ For `session.redirects.run()`, returned hop URLs are diagnostic metadata and
546
+ therefore keep declared query values and common response-only credential keys
547
+ redacted. If a login flow must consume a rotated credential from `Location`,
548
+ inspect it inside `stopWhen`; that callback receives the real hop while callback
549
+ failures are sanitized before propagation.
550
+
365
551
  ### Public local debugging checklist
366
552
 
367
553
  - Operation smoke requests use the provider server envelope:
@@ -517,6 +703,26 @@ const credentialsAuth = defineCredentialsAuth({
517
703
  `bunx playwright install chromium`, or set
518
704
  `APIFUSE__CDP_POOL__URL` for remote browser debugging.
519
705
 
706
+ ### Limiting stealth response bodies
707
+
708
+ Set `maxBodyBytes` on `ctx.stealth.fetch()` or `session.redirects.run()` when an
709
+ upstream response has a known safe maximum. The limit is opt-in and counts
710
+ decoded bytes as impit streams them. It applies to every redirect hop, uses a
711
+ parseable `Content-Length` for an early rejection, and still enforces the limit
712
+ incrementally when the header is absent or inaccurate. Exceeding the limit
713
+ aborts the response and throws a non-retryable `TransportError` with code
714
+ `response_too_large`.
715
+
716
+ Pass the limit to the transport instead of checking `Content-Length` in provider
717
+ code:
718
+
719
+ ```ts
720
+ const response = await ctx.stealth.fetch("/api/search", {
721
+ params: { query: input.query },
722
+ maxBodyBytes: 2 * 1024 * 1024,
723
+ })
724
+ ```
725
+
520
726
  ### Persisting stealth session cookies
521
727
 
522
728
  Persist `session.cookies.serialize()` as JSON when an authenticated session must
@@ -554,6 +760,65 @@ session base origin. Do not use the flat form for new persistence code. Cookie
554
760
  headers remain origin-filtered: use `toHeader(url)` for a particular request and
555
761
  never build a request header from serialized or snapshotted persistence data.
556
762
 
763
+ ### Recording and replaying streaming responses
764
+
765
+ `apifuse record` passes responses returned by `ctx.http.stream()` directly to the operation
766
+ handler while incrementally capturing a bounded preview. If the handler returns or cancels its
767
+ reader before EOF, the recorder drains the retained upstream reader before writing a JSON evidence
768
+ record to `__fixtures__/raw.json`. The record contains the status, success
769
+ flag, `content-type`/`content-length`/`content-disposition` headers when present, the
770
+ full body SHA-256 and byte count, and a base64 preview up to the configured stream preview limit.
771
+ Textual previews are decoded and passed through the fixture sanitizer before base64 encoding.
772
+ Classification uses both the declared content type and the preview bytes, so missing or incorrect
773
+ content-type headers do not bypass sanitization. PEM private-key blocks and long high-entropy
774
+ tokens in otherwise unstructured text are redacted as well. If the full preview is not valid UTF-8,
775
+ the entire lossy-decoded preview is scanned and matching decodable byte windows are sanitized. Only a
776
+ magic-number-confirmed binary preview with no textual-secret pattern anywhere in the preview bypasses
777
+ sanitization; other undecodable data fails closed.
778
+ Sanitized previews carry `preview_sanitized: true`, plus a
779
+ `preview_redaction_reason` when capture had to fail closed. The original hash and byte count always
780
+ describe upstream bytes, not a sanitized preview.
781
+
782
+ Each record includes query-free request provenance (`method`, `path`, and a one-based stream call
783
+ ordinal). Provenance never stores the origin, URL userinfo, query, or fragment. Every retained path
784
+ segment is scrubbed before persistence: credential-key segments, values following those keys,
785
+ known token shapes, and long high-entropy opaque segments become `[REDACTED]`. If an operation
786
+ opens multiple streams, the recorder finalizes every retained reader and
787
+ writes all evidence records in stream call order. Stream invocations use a tagged capture envelope
788
+ whose items distinguish stream evidence from ordinary JSON responses. Evidence-only snapshot replay
789
+ consumes that exact call order and fails immediately when evidence is exhausted or a call kind is
790
+ reordered. When request provenance is present, replay also rejects method or path changes (relative
791
+ URLs are resolved against the recorded path prefix); ordinals remain diagnostic and are not matched.
792
+ Appended fixtures replay a stream envelope only when it is the latest invocation. SSE
793
+ recording remains unsupported and fails explicitly instead of retaining an unrelated earlier
794
+ response.
795
+
796
+ #### Residual risks
797
+
798
+ - Credential-path sanitization decodes each URL path segment once. Double-encoded separators or
799
+ values such as `%252F` are not decoded recursively, so they can conceal a credential-shaped
800
+ segment from the recorder. This single-pass policy keeps path handling deterministic and avoids
801
+ interpreting ambiguous or intentionally layered encodings differently from the upstream. Never
802
+ place credentials in URL paths, and review recorded provenance before committing fixtures.
803
+ - Primitive strings embedded in prose are redacted only when they match the current PEM,
804
+ credential-assignment, known-token, or entropy heuristics. Other secret formats can remain because
805
+ blanket redaction of ordinary strings would destroy useful fixture content and create broad false
806
+ positives. Keep secrets under credential-named structured fields where possible and manually
807
+ inspect sanitized fixture text before committing it.
808
+
809
+ Stream fixture replay in `runStandardTests(..., { snapshot: true })` is evidence-only:
810
+ `ctx.http.stream()` returns a usable stream containing exactly the recorded preview,
811
+ not a fabricated full body. The replay response also carries runtime metadata
812
+ `evidence_only: true`, `body_sha256`, `body_bytes`, and the optional preview sanitization fields
813
+ for assertions about the original capture. Do not assert that the replay body hashes to
814
+ `body_sha256` when `body_bytes`
815
+ exceeds the decoded preview length or `preview_sanitized` is present; use the metadata for
816
+ full-body integrity and limit body-content assertions to the preview.
817
+
818
+ Golden snapshot suites can set `requireSnapshot: true` so a missing committed snapshot fails instead
819
+ of being created implicitly. Regenerate intentional changes with
820
+ `bun test --update-snapshots`; review and commit the resulting `transform.snap.json` file.
821
+
557
822
  ### Running the pre-submission report
558
823
 
559
824
  ```bash
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.2.0-beta.14
4
+
5
+ - Release candidate for main commit 3491acd253ca17b517985e8a618f1c2904a664a9.
6
+
7
+ ## 2.2.0-beta.13
8
+
9
+ - Release candidate for main commit 75e840d0614aea3b99a1e5cef4f93f8cdccf0507.
10
+
3
11
  ## 2.2.0-beta.12
4
12
 
5
13
  - Release candidate for main commit c66789c4745c72fc94ad3c10b3e0d7e5ed83fd25.
@@ -78,8 +86,16 @@
78
86
 
79
87
  ## Unreleased
80
88
 
89
+ - **Breaking:** Provider error `details` is now passed through verbatim; SDK observability fields (`category`, `taxonomyVersion`, `upstreamStatus`, and derived `retryable`) are no longer merged into the public body. Emitted error envelopes now require top-level `retryable`, while inbound stateful forwarding tolerates an older owner response that omits it and defaults it to `false`. The removed observability metadata is available in the new `X-ApiFuse-Error-Observability` response header.
90
+ - Unregistered `ProviderError` codes now default to HTTP 500 instead of 400 and emit an `unregistered_provider_error_code` structured-log signal; registered mappings remain unchanged and take precedence over the HTTP 400 fallback for unregistered input `ValidationError` codes.
91
+ - Add an opt-in native connection idle read timeout with a typed error, independently from TCP/SOCKS/TLS establishment deadlines.
92
+ - Add opt-in `maxBodyBytes` enforcement to stealth fetches and redirect hops, aborting oversized decoded response streams with `response_too_large`.
93
+ - Resolve relative date tokens in fixture requests before input-schema validation, add KST capture-date `fixtures.recordedAt` metadata, and support explicit KST/UTC calendars in the shared health-input resolver.
94
+ - Add opt-in `runStandardTests(provider, { upstreamStub })` real-handler E2E coverage with strict offline transport stubs, output-schema validation, and per-operation warnings when handler E2E is not enabled.
95
+ - Export native-network and request-file TypeScript contracts from the package root and `./provider`, including typed native provider declarations and optional runtime capabilities on provider/auth contexts.
81
96
  - Add `arrayBuffer()` and `bytes()` to `HttpResponse` so `ctx.http` consumers can read binary-safe upstream bodies; internal response handling is now byte-first.
82
97
  - Preserve identity-only operation `connectionId` values in `ProviderContext` without requiring credential material.
98
+ - Accept and validate `proxy.session.drainLeadSeconds` in `defineProvider`, so providers can actually declare the native sticky-expiry drain lead time the type surface already exposed; a non-positive value, or one that meets or exceeds the sticky lifetime, is rejected at define time.
83
99
 
84
100
  ## 2.1.0-beta.15
85
101
 
package/README.md CHANGED
@@ -138,14 +138,38 @@ the bad request path; provider/runtime failures include `code`, `message`, and
138
138
  - **Stealth-sensitive providers**: use `ctx.http` for normal JSON/REST calls and
139
139
  `ctx.stealth.fetch()` when you need browser-like session or cookie control.
140
140
  `ctx.stealth.fetch()` uses the impit-backed browser stealth transport and
141
- accepts request controls for `params`, `proxy`, `timeout`, `profile`,
142
- `redirect`, `throwOnHttpError`, and `stealth.insecureSkipVerify`. For login
141
+ accepts request controls for `params`, `sensitiveParams`, `proxy`, `timeout`, `profile`,
142
+ `maxBodyBytes`, `redirect`, `throwOnHttpError`, and
143
+ `stealth.insecureSkipVerify`. For login
143
144
  flows that must inspect intermediate `Location`/`Set-Cookie` headers, create
144
145
  a session with `ctx.stealth.createSession()` and use `session.redirects.run()`;
145
146
  inspect accumulated cookies through `session.cookies`. Select an SDK stealth
146
147
  `profile` such as `chrome-146`; do not tune JA3, HTTP/2 SETTINGS, or
147
148
  pseudo-header order in provider code. Chrome/Firefox-style profiles are
148
149
  supported; use `ctx.browser` when Safari-specific behavior is required.
150
+ - **Query-parameter credentials**: when an upstream requires a credential in
151
+ its URL query, pass it through `sensitiveParams`, not `params` and never a
152
+ hand-built URL. It is sent as a normal query parameter while the SDK redacts
153
+ its value from transport errors, traces, and recorded fixtures:
154
+
155
+ ```ts
156
+ const response = await ctx.http.get("/openapi/service", {
157
+ params: { page: 1 },
158
+ sensitiveParams: { serviceKey: ctx.env.get("APIFUSE__PROVIDER__EXAMPLE__API_KEY")! },
159
+ });
160
+ ```
161
+
162
+ Use this only when the upstream offers no header or body credential channel.
163
+ Declared query-key positions and exact scalar diagnostics are always redacted.
164
+ Free-text values of four or more characters are redacted as substrings; shorter
165
+ values require token boundaries so low-entropy values do not corrupt unrelated
166
+ words or timestamps. Consequently, a sub-four-character secret embedded in a
167
+ larger alphanumeric token can remain in free text; prefer higher-entropy or
168
+ non-query credentials when possible. `sensitiveParams: {}` is equivalent to
169
+ omitting the option. Redirect results structurally redact declared keys and
170
+ common response-only credential keys from hop URLs; use
171
+ `redirects.run({ stopWhen })` to inspect a real intermediate `Location` during
172
+ the run when a login flow needs a rotated value.
149
173
  - **Proxy URLs for non-stealth consumers**: use `resolveProxy()` when a
150
174
  provider-owned client outside `ctx.stealth` needs the provider's proxy, such
151
175
  as a CAPTCHA solver that must use matching egress. Pass the provider proxy
@@ -124,8 +124,10 @@ function setUpFixtureConsumer(consumerDir: string, tarballPath: string): void {
124
124
  join(consumerDir, "consumer.ts"),
125
125
  [
126
126
  'import { ProviderError, resolveProxy, SessionExpiredError, z } from "@apifuse/provider-sdk";',
127
- 'import type { ProxyProtocol, ProxyResolutionOptions, ProxyResolutionSource, ProxyVendorName, ResolvedProxyConfig } from "@apifuse/provider-sdk";',
127
+ 'import type { NativeNetworkClient, NativeNetworkConnection, NativeProviderConfig, NativeProviderContext, NativeTcpEgressGrant, ProviderContext, ProviderFileRef, ProviderFilesContext, ProviderResolvedFile } from "@apifuse/provider-sdk";',
128
+ 'import type { ProxyProtocol, ProxyResolutionOptions, ProxyResolutionSource, ProxyVendorName, RequestOptions, ResolvedProxyConfig } from "@apifuse/provider-sdk";',
128
129
  'import { defineCredentialsAuth } from "@apifuse/provider-sdk/provider";',
130
+ 'import type { NativeNetworkClient as ProviderEntryNativeNetworkClient, ProviderFilesContext as ProviderEntryFilesContext } from "@apifuse/provider-sdk/provider";',
129
131
  'import { extractProviderContract } from "@apifuse/provider-sdk/contract";',
130
132
  'import { AUTH_TURN_SCHEMA } from "@apifuse/provider-sdk/auth-turn";',
131
133
  'import { serve } from "@apifuse/provider-sdk/server";',
@@ -149,6 +151,20 @@ function setUpFixtureConsumer(consumerDir: string, tarballPath: string): void {
149
151
  "const proxyResult: Promise<ResolvedProxyConfig> = resolveProxy(proxyOptions);",
150
152
  'const proxySource: ProxyResolutionSource = "smartproxy-allocator";',
151
153
  'const proxyVendor: ProxyVendorName = "smartproxy";',
154
+ 'const requestFile: ProviderFileRef = { type: "request_file", id: "photo", filename: "photo.jpg", mime_type: "image/jpeg", size: 4 };',
155
+ 'const resolvedFile: ProviderResolvedFile = { type: "request_file", id: requestFile.id, filename: requestFile.filename, size: requestFile.size, sha256: requestFile.sha256, mimeType: requestFile.mime_type, arrayBuffer: async () => new ArrayBuffer(0), bytes: async () => new Uint8Array(), stream: () => new ReadableStream<Uint8Array>() };',
156
+ "const files: ProviderFilesContext = { has: () => true, resolve: async () => resolvedFile };",
157
+ "const providerEntryFiles: ProviderEntryFilesContext = files;",
158
+ "const connection: NativeNetworkConnection = { read: async () => null, write: async () => {}, close: async () => {} };",
159
+ "const network: NativeNetworkClient = { connectTcp: async () => connection, connectTls: async () => connection, grantTcpEgress: () => ({ revoke() {} }) };",
160
+ "const providerEntryNetwork: ProviderEntryNativeNetworkClient = network;",
161
+ "const nativeContext: NativeProviderContext = { network };",
162
+ 'const grant: NativeTcpEgressGrant = network.grantTcpEgress({ sourceHost: "booking-loco.kakao.com", sourcePort: 443, host: "loco.kakao.com", port: 5228, tls: "disabled" });',
163
+ 'const nativeConfig: NativeProviderConfig = { network: { tcp: [{ host: "booking-loco.kakao.com", ports: [443], tls: "required" }] } };',
164
+ "const providerContext = undefined as unknown as ProviderContext;",
165
+ "const optionalFiles: ProviderFilesContext | undefined = providerContext.files;",
166
+ "const optionalNative: NativeProviderContext | undefined = providerContext.native;",
167
+ 'const queryCredentialOptions: RequestOptions = { sensitiveParams: { serviceKey: "type-test-key" } };',
152
168
  "",
153
169
  "export const witnesses = {",
154
170
  " inheritedName,",
@@ -162,6 +178,19 @@ function setUpFixtureConsumer(consumerDir: string, tarballPath: string): void {
162
178
  " proxyProtocol,",
163
179
  " proxySource,",
164
180
  " proxyVendor,",
181
+ " requestFile,",
182
+ " resolvedFile,",
183
+ " files,",
184
+ " providerEntryFiles,",
185
+ " connection,",
186
+ " network,",
187
+ " providerEntryNetwork,",
188
+ " nativeContext,",
189
+ " grant,",
190
+ " nativeConfig,",
191
+ " optionalFiles,",
192
+ " optionalNative,",
193
+ " queryCredentialOptions,",
165
194
  " defineCredentialsAuth,",
166
195
  " extractProviderContract,",
167
196
  " AUTH_TURN_SCHEMA,",