@forgezero/providers 0.1.0

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.
package/dist/email.js ADDED
@@ -0,0 +1,278 @@
1
+ // src/index.ts
2
+ class ProviderError extends Error {
3
+ code;
4
+ details;
5
+ constructor(code, message, details) {
6
+ super(message);
7
+ this.code = code;
8
+ this.details = details;
9
+ this.name = "ProviderError";
10
+ }
11
+ }
12
+ function envCredentials(env) {
13
+ return {
14
+ name: "env",
15
+ async get(reference, field) {
16
+ const key = `${reference}_${field}`.replace(/[.-]/g, "_").toUpperCase();
17
+ const value = env[key];
18
+ if (value === undefined) {
19
+ throw new ProviderError("CREDENTIAL_MISSING", `Set ${key} in the environment.`);
20
+ }
21
+ return value;
22
+ }
23
+ };
24
+ }
25
+ function chainCredentials(...sources) {
26
+ return {
27
+ name: sources.map((source) => source.name).join("+"),
28
+ async get(reference, field) {
29
+ let last;
30
+ for (const source of sources) {
31
+ try {
32
+ return await source.get(reference, field);
33
+ } catch (error) {
34
+ last = error;
35
+ }
36
+ }
37
+ throw last instanceof Error ? last : new ProviderError("CREDENTIAL_MISSING", `No source held ${reference}.${field}.`);
38
+ }
39
+ };
40
+ }
41
+ function staticConfig(services) {
42
+ const health = new Map;
43
+ return {
44
+ name: "static",
45
+ async list(serviceKey) {
46
+ return (services[serviceKey] ?? []).map((provider) => ({
47
+ ...provider,
48
+ health: health.get(`${serviceKey}:${provider.providerId}`) ?? provider.health
49
+ }));
50
+ },
51
+ async recordHealth(serviceKey, providerId, next) {
52
+ health.set(`${serviceKey}:${providerId}`, next);
53
+ }
54
+ };
55
+ }
56
+ function defineProvider(spec) {
57
+ return spec;
58
+ }
59
+ var STRIKES_TO_OFFLINE = 3;
60
+ function nextHealth(current, kind) {
61
+ if (kind === "success")
62
+ return { strikes: 0, status: "ok" };
63
+ if (kind === "backoff")
64
+ return current ?? { strikes: 0, status: "ok" };
65
+ const strikes = (current?.strikes ?? 0) + 1;
66
+ return {
67
+ strikes,
68
+ status: strikes >= STRIKES_TO_OFFLINE ? "offline" : "degraded",
69
+ lastFailureAtTs: Date.now()
70
+ };
71
+ }
72
+ function createRegistry(options) {
73
+ const byId = new Map(options.providers.map((provider) => [provider.id, provider]));
74
+ async function call(serviceKey, args) {
75
+ const configured = [...await options.config.list(serviceKey)].filter((provider) => provider.enabled).sort((a, b) => a.priority - b.priority);
76
+ const attempts = [];
77
+ for (const entry of configured) {
78
+ const spec = byId.get(entry.providerId);
79
+ if (!spec) {
80
+ attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "not registered" });
81
+ continue;
82
+ }
83
+ if (entry.health?.status === "offline") {
84
+ attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "offline" });
85
+ continue;
86
+ }
87
+ options.before?.({ service: serviceKey, provider: entry.providerId });
88
+ try {
89
+ const result = await spec.invoke({
90
+ config: entry.config,
91
+ secret: (field) => options.credentials.get(entry.secretRef, field)
92
+ }, args);
93
+ attempts.push({ providerId: entry.providerId, outcome: "sent" });
94
+ await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, "success"));
95
+ const sent = { ok: true, result, provider: entry.providerId, attempts };
96
+ options.after?.(sent);
97
+ return sent;
98
+ } catch (error) {
99
+ const kind = spec.classify(error);
100
+ attempts.push({
101
+ providerId: entry.providerId,
102
+ outcome: "failed",
103
+ kind,
104
+ error: error instanceof Error ? error.message : String(error)
105
+ });
106
+ await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, kind));
107
+ if (kind === "terminal") {
108
+ const refused = {
109
+ ok: false,
110
+ attempts,
111
+ error: new ProviderError("PAYLOAD_REJECTED", "The request was refused as malformed; no provider will accept it.")
112
+ };
113
+ options.after?.(refused);
114
+ return refused;
115
+ }
116
+ }
117
+ }
118
+ const failed = {
119
+ ok: false,
120
+ attempts,
121
+ error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider is configured for "${serviceKey}".` : `Every provider for "${serviceKey}" failed or was skipped.`)
122
+ };
123
+ options.after?.(failed);
124
+ return failed;
125
+ }
126
+ return { call };
127
+ }
128
+ var VERSION = "0.1.0";
129
+
130
+ // src/email.ts
131
+ var recipients = (to) => Array.isArray(to) ? [...to] : [to];
132
+ function assertMessage(message) {
133
+ if (recipients(message.to).length === 0) {
134
+ throw new ProviderError("EMAIL_NO_RECIPIENT", "An email needs at least one recipient.");
135
+ }
136
+ if (!message.html && !message.text) {
137
+ throw new ProviderError("EMAIL_NO_BODY", "An email needs html, text, or both.");
138
+ }
139
+ }
140
+ var jetemail = defineProvider({
141
+ id: "jetemail",
142
+ service: "email",
143
+ label: "JetEmail",
144
+ credentials: {
145
+ type: "object",
146
+ additionalProperties: false,
147
+ required: ["apiKey"],
148
+ properties: {
149
+ apiKey: {
150
+ type: "string",
151
+ title: "API key",
152
+ writeOnly: true,
153
+ description: "Transactional key from the JetEmail dashboard."
154
+ }
155
+ }
156
+ },
157
+ config: {
158
+ type: "object",
159
+ additionalProperties: false,
160
+ properties: {
161
+ eu: { type: "boolean", default: false, title: "EU residency" },
162
+ from: { type: "string", format: "email", title: "Default from address" }
163
+ }
164
+ },
165
+ async invoke(context, message) {
166
+ assertMessage(message);
167
+ const config = context.config;
168
+ const doFetch = config.fetch ?? globalThis.fetch;
169
+ const endpoint = config.endpoint ?? "https://api.jetemail.com/email";
170
+ const response = await doFetch(endpoint, {
171
+ method: "POST",
172
+ signal: context.signal,
173
+ headers: {
174
+ "content-type": "application/json",
175
+ authorization: `Bearer ${await context.secret("apiKey")}`
176
+ },
177
+ body: JSON.stringify({
178
+ from: message.from ?? config.from,
179
+ to: recipients(message.to),
180
+ subject: message.subject,
181
+ html: message.html,
182
+ text: message.text,
183
+ reply_to: message.replyTo,
184
+ eu: config.eu ?? false
185
+ })
186
+ });
187
+ if (response.status === 201 || response.status === 202) {
188
+ const payload = await response.json().catch(() => ({}));
189
+ return { id: payload.id ?? "" };
190
+ }
191
+ const detail = await response.text().catch(() => "");
192
+ throw Object.assign(new Error(`JetEmail ${response.status}: ${detail.slice(0, 200)}`), {
193
+ status: response.status,
194
+ retryAfter: response.headers.get("retry-after")
195
+ });
196
+ },
197
+ classify(error) {
198
+ const status = error.status;
199
+ if (status === 400 || status === 409 || status === 422)
200
+ return "terminal";
201
+ if (status === 429)
202
+ return "backoff";
203
+ return "retryable";
204
+ }
205
+ });
206
+ var smtp = defineProvider({
207
+ id: "smtp",
208
+ service: "email",
209
+ label: "SMTP",
210
+ multiInstance: true,
211
+ credentials: {
212
+ type: "object",
213
+ additionalProperties: false,
214
+ properties: {
215
+ user: { type: "string", title: "Username" },
216
+ password: { type: "string", title: "Password", writeOnly: true }
217
+ }
218
+ },
219
+ config: {
220
+ type: "object",
221
+ additionalProperties: false,
222
+ required: ["host", "from"],
223
+ properties: {
224
+ host: { type: "string", title: "Host" },
225
+ port: { type: "integer", default: 587, minimum: 1, maximum: 65535 },
226
+ secure: {
227
+ type: "boolean",
228
+ default: false,
229
+ title: "Implicit TLS",
230
+ description: "True for port 465. Port 587 upgrades with STARTTLS and should stay false."
231
+ },
232
+ from: { type: "string", format: "email", title: "From address" }
233
+ }
234
+ },
235
+ async invoke(context, message) {
236
+ assertMessage(message);
237
+ const config = context.config;
238
+ if (!config.transport) {
239
+ throw new ProviderError("SMTP_NO_TRANSPORT", "Provide a transport in this provider's config — nodemailer on a server, an HTTP relay on an edge runtime.");
240
+ }
241
+ const [user, password] = await Promise.all([
242
+ context.secret("user").catch(() => {
243
+ return;
244
+ }),
245
+ context.secret("password").catch(() => {
246
+ return;
247
+ })
248
+ ]);
249
+ const sent = await config.transport.send({
250
+ host: config.host,
251
+ port: config.port ?? 587,
252
+ secure: config.secure ?? false,
253
+ user,
254
+ password,
255
+ from: message.from ?? config.from,
256
+ to: recipients(message.to),
257
+ subject: message.subject,
258
+ html: message.html,
259
+ text: message.text,
260
+ replyTo: message.replyTo
261
+ });
262
+ return { id: sent.messageId };
263
+ },
264
+ classify(error) {
265
+ const code = error.responseCode;
266
+ if (code === 550 || code === 553 || code === 554)
267
+ return "terminal";
268
+ if (code === 421 || code === 450 || code === 451 || code === 452)
269
+ return "backoff";
270
+ return "retryable";
271
+ }
272
+ });
273
+ var emailProviders = [jetemail, smtp];
274
+ export {
275
+ smtp,
276
+ jetemail,
277
+ emailProviders
278
+ };
package/dist/http.d.ts ADDED
@@ -0,0 +1,100 @@
1
+ import { ProviderError } from './index';
2
+ /**
3
+ * Outbound HTTP with a budget that is actually respected.
4
+ *
5
+ * Exchanges do not rate-limit requests; they rate-limit COST. Binance publishes
6
+ * 6000 weight per minute per IP, and a single depth or klines call can cost 50
7
+ * where a status call costs 1. A per-request limiter set to "6000 a minute" is
8
+ * therefore wrong by more than an order of magnitude on exactly the endpoints
9
+ * a trading system leans on, and the failure mode is a 418 and a temporary ban
10
+ * rather than a slowdown.
11
+ *
12
+ * ## The budget is per HOST, and shared
13
+ *
14
+ * Every caller behind one address spends from one pool. A retry loop written in
15
+ * three places will spend it three times over and none of the three will know.
16
+ * So the budget lives here, keyed by host, and every call passes through it.
17
+ *
18
+ * ## Reservations, not counters
19
+ *
20
+ * Weight is reserved BEFORE the request and reconciled after, because the true
21
+ * cost arrives in a response header. Counting only on the way back means a burst
22
+ * of concurrent calls all pass the check and collectively blow the budget —
23
+ * which is precisely what happens when a strategy wakes up.
24
+ */
25
+ export interface HostBudget {
26
+ host: string;
27
+ /** Units per window. `weight` for Binance, `requests` for a simpler venue. */
28
+ limit: number;
29
+ windowMs: number;
30
+ /** Assumed cost before the real one is known. */
31
+ defaultCost: number;
32
+ /**
33
+ * Stop at a fraction of the limit rather than at it.
34
+ *
35
+ * The venue's clock and ours are not the same clock, and its window boundary
36
+ * is not ours. Spending to exactly 100% means crossing it on the venue's
37
+ * arithmetic while ours says there was room — and the penalty for crossing is
38
+ * a ban, not a rejected request.
39
+ */
40
+ headroom?: number;
41
+ }
42
+ interface Window {
43
+ spent: number;
44
+ resetAtMs: number;
45
+ }
46
+ export declare class BudgetExhausted extends ProviderError {
47
+ readonly host: string;
48
+ readonly retryAfterMs: number;
49
+ constructor(host: string, retryAfterMs: number);
50
+ }
51
+ /** Tests and a credential rotation. Nothing else should need it. */
52
+ export declare function resetBudgets(): void;
53
+ export declare function spend(budget: HostBudget, cost: number, nowMs: number): void;
54
+ /** Reconcile a reservation against what the response says it actually cost. */
55
+ export declare function settle(host: string, reserved: number, actual: number): void;
56
+ export declare const budgetState: (host: string) => Window | undefined;
57
+ export interface HttpConfig {
58
+ baseUrl: string;
59
+ budget: HostBudget;
60
+ /**
61
+ * Reads the true cost out of a response. Binance reports it in
62
+ * `x-mbx-used-weight-1m` as a running total, so the adapter that knows the
63
+ * venue owns this rather than the client guessing.
64
+ */
65
+ costOf?: (response: Response) => number | undefined;
66
+ fetch?: typeof globalThis.fetch;
67
+ timeoutMs?: number;
68
+ }
69
+ export interface HttpRequest {
70
+ path: string;
71
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
72
+ query?: Record<string, string | number>;
73
+ body?: unknown;
74
+ headers?: Record<string, string>;
75
+ /** What this call is expected to cost. Defaults to the budget's default. */
76
+ weight?: number;
77
+ }
78
+ export interface HttpResponse<T = unknown> {
79
+ status: number;
80
+ body: T;
81
+ /** What the call actually cost, when the venue reported it. */
82
+ cost?: number;
83
+ }
84
+ export declare function createHttpClient(config: HttpConfig): {
85
+ call<T = unknown>(request: HttpRequest): Promise<HttpResponse<T>>;
86
+ budget: () => Window | undefined;
87
+ };
88
+ export type HttpClient = ReturnType<typeof createHttpClient>;
89
+ /**
90
+ * The registry face, so an outbound HTTP dependency gets the same credential
91
+ * rotation and health tracking as a mail relay.
92
+ *
93
+ * `classify` is where the venue-specific knowledge lives, and it is the part
94
+ * that decides whether a retry helps or makes things worse.
95
+ */
96
+ export declare const http: import("./index").ProviderSpec<HttpRequest, HttpResponse<unknown>>;
97
+ /** Binance reports a running total for the window, not a per-call cost. */
98
+ export declare const binanceWeight: (response: Response) => number | undefined;
99
+ export declare const httpProviders: readonly [import("./index").ProviderSpec<HttpRequest, HttpResponse<unknown>>];
100
+ export {};
package/dist/http.js ADDED
@@ -0,0 +1,283 @@
1
+ // src/index.ts
2
+ class ProviderError extends Error {
3
+ code;
4
+ details;
5
+ constructor(code, message, details) {
6
+ super(message);
7
+ this.code = code;
8
+ this.details = details;
9
+ this.name = "ProviderError";
10
+ }
11
+ }
12
+ function envCredentials(env) {
13
+ return {
14
+ name: "env",
15
+ async get(reference, field) {
16
+ const key = `${reference}_${field}`.replace(/[.-]/g, "_").toUpperCase();
17
+ const value = env[key];
18
+ if (value === undefined) {
19
+ throw new ProviderError("CREDENTIAL_MISSING", `Set ${key} in the environment.`);
20
+ }
21
+ return value;
22
+ }
23
+ };
24
+ }
25
+ function chainCredentials(...sources) {
26
+ return {
27
+ name: sources.map((source) => source.name).join("+"),
28
+ async get(reference, field) {
29
+ let last;
30
+ for (const source of sources) {
31
+ try {
32
+ return await source.get(reference, field);
33
+ } catch (error) {
34
+ last = error;
35
+ }
36
+ }
37
+ throw last instanceof Error ? last : new ProviderError("CREDENTIAL_MISSING", `No source held ${reference}.${field}.`);
38
+ }
39
+ };
40
+ }
41
+ function staticConfig(services) {
42
+ const health = new Map;
43
+ return {
44
+ name: "static",
45
+ async list(serviceKey) {
46
+ return (services[serviceKey] ?? []).map((provider) => ({
47
+ ...provider,
48
+ health: health.get(`${serviceKey}:${provider.providerId}`) ?? provider.health
49
+ }));
50
+ },
51
+ async recordHealth(serviceKey, providerId, next) {
52
+ health.set(`${serviceKey}:${providerId}`, next);
53
+ }
54
+ };
55
+ }
56
+ function defineProvider(spec) {
57
+ return spec;
58
+ }
59
+ var STRIKES_TO_OFFLINE = 3;
60
+ function nextHealth(current, kind) {
61
+ if (kind === "success")
62
+ return { strikes: 0, status: "ok" };
63
+ if (kind === "backoff")
64
+ return current ?? { strikes: 0, status: "ok" };
65
+ const strikes = (current?.strikes ?? 0) + 1;
66
+ return {
67
+ strikes,
68
+ status: strikes >= STRIKES_TO_OFFLINE ? "offline" : "degraded",
69
+ lastFailureAtTs: Date.now()
70
+ };
71
+ }
72
+ function createRegistry(options) {
73
+ const byId = new Map(options.providers.map((provider) => [provider.id, provider]));
74
+ async function call(serviceKey, args) {
75
+ const configured = [...await options.config.list(serviceKey)].filter((provider) => provider.enabled).sort((a, b) => a.priority - b.priority);
76
+ const attempts = [];
77
+ for (const entry of configured) {
78
+ const spec = byId.get(entry.providerId);
79
+ if (!spec) {
80
+ attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "not registered" });
81
+ continue;
82
+ }
83
+ if (entry.health?.status === "offline") {
84
+ attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "offline" });
85
+ continue;
86
+ }
87
+ options.before?.({ service: serviceKey, provider: entry.providerId });
88
+ try {
89
+ const result = await spec.invoke({
90
+ config: entry.config,
91
+ secret: (field) => options.credentials.get(entry.secretRef, field)
92
+ }, args);
93
+ attempts.push({ providerId: entry.providerId, outcome: "sent" });
94
+ await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, "success"));
95
+ const sent = { ok: true, result, provider: entry.providerId, attempts };
96
+ options.after?.(sent);
97
+ return sent;
98
+ } catch (error) {
99
+ const kind = spec.classify(error);
100
+ attempts.push({
101
+ providerId: entry.providerId,
102
+ outcome: "failed",
103
+ kind,
104
+ error: error instanceof Error ? error.message : String(error)
105
+ });
106
+ await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, kind));
107
+ if (kind === "terminal") {
108
+ const refused = {
109
+ ok: false,
110
+ attempts,
111
+ error: new ProviderError("PAYLOAD_REJECTED", "The request was refused as malformed; no provider will accept it.")
112
+ };
113
+ options.after?.(refused);
114
+ return refused;
115
+ }
116
+ }
117
+ }
118
+ const failed = {
119
+ ok: false,
120
+ attempts,
121
+ error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider is configured for "${serviceKey}".` : `Every provider for "${serviceKey}" failed or was skipped.`)
122
+ };
123
+ options.after?.(failed);
124
+ return failed;
125
+ }
126
+ return { call };
127
+ }
128
+ var VERSION = "0.1.0";
129
+
130
+ // src/http.ts
131
+ class BudgetExhausted extends ProviderError {
132
+ host;
133
+ retryAfterMs;
134
+ constructor(host, retryAfterMs) {
135
+ super("RATE_BUDGET_EXHAUSTED", `The ${host} budget is spent. Retry in ${retryAfterMs}ms.`);
136
+ this.host = host;
137
+ this.retryAfterMs = retryAfterMs;
138
+ }
139
+ }
140
+ var windows = new Map;
141
+ function resetBudgets() {
142
+ windows.clear();
143
+ }
144
+ function spend(budget, cost, nowMs) {
145
+ const ceiling = Math.floor(budget.limit * (budget.headroom ?? 0.9));
146
+ const current = windows.get(budget.host);
147
+ if (!current || current.resetAtMs <= nowMs) {
148
+ windows.set(budget.host, { spent: cost, resetAtMs: nowMs + budget.windowMs });
149
+ return;
150
+ }
151
+ if (current.spent + cost > ceiling) {
152
+ throw new BudgetExhausted(budget.host, current.resetAtMs - nowMs);
153
+ }
154
+ current.spent += cost;
155
+ }
156
+ function settle(host, reserved, actual) {
157
+ const current = windows.get(host);
158
+ if (!current)
159
+ return;
160
+ current.spent = Math.max(0, current.spent - reserved + actual);
161
+ }
162
+ var budgetState = (host) => windows.get(host);
163
+ function createHttpClient(config) {
164
+ const doFetch = config.fetch ?? globalThis.fetch;
165
+ const timeoutMs = config.timeoutMs ?? 1e4;
166
+ return {
167
+ async call(request) {
168
+ const reserved = request.weight ?? config.budget.defaultCost;
169
+ spend(config.budget, reserved, Date.now());
170
+ const url = new URL(config.baseUrl + request.path);
171
+ for (const [key, value] of Object.entries(request.query ?? {})) {
172
+ url.searchParams.set(key, String(value));
173
+ }
174
+ const controller = new AbortController;
175
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
176
+ try {
177
+ const response = await doFetch(url.toString(), {
178
+ method: request.method ?? "GET",
179
+ signal: controller.signal,
180
+ headers: {
181
+ ...request.body === undefined ? {} : { "content-type": "application/json" },
182
+ ...request.headers
183
+ },
184
+ ...request.body === undefined ? {} : { body: JSON.stringify(request.body) }
185
+ });
186
+ const cost = config.costOf?.(response);
187
+ if (cost !== undefined)
188
+ settle(config.budget.host, reserved, cost);
189
+ const text = await response.text();
190
+ let body;
191
+ try {
192
+ body = text ? JSON.parse(text) : null;
193
+ } catch {
194
+ body = text;
195
+ }
196
+ if (!response.ok) {
197
+ throw Object.assign(new Error(`${config.budget.host} ${response.status}`), {
198
+ status: response.status,
199
+ body,
200
+ retryAfter: response.headers.get("retry-after")
201
+ });
202
+ }
203
+ return { status: response.status, body, cost };
204
+ } finally {
205
+ clearTimeout(timer);
206
+ }
207
+ },
208
+ budget: () => budgetState(config.budget.host)
209
+ };
210
+ }
211
+ var http = defineProvider({
212
+ id: "http",
213
+ service: "http",
214
+ label: "HTTP",
215
+ multiInstance: true,
216
+ credentials: {
217
+ type: "object",
218
+ additionalProperties: false,
219
+ properties: {
220
+ apiKey: { type: "string", title: "API key", writeOnly: true },
221
+ apiSecret: { type: "string", title: "API secret", writeOnly: true }
222
+ }
223
+ },
224
+ config: {
225
+ type: "object",
226
+ additionalProperties: false,
227
+ required: ["baseUrl"],
228
+ properties: {
229
+ baseUrl: { type: "string", title: "Base URL" },
230
+ limit: { type: "integer", default: 6000, title: "Units per window" },
231
+ windowMs: { type: "integer", default: 60000 },
232
+ headroom: {
233
+ type: "number",
234
+ default: 0.9,
235
+ description: "Stop at this fraction of the limit. The venue's window boundary is not ours, and the penalty for crossing is a ban rather than a rejection."
236
+ }
237
+ }
238
+ },
239
+ async invoke(context, request) {
240
+ const config = context.config;
241
+ const client = createHttpClient({
242
+ baseUrl: config.baseUrl,
243
+ fetch: config.fetch,
244
+ costOf: config.costOf,
245
+ budget: {
246
+ host: new URL(config.baseUrl).host,
247
+ limit: config.limit ?? 6000,
248
+ windowMs: config.windowMs ?? 60000,
249
+ defaultCost: 1,
250
+ headroom: config.headroom
251
+ }
252
+ });
253
+ return client.call(request);
254
+ },
255
+ classify(error) {
256
+ const status = error.status;
257
+ if (status === 418)
258
+ return "backoff";
259
+ if (status === 429)
260
+ return "backoff";
261
+ if (status === 400 || status === 422)
262
+ return "terminal";
263
+ if (status === 401 || status === 403)
264
+ return "retryable";
265
+ return "retryable";
266
+ }
267
+ });
268
+ var binanceWeight = (response) => {
269
+ const header = response.headers.get("x-mbx-used-weight-1m");
270
+ return header === null ? undefined : Number(header);
271
+ };
272
+ var httpProviders = [http];
273
+ export {
274
+ spend,
275
+ settle,
276
+ resetBudgets,
277
+ httpProviders,
278
+ http,
279
+ createHttpClient,
280
+ budgetState,
281
+ binanceWeight,
282
+ BudgetExhausted
283
+ };