@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.
Files changed (59) hide show
  1. package/AUTHORING.md +238 -0
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +44 -2
  4. package/bin/apifuse-pack-smoke.ts +14 -0
  5. package/bin/apifuse-pack-types.ts +40 -1
  6. package/bin/apifuse-record.ts +622 -57
  7. package/bin/apifuse-submit-check.ts +43 -10
  8. package/dist/config/loader.d.ts +9 -1
  9. package/dist/config/loader.js +9 -0
  10. package/dist/define.d.ts +2 -1
  11. package/dist/define.js +61 -3
  12. package/dist/errors.d.ts +5 -0
  13. package/dist/errors.js +15 -0
  14. package/dist/fixture-sanitization.d.ts +26 -0
  15. package/dist/fixture-sanitization.js +216 -0
  16. package/dist/index.d.ts +4 -3
  17. package/dist/index.js +2 -1
  18. package/dist/provider.d.ts +2 -1
  19. package/dist/provider.js +1 -0
  20. package/dist/runtime/http.js +86 -32
  21. package/dist/runtime/instrumentation.js +295 -9
  22. package/dist/runtime/native-network.d.ts +53 -0
  23. package/dist/runtime/native-network.js +477 -0
  24. package/dist/runtime/proxy-nodemaven.d.ts +14 -0
  25. package/dist/runtime/proxy-nodemaven.js +20 -2
  26. package/dist/runtime/request-options.d.ts +68 -1
  27. package/dist/runtime/request-options.js +548 -0
  28. package/dist/runtime/stealth.d.ts +3 -1
  29. package/dist/runtime/stealth.js +352 -86
  30. package/dist/server/index.d.ts +1 -1
  31. package/dist/server/index.js +1 -1
  32. package/dist/server/self-test-input-tokens.d.ts +2 -1
  33. package/dist/server/self-test-input-tokens.js +18 -14
  34. package/dist/stream-evidence.d.ts +74 -0
  35. package/dist/stream-evidence.js +785 -0
  36. package/dist/testing/index.d.ts +1 -1
  37. package/dist/testing/index.js +1 -1
  38. package/dist/testing/run.d.ts +32 -2
  39. package/dist/testing/run.js +451 -19
  40. package/dist/types.d.ts +201 -7
  41. package/package.json +3 -1
  42. package/src/config/loader.ts +22 -1
  43. package/src/define.ts +81 -3
  44. package/src/errors.ts +15 -0
  45. package/src/fixture-sanitization.ts +247 -0
  46. package/src/index.ts +45 -1
  47. package/src/provider.ts +37 -0
  48. package/src/runtime/http.ts +144 -38
  49. package/src/runtime/instrumentation.ts +424 -8
  50. package/src/runtime/native-network.ts +600 -0
  51. package/src/runtime/proxy-nodemaven.ts +37 -2
  52. package/src/runtime/request-options.ts +680 -1
  53. package/src/runtime/stealth.ts +420 -88
  54. package/src/server/index.ts +4 -1
  55. package/src/server/self-test-input-tokens.ts +29 -14
  56. package/src/stream-evidence.ts +988 -0
  57. package/src/testing/index.ts +9 -1
  58. package/src/testing/run.ts +608 -12
  59. package/src/types.ts +235 -7
@@ -0,0 +1,247 @@
1
+ import type { JsonValue } from "./contract-json.js";
2
+
3
+ export const REDACTED_FIXTURE_VALUE = "[REDACTED]";
4
+
5
+ const OPAQUE_TOKEN = /^[A-Za-z0-9_+/=.:~-]+$/;
6
+ const OPAQUE_TOKEN_RUN = /[A-Za-z0-9_+/=.:~-]{24,}/g;
7
+ const URL_RUN = /https?:\/\/[^\s"'<>]+/gi;
8
+ const PEM_PRIVATE_KEY =
9
+ /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/g;
10
+
11
+ /** Matches credential field names without treating benign prefixes such as `author` as `auth`. */
12
+ export function isSensitiveFixtureKey(key: string): boolean {
13
+ const normalized = key.replace(/[-_\s]/g, "").toLowerCase();
14
+ const candidates = [normalized, normalized.replace(/(?:value|payload|header)$/, "")];
15
+ return candidates.some(
16
+ (candidate) =>
17
+ /^(?:authorization|authentication|auth|bearer|cookie|credential|password|passwd|privatekey|secret|session|sessionid|token)$/.test(
18
+ candidate,
19
+ ) ||
20
+ /^(?:api|client|service|access|consumer)(?:key|secret|token)$/.test(candidate) ||
21
+ /(?:authorization|credential|password|passwd|privatekey|secret|sessionid|token)$/.test(
22
+ candidate,
23
+ ),
24
+ );
25
+ }
26
+
27
+ /**
28
+ * Returns JSON fixture data with credential-bearing keys and heuristic-confirmed string secrets
29
+ * replaced. Ordinary short prose and identifiers are retained.
30
+ */
31
+ export function sanitizeFixture(value: JsonValue): JsonValue {
32
+ if (Array.isArray(value)) {
33
+ return value.map((item) => sanitizeFixture(item));
34
+ }
35
+
36
+ if (typeof value === "string") return sanitizeFixtureString(value);
37
+ if (value === null || typeof value !== "object") return value;
38
+
39
+ return Object.fromEntries(
40
+ Object.entries(value).map(([key, entryValue]) => [
41
+ key,
42
+ isSensitiveFixtureKey(key) ? REDACTED_FIXTURE_VALUE : sanitizeFixture(entryValue),
43
+ ]),
44
+ );
45
+ }
46
+
47
+ /** Applies the shared credential-key policy to ordinary JSON fixtures. */
48
+ export function sanitizeOrdinaryFixture(value: JsonValue): JsonValue {
49
+ if (Array.isArray(value)) return value.map((item) => sanitizeOrdinaryFixture(item));
50
+ if (value === null || typeof value !== "object") return value;
51
+ return Object.fromEntries(
52
+ Object.entries(value).map(([key, entryValue]) => [
53
+ key,
54
+ isSensitiveFixtureKey(key) ? REDACTED_FIXTURE_VALUE : sanitizeOrdinaryFixture(entryValue),
55
+ ]),
56
+ );
57
+ }
58
+
59
+ /** Sanitizes a primitive fixture string only when textual-secret heuristics match. */
60
+ export function sanitizeFixtureString(value: string): string {
61
+ let sanitized = value.replace(PEM_PRIVATE_KEY, REDACTED_FIXTURE_VALUE);
62
+ const retainedUrls: string[] = [];
63
+ sanitized = sanitized.replace(URL_RUN, (url) => {
64
+ const index =
65
+ retainedUrls.push(isCredentialBearingUrl(url) ? sanitizeUrlForLogs(url) : url) - 1;
66
+ return `APIFUSEURL${index}X`;
67
+ });
68
+ sanitized = redactSensitiveAssignments(sanitized);
69
+ sanitized = sanitized.replace(OPAQUE_TOKEN_RUN, (candidate) =>
70
+ isSensitiveFixtureValue(candidate) ? REDACTED_FIXTURE_VALUE : candidate,
71
+ );
72
+ sanitized = sanitized.replace(
73
+ /APIFUSEURL(\d+)X/g,
74
+ (_match, index: string) => retainedUrls[Number(index)] ?? REDACTED_FIXTURE_VALUE,
75
+ );
76
+ return sanitized;
77
+ }
78
+
79
+ /** True for opaque values that are unsafe to retain in paths or unstructured text. */
80
+ export function isSensitiveFixtureValue(value: string): boolean {
81
+ const candidate = decodePathSegment(value);
82
+ if (/^bot(?:\d{6,}:)?[A-Za-z0-9_-]{16,}$/i.test(candidate)) return true;
83
+ if (/^\d{6,}:[A-Za-z0-9_-]{20,}$/.test(candidate)) return true;
84
+ if (/^(?:gh[opusr]_|sk[-_]|xox[baprs]-)[A-Za-z0-9_-]{16,}$/i.test(candidate)) return true;
85
+ if (!OPAQUE_TOKEN.test(candidate) || candidate.length < 24) return false;
86
+ if (/^[a-f0-9]{32,}$/i.test(candidate)) return true;
87
+ return shannonEntropy(candidate) >= 3.5;
88
+ }
89
+
90
+ /** Sanitizes every path segment and values following a credential-like segment name. */
91
+ export function sanitizePathname(pathname: string): string {
92
+ const segments = pathname.split("/");
93
+ return segments
94
+ .map((segment, index) => {
95
+ if (!segment) return segment;
96
+ const decoded = decodePathSegment(segment);
97
+ const previous = index > 0 ? decodePathSegment(segments[index - 1] as string) : "";
98
+ if (
99
+ isSensitivePathSegment(decoded) ||
100
+ isCredentialPathKey(previous) ||
101
+ isSensitiveFixtureValue(decoded)
102
+ ) {
103
+ return REDACTED_FIXTURE_VALUE;
104
+ }
105
+ return segment;
106
+ })
107
+ .join("/");
108
+ }
109
+
110
+ function isCredentialPathKey(key: string): boolean {
111
+ const finalPathPart = key.split("/").at(-1) ?? "";
112
+ const baseSegment = finalPathPart.split(";", 1)[0] ?? "";
113
+ return isSensitiveFixtureKey(baseSegment.split(/[=:]/, 1)[0] ?? "");
114
+ }
115
+
116
+ function isSensitivePathSegment(segment: string): boolean {
117
+ return segment
118
+ .split(/[;/]/)
119
+ .some((part) => isSensitiveFixtureKey(part.split(/[=:]/, 1)[0] ?? ""));
120
+ }
121
+
122
+ /** Removes userinfo, query values, fragments, and credential-like path segments from log URLs. */
123
+ export function sanitizeUrlForLogs(value: string): string {
124
+ try {
125
+ const parsed = new URL(value, "https://fixture.invalid");
126
+ const queryMarker = parsed.search ? `?${REDACTED_FIXTURE_VALUE}` : "";
127
+ const path = sanitizePathname(parsed.pathname);
128
+ if (parsed.origin === "https://fixture.invalid" && !hasExplicitOrigin(value)) {
129
+ return `${path}${queryMarker}`;
130
+ }
131
+ return `${parsed.origin}${path}${queryMarker}`;
132
+ } catch {
133
+ return sanitizePathname(value.split(/[?#]/, 1)[0]);
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Returns query-free request provenance with each path segment scrubbed for credential-like values.
139
+ * Origins, URL userinfo, query values, and fragments are never persisted in request provenance.
140
+ */
141
+ export function requestPathForFixture(value: string): string {
142
+ try {
143
+ return sanitizePathname(new URL(value, "https://fixture.invalid").pathname);
144
+ } catch {
145
+ const path = value.split(/[?#]/, 1)[0];
146
+ return sanitizePathname(path.startsWith("/") ? path : `/${path}`);
147
+ }
148
+ }
149
+
150
+ /** Scrubs secrets and terminal/log control characters before diagnostic text is emitted. */
151
+ export function sanitizeDiagnosticText(value: string): string {
152
+ let sanitized = value
153
+ .replace(URL_RUN, (url) => sanitizeUrlForLogs(url))
154
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, `Bearer ${REDACTED_FIXTURE_VALUE}`);
155
+ sanitized = redactSensitiveAssignments(sanitized);
156
+ sanitized = sanitized.replace(OPAQUE_TOKEN_RUN, (candidate, offset: number, source: string) => {
157
+ if (/^(?:request|trace|correlation)[-_]?id[:=]/i.test(candidate)) return candidate;
158
+ const prefix = source.slice(Math.max(0, offset - 32), offset);
159
+ if (/(?:request|trace|correlation)[-_]?id\s*[:=]\s*$/i.test(prefix)) return candidate;
160
+ return isSensitiveFixtureValue(candidate) ? REDACTED_FIXTURE_VALUE : candidate;
161
+ });
162
+ return encodeDiagnosticControls(sanitized);
163
+ }
164
+
165
+ function redactSensitiveAssignments(value: string): string {
166
+ return value.replace(
167
+ /((["']?)([\w-]+)\2\s*[:=]\s*)("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;&]+)/gi,
168
+ (match, prefix: string, _quote: string, key: string, assignmentValue: string) => {
169
+ if (!isSensitiveFixtureKey(key) && key.toLowerCase() !== "key") return match;
170
+ const quote = assignmentValue.startsWith('"')
171
+ ? '"'
172
+ : assignmentValue.startsWith("'")
173
+ ? "'"
174
+ : "";
175
+ return `${prefix}${quote}${REDACTED_FIXTURE_VALUE}${quote}`;
176
+ },
177
+ );
178
+ }
179
+
180
+ function isCredentialBearingUrl(value: string): boolean {
181
+ try {
182
+ const parsed = new URL(value);
183
+ return (
184
+ parsed.username !== "" ||
185
+ parsed.password !== "" ||
186
+ parsed.hash !== "" ||
187
+ parsed.search !== "" ||
188
+ parsed.pathname.split("/").some((segment, index, segments) => {
189
+ const decoded = decodePathSegment(segment);
190
+ const previous = decodePathSegment(segments[index - 1] ?? "");
191
+ return (
192
+ isSensitiveFixtureKey(decoded) ||
193
+ isCredentialPathKey(previous) ||
194
+ isSensitiveFixtureValue(decoded)
195
+ );
196
+ })
197
+ );
198
+ } catch {
199
+ return false;
200
+ }
201
+ }
202
+
203
+ function encodeDiagnosticControls(value: string): string {
204
+ let result = "";
205
+ for (const character of value) {
206
+ const code = character.codePointAt(0) ?? 0;
207
+ if (code === 0x0a || code === 0x0d || code === 0x2028 || code === 0x2029) {
208
+ result += " ";
209
+ } else if (
210
+ (code >= 0 && code <= 0x1f) ||
211
+ (code >= 0x7f && code <= 0x9f) ||
212
+ code === 0x061c ||
213
+ code === 0x200e ||
214
+ code === 0x200f ||
215
+ (code >= 0x202a && code <= 0x202e) ||
216
+ (code >= 0x2066 && code <= 0x2069)
217
+ ) {
218
+ result += `\\u${code.toString(16).padStart(4, "0")}`;
219
+ } else {
220
+ result += character;
221
+ }
222
+ }
223
+ return result;
224
+ }
225
+
226
+ function decodePathSegment(value: string): string {
227
+ try {
228
+ return decodeURIComponent(value);
229
+ } catch {
230
+ return value;
231
+ }
232
+ }
233
+
234
+ function hasExplicitOrigin(value: string): boolean {
235
+ return /^[a-z][a-z\d+.-]*:\/\//i.test(value);
236
+ }
237
+
238
+ function shannonEntropy(value: string): number {
239
+ const counts = new Map<string, number>();
240
+ for (const character of value) counts.set(character, (counts.get(character) ?? 0) + 1);
241
+ let entropy = 0;
242
+ for (const count of counts.values()) {
243
+ const probability = count / value.length;
244
+ entropy -= probability * Math.log2(probability);
245
+ }
246
+ return entropy;
247
+ }
package/src/index.ts CHANGED
@@ -7,9 +7,14 @@ export type {
7
7
  ApiFuseConfig,
8
8
  BrowserConfig,
9
9
  ProxyConfig,
10
+ ProxyProtocol,
11
+ ProxyResolutionOptions,
12
+ ProxyResolutionSource,
13
+ ProxyVendorName,
14
+ ResolvedProxyConfig,
10
15
  SessionConfig,
11
16
  } from "./config/loader.js";
12
- export { defineConfig, loadApiFuseConfig } from "./config/loader.js";
17
+ export { defineConfig, loadApiFuseConfig, resolveProxy } from "./config/loader.js";
13
18
  export {
14
19
  canonicalJson,
15
20
  digestProviderContract,
@@ -65,6 +70,20 @@ export {
65
70
  export { createEnvContext } from "./runtime/env.js";
66
71
  export { executeOperation } from "./runtime/executor.js";
67
72
  export { createHttpClient } from "./runtime/http.js";
73
+ export {
74
+ createNativeNetworkClient,
75
+ deriveNativeCredentialAffinityKey,
76
+ NativeIdleTimeoutError,
77
+ NativeNetworkError,
78
+ NativeProxyExpiredError,
79
+ resolveNativeGatewayProxy,
80
+ type NativeGatewayProxy,
81
+ type NativeGatewayProxyResolutionInput,
82
+ type NativeGatewayProxySynthesizer,
83
+ type NativeGatewayProxySynthesisInput,
84
+ type NativeNetworkClientOptions,
85
+ type NativeNetworkErrorCode,
86
+ } from "./runtime/native-network.js";
68
87
  export type { Insight, InsightSeverity } from "./runtime/insights.js";
69
88
  export { generateInsights } from "./runtime/insights.js";
70
89
  export {
@@ -174,6 +193,26 @@ export type {
174
193
  Iso3166Alpha2CountryCode,
175
194
  Iso4217CurrencyCode,
176
195
  Iso8601Duration,
196
+ NativeContext,
197
+ NativeNetworkClient,
198
+ NativeNetworkCloseReason,
199
+ NativeNetworkConnection,
200
+ NativeNetworkConnectInput,
201
+ NativeNetworkConnectOptions,
202
+ NativeNetworkDynamicGrantOptions,
203
+ NativeNetworkEgressGrant,
204
+ NativeProviderConfig,
205
+ NativeProviderContext,
206
+ NativeProxyDrainHandler,
207
+ NativeProxyEgressInfo,
208
+ NativeProxyExpiringEvent,
209
+ NativeProxyExpiringReason,
210
+ NativeTcpDynamicEgressRule,
211
+ NativeTcpEgressGrant,
212
+ NativeTcpEgressRule,
213
+ NativeTcpPortRange,
214
+ NativeTcpTlsMode,
215
+ NativeTlsConnectOptions,
177
216
  OperationAnnotations,
178
217
  OperationApprovalPolicy,
179
218
  OperationContractMetadata,
@@ -208,6 +247,8 @@ export type {
208
247
  ProviderContext,
209
248
  ProviderDefinition,
210
249
  ProviderDeploymentOverrides,
250
+ ProviderFileRef,
251
+ ProviderFilesContext,
211
252
  ProviderHealthMonitorConfig,
212
253
  ProviderHealthProbeConfig,
213
254
  ProviderLocale,
@@ -224,6 +265,7 @@ export type {
224
265
  ProviderPublicConnectionMode,
225
266
  ProviderPublicProfile,
226
267
  ProviderReviewed,
268
+ ProviderResolvedFile,
227
269
  ProviderRuntimeState,
228
270
  ProviderSecretDeclaration,
229
271
  ProviderStateDurationString,
@@ -245,6 +287,8 @@ export type {
245
287
  StateValue,
246
288
  StateWriteOptions,
247
289
  StealthClient,
290
+ StealthCookieStore,
291
+ StealthCookieStoreV1,
248
292
  StealthFetchOptions,
249
293
  StealthPlatform,
250
294
  StealthProfile,
package/src/provider.ts CHANGED
@@ -94,6 +94,26 @@ export type {
94
94
  HttpRetryOptions,
95
95
  HttpRetrySummary,
96
96
  InferSchemaOutput,
97
+ NativeContext,
98
+ NativeNetworkClient,
99
+ NativeNetworkCloseReason,
100
+ NativeNetworkConnection,
101
+ NativeNetworkConnectInput,
102
+ NativeNetworkConnectOptions,
103
+ NativeNetworkDynamicGrantOptions,
104
+ NativeNetworkEgressGrant,
105
+ NativeProviderConfig,
106
+ NativeProviderContext,
107
+ NativeProxyDrainHandler,
108
+ NativeProxyEgressInfo,
109
+ NativeProxyExpiringEvent,
110
+ NativeProxyExpiringReason,
111
+ NativeTcpDynamicEgressRule,
112
+ NativeTcpEgressGrant,
113
+ NativeTcpEgressRule,
114
+ NativeTcpPortRange,
115
+ NativeTcpTlsMode,
116
+ NativeTlsConnectOptions,
97
117
  OperationApprovalPolicy,
98
118
  OperationContractMetadata,
99
119
  OperationDefinition,
@@ -116,6 +136,8 @@ export type {
116
136
  ProviderContext,
117
137
  ProviderDefinition,
118
138
  ProviderDeploymentOverrides,
139
+ ProviderFileRef,
140
+ ProviderFilesContext,
119
141
  ProviderLocale,
120
142
  ProviderLocaleKey,
121
143
  ProviderLocaleKeyInput,
@@ -123,6 +145,7 @@ export type {
123
145
  ProviderProxyPolicy,
124
146
  ProviderPublicConnectionMode,
125
147
  ProviderPublicProfile,
148
+ ProviderResolvedFile,
126
149
  ProviderRuntimeState,
127
150
  ProviderStateDurationString,
128
151
  ProviderStateNamespace,
@@ -135,6 +158,20 @@ export type {
135
158
  StateValue,
136
159
  StateWriteOptions,
137
160
  } from "./types.js";
161
+ export {
162
+ createNativeNetworkClient,
163
+ deriveNativeCredentialAffinityKey,
164
+ NativeIdleTimeoutError,
165
+ NativeNetworkError,
166
+ NativeProxyExpiredError,
167
+ resolveNativeGatewayProxy,
168
+ type NativeGatewayProxy,
169
+ type NativeGatewayProxyResolutionInput,
170
+ type NativeGatewayProxySynthesizer,
171
+ type NativeGatewayProxySynthesisInput,
172
+ type NativeNetworkClientOptions,
173
+ type NativeNetworkErrorCode,
174
+ } from "./runtime/native-network.js";
138
175
  export {
139
176
  HttpRetryAfterPolicy,
140
177
  HttpRetryDelayStrategy,
@@ -28,7 +28,13 @@ import {
28
28
  shouldRetryProxyTransportAttempt,
29
29
  validateUnsafeProxyTransportRetryMethods,
30
30
  } from "./proxy-retry-policy.js";
31
- import { appendQueryParams, normalizeHttpRequestBody } from "./request-options.js";
31
+ import {
32
+ normalizeHttpRequestBody,
33
+ redactSensitiveError,
34
+ redactSensitiveRequestError,
35
+ type SerializedRequestUrl,
36
+ serializeRequestUrl,
37
+ } from "./request-options.js";
32
38
 
33
39
  const DEFAULT_HTTP_BASE_URL = "http://localhost";
34
40
 
@@ -208,9 +214,58 @@ function requireNativeResponseBody(response: Response): ReadableStream<Uint8Arra
208
214
  return response.body;
209
215
  }
210
216
 
211
- function toNativeHttpStreamResponse(response: Response): HttpStreamResponse {
217
+ function sanitizeStreamErrors(
218
+ body: ReadableStream<Uint8Array>,
219
+ serializedUrl: SerializedRequestUrl,
220
+ ): ReadableStream<Uint8Array> {
221
+ if (serializedUrl.sensitiveValues.length === 0) return body;
222
+
223
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
224
+ return new ReadableStream<Uint8Array>(
225
+ {
226
+ async pull(controller) {
227
+ try {
228
+ reader ??= body.getReader();
229
+ const chunk = await reader.read();
230
+ if (chunk.done) {
231
+ controller.close();
232
+ return;
233
+ }
234
+ controller.enqueue(chunk.value);
235
+ } catch (error) {
236
+ controller.error(
237
+ redactSensitiveError(
238
+ error,
239
+ serializedUrl.sensitiveValues,
240
+ serializedUrl.requestUrl,
241
+ serializedUrl.redactedUrl,
242
+ ),
243
+ );
244
+ }
245
+ },
246
+ async cancel(reason) {
247
+ try {
248
+ await (reader ? reader.cancel(reason) : body.cancel(reason));
249
+ } catch (error) {
250
+ throw redactSensitiveError(
251
+ error,
252
+ serializedUrl.sensitiveValues,
253
+ serializedUrl.requestUrl,
254
+ serializedUrl.redactedUrl,
255
+ );
256
+ }
257
+ },
258
+ },
259
+ { highWaterMark: 0 },
260
+ );
261
+ }
262
+
263
+ function toNativeHttpStreamResponse(
264
+ response: Response,
265
+ serializedUrl: SerializedRequestUrl,
266
+ ): HttpStreamResponse {
212
267
  const headers = Object.fromEntries(response.headers.entries());
213
- const body = requireNativeResponseBody(response);
268
+ const body = sanitizeStreamErrors(requireNativeResponseBody(response), serializedUrl);
214
269
  return {
215
270
  body,
216
271
  headers,
@@ -305,6 +360,22 @@ function normalizeNativeFetchBody(body: unknown): string | ArrayBuffer | undefin
305
360
  return copied.buffer;
306
361
  }
307
362
 
363
+ function serializeHttpRequestUrl(
364
+ baseUrl: string | undefined,
365
+ url: string,
366
+ options: RequestOptions,
367
+ ): SerializedRequestUrl {
368
+ try {
369
+ return serializeRequestUrl(
370
+ resolveHttpUrl(baseUrl, url),
371
+ options.params,
372
+ options.sensitiveParams,
373
+ );
374
+ } catch (error) {
375
+ throw redactSensitiveRequestError(error, url, options.sensitiveParams);
376
+ }
377
+ }
378
+
308
379
  async function fetchNativeHttp(
309
380
  baseUrl: string | undefined,
310
381
  url: string,
@@ -316,7 +387,8 @@ async function fetchNativeHttp(
316
387
  proxyAttemptOffset = 0,
317
388
  dedupe?: { attempted: Set<string> },
318
389
  ): Promise<NativeHttpAttemptOutcome> {
319
- const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
390
+ const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
391
+ const { requestUrl } = serializedUrl;
320
392
  const controller = options.timeout ? new AbortController() : undefined;
321
393
  const timeoutHandle = options.timeout
322
394
  ? setTimeout(() => controller?.abort(), options.timeout)
@@ -374,9 +446,19 @@ async function fetchNativeHttp(
374
446
  return toNativeHttpResponse(response);
375
447
  } catch (error) {
376
448
  if (error instanceof SyntaxError) {
377
- throw error;
449
+ throw redactSensitiveError(
450
+ error,
451
+ serializedUrl.sensitiveValues,
452
+ serializedUrl.requestUrl,
453
+ serializedUrl.redactedUrl,
454
+ );
378
455
  }
379
- const transportError = toHttpTransportError(error) as NativeHttpAttemptError;
456
+ const transportError: NativeHttpAttemptError = redactSensitiveError(
457
+ toHttpTransportError(error),
458
+ serializedUrl.sensitiveValues,
459
+ serializedUrl.requestUrl,
460
+ serializedUrl.redactedUrl,
461
+ );
380
462
  transportError.proxyUsed = Boolean(proxy);
381
463
  throw transportError;
382
464
  } finally {
@@ -392,7 +474,8 @@ async function fetchNativeHttpStream(
392
474
  clientOptions: HttpClientOptions,
393
475
  warn: (message: string) => void,
394
476
  ): Promise<HttpStreamResponse> {
395
- const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
477
+ const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
478
+ const { requestUrl } = serializedUrl;
396
479
  const controller = options.timeout ? new AbortController() : undefined;
397
480
  const timeoutHandle = options.timeout
398
481
  ? setTimeout(() => controller?.abort(), options.timeout)
@@ -421,12 +504,22 @@ async function fetchNativeHttpStream(
421
504
  });
422
505
  }
423
506
 
424
- return toNativeHttpStreamResponse(response);
507
+ return toNativeHttpStreamResponse(response, serializedUrl);
425
508
  } catch (error) {
426
509
  if (error instanceof SyntaxError) {
427
- throw error;
510
+ throw redactSensitiveError(
511
+ error,
512
+ serializedUrl.sensitiveValues,
513
+ serializedUrl.requestUrl,
514
+ serializedUrl.redactedUrl,
515
+ );
428
516
  }
429
- throw toHttpTransportError(error);
517
+ throw redactSensitiveError(
518
+ toHttpTransportError(error),
519
+ serializedUrl.sensitiveValues,
520
+ serializedUrl.requestUrl,
521
+ serializedUrl.redactedUrl,
522
+ );
430
523
  } finally {
431
524
  if (timeoutHandle) clearTimeout(timeoutHandle);
432
525
  }
@@ -451,22 +544,29 @@ export function createHttpClient(
451
544
  method: string,
452
545
  options: RequestOptions & { body?: unknown } = {},
453
546
  ): Promise<HttpResponse> {
454
- if (!baseUrl && !isAbsoluteUrl(url)) {
455
- throw new TransportError(
456
- "ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared",
457
- { code: "transport_invalid_url" },
458
- );
459
- }
460
- assertNoHttpTransportOverrides(options);
461
- const headersOptions = withClientHeaders(options, clientOptions, options.body);
462
- const methodName = normalizeHttpMethod(method);
463
- const explicitRetry = headersOptions.retry !== undefined;
464
- const retryOptions =
465
- normalizeProxyTransportRetryOptions(headersOptions.retry, {
466
- label: "HTTP",
467
- }) ??
468
- (explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
469
- if (retryOptions) validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
547
+ const { explicitRetry, headersOptions, methodName, retryOptions } = (() => {
548
+ try {
549
+ if (!baseUrl && !isAbsoluteUrl(url)) {
550
+ throw new TransportError(
551
+ "ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared",
552
+ { code: "transport_invalid_url" },
553
+ );
554
+ }
555
+ assertNoHttpTransportOverrides(options);
556
+ const headersOptions = withClientHeaders(options, clientOptions, options.body);
557
+ const methodName = normalizeHttpMethod(method);
558
+ const explicitRetry = headersOptions.retry !== undefined;
559
+ const retryOptions =
560
+ normalizeProxyTransportRetryOptions(headersOptions.retry, {
561
+ label: "HTTP",
562
+ }) ??
563
+ (explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
564
+ if (retryOptions) validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
565
+ return { explicitRetry, headersOptions, methodName, retryOptions };
566
+ } catch (error) {
567
+ throw redactSensitiveRequestError(error, url, options.sensitiveParams);
568
+ }
569
+ })();
470
570
  const retryEnabled = Boolean(
471
571
  retryOptions &&
472
572
  retryOptions.attempts > 1 &&
@@ -531,9 +631,7 @@ export function createHttpClient(
531
631
  explicitRetry,
532
632
  method: methodName,
533
633
  });
534
- const dedupeContext = dedupeAllocatorEndpoints
535
- ? { attempted: new Set<string>() }
536
- : undefined;
634
+ const dedupeContext = dedupeAllocatorEndpoints ? { attempted: new Set<string>() } : undefined;
537
635
 
538
636
  const executeOnce = (proxyAttemptOffset = 0): Promise<NativeHttpAttemptOutcome> =>
539
637
  fetchNativeHttp(
@@ -654,15 +752,23 @@ export function createHttpClient(
654
752
  method: string,
655
753
  options: RequestOptions & { body?: unknown } = {},
656
754
  ): Promise<HttpStreamResponse> {
657
- if (!baseUrl && !isAbsoluteUrl(url)) {
658
- throw new TransportError(
659
- "ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared",
660
- { code: "transport_invalid_url" },
661
- );
662
- }
663
- assertNoHttpTransportOverrides(options);
664
- const headersOptions = withClientHeaders(options, clientOptions, options.body);
665
- const methodName = normalizeHttpMethod(method);
755
+ const { headersOptions, methodName } = (() => {
756
+ try {
757
+ if (!baseUrl && !isAbsoluteUrl(url)) {
758
+ throw new TransportError(
759
+ "ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared",
760
+ { code: "transport_invalid_url" },
761
+ );
762
+ }
763
+ assertNoHttpTransportOverrides(options);
764
+ return {
765
+ headersOptions: withClientHeaders(options, clientOptions, options.body),
766
+ methodName: normalizeHttpMethod(method),
767
+ };
768
+ } catch (error) {
769
+ throw redactSensitiveRequestError(error, url, options.sensitiveParams);
770
+ }
771
+ })();
666
772
  return fetchNativeHttpStream(baseUrl, url, methodName, headersOptions, clientOptions, warnOnce);
667
773
  }
668
774