@apifuse/provider-sdk 2.1.0-beta.9 → 2.2.0-beta.2

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 (138) 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-submit-check.ts +1747 -388
  7. package/dist/auth-turn/auth-turn.v1.schema.json +89 -0
  8. package/dist/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  9. package/dist/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  10. package/dist/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  11. package/dist/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  12. package/dist/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  13. package/dist/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  14. package/dist/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  15. package/dist/auth-turn/fixtures/valid/abort.json +8 -0
  16. package/dist/auth-turn/fixtures/valid/challenge.json +17 -0
  17. package/dist/auth-turn/fixtures/valid/complete.json +13 -0
  18. package/dist/auth-turn/fixtures/valid/form.json +14 -0
  19. package/dist/auth-turn/fixtures/valid/message.json +13 -0
  20. package/dist/auth-turn/fixtures/valid/multi_choice.json +15 -0
  21. package/dist/auth-turn/fixtures/valid/pending.json +5 -0
  22. package/dist/auth-turn/fixtures/valid/poll.json +9 -0
  23. package/dist/auth-turn/fixtures/valid/redirect.json +16 -0
  24. package/dist/auth-turn/fixtures/valid/retry.json +8 -0
  25. package/dist/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  26. package/dist/auth-turn/index.d.ts +195 -0
  27. package/dist/auth-turn/index.js +133 -0
  28. package/dist/auth.d.ts +76 -0
  29. package/dist/auth.js +436 -0
  30. package/dist/ceremonies/index.js +7 -31
  31. package/dist/cli/create.js +45 -30
  32. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  33. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  34. package/dist/cli/templates/provider/AGENTS.md.tpl +87 -0
  35. package/dist/cli/templates/provider/CLAUDE.md.tpl +1 -0
  36. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  37. package/dist/cli/templates/provider/README.md.tpl +163 -0
  38. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  39. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  40. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  41. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  42. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  43. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  44. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  45. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  46. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  47. package/dist/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  48. package/dist/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  49. package/dist/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  50. package/dist/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  51. package/dist/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  52. package/dist/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  53. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  54. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  55. package/dist/contract.js +1 -0
  56. package/dist/define.d.ts +6 -1
  57. package/dist/define.js +140 -70
  58. package/dist/index.d.ts +3 -2
  59. package/dist/index.js +2 -1
  60. package/dist/lint.d.ts +1 -0
  61. package/dist/lint.js +27 -0
  62. package/dist/provider.d.ts +4 -2
  63. package/dist/provider.js +2 -1
  64. package/dist/runtime/auth-flow.js +2 -0
  65. package/dist/runtime/browser.js +203 -0
  66. package/dist/runtime/http.js +46 -336
  67. package/dist/runtime/proxy-retry-policy.d.ts +40 -0
  68. package/dist/runtime/proxy-retry-policy.js +326 -0
  69. package/dist/runtime/stealth.d.ts +5 -2
  70. package/dist/runtime/stealth.js +206 -206
  71. package/dist/server/index.d.ts +4 -0
  72. package/dist/server/index.js +4 -0
  73. package/dist/server/self-test-input-tokens.d.ts +1 -0
  74. package/dist/server/self-test-input-tokens.js +37 -0
  75. package/dist/server/self-test-redaction.d.ts +20 -0
  76. package/dist/server/self-test-redaction.js +70 -0
  77. package/dist/server/self-test-token.d.ts +30 -0
  78. package/dist/server/self-test-token.js +50 -0
  79. package/dist/server/self-test.d.ts +98 -0
  80. package/dist/server/self-test.js +555 -0
  81. package/dist/server/serve.d.ts +6 -0
  82. package/dist/server/serve.js +38 -10
  83. package/dist/server/types.d.ts +1 -0
  84. package/dist/server/types.js +1 -0
  85. package/dist/testing/run.js +5 -1
  86. package/dist/types.d.ts +150 -0
  87. package/package.json +9 -2
  88. package/src/auth-turn/auth-turn.v1.schema.json +89 -0
  89. package/src/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  90. package/src/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  91. package/src/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  92. package/src/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  93. package/src/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  94. package/src/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  95. package/src/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  96. package/src/auth-turn/fixtures/valid/abort.json +8 -0
  97. package/src/auth-turn/fixtures/valid/challenge.json +17 -0
  98. package/src/auth-turn/fixtures/valid/complete.json +13 -0
  99. package/src/auth-turn/fixtures/valid/form.json +14 -0
  100. package/src/auth-turn/fixtures/valid/message.json +13 -0
  101. package/src/auth-turn/fixtures/valid/multi_choice.json +15 -0
  102. package/src/auth-turn/fixtures/valid/pending.json +5 -0
  103. package/src/auth-turn/fixtures/valid/poll.json +9 -0
  104. package/src/auth-turn/fixtures/valid/redirect.json +16 -0
  105. package/src/auth-turn/fixtures/valid/retry.json +8 -0
  106. package/src/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  107. package/src/auth-turn/index.ts +177 -0
  108. package/src/auth.ts +786 -0
  109. package/src/ceremonies/index.ts +9 -43
  110. package/src/cli/create.ts +60 -97
  111. package/src/cli/templates/provider/AGENTS.md.tpl +87 -0
  112. package/src/cli/templates/provider/CLAUDE.md.tpl +1 -0
  113. package/src/cli/templates/provider/README.md.tpl +7 -4
  114. package/src/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  115. package/src/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  116. package/src/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  117. package/src/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  118. package/src/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  119. package/src/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  120. package/src/contract.ts +1 -0
  121. package/src/define.ts +198 -71
  122. package/src/index.ts +16 -0
  123. package/src/lint.ts +33 -0
  124. package/src/provider.ts +27 -0
  125. package/src/runtime/auth-flow.ts +2 -0
  126. package/src/runtime/browser.ts +293 -1
  127. package/src/runtime/http.ts +84 -530
  128. package/src/runtime/proxy-retry-policy.ts +469 -0
  129. package/src/runtime/stealth.ts +269 -353
  130. package/src/server/index.ts +36 -0
  131. package/src/server/self-test-input-tokens.ts +46 -0
  132. package/src/server/self-test-redaction.ts +97 -0
  133. package/src/server/self-test-token.ts +70 -0
  134. package/src/server/self-test.ts +725 -0
  135. package/src/server/serve.ts +75 -6
  136. package/src/server/types.ts +1 -0
  137. package/src/testing/run.ts +9 -1
  138. package/src/types.ts +186 -0
@@ -1,10 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { Impit } from "impit";
3
3
  import { DEFAULT_SMARTPROXY_POOL_SIZE, invalidateProxyResolutionCacheAsync, ProxyResolutionError, resolveProxyConfigAsync, SMARTPROXY_MAX_POOL_SIZE, } from "../config/loader";
4
- import { ProviderError, SDKError, TransportError } from "../errors";
4
+ import { SDKError, TransportError } from "../errors";
5
5
  import { getStealthProfile } from "../stealth/profiles";
6
- import { HttpRetryPreset, HttpRetryUnsafeMethodPolicy } from "../types";
7
- import { createProxyAuthIpDeniedError, createProxyEdgeAuthRejectedError, createProxyEdgeTlsRejectedError, createProxyPoolExhaustedError, createProxyPoolStaleError, isProxyAuthIpDeniedMessage, isProxyEdgeAuthRejectedMessage, isProxyEdgeTlsRejectedResponse, isProxyPoolRefreshableError, isProxyPoolStaleMessage, isProxyPoolStaleStatus, PROXY_AUTH_IP_DENIED_CODE, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_STALE_CODE, } from "./proxy-errors";
6
+ import { createProxyAuthIpDeniedError, createProxyEdgeAuthRejectedError, createProxyEdgeTlsRejectedError, createProxyPoolExhaustedError, createProxyPoolStaleError, isProxyAuthIpDeniedMessage, isProxyEdgeAuthRejectedMessage, isProxyEdgeTlsRejectedResponse, isProxyPoolRefreshableError, isProxyPoolStaleMessage, isProxyPoolStaleStatus, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_STALE_CODE, } from "./proxy-errors";
7
+ import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, normalizeProxyTransportRetryOptions, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy";
8
8
  import { appendQueryParams } from "./request-options";
9
9
  const DEFAULT_PROFILE = "chrome-146";
10
10
  const MISSING_PROXY_WARNING = "[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
@@ -14,31 +14,7 @@ const PROXY_CONNECT_FAILURE_CODE = "proxy_connect_failed";
14
14
  const PROXY_CONNECT_FAILURE_BODY_PATTERN = /\bproxy\b.*\b(non[\s-]?200|connect|tunnel)|\bconnect\b.*\bproxy\b|\btunnel\b/i;
15
15
  const PROXY_AUTH_DIAGNOSTIC_URL = "http://example.com/";
16
16
  const PROXY_AUTH_DIAGNOSTIC_TIMEOUT_MS = 5_000;
17
- const DEFAULT_STEALTH_RETRY_METHODS = ["GET", "HEAD", "OPTIONS"];
18
- const DEFAULT_STEALTH_RETRY_ERROR_CODES = [
19
- PROXY_CONNECT_FAILURE_CODE,
20
- "transport_network_error",
21
- "transport_timeout",
22
- ];
23
- const RATE_LIMIT_STEALTH_RETRY_ERROR_CODES = ["transport_timeout"];
24
- const KNOWN_STEALTH_RETRY_METHODS = new Set([
25
- "GET",
26
- "HEAD",
27
- "POST",
28
- "PUT",
29
- "DELETE",
30
- "OPTIONS",
31
- "TRACE",
32
- "PATCH",
33
- ]);
34
- const UNSAFE_STEALTH_RETRY_METHODS = new Set([
35
- "POST",
36
- "PUT",
37
- "PATCH",
38
- "DELETE",
39
- "TRACE",
40
- ]);
41
- const MAX_STEALTH_RETRY_ATTEMPTS = 8;
17
+ const STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES = [PROXY_CONNECT_FAILURE_CODE];
42
18
  const REMOVED_CHROME_PROFILE_NAMES = new Set([
43
19
  "chrome-120",
44
20
  "chrome-124",
@@ -102,11 +78,32 @@ class CookieJarImpl {
102
78
  getAll() {
103
79
  return { ...this.cookies };
104
80
  }
81
+ has(name) {
82
+ return Object.hasOwn(this.cookies, name);
83
+ }
105
84
  toString() {
106
85
  return Object.entries(this.cookies)
107
86
  .map(([name, value]) => `${name}=${value}`)
108
87
  .join("; ");
109
88
  }
89
+ toHeader() {
90
+ return this.toString();
91
+ }
92
+ snapshot() {
93
+ return this.getAll();
94
+ }
95
+ restore(cookies) {
96
+ this.clear();
97
+ for (const [name, value] of Object.entries(cookies)) {
98
+ if (name)
99
+ this.cookies[name] = value;
100
+ }
101
+ }
102
+ clear() {
103
+ for (const name of Object.keys(this.cookies)) {
104
+ delete this.cookies[name];
105
+ }
106
+ }
110
107
  find(predicate) {
111
108
  for (const [name, value] of Object.entries(this.cookies)) {
112
109
  const cookie = `${name}=${value}`;
@@ -237,7 +234,7 @@ function splitCombinedSetCookieHeader(headerValue) {
237
234
  cookieStrings.push(finalCookie);
238
235
  return cookieStrings;
239
236
  }
240
- export async function normalizeResponse(response) {
237
+ export async function normalizeResponse(response, requestUrl) {
241
238
  const headers = Object.fromEntries(response.headers.entries());
242
239
  const cookies = new CookieJarImpl(setCookieHeadersFromResponse(response.headers));
243
240
  const bodyBytes = await response.arrayBuffer();
@@ -245,6 +242,12 @@ export async function normalizeResponse(response) {
245
242
  return {
246
243
  status: response.status,
247
244
  ok: response.status >= 200 && response.status < 300,
245
+ ...(response.url ? { url: response.url } : {}),
246
+ ...(response.redirected !== undefined
247
+ ? { redirected: response.redirected }
248
+ : requestUrl && response.url
249
+ ? { redirected: response.url !== requestUrl }
250
+ : {}),
248
251
  headers,
249
252
  rawHeaders: headerEntriesFromHeaders(response.headers),
250
253
  body,
@@ -276,130 +279,8 @@ function isPolicyManagedProxy(options) {
276
279
  const policy = options.proxyPolicy ?? options.upstream?.proxy;
277
280
  return Boolean(policy && typeof policy === "object");
278
281
  }
279
- function isRetrySafeStealthMethod(method) {
280
- return method === "GET" || method === "HEAD" || method === "OPTIONS";
281
- }
282
- function createStealthRetryOptions(preset) {
283
- switch (preset) {
284
- case HttpRetryPreset.Off:
285
- return {
286
- attempts: 1,
287
- methods: DEFAULT_STEALTH_RETRY_METHODS,
288
- errorCodes: DEFAULT_STEALTH_RETRY_ERROR_CODES,
289
- unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
290
- };
291
- case HttpRetryPreset.AggressiveRead:
292
- return {
293
- attempts: 4,
294
- methods: DEFAULT_STEALTH_RETRY_METHODS,
295
- errorCodes: DEFAULT_STEALTH_RETRY_ERROR_CODES,
296
- unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
297
- };
298
- case HttpRetryPreset.RateLimitAware:
299
- return {
300
- attempts: 3,
301
- methods: DEFAULT_STEALTH_RETRY_METHODS,
302
- errorCodes: RATE_LIMIT_STEALTH_RETRY_ERROR_CODES,
303
- unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
304
- };
305
- case HttpRetryPreset.SafeRead:
306
- case HttpRetryPreset.TransportTransient:
307
- return {
308
- attempts: 3,
309
- methods: DEFAULT_STEALTH_RETRY_METHODS,
310
- errorCodes: DEFAULT_STEALTH_RETRY_ERROR_CODES,
311
- unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
312
- };
313
- }
314
- throw new ProviderError(`Unknown stealth retry preset: ${preset}`, {
315
- code: "retry_invalid_policy",
316
- });
317
- }
318
- function normalizeStealthRetryOptions(retry) {
319
- if (retry === undefined)
320
- return undefined;
321
- if (retry === false)
322
- return createStealthRetryOptions(HttpRetryPreset.Off);
323
- if (retry === true)
324
- return createStealthRetryOptions(HttpRetryPreset.TransportTransient);
325
- if (typeof retry === "string") {
326
- if (!Object.values(HttpRetryPreset).includes(retry)) {
327
- throw new ProviderError(`Unknown stealth retry preset: ${retry}`, {
328
- code: "retry_invalid_policy",
329
- });
330
- }
331
- return createStealthRetryOptions(retry);
332
- }
333
- if (typeof retry !== "object" || retry === null || Array.isArray(retry)) {
334
- throw new ProviderError("Stealth retry policy must be a plain object", {
335
- code: "retry_invalid_policy",
336
- });
337
- }
338
- if (retry.unsafeMethodPolicy !== undefined &&
339
- !Object.values(HttpRetryUnsafeMethodPolicy).includes(retry.unsafeMethodPolicy)) {
340
- throw new ProviderError(`Unknown stealth retry unsafe method policy: ${String(retry.unsafeMethodPolicy)}`, { code: "retry_invalid_policy" });
341
- }
342
- if (retry.methods !== undefined) {
343
- if (!Array.isArray(retry.methods)) {
344
- throw new ProviderError("Stealth retry methods must be an array", {
345
- code: "retry_invalid_policy",
346
- });
347
- }
348
- const unknownMethods = retry.methods
349
- .map((method) => (typeof method === "string" ? method.toUpperCase() : ""))
350
- .filter((method) => !KNOWN_STEALTH_RETRY_METHODS.has(method));
351
- if (unknownMethods.length > 0) {
352
- throw new ProviderError(`Unknown stealth retry method(s): ${unknownMethods.join(", ")}`, { code: "retry_invalid_policy" });
353
- }
354
- }
355
- if (retry.errorCodes !== undefined) {
356
- if (!Array.isArray(retry.errorCodes) ||
357
- retry.errorCodes.some((errorCode) => typeof errorCode !== "string")) {
358
- throw new ProviderError("Stealth retry errorCodes must contain only strings", { code: "retry_invalid_policy" });
359
- }
360
- }
361
- const base = createStealthRetryOptions(retry.preset ?? HttpRetryPreset.TransportTransient);
362
- const attempts = retry.attempts === undefined || !Number.isFinite(retry.attempts)
363
- ? base.attempts
364
- : Math.max(1, Math.min(MAX_STEALTH_RETRY_ATTEMPTS, Math.floor(retry.attempts)));
365
- const normalized = {
366
- attempts,
367
- methods: retry.methods?.map((method) => method.toUpperCase()) ?? base.methods,
368
- errorCodes: retry.errorCodes ?? base.errorCodes,
369
- unsafeMethodPolicy: retry.unsafeMethodPolicy ?? base.unsafeMethodPolicy,
370
- };
371
- if (normalized.unsafeMethodPolicy !==
372
- HttpRetryUnsafeMethodPolicy.AllowExplicitUnsafe) {
373
- const unsafeMethods = normalized.methods.filter((method) => UNSAFE_STEALTH_RETRY_METHODS.has(method.toUpperCase()));
374
- if (unsafeMethods.length > 0) {
375
- throw new ProviderError(`Stealth retry methods include unsafe method(s): ${unsafeMethods.join(", ")}`, { code: "retry_unsafe_method" });
376
- }
377
- }
378
- return normalized;
379
- }
380
- function isExplicitStealthRetryAllowed(method, error, retryOptions) {
381
- if (!retryOptions || retryOptions.attempts <= 1)
382
- return false;
383
- return (retryOptions.methods.includes(method.toUpperCase()) &&
384
- retryOptions.errorCodes.includes(proxyAttemptErrorCode(error)));
385
- }
386
- function isRetryableProxyTransportError(error) {
387
- if (error instanceof TransportError) {
388
- if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
389
- return false;
390
- }
391
- return (error.code === PROXY_CONNECT_FAILURE_CODE ||
392
- error.code === "transport_network_error" ||
393
- error.code === "transport_timeout");
394
- }
395
- if (error instanceof SDKError) {
396
- return false;
397
- }
398
- const message = error instanceof Error ? error.message : String(error);
399
- return /\bproxy\b|\bnon[\s-]?200\b|\bconnect\b|\btunnel\b/i.test(message);
400
- }
401
282
  function isProxyConnectFailureResponse(response, body) {
402
- return (response.status === 0 && PROXY_CONNECT_FAILURE_BODY_PATTERN.test(body ?? ""));
283
+ return response.status === 0 && PROXY_CONNECT_FAILURE_BODY_PATTERN.test(body ?? "");
403
284
  }
404
285
  function createProxyConnectFailureError(body, cause) {
405
286
  const bodyExcerpt = (body ?? "").trim().slice(0, 1_000);
@@ -502,8 +383,7 @@ function normalizeStealthTransportError(error) {
502
383
  return createProxyEdgeAuthRejectedError(error instanceof Error ? error : undefined);
503
384
  }
504
385
  const proxyTunnelStatus = getProxyTunnelStatus(error);
505
- if (proxyTunnelStatus !== undefined &&
506
- isProxyPoolStaleStatus(proxyTunnelStatus)) {
386
+ if (proxyTunnelStatus !== undefined && isProxyPoolStaleStatus(proxyTunnelStatus)) {
507
387
  return createProxyPoolStaleError(proxyTunnelStatus, error instanceof Error ? error : undefined);
508
388
  }
509
389
  if (PROXY_CONNECT_FAILURE_BODY_PATTERN.test(message)) {
@@ -515,6 +395,9 @@ function normalizeStealthTransportError(error) {
515
395
  cause: error instanceof Error ? error : undefined,
516
396
  });
517
397
  }
398
+ function sleep(ms) {
399
+ return new Promise((resolve) => setTimeout(resolve, ms));
400
+ }
518
401
  function normalizeMethod(method) {
519
402
  switch (method.toUpperCase()) {
520
403
  case "HEAD":
@@ -537,6 +420,23 @@ function normalizeMethod(method) {
537
420
  throw new SDKError(`Unsupported stealth method: ${method}`);
538
421
  }
539
422
  }
423
+ function isRedirectStatus(status) {
424
+ return [301, 302, 303, 307, 308].includes(status);
425
+ }
426
+ function nextRedirectMethod(status, method) {
427
+ if (status === 303 && method !== "HEAD")
428
+ return "GET";
429
+ if ((status === 301 || status === 302) && method === "POST")
430
+ return "GET";
431
+ return method;
432
+ }
433
+ function locationHeader(headers) {
434
+ for (const [name, value] of Object.entries(headers)) {
435
+ if (name.toLowerCase() === "location")
436
+ return value;
437
+ }
438
+ return undefined;
439
+ }
540
440
  function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
541
441
  const clients = new Map();
542
442
  let closed = false;
@@ -564,18 +464,16 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
564
464
  return client;
565
465
  }
566
466
  async function resolveRequestProxy(options, proxyAttempt) {
567
- const rawProxyAttemptOffset = options?.proxyAttemptOffset ?? 0;
568
- const proxyAttemptOffset = Number.isFinite(rawProxyAttemptOffset)
569
- ? Math.max(0, Math.floor(rawProxyAttemptOffset))
570
- : 0;
571
467
  const resolvedProxy = await resolveProxyConfigAsync({
572
468
  proxy: options?.proxy ?? clientOptions.proxy,
573
469
  upstream: clientOptions.upstream,
574
470
  apifuseConfig: clientOptions.apifuseConfig,
575
471
  affinityKey: clientOptions.affinityKey,
576
- proxyAttempt: proxyAttempt === undefined
577
- ? proxyAttemptOffset
578
- : proxyAttemptOffset + proxyAttempt,
472
+ proxyAttempt: computeProxyAttemptIndex({
473
+ baseProxyAttempt: clientOptions.proxyAttempt,
474
+ proxyAttemptOffset: options?.proxyAttemptOffset,
475
+ retryAttemptOffset: proxyAttempt,
476
+ }),
579
477
  telemetry: clientOptions.telemetry,
580
478
  });
581
479
  if (resolvedProxy.shouldWarn && !hasWarnedMissingProxy) {
@@ -588,20 +486,32 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
588
486
  proxyHash: proxyEndpointHash(resolvedProxy.url),
589
487
  };
590
488
  }
591
- return {
489
+ const session = {
592
490
  async fetch(url, options = {}) {
593
491
  const method = normalizeMethod(options.method ?? "GET");
594
492
  const hasExplicitRetryPolicy = options.retry !== undefined;
595
- const stealthRetryOptions = normalizeStealthRetryOptions(options.retry);
493
+ const stealthRetryOptions = normalizeProxyTransportRetryOptions(options.retry, {
494
+ extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
495
+ label: "Stealth",
496
+ }) ??
497
+ (hasExplicitRetryPolicy
498
+ ? undefined
499
+ : createDefaultProxyTransportRetryOptions({
500
+ extraErrorCodes: STEALTH_PROXY_TRANSPORT_RETRY_ERROR_CODES,
501
+ label: "Stealth",
502
+ }));
503
+ if (stealthRetryOptions) {
504
+ validateUnsafeProxyTransportRetryMethods(stealthRetryOptions, "Stealth");
505
+ }
596
506
  const hasPolicyProxy = isPolicyManagedProxy(clientOptions);
597
507
  const usesPolicyAllocator = hasPolicyProxy && !options.proxy && !clientOptions.proxy;
598
- const maxAttempts = usesPolicyAllocator
599
- ? Math.max(1, Math.min(MAX_POLICY_PROXY_RETRY_ATTEMPTS, clientOptions.proxyPolicy?.session?.poolSize ??
600
- (typeof clientOptions.upstream?.proxy === "object"
601
- ? clientOptions.upstream.proxy.session?.poolSize
602
- : undefined) ??
603
- DEFAULT_SMARTPROXY_POOL_SIZE))
604
- : 1;
508
+ const retryAttemptCap = Math.max(1, stealthRetryOptions?.attempts ?? 1);
509
+ const policyProxyAttemptCap = Math.max(1, Math.min(MAX_POLICY_PROXY_RETRY_ATTEMPTS, clientOptions.proxyPolicy?.session?.poolSize ??
510
+ (typeof clientOptions.upstream?.proxy === "object"
511
+ ? clientOptions.upstream.proxy.session?.poolSize
512
+ : undefined) ??
513
+ DEFAULT_SMARTPROXY_POOL_SIZE));
514
+ const maxAttempts = usesPolicyAllocator ? policyProxyAttemptCap : retryAttemptCap;
605
515
  let lastError;
606
516
  for (let refreshAttempt = 0; refreshAttempt <= MAX_POLICY_PROXY_POOL_REFRESHES; refreshAttempt += 1) {
607
517
  let stalePoolError;
@@ -622,9 +532,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
622
532
  ...(attemptProxy?.poolIndex === undefined
623
533
  ? {}
624
534
  : { poolIndex: attemptProxy.poolIndex }),
625
- ...(attemptProxy?.proxyHash
626
- ? { proxyHash: attemptProxy.proxyHash }
627
- : {}),
535
+ ...(attemptProxy?.proxyHash ? { proxyHash: attemptProxy.proxyHash } : {}),
628
536
  outcome,
629
537
  ...(errorCode ? { errorCode } : {}),
630
538
  ...(status === undefined ? {} : { status }),
@@ -635,16 +543,14 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
635
543
  assertNoUnsupportedFingerprintOverrides(options);
636
544
  attemptProxy = await resolveRequestProxy(options, attempt);
637
545
  proxy = attemptProxy.url;
638
- if (proxy) {
546
+ if (proxy && usesPolicyAllocator) {
639
547
  if (attemptedProxies.has(proxy)) {
640
548
  break;
641
549
  }
642
550
  attemptedProxies.add(proxy);
643
551
  }
644
552
  const ignoreTlsErrors = Boolean(options.stealth?.insecureSkipVerify ??
645
- (!hasPolicyProxy &&
646
- proxy &&
647
- clientOptions.proxyStealth?.insecureSkipVerify));
553
+ (!hasPolicyProxy && proxy && clientOptions.proxyStealth?.insecureSkipVerify));
648
554
  const profileName = options.profile ?? defaultProfile;
649
555
  const requestUrl = appendQueryParams(resolveUrl(baseUrl, url), options.params);
650
556
  const headers = { ...(options.headers ?? {}) };
@@ -656,25 +562,22 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
656
562
  const requestInit = {
657
563
  headers: normalizeHeaders(headers),
658
564
  method,
565
+ ...(options.redirect ? { redirect: options.redirect } : {}),
659
566
  ...(options.timeout ? { timeout: options.timeout } : {}),
660
567
  };
661
568
  if (options.body !== undefined) {
662
569
  requestInit.body = normalizeBody(options.body);
663
570
  }
664
571
  const response = await getClient(profileName, proxy, ignoreTlsErrors).fetch(requestUrl, requestInit);
665
- const normalized = await normalizeResponse(response);
572
+ const normalized = await normalizeResponse(response, requestUrl);
666
573
  cookieJar.setFromCookieStrings(setCookieHeadersFromResponse(response.headers));
667
- if (proxy &&
668
- isProxyConnectFailureResponse(response, normalized.body)) {
574
+ if (proxy && isProxyConnectFailureResponse(response, normalized.body)) {
669
575
  throw createProxyConnectFailureError(normalized.body);
670
576
  }
671
577
  if (response.status >= 400) {
672
578
  if (proxy &&
673
579
  usesPolicyAllocator &&
674
- isProxyEdgeTlsRejectedResponse(response.status, [
675
- JSON.stringify(responseHeadersToRecord(response.headers)),
676
- normalized.body,
677
- ].join("\n"))) {
580
+ isProxyEdgeTlsRejectedResponse(response.status, [JSON.stringify(responseHeadersToRecord(response.headers)), normalized.body].join("\n"))) {
678
581
  throw createProxyEdgeTlsRejectedError(response.status);
679
582
  }
680
583
  if (proxy && isProxyAuthIpDeniedMessage(normalized.body)) {
@@ -702,9 +605,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
702
605
  const normalizedError = normalizeStealthTransportError(error);
703
606
  recordProxyAttempt("error", proxyAttemptErrorCode(normalizedError), proxyAttemptStatus(normalizedError));
704
607
  lastError = normalizedError;
705
- if (proxy &&
706
- usesPolicyAllocator &&
707
- isProxyPoolRefreshableError(normalizedError)) {
608
+ if (proxy && usesPolicyAllocator && isProxyPoolRefreshableError(normalizedError)) {
708
609
  stalePoolError = normalizedError;
709
610
  if (shouldRunProxyAuthDiagnostic(normalizedError)) {
710
611
  stalePoolDiagnosticProxy = proxy;
@@ -714,15 +615,20 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
714
615
  }
715
616
  break;
716
617
  }
717
- if (proxy &&
718
- attempt + 1 <
719
- (stealthRetryOptions
720
- ? Math.min(maxAttempts, stealthRetryOptions.attempts)
721
- : maxAttempts) &&
722
- (!hasExplicitRetryPolicy
723
- ? isRetrySafeStealthMethod(method)
724
- : isExplicitStealthRetryAllowed(method, normalizedError, stealthRetryOptions)) &&
725
- isRetryableProxyTransportError(normalizedError)) {
618
+ if (attempt + 1 <
619
+ (stealthRetryOptions
620
+ ? Math.min(maxAttempts, stealthRetryOptions.attempts)
621
+ : maxAttempts) &&
622
+ shouldRetryProxyTransportAttempt({
623
+ error: normalizedError,
624
+ explicitRetry: hasExplicitRetryPolicy,
625
+ method,
626
+ options: stealthRetryOptions,
627
+ proxyUsed: Boolean(proxy),
628
+ })) {
629
+ if (stealthRetryOptions) {
630
+ await sleep(computeProxyTransportRetryDelayMs(stealthRetryOptions, attempt + 1));
631
+ }
726
632
  continue;
727
633
  }
728
634
  throw normalizedError;
@@ -758,11 +664,112 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
758
664
  }
759
665
  throw normalizeStealthTransportError(lastError);
760
666
  },
667
+ cookies: cookieJar,
668
+ redirects: {
669
+ async run(options) {
670
+ const maxHops = options.maxHops === undefined || !Number.isFinite(options.maxHops)
671
+ ? 10
672
+ : Math.max(0, Math.floor(options.maxHops));
673
+ const hops = [];
674
+ let currentUrl = resolveUrl(baseUrl, options.url);
675
+ let method = normalizeMethod(options.method ?? "GET");
676
+ let body = options.body;
677
+ let response;
678
+ const visitedRequests = new Set();
679
+ const { url: _url, maxHops: _maxHops, stopWhen, params, ...fetchOptions } = options;
680
+ for (let hopIndex = 0; hopIndex <= maxHops; hopIndex += 1) {
681
+ visitedRequests.add(`${method} ${currentUrl}`);
682
+ response = await session.fetch(currentUrl, {
683
+ ...fetchOptions,
684
+ body,
685
+ method,
686
+ ...(hopIndex === 0 && params ? { params } : {}),
687
+ redirect: "manual",
688
+ throwOnHttpError: false,
689
+ });
690
+ if (!isRedirectStatus(response.status)) {
691
+ return {
692
+ final: response,
693
+ hops,
694
+ reason: "completed",
695
+ cookies: cookieJar.snapshot(),
696
+ };
697
+ }
698
+ const location = locationHeader(response.headers);
699
+ const nextUrl = location
700
+ ? new URL(location, response.url ?? currentUrl).toString()
701
+ : undefined;
702
+ const hop = {
703
+ url: response.url ?? currentUrl,
704
+ status: response.status,
705
+ method,
706
+ ...(location ? { location } : {}),
707
+ ...(nextUrl ? { nextUrl } : {}),
708
+ };
709
+ hops.push(hop);
710
+ if (stopWhen && (await stopWhen(hop))) {
711
+ return {
712
+ final: response,
713
+ hops,
714
+ reason: "stopped",
715
+ cookies: cookieJar.snapshot(),
716
+ };
717
+ }
718
+ if (!nextUrl) {
719
+ return {
720
+ final: response,
721
+ hops,
722
+ reason: "missing_location",
723
+ cookies: cookieJar.snapshot(),
724
+ };
725
+ }
726
+ if (hops.length > maxHops) {
727
+ return {
728
+ final: response,
729
+ hops,
730
+ reason: "max_hops",
731
+ cookies: cookieJar.snapshot(),
732
+ };
733
+ }
734
+ const nextMethod = nextRedirectMethod(response.status, method);
735
+ if (nextMethod !== method) {
736
+ body = undefined;
737
+ }
738
+ if (visitedRequests.has(`${nextMethod} ${nextUrl}`)) {
739
+ return {
740
+ final: response,
741
+ hops,
742
+ reason: "loop",
743
+ cookies: cookieJar.snapshot(),
744
+ };
745
+ }
746
+ method = nextMethod;
747
+ currentUrl = nextUrl;
748
+ }
749
+ if (!response) {
750
+ response = await session.fetch(currentUrl, {
751
+ ...fetchOptions,
752
+ body,
753
+ method,
754
+ ...(params ? { params } : {}),
755
+ redirect: "manual",
756
+ throwOnHttpError: false,
757
+ });
758
+ }
759
+ return {
760
+ final: response,
761
+ hops,
762
+ reason: "max_hops",
763
+ cookies: cookieJar.snapshot(),
764
+ };
765
+ },
766
+ },
761
767
  close() {
762
768
  closed = true;
763
769
  clients.clear();
764
770
  },
765
771
  };
772
+ return session;
766
773
  async function classifyProxyAuthDiagnostic(profileName, proxy) {
767
774
  try {
768
775
  const response = await getClient(profileName, proxy, false).fetch(PROXY_AUTH_DIAGNOSTIC_URL, {
@@ -774,10 +781,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
774
781
  }
775
782
  catch (error) {
776
783
  const message = error instanceof Error
777
- ? [
778
- error.message,
779
- error.cause instanceof Error ? error.cause.message : "",
780
- ]
784
+ ? [error.message, error.cause instanceof Error ? error.cause.message : ""]
781
785
  .filter(Boolean)
782
786
  .join(" ")
783
787
  : String(error);
@@ -805,12 +809,8 @@ function hasHeader(headers, name) {
805
809
  return Object.keys(headers).some((key) => key.toLowerCase() === needle);
806
810
  }
807
811
  export function createStealthClient(baseUrl, defaultProfileOrOptions = DEFAULT_PROFILE, clientOptions = {}) {
808
- const defaultProfile = typeof defaultProfileOrOptions === "string"
809
- ? defaultProfileOrOptions
810
- : DEFAULT_PROFILE;
811
- const resolvedClientOptions = typeof defaultProfileOrOptions === "string"
812
- ? clientOptions
813
- : defaultProfileOrOptions;
812
+ const defaultProfile = typeof defaultProfileOrOptions === "string" ? defaultProfileOrOptions : DEFAULT_PROFILE;
813
+ const resolvedClientOptions = typeof defaultProfileOrOptions === "string" ? clientOptions : defaultProfileOrOptions;
814
814
  let sharedSession = null;
815
815
  function getSharedSession() {
816
816
  if (!sharedSession) {
@@ -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;