@apifuse/provider-sdk 2.1.0-beta.8 → 2.2.0-beta.1

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 (133) hide show
  1. package/AUTHORING.md +134 -0
  2. package/CHANGELOG.md +61 -0
  3. package/README.md +21 -9
  4. package/SUBMISSION.md +10 -11
  5. package/bin/apifuse-pack-check.ts +22 -0
  6. package/bin/apifuse-perf.ts +18 -9
  7. package/bin/apifuse-submit-check.ts +1747 -388
  8. package/dist/auth-turn/auth-turn.v1.schema.json +89 -0
  9. package/dist/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  10. package/dist/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  11. package/dist/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  12. package/dist/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  13. package/dist/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  14. package/dist/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  15. package/dist/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  16. package/dist/auth-turn/fixtures/valid/abort.json +8 -0
  17. package/dist/auth-turn/fixtures/valid/challenge.json +17 -0
  18. package/dist/auth-turn/fixtures/valid/complete.json +13 -0
  19. package/dist/auth-turn/fixtures/valid/form.json +14 -0
  20. package/dist/auth-turn/fixtures/valid/message.json +13 -0
  21. package/dist/auth-turn/fixtures/valid/multi_choice.json +15 -0
  22. package/dist/auth-turn/fixtures/valid/pending.json +5 -0
  23. package/dist/auth-turn/fixtures/valid/poll.json +9 -0
  24. package/dist/auth-turn/fixtures/valid/redirect.json +16 -0
  25. package/dist/auth-turn/fixtures/valid/retry.json +8 -0
  26. package/dist/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  27. package/dist/auth-turn/index.d.ts +195 -0
  28. package/dist/auth-turn/index.js +133 -0
  29. package/dist/auth.d.ts +76 -0
  30. package/dist/auth.js +436 -0
  31. package/dist/ceremonies/index.js +7 -31
  32. package/dist/cli/create.js +45 -30
  33. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  34. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  35. package/dist/cli/templates/provider/AGENTS.md.tpl +87 -0
  36. package/dist/cli/templates/provider/CLAUDE.md.tpl +1 -0
  37. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  38. package/dist/cli/templates/provider/README.md.tpl +163 -0
  39. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  40. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  41. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  42. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  43. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  44. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  45. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  46. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  47. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  48. package/dist/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  49. package/dist/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  50. package/dist/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  51. package/dist/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  52. package/dist/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  53. package/dist/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  54. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  55. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  56. package/dist/contract.js +1 -0
  57. package/dist/define.d.ts +6 -1
  58. package/dist/define.js +140 -70
  59. package/dist/index.d.ts +3 -2
  60. package/dist/index.js +2 -1
  61. package/dist/lint.d.ts +1 -0
  62. package/dist/lint.js +27 -0
  63. package/dist/provider.d.ts +4 -2
  64. package/dist/provider.js +2 -1
  65. package/dist/runtime/auth-flow.js +2 -0
  66. package/dist/runtime/browser.js +203 -0
  67. package/dist/runtime/http.js +28 -8
  68. package/dist/runtime/stealth.d.ts +5 -2
  69. package/dist/runtime/stealth.js +157 -4
  70. package/dist/server/index.d.ts +4 -0
  71. package/dist/server/index.js +4 -0
  72. package/dist/server/self-test-input-tokens.d.ts +1 -0
  73. package/dist/server/self-test-input-tokens.js +37 -0
  74. package/dist/server/self-test-redaction.d.ts +20 -0
  75. package/dist/server/self-test-redaction.js +70 -0
  76. package/dist/server/self-test-token.d.ts +30 -0
  77. package/dist/server/self-test-token.js +50 -0
  78. package/dist/server/self-test.d.ts +98 -0
  79. package/dist/server/self-test.js +555 -0
  80. package/dist/server/serve.d.ts +6 -0
  81. package/dist/server/serve.js +33 -8
  82. package/dist/testing/run.js +5 -1
  83. package/dist/types.d.ts +152 -0
  84. package/package.json +10 -3
  85. package/src/auth-turn/auth-turn.v1.schema.json +89 -0
  86. package/src/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  87. package/src/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  88. package/src/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  89. package/src/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  90. package/src/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  91. package/src/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  92. package/src/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  93. package/src/auth-turn/fixtures/valid/abort.json +8 -0
  94. package/src/auth-turn/fixtures/valid/challenge.json +17 -0
  95. package/src/auth-turn/fixtures/valid/complete.json +13 -0
  96. package/src/auth-turn/fixtures/valid/form.json +14 -0
  97. package/src/auth-turn/fixtures/valid/message.json +13 -0
  98. package/src/auth-turn/fixtures/valid/multi_choice.json +15 -0
  99. package/src/auth-turn/fixtures/valid/pending.json +5 -0
  100. package/src/auth-turn/fixtures/valid/poll.json +9 -0
  101. package/src/auth-turn/fixtures/valid/redirect.json +16 -0
  102. package/src/auth-turn/fixtures/valid/retry.json +8 -0
  103. package/src/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  104. package/src/auth-turn/index.ts +177 -0
  105. package/src/auth.ts +786 -0
  106. package/src/ceremonies/index.ts +9 -43
  107. package/src/cli/create.ts +60 -97
  108. package/src/cli/templates/provider/AGENTS.md.tpl +87 -0
  109. package/src/cli/templates/provider/CLAUDE.md.tpl +1 -0
  110. package/src/cli/templates/provider/README.md.tpl +7 -4
  111. package/src/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  112. package/src/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  113. package/src/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  114. package/src/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  115. package/src/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  116. package/src/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  117. package/src/contract.ts +1 -0
  118. package/src/define.ts +198 -71
  119. package/src/index.ts +16 -0
  120. package/src/lint.ts +33 -0
  121. package/src/provider.ts +27 -0
  122. package/src/runtime/auth-flow.ts +2 -0
  123. package/src/runtime/browser.ts +293 -1
  124. package/src/runtime/http.ts +48 -7
  125. package/src/runtime/stealth.ts +190 -6
  126. package/src/server/index.ts +36 -0
  127. package/src/server/self-test-input-tokens.ts +46 -0
  128. package/src/server/self-test-redaction.ts +97 -0
  129. package/src/server/self-test-token.ts +70 -0
  130. package/src/server/self-test.ts +725 -0
  131. package/src/server/serve.ts +67 -4
  132. package/src/testing/run.ts +9 -1
  133. package/src/types.ts +188 -0
@@ -3,6 +3,106 @@ import { ProviderError } from "../errors";
3
3
  const require = createRequire(import.meta.url);
4
4
  const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
5
5
  const SELECTOR_POLL_INTERVAL_MS = 100;
6
+ const RESOURCE_POLICY_ROUTE_PATTERN = "**/*";
7
+ const DEFAULT_RESOURCE_METHODS = ["GET", "HEAD"];
8
+ function toResourceBody(body) {
9
+ if (body === undefined || typeof body === "string" || Buffer.isBuffer(body)) {
10
+ return body;
11
+ }
12
+ if (body instanceof ArrayBuffer) {
13
+ return Buffer.from(new Uint8Array(body));
14
+ }
15
+ return Buffer.from(body);
16
+ }
17
+ function isResourceMethod(method) {
18
+ return method === "GET" || method === "HEAD";
19
+ }
20
+ async function toResourceRequest(request) {
21
+ const method = request.method().toUpperCase();
22
+ if (!isResourceMethod(method)) {
23
+ return null;
24
+ }
25
+ return {
26
+ headers: await request.allHeaders(),
27
+ method,
28
+ resourceType: request.resourceType(),
29
+ url: request.url(),
30
+ };
31
+ }
32
+ function toCdpResourceRequest(params) {
33
+ if (!isRecord(params)) {
34
+ return null;
35
+ }
36
+ const requestId = params.requestId;
37
+ const rawRequest = params.request;
38
+ if (typeof requestId !== "string" || !isRecord(rawRequest)) {
39
+ return null;
40
+ }
41
+ const url = rawRequest.url;
42
+ const method = String(rawRequest.method ?? "").toUpperCase();
43
+ if (typeof url !== "string" || !isResourceMethod(method)) {
44
+ return null;
45
+ }
46
+ return {
47
+ requestId,
48
+ request: {
49
+ headers: toCdpResourceHeaders(rawRequest.headers),
50
+ method,
51
+ resourceType: typeof params.resourceType === "string" ? params.resourceType : undefined,
52
+ url,
53
+ },
54
+ };
55
+ }
56
+ function getCdpPausedRequestId(params) {
57
+ if (!isRecord(params) || typeof params.requestId !== "string") {
58
+ return null;
59
+ }
60
+ return params.requestId;
61
+ }
62
+ function toCdpResourceHeaders(value) {
63
+ if (!isRecord(value)) {
64
+ return {};
65
+ }
66
+ const headers = {};
67
+ for (const [name, headerValue] of Object.entries(value)) {
68
+ if (typeof headerValue === "string") {
69
+ headers[name] = headerValue;
70
+ }
71
+ }
72
+ return headers;
73
+ }
74
+ function matchesResourceRoute(match, request) {
75
+ if (typeof match === "string") {
76
+ return request.url === match;
77
+ }
78
+ if (match instanceof RegExp) {
79
+ return match.test(request.url);
80
+ }
81
+ return match(request);
82
+ }
83
+ function toCdpFulfillParams(requestId, decision) {
84
+ const body = toResourceBody(decision.body);
85
+ return {
86
+ ...(body === undefined
87
+ ? {}
88
+ : { body: Buffer.from(body).toString("base64") }),
89
+ ...(decision.headers === undefined
90
+ ? {}
91
+ : {
92
+ responseHeaders: Object.entries(decision.headers).map(([name, value]) => ({ name, value })),
93
+ }),
94
+ requestId,
95
+ responseCode: decision.status ?? 200,
96
+ };
97
+ }
98
+ async function fulfillResourceRoute(route, decision) {
99
+ const body = toResourceBody(decision.body);
100
+ await route.fulfill({
101
+ ...(body === undefined ? {} : { body }),
102
+ ...(decision.headers === undefined ? {} : { headers: decision.headers }),
103
+ status: decision.status ?? 200,
104
+ });
105
+ }
6
106
  function getDefaultCdpPoolUrl(env = process.env) {
7
107
  return env.APIFUSE__CDP_POOL__URL;
8
108
  }
@@ -256,6 +356,38 @@ class PlaywrightBrowserPage {
256
356
  async close() {
257
357
  await this.page.close();
258
358
  }
359
+ async withResourcePolicy(policy, run) {
360
+ const allowedMethods = new Set(policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS);
361
+ const handler = async (route) => {
362
+ const request = await toResourceRequest(route.request());
363
+ if (!request || !allowedMethods.has(request.method)) {
364
+ await route.abort("blockedbyclient");
365
+ return;
366
+ }
367
+ for (const resourceRoute of policy.routes) {
368
+ if (!matchesResourceRoute(resourceRoute.match, request)) {
369
+ continue;
370
+ }
371
+ const decision = await resourceRoute.handle(request);
372
+ switch (decision.action) {
373
+ case "fulfill":
374
+ await fulfillResourceRoute(route, decision);
375
+ return;
376
+ case "block":
377
+ await route.abort("blockedbyclient");
378
+ return;
379
+ }
380
+ }
381
+ await route.abort("blockedbyclient");
382
+ };
383
+ await this.page.route(RESOURCE_POLICY_ROUTE_PATTERN, handler);
384
+ try {
385
+ return await run();
386
+ }
387
+ finally {
388
+ await this.page.unroute(RESOURCE_POLICY_ROUTE_PATTERN, handler);
389
+ }
390
+ }
259
391
  }
260
392
  class PlaywrightBrowserClient {
261
393
  options;
@@ -737,6 +869,77 @@ class CdpPoolBrowserPage {
737
869
  await this.pageClient.close();
738
870
  }
739
871
  }
872
+ async withResourcePolicy(policy, run) {
873
+ const allowedMethods = new Set(policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS);
874
+ const handlePausedRequest = (params) => {
875
+ void this.handleResourcePolicyPausedRequest(params, policy, allowedMethods);
876
+ };
877
+ const unsubscribe = this.pageClient.on("Fetch.requestPaused", handlePausedRequest);
878
+ try {
879
+ await this.pageClient.send("Fetch.enable", {
880
+ patterns: [{ requestStage: "Request", urlPattern: "*" }],
881
+ });
882
+ }
883
+ catch (error) {
884
+ unsubscribe();
885
+ throw new ProviderError("CDP browser target does not support BrowserPage.withResourcePolicy()", {
886
+ cause: error instanceof Error ? error : undefined,
887
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
888
+ fix: "Use a Chromium CDP target with the Fetch domain enabled, or use the local Playwright browser runtime.",
889
+ });
890
+ }
891
+ try {
892
+ return await run();
893
+ }
894
+ finally {
895
+ unsubscribe();
896
+ await this.pageClient.send("Fetch.disable");
897
+ }
898
+ }
899
+ async handleResourcePolicyPausedRequest(params, policy, allowedMethods) {
900
+ const requestId = getCdpPausedRequestId(params);
901
+ if (requestId === null) {
902
+ return;
903
+ }
904
+ try {
905
+ const parsed = toCdpResourceRequest(params);
906
+ if (!parsed || !allowedMethods.has(parsed.request.method)) {
907
+ await this.failCdpResourceRequest(requestId);
908
+ return;
909
+ }
910
+ for (const resourceRoute of policy.routes) {
911
+ if (!matchesResourceRoute(resourceRoute.match, parsed.request)) {
912
+ continue;
913
+ }
914
+ const decision = await resourceRoute.handle(parsed.request);
915
+ switch (decision.action) {
916
+ case "fulfill":
917
+ await this.pageClient.send("Fetch.fulfillRequest", toCdpFulfillParams(parsed.requestId, decision));
918
+ return;
919
+ case "block":
920
+ await this.failCdpResourceRequest(parsed.requestId);
921
+ return;
922
+ }
923
+ }
924
+ await this.failCdpResourceRequest(parsed.requestId);
925
+ }
926
+ catch {
927
+ await this.failCdpResourceRequest(requestId);
928
+ }
929
+ }
930
+ async failCdpResourceRequest(requestId) {
931
+ try {
932
+ await this.pageClient.send("Fetch.failRequest", {
933
+ errorReason: "BlockedByClient",
934
+ requestId,
935
+ });
936
+ }
937
+ catch (error) {
938
+ if (error instanceof Error) {
939
+ return;
940
+ }
941
+ }
942
+ }
740
943
  async initialize() {
741
944
  if (this.initialized) {
742
945
  return;
@@ -396,7 +396,8 @@ function toHttpTransportError(error) {
396
396
  }
397
397
  async function toNativeHttpResponse(response) {
398
398
  const headers = Object.fromEntries(response.headers.entries());
399
- const rawText = await response.text();
399
+ const bodyBytes = new Uint8Array(await response.arrayBuffer());
400
+ const rawText = new TextDecoder().decode(bodyBytes);
400
401
  const data = parseHttpData(rawText, headers);
401
402
  return {
402
403
  data,
@@ -412,6 +413,8 @@ async function toNativeHttpResponse(response) {
412
413
  ok: response.status >= 200 && response.status < 300,
413
414
  status: response.status,
414
415
  text: async () => rawText,
416
+ arrayBuffer: async () => bodyBytes.buffer.slice(bodyBytes.byteOffset, bodyBytes.byteOffset + bodyBytes.byteLength),
417
+ bytes: async () => bodyBytes.slice(0),
415
418
  };
416
419
  }
417
420
  async function drainNativeResponseBody(response) {
@@ -474,12 +477,18 @@ function isAbsoluteUrl(url) {
474
477
  function resolveHttpUrl(baseUrl, url) {
475
478
  return new URL(url, baseUrl ?? DEFAULT_HTTP_BASE_URL).toString();
476
479
  }
477
- async function resolveNativeProxy(options, clientOptions, warn) {
480
+ async function resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset = 0) {
481
+ const baseProxyAttempt = clientOptions.proxyAttempt === undefined ||
482
+ !Number.isFinite(clientOptions.proxyAttempt)
483
+ ? 0
484
+ : Math.max(0, Math.floor(clientOptions.proxyAttempt));
478
485
  const resolvedProxy = await resolveProxyConfigAsync({
479
486
  proxy: options.proxy ?? clientOptions.proxy,
480
487
  upstream: clientOptions.upstream,
481
488
  apifuseConfig: clientOptions.apifuseConfig,
489
+ proxyPolicy: clientOptions.proxyPolicy,
482
490
  affinityKey: clientOptions.affinityKey,
491
+ proxyAttempt: baseProxyAttempt + proxyAttemptOffset,
483
492
  telemetry: clientOptions.telemetry,
484
493
  });
485
494
  if (resolvedProxy.shouldWarn) {
@@ -501,14 +510,15 @@ function normalizeNativeFetchBody(body) {
501
510
  copied.set(normalized);
502
511
  return copied.buffer;
503
512
  }
504
- async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes) {
513
+ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0) {
505
514
  const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
506
515
  const controller = options.timeout ? new AbortController() : undefined;
507
516
  const timeoutHandle = options.timeout
508
517
  ? setTimeout(() => controller?.abort(), options.timeout)
509
518
  : undefined;
519
+ let proxy;
510
520
  try {
511
- const proxy = await resolveNativeProxy(options, clientOptions, warn);
521
+ proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
512
522
  const requestInit = {
513
523
  headers: options.headers,
514
524
  method,
@@ -529,6 +539,7 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
529
539
  status: response.status,
530
540
  headers,
531
541
  retryable: statusRetryCodes.includes(response.status),
542
+ proxyUsed: Boolean(proxy),
532
543
  };
533
544
  }
534
545
  if (response.status >= 400 && options.throwOnHttpError !== false) {
@@ -544,7 +555,9 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
544
555
  if (error instanceof SyntaxError) {
545
556
  throw error;
546
557
  }
547
- throw toHttpTransportError(error);
558
+ const transportError = toHttpTransportError(error);
559
+ transportError.proxyUsed = Boolean(proxy);
560
+ throw transportError;
548
561
  }
549
562
  finally {
550
563
  if (timeoutHandle)
@@ -608,20 +621,25 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
608
621
  assertNoHttpTransportOverrides(options);
609
622
  const headersOptions = withClientHeaders(options, clientOptions, options.body);
610
623
  const methodName = normalizeHttpMethod(method);
611
- const retryOptions = normalizeRetryOptions(headersOptions.retry);
624
+ const explicitRetry = headersOptions.retry !== undefined;
625
+ const retryOptions = normalizeRetryOptions(headersOptions.retry) ??
626
+ (explicitRetry
627
+ ? undefined
628
+ : createRetryOptions(HttpRetryPreset.TransportTransient));
612
629
  if (retryOptions)
613
630
  validateUnsafeRetryMethods(retryOptions);
614
631
  const retryEnabled = Boolean(retryOptions &&
615
632
  retryOptions.attempts > 1 &&
616
633
  isMethodRetryable(methodName, retryOptions));
617
634
  const statusRetryEnabled = Boolean(retryEnabled &&
635
+ explicitRetry &&
618
636
  retryOptions &&
619
637
  retryOptions.statusCodes.length > 0 &&
620
638
  headersOptions.throwOnHttpError !== false);
621
639
  const attemptOptions = statusRetryEnabled
622
640
  ? { ...headersOptions, throwOnHttpError: false }
623
641
  : headersOptions;
624
- const executeOnce = () => fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined);
642
+ const executeOnce = (proxyAttemptOffset = 0) => fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined, proxyAttemptOffset);
625
643
  if (!retryEnabled || !retryOptions) {
626
644
  const outcome = await executeOnce();
627
645
  if (isHttpStatusOutcome(outcome)) {
@@ -633,7 +651,7 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
633
651
  let lastStatus;
634
652
  for (let attempt = 1; attempt <= retryOptions.attempts; attempt += 1) {
635
653
  try {
636
- const outcome = await executeOnce();
654
+ const outcome = await executeOnce(attempt - 1);
637
655
  if (isHttpStatusOutcome(outcome)) {
638
656
  lastStatus = outcome.status;
639
657
  if (outcome.retryable && attempt < retryOptions.attempts) {
@@ -663,7 +681,9 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
663
681
  catch (error) {
664
682
  lastErrorCode = retryErrorCode(error);
665
683
  lastStatus = retryErrorStatus(error);
684
+ const proxyUsed = Boolean(error.proxyUsed);
666
685
  if (attempt < retryOptions.attempts &&
686
+ (explicitRetry || proxyUsed) &&
667
687
  shouldRetryTransportError(error, retryOptions)) {
668
688
  await sleep(computeRetryDelayMs(retryOptions, attempt));
669
689
  continue;
@@ -12,7 +12,10 @@ export type StealthClientOptions = ProxyResolutionOptions & {
12
12
  insecureSkipVerify?: boolean;
13
13
  };
14
14
  };
15
- type StealthTransportResponse = Pick<ImpitResponse, "arrayBuffer" | "headers" | "json" | "ok" | "status" | "text" | "url">;
16
- export declare function normalizeResponse(response: StealthTransportResponse): Promise<StealthResponse>;
15
+ type StealthTransportResponse = Pick<ImpitResponse, "arrayBuffer" | "headers" | "json" | "ok" | "status" | "text"> & {
16
+ url?: string;
17
+ redirected?: boolean;
18
+ };
19
+ export declare function normalizeResponse(response: StealthTransportResponse, requestUrl?: string): Promise<StealthResponse>;
17
20
  export declare function createStealthClient(baseUrl: string, defaultProfileOrOptions?: string | StealthClientOptions, clientOptions?: StealthClientOptions): StealthClient;
18
21
  export {};
@@ -102,11 +102,32 @@ class CookieJarImpl {
102
102
  getAll() {
103
103
  return { ...this.cookies };
104
104
  }
105
+ has(name) {
106
+ return Object.hasOwn(this.cookies, name);
107
+ }
105
108
  toString() {
106
109
  return Object.entries(this.cookies)
107
110
  .map(([name, value]) => `${name}=${value}`)
108
111
  .join("; ");
109
112
  }
113
+ toHeader() {
114
+ return this.toString();
115
+ }
116
+ snapshot() {
117
+ return this.getAll();
118
+ }
119
+ restore(cookies) {
120
+ this.clear();
121
+ for (const [name, value] of Object.entries(cookies)) {
122
+ if (name)
123
+ this.cookies[name] = value;
124
+ }
125
+ }
126
+ clear() {
127
+ for (const name of Object.keys(this.cookies)) {
128
+ delete this.cookies[name];
129
+ }
130
+ }
110
131
  find(predicate) {
111
132
  for (const [name, value] of Object.entries(this.cookies)) {
112
133
  const cookie = `${name}=${value}`;
@@ -237,13 +258,20 @@ function splitCombinedSetCookieHeader(headerValue) {
237
258
  cookieStrings.push(finalCookie);
238
259
  return cookieStrings;
239
260
  }
240
- export async function normalizeResponse(response) {
261
+ export async function normalizeResponse(response, requestUrl) {
241
262
  const headers = Object.fromEntries(response.headers.entries());
242
263
  const cookies = new CookieJarImpl(setCookieHeadersFromResponse(response.headers));
243
- const body = await response.text();
264
+ const bodyBytes = await response.arrayBuffer();
265
+ const body = new TextDecoder().decode(bodyBytes);
244
266
  return {
245
267
  status: response.status,
246
268
  ok: response.status >= 200 && response.status < 300,
269
+ ...(response.url ? { url: response.url } : {}),
270
+ ...(response.redirected !== undefined
271
+ ? { redirected: response.redirected }
272
+ : requestUrl && response.url
273
+ ? { redirected: response.url !== requestUrl }
274
+ : {}),
247
275
  headers,
248
276
  rawHeaders: headerEntriesFromHeaders(response.headers),
249
277
  body,
@@ -251,6 +279,12 @@ export async function normalizeResponse(response) {
251
279
  json() {
252
280
  return Promise.resolve(JSON.parse(body));
253
281
  },
282
+ arrayBuffer() {
283
+ return Promise.resolve(bodyBytes.slice(0));
284
+ },
285
+ bytes() {
286
+ return Promise.resolve(new Uint8Array(bodyBytes.slice(0)));
287
+ },
254
288
  };
255
289
  }
256
290
  function normalizeBody(body) {
@@ -530,6 +564,23 @@ function normalizeMethod(method) {
530
564
  throw new SDKError(`Unsupported stealth method: ${method}`);
531
565
  }
532
566
  }
567
+ function isRedirectStatus(status) {
568
+ return [301, 302, 303, 307, 308].includes(status);
569
+ }
570
+ function nextRedirectMethod(status, method) {
571
+ if (status === 303 && method !== "HEAD")
572
+ return "GET";
573
+ if ((status === 301 || status === 302) && method === "POST")
574
+ return "GET";
575
+ return method;
576
+ }
577
+ function locationHeader(headers) {
578
+ for (const [name, value] of Object.entries(headers)) {
579
+ if (name.toLowerCase() === "location")
580
+ return value;
581
+ }
582
+ return undefined;
583
+ }
533
584
  function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
534
585
  const clients = new Map();
535
586
  let closed = false;
@@ -581,7 +632,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
581
632
  proxyHash: proxyEndpointHash(resolvedProxy.url),
582
633
  };
583
634
  }
584
- return {
635
+ const session = {
585
636
  async fetch(url, options = {}) {
586
637
  const method = normalizeMethod(options.method ?? "GET");
587
638
  const hasExplicitRetryPolicy = options.retry !== undefined;
@@ -649,13 +700,14 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
649
700
  const requestInit = {
650
701
  headers: normalizeHeaders(headers),
651
702
  method,
703
+ ...(options.redirect ? { redirect: options.redirect } : {}),
652
704
  ...(options.timeout ? { timeout: options.timeout } : {}),
653
705
  };
654
706
  if (options.body !== undefined) {
655
707
  requestInit.body = normalizeBody(options.body);
656
708
  }
657
709
  const response = await getClient(profileName, proxy, ignoreTlsErrors).fetch(requestUrl, requestInit);
658
- const normalized = await normalizeResponse(response);
710
+ const normalized = await normalizeResponse(response, requestUrl);
659
711
  cookieJar.setFromCookieStrings(setCookieHeadersFromResponse(response.headers));
660
712
  if (proxy &&
661
713
  isProxyConnectFailureResponse(response, normalized.body)) {
@@ -751,11 +803,112 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
751
803
  }
752
804
  throw normalizeStealthTransportError(lastError);
753
805
  },
806
+ cookies: cookieJar,
807
+ redirects: {
808
+ async run(options) {
809
+ const maxHops = options.maxHops === undefined || !Number.isFinite(options.maxHops)
810
+ ? 10
811
+ : Math.max(0, Math.floor(options.maxHops));
812
+ const hops = [];
813
+ let currentUrl = resolveUrl(baseUrl, options.url);
814
+ let method = normalizeMethod(options.method ?? "GET");
815
+ let body = options.body;
816
+ let response;
817
+ const visitedRequests = new Set();
818
+ const { url: _url, maxHops: _maxHops, stopWhen, params, ...fetchOptions } = options;
819
+ for (let hopIndex = 0; hopIndex <= maxHops; hopIndex += 1) {
820
+ visitedRequests.add(`${method} ${currentUrl}`);
821
+ response = await session.fetch(currentUrl, {
822
+ ...fetchOptions,
823
+ body,
824
+ method,
825
+ ...(hopIndex === 0 && params ? { params } : {}),
826
+ redirect: "manual",
827
+ throwOnHttpError: false,
828
+ });
829
+ if (!isRedirectStatus(response.status)) {
830
+ return {
831
+ final: response,
832
+ hops,
833
+ reason: "completed",
834
+ cookies: cookieJar.snapshot(),
835
+ };
836
+ }
837
+ const location = locationHeader(response.headers);
838
+ const nextUrl = location
839
+ ? new URL(location, response.url ?? currentUrl).toString()
840
+ : undefined;
841
+ const hop = {
842
+ url: response.url ?? currentUrl,
843
+ status: response.status,
844
+ method,
845
+ ...(location ? { location } : {}),
846
+ ...(nextUrl ? { nextUrl } : {}),
847
+ };
848
+ hops.push(hop);
849
+ if (stopWhen && (await stopWhen(hop))) {
850
+ return {
851
+ final: response,
852
+ hops,
853
+ reason: "stopped",
854
+ cookies: cookieJar.snapshot(),
855
+ };
856
+ }
857
+ if (!nextUrl) {
858
+ return {
859
+ final: response,
860
+ hops,
861
+ reason: "missing_location",
862
+ cookies: cookieJar.snapshot(),
863
+ };
864
+ }
865
+ if (hops.length > maxHops) {
866
+ return {
867
+ final: response,
868
+ hops,
869
+ reason: "max_hops",
870
+ cookies: cookieJar.snapshot(),
871
+ };
872
+ }
873
+ const nextMethod = nextRedirectMethod(response.status, method);
874
+ if (nextMethod !== method) {
875
+ body = undefined;
876
+ }
877
+ if (visitedRequests.has(`${nextMethod} ${nextUrl}`)) {
878
+ return {
879
+ final: response,
880
+ hops,
881
+ reason: "loop",
882
+ cookies: cookieJar.snapshot(),
883
+ };
884
+ }
885
+ method = nextMethod;
886
+ currentUrl = nextUrl;
887
+ }
888
+ if (!response) {
889
+ response = await session.fetch(currentUrl, {
890
+ ...fetchOptions,
891
+ body,
892
+ method,
893
+ ...(params ? { params } : {}),
894
+ redirect: "manual",
895
+ throwOnHttpError: false,
896
+ });
897
+ }
898
+ return {
899
+ final: response,
900
+ hops,
901
+ reason: "max_hops",
902
+ cookies: cookieJar.snapshot(),
903
+ };
904
+ },
905
+ },
754
906
  close() {
755
907
  closed = true;
756
908
  clients.clear();
757
909
  },
758
910
  };
911
+ return session;
759
912
  async function classifyProxyAuthDiagnostic(profileName, proxy) {
760
913
  try {
761
914
  const response = await getClient(profileName, proxy, false).fetch(PROXY_AUTH_DIAGNOSTIC_URL, {
@@ -1,3 +1,7 @@
1
1
  export { createServerApp, type ServeOptions, serve } from "./serve";
2
+ export { computeSelfTestPlanDigest, createSelfTestApp, createSelfTestInvoke, DEFAULT_SELF_TEST_REQUEST_BUDGET_MS, isSelfTestReadOnlyOperation, PROVIDER_RUNTIME_SELF_TEST_REQUEST_BUDGET_MS_ENV, resolveSelfTestPort, SELF_TEST_HEALTHZ_PATH, SELF_TEST_PATH, SELF_TEST_SCHEMA_VERSION, type SelfTestAppOptions, type SelfTestCaseResult, type SelfTestCaseStatus, type SelfTestOperationInvoke, type SelfTestRequest, SelfTestRequestSchema, type SelfTestResponse, } from "./self-test";
3
+ export { resolveHealthCheckInputDateTokens } from "./self-test-input-tokens";
4
+ export { collectSelfTestSensitiveValues, redactSelfTestText, SELF_TEST_MAX_TEXT_LENGTH, SELF_TEST_REDACTED_PLACEHOLDER, } from "./self-test-redaction";
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";
2
6
  export type { AuthFlowRequest, AuthFlowResponse, AuthFlowSuccessResponse, ConnectionMode, OperationConnection, OperationErrorResponse, OperationRequest, OperationResponse, OperationSuccessResponse, } from "./types";
3
7
  export { AuthFlowRequestSchema, AuthFlowSuccessResponseSchema, ConnectionModeSchema, ErrorEnvelopeSchema, OperationConnectionSchema, OperationErrorResponseSchema, OperationRequestSchema, OperationSuccessResponseSchema, } from "./types";
@@ -1,2 +1,6 @@
1
1
  export { createServerApp, serve } from "./serve";
2
+ export { computeSelfTestPlanDigest, createSelfTestApp, createSelfTestInvoke, DEFAULT_SELF_TEST_REQUEST_BUDGET_MS, isSelfTestReadOnlyOperation, PROVIDER_RUNTIME_SELF_TEST_REQUEST_BUDGET_MS_ENV, resolveSelfTestPort, SELF_TEST_HEALTHZ_PATH, SELF_TEST_PATH, SELF_TEST_SCHEMA_VERSION, SelfTestRequestSchema, } from "./self-test";
3
+ export { resolveHealthCheckInputDateTokens } from "./self-test-input-tokens";
4
+ export { collectSelfTestSensitiveValues, redactSelfTestText, SELF_TEST_MAX_TEXT_LENGTH, SELF_TEST_REDACTED_PLACEHOLDER, } from "./self-test-redaction";
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";
2
6
  export { AuthFlowRequestSchema, AuthFlowSuccessResponseSchema, ConnectionModeSchema, ErrorEnvelopeSchema, OperationConnectionSchema, OperationErrorResponseSchema, OperationRequestSchema, OperationSuccessResponseSchema, } from "./types";
@@ -0,0 +1 @@
1
+ export declare function resolveHealthCheckInputDateTokens(value: unknown, now?: Date): unknown;
@@ -0,0 +1,37 @@
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.
5
+ *
6
+ * Supported token: `+<days>d` or `+<days>d:YYYYMMDD` (1..365 days ahead, KST).
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));
12
+ const isoDate = date.toISOString().slice(0, 10);
13
+ return format === "YYYYMMDD" ? isoDate.replace(/-/g, "") : isoDate;
14
+ }
15
+ export function resolveHealthCheckInputDateTokens(value, now = new Date()) {
16
+ if (typeof value === "string") {
17
+ const relative = value.match(RELATIVE_KST_DATE_TOKEN);
18
+ if (!relative)
19
+ return value;
20
+ const daysAhead = Number(relative[1]);
21
+ if (!Number.isInteger(daysAhead) || daysAhead < 1 || daysAhead > 365) {
22
+ return value;
23
+ }
24
+ const format = relative[2]?.toUpperCase() === "YYYYMMDD" ? "YYYYMMDD" : "YYYY-MM-DD";
25
+ return dateFromKstDaysAhead(daysAhead, now, format);
26
+ }
27
+ if (Array.isArray(value)) {
28
+ return value.map((entry) => resolveHealthCheckInputDateTokens(entry, now));
29
+ }
30
+ if (value && typeof value === "object") {
31
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
32
+ key,
33
+ resolveHealthCheckInputDateTokens(entry, now),
34
+ ]));
35
+ }
36
+ return value;
37
+ }
@@ -0,0 +1,20 @@
1
+ import type { ProviderDefinition } from "../types";
2
+ export declare const SELF_TEST_REDACTED_PLACEHOLDER = "[REDACTED]";
3
+ /** Maximum length of any string echoed in a self-test response. */
4
+ export declare const SELF_TEST_MAX_TEXT_LENGTH = 300;
5
+ /**
6
+ * Collects every secret-shaped value known to the self-test runtime so probe
7
+ * results can be scrubbed before they leave the pod: master secrets, declared
8
+ * provider env secrets, health-probe credential env values, and request-supplied
9
+ * credential inputs.
10
+ */
11
+ export declare function collectSelfTestSensitiveValues(provider: ProviderDefinition, options?: {
12
+ env?: Readonly<Record<string, string | undefined>>;
13
+ credentialInputs?: Readonly<Record<string, string>>;
14
+ }): string[];
15
+ /**
16
+ * Scrubs known secret values and header-shaped credentials from a string and
17
+ * caps its length. Every string embedded in a SelfTestResponse MUST pass
18
+ * through this before serialization.
19
+ */
20
+ export declare function redactSelfTestText(text: string, sensitiveValues: readonly string[], maxLength?: number): string;