{"version":3,"sources":["../src/index.ts","../src/module.ts","../src/dispatcher.ts","../src/format.ts","../src/log.ts","../src/channels/sms.ts","../src/channels/push.ts","../src/channels/email.ts","../src/templates/resolver.ts","../src/templates/layout.ts","../src/config-guard.ts","../src/delivery.ts","../src/templates/admin-routes.ts","../src/templates/admin.ts","../src/sender-dns.ts","../src/eraser.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 { messageStats } from './log';\nexport type { IMessageStats } from './log';\n// Account deletion — the purge redacts the person's recipient in the message log.\nexport { accountEraser } from './eraser';\nexport type { ICourierAccountEraser, ICourierErasureResult, ICourierErasureSubject } from './eraser';\nexport { SmsChannel } from './channels/sms';\nexport { PushChannel } from './channels/push';\nexport { EmailChannel } from './channels/email';\nexport {\n\tDBTemplateResolver,\n\tFSTemplateResolver,\n\tDefaultTemplates,\n\trenderFragment,\n\tgetLayoutHtml,\n\ttemplateVariables,\n} 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 { ITemplateAdminOptions } 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\n// The sending domain's SPF/DKIM/DMARC live in public DNS, owned by whoever runs\n// the domain — courier only declares a `from`. Nothing connects the two, and a\n// mismatch is not a send failure: the RECEIVER drops or spam-files the message,\n// so there is no bounce and no log. DNS-only by design, so it needs no provider\n// API and no credentials and works for any SMTP backend.\nexport { checkSenderDns, describeSenderDnsProblems, senderDnsCheck, senderDomain } from './sender-dns';\nexport type { ISenderDnsRecord, ISenderDnsReport, ResolveTxt } from './sender-dns';\n","import type { IAdminDescription, IFonderieModule, IFonderieApp, ILocaleSettings } from '@fonderie/core';\nimport { defineLocales } 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, describeTemplateAdminRoutes } from './templates/admin-routes';\nimport type { ITemplateAdminOptions } from './templates/admin-routes';\nimport { senderDnsCheck } from './sender-dns';\n\nexport class CourierModule implements IFonderieModule {\n\treadonly name = '@fonderie/courier';\n\t// Baked in at build time by tsup.base, so the operator's Modules page can\n\t// say what is actually deployed rather than 'not reported'.\n\treadonly version = process.env['FONDERIE_PKG_VERSION'] ?? '0.0.0-dev';\n\treadonly deps = ['@fonderie/events'];\n\treadonly dispatcher: Dispatcher;\n\tprivate readonly resolver: ITemplateResolver;\n\t// The app's locales arrive at install; until then (describeAdmin runs from\n\t// constructor state) the core default, which is what an app without a\n\t// `locales` config gets anyway.\n\tprivate locales: ILocaleSettings = defineLocales();\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\tthis.resolver = createTemplateResolver(templateSource, config, store);\n\n\t\tthis.dispatcher = new Dispatcher(config, this.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// Conditional spread rather than `{ brandName: this.config.brandName }` —\n\t// exactOptionalPropertyTypes rejects an explicit undefined here.\n\tprivate brandOpts(): ITemplateAdminOptions {\n\t\t// The module-shipped default types are system templates too: their\n\t\t// default-locale row can be edited, never deleted.\n\t\tconst input = this.config.templates?.defaults;\n\t\tconst defaults = new DefaultTemplates(input ? (Array.isArray(input) ? input : [input]) : []);\n\t\treturn {\n\t\t\t...(this.config.brandName ? { brandName: this.config.brandName } : {}),\n\t\t\tsystemTypes: defaults.types(),\n\t\t\tdefaults,\n\t\t\t// A getter: routes are described before install, and read the locales\n\t\t\t// per request, after install has set them.\n\t\t\tlocales: () => this.locales,\n\t\t};\n\t}\n\n\t// Template admin needs db templates; the DNS check needs an email channel.\n\tdescribeAdmin(): IAdminDescription {\n\t\tconst out: IAdminDescription = {};\n\t\t// brandName travels with the routes: the preview renders the shell, and on a\n\t\t// real send the Dispatcher — not the resolver — is what merges it in.\n\t\tif (this.store) out.routes = describeTemplateAdminRoutes(this.store, this.brandOpts());\n\t\tif (this.config.email) out.checks = [senderDnsCheck(this.config.email)];\n\t\treturn out;\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// Core owns the locales: which one the default copy is in, and each\n\t\t// market's fallback chain. The resolver follows them on every send.\n\t\tthis.locales = app.locales ?? defineLocales();\n\t\tthis.resolver.setLocales?.(this.locales);\n\t\tthis.dispatcher.setLocales(this.locales);\n\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(\n\t\t\t\tstore,\n\t\t\t\tthis.config.adminToken,\n\t\t\t\tthis.brandOpts(),\n\t\t\t)) {\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\nimport type { ILocaleSettings } from '@fonderie/core';\nimport { defineLocales } from '@fonderie/core';\n\nimport type { ICourierConfig } from './config';\nimport { applyFormats } from './format';\nimport type { ICourierMessage, ICourierChannel, ITemplateResolver } from './types';\nimport {\n\tinsertMessageLog,\n\tmarkMessageSent,\n\tmarkMessageFailed,\n\tsetMessageResolvedLocale,\n} 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\tprivate locales: ILocaleSettings = defineLocales();\n\n\t/** The app's locales — the default language when a message names none. */\n\tsetLocales(locales: ILocaleSettings): void {\n\t\tthis.locales = locales;\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\t/**\n\t * The language of the account the recipient's address belongs to — read\n\t * from @fonderie/auth's users table when it is in the same database. No\n\t * auth, no account, or `recipientLocaleLookup: false`: undefined.\n\t */\n\tprivate async accountLocale(message: ICourierMessage): Promise<string | undefined> {\n\t\tif (!this.store || this.config.recipientLocaleLookup === false) return undefined;\n\t\tconst { email, phone } = message.recipient;\n\t\tif (!email && !phone) return undefined;\n\t\ttry {\n\t\t\tconst [row] = await this.store.query<{ locale: string | null }>(\n\t\t\t\t`SELECT locale FROM fonderie_users\n\t\t\t\t WHERE ($1::text IS NOT NULL AND lower(email) = lower($1))\n\t\t\t\t    OR ($2::text IS NOT NULL AND phone = $2)\n\t\t\t\t ORDER BY (lower(email) = lower($1)) DESC NULLS LAST\n\t\t\t\t LIMIT 1`,\n\t\t\t\t[email ?? null, phone ?? null],\n\t\t\t);\n\t\t\treturn row?.locale || undefined;\n\t\t} catch {\n\t\t\treturn undefined; // no users table here (courier without auth)\n\t\t}\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\t// The configured product name is available to EVERY template without each\n\t\t// call site remembering to pass it. Spread first so a message that supplies\n\t\t// its own brandName still wins (a multi-tenant app may brand per workspace).\n\t\tconst withBrand = this.config.brandName\n\t\t\t? { brandName: this.config.brandName, ...message.data }\n\t\t\t: message.data;\n\t\t// Whose language: the sender's explicit choice, else the recipient's own\n\t\t// account, else the sender's fallback (the business's), else the default.\n\t\t// Senders without a session (billing webhooks, invitations) used to pass\n\t\t// nothing, so a French-speaking customer got their receipt in English.\n\t\tconst locale = message.locale ?? (await this.accountLocale(message)) ?? message.fallbackLocale;\n\t\t// Amounts and dates the sender marked under $format, in that same\n\t\t// language: 19,99 $ for fr-CA, $19.99 for en-CA — not one fixed locale.\n\t\tconst data = applyFormats(withBrand, locale ?? this.locales.default);\n\t\tconst template = await this.resolver.resolve(message.type, data, 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 (locale) logEntry.locale = 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\t\t\t\tif (this.store && logId && template.locale) {\n\t\t\t\t\tawait setMessageResolvedLocale(logId, template.locale, this.store).catch(() => undefined);\n\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// The provider id and the 'sent' status in ONE write — delivery\n\t\t\t\t\t\t// webhooks key on the id, and they can arrive within seconds of\n\t\t\t\t\t\t// the send; a row never carries the id while still 'pending'.\n\t\t\t\t\t\t// AWAITED, like the failure write below. Detaching these left\n\t\t\t\t\t\t// the row at 'pending' forever whenever the process stopped\n\t\t\t\t\t\t// before the write landed — which on serverless is routine,\n\t\t\t\t\t\t// since the instance is frozen the moment the handler returns.\n\t\t\t\t\t\t// The send had happened; only the record of it was lost, so\n\t\t\t\t\t\t// the log under-reported successes exactly where it is the\n\t\t\t\t\t\t// only evidence a send occurred. Awaiting costs nothing that\n\t\t\t\t\t\t// matters: this runs in the consumer, not the request path.\n\t\t\t\t\t\tawait markMessageSent(logId, this.store, result?.providerMessageId).catch(\n\t\t\t\t\t\t\t() => undefined,\n\t\t\t\t\t\t);\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\tawait markMessageFailed(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 { COURIER_FORMAT_KEY, type ICourierFormatValue } from '@fonderie/core';\n\n/**\n * Format the values a message marked under `$format` in the reader's language,\n * replacing the plain-string versions the sender sent under the same keys.\n * `$format` itself is dropped. Anything that cannot be formatted keeps the\n * sender's string — a notification must never fail over a format.\n */\nexport function applyFormats(data: Record<string, unknown>, locale: string): Record<string, unknown> {\n\tconst formats = data[COURIER_FORMAT_KEY] as Record<string, ICourierFormatValue> | undefined;\n\tif (!formats || typeof formats !== 'object') return data;\n\tconst out: Record<string, unknown> = { ...data };\n\tdelete out[COURIER_FORMAT_KEY];\n\tfor (const [key, value] of Object.entries(formats)) {\n\t\tconst formatted = formatOne(value, locale);\n\t\tif (formatted !== undefined) out[key] = formatted;\n\t}\n\treturn out;\n}\n\nfunction formatOne(value: ICourierFormatValue, locale: string): string | undefined {\n\ttry {\n\t\tif ('money' in value) {\n\t\t\tconst { amount, currency, precision } = value.money;\n\t\t\tconst major = Number(amount) / 10 ** precision;\n\t\t\tif (!Number.isFinite(major)) return undefined;\n\t\t\ttry {\n\t\t\t\treturn new Intl.NumberFormat(locale, {\n\t\t\t\t\tstyle: 'currency',\n\t\t\t\t\tcurrency,\n\t\t\t\t\tminimumFractionDigits: precision,\n\t\t\t\t\tmaximumFractionDigits: precision,\n\t\t\t\t}).format(major);\n\t\t\t} catch {\n\t\t\t\t// Not an ISO currency (a credits wallet's 'CREDITS'): the number in the\n\t\t\t\t// reader's style, then the unit.\n\t\t\t\tconst n = new Intl.NumberFormat(locale, { minimumFractionDigits: precision, maximumFractionDigits: precision }).format(major);\n\t\t\t\treturn `${n} ${currency}`;\n\t\t\t}\n\t\t}\n\t\tif ('date' in value) {\n\t\t\tconst d = new Date(value.date);\n\t\t\tif (Number.isNaN(d.getTime())) return undefined;\n\t\t\treturn new Intl.DateTimeFormat(locale, { dateStyle: value.style ?? 'long' }).format(d);\n\t\t}\n\t} catch {\n\t\treturn undefined;\n\t}\n\treturn undefined;\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\t/** The version sent: a saved row's tag, a built-in's language, or the system locale. */\n\tresolvedLocale: 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// Separate from the insert on purpose: an app deployed before its migration ran\n// has no resolved_locale column, and folding it into the INSERT would lose the\n// whole log row — the only record the send happened — over a missing detail.\nexport async function setMessageResolvedLocale(\n\tid: string,\n\tresolvedLocale: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET resolved_locale = $2\n\t\t WHERE id = $1`,\n\t\t[id, resolvedLocale],\n\t);\n}\n\n// The send's outcome, in ONE statement: the provider's message id (only known\n// once the provider responds — delivery webhooks match on it, so without it\n// every delivered/opened/bounced update targets zero rows) together with the\n// 'sent' status. As two writes, a failure between them left a row 'pending'\n// that already carried a provider id — a message that went out, recorded as\n// one that never did.\nexport async function markMessageSent(\n\tid: string,\n\tstore: IStoreAdapter,\n\tproviderMessageId?: string | null,\n): 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     provider_message_id = COALESCE($2, provider_message_id)\n\t\t WHERE id = $1`,\n\t\t[id, providerMessageId ?? null],\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\nexport interface IMessageStats {\n\t/** Successfully handed to the provider in the window. */\n\tsent: number;\n\t/** Rejected by the provider in the window. */\n\tfailed: number;\n\t/**\n\t * Logged but never resolved either way. Should be ~0; a growing number\n\t * means sends are being started and abandoned.\n\t */\n\tpending: number;\n\tsentAt: Date | null;\n\tfailedAt: Date | null;\n\t/** The most recent provider error, which is usually the whole diagnosis. */\n\tlastError: string | null;\n}\n\n/**\n * What actually happened to outbound messages, over the last `hours`.\n *\n * This is the ONLY record of whether a message was sent, and reading it is not\n * optional for anyone who wants to know. A send failure is caught by the\n * dispatcher and deliberately not rethrown — a bad address must not poison the\n * event — so the event bus marks its row `processed` whether the message left\n * or not. An SMTP rejection and a clean send are indistinguishable in the\n * queue, and `deadLetters()` stays empty no matter how badly email is failing.\n *\n * Note `sent` means the provider ACCEPTED it. A provider that accepts and then\n * bounces asynchronously (an unverified sending domain, typically) looks like\n * success here; its dashboard is the source of truth for that.\n */\nexport async function messageStats(\n\tstore: IStoreAdapter,\n\toptions: { hours?: number } = {},\n): Promise<IMessageStats> {\n\tconst rows = await store.query<{\n\t\tstatus: string;\n\t\tcount: string;\n\t\tlast: Date | null;\n\t\terr: string | null;\n\t}>(\n\t\t`SELECT status, count(*)::text AS count, max(created_at) AS last,\n\t\t        (array_agg(error ORDER BY created_at DESC)\n\t\t           FILTER (WHERE error IS NOT NULL))[1] AS err\n\t\t   FROM fonderie_message_log\n\t\t  WHERE created_at > now() - make_interval(hours => $1)\n\t\t  GROUP BY status`,\n\t\t[options.hours ?? 24],\n\t);\n\tconst of = (s: string) => rows.find((r) => r.status === s);\n\tconst sent = of('sent');\n\tconst failed = of('failed');\n\treturn {\n\t\tsent: Number(sent?.count ?? 0),\n\t\tfailed: Number(failed?.count ?? 0),\n\t\tpending: Number(of('pending')?.count ?? 0),\n\t\tsentAt: sent?.last ? new Date(sent.last) : null,\n\t\tfailedAt: failed?.last ? new Date(failed.last) : null,\n\t\tlastError: failed?.err ?? null,\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\t...(this.config.replyTo ? { reply_to: this.config.replyTo } : {}),\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\t...(this.config.replyTo ? { replyTo: this.config.replyTo } : {}),\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","import { canonicalLocale, defineLocales, localeChain, localeCopyKeys } from '@fonderie/core';\nimport type { IDefaultTemplateCopy, ILocaleSettings } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type {\n\tITemplateResolver,\n\tIRenderedTemplate,\n\tIDefaultTemplate,\n\tDefaultTemplateMap,\n} from '../types';\nimport { EMAIL_THEME, defaultEmailLayout, 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\n// The template contract in one place: a variable is {{word}}, a section is\n// {{#word}}…{{/word}}. \\w+ only — {{ spaced }} and {{dotted.path}} are left\n// alone, deliberately. Named because the renderer is no longer the only thing\n// that reads it: an editor asking \"which variables does this use?\" must agree\n// with what substitution will actually do, or it offers the wrong fields.\nconst VAR_RE = /\\{\\{(\\w+)\\}\\}/g;\nconst SECTION_RE = /\\{\\{#(\\w+)\\}\\}([\\s\\S]*?)\\{\\{\\/\\1\\}\\}/g;\n\n// Every variable a template refers to, in first-seen order — section names\n// included, since a section's presence is driven by the same data key.\n// Implicit ones the layout injects ({{subject}}, {{preheader}}, {{brandName}})\n// are NOT filtered here; the caller knows whether it supplies them.\nexport function templateVariables(...parts: Array<string | null | undefined>): string[] {\n\tconst found = new Set<string>();\n\tfor (const part of parts) {\n\t\tif (!part) continue;\n\t\t// Fresh regexes: the shared ones carry /g, so lastIndex would leak\n\t\t// between calls.\n\t\tfor (const m of part.matchAll(new RegExp(SECTION_RE.source, 'g'))) found.add(m[1] as string);\n\t\tfor (const m of part.matchAll(new RegExp(VAR_RE.source, 'g'))) found.add(m[1] as string);\n\t}\n\treturn [...found];\n}\n\n/**\n * Optional blocks: `{{#key}}…{{/key}}` renders its body only when `key` has a\n * non-empty value.\n *\n * Needed because a template with no conditionals cannot omit anything, and some\n * fields are genuinely absent rather than empty — an invoice number exists only\n * when the charge went through an invoice. Without this, the choices are a\n * dangling \"Invoice \" with nothing after it, or an anchor with an empty href\n * that looks like a link and does nothing. Both are worse than saying less.\n *\n * Deliberately NOT a general template language: one construct, no nesting of the\n * same key, no expressions. Anything more and app authors start putting logic in\n * templates, which is how email rendering becomes unreviewable.\n *\n * Runs BEFORE variable substitution, so a value inside a section still\n * interpolates normally. Whitespace-only counts as absent — a provider returning\n * \"\" and one returning \"  \" should not render differently.\n */\nfunction renderSections(template: string, data: Record<string, unknown>): string {\n\treturn template.replace(SECTION_RE, (_, key: string, body: string) => {\n\t\tconst value = data[key];\n\t\tconst present = value !== undefined && value !== null && String(value).trim() !== '';\n\t\treturn present ? body : '';\n\t});\n}\n\nfunction render(\n\ttemplate: string,\n\tdata: Record<string, unknown>,\n\topts: { escapeHtml?: boolean } = {},\n): string {\n\treturn renderSections(template, data).replace(VAR_RE, (_, 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.\n//\n// `brandName` is the product the RECIPIENT believes they are hearing from — the\n// app built on Fonderie, not Fonderie itself. A user who signed up for\n// the app has never heard of Fonderie, so an email headed \"Fonderie\" reads\n// as a different company at best and a phishing attempt at worst. Apps pass\n// their own name; anything that does not falls back to EMAIL_THEME.brand, so\n// the shell is never left with an empty heading.\n//\n// It interpolates like any other variable, which means it is HTML-escaped along\n// with the rest — an app name containing & or < cannot break the shell.\nfunction composeHtml(\n\tbodyHtml: string,\n\tlayoutHtml: string | undefined,\n\tsubject: string | undefined,\n\tdata: Record<string, unknown>,\n\tlanguage?: string,\n): string {\n\t// No operator shell → the built-in one, in the email's own language.\n\tconst wrapped = wrapLayout(bodyHtml, layoutHtml ?? defaultEmailLayout(language));\n\treturn render(\n\t\twrapped,\n\t\t{ subject: subject ?? '', preheader: '', brandName: EMAIL_THEME.brand, ...data },\n\t\t{ escapeHtml: true },\n\t);\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\t// The locale this copy is in: names the built-in shell's language, and is\n\t// reported as the version sent.\n\tlocale?: string,\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, locale) } : {}),\n\t\t...(locale ? { locale } : {}),\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\t/**\n\t * The built-in copy of `type` in `locale`: its exact key, else its language\n\t * ('fr-CA' → 'fr'). Built-in copy has no market-specific terms, so the\n\t * language is safe here where it would not be for an app's own versions.\n\t * Returns the matched key with it — that is the version reported as sent.\n\t */\n\tgetLocalized(type: string, locale: string): { copy: IDefaultTemplateCopy; locale: string } | undefined {\n\t\tconst all = this.map.get(type)?.locales;\n\t\tif (!all) return undefined;\n\t\tfor (const key of localeCopyKeys(locale)) {\n\t\t\tconst copy = all[key];\n\t\t\tif (copy) return { copy, locale: key };\n\t\t}\n\t\treturn undefined;\n\t}\n\t/** Every language a built-in email ships in besides English. */\n\tlanguages(type: string): string[] {\n\t\treturn Object.keys(this.map.get(type)?.locales ?? {});\n\t}\n\tget size(): number {\n\t\treturn this.map.size;\n\t}\n\t/** Every type a module ships a default for. */\n\ttypes(): string[] {\n\t\treturn [...this.map.keys()];\n\t}\n}\n\n// DB-backed resolver. First match wins, for a user in fr-FR whose app declares\n// fr-FR → fr-CA:\n//\n//   1 saved fr-FR, 2 saved fr-CA   the app's versions, along the declared chain\n//   3 built-in French              Fonderie's copy, matched by language\n//   4 saved default                the system locale's version (untagged row)\n//   5 built-in English\n//\n// The system locale comes LAST on purpose: every built-in email also has a\n// saved default row, so trying it before step 3 would send a French user the\n// app's English instead of the French that ships. An app's versions never fall\n// to a sibling market on their own — only along a chain it declared.\nexport class DBTemplateResolver implements ITemplateResolver {\n\tprivate locales: ILocaleSettings = defineLocales();\n\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate defaults?: DefaultTemplates,\n\t) {}\n\n\t/** The app's locales, handed over at install (core owns them). */\n\tsetLocales(settings: ILocaleSettings): void {\n\t\tthis.locales = settings;\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 choice = await chooseCopy(this.store, this.defaults, this.locales, type, locale);\n\t\t// Neither the app nor any module provides this type: the JSON dump.\n\t\tif (!choice) return { text: `${type}: ${JSON.stringify(data)}` };\n\t\treturn this.renderWith(choice.copy, data, choice.sent);\n\t}\n\n\tprivate async renderWith(\n\t\tfrag: { subject?: string | null; text: string; html?: string | null },\n\t\tdata: Record<string, unknown>,\n\t\tsent: string,\n\t): Promise<IRenderedTemplate> {\n\t\tconst layoutHtml = frag.html\n\t\t\t? await getLayoutHtml(this.store, sent === this.locales.default ? undefined : sent, this.locales)\n\t\t\t: undefined;\n\t\treturn renderFragment(frag, layoutHtml, data, sent);\n\t}\n}\n\n/** Which copy a send in `locale` uses, and why — shared by sends and the console. */\nexport interface ICopyChoice {\n\tcopy: { subject?: string | null; text: string; html?: string | null };\n\t/** The version used: a saved row's tag, a built-in's language, or the system locale. */\n\tsent: string;\n\tsource: 'saved' | 'built-in';\n\t/** The locales tried before the system locale, in order. */\n\tchain: string[];\n}\n\n/**\n * The one decision both a real send and the console's \"who receives what\" make,\n * so the two can never disagree. First match wins: saved along the chain,\n * built-in by language along the chain, saved default, built-in English.\n */\nexport async function chooseCopy(\n\tstore: IStoreAdapter,\n\tdefaults: DefaultTemplates | undefined,\n\tsettings: ILocaleSettings,\n\ttype: string,\n\tlocale?: string,\n): Promise<ICopyChoice | undefined> {\n\tconst chain = localeChain(locale, settings);\n\tconst row = await pickRow(store, type, chain);\n\tif (row?.locale) return { copy: row, sent: row.locale, source: 'saved', chain };\n\tfor (const tag of chain) {\n\t\tconst shipped = defaults?.getLocalized(type, tag);\n\t\tif (shipped) return { copy: shipped.copy, sent: shipped.locale, source: 'built-in', chain };\n\t}\n\tif (row) return { copy: row, sent: settings.default, source: 'saved', chain };\n\tconst def = defaults?.get(type);\n\tif (def) return { copy: def, sent: settings.default, source: 'built-in', chain };\n\treturn undefined;\n}\n\ninterface IStoredCopy {\n\tlocale: string | null;\n\tsubject: string | null;\n\thtml: string | null;\n\ttext: string;\n}\n\n// The first active row along `chain`, else the untagged default row, in one\n// query. Tags compare case-insensitively: rows saved before canonicalization\n// may say 'fr-ca'. Returns the row with its canonical tag (null = default).\nasync function pickRow(store: IStoreAdapter, type: string, chain: string[]): Promise<IStoredCopy | undefined> {\n\tconst rows = await store.query<IStoredCopy>(\n\t\t`SELECT locale, subject, html, text\n\t\t FROM fonderie_courier_templates\n\t\t WHERE type = $1 AND active = true AND (locale IS NULL OR lower(locale) = ANY($2::text[]))`,\n\t\t[type, chain.map((t) => t.toLowerCase())],\n\t);\n\tfor (const tag of chain) {\n\t\tconst hit = rows.find((r) => r.locale != null && canonicalLocale(r.locale) === tag);\n\t\tif (hit) return { ...hit, locale: tag };\n\t}\n\tconst base = rows.find((r) => r.locale == null);\n\treturn base ? { ...base, locale: null } : undefined;\n}\n\n// The operator's shell for this locale — the first saved _layout along its\n// chain, else the untagged one — or undefined to mean \"the built-in shell\",\n// which the renderer then draws in the email's language.\n//\n// Exported because the admin preview has to reach it: rendering a fragment\n// without the shell produces an email that looks nothing like the one that\n// sends, and a preview that lies is worse than no preview. One definition\n// rather than a second copy of the query living in the route file.\nexport async function getLayoutHtml(\n\tstore: IStoreAdapter,\n\tlocale?: string,\n\tsettings: ILocaleSettings = defineLocales(),\n): Promise<string | undefined> {\n\tconst row = await pickRow(store, LAYOUT_TYPE, localeChain(locale, settings));\n\treturn row?.html ?? undefined;\n}\n\n// Filesystem resolver — {type}.{locale}.txt|.html|.subject.txt, then {type}.*,\n// with the same order as the DB resolver. A locale's files are one version: if\n// any of them exists that locale is used, and a part it lacks is NOT borrowed\n// from another locale — mixing a French body with the default subject is how\n// half-translated mail goes out.\nexport class FSTemplateResolver implements ITemplateResolver {\n\tprivate locales: ILocaleSettings = defineLocales();\n\n\tconstructor(\n\t\tprivate directory: string,\n\t\tprivate defaults?: DefaultTemplates,\n\t) {}\n\n\tsetLocales(settings: ILocaleSettings): void {\n\t\tthis.locales = settings;\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\t\tconst readSet = async (prefix: string) => {\n\t\t\tconst [text, html, subject] = await Promise.all([\n\t\t\t\treadOptional(join(this.directory, `${prefix}.txt`)),\n\t\t\t\treadOptional(join(this.directory, `${prefix}.html`)),\n\t\t\t\treadOptional(join(this.directory, `${prefix}.subject.txt`)),\n\t\t\t]);\n\t\t\t// Presence (=== null), not truthiness: an app that ships even an empty\n\t\t\t// file keeps control of that version, mirroring the DB's row check.\n\t\t\treturn text === null && html === null && subject === null ? null : { text, html, subject };\n\t\t};\n\t\tconst layoutFor = async (sent: string): Promise<string | undefined> => {\n\t\t\tconst tags = sent === this.locales.default ? [] : localeChain(sent, this.locales);\n\t\t\tfor (const tag of tags) {\n\t\t\t\tconst v = await readOptional(join(this.directory, `${LAYOUT_TYPE}.${tag}.html`));\n\t\t\t\tif (v !== null) return v;\n\t\t\t}\n\t\t\treturn (await readOptional(join(this.directory, `${LAYOUT_TYPE}.html`))) ?? undefined;\n\t\t};\n\t\tconst system = this.locales.default;\n\t\tconst chain = localeChain(locale, this.locales);\n\n\t\tlet files: Awaited<ReturnType<typeof readSet>> = null;\n\t\tlet sent = system;\n\t\tfor (const tag of chain) {\n\t\t\tfiles = await readSet(`${type}.${tag}`);\n\t\t\tif (files) {\n\t\t\t\tsent = tag;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (!files) {\n\t\t\tfor (const tag of chain) {\n\t\t\t\tconst shipped = this.defaults?.getLocalized(type, tag);\n\t\t\t\tif (shipped) {\n\t\t\t\t\tconst layout = shipped.copy.html ? await layoutFor(shipped.locale) : undefined;\n\t\t\t\t\treturn renderFragment(shipped.copy, layout, data, shipped.locale);\n\t\t\t\t}\n\t\t\t}\n\t\t\tfiles = await readSet(type);\n\t\t}\n\n\t\t// App shipped NOTHING for this type → the module default, in the app's own\n\t\t// _layout shell if it ships one, then the JSON dump.\n\t\tif (!files) {\n\t\t\tconst def = this.defaults?.get(type);\n\t\t\tif (def) return renderFragment(def, def.html ? await layoutFor(system) : undefined, data, system);\n\t\t\treturn { text: `${type}: ${JSON.stringify(data)}` };\n\t\t}\n\n\t\tconst { text, html, subject } = files;\n\t\tconst renderedSubject = subject ? render(subject, data) : undefined;\n\t\tconst layout = html ? await layoutFor(sent) : undefined;\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, renderedSubject, data, sent) } : {}),\n\t\t\tlocale: sent,\n\t\t};\n\t}\n}\n","// Shared, composable email layout — a branded shell that wraps every template\n// body. Templates stored in the DB or on disk are **body fragments**; the\n// resolver injects them into `{{content}}` here, then interpolates `{{vars}}`\n// over the whole. One shell, many bodies — so DB and FS sources render an\n// identical, production-grade frame for free.\n//\n// Cross-client hardening (Outlook/Gmail/Apple Mail) follows the well-worn\n// \"hybrid responsive\" pattern popularised by Cerberus (MIT): a max-width table\n// container, inline styles on the critical structure, and a `<style>` block in\n// `<head>` for the friendly classes and small-screen media query. Founders can\n// override the whole shell by storing a `_layout` template (DB row or\n// `_layout.html` file) in their own source.\n//\n// The theme tokens mirror the product design system (organization/ui/base.css):\n// near-black primary, a mint brand accent, Inter, and the `#fafafa` canvas —\n// retune `EMAIL_THEME` to rebrand every email at once.\n\n// THE ATTRIBUTION ROW\n//\n// \"Powered by Fonderie\" renders below the card, on the canvas rather than inside\n// the frame, and links to the project site.\n//\n// The link is what makes the attribution work at all. \"Fonderie\" is a common\n// French noun, so a reader who is curious and searches it finds metal foundries;\n// without somewhere to click, the line is decoration. Linking the brand word to\n// its own domain is also the shape filters expect — the deceptive-mismatch\n// signal is about text that IMPERSONATES a different destination, not a name\n// pointing at its own site.\n//\n// The cost to weigh: link-domain reputation is a spam signal, so one domain in\n// every message from every app built on Fonderie couples their sender\n// reputations. That matters at fleet scale; revisit this if third parties ship\n// on Fonderie in volume.\n//\n// (Mail clients never FETCH links — only images trigger remote-content blocking\n// — so this is about spam scoring, not asset loading. The shell has no images.)\n//\n// Every anchor carries target=\"_blank\" rel=\"noopener noreferrer\".\n//\n// Webmail (Gmail, Outlook web) usually opens links in a new tab regardless, and\n// a desktop client hands off to the system browser — so this is belt and braces\n// rather than the only thing keeping the reader in their inbox. It costs\n// nothing, and the case it covers is real: a client that navigates in place\n// takes the reader AWAY from the message, and an email is not a page you can\n// press Back to.\n//\n// rel is set for the same reason it is on the web: never hand a target window a\n// live `opener` reference.\n\n// Keep explanations like this OUT of the template literal below: anything inside\n// it is HTML that ships to recipients.\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\n// The shell's own words, per language. Placeholders rather than a function so\n// the template below stays one readable document; filled in by\n// defaultEmailLayout(). {{brandName}} stays a template variable — it\n// interpolates (escaped) with the rest at render time.\nconst LAYOUT_LANG = '__LAYOUT_LANG__';\nconst LAYOUT_FOOTER = '__LAYOUT_FOOTER__';\nconst LAYOUT_POWERED_BY = '__LAYOUT_POWERED_BY__';\n\nconst LAYOUT_TEMPLATE = `<!DOCTYPE html>\n<html lang=\"${LAYOUT_LANG}\" 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.email-powered { padding: 18px 8px 0 8px; text-align: center; }\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;\">{{brandName}}</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\t${LAYOUT_FOOTER}\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\t<tr><td class=\"email-powered\" style=\"padding:18px 8px 0 8px;text-align:center;\">\n\t\t\t\t\t\t<span style=\"font:400 12px/1.5 ${FONT_SANS};color:${EMAIL_THEME.muted};letter-spacing:-0.01em;\">${LAYOUT_POWERED_BY} <a href=\"https://fonderiejs.com\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:${EMAIL_THEME.muted};text-decoration:underline;\">Fonderie</a></span>\n\t\t\t\t\t</td></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/** Languages the built-in shell speaks; anything else gets English. */\nexport const LAYOUT_STRINGS: Readonly<Record<string, { footer: string; poweredBy: string }>> = {\n\ten: {\n\t\tfooter:\n\t\t\t\"You're receiving this because someone used this address at {{brandName}}. If that wasn't you, you can ignore it.\",\n\t\tpoweredBy: 'Powered by',\n\t},\n\tfr: {\n\t\tfooter:\n\t\t\t'Vous recevez ce message parce que quelqu’un a utilisé cette adresse sur {{brandName}}. Si ce n’était pas vous, vous pouvez l’ignorer.',\n\t\tpoweredBy: 'Propulsé par',\n\t},\n\tes: {\n\t\tfooter:\n\t\t\t'Recibes este mensaje porque alguien usó esta dirección en {{brandName}}. Si no fuiste tú, puedes ignorarlo.',\n\t\tpoweredBy: 'Con la tecnología de',\n\t},\n};\n\n/**\n * The built-in shell in `language` ('fr', or a full tag like 'fr-CA'), English\n * when the shell does not speak it. A French email in an English shell reads as\n * a machine translation of half the message.\n */\nexport function defaultEmailLayout(language = 'en'): string {\n\tconst lang = language.split('-')[0]?.toLowerCase() ?? 'en';\n\tconst words = LAYOUT_STRINGS[lang] ?? (LAYOUT_STRINGS['en'] as { footer: string; poweredBy: string });\n\tconst tag = LAYOUT_STRINGS[lang] ? lang : 'en';\n\t// Function replacers: a `$` in the copy must not be read as a pattern.\n\treturn LAYOUT_TEMPLATE.replace(LAYOUT_LANG, () => tag)\n\t\t.replace(LAYOUT_FOOTER, () => words.footer)\n\t\t.replace(LAYOUT_POWERED_BY, () => words.poweredBy);\n}\n\n/** The built-in shell in English — kept for callers that predate languages. */\nexport const DEFAULT_EMAIL_LAYOUT = defaultEmailLayout('en');\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 { 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\tconst problems: IReadinessProblem[] = [...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\tdomain: 'courier',\n\t\treason: 'CHANNEL_WITHOUT_PROVIDER',\n\t\tmetadata: { count: types.length, channel, types: types.join(', ') },\n\t}));\n\n\t// The INVERSE gap, and the quieter one: a message type whose CONTENT is\n\t// registered but which is routed nowhere. `dispatch` logs 'no channels\n\t// configured' and returns, so the notice is published, never delivered and\n\t// never retried — while the outbox reports success, because the event WAS\n\t// consumed. Nothing anywhere says a user did not get their mail.\n\t//\n\t// Shipping a package's default templates is a statement that the app intends\n\t// to send those notices, so a key with content but no route is almost always\n\t// drift: the package added a notice and the hand-written channel map did not\n\t// follow. Caught at BOOT rather than when the first user happens to trigger\n\t// it — and surfaced through checkReadiness, so it also shows up wherever the\n\t// app already reports health.\n\tconst declared = new Set<string>();\n\tconst defaults = config.templates?.defaults;\n\tfor (const map of defaults ? (Array.isArray(defaults) ? defaults : [defaults]) : []) {\n\t\tfor (const key of Object.keys(map ?? {})) declared.add(key);\n\t}\n\t// Membership, not contents: a type mapped to an EMPTY list is a deliberate\n\t// \"this app does not send that one\" (phone OTP in an email-only product, say)\n\t// and must not be nagged about. An ABSENT key is the drift. That distinction\n\t// is the difference between a guard people act on and one they mute.\n\tconst routed = new Set(Object.keys(config.channels ?? {}));\n\tconst unrouted = [...declared].filter((type) => !routed.has(type));\n\n\tif (unrouted.length > 0) {\n\t\tproblems.push({\n\t\t\tmodule: MODULE,\n\t\t\tseverity: 'warning' as const,\n\t\t\tmessage:\n\t\t\t\t`${unrouted.length} message type(s) ship a default template but are routed to no ` +\n\t\t\t\t`channel, so they are published and never delivered: ${unrouted.join(', ')}. ` +\n\t\t\t\t\"Add them to `config.channels` — deriving it from the package's MESSAGE_KEYS \" +\n\t\t\t\t'rather than listing types by hand keeps it from drifting again. If one is ' +\n\t\t\t\t'deliberately not sent, map it to an empty list to say so.',\n\t\t\tdomain: 'courier',\n\t\t\treason: 'TEMPLATES_UNROUTED',\n\t\t\tmetadata: { count: unrouted.length, types: unrouted.join(', ') },\n\t\t});\n\t}\n\n\treturn problems;\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 { IAdminRoute, IFonderieContext, ILocaleSettings, Middleware } from '@fonderie/core';\nimport { canonicalLocale, defineLocales, 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\tSEEDED_TEMPLATE_TYPES,\n\tdeleteTemplate,\n} from './admin';\nimport { DefaultTemplates, chooseCopy, getLayoutHtml, renderFragment, templateVariables } from './resolver';\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\ntype RouteRow = [string, string, Middleware];\n\n// What the route table needs from ICourierConfig. Only the preview reads it —\n// every other handler is pure storage.\nexport interface ITemplateAdminOptions {\n\tbrandName?: string;\n\t/**\n\t * Types a module ships a default for (auth, billing, workspaces …). With\n\t * the seeded built-ins, their DEFAULT-LOCALE row is a system template: it\n\t * can be edited and rolled back, never deleted — deleting a seeded row with\n\t * no code default would leave that email as a raw data dump.\n\t */\n\tsystemTypes?: Iterable<string>;\n\t/** The app's locales, read per request (they are set at install). */\n\tlocales?: () => ILocaleSettings;\n\t/** The module-shipped copy, in every language it ships in. */\n\tdefaults?: DefaultTemplates;\n}\n\n/** A built-in email's default-locale row. Locale variants and app types are the operator's. */\nexport function isSystemTemplate(\n\ttype: string,\n\tlocale: string | null,\n\tsystemTypes: ReadonlySet<string>,\n): boolean {\n\treturn locale === null && (SEEDED_TEMPLATE_TYPES.includes(type) || systemTypes.has(type));\n}\n\n// The legacy standalone surface (bare /admin/*, guarded by this module's own\n// token). Registered by CourierModule.install when an adminToken is configured.\nexport function buildTemplateAdminRoutes(\n\tstore: IStoreAdapter,\n\tadminToken: string,\n\topts: ITemplateAdminOptions = {},\n): RouteRow[] {\n\treturn templateAdminRouteTable(store, opts).map(([m, p, h]) => [m, p, guarded(adminToken, h)]);\n}\n\n// The same handlers, unguarded and prefix-relative, for @fonderie/admin to mount\n// under its own prefix behind its own token.\nexport function describeTemplateAdminRoutes(\n\tstore: IStoreAdapter,\n\topts: ITemplateAdminOptions = {},\n): IAdminRoute[] {\n\treturn templateAdminRouteTable(store, opts).map(([method, path, h]) => ({\n\t\tmethod,\n\t\tpath: path.replace(/^\\/admin/, ''),\n\t\thandlers: [h],\n\t}));\n}\n\nfunction templateAdminRouteTable(store: IStoreAdapter, opts: ITemplateAdminOptions = {}): RouteRow[] {\n\tconst brandName = opts.brandName;\n\tconst systemTypes = new Set(opts.systemTypes ?? []);\n\tconst locales = opts.locales ?? (() => defineLocales());\n\tconst defaults = opts.defaults ?? new DefaultTemplates();\n\treturn [\n\t\t// Everything an operator needs to see which emails exist and in which\n\t\t// languages — saved AND built-in — in one read. The flat /templates list\n\t\t// only has saved rows, so a built-in email nobody edited (every billing\n\t\t// email, until someone does) was invisible. Its own path, not\n\t\t// /templates/catalog, so it can never shadow a template named \"catalog\".\n\t\t['GET', '/admin/template-catalog', async () => {\n\t\t\tconst settings = locales();\n\t\t\tconst rows = await listTemplateEntries(store);\n\t\t\tconst types = [...new Set([...rows.map((r) => r.type), ...defaults.types()])]\n\t\t\t\t.filter((t) => t !== '_layout')\n\t\t\t\t.sort();\n\t\t\tconst emails = types.map((type) => ({\n\t\t\t\ttype,\n\t\t\t\tsystem: isSystemTemplate(type, null, systemTypes),\n\t\t\t\tbuiltIn: {\n\t\t\t\t\t// The English copy is the default version's built-in fallback.\n\t\t\t\t\tdefault: defaults.get(type) !== undefined,\n\t\t\t\t\tlanguages: defaults.languages(type).sort(),\n\t\t\t\t},\n\t\t\t\tversions: rows\n\t\t\t\t\t.filter((r) => r.type === type)\n\t\t\t\t\t.map((r) => ({\n\t\t\t\t\t\tlocale: r.locale,\n\t\t\t\t\t\tactive: r.active,\n\t\t\t\t\t\tversion: r.version,\n\t\t\t\t\t\tupdatedAt: r.updatedAt,\n\t\t\t\t\t})),\n\t\t\t}));\n\t\t\treturn setApiResponse(HTTP.OK, 'TEMPLATE_CATALOG', 'Template catalog', {\n\t\t\t\tdefaultLocale: settings.default,\n\t\t\t\tfallbacks: settings.fallbacks,\n\t\t\t\temails,\n\t\t\t});\n\t\t}],\n\t\t['GET', '/admin/templates', async () => {\n\t\t\tconst rows = await listTemplateEntries(store);\n\t\t\t// `system` tells a console which rows it must not offer to delete.\n\t\t\treturn setApiResponse(\n\t\t\t\tHTTP.OK,\n\t\t\t\t'TEMPLATES_LISTED',\n\t\t\t\t'Templates',\n\t\t\t\trows.map((r) => ({ ...r, system: isSystemTemplate(r.type, r.locale, systemTypes) })),\n\t\t\t);\n\t\t}],\n\t\t['GET', '/admin/templates/:type', 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', 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\t// Stored canonical ('fr-ca' → 'fr-CA'), so one locale is one row.\n\t\t\tconst requested = localeOf(ctx);\n\t\t\tconst locale = requested === null ? null : canonicalLocale(requested);\n\t\t\tif (requested !== null && !locale) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', `\"${requested}\" is not a locale tag`);\n\t\t\t}\n\t\t\t// The untagged row IS the system locale's version. A second copy tagged\n\t\t\t// with it would silently override the default for exactly those users,\n\t\t\t// and edits would land in one copy while mail went out from the other.\n\t\t\tconst system = locales().default;\n\t\t\tif (locale === system) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.CONFLICT,\n\t\t\t\t\t'DEFAULT_LOCALE',\n\t\t\t\t\t`${system} is the default version of this email. Edit the default instead of adding ${system}.`,\n\t\t\t\t\t{ defaultLocale: system },\n\t\t\t\t);\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,\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', async (ctx) => {\n\t\t\tif (isSystemTemplate(typeOf(ctx), localeOf(ctx), systemTypes)) {\n\t\t\t\treturn setApiResponse(\n\t\t\t\t\tHTTP.CONFLICT,\n\t\t\t\t\t'SYSTEM_TEMPLATE',\n\t\t\t\t\t'This is a built-in email. Edit it or roll it back to an earlier version; it cannot be deleted.',\n\t\t\t\t);\n\t\t\t}\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// Fonderie's own copy of an email in one language (none = the English\n\t\t// default), so an editor can open a language nobody has saved yet and a\n\t\t// saved version can be compared with — or reset to — what ships.\n\t\t['GET', '/admin/templates/:type/built-in', async (ctx) => {\n\t\t\tconst settings = locales();\n\t\t\tconst tag = canonicalLocale(localeOf(ctx));\n\t\t\tconst type = typeOf(ctx);\n\t\t\tconst shipped =\n\t\t\t\ttag && tag !== settings.default\n\t\t\t\t\t? defaults.getLocalized(type, tag)\n\t\t\t\t\t: (() => {\n\t\t\t\t\t\t\tconst def = defaults.get(type);\n\t\t\t\t\t\t\treturn def ? { copy: def, locale: settings.default } : undefined;\n\t\t\t\t\t\t})();\n\t\t\tif (!shipped) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No built-in copy in that language');\n\t\t\tconst { subject, html, text } = shipped.copy;\n\t\t\treturn setApiResponse(HTTP.OK, 'BUILT_IN_TEMPLATE', 'Built-in copy', {\n\t\t\t\ttype,\n\t\t\t\tlocale: shipped.locale,\n\t\t\t\tsubject: subject ?? null,\n\t\t\t\thtml: html ?? null,\n\t\t\t\ttext,\n\t\t\t});\n\t\t}],\n\t\t// Who receives what: the version a send in `locale` would use, decided by\n\t\t// the same function a real send runs — so the console cannot disagree\n\t\t// with delivery.\n\t\t['GET', '/admin/templates/:type/resolve', async (ctx) => {\n\t\t\tconst settings = locales();\n\t\t\tconst requested = localeOf(ctx);\n\t\t\tconst choice = await chooseCopy(store, defaults, settings, typeOf(ctx), requested ?? undefined);\n\t\t\tif (!choice) return setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'Nothing would be sent for this email');\n\t\t\treturn setApiResponse(HTTP.OK, 'TEMPLATE_RESOLUTION', 'Resolution', {\n\t\t\t\trequested: canonicalLocale(requested) ?? settings.default,\n\t\t\t\tchain: choice.chain,\n\t\t\t\tdefaultLocale: settings.default,\n\t\t\t\tsent: choice.sent,\n\t\t\t\tsource: choice.source,\n\t\t\t});\n\t\t}],\n\t\t['GET', '/admin/templates/:type/revisions', 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', 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\t// Renders what the editor is HOLDING, not what is stored — the point is\n\t\t// to see the change before saving it. Goes through renderFragment and the\n\t\t// operator's own _layout row, because a fragment rendered without the\n\t\t// shell looks nothing like the mail that sends, and a preview that lies\n\t\t// is worse than none.\n\t\t//\n\t\t// POST, so the scope derivation makes this `write`. Correct: the body is\n\t\t// arbitrary content handed to the renderer, and anyone in the editor\n\t\t// already needs `write` to save.\n\t\t['POST', '/admin/templates/:type/preview', 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\tconst data = (b['data'] ?? {}) as Record<string, unknown>;\n\t\t\tif (typeof data !== 'object' || Array.isArray(data)) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.data must be an object');\n\t\t\t}\n\t\t\t// Rendered as it would send in this locale: its saved shell along its\n\t\t\t// chain, else the built-in shell in its language.\n\t\t\tconst settings = locales();\n\t\t\tconst tag = canonicalLocale(localeOf(ctx)) ?? settings.default;\n\t\t\tconst html = typeof b['html'] === 'string' && b['html'] ? b['html'] : null;\n\t\t\tconst rendered = renderFragment(\n\t\t\t\t{\n\t\t\t\t\ttext: b['text'],\n\t\t\t\t\t...(typeof b['subject'] === 'string' ? { subject: b['subject'] } : {}),\n\t\t\t\t\t...(html ? { html } : {}),\n\t\t\t\t},\n\t\t\t\t// Only fetched when there is HTML to wrap, mirroring the resolver.\n\t\t\t\thtml\n\t\t\t\t\t? await getLayoutHtml(store, tag === settings.default ? undefined : tag, settings)\n\t\t\t\t\t: undefined,\n\t\t\t\t// brandName is merged by the Dispatcher on a real send, never by\n\t\t\t\t// the resolver — so without this the shell renders the default\n\t\t\t\t// brand and the preview quietly misreports it.\n\t\t\t\t{ ...(brandName ? { brandName } : {}), ...data },\n\t\t\t\ttag,\n\t\t\t);\n\t\t\t// The variables THIS content uses, so an editor can offer exactly the\n\t\t\t// right fields without re-implementing the {{var}} contract on the\n\t\t\t// client — four copies of that regex already exist in this repo.\n\t\t\treturn setApiResponse(HTTP.OK, 'TEMPLATE_PREVIEW', 'Rendered preview', {\n\t\t\t\t...rendered,\n\t\t\t\tvariables: templateVariables(b['subject'] as string, b['text'], html),\n\t\t\t});\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\n// The built-in emails seeded by migration 002 — default-locale rows the\n// product relies on. Keep in sync with 002_seed_templates.sql (a test parses\n// the SQL and fails when they drift).\nexport const SEEDED_TEMPLATE_TYPES: readonly string[] = [\n\t'email-verification',\n\t'password-reset',\n\t'workspace-invitation',\n\t'email-changed',\n\t'email-registration',\n\t'phone-otp',\n\t'mfa-enabled',\n\t'mfa-disabled',\n\t'mfa-backup-codes-regenerated',\n\t'phone-changed',\n];\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 { IAdminCheck, IFinding } from '@fonderie/core';\nimport { resolveTxt as nodeResolveTxt } from 'node:dns/promises';\n\n/** A TXT lookup, injectable so the check can be tested without a network. */\nexport type ResolveTxt = (hostname: string) => Promise<string[][]>;\n\nexport interface ISenderDnsRecord {\n\t/** Why this record is a problem or advice — see IFinding. */\n\treason?: string;\n\tmetadata?: Record<string, string | number>;\n\tkind: 'spf' | 'dmarc' | 'dkim';\n\tpresent: boolean;\n\t/**\n\t * Where it was actually found. Equals the sending domain for SPF and DKIM;\n\t * for DMARC it may be a parent, which is legitimate (see below).\n\t */\n\tfoundAt?: string;\n\tvalue?: string;\n\t/**\n\t * A hard failure: this domain cannot authenticate at all. Flips `ok`.\n\t */\n\tproblem?: string;\n\t/**\n\t * Worth saying, but not broken — a deliberate monitoring stage, or a valid\n\t * setup with a caveat. Reported alongside problems and deliberately does NOT\n\t * flip `ok`, so a considered choice does not leave a deployment permanently\n\t * red (which is how a check gets muted).\n\t */\n\tadvice?: string;\n\t/** True when nothing was looked up — a DKIM check with no selector supplied. */\n\tskipped?: boolean;\n}\n\nexport interface ISenderDnsReport {\n\t/** The domain taken from the configured `from` address. */\n\tdomain?: string;\n\t/** Set when `from` could not be parsed into a domain. */\n\terror?: string;\n\trecords: ISenderDnsRecord[];\n\tok: boolean;\n}\n\n/** `\"Name <a@b.com>\"` and `\"a@b.com\"` both yield `b.com`. */\nexport function senderDomain(from: string): string | null {\n\tconst angle = from.match(/<([^>]+)>/);\n\tconst address = (angle?.[1] ?? from).trim();\n\tconst at = address.lastIndexOf('@');\n\tif (at < 1 || at === address.length - 1) return null;\n\tconst domain = address\n\t\t.slice(at + 1)\n\t\t.trim()\n\t\t.toLowerCase()\n\t\t.replace(/\\.$/, '');\n\treturn domain.includes('.') ? domain : null;\n}\n\n/** Absent and empty are the same answer here; only a real fault is an error. */\nasync function txt(host: string, resolve: ResolveTxt): Promise<string[]> {\n\ttry {\n\t\t// A single TXT record can be split into multiple strings; the record's\n\t\t// value is their concatenation, not the first chunk.\n\t\treturn (await resolve(host)).map((chunks) => chunks.join(''));\n\t} catch {\n\t\t// NXDOMAIN / ENODATA / SERVFAIL all mean \"nothing usable here\". Treating a\n\t\t// lookup failure as a finding would make this check fire on every\n\t\t// air-gapped CI run, and a check that cries wolf is one people mute.\n\t\treturn [];\n\t}\n}\n\n/**\n * Check that the domain we send mail AS is actually set up to authorise us.\n *\n * The asymmetry: courier declares a `from` address, while SPF, DKIM and DMARC\n * live in public DNS, owned by whoever runs the domain. Nothing connects them,\n * and a mismatch does not fail a send — the provider accepts the message and\n * the RECEIVER drops it or files it as spam. No bounce, no error, no log. It is\n * the quietest failure in the system, and the one most likely to be discovered\n * by a customer saying \"I never got the email\".\n *\n * Deliberately DNS-only: no provider API and no credentials, so it works for\n * any SMTP backend rather than only the ones with a domains endpoint.\n *\n * Never throws — a diagnostic must not take down what it diagnoses.\n */\nexport async function checkSenderDns(\n\tfrom: string,\n\topts: { dkimSelectors?: string[]; returnPathDomain?: string; resolveTxt?: ResolveTxt } = {},\n): Promise<ISenderDnsReport> {\n\tconst resolve = opts.resolveTxt ?? nodeResolveTxt;\n\tconst domain = senderDomain(from);\n\tif (!domain) {\n\t\treturn { error: `cannot read a domain from the from address: ${from}`, records: [], ok: false };\n\t}\n\n\tconst records: ISenderDnsRecord[] = [];\n\n\t// ---- SPF -------------------------------------------------------------\n\t// SPF authenticates the ENVELOPE SENDER (the Return-Path), not the From\n\t// header, and it is not inherited from a parent domain.\n\t//\n\t// That distinction decides whether an absent record is a fault. Most hosted\n\t// senders put the Return-Path on their own bounce subdomain — Resend uses\n\t// send.<domain>, with the MX and SPF there — so the From domain legitimately\n\t// has NO SPF and DMARC passes on aligned DKIM instead. Reporting that as\n\t// broken is a false alarm on a textbook-correct setup, which was this\n\t// check's first live finding and is why the rule below is not \"SPF missing =\n\t// bad\". Pass `returnPathDomain` to check where SPF actually applies.\n\tconst spfDomain = opts.returnPathDomain?.toLowerCase().replace(/\\.$/, '') ?? domain;\n\tconst spfAll = (await txt(spfDomain, resolve)).filter((v) =>\n\t\tv.toLowerCase().startsWith('v=spf1'),\n\t);\n\tif (spfAll.length === 0) {\n\t\t// Resolved after DKIM below, because whether this is a problem depends on\n\t\t// whether anything else can align.\n\t\trecords.push({ kind: 'spf', present: false });\n\t} else if (spfAll.length > 1) {\n\t\t// RFC 7208: more than one SPF record is a permerror, and a permerror is\n\t\t// treated as no SPF at all. Two \"correct\" records are worse than one.\n\t\trecords.push({\n\t\t\tkind: 'spf',\n\t\t\tpresent: true,\n\t\t\tfoundAt: domain,\n\t\t\tvalue: spfAll.join(' | '),\n\t\t\tproblem: `${spfAll.length} v=spf1 records on ${spfDomain} — RFC 7208 makes that a permerror, which receivers treat as having NO SPF`,\n\t\t\treason: 'SPF_MULTIPLE',\n\t\t\tmetadata: { count: spfAll.length, domain: spfDomain },\n\t\t});\n\t} else {\n\t\trecords.push({ kind: 'spf', present: true, foundAt: spfDomain, value: spfAll[0]! });\n\t}\n\n\t// ---- DMARC -----------------------------------------------------------\n\t// DMARC *is* inherited: with no record at _dmarc.<domain>, a receiver falls\n\t// back to the organisational domain (RFC 7489 §6.6.3). So a sending\n\t// subdomain covered by its apex is correctly configured, and reporting it as\n\t// missing would be a false alarm on the exact setup we recommend.\n\t//\n\t// Determining the true organisational domain needs the Public Suffix List,\n\t// which is a dependency and a refresh problem this package should not take\n\t// on. Walking up to the last two labels is deliberately PERMISSIVE: it can\n\t// accept a record slightly higher than a receiver would consult, which risks\n\t// staying quiet when we could have warned — the safe direction for a check\n\t// whose credibility depends on never being wrong when it does speak.\n\tlet dmarc: ISenderDnsRecord = {\n\t\tkind: 'dmarc',\n\t\tpresent: false,\n\t\tproblem: `no _dmarc record for ${domain} or any parent — without DMARC, SPF and DKIM results carry no policy and receivers apply their own judgement`,\n\t\treason: 'DMARC_MISSING',\n\t\tmetadata: { domain },\n\t};\n\tconst labels = domain.split('.');\n\tfor (let i = 0; i + 2 <= labels.length; i++) {\n\t\tconst at = labels.slice(i).join('.');\n\t\tconst found = (await txt(`_dmarc.${at}`, resolve)).find((v) =>\n\t\t\tv.toLowerCase().startsWith('v=dmarc1'),\n\t\t);\n\t\tif (found) {\n\t\t\tdmarc = { kind: 'dmarc', present: true, foundAt: at, value: found };\n\t\t\t// p=none publishes a policy but asks receivers to do nothing with it.\n\t\t\t// That is a valid monitoring stage, not a finished setup — worth\n\t\t\t// saying once, not worth failing over.\n\t\t\tif (/[;\\s]p=none\\b/i.test(found)) {\n\t\t\t\tdmarc.advice = `DMARC at ${at} is p=none — monitoring only, so a forged sender is still delivered. Deliberate while collecting rua reports; move to quarantine or reject once they look clean.`;\n\t\t\t\tdmarc.reason = 'DMARC_MONITORING_ONLY';\n\t\t\t\tdmarc.metadata = { domain: at };\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\trecords.push(dmarc);\n\n\t// ---- DKIM ------------------------------------------------------------\n\t// A selector cannot be discovered from DNS — it is chosen by whoever signs\n\t// the mail and only appears in the header of a message already sent. With\n\t// none supplied there is nothing to look up, which is reported as skipped\n\t// rather than missing: absence of a check is not evidence of a problem.\n\tconst selectors = opts.dkimSelectors ?? [];\n\tif (selectors.length === 0) {\n\t\t// No `problem` key at all — the repo runs exactOptionalPropertyTypes, and\n\t\t// omitting it is also the honest encoding: there is nothing wrong here.\n\t\trecords.push({ kind: 'dkim', present: false, skipped: true });\n\t} else {\n\t\tfor (const selector of selectors) {\n\t\t\tconst host = `${selector}._domainkey.${domain}`;\n\t\t\tconst found = (await txt(host, resolve)).find((v) => /(^|;)\\s*(v=DKIM1|k=|p=)/i.test(v));\n\t\t\trecords.push(\n\t\t\t\tfound\n\t\t\t\t\t? { kind: 'dkim', present: true, foundAt: host, value: found.slice(0, 80) }\n\t\t\t\t\t: {\n\t\t\t\t\t\t\tkind: 'dkim',\n\t\t\t\t\t\t\tpresent: false,\n\t\t\t\t\t\t\tproblem: `no DKIM key at ${host} — mail signed with selector '${selector}' cannot be verified`,\n\t\t\t\t\t\t\treason: 'DKIM_KEY_MISSING',\n\t\t\t\t\t\t\tmetadata: { host, selector },\n\t\t\t\t\t\t},\n\t\t\t);\n\t\t}\n\t}\n\n\t// ---- resolve the absent-SPF case, now that DKIM is known ---------------\n\t// DMARC passes when EITHER an aligned SPF or an aligned DKIM passes, so an\n\t// absent SPF on the From domain is only fatal when nothing else can align.\n\tconst spf = records.find((r) => r.kind === 'spf')!;\n\tif (!spf.present && !spf.problem) {\n\t\tconst dkimOk = records.some((r) => r.kind === 'dkim' && r.present);\n\t\tconst dkimChecked = records.some((r) => r.kind === 'dkim' && !r.skipped);\n\t\tif (dkimOk) {\n\t\t\t// The normal hosted-sender shape: bounce subdomain carries SPF, the From\n\t\t\t// domain carries DKIM. Nothing is wrong.\n\t\t\tspf.advice =\n\t\t\t\t`no v=spf1 on ${spfDomain}, which is expected when the provider owns the Return-Path ` +\n\t\t\t\t'(SPF is checked against the envelope sender, not From). DMARC passes on the aligned ' +\n\t\t\t\t'DKIM key found below. Pass `returnPathDomain` to verify SPF where it actually applies.';\n\t\t\tspf.reason = 'SPF_ABSENT_DKIM_ALIGNS';\n\t\t\tspf.metadata = { domain: spfDomain };\n\t\t} else if (dkimChecked) {\n\t\t\t// Checked for DKIM and found none: nothing can align, DMARC fails.\n\t\t\tspf.problem =\n\t\t\t\t`no v=spf1 on ${spfDomain} AND no DKIM key — neither can align, so DMARC cannot pass ` +\n\t\t\t\t'and receivers will treat this mail as unauthenticated';\n\t\t\tspf.reason = 'SPF_AND_DKIM_MISSING';\n\t\t\tspf.metadata = { domain: spfDomain };\n\t\t} else {\n\t\t\t// No selector supplied, so DKIM is unknown. Absence of a check is not\n\t\t\t// evidence of a problem — say what is missing instead of guessing.\n\t\t\tspf.advice =\n\t\t\t\t`no v=spf1 on ${spfDomain}. That is fine if the provider owns the Return-Path and DKIM ` +\n\t\t\t\t'is published — pass `dkimSelectors` so this can tell the difference instead of guessing.';\n\t\t\tspf.reason = 'SPF_ABSENT_DKIM_UNKNOWN';\n\t\t\tspf.metadata = { domain: spfDomain };\n\t\t}\n\t}\n\n\t// `ok` tracks only hard failures; advice is reported without flipping it.\n\treturn { domain, records, ok: records.every((r) => !r.problem) };\n}\n\n/**\n * Problems and advice as findings (see IFinding): each carries its reason for a\n * console to translate, and advice is marked advice — a passing-by-advice line\n * inside a failing check was being shown as an error.\n */\nexport function senderDnsFindings(report: ISenderDnsReport): IFinding[] {\n\tif (report.error)\n\t\treturn [\n\t\t\t{\n\t\t\t\tmessage: `sender DNS check failed: ${report.error}`,\n\t\t\t\tdomain: 'courier',\n\t\t\t\treason: 'SENDER_DNS_CHECK_FAILED',\n\t\t\t\tmetadata: { detail: report.error },\n\t\t\t},\n\t\t];\n\tconst reasonOf = (r: ISenderDnsRecord) =>\n\t\tr.reason ? { domain: 'courier', reason: r.reason } : {};\n\tconst metaOf = (r: ISenderDnsRecord) => (r.metadata ? { metadata: r.metadata } : {});\n\t// Problems first: if a reader stops after one line, it should be the one that\n\t// means mail is failing, not the one that means it could be tightened.\n\treturn [\n\t\t...report.records\n\t\t\t.filter((r) => r.problem)\n\t\t\t.map(\n\t\t\t\t(r): IFinding => ({\n\t\t\t\t\tmessage: `${r.kind.toUpperCase()}: ${r.problem}`,\n\t\t\t\t\t...reasonOf(r),\n\t\t\t\t\t...metaOf(r),\n\t\t\t\t\tseverity: 'error',\n\t\t\t\t}),\n\t\t\t),\n\t\t...report.records\n\t\t\t.filter((r) => r.advice)\n\t\t\t.map(\n\t\t\t\t(r): IFinding => ({\n\t\t\t\t\tmessage: `${r.kind.toUpperCase()} (advice): ${r.advice}`,\n\t\t\t\t\t...reasonOf(r),\n\t\t\t\t\t...metaOf(r),\n\t\t\t\t\tseverity: 'advice',\n\t\t\t\t}),\n\t\t\t),\n\t];\n}\n\n/** One line per problem, for a log. Empty when the domain is set up correctly. */\nexport function describeSenderDnsProblems(report: ISenderDnsReport): string[] {\n\treturn senderDnsFindings(report).map((f) => f.message);\n}\n\n// The doctor check over a configured email channel. `resolveTxt` is for tests.\nexport function senderDnsCheck(\n\temail: { from: string; senderDns?: { dkimSelectors?: string[]; returnPathDomain?: string } },\n\tresolveTxt?: ResolveTxt,\n): IAdminCheck {\n\treturn {\n\t\tname: 'courier.sender-dns',\n\t\trun: async () => {\n\t\t\tconst report = await checkSenderDns(email.from, {\n\t\t\t\t...(email.senderDns ?? {}),\n\t\t\t\t...(resolveTxt ? { resolveTxt } : {}),\n\t\t\t});\n\t\t\treturn { ok: report.ok, findings: senderDnsFindings(report) };\n\t\t},\n\t};\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\n/** The person being erased, as the account-deletion purge describes them. */\nexport interface ICourierErasureSubject {\n\tuserId: string;\n\temail: string | null;\n\tphone: string | null;\n}\n\nexport interface ICourierErasureResult {\n\t/** Message-log rows redacted. 0 on a repeat run. */\n\terased: number;\n\t/** What was deliberately kept, and why — absent when nothing was. */\n\tkept?: string;\n}\n\n/** The shape the account-deletion purge calls, in-process, before the user row goes. */\nexport interface ICourierAccountEraser {\n\treadonly name: 'courier';\n\terase(subject: ICourierErasureSubject): Promise<ICourierErasureResult>;\n}\n\n/** What a redacted recipient (and any error text that may quote it) reads as. */\nexport const ERASED_RECIPIENT = 'erased';\n\n/**\n * The address the account is stored under: lowercase, '+tag' dropped from the\n * local part — the same rule @fonderie/auth applies, so a message sent to\n * `Ana+news@Acme.example` belongs to the account `ana@acme.example`.\n */\nexport function erasureEmailKey(email: string): string | null {\n\tconst lower = email.trim().toLowerCase();\n\tconst at = lower.indexOf('@');\n\tif (at <= 0 || at !== lower.lastIndexOf('@') || at === lower.length - 1) return null;\n\tconst local = lower.slice(0, at);\n\tconst plus = local.indexOf('+');\n\tconst bare = plus === -1 ? local : local.slice(0, plus);\n\treturn bare ? `${bare}@${lower.slice(at + 1)}` : null;\n}\n\n/** A phone without the separators people type: '+1 (555) 000-1234' → '+15550001234'. */\nexport function erasurePhoneKey(phone: string): string | null {\n\tconst bare = phone.replace(/[\\s\\-().]/g, '');\n\treturn bare.length > 0 ? bare : null;\n}\n\n/**\n * Erase a person from the message log at account purge.\n *\n * `fonderie_message_log.recipient` holds the raw address a message went to.\n * Rows sent to the person's email (any case, any '+tag' variant, bare or as\n * `Name <address>`) or phone keep their place — message type, channel, status,\n * timestamps and provider ids are delivery statistics, not personal — but the\n * recipient becomes `'erased'`, and so do `error` and `bounce_reason` when set,\n * because a provider's rejection text routinely quotes the address.\n *\n * Push rows are addressed to a device token, which the purge does not know and\n * nothing links to the account; those are not matched here.\n *\n * One UPDATE. Idempotent: a second run matches nothing (`'erased'` is neither\n * an email nor the phone) and returns `{ erased: 0 }`.\n */\nexport function accountEraser(store: IStoreAdapter): ICourierAccountEraser {\n\treturn {\n\t\tname: 'courier',\n\t\tasync erase({ email, phone }) {\n\t\t\tconst emailKey = email ? erasureEmailKey(email) : null;\n\t\t\tconst phoneKey = phone ? erasurePhoneKey(phone) : null;\n\t\t\tif (!emailKey && !phoneKey) return { erased: 0 };\n\n\t\t\t// The address part of the recipient: the <…> of 'Name <a@b>', else all of it.\n\t\t\tconst address = `btrim(coalesce(substring(recipient from '<([^<>]+)>'), recipient))`;\n\t\t\tconst rows = await store.query<{ id: string }>(\n\t\t\t\t`UPDATE fonderie_message_log\n\t\t\t\t    SET recipient     = $3,\n\t\t\t\t        error         = CASE WHEN error IS NULL THEN NULL ELSE $3 END,\n\t\t\t\t        bounce_reason = CASE WHEN bounce_reason IS NULL THEN NULL ELSE $3 END\n\t\t\t\t  WHERE ($1::text IS NOT NULL\n\t\t\t\t         AND position('@' in recipient) > 0\n\t\t\t\t         AND lower(regexp_replace(${address}, '[+][^@]*@', '@')) = $1)\n\t\t\t\t     OR ($2::text IS NOT NULL\n\t\t\t\t         AND position('@' in recipient) = 0\n\t\t\t\t         AND regexp_replace(recipient, '[[:space:]().-]', '', 'g') = $2)\n\t\t\t\t  RETURNING id`,\n\t\t\t\t[emailKey, phoneKey, ERASED_RECIPIENT],\n\t\t\t);\n\n\t\t\tconst result: ICourierErasureResult = { erased: rows.length };\n\t\t\tif (rows.length > 0) {\n\t\t\t\tresult.kept =\n\t\t\t\t\t`${rows.length} message-log row(s) kept with the recipient, error and bounce reason ` +\n\t\t\t\t\t`redacted — type, channel, status and timestamps are delivery statistics`;\n\t\t\t}\n\t\t\treturn result;\n\t\t},\n\t};\n}\n","import type { DefaultTemplateMap } from './types';\n\nexport interface IEmailChannelConfig {\n\tprovider: 'resend' | 'ses' | 'smtp';\n\tfrom: string;\n\t/**\n\t * Where replies should go, when that is not the From address.\n\t *\n\t * Needed as soon as you send from a dedicated sending subdomain — the shape\n\t * that isolates sending reputation from the apex. Such a subdomain usually\n\t * has no MX at all, so a reply to the From address bounces. Recipients DO\n\t * reply to transactional mail (a question about a receipt, a \"this wasn't\n\t * me\" about a password reset), and a bounced reply is worse than no reply:\n\t * the sender believes they reached you.\n\t *\n\t * Set it to an address that actually receives — typically on the apex.\n\t */\n\treplyTo?: string;\n\t/**\n\t * What the doctor's sender-DNS check cannot discover on its own: DKIM\n\t * selectors (a selector is not readable from DNS) and the envelope domain\n\t * when it differs from the From domain (SPF checks the envelope). See\n\t * docs/EMAIL-SETUP.md §7.\n\t */\n\tsenderDns?: { dkimSelectors?: string[]; returnPathDomain?: 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/**\n\t * The product name shown in email — the app the RECIPIENT signed up for, not\n\t * the framework underneath it. A user of the app has never heard of\n\t * Fonderie, so an email headed \"Fonderie\" reads as a different company at\n\t * best and as phishing at worst.\n\t *\n\t * Set once here rather than per message; a message may still override it by\n\t * passing `brandName` in its own data. Unset falls back to EMAIL_THEME.brand.\n\t */\n\tbrandName?: string;\n\n\t/**\n\t * When a message names no language, use the language of the account its\n\t * recipient's email or phone belongs to (@fonderie/auth's users, same\n\t * database). Default true. Set false when courier's database is not the\n\t * one holding the accounts, or to keep messages on the sender's choice only.\n\t */\n\trecipientLocaleLookup?: boolean;\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":["import_core","import_core","body","body","nodemailer","body","import_middlewares","import_core","body","text","html","subject","sent","layout","import_core","body","cryptoVerify","import_core","import_store","opts","nodeResolveTxt"]}