@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/dist/runtime/http.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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;
|