@apifuse/provider-sdk 2.2.0-beta.12 → 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 +201 -0
- package/CHANGELOG.md +10 -0
- package/README.md +26 -2
- package/bin/apifuse-pack-types.ts +30 -1
- package/bin/apifuse-record.ts +622 -57
- package/bin/apifuse-submit-check.ts +43 -10
- package/dist/define.d.ts +2 -1
- package/dist/define.js +61 -3
- package/dist/fixture-sanitization.d.ts +26 -0
- package/dist/fixture-sanitization.js +216 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- 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 +239 -39
- 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 +162 -0
- package/package.json +2 -1
- package/src/define.ts +81 -3
- package/src/fixture-sanitization.ts +247 -0
- package/src/index.ts +37 -0
- 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 +293 -40
- 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 +194 -0
package/dist/runtime/stealth.js
CHANGED
|
@@ -6,7 +6,7 @@ import { SDKError, StealthCookieStoreVersionError, TransportError } from "../err
|
|
|
6
6
|
import { getStealthProfile } from "../stealth/profiles.js";
|
|
7
7
|
import { createProxyAuthIpDeniedError, createProxyEdgeAuthRejectedError, createProxyEdgeTlsRejectedError, createProxyPoolExhaustedError, createProxyPoolStaleError, isProxyAuthIpDeniedMessage, isProxyEdgeAuthRejectedMessage, isProxyEdgeTlsRejectedResponse, isProxyPoolRefreshableError, isProxyPoolStaleMessage, isProxyPoolStaleStatus, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_STALE_CODE, } from "./proxy-errors.js";
|
|
8
8
|
import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, normalizeProxyTransportRetryOptions, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy.js";
|
|
9
|
-
import {
|
|
9
|
+
import { isSensitiveKey, redactSensitiveError, redactSensitiveRequestError, redactSensitiveText, redactUrlQueryParams, normalizeSensitiveParams, serializeRequestUrl, } from "./request-options.js";
|
|
10
10
|
const DEFAULT_PROFILE = "chrome-146";
|
|
11
11
|
const MISSING_PROXY_WARNING = "[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
|
|
12
12
|
const MAX_POLICY_PROXY_POOL_REFRESHES = 1;
|
|
@@ -15,6 +15,14 @@ const PROXY_CONNECT_FAILURE_BODY_PATTERN = /\bproxy\b.*\b(non[\s-]?200|connect|t
|
|
|
15
15
|
const PROXY_AUTH_DIAGNOSTIC_URL = "http://example.com/";
|
|
16
16
|
const PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS = 5_000;
|
|
17
17
|
const STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES = [PROXY_CONNECT_FAILURE_CODE];
|
|
18
|
+
function sensitiveQueryParamNames(url) {
|
|
19
|
+
const queryStart = url.indexOf("?");
|
|
20
|
+
if (queryStart === -1)
|
|
21
|
+
return [];
|
|
22
|
+
const fragmentStart = url.indexOf("#", queryStart);
|
|
23
|
+
const query = url.slice(queryStart + 1, fragmentStart === -1 ? undefined : fragmentStart);
|
|
24
|
+
return [...new URLSearchParams(query).keys()].filter(isSensitiveKey);
|
|
25
|
+
}
|
|
18
26
|
const REMOVED_CHROME_PROFILE_NAMES = new Set([
|
|
19
27
|
"chrome-120",
|
|
20
28
|
"chrome-124",
|
|
@@ -293,10 +301,12 @@ function splitCombinedSetCookieHeader(headerValue) {
|
|
|
293
301
|
cookieStrings.push(finalCookie);
|
|
294
302
|
return cookieStrings;
|
|
295
303
|
}
|
|
296
|
-
export async function normalizeResponse(response, requestUrl) {
|
|
304
|
+
export async function normalizeResponse(response, requestUrl, maxBodyBytes) {
|
|
297
305
|
const headers = Object.fromEntries(response.headers.entries());
|
|
298
306
|
const cookies = new CookieJarImpl(setCookieHeadersFromResponse(response.headers), response.url ?? requestUrl);
|
|
299
|
-
const bodyBytes =
|
|
307
|
+
const bodyBytes = maxBodyBytes === undefined
|
|
308
|
+
? await response.arrayBuffer()
|
|
309
|
+
: await readResponseBodyWithLimit(response, maxBodyBytes);
|
|
300
310
|
const body = new TextDecoder().decode(bodyBytes);
|
|
301
311
|
return {
|
|
302
312
|
status: response.status,
|
|
@@ -322,6 +332,75 @@ export async function normalizeResponse(response, requestUrl) {
|
|
|
322
332
|
},
|
|
323
333
|
};
|
|
324
334
|
}
|
|
335
|
+
function responseTooLargeError(maxBodyBytes, observedBytes) {
|
|
336
|
+
return new TransportError(`Response body exceeded maxBodyBytes limit of ${maxBodyBytes} bytes (observed ${observedBytes} bytes)`, {
|
|
337
|
+
code: "response_too_large",
|
|
338
|
+
category: "upstream_http",
|
|
339
|
+
retryable: false,
|
|
340
|
+
status: 0,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
function declaredContentLength(headers) {
|
|
344
|
+
const contentLength = headers.get("content-length")?.trim();
|
|
345
|
+
if (!contentLength || !/^\d+$/.test(contentLength))
|
|
346
|
+
return undefined;
|
|
347
|
+
const parsed = Number(contentLength);
|
|
348
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
349
|
+
}
|
|
350
|
+
function abortTransportResponse(response) {
|
|
351
|
+
if (!response.abort)
|
|
352
|
+
return false;
|
|
353
|
+
try {
|
|
354
|
+
response.abort();
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// The size error remains the primary failure if impit has already closed the response.
|
|
358
|
+
}
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
async function readResponseBodyWithLimit(response, maxBodyBytes) {
|
|
362
|
+
const contentLength = declaredContentLength(response.headers);
|
|
363
|
+
if (contentLength !== undefined && contentLength > maxBodyBytes) {
|
|
364
|
+
if (!abortTransportResponse(response)) {
|
|
365
|
+
await response.body?.cancel().catch(() => undefined);
|
|
366
|
+
}
|
|
367
|
+
throw responseTooLargeError(maxBodyBytes, contentLength);
|
|
368
|
+
}
|
|
369
|
+
if (!response.body) {
|
|
370
|
+
throw new TransportError("Response body stream is unavailable", {
|
|
371
|
+
code: "transport_stream_unavailable",
|
|
372
|
+
category: "upstream_http",
|
|
373
|
+
status: 0,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
const reader = response.body.getReader();
|
|
377
|
+
const chunks = [];
|
|
378
|
+
let receivedBytes = 0;
|
|
379
|
+
try {
|
|
380
|
+
while (true) {
|
|
381
|
+
const { done, value } = await reader.read();
|
|
382
|
+
if (done)
|
|
383
|
+
break;
|
|
384
|
+
receivedBytes += value.byteLength;
|
|
385
|
+
if (receivedBytes > maxBodyBytes) {
|
|
386
|
+
await reader.cancel().catch(() => undefined);
|
|
387
|
+
abortTransportResponse(response);
|
|
388
|
+
throw responseTooLargeError(maxBodyBytes, receivedBytes);
|
|
389
|
+
}
|
|
390
|
+
chunks.push(value);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
finally {
|
|
394
|
+
reader.releaseLock();
|
|
395
|
+
}
|
|
396
|
+
const bodyBytes = new Uint8Array(receivedBytes);
|
|
397
|
+
let offset = 0;
|
|
398
|
+
for (const chunk of chunks) {
|
|
399
|
+
bodyBytes.set(chunk, offset);
|
|
400
|
+
offset += chunk.byteLength;
|
|
401
|
+
}
|
|
402
|
+
return bodyBytes.buffer;
|
|
403
|
+
}
|
|
325
404
|
function normalizeBody(body) {
|
|
326
405
|
if (body === undefined) {
|
|
327
406
|
return "";
|
|
@@ -552,21 +631,29 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
552
631
|
}
|
|
553
632
|
const session = {
|
|
554
633
|
async fetch(url, options = {}) {
|
|
555
|
-
const method =
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}) ??
|
|
561
|
-
(hasExplicitRetryPolicy
|
|
562
|
-
? undefined
|
|
563
|
-
: createDefaultProxyTransportRetryOptions({
|
|
634
|
+
const { hasExplicitRetryPolicy, method, stealthRetryOptions } = (() => {
|
|
635
|
+
try {
|
|
636
|
+
const method = normalizeMethod(options.method ?? "GET");
|
|
637
|
+
const hasExplicitRetryPolicy = options.retry !== undefined;
|
|
638
|
+
const stealthRetryOptions = normalizeProxyTransportRetryOptions(options.retry, {
|
|
564
639
|
extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
|
|
565
640
|
label: "Stealth",
|
|
566
|
-
})
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
641
|
+
}) ??
|
|
642
|
+
(hasExplicitRetryPolicy
|
|
643
|
+
? undefined
|
|
644
|
+
: createDefaultProxyTransportRetryOptions({
|
|
645
|
+
extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
|
|
646
|
+
label: "Stealth",
|
|
647
|
+
}));
|
|
648
|
+
if (stealthRetryOptions) {
|
|
649
|
+
validateUnsafeProxyTransportRetryMethods(stealthRetryOptions, "Stealth");
|
|
650
|
+
}
|
|
651
|
+
return { hasExplicitRetryPolicy, method, stealthRetryOptions };
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
655
|
+
}
|
|
656
|
+
})();
|
|
570
657
|
const hasPolicyProxy = isPolicyManagedProxy(clientOptions);
|
|
571
658
|
const usesPolicyAllocator = hasPolicyProxy && !options.proxy && !clientOptions.proxy;
|
|
572
659
|
const retryAttemptCap = Math.max(1, stealthRetryOptions?.attempts ?? 1);
|
|
@@ -600,6 +687,11 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
600
687
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
601
688
|
let proxy;
|
|
602
689
|
let attemptProxy;
|
|
690
|
+
// Reuse the exact serialization used by this outbound attempt in its catch path.
|
|
691
|
+
let serializedUrl;
|
|
692
|
+
let fallbackSensitiveValues = [];
|
|
693
|
+
let fallbackRequestUrl;
|
|
694
|
+
let fallbackRedactedUrl;
|
|
603
695
|
const attemptStartedAt = Date.now();
|
|
604
696
|
let attemptRecorded = false;
|
|
605
697
|
const recordProxyAttempt = (outcome, errorCode, status) => {
|
|
@@ -620,6 +712,16 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
620
712
|
});
|
|
621
713
|
};
|
|
622
714
|
try {
|
|
715
|
+
const sensitiveParams = normalizeSensitiveParams(options.sensitiveParams);
|
|
716
|
+
const structural = redactUrlQueryParams(url, Object.keys(sensitiveParams ?? {}));
|
|
717
|
+
fallbackSensitiveValues = [
|
|
718
|
+
...new Set([
|
|
719
|
+
...Object.values(sensitiveParams ?? {}).map(String),
|
|
720
|
+
...structural.sensitiveValues,
|
|
721
|
+
]),
|
|
722
|
+
].filter((value) => value !== "");
|
|
723
|
+
fallbackRequestUrl = url;
|
|
724
|
+
fallbackRedactedUrl = structural.redactedUrl;
|
|
623
725
|
assertNoUnsupportedFingerprintOverrides(options);
|
|
624
726
|
attemptProxy = await resolveRequestProxy(options, attempt, refreshAttempt);
|
|
625
727
|
proxy = attemptProxy.url;
|
|
@@ -637,7 +739,8 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
637
739
|
const ignoreTlsErrors = Boolean(options.stealth?.insecureSkipVerify ??
|
|
638
740
|
(!hasPolicyProxy && proxy && clientOptions.proxyStealth?.insecureSkipVerify));
|
|
639
741
|
const profileName = options.profile ?? defaultProfile;
|
|
640
|
-
|
|
742
|
+
serializedUrl = serializeRequestUrl(resolveUrl(baseUrl, url), options.params, sensitiveParams);
|
|
743
|
+
const { requestUrl } = serializedUrl;
|
|
641
744
|
const headers = { ...(options.headers ?? {}) };
|
|
642
745
|
if (!hasHeader(headers, "Cookie")) {
|
|
643
746
|
const cookieHeader = cookieJar.toHeader(requestUrl);
|
|
@@ -654,7 +757,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
654
757
|
requestInit.body = normalizeBody(options.body);
|
|
655
758
|
}
|
|
656
759
|
const response = await getClient(profileName, proxy, ignoreTlsErrors).fetch(requestUrl, requestInit);
|
|
657
|
-
const normalized = await normalizeResponse(response, requestUrl);
|
|
760
|
+
const normalized = await normalizeResponse(response, requestUrl, options.maxBodyBytes);
|
|
658
761
|
cookieJar.setFromCookieStrings(setCookieHeadersFromResponse(response.headers), response.url ?? requestUrl);
|
|
659
762
|
if (proxy && isProxyConnectFailureResponse(response, normalized.body)) {
|
|
660
763
|
throw createProxyConnectFailureError(normalized.body);
|
|
@@ -687,12 +790,23 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
687
790
|
return normalized;
|
|
688
791
|
}
|
|
689
792
|
catch (error) {
|
|
690
|
-
const
|
|
793
|
+
const sensitiveValues = serializedUrl?.sensitiveValues ?? fallbackSensitiveValues;
|
|
794
|
+
let normalizedError;
|
|
795
|
+
try {
|
|
796
|
+
normalizedError = normalizeStealthTransportError(error);
|
|
797
|
+
}
|
|
798
|
+
catch (normalizationError) {
|
|
799
|
+
throw redactSensitiveError(normalizationError, sensitiveValues, serializedUrl?.requestUrl ?? fallbackRequestUrl, serializedUrl?.redactedUrl ?? fallbackRedactedUrl);
|
|
800
|
+
}
|
|
801
|
+
const retryErrorCode = proxyAttemptErrorCode(normalizedError);
|
|
802
|
+
const refreshableProxyError = isProxyPoolRefreshableError(normalizedError);
|
|
803
|
+
const runProxyAuthDiagnostic = shouldRunProxyAuthDiagnostic(normalizedError);
|
|
804
|
+
normalizedError = redactSensitiveError(normalizedError, sensitiveValues, serializedUrl?.requestUrl ?? fallbackRequestUrl, serializedUrl?.redactedUrl ?? fallbackRedactedUrl);
|
|
691
805
|
recordProxyAttempt("error", proxyAttemptErrorCode(normalizedError), proxyAttemptStatus(normalizedError));
|
|
692
806
|
lastError = normalizedError;
|
|
693
|
-
if (proxy && rotatesRegistryChain &&
|
|
807
|
+
if (proxy && rotatesRegistryChain && refreshableProxyError) {
|
|
694
808
|
stalePoolError = normalizedError;
|
|
695
|
-
if (
|
|
809
|
+
if (runProxyAuthDiagnostic) {
|
|
696
810
|
stalePoolDiagnosticProxy = proxy;
|
|
697
811
|
}
|
|
698
812
|
if (attempt + 1 < maxAttempts) {
|
|
@@ -722,7 +836,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
722
836
|
});
|
|
723
837
|
if (attempt + 1 < transportRetryCap &&
|
|
724
838
|
shouldRetryProxyTransportAttempt({
|
|
725
|
-
error:
|
|
839
|
+
error: { code: retryErrorCode },
|
|
726
840
|
explicitRetry: hasExplicitRetryPolicy,
|
|
727
841
|
method,
|
|
728
842
|
options: stealthRetryOptions,
|
|
@@ -772,23 +886,78 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
772
886
|
const maxHops = options.maxHops === undefined || !Number.isFinite(options.maxHops)
|
|
773
887
|
? 10
|
|
774
888
|
: Math.max(0, Math.floor(options.maxHops));
|
|
889
|
+
const { url: _url, maxHops: _maxHops, stopWhen, params, sensitiveParams, ...fetchOptions } = options;
|
|
775
890
|
const hops = [];
|
|
776
|
-
let currentUrl = resolveUrl(baseUrl, options.url);
|
|
777
891
|
let method = normalizeMethod(options.method ?? "GET");
|
|
778
892
|
let body = options.body;
|
|
779
893
|
let response;
|
|
780
894
|
const visitedRequests = new Set();
|
|
781
|
-
const
|
|
895
|
+
const initialParams = params
|
|
896
|
+
? Object.fromEntries(Object.entries(params).map(([key, value]) => [
|
|
897
|
+
key,
|
|
898
|
+
Array.isArray(value) ? [...value] : value,
|
|
899
|
+
]))
|
|
900
|
+
: undefined;
|
|
901
|
+
const normalizedSensitiveParams = normalizeSensitiveParams(sensitiveParams);
|
|
902
|
+
const initialSensitiveParams = normalizedSensitiveParams
|
|
903
|
+
? { ...normalizedSensitiveParams }
|
|
904
|
+
: undefined;
|
|
905
|
+
const sensitiveParamNames = initialSensitiveParams
|
|
906
|
+
? Object.keys(initialSensitiveParams)
|
|
907
|
+
: [];
|
|
908
|
+
const callerStructural = redactUrlQueryParams(options.url, sensitiveParamNames);
|
|
909
|
+
const sensitiveValues = new Set([
|
|
910
|
+
...Object.values(initialSensitiveParams ?? {}),
|
|
911
|
+
...callerStructural.sensitiveValues,
|
|
912
|
+
].filter((value) => value !== ""));
|
|
913
|
+
const redactRedirectUrl = (value) => {
|
|
914
|
+
const structural = redactUrlQueryParams(value, [
|
|
915
|
+
...new Set([...sensitiveParamNames, ...sensitiveQueryParamNames(value)]),
|
|
916
|
+
]);
|
|
917
|
+
for (const sensitiveValue of structural.sensitiveValues) {
|
|
918
|
+
sensitiveValues.add(sensitiveValue);
|
|
919
|
+
}
|
|
920
|
+
return redactSensitiveText(structural.redactedUrl, [...sensitiveValues]);
|
|
921
|
+
};
|
|
922
|
+
let currentUrl;
|
|
923
|
+
let initialUrl;
|
|
924
|
+
try {
|
|
925
|
+
currentUrl = resolveUrl(baseUrl, options.url);
|
|
926
|
+
redactRedirectUrl(currentUrl);
|
|
927
|
+
initialUrl = serializeRequestUrl(currentUrl, initialParams, initialSensitiveParams);
|
|
928
|
+
for (const value of initialUrl.sensitiveValues) {
|
|
929
|
+
if (value !== "")
|
|
930
|
+
sensitiveValues.add(value);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
catch (error) {
|
|
934
|
+
throw redactSensitiveError(error, [...sensitiveValues], options.url, redactRedirectUrl(options.url));
|
|
935
|
+
}
|
|
782
936
|
for (let hopIndex = 0; hopIndex <= maxHops; hopIndex += 1) {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
937
|
+
const outboundUrl = hopIndex === 0 ? initialUrl.requestUrl : serializeRequestUrl(currentUrl).requestUrl;
|
|
938
|
+
// Preserve params-only loop bookkeeping from before sensitiveParams:
|
|
939
|
+
// the first visited key is the caller's resolved URL, not its expanded query.
|
|
940
|
+
const visitedUrl = hopIndex === 0 && !initialSensitiveParams ? currentUrl : outboundUrl;
|
|
941
|
+
visitedRequests.add(`${method} ${visitedUrl}`);
|
|
942
|
+
try {
|
|
943
|
+
response = await session.fetch(currentUrl, {
|
|
944
|
+
...fetchOptions,
|
|
945
|
+
body,
|
|
946
|
+
method,
|
|
947
|
+
...(hopIndex === 0 && initialParams ? { params: initialParams } : {}),
|
|
948
|
+
...(hopIndex === 0 && initialSensitiveParams
|
|
949
|
+
? { sensitiveParams: initialSensitiveParams }
|
|
950
|
+
: {}),
|
|
951
|
+
redirect: "manual",
|
|
952
|
+
throwOnHttpError: false,
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
catch (error) {
|
|
956
|
+
throw redactSensitiveError(error, [...sensitiveValues], outboundUrl, redactRedirectUrl(outboundUrl));
|
|
957
|
+
}
|
|
958
|
+
// StealthResponse.url is programmatic metadata and remains raw. Only the
|
|
959
|
+
// redirect hop emitted below is a diagnostic surface.
|
|
960
|
+
const responseUrl = response.url ?? (hopIndex === 0 && initialSensitiveParams ? outboundUrl : currentUrl);
|
|
792
961
|
if (!isRedirectStatus(response.status)) {
|
|
793
962
|
return {
|
|
794
963
|
final: response,
|
|
@@ -799,18 +968,49 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
799
968
|
};
|
|
800
969
|
}
|
|
801
970
|
const location = locationHeader(response.headers);
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
971
|
+
const redactedResponseUrl = redactRedirectUrl(responseUrl);
|
|
972
|
+
const redactedLocation = location ? redactRedirectUrl(location) : undefined;
|
|
973
|
+
let nextUrl;
|
|
974
|
+
try {
|
|
975
|
+
nextUrl = location ? new URL(location, responseUrl).toString() : undefined;
|
|
976
|
+
}
|
|
977
|
+
catch (error) {
|
|
978
|
+
throw redactSensitiveError(error, [...sensitiveValues], location, redactedLocation);
|
|
979
|
+
}
|
|
980
|
+
const realHop = {
|
|
981
|
+
url: responseUrl,
|
|
807
982
|
status: response.status,
|
|
808
983
|
method,
|
|
809
984
|
...(location ? { location } : {}),
|
|
810
985
|
...(nextUrl ? { nextUrl } : {}),
|
|
811
986
|
};
|
|
987
|
+
const hop = {
|
|
988
|
+
...realHop,
|
|
989
|
+
url: redactedResponseUrl,
|
|
990
|
+
...(redactedLocation ? { location: redactedLocation } : {}),
|
|
991
|
+
...(nextUrl ? { nextUrl: redactRedirectUrl(nextUrl) } : {}),
|
|
992
|
+
};
|
|
812
993
|
hops.push(hop);
|
|
813
|
-
|
|
994
|
+
let shouldStop = false;
|
|
995
|
+
if (stopWhen) {
|
|
996
|
+
try {
|
|
997
|
+
shouldStop = await stopWhen(realHop);
|
|
998
|
+
}
|
|
999
|
+
catch (error) {
|
|
1000
|
+
let sanitizedError = error;
|
|
1001
|
+
for (const [rawUrl, safeUrl] of [
|
|
1002
|
+
[responseUrl, redactedResponseUrl],
|
|
1003
|
+
[location, redactedLocation],
|
|
1004
|
+
[nextUrl, nextUrl ? redactRedirectUrl(nextUrl) : undefined],
|
|
1005
|
+
]) {
|
|
1006
|
+
if (!rawUrl || !safeUrl)
|
|
1007
|
+
continue;
|
|
1008
|
+
sanitizedError = redactSensitiveError(sanitizedError, [...sensitiveValues], rawUrl, safeUrl);
|
|
1009
|
+
}
|
|
1010
|
+
throw sanitizedError;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (shouldStop) {
|
|
814
1014
|
return {
|
|
815
1015
|
final: response,
|
|
816
1016
|
hops,
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createServerApp, type ProviderServerCloseOptions, type ProviderServerHandle, type ProviderServerLogEvent, type ProviderServerLogger, type ProviderServerOperationExecutor, type ProviderServerOperationExecutorInput, type ProviderServerOptions, type ProviderServerStatefulForwardEnvelope, type ServeOptions, serve, } from "./serve.js";
|
|
2
2
|
export { computeSelfTestPlanDigest, createSelfTestApp, createSelfTestAuthFlowInvoke, createSelfTestInvoke, DEFAULT_SELF_TEST_REQUEST_BUDGET_MS, isSelfTestReadOnlyOperation, PROVIDER_RUNTIME_SELF_TEST_REQUEST_BUDGET_MS_ENV, resolveSelfTestPort, SELF_TEST_AUTH_FLOW_MULTI_TURN_SKIP_REASON, SELF_TEST_AUTH_FLOW_REJECTED_SKIP_REASON, SELF_TEST_HEALTHZ_PATH, SELF_TEST_PATH, SELF_TEST_SCHEMA_VERSION, type SelfTestAppOptions, type SelfTestAuthFlowInvoke, type SelfTestAuthFlowRoute, type SelfTestCaseResult, type SelfTestCaseStatus, type SelfTestOperationInvoke, type SelfTestRequest, SelfTestRequestSchema, type SelfTestResponse, } from "./self-test.js";
|
|
3
|
-
export { resolveHealthCheckInputDateTokens } from "./self-test-input-tokens.js";
|
|
3
|
+
export { type InputDateTokenCalendar, resolveHealthCheckInputDateTokens, } from "./self-test-input-tokens.js";
|
|
4
4
|
export { collectSelfTestSensitiveValues, redactSelfTestText, SELF_TEST_MAX_TEXT_LENGTH, SELF_TEST_REDACTED_PLACEHOLDER, } from "./self-test-redaction.js";
|
|
5
5
|
export { DEFAULT_SELF_TEST_PORT, deriveSelfTestToken, PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_ENV, PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_PREVIOUS_ENV, PROVIDER_RUNTIME_SELF_TEST_PORT_ENV, resolveSelfTestMasterSecrets, type SelfTestMasterSecrets, verifySelfTestAuthorization, } from "./self-test-token.js";
|
|
6
6
|
export type { AuthFlowRequest, AuthFlowResponse, AuthFlowSuccessResponse, ConnectionMode, OperationConnection, OperationErrorResponse, OperationRequest, OperationResponse, OperationSuccessResponse, } from "./types.js";
|
package/dist/server/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createServerApp, serve, } from "./serve.js";
|
|
2
2
|
export { computeSelfTestPlanDigest, createSelfTestApp, createSelfTestAuthFlowInvoke, createSelfTestInvoke, DEFAULT_SELF_TEST_REQUEST_BUDGET_MS, isSelfTestReadOnlyOperation, PROVIDER_RUNTIME_SELF_TEST_REQUEST_BUDGET_MS_ENV, resolveSelfTestPort, SELF_TEST_AUTH_FLOW_MULTI_TURN_SKIP_REASON, SELF_TEST_AUTH_FLOW_REJECTED_SKIP_REASON, SELF_TEST_HEALTHZ_PATH, SELF_TEST_PATH, SELF_TEST_SCHEMA_VERSION, SelfTestRequestSchema, } from "./self-test.js";
|
|
3
|
-
export { resolveHealthCheckInputDateTokens } from "./self-test-input-tokens.js";
|
|
3
|
+
export { resolveHealthCheckInputDateTokens, } from "./self-test-input-tokens.js";
|
|
4
4
|
export { collectSelfTestSensitiveValues, redactSelfTestText, SELF_TEST_MAX_TEXT_LENGTH, SELF_TEST_REDACTED_PLACEHOLDER, } from "./self-test-redaction.js";
|
|
5
5
|
export { DEFAULT_SELF_TEST_PORT, deriveSelfTestToken, PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_ENV, PROVIDER_RUNTIME_SELF_TEST_MASTER_SECRET_PREVIOUS_ENV, PROVIDER_RUNTIME_SELF_TEST_PORT_ENV, resolveSelfTestMasterSecrets, verifySelfTestAuthorization, } from "./self-test-token.js";
|
|
6
6
|
export { AuthFlowRequestSchema, AuthFlowSuccessResponseSchema, ConnectionModeSchema, ErrorEnvelopeSchema, OperationConnectionSchema, OperationErrorResponseSchema, OperationRequestSchema, OperationSuccessResponseSchema, } from "./types.js";
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type InputDateTokenCalendar = "KST" | "UTC";
|
|
2
|
+
export declare function resolveHealthCheckInputDateTokens(value: unknown, now?: Date, calendar?: InputDateTokenCalendar): unknown;
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Relative
|
|
3
|
-
*
|
|
4
|
-
* so
|
|
2
|
+
* Relative date-token resolution for health-check case inputs and fixture
|
|
3
|
+
* requests. The calendar defaults to KST; callers that need UTC must opt in
|
|
4
|
+
* explicitly so UTC and KST do not silently disagree from 15:00–23:59 UTC.
|
|
5
5
|
*
|
|
6
|
-
* Supported token: `+<days>d` or `+<days>d:YYYYMMDD` (1..365 days ahead
|
|
6
|
+
* Supported token: `+<days>d` or `+<days>d:YYYYMMDD` (1..365 days ahead).
|
|
7
7
|
*/
|
|
8
|
-
const
|
|
9
|
-
function
|
|
10
|
-
const
|
|
11
|
-
const date = new Date(Date.UTC(
|
|
8
|
+
const RELATIVE_DATE_TOKEN = /^\+(\d{1,3})d(?::(YYYYMMDD))?$/i;
|
|
9
|
+
function dateFromDaysAhead(daysAhead, now = new Date(), format = "YYYY-MM-DD", calendar = "KST") {
|
|
10
|
+
const calendarNow = new Date(now.getTime() + (calendar === "KST" ? 9 * 60 * 60 * 1000 : 0));
|
|
11
|
+
const date = new Date(Date.UTC(calendarNow.getUTCFullYear(), calendarNow.getUTCMonth(), calendarNow.getUTCDate() + daysAhead));
|
|
12
12
|
const isoDate = date.toISOString().slice(0, 10);
|
|
13
13
|
return format === "YYYYMMDD" ? isoDate.replace(/-/g, "") : isoDate;
|
|
14
14
|
}
|
|
15
|
-
export function resolveHealthCheckInputDateTokens(value, now = new Date()) {
|
|
15
|
+
export function resolveHealthCheckInputDateTokens(value, now = new Date(), calendar = "KST") {
|
|
16
16
|
if (typeof value === "string") {
|
|
17
|
-
const relative = value.match(
|
|
17
|
+
const relative = value.match(RELATIVE_DATE_TOKEN);
|
|
18
18
|
if (!relative)
|
|
19
19
|
return value;
|
|
20
20
|
const daysAhead = Number(relative[1]);
|
|
@@ -22,16 +22,20 @@ export function resolveHealthCheckInputDateTokens(value, now = new Date()) {
|
|
|
22
22
|
return value;
|
|
23
23
|
}
|
|
24
24
|
const format = relative[2]?.toUpperCase() === "YYYYMMDD" ? "YYYYMMDD" : "YYYY-MM-DD";
|
|
25
|
-
return
|
|
25
|
+
return dateFromDaysAhead(daysAhead, now, format, calendar);
|
|
26
26
|
}
|
|
27
27
|
if (Array.isArray(value)) {
|
|
28
|
-
|
|
28
|
+
const resolved = value.map((entry) => resolveHealthCheckInputDateTokens(entry, now, calendar));
|
|
29
|
+
return resolved.some((entry, index) => entry !== value[index]) ? resolved : value;
|
|
29
30
|
}
|
|
30
31
|
if (value && typeof value === "object") {
|
|
31
|
-
|
|
32
|
+
const resolved = Object.fromEntries(Object.entries(value).map(([key, entry]) => [
|
|
32
33
|
key,
|
|
33
|
-
resolveHealthCheckInputDateTokens(entry, now),
|
|
34
|
+
resolveHealthCheckInputDateTokens(entry, now, calendar),
|
|
34
35
|
]));
|
|
36
|
+
return Object.entries(resolved).some(([key, entry]) => entry !== Reflect.get(value, key))
|
|
37
|
+
? resolved
|
|
38
|
+
: value;
|
|
35
39
|
}
|
|
36
40
|
return value;
|
|
37
41
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type JsonValue } from "./contract-json.js";
|
|
2
|
+
import type { HttpStreamResponse } from "./types.js";
|
|
3
|
+
export declare const STREAM_PREVIEW_BYTES = 4096;
|
|
4
|
+
export declare const STREAM_FINALIZE_TIMEOUT_MS = 30000;
|
|
5
|
+
declare const STREAM_EVIDENCE_HEADER_NAMES: readonly ["content-disposition", "content-length", "content-type"];
|
|
6
|
+
type StreamEvidenceHeaderName = (typeof STREAM_EVIDENCE_HEADER_NAMES)[number];
|
|
7
|
+
export type StreamEvidenceHeaders = Partial<Record<StreamEvidenceHeaderName, string>>;
|
|
8
|
+
export interface StreamEvidenceRequest {
|
|
9
|
+
ordinal: number;
|
|
10
|
+
method: string;
|
|
11
|
+
path: string;
|
|
12
|
+
}
|
|
13
|
+
declare const STREAM_EVIDENCE_REDACTION_REASONS: readonly ["high-entropy-token", "malformed-json", "pem-private-key", "sanitized-preview-too-large", "sanitizer-output-invalid", "sensitive-delimited-column", "textual-xml", "truncated-form", "truncated-json", "undecodable-text"];
|
|
14
|
+
export type StreamEvidenceRedactionReason = (typeof STREAM_EVIDENCE_REDACTION_REASONS)[number];
|
|
15
|
+
export interface StreamEvidenceRecord {
|
|
16
|
+
__apifuse_stream__: true;
|
|
17
|
+
status: number;
|
|
18
|
+
ok: boolean;
|
|
19
|
+
headers: StreamEvidenceHeaders;
|
|
20
|
+
body_sha256: string;
|
|
21
|
+
body_bytes: number;
|
|
22
|
+
body_preview_base64: string;
|
|
23
|
+
request?: StreamEvidenceRequest;
|
|
24
|
+
preview_sanitized?: true;
|
|
25
|
+
preview_redaction_reason?: StreamEvidenceRedactionReason;
|
|
26
|
+
}
|
|
27
|
+
export type StreamEvidenceReplayResponse = HttpStreamResponse & {
|
|
28
|
+
evidence_only: true;
|
|
29
|
+
body_sha256: string;
|
|
30
|
+
body_bytes: number;
|
|
31
|
+
preview_sanitized?: true;
|
|
32
|
+
preview_redaction_reason?: StreamEvidenceRedactionReason;
|
|
33
|
+
};
|
|
34
|
+
export interface StreamEvidenceCapture {
|
|
35
|
+
response: HttpStreamResponse;
|
|
36
|
+
getEvidence(): Promise<StreamEvidenceRecord>;
|
|
37
|
+
}
|
|
38
|
+
export type StreamCaptureGroupItem = {
|
|
39
|
+
kind: "stream";
|
|
40
|
+
evidence: StreamEvidenceRecord;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "response";
|
|
43
|
+
value: JsonValue;
|
|
44
|
+
};
|
|
45
|
+
export interface StreamCaptureGroup {
|
|
46
|
+
items: StreamCaptureGroupItem[];
|
|
47
|
+
}
|
|
48
|
+
export interface StreamCaptureEnvelope {
|
|
49
|
+
__apifuse_capture__: true;
|
|
50
|
+
items: StreamCaptureGroupItem[];
|
|
51
|
+
}
|
|
52
|
+
export type StreamEvidenceCaptureOptions = {
|
|
53
|
+
requestUrl: string;
|
|
54
|
+
sanitizeFixture?: (value: JsonValue) => JsonValue;
|
|
55
|
+
request?: StreamEvidenceRequest;
|
|
56
|
+
finalizeTimeoutMs?: number;
|
|
57
|
+
};
|
|
58
|
+
export declare function parseStreamEvidenceRecord(value: unknown): StreamEvidenceRecord;
|
|
59
|
+
export declare function isStreamEvidenceRecord(value: unknown): value is StreamEvidenceRecord;
|
|
60
|
+
export declare function findStreamEvidenceRecord(value: unknown): StreamEvidenceRecord | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Selects every stream captured by the latest recording invocation, in stream call order.
|
|
63
|
+
* Evidence without request ordinals uses the legacy latest-record-only behavior.
|
|
64
|
+
*/
|
|
65
|
+
export declare function findStreamEvidenceRecords(value: unknown): StreamEvidenceRecord[];
|
|
66
|
+
/** Returns a tagged latest mixed response group used by evidence-only snapshot replay. */
|
|
67
|
+
export declare function findStreamCaptureGroup(value: unknown): StreamCaptureGroup | undefined;
|
|
68
|
+
/** Creates a discriminated invocation envelope so ordinary arrays cannot mimic capture timelines. */
|
|
69
|
+
export declare function createStreamCaptureEnvelope(items: StreamCaptureGroupItem[]): StreamCaptureEnvelope;
|
|
70
|
+
export declare function isStreamEvidenceReplayResponse(response: HttpStreamResponse): response is StreamEvidenceReplayResponse;
|
|
71
|
+
export declare function captureStreamEvidence(response: HttpStreamResponse, options: StreamEvidenceCaptureOptions): StreamEvidenceCapture;
|
|
72
|
+
export declare function replayStreamEvidence(evidence: StreamEvidenceRecord): StreamEvidenceReplayResponse;
|
|
73
|
+
export declare function hasStreamEvidenceMarker(value: unknown): boolean;
|
|
74
|
+
export {};
|