@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.
Files changed (52) hide show
  1. package/AUTHORING.md +201 -0
  2. package/CHANGELOG.md +10 -0
  3. package/README.md +26 -2
  4. package/bin/apifuse-pack-types.ts +30 -1
  5. package/bin/apifuse-record.ts +622 -57
  6. package/bin/apifuse-submit-check.ts +43 -10
  7. package/dist/define.d.ts +2 -1
  8. package/dist/define.js +61 -3
  9. package/dist/fixture-sanitization.d.ts +26 -0
  10. package/dist/fixture-sanitization.js +216 -0
  11. package/dist/index.d.ts +2 -1
  12. package/dist/index.js +1 -0
  13. package/dist/provider.d.ts +2 -1
  14. package/dist/provider.js +1 -0
  15. package/dist/runtime/http.js +86 -32
  16. package/dist/runtime/instrumentation.js +295 -9
  17. package/dist/runtime/native-network.d.ts +53 -0
  18. package/dist/runtime/native-network.js +477 -0
  19. package/dist/runtime/proxy-nodemaven.d.ts +14 -0
  20. package/dist/runtime/proxy-nodemaven.js +20 -2
  21. package/dist/runtime/request-options.d.ts +68 -1
  22. package/dist/runtime/request-options.js +548 -0
  23. package/dist/runtime/stealth.d.ts +3 -1
  24. package/dist/runtime/stealth.js +239 -39
  25. package/dist/server/index.d.ts +1 -1
  26. package/dist/server/index.js +1 -1
  27. package/dist/server/self-test-input-tokens.d.ts +2 -1
  28. package/dist/server/self-test-input-tokens.js +18 -14
  29. package/dist/stream-evidence.d.ts +74 -0
  30. package/dist/stream-evidence.js +785 -0
  31. package/dist/testing/index.d.ts +1 -1
  32. package/dist/testing/index.js +1 -1
  33. package/dist/testing/run.d.ts +32 -2
  34. package/dist/testing/run.js +451 -19
  35. package/dist/types.d.ts +162 -0
  36. package/package.json +2 -1
  37. package/src/define.ts +81 -3
  38. package/src/fixture-sanitization.ts +247 -0
  39. package/src/index.ts +37 -0
  40. package/src/provider.ts +37 -0
  41. package/src/runtime/http.ts +144 -38
  42. package/src/runtime/instrumentation.ts +424 -8
  43. package/src/runtime/native-network.ts +600 -0
  44. package/src/runtime/proxy-nodemaven.ts +37 -2
  45. package/src/runtime/request-options.ts +680 -1
  46. package/src/runtime/stealth.ts +293 -40
  47. package/src/server/index.ts +4 -1
  48. package/src/server/self-test-input-tokens.ts +29 -14
  49. package/src/stream-evidence.ts +988 -0
  50. package/src/testing/index.ts +9 -1
  51. package/src/testing/run.ts +608 -12
  52. package/src/types.ts +194 -0
@@ -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 { appendQueryParams } from "./request-options.js";
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 = await response.arrayBuffer();
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 = normalizeMethod(options.method ?? "GET");
556
- const hasExplicitRetryPolicy = options.retry !== undefined;
557
- const stealthRetryOptions = normalizeProxyTransportRetryOptions(options.retry, {
558
- extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
559
- label: "Stealth",
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
- if (stealthRetryOptions) {
568
- validateUnsafeProxyTransportRetryMethods(stealthRetryOptions, "Stealth");
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
- const requestUrl = appendQueryParams(resolveUrl(baseUrl, url), options.params);
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 normalizedError = normalizeStealthTransportError(error);
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 && isProxyPoolRefreshableError(normalizedError)) {
807
+ if (proxy && rotatesRegistryChain && refreshableProxyError) {
694
808
  stalePoolError = normalizedError;
695
- if (shouldRunProxyAuthDiagnostic(normalizedError)) {
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: normalizedError,
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 { url: _url, maxHops: _maxHops, stopWhen, params, ...fetchOptions } = options;
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
- visitedRequests.add(`${method} ${currentUrl}`);
784
- response = await session.fetch(currentUrl, {
785
- ...fetchOptions,
786
- body,
787
- method,
788
- ...(hopIndex === 0 && params ? { params } : {}),
789
- redirect: "manual",
790
- throwOnHttpError: false,
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 nextUrl = location
803
- ? new URL(location, response.url ?? currentUrl).toString()
804
- : undefined;
805
- const hop = {
806
- url: response.url ?? currentUrl,
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
- if (stopWhen && (await stopWhen(hop))) {
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,
@@ -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";
@@ -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 declare function resolveHealthCheckInputDateTokens(value: unknown, now?: Date): unknown;
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-KST date token resolution for health-check case inputs, ported from
3
- * the monorepo health-monitor (`apps/health-monitor/src/lib/health-check-input.ts`)
4
- * so provider self-tests resolve durable probe inputs identically.
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, KST).
6
+ * Supported token: `+<days>d` or `+<days>d:YYYYMMDD` (1..365 days ahead).
7
7
  */
8
- const RELATIVE_KST_DATE_TOKEN = /^\+(\d{1,3})d(?::(YYYYMMDD))?$/i;
9
- function dateFromKstDaysAhead(daysAhead, now = new Date(), format = "YYYY-MM-DD") {
10
- const kstNow = new Date(now.getTime() + 9 * 60 * 60 * 1000);
11
- const date = new Date(Date.UTC(kstNow.getUTCFullYear(), kstNow.getUTCMonth(), kstNow.getUTCDate() + daysAhead));
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(RELATIVE_KST_DATE_TOKEN);
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 dateFromKstDaysAhead(daysAhead, now, format);
25
+ return dateFromDaysAhead(daysAhead, now, format, calendar);
26
26
  }
27
27
  if (Array.isArray(value)) {
28
- return value.map((entry) => resolveHealthCheckInputDateTokens(entry, now));
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
- return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
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 {};