@forgezero/providers 0.1.8 → 0.1.9

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/binance.js CHANGED
@@ -144,7 +144,7 @@ function createRegistry(options) {
144
144
  }
145
145
  return { call };
146
146
  }
147
- var VERSION = "0.1.8";
147
+ var VERSION = "0.1.9";
148
148
 
149
149
  // src/http.ts
150
150
  class BudgetExhausted extends ProviderError {
package/dist/chain.js CHANGED
@@ -144,7 +144,7 @@ function createRegistry(options) {
144
144
  }
145
145
  return { call };
146
146
  }
147
- var VERSION = "0.1.8";
147
+ var VERSION = "0.1.9";
148
148
 
149
149
  // src/chain.ts
150
150
  var hexToNumber = (value) => Number(BigInt(value));
package/dist/database.js CHANGED
@@ -144,7 +144,7 @@ function createRegistry(options) {
144
144
  }
145
145
  return { call };
146
146
  }
147
- var VERSION = "0.1.8";
147
+ var VERSION = "0.1.9";
148
148
 
149
149
  // src/database.ts
150
150
  var pools = new Map;
package/dist/email.d.ts CHANGED
@@ -1,8 +1,9 @@
1
- import { type EmailMessage } from './index';
1
+ import { type EmailBatch, type EmailBatchResult, type EmailMessage } from './index';
2
2
  export interface JetEmailConfig {
3
3
  /** EU residency endpoint. */
4
4
  eu?: boolean;
5
5
  endpoint?: string;
6
+ batchEndpoint?: string;
6
7
  fetch?: typeof globalThis.fetch;
7
8
  }
8
9
  /**
@@ -22,6 +23,8 @@ export interface JetEmailConfig {
22
23
  export declare const jetemail: import("./index").ProviderSpec<EmailMessage, {
23
24
  id: string;
24
25
  }>;
26
+ /** JetEmail's native POST /email-batch capability. */
27
+ export declare const jetemailBatch: import("./index").ProviderSpec<EmailBatch, EmailBatchResult>;
25
28
  /**
26
29
  * A transport, so this package holds no socket code and no nodemailer.
27
30
  *
@@ -60,9 +63,17 @@ export interface SmtpTransport {
60
63
  export declare const smtp: import("./index").ProviderSpec<EmailMessage, {
61
64
  id: string;
62
65
  }>;
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>;
63
72
  /** Ready to register. Order here is irrelevant; the config decides priority. */
64
73
  export declare const emailProviders: readonly [import("./index").ProviderSpec<EmailMessage, {
65
74
  id: string;
66
75
  }>, import("./index").ProviderSpec<EmailMessage, {
67
76
  id: string;
68
77
  }>];
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>];
package/dist/email.js CHANGED
@@ -144,7 +144,7 @@ function createRegistry(options) {
144
144
  }
145
145
  return { call };
146
146
  }
147
- var VERSION = "0.1.8";
147
+ var VERSION = "0.1.9";
148
148
 
149
149
  // src/email.ts
150
150
  var recipients = (to) => Array.isArray(to) ? [...to] : [to];
@@ -191,7 +191,8 @@ var jetemail = defineProvider({
191
191
  signal: context.signal,
192
192
  headers: {
193
193
  "content-type": "application/json",
194
- authorization: `Bearer ${await context.secret("apiKey")}`
194
+ authorization: `Bearer ${await context.secret("apiKey")}`,
195
+ ...message.idempotencyKey ? { "idempotency-key": message.idempotencyKey } : {}
195
196
  },
196
197
  body: JSON.stringify({
197
198
  from: message.from ?? config.from,
@@ -222,6 +223,72 @@ var jetemail = defineProvider({
222
223
  return "retryable";
223
224
  }
224
225
  });
226
+ function assertBatch(batch) {
227
+ if (!Array.isArray(batch.emails) || batch.emails.length < 1 || batch.emails.length > 100) {
228
+ throw new ProviderError("EMAIL_BATCH_SIZE", "An email batch must contain between 1 and 100 messages.");
229
+ }
230
+ for (const message of batch.emails)
231
+ assertMessage(message);
232
+ }
233
+ var jetemailBatch = defineProvider({
234
+ id: "jetemail",
235
+ service: "email.batch",
236
+ label: "JetEmail",
237
+ credentials: jetemail.credentials,
238
+ config: jetemail.config,
239
+ async invoke(context, batch) {
240
+ assertBatch(batch);
241
+ const config = context.config;
242
+ const doFetch = config.fetch ?? globalThis.fetch;
243
+ const response = await doFetch(config.batchEndpoint ?? "https://api.jetemail.com/email-batch", {
244
+ method: "POST",
245
+ signal: context.signal,
246
+ headers: {
247
+ "content-type": "application/json",
248
+ authorization: `Bearer ${await context.secret("apiKey")}`
249
+ },
250
+ body: JSON.stringify({
251
+ emails: batch.emails.map((message) => ({
252
+ from: message.from ?? config.from,
253
+ to: recipients(message.to),
254
+ subject: message.subject,
255
+ html: message.html,
256
+ text: message.text,
257
+ reply_to: message.replyTo,
258
+ eu: config.eu ?? false
259
+ }))
260
+ })
261
+ });
262
+ if (response.status !== 207) {
263
+ const detail = await response.text().catch(() => "");
264
+ throw Object.assign(new Error(`JetEmail batch ${response.status}: ${detail.slice(0, 200)}`), {
265
+ status: response.status,
266
+ retryAfter: response.headers.get("retry-after")
267
+ });
268
+ }
269
+ const payload = await response.json().catch(() => null);
270
+ if (!payload?.summary || !Array.isArray(payload.results) || ![payload.summary.total, payload.summary.successful, payload.summary.failed].every((value) => Number.isInteger(value) && value >= 0)) {
271
+ throw new ProviderError("JETEMAIL_BATCH_RESPONSE", "JetEmail returned a malformed batch response.");
272
+ }
273
+ const summary = payload.summary;
274
+ if (summary.total !== batch.emails.length || summary.successful + summary.failed !== summary.total) {
275
+ throw new ProviderError("JETEMAIL_BATCH_RESPONSE", "JetEmail batch totals do not match the request.");
276
+ }
277
+ const results = payload.results.map((item) => {
278
+ if (!item || typeof item !== "object")
279
+ return { status: "failed", error: "Malformed provider result" };
280
+ const row = item;
281
+ return {
282
+ status: row.status === "success" ? "success" : "failed",
283
+ ...typeof row.id === "string" ? { id: row.id } : {},
284
+ ...typeof row.response === "string" ? { response: row.response.slice(0, 500) } : {},
285
+ ...typeof row.error === "string" ? { error: row.error.slice(0, 500) } : {}
286
+ };
287
+ });
288
+ return { summary, results };
289
+ },
290
+ classify: jetemail.classify
291
+ });
225
292
  var smtp = defineProvider({
226
293
  id: "smtp",
227
294
  service: "email",
@@ -289,9 +356,45 @@ var smtp = defineProvider({
289
356
  return "retryable";
290
357
  }
291
358
  });
359
+ var smtpBatch = defineProvider({
360
+ id: "smtp",
361
+ service: "email.batch",
362
+ label: "SMTP",
363
+ 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);
382
+ }
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
+ },
389
+ classify: smtp.classify
390
+ });
292
391
  var emailProviders = [jetemail, smtp];
392
+ var emailBatchProviders = [jetemailBatch, smtpBatch];
293
393
  export {
394
+ smtpBatch,
294
395
  smtp,
396
+ jetemailBatch,
295
397
  jetemail,
296
- emailProviders
398
+ emailProviders,
399
+ emailBatchProviders
297
400
  };
package/dist/http.js CHANGED
@@ -144,7 +144,7 @@ function createRegistry(options) {
144
144
  }
145
145
  return { call };
146
146
  }
147
- var VERSION = "0.1.8";
147
+ var VERSION = "0.1.9";
148
148
 
149
149
  // src/http.ts
150
150
  class BudgetExhausted extends ProviderError {
package/dist/index.d.ts CHANGED
@@ -155,5 +155,25 @@ export interface EmailMessage {
155
155
  text?: string;
156
156
  from?: string;
157
157
  replyTo?: string;
158
+ /** Stable caller-owned key for providers that support safe replay. */
159
+ idempotencyKey?: string;
158
160
  }
159
- export declare const VERSION = "0.1.8";
161
+ export interface EmailBatch {
162
+ /** Provider-neutral hard ceiling matches JetEmail's native batch contract. */
163
+ emails: readonly EmailMessage[];
164
+ }
165
+ export interface EmailBatchItemResult {
166
+ status: 'success' | 'failed';
167
+ id?: string;
168
+ response?: string;
169
+ error?: string;
170
+ }
171
+ export interface EmailBatchResult {
172
+ summary: {
173
+ total: number;
174
+ successful: number;
175
+ failed: number;
176
+ };
177
+ results: readonly EmailBatchItemResult[];
178
+ }
179
+ export declare const VERSION = "0.1.9";
package/dist/index.js CHANGED
@@ -144,7 +144,7 @@ function createRegistry(options) {
144
144
  }
145
145
  return { call };
146
146
  }
147
- var VERSION = "0.1.8";
147
+ var VERSION = "0.1.9";
148
148
  export {
149
149
  staticConfig,
150
150
  nextHealth,
package/dist/pool.js CHANGED
@@ -144,7 +144,7 @@ function createRegistry(options) {
144
144
  }
145
145
  return { call };
146
146
  }
147
- var VERSION = "0.1.8";
147
+ var VERSION = "0.1.9";
148
148
 
149
149
  // src/http.ts
150
150
  class BudgetExhausted extends ProviderError {
@@ -0,0 +1,19 @@
1
+ import { type RealtimeBatch } from '@forgezero/runtime/realtime';
2
+ export interface RealtimeProviderOptions {
3
+ endpoint: string;
4
+ secret: string;
5
+ producer: string;
6
+ requestTimeoutMs?: number;
7
+ fetcher?: typeof fetch;
8
+ }
9
+ export declare class RealtimeProviderError extends Error {
10
+ readonly code: 'INVALID_CONFIG' | 'UNAVAILABLE' | 'REFUSED';
11
+ constructor(code: 'INVALID_CONFIG' | 'UNAVAILABLE' | 'REFUSED', message: string);
12
+ }
13
+ /** One external request per batch; Worker/DO bindings own the internal fan-out. */
14
+ export declare function cloudflareRealtime(options: RealtimeProviderOptions): {
15
+ publish(input: RealtimeBatch): Promise<{
16
+ delivered: number;
17
+ shards: number;
18
+ }>;
19
+ };
@@ -0,0 +1,66 @@
1
+ // src/realtime.ts
2
+ import { realtimeBatchBytes, realtimeHmac, validateRealtimeBatch } from "@forgezero/runtime/realtime";
3
+
4
+ class RealtimeProviderError extends Error {
5
+ code;
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.code = code;
9
+ this.name = "RealtimeProviderError";
10
+ }
11
+ }
12
+ var producerPattern = /^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,127}$/;
13
+ function cloudflareRealtime(options) {
14
+ const endpoint = new URL(options.endpoint);
15
+ if (endpoint.protocol !== "https:" || endpoint.username || endpoint.password || endpoint.search || endpoint.hash || !producerPattern.test(options.producer) || new TextEncoder().encode(options.secret).byteLength < 32) {
16
+ throw new RealtimeProviderError("INVALID_CONFIG", "Realtime endpoint, producer, or secret is invalid.");
17
+ }
18
+ endpoint.pathname = "/__fz/realtime/publish";
19
+ const timeout = options.requestTimeoutMs ?? 5000;
20
+ if (!Number.isSafeInteger(timeout) || timeout < 100 || timeout > 30000) {
21
+ throw new RealtimeProviderError("INVALID_CONFIG", "Realtime timeout is invalid.");
22
+ }
23
+ const fetcher = options.fetcher ?? fetch;
24
+ return {
25
+ async publish(input) {
26
+ const batch = validateRealtimeBatch(input);
27
+ const body = realtimeBatchBytes(batch);
28
+ const timestamp = Math.floor(Date.now() / 1000).toString();
29
+ const signature = await realtimeHmac(options.secret, `publish
30
+ ${options.producer}
31
+ ${timestamp}
32
+ ${body}`);
33
+ let response;
34
+ try {
35
+ response = await fetcher(endpoint, {
36
+ method: "POST",
37
+ body,
38
+ redirect: "error",
39
+ signal: AbortSignal.timeout(timeout),
40
+ headers: {
41
+ "content-type": "application/json",
42
+ "x-fz-realtime-producer": options.producer,
43
+ "x-fz-realtime-timestamp": timestamp,
44
+ "x-fz-realtime-signature": signature,
45
+ "idempotency-key": batch.batchId
46
+ }
47
+ });
48
+ } catch {
49
+ throw new RealtimeProviderError("UNAVAILABLE", "Realtime edge is unavailable.");
50
+ }
51
+ if (!response.ok) {
52
+ await response.body?.cancel();
53
+ throw new RealtimeProviderError(response.status >= 500 ? "UNAVAILABLE" : "REFUSED", `Realtime edge returned HTTP ${response.status}.`);
54
+ }
55
+ const result = await response.json();
56
+ if (!Number.isSafeInteger(result.delivered) || !Number.isSafeInteger(result.shards)) {
57
+ throw new RealtimeProviderError("UNAVAILABLE", "Realtime edge returned malformed evidence.");
58
+ }
59
+ return { delivered: result.delivered, shards: result.shards };
60
+ }
61
+ };
62
+ }
63
+ export {
64
+ cloudflareRealtime,
65
+ RealtimeProviderError
66
+ };
package/dist/storage.js CHANGED
@@ -144,7 +144,7 @@ function createRegistry(options) {
144
144
  }
145
145
  return { call };
146
146
  }
147
- var VERSION = "0.1.8";
147
+ var VERSION = "0.1.9";
148
148
 
149
149
  // src/storage.ts
150
150
  var encoder = new TextEncoder;
@@ -144,7 +144,7 @@ function createRegistry(options) {
144
144
  }
145
145
  return { call };
146
146
  }
147
- var VERSION = "0.1.8";
147
+ var VERSION = "0.1.9";
148
148
 
149
149
  // src/translation.ts
150
150
  var GOOGLE_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/models";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgezero/providers",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -43,11 +43,15 @@
43
43
  "types": "./dist/translation.d.ts",
44
44
  "default": "./dist/translation.js"
45
45
  }
46
- },
46
+ },
47
+ "./realtime": {
48
+ "types": "./dist/realtime.d.ts",
49
+ "default": "./dist/realtime.js"
50
+ },
47
51
  "scripts": {
48
52
  "check": "tsc --noEmit",
49
53
  "prebuild": "rm -rf dist",
50
- "build": "bun build src/index.ts src/email.ts src/chain.ts src/database.ts src/http.ts src/pool.ts src/storage.ts src/binance.ts src/translation.ts --root src --outdir dist --target browser --format esm --packages external && tsc --emitDeclarationOnly --declaration --noEmit false --outDir dist",
54
+ "build": "bun build src/index.ts src/email.ts src/chain.ts src/database.ts src/http.ts src/pool.ts src/storage.ts src/binance.ts src/translation.ts src/realtime.ts --root src --outdir dist --target browser --format esm --packages external && tsc --emitDeclarationOnly --declaration --noEmit false --outDir dist",
51
55
  "prepublishOnly": "bun run check && bun run build"
52
56
  },
53
57
  "devDependencies": {