@forgezero/providers 0.1.9 → 0.1.11

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.d.ts CHANGED
@@ -6,25 +6,12 @@ export interface JetEmailConfig {
6
6
  batchEndpoint?: string;
7
7
  fetch?: typeof globalThis.fetch;
8
8
  }
9
- /**
10
- * Status handling, from the published API:
11
- *
12
- * 201 sent 202 scheduled
13
- * 400 bad input → TERMINAL: our payload is malformed and no relay will
14
- * accept it, so trying the next one wastes a round trip
15
- * and buries the real error
16
- * 401 bad key → RETRYABLE: THIS key is wrong; the next provider's may
17
- * be fine. This is the case people get wrong by treating
18
- * any 4xx as fatal
19
- * 409 idempotency → TERMINAL: the message already exists. Re-sending via
20
- * another provider would deliver it twice
21
- * 429 rate limited → BACKOFF: it is working, we are asking too fast
22
- */
23
- export declare const jetemail: import("./index").ProviderSpec<EmailMessage, {
24
- id: string;
9
+ export declare const jetemail: import("./index").ProviderDefinition<{
10
+ send: import("./index").ProviderMethodSpec<EmailMessage, {
11
+ id: string;
12
+ }>;
13
+ sendBatch: import("./index").ProviderMethodSpec<EmailBatch, EmailBatchResult>;
25
14
  }>;
26
- /** JetEmail's native POST /email-batch capability. */
27
- export declare const jetemailBatch: import("./index").ProviderSpec<EmailBatch, EmailBatchResult>;
28
15
  /**
29
16
  * A transport, so this package holds no socket code and no nodemailer.
30
17
  *
@@ -49,31 +36,26 @@ export interface SmtpTransport {
49
36
  messageId: string;
50
37
  }>;
51
38
  }
52
- /**
53
- * SMTP is `multiInstance`: a deployment may hold several named relays, each its
54
- * own priority slot — a primary, a backup with a different vendor, and a
55
- * loopback for development.
56
- *
57
- * SMTP reply codes carry the same distinction as HTTP status:
58
- *
59
- * 5xx permanent → TERMINAL for 550/553 (bad recipient — every relay agrees)
60
- * RETRYABLE for 535 (auth failed — THIS relay's credentials)
61
- * 4xx transient → RETRYABLE, and 421/450/451 specifically mean try later
62
- */
63
- export declare const smtp: import("./index").ProviderSpec<EmailMessage, {
64
- id: string;
39
+ export declare const smtp: import("./index").ProviderDefinition<{
40
+ send: import("./index").ProviderMethodSpec<EmailMessage, {
41
+ id: string;
42
+ }>;
65
43
  }>;
66
- /**
67
- * SMTP has no native batch protocol. This adapter deliberately returns one
68
- * result per message and never throws after a partial send, so the registry
69
- * cannot replay an already-delivered prefix through another provider.
70
- */
71
- export declare const smtpBatch: import("./index").ProviderSpec<EmailBatch, EmailBatchResult>;
72
- /** Ready to register. Order here is irrelevant; the config decides priority. */
73
- export declare const emailProviders: readonly [import("./index").ProviderSpec<EmailMessage, {
74
- id: string;
75
- }>, import("./index").ProviderSpec<EmailMessage, {
76
- id: string;
44
+ /** Ready to register for the email service's `send` method. */
45
+ export declare const emailProviders: readonly [import("./index").ProviderDefinition<{
46
+ send: import("./index").ProviderMethodSpec<EmailMessage, {
47
+ id: string;
48
+ }>;
49
+ sendBatch: import("./index").ProviderMethodSpec<EmailBatch, EmailBatchResult>;
50
+ }>, import("./index").ProviderDefinition<{
51
+ send: import("./index").ProviderMethodSpec<EmailMessage, {
52
+ id: string;
53
+ }>;
77
54
  }>];
78
- /** The separate batch service attaches the providers' batch methods in priority order. */
79
- export declare const emailBatchProviders: readonly [import("./index").ProviderSpec<EmailBatch, EmailBatchResult>, import("./index").ProviderSpec<EmailBatch, EmailBatchResult>];
55
+ /** Provider-neutral service contract applications and SSOT handlers call. */
56
+ export declare const emailService: import("./index").ServiceDefinition<"email", {
57
+ send: import("./index").ServiceMethodContract<EmailMessage, {
58
+ id: string;
59
+ }>;
60
+ sendBatch: import("./index").ServiceMethodContract<EmailBatch, EmailBatchResult>;
61
+ }>;
package/dist/email.js CHANGED
@@ -38,25 +38,71 @@ function chainCredentials(...sources) {
38
38
  }
39
39
  };
40
40
  }
41
- function staticConfig(services) {
41
+ function scopeCredentials(source, reference) {
42
+ return {
43
+ name: `${source.name}:${reference}`,
44
+ get: (field) => source.get(reference, field)
45
+ };
46
+ }
47
+ function chainScopedCredentials(...sources) {
48
+ return {
49
+ name: sources.map((source) => source.name).join("+"),
50
+ async get(field) {
51
+ let last;
52
+ for (const source of sources) {
53
+ try {
54
+ return await source.get(field);
55
+ } catch (error) {
56
+ last = error;
57
+ }
58
+ }
59
+ throw last instanceof Error ? last : new ProviderError("CREDENTIAL_MISSING", `No scoped source held field "${field}".`);
60
+ }
61
+ };
62
+ }
63
+ function staticConfig(config) {
64
+ const providers = new Map(config.providers.map((provider) => [provider.instanceKey, provider]));
65
+ if (providers.size !== config.providers.length) {
66
+ throw new ProviderError("CONFIG_DUPLICATE_INSTANCE", "Provider instance keys must be unique.");
67
+ }
42
68
  const health = new Map;
43
69
  return {
44
70
  name: "static",
45
- async list(serviceKey) {
46
- return (services[serviceKey] ?? []).map((provider) => ({
47
- ...provider,
48
- health: health.get(`${serviceKey}:${provider.providerId}`) ?? provider.health
71
+ async provider(instanceKey) {
72
+ const provider = providers.get(instanceKey);
73
+ return provider ? { ...provider, config: { ...provider.config } } : undefined;
74
+ },
75
+ async list(serviceKey, methodKey) {
76
+ return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
77
+ ...attachment,
78
+ health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}`) ?? attachment.health
49
79
  }));
50
80
  },
51
- async recordHealth(serviceKey, providerId, next) {
52
- health.set(`${serviceKey}:${providerId}`, next);
81
+ async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, next) {
82
+ health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}`, next);
53
83
  }
54
84
  };
55
85
  }
86
+ function defineProviderMethod(method) {
87
+ return method;
88
+ }
56
89
  function defineProvider(spec) {
57
90
  return spec;
58
91
  }
92
+ function defineSingleMethodProvider(spec) {
93
+ const { method, invoke, classify, ...identity } = spec;
94
+ return defineProvider({
95
+ ...identity,
96
+ methods: { [method]: defineProviderMethod({ invoke, classify }) }
97
+ });
98
+ }
59
99
  var STRIKES_TO_OFFLINE = 3;
100
+ function serviceMethod() {
101
+ return Object.freeze({});
102
+ }
103
+ function defineService(definition) {
104
+ return definition;
105
+ }
60
106
  function nextHealth(current, kind) {
61
107
  if (kind === "success")
62
108
  return { strikes: 0, status: "ok" };
@@ -71,80 +117,188 @@ function nextHealth(current, kind) {
71
117
  }
72
118
  function createRegistry(options) {
73
119
  const byId = new Map(options.providers.map((provider) => [provider.id, provider]));
74
- async function call(serviceKey, args, callOptions = {}) {
75
- const configured = [...await options.config.list(serviceKey)].filter((provider) => provider.enabled).sort((a, b) => a.priority - b.priority);
120
+ async function call(serviceKey, methodKey, args, callOptions = {}) {
121
+ const configured = [...await options.config.list(serviceKey, methodKey)].filter((attachment) => attachment.enabled).sort((a, b) => a.priority - b.priority);
76
122
  const attempts = [];
77
- for (const entry of configured) {
123
+ for (const attachment of configured) {
78
124
  if (callOptions.signal?.aborted) {
79
125
  const cancelled = {
80
126
  ok: false,
127
+ fallbackUsed: attempts.length > 0,
81
128
  attempts,
82
- error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
129
+ error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
83
130
  };
84
- options.after?.(cancelled);
131
+ await options.after?.(cancelled);
85
132
  return cancelled;
86
133
  }
87
- const spec = byId.get(entry.providerId);
88
- if (!spec) {
89
- attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "not registered" });
134
+ const instance = await options.config.provider(attachment.instanceKey);
135
+ if (!instance) {
136
+ attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance not registered" });
90
137
  continue;
91
138
  }
92
- if (entry.health?.status === "offline") {
93
- attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "offline" });
139
+ const spec = byId.get(instance.providerId);
140
+ const method = spec?.methods[attachment.providerMethod];
141
+ if (!spec || !method) {
142
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: !spec ? "provider not registered" : "method not supported" });
94
143
  continue;
95
144
  }
96
- options.before?.({ service: serviceKey, provider: entry.providerId });
145
+ if (!instance.enabled) {
146
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance disabled" });
147
+ continue;
148
+ }
149
+ if (attachment.health?.status === "offline") {
150
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "offline" });
151
+ continue;
152
+ }
153
+ await options.before?.({ service: serviceKey, method: methodKey, provider: instance.providerId, instance: instance.instanceKey });
154
+ const startedAt = performance.now();
97
155
  try {
98
- const result = await spec.invoke({
99
- config: entry.config,
100
- secret: (field) => options.credentials.get(entry.secretRef, field),
156
+ const result = await method.invoke({
157
+ config: instance.config,
158
+ secret: (field) => options.credentials.get(instance.secretRef, field),
101
159
  signal: callOptions.signal
102
160
  }, args);
103
- attempts.push({ providerId: entry.providerId, outcome: "sent" });
104
- await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, "success"));
105
- const sent = { ok: true, result, provider: entry.providerId, attempts };
106
- options.after?.(sent);
161
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "sent", durationMs: performance.now() - startedAt });
162
+ await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, "success"));
163
+ const sent = {
164
+ ok: true,
165
+ result,
166
+ provider: instance.providerId,
167
+ instance: instance.instanceKey,
168
+ method: attachment.providerMethod,
169
+ selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod },
170
+ fallbackUsed: attempts.length > 1,
171
+ attempts
172
+ };
173
+ await options.after?.(sent);
107
174
  return sent;
108
175
  } catch (error) {
109
176
  if (callOptions.signal?.aborted) {
110
177
  const cancelled = {
111
178
  ok: false,
179
+ fallbackUsed: attempts.length > 0,
112
180
  attempts,
113
- error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
181
+ error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
114
182
  };
115
- options.after?.(cancelled);
183
+ await options.after?.(cancelled);
116
184
  return cancelled;
117
185
  }
118
- const kind = spec.classify(error);
186
+ const kind = method.classify(error);
187
+ const message = error instanceof Error ? error.message : String(error);
188
+ const code = error instanceof ProviderError ? error.code : typeof error?.code === "string" ? error.code : undefined;
119
189
  attempts.push({
120
- providerId: entry.providerId,
190
+ providerId: instance.providerId,
191
+ instanceKey: instance.instanceKey,
192
+ providerMethod: attachment.providerMethod,
121
193
  outcome: "failed",
122
194
  kind,
123
- error: error instanceof Error ? error.message : String(error)
195
+ durationMs: performance.now() - startedAt,
196
+ failure: { kind, ...code ? { code } : {}, message },
197
+ error: message
124
198
  });
125
- await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, kind));
199
+ await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, kind));
126
200
  if (kind === "terminal") {
127
201
  const refused = {
128
202
  ok: false,
203
+ fallbackUsed: attempts.length > 1,
129
204
  attempts,
130
205
  error: new ProviderError("PAYLOAD_REJECTED", "The request was refused as malformed; no provider will accept it.")
131
206
  };
132
- options.after?.(refused);
207
+ await options.after?.(refused);
133
208
  return refused;
134
209
  }
135
210
  }
136
211
  }
137
212
  const failed = {
138
213
  ok: false,
214
+ fallbackUsed: attempts.length > 1,
139
215
  attempts,
140
- 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.`)
216
+ error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider method is configured for "${serviceKey}.${methodKey}".` : `Every provider method for "${serviceKey}.${methodKey}" failed or was skipped.`)
141
217
  };
142
- options.after?.(failed);
218
+ await options.after?.(failed);
143
219
  return failed;
144
220
  }
145
- return { call };
221
+ function service(definition) {
222
+ return {
223
+ call(method, args, callOptions) {
224
+ return call(definition.key, method, args, callOptions);
225
+ }
226
+ };
227
+ }
228
+ return { call, service };
229
+ }
230
+ function createService(definition, methods, options = {}) {
231
+ const providers = new Map;
232
+ const credentials = new Map;
233
+ const instances = [];
234
+ const serviceMethods = {};
235
+ for (const methodKey of Object.keys(definition.methods)) {
236
+ const attachments = methods[methodKey];
237
+ if (!Array.isArray(attachments)) {
238
+ throw new ProviderError("CONFIG_METHOD_MISSING", `Service method "${definition.key}.${methodKey}" needs an attachment array.`);
239
+ }
240
+ const priorities = new Set;
241
+ serviceMethods[methodKey] = attachments.map((attachment, index) => {
242
+ if (!Number.isInteger(attachment.priority) || attachment.priority < 1 || attachment.priority > 1000) {
243
+ throw new ProviderError("CONFIG_PRIORITY", `Priority for "${definition.key}.${methodKey}" must be an integer from 1 to 1000.`);
244
+ }
245
+ if (priorities.has(attachment.priority)) {
246
+ throw new ProviderError("CONFIG_PRIORITY_DUPLICATE", `Priorities for "${definition.key}.${methodKey}" must be unique.`);
247
+ }
248
+ priorities.add(attachment.priority);
249
+ if (!attachment.provider.methods[attachment.method]) {
250
+ throw new ProviderError("CONFIG_METHOD_UNSUPPORTED", `Provider "${attachment.provider.id}" does not support "${attachment.method}".`);
251
+ }
252
+ const existing = providers.get(attachment.provider.id);
253
+ if (existing && existing !== attachment.provider) {
254
+ throw new ProviderError("CONFIG_PROVIDER_DUPLICATE", `Provider id "${attachment.provider.id}" has more than one definition.`);
255
+ }
256
+ providers.set(attachment.provider.id, attachment.provider);
257
+ const internalKey = `${definition.key}:${methodKey}:${index}`;
258
+ credentials.set(internalKey, attachment.credentials);
259
+ instances.push({
260
+ instanceKey: internalKey,
261
+ providerId: attachment.provider.id,
262
+ enabled: attachment.enabled ?? true,
263
+ config: { ...attachment.config ?? {} },
264
+ secretRef: internalKey
265
+ });
266
+ return {
267
+ instanceKey: internalKey,
268
+ providerMethod: attachment.method,
269
+ priority: attachment.priority,
270
+ enabled: attachment.enabled ?? true
271
+ };
272
+ });
273
+ }
274
+ const registry = createRegistry({
275
+ providers: [...providers.values()],
276
+ config: staticConfig({ providers: instances, services: { [definition.key]: serviceMethods } }),
277
+ credentials: {
278
+ name: [...new Set([...credentials.values()].map((source) => source.name))].join("+"),
279
+ async get(reference, field) {
280
+ const source = credentials.get(reference);
281
+ if (!source)
282
+ throw new ProviderError("CREDENTIAL_SOURCE_MISSING", "The provider credential source is unavailable.");
283
+ return source.get(field);
284
+ }
285
+ },
286
+ ...options
287
+ });
288
+ const dynamic = registry.service(definition);
289
+ return {
290
+ async call(method, args, callOptions) {
291
+ const result = await dynamic.call(method, args, callOptions);
292
+ const { instance: _instance, selected, attempts, ...rest } = result;
293
+ return {
294
+ ...rest,
295
+ ...selected ? { selected: { providerId: selected.providerId, providerMethod: selected.providerMethod } } : {},
296
+ attempts: attempts.map(({ instanceKey: _instanceKey, ...attempt }) => attempt)
297
+ };
298
+ }
299
+ };
146
300
  }
147
- var VERSION = "0.1.9";
301
+ var VERSION = "0.1.11";
148
302
 
149
303
  // src/email.ts
150
304
  var recipients = (to) => Array.isArray(to) ? [...to] : [to];
@@ -156,31 +310,28 @@ function assertMessage(message) {
156
310
  throw new ProviderError("EMAIL_NO_BODY", "An email needs html, text, or both.");
157
311
  }
158
312
  }
159
- var jetemail = defineProvider({
160
- id: "jetemail",
161
- service: "email",
162
- label: "JetEmail",
163
- credentials: {
164
- type: "object",
165
- additionalProperties: false,
166
- required: ["apiKey"],
167
- properties: {
168
- apiKey: {
169
- type: "string",
170
- title: "API key",
171
- writeOnly: true,
172
- description: "Transactional key from the JetEmail dashboard."
173
- }
313
+ var jetemailCredentials = {
314
+ type: "object",
315
+ additionalProperties: false,
316
+ required: ["apiKey"],
317
+ properties: {
318
+ apiKey: {
319
+ type: "string",
320
+ title: "API key",
321
+ writeOnly: true,
322
+ description: "Transactional key from the JetEmail dashboard."
174
323
  }
175
- },
176
- config: {
177
- type: "object",
178
- additionalProperties: false,
179
- properties: {
180
- eu: { type: "boolean", default: false, title: "EU residency" },
181
- from: { type: "string", format: "email", title: "Default from address" }
182
- }
183
- },
324
+ }
325
+ };
326
+ var jetemailConfig = {
327
+ type: "object",
328
+ additionalProperties: false,
329
+ properties: {
330
+ eu: { type: "boolean", default: false, title: "EU residency" },
331
+ from: { type: "string", format: "email", title: "Default from address" }
332
+ }
333
+ };
334
+ var jetemailSend = defineProviderMethod({
184
335
  async invoke(context, message) {
185
336
  assertMessage(message);
186
337
  const config = context.config;
@@ -230,12 +381,7 @@ function assertBatch(batch) {
230
381
  for (const message of batch.emails)
231
382
  assertMessage(message);
232
383
  }
233
- var jetemailBatch = defineProvider({
234
- id: "jetemail",
235
- service: "email.batch",
236
- label: "JetEmail",
237
- credentials: jetemail.credentials,
238
- config: jetemail.config,
384
+ var jetemailSendBatch = defineProviderMethod({
239
385
  async invoke(context, batch) {
240
386
  assertBatch(batch);
241
387
  const config = context.config;
@@ -287,37 +433,19 @@ var jetemailBatch = defineProvider({
287
433
  });
288
434
  return { summary, results };
289
435
  },
290
- classify: jetemail.classify
436
+ classify: jetemailSend.classify
291
437
  });
292
- var smtp = defineProvider({
293
- id: "smtp",
294
- service: "email",
295
- label: "SMTP",
296
- multiInstance: true,
297
- credentials: {
298
- type: "object",
299
- additionalProperties: false,
300
- properties: {
301
- user: { type: "string", title: "Username" },
302
- password: { type: "string", title: "Password", writeOnly: true }
303
- }
304
- },
305
- config: {
306
- type: "object",
307
- additionalProperties: false,
308
- required: ["host", "from"],
309
- properties: {
310
- host: { type: "string", title: "Host" },
311
- port: { type: "integer", default: 587, minimum: 1, maximum: 65535 },
312
- secure: {
313
- type: "boolean",
314
- default: false,
315
- title: "Implicit TLS",
316
- description: "True for port 465. Port 587 upgrades with STARTTLS and should stay false."
317
- },
318
- from: { type: "string", format: "email", title: "From address" }
319
- }
320
- },
438
+ var jetemail = defineProvider({
439
+ id: "jetemail",
440
+ label: "JetEmail",
441
+ credentials: jetemailCredentials,
442
+ config: jetemailConfig,
443
+ methods: {
444
+ send: jetemailSend,
445
+ sendBatch: jetemailSendBatch
446
+ }
447
+ });
448
+ var smtpSend = defineProviderMethod({
321
449
  async invoke(context, message) {
322
450
  assertMessage(message);
323
451
  const config = context.config;
@@ -356,45 +484,47 @@ var smtp = defineProvider({
356
484
  return "retryable";
357
485
  }
358
486
  });
359
- var smtpBatch = defineProvider({
487
+ var smtp = defineProvider({
360
488
  id: "smtp",
361
- service: "email.batch",
362
489
  label: "SMTP",
363
490
  multiInstance: true,
364
- credentials: smtp.credentials,
365
- config: smtp.config,
366
- async invoke(context, batch) {
367
- assertBatch(batch);
368
- const results = [];
369
- for (let offset = 0;offset < batch.emails.length; offset += 5) {
370
- const rows = await Promise.all(batch.emails.slice(offset, offset + 5).map(async (message) => {
371
- try {
372
- const sent = await smtp.invoke(context, message);
373
- return { status: "success", id: sent.id };
374
- } catch (cause) {
375
- return {
376
- status: "failed",
377
- error: cause instanceof Error ? cause.message.slice(0, 500) : "SMTP send failed"
378
- };
379
- }
380
- }));
381
- results.push(...rows);
491
+ credentials: {
492
+ type: "object",
493
+ additionalProperties: false,
494
+ properties: {
495
+ user: { type: "string", title: "Username" },
496
+ password: { type: "string", title: "Password", writeOnly: true }
382
497
  }
383
- const successful = results.filter((result) => result.status === "success").length;
384
- return {
385
- summary: { total: results.length, successful, failed: results.length - successful },
386
- results
387
- };
388
498
  },
389
- classify: smtp.classify
499
+ config: {
500
+ type: "object",
501
+ additionalProperties: false,
502
+ required: ["host", "from"],
503
+ properties: {
504
+ host: { type: "string", title: "Host" },
505
+ port: { type: "integer", default: 587, minimum: 1, maximum: 65535 },
506
+ secure: {
507
+ type: "boolean",
508
+ default: false,
509
+ title: "Implicit TLS",
510
+ description: "True for port 465. Port 587 upgrades with STARTTLS and should stay false."
511
+ },
512
+ from: { type: "string", format: "email", title: "From address" }
513
+ }
514
+ },
515
+ methods: { send: smtpSend }
390
516
  });
391
517
  var emailProviders = [jetemail, smtp];
392
- var emailBatchProviders = [jetemailBatch, smtpBatch];
518
+ var emailService = defineService({
519
+ key: "email",
520
+ methods: {
521
+ send: serviceMethod(),
522
+ sendBatch: serviceMethod()
523
+ }
524
+ });
393
525
  export {
394
- smtpBatch,
395
526
  smtp,
396
- jetemailBatch,
397
527
  jetemail,
398
- emailProviders,
399
- emailBatchProviders
528
+ emailService,
529
+ emailProviders
400
530
  };
package/dist/http.d.ts CHANGED
@@ -93,8 +93,8 @@ export type HttpClient = ReturnType<typeof createHttpClient>;
93
93
  * `classify` is where the venue-specific knowledge lives, and it is the part
94
94
  * that decides whether a retry helps or makes things worse.
95
95
  */
96
- export declare const http: import("./index").ProviderSpec<HttpRequest, HttpResponse<unknown>>;
96
+ export declare const http: import("./index").ProviderDefinition<Record<"request", import("./index").ProviderMethodSpec<HttpRequest, HttpResponse<unknown>>>>;
97
97
  /** Binance reports a running total for the window, not a per-call cost. */
98
98
  export declare const binanceWeight: (response: Response) => number | undefined;
99
- export declare const httpProviders: readonly [import("./index").ProviderSpec<HttpRequest, HttpResponse<unknown>>];
99
+ export declare const httpProviders: readonly [import("./index").ProviderDefinition<Record<"request", import("./index").ProviderMethodSpec<HttpRequest, HttpResponse<unknown>>>>];
100
100
  export {};