@fonderie/courier 5.2.1 → 6.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/brain/signatures.md +4 -3
- package/dist/index.cjs +76 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +77 -46
- package/dist/index.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +5 -2
- package/dist/types.d.ts +5 -2
- package/package.json +3 -3
package/brain/signatures.md
CHANGED
|
@@ -16,7 +16,7 @@ new CourierModule(config: ICourierConfig, store?: IStoreAdapter | undefined, bus
|
|
|
16
16
|
|
|
17
17
|
function validateCourierConfig(config: ICourierConfig, registeredChannels: Iterable<string>): void
|
|
18
18
|
|
|
19
|
-
function handleSendGridDelivery(req: Request, store: IStoreAdapter,
|
|
19
|
+
function handleSendGridDelivery(req: Request, store: IStoreAdapter, publicKey?: string | undefined): Promise<Response>
|
|
20
20
|
|
|
21
21
|
function handleMailgunDelivery(req: Request, store: IStoreAdapter, signingKey?: string | undefined): Promise<Response>
|
|
22
22
|
|
|
@@ -37,7 +37,7 @@ new PushChannel(config: IPushChannelConfig): PushChannel
|
|
|
37
37
|
|
|
38
38
|
new EmailChannel(config: IEmailChannelConfig): EmailChannel
|
|
39
39
|
.name: "email"
|
|
40
|
-
.send(message: ICourierMessage, template: IRenderedTemplate): Promise<void>
|
|
40
|
+
.send(message: ICourierMessage, template: IRenderedTemplate): Promise<void | ISendResult>
|
|
41
41
|
|
|
42
42
|
new DBTemplateResolver(store: IStoreAdapter, defaults?: DefaultTemplates | undefined): DBTemplateResolver
|
|
43
43
|
.resolve(type: string, data: Record<string, unknown>, locale?: string | undefined): Promise<IRenderedTemplate>
|
|
@@ -122,7 +122,7 @@ interface ICourierMessage {
|
|
|
122
122
|
|
|
123
123
|
interface ICourierChannel {
|
|
124
124
|
name: string;
|
|
125
|
-
send(message: ICourierMessage, template: IRenderedTemplate): Promise<void>;
|
|
125
|
+
send(message: ICourierMessage, template: IRenderedTemplate): Promise<ISendResult | void>;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
interface IRenderedTemplate {
|
|
@@ -161,6 +161,7 @@ interface ICourierConfig {
|
|
|
161
161
|
sendgrid?: string;
|
|
162
162
|
mailgun?: string;
|
|
163
163
|
};
|
|
164
|
+
allowUnverifiedMailtrap?: boolean;
|
|
164
165
|
};
|
|
165
166
|
}
|
|
166
167
|
|
package/dist/index.cjs
CHANGED
|
@@ -75,6 +75,14 @@ async function insertMessageLog(entry, store) {
|
|
|
75
75
|
);
|
|
76
76
|
return row?.id ?? "";
|
|
77
77
|
}
|
|
78
|
+
async function setMessageProviderId(id, providerMessageId, store) {
|
|
79
|
+
await store.query(
|
|
80
|
+
`UPDATE fonderie_message_log
|
|
81
|
+
SET provider_message_id = $2
|
|
82
|
+
WHERE id = $1`,
|
|
83
|
+
[id, providerMessageId]
|
|
84
|
+
);
|
|
85
|
+
}
|
|
78
86
|
async function markMessageSent(id, store) {
|
|
79
87
|
await store.query(
|
|
80
88
|
`UPDATE fonderie_message_log
|
|
@@ -172,8 +180,13 @@ var Dispatcher = class {
|
|
|
172
180
|
if (message.locale) logEntry.locale = message.locale;
|
|
173
181
|
const logId = this.store ? await insertMessageLog(logEntry, this.store).catch(() => "") : "";
|
|
174
182
|
try {
|
|
175
|
-
await channel.send(message, template);
|
|
183
|
+
const result = await channel.send(message, template);
|
|
176
184
|
if (this.store && logId) {
|
|
185
|
+
if (result?.providerMessageId) {
|
|
186
|
+
await setMessageProviderId(logId, result.providerMessageId, this.store).catch(
|
|
187
|
+
() => void 0
|
|
188
|
+
);
|
|
189
|
+
}
|
|
177
190
|
markMessageSent(logId, this.store).catch(() => void 0);
|
|
178
191
|
}
|
|
179
192
|
} catch (err) {
|
|
@@ -320,12 +333,12 @@ var EmailChannel = class {
|
|
|
320
333
|
return;
|
|
321
334
|
}
|
|
322
335
|
if (this.config.provider === "resend") {
|
|
323
|
-
|
|
324
|
-
} else if (this.config.provider === "smtp") {
|
|
325
|
-
await this.sendViaSMTP(to, template);
|
|
326
|
-
} else {
|
|
327
|
-
console.warn(`[courier:email] provider ${this.config.provider} not implemented`);
|
|
336
|
+
return this.sendViaResend(to, template);
|
|
328
337
|
}
|
|
338
|
+
if (this.config.provider === "smtp") {
|
|
339
|
+
return this.sendViaSMTP(to, template);
|
|
340
|
+
}
|
|
341
|
+
console.warn(`[courier:email] provider ${this.config.provider} not implemented`);
|
|
329
342
|
}
|
|
330
343
|
async sendViaResend(to, template) {
|
|
331
344
|
if (!this.config.apiKey) {
|
|
@@ -349,18 +362,23 @@ var EmailChannel = class {
|
|
|
349
362
|
const body2 = await res.text();
|
|
350
363
|
throw new Error(`[courier:email] Resend error ${res.status}: ${body2}`);
|
|
351
364
|
}
|
|
365
|
+
const data = await res.json().catch(() => null);
|
|
366
|
+
return typeof data?.id === "string" ? { providerMessageId: data.id } : {};
|
|
352
367
|
}
|
|
353
368
|
async sendViaSMTP(to, template) {
|
|
354
369
|
if (!this.transport) {
|
|
355
370
|
throw new Error("SMTP transport not initialised \u2014 check smtp config");
|
|
356
371
|
}
|
|
357
|
-
await this.transport.sendMail({
|
|
372
|
+
const info = await this.transport.sendMail({
|
|
358
373
|
from: this.config.from,
|
|
359
374
|
to,
|
|
360
375
|
subject: template.subject ?? "(no subject)",
|
|
361
376
|
text: template.text,
|
|
362
377
|
...template.html !== void 0 ? { html: template.html } : {}
|
|
363
378
|
});
|
|
379
|
+
const raw = typeof info?.messageId === "string" ? info.messageId : "";
|
|
380
|
+
const providerMessageId = raw.replace(/^<|>$/g, "");
|
|
381
|
+
return providerMessageId ? { providerMessageId } : {};
|
|
364
382
|
}
|
|
365
383
|
};
|
|
366
384
|
|
|
@@ -647,29 +665,34 @@ function validateCourierConfig(config, registeredChannels) {
|
|
|
647
665
|
// src/delivery.ts
|
|
648
666
|
var import_node_crypto = require("crypto");
|
|
649
667
|
var import_core = require("@fonderie/core");
|
|
650
|
-
async function handleSendGridDelivery(req, store,
|
|
651
|
-
if (
|
|
652
|
-
|
|
653
|
-
const ts = req.headers.get("x-twilio-email-event-webhook-timestamp") ?? "";
|
|
654
|
-
const body2 = await req.text();
|
|
655
|
-
if (!verifySendGridSignature(webhookSecret, ts, body2, sig)) {
|
|
656
|
-
return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
|
|
657
|
-
}
|
|
658
|
-
const events2 = parseJson(body2);
|
|
659
|
-
if (!Array.isArray(events2)) return Response.json({ ok: true });
|
|
660
|
-
await processSendGridEvents(events2, store);
|
|
661
|
-
return Response.json({ ok: true });
|
|
668
|
+
async function handleSendGridDelivery(req, store, publicKey) {
|
|
669
|
+
if (!publicKey) {
|
|
670
|
+
return Response.json({ error: "VERIFICATION_NOT_CONFIGURED" }, { status: 401 });
|
|
662
671
|
}
|
|
663
|
-
const
|
|
672
|
+
const sig = req.headers.get("x-twilio-email-event-webhook-signature") ?? "";
|
|
673
|
+
const ts = req.headers.get("x-twilio-email-event-webhook-timestamp") ?? "";
|
|
674
|
+
const body2 = await req.text();
|
|
675
|
+
if (!verifySendGridSignature(publicKey, ts, body2, sig)) {
|
|
676
|
+
return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
|
|
677
|
+
}
|
|
678
|
+
const events = parseJson(body2);
|
|
664
679
|
if (!Array.isArray(events)) return Response.json({ ok: true });
|
|
665
680
|
await processSendGridEvents(events, store);
|
|
666
681
|
return Response.json({ ok: true });
|
|
667
682
|
}
|
|
668
|
-
function verifySendGridSignature(
|
|
683
|
+
function verifySendGridSignature(publicKeyB64, timestamp, body2, signature) {
|
|
669
684
|
try {
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
685
|
+
const key = (0, import_node_crypto.createPublicKey)({
|
|
686
|
+
key: Buffer.from(publicKeyB64, "base64"),
|
|
687
|
+
format: "der",
|
|
688
|
+
type: "spki"
|
|
689
|
+
});
|
|
690
|
+
return (0, import_node_crypto.verify)(
|
|
691
|
+
"sha256",
|
|
692
|
+
Buffer.from(timestamp + body2),
|
|
693
|
+
key,
|
|
694
|
+
Buffer.from(signature, "base64")
|
|
695
|
+
);
|
|
673
696
|
} catch {
|
|
674
697
|
return false;
|
|
675
698
|
}
|
|
@@ -698,12 +721,13 @@ async function processSendGridEvents(events, store) {
|
|
|
698
721
|
}
|
|
699
722
|
}
|
|
700
723
|
async function handleMailgunDelivery(req, store, signingKey) {
|
|
724
|
+
if (!signingKey) {
|
|
725
|
+
return Response.json({ error: "VERIFICATION_NOT_CONFIGURED" }, { status: 401 });
|
|
726
|
+
}
|
|
701
727
|
const body2 = await req.json();
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
|
|
706
|
-
}
|
|
728
|
+
const { signature } = body2;
|
|
729
|
+
if (!signature || !verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {
|
|
730
|
+
return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
|
|
707
731
|
}
|
|
708
732
|
const event = body2["event-data"];
|
|
709
733
|
if (event) {
|
|
@@ -948,22 +972,29 @@ var CourierModule = class {
|
|
|
948
972
|
install(app) {
|
|
949
973
|
validateCourierConfig(this.config, this.dispatcher.channelNames());
|
|
950
974
|
const store = this.store;
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
975
|
+
const delivery = this.config.delivery;
|
|
976
|
+
const signingKeys = delivery?.signingKeys;
|
|
977
|
+
if (signingKeys?.sendgrid) {
|
|
978
|
+
app.addRoute(
|
|
979
|
+
"POST",
|
|
980
|
+
"/courier/delivery/sendgrid",
|
|
981
|
+
(ctx) => handleSendGridDelivery(ctx.request, store, signingKeys.sendgrid)
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
if (signingKeys?.mailgun) {
|
|
985
|
+
app.addRoute(
|
|
986
|
+
"POST",
|
|
987
|
+
"/courier/delivery/mailgun",
|
|
988
|
+
(ctx) => handleMailgunDelivery(ctx.request, store, signingKeys.mailgun)
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
if (delivery?.allowUnverifiedMailtrap) {
|
|
992
|
+
app.addRoute(
|
|
993
|
+
"POST",
|
|
994
|
+
"/courier/delivery/mailtrap",
|
|
995
|
+
(ctx) => handleMailtrapDelivery(ctx.request, store)
|
|
996
|
+
);
|
|
997
|
+
}
|
|
967
998
|
if (this.config.adminToken) {
|
|
968
999
|
if (!store) {
|
|
969
1000
|
throw new Error("[courier] adminToken requires @fonderie/store (db templates)");
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/module.ts","../src/log.ts","../src/dispatcher.ts","../src/channels/sms.ts","../src/channels/push.ts","../src/channels/email.ts","../src/templates/layout.ts","../src/templates/resolver.ts","../src/config-guard.ts","../src/delivery.ts","../src/templates/admin-routes.ts","../src/templates/admin.ts","../src/config.ts"],"sourcesContent":["export { CourierModule } from './module';\n// Production-readiness — warn on message types routed to a channel with no\n// provider. Runs automatically at boot (CourierModule.install); exported for an\n// app's own preflight.\nexport { validateCourierConfig } from './config-guard';\nexport { handleSendGridDelivery, handleMailgunDelivery, handleMailtrapDelivery } from './delivery';\nexport { Dispatcher } from './dispatcher';\nexport { SmsChannel } from './channels/sms';\nexport { PushChannel } from './channels/push';\nexport { EmailChannel } from './channels/email';\nexport { DBTemplateResolver, FSTemplateResolver, DefaultTemplates, renderFragment } from './templates/resolver';\n// Versioned template management (on the @fonderie/store primitive): edit with\n// optimistic concurrency, revision history, rollback. The resolver is unchanged.\nexport {\n\tsetTemplate,\n\trollbackTemplate,\n\tlistTemplateRevisions,\n\tgetTemplateEntry,\n\tlistTemplateEntries,\n\tdeleteTemplate,\n} from './templates/admin';\nexport { buildTemplateAdminRoutes } from './templates/admin-routes';\nexport type { ITemplateEntry, ITemplateRevision } from './templates/admin';\nexport type { IMessageLog, MessageLogStatus } from './log';\nexport type {\n\tICourierMessage,\n\tICourierChannel,\n\tIRenderedTemplate,\n\tITemplateResolver,\n\tIDefaultTemplate,\n\tDefaultTemplateMap,\n} from './types';\nexport { Channel } from './config';\nexport type {\n\tICourierConfig,\n\tIEmailChannelConfig,\n\tISmsChannelConfig,\n\tIPushChannelConfig,\n} from './config';\n","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 type { IReadinessProblem } from '@fonderie/core';\nimport { validateAdminToken } from '@fonderie/core/middlewares';\nimport { DBTemplateResolver, FSTemplateResolver, DefaultTemplates } from './templates/resolver';\nimport { validateCourierConfig, collectCourierConfigProblems } from './config-guard';\nimport { handleSendGridDelivery, handleMailgunDelivery, handleMailtrapDelivery } from './delivery';\nimport { buildTemplateAdminRoutes } from './templates/admin-routes';\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\t// Report config problems for app.checkProductionReadiness() (data, not warn).\n\tcheckReadiness(): IReadinessProblem[] {\n\t\treturn [\n\t\t\t...collectCourierConfigProblems(this.config, this.dispatcher.channelNames()),\n\t\t\t// Shared admin-token strength rule (@fonderie/core/middlewares) — the\n\t\t\t// /admin/templates surface must not be guarded by a weak/placeholder token.\n\t\t\t...validateAdminToken(this.config.adminToken, { module: this.name }),\n\t\t];\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\t// Boot-time preflight: warn if any routed message type has no provider.\n\t\tvalidateCourierConfig(this.config, this.dispatcher.channelNames());\n\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\n\t\t// Versioned template admin routes — only when a token is configured and a\n\t\t// store is present (db templates). Mirrors @fonderie/config's admin surface.\n\t\tif (this.config.adminToken) {\n\t\t\tif (!store) {\n\t\t\t\tthrow new Error('[courier] adminToken requires @fonderie/store (db templates)');\n\t\t\t}\n\t\t\tfor (const [method, path, handler] of buildTemplateAdminRoutes(store, this.config.adminToken)) {\n\t\t\t\tapp.addRoute(method, path, handler);\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction createTemplateResolver(\n\tsource: 'db' | 'fs',\n\tconfig: ICourierConfig,\n\tstore?: IStoreAdapter,\n): ITemplateResolver {\n\t// Aggregate the module-shipped defaults (one map or an array) into a single\n\t// lookup, mirroring how getMigrationsPath() results are collected at boot.\n\tconst input = config.templates?.defaults;\n\tconst defaults = new DefaultTemplates(input ? (Array.isArray(input) ? input : [input]) : []);\n\n\tif (source === 'fs') {\n\t\treturn new FSTemplateResolver(config.templates?.directory ?? './templates', defaults);\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, defaults);\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\t// Names of the currently-registered channels — used by the boot-time config\n\t// guard to detect message types routed to a channel with no provider.\n\tchannelNames(): string[] {\n\t\treturn [...this.channels.keys()];\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\t// Omit `html` when the template has none rather than passing `undefined`:\n\t\t// nodemailer 10's SendMailOptions is an exact-optional type, so an explicit\n\t\t// `html: undefined` is a type error (and was never meaningful at runtime).\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\ttext: template.text,\n\t\t\t...(template.html !== undefined ? { html: template.html } : {}),\n\t\t});\n\t}\n}\n","// Shared, composable email layout — a branded shell that wraps every template\n// body. Templates stored in the DB or on disk are **body fragments**; the\n// resolver injects them into `{{content}}` here, then interpolates `{{vars}}`\n// over the whole. One shell, many bodies — so DB and FS sources render an\n// identical, production-grade frame for free.\n//\n// Cross-client hardening (Outlook/Gmail/Apple Mail) follows the well-worn\n// \"hybrid responsive\" pattern popularised by Cerberus (MIT): a max-width table\n// container, inline styles on the critical structure, and a `<style>` block in\n// `<head>` for the friendly classes and small-screen media query. Founders can\n// override the whole shell by storing a `_layout` template (DB row or\n// `_layout.html` file) in their own source.\n//\n// The theme tokens mirror the product design system (organization/ui/base.css):\n// near-black primary, a mint brand accent, Inter, and the `#fafafa` canvas —\n// retune `EMAIL_THEME` to rebrand every email at once.\n\n// A single small token set — retune these to match your brand.\nexport const EMAIL_THEME = {\n\tbrand: 'Fonderie',\n\taccent: '#171717', // primary action (button) background — the product's primary\n\taccentText: '#ffffff', // text on the accent\n\tbrandAccent: '#171717', // brand highlight (top rule) — monochrome near-black\n\tlink: '#009767', // links — the accessible (darkened) brand teal\n\tink: '#171717', // body copy\n\tmuted: '#5c5c5c', // secondary copy\n\tline: '#e0e0e0', // hairline borders\n\tcanvas: '#fafafa', // page background behind the card\n\tcard: '#ffffff', // the card itself\n} as const;\n\n// The product font stack (Inter first, with system fallbacks for clients that\n// lack it). Shared by every text element so the frame stays consistent.\nconst FONT_SANS =\n\t\"Inter, 'Inter Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif\";\nconst FONT_MONO = \"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace\";\n\n// The `{{content}}` marker is replaced with the body fragment BEFORE variable\n// interpolation, so a body's own `{{firstName}}` etc. still resolve.\nexport const LAYOUT_CONTENT_SLOT = '{{content}}';\n\nexport const DEFAULT_EMAIL_LAYOUT = `<!DOCTYPE html>\n<html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n<head>\n\t<meta charset=\"utf-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"color-scheme\" content=\"light dark\">\n\t<meta name=\"supported-color-schemes\" content=\"light dark\">\n\t<title>{{subject}}</title>\n\t<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->\n\t<style>\n\t\tbody { margin: 0; padding: 0; width: 100% !important; background: ${EMAIL_THEME.canvas}; }\n\t\t.email-body { background: ${EMAIL_THEME.canvas}; }\n\t\t.email-container { width: 100%; max-width: 560px; margin: 0 auto; }\n\t\t.email-card {\n\t\t\tbackground: ${EMAIL_THEME.card};\n\t\t\tborder: 1px solid ${EMAIL_THEME.line};\n\t\t\tborder-radius: 8px;\n\t\t\toverflow: hidden;\n\t\t}\n\t\t.email-accent { height: 3px; background: ${EMAIL_THEME.brandAccent}; font-size: 0; line-height: 0; }\n\t\t.email-header { padding: 28px 32px 0 32px; }\n\t\t.email-brand { font: 800 19px/1 ${FONT_SANS}; color: ${EMAIL_THEME.ink}; letter-spacing: -0.03em; }\n\t\t.email-content {\n\t\t\tpadding: 20px 32px 8px 32px;\n\t\t\tfont: 400 16px/1.7 ${FONT_SANS};\n\t\t\tcolor: ${EMAIL_THEME.ink};\n\t\t\tletter-spacing: -0.01em;\n\t\t}\n\t\t.email-content h1 { margin: 0 0 12px 0; font-size: 22px; line-height: 1.3; font-weight: 700; letter-spacing: -0.03em; }\n\t\t.email-content p { margin: 0 0 16px 0; }\n\t\t.email-content a { color: ${EMAIL_THEME.link}; }\n\t\t.email-footer {\n\t\t\tpadding: 16px 32px 28px 32px;\n\t\t\tfont: 400 13px/1.6 ${FONT_SANS};\n\t\t\tcolor: ${EMAIL_THEME.muted};\n\t\t\tletter-spacing: -0.01em;\n\t\t}\n\t\t.btn {\n\t\t\tdisplay: inline-block;\n\t\t\tbackground: ${EMAIL_THEME.accent};\n\t\t\tcolor: ${EMAIL_THEME.accentText} !important;\n\t\t\ttext-decoration: none;\n\t\t\tfont: 600 16px/1 ${FONT_SANS};\n\t\t\tletter-spacing: -0.01em;\n\t\t\tpadding: 13px 26px;\n\t\t\tborder-radius: 6px;\n\t\t}\n\t\t.pin-code {\n\t\t\tdisplay: inline-block;\n\t\t\tfont: 700 30px/1 ${FONT_MONO};\n\t\t\tletter-spacing: 0.28em;\n\t\t\tcolor: ${EMAIL_THEME.ink};\n\t\t\tbackground: ${EMAIL_THEME.canvas};\n\t\t\tborder: 1px solid ${EMAIL_THEME.line};\n\t\t\tborder-radius: 6px;\n\t\t\tpadding: 14px 22px 14px 30px;\n\t\t}\n\t\t.muted { color: ${EMAIL_THEME.muted}; }\n\t\t@media only screen and (max-width: 599px) {\n\t\t\t.email-header, .email-content, .email-footer { padding-left: 22px !important; padding-right: 22px !important; }\n\t\t\t.pin-code { font-size: 26px !important; letter-spacing: 0.2em !important; }\n\t\t}\n\t</style>\n</head>\n<body class=\"email-body\">\n\t<div style=\"display:none;max-height:0;overflow:hidden;opacity:0;\">{{preheader}}</div>\n\t<table role=\"presentation\" class=\"email-body\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"background:${EMAIL_THEME.canvas};\">\n\t\t<tr>\n\t\t\t<td align=\"center\" style=\"padding: 32px 16px;\">\n\t\t\t\t<table role=\"presentation\" class=\"email-container\" width=\"560\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"max-width:560px;\">\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td class=\"email-card\" style=\"background:${EMAIL_THEME.card};border:1px solid ${EMAIL_THEME.line};border-radius:8px;\">\n\t\t\t\t\t\t\t<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n\t\t\t\t\t\t\t\t<tr><td class=\"email-accent\" style=\"height:3px;background:${EMAIL_THEME.brandAccent};font-size:0;line-height:0;\"> </td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-header\" style=\"padding:28px 32px 0 32px;\">\n\t\t\t\t\t\t\t\t\t<span class=\"email-brand\" style=\"font-weight:800;font-size:19px;color:${EMAIL_THEME.ink};letter-spacing:-0.03em;\">${EMAIL_THEME.brand}</span>\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-content\" style=\"padding:20px 32px 8px 32px;color:${EMAIL_THEME.ink};\">\n${LAYOUT_CONTENT_SLOT}\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-footer\" style=\"padding:16px 32px 28px 32px;color:${EMAIL_THEME.muted};font-size:13px;\">\n\t\t\t\t\t\t\t\t\tYou're receiving this because someone used this address at ${EMAIL_THEME.brand}. If that wasn't you, you can ignore it.\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t</table>\n\t\t\t</td>\n\t\t</tr>\n\t</table>\n</body>\n</html>`;\n\n// Inject a body fragment into a layout shell. If the body is already a full\n// HTML document (a founder chose to store a complete template), it is returned\n// untouched — never double-wrapped.\nexport function wrapLayout(bodyHtml: string, layoutHtml: string = DEFAULT_EMAIL_LAYOUT): string {\n\tconst trimmed = bodyHtml.trimStart().toLowerCase();\n\tif (trimmed.startsWith('<!doctype') || trimmed.startsWith('<html')) {\n\t\treturn bodyHtml;\n\t}\n\treturn layoutHtml.replace(LAYOUT_CONTENT_SLOT, bodyHtml);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ITemplateResolver, IRenderedTemplate, IDefaultTemplate, DefaultTemplateMap } from '../types';\nimport { wrapLayout } from './layout';\n\n// The stored template id for a founder-supplied layout shell (DB row `type` or\n// FS file `_layout.html`). Absent → the built-in DEFAULT_EMAIL_LAYOUT is used.\nconst LAYOUT_TYPE = '_layout';\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// Compose a body fragment into its layout shell, then interpolate variables\n// over the whole. `subject`/`preheader` become available to the shell's title\n// and inbox preview text.\nfunction composeHtml(\n\tbodyHtml: string,\n\tlayoutHtml: string | undefined,\n\tsubject: string | undefined,\n\tdata: Record<string, unknown>,\n): string {\n\tconst wrapped = wrapLayout(bodyHtml, layoutHtml);\n\treturn render(wrapped, { subject: subject ?? '', preheader: '', ...data });\n}\n\n// Render a resolved fragment — a DB row, an FS file set, or a MODULE DEFAULT —\n// into the final template. Factored out so a module default renders\n// byte-identically to a DB row: same {{var}} interpolation, same layout\n// composition. `text` is required; `subject`/`html` are optional (email only).\nexport function renderFragment(\n\tfrag: { subject?: string | null; text: string; html?: string | null },\n\tlayoutHtml: string | undefined,\n\tdata: Record<string, unknown>,\n): IRenderedTemplate {\n\tconst subject = frag.subject ? render(frag.subject, data) : undefined;\n\treturn {\n\t\ttext: render(frag.text, data),\n\t\t...(subject ? { subject } : {}),\n\t\t...(frag.html ? { html: composeHtml(frag.html, layoutHtml, subject, data) } : {}),\n\t};\n}\n\n// The module-shipped default templates an app hands courier, merged into one\n// lookup (aggregated like getMigrationsPath()). Per-key app overrides — a DB row\n// or an FS file — always win over a default; the default wins over the\n// last-resort JSON dump. A later map wins on key collision.\nexport class DefaultTemplates {\n\tprivate readonly map = new Map<string, IDefaultTemplate>();\n\tconstructor(maps: DefaultTemplateMap[] = []) {\n\t\tfor (const m of maps) {\n\t\t\tfor (const [key, tmpl] of Object.entries(m)) this.map.set(key, tmpl);\n\t\t}\n\t}\n\tget(type: string): IDefaultTemplate | undefined {\n\t\treturn this.map.get(type);\n\t}\n\tget size(): number {\n\t\treturn this.map.size;\n\t}\n}\n\n// DB-backed resolver — reads from fonderie_courier_templates with locale fallback\nexport class DBTemplateResolver implements ITemplateResolver {\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate defaults?: DefaultTemplates,\n\t) {}\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// Serve the exact locale, else the neutral NULL default — never a\n\t\t\t// sibling region (en-CA must not fall back to en-US); the WHERE\n\t\t\t// excludes other locales so legal/jurisdictional copy can't bleed.\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 AND (locale = $2 OR locale IS NULL)\n\t\t\t ORDER BY (locale IS NOT DISTINCT FROM $2) 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\t// No app row → fall back to the module default (rendered identically),\n\t\t\t// then to the JSON dump — now only reached for a type neither the app\n\t\t\t// nor any module provides.\n\t\t\tconst def = this.defaults?.get(type);\n\t\t\tif (def) {\n\t\t\t\treturn renderFragment(def, def.html ? await this.layout(locale) : undefined, data);\n\t\t\t}\n\t\t\treturn { text: `${type}: ${JSON.stringify(data)}` };\n\t\t}\n\n\t\tconst layoutHtml = row.html ? await this.layout(locale) : undefined;\n\t\treturn renderFragment(row, layoutHtml, data);\n\t}\n\n\t// Optional founder-supplied layout shell; undefined → built-in default.\n\tprivate async layout(locale?: string): Promise<string | undefined> {\n\t\tconst [row] = await this.store.query<{ html: string | null }>(\n\t\t\t`SELECT html\n\t\t\t FROM fonderie_courier_templates\n\t\t\t WHERE type = $1 AND active = true AND (locale = $2 OR locale IS NULL)\n\t\t\t ORDER BY (locale IS NOT DISTINCT FROM $2) DESC\n\t\t\t LIMIT 1`,\n\t\t\t[LAYOUT_TYPE, locale ?? null],\n\t\t);\n\t\treturn row?.html ?? undefined;\n\t}\n}\n\n// Filesystem resolver — reads {type}.{locale}.txt → {type}.txt with fallback\nexport class FSTemplateResolver implements ITemplateResolver {\n\tconstructor(\n\t\tprivate directory: string,\n\t\tprivate defaults?: DefaultTemplates,\n\t) {}\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\t\tconst layoutPrefix = locale ? `${LAYOUT_TYPE}.${locale}` : null;\n\n\t\tconst [text, html, subject, layout] = 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\n\t\t\tlayoutPrefix\n\t\t\t\t? readOptional(join(this.directory, `${layoutPrefix}.html`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${LAYOUT_TYPE}.html`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${LAYOUT_TYPE}.html`)),\n\t\t]);\n\n\t\t// App shipped NOTHING for this type (no text/html/subject file) → fall back\n\t\t// to the module default, using the app's own _layout shell if it ships one,\n\t\t// then to the JSON dump. Test file PRESENCE (=== null), not truthiness, so an\n\t\t// app that ships even an empty file keeps per-key control below — mirroring\n\t\t// the DB resolver's row-presence check, not overriding it with a default.\n\t\tif (text === null && html === null && subject === null) {\n\t\t\tconst def = this.defaults?.get(type);\n\t\t\tif (def) {\n\t\t\t\treturn renderFragment(def, def.html ? (layout ?? undefined) : undefined, data);\n\t\t\t}\n\t\t\treturn { text: `${type}: ${JSON.stringify(data)}` };\n\t\t}\n\n\t\tconst renderedSubject = subject ? render(subject, data) : undefined;\n\n\t\treturn {\n\t\t\ttext: text ? render(text, data) : `${type}: ${JSON.stringify(data)}`,\n\t\t\t...(renderedSubject ? { subject: renderedSubject } : {}),\n\t\t\t...(html ? { html: composeHtml(html, layout ?? undefined, renderedSubject, data) } : {}),\n\t\t};\n\t}\n}\n","import type { IReadinessProblem } from '@fonderie/core';\nimport type { ICourierConfig } from './config';\n\nconst MODULE = '@fonderie/courier';\n\n// Pure production-readiness assessment (no side effects) — shared by the\n// boot-time guard and `CourierModule.checkReadiness`. A message type routed to a\n// channel with no registered provider is silently dropped by the dispatcher —\n// with `requireVerification` on, verification / reset emails vanish and users\n// lock themselves out. These are `warning`-severity (channel setups vary\n// legitimately — an app may run in-app only — so this never blocks boot, unlike\n// auth's fatal weak-secret). Checks the channels actually **registered** on the\n// dispatcher, so channels added via `registerChannel` count as present.\nexport function collectCourierConfigProblems(\n\tconfig: ICourierConfig,\n\tregisteredChannels: Iterable<string>,\n): IReadinessProblem[] {\n\tconst registered = new Set(registeredChannels);\n\tconst gaps = new Map<string, string[]>(); // channel → message types routed to it\n\n\tfor (const [type, channels] of Object.entries(config.channels ?? {})) {\n\t\tfor (const channel of channels) {\n\t\t\tif (!registered.has(channel)) {\n\t\t\t\tconst types = gaps.get(channel) ?? [];\n\t\t\t\ttypes.push(type);\n\t\t\t\tgaps.set(channel, types);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn [...gaps].map(([channel, types]) => ({\n\t\tmodule: MODULE,\n\t\tseverity: 'warning' as const,\n\t\tmessage:\n\t\t\t`${types.length} message type(s) route to the '${channel}' channel but ` +\n\t\t\t`no '${channel}' provider is registered — these will be silently dropped: ` +\n\t\t\t`${types.join(', ')}. Configure \\`config.${channel}\\` (or register a channel).`,\n\t}));\n}\n\n// Boot-time preflight, run from `CourierModule.install`. Warn-only.\nexport function validateCourierConfig(\n\tconfig: ICourierConfig,\n\tregisteredChannels: Iterable<string>,\n): void {\n\tfor (const problem of collectCourierConfigProblems(config, registeredChannels)) {\n\t\tconsole.warn(`[courier] ${problem.message}`);\n\t}\n}\n","import { createHmac } from 'node:crypto';\n\nimport { constantTimeEqual } from '@fonderie/core';\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\treturn constantTimeEqual(Buffer.from(signature, 'base64'), Buffer.from(expected, 'base64'));\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\treturn constantTimeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'));\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","import type { IFonderieContext, Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport { requireAdminToken } from '@fonderie/core/middlewares';\nimport { VersionConflictError } from '@fonderie/store';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport {\n\tsetTemplate,\n\trollbackTemplate,\n\tlistTemplateRevisions,\n\tgetTemplateEntry,\n\tlistTemplateEntries,\n\tdeleteTemplate,\n} from './admin';\n\n// Bearer-token guard (mirrors @fonderie/config's admin surface). Only registered\n// when a token is configured — no token, no exposed template admin routes.\n// Wrap a handler in the shared admin-token guard (@fonderie/core/middlewares) —\n// one constant-time Bearer check across billing/config/courier.\nfunction guarded(adminToken: string, handler: Middleware): Middleware {\n\tconst guard = requireAdminToken(adminToken);\n\treturn (ctx, next) => guard(ctx, () => handler(ctx, next));\n}\n\nconst actorOf = (ctx: IFonderieContext) => ctx.request.headers.get('x-actor') || 'admin-token';\nconst typeOf = (ctx: IFonderieContext) => ctx.meta.params?.['type'] ?? '';\nconst localeOf = (ctx: IFonderieContext): string | null =>\n\tnew URL(ctx.request.url).searchParams.get('locale');\nconst body = (ctx: IFonderieContext): Record<string, unknown> =>\n\t(ctx.meta['body'] as Record<string, unknown> | undefined) ?? {};\n\nfunction conflictOr(err: unknown): Response {\n\tif (err instanceof VersionConflictError) {\n\t\treturn setApiResponse(HTTP.CONFLICT, 'VERSION_CONFLICT', err.message, {\n\t\t\tcurrentVersion: err.currentVersion,\n\t\t});\n\t}\n\tthrow err;\n}\n\n// Build the template admin route table (registered by CourierModule.install when\n// an adminToken is configured).\nexport function buildTemplateAdminRoutes(\n\tstore: IStoreAdapter,\n\tadminToken: string,\n): Array<[string, string, Middleware]> {\n\tconst g = (h: Middleware): Middleware => guarded(adminToken, h);\n\n\treturn [\n\t\t['GET', '/admin/templates', g(async () => {\n\t\t\treturn setApiResponse(HTTP.OK, 'TEMPLATES_LISTED', 'Templates', await listTemplateEntries(store));\n\t\t})],\n\t\t['GET', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst row = await getTemplateEntry(typeOf(ctx), localeOf(ctx), store);\n\t\t\treturn row\n\t\t\t\t? setApiResponse(HTTP.OK, 'TEMPLATE', 'Template', row)\n\t\t\t\t: setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such template');\n\t\t})],\n\t\t['PUT', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tif (typeof b['text'] !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.text (string) is required');\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst opts: Parameters<typeof setTemplate>[0] = {\n\t\t\t\t\ttype: typeOf(ctx),\n\t\t\t\t\ttext: b['text'],\n\t\t\t\t\tlocale: localeOf(ctx),\n\t\t\t\t\tactor: actorOf(ctx),\n\t\t\t\t};\n\t\t\t\tif (typeof b['subject'] === 'string') opts.subject = b['subject'];\n\t\t\t\tif (typeof b['html'] === 'string') opts.html = b['html'];\n\t\t\t\tif (typeof b['active'] === 'boolean') opts.active = b['active'];\n\t\t\t\tif (typeof b['ifVersion'] === 'number') opts.ifVersion = b['ifVersion'];\n\t\t\t\treturn setApiResponse(HTTP.OK, 'TEMPLATE_SET', 'Template saved', await setTemplate(opts, store));\n\t\t\t} catch (err) {\n\t\t\t\treturn conflictOr(err);\n\t\t\t}\n\t\t})],\n\t\t['DELETE', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst ok = await deleteTemplate(typeOf(ctx), localeOf(ctx), store);\n\t\t\treturn setApiResponse(ok ? HTTP.OK : HTTP.NOT_FOUND, ok ? 'DELETED' : 'NOT_FOUND', ok ? 'Deleted' : 'No such template');\n\t\t})],\n\t\t['GET', '/admin/templates/:type/revisions', g(async (ctx) => {\n\t\t\treturn setApiResponse(HTTP.OK, 'REVISIONS', 'Template revisions', await listTemplateRevisions(typeOf(ctx), localeOf(ctx), store));\n\t\t})],\n\t\t['POST', '/admin/templates/:type/rollback', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tconst toVersion = Number(b['toVersion']);\n\t\t\tif (!Number.isInteger(toVersion)) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.toVersion (int) is required');\n\t\t\t}\n\t\t\tconst row = await rollbackTemplate(\n\t\t\t\t{ type: typeOf(ctx), locale: localeOf(ctx), toVersion, actor: actorOf(ctx) },\n\t\t\t\tstore,\n\t\t\t);\n\t\t\treturn setApiResponse(HTTP.OK, 'ROLLED_BACK', `Rolled back to v${toVersion}`, row);\n\t\t})],\n\t];\n}\n","import type { IStoreAdapter, IVersionedResource } from '@fonderie/store';\nimport { versionedWrite, versionedRollback } from '@fonderie/store';\n\n// Versioned management for email templates, on the shared `@fonderie/store`\n// primitive. Templates are keyed by (type, locale) — a NULL locale is the base,\n// with per-locale overrides on top — and their content is subject/html/text.\n// The resolver reads the current row unchanged; this adds edit + history +\n// rollback + optimistic concurrency on top.\n\nexport interface ITemplateEntry {\n\ttype: string;\n\tlocale: string | null;\n\tsubject: string | null;\n\thtml: string | null;\n\ttext: string;\n\tactive: boolean;\n\tversion: number;\n\tupdatedBy: string | null;\n\tupdatedAt: string;\n}\n\nexport interface ITemplateRevision {\n\ttype: string;\n\tlocale: string | null;\n\tsubject: string | null;\n\thtml: string | null;\n\ttext: string;\n\tversion: number;\n\tactor: string | null;\n\tcreatedAt: string;\n}\n\nconst ENTRY_COLS = `type, locale, subject, html, text, active, version, updated_by AS \"updatedBy\", updated_at AS \"updatedAt\"`;\n\nconst TEMPLATE_RESOURCE: IVersionedResource = {\n\ttable: 'fonderie_courier_templates',\n\trevisions: 'fonderie_courier_template_revisions',\n\tchannel: 'fonderie_courier_templates_changed',\n\tkeyColumns: ['type', 'locale'],\n\tcontentColumns: ['subject', 'html', 'text'],\n\tmetaColumns: ['active'],\n\treturning: ENTRY_COLS,\n};\n\n// Upsert a template (full body — a revision is a full snapshot). Pass `ifVersion`\n// for optimistic concurrency (else `VersionConflictError`). `locale` omitted /\n// null targets the base template.\nexport async function setTemplate(\n\topts: {\n\t\ttype: string;\n\t\ttext: string;\n\t\tlocale?: string | null;\n\t\tsubject?: string | null;\n\t\thtml?: string | null;\n\t\tactive?: boolean;\n\t\tifVersion?: number;\n\t\tactor?: string;\n\t},\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry> {\n\tconst data: Record<string, unknown> = {\n\t\tsubject: opts.subject ?? null,\n\t\thtml: opts.html ?? null,\n\t\ttext: opts.text,\n\t};\n\tif (opts.active !== undefined) data['active'] = opts.active;\n\treturn versionedWrite<ITemplateEntry>(TEMPLATE_RESOURCE, store, {\n\t\tkey: opts.type,\n\t\tscope: opts.locale ?? null,\n\t\tdata,\n\t\t...(opts.ifVersion !== undefined ? { ifVersion: opts.ifVersion } : {}),\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function rollbackTemplate(\n\topts: { type: string; locale?: string | null; toVersion: number; actor?: string },\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry> {\n\treturn versionedRollback<ITemplateEntry>(TEMPLATE_RESOURCE, store, {\n\t\tkey: opts.type,\n\t\tscope: opts.locale ?? null,\n\t\ttoVersion: opts.toVersion,\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function listTemplateRevisions(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<ITemplateRevision[]> {\n\treturn store.query<ITemplateRevision>(\n\t\t`SELECT type, locale, subject, html, text, version, actor, created_at AS \"createdAt\"\n\t\t FROM fonderie_courier_template_revisions\n\t\t WHERE type = $1 AND locale IS NOT DISTINCT FROM $2\n\t\t ORDER BY version DESC`,\n\t\t[type, locale],\n\t);\n}\n\nexport async function getTemplateEntry(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry | null> {\n\tconst [row] = await store.query<ITemplateEntry>(\n\t\t`SELECT ${ENTRY_COLS} FROM fonderie_courier_templates WHERE type = $1 AND locale IS NOT DISTINCT FROM $2`,\n\t\t[type, locale],\n\t);\n\treturn row ?? null;\n}\n\nexport async function listTemplateEntries(store: IStoreAdapter): Promise<ITemplateEntry[]> {\n\treturn store.query<ITemplateEntry>(\n\t\t`SELECT ${ENTRY_COLS} FROM fonderie_courier_templates ORDER BY type, locale NULLS FIRST`,\n\t);\n}\n\nexport async function deleteTemplate(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst rows = await store.query<{ type: string }>(\n\t\t`DELETE FROM fonderie_courier_templates WHERE type = $1 AND locale IS NOT DISTINCT FROM $2 RETURNING type`,\n\t\t[type, locale],\n\t);\n\treturn rows.length > 0;\n}\n","import type { DefaultTemplateMap } from './types';\n\nexport 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// When set, exposes Bearer-guarded template admin routes (/admin/templates/*)\n\t// for versioned edit/history/rollback. Requires @fonderie/store (db templates).\n\tadminToken?: string;\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\t// Module-shipped default templates. The app imports each module's\n\t\t// DEFAULT_TEMPLATES (auth, workspaces, billing, …) and passes them here;\n\t\t// the resolver falls back to a default when no app override (DB row / FS\n\t\t// file) exists, before the last-resort JSON dump. Aggregated like\n\t\t// getMigrationsPath() — pass one map or an array of them.\n\t\tdefaults?: DefaultTemplateMap | DefaultTemplateMap[];\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":["body","body","nodemailer","body","import_middlewares","body","events","import_core","import_store"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/module.ts","../src/log.ts","../src/dispatcher.ts","../src/channels/sms.ts","../src/channels/push.ts","../src/channels/email.ts","../src/templates/layout.ts","../src/templates/resolver.ts","../src/config-guard.ts","../src/delivery.ts","../src/templates/admin-routes.ts","../src/templates/admin.ts","../src/config.ts"],"sourcesContent":["export { CourierModule } from './module';\n// Production-readiness — warn on message types routed to a channel with no\n// provider. Runs automatically at boot (CourierModule.install); exported for an\n// app's own preflight.\nexport { validateCourierConfig } from './config-guard';\nexport { handleSendGridDelivery, handleMailgunDelivery, handleMailtrapDelivery } from './delivery';\nexport { Dispatcher } from './dispatcher';\nexport { SmsChannel } from './channels/sms';\nexport { PushChannel } from './channels/push';\nexport { EmailChannel } from './channels/email';\nexport { DBTemplateResolver, FSTemplateResolver, DefaultTemplates, renderFragment } from './templates/resolver';\n// Versioned template management (on the @fonderie/store primitive): edit with\n// optimistic concurrency, revision history, rollback. The resolver is unchanged.\nexport {\n\tsetTemplate,\n\trollbackTemplate,\n\tlistTemplateRevisions,\n\tgetTemplateEntry,\n\tlistTemplateEntries,\n\tdeleteTemplate,\n} from './templates/admin';\nexport { buildTemplateAdminRoutes } from './templates/admin-routes';\nexport type { ITemplateEntry, ITemplateRevision } from './templates/admin';\nexport type { IMessageLog, MessageLogStatus } from './log';\nexport type {\n\tICourierMessage,\n\tICourierChannel,\n\tIRenderedTemplate,\n\tITemplateResolver,\n\tIDefaultTemplate,\n\tDefaultTemplateMap,\n} from './types';\nexport { Channel } from './config';\nexport type {\n\tICourierConfig,\n\tIEmailChannelConfig,\n\tISmsChannelConfig,\n\tIPushChannelConfig,\n} from './config';\n","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 type { IReadinessProblem } from '@fonderie/core';\nimport { validateAdminToken } from '@fonderie/core/middlewares';\nimport { DBTemplateResolver, FSTemplateResolver, DefaultTemplates } from './templates/resolver';\nimport { validateCourierConfig, collectCourierConfigProblems } from './config-guard';\nimport { handleSendGridDelivery, handleMailgunDelivery, handleMailtrapDelivery } from './delivery';\nimport { buildTemplateAdminRoutes } from './templates/admin-routes';\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\t// Report config problems for app.checkProductionReadiness() (data, not warn).\n\tcheckReadiness(): IReadinessProblem[] {\n\t\treturn [\n\t\t\t...collectCourierConfigProblems(this.config, this.dispatcher.channelNames()),\n\t\t\t// Shared admin-token strength rule (@fonderie/core/middlewares) — the\n\t\t\t// /admin/templates surface must not be guarded by a weak/placeholder token.\n\t\t\t...validateAdminToken(this.config.adminToken, { module: this.name }),\n\t\t];\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\t// Boot-time preflight: warn if any routed message type has no provider.\n\t\tvalidateCourierConfig(this.config, this.dispatcher.channelNames());\n\n\t\tconst store = this.store;\n\t\tconst delivery = this.config.delivery;\n\t\tconst signingKeys = delivery?.signingKeys;\n\n\t\t// Fail closed: a delivery route only exists when its verification key is\n\t\t// configured — an unverified endpoint would accept forged delivered/\n\t\t// opened/bounced events from anyone who finds the URL. (The handlers\n\t\t// also 401 without a key, as defense in depth.)\n\t\tif (signingKeys?.sendgrid) {\n\t\t\tapp.addRoute('POST', '/courier/delivery/sendgrid', (ctx) =>\n\t\t\t\thandleSendGridDelivery(ctx.request, store!, signingKeys.sendgrid),\n\t\t\t);\n\t\t}\n\t\tif (signingKeys?.mailgun) {\n\t\t\tapp.addRoute('POST', '/courier/delivery/mailgun', (ctx) =>\n\t\t\t\thandleMailgunDelivery(ctx.request, store!, signingKeys.mailgun),\n\t\t\t);\n\t\t}\n\t\t// Mailtrap has NO signature scheme — explicit dev/test opt-in only.\n\t\tif (delivery?.allowUnverifiedMailtrap) {\n\t\t\tapp.addRoute('POST', '/courier/delivery/mailtrap', (ctx) =>\n\t\t\t\thandleMailtrapDelivery(ctx.request, store!),\n\t\t\t);\n\t\t}\n\n\t\t// Versioned template admin routes — only when a token is configured and a\n\t\t// store is present (db templates). Mirrors @fonderie/config's admin surface.\n\t\tif (this.config.adminToken) {\n\t\t\tif (!store) {\n\t\t\t\tthrow new Error('[courier] adminToken requires @fonderie/store (db templates)');\n\t\t\t}\n\t\t\tfor (const [method, path, handler] of buildTemplateAdminRoutes(store, this.config.adminToken)) {\n\t\t\t\tapp.addRoute(method, path, handler);\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction createTemplateResolver(\n\tsource: 'db' | 'fs',\n\tconfig: ICourierConfig,\n\tstore?: IStoreAdapter,\n): ITemplateResolver {\n\t// Aggregate the module-shipped defaults (one map or an array) into a single\n\t// lookup, mirroring how getMigrationsPath() results are collected at boot.\n\tconst input = config.templates?.defaults;\n\tconst defaults = new DefaultTemplates(input ? (Array.isArray(input) ? input : [input]) : []);\n\n\tif (source === 'fs') {\n\t\treturn new FSTemplateResolver(config.templates?.directory ?? './templates', defaults);\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, defaults);\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\n// Persist the provider's message id AFTER the send (the id is only known once\n// the provider responds). Delivery webhooks match on this column — without it\n// every delivered/opened/bounced update targets zero rows.\nexport async function setMessageProviderId(\n\tid: string,\n\tproviderMessageId: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET provider_message_id = $2\n\t\t WHERE id = $1`,\n\t\t[id, providerMessageId],\n\t);\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, setMessageProviderId } 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\t// Names of the currently-registered channels — used by the boot-time config\n\t// guard to detect message types routed to a channel with no provider.\n\tchannelNames(): string[] {\n\t\treturn [...this.channels.keys()];\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\tconst result = await channel.send(message, template);\n\n\t\t\t\t\tif (this.store && logId) {\n\t\t\t\t\t\t// Persist the provider id FIRST — delivery webhooks key on it,\n\t\t\t\t\t\t// and they can arrive within seconds of the send.\n\t\t\t\t\t\tif (result?.providerMessageId) {\n\t\t\t\t\t\t\tawait setMessageProviderId(logId, result.providerMessageId, this.store).catch(\n\t\t\t\t\t\t\t\t() => undefined,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\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, ISendResult } 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<ISendResult | 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\treturn this.sendViaResend(to, template);\n\t\t}\n\t\tif (this.config.provider === 'smtp') {\n\t\t\treturn this.sendViaSMTP(to, template);\n\t\t}\n\t\tconsole.warn(`[courier:email] provider ${this.config.provider} not implemented`);\n\t}\n\n\tprivate async sendViaResend(to: string, template: IRenderedTemplate): Promise<ISendResult> {\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\n\t\t// Resend returns { id } — persist it so delivery webhooks can correlate.\n\t\tconst data = (await res.json().catch(() => null)) as { id?: string } | null;\n\t\treturn typeof data?.id === 'string' ? { providerMessageId: data.id } : {};\n\t}\n\n\tprivate async sendViaSMTP(to: string, template: IRenderedTemplate): Promise<ISendResult> {\n\t\tif (!this.transport) {\n\t\t\tthrow new Error('SMTP transport not initialised — check smtp config');\n\t\t}\n\n\t\t// Omit `html` when the template has none rather than passing `undefined`:\n\t\t// nodemailer 10's SendMailOptions is an exact-optional type, so an explicit\n\t\t// `html: undefined` is a type error (and was never meaningful at runtime).\n\t\tconst info = await 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\ttext: template.text,\n\t\t\t...(template.html !== undefined ? { html: template.html } : {}),\n\t\t});\n\n\t\t// nodemailer reports the Message-ID as \"<id@host>\"; providers' delivery\n\t\t// events reference it WITHOUT the angle brackets (e.g. Mailgun's\n\t\t// message.headers.message-id) — strip them so the lookup matches.\n\t\tconst raw = typeof info?.messageId === 'string' ? info.messageId : '';\n\t\tconst providerMessageId = raw.replace(/^<|>$/g, '');\n\t\treturn providerMessageId ? { providerMessageId } : {};\n\t}\n}\n","// Shared, composable email layout — a branded shell that wraps every template\n// body. Templates stored in the DB or on disk are **body fragments**; the\n// resolver injects them into `{{content}}` here, then interpolates `{{vars}}`\n// over the whole. One shell, many bodies — so DB and FS sources render an\n// identical, production-grade frame for free.\n//\n// Cross-client hardening (Outlook/Gmail/Apple Mail) follows the well-worn\n// \"hybrid responsive\" pattern popularised by Cerberus (MIT): a max-width table\n// container, inline styles on the critical structure, and a `<style>` block in\n// `<head>` for the friendly classes and small-screen media query. Founders can\n// override the whole shell by storing a `_layout` template (DB row or\n// `_layout.html` file) in their own source.\n//\n// The theme tokens mirror the product design system (organization/ui/base.css):\n// near-black primary, a mint brand accent, Inter, and the `#fafafa` canvas —\n// retune `EMAIL_THEME` to rebrand every email at once.\n\n// A single small token set — retune these to match your brand.\nexport const EMAIL_THEME = {\n\tbrand: 'Fonderie',\n\taccent: '#171717', // primary action (button) background — the product's primary\n\taccentText: '#ffffff', // text on the accent\n\tbrandAccent: '#171717', // brand highlight (top rule) — monochrome near-black\n\tlink: '#009767', // links — the accessible (darkened) brand teal\n\tink: '#171717', // body copy\n\tmuted: '#5c5c5c', // secondary copy\n\tline: '#e0e0e0', // hairline borders\n\tcanvas: '#fafafa', // page background behind the card\n\tcard: '#ffffff', // the card itself\n} as const;\n\n// The product font stack (Inter first, with system fallbacks for clients that\n// lack it). Shared by every text element so the frame stays consistent.\nconst FONT_SANS =\n\t\"Inter, 'Inter Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif\";\nconst FONT_MONO = \"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace\";\n\n// The `{{content}}` marker is replaced with the body fragment BEFORE variable\n// interpolation, so a body's own `{{firstName}}` etc. still resolve.\nexport const LAYOUT_CONTENT_SLOT = '{{content}}';\n\nexport const DEFAULT_EMAIL_LAYOUT = `<!DOCTYPE html>\n<html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n<head>\n\t<meta charset=\"utf-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"color-scheme\" content=\"light dark\">\n\t<meta name=\"supported-color-schemes\" content=\"light dark\">\n\t<title>{{subject}}</title>\n\t<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->\n\t<style>\n\t\tbody { margin: 0; padding: 0; width: 100% !important; background: ${EMAIL_THEME.canvas}; }\n\t\t.email-body { background: ${EMAIL_THEME.canvas}; }\n\t\t.email-container { width: 100%; max-width: 560px; margin: 0 auto; }\n\t\t.email-card {\n\t\t\tbackground: ${EMAIL_THEME.card};\n\t\t\tborder: 1px solid ${EMAIL_THEME.line};\n\t\t\tborder-radius: 8px;\n\t\t\toverflow: hidden;\n\t\t}\n\t\t.email-accent { height: 3px; background: ${EMAIL_THEME.brandAccent}; font-size: 0; line-height: 0; }\n\t\t.email-header { padding: 28px 32px 0 32px; }\n\t\t.email-brand { font: 800 19px/1 ${FONT_SANS}; color: ${EMAIL_THEME.ink}; letter-spacing: -0.03em; }\n\t\t.email-content {\n\t\t\tpadding: 20px 32px 8px 32px;\n\t\t\tfont: 400 16px/1.7 ${FONT_SANS};\n\t\t\tcolor: ${EMAIL_THEME.ink};\n\t\t\tletter-spacing: -0.01em;\n\t\t}\n\t\t.email-content h1 { margin: 0 0 12px 0; font-size: 22px; line-height: 1.3; font-weight: 700; letter-spacing: -0.03em; }\n\t\t.email-content p { margin: 0 0 16px 0; }\n\t\t.email-content a { color: ${EMAIL_THEME.link}; }\n\t\t.email-footer {\n\t\t\tpadding: 16px 32px 28px 32px;\n\t\t\tfont: 400 13px/1.6 ${FONT_SANS};\n\t\t\tcolor: ${EMAIL_THEME.muted};\n\t\t\tletter-spacing: -0.01em;\n\t\t}\n\t\t.btn {\n\t\t\tdisplay: inline-block;\n\t\t\tbackground: ${EMAIL_THEME.accent};\n\t\t\tcolor: ${EMAIL_THEME.accentText} !important;\n\t\t\ttext-decoration: none;\n\t\t\tfont: 600 16px/1 ${FONT_SANS};\n\t\t\tletter-spacing: -0.01em;\n\t\t\tpadding: 13px 26px;\n\t\t\tborder-radius: 6px;\n\t\t}\n\t\t.pin-code {\n\t\t\tdisplay: inline-block;\n\t\t\tfont: 700 30px/1 ${FONT_MONO};\n\t\t\tletter-spacing: 0.28em;\n\t\t\tcolor: ${EMAIL_THEME.ink};\n\t\t\tbackground: ${EMAIL_THEME.canvas};\n\t\t\tborder: 1px solid ${EMAIL_THEME.line};\n\t\t\tborder-radius: 6px;\n\t\t\tpadding: 14px 22px 14px 30px;\n\t\t}\n\t\t.muted { color: ${EMAIL_THEME.muted}; }\n\t\t@media only screen and (max-width: 599px) {\n\t\t\t.email-header, .email-content, .email-footer { padding-left: 22px !important; padding-right: 22px !important; }\n\t\t\t.pin-code { font-size: 26px !important; letter-spacing: 0.2em !important; }\n\t\t}\n\t</style>\n</head>\n<body class=\"email-body\">\n\t<div style=\"display:none;max-height:0;overflow:hidden;opacity:0;\">{{preheader}}</div>\n\t<table role=\"presentation\" class=\"email-body\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"background:${EMAIL_THEME.canvas};\">\n\t\t<tr>\n\t\t\t<td align=\"center\" style=\"padding: 32px 16px;\">\n\t\t\t\t<table role=\"presentation\" class=\"email-container\" width=\"560\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"max-width:560px;\">\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td class=\"email-card\" style=\"background:${EMAIL_THEME.card};border:1px solid ${EMAIL_THEME.line};border-radius:8px;\">\n\t\t\t\t\t\t\t<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n\t\t\t\t\t\t\t\t<tr><td class=\"email-accent\" style=\"height:3px;background:${EMAIL_THEME.brandAccent};font-size:0;line-height:0;\"> </td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-header\" style=\"padding:28px 32px 0 32px;\">\n\t\t\t\t\t\t\t\t\t<span class=\"email-brand\" style=\"font-weight:800;font-size:19px;color:${EMAIL_THEME.ink};letter-spacing:-0.03em;\">${EMAIL_THEME.brand}</span>\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-content\" style=\"padding:20px 32px 8px 32px;color:${EMAIL_THEME.ink};\">\n${LAYOUT_CONTENT_SLOT}\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-footer\" style=\"padding:16px 32px 28px 32px;color:${EMAIL_THEME.muted};font-size:13px;\">\n\t\t\t\t\t\t\t\t\tYou're receiving this because someone used this address at ${EMAIL_THEME.brand}. If that wasn't you, you can ignore it.\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t</table>\n\t\t\t</td>\n\t\t</tr>\n\t</table>\n</body>\n</html>`;\n\n// Inject a body fragment into a layout shell. If the body is already a full\n// HTML document (a founder chose to store a complete template), it is returned\n// untouched — never double-wrapped.\nexport function wrapLayout(bodyHtml: string, layoutHtml: string = DEFAULT_EMAIL_LAYOUT): string {\n\tconst trimmed = bodyHtml.trimStart().toLowerCase();\n\tif (trimmed.startsWith('<!doctype') || trimmed.startsWith('<html')) {\n\t\treturn bodyHtml;\n\t}\n\treturn layoutHtml.replace(LAYOUT_CONTENT_SLOT, bodyHtml);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ITemplateResolver, IRenderedTemplate, IDefaultTemplate, DefaultTemplateMap } from '../types';\nimport { wrapLayout } from './layout';\n\n// The stored template id for a founder-supplied layout shell (DB row `type` or\n// FS file `_layout.html`). Absent → the built-in DEFAULT_EMAIL_LAYOUT is used.\nconst LAYOUT_TYPE = '_layout';\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// Compose a body fragment into its layout shell, then interpolate variables\n// over the whole. `subject`/`preheader` become available to the shell's title\n// and inbox preview text.\nfunction composeHtml(\n\tbodyHtml: string,\n\tlayoutHtml: string | undefined,\n\tsubject: string | undefined,\n\tdata: Record<string, unknown>,\n): string {\n\tconst wrapped = wrapLayout(bodyHtml, layoutHtml);\n\treturn render(wrapped, { subject: subject ?? '', preheader: '', ...data });\n}\n\n// Render a resolved fragment — a DB row, an FS file set, or a MODULE DEFAULT —\n// into the final template. Factored out so a module default renders\n// byte-identically to a DB row: same {{var}} interpolation, same layout\n// composition. `text` is required; `subject`/`html` are optional (email only).\nexport function renderFragment(\n\tfrag: { subject?: string | null; text: string; html?: string | null },\n\tlayoutHtml: string | undefined,\n\tdata: Record<string, unknown>,\n): IRenderedTemplate {\n\tconst subject = frag.subject ? render(frag.subject, data) : undefined;\n\treturn {\n\t\ttext: render(frag.text, data),\n\t\t...(subject ? { subject } : {}),\n\t\t...(frag.html ? { html: composeHtml(frag.html, layoutHtml, subject, data) } : {}),\n\t};\n}\n\n// The module-shipped default templates an app hands courier, merged into one\n// lookup (aggregated like getMigrationsPath()). Per-key app overrides — a DB row\n// or an FS file — always win over a default; the default wins over the\n// last-resort JSON dump. A later map wins on key collision.\nexport class DefaultTemplates {\n\tprivate readonly map = new Map<string, IDefaultTemplate>();\n\tconstructor(maps: DefaultTemplateMap[] = []) {\n\t\tfor (const m of maps) {\n\t\t\tfor (const [key, tmpl] of Object.entries(m)) this.map.set(key, tmpl);\n\t\t}\n\t}\n\tget(type: string): IDefaultTemplate | undefined {\n\t\treturn this.map.get(type);\n\t}\n\tget size(): number {\n\t\treturn this.map.size;\n\t}\n}\n\n// DB-backed resolver — reads from fonderie_courier_templates with locale fallback\nexport class DBTemplateResolver implements ITemplateResolver {\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate defaults?: DefaultTemplates,\n\t) {}\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// Serve the exact locale, else the neutral NULL default — never a\n\t\t\t// sibling region (en-CA must not fall back to en-US); the WHERE\n\t\t\t// excludes other locales so legal/jurisdictional copy can't bleed.\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 AND (locale = $2 OR locale IS NULL)\n\t\t\t ORDER BY (locale IS NOT DISTINCT FROM $2) 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\t// No app row → fall back to the module default (rendered identically),\n\t\t\t// then to the JSON dump — now only reached for a type neither the app\n\t\t\t// nor any module provides.\n\t\t\tconst def = this.defaults?.get(type);\n\t\t\tif (def) {\n\t\t\t\treturn renderFragment(def, def.html ? await this.layout(locale) : undefined, data);\n\t\t\t}\n\t\t\treturn { text: `${type}: ${JSON.stringify(data)}` };\n\t\t}\n\n\t\tconst layoutHtml = row.html ? await this.layout(locale) : undefined;\n\t\treturn renderFragment(row, layoutHtml, data);\n\t}\n\n\t// Optional founder-supplied layout shell; undefined → built-in default.\n\tprivate async layout(locale?: string): Promise<string | undefined> {\n\t\tconst [row] = await this.store.query<{ html: string | null }>(\n\t\t\t`SELECT html\n\t\t\t FROM fonderie_courier_templates\n\t\t\t WHERE type = $1 AND active = true AND (locale = $2 OR locale IS NULL)\n\t\t\t ORDER BY (locale IS NOT DISTINCT FROM $2) DESC\n\t\t\t LIMIT 1`,\n\t\t\t[LAYOUT_TYPE, locale ?? null],\n\t\t);\n\t\treturn row?.html ?? undefined;\n\t}\n}\n\n// Filesystem resolver — reads {type}.{locale}.txt → {type}.txt with fallback\nexport class FSTemplateResolver implements ITemplateResolver {\n\tconstructor(\n\t\tprivate directory: string,\n\t\tprivate defaults?: DefaultTemplates,\n\t) {}\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\t\tconst layoutPrefix = locale ? `${LAYOUT_TYPE}.${locale}` : null;\n\n\t\tconst [text, html, subject, layout] = 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\n\t\t\tlayoutPrefix\n\t\t\t\t? readOptional(join(this.directory, `${layoutPrefix}.html`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${LAYOUT_TYPE}.html`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${LAYOUT_TYPE}.html`)),\n\t\t]);\n\n\t\t// App shipped NOTHING for this type (no text/html/subject file) → fall back\n\t\t// to the module default, using the app's own _layout shell if it ships one,\n\t\t// then to the JSON dump. Test file PRESENCE (=== null), not truthiness, so an\n\t\t// app that ships even an empty file keeps per-key control below — mirroring\n\t\t// the DB resolver's row-presence check, not overriding it with a default.\n\t\tif (text === null && html === null && subject === null) {\n\t\t\tconst def = this.defaults?.get(type);\n\t\t\tif (def) {\n\t\t\t\treturn renderFragment(def, def.html ? (layout ?? undefined) : undefined, data);\n\t\t\t}\n\t\t\treturn { text: `${type}: ${JSON.stringify(data)}` };\n\t\t}\n\n\t\tconst renderedSubject = subject ? render(subject, data) : undefined;\n\n\t\treturn {\n\t\t\ttext: text ? render(text, data) : `${type}: ${JSON.stringify(data)}`,\n\t\t\t...(renderedSubject ? { subject: renderedSubject } : {}),\n\t\t\t...(html ? { html: composeHtml(html, layout ?? undefined, renderedSubject, data) } : {}),\n\t\t};\n\t}\n}\n","import type { IReadinessProblem } from '@fonderie/core';\nimport type { ICourierConfig } from './config';\n\nconst MODULE = '@fonderie/courier';\n\n// Pure production-readiness assessment (no side effects) — shared by the\n// boot-time guard and `CourierModule.checkReadiness`. A message type routed to a\n// channel with no registered provider is silently dropped by the dispatcher —\n// with `requireVerification` on, verification / reset emails vanish and users\n// lock themselves out. These are `warning`-severity (channel setups vary\n// legitimately — an app may run in-app only — so this never blocks boot, unlike\n// auth's fatal weak-secret). Checks the channels actually **registered** on the\n// dispatcher, so channels added via `registerChannel` count as present.\nexport function collectCourierConfigProblems(\n\tconfig: ICourierConfig,\n\tregisteredChannels: Iterable<string>,\n): IReadinessProblem[] {\n\tconst registered = new Set(registeredChannels);\n\tconst gaps = new Map<string, string[]>(); // channel → message types routed to it\n\n\tfor (const [type, channels] of Object.entries(config.channels ?? {})) {\n\t\tfor (const channel of channels) {\n\t\t\tif (!registered.has(channel)) {\n\t\t\t\tconst types = gaps.get(channel) ?? [];\n\t\t\t\ttypes.push(type);\n\t\t\t\tgaps.set(channel, types);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn [...gaps].map(([channel, types]) => ({\n\t\tmodule: MODULE,\n\t\tseverity: 'warning' as const,\n\t\tmessage:\n\t\t\t`${types.length} message type(s) route to the '${channel}' channel but ` +\n\t\t\t`no '${channel}' provider is registered — these will be silently dropped: ` +\n\t\t\t`${types.join(', ')}. Configure \\`config.${channel}\\` (or register a channel).`,\n\t}));\n}\n\n// Boot-time preflight, run from `CourierModule.install`. Warn-only.\nexport function validateCourierConfig(\n\tconfig: ICourierConfig,\n\tregisteredChannels: Iterable<string>,\n): void {\n\tfor (const problem of collectCourierConfigProblems(config, registeredChannels)) {\n\t\tconsole.warn(`[courier] ${problem.message}`);\n\t}\n}\n","import { createHmac, createPublicKey, verify as cryptoVerify } from 'node:crypto';\n\nimport { constantTimeEqual } from '@fonderie/core';\n\nimport type { IStoreAdapter } from '@fonderie/store';\nimport {\n\tmarkMessageDelivered,\n\tmarkMessageOpened,\n\tmarkMessageClicked,\n\tmarkMessageBounced,\n} from './log';\n\n// Delivery webhooks are FAIL-CLOSED: every handler requires its verification\n// key and rejects unverified payloads with 401. Without verification, anyone\n// who finds the endpoint can forge delivered/opened/bounced events — poisoning\n// the message log and (via bounce handling) suppressing real mail. The module\n// additionally only registers a delivery route when its key is configured.\n\n// ── SendGrid ──────────────────────────────────────────────────────\n//\n// Verifies the X-Twilio-Email-Event-Webhook-Signature header. SendGrid signs\n// with ECDSA (P-256 over `timestamp + body`, base64 DER signature) — NOT HMAC;\n// `publicKey` is the base64 \"Verification Key\" from the SendGrid dashboard.\n// Expects an array of event objects in the request body.\n// https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/getting-started-event-webhook-security-features\n\nexport async function handleSendGridDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n\tpublicKey?: string,\n): Promise<Response> {\n\tif (!publicKey) {\n\t\treturn Response.json({ error: 'VERIFICATION_NOT_CONFIGURED' }, { status: 401 });\n\t}\n\n\tconst sig = req.headers.get('x-twilio-email-event-webhook-signature') ?? '';\n\tconst ts = req.headers.get('x-twilio-email-event-webhook-timestamp') ?? '';\n\tconst body = await req.text();\n\n\tif (!verifySendGridSignature(publicKey, ts, body, sig)) {\n\t\treturn Response.json({ error: 'INVALID_SIGNATURE' }, { status: 401 });\n\t}\n\n\tconst events = parseJson(body) as SendGridEvent[] | null;\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\tpublicKeyB64: string,\n\ttimestamp: string,\n\tbody: string,\n\tsignature: string,\n): boolean {\n\ttry {\n\t\tconst key = createPublicKey({\n\t\t\tkey: Buffer.from(publicKeyB64, 'base64'),\n\t\t\tformat: 'der',\n\t\t\ttype: 'spki',\n\t\t});\n\t\treturn cryptoVerify(\n\t\t\t'sha256',\n\t\t\tBuffer.from(timestamp + body),\n\t\t\tkey,\n\t\t\tBuffer.from(signature, 'base64'),\n\t\t);\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\tif (!signingKey) {\n\t\treturn Response.json({ error: 'VERIFICATION_NOT_CONFIGURED' }, { status: 401 });\n\t}\n\n\tconst body = (await req.json()) as MailgunPayload;\n\n\tconst { signature } = body;\n\tif (!signature || !verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {\n\t\treturn Response.json({ error: 'INVALID_SIGNATURE' }, { status: 401 });\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\treturn constantTimeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'));\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 — has NO signature scheme) ────────────\n//\n// Registered only when config.delivery.allowUnverifiedMailtrap is explicitly\n// true: with no signature to verify, the route accepts forged events by\n// construction, so it must be a deliberate dev/test opt-in, never a default.\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","import type { IFonderieContext, Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport { requireAdminToken } from '@fonderie/core/middlewares';\nimport { VersionConflictError } from '@fonderie/store';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport {\n\tsetTemplate,\n\trollbackTemplate,\n\tlistTemplateRevisions,\n\tgetTemplateEntry,\n\tlistTemplateEntries,\n\tdeleteTemplate,\n} from './admin';\n\n// Bearer-token guard (mirrors @fonderie/config's admin surface). Only registered\n// when a token is configured — no token, no exposed template admin routes.\n// Wrap a handler in the shared admin-token guard (@fonderie/core/middlewares) —\n// one constant-time Bearer check across billing/config/courier.\nfunction guarded(adminToken: string, handler: Middleware): Middleware {\n\tconst guard = requireAdminToken(adminToken);\n\treturn (ctx, next) => guard(ctx, () => handler(ctx, next));\n}\n\nconst actorOf = (ctx: IFonderieContext) => ctx.request.headers.get('x-actor') || 'admin-token';\nconst typeOf = (ctx: IFonderieContext) => ctx.meta.params?.['type'] ?? '';\nconst localeOf = (ctx: IFonderieContext): string | null =>\n\tnew URL(ctx.request.url).searchParams.get('locale');\nconst body = (ctx: IFonderieContext): Record<string, unknown> =>\n\t(ctx.meta['body'] as Record<string, unknown> | undefined) ?? {};\n\nfunction conflictOr(err: unknown): Response {\n\tif (err instanceof VersionConflictError) {\n\t\treturn setApiResponse(HTTP.CONFLICT, 'VERSION_CONFLICT', err.message, {\n\t\t\tcurrentVersion: err.currentVersion,\n\t\t});\n\t}\n\tthrow err;\n}\n\n// Build the template admin route table (registered by CourierModule.install when\n// an adminToken is configured).\nexport function buildTemplateAdminRoutes(\n\tstore: IStoreAdapter,\n\tadminToken: string,\n): Array<[string, string, Middleware]> {\n\tconst g = (h: Middleware): Middleware => guarded(adminToken, h);\n\n\treturn [\n\t\t['GET', '/admin/templates', g(async () => {\n\t\t\treturn setApiResponse(HTTP.OK, 'TEMPLATES_LISTED', 'Templates', await listTemplateEntries(store));\n\t\t})],\n\t\t['GET', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst row = await getTemplateEntry(typeOf(ctx), localeOf(ctx), store);\n\t\t\treturn row\n\t\t\t\t? setApiResponse(HTTP.OK, 'TEMPLATE', 'Template', row)\n\t\t\t\t: setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such template');\n\t\t})],\n\t\t['PUT', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tif (typeof b['text'] !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.text (string) is required');\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst opts: Parameters<typeof setTemplate>[0] = {\n\t\t\t\t\ttype: typeOf(ctx),\n\t\t\t\t\ttext: b['text'],\n\t\t\t\t\tlocale: localeOf(ctx),\n\t\t\t\t\tactor: actorOf(ctx),\n\t\t\t\t};\n\t\t\t\tif (typeof b['subject'] === 'string') opts.subject = b['subject'];\n\t\t\t\tif (typeof b['html'] === 'string') opts.html = b['html'];\n\t\t\t\tif (typeof b['active'] === 'boolean') opts.active = b['active'];\n\t\t\t\tif (typeof b['ifVersion'] === 'number') opts.ifVersion = b['ifVersion'];\n\t\t\t\treturn setApiResponse(HTTP.OK, 'TEMPLATE_SET', 'Template saved', await setTemplate(opts, store));\n\t\t\t} catch (err) {\n\t\t\t\treturn conflictOr(err);\n\t\t\t}\n\t\t})],\n\t\t['DELETE', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst ok = await deleteTemplate(typeOf(ctx), localeOf(ctx), store);\n\t\t\treturn setApiResponse(ok ? HTTP.OK : HTTP.NOT_FOUND, ok ? 'DELETED' : 'NOT_FOUND', ok ? 'Deleted' : 'No such template');\n\t\t})],\n\t\t['GET', '/admin/templates/:type/revisions', g(async (ctx) => {\n\t\t\treturn setApiResponse(HTTP.OK, 'REVISIONS', 'Template revisions', await listTemplateRevisions(typeOf(ctx), localeOf(ctx), store));\n\t\t})],\n\t\t['POST', '/admin/templates/:type/rollback', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tconst toVersion = Number(b['toVersion']);\n\t\t\tif (!Number.isInteger(toVersion)) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.toVersion (int) is required');\n\t\t\t}\n\t\t\tconst row = await rollbackTemplate(\n\t\t\t\t{ type: typeOf(ctx), locale: localeOf(ctx), toVersion, actor: actorOf(ctx) },\n\t\t\t\tstore,\n\t\t\t);\n\t\t\treturn setApiResponse(HTTP.OK, 'ROLLED_BACK', `Rolled back to v${toVersion}`, row);\n\t\t})],\n\t];\n}\n","import type { IStoreAdapter, IVersionedResource } from '@fonderie/store';\nimport { versionedWrite, versionedRollback } from '@fonderie/store';\n\n// Versioned management for email templates, on the shared `@fonderie/store`\n// primitive. Templates are keyed by (type, locale) — a NULL locale is the base,\n// with per-locale overrides on top — and their content is subject/html/text.\n// The resolver reads the current row unchanged; this adds edit + history +\n// rollback + optimistic concurrency on top.\n\nexport interface ITemplateEntry {\n\ttype: string;\n\tlocale: string | null;\n\tsubject: string | null;\n\thtml: string | null;\n\ttext: string;\n\tactive: boolean;\n\tversion: number;\n\tupdatedBy: string | null;\n\tupdatedAt: string;\n}\n\nexport interface ITemplateRevision {\n\ttype: string;\n\tlocale: string | null;\n\tsubject: string | null;\n\thtml: string | null;\n\ttext: string;\n\tversion: number;\n\tactor: string | null;\n\tcreatedAt: string;\n}\n\nconst ENTRY_COLS = `type, locale, subject, html, text, active, version, updated_by AS \"updatedBy\", updated_at AS \"updatedAt\"`;\n\nconst TEMPLATE_RESOURCE: IVersionedResource = {\n\ttable: 'fonderie_courier_templates',\n\trevisions: 'fonderie_courier_template_revisions',\n\tchannel: 'fonderie_courier_templates_changed',\n\tkeyColumns: ['type', 'locale'],\n\tcontentColumns: ['subject', 'html', 'text'],\n\tmetaColumns: ['active'],\n\treturning: ENTRY_COLS,\n};\n\n// Upsert a template (full body — a revision is a full snapshot). Pass `ifVersion`\n// for optimistic concurrency (else `VersionConflictError`). `locale` omitted /\n// null targets the base template.\nexport async function setTemplate(\n\topts: {\n\t\ttype: string;\n\t\ttext: string;\n\t\tlocale?: string | null;\n\t\tsubject?: string | null;\n\t\thtml?: string | null;\n\t\tactive?: boolean;\n\t\tifVersion?: number;\n\t\tactor?: string;\n\t},\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry> {\n\tconst data: Record<string, unknown> = {\n\t\tsubject: opts.subject ?? null,\n\t\thtml: opts.html ?? null,\n\t\ttext: opts.text,\n\t};\n\tif (opts.active !== undefined) data['active'] = opts.active;\n\treturn versionedWrite<ITemplateEntry>(TEMPLATE_RESOURCE, store, {\n\t\tkey: opts.type,\n\t\tscope: opts.locale ?? null,\n\t\tdata,\n\t\t...(opts.ifVersion !== undefined ? { ifVersion: opts.ifVersion } : {}),\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function rollbackTemplate(\n\topts: { type: string; locale?: string | null; toVersion: number; actor?: string },\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry> {\n\treturn versionedRollback<ITemplateEntry>(TEMPLATE_RESOURCE, store, {\n\t\tkey: opts.type,\n\t\tscope: opts.locale ?? null,\n\t\ttoVersion: opts.toVersion,\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function listTemplateRevisions(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<ITemplateRevision[]> {\n\treturn store.query<ITemplateRevision>(\n\t\t`SELECT type, locale, subject, html, text, version, actor, created_at AS \"createdAt\"\n\t\t FROM fonderie_courier_template_revisions\n\t\t WHERE type = $1 AND locale IS NOT DISTINCT FROM $2\n\t\t ORDER BY version DESC`,\n\t\t[type, locale],\n\t);\n}\n\nexport async function getTemplateEntry(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry | null> {\n\tconst [row] = await store.query<ITemplateEntry>(\n\t\t`SELECT ${ENTRY_COLS} FROM fonderie_courier_templates WHERE type = $1 AND locale IS NOT DISTINCT FROM $2`,\n\t\t[type, locale],\n\t);\n\treturn row ?? null;\n}\n\nexport async function listTemplateEntries(store: IStoreAdapter): Promise<ITemplateEntry[]> {\n\treturn store.query<ITemplateEntry>(\n\t\t`SELECT ${ENTRY_COLS} FROM fonderie_courier_templates ORDER BY type, locale NULLS FIRST`,\n\t);\n}\n\nexport async function deleteTemplate(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst rows = await store.query<{ type: string }>(\n\t\t`DELETE FROM fonderie_courier_templates WHERE type = $1 AND locale IS NOT DISTINCT FROM $2 RETURNING type`,\n\t\t[type, locale],\n\t);\n\treturn rows.length > 0;\n}\n","import type { DefaultTemplateMap } from './types';\n\nexport 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// When set, exposes Bearer-guarded template admin routes (/admin/templates/*)\n\t// for versioned edit/history/rollback. Requires @fonderie/store (db templates).\n\tadminToken?: string;\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\t// Module-shipped default templates. The app imports each module's\n\t\t// DEFAULT_TEMPLATES (auth, workspaces, billing, …) and passes them here;\n\t\t// the resolver falls back to a default when no app override (DB row / FS\n\t\t// file) exists, before the last-resort JSON dump. Aggregated like\n\t\t// getMigrationsPath() — pass one map or an array of them.\n\t\tdefaults?: DefaultTemplateMap | DefaultTemplateMap[];\n\t};\n\n\t// Inbound delivery-webhook verification. FAIL-CLOSED: a provider's route is\n\t// only registered when its key is set (an unverified endpoint would accept\n\t// forged delivered/opened/bounced events from anyone who finds the URL).\n\tdelivery?: {\n\t\tsigningKeys?: {\n\t\t\t// SendGrid signs with ECDSA, not HMAC: this is the base64\n\t\t\t// \"Verification Key\" (public key) from Settings → Mail Settings →\n\t\t\t// Event Webhook → Signature Verification.\n\t\t\tsendgrid?: string;\n\t\t\tmailgun?: string; // Mailgun HTTP webhook signing key (HMAC-SHA256)\n\t\t};\n\t\t// Mailtrap's webhook has NO signature scheme, so its route accepts\n\t\t// forged events by construction. Explicit dev/test opt-in only.\n\t\tallowUnverifiedMailtrap?: boolean;\n\t};\n}\n"],"mappings":"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","names":["body","body","nodemailer","body","import_middlewares","body","cryptoVerify","import_core","import_store"]}
|