@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
@@ -2,7 +2,7 @@ import { policyRotatesTransportVendorChain, resolvePolicyTransportAttemptCap, re
2
2
  import { ProviderError, TransportError } from "../errors.js";
3
3
  import { parseSseStream, readableBytes, readableLines, readableTextChunks } from "../stream.js";
4
4
  import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, isProxyTransportRetryMethod, normalizeProxyTransportRetryOptions, proxyTransportRetryErrorCode, proxyTransportRetryErrorStatus, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy.js";
5
- import { appendQueryParams, normalizeHttpRequestBody } from "./request-options.js";
5
+ import { normalizeHttpRequestBody, redactSensitiveError, redactSensitiveRequestError, serializeRequestUrl, } from "./request-options.js";
6
6
  const DEFAULT_HTTP_BASE_URL = "http://localhost";
7
7
  function isHttpStatusOutcome(outcome) {
8
8
  return "kind" in outcome && outcome.kind === "http-status";
@@ -124,9 +124,38 @@ function requireNativeResponseBody(response) {
124
124
  }
125
125
  return response.body;
126
126
  }
127
- function toNativeHttpStreamResponse(response) {
127
+ function sanitizeStreamErrors(body, serializedUrl) {
128
+ if (serializedUrl.sensitiveValues.length === 0)
129
+ return body;
130
+ let reader;
131
+ return new ReadableStream({
132
+ async pull(controller) {
133
+ try {
134
+ reader ??= body.getReader();
135
+ const chunk = await reader.read();
136
+ if (chunk.done) {
137
+ controller.close();
138
+ return;
139
+ }
140
+ controller.enqueue(chunk.value);
141
+ }
142
+ catch (error) {
143
+ controller.error(redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl));
144
+ }
145
+ },
146
+ async cancel(reason) {
147
+ try {
148
+ await (reader ? reader.cancel(reason) : body.cancel(reason));
149
+ }
150
+ catch (error) {
151
+ throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
152
+ }
153
+ },
154
+ }, { highWaterMark: 0 });
155
+ }
156
+ function toNativeHttpStreamResponse(response, serializedUrl) {
128
157
  const headers = Object.fromEntries(response.headers.entries());
129
- const body = requireNativeResponseBody(response);
158
+ const body = sanitizeStreamErrors(requireNativeResponseBody(response), serializedUrl);
130
159
  return {
131
160
  body,
132
161
  headers,
@@ -202,8 +231,17 @@ function normalizeNativeFetchBody(body) {
202
231
  copied.set(normalized);
203
232
  return copied.buffer;
204
233
  }
234
+ function serializeHttpRequestUrl(baseUrl, url, options) {
235
+ try {
236
+ return serializeRequestUrl(resolveHttpUrl(baseUrl, url), options.params, options.sensitiveParams);
237
+ }
238
+ catch (error) {
239
+ throw redactSensitiveRequestError(error, url, options.sensitiveParams);
240
+ }
241
+ }
205
242
  async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0, dedupe) {
206
- const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
243
+ const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
244
+ const { requestUrl } = serializedUrl;
207
245
  const controller = options.timeout ? new AbortController() : undefined;
208
246
  const timeoutHandle = options.timeout
209
247
  ? setTimeout(() => controller?.abort(), options.timeout)
@@ -258,9 +296,9 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
258
296
  }
259
297
  catch (error) {
260
298
  if (error instanceof SyntaxError) {
261
- throw error;
299
+ throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
262
300
  }
263
- const transportError = toHttpTransportError(error);
301
+ const transportError = redactSensitiveError(toHttpTransportError(error), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
264
302
  transportError.proxyUsed = Boolean(proxy);
265
303
  throw transportError;
266
304
  }
@@ -270,7 +308,8 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
270
308
  }
271
309
  }
272
310
  async function fetchNativeHttpStream(baseUrl, url, method, options, clientOptions, warn) {
273
- const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
311
+ const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
312
+ const { requestUrl } = serializedUrl;
274
313
  const controller = options.timeout ? new AbortController() : undefined;
275
314
  const timeoutHandle = options.timeout
276
315
  ? setTimeout(() => controller?.abort(), options.timeout)
@@ -296,13 +335,13 @@ async function fetchNativeHttpStream(baseUrl, url, method, options, clientOption
296
335
  status: response.status,
297
336
  });
298
337
  }
299
- return toNativeHttpStreamResponse(response);
338
+ return toNativeHttpStreamResponse(response, serializedUrl);
300
339
  }
301
340
  catch (error) {
302
341
  if (error instanceof SyntaxError) {
303
- throw error;
342
+ throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
304
343
  }
305
- throw toHttpTransportError(error);
344
+ throw redactSensitiveError(toHttpTransportError(error), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
306
345
  }
307
346
  finally {
308
347
  if (timeoutHandle)
@@ -320,19 +359,27 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
320
359
  warn(message);
321
360
  };
322
361
  async function request(url, method, options = {}) {
323
- if (!baseUrl && !isAbsoluteUrl(url)) {
324
- throw new TransportError("ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared", { code: "transport_invalid_url" });
325
- }
326
- assertNoHttpTransportOverrides(options);
327
- const headersOptions = withClientHeaders(options, clientOptions, options.body);
328
- const methodName = normalizeHttpMethod(method);
329
- const explicitRetry = headersOptions.retry !== undefined;
330
- const retryOptions = normalizeProxyTransportRetryOptions(headersOptions.retry, {
331
- label: "HTTP",
332
- }) ??
333
- (explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
334
- if (retryOptions)
335
- validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
362
+ const { explicitRetry, headersOptions, methodName, retryOptions } = (() => {
363
+ try {
364
+ if (!baseUrl && !isAbsoluteUrl(url)) {
365
+ throw new TransportError("ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared", { code: "transport_invalid_url" });
366
+ }
367
+ assertNoHttpTransportOverrides(options);
368
+ const headersOptions = withClientHeaders(options, clientOptions, options.body);
369
+ const methodName = normalizeHttpMethod(method);
370
+ const explicitRetry = headersOptions.retry !== undefined;
371
+ const retryOptions = normalizeProxyTransportRetryOptions(headersOptions.retry, {
372
+ label: "HTTP",
373
+ }) ??
374
+ (explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
375
+ if (retryOptions)
376
+ validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
377
+ return { explicitRetry, headersOptions, methodName, retryOptions };
378
+ }
379
+ catch (error) {
380
+ throw redactSensitiveRequestError(error, url, options.sensitiveParams);
381
+ }
382
+ })();
336
383
  const retryEnabled = Boolean(retryOptions &&
337
384
  retryOptions.attempts > 1 &&
338
385
  isProxyTransportRetryMethod(methodName, retryOptions));
@@ -391,9 +438,7 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
391
438
  explicitRetry,
392
439
  method: methodName,
393
440
  });
394
- const dedupeContext = dedupeAllocatorEndpoints
395
- ? { attempted: new Set() }
396
- : undefined;
441
+ const dedupeContext = dedupeAllocatorEndpoints ? { attempted: new Set() } : undefined;
397
442
  const executeOnce = (proxyAttemptOffset = 0) => fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined, proxyAttemptOffset, dedupeContext);
398
443
  if (!retryEnabled || !retryOptions) {
399
444
  const outcome = await executeOnce();
@@ -492,12 +537,21 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
492
537
  });
493
538
  }
494
539
  async function streamRequest(url, method, options = {}) {
495
- if (!baseUrl && !isAbsoluteUrl(url)) {
496
- throw new TransportError("ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared", { code: "transport_invalid_url" });
497
- }
498
- assertNoHttpTransportOverrides(options);
499
- const headersOptions = withClientHeaders(options, clientOptions, options.body);
500
- const methodName = normalizeHttpMethod(method);
540
+ const { headersOptions, methodName } = (() => {
541
+ try {
542
+ if (!baseUrl && !isAbsoluteUrl(url)) {
543
+ throw new TransportError("ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared", { code: "transport_invalid_url" });
544
+ }
545
+ assertNoHttpTransportOverrides(options);
546
+ return {
547
+ headersOptions: withClientHeaders(options, clientOptions, options.body),
548
+ methodName: normalizeHttpMethod(method),
549
+ };
550
+ }
551
+ catch (error) {
552
+ throw redactSensitiveRequestError(error, url, options.sensitiveParams);
553
+ }
554
+ })();
501
555
  return fetchNativeHttpStream(baseUrl, url, methodName, headersOptions, clientOptions, warnOnce);
502
556
  }
503
557
  return {
@@ -1,5 +1,8 @@
1
+ import { readableBytes, readableLines, readableTextChunks } from "../stream.js";
2
+ import { parseHttpRequestInvocation, isSensitiveKey, redactSensitiveError, redactSensitiveText, redactUrlQueryParams, requestOptionsFromHttpInvocation, serializeRequestUrl, } from "./request-options.js";
1
3
  import { createTraceContext, getTraceRecorder, } from "./trace.js";
2
4
  const BROWSER_PAGE_METHODS = new Set(["goto", "fill", "click", "type", "waitForSelector"]);
5
+ const DIAGNOSTIC_BASE_URL = "http://apifuse-instrumentation.invalid";
3
6
  function isThenable(value) {
4
7
  return ((typeof value === "object" || typeof value === "function") &&
5
8
  value !== null &&
@@ -51,8 +54,173 @@ function getUrl(namespace, args, result) {
51
54
  }
52
55
  return undefined;
53
56
  }
57
+ function requestOptionsForInvocation(namespace, methodName, args) {
58
+ if (namespace === "http") {
59
+ const invocation = parseHttpRequestInvocation(methodName, [...args]);
60
+ return invocation ? requestOptionsFromHttpInvocation(invocation) : undefined;
61
+ }
62
+ if (namespace !== "stealth" || methodName !== "fetch" || typeof args[0] !== "string") {
63
+ return undefined;
64
+ }
65
+ const options = args[1];
66
+ return options !== null && typeof options === "object" && !Array.isArray(options)
67
+ ? options
68
+ : undefined;
69
+ }
70
+ function fallbackSensitiveValues(options) {
71
+ const sensitiveParams = options?.sensitiveParams;
72
+ if (!sensitiveParams || typeof sensitiveParams !== "object")
73
+ return [];
74
+ return Object.values(sensitiveParams).map(String);
75
+ }
76
+ function stripDiagnosticBase(url) {
77
+ return url.startsWith(DIAGNOSTIC_BASE_URL) ? url.slice(DIAGNOSTIC_BASE_URL.length) || "/" : url;
78
+ }
79
+ function serializeDiagnosticUrl(url, options) {
80
+ try {
81
+ return {
82
+ degraded: false,
83
+ serializedUrl: serializeRequestUrl(url, options?.params, options?.sensitiveParams),
84
+ };
85
+ }
86
+ catch {
87
+ try {
88
+ const absoluteUrl = new URL(url, DIAGNOSTIC_BASE_URL).toString();
89
+ const serialized = serializeRequestUrl(absoluteUrl, options?.params, options?.sensitiveParams);
90
+ return {
91
+ degraded: false,
92
+ serializedUrl: {
93
+ requestUrl: stripDiagnosticBase(serialized.requestUrl),
94
+ redactedUrl: stripDiagnosticBase(serialized.redactedUrl),
95
+ sensitiveValues: serialized.sensitiveValues,
96
+ },
97
+ };
98
+ }
99
+ catch {
100
+ const sensitiveParamNames = Object.keys(options?.sensitiveParams ?? {});
101
+ const structural = redactUrlQueryParams(url, sensitiveParamNames);
102
+ return {
103
+ degraded: true,
104
+ serializedUrl: {
105
+ requestUrl: url,
106
+ redactedUrl: structural.redactedUrl,
107
+ sensitiveValues: [
108
+ ...new Set([...fallbackSensitiveValues(options), ...structural.sensitiveValues]),
109
+ ],
110
+ },
111
+ };
112
+ }
113
+ }
114
+ }
115
+ function snapshotRequestDiagnostics(namespace, methodName, args) {
116
+ const options = requestOptionsForInvocation(namespace, methodName, args);
117
+ const url = typeof args[0] === "string" ? args[0] : undefined;
118
+ const hasSensitiveParams = Boolean(options?.sensitiveParams && Object.keys(options.sensitiveParams).length > 0);
119
+ const sensitiveParamNames = Object.keys(options?.sensitiveParams ?? {});
120
+ const diagnosticUrl = hasSensitiveParams && url ? serializeDiagnosticUrl(url, options) : undefined;
121
+ const traceUrl = hasSensitiveParams && url
122
+ ? serializeDiagnosticUrl(url, { sensitiveParams: options?.sensitiveParams }).serializedUrl
123
+ .redactedUrl
124
+ : undefined;
125
+ return {
126
+ ...(diagnosticUrl?.degraded ? { diagnosticUrlDegraded: true } : {}),
127
+ ...(hasSensitiveParams ? { requestId: crypto.randomUUID() } : {}),
128
+ serializedUrl: diagnosticUrl?.serializedUrl,
129
+ sensitiveParamNames,
130
+ sensitiveValues: diagnosticUrl?.serializedUrl.sensitiveValues ?? fallbackSensitiveValues(options),
131
+ traceUrl,
132
+ };
133
+ }
134
+ function sanitizeRequestError(error, diagnostics) {
135
+ return redactSensitiveError(error, diagnostics.sensitiveValues, diagnostics.serializedUrl?.requestUrl, diagnostics.serializedUrl?.redactedUrl);
136
+ }
137
+ function isHttpStreamResponse(value) {
138
+ return (typeof value === "object" &&
139
+ value !== null &&
140
+ "body" in value &&
141
+ value.body instanceof ReadableStream);
142
+ }
143
+ function instrumentHttpStreamConsumption(value, recorder, args, diagnostics) {
144
+ const source = value.body;
145
+ let reader;
146
+ const body = new ReadableStream({
147
+ async pull(controller) {
148
+ try {
149
+ reader ??= source.getReader();
150
+ const chunk = await reader.read();
151
+ if (chunk.done) {
152
+ controller.close();
153
+ return;
154
+ }
155
+ controller.enqueue(chunk.value);
156
+ }
157
+ catch (error) {
158
+ const sanitizedError = sanitizeRequestError(error, diagnostics);
159
+ try {
160
+ await recorder.runSpan("http.stream.consume", () => {
161
+ throw sanitizedError;
162
+ }, {
163
+ onError: (spanError) => buildSpanAttributes("http", "stream", args, undefined, spanError, diagnostics),
164
+ });
165
+ }
166
+ catch (recordedError) {
167
+ controller.error(recordedError);
168
+ }
169
+ }
170
+ },
171
+ async cancel(reason) {
172
+ try {
173
+ await (reader ? reader.cancel(reason) : source.cancel(reason));
174
+ }
175
+ catch (error) {
176
+ throw sanitizeRequestError(error, diagnostics);
177
+ }
178
+ },
179
+ }, { highWaterMark: 0 });
180
+ const instrumented = {
181
+ ...value,
182
+ body,
183
+ bytes: () => readableBytes(body),
184
+ textChunks: () => readableTextChunks(body),
185
+ lines: () => readableLines(body),
186
+ };
187
+ try {
188
+ Object.assign(value, instrumented);
189
+ return value;
190
+ }
191
+ catch {
192
+ return instrumented;
193
+ }
194
+ }
195
+ function isAsyncIterable(value) {
196
+ return (typeof value === "object" &&
197
+ value !== null &&
198
+ Symbol.asyncIterator in value &&
199
+ typeof value[Symbol.asyncIterator] === "function");
200
+ }
201
+ function instrumentHttpSseConsumption(value, recorder, args, diagnostics) {
202
+ const source = value;
203
+ return {
204
+ async *[Symbol.asyncIterator]() {
205
+ try {
206
+ for await (const event of source)
207
+ yield event;
208
+ }
209
+ catch (error) {
210
+ const sanitizedError = sanitizeRequestError(error, diagnostics);
211
+ return await recorder.runSpan("http.sse.consume", () => {
212
+ throw sanitizedError;
213
+ }, {
214
+ onError: (spanError) => buildSpanAttributes("http", "sse", args, undefined, spanError, diagnostics),
215
+ });
216
+ }
217
+ },
218
+ };
219
+ }
54
220
  function getMethod(namespace, methodName, args) {
55
221
  if (namespace === "http") {
222
+ // Preserve the original instrumentation contract: generic entry points are
223
+ // reported as REQUEST/STREAM/SSE, independent of transport options.
56
224
  return methodName.toUpperCase();
57
225
  }
58
226
  if (namespace === "stealth") {
@@ -64,15 +232,23 @@ function getMethod(namespace, methodName, args) {
64
232
  }
65
233
  return undefined;
66
234
  }
67
- function buildSpanAttributes(namespace, methodName, args, result, error) {
235
+ function buildSpanAttributes(namespace, methodName, args, result, error, diagnostics = { sensitiveParamNames: [], sensitiveValues: [] }) {
68
236
  const attributes = {};
69
- const url = getUrl(namespace, args, result);
237
+ const rawUrl = getUrl(namespace, args, result);
238
+ const structuralUrl = diagnostics.traceUrl ?? rawUrl;
239
+ const url = structuralUrl
240
+ ? redactSensitiveText(structuralUrl, diagnostics.sensitiveValues, diagnostics.serializedUrl?.requestUrl, diagnostics.serializedUrl?.redactedUrl)
241
+ : undefined;
70
242
  const method = getMethod(namespace, methodName, args);
71
243
  const status = error ? getErrorStatus(error) : getResponseStatus(namespace, result);
72
244
  const duration = error ? undefined : getResponseDuration(result);
73
245
  if (url) {
74
246
  attributes.url = url;
75
247
  }
248
+ if (diagnostics.requestId)
249
+ attributes.request_id = diagnostics.requestId;
250
+ if (diagnostics.diagnosticUrlDegraded)
251
+ attributes.redaction_degraded = true;
76
252
  if (method) {
77
253
  attributes.method = method;
78
254
  }
@@ -91,6 +267,51 @@ function buildSpanAttributes(namespace, methodName, args, result, error) {
91
267
  }
92
268
  return attributes;
93
269
  }
270
+ function isStealthRedirectRunResult(value) {
271
+ return (typeof value === "object" &&
272
+ value !== null &&
273
+ "reason" in value &&
274
+ typeof value.reason === "string" &&
275
+ "hops" in value &&
276
+ Array.isArray(value.hops) &&
277
+ "final" in value &&
278
+ typeof value.final === "object" &&
279
+ value.final !== null);
280
+ }
281
+ function buildStealthRedirectAttributes(args, result, error, diagnostics) {
282
+ const attributes = buildSpanAttributes("stealth", "fetch", args, undefined, error, diagnostics);
283
+ if (!isStealthRedirectRunResult(result))
284
+ return attributes;
285
+ attributes.redirect_reason = result.reason;
286
+ attributes.redirect_hop_count = result.hops.length;
287
+ attributes.status = result.final.status;
288
+ if (result.hops.length > 0) {
289
+ const sensitiveValues = new Set(diagnostics.sensitiveValues);
290
+ const path = result.hops
291
+ .map((hop) => {
292
+ const hopUrl = hop.nextUrl ?? hop.url;
293
+ const responseSensitiveParamNames = [...queryParamNames(hopUrl)].filter(isSensitiveKey);
294
+ const structural = redactUrlQueryParams(hopUrl, [
295
+ ...new Set([...diagnostics.sensitiveParamNames, ...responseSensitiveParamNames]),
296
+ ]);
297
+ for (const value of structural.sensitiveValues)
298
+ sensitiveValues.add(value);
299
+ const safeUrl = redactSensitiveText(structural.redactedUrl, [...sensitiveValues]);
300
+ return `${hop.method} ${hop.status} ${safeUrl}`;
301
+ })
302
+ .join(" -> ");
303
+ attributes.redirect_path = redactSensitiveText(path, [...sensitiveValues], diagnostics.serializedUrl?.requestUrl, diagnostics.serializedUrl?.redactedUrl);
304
+ }
305
+ return attributes;
306
+ }
307
+ function queryParamNames(url) {
308
+ const queryStart = url.indexOf("?");
309
+ if (queryStart === -1)
310
+ return new Set();
311
+ const fragmentStart = url.indexOf("#", queryStart);
312
+ const query = url.slice(queryStart + 1, fragmentStart === -1 ? undefined : fragmentStart);
313
+ return new Set(new URLSearchParams(query).keys());
314
+ }
94
315
  function getBrowserPageAttributes(methodName, args, elapsedMs, error) {
95
316
  const attributes = {};
96
317
  if (methodName === "goto") {
@@ -151,7 +372,49 @@ function wrapPage(page, trace) {
151
372
  },
152
373
  });
153
374
  }
154
- function wrapNamespace(namespace, target, trace) {
375
+ function wrapStealthRedirects(redirects, trace) {
376
+ const recorder = getTraceRecorder(trace);
377
+ if (!recorder)
378
+ return redirects;
379
+ return {
380
+ run(...args) {
381
+ const diagnosticArgs = [args[0].url, args[0]];
382
+ const diagnostics = snapshotRequestDiagnostics("stealth", "fetch", diagnosticArgs);
383
+ let result;
384
+ try {
385
+ result = redirects.run(...args);
386
+ }
387
+ catch (error) {
388
+ const sanitizedError = sanitizeRequestError(error, diagnostics);
389
+ recorder
390
+ .runSpan("stealth.redirects.run", () => {
391
+ throw sanitizedError;
392
+ }, {
393
+ onError: (spanError) => buildStealthRedirectAttributes(diagnosticArgs, undefined, spanError, diagnostics),
394
+ })
395
+ .catch(() => undefined);
396
+ throw sanitizedError;
397
+ }
398
+ const sanitizedResult = Promise.resolve(result).catch((error) => {
399
+ throw sanitizeRequestError(error, diagnostics);
400
+ });
401
+ return recorder.runSpan("stealth.redirects.run", () => sanitizedResult, {
402
+ onSuccess: (spanResult) => buildStealthRedirectAttributes(diagnosticArgs, spanResult, undefined, diagnostics),
403
+ onError: (error) => buildStealthRedirectAttributes(diagnosticArgs, undefined, error, diagnostics),
404
+ });
405
+ },
406
+ };
407
+ }
408
+ function wrapStealthSession(session, trace) {
409
+ const wrappedSession = wrapNamespace("stealth", session, trace);
410
+ const redirects = wrapStealthRedirects(session.redirects, trace);
411
+ return new Proxy(wrappedSession, {
412
+ get(target, property, receiver) {
413
+ return property === "redirects" ? redirects : Reflect.get(target, property, receiver);
414
+ },
415
+ });
416
+ }
417
+ function wrapNamespace(namespace, target, trace, shouldInstrument) {
155
418
  const recorder = getTraceRecorder(trace);
156
419
  if (!recorder) {
157
420
  return target;
@@ -220,6 +483,10 @@ function wrapNamespace(namespace, target, trace) {
220
483
  return wrapped;
221
484
  }
222
485
  const wrapped = (...args) => {
486
+ if (shouldInstrument && !shouldInstrument(methodName, args)) {
487
+ return Reflect.apply(value, namespaceTarget, args);
488
+ }
489
+ const requestDiagnostics = snapshotRequestDiagnostics(namespace, methodName, args);
223
490
  // Invoke first and decide by the RETURN VALUE. `runSpan` always
224
491
  // returns a Promise, so unconditionally span-wrapping every member
225
492
  // silently rewrote synchronous contracts: `ctx.state.namespace()`
@@ -234,18 +501,19 @@ function wrapNamespace(namespace, target, trace) {
234
501
  result = Reflect.apply(value, namespaceTarget, args);
235
502
  }
236
503
  catch (error) {
504
+ const sanitizedError = sanitizeRequestError(error, requestDiagnostics);
237
505
  // A promise-returning implementation may still throw
238
506
  // SYNCHRONOUSLY during pre-flight validation. Preserve the
239
507
  // synchronous throw contract, but keep recording the failure
240
508
  // span (the pre-fidelity wrapper captured these).
241
509
  recorder
242
510
  .runSpan(`${namespace}.${methodName}`, () => {
243
- throw error;
511
+ throw sanitizedError;
244
512
  }, {
245
- onError: (spanError) => buildSpanAttributes(namespace, methodName, args, undefined, spanError),
513
+ onError: (spanError) => buildSpanAttributes(namespace, methodName, args, undefined, spanError, requestDiagnostics),
246
514
  })
247
515
  .catch(() => undefined);
248
- throw error;
516
+ throw sanitizedError;
249
517
  }
250
518
  if (!isThenable(result)) {
251
519
  // The state namespace factory returns the object whose METHODS
@@ -258,12 +526,30 @@ function wrapNamespace(namespace, target, trace) {
258
526
  result !== null) {
259
527
  return wrapNamespace(namespace, result, trace);
260
528
  }
529
+ if (namespace === "stealth" &&
530
+ methodName === "createSession" &&
531
+ typeof result === "object" &&
532
+ result !== null) {
533
+ return wrapStealthSession(result, trace);
534
+ }
261
535
  return result;
262
536
  }
263
- return recorder.runSpan(`${namespace}.${methodName}`, () => result, {
264
- onSuccess: (spanResult) => buildSpanAttributes(namespace, methodName, args, spanResult),
265
- onError: (error) => buildSpanAttributes(namespace, methodName, args, undefined, error),
537
+ const sanitizedResult = Promise.resolve(result).catch((error) => {
538
+ throw sanitizeRequestError(error, requestDiagnostics);
539
+ });
540
+ const tracedResult = recorder.runSpan(`${namespace}.${methodName}`, () => sanitizedResult, {
541
+ onSuccess: (spanResult) => buildSpanAttributes(namespace, methodName, args, spanResult, undefined, requestDiagnostics),
542
+ onError: (error) => buildSpanAttributes(namespace, methodName, args, undefined, error, requestDiagnostics),
266
543
  });
544
+ return namespace === "http" && methodName === "stream"
545
+ ? tracedResult.then((spanResult) => isHttpStreamResponse(spanResult)
546
+ ? instrumentHttpStreamConsumption(spanResult, recorder, args, requestDiagnostics)
547
+ : spanResult)
548
+ : namespace === "http" && methodName === "sse"
549
+ ? tracedResult.then((spanResult) => isAsyncIterable(spanResult)
550
+ ? instrumentHttpSseConsumption(spanResult, recorder, args, requestDiagnostics)
551
+ : spanResult)
552
+ : tracedResult;
267
553
  };
268
554
  wrappedMethods.set(property, wrapped);
269
555
  return wrapped;
@@ -0,0 +1,53 @@
1
+ import { Socket } from "node:net";
2
+ import { type TLSSocket } from "node:tls";
3
+ import { TransportError } from "../errors.js";
4
+ import type { NativeNetworkClient, NativeNetworkConnection, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProxyEgressInfo, ProviderProxyPolicy, ProviderProxyProvider } from "../types.js";
5
+ export type NativeNetworkErrorCode = "native_connection_aborted" | "native_connection_closed" | "native_connection_failed" | "native_connection_idle_timeout" | "native_connection_timeout" | "native_dynamic_egress_unsupported" | "native_proxy_expired" | "native_proxy_invalid";
6
+ export declare class NativeNetworkError extends TransportError {
7
+ constructor(message: string, code: NativeNetworkErrorCode);
8
+ get code(): NativeNetworkErrorCode;
9
+ }
10
+ export declare class NativeProxyExpiredError extends NativeNetworkError {
11
+ readonly expiresAt: string;
12
+ constructor(expiresAt: string);
13
+ }
14
+ /** Raised when an established connection exceeds its opt-in read-idle window. */
15
+ export declare class NativeIdleTimeoutError extends NativeNetworkError {
16
+ constructor();
17
+ }
18
+ export type NativeGatewayProxy = NativeProxyEgressInfo & {
19
+ readonly url: string;
20
+ };
21
+ export type NativeGatewayProxySynthesisInput = {
22
+ readonly vendor: ProviderProxyProvider;
23
+ readonly policy: ProviderProxyPolicy;
24
+ readonly affinityKey?: string;
25
+ readonly now: number;
26
+ };
27
+ /** A vendor adapter in the ordered native gateway resolution chain. */
28
+ export type NativeGatewayProxySynthesizer = (input: NativeGatewayProxySynthesisInput) => NativeGatewayProxy | undefined;
29
+ export type NativeGatewayProxyResolutionInput = {
30
+ readonly policy: ProviderProxyPolicy;
31
+ readonly affinityKey?: string;
32
+ readonly now?: number;
33
+ readonly gatewaySynthesizers?: readonly NativeGatewayProxySynthesizer[];
34
+ };
35
+ export type NativeNetworkClientOptions = {
36
+ readonly proxyPolicy?: ProviderProxyPolicy;
37
+ readonly affinityKey?: string;
38
+ /** Stable credential/account identity; hashed before vendor synthesis. */
39
+ readonly credentialIdentity?: string;
40
+ /** Vendor adapters in priority order within each policy vendor slot. */
41
+ readonly gatewaySynthesizers?: readonly NativeGatewayProxySynthesizer[];
42
+ /** Warning-level lifecycle diagnostic sink. */
43
+ readonly warn?: (message: string) => void;
44
+ /** Delegate to the deployment's native egress authorization layer. */
45
+ readonly grantTcpEgress?: (input: NativeNetworkDynamicGrantOptions) => NativeNetworkEgressGrant;
46
+ };
47
+ /** Domain-separated, process-independent affinity derived from credential identity. */
48
+ export declare function deriveNativeCredentialAffinityKey(credentialIdentity: string): string;
49
+ /** Resolve the first configured native gateway without invoking an allocator API. */
50
+ export declare function resolveNativeGatewayProxy(input: NativeGatewayProxyResolutionInput): NativeGatewayProxy | undefined;
51
+ export declare function createNativeNetworkConnection(socket: Socket | TLSSocket, proxy: NativeGatewayProxy | undefined, options: NativeNetworkClientOptions, idleTimeoutMs?: number): NativeNetworkConnection;
52
+ /** Create the SDK byte-stream runtime; deployment egress authorization stays delegated. */
53
+ export declare function createNativeNetworkClient(options?: NativeNetworkClientOptions): NativeNetworkClient;