@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 +1 -1
- package/dist/chain.js +1 -1
- package/dist/database.js +1 -1
- package/dist/email.d.ts +12 -1
- package/dist/email.js +106 -3
- package/dist/http.js +1 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +1 -1
- package/dist/pool.js +1 -1
- package/dist/realtime.d.ts +19 -0
- package/dist/realtime.js +66 -0
- package/dist/storage.js +1 -1
- package/dist/translation.js +1 -1
- package/package.json +7 -3
package/dist/binance.js
CHANGED
package/dist/chain.js
CHANGED
package/dist/database.js
CHANGED
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.
|
|
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
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
|
|
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
package/dist/pool.js
CHANGED
|
@@ -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
|
+
};
|
package/dist/realtime.js
ADDED
|
@@ -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
package/dist/translation.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgezero/providers",
|
|
3
|
-
|
|
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": {
|