import { ILocaleSettings, IFonderieModule, IAdminDescription, IReadinessProblem, IFonderieApp, IDefaultTemplateCopy, Middleware, IAdminCheck } from '@fonderie/core'; import { IStoreAdapter } from '@fonderie/store'; import { EventBus } from '@fonderie/events'; import { D as DefaultTemplateMap, I as ITemplateResolver, a as ICourierChannel, b as IRenderedTemplate } from './types-DNjz5M-t.cjs'; import { ISendResult } from './types.cjs'; export { d as IMessageLog, e as IMessageStats, M as MessageLogStatus, m as messageStats } from './types-DNjz5M-t.cjs'; import { ICourierMessage, IDefaultTemplate } from '@fonderie/core/types'; export { ICourierMessage, IDefaultTemplate } from '@fonderie/core/types'; interface IEmailChannelConfig { provider: 'resend' | 'ses' | 'smtp'; from: string; /** * Where replies should go, when that is not the From address. * * Needed as soon as you send from a dedicated sending subdomain — the shape * that isolates sending reputation from the apex. Such a subdomain usually * has no MX at all, so a reply to the From address bounces. Recipients DO * reply to transactional mail (a question about a receipt, a "this wasn't * me" about a password reset), and a bounced reply is worse than no reply: * the sender believes they reached you. * * Set it to an address that actually receives — typically on the apex. */ replyTo?: string; /** * What the doctor's sender-DNS check cannot discover on its own: DKIM * selectors (a selector is not readable from DNS) and the envelope domain * when it differs from the From domain (SPF checks the envelope). See * docs/EMAIL-SETUP.md §7. */ senderDns?: { dkimSelectors?: string[]; returnPathDomain?: string; }; apiKey?: string; smtp?: { host: string; port: number; secure: boolean; user: string; pass: string; }; } interface ISmsChannelConfig { provider: 'twilio' | 'vonage'; from: string; accountSid?: string; authToken?: string; apiKey?: string; apiSecret?: string; } interface IPushChannelConfig { provider: 'fcm'; serviceAccount: Record; } declare const Channel: { readonly EMAIL: "email"; readonly SMS: "sms"; readonly PUSH: "push"; }; interface ICourierConfig { channels: Record>; sms?: ISmsChannelConfig; push?: IPushChannelConfig; email?: IEmailChannelConfig; /** * The product name shown in email — the app the RECIPIENT signed up for, not * the framework underneath it. A user of the app has never heard of * Fonderie, so an email headed "Fonderie" reads as a different company at * best and as phishing at worst. * * Set once here rather than per message; a message may still override it by * passing `brandName` in its own data. Unset falls back to EMAIL_THEME.brand. */ brandName?: string; /** * When a message names no language, use the language of the account its * recipient's email or phone belongs to (@fonderie/auth's users, same * database). Default true. Set false when courier's database is not the * one holding the accounts, or to keep messages on the sender's choice only. */ recipientLocaleLookup?: boolean; adminToken?: string; templates?: { source: 'db' | 'fs'; directory?: string; defaults?: DefaultTemplateMap | DefaultTemplateMap[]; }; delivery?: { signingKeys?: { sendgrid?: string; mailgun?: string; }; allowUnverifiedMailtrap?: boolean; }; } declare class Dispatcher { private config; private resolver; private store?; private channels; constructor(config: ICourierConfig, resolver: ITemplateResolver, store?: IStoreAdapter | undefined); private locales; /** The app's locales — the default language when a message names none. */ setLocales(locales: ILocaleSettings): void; registerChannel(channel: ICourierChannel): this; channelNames(): string[]; /** * The language of the account the recipient's address belongs to — read * from @fonderie/auth's users table when it is in the same database. No * auth, no account, or `recipientLocaleLookup: false`: undefined. */ private accountLocale; dispatch(message: ICourierMessage): Promise; } declare class CourierModule implements IFonderieModule { private config; private store?; readonly name = "@fonderie/courier"; readonly version: string; readonly deps: string[]; readonly dispatcher: Dispatcher; private readonly resolver; private locales; constructor(config: ICourierConfig, store?: IStoreAdapter | undefined, bus?: EventBus); private brandOpts; describeAdmin(): IAdminDescription; checkReadiness(): IReadinessProblem[]; install(app: IFonderieApp): void; } declare function validateCourierConfig(config: ICourierConfig, registeredChannels: Iterable): void; declare function handleSendGridDelivery(req: Request, store: IStoreAdapter, publicKey?: string): Promise; declare function handleMailgunDelivery(req: Request, store: IStoreAdapter, signingKey?: string): Promise; declare function handleMailtrapDelivery(req: Request, store: IStoreAdapter): Promise; /** The person being erased, as the account-deletion purge describes them. */ interface ICourierErasureSubject { userId: string; email: string | null; phone: string | null; } interface ICourierErasureResult { /** Message-log rows redacted. 0 on a repeat run. */ erased: number; /** What was deliberately kept, and why — absent when nothing was. */ kept?: string; } /** The shape the account-deletion purge calls, in-process, before the user row goes. */ interface ICourierAccountEraser { readonly name: 'courier'; erase(subject: ICourierErasureSubject): Promise; } /** * Erase a person from the message log at account purge. * * `fonderie_message_log.recipient` holds the raw address a message went to. * Rows sent to the person's email (any case, any '+tag' variant, bare or as * `Name
`) or phone keep their place — message type, channel, status, * timestamps and provider ids are delivery statistics, not personal — but the * recipient becomes `'erased'`, and so do `error` and `bounce_reason` when set, * because a provider's rejection text routinely quotes the address. * * Push rows are addressed to a device token, which the purge does not know and * nothing links to the account; those are not matched here. * * One UPDATE. Idempotent: a second run matches nothing (`'erased'` is neither * an email nor the phone) and returns `{ erased: 0 }`. */ declare function accountEraser(store: IStoreAdapter): ICourierAccountEraser; declare class SmsChannel implements ICourierChannel { private config; readonly name = "sms"; constructor(config: ISmsChannelConfig); send(message: ICourierMessage, template: IRenderedTemplate): Promise; private sendViaTwilio; private sendViaVonage; } declare class PushChannel implements ICourierChannel { private config; readonly name = "push"; constructor(config: IPushChannelConfig); send(message: ICourierMessage, template: IRenderedTemplate): Promise; private sendViaFCM; } declare class EmailChannel implements ICourierChannel { private config; readonly name = "email"; private transport; constructor(config: IEmailChannelConfig); send(message: ICourierMessage, template: IRenderedTemplate): Promise; private sendViaResend; private sendViaSMTP; } declare function templateVariables(...parts: Array): string[]; declare function renderFragment(frag: { subject?: string | null; text: string; html?: string | null; }, layoutHtml: string | undefined, data: Record, locale?: string): IRenderedTemplate; declare class DefaultTemplates { private readonly map; constructor(maps?: DefaultTemplateMap[]); get(type: string): IDefaultTemplate | undefined; /** * The built-in copy of `type` in `locale`: its exact key, else its language * ('fr-CA' → 'fr'). Built-in copy has no market-specific terms, so the * language is safe here where it would not be for an app's own versions. * Returns the matched key with it — that is the version reported as sent. */ getLocalized(type: string, locale: string): { copy: IDefaultTemplateCopy; locale: string; } | undefined; /** Every language a built-in email ships in besides English. */ languages(type: string): string[]; get size(): number; /** Every type a module ships a default for. */ types(): string[]; } declare class DBTemplateResolver implements ITemplateResolver { private store; private defaults?; private locales; constructor(store: IStoreAdapter, defaults?: DefaultTemplates | undefined); /** The app's locales, handed over at install (core owns them). */ setLocales(settings: ILocaleSettings): void; resolve(type: string, data: Record, locale?: string): Promise; private renderWith; } declare function getLayoutHtml(store: IStoreAdapter, locale?: string, settings?: ILocaleSettings): Promise; declare class FSTemplateResolver implements ITemplateResolver { private directory; private defaults?; private locales; constructor(directory: string, defaults?: DefaultTemplates | undefined); setLocales(settings: ILocaleSettings): void; resolve(type: string, data: Record, locale?: string): Promise; } interface ITemplateEntry { type: string; locale: string | null; subject: string | null; html: string | null; text: string; active: boolean; version: number; updatedBy: string | null; updatedAt: string; } interface ITemplateRevision { type: string; locale: string | null; subject: string | null; html: string | null; text: string; version: number; actor: string | null; createdAt: string; } declare function setTemplate(opts: { type: string; text: string; locale?: string | null; subject?: string | null; html?: string | null; active?: boolean; ifVersion?: number; actor?: string; }, store: IStoreAdapter): Promise; declare function rollbackTemplate(opts: { type: string; locale?: string | null; toVersion: number; actor?: string; }, store: IStoreAdapter): Promise; declare function listTemplateRevisions(type: string, locale: string | null, store: IStoreAdapter): Promise; declare function getTemplateEntry(type: string, locale: string | null, store: IStoreAdapter): Promise; declare function listTemplateEntries(store: IStoreAdapter): Promise; declare function deleteTemplate(type: string, locale: string | null, store: IStoreAdapter): Promise; type RouteRow = [string, string, Middleware]; interface ITemplateAdminOptions { brandName?: string; /** * Types a module ships a default for (auth, billing, workspaces …). With * the seeded built-ins, their DEFAULT-LOCALE row is a system template: it * can be edited and rolled back, never deleted — deleting a seeded row with * no code default would leave that email as a raw data dump. */ systemTypes?: Iterable; /** The app's locales, read per request (they are set at install). */ locales?: () => ILocaleSettings; /** The module-shipped copy, in every language it ships in. */ defaults?: DefaultTemplates; } declare function buildTemplateAdminRoutes(store: IStoreAdapter, adminToken: string, opts?: ITemplateAdminOptions): RouteRow[]; /** A TXT lookup, injectable so the check can be tested without a network. */ type ResolveTxt = (hostname: string) => Promise; interface ISenderDnsRecord { /** Why this record is a problem or advice — see IFinding. */ reason?: string; metadata?: Record; kind: 'spf' | 'dmarc' | 'dkim'; present: boolean; /** * Where it was actually found. Equals the sending domain for SPF and DKIM; * for DMARC it may be a parent, which is legitimate (see below). */ foundAt?: string; value?: string; /** * A hard failure: this domain cannot authenticate at all. Flips `ok`. */ problem?: string; /** * Worth saying, but not broken — a deliberate monitoring stage, or a valid * setup with a caveat. Reported alongside problems and deliberately does NOT * flip `ok`, so a considered choice does not leave a deployment permanently * red (which is how a check gets muted). */ advice?: string; /** True when nothing was looked up — a DKIM check with no selector supplied. */ skipped?: boolean; } interface ISenderDnsReport { /** The domain taken from the configured `from` address. */ domain?: string; /** Set when `from` could not be parsed into a domain. */ error?: string; records: ISenderDnsRecord[]; ok: boolean; } /** `"Name "` and `"a@b.com"` both yield `b.com`. */ declare function senderDomain(from: string): string | null; /** * Check that the domain we send mail AS is actually set up to authorise us. * * The asymmetry: courier declares a `from` address, while SPF, DKIM and DMARC * live in public DNS, owned by whoever runs the domain. Nothing connects them, * and a mismatch does not fail a send — the provider accepts the message and * the RECEIVER drops it or files it as spam. No bounce, no error, no log. It is * the quietest failure in the system, and the one most likely to be discovered * by a customer saying "I never got the email". * * Deliberately DNS-only: no provider API and no credentials, so it works for * any SMTP backend rather than only the ones with a domains endpoint. * * Never throws — a diagnostic must not take down what it diagnoses. */ declare function checkSenderDns(from: string, opts?: { dkimSelectors?: string[]; returnPathDomain?: string; resolveTxt?: ResolveTxt; }): Promise; /** One line per problem, for a log. Empty when the domain is set up correctly. */ declare function describeSenderDnsProblems(report: ISenderDnsReport): string[]; declare function senderDnsCheck(email: { from: string; senderDns?: { dkimSelectors?: string[]; returnPathDomain?: string; }; }, resolveTxt?: ResolveTxt): IAdminCheck; export { Channel, CourierModule, DBTemplateResolver, DefaultTemplateMap, DefaultTemplates, Dispatcher, EmailChannel, FSTemplateResolver, ICourierChannel, IRenderedTemplate, ITemplateResolver, PushChannel, SmsChannel, accountEraser, buildTemplateAdminRoutes, checkSenderDns, deleteTemplate, describeSenderDnsProblems, getLayoutHtml, getTemplateEntry, handleMailgunDelivery, handleMailtrapDelivery, handleSendGridDelivery, listTemplateEntries, listTemplateRevisions, renderFragment, rollbackTemplate, senderDnsCheck, senderDomain, setTemplate, templateVariables, validateCourierConfig }; export type { ICourierAccountEraser, ICourierConfig, ICourierErasureResult, ICourierErasureSubject, IEmailChannelConfig, IPushChannelConfig, ISenderDnsRecord, ISenderDnsReport, ISmsChannelConfig, ITemplateAdminOptions, ITemplateEntry, ITemplateRevision, ResolveTxt };