@apifuse/provider-sdk 2.2.0-beta.11 → 2.2.0-beta.13
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 +238 -0
- package/CHANGELOG.md +14 -0
- package/README.md +44 -2
- package/bin/apifuse-pack-smoke.ts +14 -0
- package/bin/apifuse-pack-types.ts +40 -1
- package/bin/apifuse-record.ts +622 -57
- package/bin/apifuse-submit-check.ts +43 -10
- package/dist/config/loader.d.ts +9 -1
- package/dist/config/loader.js +9 -0
- package/dist/define.d.ts +2 -1
- package/dist/define.js +61 -3
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +15 -0
- package/dist/fixture-sanitization.d.ts +26 -0
- package/dist/fixture-sanitization.js +216 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/provider.d.ts +2 -1
- package/dist/provider.js +1 -0
- package/dist/runtime/http.js +86 -32
- package/dist/runtime/instrumentation.js +295 -9
- package/dist/runtime/native-network.d.ts +53 -0
- package/dist/runtime/native-network.js +477 -0
- package/dist/runtime/proxy-nodemaven.d.ts +14 -0
- package/dist/runtime/proxy-nodemaven.js +20 -2
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/stealth.d.ts +3 -1
- package/dist/runtime/stealth.js +352 -86
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +451 -19
- package/dist/types.d.ts +201 -7
- package/package.json +3 -1
- package/src/config/loader.ts +22 -1
- package/src/define.ts +81 -3
- package/src/errors.ts +15 -0
- package/src/fixture-sanitization.ts +247 -0
- package/src/index.ts +45 -1
- package/src/provider.ts +37 -0
- package/src/runtime/http.ts +144 -38
- package/src/runtime/instrumentation.ts +424 -8
- package/src/runtime/native-network.ts +600 -0
- package/src/runtime/proxy-nodemaven.ts +37 -2
- package/src/runtime/request-options.ts +680 -1
- package/src/runtime/stealth.ts +420 -88
- package/src/server/index.ts +4 -1
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/stream-evidence.ts +988 -0
- package/src/testing/index.ts +9 -1
- package/src/testing/run.ts +608 -12
- package/src/types.ts +235 -7
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
|
|
@@ -362,6 +446,44 @@ Note the asymmetry: the gate treats whitespace-only values as missing, but
|
|
|
362
446
|
`ctx.env.get()` still returns the raw value to handlers — trim at the point of
|
|
363
447
|
use if the upstream is whitespace-sensitive.
|
|
364
448
|
|
|
449
|
+
### Credentials forced into query parameters
|
|
450
|
+
|
|
451
|
+
Prefer an authorization header or request body whenever the upstream supports
|
|
452
|
+
one. When the upstream requires a credential in the URL query (for example
|
|
453
|
+
`serviceKey`, `confmKey`, or `crtfc_key`), use `sensitiveParams`:
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
const response = await ctx.http.get("/openapi/lookup", {
|
|
457
|
+
params: { pageNo: 1, numOfRows: 100 },
|
|
458
|
+
sensitiveParams: {
|
|
459
|
+
serviceKey: ctx.env.get("APIFUSE__PROVIDER__EXAMPLE__SERVICE_KEY")!,
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
`sensitiveParams` is merged into the outgoing query like `params`, while its
|
|
465
|
+
values are redacted from SDK transport errors, traces, and `apifuse record`
|
|
466
|
+
fixtures. Do not put query credentials in `params`, and do not hand-build a URL
|
|
467
|
+
containing a key; those paths cannot declare which query values are secret.
|
|
468
|
+
|
|
469
|
+
#### Residual risks
|
|
470
|
+
|
|
471
|
+
Redaction is unconditional in structural positions (declared query keys and
|
|
472
|
+
exact scalar fixture/error fields) for values of every length. In unstructured
|
|
473
|
+
free text, values of four or more characters are replaced as substrings; shorter
|
|
474
|
+
values are replaced only at token boundaries to avoid corrupting unrelated text
|
|
475
|
+
(for example, a secret `api` must not rewrite `rapid`). The residual risk is that
|
|
476
|
+
a sub-four-character secret embedded directly inside a larger alphanumeric token
|
|
477
|
+
can remain in free text. Prefer a higher-entropy credential, or a header/body
|
|
478
|
+
credential channel, whenever the upstream permits it. An empty
|
|
479
|
+
`sensitiveParams: {}` is treated exactly as if the option were omitted.
|
|
480
|
+
|
|
481
|
+
For `session.redirects.run()`, returned hop URLs are diagnostic metadata and
|
|
482
|
+
therefore keep declared query values and common response-only credential keys
|
|
483
|
+
redacted. If a login flow must consume a rotated credential from `Location`,
|
|
484
|
+
inspect it inside `stopWhen`; that callback receives the real hop while callback
|
|
485
|
+
failures are sanitized before propagation.
|
|
486
|
+
|
|
365
487
|
### Public local debugging checklist
|
|
366
488
|
|
|
367
489
|
- Operation smoke requests use the provider server envelope:
|
|
@@ -517,6 +639,122 @@ const credentialsAuth = defineCredentialsAuth({
|
|
|
517
639
|
`bunx playwright install chromium`, or set
|
|
518
640
|
`APIFUSE__CDP_POOL__URL` for remote browser debugging.
|
|
519
641
|
|
|
642
|
+
### Limiting stealth response bodies
|
|
643
|
+
|
|
644
|
+
Set `maxBodyBytes` on `ctx.stealth.fetch()` or `session.redirects.run()` when an
|
|
645
|
+
upstream response has a known safe maximum. The limit is opt-in and counts
|
|
646
|
+
decoded bytes as impit streams them. It applies to every redirect hop, uses a
|
|
647
|
+
parseable `Content-Length` for an early rejection, and still enforces the limit
|
|
648
|
+
incrementally when the header is absent or inaccurate. Exceeding the limit
|
|
649
|
+
aborts the response and throws a non-retryable `TransportError` with code
|
|
650
|
+
`response_too_large`.
|
|
651
|
+
|
|
652
|
+
Pass the limit to the transport instead of checking `Content-Length` in provider
|
|
653
|
+
code:
|
|
654
|
+
|
|
655
|
+
```ts
|
|
656
|
+
const response = await ctx.stealth.fetch("/api/search", {
|
|
657
|
+
params: { query: input.query },
|
|
658
|
+
maxBodyBytes: 2 * 1024 * 1024,
|
|
659
|
+
})
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### Persisting stealth session cookies
|
|
663
|
+
|
|
664
|
+
Persist `session.cookies.serialize()` as JSON when an authenticated session must
|
|
665
|
+
survive a restart or move to another replica. The returned
|
|
666
|
+
`StealthCookieStoreV1` has an explicit version and retains every cookie together
|
|
667
|
+
with its Domain, Path, Secure, expiry, host-only, and other cookie attributes.
|
|
668
|
+
Restore it with `session.cookies.deserialize()`. Unsupported future versions
|
|
669
|
+
fail explicitly instead of being accepted as a partial cookie jar.
|
|
670
|
+
|
|
671
|
+
Credential values are strings, so stringify the store at the credential
|
|
672
|
+
boundary and parse it when rebuilding the session:
|
|
673
|
+
|
|
674
|
+
```ts
|
|
675
|
+
// After login (including any redirects across sibling hosts):
|
|
676
|
+
const result = await session.redirects.run({ url: loginUrl });
|
|
677
|
+
return {
|
|
678
|
+
credential: {
|
|
679
|
+
cookieStore: JSON.stringify(result.cookieStore),
|
|
680
|
+
},
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// In a later operation or replica:
|
|
684
|
+
const persisted = ctx.credential.get("cookieStore");
|
|
685
|
+
if (persisted) {
|
|
686
|
+
session.cookies.deserialize(JSON.parse(persisted));
|
|
687
|
+
}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
`snapshot()` and `restore()` remain only for backward compatibility with flat
|
|
691
|
+
`Record<string, string>` credentials. `snapshot()` enumerates cookies across all
|
|
692
|
+
hosts and paths, but the flat shape is inherently lossy: duplicate names
|
|
693
|
+
collapse and Domain, Path, Secure, expiry, and host-only attributes cannot be
|
|
694
|
+
represented. `restore()` therefore recreates host-only `Path=/` cookies on the
|
|
695
|
+
session base origin. Do not use the flat form for new persistence code. Cookie
|
|
696
|
+
headers remain origin-filtered: use `toHeader(url)` for a particular request and
|
|
697
|
+
never build a request header from serialized or snapshotted persistence data.
|
|
698
|
+
|
|
699
|
+
### Recording and replaying streaming responses
|
|
700
|
+
|
|
701
|
+
`apifuse record` passes responses returned by `ctx.http.stream()` directly to the operation
|
|
702
|
+
handler while incrementally capturing a bounded preview. If the handler returns or cancels its
|
|
703
|
+
reader before EOF, the recorder drains the retained upstream reader before writing a JSON evidence
|
|
704
|
+
record to `__fixtures__/raw.json`. The record contains the status, success
|
|
705
|
+
flag, `content-type`/`content-length`/`content-disposition` headers when present, the
|
|
706
|
+
full body SHA-256 and byte count, and a base64 preview up to the configured stream preview limit.
|
|
707
|
+
Textual previews are decoded and passed through the fixture sanitizer before base64 encoding.
|
|
708
|
+
Classification uses both the declared content type and the preview bytes, so missing or incorrect
|
|
709
|
+
content-type headers do not bypass sanitization. PEM private-key blocks and long high-entropy
|
|
710
|
+
tokens in otherwise unstructured text are redacted as well. If the full preview is not valid UTF-8,
|
|
711
|
+
the entire lossy-decoded preview is scanned and matching decodable byte windows are sanitized. Only a
|
|
712
|
+
magic-number-confirmed binary preview with no textual-secret pattern anywhere in the preview bypasses
|
|
713
|
+
sanitization; other undecodable data fails closed.
|
|
714
|
+
Sanitized previews carry `preview_sanitized: true`, plus a
|
|
715
|
+
`preview_redaction_reason` when capture had to fail closed. The original hash and byte count always
|
|
716
|
+
describe upstream bytes, not a sanitized preview.
|
|
717
|
+
|
|
718
|
+
Each record includes query-free request provenance (`method`, `path`, and a one-based stream call
|
|
719
|
+
ordinal). Provenance never stores the origin, URL userinfo, query, or fragment. Every retained path
|
|
720
|
+
segment is scrubbed before persistence: credential-key segments, values following those keys,
|
|
721
|
+
known token shapes, and long high-entropy opaque segments become `[REDACTED]`. If an operation
|
|
722
|
+
opens multiple streams, the recorder finalizes every retained reader and
|
|
723
|
+
writes all evidence records in stream call order. Stream invocations use a tagged capture envelope
|
|
724
|
+
whose items distinguish stream evidence from ordinary JSON responses. Evidence-only snapshot replay
|
|
725
|
+
consumes that exact call order and fails immediately when evidence is exhausted or a call kind is
|
|
726
|
+
reordered. When request provenance is present, replay also rejects method or path changes (relative
|
|
727
|
+
URLs are resolved against the recorded path prefix); ordinals remain diagnostic and are not matched.
|
|
728
|
+
Appended fixtures replay a stream envelope only when it is the latest invocation. SSE
|
|
729
|
+
recording remains unsupported and fails explicitly instead of retaining an unrelated earlier
|
|
730
|
+
response.
|
|
731
|
+
|
|
732
|
+
#### Residual risks
|
|
733
|
+
|
|
734
|
+
- Credential-path sanitization decodes each URL path segment once. Double-encoded separators or
|
|
735
|
+
values such as `%252F` are not decoded recursively, so they can conceal a credential-shaped
|
|
736
|
+
segment from the recorder. This single-pass policy keeps path handling deterministic and avoids
|
|
737
|
+
interpreting ambiguous or intentionally layered encodings differently from the upstream. Never
|
|
738
|
+
place credentials in URL paths, and review recorded provenance before committing fixtures.
|
|
739
|
+
- Primitive strings embedded in prose are redacted only when they match the current PEM,
|
|
740
|
+
credential-assignment, known-token, or entropy heuristics. Other secret formats can remain because
|
|
741
|
+
blanket redaction of ordinary strings would destroy useful fixture content and create broad false
|
|
742
|
+
positives. Keep secrets under credential-named structured fields where possible and manually
|
|
743
|
+
inspect sanitized fixture text before committing it.
|
|
744
|
+
|
|
745
|
+
Stream fixture replay in `runStandardTests(..., { snapshot: true })` is evidence-only:
|
|
746
|
+
`ctx.http.stream()` returns a usable stream containing exactly the recorded preview,
|
|
747
|
+
not a fabricated full body. The replay response also carries runtime metadata
|
|
748
|
+
`evidence_only: true`, `body_sha256`, `body_bytes`, and the optional preview sanitization fields
|
|
749
|
+
for assertions about the original capture. Do not assert that the replay body hashes to
|
|
750
|
+
`body_sha256` when `body_bytes`
|
|
751
|
+
exceeds the decoded preview length or `preview_sanitized` is present; use the metadata for
|
|
752
|
+
full-body integrity and limit body-content assertions to the preview.
|
|
753
|
+
|
|
754
|
+
Golden snapshot suites can set `requireSnapshot: true` so a missing committed snapshot fails instead
|
|
755
|
+
of being created implicitly. Regenerate intentional changes with
|
|
756
|
+
`bun test --update-snapshots`; review and commit the resulting `transform.snap.json` file.
|
|
757
|
+
|
|
520
758
|
### Running the pre-submission report
|
|
521
759
|
|
|
522
760
|
```bash
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @apifuse/provider-sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0-beta.13
|
|
4
|
+
|
|
5
|
+
- Release candidate for main commit 75e840d0614aea3b99a1e5cef4f93f8cdccf0507.
|
|
6
|
+
|
|
7
|
+
## 2.2.0-beta.12
|
|
8
|
+
|
|
9
|
+
- Release candidate for main commit c66789c4745c72fc94ad3c10b3e0d7e5ed83fd25.
|
|
10
|
+
|
|
3
11
|
## 2.2.0-beta.11
|
|
4
12
|
|
|
5
13
|
- Release candidate for main commit f6f739bd5265afe714bbace9900edc2695fcf826.
|
|
@@ -74,8 +82,14 @@
|
|
|
74
82
|
|
|
75
83
|
## Unreleased
|
|
76
84
|
|
|
85
|
+
- Add an opt-in native connection idle read timeout with a typed error, independently from TCP/SOCKS/TLS establishment deadlines.
|
|
86
|
+
- Add opt-in `maxBodyBytes` enforcement to stealth fetches and redirect hops, aborting oversized decoded response streams with `response_too_large`.
|
|
87
|
+
- 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.
|
|
88
|
+
- 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.
|
|
89
|
+
- 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.
|
|
77
90
|
- Add `arrayBuffer()` and `bytes()` to `HttpResponse` so `ctx.http` consumers can read binary-safe upstream bodies; internal response handling is now byte-first.
|
|
78
91
|
- Preserve identity-only operation `connectionId` values in `ProviderContext` without requiring credential material.
|
|
92
|
+
- 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.
|
|
79
93
|
|
|
80
94
|
## 2.1.0-beta.15
|
|
81
95
|
|
package/README.md
CHANGED
|
@@ -138,14 +138,56 @@ 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
|
|
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.
|
|
173
|
+
- **Proxy URLs for non-stealth consumers**: use `resolveProxy()` when a
|
|
174
|
+
provider-owned client outside `ctx.stealth` needs the provider's proxy, such
|
|
175
|
+
as a CAPTCHA solver that must use matching egress. Pass the provider proxy
|
|
176
|
+
policy used by the provider and consume the returned `url`; the SDK owns
|
|
177
|
+
vendor selection, allocation, failover, and URL formats. Never call proxy
|
|
178
|
+
allocator APIs or hardcode proxy vendor hostnames in provider code.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { resolveProxy } from "@apifuse/provider-sdk"
|
|
182
|
+
|
|
183
|
+
const resolvedProxy = await resolveProxy({
|
|
184
|
+
proxyPolicy,
|
|
185
|
+
affinityKey: connectionId,
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
if (!resolvedProxy.url) throw new Error("This login requires proxy egress")
|
|
189
|
+
const captchaTask = { proxy: resolvedProxy.url }
|
|
190
|
+
```
|
|
149
191
|
- **Browser providers**: for TypeScript Providers use `runtime: "browser"` plus
|
|
150
192
|
`browser.engine: "playwright-stealth"`; `nodriver` is a Python-runtime path.
|
|
151
193
|
Install local browser assets with `bunx playwright install chromium` when
|
|
@@ -59,6 +59,20 @@ try {
|
|
|
59
59
|
);
|
|
60
60
|
|
|
61
61
|
run("bun", ["install"], consumerDir);
|
|
62
|
+
run(
|
|
63
|
+
"bun",
|
|
64
|
+
[
|
|
65
|
+
"--eval",
|
|
66
|
+
[
|
|
67
|
+
'import { resolveProxy } from "@apifuse/provider-sdk";',
|
|
68
|
+
'if (typeof resolveProxy !== "function") throw new Error("resolveProxy is not exported");',
|
|
69
|
+
'const resolved = await resolveProxy({ proxy: "http://127.0.0.1:8080" });',
|
|
70
|
+
'if (resolved.url !== "http://127.0.0.1:8080") throw new Error("resolveProxy returned the wrong URL");',
|
|
71
|
+
'console.log("packed root resolveProxy export OK");',
|
|
72
|
+
].join("\n"),
|
|
73
|
+
],
|
|
74
|
+
consumerDir,
|
|
75
|
+
);
|
|
62
76
|
|
|
63
77
|
const cliBin = join(consumerDir, "node_modules", ".bin", "apifuse");
|
|
64
78
|
if (!existsSync(cliBin)) {
|
|
@@ -123,8 +123,11 @@ function setUpFixtureConsumer(consumerDir: string, tarballPath: string): void {
|
|
|
123
123
|
writeFileSync(
|
|
124
124
|
join(consumerDir, "consumer.ts"),
|
|
125
125
|
[
|
|
126
|
-
'import { ProviderError, SessionExpiredError, z } from "@apifuse/provider-sdk";',
|
|
126
|
+
'import { ProviderError, resolveProxy, SessionExpiredError, z } 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";',
|
|
127
129
|
'import { defineCredentialsAuth } from "@apifuse/provider-sdk/provider";',
|
|
130
|
+
'import type { NativeNetworkClient as ProviderEntryNativeNetworkClient, ProviderFilesContext as ProviderEntryFilesContext } from "@apifuse/provider-sdk/provider";',
|
|
128
131
|
'import { extractProviderContract } from "@apifuse/provider-sdk/contract";',
|
|
129
132
|
'import { AUTH_TURN_SCHEMA } from "@apifuse/provider-sdk/auth-turn";',
|
|
130
133
|
'import { serve } from "@apifuse/provider-sdk/server";',
|
|
@@ -143,6 +146,25 @@ function setUpFixtureConsumer(consumerDir: string, tarballPath: string): void {
|
|
|
143
146
|
"// Re-exported zod must keep .refine() callback parameter inference.",
|
|
144
147
|
"const refined = z.object({ shopId: z.string() }).refine((value) => value.shopId.length > 0);",
|
|
145
148
|
'const refinedString = z.string().refine((value) => value.startsWith("tabelog:"));',
|
|
149
|
+
'const proxyOptions: ProxyResolutionOptions = { proxyPolicy: { mode: "disabled" } };',
|
|
150
|
+
'const proxyProtocol: ProxyProtocol = "http";',
|
|
151
|
+
"const proxyResult: Promise<ResolvedProxyConfig> = resolveProxy(proxyOptions);",
|
|
152
|
+
'const proxySource: ProxyResolutionSource = "smartproxy-allocator";',
|
|
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" } };',
|
|
146
168
|
"",
|
|
147
169
|
"export const witnesses = {",
|
|
148
170
|
" inheritedName,",
|
|
@@ -152,6 +174,23 @@ function setUpFixtureConsumer(consumerDir: string, tarballPath: string): void {
|
|
|
152
174
|
" sessionExpired,",
|
|
153
175
|
" refined,",
|
|
154
176
|
" refinedString,",
|
|
177
|
+
" proxyResult,",
|
|
178
|
+
" proxyProtocol,",
|
|
179
|
+
" proxySource,",
|
|
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,",
|
|
155
194
|
" defineCredentialsAuth,",
|
|
156
195
|
" extractProviderContract,",
|
|
157
196
|
" AUTH_TURN_SCHEMA,",
|