@apifuse/provider-sdk 2.1.0-beta.9 → 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 (263) hide show
  1. package/AUTHORING.md +240 -0
  2. package/CHANGELOG.md +93 -0
  3. package/README.md +26 -10
  4. package/SUBMISSION.md +11 -12
  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 +36 -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 +2177 -353
  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/auth-turn.v1.schema.json +89 -0
  20. package/dist/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  21. package/dist/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  22. package/dist/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  23. package/dist/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  24. package/dist/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  25. package/dist/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  26. package/dist/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  27. package/dist/auth-turn/fixtures/valid/abort.json +8 -0
  28. package/dist/auth-turn/fixtures/valid/challenge.json +17 -0
  29. package/dist/auth-turn/fixtures/valid/complete.json +13 -0
  30. package/dist/auth-turn/fixtures/valid/form.json +14 -0
  31. package/dist/auth-turn/fixtures/valid/message.json +13 -0
  32. package/dist/auth-turn/fixtures/valid/multi_choice.json +15 -0
  33. package/dist/auth-turn/fixtures/valid/pending.json +5 -0
  34. package/dist/auth-turn/fixtures/valid/poll.json +9 -0
  35. package/dist/auth-turn/fixtures/valid/redirect.json +16 -0
  36. package/dist/auth-turn/fixtures/valid/retry.json +8 -0
  37. package/dist/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  38. package/dist/auth-turn/index.d.ts +195 -0
  39. package/dist/auth-turn/index.js +133 -0
  40. package/dist/auth.d.ts +76 -0
  41. package/dist/auth.js +427 -0
  42. package/dist/ceremonies/index.d.ts +1 -1
  43. package/dist/ceremonies/index.js +14 -48
  44. package/dist/cli/commands.d.ts +1 -1
  45. package/dist/cli/commands.js +8 -0
  46. package/dist/cli/create.d.ts +3 -0
  47. package/dist/cli/create.js +47 -33
  48. package/dist/cli/prompt-assets.d.ts +80 -0
  49. package/dist/cli/prompt-assets.js +743 -0
  50. package/dist/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  51. package/dist/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  52. package/dist/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl +57 -0
  53. package/dist/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  54. package/dist/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  55. package/dist/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl +13 -0
  56. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  57. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  58. package/dist/cli/templates/provider/AGENTS.md.tpl +96 -0
  59. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  60. package/dist/cli/templates/provider/README.md.tpl +163 -0
  61. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  62. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  63. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  64. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  65. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  66. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  67. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  68. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  69. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  70. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  71. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  72. package/dist/config/loader.d.ts +149 -8
  73. package/dist/config/loader.js +378 -89
  74. package/dist/contract-serialization.d.ts +2 -2
  75. package/dist/contract-serialization.js +3 -6
  76. package/dist/contract-types.d.ts +2 -2
  77. package/dist/contract.d.ts +3 -3
  78. package/dist/contract.js +5 -6
  79. package/dist/define.d.ts +13 -1
  80. package/dist/define.js +245 -178
  81. package/dist/dev.d.ts +1 -1
  82. package/dist/dev.js +1 -1
  83. package/dist/errors.d.ts +4 -1
  84. package/dist/errors.js +48 -0
  85. package/dist/i18n/catalog.d.ts +2 -2
  86. package/dist/i18n/catalog.js +4 -10
  87. package/dist/i18n/index.d.ts +2 -2
  88. package/dist/i18n/index.js +2 -2
  89. package/dist/i18n/keys.d.ts +2 -2
  90. package/dist/index.d.ts +44 -41
  91. package/dist/index.js +39 -36
  92. package/dist/lint.d.ts +1 -0
  93. package/dist/lint.js +35 -15
  94. package/dist/provider.d.ts +11 -9
  95. package/dist/provider.js +9 -8
  96. package/dist/public-schema-field-lint.d.ts +1 -1
  97. package/dist/recipes/gov-api.js +1 -1
  98. package/dist/runtime/auth-flow.d.ts +1 -1
  99. package/dist/runtime/auth-flow.js +4 -2
  100. package/dist/runtime/browser.d.ts +1 -1
  101. package/dist/runtime/browser.js +214 -25
  102. package/dist/runtime/cache.d.ts +1 -1
  103. package/dist/runtime/cache.js +4 -8
  104. package/dist/runtime/choice.d.ts +1 -1
  105. package/dist/runtime/choice.js +31 -35
  106. package/dist/runtime/credential.d.ts +1 -1
  107. package/dist/runtime/credential.js +1 -1
  108. package/dist/runtime/env.d.ts +1 -1
  109. package/dist/runtime/executor.d.ts +1 -1
  110. package/dist/runtime/executor.js +15 -3
  111. package/dist/runtime/http.d.ts +2 -2
  112. package/dist/runtime/http.js +160 -344
  113. package/dist/runtime/insights.d.ts +1 -1
  114. package/dist/runtime/insights.js +6 -13
  115. package/dist/runtime/instrumentation.d.ts +2 -2
  116. package/dist/runtime/instrumentation.js +56 -19
  117. package/dist/runtime/keyring.js +1 -1
  118. package/dist/runtime/namespace.js +1 -1
  119. package/dist/runtime/otlp.d.ts +1 -1
  120. package/dist/runtime/perf.d.ts +1 -1
  121. package/dist/runtime/provider.d.ts +1 -1
  122. package/dist/runtime/provider.js +1 -2
  123. package/dist/runtime/proxy-errors.d.ts +1 -1
  124. package/dist/runtime/proxy-errors.js +9 -7
  125. package/dist/runtime/proxy-nodemaven.d.ts +35 -0
  126. package/dist/runtime/proxy-nodemaven.js +128 -0
  127. package/dist/runtime/proxy-retry-policy.d.ts +40 -0
  128. package/dist/runtime/proxy-retry-policy.js +326 -0
  129. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  130. package/dist/runtime/proxy-telemetry.js +55 -52
  131. package/dist/runtime/redis.d.ts +1 -1
  132. package/dist/runtime/redis.js +2 -4
  133. package/dist/runtime/request-options.d.ts +1 -1
  134. package/dist/runtime/secrets.d.ts +27 -0
  135. package/dist/runtime/secrets.js +51 -0
  136. package/dist/runtime/state.d.ts +2 -2
  137. package/dist/runtime/state.js +15 -4
  138. package/dist/runtime/stealth.d.ts +7 -4
  139. package/dist/runtime/stealth.js +257 -215
  140. package/dist/runtime/stt.d.ts +1 -1
  141. package/dist/runtime/stt.js +11 -15
  142. package/dist/runtime/trace.d.ts +2 -2
  143. package/dist/runtime/trace.js +2 -4
  144. package/dist/runtime/waterfall.d.ts +1 -1
  145. package/dist/schema.d.ts +1 -1
  146. package/dist/schema.js +7 -15
  147. package/dist/serve.d.ts +1 -1
  148. package/dist/serve.js +1 -1
  149. package/dist/server/index.d.ts +7 -3
  150. package/dist/server/index.js +6 -2
  151. package/dist/server/self-test-input-tokens.d.ts +1 -0
  152. package/dist/server/self-test-input-tokens.js +37 -0
  153. package/dist/server/self-test-redaction.d.ts +20 -0
  154. package/dist/server/self-test-redaction.js +70 -0
  155. package/dist/server/self-test-token.d.ts +30 -0
  156. package/dist/server/self-test-token.js +50 -0
  157. package/dist/server/self-test.d.ts +199 -0
  158. package/dist/server/self-test.js +1113 -0
  159. package/dist/server/serve.d.ts +14 -3
  160. package/dist/server/serve.js +135 -64
  161. package/dist/server/types.d.ts +10 -9
  162. package/dist/server/types.js +3 -7
  163. package/dist/stealth/profiles.d.ts +1 -1
  164. package/dist/stealth/profiles.js +5 -14
  165. package/dist/stream.d.ts +1 -1
  166. package/dist/testing/index.d.ts +2 -2
  167. package/dist/testing/index.js +2 -2
  168. package/dist/testing/run.d.ts +1 -1
  169. package/dist/testing/run.js +12 -15
  170. package/dist/types.d.ts +237 -1
  171. package/dist/user-input.d.ts +30 -0
  172. package/dist/user-input.js +66 -0
  173. package/package.json +16 -5
  174. package/src/auth-turn/auth-turn.v1.schema.json +89 -0
  175. package/src/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  176. package/src/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  177. package/src/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  178. package/src/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  179. package/src/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  180. package/src/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  181. package/src/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  182. package/src/auth-turn/fixtures/valid/abort.json +8 -0
  183. package/src/auth-turn/fixtures/valid/challenge.json +17 -0
  184. package/src/auth-turn/fixtures/valid/complete.json +13 -0
  185. package/src/auth-turn/fixtures/valid/form.json +14 -0
  186. package/src/auth-turn/fixtures/valid/message.json +13 -0
  187. package/src/auth-turn/fixtures/valid/multi_choice.json +15 -0
  188. package/src/auth-turn/fixtures/valid/pending.json +5 -0
  189. package/src/auth-turn/fixtures/valid/poll.json +9 -0
  190. package/src/auth-turn/fixtures/valid/redirect.json +16 -0
  191. package/src/auth-turn/fixtures/valid/retry.json +8 -0
  192. package/src/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  193. package/src/auth-turn/index.ts +177 -0
  194. package/src/auth.ts +728 -0
  195. package/src/ceremonies/index.ts +33 -121
  196. package/src/cli/commands.ts +10 -0
  197. package/src/cli/create.ts +69 -99
  198. package/src/cli/prompt-assets.ts +865 -0
  199. package/src/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  200. package/src/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  201. package/src/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl +57 -0
  202. package/src/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  203. package/src/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  204. package/src/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl +13 -0
  205. package/src/cli/templates/provider/AGENTS.md.tpl +96 -0
  206. package/src/cli/templates/provider/README.md.tpl +7 -4
  207. package/src/config/loader.ts +543 -208
  208. package/src/contract-serialization.ts +5 -11
  209. package/src/contract-types.ts +2 -2
  210. package/src/contract.ts +13 -28
  211. package/src/define.ts +397 -528
  212. package/src/dev.ts +4 -9
  213. package/src/errors.ts +58 -5
  214. package/src/i18n/catalog.ts +10 -32
  215. package/src/i18n/index.ts +2 -2
  216. package/src/i18n/keys.ts +5 -11
  217. package/src/index.ts +64 -41
  218. package/src/lint.ts +122 -159
  219. package/src/provider.ts +40 -9
  220. package/src/public-schema-field-lint.ts +7 -33
  221. package/src/recipes/gov-api.ts +2 -5
  222. package/src/runtime/auth-flow.ts +6 -6
  223. package/src/runtime/browser.ts +320 -151
  224. package/src/runtime/cache.ts +20 -67
  225. package/src/runtime/choice.ts +79 -132
  226. package/src/runtime/credential.ts +2 -2
  227. package/src/runtime/env.ts +1 -1
  228. package/src/runtime/executor.ts +23 -20
  229. package/src/runtime/http.ts +216 -539
  230. package/src/runtime/insights.ts +15 -53
  231. package/src/runtime/instrumentation.ts +78 -65
  232. package/src/runtime/keyring.ts +7 -19
  233. package/src/runtime/namespace.ts +2 -7
  234. package/src/runtime/otlp.ts +12 -23
  235. package/src/runtime/perf.ts +1 -1
  236. package/src/runtime/provider.ts +4 -9
  237. package/src/runtime/proxy-errors.ts +29 -42
  238. package/src/runtime/proxy-nodemaven.ts +178 -0
  239. package/src/runtime/proxy-retry-policy.ts +469 -0
  240. package/src/runtime/proxy-telemetry.ts +79 -77
  241. package/src/runtime/redis.ts +4 -12
  242. package/src/runtime/request-options.ts +4 -13
  243. package/src/runtime/secrets.ts +64 -0
  244. package/src/runtime/state.ts +41 -110
  245. package/src/runtime/stealth.ts +331 -369
  246. package/src/runtime/stt.ts +38 -94
  247. package/src/runtime/trace.ts +14 -44
  248. package/src/runtime/waterfall.ts +5 -18
  249. package/src/schema.ts +23 -84
  250. package/src/serve.ts +1 -1
  251. package/src/server/index.ts +44 -3
  252. package/src/server/self-test-input-tokens.ts +46 -0
  253. package/src/server/self-test-redaction.ts +97 -0
  254. package/src/server/self-test-token.ts +70 -0
  255. package/src/server/self-test.ts +1450 -0
  256. package/src/server/serve.ts +206 -216
  257. package/src/server/types.ts +7 -19
  258. package/src/stealth/profiles.ts +10 -26
  259. package/src/stream.ts +8 -19
  260. package/src/testing/index.ts +2 -2
  261. package/src/testing/run.ts +24 -64
  262. package/src/types.ts +274 -1
  263. package/src/user-input.ts +118 -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
  }
@@ -396,22 +90,21 @@ function toHttpTransportError(error) {
396
90
  }
397
91
  async function toNativeHttpResponse(response) {
398
92
  const headers = Object.fromEntries(response.headers.entries());
399
- const rawText = await response.text();
93
+ const bodyBytes = new Uint8Array(await response.arrayBuffer());
94
+ const rawText = new TextDecoder().decode(bodyBytes);
400
95
  const data = parseHttpData(rawText, headers);
401
96
  return {
402
97
  data,
403
98
  headers,
404
99
  json: async () => {
405
- const contentType = headers["content-type"] ??
406
- headers["Content-Type"] ??
407
- headers["CONTENT-TYPE"];
408
- return parseJson(contentType?.includes("application/json") && !rawText
409
- ? "null"
410
- : rawText);
100
+ const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
101
+ return parseJson(contentType?.includes("application/json") && !rawText ? "null" : rawText);
411
102
  },
412
103
  ok: response.status >= 200 && response.status < 300,
413
104
  status: response.status,
414
105
  text: async () => rawText,
106
+ arrayBuffer: async () => bodyBytes.buffer.slice(bodyBytes.byteOffset, bodyBytes.byteOffset + bodyBytes.byteLength),
107
+ bytes: async () => bodyBytes.slice(0),
415
108
  };
416
109
  }
417
110
  async function drainNativeResponseBody(response) {
@@ -474,12 +167,20 @@ function isAbsoluteUrl(url) {
474
167
  function resolveHttpUrl(baseUrl, url) {
475
168
  return new URL(url, baseUrl ?? DEFAULT_HTTP_BASE_URL).toString();
476
169
  }
477
- async function resolveNativeProxy(options, clientOptions, warn) {
170
+ async function resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset = 0) {
478
171
  const resolvedProxy = await resolveProxyConfigAsync({
479
172
  proxy: options.proxy ?? clientOptions.proxy,
480
173
  upstream: clientOptions.upstream,
481
174
  apifuseConfig: clientOptions.apifuseConfig,
175
+ proxyPolicy: clientOptions.proxyPolicy,
482
176
  affinityKey: clientOptions.affinityKey,
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"],
483
184
  telemetry: clientOptions.telemetry,
484
185
  });
485
186
  if (resolvedProxy.shouldWarn) {
@@ -501,14 +202,28 @@ function normalizeNativeFetchBody(body) {
501
202
  copied.set(normalized);
502
203
  return copied.buffer;
503
204
  }
504
- async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes) {
205
+ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0, dedupe) {
505
206
  const requestUrl = appendQueryParams(resolveHttpUrl(baseUrl, url), options.params);
506
207
  const controller = options.timeout ? new AbortController() : undefined;
507
208
  const timeoutHandle = options.timeout
508
209
  ? setTimeout(() => controller?.abort(), options.timeout)
509
210
  : undefined;
211
+ let proxy;
510
212
  try {
511
- const proxy = await resolveNativeProxy(options, clientOptions, warn);
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.
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
+ }
512
227
  const requestInit = {
513
228
  headers: options.headers,
514
229
  method,
@@ -529,6 +244,7 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
529
244
  status: response.status,
530
245
  headers,
531
246
  retryable: statusRetryCodes.includes(response.status),
247
+ proxyUsed: Boolean(proxy),
532
248
  };
533
249
  }
534
250
  if (response.status >= 400 && options.throwOnHttpError !== false) {
@@ -544,7 +260,9 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
544
260
  if (error instanceof SyntaxError) {
545
261
  throw error;
546
262
  }
547
- throw toHttpTransportError(error);
263
+ const transportError = toHttpTransportError(error);
264
+ transportError.proxyUsed = Boolean(proxy);
265
+ throw transportError;
548
266
  }
549
267
  finally {
550
268
  if (timeoutHandle)
@@ -608,49 +326,130 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
608
326
  assertNoHttpTransportOverrides(options);
609
327
  const headersOptions = withClientHeaders(options, clientOptions, options.body);
610
328
  const methodName = normalizeHttpMethod(method);
611
- const retryOptions = normalizeRetryOptions(headersOptions.retry);
329
+ const explicitRetry = headersOptions.retry !== undefined;
330
+ const retryOptions = normalizeProxyTransportRetryOptions(headersOptions.retry, {
331
+ label: "HTTP",
332
+ }) ??
333
+ (explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
612
334
  if (retryOptions)
613
- validateUnsafeRetryMethods(retryOptions);
335
+ validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
614
336
  const retryEnabled = Boolean(retryOptions &&
615
337
  retryOptions.attempts > 1 &&
616
- isMethodRetryable(methodName, retryOptions));
338
+ isProxyTransportRetryMethod(methodName, retryOptions));
617
339
  const statusRetryEnabled = Boolean(retryEnabled &&
340
+ explicitRetry &&
618
341
  retryOptions &&
619
342
  retryOptions.statusCodes.length > 0 &&
620
343
  headersOptions.throwOnHttpError !== false);
621
344
  const attemptOptions = statusRetryEnabled
622
345
  ? { ...headersOptions, throwOnHttpError: false }
623
346
  : headersOptions;
624
- const executeOnce = () => fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined);
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);
625
398
  if (!retryEnabled || !retryOptions) {
626
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
+ }
627
407
  if (isHttpStatusOutcome(outcome)) {
628
408
  throw toUpstreamHttpError(outcome.status);
629
409
  }
630
410
  return outcome;
631
411
  }
412
+ let lastError;
632
413
  let lastErrorCode;
633
414
  let lastStatus;
634
- 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;
635
427
  try {
636
- const outcome = await executeOnce();
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;
637
437
  if (isHttpStatusOutcome(outcome)) {
638
438
  lastStatus = outcome.status;
639
- if (outcome.retryable && attempt < retryOptions.attempts) {
640
- await sleep(computeRetryDelayMs(retryOptions, attempt, outcome.headers));
439
+ if (outcome.retryable && issued < retryOptions.attempts) {
440
+ await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers));
641
441
  continue;
642
442
  }
643
443
  throw toUpstreamHttpError(outcome.status);
644
444
  }
645
445
  const response = outcome;
646
- if (response.status >= 400 &&
647
- headersOptions.throwOnHttpError !== false) {
446
+ if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
648
447
  throw toUpstreamHttpError(response.status);
649
448
  }
650
- if (attempt > 1) {
449
+ if (issued > 1) {
651
450
  const summary = {
652
- attempts: attempt,
653
- retries: attempt - 1,
451
+ attempts: issued,
452
+ retries: issued - 1,
654
453
  ...(retryOptions.preset ? { preset: retryOptions.preset } : {}),
655
454
  transport: "native",
656
455
  ...(lastErrorCode ? { lastErrorCode } : {}),
@@ -661,16 +460,33 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
661
460
  return response;
662
461
  }
663
462
  catch (error) {
664
- lastErrorCode = retryErrorCode(error);
665
- lastStatus = retryErrorStatus(error);
666
- if (attempt < retryOptions.attempts &&
667
- shouldRetryTransportError(error, retryOptions)) {
668
- await sleep(computeRetryDelayMs(retryOptions, attempt));
463
+ if (!issuedThisAttempt)
464
+ issued += 1;
465
+ lastError = error;
466
+ lastErrorCode = proxyTransportRetryErrorCode(error);
467
+ lastStatus = proxyTransportRetryErrorStatus(error);
468
+ const proxyUsed = Boolean(error.proxyUsed);
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));
669
478
  continue;
670
479
  }
671
480
  throw error;
672
481
  }
673
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
+ }
674
490
  throw new TransportError("HTTP retry exhausted without a terminal result", {
675
491
  code: "retry_exhausted",
676
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;