@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
@@ -0,0 +1,326 @@
1
+ import { ProviderError, TransportError } from "../errors.js";
2
+ import { HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, } from "../types.js";
3
+ export const DEFAULT_PROXY_TRANSPORT_RETRY_METHODS = ["GET", "HEAD", "OPTIONS"];
4
+ export const DEFAULT_PROXY_TRANSPORT_RETRY_ERROR_CODES = [
5
+ "transport_network_error",
6
+ "transport_timeout",
7
+ ];
8
+ const SAFE_RETRY_STATUS_CODES = [408, 429, 500, 502, 503, 504];
9
+ const RATE_LIMIT_RETRY_STATUS_CODES = [429, 503];
10
+ const RATE_LIMIT_RETRY_ERROR_CODES = ["transport_timeout"];
11
+ const KNOWN_RETRY_METHODS = new Set([
12
+ "GET",
13
+ "HEAD",
14
+ "POST",
15
+ "PUT",
16
+ "DELETE",
17
+ "OPTIONS",
18
+ "TRACE",
19
+ "PATCH",
20
+ ]);
21
+ const UNSAFE_RETRY_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE", "TRACE"]);
22
+ export const MAX_PROXY_TRANSPORT_RETRY_ATTEMPTS = 8;
23
+ const MAX_RETRY_DELAY_MS = 30_000;
24
+ function hasOwnValue(values, value) {
25
+ if (typeof value !== "string")
26
+ return false;
27
+ return Object.values(values).some((candidate) => candidate === value);
28
+ }
29
+ function createInvalidRetryPolicyError(message, label) {
30
+ return new ProviderError(message.replace("{{label}}", label), {
31
+ code: "retry_invalid_policy",
32
+ });
33
+ }
34
+ function retryErrorCode(error) {
35
+ if (error instanceof TransportError) {
36
+ return error.code;
37
+ }
38
+ if (error && typeof error === "object" && "code" in error) {
39
+ const code = Reflect.get(error, "code");
40
+ return typeof code === "string" ? code : undefined;
41
+ }
42
+ return undefined;
43
+ }
44
+ function retryErrorStatus(error) {
45
+ if (error instanceof TransportError) {
46
+ return error.status ?? error.upstreamStatus;
47
+ }
48
+ return undefined;
49
+ }
50
+ export function proxyTransportRetryErrorCode(error) {
51
+ return retryErrorCode(error);
52
+ }
53
+ export function proxyTransportRetryErrorStatus(error) {
54
+ return retryErrorStatus(error);
55
+ }
56
+ function createRetryOptions(preset, extraErrorCodes, label) {
57
+ const defaultErrorCodes = [...DEFAULT_PROXY_TRANSPORT_RETRY_ERROR_CODES, ...extraErrorCodes];
58
+ switch (preset) {
59
+ case HttpRetryPreset.Off:
60
+ return {
61
+ preset,
62
+ attempts: 1,
63
+ methods: DEFAULT_PROXY_TRANSPORT_RETRY_METHODS,
64
+ statusCodes: [],
65
+ errorCodes: defaultErrorCodes,
66
+ delayStrategy: HttpRetryDelayStrategy.Exponential,
67
+ baseDelayMs: 100,
68
+ maxDelayMs: 1_000,
69
+ jitter: HttpRetryJitter.Full,
70
+ retryAfter: HttpRetryAfterPolicy.Ignore,
71
+ unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
72
+ };
73
+ case HttpRetryPreset.SafeRead:
74
+ return {
75
+ preset,
76
+ attempts: 3,
77
+ methods: DEFAULT_PROXY_TRANSPORT_RETRY_METHODS,
78
+ statusCodes: SAFE_RETRY_STATUS_CODES,
79
+ errorCodes: defaultErrorCodes,
80
+ delayStrategy: HttpRetryDelayStrategy.Exponential,
81
+ baseDelayMs: 100,
82
+ maxDelayMs: 2_000,
83
+ jitter: HttpRetryJitter.Full,
84
+ retryAfter: HttpRetryAfterPolicy.Cap,
85
+ unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
86
+ };
87
+ case HttpRetryPreset.AggressiveRead:
88
+ return {
89
+ preset,
90
+ attempts: 4,
91
+ methods: DEFAULT_PROXY_TRANSPORT_RETRY_METHODS,
92
+ statusCodes: SAFE_RETRY_STATUS_CODES,
93
+ errorCodes: defaultErrorCodes,
94
+ delayStrategy: HttpRetryDelayStrategy.Exponential,
95
+ baseDelayMs: 150,
96
+ maxDelayMs: 5_000,
97
+ jitter: HttpRetryJitter.Full,
98
+ retryAfter: HttpRetryAfterPolicy.Cap,
99
+ unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
100
+ };
101
+ case HttpRetryPreset.RateLimitAware:
102
+ return {
103
+ preset,
104
+ attempts: 3,
105
+ methods: DEFAULT_PROXY_TRANSPORT_RETRY_METHODS,
106
+ statusCodes: RATE_LIMIT_RETRY_STATUS_CODES,
107
+ errorCodes: RATE_LIMIT_RETRY_ERROR_CODES,
108
+ delayStrategy: HttpRetryDelayStrategy.Exponential,
109
+ baseDelayMs: 250,
110
+ maxDelayMs: 5_000,
111
+ jitter: HttpRetryJitter.Equal,
112
+ retryAfter: HttpRetryAfterPolicy.Respect,
113
+ unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
114
+ };
115
+ case HttpRetryPreset.TransportTransient:
116
+ return {
117
+ preset,
118
+ attempts: 3,
119
+ methods: DEFAULT_PROXY_TRANSPORT_RETRY_METHODS,
120
+ statusCodes: [],
121
+ errorCodes: defaultErrorCodes,
122
+ delayStrategy: HttpRetryDelayStrategy.Exponential,
123
+ baseDelayMs: 100,
124
+ maxDelayMs: 1_000,
125
+ jitter: HttpRetryJitter.Full,
126
+ retryAfter: HttpRetryAfterPolicy.Ignore,
127
+ unsafeMethodPolicy: HttpRetryUnsafeMethodPolicy.Reject,
128
+ };
129
+ }
130
+ throw createInvalidRetryPolicyError(`Unknown ${label} retry preset: ${preset}`, label);
131
+ }
132
+ function clampPositiveInteger(value, fallback, max) {
133
+ if (value === undefined)
134
+ return fallback;
135
+ if (!Number.isFinite(value) || value < 1)
136
+ return fallback;
137
+ return Math.min(Math.floor(value), max);
138
+ }
139
+ function clampDelay(value, fallback) {
140
+ if (value === undefined)
141
+ return fallback;
142
+ if (!Number.isFinite(value) || value < 0)
143
+ return fallback;
144
+ return Math.min(Math.floor(value), MAX_RETRY_DELAY_MS);
145
+ }
146
+ export function createDefaultProxyTransportRetryOptions(options = {}) {
147
+ return createRetryOptions(HttpRetryPreset.TransportTransient, options.extraErrorCodes ?? [], options.label ?? "Proxy transport");
148
+ }
149
+ export function normalizeProxyTransportRetryOptions(retry, options = {}) {
150
+ const label = options.label ?? "Proxy transport";
151
+ const extraErrorCodes = options.extraErrorCodes ?? [];
152
+ if (retry === undefined || retry === false) {
153
+ return undefined;
154
+ }
155
+ if (retry === true) {
156
+ return createRetryOptions(HttpRetryPreset.TransportTransient, extraErrorCodes, label);
157
+ }
158
+ if (typeof retry === "string") {
159
+ if (!hasOwnValue(HttpRetryPreset, retry)) {
160
+ throw createInvalidRetryPolicyError(`Unknown ${label} retry preset: ${retry}`, label);
161
+ }
162
+ return createRetryOptions(retry, extraErrorCodes, label);
163
+ }
164
+ if (typeof retry !== "object" || retry === null) {
165
+ throw createInvalidRetryPolicyError(`${label} retry policy must be an object`, label);
166
+ }
167
+ if (Array.isArray(retry)) {
168
+ throw createInvalidRetryPolicyError(`${label} retry policy must be a plain object`, label);
169
+ }
170
+ validateRetryOptionsShape(retry, label);
171
+ const base = createRetryOptions(retry.preset ?? HttpRetryPreset.TransportTransient, extraErrorCodes, label);
172
+ const maxDelayMs = clampDelay(retry.maxDelayMs, base.maxDelayMs);
173
+ return {
174
+ preset: retry.preset ?? base.preset,
175
+ attempts: clampPositiveInteger(retry.attempts, base.attempts, MAX_PROXY_TRANSPORT_RETRY_ATTEMPTS),
176
+ methods: retry.methods?.map((method) => method.toUpperCase()) ?? base.methods,
177
+ statusCodes: retry.statusCodes?.filter((status) => Number.isInteger(status)) ?? base.statusCodes,
178
+ errorCodes: retry.errorCodes ?? base.errorCodes,
179
+ delayStrategy: retry.delayStrategy ?? base.delayStrategy,
180
+ baseDelayMs: clampDelay(retry.baseDelayMs, base.baseDelayMs),
181
+ maxDelayMs,
182
+ jitter: retry.jitter ?? base.jitter,
183
+ retryAfter: retry.retryAfter ?? base.retryAfter,
184
+ unsafeMethodPolicy: retry.unsafeMethodPolicy ?? base.unsafeMethodPolicy,
185
+ };
186
+ }
187
+ function validateRetryOptionsShape(retry, label) {
188
+ if (retry.preset !== undefined && !hasOwnValue(HttpRetryPreset, retry.preset)) {
189
+ throw createInvalidRetryPolicyError(`Unknown ${label} retry preset: ${String(retry.preset)}`, label);
190
+ }
191
+ if (retry.delayStrategy !== undefined &&
192
+ !hasOwnValue(HttpRetryDelayStrategy, retry.delayStrategy)) {
193
+ throw createInvalidRetryPolicyError(`Unknown ${label} retry delay strategy: ${String(retry.delayStrategy)}`, label);
194
+ }
195
+ if (retry.jitter !== undefined && !hasOwnValue(HttpRetryJitter, retry.jitter)) {
196
+ throw createInvalidRetryPolicyError(`Unknown ${label} retry jitter policy: ${String(retry.jitter)}`, label);
197
+ }
198
+ if (retry.retryAfter !== undefined && !hasOwnValue(HttpRetryAfterPolicy, retry.retryAfter)) {
199
+ throw createInvalidRetryPolicyError(`Unknown ${label} retry-after policy: ${String(retry.retryAfter)}`, label);
200
+ }
201
+ if (retry.unsafeMethodPolicy !== undefined &&
202
+ !hasOwnValue(HttpRetryUnsafeMethodPolicy, retry.unsafeMethodPolicy)) {
203
+ throw createInvalidRetryPolicyError(`Unknown ${label} retry unsafe method policy: ${String(retry.unsafeMethodPolicy)}`, label);
204
+ }
205
+ if (retry.methods !== undefined) {
206
+ if (!Array.isArray(retry.methods)) {
207
+ throw createInvalidRetryPolicyError(`${label} retry methods must be an array`, label);
208
+ }
209
+ const nonStringMethods = retry.methods.filter((method) => typeof method !== "string");
210
+ if (nonStringMethods.length > 0) {
211
+ throw createInvalidRetryPolicyError(`${label} retry methods must contain only strings`, label);
212
+ }
213
+ const unknownMethods = retry.methods
214
+ .map((method) => method.toUpperCase())
215
+ .filter((method) => !KNOWN_RETRY_METHODS.has(method));
216
+ if (unknownMethods.length > 0) {
217
+ throw createInvalidRetryPolicyError(`Unknown ${label} retry method(s): ${unknownMethods.join(", ")}`, label);
218
+ }
219
+ }
220
+ if (retry.statusCodes !== undefined) {
221
+ if (!Array.isArray(retry.statusCodes)) {
222
+ throw createInvalidRetryPolicyError(`${label} retry statusCodes must be an array`, label);
223
+ }
224
+ const invalidStatusCodes = retry.statusCodes.filter((status) => !Number.isInteger(status) || Number(status) < 100 || Number(status) > 599);
225
+ if (invalidStatusCodes.length > 0) {
226
+ throw createInvalidRetryPolicyError(`${label} retry statusCodes must contain HTTP status integers in [100, 599]`, label);
227
+ }
228
+ }
229
+ if (retry.errorCodes !== undefined) {
230
+ if (!Array.isArray(retry.errorCodes)) {
231
+ throw createInvalidRetryPolicyError(`${label} retry errorCodes must be an array`, label);
232
+ }
233
+ const nonStringErrorCodes = retry.errorCodes.filter((errorCode) => typeof errorCode !== "string");
234
+ if (nonStringErrorCodes.length > 0) {
235
+ throw createInvalidRetryPolicyError(`${label} retry errorCodes must contain only strings`, label);
236
+ }
237
+ }
238
+ if (retry.preset === HttpRetryPreset.Off &&
239
+ ((retry.attempts !== undefined && retry.attempts > 1) ||
240
+ (retry.statusCodes !== undefined && retry.statusCodes.length > 0))) {
241
+ throw createInvalidRetryPolicyError(`${label} retry preset off cannot be combined with retry-enabling overrides`, label);
242
+ }
243
+ }
244
+ export function validateUnsafeProxyTransportRetryMethods(options, label = "Proxy transport") {
245
+ if (options.unsafeMethodPolicy === HttpRetryUnsafeMethodPolicy.AllowExplicitUnsafe) {
246
+ return;
247
+ }
248
+ const unsafeMethods = options.methods.filter((method) => UNSAFE_RETRY_METHODS.has(method.toUpperCase()));
249
+ if (unsafeMethods.length === 0)
250
+ return;
251
+ throw new ProviderError(`${label} retry methods include unsafe method(s): ${unsafeMethods.join(", ")}`, { code: "retry_unsafe_method" });
252
+ }
253
+ export function isProxyTransportRetryMethod(method, options) {
254
+ return options.methods
255
+ .map((allowedMethod) => allowedMethod.toUpperCase())
256
+ .includes(method.toUpperCase());
257
+ }
258
+ export function shouldRetryProxyTransportError(error, options) {
259
+ const code = retryErrorCode(error);
260
+ return Boolean(code && options.errorCodes.includes(code));
261
+ }
262
+ export function shouldRetryProxyTransportAttempt(input) {
263
+ const { error, explicitRetry, method, options, proxyUsed } = input;
264
+ if (!options || options.attempts <= 1)
265
+ return false;
266
+ if (!explicitRetry && !proxyUsed)
267
+ return false;
268
+ return (isProxyTransportRetryMethod(method, options) && shouldRetryProxyTransportError(error, options));
269
+ }
270
+ function retryAfterHeader(headers) {
271
+ for (const [name, value] of Object.entries(headers)) {
272
+ if (name.toLowerCase() === "retry-after")
273
+ return value;
274
+ }
275
+ return undefined;
276
+ }
277
+ function parseRetryAfterMs(headers, now = Date.now()) {
278
+ const value = retryAfterHeader(headers);
279
+ if (!value)
280
+ return undefined;
281
+ const seconds = Number(value);
282
+ if (Number.isFinite(seconds)) {
283
+ return Math.max(0, Math.floor(seconds * 1_000));
284
+ }
285
+ const dateMs = Date.parse(value);
286
+ if (!Number.isNaN(dateMs)) {
287
+ return Math.max(0, dateMs - now);
288
+ }
289
+ return undefined;
290
+ }
291
+ export function computeProxyTransportRetryDelayMs(options, attemptIndex, headers) {
292
+ const multiplier = options.delayStrategy === HttpRetryDelayStrategy.Exponential
293
+ ? 2 ** Math.max(0, attemptIndex - 1)
294
+ : 1;
295
+ const configuredDelay = Math.min(options.baseDelayMs * multiplier, options.maxDelayMs);
296
+ const retryAfterMs = options.retryAfter === HttpRetryAfterPolicy.Ignore
297
+ ? undefined
298
+ : headers
299
+ ? parseRetryAfterMs(headers)
300
+ : undefined;
301
+ if (retryAfterMs !== undefined) {
302
+ const boundedRetryAfterMs = Math.min(retryAfterMs, options.maxDelayMs);
303
+ if (options.retryAfter === HttpRetryAfterPolicy.Cap) {
304
+ return Math.min(boundedRetryAfterMs, configuredDelay);
305
+ }
306
+ return boundedRetryAfterMs;
307
+ }
308
+ switch (options.jitter) {
309
+ case HttpRetryJitter.None:
310
+ return configuredDelay;
311
+ case HttpRetryJitter.Equal:
312
+ return Math.floor(configuredDelay / 2 + Math.random() * (configuredDelay / 2));
313
+ case HttpRetryJitter.Full:
314
+ return Math.floor(Math.random() * configuredDelay);
315
+ }
316
+ }
317
+ export function normalizeProxyAttemptIndex(value) {
318
+ if (value === undefined || !Number.isFinite(value))
319
+ return 0;
320
+ return Math.max(0, Math.floor(value));
321
+ }
322
+ export function computeProxyAttemptIndex(options) {
323
+ return (normalizeProxyAttemptIndex(options.baseProxyAttempt) +
324
+ normalizeProxyAttemptIndex(options.proxyAttemptOffset) +
325
+ normalizeProxyAttemptIndex(options.retryAttemptOffset));
326
+ }
@@ -1,8 +1,9 @@
1
- import type { ProxyAttemptTelemetryEvent, ProxyResolutionTelemetryEvent, ProxyTelemetrySink } from "../config/loader";
1
+ import type { ProxyAttemptTelemetryEvent, ProxyResolutionTelemetryEvent, ProxyTelemetrySink, ProxyVendorFailoverTelemetryEvent } from "../config/loader.js";
2
2
  export declare const PROVIDER_TELEMETRY_HEADER = "X-ApiFuse-Provider-Telemetry";
3
3
  export declare class ProxyTelemetryCollector implements ProxyTelemetrySink {
4
4
  #private;
5
5
  recordProxyResolution(event: ProxyResolutionTelemetryEvent): void;
6
+ recordProxyVendorFailover(event: ProxyVendorFailoverTelemetryEvent): void;
6
7
  recordProxyAttempt(event: ProxyAttemptTelemetryEvent): void;
7
8
  toHeaderValue(): string | undefined;
8
9
  }
@@ -1,6 +1,7 @@
1
1
  export const PROVIDER_TELEMETRY_HEADER = "X-ApiFuse-Provider-Telemetry";
2
2
  const MAX_HEADER_BYTES = 4_096;
3
3
  const MAX_PROXY_ATTEMPT_SAMPLES = 24;
4
+ const MAX_PROXY_FAILOVER_SAMPLES = 12;
4
5
  const CACHE_STATUS_SEVERITY = {
5
6
  disabled: 0,
6
7
  memory_hit: 1,
@@ -20,9 +21,7 @@ function maxOptional(left, right) {
20
21
  return values.length > 0 ? Math.max(...values) : undefined;
21
22
  }
22
23
  function worseStatus(left, right) {
23
- return CACHE_STATUS_SEVERITY[right] > CACHE_STATUS_SEVERITY[left]
24
- ? right
25
- : left;
24
+ return CACHE_STATUS_SEVERITY[right] > CACHE_STATUS_SEVERITY[left] ? right : left;
26
25
  }
27
26
  function encodeBase64Url(value) {
28
27
  return Buffer.from(value, "utf8").toString("base64url");
@@ -30,15 +29,15 @@ function encodeBase64Url(value) {
30
29
  export class ProxyTelemetryCollector {
31
30
  #events = [];
32
31
  #attempts = [];
32
+ #failovers = [];
33
33
  recordProxyResolution(event) {
34
34
  this.#events.push({
35
- provider: "smartproxy",
35
+ provider: event.provider,
36
+ ...(event.protocol ? { protocol: event.protocol } : {}),
36
37
  cacheStatus: event.cacheStatus,
37
38
  cacheHit: event.cacheHit,
38
39
  resolutionMs: Math.max(0, Math.floor(event.resolutionMs)),
39
- allocatorMs: event.allocatorMs === undefined
40
- ? undefined
41
- : Math.max(0, Math.floor(event.allocatorMs)),
40
+ allocatorMs: event.allocatorMs === undefined ? undefined : Math.max(0, Math.floor(event.allocatorMs)),
42
41
  allocatorStatus: event.allocatorStatus === undefined
43
42
  ? undefined
44
43
  : Math.max(0, Math.floor(event.allocatorStatus)),
@@ -46,32 +45,33 @@ export class ProxyTelemetryCollector {
46
45
  allocatorAttempts: event.allocatorAttempts === undefined
47
46
  ? undefined
48
47
  : Math.max(1, Math.floor(event.allocatorAttempts)),
49
- lockWaitMs: event.lockWaitMs === undefined
50
- ? undefined
51
- : Math.max(0, Math.floor(event.lockWaitMs)),
52
- redisReadMs: event.redisReadMs === undefined
53
- ? undefined
54
- : Math.max(0, Math.floor(event.redisReadMs)),
55
- redisWriteMs: event.redisWriteMs === undefined
56
- ? undefined
57
- : Math.max(0, Math.floor(event.redisWriteMs)),
58
- poolAgeMs: event.poolAgeMs === undefined
59
- ? undefined
60
- : Math.max(0, Math.floor(event.poolAgeMs)),
48
+ lockWaitMs: event.lockWaitMs === undefined ? undefined : Math.max(0, Math.floor(event.lockWaitMs)),
49
+ redisReadMs: event.redisReadMs === undefined ? undefined : Math.max(0, Math.floor(event.redisReadMs)),
50
+ redisWriteMs: event.redisWriteMs === undefined ? undefined : Math.max(0, Math.floor(event.redisWriteMs)),
51
+ poolAgeMs: event.poolAgeMs === undefined ? undefined : Math.max(0, Math.floor(event.poolAgeMs)),
61
52
  poolExpiresInMs: event.poolExpiresInMs === undefined
62
53
  ? undefined
63
54
  : Math.max(0, Math.floor(event.poolExpiresInMs)),
64
55
  attempts: Math.max(1, Math.floor(event.attempts || 1)),
65
- refreshes: event.refreshes === undefined
66
- ? undefined
67
- : Math.max(0, Math.floor(event.refreshes)),
56
+ refreshes: event.refreshes === undefined ? undefined : Math.max(0, Math.floor(event.refreshes)),
57
+ });
58
+ }
59
+ recordProxyVendorFailover(event) {
60
+ if (this.#failovers.length >= MAX_PROXY_FAILOVER_SAMPLES)
61
+ return;
62
+ this.#failovers.push({
63
+ vendor: event.vendor,
64
+ ...(event.nextVendor ? { nextVendor: event.nextVendor } : {}),
65
+ phase: event.phase,
66
+ reason: event.reason,
67
+ ...(event.attempt === undefined ? {} : { attempt: Math.max(0, Math.floor(event.attempt)) }),
68
68
  });
69
69
  }
70
70
  recordProxyAttempt(event) {
71
71
  if (this.#attempts.length >= MAX_PROXY_ATTEMPT_SAMPLES)
72
72
  return;
73
73
  this.#attempts.push({
74
- provider: "smartproxy",
74
+ provider: event.provider,
75
75
  attempt: Math.max(1, Math.floor(event.attempt || 1)),
76
76
  ...(event.poolIndex === undefined
77
77
  ? {}
@@ -79,9 +79,7 @@ export class ProxyTelemetryCollector {
79
79
  ...(event.proxyHash ? { proxyHash: event.proxyHash.slice(0, 16) } : {}),
80
80
  outcome: event.outcome === "ok" ? "ok" : "error",
81
81
  ...(event.errorCode ? { errorCode: event.errorCode.slice(0, 80) } : {}),
82
- ...(event.status === undefined
83
- ? {}
84
- : { status: Math.max(0, Math.floor(event.status)) }),
82
+ ...(event.status === undefined ? {} : { status: Math.max(0, Math.floor(event.status)) }),
85
83
  ...(event.durationMs === undefined
86
84
  ? {}
87
85
  : { durationMs: Math.max(0, Math.floor(event.durationMs)) }),
@@ -91,8 +89,16 @@ export class ProxyTelemetryCollector {
91
89
  const [first, ...rest] = this.#events;
92
90
  if (!first)
93
91
  return undefined;
92
+ // The serving vendor/protocol is the last recorded resolution (a failed
93
+ // vendor records first, the vendor that served records last).
94
+ const serving = this.#events[this.#events.length - 1] ?? first;
95
+ const vendors = [];
96
+ for (const event of this.#events) {
97
+ if (!vendors.includes(event.provider))
98
+ vendors.push(event.provider);
99
+ }
94
100
  const aggregate = rest.reduce((acc, event) => ({
95
- provider: "smartproxy",
101
+ provider: event.provider,
96
102
  cacheStatus: worseStatus(acc.cacheStatus, event.cacheStatus),
97
103
  cacheHit: acc.cacheHit && event.cacheHit,
98
104
  resolutionMs: acc.resolutionMs + event.resolutionMs,
@@ -111,13 +117,12 @@ export class ProxyTelemetryCollector {
111
117
  const payload = {
112
118
  v: 1,
113
119
  proxy: {
114
- provider: "smartproxy",
120
+ provider: serving.provider,
121
+ ...(serving.protocol ? { protocol: serving.protocol } : {}),
115
122
  cacheStatus: aggregate.cacheStatus,
116
123
  cacheHit: aggregate.cacheHit,
117
124
  resolutionMs: aggregate.resolutionMs,
118
- ...(aggregate.allocatorMs !== undefined
119
- ? { allocatorMs: aggregate.allocatorMs }
120
- : {}),
125
+ ...(aggregate.allocatorMs !== undefined ? { allocatorMs: aggregate.allocatorMs } : {}),
121
126
  ...(aggregate.allocatorStatus !== undefined
122
127
  ? { allocatorStatus: aggregate.allocatorStatus }
123
128
  : {}),
@@ -127,40 +132,38 @@ export class ProxyTelemetryCollector {
127
132
  ...(aggregate.allocatorAttempts !== undefined
128
133
  ? { allocatorAttempts: aggregate.allocatorAttempts }
129
134
  : {}),
130
- ...(aggregate.lockWaitMs !== undefined
131
- ? { lockWaitMs: aggregate.lockWaitMs }
132
- : {}),
133
- ...(aggregate.redisReadMs !== undefined
134
- ? { redisReadMs: aggregate.redisReadMs }
135
- : {}),
136
- ...(aggregate.redisWriteMs !== undefined
137
- ? { redisWriteMs: aggregate.redisWriteMs }
138
- : {}),
139
- ...(aggregate.poolAgeMs !== undefined
140
- ? { poolAgeMs: aggregate.poolAgeMs }
141
- : {}),
135
+ ...(aggregate.lockWaitMs !== undefined ? { lockWaitMs: aggregate.lockWaitMs } : {}),
136
+ ...(aggregate.redisReadMs !== undefined ? { redisReadMs: aggregate.redisReadMs } : {}),
137
+ ...(aggregate.redisWriteMs !== undefined ? { redisWriteMs: aggregate.redisWriteMs } : {}),
138
+ ...(aggregate.poolAgeMs !== undefined ? { poolAgeMs: aggregate.poolAgeMs } : {}),
142
139
  ...(aggregate.poolExpiresInMs !== undefined
143
140
  ? { poolExpiresInMs: aggregate.poolExpiresInMs }
144
141
  : {}),
145
142
  attempts: aggregate.attempts,
146
- ...(aggregate.refreshes !== undefined
147
- ? { refreshes: aggregate.refreshes }
148
- : {}),
143
+ ...(aggregate.refreshes !== undefined ? { refreshes: aggregate.refreshes } : {}),
149
144
  ...(this.#attempts.length > 0
150
145
  ? {
151
146
  attemptSamples: this.#attempts.map((attempt, index) => ({
152
147
  n: index + 1,
153
148
  a: attempt.attempt,
154
- ...(attempt.poolIndex === undefined
155
- ? {}
156
- : { i: attempt.poolIndex }),
149
+ ...(attempt.poolIndex === undefined ? {} : { i: attempt.poolIndex }),
157
150
  ...(attempt.proxyHash ? { h: attempt.proxyHash } : {}),
158
151
  o: attempt.outcome,
159
152
  ...(attempt.errorCode ? { c: attempt.errorCode } : {}),
160
153
  ...(attempt.status === undefined ? {} : { s: attempt.status }),
161
- ...(attempt.durationMs === undefined
162
- ? {}
163
- : { d: attempt.durationMs }),
154
+ ...(attempt.durationMs === undefined ? {} : { d: attempt.durationMs }),
155
+ })),
156
+ }
157
+ : {}),
158
+ ...(vendors.length > 1 ? { vendors } : {}),
159
+ ...(this.#failovers.length > 0
160
+ ? {
161
+ failovers: this.#failovers.map((failover) => ({
162
+ v: failover.vendor,
163
+ ...(failover.nextVendor ? { nx: failover.nextVendor } : {}),
164
+ p: failover.phase,
165
+ r: failover.reason,
166
+ ...(failover.attempt === undefined ? {} : { a: failover.attempt }),
164
167
  })),
165
168
  }
166
169
  : {}),
@@ -1,4 +1,4 @@
1
- import Redis from "ioredis";
1
+ import { Redis } from "ioredis";
2
2
  export type ProviderRedisClient = Redis;
3
3
  export type ProviderRedisClientOptions = {
4
4
  readonly redisUrl: string;
@@ -1,4 +1,4 @@
1
- import Redis from "ioredis";
1
+ import { Redis } from "ioredis";
2
2
  export function createProviderRedisClient(options) {
3
3
  const redis = new Redis(options.redisUrl, {
4
4
  connectTimeout: options.timeoutMs,
@@ -23,9 +23,7 @@ export async function withRedisTimeout(operation, options) {
23
23
  }
24
24
  }, options.timeoutMs);
25
25
  });
26
- const operationResult = options.onError
27
- ? operation().catch(options.onError)
28
- : operation();
26
+ const operationResult = options.onError ? operation().catch(options.onError) : operation();
29
27
  return await Promise.race([operationResult, timeout]);
30
28
  }
31
29
  finally {
@@ -1,3 +1,3 @@
1
- import type { RequestParams } from "../types";
1
+ import type { RequestParams } from "../types.js";
2
2
  export declare function appendQueryParams(url: string, params?: RequestParams): string;
3
3
  export declare function normalizeHttpRequestBody(body: unknown): string | Buffer | undefined;
@@ -0,0 +1,27 @@
1
+ import type { EnvContext, ProviderDefinition } from "../types.js";
2
+ /**
3
+ * Canonical error code for a declared-but-unprovisioned provider secret.
4
+ *
5
+ * The SDK is the single source of truth for env/secret presence validation:
6
+ * providers declare secrets in `defineProvider({ secrets: [...] })` and the
7
+ * runtime enforces presence before any handler or auth-flow code runs.
8
+ * Provider-local presence guards (requireServiceKey/requireApiKey style) are a
9
+ * deprecated antipattern — see the `sdk-owned-secret-presence` submit-check
10
+ * rule.
11
+ */
12
+ export declare const MISSING_SECRET_CODE = "MISSING_SECRET";
13
+ /**
14
+ * Names of declared `required: true` secrets whose env values are unset or
15
+ * whitespace-only. Whitespace-only values count as missing for parity with the
16
+ * `.trim()` guards well-built providers used before the SDK owned this check —
17
+ * a blank value provisioned by a broken secret pipeline must not pass the gate.
18
+ */
19
+ export declare function listMissingRequiredSecrets(provider: ProviderDefinition, env: EnvContext): string[];
20
+ /**
21
+ * Throws the canonical structured missing-secret error when any declared
22
+ * `required: true` secret is absent. All missing names are reported in a
23
+ * single error so operators can provision the full set in one pass instead of
24
+ * discovering them one deploy at a time (the 2026-07-22 unprovisioned-secret
25
+ * incident failure mode).
26
+ */
27
+ export declare function assertRequiredSecretsPresent(provider: ProviderDefinition, env: EnvContext): void;
@@ -0,0 +1,51 @@
1
+ import { ProviderSecretError } from "../errors.js";
2
+ /**
3
+ * Canonical error code for a declared-but-unprovisioned provider secret.
4
+ *
5
+ * The SDK is the single source of truth for env/secret presence validation:
6
+ * providers declare secrets in `defineProvider({ secrets: [...] })` and the
7
+ * runtime enforces presence before any handler or auth-flow code runs.
8
+ * Provider-local presence guards (requireServiceKey/requireApiKey style) are a
9
+ * deprecated antipattern — see the `sdk-owned-secret-presence` submit-check
10
+ * rule.
11
+ */
12
+ export const MISSING_SECRET_CODE = "MISSING_SECRET";
13
+ /**
14
+ * Names of declared `required: true` secrets whose env values are unset or
15
+ * whitespace-only. Whitespace-only values count as missing for parity with the
16
+ * `.trim()` guards well-built providers used before the SDK owned this check —
17
+ * a blank value provisioned by a broken secret pipeline must not pass the gate.
18
+ */
19
+ export function listMissingRequiredSecrets(provider, env) {
20
+ const missing = [];
21
+ for (const secret of provider.secrets ?? []) {
22
+ if (secret.required !== true) {
23
+ continue;
24
+ }
25
+ const value = env.get(secret.name);
26
+ if (value === undefined || value.trim() === "") {
27
+ missing.push(secret.name);
28
+ }
29
+ }
30
+ return missing;
31
+ }
32
+ /**
33
+ * Throws the canonical structured missing-secret error when any declared
34
+ * `required: true` secret is absent. All missing names are reported in a
35
+ * single error so operators can provision the full set in one pass instead of
36
+ * discovering them one deploy at a time (the 2026-07-22 unprovisioned-secret
37
+ * incident failure mode).
38
+ */
39
+ export function assertRequiredSecretsPresent(provider, env) {
40
+ const missing = listMissingRequiredSecrets(provider, env);
41
+ if (missing.length === 0) {
42
+ return;
43
+ }
44
+ const names = missing.join(", ");
45
+ throw new ProviderSecretError(`Missing required provider secret${missing.length > 1 ? "s" : ""}: ${names}`, {
46
+ code: MISSING_SECRET_CODE,
47
+ category: "credential_unavailable",
48
+ retryable: false,
49
+ fix: `Provision ${names} in the provider environment (e.g. Doppler). Declared in defineProvider({ secrets: [...] }).`,
50
+ });
51
+ }
@@ -1,5 +1,5 @@
1
- import { ProviderError } from "../errors";
2
- import type { ProviderRuntimeState } from "../types";
1
+ import { ProviderError } from "../errors.js";
2
+ import type { ProviderRuntimeState } from "../types.js";
3
3
  type RedisProviderRuntimeStateOptions = {
4
4
  readonly redisUrl: string;
5
5
  readonly providerId?: string;