@fonderie/courier 7.0.0 → 7.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +14 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -734,6 +734,17 @@ async function processSendGridEvents(events, store) {
|
|
|
734
734
|
}
|
|
735
735
|
}
|
|
736
736
|
}
|
|
737
|
+
var seenMailgunTokens = /* @__PURE__ */ new Map();
|
|
738
|
+
function rememberMailgunToken(token) {
|
|
739
|
+
const now = Date.now();
|
|
740
|
+
if (seenMailgunTokens.size > 4096) {
|
|
741
|
+
for (const [t, exp] of seenMailgunTokens) if (exp <= now) seenMailgunTokens.delete(t);
|
|
742
|
+
}
|
|
743
|
+
const existing = seenMailgunTokens.get(token);
|
|
744
|
+
if (existing !== void 0 && existing > now) return false;
|
|
745
|
+
seenMailgunTokens.set(token, now + TIMESTAMP_TOLERANCE_S * 1e3);
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
737
748
|
async function handleMailgunDelivery(req, store, signingKey) {
|
|
738
749
|
if (!signingKey) {
|
|
739
750
|
return Response.json({ error: "VERIFICATION_NOT_CONFIGURED" }, { status: 401 });
|
|
@@ -746,6 +757,9 @@ async function handleMailgunDelivery(req, store, signingKey) {
|
|
|
746
757
|
if (!verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {
|
|
747
758
|
return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
|
|
748
759
|
}
|
|
760
|
+
if (!rememberMailgunToken(signature.token)) {
|
|
761
|
+
return Response.json({ error: "REPLAYED_TOKEN" }, { status: 401 });
|
|
762
|
+
}
|
|
749
763
|
const event = body2["event-data"];
|
|
750
764
|
if (event) {
|
|
751
765
|
await processMailgunEvent(event, store);
|
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 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\n// HTML-entity-escape interpolated VALUES in html templates. Template markup\n// itself is trusted (authored by the app/module); the DATA is not — e.g.\n// {{firstName}} is registration-controlled, and unescaped it lets a user\n// inject markup/links into platform-branded emails (phishing content with the\n// platform's own sender reputation). Text/subject parts stay raw: they are\n// not HTML contexts.\nfunction escapeHtml(value: string): string {\n\treturn value\n\t\t.replace(/&/g, '&')\n\t\t.replace(/</g, '<')\n\t\t.replace(/>/g, '>')\n\t\t.replace(/\"/g, '"')\n\t\t.replace(/'/g, ''');\n}\n\nfunction render(\n\ttemplate: string,\n\tdata: Record<string, unknown>,\n\topts: { escapeHtml?: boolean } = {},\n): string {\n\treturn template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) => {\n\t\tconst value = data[key];\n\t\tif (value === undefined || value === null) return '';\n\t\tconst s = String(value);\n\t\treturn opts.escapeHtml ? escapeHtml(s) : s;\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 }, { escapeHtml: true });\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// Signed timestamps must also be FRESH: both providers sign a timestamp, and\n// without a freshness window a captured payload replays forever — and\n// Mailgun's scheme signs only timestamp+token (never the body), so a stale\n// signature could be replayed with a FORGED body.\n\nconst TIMESTAMP_TOLERANCE_S = 5 * 60;\n\nfunction isFreshTimestamp(unixSeconds: string): boolean {\n\tconst ts = Number(unixSeconds);\n\tif (!Number.isFinite(ts)) return false;\n\treturn Math.abs(Date.now() / 1000 - ts) <= TIMESTAMP_TOLERANCE_S;\n}\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 (!isFreshTimestamp(ts)) {\n\t\treturn Response.json({ error: 'STALE_TIMESTAMP' }, { status: 401 });\n\t}\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 || !isFreshTimestamp(signature.timestamp)) {\n\t\treturn Response.json({ error: 'STALE_TIMESTAMP' }, { status: 401 });\n\t}\n\tif (!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"]}
|
|
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\n// HTML-entity-escape interpolated VALUES in html templates. Template markup\n// itself is trusted (authored by the app/module); the DATA is not — e.g.\n// {{firstName}} is registration-controlled, and unescaped it lets a user\n// inject markup/links into platform-branded emails (phishing content with the\n// platform's own sender reputation). Text/subject parts stay raw: they are\n// not HTML contexts.\n//\n// SCOPE: this escaping is correct for element content and QUOTED attribute\n// values (it escapes & < > \" '). It is NOT a URL/scheme sanitizer and does not\n// cover UNQUOTED attributes. A template must therefore never interpolate a\n// value into an unquoted attribute, and any `<a href=\"{{url}}\">` link template\n// must validate the scheme itself (allow http/https only) — a `{{url}}` of\n// `javascript:…` passes this escaper unchanged. No shipped module template\n// interpolates into an attribute; this is a guardrail for app authors.\nfunction escapeHtml(value: string): string {\n\treturn value\n\t\t.replace(/&/g, '&')\n\t\t.replace(/</g, '<')\n\t\t.replace(/>/g, '>')\n\t\t.replace(/\"/g, '"')\n\t\t.replace(/'/g, ''');\n}\n\nfunction render(\n\ttemplate: string,\n\tdata: Record<string, unknown>,\n\topts: { escapeHtml?: boolean } = {},\n): string {\n\treturn template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) => {\n\t\tconst value = data[key];\n\t\tif (value === undefined || value === null) return '';\n\t\tconst s = String(value);\n\t\treturn opts.escapeHtml ? escapeHtml(s) : s;\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 }, { escapeHtml: true });\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// Signed timestamps must also be FRESH: both providers sign a timestamp, and\n// without a freshness window a captured payload replays forever — and\n// Mailgun's scheme signs only timestamp+token (never the body), so a stale\n// signature could be replayed with a FORGED body.\n\nconst TIMESTAMP_TOLERANCE_S = 5 * 60;\n\nfunction isFreshTimestamp(unixSeconds: string): boolean {\n\tconst ts = Number(unixSeconds);\n\tif (!Number.isFinite(ts)) return false;\n\treturn Math.abs(Date.now() / 1000 - ts) <= TIMESTAMP_TOLERANCE_S;\n}\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 (!isFreshTimestamp(ts)) {\n\t\treturn Response.json({ error: 'STALE_TIMESTAMP' }, { status: 401 });\n\t}\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\n// Mailgun signs only timestamp+token (NOT the body), so within the freshness\n// window a captured signature could be replayed with a forged body. We record\n// each accepted token until its freshness window elapses and reject a reuse.\n// NOTE: this is per-PROCESS (in-memory) — a multi-instance deployment behind a\n// load balancer only gets same-instance protection; the ±window bound still\n// caps the exposure everywhere. Impact of a replay is log/analytics integrity\n// only (no send-time suppression gate consumes these events).\nconst seenMailgunTokens = new Map<string, number>();\n\nfunction rememberMailgunToken(token: string): boolean {\n\tconst now = Date.now();\n\t// Opportunistic prune so the map can't grow unbounded.\n\tif (seenMailgunTokens.size > 4096) {\n\t\tfor (const [t, exp] of seenMailgunTokens) if (exp <= now) seenMailgunTokens.delete(t);\n\t}\n\tconst existing = seenMailgunTokens.get(token);\n\tif (existing !== undefined && existing > now) return false; // replay within window\n\tseenMailgunTokens.set(token, now + TIMESTAMP_TOLERANCE_S * 1000);\n\treturn true;\n}\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 || !isFreshTimestamp(signature.timestamp)) {\n\t\treturn Response.json({ error: 'STALE_TIMESTAMP' }, { status: 401 });\n\t}\n\tif (!verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {\n\t\treturn Response.json({ error: 'INVALID_SIGNATURE' }, { status: 401 });\n\t}\n\tif (!rememberMailgunToken(signature.token)) {\n\t\treturn Response.json({ error: 'REPLAYED_TOKEN' }, { 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"]}
|
package/dist/index.js
CHANGED
|
@@ -678,6 +678,17 @@ async function processSendGridEvents(events, store) {
|
|
|
678
678
|
}
|
|
679
679
|
}
|
|
680
680
|
}
|
|
681
|
+
var seenMailgunTokens = /* @__PURE__ */ new Map();
|
|
682
|
+
function rememberMailgunToken(token) {
|
|
683
|
+
const now = Date.now();
|
|
684
|
+
if (seenMailgunTokens.size > 4096) {
|
|
685
|
+
for (const [t, exp] of seenMailgunTokens) if (exp <= now) seenMailgunTokens.delete(t);
|
|
686
|
+
}
|
|
687
|
+
const existing = seenMailgunTokens.get(token);
|
|
688
|
+
if (existing !== void 0 && existing > now) return false;
|
|
689
|
+
seenMailgunTokens.set(token, now + TIMESTAMP_TOLERANCE_S * 1e3);
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
681
692
|
async function handleMailgunDelivery(req, store, signingKey) {
|
|
682
693
|
if (!signingKey) {
|
|
683
694
|
return Response.json({ error: "VERIFICATION_NOT_CONFIGURED" }, { status: 401 });
|
|
@@ -690,6 +701,9 @@ async function handleMailgunDelivery(req, store, signingKey) {
|
|
|
690
701
|
if (!verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {
|
|
691
702
|
return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
|
|
692
703
|
}
|
|
704
|
+
if (!rememberMailgunToken(signature.token)) {
|
|
705
|
+
return Response.json({ error: "REPLAYED_TOKEN" }, { status: 401 });
|
|
706
|
+
}
|
|
693
707
|
const event = body2["event-data"];
|
|
694
708
|
if (event) {
|
|
695
709
|
await processMailgunEvent(event, store);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/module.ts","../src/log.ts","../src/dispatcher.ts","../src/channels/sms.ts","../src/channels/push.ts","../src/channels/email.ts","../src/templates/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":["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\n// HTML-entity-escape interpolated VALUES in html templates. Template markup\n// itself is trusted (authored by the app/module); the DATA is not — e.g.\n// {{firstName}} is registration-controlled, and unescaped it lets a user\n// inject markup/links into platform-branded emails (phishing content with the\n// platform's own sender reputation). Text/subject parts stay raw: they are\n// not HTML contexts.\nfunction escapeHtml(value: string): string {\n\treturn value\n\t\t.replace(/&/g, '&')\n\t\t.replace(/</g, '<')\n\t\t.replace(/>/g, '>')\n\t\t.replace(/\"/g, '"')\n\t\t.replace(/'/g, ''');\n}\n\nfunction render(\n\ttemplate: string,\n\tdata: Record<string, unknown>,\n\topts: { escapeHtml?: boolean } = {},\n): string {\n\treturn template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) => {\n\t\tconst value = data[key];\n\t\tif (value === undefined || value === null) return '';\n\t\tconst s = String(value);\n\t\treturn opts.escapeHtml ? escapeHtml(s) : s;\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 }, { escapeHtml: true });\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// Signed timestamps must also be FRESH: both providers sign a timestamp, and\n// without a freshness window a captured payload replays forever — and\n// Mailgun's scheme signs only timestamp+token (never the body), so a stale\n// signature could be replayed with a FORGED body.\n\nconst TIMESTAMP_TOLERANCE_S = 5 * 60;\n\nfunction isFreshTimestamp(unixSeconds: string): boolean {\n\tconst ts = Number(unixSeconds);\n\tif (!Number.isFinite(ts)) return false;\n\treturn Math.abs(Date.now() / 1000 - ts) <= TIMESTAMP_TOLERANCE_S;\n}\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 (!isFreshTimestamp(ts)) {\n\t\treturn Response.json({ error: 'STALE_TIMESTAMP' }, { status: 401 });\n\t}\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 || !isFreshTimestamp(signature.timestamp)) {\n\t\treturn Response.json({ error: 'STALE_TIMESTAMP' }, { status: 401 });\n\t}\n\tif (!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","body","body"]}
|
|
1
|
+
{"version":3,"sources":["../src/module.ts","../src/log.ts","../src/dispatcher.ts","../src/channels/sms.ts","../src/channels/push.ts","../src/channels/email.ts","../src/templates/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":["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\n// HTML-entity-escape interpolated VALUES in html templates. Template markup\n// itself is trusted (authored by the app/module); the DATA is not — e.g.\n// {{firstName}} is registration-controlled, and unescaped it lets a user\n// inject markup/links into platform-branded emails (phishing content with the\n// platform's own sender reputation). Text/subject parts stay raw: they are\n// not HTML contexts.\n//\n// SCOPE: this escaping is correct for element content and QUOTED attribute\n// values (it escapes & < > \" '). It is NOT a URL/scheme sanitizer and does not\n// cover UNQUOTED attributes. A template must therefore never interpolate a\n// value into an unquoted attribute, and any `<a href=\"{{url}}\">` link template\n// must validate the scheme itself (allow http/https only) — a `{{url}}` of\n// `javascript:…` passes this escaper unchanged. No shipped module template\n// interpolates into an attribute; this is a guardrail for app authors.\nfunction escapeHtml(value: string): string {\n\treturn value\n\t\t.replace(/&/g, '&')\n\t\t.replace(/</g, '<')\n\t\t.replace(/>/g, '>')\n\t\t.replace(/\"/g, '"')\n\t\t.replace(/'/g, ''');\n}\n\nfunction render(\n\ttemplate: string,\n\tdata: Record<string, unknown>,\n\topts: { escapeHtml?: boolean } = {},\n): string {\n\treturn template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) => {\n\t\tconst value = data[key];\n\t\tif (value === undefined || value === null) return '';\n\t\tconst s = String(value);\n\t\treturn opts.escapeHtml ? escapeHtml(s) : s;\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 }, { escapeHtml: true });\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// Signed timestamps must also be FRESH: both providers sign a timestamp, and\n// without a freshness window a captured payload replays forever — and\n// Mailgun's scheme signs only timestamp+token (never the body), so a stale\n// signature could be replayed with a FORGED body.\n\nconst TIMESTAMP_TOLERANCE_S = 5 * 60;\n\nfunction isFreshTimestamp(unixSeconds: string): boolean {\n\tconst ts = Number(unixSeconds);\n\tif (!Number.isFinite(ts)) return false;\n\treturn Math.abs(Date.now() / 1000 - ts) <= TIMESTAMP_TOLERANCE_S;\n}\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 (!isFreshTimestamp(ts)) {\n\t\treturn Response.json({ error: 'STALE_TIMESTAMP' }, { status: 401 });\n\t}\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\n// Mailgun signs only timestamp+token (NOT the body), so within the freshness\n// window a captured signature could be replayed with a forged body. We record\n// each accepted token until its freshness window elapses and reject a reuse.\n// NOTE: this is per-PROCESS (in-memory) — a multi-instance deployment behind a\n// load balancer only gets same-instance protection; the ±window bound still\n// caps the exposure everywhere. Impact of a replay is log/analytics integrity\n// only (no send-time suppression gate consumes these events).\nconst seenMailgunTokens = new Map<string, number>();\n\nfunction rememberMailgunToken(token: string): boolean {\n\tconst now = Date.now();\n\t// Opportunistic prune so the map can't grow unbounded.\n\tif (seenMailgunTokens.size > 4096) {\n\t\tfor (const [t, exp] of seenMailgunTokens) if (exp <= now) seenMailgunTokens.delete(t);\n\t}\n\tconst existing = seenMailgunTokens.get(token);\n\tif (existing !== undefined && existing > now) return false; // replay within window\n\tseenMailgunTokens.set(token, now + TIMESTAMP_TOLERANCE_S * 1000);\n\treturn true;\n}\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 || !isFreshTimestamp(signature.timestamp)) {\n\t\treturn Response.json({ error: 'STALE_TIMESTAMP' }, { status: 401 });\n\t}\n\tif (!verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {\n\t\treturn Response.json({ error: 'INVALID_SIGNATURE' }, { status: 401 });\n\t}\n\tif (!rememberMailgunToken(signature.token)) {\n\t\treturn Response.json({ error: 'REPLAYED_TOKEN' }, { 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","body","body"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fonderie/courier",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.1",
|
|
4
4
|
"description": "Transactional messaging for SaaS — multi-channel delivery (email, SMS, push) with FS or DB templates, per-message-type channel routing, and a persistent message log.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fonderiejs",
|