@fonderie/courier 1.0.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/index.js ADDED
@@ -0,0 +1,579 @@
1
+ // src/module.ts
2
+ import { NOTIFICATION_EVENT } from "@fonderie/events";
3
+
4
+ // src/log.ts
5
+ async function insertMessageLog(entry, store) {
6
+ const [row] = await store.query(
7
+ `INSERT INTO fonderie_message_log
8
+ (message_type, channel, recipient, locale, provider, provider_message_id)
9
+ VALUES ($1, $2, $3, $4, $5, $6)
10
+ RETURNING id`,
11
+ [
12
+ entry.messageType,
13
+ entry.channel,
14
+ entry.recipient,
15
+ entry.locale ?? null,
16
+ entry.provider ?? null,
17
+ entry.providerMessageId ?? null
18
+ ]
19
+ );
20
+ return row?.id ?? "";
21
+ }
22
+ async function markMessageSent(id, store) {
23
+ await store.query(
24
+ `UPDATE fonderie_message_log
25
+ SET status = 'sent', sent_at = now(), attempts = attempts + 1
26
+ WHERE id = $1`,
27
+ [id]
28
+ );
29
+ }
30
+ async function markMessageFailed(id, error, store) {
31
+ await store.query(
32
+ `UPDATE fonderie_message_log
33
+ SET status = 'failed', error = $2, attempts = attempts + 1
34
+ WHERE id = $1`,
35
+ [id, error]
36
+ );
37
+ }
38
+ async function markMessageDelivered(providerMessageId, store) {
39
+ await store.query(
40
+ `UPDATE fonderie_message_log
41
+ SET status = 'delivered'
42
+ WHERE provider_message_id = $1`,
43
+ [providerMessageId]
44
+ );
45
+ }
46
+ async function markMessageOpened(providerMessageId, store) {
47
+ await store.query(
48
+ `UPDATE fonderie_message_log
49
+ SET status = 'opened', opened_at = now()
50
+ WHERE provider_message_id = $1 AND opened_at IS NULL`,
51
+ [providerMessageId]
52
+ );
53
+ }
54
+ async function markMessageClicked(providerMessageId, store) {
55
+ await store.query(
56
+ `UPDATE fonderie_message_log
57
+ SET status = 'clicked', clicked_at = now()
58
+ WHERE provider_message_id = $1 AND clicked_at IS NULL`,
59
+ [providerMessageId]
60
+ );
61
+ }
62
+ async function markMessageBounced(providerMessageId, reason, store) {
63
+ await store.query(
64
+ `UPDATE fonderie_message_log
65
+ SET status = 'bounced', bounced_at = now(), bounce_reason = $2
66
+ WHERE provider_message_id = $1`,
67
+ [providerMessageId, reason]
68
+ );
69
+ }
70
+
71
+ // src/dispatcher.ts
72
+ function resolveRecipient(message, channel) {
73
+ if (channel === "email") return message.recipient.email ?? "";
74
+ if (channel === "sms") return message.recipient.phone ?? "";
75
+ if (channel === "push") return message.recipient.deviceToken ?? "";
76
+ return message.recipient.email ?? message.recipient.phone ?? "";
77
+ }
78
+ var Dispatcher = class {
79
+ constructor(config, resolver, store) {
80
+ this.config = config;
81
+ this.resolver = resolver;
82
+ this.store = store;
83
+ }
84
+ config;
85
+ resolver;
86
+ store;
87
+ channels = /* @__PURE__ */ new Map();
88
+ registerChannel(channel) {
89
+ this.channels.set(channel.name, channel);
90
+ return this;
91
+ }
92
+ async dispatch(message) {
93
+ const channelNames = this.config.channels[message.type];
94
+ if (!channelNames || channelNames.length === 0) {
95
+ console.warn(`[courier] no channels configured for message type: ${message.type}`);
96
+ return;
97
+ }
98
+ const template = await this.resolver.resolve(message.type, message.data, message.locale);
99
+ await Promise.allSettled(
100
+ channelNames.map(async (name) => {
101
+ const channel = this.channels.get(name);
102
+ if (!channel) {
103
+ console.warn(`[courier] channel "${name}" not registered`);
104
+ return;
105
+ }
106
+ const logEntry = {
107
+ messageType: message.type,
108
+ channel: name,
109
+ recipient: resolveRecipient(message, name)
110
+ };
111
+ if (message.locale) logEntry.locale = message.locale;
112
+ const logId = this.store ? await insertMessageLog(logEntry, this.store).catch(() => "") : "";
113
+ try {
114
+ await channel.send(message, template);
115
+ if (this.store && logId) {
116
+ markMessageSent(logId, this.store).catch(() => void 0);
117
+ }
118
+ } catch (err) {
119
+ const errMsg = err instanceof Error ? err.message : String(err);
120
+ console.error(`[courier:${name}] failed to send ${message.type}:`, err);
121
+ if (this.store && logId) {
122
+ markMessageFailed(logId, errMsg, this.store).catch(() => void 0);
123
+ }
124
+ }
125
+ })
126
+ );
127
+ }
128
+ };
129
+
130
+ // src/channels/sms.ts
131
+ var SmsChannel = class {
132
+ constructor(config) {
133
+ this.config = config;
134
+ }
135
+ config;
136
+ name = "sms";
137
+ async send(message, template) {
138
+ const to = message.recipient.phone;
139
+ if (!to) {
140
+ console.warn("[courier:sms] no phone number for recipient \u2014 skipping");
141
+ return;
142
+ }
143
+ if (this.config.provider === "twilio") {
144
+ await this.sendViaTwilio(to, template.text);
145
+ } else if (this.config.provider === "vonage") {
146
+ await this.sendViaVonage(to, template.text);
147
+ } else {
148
+ console.warn(`[courier:sms] provider ${this.config.provider} not implemented`);
149
+ }
150
+ }
151
+ async sendViaTwilio(to, text) {
152
+ const { accountSid, authToken } = this.config;
153
+ if (!accountSid || !authToken) {
154
+ throw new Error("Twilio accountSid and authToken required");
155
+ }
156
+ const res = await fetch(
157
+ `https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`,
158
+ {
159
+ method: "POST",
160
+ headers: {
161
+ Authorization: `Basic ${Buffer.from(`${accountSid}:${authToken}`).toString("base64")}`,
162
+ "Content-Type": "application/x-www-form-urlencoded"
163
+ },
164
+ body: new URLSearchParams({ From: this.config.from, To: to, Body: text })
165
+ }
166
+ );
167
+ if (!res.ok) {
168
+ const body = await res.text();
169
+ throw new Error(`[courier:sms] Twilio error ${res.status}: ${body}`);
170
+ }
171
+ }
172
+ async sendViaVonage(to, text) {
173
+ const { apiKey, apiSecret } = this.config;
174
+ if (!apiKey || !apiSecret) {
175
+ throw new Error("Vonage apiKey and apiSecret required");
176
+ }
177
+ const res = await fetch("https://rest.nexmo.com/sms/json", {
178
+ method: "POST",
179
+ headers: { "Content-Type": "application/json" },
180
+ body: JSON.stringify({
181
+ api_key: apiKey,
182
+ api_secret: apiSecret,
183
+ from: this.config.from,
184
+ to,
185
+ text
186
+ })
187
+ });
188
+ if (!res.ok) {
189
+ const body = await res.text();
190
+ throw new Error(`[courier:sms] Vonage error ${res.status}: ${body}`);
191
+ }
192
+ }
193
+ };
194
+
195
+ // src/channels/push.ts
196
+ var PushChannel = class {
197
+ constructor(config) {
198
+ this.config = config;
199
+ }
200
+ config;
201
+ name = "push";
202
+ async send(message, template) {
203
+ const token = message.recipient.deviceToken;
204
+ if (!token) {
205
+ console.warn("[courier:push] no device token for recipient \u2014 skipping");
206
+ return;
207
+ }
208
+ if (this.config.provider === "fcm") {
209
+ await this.sendViaFCM(token, template);
210
+ }
211
+ }
212
+ async sendViaFCM(token, template) {
213
+ const apiKey = this.config.serviceAccount["apiKey"];
214
+ if (!apiKey) {
215
+ throw new Error("[courier:push] FCM apiKey is required in serviceAccount");
216
+ }
217
+ const res = await fetch("https://fcm.googleapis.com/fcm/send", {
218
+ method: "POST",
219
+ headers: {
220
+ Authorization: `key=${apiKey}`,
221
+ "Content-Type": "application/json"
222
+ },
223
+ body: JSON.stringify({
224
+ to: token,
225
+ notification: {
226
+ title: template.subject,
227
+ body: template.text
228
+ }
229
+ })
230
+ });
231
+ if (!res.ok) {
232
+ const body = await res.text();
233
+ throw new Error(`[courier:push] FCM error ${res.status}: ${body}`);
234
+ }
235
+ }
236
+ };
237
+
238
+ // src/channels/email.ts
239
+ import nodemailer from "nodemailer";
240
+ var EmailChannel = class {
241
+ constructor(config) {
242
+ this.config = config;
243
+ if (config.provider === "smtp" && config.smtp) {
244
+ this.transport = nodemailer.createTransport({
245
+ host: config.smtp.host,
246
+ port: config.smtp.port,
247
+ secure: config.smtp.secure,
248
+ auth: { user: config.smtp.user, pass: config.smtp.pass }
249
+ });
250
+ }
251
+ }
252
+ config;
253
+ name = "email";
254
+ transport = null;
255
+ async send(message, template) {
256
+ const to = message.recipient.email;
257
+ if (!to) {
258
+ console.warn("[courier:email] no email address for recipient \u2014 skipping");
259
+ return;
260
+ }
261
+ if (this.config.provider === "resend") {
262
+ await this.sendViaResend(to, template);
263
+ } else if (this.config.provider === "smtp") {
264
+ await this.sendViaSMTP(to, template);
265
+ } else {
266
+ console.warn(`[courier:email] provider ${this.config.provider} not implemented`);
267
+ }
268
+ }
269
+ async sendViaResend(to, template) {
270
+ if (!this.config.apiKey) {
271
+ throw new Error("Resend apiKey is required");
272
+ }
273
+ const res = await fetch("https://api.resend.com/emails", {
274
+ method: "POST",
275
+ headers: {
276
+ Authorization: `Bearer ${this.config.apiKey}`,
277
+ "Content-Type": "application/json"
278
+ },
279
+ body: JSON.stringify({
280
+ from: this.config.from,
281
+ to,
282
+ subject: template.subject ?? "(no subject)",
283
+ html: template.html,
284
+ text: template.text
285
+ })
286
+ });
287
+ if (!res.ok) {
288
+ const body = await res.text();
289
+ throw new Error(`[courier:email] Resend error ${res.status}: ${body}`);
290
+ }
291
+ }
292
+ async sendViaSMTP(to, template) {
293
+ if (!this.transport) {
294
+ throw new Error("SMTP transport not initialised \u2014 check smtp config");
295
+ }
296
+ await this.transport.sendMail({
297
+ from: this.config.from,
298
+ to,
299
+ subject: template.subject ?? "(no subject)",
300
+ html: template.html,
301
+ text: template.text
302
+ });
303
+ }
304
+ };
305
+
306
+ // src/templates/resolver.ts
307
+ function render(template, data) {
308
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
309
+ const value = data[key];
310
+ return value !== void 0 && value !== null ? String(value) : "";
311
+ });
312
+ }
313
+ var DBTemplateResolver = class {
314
+ constructor(store) {
315
+ this.store = store;
316
+ }
317
+ store;
318
+ async resolve(type, data, locale) {
319
+ const [row] = await this.store.query(
320
+ `SELECT subject, html, text
321
+ FROM fonderie_courier_templates
322
+ WHERE type = $1 AND active = true
323
+ ORDER BY (locale = $2)::int DESC, (locale IS NULL)::int DESC
324
+ LIMIT 1`,
325
+ [type, locale ?? null]
326
+ );
327
+ if (!row) {
328
+ return { text: `${type}: ${JSON.stringify(data)}` };
329
+ }
330
+ return {
331
+ text: render(row.text, data),
332
+ ...row.subject ? { subject: render(row.subject, data) } : {},
333
+ ...row.html ? { html: render(row.html, data) } : {}
334
+ };
335
+ }
336
+ };
337
+ var FSTemplateResolver = class {
338
+ constructor(directory) {
339
+ this.directory = directory;
340
+ }
341
+ directory;
342
+ async resolve(type, data, locale) {
343
+ const { readFile } = await import("fs/promises");
344
+ const { join } = await import("path");
345
+ const readOptional = async (path) => {
346
+ try {
347
+ return await readFile(path, "utf8");
348
+ } catch {
349
+ return null;
350
+ }
351
+ };
352
+ const localePrefix = locale ? `${type}.${locale}` : null;
353
+ const [text, html, subject] = await Promise.all([
354
+ localePrefix ? readOptional(join(this.directory, `${localePrefix}.txt`)).then(
355
+ (v) => v ?? readOptional(join(this.directory, `${type}.txt`))
356
+ ) : readOptional(join(this.directory, `${type}.txt`)),
357
+ localePrefix ? readOptional(join(this.directory, `${localePrefix}.html`)).then(
358
+ (v) => v ?? readOptional(join(this.directory, `${type}.html`))
359
+ ) : readOptional(join(this.directory, `${type}.html`)),
360
+ localePrefix ? readOptional(join(this.directory, `${localePrefix}.subject.txt`)).then(
361
+ (v) => v ?? readOptional(join(this.directory, `${type}.subject.txt`))
362
+ ) : readOptional(join(this.directory, `${type}.subject.txt`))
363
+ ]);
364
+ return {
365
+ text: text ? render(text, data) : `${type}: ${JSON.stringify(data)}`,
366
+ ...subject ? { subject: render(subject, data) } : {},
367
+ ...html ? { html: render(html, data) } : {}
368
+ };
369
+ }
370
+ };
371
+
372
+ // src/delivery.ts
373
+ import { createHmac, timingSafeEqual } from "crypto";
374
+ async function handleSendGridDelivery(req, store, webhookSecret) {
375
+ if (webhookSecret) {
376
+ const sig = req.headers.get("x-twilio-email-event-webhook-signature") ?? "";
377
+ const ts = req.headers.get("x-twilio-email-event-webhook-timestamp") ?? "";
378
+ const body = await req.text();
379
+ if (!verifySendGridSignature(webhookSecret, ts, body, sig)) {
380
+ return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
381
+ }
382
+ const events2 = parseJson(body);
383
+ if (!Array.isArray(events2)) return Response.json({ ok: true });
384
+ await processSendGridEvents(events2, store);
385
+ return Response.json({ ok: true });
386
+ }
387
+ const events = await req.json();
388
+ if (!Array.isArray(events)) return Response.json({ ok: true });
389
+ await processSendGridEvents(events, store);
390
+ return Response.json({ ok: true });
391
+ }
392
+ function verifySendGridSignature(secret, timestamp, body, signature) {
393
+ try {
394
+ const payload = timestamp + body;
395
+ const expected = createHmac("sha256", secret).update(payload).digest("base64");
396
+ const sigBuf = Buffer.from(signature, "base64");
397
+ const expBuf = Buffer.from(expected, "base64");
398
+ if (sigBuf.length !== expBuf.length) return false;
399
+ return timingSafeEqual(sigBuf, expBuf);
400
+ } catch {
401
+ return false;
402
+ }
403
+ }
404
+ async function processSendGridEvents(events, store) {
405
+ for (const ev of events) {
406
+ const msgId = ev["sg_message_id"];
407
+ if (!msgId || typeof msgId !== "string") continue;
408
+ const id = msgId.split(".")[0] ?? msgId;
409
+ switch (ev.event) {
410
+ case "delivered":
411
+ await markMessageDelivered(id, store);
412
+ break;
413
+ case "open":
414
+ await markMessageOpened(id, store);
415
+ break;
416
+ case "click":
417
+ await markMessageClicked(id, store);
418
+ break;
419
+ case "bounce":
420
+ case "blocked":
421
+ case "dropped":
422
+ await markMessageBounced(id, typeof ev["reason"] === "string" ? ev["reason"] : ev.event, store);
423
+ break;
424
+ }
425
+ }
426
+ }
427
+ async function handleMailgunDelivery(req, store, signingKey) {
428
+ const body = await req.json();
429
+ if (signingKey) {
430
+ const { signature } = body;
431
+ if (!signature || !verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {
432
+ return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
433
+ }
434
+ }
435
+ const event = body["event-data"];
436
+ if (event) {
437
+ await processMailgunEvent(event, store);
438
+ }
439
+ return Response.json({ ok: true });
440
+ }
441
+ function verifyMailgunSignature(signingKey, timestamp, token, signature) {
442
+ try {
443
+ const value = timestamp + token;
444
+ const expected = createHmac("sha256", signingKey).update(value).digest("hex");
445
+ const expBuf = Buffer.from(expected, "hex");
446
+ const sigBuf = Buffer.from(signature, "hex");
447
+ if (expBuf.length !== sigBuf.length) return false;
448
+ return timingSafeEqual(expBuf, sigBuf);
449
+ } catch {
450
+ return false;
451
+ }
452
+ }
453
+ async function processMailgunEvent(event, store) {
454
+ const msgId = event.message?.headers?.["message-id"];
455
+ if (!msgId) return;
456
+ switch (event.event) {
457
+ case "delivered":
458
+ await markMessageDelivered(msgId, store);
459
+ break;
460
+ case "opened":
461
+ await markMessageOpened(msgId, store);
462
+ break;
463
+ case "clicked":
464
+ await markMessageClicked(msgId, store);
465
+ break;
466
+ case "failed":
467
+ case "bounced": {
468
+ const reason = event["delivery-status"]?.message ?? event.event;
469
+ await markMessageBounced(msgId, reason, store);
470
+ break;
471
+ }
472
+ }
473
+ }
474
+ async function handleMailtrapDelivery(req, store) {
475
+ const events = await req.json();
476
+ if (!Array.isArray(events)) return Response.json({ ok: true });
477
+ for (const ev of events) {
478
+ const msgId = ev.message_id;
479
+ if (!msgId) continue;
480
+ switch (ev.event) {
481
+ case "delivery":
482
+ await markMessageDelivered(msgId, store);
483
+ break;
484
+ case "open":
485
+ await markMessageOpened(msgId, store);
486
+ break;
487
+ case "click":
488
+ await markMessageClicked(msgId, store);
489
+ break;
490
+ case "bounce":
491
+ case "soft_bounce":
492
+ await markMessageBounced(msgId, ev.event, store);
493
+ break;
494
+ }
495
+ }
496
+ return Response.json({ ok: true });
497
+ }
498
+ function parseJson(text) {
499
+ try {
500
+ return JSON.parse(text);
501
+ } catch {
502
+ return null;
503
+ }
504
+ }
505
+
506
+ // src/module.ts
507
+ var CourierModule = class {
508
+ constructor(config, store, bus) {
509
+ this.config = config;
510
+ this.store = store;
511
+ const templateSource = config.templates?.source ?? "db";
512
+ const resolver = createTemplateResolver(templateSource, config, store);
513
+ this.dispatcher = new Dispatcher(config, resolver, store);
514
+ if (config.email) this.dispatcher.registerChannel(new EmailChannel(config.email));
515
+ if (config.sms) this.dispatcher.registerChannel(new SmsChannel(config.sms));
516
+ if (config.push) this.dispatcher.registerChannel(new PushChannel(config.push));
517
+ bus?.on(
518
+ NOTIFICATION_EVENT,
519
+ async (msg) => {
520
+ await this.dispatcher.dispatch(msg);
521
+ },
522
+ "courier"
523
+ );
524
+ }
525
+ config;
526
+ store;
527
+ name = "@fonderie/courier";
528
+ deps = ["@fonderie/events"];
529
+ dispatcher;
530
+ install(app) {
531
+ const store = this.store;
532
+ const signingKeys = this.config.delivery?.signingKeys;
533
+ app.addRoute(
534
+ "POST",
535
+ "/courier/delivery/sendgrid",
536
+ (ctx) => handleSendGridDelivery(ctx.request, store, signingKeys?.sendgrid)
537
+ );
538
+ app.addRoute(
539
+ "POST",
540
+ "/courier/delivery/mailgun",
541
+ (ctx) => handleMailgunDelivery(ctx.request, store, signingKeys?.mailgun)
542
+ );
543
+ app.addRoute(
544
+ "POST",
545
+ "/courier/delivery/mailtrap",
546
+ (ctx) => handleMailtrapDelivery(ctx.request, store)
547
+ );
548
+ }
549
+ };
550
+ function createTemplateResolver(source, config, store) {
551
+ if (source === "fs") {
552
+ return new FSTemplateResolver(config.templates?.directory ?? "./templates");
553
+ }
554
+ if (!store) {
555
+ throw new Error("[courier] store is required for DB template resolution");
556
+ }
557
+ return new DBTemplateResolver(store);
558
+ }
559
+
560
+ // src/config.ts
561
+ var Channel = {
562
+ EMAIL: "email",
563
+ SMS: "sms",
564
+ PUSH: "push"
565
+ };
566
+ export {
567
+ Channel,
568
+ CourierModule,
569
+ DBTemplateResolver,
570
+ Dispatcher,
571
+ EmailChannel,
572
+ FSTemplateResolver,
573
+ PushChannel,
574
+ SmsChannel,
575
+ handleMailgunDelivery,
576
+ handleMailtrapDelivery,
577
+ handleSendGridDelivery
578
+ };
579
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/module.ts","../src/log.ts","../src/dispatcher.ts","../src/channels/sms.ts","../src/channels/push.ts","../src/channels/email.ts","../src/templates/resolver.ts","../src/delivery.ts","../src/config.ts"],"sourcesContent":["import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport type { EventBus } from '@fonderie/events';\nimport { NOTIFICATION_EVENT } from '@fonderie/events';\n\nimport type { ITemplateResolver, ICourierMessage } from './types';\nimport type { ICourierConfig } from './config';\n\nimport { Dispatcher } from './dispatcher';\nimport { SmsChannel } from './channels/sms';\nimport { PushChannel } from './channels/push';\nimport { EmailChannel } from './channels/email';\nimport { DBTemplateResolver, FSTemplateResolver } from './templates/resolver';\nimport { handleSendGridDelivery, handleMailgunDelivery, handleMailtrapDelivery } from './delivery';\n\nexport class CourierModule implements IFonderieModule {\n\treadonly name = '@fonderie/courier';\n\treadonly deps = ['@fonderie/events'];\n\treadonly dispatcher: Dispatcher;\n\n\tconstructor(\n\t\tprivate config: ICourierConfig,\n\t\tprivate store?: IStoreAdapter,\n\t\tbus?: EventBus,\n\t) {\n\t\tconst templateSource = config.templates?.source ?? 'db';\n\t\tconst resolver = createTemplateResolver(templateSource, config, store);\n\n\t\tthis.dispatcher = new Dispatcher(config, resolver, store);\n\n\t\tif (config.email) this.dispatcher.registerChannel(new EmailChannel(config.email));\n\t\tif (config.sms) this.dispatcher.registerChannel(new SmsChannel(config.sms));\n\t\tif (config.push) this.dispatcher.registerChannel(new PushChannel(config.push));\n\n\t\tbus?.on<ICourierMessage>(\n\t\t\tNOTIFICATION_EVENT,\n\t\t\tasync (msg) => {\n\t\t\t\tawait this.dispatcher.dispatch(msg);\n\t\t\t},\n\t\t\t'courier',\n\t\t);\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\tconst store = this.store;\n\t\tconst signingKeys = this.config.delivery?.signingKeys;\n\n\t\tapp.addRoute('POST', '/courier/delivery/sendgrid', (ctx) =>\n\t\t\thandleSendGridDelivery(ctx.request, store!, signingKeys?.sendgrid),\n\t\t);\n\t\tapp.addRoute('POST', '/courier/delivery/mailgun', (ctx) =>\n\t\t\thandleMailgunDelivery(ctx.request, store!, signingKeys?.mailgun),\n\t\t);\n\t\tapp.addRoute('POST', '/courier/delivery/mailtrap', (ctx) =>\n\t\t\thandleMailtrapDelivery(ctx.request, store!),\n\t\t);\n\t}\n}\n\nfunction createTemplateResolver(\n\tsource: 'db' | 'fs',\n\tconfig: ICourierConfig,\n\tstore?: IStoreAdapter,\n): ITemplateResolver {\n\tif (source === 'fs') {\n\t\treturn new FSTemplateResolver(config.templates?.directory ?? './templates');\n\t}\n\n\tif (!store) {\n\t\tthrow new Error('[courier] store is required for DB template resolution');\n\t}\n\n\treturn new DBTemplateResolver(store);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nexport type MessageLogStatus =\n\t| 'pending' | 'sent' | 'failed'\n\t| 'delivered' | 'opened' | 'clicked' | 'bounced' | 'spam';\n\nexport interface IMessageLog {\n\tid: string;\n\tmessageType: string;\n\tchannel: string;\n\trecipient: string;\n\tlocale: string | null;\n\tstatus: MessageLogStatus;\n\terror: string | null;\n\tattempts: number;\n\tprovider: string | null;\n\tproviderMessageId: string | null;\n\topenedAt: string | null;\n\tclickedAt: string | null;\n\tbouncedAt: string | null;\n\tbounceReason: string | null;\n\tcreatedAt: string;\n\tsentAt: string | null;\n}\n\nexport async function insertMessageLog(\n\tentry: {\n\t\tmessageType: string;\n\t\tchannel: string;\n\t\trecipient: string;\n\t\tlocale?: string;\n\t\tprovider?: string;\n\t\tproviderMessageId?: string;\n\t},\n\tstore: IStoreAdapter,\n): Promise<string> {\n\tconst [row] = await store.query<{ id: string }>(\n\t\t`INSERT INTO fonderie_message_log\n\t\t\t(message_type, channel, recipient, locale, provider, provider_message_id)\n\t\t VALUES ($1, $2, $3, $4, $5, $6)\n\t\t RETURNING id`,\n\t\t[\n\t\t\tentry.messageType,\n\t\t\tentry.channel,\n\t\t\tentry.recipient,\n\t\t\tentry.locale ?? null,\n\t\t\tentry.provider ?? null,\n\t\t\tentry.providerMessageId ?? null,\n\t\t],\n\t);\n\treturn row?.id ?? '';\n}\n\nexport async function markMessageSent(id: string, store: IStoreAdapter): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'sent', sent_at = now(), attempts = attempts + 1\n\t\t WHERE id = $1`,\n\t\t[id],\n\t);\n}\n\nexport async function markMessageFailed(\n\tid: string,\n\terror: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'failed', error = $2, attempts = attempts + 1\n\t\t WHERE id = $1`,\n\t\t[id, error],\n\t);\n}\n\nexport async function markMessageDelivered(\n\tproviderMessageId: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'delivered'\n\t\t WHERE provider_message_id = $1`,\n\t\t[providerMessageId],\n\t);\n}\n\nexport async function markMessageOpened(\n\tproviderMessageId: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'opened', opened_at = now()\n\t\t WHERE provider_message_id = $1 AND opened_at IS NULL`,\n\t\t[providerMessageId],\n\t);\n}\n\nexport async function markMessageClicked(\n\tproviderMessageId: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'clicked', clicked_at = now()\n\t\t WHERE provider_message_id = $1 AND clicked_at IS NULL`,\n\t\t[providerMessageId],\n\t);\n}\n\nexport async function markMessageBounced(\n\tproviderMessageId: string,\n\treason: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'bounced', bounced_at = now(), bounce_reason = $2\n\t\t WHERE provider_message_id = $1`,\n\t\t[providerMessageId, reason],\n\t);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ICourierConfig } from './config';\nimport type { ICourierMessage, ICourierChannel, ITemplateResolver } from './types';\nimport { insertMessageLog, markMessageSent, markMessageFailed } from './log';\n\nfunction resolveRecipient(message: ICourierMessage, channel: string): string {\n\tif (channel === 'email') return message.recipient.email ?? '';\n\tif (channel === 'sms') return message.recipient.phone ?? '';\n\tif (channel === 'push') return message.recipient.deviceToken ?? '';\n\treturn message.recipient.email ?? message.recipient.phone ?? '';\n}\n\nexport class Dispatcher {\n\tprivate channels: Map<string, ICourierChannel> = new Map();\n\n\tconstructor(\n\t\tprivate config: ICourierConfig,\n\t\tprivate resolver: ITemplateResolver,\n\t\tprivate store?: IStoreAdapter,\n\t) {}\n\n\tregisterChannel(channel: ICourierChannel): this {\n\t\tthis.channels.set(channel.name, channel);\n\t\treturn this;\n\t}\n\n\tasync dispatch(message: ICourierMessage): Promise<void> {\n\t\tconst channelNames = this.config.channels[message.type];\n\n\t\tif (!channelNames || channelNames.length === 0) {\n\t\t\tconsole.warn(`[courier] no channels configured for message type: ${message.type}`);\n\t\t\treturn;\n\t\t}\n\n\t\tconst template = await this.resolver.resolve(message.type, message.data, message.locale);\n\n\t\tawait Promise.allSettled(\n\t\t\tchannelNames.map(async (name) => {\n\t\t\t\tconst channel = this.channels.get(name);\n\t\t\t\tif (!channel) {\n\t\t\t\t\tconsole.warn(`[courier] channel \"${name}\" not registered`);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Insert log entry (fire-and-forget if store unavailable)\n\t\t\t\tconst logEntry: Parameters<typeof insertMessageLog>[0] = {\n\t\t\t\t\tmessageType: message.type,\n\t\t\t\t\tchannel: name,\n\t\t\t\t\trecipient: resolveRecipient(message, name),\n\t\t\t\t};\n\t\t\t\tif (message.locale) logEntry.locale = message.locale;\n\n\t\t\t\tconst logId = this.store\n\t\t\t\t\t? await insertMessageLog(logEntry, this.store).catch(() => '')\n\t\t\t\t\t: '';\n\n\t\t\t\ttry {\n\t\t\t\t\tawait channel.send(message, template);\n\n\t\t\t\t\tif (this.store && logId) {\n\t\t\t\t\t\tmarkMessageSent(logId, this.store).catch(() => undefined);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tconsole.error(`[courier:${name}] failed to send ${message.type}:`, err);\n\n\t\t\t\t\tif (this.store && logId) {\n\t\t\t\t\t\tmarkMessageFailed(logId, errMsg, this.store).catch(() => undefined);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\t}\n}\n","import type { ISmsChannelConfig } from '../config';\nimport type { ICourierChannel, ICourierMessage, IRenderedTemplate } from '../types';\n\nexport class SmsChannel implements ICourierChannel {\n\treadonly name = 'sms';\n\n\tconstructor(private config: ISmsChannelConfig) {}\n\n\tasync send(message: ICourierMessage, template: IRenderedTemplate): Promise<void> {\n\t\tconst to = message.recipient.phone;\n\t\tif (!to) {\n\t\t\tconsole.warn('[courier:sms] no phone number for recipient — skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.config.provider === 'twilio') {\n\t\t\tawait this.sendViaTwilio(to, template.text);\n\t\t} else if (this.config.provider === 'vonage') {\n\t\t\tawait this.sendViaVonage(to, template.text);\n\t\t} else {\n\t\t\tconsole.warn(`[courier:sms] provider ${this.config.provider} not implemented`);\n\t\t}\n\t}\n\n\tprivate async sendViaTwilio(to: string, text: string): Promise<void> {\n\t\tconst { accountSid, authToken } = this.config;\n\t\tif (!accountSid || !authToken) {\n\t\t\tthrow new Error('Twilio accountSid and authToken required');\n\t\t}\n\n\t\tconst res = await fetch(\n\t\t\t`https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`,\n\t\t\t{\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Basic ${Buffer.from(`${accountSid}:${authToken}`).toString('base64')}`,\n\t\t\t\t\t'Content-Type': 'application/x-www-form-urlencoded',\n\t\t\t\t},\n\t\t\t\tbody: new URLSearchParams({ From: this.config.from, To: to, Body: text }),\n\t\t\t},\n\t\t);\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:sms] Twilio error ${res.status}: ${body}`);\n\t\t}\n\t}\n\n\tprivate async sendViaVonage(to: string, text: string): Promise<void> {\n\t\tconst { apiKey, apiSecret } = this.config;\n\t\tif (!apiKey || !apiSecret) {\n\t\t\tthrow new Error('Vonage apiKey and apiSecret required');\n\t\t}\n\n\t\tconst res = await fetch('https://rest.nexmo.com/sms/json', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({\n\t\t\t\tapi_key: apiKey,\n\t\t\t\tapi_secret: apiSecret,\n\t\t\t\tfrom: this.config.from,\n\t\t\t\tto,\n\t\t\t\ttext,\n\t\t\t}),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:sms] Vonage error ${res.status}: ${body}`);\n\t\t}\n\t}\n}\n","import type { IPushChannelConfig } from '../config';\nimport type { ICourierChannel, ICourierMessage, IRenderedTemplate } from '../types';\n\nexport class PushChannel implements ICourierChannel {\n\treadonly name = 'push';\n\n\tconstructor(private config: IPushChannelConfig) {}\n\n\tasync send(message: ICourierMessage, template: IRenderedTemplate): Promise<void> {\n\t\tconst token = message.recipient.deviceToken;\n\t\tif (!token) {\n\t\t\tconsole.warn('[courier:push] no device token for recipient — skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.config.provider === 'fcm') {\n\t\t\tawait this.sendViaFCM(token, template);\n\t\t}\n\t}\n\n\tprivate async sendViaFCM(token: string, template: IRenderedTemplate): Promise<void> {\n\t\tconst apiKey = this.config.serviceAccount['apiKey'] as string | undefined;\n\t\tif (!apiKey) {\n\t\t\tthrow new Error('[courier:push] FCM apiKey is required in serviceAccount');\n\t\t}\n\n\t\tconst res = await fetch('https://fcm.googleapis.com/fcm/send', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\tAuthorization: `key=${apiKey}`,\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tto: token,\n\t\t\t\tnotification: {\n\t\t\t\t\ttitle: template.subject,\n\t\t\t\t\tbody: template.text,\n\t\t\t\t},\n\t\t\t}),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:push] FCM error ${res.status}: ${body}`);\n\t\t}\n\t}\n}\n","import nodemailer from 'nodemailer';\n\nimport type { ICourierChannel, ICourierMessage, IRenderedTemplate } from '../types';\nimport type { IEmailChannelConfig } from '../config';\n\nexport class EmailChannel implements ICourierChannel {\n\treadonly name = 'email';\n\tprivate transport: ReturnType<typeof nodemailer.createTransport> | null = null;\n\n\tconstructor(private config: IEmailChannelConfig) {\n\t\tif (config.provider === 'smtp' && config.smtp) {\n\t\t\tthis.transport = nodemailer.createTransport({\n\t\t\t\thost: config.smtp.host,\n\t\t\t\tport: config.smtp.port,\n\t\t\t\tsecure: config.smtp.secure,\n\t\t\t\tauth: { user: config.smtp.user, pass: config.smtp.pass },\n\t\t\t});\n\t\t}\n\t}\n\n\tasync send(message: ICourierMessage, template: IRenderedTemplate): Promise<void> {\n\t\tconst to = message.recipient.email;\n\t\tif (!to) {\n\t\t\tconsole.warn('[courier:email] no email address for recipient — skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.config.provider === 'resend') {\n\t\t\tawait this.sendViaResend(to, template);\n\t\t} else if (this.config.provider === 'smtp') {\n\t\t\tawait this.sendViaSMTP(to, template);\n\t\t} else {\n\t\t\tconsole.warn(`[courier:email] provider ${this.config.provider} not implemented`);\n\t\t}\n\t}\n\n\tprivate async sendViaResend(to: string, template: IRenderedTemplate): Promise<void> {\n\t\tif (!this.config.apiKey) {\n\t\t\tthrow new Error('Resend apiKey is required');\n\t\t}\n\n\t\tconst res = await fetch('https://api.resend.com/emails', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${this.config.apiKey}`,\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tfrom: this.config.from,\n\t\t\t\tto,\n\t\t\t\tsubject: template.subject ?? '(no subject)',\n\t\t\t\thtml: template.html,\n\t\t\t\ttext: template.text,\n\t\t\t}),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:email] Resend error ${res.status}: ${body}`);\n\t\t}\n\t}\n\n\tprivate async sendViaSMTP(to: string, template: IRenderedTemplate): Promise<void> {\n\t\tif (!this.transport) {\n\t\t\tthrow new Error('SMTP transport not initialised — check smtp config');\n\t\t}\n\n\t\tawait this.transport.sendMail({\n\t\t\tfrom: this.config.from,\n\t\t\tto,\n\t\t\tsubject: template.subject ?? '(no subject)',\n\t\t\thtml: template.html,\n\t\t\ttext: template.text,\n\t\t});\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ITemplateResolver, IRenderedTemplate } from '../types';\n\nfunction render(template: string, data: Record<string, unknown>): string {\n\treturn template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) => {\n\t\tconst value = data[key];\n\t\treturn value !== undefined && value !== null ? String(value) : '';\n\t});\n}\n\n// DB-backed resolver — reads from fonderie_courier_templates with locale fallback\nexport class DBTemplateResolver implements ITemplateResolver {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync resolve(\n\t\ttype: string,\n\t\tdata: Record<string, unknown>,\n\t\tlocale?: string,\n\t): Promise<IRenderedTemplate> {\n\t\tconst [row] = await this.store.query<{\n\t\t\tsubject: string | null;\n\t\t\thtml: string | null;\n\t\t\ttext: string;\n\t\t}>(\n\t\t\t`SELECT subject, html, text\n\t\t\t FROM fonderie_courier_templates\n\t\t\t WHERE type = $1 AND active = true\n\t\t\t ORDER BY (locale = $2)::int DESC, (locale IS NULL)::int DESC\n\t\t\t LIMIT 1`,\n\t\t\t[type, locale ?? null],\n\t\t);\n\n\t\tif (!row) {\n\t\t\treturn { text: `${type}: ${JSON.stringify(data)}` };\n\t\t}\n\n\t\treturn {\n\t\t\ttext: render(row.text, data),\n\t\t\t...(row.subject ? { subject: render(row.subject, data) } : {}),\n\t\t\t...(row.html ? { html: render(row.html, data) } : {}),\n\t\t};\n\t}\n}\n\n// Filesystem resolver — reads {type}.{locale}.txt → {type}.txt with fallback\nexport class FSTemplateResolver implements ITemplateResolver {\n\tconstructor(private directory: string) {}\n\n\tasync resolve(\n\t\ttype: string,\n\t\tdata: Record<string, unknown>,\n\t\tlocale?: string,\n\t): Promise<IRenderedTemplate> {\n\t\tconst { readFile } = await import('node:fs/promises');\n\t\tconst { join } = await import('node:path');\n\n\t\tconst readOptional = async (path: string): Promise<string | null> => {\n\t\t\ttry {\n\t\t\t\treturn await readFile(path, 'utf8');\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t};\n\n\t\t// Per-locale variants take priority over generic variants\n\t\tconst localePrefix = locale ? `${type}.${locale}` : null;\n\n\t\tconst [text, html, subject] = await Promise.all([\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.txt`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.txt`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.txt`)),\n\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.html`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.html`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.html`)),\n\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.subject.txt`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.subject.txt`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.subject.txt`)),\n\t\t]);\n\n\t\treturn {\n\t\t\ttext: text ? render(text, data) : `${type}: ${JSON.stringify(data)}`,\n\t\t\t...(subject ? { subject: render(subject, data) } : {}),\n\t\t\t...(html ? { html: render(html, data) } : {}),\n\t\t};\n\t}\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto';\n\nimport type { IStoreAdapter } from '@fonderie/store';\nimport {\n\tmarkMessageDelivered,\n\tmarkMessageOpened,\n\tmarkMessageClicked,\n\tmarkMessageBounced,\n} from './log';\n\n// ── SendGrid ──────────────────────────────────────────────────────\n//\n// Verifies the X-Twilio-Email-Event-Webhook-Signature header.\n// Expects an array of event objects in the request body.\n// https://docs.sendgrid.com/for-developers/tracking-events/getting-started-event-webhook-security-features\n\nexport async function handleSendGridDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n\twebhookSecret?: string,\n): Promise<Response> {\n\tif (webhookSecret) {\n\t\tconst sig = req.headers.get('x-twilio-email-event-webhook-signature') ?? '';\n\t\tconst ts = req.headers.get('x-twilio-email-event-webhook-timestamp') ?? '';\n\t\tconst body = await req.text();\n\n\t\tif (!verifySendGridSignature(webhookSecret, ts, body, sig)) {\n\t\t\treturn Response.json({ error: 'INVALID_SIGNATURE' }, { status: 401 });\n\t\t}\n\n\t\tconst events = parseJson(body) as SendGridEvent[] | null;\n\t\tif (!Array.isArray(events)) return Response.json({ ok: true });\n\n\t\tawait processSendGridEvents(events, store);\n\t\treturn Response.json({ ok: true });\n\t}\n\n\tconst events = (await req.json()) as SendGridEvent[];\n\tif (!Array.isArray(events)) return Response.json({ ok: true });\n\n\tawait processSendGridEvents(events, store);\n\treturn Response.json({ ok: true });\n}\n\nfunction verifySendGridSignature(\n\tsecret: string,\n\ttimestamp: string,\n\tbody: string,\n\tsignature: string,\n): boolean {\n\ttry {\n\t\tconst payload = timestamp + body;\n\t\tconst expected = createHmac('sha256', secret).update(payload).digest('base64');\n\t\tconst sigBuf = Buffer.from(signature, 'base64');\n\t\tconst expBuf = Buffer.from(expected, 'base64');\n\t\tif (sigBuf.length !== expBuf.length) return false;\n\t\treturn timingSafeEqual(sigBuf, expBuf);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\ninterface SendGridEvent {\n\tevent: string;\n\tsg_message_id?: string;\n\treason?: string;\n\t[key: string]: unknown;\n}\n\nasync function processSendGridEvents(\n\tevents: SendGridEvent[],\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tfor (const ev of events) {\n\t\tconst msgId = ev['sg_message_id'];\n\t\tif (!msgId || typeof msgId !== 'string') continue;\n\t\t// sg_message_id is \"msgId.filterXxx\" — strip the filter suffix\n\t\tconst id = msgId.split('.')[0] ?? msgId;\n\n\t\tswitch (ev.event) {\n\t\t\tcase 'delivered': await markMessageDelivered(id, store); break;\n\t\t\tcase 'open': await markMessageOpened(id, store); break;\n\t\t\tcase 'click': await markMessageClicked(id, store); break;\n\t\t\tcase 'bounce':\n\t\t\tcase 'blocked':\n\t\t\tcase 'dropped':\n\t\t\t\tawait markMessageBounced(id, typeof ev['reason'] === 'string' ? ev['reason'] : ev.event, store);\n\t\t\t\tbreak;\n\t\t}\n\t}\n}\n\n// ── Mailgun ───────────────────────────────────────────────────────\n//\n// Verifies signature using token + timestamp + signing key (HMAC-SHA256).\n// https://documentation.mailgun.com/docs/mailgun/user-manual/tracking-messages/#securing-webhooks\n\nexport async function handleMailgunDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n\tsigningKey?: string,\n): Promise<Response> {\n\tconst body = (await req.json()) as MailgunPayload;\n\n\tif (signingKey) {\n\t\tconst { signature } = body;\n\t\tif (!signature || !verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {\n\t\t\treturn Response.json({ error: 'INVALID_SIGNATURE' }, { status: 401 });\n\t\t}\n\t}\n\n\tconst event = body['event-data'];\n\tif (event) {\n\t\tawait processMailgunEvent(event, store);\n\t}\n\n\treturn Response.json({ ok: true });\n}\n\nfunction verifyMailgunSignature(\n\tsigningKey: string,\n\ttimestamp: string,\n\ttoken: string,\n\tsignature: string,\n): boolean {\n\ttry {\n\t\tconst value = timestamp + token;\n\t\tconst expected = createHmac('sha256', signingKey).update(value).digest('hex');\n\t\tconst expBuf = Buffer.from(expected, 'hex');\n\t\tconst sigBuf = Buffer.from(signature, 'hex');\n\t\tif (expBuf.length !== sigBuf.length) return false;\n\t\treturn timingSafeEqual(expBuf, sigBuf);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\ninterface MailgunPayload {\n\tsignature?: { timestamp: string; token: string; signature: string };\n\t'event-data'?: MailgunEvent;\n\t[key: string]: unknown;\n}\n\ninterface MailgunEvent {\n\tevent: string;\n\tmessage?: { headers?: { 'message-id'?: string } };\n\t'delivery-status'?: { message?: string };\n\t[key: string]: unknown;\n}\n\nasync function processMailgunEvent(event: MailgunEvent, store: IStoreAdapter): Promise<void> {\n\tconst msgId = event.message?.headers?.['message-id'];\n\tif (!msgId) return;\n\n\tswitch (event.event) {\n\t\tcase 'delivered': await markMessageDelivered(msgId, store); break;\n\t\tcase 'opened': await markMessageOpened(msgId, store); break;\n\t\tcase 'clicked': await markMessageClicked(msgId, store); break;\n\t\tcase 'failed':\n\t\tcase 'bounced': {\n\t\t\tconst reason = event['delivery-status']?.message ?? event.event;\n\t\t\tawait markMessageBounced(msgId, reason, store);\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n// ── Mailtrap (testing only, no signature) ────────────────────────\n\nexport async function handleMailtrapDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n): Promise<Response> {\n\tconst events = (await req.json()) as MailtrapEvent[];\n\tif (!Array.isArray(events)) return Response.json({ ok: true });\n\n\tfor (const ev of events) {\n\t\tconst msgId = ev.message_id;\n\t\tif (!msgId) continue;\n\n\t\tswitch (ev.event) {\n\t\t\tcase 'delivery': await markMessageDelivered(msgId, store); break;\n\t\t\tcase 'open': await markMessageOpened(msgId, store); break;\n\t\t\tcase 'click': await markMessageClicked(msgId, store); break;\n\t\t\tcase 'bounce':\n\t\t\tcase 'soft_bounce':\n\t\t\t\tawait markMessageBounced(msgId, ev.event, store);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn Response.json({ ok: true });\n}\n\ninterface MailtrapEvent {\n\tevent: string;\n\tmessage_id?: string;\n\t[key: string]: unknown;\n}\n\n// ── Utility ───────────────────────────────────────────────────────\n\nfunction parseJson(text: string): unknown {\n\ttry { return JSON.parse(text); } catch { return null; }\n}\n","export interface IEmailChannelConfig {\n\tprovider: 'resend' | 'ses' | 'smtp';\n\tfrom: string;\n\tapiKey?: string;\n\tsmtp?: {\n\t\thost: string;\n\t\tport: number;\n\t\tsecure: boolean;\n\t\tuser: string;\n\t\tpass: string;\n\t};\n}\n\nexport interface ISmsChannelConfig {\n\tprovider: 'twilio' | 'vonage';\n\tfrom: string;\n\taccountSid?: string; // twilio\n\tauthToken?: string; // twilio\n\tapiKey?: string; // vonage\n\tapiSecret?: string; // vonage\n}\n\nexport interface IPushChannelConfig {\n\tprovider: 'fcm';\n\tserviceAccount: Record<string, unknown>;\n}\n\nexport const Channel = {\n\tEMAIL: 'email',\n\tSMS: 'sms',\n\tPUSH: 'push',\n} as const satisfies Record<string, 'email' | 'sms' | 'push'>;\n\nexport interface ICourierConfig {\n\t// Which channels handle which message types\n\t// e.g. { 'password-reset': ['email'], 'new-message': ['push', 'sms'] }\n\tchannels: Record<string, Array<'email' | 'sms' | 'push'>>;\n\n\tsms?: ISmsChannelConfig;\n\tpush?: IPushChannelConfig;\n\temail?: IEmailChannelConfig;\n\n\t// Where templates are loaded from\n\t// 'db' requires @fonderie/store to be configured\n\t// 'fs' reads from a local directory\n\ttemplates?: {\n\t\tsource: 'db' | 'fs';\n\t\tdirectory?: string; // for 'fs' source\n\t};\n\n\t// Signing keys for verifying inbound delivery webhook payloads\n\tdelivery?: {\n\t\tsigningKeys?: {\n\t\t\tsendgrid?: string; // HMAC-SHA256 key from SendGrid dashboard\n\t\t\tmailgun?: string; // Mailgun HTTP webhook signing key\n\t\t};\n\t};\n}\n"],"mappings":"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","names":["events"]}
@@ -0,0 +1,7 @@
1
+ // src/migrations/index.ts
2
+ import { createMigrationsPath } from "@fonderie/store";
3
+ var getMigrationsPath = () => createMigrationsPath(import.meta.url);
4
+ export {
5
+ getMigrationsPath
6
+ };
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/migrations/index.ts"],"sourcesContent":["import { createMigrationsPath } from '@fonderie/store';\n\nexport const getMigrationsPath = (): string => createMigrationsPath(import.meta.url);\n"],"mappings":";AAAA,SAAS,4BAA4B;AAE9B,IAAM,oBAAoB,MAAc,qBAAqB,YAAY,GAAG;","names":[]}