@apifuse/provider-sdk 2.2.0-beta.1 → 2.2.0-beta.10

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 (203) hide show
  1. package/AUTHORING.md +106 -0
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +5 -1
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +44 -59
  6. package/bin/apifuse-create.ts +1 -1
  7. package/bin/apifuse-dev.ts +27 -52
  8. package/bin/apifuse-pack-check.ts +14 -0
  9. package/bin/apifuse-pack-smoke.ts +22 -81
  10. package/bin/apifuse-pack-types.ts +266 -0
  11. package/bin/apifuse-perf.ts +45 -127
  12. package/bin/apifuse-record.ts +53 -70
  13. package/bin/apifuse-submit-check.ts +510 -45
  14. package/bin/apifuse-sync-assets.ts +117 -0
  15. package/bin/apifuse.ts +1 -1
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/bin/submit-check-xml-semantics.ts +204 -0
  18. package/bin/submit-check-xml.ts +134 -0
  19. package/dist/auth-turn/index.d.ts +3 -3
  20. package/dist/auth.d.ts +2 -2
  21. package/dist/auth.js +9 -18
  22. package/dist/ceremonies/index.d.ts +1 -1
  23. package/dist/ceremonies/index.js +8 -18
  24. package/dist/cli/commands.d.ts +1 -1
  25. package/dist/cli/commands.js +8 -0
  26. package/dist/cli/create.d.ts +3 -0
  27. package/dist/cli/create.js +34 -35
  28. package/dist/cli/prompt-assets.d.ts +80 -0
  29. package/dist/cli/prompt-assets.js +743 -0
  30. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  31. package/dist/config/loader.d.ts +149 -8
  32. package/dist/config/loader.js +378 -89
  33. package/dist/contract-serialization.d.ts +2 -2
  34. package/dist/contract-serialization.js +3 -6
  35. package/dist/contract-types.d.ts +2 -2
  36. package/dist/contract.d.ts +3 -3
  37. package/dist/contract.js +4 -6
  38. package/dist/define.d.ts +8 -1
  39. package/dist/define.js +119 -122
  40. package/dist/dev.d.ts +1 -1
  41. package/dist/dev.js +1 -1
  42. package/dist/errors.d.ts +4 -1
  43. package/dist/errors.js +48 -0
  44. package/dist/i18n/catalog.d.ts +2 -2
  45. package/dist/i18n/catalog.js +4 -10
  46. package/dist/i18n/index.d.ts +2 -2
  47. package/dist/i18n/index.js +2 -2
  48. package/dist/i18n/keys.d.ts +2 -2
  49. package/dist/index.d.ts +44 -42
  50. package/dist/index.js +39 -37
  51. package/dist/lint.js +8 -15
  52. package/dist/provider.d.ts +11 -11
  53. package/dist/provider.js +9 -9
  54. package/dist/public-schema-field-lint.d.ts +1 -1
  55. package/dist/recipes/gov-api.js +1 -1
  56. package/dist/runtime/auth-flow.d.ts +1 -1
  57. package/dist/runtime/auth-flow.js +3 -3
  58. package/dist/runtime/browser.d.ts +1 -1
  59. package/dist/runtime/browser.js +15 -29
  60. package/dist/runtime/cache.d.ts +1 -1
  61. package/dist/runtime/cache.js +4 -8
  62. package/dist/runtime/choice.d.ts +1 -1
  63. package/dist/runtime/choice.js +31 -35
  64. package/dist/runtime/credential.d.ts +1 -1
  65. package/dist/runtime/credential.js +1 -1
  66. package/dist/runtime/env.d.ts +1 -1
  67. package/dist/runtime/executor.d.ts +1 -1
  68. package/dist/runtime/executor.js +15 -3
  69. package/dist/runtime/http.d.ts +2 -2
  70. package/dist/runtime/http.js +144 -348
  71. package/dist/runtime/insights.d.ts +1 -1
  72. package/dist/runtime/insights.js +6 -13
  73. package/dist/runtime/instrumentation.d.ts +2 -2
  74. package/dist/runtime/instrumentation.js +56 -19
  75. package/dist/runtime/keyring.js +1 -1
  76. package/dist/runtime/namespace.js +1 -1
  77. package/dist/runtime/otlp.d.ts +1 -1
  78. package/dist/runtime/perf.d.ts +1 -1
  79. package/dist/runtime/provider.d.ts +1 -1
  80. package/dist/runtime/provider.js +1 -2
  81. package/dist/runtime/proxy-errors.d.ts +1 -1
  82. package/dist/runtime/proxy-errors.js +9 -7
  83. package/dist/runtime/proxy-nodemaven.d.ts +35 -0
  84. package/dist/runtime/proxy-nodemaven.js +128 -0
  85. package/dist/runtime/proxy-retry-policy.d.ts +40 -0
  86. package/dist/runtime/proxy-retry-policy.js +326 -0
  87. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  88. package/dist/runtime/proxy-telemetry.js +55 -52
  89. package/dist/runtime/redis.d.ts +1 -1
  90. package/dist/runtime/redis.js +2 -4
  91. package/dist/runtime/request-options.d.ts +1 -1
  92. package/dist/runtime/secrets.d.ts +27 -0
  93. package/dist/runtime/secrets.js +51 -0
  94. package/dist/runtime/state.d.ts +2 -2
  95. package/dist/runtime/state.js +15 -4
  96. package/dist/runtime/stealth.d.ts +2 -2
  97. package/dist/runtime/stealth.js +108 -212
  98. package/dist/runtime/stt.d.ts +1 -1
  99. package/dist/runtime/stt.js +11 -15
  100. package/dist/runtime/trace.d.ts +2 -2
  101. package/dist/runtime/trace.js +2 -4
  102. package/dist/runtime/waterfall.d.ts +1 -1
  103. package/dist/schema.d.ts +1 -1
  104. package/dist/schema.js +7 -15
  105. package/dist/serve.d.ts +1 -1
  106. package/dist/serve.js +1 -1
  107. package/dist/server/index.d.ts +7 -7
  108. package/dist/server/index.js +6 -6
  109. package/dist/server/self-test-redaction.d.ts +1 -1
  110. package/dist/server/self-test-redaction.js +1 -1
  111. package/dist/server/self-test.d.ts +104 -3
  112. package/dist/server/self-test.js +673 -115
  113. package/dist/server/serve.d.ts +8 -3
  114. package/dist/server/serve.js +105 -59
  115. package/dist/server/types.d.ts +10 -9
  116. package/dist/server/types.js +3 -7
  117. package/dist/stealth/profiles.d.ts +1 -1
  118. package/dist/stealth/profiles.js +5 -14
  119. package/dist/stream.d.ts +1 -1
  120. package/dist/testing/index.d.ts +2 -2
  121. package/dist/testing/index.js +2 -2
  122. package/dist/testing/run.d.ts +1 -1
  123. package/dist/testing/run.js +7 -14
  124. package/dist/types.d.ts +87 -1
  125. package/dist/user-input.d.ts +30 -0
  126. package/dist/user-input.js +66 -0
  127. package/package.json +9 -5
  128. package/src/auth-turn/index.ts +1 -1
  129. package/src/auth.ts +28 -86
  130. package/src/ceremonies/index.ts +24 -78
  131. package/src/cli/commands.ts +10 -0
  132. package/src/cli/create.ts +42 -35
  133. package/src/cli/prompt-assets.ts +865 -0
  134. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  135. package/src/config/loader.ts +543 -208
  136. package/src/contract-serialization.ts +5 -11
  137. package/src/contract-types.ts +2 -2
  138. package/src/contract.ts +12 -28
  139. package/src/define.ts +247 -505
  140. package/src/dev.ts +4 -9
  141. package/src/errors.ts +58 -5
  142. package/src/i18n/catalog.ts +10 -32
  143. package/src/i18n/index.ts +2 -2
  144. package/src/i18n/keys.ts +5 -11
  145. package/src/index.ts +49 -42
  146. package/src/lint.ts +89 -159
  147. package/src/provider.ts +15 -11
  148. package/src/public-schema-field-lint.ts +7 -33
  149. package/src/recipes/gov-api.ts +2 -5
  150. package/src/runtime/auth-flow.ts +5 -7
  151. package/src/runtime/browser.ts +64 -187
  152. package/src/runtime/cache.ts +20 -67
  153. package/src/runtime/choice.ts +79 -132
  154. package/src/runtime/credential.ts +2 -2
  155. package/src/runtime/env.ts +1 -1
  156. package/src/runtime/executor.ts +23 -20
  157. package/src/runtime/http.ts +195 -559
  158. package/src/runtime/insights.ts +15 -53
  159. package/src/runtime/instrumentation.ts +78 -65
  160. package/src/runtime/keyring.ts +7 -19
  161. package/src/runtime/namespace.ts +2 -7
  162. package/src/runtime/otlp.ts +12 -23
  163. package/src/runtime/perf.ts +1 -1
  164. package/src/runtime/provider.ts +4 -9
  165. package/src/runtime/proxy-errors.ts +29 -42
  166. package/src/runtime/proxy-nodemaven.ts +178 -0
  167. package/src/runtime/proxy-retry-policy.ts +469 -0
  168. package/src/runtime/proxy-telemetry.ts +79 -77
  169. package/src/runtime/redis.ts +4 -12
  170. package/src/runtime/request-options.ts +4 -13
  171. package/src/runtime/secrets.ts +64 -0
  172. package/src/runtime/state.ts +41 -110
  173. package/src/runtime/stealth.ts +162 -377
  174. package/src/runtime/stt.ts +38 -94
  175. package/src/runtime/trace.ts +14 -44
  176. package/src/runtime/waterfall.ts +5 -18
  177. package/src/schema.ts +23 -84
  178. package/src/serve.ts +1 -1
  179. package/src/server/index.ts +12 -7
  180. package/src/server/self-test-redaction.ts +2 -2
  181. package/src/server/self-test.ts +857 -132
  182. package/src/server/serve.ts +172 -245
  183. package/src/server/types.ts +7 -19
  184. package/src/stealth/profiles.ts +10 -26
  185. package/src/stream.ts +8 -19
  186. package/src/testing/index.ts +2 -2
  187. package/src/testing/run.ts +19 -67
  188. package/src/types.ts +88 -1
  189. package/src/user-input.ts +118 -0
  190. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  191. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  192. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  193. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  194. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  195. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  196. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  197. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  198. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  199. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  200. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  201. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  202. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  203. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -1,316 +1,14 @@
1
- import { resolveProxyConfigAsync } from "../config/loader";
2
- import { ProviderError, TransportError } from "../errors";
3
- import { parseSseStream, readableBytes, readableLines, readableTextChunks, } from "../stream";
4
- import { HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, } from "../types";
5
- import { appendQueryParams, normalizeHttpRequestBody } from "./request-options";
1
+ import { policyRotatesTransportVendorChain, resolvePolicyTransportAttemptCap, resolveProxyConfigAsync, } from "../config/loader.js";
2
+ import { ProviderError, TransportError } from "../errors.js";
3
+ import { parseSseStream, readableBytes, readableLines, readableTextChunks } from "../stream.js";
4
+ import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, isProxyTransportRetryMethod, normalizeProxyTransportRetryOptions, proxyTransportRetryErrorCode, proxyTransportRetryErrorStatus, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy.js";
5
+ import { appendQueryParams, normalizeHttpRequestBody } from "./request-options.js";
6
6
  const DEFAULT_HTTP_BASE_URL = "http://localhost";
7
7
  function isHttpStatusOutcome(outcome) {
8
8
  return "kind" in outcome && outcome.kind === "http-status";
9
9
  }
10
- const DEFAULT_RETRY_METHODS = ["GET", "HEAD", "OPTIONS"];
11
- const DEFAULT_RETRY_ERROR_CODES = [
12
- "transport_network_error",
13
- "transport_timeout",
14
- ];
15
- const SAFE_RETRY_STATUS_CODES = [408, 429, 500, 502, 503, 504];
16
- const RATE_LIMIT_RETRY_STATUS_CODES = [429, 503];
17
- const KNOWN_RETRY_METHODS = new Set([
18
- "GET",
19
- "HEAD",
20
- "POST",
21
- "PUT",
22
- "DELETE",
23
- "OPTIONS",
24
- "TRACE",
25
- "PATCH",
26
- ]);
27
- const UNSAFE_RETRY_METHODS = new Set([
28
- "POST",
29
- "PUT",
30
- "PATCH",
31
- "DELETE",
32
- "TRACE",
33
- ]);
34
- const MAX_RETRY_ATTEMPTS = 8;
35
- const MAX_RETRY_DELAY_MS = 30_000;
36
- function hasOwnValue(values, value) {
37
- if (typeof value !== "string")
38
- return false;
39
- return Object.values(values).some((candidate) => candidate === value);
40
- }
41
- function createInvalidRetryPolicyError(message) {
42
- return new ProviderError(message, { code: "retry_invalid_policy" });
43
- }
44
- function createRetryOptions(preset) {
45
- switch (preset) {
46
- case HttpRetryPreset.Off:
47
- return {
48
- preset,
49
- attempts: 1,
50
- methods: DEFAULT_RETRY_METHODS,
51
- statusCodes: [],
52
- errorCodes: DEFAULT_RETRY_ERROR_CODES,
53
- delayStrategy: HttpRetryDelayStrategy.Exponential,
54
- baseDelayMs: 100,
55
- maxDelayMs: 1_000,
56
- jitter: HttpRetryJitter.Full,
57
- retryAfter: HttpRetryAfterPolicy.Ignore,
58
- unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
59
- };
60
- case HttpRetryPreset.SafeRead:
61
- return {
62
- preset,
63
- attempts: 3,
64
- methods: DEFAULT_RETRY_METHODS,
65
- statusCodes: SAFE_RETRY_STATUS_CODES,
66
- errorCodes: DEFAULT_RETRY_ERROR_CODES,
67
- delayStrategy: HttpRetryDelayStrategy.Exponential,
68
- baseDelayMs: 100,
69
- maxDelayMs: 2_000,
70
- jitter: HttpRetryJitter.Full,
71
- retryAfter: HttpRetryAfterPolicy.Cap,
72
- unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
73
- };
74
- case HttpRetryPreset.AggressiveRead:
75
- return {
76
- preset,
77
- attempts: 4,
78
- methods: DEFAULT_RETRY_METHODS,
79
- statusCodes: SAFE_RETRY_STATUS_CODES,
80
- errorCodes: DEFAULT_RETRY_ERROR_CODES,
81
- delayStrategy: HttpRetryDelayStrategy.Exponential,
82
- baseDelayMs: 150,
83
- maxDelayMs: 5_000,
84
- jitter: HttpRetryJitter.Full,
85
- retryAfter: HttpRetryAfterPolicy.Cap,
86
- unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
87
- };
88
- case HttpRetryPreset.RateLimitAware:
89
- return {
90
- preset,
91
- attempts: 3,
92
- methods: DEFAULT_RETRY_METHODS,
93
- statusCodes: RATE_LIMIT_RETRY_STATUS_CODES,
94
- errorCodes: ["transport_timeout"],
95
- delayStrategy: HttpRetryDelayStrategy.Exponential,
96
- baseDelayMs: 250,
97
- maxDelayMs: 5_000,
98
- jitter: HttpRetryJitter.Equal,
99
- retryAfter: HttpRetryAfterPolicy.Respect,
100
- unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
101
- };
102
- case HttpRetryPreset.TransportTransient:
103
- return {
104
- preset,
105
- attempts: 3,
106
- methods: DEFAULT_RETRY_METHODS,
107
- statusCodes: [],
108
- errorCodes: DEFAULT_RETRY_ERROR_CODES,
109
- delayStrategy: HttpRetryDelayStrategy.Exponential,
110
- baseDelayMs: 100,
111
- maxDelayMs: 1_000,
112
- jitter: HttpRetryJitter.Full,
113
- retryAfter: HttpRetryAfterPolicy.Ignore,
114
- unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
115
- };
116
- }
117
- throw createInvalidRetryPolicyError(`Unknown HTTP retry preset: ${preset}`);
118
- }
119
- function clampPositiveInteger(value, fallback, max) {
120
- if (value === undefined)
121
- return fallback;
122
- if (!Number.isFinite(value) || value < 1)
123
- return fallback;
124
- return Math.min(Math.floor(value), max);
125
- }
126
- function clampDelay(value, fallback) {
127
- if (value === undefined)
128
- return fallback;
129
- if (!Number.isFinite(value) || value < 0)
130
- return fallback;
131
- return Math.min(Math.floor(value), MAX_RETRY_DELAY_MS);
132
- }
133
- function normalizeRetryOptions(retry) {
134
- if (retry === undefined || retry === false) {
135
- return undefined;
136
- }
137
- if (retry === true) {
138
- return createRetryOptions(HttpRetryPreset.TransportTransient);
139
- }
140
- if (typeof retry === "string") {
141
- if (!hasOwnValue(HttpRetryPreset, retry)) {
142
- throw createInvalidRetryPolicyError(`Unknown HTTP retry preset: ${retry}`);
143
- }
144
- return createRetryOptions(retry);
145
- }
146
- if (typeof retry !== "object" || retry === null) {
147
- throw createInvalidRetryPolicyError("HTTP retry policy must be an object");
148
- }
149
- if (Array.isArray(retry)) {
150
- throw createInvalidRetryPolicyError("HTTP retry policy must be a plain object");
151
- }
152
- validateRetryOptionsShape(retry);
153
- const base = createRetryOptions(retry.preset ?? HttpRetryPreset.TransportTransient);
154
- const maxDelayMs = clampDelay(retry.maxDelayMs, base.maxDelayMs);
155
- const normalized = {
156
- preset: retry.preset ?? base.preset,
157
- attempts: clampPositiveInteger(retry.attempts, base.attempts, MAX_RETRY_ATTEMPTS),
158
- methods: retry.methods?.map((method) => method.toUpperCase()) ?? base.methods,
159
- statusCodes: retry.statusCodes?.filter((status) => Number.isInteger(status)) ??
160
- base.statusCodes,
161
- errorCodes: retry.errorCodes ?? base.errorCodes,
162
- delayStrategy: retry.delayStrategy ?? base.delayStrategy,
163
- baseDelayMs: clampDelay(retry.baseDelayMs, base.baseDelayMs),
164
- maxDelayMs,
165
- jitter: retry.jitter ?? base.jitter,
166
- retryAfter: retry.retryAfter ?? base.retryAfter,
167
- unsafeMethodPolicy: retry.unsafeMethodPolicy ?? base.unsafeMethodPolicy,
168
- };
169
- return normalized;
170
- }
171
- function validateRetryOptionsShape(retry) {
172
- if (retry.preset !== undefined &&
173
- !hasOwnValue(HttpRetryPreset, retry.preset)) {
174
- throw createInvalidRetryPolicyError(`Unknown HTTP retry preset: ${String(retry.preset)}`);
175
- }
176
- if (retry.delayStrategy !== undefined &&
177
- !hasOwnValue(HttpRetryDelayStrategy, retry.delayStrategy)) {
178
- throw createInvalidRetryPolicyError(`Unknown HTTP retry delay strategy: ${String(retry.delayStrategy)}`);
179
- }
180
- if (retry.jitter !== undefined &&
181
- !hasOwnValue(HttpRetryJitter, retry.jitter)) {
182
- throw createInvalidRetryPolicyError(`Unknown HTTP retry jitter policy: ${String(retry.jitter)}`);
183
- }
184
- if (retry.retryAfter !== undefined &&
185
- !hasOwnValue(HttpRetryAfterPolicy, retry.retryAfter)) {
186
- throw createInvalidRetryPolicyError(`Unknown HTTP retry-after policy: ${String(retry.retryAfter)}`);
187
- }
188
- if (retry.unsafeMethodPolicy !== undefined &&
189
- !hasOwnValue(HttpRetryUnsafeMethodPolicy, retry.unsafeMethodPolicy)) {
190
- throw createInvalidRetryPolicyError(`Unknown HTTP retry unsafe method policy: ${String(retry.unsafeMethodPolicy)}`);
191
- }
192
- if (retry.methods !== undefined) {
193
- if (!Array.isArray(retry.methods)) {
194
- throw createInvalidRetryPolicyError("HTTP retry methods must be an array");
195
- }
196
- const nonStringMethods = retry.methods.filter((method) => typeof method !== "string");
197
- if (nonStringMethods.length > 0) {
198
- throw createInvalidRetryPolicyError("HTTP retry methods must contain only strings");
199
- }
200
- const unknownMethods = retry.methods
201
- .map((method) => method.toUpperCase())
202
- .filter((method) => !KNOWN_RETRY_METHODS.has(method));
203
- if (unknownMethods.length > 0) {
204
- throw createInvalidRetryPolicyError(`Unknown HTTP retry method(s): ${unknownMethods.join(", ")}`);
205
- }
206
- }
207
- if (retry.statusCodes !== undefined) {
208
- if (!Array.isArray(retry.statusCodes)) {
209
- throw createInvalidRetryPolicyError("HTTP retry statusCodes must be an array");
210
- }
211
- const invalidStatusCodes = retry.statusCodes.filter((status) => !Number.isInteger(status) ||
212
- Number(status) < 100 ||
213
- Number(status) > 599);
214
- if (invalidStatusCodes.length > 0) {
215
- throw createInvalidRetryPolicyError("HTTP retry statusCodes must contain HTTP status integers in [100, 599]");
216
- }
217
- }
218
- if (retry.errorCodes !== undefined) {
219
- if (!Array.isArray(retry.errorCodes)) {
220
- throw createInvalidRetryPolicyError("HTTP retry errorCodes must be an array");
221
- }
222
- const nonStringErrorCodes = retry.errorCodes.filter((errorCode) => typeof errorCode !== "string");
223
- if (nonStringErrorCodes.length > 0) {
224
- throw createInvalidRetryPolicyError("HTTP retry errorCodes must contain only strings");
225
- }
226
- }
227
- if (retry.preset === HttpRetryPreset.Off &&
228
- ((retry.attempts !== undefined && retry.attempts > 1) ||
229
- (retry.statusCodes !== undefined && retry.statusCodes.length > 0))) {
230
- throw createInvalidRetryPolicyError("HTTP retry preset off cannot be combined with retry-enabling overrides");
231
- }
232
- }
233
- function validateUnsafeRetryMethods(options) {
234
- if (options.unsafeMethodPolicy ===
235
- HttpRetryUnsafeMethodPolicy.AllowExplicitUnsafe) {
236
- return;
237
- }
238
- const unsafeMethods = options.methods.filter((method) => UNSAFE_RETRY_METHODS.has(method.toUpperCase()));
239
- if (unsafeMethods.length === 0)
240
- return;
241
- throw new ProviderError(`HTTP retry methods include unsafe method(s): ${unsafeMethods.join(", ")}`, { code: "retry_unsafe_method" });
242
- }
243
- function isMethodRetryable(method, options) {
244
- return options.methods
245
- .map((allowedMethod) => allowedMethod.toUpperCase())
246
- .includes(method.toUpperCase());
247
- }
248
- function retryErrorCode(error) {
249
- if (error instanceof TransportError) {
250
- return error.code;
251
- }
252
- if (error && typeof error === "object" && "code" in error) {
253
- const code = Reflect.get(error, "code");
254
- return typeof code === "string" ? code : undefined;
255
- }
256
- return undefined;
257
- }
258
- function retryErrorStatus(error) {
259
- if (error instanceof TransportError) {
260
- return error.status ?? error.upstreamStatus;
261
- }
262
- return undefined;
263
- }
264
- function shouldRetryTransportError(error, options) {
265
- const code = retryErrorCode(error);
266
- return Boolean(code && options.errorCodes.includes(code));
267
- }
268
- function retryAfterHeader(headers) {
269
- for (const [name, value] of Object.entries(headers)) {
270
- if (name.toLowerCase() === "retry-after")
271
- return value;
272
- }
273
- return undefined;
274
- }
275
- function parseRetryAfterMs(headers, now = Date.now()) {
276
- const value = retryAfterHeader(headers);
277
- if (!value)
278
- return undefined;
279
- const seconds = Number(value);
280
- if (Number.isFinite(seconds)) {
281
- return Math.max(0, Math.floor(seconds * 1_000));
282
- }
283
- const dateMs = Date.parse(value);
284
- if (!Number.isNaN(dateMs)) {
285
- return Math.max(0, dateMs - now);
286
- }
287
- return undefined;
288
- }
289
- function computeRetryDelayMs(options, attemptIndex, headers) {
290
- const multiplier = options.delayStrategy === HttpRetryDelayStrategy.Exponential
291
- ? 2 ** Math.max(0, attemptIndex - 1)
292
- : 1;
293
- const configuredDelay = Math.min(options.baseDelayMs * multiplier, options.maxDelayMs);
294
- const retryAfterMs = options.retryAfter === HttpRetryAfterPolicy.Ignore
295
- ? undefined
296
- : headers
297
- ? parseRetryAfterMs(headers)
298
- : undefined;
299
- if (retryAfterMs !== undefined) {
300
- const boundedRetryAfterMs = Math.min(retryAfterMs, options.maxDelayMs);
301
- if (options.retryAfter === HttpRetryAfterPolicy.Cap) {
302
- return Math.min(boundedRetryAfterMs, configuredDelay);
303
- }
304
- return boundedRetryAfterMs;
305
- }
306
- switch (options.jitter) {
307
- case HttpRetryJitter.None:
308
- return configuredDelay;
309
- case HttpRetryJitter.Equal:
310
- return Math.floor(configuredDelay / 2 + Math.random() * (configuredDelay / 2));
311
- case HttpRetryJitter.Full:
312
- return Math.floor(Math.random() * configuredDelay);
313
- }
10
+ function isDedupeSkipOutcome(outcome) {
11
+ return "kind" in outcome && outcome.kind === "dedupe-skip";
314
12
  }
315
13
  async function sleep(ms) {
316
14
  if (ms <= 0)
@@ -330,9 +28,7 @@ function hasHeader(headers, name) {
330
28
  }
331
29
  function withClientHeaders(options, clientOptions, body) {
332
30
  const headers = {
333
- ...(clientOptions.userAgent
334
- ? { "User-Agent": clientOptions.userAgent }
335
- : {}),
31
+ ...(clientOptions.userAgent ? { "User-Agent": clientOptions.userAgent } : {}),
336
32
  ...options?.headers,
337
33
  };
338
34
  if (body !== undefined && !hasHeader(headers, "Content-Type")) {
@@ -344,9 +40,7 @@ function withClientHeaders(options, clientOptions, body) {
344
40
  };
345
41
  }
346
42
  function parseHttpData(body, headers) {
347
- const contentType = headers["content-type"] ??
348
- headers["Content-Type"] ??
349
- headers["CONTENT-TYPE"];
43
+ const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
350
44
  if (contentType?.includes("application/json")) {
351
45
  return body ? JSON.parse(body) : null;
352
46
  }
@@ -403,12 +97,8 @@ async function toNativeHttpResponse(response) {
403
97
  data,
404
98
  headers,
405
99
  json: async () => {
406
- const contentType = headers["content-type"] ??
407
- headers["Content-Type"] ??
408
- headers["CONTENT-TYPE"];
409
- return parseJson(contentType?.includes("application/json") && !rawText
410
- ? "null"
411
- : rawText);
100
+ const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
101
+ return parseJson(contentType?.includes("application/json") && !rawText ? "null" : rawText);
412
102
  },
413
103
  ok: response.status >= 200 && response.status < 300,
414
104
  status: response.status,
@@ -478,17 +168,19 @@ function resolveHttpUrl(baseUrl, url) {
478
168
  return new URL(url, baseUrl ?? DEFAULT_HTTP_BASE_URL).toString();
479
169
  }
480
170
  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));
485
171
  const resolvedProxy = await resolveProxyConfigAsync({
486
172
  proxy: options.proxy ?? clientOptions.proxy,
487
173
  upstream: clientOptions.upstream,
488
174
  apifuseConfig: clientOptions.apifuseConfig,
489
175
  proxyPolicy: clientOptions.proxyPolicy,
490
176
  affinityKey: clientOptions.affinityKey,
491
- proxyAttempt: baseProxyAttempt + proxyAttemptOffset,
177
+ proxyAttempt: computeProxyAttemptIndex({
178
+ baseProxyAttempt: clientOptions.proxyAttempt,
179
+ retryAttemptOffset: proxyAttemptOffset,
180
+ }),
181
+ // Bun's native fetch proxy option tunnels HTTP CONNECT only; SOCKS5 is not
182
+ // supported here, so a socks5 policy fails loudly rather than downgrading.
183
+ transportProtocols: ["http"],
492
184
  telemetry: clientOptions.telemetry,
493
185
  });
494
186
  if (resolvedProxy.shouldWarn) {
@@ -510,7 +202,7 @@ function normalizeNativeFetchBody(body) {
510
202
  copied.set(normalized);
511
203
  return copied.buffer;
512
204
  }
513
- async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0) {
205
+ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0, dedupe) {
514
206
  const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
515
207
  const controller = options.timeout ? new AbortController() : undefined;
516
208
  const timeoutHandle = options.timeout
@@ -518,7 +210,20 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
518
210
  : undefined;
519
211
  let proxy;
520
212
  try {
213
+ // Resolve inside the try (and after the timeout is armed) so allocator
214
+ // failures are branded as TransportErrors and count against the request
215
+ // deadline, exactly as an inline resolve would.
521
216
  proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
217
+ // For a registry allocator chain, skip an endpoint a prior attempt already
218
+ // tried rather than re-issuing the same request. Returning the sentinel
219
+ // (instead of breaking) lets the loop keep advancing the flat offset until
220
+ // it crosses into the fallback vendor's pool span.
221
+ if (dedupe && proxy) {
222
+ if (dedupe.attempted.has(proxy)) {
223
+ return { kind: "dedupe-skip" };
224
+ }
225
+ dedupe.attempted.add(proxy);
226
+ }
522
227
  const requestInit = {
523
228
  headers: options.headers,
524
229
  method,
@@ -622,15 +327,15 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
622
327
  const headersOptions = withClientHeaders(options, clientOptions, options.body);
623
328
  const methodName = normalizeHttpMethod(method);
624
329
  const explicitRetry = headersOptions.retry !== undefined;
625
- const retryOptions = normalizeRetryOptions(headersOptions.retry) ??
626
- (explicitRetry
627
- ? undefined
628
- : createRetryOptions(HttpRetryPreset.TransportTransient));
330
+ const retryOptions = normalizeProxyTransportRetryOptions(headersOptions.retry, {
331
+ label: "HTTP",
332
+ }) ??
333
+ (explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
629
334
  if (retryOptions)
630
- validateUnsafeRetryMethods(retryOptions);
335
+ validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
631
336
  const retryEnabled = Boolean(retryOptions &&
632
337
  retryOptions.attempts > 1 &&
633
- isMethodRetryable(methodName, retryOptions));
338
+ isProxyTransportRetryMethod(methodName, retryOptions));
634
339
  const statusRetryEnabled = Boolean(retryEnabled &&
635
340
  explicitRetry &&
636
341
  retryOptions &&
@@ -639,36 +344,112 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
639
344
  const attemptOptions = statusRetryEnabled
640
345
  ? { ...headersOptions, throwOnHttpError: false }
641
346
  : headersOptions;
642
- const executeOnce = (proxyAttemptOffset = 0) => fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined, proxyAttemptOffset);
347
+ // Span the whole vendor chain on transport failures. Like ctx.stealth, a
348
+ // policy-managed proxy resolves a *different* endpoint/vendor per attempt
349
+ // (the flat proxyAttemptOffset rotates across the concatenated vendor pool
350
+ // spans), so a transport failure should advance to the next endpoint —
351
+ // potentially crossing into the fallback vendor — rather than stopping at
352
+ // the per-endpoint retry budget and stranding the request on the primary
353
+ // vendor. resolvePolicyTransportAttemptCap widens the cap to the chain span
354
+ // only for implicit, safe-method allocator requests; explicit retry
355
+ // policies (their documented `attempts` ceiling), unsafe methods, and
356
+ // static/non-registry vendors keep the retry budget. Status-code retries
357
+ // stay bounded by the retry budget regardless; only transport rotation gets
358
+ // the full span.
359
+ const policyProxy = (() => {
360
+ const policy = clientOptions.proxyPolicy ?? clientOptions.upstream?.proxy;
361
+ return policy && typeof policy === "object" ? policy : undefined;
362
+ })();
363
+ const usesPolicyAllocator = Boolean(policyProxy) && !options.proxy && !clientOptions.proxy;
364
+ const transportAttemptCap = retryOptions
365
+ ? resolvePolicyTransportAttemptCap({
366
+ policy: policyProxy,
367
+ usesPolicyAllocator,
368
+ retryAttempts: retryOptions.attempts,
369
+ explicitRetry,
370
+ method: methodName,
371
+ })
372
+ : 1;
373
+ // Track resolved endpoints across a policy-allocator chain. Successive
374
+ // attempts rotate the flat offset across the concatenated vendor pool
375
+ // spans, but an under-filled allocation (fewer live endpoints than the
376
+ // configured pool size) makes the modulo mapping repeat endpoints before
377
+ // the offset reaches the next vendor. Rather than re-hammering an
378
+ // already-tried endpoint under backoff, fetchNativeHttp returns a skip
379
+ // sentinel for a duplicate; the loop then advances the flat offset without
380
+ // issuing the request, so it keeps walking toward — and into — the fallback
381
+ // vendor's pool span instead of stalling on the primary vendor.
382
+ // De-duplication is gated on the SAME predicate that widens the attempt cap
383
+ // (implicit, safe-method, registry-chain rotation). It must NOT engage for
384
+ // an explicit retry policy: there the caller's `attempts` count is the
385
+ // contract and each attempt must issue against whatever endpoint it resolves
386
+ // — even a repeat — instead of being silently skipped (which would collapse
387
+ // a `poolSize: 1` + `attempts: 3` request to a single fetch).
388
+ const dedupeAllocatorEndpoints = policyRotatesTransportVendorChain({
389
+ policy: policyProxy,
390
+ usesPolicyAllocator,
391
+ explicitRetry,
392
+ method: methodName,
393
+ });
394
+ const dedupeContext = dedupeAllocatorEndpoints
395
+ ? { attempted: new Set() }
396
+ : undefined;
397
+ const executeOnce = (proxyAttemptOffset = 0) => fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined, proxyAttemptOffset, dedupeContext);
643
398
  if (!retryEnabled || !retryOptions) {
644
399
  const outcome = await executeOnce();
400
+ if (isDedupeSkipOutcome(outcome)) {
401
+ // Single-shot path never de-duplicates (dedupeContext is undefined),
402
+ // but keep the union total.
403
+ throw new TransportError("HTTP request produced no terminal result", {
404
+ code: "retry_exhausted",
405
+ });
406
+ }
645
407
  if (isHttpStatusOutcome(outcome)) {
646
408
  throw toUpstreamHttpError(outcome.status);
647
409
  }
648
410
  return outcome;
649
411
  }
412
+ let lastError;
650
413
  let lastErrorCode;
651
414
  let lastStatus;
652
- for (let attempt = 1; attempt <= retryOptions.attempts; attempt += 1) {
415
+ // `attempt` walks the flat proxy offset across the full chain span; `issued`
416
+ // counts requests that were actually sent (skipped duplicate offsets do not
417
+ // increment it). Retry summaries and the status-retry budget must reflect
418
+ // issued requests, not the raw offset, so they stay accurate when partial
419
+ // allocations skip offsets.
420
+ let issued = 0;
421
+ for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
422
+ // Whether this offset actually issued a request (vs. a skipped duplicate),
423
+ // so the catch counts a thrown *transport* failure once without
424
+ // double-counting a status outcome that already incremented before it
425
+ // re-threw as an upstream HTTP error.
426
+ let issuedThisAttempt = false;
653
427
  try {
654
428
  const outcome = await executeOnce(attempt - 1);
429
+ if (isDedupeSkipOutcome(outcome)) {
430
+ // Duplicate endpoint from a partial allocation: advance the flat
431
+ // offset without issuing the request (no backoff, not a failure) so
432
+ // the loop keeps rotating toward the fallback vendor.
433
+ continue;
434
+ }
435
+ issued += 1;
436
+ issuedThisAttempt = true;
655
437
  if (isHttpStatusOutcome(outcome)) {
656
438
  lastStatus = outcome.status;
657
- if (outcome.retryable && attempt < retryOptions.attempts) {
658
- await sleep(computeRetryDelayMs(retryOptions, attempt, outcome.headers));
439
+ if (outcome.retryable && issued < retryOptions.attempts) {
440
+ await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers));
659
441
  continue;
660
442
  }
661
443
  throw toUpstreamHttpError(outcome.status);
662
444
  }
663
445
  const response = outcome;
664
- if (response.status >= 400 &&
665
- headersOptions.throwOnHttpError !== false) {
446
+ if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
666
447
  throw toUpstreamHttpError(response.status);
667
448
  }
668
- if (attempt > 1) {
449
+ if (issued > 1) {
669
450
  const summary = {
670
- attempts: attempt,
671
- retries: attempt - 1,
451
+ attempts: issued,
452
+ retries: issued - 1,
672
453
  ...(retryOptions.preset ? { preset: retryOptions.preset } : {}),
673
454
  transport: "native",
674
455
  ...(lastErrorCode ? { lastErrorCode } : {}),
@@ -679,18 +460,33 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
679
460
  return response;
680
461
  }
681
462
  catch (error) {
682
- lastErrorCode = retryErrorCode(error);
683
- lastStatus = retryErrorStatus(error);
463
+ if (!issuedThisAttempt)
464
+ issued += 1;
465
+ lastError = error;
466
+ lastErrorCode = proxyTransportRetryErrorCode(error);
467
+ lastStatus = proxyTransportRetryErrorStatus(error);
684
468
  const proxyUsed = Boolean(error.proxyUsed);
685
- if (attempt < retryOptions.attempts &&
686
- (explicitRetry || proxyUsed) &&
687
- shouldRetryTransportError(error, retryOptions)) {
688
- await sleep(computeRetryDelayMs(retryOptions, attempt));
469
+ if (attempt < transportAttemptCap &&
470
+ shouldRetryProxyTransportAttempt({
471
+ error,
472
+ explicitRetry,
473
+ method: methodName,
474
+ options: retryOptions,
475
+ proxyUsed,
476
+ })) {
477
+ await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt));
689
478
  continue;
690
479
  }
691
480
  throw error;
692
481
  }
693
482
  }
483
+ // Reached when the attempt cap is consumed without a terminal outcome —
484
+ // e.g. the final offsets of a partial allocation all resolved to
485
+ // already-tried endpoints and were skipped. Surface the last real transport
486
+ // failure rather than a synthetic exhaustion error.
487
+ if (lastError !== undefined) {
488
+ throw lastError;
489
+ }
694
490
  throw new TransportError("HTTP retry exhausted without a terminal result", {
695
491
  code: "retry_exhausted",
696
492
  });
@@ -1,4 +1,4 @@
1
- import type { Span } from "./trace";
1
+ import type { Span } from "./trace.js";
2
2
  export type InsightSeverity = "info" | "warning" | "error";
3
3
  export type Insight = {
4
4
  id: string;
@@ -1,4 +1,4 @@
1
- import { computePercentile } from "./perf";
1
+ import { computePercentile } from "./perf.js";
2
2
  const LARGE_RESPONSE_BYTES = 100_000;
3
3
  const SLOW_TRANSFORM_MS = 10;
4
4
  const DNS_WARN_MS = 5;
@@ -188,8 +188,7 @@ function getDnsRepeatedCandidate(spans) {
188
188
  if (entry.totalCount < 2) {
189
189
  continue;
190
190
  }
191
- const avgDnsMs = entry.dnsDurations.reduce((sum, value) => sum + value, 0) /
192
- entry.dnsDurations.length;
191
+ const avgDnsMs = entry.dnsDurations.reduce((sum, value) => sum + value, 0) / entry.dnsDurations.length;
193
192
  const reuseRate = entry.reuseCount / entry.totalCount;
194
193
  if (avgDnsMs > DNS_WARN_MS && reuseRate < 0.2) {
195
194
  if (!candidate || avgDnsMs > candidate.avgDnsMs) {
@@ -218,9 +217,7 @@ function getDnsRepeatedInsight(spans) {
218
217
  }
219
218
  function getProxyOverheadInsight(spans) {
220
219
  const requestSpans = spans.filter(isRequestSpan);
221
- const proxiedDurations = requestSpans
222
- .filter(hasProxy)
223
- .map((span) => span.duration_ms);
220
+ const proxiedDurations = requestSpans.filter(hasProxy).map((span) => span.duration_ms);
224
221
  const directDurations = requestSpans
225
222
  .filter((span) => !hasProxy(span))
226
223
  .map((span) => span.duration_ms);
@@ -230,10 +227,8 @@ function getProxyOverheadInsight(spans) {
230
227
  message: "✓ Proxy overhead: insufficient proxy/direct samples",
231
228
  };
232
229
  }
233
- const proxyAvg = proxiedDurations.reduce((sum, value) => sum + value, 0) /
234
- proxiedDurations.length;
235
- const directAvg = directDurations.reduce((sum, value) => sum + value, 0) /
236
- directDurations.length;
230
+ const proxyAvg = proxiedDurations.reduce((sum, value) => sum + value, 0) / proxiedDurations.length;
231
+ const directAvg = directDurations.reduce((sum, value) => sum + value, 0) / directDurations.length;
237
232
  if (directAvg > 0 && proxyAvg >= directAvg * 2) {
238
233
  return {
239
234
  triggered: true,
@@ -254,9 +249,7 @@ function getBrowserIdleInsight(spans) {
254
249
  if (waitMs !== undefined) {
255
250
  return waitMs;
256
251
  }
257
- return span.name.toLowerCase().includes("wait")
258
- ? span.duration_ms
259
- : undefined;
252
+ return span.name.toLowerCase().includes("wait") ? span.duration_ms : undefined;
260
253
  })
261
254
  .filter((value) => value !== undefined);
262
255
  if (waits.length === 0) {
@@ -1,5 +1,5 @@
1
- import type { ProviderContext } from "../types";
2
- import { type CreateTraceContextOptions, type TraceContext } from "./trace";
1
+ import type { ProviderContext } from "../types.js";
2
+ import { type CreateTraceContextOptions, type TraceContext } from "./trace.js";
3
3
  export interface InstrumentationOptions extends CreateTraceContextOptions {
4
4
  }
5
5
  export type InstrumentedProviderContext<T extends ProviderContext> = Omit<T, "trace"> & {