@fonderie/courier 6.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 CHANGED
@@ -513,15 +513,20 @@ function wrapLayout(bodyHtml, layoutHtml = DEFAULT_EMAIL_LAYOUT) {
513
513
 
514
514
  // src/templates/resolver.ts
515
515
  var LAYOUT_TYPE = "_layout";
516
- function render(template, data) {
516
+ function escapeHtml(value) {
517
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
518
+ }
519
+ function render(template, data, opts = {}) {
517
520
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
518
521
  const value = data[key];
519
- return value !== void 0 && value !== null ? String(value) : "";
522
+ if (value === void 0 || value === null) return "";
523
+ const s = String(value);
524
+ return opts.escapeHtml ? escapeHtml(s) : s;
520
525
  });
521
526
  }
522
527
  function composeHtml(bodyHtml, layoutHtml, subject, data) {
523
528
  const wrapped = wrapLayout(bodyHtml, layoutHtml);
524
- return render(wrapped, { subject: subject ?? "", preheader: "", ...data });
529
+ return render(wrapped, { subject: subject ?? "", preheader: "", ...data }, { escapeHtml: true });
525
530
  }
526
531
  function renderFragment(frag, layoutHtml, data) {
527
532
  const subject = frag.subject ? render(frag.subject, data) : void 0;
@@ -665,6 +670,12 @@ function validateCourierConfig(config, registeredChannels) {
665
670
  // src/delivery.ts
666
671
  var import_node_crypto = require("crypto");
667
672
  var import_core = require("@fonderie/core");
673
+ var TIMESTAMP_TOLERANCE_S = 5 * 60;
674
+ function isFreshTimestamp(unixSeconds) {
675
+ const ts = Number(unixSeconds);
676
+ if (!Number.isFinite(ts)) return false;
677
+ return Math.abs(Date.now() / 1e3 - ts) <= TIMESTAMP_TOLERANCE_S;
678
+ }
668
679
  async function handleSendGridDelivery(req, store, publicKey) {
669
680
  if (!publicKey) {
670
681
  return Response.json({ error: "VERIFICATION_NOT_CONFIGURED" }, { status: 401 });
@@ -672,6 +683,9 @@ async function handleSendGridDelivery(req, store, publicKey) {
672
683
  const sig = req.headers.get("x-twilio-email-event-webhook-signature") ?? "";
673
684
  const ts = req.headers.get("x-twilio-email-event-webhook-timestamp") ?? "";
674
685
  const body2 = await req.text();
686
+ if (!isFreshTimestamp(ts)) {
687
+ return Response.json({ error: "STALE_TIMESTAMP" }, { status: 401 });
688
+ }
675
689
  if (!verifySendGridSignature(publicKey, ts, body2, sig)) {
676
690
  return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
677
691
  }
@@ -720,15 +734,32 @@ async function processSendGridEvents(events, store) {
720
734
  }
721
735
  }
722
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
+ }
723
748
  async function handleMailgunDelivery(req, store, signingKey) {
724
749
  if (!signingKey) {
725
750
  return Response.json({ error: "VERIFICATION_NOT_CONFIGURED" }, { status: 401 });
726
751
  }
727
752
  const body2 = await req.json();
728
753
  const { signature } = body2;
729
- if (!signature || !verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {
754
+ if (!signature || !isFreshTimestamp(signature.timestamp)) {
755
+ return Response.json({ error: "STALE_TIMESTAMP" }, { status: 401 });
756
+ }
757
+ if (!verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {
730
758
  return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
731
759
  }
760
+ if (!rememberMailgunToken(signature.token)) {
761
+ return Response.json({ error: "REPLAYED_TOKEN" }, { status: 401 });
762
+ }
732
763
  const event = body2["event-data"];
733
764
  if (event) {
734
765
  await processMailgunEvent(event, store);
@@ -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;\">&nbsp;</td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-header\" style=\"padding:28px 32px 0 32px;\">\n\t\t\t\t\t\t\t\t\t<span class=\"email-brand\" style=\"font-weight:800;font-size:19px;color:${EMAIL_THEME.ink};letter-spacing:-0.03em;\">${EMAIL_THEME.brand}</span>\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-content\" style=\"padding:20px 32px 8px 32px;color:${EMAIL_THEME.ink};\">\n${LAYOUT_CONTENT_SLOT}\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-footer\" style=\"padding:16px 32px 28px 32px;color:${EMAIL_THEME.muted};font-size:13px;\">\n\t\t\t\t\t\t\t\t\tYou're receiving this because someone used this address at ${EMAIL_THEME.brand}. If that wasn't you, you can ignore it.\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t</table>\n\t\t\t</td>\n\t\t</tr>\n\t</table>\n</body>\n</html>`;\n\n// Inject a body fragment into a layout shell. If the body is already a full\n// HTML document (a founder chose to store a complete template), it is returned\n// untouched — never double-wrapped.\nexport function wrapLayout(bodyHtml: string, layoutHtml: string = DEFAULT_EMAIL_LAYOUT): string {\n\tconst trimmed = bodyHtml.trimStart().toLowerCase();\n\tif (trimmed.startsWith('<!doctype') || trimmed.startsWith('<html')) {\n\t\treturn bodyHtml;\n\t}\n\treturn layoutHtml.replace(LAYOUT_CONTENT_SLOT, bodyHtml);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ITemplateResolver, IRenderedTemplate, IDefaultTemplate, DefaultTemplateMap } from '../types';\nimport { wrapLayout } from './layout';\n\n// The stored template id for a founder-supplied layout shell (DB row `type` or\n// FS file `_layout.html`). Absent → the built-in DEFAULT_EMAIL_LAYOUT is used.\nconst LAYOUT_TYPE = '_layout';\n\nfunction render(template: string, data: Record<string, unknown>): string {\n\treturn template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) => {\n\t\tconst value = data[key];\n\t\treturn value !== undefined && value !== null ? String(value) : '';\n\t});\n}\n\n// Compose a body fragment into its layout shell, then interpolate variables\n// over the whole. `subject`/`preheader` become available to the shell's title\n// and inbox preview text.\nfunction composeHtml(\n\tbodyHtml: string,\n\tlayoutHtml: string | undefined,\n\tsubject: string | undefined,\n\tdata: Record<string, unknown>,\n): string {\n\tconst wrapped = wrapLayout(bodyHtml, layoutHtml);\n\treturn render(wrapped, { subject: subject ?? '', preheader: '', ...data });\n}\n\n// Render a resolved fragment — a DB row, an FS file set, or a MODULE DEFAULT —\n// into the final template. Factored out so a module default renders\n// byte-identically to a DB row: same {{var}} interpolation, same layout\n// composition. `text` is required; `subject`/`html` are optional (email only).\nexport function renderFragment(\n\tfrag: { subject?: string | null; text: string; html?: string | null },\n\tlayoutHtml: string | undefined,\n\tdata: Record<string, unknown>,\n): IRenderedTemplate {\n\tconst subject = frag.subject ? render(frag.subject, data) : undefined;\n\treturn {\n\t\ttext: render(frag.text, data),\n\t\t...(subject ? { subject } : {}),\n\t\t...(frag.html ? { html: composeHtml(frag.html, layoutHtml, subject, data) } : {}),\n\t};\n}\n\n// The module-shipped default templates an app hands courier, merged into one\n// lookup (aggregated like getMigrationsPath()). Per-key app overrides — a DB row\n// or an FS file — always win over a default; the default wins over the\n// last-resort JSON dump. A later map wins on key collision.\nexport class DefaultTemplates {\n\tprivate readonly map = new Map<string, IDefaultTemplate>();\n\tconstructor(maps: DefaultTemplateMap[] = []) {\n\t\tfor (const m of maps) {\n\t\t\tfor (const [key, tmpl] of Object.entries(m)) this.map.set(key, tmpl);\n\t\t}\n\t}\n\tget(type: string): IDefaultTemplate | undefined {\n\t\treturn this.map.get(type);\n\t}\n\tget size(): number {\n\t\treturn this.map.size;\n\t}\n}\n\n// DB-backed resolver — reads from fonderie_courier_templates with locale fallback\nexport class DBTemplateResolver implements ITemplateResolver {\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate defaults?: DefaultTemplates,\n\t) {}\n\n\tasync resolve(\n\t\ttype: string,\n\t\tdata: Record<string, unknown>,\n\t\tlocale?: string,\n\t): Promise<IRenderedTemplate> {\n\t\tconst [row] = await this.store.query<{\n\t\t\tsubject: string | null;\n\t\t\thtml: string | null;\n\t\t\ttext: string;\n\t\t}>(\n\t\t\t// Serve the exact locale, else the neutral NULL default — never a\n\t\t\t// sibling region (en-CA must not fall back to en-US); the WHERE\n\t\t\t// excludes other locales so legal/jurisdictional copy can't bleed.\n\t\t\t`SELECT subject, html, text\n\t\t\t FROM fonderie_courier_templates\n\t\t\t WHERE type = $1 AND active = true AND (locale = $2 OR locale IS NULL)\n\t\t\t ORDER BY (locale IS NOT DISTINCT FROM $2) DESC\n\t\t\t LIMIT 1`,\n\t\t\t[type, locale ?? null],\n\t\t);\n\n\t\tif (!row) {\n\t\t\t// No app row → fall back to the module default (rendered identically),\n\t\t\t// then to the JSON dump — now only reached for a type neither the app\n\t\t\t// nor any module provides.\n\t\t\tconst def = this.defaults?.get(type);\n\t\t\tif (def) {\n\t\t\t\treturn renderFragment(def, def.html ? await this.layout(locale) : undefined, data);\n\t\t\t}\n\t\t\treturn { text: `${type}: ${JSON.stringify(data)}` };\n\t\t}\n\n\t\tconst layoutHtml = row.html ? await this.layout(locale) : undefined;\n\t\treturn renderFragment(row, layoutHtml, data);\n\t}\n\n\t// Optional founder-supplied layout shell; undefined → built-in default.\n\tprivate async layout(locale?: string): Promise<string | undefined> {\n\t\tconst [row] = await this.store.query<{ html: string | null }>(\n\t\t\t`SELECT html\n\t\t\t FROM fonderie_courier_templates\n\t\t\t WHERE type = $1 AND active = true AND (locale = $2 OR locale IS NULL)\n\t\t\t ORDER BY (locale IS NOT DISTINCT FROM $2) DESC\n\t\t\t LIMIT 1`,\n\t\t\t[LAYOUT_TYPE, locale ?? null],\n\t\t);\n\t\treturn row?.html ?? undefined;\n\t}\n}\n\n// Filesystem resolver — reads {type}.{locale}.txt → {type}.txt with fallback\nexport class FSTemplateResolver implements ITemplateResolver {\n\tconstructor(\n\t\tprivate directory: string,\n\t\tprivate defaults?: DefaultTemplates,\n\t) {}\n\n\tasync resolve(\n\t\ttype: string,\n\t\tdata: Record<string, unknown>,\n\t\tlocale?: string,\n\t): Promise<IRenderedTemplate> {\n\t\tconst { readFile } = await import('node:fs/promises');\n\t\tconst { join } = await import('node:path');\n\n\t\tconst readOptional = async (path: string): Promise<string | null> => {\n\t\t\ttry {\n\t\t\t\treturn await readFile(path, 'utf8');\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t};\n\n\t\t// Per-locale variants take priority over generic variants\n\t\tconst localePrefix = locale ? `${type}.${locale}` : null;\n\t\tconst layoutPrefix = locale ? `${LAYOUT_TYPE}.${locale}` : null;\n\n\t\tconst [text, html, subject, layout] = await Promise.all([\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.txt`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.txt`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.txt`)),\n\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.html`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.html`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.html`)),\n\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.subject.txt`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.subject.txt`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.subject.txt`)),\n\n\t\t\tlayoutPrefix\n\t\t\t\t? readOptional(join(this.directory, `${layoutPrefix}.html`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${LAYOUT_TYPE}.html`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${LAYOUT_TYPE}.html`)),\n\t\t]);\n\n\t\t// App shipped NOTHING for this type (no text/html/subject file) → fall back\n\t\t// to the module default, using the app's own _layout shell if it ships one,\n\t\t// then to the JSON dump. Test file PRESENCE (=== null), not truthiness, so an\n\t\t// app that ships even an empty file keeps per-key control below — mirroring\n\t\t// the DB resolver's row-presence check, not overriding it with a default.\n\t\tif (text === null && html === null && subject === null) {\n\t\t\tconst def = this.defaults?.get(type);\n\t\t\tif (def) {\n\t\t\t\treturn renderFragment(def, def.html ? (layout ?? undefined) : undefined, data);\n\t\t\t}\n\t\t\treturn { text: `${type}: ${JSON.stringify(data)}` };\n\t\t}\n\n\t\tconst renderedSubject = subject ? render(subject, data) : undefined;\n\n\t\treturn {\n\t\t\ttext: text ? render(text, data) : `${type}: ${JSON.stringify(data)}`,\n\t\t\t...(renderedSubject ? { subject: renderedSubject } : {}),\n\t\t\t...(html ? { html: composeHtml(html, layout ?? undefined, renderedSubject, data) } : {}),\n\t\t};\n\t}\n}\n","import type { IReadinessProblem } from '@fonderie/core';\nimport type { ICourierConfig } from './config';\n\nconst MODULE = '@fonderie/courier';\n\n// Pure production-readiness assessment (no side effects) — shared by the\n// boot-time guard and `CourierModule.checkReadiness`. A message type routed to a\n// channel with no registered provider is silently dropped by the dispatcher —\n// with `requireVerification` on, verification / reset emails vanish and users\n// lock themselves out. These are `warning`-severity (channel setups vary\n// legitimately — an app may run in-app only — so this never blocks boot, unlike\n// auth's fatal weak-secret). Checks the channels actually **registered** on the\n// dispatcher, so channels added via `registerChannel` count as present.\nexport function collectCourierConfigProblems(\n\tconfig: ICourierConfig,\n\tregisteredChannels: Iterable<string>,\n): IReadinessProblem[] {\n\tconst registered = new Set(registeredChannels);\n\tconst gaps = new Map<string, string[]>(); // channel → message types routed to it\n\n\tfor (const [type, channels] of Object.entries(config.channels ?? {})) {\n\t\tfor (const channel of channels) {\n\t\t\tif (!registered.has(channel)) {\n\t\t\t\tconst types = gaps.get(channel) ?? [];\n\t\t\t\ttypes.push(type);\n\t\t\t\tgaps.set(channel, types);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn [...gaps].map(([channel, types]) => ({\n\t\tmodule: MODULE,\n\t\tseverity: 'warning' as const,\n\t\tmessage:\n\t\t\t`${types.length} message type(s) route to the '${channel}' channel but ` +\n\t\t\t`no '${channel}' provider is registered — these will be silently dropped: ` +\n\t\t\t`${types.join(', ')}. Configure \\`config.${channel}\\` (or register a channel).`,\n\t}));\n}\n\n// Boot-time preflight, run from `CourierModule.install`. Warn-only.\nexport function validateCourierConfig(\n\tconfig: ICourierConfig,\n\tregisteredChannels: Iterable<string>,\n): void {\n\tfor (const problem of collectCourierConfigProblems(config, registeredChannels)) {\n\t\tconsole.warn(`[courier] ${problem.message}`);\n\t}\n}\n","import { createHmac, createPublicKey, verify as cryptoVerify } from 'node:crypto';\n\nimport { constantTimeEqual } from '@fonderie/core';\n\nimport type { IStoreAdapter } from '@fonderie/store';\nimport {\n\tmarkMessageDelivered,\n\tmarkMessageOpened,\n\tmarkMessageClicked,\n\tmarkMessageBounced,\n} from './log';\n\n// Delivery webhooks are FAIL-CLOSED: every handler requires its verification\n// key and rejects unverified payloads with 401. Without verification, anyone\n// who finds the endpoint can forge delivered/opened/bounced events — poisoning\n// the message log and (via bounce handling) suppressing real mail. The module\n// additionally only registers a delivery route when its key is configured.\n\n// ── SendGrid ──────────────────────────────────────────────────────\n//\n// Verifies the X-Twilio-Email-Event-Webhook-Signature header. SendGrid signs\n// with ECDSA (P-256 over `timestamp + body`, base64 DER signature) — NOT HMAC;\n// `publicKey` is the base64 \"Verification Key\" from the SendGrid dashboard.\n// Expects an array of event objects in the request body.\n// https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/getting-started-event-webhook-security-features\n\nexport async function handleSendGridDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n\tpublicKey?: string,\n): Promise<Response> {\n\tif (!publicKey) {\n\t\treturn Response.json({ error: 'VERIFICATION_NOT_CONFIGURED' }, { status: 401 });\n\t}\n\n\tconst sig = req.headers.get('x-twilio-email-event-webhook-signature') ?? '';\n\tconst ts = req.headers.get('x-twilio-email-event-webhook-timestamp') ?? '';\n\tconst body = await req.text();\n\n\tif (!verifySendGridSignature(publicKey, ts, body, sig)) {\n\t\treturn Response.json({ error: 'INVALID_SIGNATURE' }, { status: 401 });\n\t}\n\n\tconst events = parseJson(body) as SendGridEvent[] | null;\n\tif (!Array.isArray(events)) return Response.json({ ok: true });\n\n\tawait processSendGridEvents(events, store);\n\treturn Response.json({ ok: true });\n}\n\nfunction verifySendGridSignature(\n\tpublicKeyB64: string,\n\ttimestamp: string,\n\tbody: string,\n\tsignature: string,\n): boolean {\n\ttry {\n\t\tconst key = createPublicKey({\n\t\t\tkey: Buffer.from(publicKeyB64, 'base64'),\n\t\t\tformat: 'der',\n\t\t\ttype: 'spki',\n\t\t});\n\t\treturn cryptoVerify(\n\t\t\t'sha256',\n\t\t\tBuffer.from(timestamp + body),\n\t\t\tkey,\n\t\t\tBuffer.from(signature, 'base64'),\n\t\t);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\ninterface SendGridEvent {\n\tevent: string;\n\tsg_message_id?: string;\n\treason?: string;\n\t[key: string]: unknown;\n}\n\nasync function processSendGridEvents(\n\tevents: SendGridEvent[],\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tfor (const ev of events) {\n\t\tconst msgId = ev['sg_message_id'];\n\t\tif (!msgId || typeof msgId !== 'string') continue;\n\t\t// sg_message_id is \"msgId.filterXxx\" — strip the filter suffix\n\t\tconst id = msgId.split('.')[0] ?? msgId;\n\n\t\tswitch (ev.event) {\n\t\t\tcase 'delivered': await markMessageDelivered(id, store); break;\n\t\t\tcase 'open': await markMessageOpened(id, store); break;\n\t\t\tcase 'click': await markMessageClicked(id, store); break;\n\t\t\tcase 'bounce':\n\t\t\tcase 'blocked':\n\t\t\tcase 'dropped':\n\t\t\t\tawait markMessageBounced(id, typeof ev['reason'] === 'string' ? ev['reason'] : ev.event, store);\n\t\t\t\tbreak;\n\t\t}\n\t}\n}\n\n// ── Mailgun ───────────────────────────────────────────────────────\n//\n// Verifies signature using token + timestamp + signing key (HMAC-SHA256).\n// https://documentation.mailgun.com/docs/mailgun/user-manual/tracking-messages/#securing-webhooks\n\nexport async function handleMailgunDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n\tsigningKey?: string,\n): Promise<Response> {\n\tif (!signingKey) {\n\t\treturn Response.json({ error: 'VERIFICATION_NOT_CONFIGURED' }, { status: 401 });\n\t}\n\n\tconst body = (await req.json()) as MailgunPayload;\n\n\tconst { signature } = body;\n\tif (!signature || !verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {\n\t\treturn Response.json({ error: 'INVALID_SIGNATURE' }, { status: 401 });\n\t}\n\n\tconst event = body['event-data'];\n\tif (event) {\n\t\tawait processMailgunEvent(event, store);\n\t}\n\n\treturn Response.json({ ok: true });\n}\n\nfunction verifyMailgunSignature(\n\tsigningKey: string,\n\ttimestamp: string,\n\ttoken: string,\n\tsignature: string,\n): boolean {\n\ttry {\n\t\tconst value = timestamp + token;\n\t\tconst expected = createHmac('sha256', signingKey).update(value).digest('hex');\n\t\treturn constantTimeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'));\n\t} catch {\n\t\treturn false;\n\t}\n}\n\ninterface MailgunPayload {\n\tsignature?: { timestamp: string; token: string; signature: string };\n\t'event-data'?: MailgunEvent;\n\t[key: string]: unknown;\n}\n\ninterface MailgunEvent {\n\tevent: string;\n\tmessage?: { headers?: { 'message-id'?: string } };\n\t'delivery-status'?: { message?: string };\n\t[key: string]: unknown;\n}\n\nasync function processMailgunEvent(event: MailgunEvent, store: IStoreAdapter): Promise<void> {\n\tconst msgId = event.message?.headers?.['message-id'];\n\tif (!msgId) return;\n\n\tswitch (event.event) {\n\t\tcase 'delivered': await markMessageDelivered(msgId, store); break;\n\t\tcase 'opened': await markMessageOpened(msgId, store); break;\n\t\tcase 'clicked': await markMessageClicked(msgId, store); break;\n\t\tcase 'failed':\n\t\tcase 'bounced': {\n\t\t\tconst reason = event['delivery-status']?.message ?? event.event;\n\t\t\tawait markMessageBounced(msgId, reason, store);\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n// ── Mailtrap (testing only — has NO signature scheme) ────────────\n//\n// Registered only when config.delivery.allowUnverifiedMailtrap is explicitly\n// true: with no signature to verify, the route accepts forged events by\n// construction, so it must be a deliberate dev/test opt-in, never a default.\n\nexport async function handleMailtrapDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n): Promise<Response> {\n\tconst events = (await req.json()) as MailtrapEvent[];\n\tif (!Array.isArray(events)) return Response.json({ ok: true });\n\n\tfor (const ev of events) {\n\t\tconst msgId = ev.message_id;\n\t\tif (!msgId) continue;\n\n\t\tswitch (ev.event) {\n\t\t\tcase 'delivery': await markMessageDelivered(msgId, store); break;\n\t\t\tcase 'open': await markMessageOpened(msgId, store); break;\n\t\t\tcase 'click': await markMessageClicked(msgId, store); break;\n\t\t\tcase 'bounce':\n\t\t\tcase 'soft_bounce':\n\t\t\t\tawait markMessageBounced(msgId, ev.event, store);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn Response.json({ ok: true });\n}\n\ninterface MailtrapEvent {\n\tevent: string;\n\tmessage_id?: string;\n\t[key: string]: unknown;\n}\n\n// ── Utility ───────────────────────────────────────────────────────\n\nfunction parseJson(text: string): unknown {\n\ttry { return JSON.parse(text); } catch { return null; }\n}\n","import type { IFonderieContext, Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport { requireAdminToken } from '@fonderie/core/middlewares';\nimport { VersionConflictError } from '@fonderie/store';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport {\n\tsetTemplate,\n\trollbackTemplate,\n\tlistTemplateRevisions,\n\tgetTemplateEntry,\n\tlistTemplateEntries,\n\tdeleteTemplate,\n} from './admin';\n\n// Bearer-token guard (mirrors @fonderie/config's admin surface). Only registered\n// when a token is configured — no token, no exposed template admin routes.\n// Wrap a handler in the shared admin-token guard (@fonderie/core/middlewares) —\n// one constant-time Bearer check across billing/config/courier.\nfunction guarded(adminToken: string, handler: Middleware): Middleware {\n\tconst guard = requireAdminToken(adminToken);\n\treturn (ctx, next) => guard(ctx, () => handler(ctx, next));\n}\n\nconst actorOf = (ctx: IFonderieContext) => ctx.request.headers.get('x-actor') || 'admin-token';\nconst typeOf = (ctx: IFonderieContext) => ctx.meta.params?.['type'] ?? '';\nconst localeOf = (ctx: IFonderieContext): string | null =>\n\tnew URL(ctx.request.url).searchParams.get('locale');\nconst body = (ctx: IFonderieContext): Record<string, unknown> =>\n\t(ctx.meta['body'] as Record<string, unknown> | undefined) ?? {};\n\nfunction conflictOr(err: unknown): Response {\n\tif (err instanceof VersionConflictError) {\n\t\treturn setApiResponse(HTTP.CONFLICT, 'VERSION_CONFLICT', err.message, {\n\t\t\tcurrentVersion: err.currentVersion,\n\t\t});\n\t}\n\tthrow err;\n}\n\n// Build the template admin route table (registered by CourierModule.install when\n// an adminToken is configured).\nexport function buildTemplateAdminRoutes(\n\tstore: IStoreAdapter,\n\tadminToken: string,\n): Array<[string, string, Middleware]> {\n\tconst g = (h: Middleware): Middleware => guarded(adminToken, h);\n\n\treturn [\n\t\t['GET', '/admin/templates', g(async () => {\n\t\t\treturn setApiResponse(HTTP.OK, 'TEMPLATES_LISTED', 'Templates', await listTemplateEntries(store));\n\t\t})],\n\t\t['GET', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst row = await getTemplateEntry(typeOf(ctx), localeOf(ctx), store);\n\t\t\treturn row\n\t\t\t\t? setApiResponse(HTTP.OK, 'TEMPLATE', 'Template', row)\n\t\t\t\t: setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such template');\n\t\t})],\n\t\t['PUT', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tif (typeof b['text'] !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.text (string) is required');\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst opts: Parameters<typeof setTemplate>[0] = {\n\t\t\t\t\ttype: typeOf(ctx),\n\t\t\t\t\ttext: b['text'],\n\t\t\t\t\tlocale: localeOf(ctx),\n\t\t\t\t\tactor: actorOf(ctx),\n\t\t\t\t};\n\t\t\t\tif (typeof b['subject'] === 'string') opts.subject = b['subject'];\n\t\t\t\tif (typeof b['html'] === 'string') opts.html = b['html'];\n\t\t\t\tif (typeof b['active'] === 'boolean') opts.active = b['active'];\n\t\t\t\tif (typeof b['ifVersion'] === 'number') opts.ifVersion = b['ifVersion'];\n\t\t\t\treturn setApiResponse(HTTP.OK, 'TEMPLATE_SET', 'Template saved', await setTemplate(opts, store));\n\t\t\t} catch (err) {\n\t\t\t\treturn conflictOr(err);\n\t\t\t}\n\t\t})],\n\t\t['DELETE', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst ok = await deleteTemplate(typeOf(ctx), localeOf(ctx), store);\n\t\t\treturn setApiResponse(ok ? HTTP.OK : HTTP.NOT_FOUND, ok ? 'DELETED' : 'NOT_FOUND', ok ? 'Deleted' : 'No such template');\n\t\t})],\n\t\t['GET', '/admin/templates/:type/revisions', g(async (ctx) => {\n\t\t\treturn setApiResponse(HTTP.OK, 'REVISIONS', 'Template revisions', await listTemplateRevisions(typeOf(ctx), localeOf(ctx), store));\n\t\t})],\n\t\t['POST', '/admin/templates/:type/rollback', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tconst toVersion = Number(b['toVersion']);\n\t\t\tif (!Number.isInteger(toVersion)) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.toVersion (int) is required');\n\t\t\t}\n\t\t\tconst row = await rollbackTemplate(\n\t\t\t\t{ type: typeOf(ctx), locale: localeOf(ctx), toVersion, actor: actorOf(ctx) },\n\t\t\t\tstore,\n\t\t\t);\n\t\t\treturn setApiResponse(HTTP.OK, 'ROLLED_BACK', `Rolled back to v${toVersion}`, row);\n\t\t})],\n\t];\n}\n","import type { IStoreAdapter, IVersionedResource } from '@fonderie/store';\nimport { versionedWrite, versionedRollback } from '@fonderie/store';\n\n// Versioned management for email templates, on the shared `@fonderie/store`\n// primitive. Templates are keyed by (type, locale) — a NULL locale is the base,\n// with per-locale overrides on top — and their content is subject/html/text.\n// The resolver reads the current row unchanged; this adds edit + history +\n// rollback + optimistic concurrency on top.\n\nexport interface ITemplateEntry {\n\ttype: string;\n\tlocale: string | null;\n\tsubject: string | null;\n\thtml: string | null;\n\ttext: string;\n\tactive: boolean;\n\tversion: number;\n\tupdatedBy: string | null;\n\tupdatedAt: string;\n}\n\nexport interface ITemplateRevision {\n\ttype: string;\n\tlocale: string | null;\n\tsubject: string | null;\n\thtml: string | null;\n\ttext: string;\n\tversion: number;\n\tactor: string | null;\n\tcreatedAt: string;\n}\n\nconst ENTRY_COLS = `type, locale, subject, html, text, active, version, updated_by AS \"updatedBy\", updated_at AS \"updatedAt\"`;\n\nconst TEMPLATE_RESOURCE: IVersionedResource = {\n\ttable: 'fonderie_courier_templates',\n\trevisions: 'fonderie_courier_template_revisions',\n\tchannel: 'fonderie_courier_templates_changed',\n\tkeyColumns: ['type', 'locale'],\n\tcontentColumns: ['subject', 'html', 'text'],\n\tmetaColumns: ['active'],\n\treturning: ENTRY_COLS,\n};\n\n// Upsert a template (full body — a revision is a full snapshot). Pass `ifVersion`\n// for optimistic concurrency (else `VersionConflictError`). `locale` omitted /\n// null targets the base template.\nexport async function setTemplate(\n\topts: {\n\t\ttype: string;\n\t\ttext: string;\n\t\tlocale?: string | null;\n\t\tsubject?: string | null;\n\t\thtml?: string | null;\n\t\tactive?: boolean;\n\t\tifVersion?: number;\n\t\tactor?: string;\n\t},\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry> {\n\tconst data: Record<string, unknown> = {\n\t\tsubject: opts.subject ?? null,\n\t\thtml: opts.html ?? null,\n\t\ttext: opts.text,\n\t};\n\tif (opts.active !== undefined) data['active'] = opts.active;\n\treturn versionedWrite<ITemplateEntry>(TEMPLATE_RESOURCE, store, {\n\t\tkey: opts.type,\n\t\tscope: opts.locale ?? null,\n\t\tdata,\n\t\t...(opts.ifVersion !== undefined ? { ifVersion: opts.ifVersion } : {}),\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function rollbackTemplate(\n\topts: { type: string; locale?: string | null; toVersion: number; actor?: string },\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry> {\n\treturn versionedRollback<ITemplateEntry>(TEMPLATE_RESOURCE, store, {\n\t\tkey: opts.type,\n\t\tscope: opts.locale ?? null,\n\t\ttoVersion: opts.toVersion,\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function listTemplateRevisions(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<ITemplateRevision[]> {\n\treturn store.query<ITemplateRevision>(\n\t\t`SELECT type, locale, subject, html, text, version, actor, created_at AS \"createdAt\"\n\t\t FROM fonderie_courier_template_revisions\n\t\t WHERE type = $1 AND locale IS NOT DISTINCT FROM $2\n\t\t ORDER BY version DESC`,\n\t\t[type, locale],\n\t);\n}\n\nexport async function getTemplateEntry(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry | null> {\n\tconst [row] = await store.query<ITemplateEntry>(\n\t\t`SELECT ${ENTRY_COLS} FROM fonderie_courier_templates WHERE type = $1 AND locale IS NOT DISTINCT FROM $2`,\n\t\t[type, locale],\n\t);\n\treturn row ?? null;\n}\n\nexport async function listTemplateEntries(store: IStoreAdapter): Promise<ITemplateEntry[]> {\n\treturn store.query<ITemplateEntry>(\n\t\t`SELECT ${ENTRY_COLS} FROM fonderie_courier_templates ORDER BY type, locale NULLS FIRST`,\n\t);\n}\n\nexport async function deleteTemplate(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst rows = await store.query<{ type: string }>(\n\t\t`DELETE FROM fonderie_courier_templates WHERE type = $1 AND locale IS NOT DISTINCT FROM $2 RETURNING type`,\n\t\t[type, locale],\n\t);\n\treturn rows.length > 0;\n}\n","import type { DefaultTemplateMap } from './types';\n\nexport interface IEmailChannelConfig {\n\tprovider: 'resend' | 'ses' | 'smtp';\n\tfrom: string;\n\tapiKey?: string;\n\tsmtp?: {\n\t\thost: string;\n\t\tport: number;\n\t\tsecure: boolean;\n\t\tuser: string;\n\t\tpass: string;\n\t};\n}\n\nexport interface ISmsChannelConfig {\n\tprovider: 'twilio' | 'vonage';\n\tfrom: string;\n\taccountSid?: string; // twilio\n\tauthToken?: string; // twilio\n\tapiKey?: string; // vonage\n\tapiSecret?: string; // vonage\n}\n\nexport interface IPushChannelConfig {\n\tprovider: 'fcm';\n\tserviceAccount: Record<string, unknown>;\n}\n\nexport const Channel = {\n\tEMAIL: 'email',\n\tSMS: 'sms',\n\tPUSH: 'push',\n} as const satisfies Record<string, 'email' | 'sms' | 'push'>;\n\nexport interface ICourierConfig {\n\t// Which channels handle which message types\n\t// e.g. { 'password-reset': ['email'], 'new-message': ['push', 'sms'] }\n\tchannels: Record<string, Array<'email' | 'sms' | 'push'>>;\n\n\tsms?: ISmsChannelConfig;\n\tpush?: IPushChannelConfig;\n\temail?: IEmailChannelConfig;\n\n\t// When set, exposes Bearer-guarded template admin routes (/admin/templates/*)\n\t// for versioned edit/history/rollback. Requires @fonderie/store (db templates).\n\tadminToken?: string;\n\n\t// Where templates are loaded from\n\t// 'db' requires @fonderie/store to be configured\n\t// 'fs' reads from a local directory\n\ttemplates?: {\n\t\tsource: 'db' | 'fs';\n\t\tdirectory?: string; // for 'fs' source\n\t\t// Module-shipped default templates. The app imports each module's\n\t\t// DEFAULT_TEMPLATES (auth, workspaces, billing, …) and passes them here;\n\t\t// the resolver falls back to a default when no app override (DB row / FS\n\t\t// file) exists, before the last-resort JSON dump. Aggregated like\n\t\t// getMigrationsPath() — pass one map or an array of them.\n\t\tdefaults?: DefaultTemplateMap | DefaultTemplateMap[];\n\t};\n\n\t// Inbound delivery-webhook verification. FAIL-CLOSED: a provider's route is\n\t// only registered when its key is set (an unverified endpoint would accept\n\t// forged delivered/opened/bounced events from anyone who finds the URL).\n\tdelivery?: {\n\t\tsigningKeys?: {\n\t\t\t// SendGrid signs with ECDSA, not HMAC: this is the base64\n\t\t\t// \"Verification Key\" (public key) from Settings → Mail Settings →\n\t\t\t// Event Webhook → Signature Verification.\n\t\t\tsendgrid?: string;\n\t\t\tmailgun?: string; // Mailgun HTTP webhook signing key (HMAC-SHA256)\n\t\t};\n\t\t// Mailtrap's webhook has NO signature scheme, so its route accepts\n\t\t// forged events by construction. Explicit dev/test opt-in only.\n\t\tallowUnverifiedMailtrap?: boolean;\n\t};\n}\n"],"mappings":"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","names":["body","body","nodemailer","body","import_middlewares","body","cryptoVerify","import_core","import_store"]}
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;\">&nbsp;</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, '&amp;')\n\t\t.replace(/</g, '&lt;')\n\t\t.replace(/>/g, '&gt;')\n\t\t.replace(/\"/g, '&quot;')\n\t\t.replace(/'/g, '&#39;');\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
@@ -457,15 +457,20 @@ function wrapLayout(bodyHtml, layoutHtml = DEFAULT_EMAIL_LAYOUT) {
457
457
 
458
458
  // src/templates/resolver.ts
459
459
  var LAYOUT_TYPE = "_layout";
460
- function render(template, data) {
460
+ function escapeHtml(value) {
461
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
462
+ }
463
+ function render(template, data, opts = {}) {
461
464
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
462
465
  const value = data[key];
463
- return value !== void 0 && value !== null ? String(value) : "";
466
+ if (value === void 0 || value === null) return "";
467
+ const s = String(value);
468
+ return opts.escapeHtml ? escapeHtml(s) : s;
464
469
  });
465
470
  }
466
471
  function composeHtml(bodyHtml, layoutHtml, subject, data) {
467
472
  const wrapped = wrapLayout(bodyHtml, layoutHtml);
468
- return render(wrapped, { subject: subject ?? "", preheader: "", ...data });
473
+ return render(wrapped, { subject: subject ?? "", preheader: "", ...data }, { escapeHtml: true });
469
474
  }
470
475
  function renderFragment(frag, layoutHtml, data) {
471
476
  const subject = frag.subject ? render(frag.subject, data) : void 0;
@@ -609,6 +614,12 @@ function validateCourierConfig(config, registeredChannels) {
609
614
  // src/delivery.ts
610
615
  import { createHmac, createPublicKey, verify as cryptoVerify } from "crypto";
611
616
  import { constantTimeEqual } from "@fonderie/core";
617
+ var TIMESTAMP_TOLERANCE_S = 5 * 60;
618
+ function isFreshTimestamp(unixSeconds) {
619
+ const ts = Number(unixSeconds);
620
+ if (!Number.isFinite(ts)) return false;
621
+ return Math.abs(Date.now() / 1e3 - ts) <= TIMESTAMP_TOLERANCE_S;
622
+ }
612
623
  async function handleSendGridDelivery(req, store, publicKey) {
613
624
  if (!publicKey) {
614
625
  return Response.json({ error: "VERIFICATION_NOT_CONFIGURED" }, { status: 401 });
@@ -616,6 +627,9 @@ async function handleSendGridDelivery(req, store, publicKey) {
616
627
  const sig = req.headers.get("x-twilio-email-event-webhook-signature") ?? "";
617
628
  const ts = req.headers.get("x-twilio-email-event-webhook-timestamp") ?? "";
618
629
  const body2 = await req.text();
630
+ if (!isFreshTimestamp(ts)) {
631
+ return Response.json({ error: "STALE_TIMESTAMP" }, { status: 401 });
632
+ }
619
633
  if (!verifySendGridSignature(publicKey, ts, body2, sig)) {
620
634
  return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
621
635
  }
@@ -664,15 +678,32 @@ async function processSendGridEvents(events, store) {
664
678
  }
665
679
  }
666
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
+ }
667
692
  async function handleMailgunDelivery(req, store, signingKey) {
668
693
  if (!signingKey) {
669
694
  return Response.json({ error: "VERIFICATION_NOT_CONFIGURED" }, { status: 401 });
670
695
  }
671
696
  const body2 = await req.json();
672
697
  const { signature } = body2;
673
- if (!signature || !verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {
698
+ if (!signature || !isFreshTimestamp(signature.timestamp)) {
699
+ return Response.json({ error: "STALE_TIMESTAMP" }, { status: 401 });
700
+ }
701
+ if (!verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {
674
702
  return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
675
703
  }
704
+ if (!rememberMailgunToken(signature.token)) {
705
+ return Response.json({ error: "REPLAYED_TOKEN" }, { status: 401 });
706
+ }
676
707
  const event = body2["event-data"];
677
708
  if (event) {
678
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;\">&nbsp;</td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-header\" style=\"padding:28px 32px 0 32px;\">\n\t\t\t\t\t\t\t\t\t<span class=\"email-brand\" style=\"font-weight:800;font-size:19px;color:${EMAIL_THEME.ink};letter-spacing:-0.03em;\">${EMAIL_THEME.brand}</span>\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-content\" style=\"padding:20px 32px 8px 32px;color:${EMAIL_THEME.ink};\">\n${LAYOUT_CONTENT_SLOT}\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-footer\" style=\"padding:16px 32px 28px 32px;color:${EMAIL_THEME.muted};font-size:13px;\">\n\t\t\t\t\t\t\t\t\tYou're receiving this because someone used this address at ${EMAIL_THEME.brand}. If that wasn't you, you can ignore it.\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t</table>\n\t\t\t</td>\n\t\t</tr>\n\t</table>\n</body>\n</html>`;\n\n// Inject a body fragment into a layout shell. If the body is already a full\n// HTML document (a founder chose to store a complete template), it is returned\n// untouched — never double-wrapped.\nexport function wrapLayout(bodyHtml: string, layoutHtml: string = DEFAULT_EMAIL_LAYOUT): string {\n\tconst trimmed = bodyHtml.trimStart().toLowerCase();\n\tif (trimmed.startsWith('<!doctype') || trimmed.startsWith('<html')) {\n\t\treturn bodyHtml;\n\t}\n\treturn layoutHtml.replace(LAYOUT_CONTENT_SLOT, bodyHtml);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ITemplateResolver, IRenderedTemplate, IDefaultTemplate, DefaultTemplateMap } from '../types';\nimport { wrapLayout } from './layout';\n\n// The stored template id for a founder-supplied layout shell (DB row `type` or\n// FS file `_layout.html`). Absent → the built-in DEFAULT_EMAIL_LAYOUT is used.\nconst LAYOUT_TYPE = '_layout';\n\nfunction render(template: string, data: Record<string, unknown>): string {\n\treturn template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) => {\n\t\tconst value = data[key];\n\t\treturn value !== undefined && value !== null ? String(value) : '';\n\t});\n}\n\n// Compose a body fragment into its layout shell, then interpolate variables\n// over the whole. `subject`/`preheader` become available to the shell's title\n// and inbox preview text.\nfunction composeHtml(\n\tbodyHtml: string,\n\tlayoutHtml: string | undefined,\n\tsubject: string | undefined,\n\tdata: Record<string, unknown>,\n): string {\n\tconst wrapped = wrapLayout(bodyHtml, layoutHtml);\n\treturn render(wrapped, { subject: subject ?? '', preheader: '', ...data });\n}\n\n// Render a resolved fragment — a DB row, an FS file set, or a MODULE DEFAULT —\n// into the final template. Factored out so a module default renders\n// byte-identically to a DB row: same {{var}} interpolation, same layout\n// composition. `text` is required; `subject`/`html` are optional (email only).\nexport function renderFragment(\n\tfrag: { subject?: string | null; text: string; html?: string | null },\n\tlayoutHtml: string | undefined,\n\tdata: Record<string, unknown>,\n): IRenderedTemplate {\n\tconst subject = frag.subject ? render(frag.subject, data) : undefined;\n\treturn {\n\t\ttext: render(frag.text, data),\n\t\t...(subject ? { subject } : {}),\n\t\t...(frag.html ? { html: composeHtml(frag.html, layoutHtml, subject, data) } : {}),\n\t};\n}\n\n// The module-shipped default templates an app hands courier, merged into one\n// lookup (aggregated like getMigrationsPath()). Per-key app overrides — a DB row\n// or an FS file — always win over a default; the default wins over the\n// last-resort JSON dump. A later map wins on key collision.\nexport class DefaultTemplates {\n\tprivate readonly map = new Map<string, IDefaultTemplate>();\n\tconstructor(maps: DefaultTemplateMap[] = []) {\n\t\tfor (const m of maps) {\n\t\t\tfor (const [key, tmpl] of Object.entries(m)) this.map.set(key, tmpl);\n\t\t}\n\t}\n\tget(type: string): IDefaultTemplate | undefined {\n\t\treturn this.map.get(type);\n\t}\n\tget size(): number {\n\t\treturn this.map.size;\n\t}\n}\n\n// DB-backed resolver — reads from fonderie_courier_templates with locale fallback\nexport class DBTemplateResolver implements ITemplateResolver {\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate defaults?: DefaultTemplates,\n\t) {}\n\n\tasync resolve(\n\t\ttype: string,\n\t\tdata: Record<string, unknown>,\n\t\tlocale?: string,\n\t): Promise<IRenderedTemplate> {\n\t\tconst [row] = await this.store.query<{\n\t\t\tsubject: string | null;\n\t\t\thtml: string | null;\n\t\t\ttext: string;\n\t\t}>(\n\t\t\t// Serve the exact locale, else the neutral NULL default — never a\n\t\t\t// sibling region (en-CA must not fall back to en-US); the WHERE\n\t\t\t// excludes other locales so legal/jurisdictional copy can't bleed.\n\t\t\t`SELECT subject, html, text\n\t\t\t FROM fonderie_courier_templates\n\t\t\t WHERE type = $1 AND active = true AND (locale = $2 OR locale IS NULL)\n\t\t\t ORDER BY (locale IS NOT DISTINCT FROM $2) DESC\n\t\t\t LIMIT 1`,\n\t\t\t[type, locale ?? null],\n\t\t);\n\n\t\tif (!row) {\n\t\t\t// No app row → fall back to the module default (rendered identically),\n\t\t\t// then to the JSON dump — now only reached for a type neither the app\n\t\t\t// nor any module provides.\n\t\t\tconst def = this.defaults?.get(type);\n\t\t\tif (def) {\n\t\t\t\treturn renderFragment(def, def.html ? await this.layout(locale) : undefined, data);\n\t\t\t}\n\t\t\treturn { text: `${type}: ${JSON.stringify(data)}` };\n\t\t}\n\n\t\tconst layoutHtml = row.html ? await this.layout(locale) : undefined;\n\t\treturn renderFragment(row, layoutHtml, data);\n\t}\n\n\t// Optional founder-supplied layout shell; undefined → built-in default.\n\tprivate async layout(locale?: string): Promise<string | undefined> {\n\t\tconst [row] = await this.store.query<{ html: string | null }>(\n\t\t\t`SELECT html\n\t\t\t FROM fonderie_courier_templates\n\t\t\t WHERE type = $1 AND active = true AND (locale = $2 OR locale IS NULL)\n\t\t\t ORDER BY (locale IS NOT DISTINCT FROM $2) DESC\n\t\t\t LIMIT 1`,\n\t\t\t[LAYOUT_TYPE, locale ?? null],\n\t\t);\n\t\treturn row?.html ?? undefined;\n\t}\n}\n\n// Filesystem resolver — reads {type}.{locale}.txt → {type}.txt with fallback\nexport class FSTemplateResolver implements ITemplateResolver {\n\tconstructor(\n\t\tprivate directory: string,\n\t\tprivate defaults?: DefaultTemplates,\n\t) {}\n\n\tasync resolve(\n\t\ttype: string,\n\t\tdata: Record<string, unknown>,\n\t\tlocale?: string,\n\t): Promise<IRenderedTemplate> {\n\t\tconst { readFile } = await import('node:fs/promises');\n\t\tconst { join } = await import('node:path');\n\n\t\tconst readOptional = async (path: string): Promise<string | null> => {\n\t\t\ttry {\n\t\t\t\treturn await readFile(path, 'utf8');\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t};\n\n\t\t// Per-locale variants take priority over generic variants\n\t\tconst localePrefix = locale ? `${type}.${locale}` : null;\n\t\tconst layoutPrefix = locale ? `${LAYOUT_TYPE}.${locale}` : null;\n\n\t\tconst [text, html, subject, layout] = await Promise.all([\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.txt`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.txt`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.txt`)),\n\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.html`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.html`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.html`)),\n\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.subject.txt`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.subject.txt`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.subject.txt`)),\n\n\t\t\tlayoutPrefix\n\t\t\t\t? readOptional(join(this.directory, `${layoutPrefix}.html`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${LAYOUT_TYPE}.html`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${LAYOUT_TYPE}.html`)),\n\t\t]);\n\n\t\t// App shipped NOTHING for this type (no text/html/subject file) → fall back\n\t\t// to the module default, using the app's own _layout shell if it ships one,\n\t\t// then to the JSON dump. Test file PRESENCE (=== null), not truthiness, so an\n\t\t// app that ships even an empty file keeps per-key control below — mirroring\n\t\t// the DB resolver's row-presence check, not overriding it with a default.\n\t\tif (text === null && html === null && subject === null) {\n\t\t\tconst def = this.defaults?.get(type);\n\t\t\tif (def) {\n\t\t\t\treturn renderFragment(def, def.html ? (layout ?? undefined) : undefined, data);\n\t\t\t}\n\t\t\treturn { text: `${type}: ${JSON.stringify(data)}` };\n\t\t}\n\n\t\tconst renderedSubject = subject ? render(subject, data) : undefined;\n\n\t\treturn {\n\t\t\ttext: text ? render(text, data) : `${type}: ${JSON.stringify(data)}`,\n\t\t\t...(renderedSubject ? { subject: renderedSubject } : {}),\n\t\t\t...(html ? { html: composeHtml(html, layout ?? undefined, renderedSubject, data) } : {}),\n\t\t};\n\t}\n}\n","import type { IReadinessProblem } from '@fonderie/core';\nimport type { ICourierConfig } from './config';\n\nconst MODULE = '@fonderie/courier';\n\n// Pure production-readiness assessment (no side effects) — shared by the\n// boot-time guard and `CourierModule.checkReadiness`. A message type routed to a\n// channel with no registered provider is silently dropped by the dispatcher —\n// with `requireVerification` on, verification / reset emails vanish and users\n// lock themselves out. These are `warning`-severity (channel setups vary\n// legitimately — an app may run in-app only — so this never blocks boot, unlike\n// auth's fatal weak-secret). Checks the channels actually **registered** on the\n// dispatcher, so channels added via `registerChannel` count as present.\nexport function collectCourierConfigProblems(\n\tconfig: ICourierConfig,\n\tregisteredChannels: Iterable<string>,\n): IReadinessProblem[] {\n\tconst registered = new Set(registeredChannels);\n\tconst gaps = new Map<string, string[]>(); // channel → message types routed to it\n\n\tfor (const [type, channels] of Object.entries(config.channels ?? {})) {\n\t\tfor (const channel of channels) {\n\t\t\tif (!registered.has(channel)) {\n\t\t\t\tconst types = gaps.get(channel) ?? [];\n\t\t\t\ttypes.push(type);\n\t\t\t\tgaps.set(channel, types);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn [...gaps].map(([channel, types]) => ({\n\t\tmodule: MODULE,\n\t\tseverity: 'warning' as const,\n\t\tmessage:\n\t\t\t`${types.length} message type(s) route to the '${channel}' channel but ` +\n\t\t\t`no '${channel}' provider is registered — these will be silently dropped: ` +\n\t\t\t`${types.join(', ')}. Configure \\`config.${channel}\\` (or register a channel).`,\n\t}));\n}\n\n// Boot-time preflight, run from `CourierModule.install`. Warn-only.\nexport function validateCourierConfig(\n\tconfig: ICourierConfig,\n\tregisteredChannels: Iterable<string>,\n): void {\n\tfor (const problem of collectCourierConfigProblems(config, registeredChannels)) {\n\t\tconsole.warn(`[courier] ${problem.message}`);\n\t}\n}\n","import { createHmac, createPublicKey, verify as cryptoVerify } from 'node:crypto';\n\nimport { constantTimeEqual } from '@fonderie/core';\n\nimport type { IStoreAdapter } from '@fonderie/store';\nimport {\n\tmarkMessageDelivered,\n\tmarkMessageOpened,\n\tmarkMessageClicked,\n\tmarkMessageBounced,\n} from './log';\n\n// Delivery webhooks are FAIL-CLOSED: every handler requires its verification\n// key and rejects unverified payloads with 401. Without verification, anyone\n// who finds the endpoint can forge delivered/opened/bounced events — poisoning\n// the message log and (via bounce handling) suppressing real mail. The module\n// additionally only registers a delivery route when its key is configured.\n\n// ── SendGrid ──────────────────────────────────────────────────────\n//\n// Verifies the X-Twilio-Email-Event-Webhook-Signature header. SendGrid signs\n// with ECDSA (P-256 over `timestamp + body`, base64 DER signature) — NOT HMAC;\n// `publicKey` is the base64 \"Verification Key\" from the SendGrid dashboard.\n// Expects an array of event objects in the request body.\n// https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/getting-started-event-webhook-security-features\n\nexport async function handleSendGridDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n\tpublicKey?: string,\n): Promise<Response> {\n\tif (!publicKey) {\n\t\treturn Response.json({ error: 'VERIFICATION_NOT_CONFIGURED' }, { status: 401 });\n\t}\n\n\tconst sig = req.headers.get('x-twilio-email-event-webhook-signature') ?? '';\n\tconst ts = req.headers.get('x-twilio-email-event-webhook-timestamp') ?? '';\n\tconst body = await req.text();\n\n\tif (!verifySendGridSignature(publicKey, ts, body, sig)) {\n\t\treturn Response.json({ error: 'INVALID_SIGNATURE' }, { status: 401 });\n\t}\n\n\tconst events = parseJson(body) as SendGridEvent[] | null;\n\tif (!Array.isArray(events)) return Response.json({ ok: true });\n\n\tawait processSendGridEvents(events, store);\n\treturn Response.json({ ok: true });\n}\n\nfunction verifySendGridSignature(\n\tpublicKeyB64: string,\n\ttimestamp: string,\n\tbody: string,\n\tsignature: string,\n): boolean {\n\ttry {\n\t\tconst key = createPublicKey({\n\t\t\tkey: Buffer.from(publicKeyB64, 'base64'),\n\t\t\tformat: 'der',\n\t\t\ttype: 'spki',\n\t\t});\n\t\treturn cryptoVerify(\n\t\t\t'sha256',\n\t\t\tBuffer.from(timestamp + body),\n\t\t\tkey,\n\t\t\tBuffer.from(signature, 'base64'),\n\t\t);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\ninterface SendGridEvent {\n\tevent: string;\n\tsg_message_id?: string;\n\treason?: string;\n\t[key: string]: unknown;\n}\n\nasync function processSendGridEvents(\n\tevents: SendGridEvent[],\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tfor (const ev of events) {\n\t\tconst msgId = ev['sg_message_id'];\n\t\tif (!msgId || typeof msgId !== 'string') continue;\n\t\t// sg_message_id is \"msgId.filterXxx\" — strip the filter suffix\n\t\tconst id = msgId.split('.')[0] ?? msgId;\n\n\t\tswitch (ev.event) {\n\t\t\tcase 'delivered': await markMessageDelivered(id, store); break;\n\t\t\tcase 'open': await markMessageOpened(id, store); break;\n\t\t\tcase 'click': await markMessageClicked(id, store); break;\n\t\t\tcase 'bounce':\n\t\t\tcase 'blocked':\n\t\t\tcase 'dropped':\n\t\t\t\tawait markMessageBounced(id, typeof ev['reason'] === 'string' ? ev['reason'] : ev.event, store);\n\t\t\t\tbreak;\n\t\t}\n\t}\n}\n\n// ── Mailgun ───────────────────────────────────────────────────────\n//\n// Verifies signature using token + timestamp + signing key (HMAC-SHA256).\n// https://documentation.mailgun.com/docs/mailgun/user-manual/tracking-messages/#securing-webhooks\n\nexport async function handleMailgunDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n\tsigningKey?: string,\n): Promise<Response> {\n\tif (!signingKey) {\n\t\treturn Response.json({ error: 'VERIFICATION_NOT_CONFIGURED' }, { status: 401 });\n\t}\n\n\tconst body = (await req.json()) as MailgunPayload;\n\n\tconst { signature } = body;\n\tif (!signature || !verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {\n\t\treturn Response.json({ error: 'INVALID_SIGNATURE' }, { status: 401 });\n\t}\n\n\tconst event = body['event-data'];\n\tif (event) {\n\t\tawait processMailgunEvent(event, store);\n\t}\n\n\treturn Response.json({ ok: true });\n}\n\nfunction verifyMailgunSignature(\n\tsigningKey: string,\n\ttimestamp: string,\n\ttoken: string,\n\tsignature: string,\n): boolean {\n\ttry {\n\t\tconst value = timestamp + token;\n\t\tconst expected = createHmac('sha256', signingKey).update(value).digest('hex');\n\t\treturn constantTimeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'));\n\t} catch {\n\t\treturn false;\n\t}\n}\n\ninterface MailgunPayload {\n\tsignature?: { timestamp: string; token: string; signature: string };\n\t'event-data'?: MailgunEvent;\n\t[key: string]: unknown;\n}\n\ninterface MailgunEvent {\n\tevent: string;\n\tmessage?: { headers?: { 'message-id'?: string } };\n\t'delivery-status'?: { message?: string };\n\t[key: string]: unknown;\n}\n\nasync function processMailgunEvent(event: MailgunEvent, store: IStoreAdapter): Promise<void> {\n\tconst msgId = event.message?.headers?.['message-id'];\n\tif (!msgId) return;\n\n\tswitch (event.event) {\n\t\tcase 'delivered': await markMessageDelivered(msgId, store); break;\n\t\tcase 'opened': await markMessageOpened(msgId, store); break;\n\t\tcase 'clicked': await markMessageClicked(msgId, store); break;\n\t\tcase 'failed':\n\t\tcase 'bounced': {\n\t\t\tconst reason = event['delivery-status']?.message ?? event.event;\n\t\t\tawait markMessageBounced(msgId, reason, store);\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n// ── Mailtrap (testing only — has NO signature scheme) ────────────\n//\n// Registered only when config.delivery.allowUnverifiedMailtrap is explicitly\n// true: with no signature to verify, the route accepts forged events by\n// construction, so it must be a deliberate dev/test opt-in, never a default.\n\nexport async function handleMailtrapDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n): Promise<Response> {\n\tconst events = (await req.json()) as MailtrapEvent[];\n\tif (!Array.isArray(events)) return Response.json({ ok: true });\n\n\tfor (const ev of events) {\n\t\tconst msgId = ev.message_id;\n\t\tif (!msgId) continue;\n\n\t\tswitch (ev.event) {\n\t\t\tcase 'delivery': await markMessageDelivered(msgId, store); break;\n\t\t\tcase 'open': await markMessageOpened(msgId, store); break;\n\t\t\tcase 'click': await markMessageClicked(msgId, store); break;\n\t\t\tcase 'bounce':\n\t\t\tcase 'soft_bounce':\n\t\t\t\tawait markMessageBounced(msgId, ev.event, store);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn Response.json({ ok: true });\n}\n\ninterface MailtrapEvent {\n\tevent: string;\n\tmessage_id?: string;\n\t[key: string]: unknown;\n}\n\n// ── Utility ───────────────────────────────────────────────────────\n\nfunction parseJson(text: string): unknown {\n\ttry { return JSON.parse(text); } catch { return null; }\n}\n","import type { IFonderieContext, Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport { requireAdminToken } from '@fonderie/core/middlewares';\nimport { VersionConflictError } from '@fonderie/store';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport {\n\tsetTemplate,\n\trollbackTemplate,\n\tlistTemplateRevisions,\n\tgetTemplateEntry,\n\tlistTemplateEntries,\n\tdeleteTemplate,\n} from './admin';\n\n// Bearer-token guard (mirrors @fonderie/config's admin surface). Only registered\n// when a token is configured — no token, no exposed template admin routes.\n// Wrap a handler in the shared admin-token guard (@fonderie/core/middlewares) —\n// one constant-time Bearer check across billing/config/courier.\nfunction guarded(adminToken: string, handler: Middleware): Middleware {\n\tconst guard = requireAdminToken(adminToken);\n\treturn (ctx, next) => guard(ctx, () => handler(ctx, next));\n}\n\nconst actorOf = (ctx: IFonderieContext) => ctx.request.headers.get('x-actor') || 'admin-token';\nconst typeOf = (ctx: IFonderieContext) => ctx.meta.params?.['type'] ?? '';\nconst localeOf = (ctx: IFonderieContext): string | null =>\n\tnew URL(ctx.request.url).searchParams.get('locale');\nconst body = (ctx: IFonderieContext): Record<string, unknown> =>\n\t(ctx.meta['body'] as Record<string, unknown> | undefined) ?? {};\n\nfunction conflictOr(err: unknown): Response {\n\tif (err instanceof VersionConflictError) {\n\t\treturn setApiResponse(HTTP.CONFLICT, 'VERSION_CONFLICT', err.message, {\n\t\t\tcurrentVersion: err.currentVersion,\n\t\t});\n\t}\n\tthrow err;\n}\n\n// Build the template admin route table (registered by CourierModule.install when\n// an adminToken is configured).\nexport function buildTemplateAdminRoutes(\n\tstore: IStoreAdapter,\n\tadminToken: string,\n): Array<[string, string, Middleware]> {\n\tconst g = (h: Middleware): Middleware => guarded(adminToken, h);\n\n\treturn [\n\t\t['GET', '/admin/templates', g(async () => {\n\t\t\treturn setApiResponse(HTTP.OK, 'TEMPLATES_LISTED', 'Templates', await listTemplateEntries(store));\n\t\t})],\n\t\t['GET', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst row = await getTemplateEntry(typeOf(ctx), localeOf(ctx), store);\n\t\t\treturn row\n\t\t\t\t? setApiResponse(HTTP.OK, 'TEMPLATE', 'Template', row)\n\t\t\t\t: setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such template');\n\t\t})],\n\t\t['PUT', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tif (typeof b['text'] !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.text (string) is required');\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst opts: Parameters<typeof setTemplate>[0] = {\n\t\t\t\t\ttype: typeOf(ctx),\n\t\t\t\t\ttext: b['text'],\n\t\t\t\t\tlocale: localeOf(ctx),\n\t\t\t\t\tactor: actorOf(ctx),\n\t\t\t\t};\n\t\t\t\tif (typeof b['subject'] === 'string') opts.subject = b['subject'];\n\t\t\t\tif (typeof b['html'] === 'string') opts.html = b['html'];\n\t\t\t\tif (typeof b['active'] === 'boolean') opts.active = b['active'];\n\t\t\t\tif (typeof b['ifVersion'] === 'number') opts.ifVersion = b['ifVersion'];\n\t\t\t\treturn setApiResponse(HTTP.OK, 'TEMPLATE_SET', 'Template saved', await setTemplate(opts, store));\n\t\t\t} catch (err) {\n\t\t\t\treturn conflictOr(err);\n\t\t\t}\n\t\t})],\n\t\t['DELETE', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst ok = await deleteTemplate(typeOf(ctx), localeOf(ctx), store);\n\t\t\treturn setApiResponse(ok ? HTTP.OK : HTTP.NOT_FOUND, ok ? 'DELETED' : 'NOT_FOUND', ok ? 'Deleted' : 'No such template');\n\t\t})],\n\t\t['GET', '/admin/templates/:type/revisions', g(async (ctx) => {\n\t\t\treturn setApiResponse(HTTP.OK, 'REVISIONS', 'Template revisions', await listTemplateRevisions(typeOf(ctx), localeOf(ctx), store));\n\t\t})],\n\t\t['POST', '/admin/templates/:type/rollback', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tconst toVersion = Number(b['toVersion']);\n\t\t\tif (!Number.isInteger(toVersion)) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.toVersion (int) is required');\n\t\t\t}\n\t\t\tconst row = await rollbackTemplate(\n\t\t\t\t{ type: typeOf(ctx), locale: localeOf(ctx), toVersion, actor: actorOf(ctx) },\n\t\t\t\tstore,\n\t\t\t);\n\t\t\treturn setApiResponse(HTTP.OK, 'ROLLED_BACK', `Rolled back to v${toVersion}`, row);\n\t\t})],\n\t];\n}\n","import type { IStoreAdapter, IVersionedResource } from '@fonderie/store';\nimport { versionedWrite, versionedRollback } from '@fonderie/store';\n\n// Versioned management for email templates, on the shared `@fonderie/store`\n// primitive. Templates are keyed by (type, locale) — a NULL locale is the base,\n// with per-locale overrides on top — and their content is subject/html/text.\n// The resolver reads the current row unchanged; this adds edit + history +\n// rollback + optimistic concurrency on top.\n\nexport interface ITemplateEntry {\n\ttype: string;\n\tlocale: string | null;\n\tsubject: string | null;\n\thtml: string | null;\n\ttext: string;\n\tactive: boolean;\n\tversion: number;\n\tupdatedBy: string | null;\n\tupdatedAt: string;\n}\n\nexport interface ITemplateRevision {\n\ttype: string;\n\tlocale: string | null;\n\tsubject: string | null;\n\thtml: string | null;\n\ttext: string;\n\tversion: number;\n\tactor: string | null;\n\tcreatedAt: string;\n}\n\nconst ENTRY_COLS = `type, locale, subject, html, text, active, version, updated_by AS \"updatedBy\", updated_at AS \"updatedAt\"`;\n\nconst TEMPLATE_RESOURCE: IVersionedResource = {\n\ttable: 'fonderie_courier_templates',\n\trevisions: 'fonderie_courier_template_revisions',\n\tchannel: 'fonderie_courier_templates_changed',\n\tkeyColumns: ['type', 'locale'],\n\tcontentColumns: ['subject', 'html', 'text'],\n\tmetaColumns: ['active'],\n\treturning: ENTRY_COLS,\n};\n\n// Upsert a template (full body — a revision is a full snapshot). Pass `ifVersion`\n// for optimistic concurrency (else `VersionConflictError`). `locale` omitted /\n// null targets the base template.\nexport async function setTemplate(\n\topts: {\n\t\ttype: string;\n\t\ttext: string;\n\t\tlocale?: string | null;\n\t\tsubject?: string | null;\n\t\thtml?: string | null;\n\t\tactive?: boolean;\n\t\tifVersion?: number;\n\t\tactor?: string;\n\t},\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry> {\n\tconst data: Record<string, unknown> = {\n\t\tsubject: opts.subject ?? null,\n\t\thtml: opts.html ?? null,\n\t\ttext: opts.text,\n\t};\n\tif (opts.active !== undefined) data['active'] = opts.active;\n\treturn versionedWrite<ITemplateEntry>(TEMPLATE_RESOURCE, store, {\n\t\tkey: opts.type,\n\t\tscope: opts.locale ?? null,\n\t\tdata,\n\t\t...(opts.ifVersion !== undefined ? { ifVersion: opts.ifVersion } : {}),\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function rollbackTemplate(\n\topts: { type: string; locale?: string | null; toVersion: number; actor?: string },\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry> {\n\treturn versionedRollback<ITemplateEntry>(TEMPLATE_RESOURCE, store, {\n\t\tkey: opts.type,\n\t\tscope: opts.locale ?? null,\n\t\ttoVersion: opts.toVersion,\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function listTemplateRevisions(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<ITemplateRevision[]> {\n\treturn store.query<ITemplateRevision>(\n\t\t`SELECT type, locale, subject, html, text, version, actor, created_at AS \"createdAt\"\n\t\t FROM fonderie_courier_template_revisions\n\t\t WHERE type = $1 AND locale IS NOT DISTINCT FROM $2\n\t\t ORDER BY version DESC`,\n\t\t[type, locale],\n\t);\n}\n\nexport async function getTemplateEntry(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry | null> {\n\tconst [row] = await store.query<ITemplateEntry>(\n\t\t`SELECT ${ENTRY_COLS} FROM fonderie_courier_templates WHERE type = $1 AND locale IS NOT DISTINCT FROM $2`,\n\t\t[type, locale],\n\t);\n\treturn row ?? null;\n}\n\nexport async function listTemplateEntries(store: IStoreAdapter): Promise<ITemplateEntry[]> {\n\treturn store.query<ITemplateEntry>(\n\t\t`SELECT ${ENTRY_COLS} FROM fonderie_courier_templates ORDER BY type, locale NULLS FIRST`,\n\t);\n}\n\nexport async function deleteTemplate(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst rows = await store.query<{ type: string }>(\n\t\t`DELETE FROM fonderie_courier_templates WHERE type = $1 AND locale IS NOT DISTINCT FROM $2 RETURNING type`,\n\t\t[type, locale],\n\t);\n\treturn rows.length > 0;\n}\n","import type { DefaultTemplateMap } from './types';\n\nexport interface IEmailChannelConfig {\n\tprovider: 'resend' | 'ses' | 'smtp';\n\tfrom: string;\n\tapiKey?: string;\n\tsmtp?: {\n\t\thost: string;\n\t\tport: number;\n\t\tsecure: boolean;\n\t\tuser: string;\n\t\tpass: string;\n\t};\n}\n\nexport interface ISmsChannelConfig {\n\tprovider: 'twilio' | 'vonage';\n\tfrom: string;\n\taccountSid?: string; // twilio\n\tauthToken?: string; // twilio\n\tapiKey?: string; // vonage\n\tapiSecret?: string; // vonage\n}\n\nexport interface IPushChannelConfig {\n\tprovider: 'fcm';\n\tserviceAccount: Record<string, unknown>;\n}\n\nexport const Channel = {\n\tEMAIL: 'email',\n\tSMS: 'sms',\n\tPUSH: 'push',\n} as const satisfies Record<string, 'email' | 'sms' | 'push'>;\n\nexport interface ICourierConfig {\n\t// Which channels handle which message types\n\t// e.g. { 'password-reset': ['email'], 'new-message': ['push', 'sms'] }\n\tchannels: Record<string, Array<'email' | 'sms' | 'push'>>;\n\n\tsms?: ISmsChannelConfig;\n\tpush?: IPushChannelConfig;\n\temail?: IEmailChannelConfig;\n\n\t// When set, exposes Bearer-guarded template admin routes (/admin/templates/*)\n\t// for versioned edit/history/rollback. Requires @fonderie/store (db templates).\n\tadminToken?: string;\n\n\t// Where templates are loaded from\n\t// 'db' requires @fonderie/store to be configured\n\t// 'fs' reads from a local directory\n\ttemplates?: {\n\t\tsource: 'db' | 'fs';\n\t\tdirectory?: string; // for 'fs' source\n\t\t// Module-shipped default templates. The app imports each module's\n\t\t// DEFAULT_TEMPLATES (auth, workspaces, billing, …) and passes them here;\n\t\t// the resolver falls back to a default when no app override (DB row / FS\n\t\t// file) exists, before the last-resort JSON dump. Aggregated like\n\t\t// getMigrationsPath() — pass one map or an array of them.\n\t\tdefaults?: DefaultTemplateMap | DefaultTemplateMap[];\n\t};\n\n\t// Inbound delivery-webhook verification. FAIL-CLOSED: a provider's route is\n\t// only registered when its key is set (an unverified endpoint would accept\n\t// forged delivered/opened/bounced events from anyone who finds the URL).\n\tdelivery?: {\n\t\tsigningKeys?: {\n\t\t\t// SendGrid signs with ECDSA, not HMAC: this is the base64\n\t\t\t// \"Verification Key\" (public key) from Settings → Mail Settings →\n\t\t\t// Event Webhook → Signature Verification.\n\t\t\tsendgrid?: string;\n\t\t\tmailgun?: string; // Mailgun HTTP webhook signing key (HMAC-SHA256)\n\t\t};\n\t\t// Mailtrap's webhook has NO signature scheme, so its route accepts\n\t\t// forged events by construction. Explicit dev/test opt-in only.\n\t\tallowUnverifiedMailtrap?: boolean;\n\t};\n}\n"],"mappings":"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","names":["body","body","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;\">&nbsp;</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, '&amp;')\n\t\t.replace(/</g, '&lt;')\n\t\t.replace(/>/g, '&gt;')\n\t\t.replace(/\"/g, '&quot;')\n\t\t.replace(/'/g, '&#39;');\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": "6.0.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",
@@ -51,7 +51,7 @@
51
51
  "nodemailer": "^10.0.1"
52
52
  },
53
53
  "peerDependencies": {
54
- "@fonderie/core": "^0.9.0",
54
+ "@fonderie/core": "^0.10.0",
55
55
  "@fonderie/events": "^5.0.0",
56
56
  "@fonderie/store": "^0.3.0"
57
57
  },