@fonderie/courier 5.1.1 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/brain/signatures.md +17 -2
- package/dist/index.cjs +48 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -6
- package/dist/index.d.ts +20 -6
- package/dist/index.js +46 -10
- package/dist/index.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +4 -3
- package/dist/types.d.ts +4 -3
- package/package.json +2 -2
package/brain/signatures.md
CHANGED
|
@@ -39,12 +39,18 @@ new EmailChannel(config: IEmailChannelConfig): EmailChannel
|
|
|
39
39
|
.name: "email"
|
|
40
40
|
.send(message: ICourierMessage, template: IRenderedTemplate): Promise<void>
|
|
41
41
|
|
|
42
|
-
new DBTemplateResolver(store: IStoreAdapter): DBTemplateResolver
|
|
42
|
+
new DBTemplateResolver(store: IStoreAdapter, defaults?: DefaultTemplates | undefined): DBTemplateResolver
|
|
43
43
|
.resolve(type: string, data: Record<string, unknown>, locale?: string | undefined): Promise<IRenderedTemplate>
|
|
44
44
|
|
|
45
|
-
new FSTemplateResolver(directory: string): FSTemplateResolver
|
|
45
|
+
new FSTemplateResolver(directory: string, defaults?: DefaultTemplates | undefined): FSTemplateResolver
|
|
46
46
|
.resolve(type: string, data: Record<string, unknown>, locale?: string | undefined): Promise<IRenderedTemplate>
|
|
47
47
|
|
|
48
|
+
new DefaultTemplates(maps?: DefaultTemplateMap[]): DefaultTemplates
|
|
49
|
+
.get(type: string): IDefaultTemplate | undefined
|
|
50
|
+
.size: number
|
|
51
|
+
|
|
52
|
+
function renderFragment(frag: { subject?: string | null; text: string; html?: string | null; }, layoutHtml: string | undefined, data: Record<string, unknown>): IRenderedTemplate
|
|
53
|
+
|
|
48
54
|
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<...>
|
|
49
55
|
|
|
50
56
|
function rollbackTemplate(opts: { type: string; locale?: string | null; toVersion: number; actor?: string; }, store: IStoreAdapter): Promise<ITemplateEntry>
|
|
@@ -129,6 +135,14 @@ interface ITemplateResolver {
|
|
|
129
135
|
resolve(type: string, data: Record<string, unknown>, locale?: string): Promise<IRenderedTemplate>;
|
|
130
136
|
}
|
|
131
137
|
|
|
138
|
+
interface IDefaultTemplate {
|
|
139
|
+
subject?: string;
|
|
140
|
+
text: string;
|
|
141
|
+
html?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
type DefaultTemplateMap = Record<string, IDefaultTemplate>;
|
|
145
|
+
|
|
132
146
|
const Channel: { readonly EMAIL: "email"; readonly SMS: "sms"; readonly PUSH: "push"; }
|
|
133
147
|
|
|
134
148
|
interface ICourierConfig {
|
|
@@ -140,6 +154,7 @@ interface ICourierConfig {
|
|
|
140
154
|
templates?: {
|
|
141
155
|
source: 'db' | 'fs';
|
|
142
156
|
directory?: string;
|
|
157
|
+
defaults?: DefaultTemplateMap | DefaultTemplateMap[];
|
|
143
158
|
};
|
|
144
159
|
delivery?: {
|
|
145
160
|
signingKeys?: {
|
package/dist/index.cjs
CHANGED
|
@@ -33,6 +33,7 @@ __export(index_exports, {
|
|
|
33
33
|
Channel: () => Channel,
|
|
34
34
|
CourierModule: () => CourierModule,
|
|
35
35
|
DBTemplateResolver: () => DBTemplateResolver,
|
|
36
|
+
DefaultTemplates: () => DefaultTemplates,
|
|
36
37
|
Dispatcher: () => Dispatcher,
|
|
37
38
|
EmailChannel: () => EmailChannel,
|
|
38
39
|
FSTemplateResolver: () => FSTemplateResolver,
|
|
@@ -46,6 +47,7 @@ __export(index_exports, {
|
|
|
46
47
|
handleSendGridDelivery: () => handleSendGridDelivery,
|
|
47
48
|
listTemplateEntries: () => listTemplateEntries,
|
|
48
49
|
listTemplateRevisions: () => listTemplateRevisions,
|
|
50
|
+
renderFragment: () => renderFragment,
|
|
49
51
|
rollbackTemplate: () => rollbackTemplate,
|
|
50
52
|
setTemplate: () => setTemplate,
|
|
51
53
|
validateCourierConfig: () => validateCourierConfig
|
|
@@ -503,11 +505,35 @@ function composeHtml(bodyHtml, layoutHtml, subject, data) {
|
|
|
503
505
|
const wrapped = wrapLayout(bodyHtml, layoutHtml);
|
|
504
506
|
return render(wrapped, { subject: subject ?? "", preheader: "", ...data });
|
|
505
507
|
}
|
|
508
|
+
function renderFragment(frag, layoutHtml, data) {
|
|
509
|
+
const subject = frag.subject ? render(frag.subject, data) : void 0;
|
|
510
|
+
return {
|
|
511
|
+
text: render(frag.text, data),
|
|
512
|
+
...subject ? { subject } : {},
|
|
513
|
+
...frag.html ? { html: composeHtml(frag.html, layoutHtml, subject, data) } : {}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
var DefaultTemplates = class {
|
|
517
|
+
map = /* @__PURE__ */ new Map();
|
|
518
|
+
constructor(maps = []) {
|
|
519
|
+
for (const m of maps) {
|
|
520
|
+
for (const [key, tmpl] of Object.entries(m)) this.map.set(key, tmpl);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
get(type) {
|
|
524
|
+
return this.map.get(type);
|
|
525
|
+
}
|
|
526
|
+
get size() {
|
|
527
|
+
return this.map.size;
|
|
528
|
+
}
|
|
529
|
+
};
|
|
506
530
|
var DBTemplateResolver = class {
|
|
507
|
-
constructor(store) {
|
|
531
|
+
constructor(store, defaults) {
|
|
508
532
|
this.store = store;
|
|
533
|
+
this.defaults = defaults;
|
|
509
534
|
}
|
|
510
535
|
store;
|
|
536
|
+
defaults;
|
|
511
537
|
async resolve(type, data, locale) {
|
|
512
538
|
const [row] = await this.store.query(
|
|
513
539
|
// Serve the exact locale, else the neutral NULL default — never a
|
|
@@ -521,15 +547,14 @@ var DBTemplateResolver = class {
|
|
|
521
547
|
[type, locale ?? null]
|
|
522
548
|
);
|
|
523
549
|
if (!row) {
|
|
550
|
+
const def = this.defaults?.get(type);
|
|
551
|
+
if (def) {
|
|
552
|
+
return renderFragment(def, def.html ? await this.layout(locale) : void 0, data);
|
|
553
|
+
}
|
|
524
554
|
return { text: `${type}: ${JSON.stringify(data)}` };
|
|
525
555
|
}
|
|
526
|
-
const subject = row.subject ? render(row.subject, data) : void 0;
|
|
527
556
|
const layoutHtml = row.html ? await this.layout(locale) : void 0;
|
|
528
|
-
return
|
|
529
|
-
text: render(row.text, data),
|
|
530
|
-
...subject ? { subject } : {},
|
|
531
|
-
...row.html ? { html: composeHtml(row.html, layoutHtml, subject, data) } : {}
|
|
532
|
-
};
|
|
557
|
+
return renderFragment(row, layoutHtml, data);
|
|
533
558
|
}
|
|
534
559
|
// Optional founder-supplied layout shell; undefined → built-in default.
|
|
535
560
|
async layout(locale) {
|
|
@@ -545,10 +570,12 @@ var DBTemplateResolver = class {
|
|
|
545
570
|
}
|
|
546
571
|
};
|
|
547
572
|
var FSTemplateResolver = class {
|
|
548
|
-
constructor(directory) {
|
|
573
|
+
constructor(directory, defaults) {
|
|
549
574
|
this.directory = directory;
|
|
575
|
+
this.defaults = defaults;
|
|
550
576
|
}
|
|
551
577
|
directory;
|
|
578
|
+
defaults;
|
|
552
579
|
async resolve(type, data, locale) {
|
|
553
580
|
const { readFile } = await import("fs/promises");
|
|
554
581
|
const { join } = await import("path");
|
|
@@ -575,6 +602,13 @@ var FSTemplateResolver = class {
|
|
|
575
602
|
(v) => v ?? readOptional(join(this.directory, `${LAYOUT_TYPE}.html`))
|
|
576
603
|
) : readOptional(join(this.directory, `${LAYOUT_TYPE}.html`))
|
|
577
604
|
]);
|
|
605
|
+
if (text === null && html === null && subject === null) {
|
|
606
|
+
const def = this.defaults?.get(type);
|
|
607
|
+
if (def) {
|
|
608
|
+
return renderFragment(def, def.html ? layout ?? void 0 : void 0, data);
|
|
609
|
+
}
|
|
610
|
+
return { text: `${type}: ${JSON.stringify(data)}` };
|
|
611
|
+
}
|
|
578
612
|
const renderedSubject = subject ? render(subject, data) : void 0;
|
|
579
613
|
return {
|
|
580
614
|
text: text ? render(text, data) : `${type}: ${JSON.stringify(data)}`,
|
|
@@ -941,13 +975,15 @@ var CourierModule = class {
|
|
|
941
975
|
}
|
|
942
976
|
};
|
|
943
977
|
function createTemplateResolver(source, config, store) {
|
|
978
|
+
const input = config.templates?.defaults;
|
|
979
|
+
const defaults = new DefaultTemplates(input ? Array.isArray(input) ? input : [input] : []);
|
|
944
980
|
if (source === "fs") {
|
|
945
|
-
return new FSTemplateResolver(config.templates?.directory ?? "./templates");
|
|
981
|
+
return new FSTemplateResolver(config.templates?.directory ?? "./templates", defaults);
|
|
946
982
|
}
|
|
947
983
|
if (!store) {
|
|
948
984
|
throw new Error("[courier] store is required for DB template resolution");
|
|
949
985
|
}
|
|
950
|
-
return new DBTemplateResolver(store);
|
|
986
|
+
return new DBTemplateResolver(store, defaults);
|
|
951
987
|
}
|
|
952
988
|
|
|
953
989
|
// src/config.ts
|
|
@@ -961,6 +997,7 @@ var Channel = {
|
|
|
961
997
|
Channel,
|
|
962
998
|
CourierModule,
|
|
963
999
|
DBTemplateResolver,
|
|
1000
|
+
DefaultTemplates,
|
|
964
1001
|
Dispatcher,
|
|
965
1002
|
EmailChannel,
|
|
966
1003
|
FSTemplateResolver,
|
|
@@ -974,6 +1011,7 @@ var Channel = {
|
|
|
974
1011
|
handleSendGridDelivery,
|
|
975
1012
|
listTemplateEntries,
|
|
976
1013
|
listTemplateRevisions,
|
|
1014
|
+
renderFragment,
|
|
977
1015
|
rollbackTemplate,
|
|
978
1016
|
setTemplate,
|
|
979
1017
|
validateCourierConfig
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/module.ts","../src/log.ts","../src/dispatcher.ts","../src/channels/sms.ts","../src/channels/push.ts","../src/channels/email.ts","../src/templates/layout.ts","../src/templates/resolver.ts","../src/config-guard.ts","../src/delivery.ts","../src/templates/admin-routes.ts","../src/templates/admin.ts","../src/config.ts"],"sourcesContent":["export { CourierModule } from './module';\n// Production-readiness — warn on message types routed to a channel with no\n// provider. Runs automatically at boot (CourierModule.install); exported for an\n// app's own preflight.\nexport { validateCourierConfig } from './config-guard';\nexport { handleSendGridDelivery, handleMailgunDelivery, handleMailtrapDelivery } from './delivery';\nexport { Dispatcher } from './dispatcher';\nexport { SmsChannel } from './channels/sms';\nexport { PushChannel } from './channels/push';\nexport { EmailChannel } from './channels/email';\nexport { DBTemplateResolver, FSTemplateResolver } from './templates/resolver';\n// Versioned template management (on the @fonderie/store primitive): edit with\n// optimistic concurrency, revision history, rollback. The resolver is unchanged.\nexport {\n\tsetTemplate,\n\trollbackTemplate,\n\tlistTemplateRevisions,\n\tgetTemplateEntry,\n\tlistTemplateEntries,\n\tdeleteTemplate,\n} from './templates/admin';\nexport { buildTemplateAdminRoutes } from './templates/admin-routes';\nexport type { ITemplateEntry, ITemplateRevision } from './templates/admin';\nexport type { IMessageLog, MessageLogStatus } from './log';\nexport type {\n\tICourierMessage,\n\tICourierChannel,\n\tIRenderedTemplate,\n\tITemplateResolver,\n} from './types';\nexport { Channel } from './config';\nexport type {\n\tICourierConfig,\n\tIEmailChannelConfig,\n\tISmsChannelConfig,\n\tIPushChannelConfig,\n} from './config';\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport type { EventBus } from '@fonderie/events';\nimport { NOTIFICATION_EVENT } from '@fonderie/events';\n\nimport type { ITemplateResolver, ICourierMessage } from './types';\nimport type { ICourierConfig } from './config';\n\nimport { Dispatcher } from './dispatcher';\nimport { SmsChannel } from './channels/sms';\nimport { PushChannel } from './channels/push';\nimport { EmailChannel } from './channels/email';\nimport type { IReadinessProblem } from '@fonderie/core';\nimport { validateAdminToken } from '@fonderie/core/middlewares';\nimport { DBTemplateResolver, FSTemplateResolver } from './templates/resolver';\nimport { validateCourierConfig, collectCourierConfigProblems } from './config-guard';\nimport { handleSendGridDelivery, handleMailgunDelivery, handleMailtrapDelivery } from './delivery';\nimport { buildTemplateAdminRoutes } from './templates/admin-routes';\n\nexport class CourierModule implements IFonderieModule {\n\treadonly name = '@fonderie/courier';\n\treadonly deps = ['@fonderie/events'];\n\treadonly dispatcher: Dispatcher;\n\n\tconstructor(\n\t\tprivate config: ICourierConfig,\n\t\tprivate store?: IStoreAdapter,\n\t\tbus?: EventBus,\n\t) {\n\t\tconst templateSource = config.templates?.source ?? 'db';\n\t\tconst resolver = createTemplateResolver(templateSource, config, store);\n\n\t\tthis.dispatcher = new Dispatcher(config, resolver, store);\n\n\t\tif (config.email) this.dispatcher.registerChannel(new EmailChannel(config.email));\n\t\tif (config.sms) this.dispatcher.registerChannel(new SmsChannel(config.sms));\n\t\tif (config.push) this.dispatcher.registerChannel(new PushChannel(config.push));\n\n\t\tbus?.on<ICourierMessage>(\n\t\t\tNOTIFICATION_EVENT,\n\t\t\tasync (msg) => {\n\t\t\t\tawait this.dispatcher.dispatch(msg);\n\t\t\t},\n\t\t\t'courier',\n\t\t);\n\t}\n\n\t// Report config problems for app.checkProductionReadiness() (data, not warn).\n\tcheckReadiness(): IReadinessProblem[] {\n\t\treturn [\n\t\t\t...collectCourierConfigProblems(this.config, this.dispatcher.channelNames()),\n\t\t\t// Shared admin-token strength rule (@fonderie/core/middlewares) — the\n\t\t\t// /admin/templates surface must not be guarded by a weak/placeholder token.\n\t\t\t...validateAdminToken(this.config.adminToken, { module: this.name }),\n\t\t];\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\t// Boot-time preflight: warn if any routed message type has no provider.\n\t\tvalidateCourierConfig(this.config, this.dispatcher.channelNames());\n\n\t\tconst store = this.store;\n\t\tconst signingKeys = this.config.delivery?.signingKeys;\n\n\t\tapp.addRoute('POST', '/courier/delivery/sendgrid', (ctx) =>\n\t\t\thandleSendGridDelivery(ctx.request, store!, signingKeys?.sendgrid),\n\t\t);\n\t\tapp.addRoute('POST', '/courier/delivery/mailgun', (ctx) =>\n\t\t\thandleMailgunDelivery(ctx.request, store!, signingKeys?.mailgun),\n\t\t);\n\t\tapp.addRoute('POST', '/courier/delivery/mailtrap', (ctx) =>\n\t\t\thandleMailtrapDelivery(ctx.request, store!),\n\t\t);\n\n\t\t// Versioned template admin routes — only when a token is configured and a\n\t\t// store is present (db templates). Mirrors @fonderie/config's admin surface.\n\t\tif (this.config.adminToken) {\n\t\t\tif (!store) {\n\t\t\t\tthrow new Error('[courier] adminToken requires @fonderie/store (db templates)');\n\t\t\t}\n\t\t\tfor (const [method, path, handler] of buildTemplateAdminRoutes(store, this.config.adminToken)) {\n\t\t\t\tapp.addRoute(method, path, handler);\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction createTemplateResolver(\n\tsource: 'db' | 'fs',\n\tconfig: ICourierConfig,\n\tstore?: IStoreAdapter,\n): ITemplateResolver {\n\tif (source === 'fs') {\n\t\treturn new FSTemplateResolver(config.templates?.directory ?? './templates');\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);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nexport type MessageLogStatus =\n\t| 'pending' | 'sent' | 'failed'\n\t| 'delivered' | 'opened' | 'clicked' | 'bounced' | 'spam';\n\nexport interface IMessageLog {\n\tid: string;\n\tmessageType: string;\n\tchannel: string;\n\trecipient: string;\n\tlocale: string | null;\n\tstatus: MessageLogStatus;\n\terror: string | null;\n\tattempts: number;\n\tprovider: string | null;\n\tproviderMessageId: string | null;\n\topenedAt: string | null;\n\tclickedAt: string | null;\n\tbouncedAt: string | null;\n\tbounceReason: string | null;\n\tcreatedAt: string;\n\tsentAt: string | null;\n}\n\nexport async function insertMessageLog(\n\tentry: {\n\t\tmessageType: string;\n\t\tchannel: string;\n\t\trecipient: string;\n\t\tlocale?: string;\n\t\tprovider?: string;\n\t\tproviderMessageId?: string;\n\t},\n\tstore: IStoreAdapter,\n): Promise<string> {\n\tconst [row] = await store.query<{ id: string }>(\n\t\t`INSERT INTO fonderie_message_log\n\t\t\t(message_type, channel, recipient, locale, provider, provider_message_id)\n\t\t VALUES ($1, $2, $3, $4, $5, $6)\n\t\t RETURNING id`,\n\t\t[\n\t\t\tentry.messageType,\n\t\t\tentry.channel,\n\t\t\tentry.recipient,\n\t\t\tentry.locale ?? null,\n\t\t\tentry.provider ?? null,\n\t\t\tentry.providerMessageId ?? null,\n\t\t],\n\t);\n\treturn row?.id ?? '';\n}\n\nexport async function markMessageSent(id: string, store: IStoreAdapter): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'sent', sent_at = now(), attempts = attempts + 1\n\t\t WHERE id = $1`,\n\t\t[id],\n\t);\n}\n\nexport async function markMessageFailed(\n\tid: string,\n\terror: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'failed', error = $2, attempts = attempts + 1\n\t\t WHERE id = $1`,\n\t\t[id, error],\n\t);\n}\n\nexport async function markMessageDelivered(\n\tproviderMessageId: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'delivered'\n\t\t WHERE provider_message_id = $1`,\n\t\t[providerMessageId],\n\t);\n}\n\nexport async function markMessageOpened(\n\tproviderMessageId: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'opened', opened_at = now()\n\t\t WHERE provider_message_id = $1 AND opened_at IS NULL`,\n\t\t[providerMessageId],\n\t);\n}\n\nexport async function markMessageClicked(\n\tproviderMessageId: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'clicked', clicked_at = now()\n\t\t WHERE provider_message_id = $1 AND clicked_at IS NULL`,\n\t\t[providerMessageId],\n\t);\n}\n\nexport async function markMessageBounced(\n\tproviderMessageId: string,\n\treason: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'bounced', bounced_at = now(), bounce_reason = $2\n\t\t WHERE provider_message_id = $1`,\n\t\t[providerMessageId, reason],\n\t);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ICourierConfig } from './config';\nimport type { ICourierMessage, ICourierChannel, ITemplateResolver } from './types';\nimport { insertMessageLog, markMessageSent, markMessageFailed } from './log';\n\nfunction resolveRecipient(message: ICourierMessage, channel: string): string {\n\tif (channel === 'email') return message.recipient.email ?? '';\n\tif (channel === 'sms') return message.recipient.phone ?? '';\n\tif (channel === 'push') return message.recipient.deviceToken ?? '';\n\treturn message.recipient.email ?? message.recipient.phone ?? '';\n}\n\nexport class Dispatcher {\n\tprivate channels: Map<string, ICourierChannel> = new Map();\n\n\tconstructor(\n\t\tprivate config: ICourierConfig,\n\t\tprivate resolver: ITemplateResolver,\n\t\tprivate store?: IStoreAdapter,\n\t) {}\n\n\tregisterChannel(channel: ICourierChannel): this {\n\t\tthis.channels.set(channel.name, channel);\n\t\treturn this;\n\t}\n\n\t// Names of the currently-registered channels — used by the boot-time config\n\t// guard to detect message types routed to a channel with no provider.\n\tchannelNames(): string[] {\n\t\treturn [...this.channels.keys()];\n\t}\n\n\tasync dispatch(message: ICourierMessage): Promise<void> {\n\t\tconst channelNames = this.config.channels[message.type];\n\n\t\tif (!channelNames || channelNames.length === 0) {\n\t\t\tconsole.warn(`[courier] no channels configured for message type: ${message.type}`);\n\t\t\treturn;\n\t\t}\n\n\t\tconst template = await this.resolver.resolve(message.type, message.data, message.locale);\n\n\t\tawait Promise.allSettled(\n\t\t\tchannelNames.map(async (name) => {\n\t\t\t\tconst channel = this.channels.get(name);\n\t\t\t\tif (!channel) {\n\t\t\t\t\tconsole.warn(`[courier] channel \"${name}\" not registered`);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Insert log entry (fire-and-forget if store unavailable)\n\t\t\t\tconst logEntry: Parameters<typeof insertMessageLog>[0] = {\n\t\t\t\t\tmessageType: message.type,\n\t\t\t\t\tchannel: name,\n\t\t\t\t\trecipient: resolveRecipient(message, name),\n\t\t\t\t};\n\t\t\t\tif (message.locale) logEntry.locale = message.locale;\n\n\t\t\t\tconst logId = this.store\n\t\t\t\t\t? await insertMessageLog(logEntry, this.store).catch(() => '')\n\t\t\t\t\t: '';\n\n\t\t\t\ttry {\n\t\t\t\t\tawait channel.send(message, template);\n\n\t\t\t\t\tif (this.store && logId) {\n\t\t\t\t\t\tmarkMessageSent(logId, this.store).catch(() => undefined);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tconsole.error(`[courier:${name}] failed to send ${message.type}:`, err);\n\n\t\t\t\t\tif (this.store && logId) {\n\t\t\t\t\t\tmarkMessageFailed(logId, errMsg, this.store).catch(() => undefined);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\t}\n}\n","import type { ISmsChannelConfig } from '../config';\nimport type { ICourierChannel, ICourierMessage, IRenderedTemplate } from '../types';\n\nexport class SmsChannel implements ICourierChannel {\n\treadonly name = 'sms';\n\n\tconstructor(private config: ISmsChannelConfig) {}\n\n\tasync send(message: ICourierMessage, template: IRenderedTemplate): Promise<void> {\n\t\tconst to = message.recipient.phone;\n\t\tif (!to) {\n\t\t\tconsole.warn('[courier:sms] no phone number for recipient — skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.config.provider === 'twilio') {\n\t\t\tawait this.sendViaTwilio(to, template.text);\n\t\t} else if (this.config.provider === 'vonage') {\n\t\t\tawait this.sendViaVonage(to, template.text);\n\t\t} else {\n\t\t\tconsole.warn(`[courier:sms] provider ${this.config.provider} not implemented`);\n\t\t}\n\t}\n\n\tprivate async sendViaTwilio(to: string, text: string): Promise<void> {\n\t\tconst { accountSid, authToken } = this.config;\n\t\tif (!accountSid || !authToken) {\n\t\t\tthrow new Error('Twilio accountSid and authToken required');\n\t\t}\n\n\t\tconst res = await fetch(\n\t\t\t`https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`,\n\t\t\t{\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Basic ${Buffer.from(`${accountSid}:${authToken}`).toString('base64')}`,\n\t\t\t\t\t'Content-Type': 'application/x-www-form-urlencoded',\n\t\t\t\t},\n\t\t\t\tbody: new URLSearchParams({ From: this.config.from, To: to, Body: text }),\n\t\t\t},\n\t\t);\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:sms] Twilio error ${res.status}: ${body}`);\n\t\t}\n\t}\n\n\tprivate async sendViaVonage(to: string, text: string): Promise<void> {\n\t\tconst { apiKey, apiSecret } = this.config;\n\t\tif (!apiKey || !apiSecret) {\n\t\t\tthrow new Error('Vonage apiKey and apiSecret required');\n\t\t}\n\n\t\tconst res = await fetch('https://rest.nexmo.com/sms/json', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({\n\t\t\t\tapi_key: apiKey,\n\t\t\t\tapi_secret: apiSecret,\n\t\t\t\tfrom: this.config.from,\n\t\t\t\tto,\n\t\t\t\ttext,\n\t\t\t}),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:sms] Vonage error ${res.status}: ${body}`);\n\t\t}\n\t}\n}\n","import type { IPushChannelConfig } from '../config';\nimport type { ICourierChannel, ICourierMessage, IRenderedTemplate } from '../types';\n\nexport class PushChannel implements ICourierChannel {\n\treadonly name = 'push';\n\n\tconstructor(private config: IPushChannelConfig) {}\n\n\tasync send(message: ICourierMessage, template: IRenderedTemplate): Promise<void> {\n\t\tconst token = message.recipient.deviceToken;\n\t\tif (!token) {\n\t\t\tconsole.warn('[courier:push] no device token for recipient — skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.config.provider === 'fcm') {\n\t\t\tawait this.sendViaFCM(token, template);\n\t\t}\n\t}\n\n\tprivate async sendViaFCM(token: string, template: IRenderedTemplate): Promise<void> {\n\t\tconst apiKey = this.config.serviceAccount['apiKey'] as string | undefined;\n\t\tif (!apiKey) {\n\t\t\tthrow new Error('[courier:push] FCM apiKey is required in serviceAccount');\n\t\t}\n\n\t\tconst res = await fetch('https://fcm.googleapis.com/fcm/send', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\tAuthorization: `key=${apiKey}`,\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tto: token,\n\t\t\t\tnotification: {\n\t\t\t\t\ttitle: template.subject,\n\t\t\t\t\tbody: template.text,\n\t\t\t\t},\n\t\t\t}),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:push] FCM error ${res.status}: ${body}`);\n\t\t}\n\t}\n}\n","import nodemailer from 'nodemailer';\n\nimport type { ICourierChannel, ICourierMessage, IRenderedTemplate } from '../types';\nimport type { IEmailChannelConfig } from '../config';\n\nexport class EmailChannel implements ICourierChannel {\n\treadonly name = 'email';\n\tprivate transport: ReturnType<typeof nodemailer.createTransport> | null = null;\n\n\tconstructor(private config: IEmailChannelConfig) {\n\t\tif (config.provider === 'smtp' && config.smtp) {\n\t\t\tthis.transport = nodemailer.createTransport({\n\t\t\t\thost: config.smtp.host,\n\t\t\t\tport: config.smtp.port,\n\t\t\t\tsecure: config.smtp.secure,\n\t\t\t\tauth: { user: config.smtp.user, pass: config.smtp.pass },\n\t\t\t});\n\t\t}\n\t}\n\n\tasync send(message: ICourierMessage, template: IRenderedTemplate): Promise<void> {\n\t\tconst to = message.recipient.email;\n\t\tif (!to) {\n\t\t\tconsole.warn('[courier:email] no email address for recipient — skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.config.provider === 'resend') {\n\t\t\tawait this.sendViaResend(to, template);\n\t\t} else if (this.config.provider === 'smtp') {\n\t\t\tawait this.sendViaSMTP(to, template);\n\t\t} else {\n\t\t\tconsole.warn(`[courier:email] provider ${this.config.provider} not implemented`);\n\t\t}\n\t}\n\n\tprivate async sendViaResend(to: string, template: IRenderedTemplate): Promise<void> {\n\t\tif (!this.config.apiKey) {\n\t\t\tthrow new Error('Resend apiKey is required');\n\t\t}\n\n\t\tconst res = await fetch('https://api.resend.com/emails', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${this.config.apiKey}`,\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tfrom: this.config.from,\n\t\t\t\tto,\n\t\t\t\tsubject: template.subject ?? '(no subject)',\n\t\t\t\thtml: template.html,\n\t\t\t\ttext: template.text,\n\t\t\t}),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:email] Resend error ${res.status}: ${body}`);\n\t\t}\n\t}\n\n\tprivate async sendViaSMTP(to: string, template: IRenderedTemplate): Promise<void> {\n\t\tif (!this.transport) {\n\t\t\tthrow new Error('SMTP transport not initialised — check smtp config');\n\t\t}\n\n\t\tawait this.transport.sendMail({\n\t\t\tfrom: this.config.from,\n\t\t\tto,\n\t\t\tsubject: template.subject ?? '(no subject)',\n\t\t\thtml: template.html,\n\t\t\ttext: template.text,\n\t\t});\n\t}\n}\n","// Shared, composable email layout — a branded shell that wraps every template\n// body. Templates stored in the DB or on disk are **body fragments**; the\n// resolver injects them into `{{content}}` here, then interpolates `{{vars}}`\n// over the whole. One shell, many bodies — so DB and FS sources render an\n// identical, production-grade frame for free.\n//\n// Cross-client hardening (Outlook/Gmail/Apple Mail) follows the well-worn\n// \"hybrid responsive\" pattern popularised by Cerberus (MIT): a max-width table\n// container, inline styles on the critical structure, and a `<style>` block in\n// `<head>` for the friendly classes and small-screen media query. Founders can\n// override the whole shell by storing a `_layout` template (DB row or\n// `_layout.html` file) in their own source.\n//\n// The theme tokens mirror the product design system (organization/ui/base.css):\n// near-black primary, a mint brand accent, Inter, and the `#fafafa` canvas —\n// retune `EMAIL_THEME` to rebrand every email at once.\n\n// A single small token set — retune these to match your brand.\nexport const EMAIL_THEME = {\n\tbrand: 'Fonderie',\n\taccent: '#171717', // primary action (button) background — the product's primary\n\taccentText: '#ffffff', // text on the accent\n\tbrandAccent: '#171717', // brand highlight (top rule) — monochrome near-black\n\tlink: '#009767', // links — the accessible (darkened) brand teal\n\tink: '#171717', // body copy\n\tmuted: '#5c5c5c', // secondary copy\n\tline: '#e0e0e0', // hairline borders\n\tcanvas: '#fafafa', // page background behind the card\n\tcard: '#ffffff', // the card itself\n} as const;\n\n// The product font stack (Inter first, with system fallbacks for clients that\n// lack it). Shared by every text element so the frame stays consistent.\nconst FONT_SANS =\n\t\"Inter, 'Inter Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif\";\nconst FONT_MONO = \"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace\";\n\n// The `{{content}}` marker is replaced with the body fragment BEFORE variable\n// interpolation, so a body's own `{{firstName}}` etc. still resolve.\nexport const LAYOUT_CONTENT_SLOT = '{{content}}';\n\nexport const DEFAULT_EMAIL_LAYOUT = `<!DOCTYPE html>\n<html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n<head>\n\t<meta charset=\"utf-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"color-scheme\" content=\"light dark\">\n\t<meta name=\"supported-color-schemes\" content=\"light dark\">\n\t<title>{{subject}}</title>\n\t<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->\n\t<style>\n\t\tbody { margin: 0; padding: 0; width: 100% !important; background: ${EMAIL_THEME.canvas}; }\n\t\t.email-body { background: ${EMAIL_THEME.canvas}; }\n\t\t.email-container { width: 100%; max-width: 560px; margin: 0 auto; }\n\t\t.email-card {\n\t\t\tbackground: ${EMAIL_THEME.card};\n\t\t\tborder: 1px solid ${EMAIL_THEME.line};\n\t\t\tborder-radius: 8px;\n\t\t\toverflow: hidden;\n\t\t}\n\t\t.email-accent { height: 3px; background: ${EMAIL_THEME.brandAccent}; font-size: 0; line-height: 0; }\n\t\t.email-header { padding: 28px 32px 0 32px; }\n\t\t.email-brand { font: 800 19px/1 ${FONT_SANS}; color: ${EMAIL_THEME.ink}; letter-spacing: -0.03em; }\n\t\t.email-content {\n\t\t\tpadding: 20px 32px 8px 32px;\n\t\t\tfont: 400 16px/1.7 ${FONT_SANS};\n\t\t\tcolor: ${EMAIL_THEME.ink};\n\t\t\tletter-spacing: -0.01em;\n\t\t}\n\t\t.email-content h1 { margin: 0 0 12px 0; font-size: 22px; line-height: 1.3; font-weight: 700; letter-spacing: -0.03em; }\n\t\t.email-content p { margin: 0 0 16px 0; }\n\t\t.email-content a { color: ${EMAIL_THEME.link}; }\n\t\t.email-footer {\n\t\t\tpadding: 16px 32px 28px 32px;\n\t\t\tfont: 400 13px/1.6 ${FONT_SANS};\n\t\t\tcolor: ${EMAIL_THEME.muted};\n\t\t\tletter-spacing: -0.01em;\n\t\t}\n\t\t.btn {\n\t\t\tdisplay: inline-block;\n\t\t\tbackground: ${EMAIL_THEME.accent};\n\t\t\tcolor: ${EMAIL_THEME.accentText} !important;\n\t\t\ttext-decoration: none;\n\t\t\tfont: 600 16px/1 ${FONT_SANS};\n\t\t\tletter-spacing: -0.01em;\n\t\t\tpadding: 13px 26px;\n\t\t\tborder-radius: 6px;\n\t\t}\n\t\t.pin-code {\n\t\t\tdisplay: inline-block;\n\t\t\tfont: 700 30px/1 ${FONT_MONO};\n\t\t\tletter-spacing: 0.28em;\n\t\t\tcolor: ${EMAIL_THEME.ink};\n\t\t\tbackground: ${EMAIL_THEME.canvas};\n\t\t\tborder: 1px solid ${EMAIL_THEME.line};\n\t\t\tborder-radius: 6px;\n\t\t\tpadding: 14px 22px 14px 30px;\n\t\t}\n\t\t.muted { color: ${EMAIL_THEME.muted}; }\n\t\t@media only screen and (max-width: 599px) {\n\t\t\t.email-header, .email-content, .email-footer { padding-left: 22px !important; padding-right: 22px !important; }\n\t\t\t.pin-code { font-size: 26px !important; letter-spacing: 0.2em !important; }\n\t\t}\n\t</style>\n</head>\n<body class=\"email-body\">\n\t<div style=\"display:none;max-height:0;overflow:hidden;opacity:0;\">{{preheader}}</div>\n\t<table role=\"presentation\" class=\"email-body\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"background:${EMAIL_THEME.canvas};\">\n\t\t<tr>\n\t\t\t<td align=\"center\" style=\"padding: 32px 16px;\">\n\t\t\t\t<table role=\"presentation\" class=\"email-container\" width=\"560\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"max-width:560px;\">\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td class=\"email-card\" style=\"background:${EMAIL_THEME.card};border:1px solid ${EMAIL_THEME.line};border-radius:8px;\">\n\t\t\t\t\t\t\t<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n\t\t\t\t\t\t\t\t<tr><td class=\"email-accent\" style=\"height:3px;background:${EMAIL_THEME.brandAccent};font-size:0;line-height:0;\"> </td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-header\" style=\"padding:28px 32px 0 32px;\">\n\t\t\t\t\t\t\t\t\t<span class=\"email-brand\" style=\"font-weight:800;font-size:19px;color:${EMAIL_THEME.ink};letter-spacing:-0.03em;\">${EMAIL_THEME.brand}</span>\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-content\" style=\"padding:20px 32px 8px 32px;color:${EMAIL_THEME.ink};\">\n${LAYOUT_CONTENT_SLOT}\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-footer\" style=\"padding:16px 32px 28px 32px;color:${EMAIL_THEME.muted};font-size:13px;\">\n\t\t\t\t\t\t\t\t\tYou're receiving this because someone used this address at ${EMAIL_THEME.brand}. If that wasn't you, you can ignore it.\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t</table>\n\t\t\t</td>\n\t\t</tr>\n\t</table>\n</body>\n</html>`;\n\n// Inject a body fragment into a layout shell. If the body is already a full\n// HTML document (a founder chose to store a complete template), it is returned\n// untouched — never double-wrapped.\nexport function wrapLayout(bodyHtml: string, layoutHtml: string = DEFAULT_EMAIL_LAYOUT): string {\n\tconst trimmed = bodyHtml.trimStart().toLowerCase();\n\tif (trimmed.startsWith('<!doctype') || trimmed.startsWith('<html')) {\n\t\treturn bodyHtml;\n\t}\n\treturn layoutHtml.replace(LAYOUT_CONTENT_SLOT, bodyHtml);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ITemplateResolver, IRenderedTemplate } from '../types';\nimport { wrapLayout } from './layout';\n\n// The stored template id for a founder-supplied layout shell (DB row `type` or\n// FS file `_layout.html`). Absent → the built-in DEFAULT_EMAIL_LAYOUT is used.\nconst LAYOUT_TYPE = '_layout';\n\nfunction render(template: string, data: Record<string, unknown>): string {\n\treturn template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) => {\n\t\tconst value = data[key];\n\t\treturn value !== undefined && value !== null ? String(value) : '';\n\t});\n}\n\n// Compose a body fragment into its layout shell, then interpolate variables\n// over the whole. `subject`/`preheader` become available to the shell's title\n// and inbox preview text.\nfunction composeHtml(\n\tbodyHtml: string,\n\tlayoutHtml: string | undefined,\n\tsubject: string | undefined,\n\tdata: Record<string, unknown>,\n): string {\n\tconst wrapped = wrapLayout(bodyHtml, layoutHtml);\n\treturn render(wrapped, { subject: subject ?? '', preheader: '', ...data });\n}\n\n// DB-backed resolver — reads from fonderie_courier_templates with locale fallback\nexport class DBTemplateResolver implements ITemplateResolver {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync resolve(\n\t\ttype: string,\n\t\tdata: Record<string, unknown>,\n\t\tlocale?: string,\n\t): Promise<IRenderedTemplate> {\n\t\tconst [row] = await this.store.query<{\n\t\t\tsubject: string | null;\n\t\t\thtml: string | null;\n\t\t\ttext: string;\n\t\t}>(\n\t\t\t// Serve the exact locale, else the neutral NULL default — never a\n\t\t\t// sibling region (en-CA must not fall back to en-US); the WHERE\n\t\t\t// excludes other locales so legal/jurisdictional copy can't bleed.\n\t\t\t`SELECT subject, html, text\n\t\t\t FROM fonderie_courier_templates\n\t\t\t WHERE type = $1 AND active = true AND (locale = $2 OR locale IS NULL)\n\t\t\t ORDER BY (locale IS NOT DISTINCT FROM $2) DESC\n\t\t\t LIMIT 1`,\n\t\t\t[type, locale ?? null],\n\t\t);\n\n\t\tif (!row) {\n\t\t\treturn { text: `${type}: ${JSON.stringify(data)}` };\n\t\t}\n\n\t\tconst subject = row.subject ? render(row.subject, data) : undefined;\n\t\tconst layoutHtml = row.html ? await this.layout(locale) : undefined;\n\n\t\treturn {\n\t\t\ttext: render(row.text, data),\n\t\t\t...(subject ? { subject } : {}),\n\t\t\t...(row.html ? { html: composeHtml(row.html, layoutHtml, subject, data) } : {}),\n\t\t};\n\t}\n\n\t// Optional founder-supplied layout shell; undefined → built-in default.\n\tprivate async layout(locale?: string): Promise<string | undefined> {\n\t\tconst [row] = await this.store.query<{ html: string | null }>(\n\t\t\t`SELECT html\n\t\t\t FROM fonderie_courier_templates\n\t\t\t WHERE type = $1 AND active = true AND (locale = $2 OR locale IS NULL)\n\t\t\t ORDER BY (locale IS NOT DISTINCT FROM $2) DESC\n\t\t\t LIMIT 1`,\n\t\t\t[LAYOUT_TYPE, locale ?? null],\n\t\t);\n\t\treturn row?.html ?? undefined;\n\t}\n}\n\n// Filesystem resolver — reads {type}.{locale}.txt → {type}.txt with fallback\nexport class FSTemplateResolver implements ITemplateResolver {\n\tconstructor(private directory: string) {}\n\n\tasync resolve(\n\t\ttype: string,\n\t\tdata: Record<string, unknown>,\n\t\tlocale?: string,\n\t): Promise<IRenderedTemplate> {\n\t\tconst { readFile } = await import('node:fs/promises');\n\t\tconst { join } = await import('node:path');\n\n\t\tconst readOptional = async (path: string): Promise<string | null> => {\n\t\t\ttry {\n\t\t\t\treturn await readFile(path, 'utf8');\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t};\n\n\t\t// Per-locale variants take priority over generic variants\n\t\tconst localePrefix = locale ? `${type}.${locale}` : null;\n\t\tconst layoutPrefix = locale ? `${LAYOUT_TYPE}.${locale}` : null;\n\n\t\tconst [text, html, subject, layout] = await Promise.all([\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.txt`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.txt`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.txt`)),\n\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.html`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.html`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.html`)),\n\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.subject.txt`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.subject.txt`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.subject.txt`)),\n\n\t\t\tlayoutPrefix\n\t\t\t\t? readOptional(join(this.directory, `${layoutPrefix}.html`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${LAYOUT_TYPE}.html`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${LAYOUT_TYPE}.html`)),\n\t\t]);\n\n\t\tconst renderedSubject = subject ? render(subject, data) : undefined;\n\n\t\treturn {\n\t\t\ttext: text ? render(text, data) : `${type}: ${JSON.stringify(data)}`,\n\t\t\t...(renderedSubject ? { subject: renderedSubject } : {}),\n\t\t\t...(html ? { html: composeHtml(html, layout ?? undefined, renderedSubject, data) } : {}),\n\t\t};\n\t}\n}\n","import type { IReadinessProblem } from '@fonderie/core';\nimport type { ICourierConfig } from './config';\n\nconst MODULE = '@fonderie/courier';\n\n// Pure production-readiness assessment (no side effects) — shared by the\n// boot-time guard and `CourierModule.checkReadiness`. A message type routed to a\n// channel with no registered provider is silently dropped by the dispatcher —\n// with `requireVerification` on, verification / reset emails vanish and users\n// lock themselves out. These are `warning`-severity (channel setups vary\n// legitimately — an app may run in-app only — so this never blocks boot, unlike\n// auth's fatal weak-secret). Checks the channels actually **registered** on the\n// dispatcher, so channels added via `registerChannel` count as present.\nexport function collectCourierConfigProblems(\n\tconfig: ICourierConfig,\n\tregisteredChannels: Iterable<string>,\n): IReadinessProblem[] {\n\tconst registered = new Set(registeredChannels);\n\tconst gaps = new Map<string, string[]>(); // channel → message types routed to it\n\n\tfor (const [type, channels] of Object.entries(config.channels ?? {})) {\n\t\tfor (const channel of channels) {\n\t\t\tif (!registered.has(channel)) {\n\t\t\t\tconst types = gaps.get(channel) ?? [];\n\t\t\t\ttypes.push(type);\n\t\t\t\tgaps.set(channel, types);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn [...gaps].map(([channel, types]) => ({\n\t\tmodule: MODULE,\n\t\tseverity: 'warning' as const,\n\t\tmessage:\n\t\t\t`${types.length} message type(s) route to the '${channel}' channel but ` +\n\t\t\t`no '${channel}' provider is registered — these will be silently dropped: ` +\n\t\t\t`${types.join(', ')}. Configure \\`config.${channel}\\` (or register a channel).`,\n\t}));\n}\n\n// Boot-time preflight, run from `CourierModule.install`. Warn-only.\nexport function validateCourierConfig(\n\tconfig: ICourierConfig,\n\tregisteredChannels: Iterable<string>,\n): void {\n\tfor (const problem of collectCourierConfigProblems(config, registeredChannels)) {\n\t\tconsole.warn(`[courier] ${problem.message}`);\n\t}\n}\n","import { createHmac } from 'node:crypto';\n\nimport { constantTimeEqual } from '@fonderie/core';\n\nimport type { IStoreAdapter } from '@fonderie/store';\nimport {\n\tmarkMessageDelivered,\n\tmarkMessageOpened,\n\tmarkMessageClicked,\n\tmarkMessageBounced,\n} from './log';\n\n// ── SendGrid ──────────────────────────────────────────────────────\n//\n// Verifies the X-Twilio-Email-Event-Webhook-Signature header.\n// Expects an array of event objects in the request body.\n// https://docs.sendgrid.com/for-developers/tracking-events/getting-started-event-webhook-security-features\n\nexport async function handleSendGridDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n\twebhookSecret?: string,\n): Promise<Response> {\n\tif (webhookSecret) {\n\t\tconst sig = req.headers.get('x-twilio-email-event-webhook-signature') ?? '';\n\t\tconst ts = req.headers.get('x-twilio-email-event-webhook-timestamp') ?? '';\n\t\tconst body = await req.text();\n\n\t\tif (!verifySendGridSignature(webhookSecret, ts, body, sig)) {\n\t\t\treturn Response.json({ error: 'INVALID_SIGNATURE' }, { status: 401 });\n\t\t}\n\n\t\tconst events = parseJson(body) as SendGridEvent[] | null;\n\t\tif (!Array.isArray(events)) return Response.json({ ok: true });\n\n\t\tawait processSendGridEvents(events, store);\n\t\treturn Response.json({ ok: true });\n\t}\n\n\tconst events = (await req.json()) as SendGridEvent[];\n\tif (!Array.isArray(events)) return Response.json({ ok: true });\n\n\tawait processSendGridEvents(events, store);\n\treturn Response.json({ ok: true });\n}\n\nfunction verifySendGridSignature(\n\tsecret: string,\n\ttimestamp: string,\n\tbody: string,\n\tsignature: string,\n): boolean {\n\ttry {\n\t\tconst payload = timestamp + body;\n\t\tconst expected = createHmac('sha256', secret).update(payload).digest('base64');\n\t\treturn constantTimeEqual(Buffer.from(signature, 'base64'), Buffer.from(expected, 'base64'));\n\t} catch {\n\t\treturn false;\n\t}\n}\n\ninterface SendGridEvent {\n\tevent: string;\n\tsg_message_id?: string;\n\treason?: string;\n\t[key: string]: unknown;\n}\n\nasync function processSendGridEvents(\n\tevents: SendGridEvent[],\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tfor (const ev of events) {\n\t\tconst msgId = ev['sg_message_id'];\n\t\tif (!msgId || typeof msgId !== 'string') continue;\n\t\t// sg_message_id is \"msgId.filterXxx\" — strip the filter suffix\n\t\tconst id = msgId.split('.')[0] ?? msgId;\n\n\t\tswitch (ev.event) {\n\t\t\tcase 'delivered': await markMessageDelivered(id, store); break;\n\t\t\tcase 'open': await markMessageOpened(id, store); break;\n\t\t\tcase 'click': await markMessageClicked(id, store); break;\n\t\t\tcase 'bounce':\n\t\t\tcase 'blocked':\n\t\t\tcase 'dropped':\n\t\t\t\tawait markMessageBounced(id, typeof ev['reason'] === 'string' ? ev['reason'] : ev.event, store);\n\t\t\t\tbreak;\n\t\t}\n\t}\n}\n\n// ── Mailgun ───────────────────────────────────────────────────────\n//\n// Verifies signature using token + timestamp + signing key (HMAC-SHA256).\n// https://documentation.mailgun.com/docs/mailgun/user-manual/tracking-messages/#securing-webhooks\n\nexport async function handleMailgunDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n\tsigningKey?: string,\n): Promise<Response> {\n\tconst body = (await req.json()) as MailgunPayload;\n\n\tif (signingKey) {\n\t\tconst { signature } = body;\n\t\tif (!signature || !verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {\n\t\t\treturn Response.json({ error: 'INVALID_SIGNATURE' }, { status: 401 });\n\t\t}\n\t}\n\n\tconst event = body['event-data'];\n\tif (event) {\n\t\tawait processMailgunEvent(event, store);\n\t}\n\n\treturn Response.json({ ok: true });\n}\n\nfunction verifyMailgunSignature(\n\tsigningKey: string,\n\ttimestamp: string,\n\ttoken: string,\n\tsignature: string,\n): boolean {\n\ttry {\n\t\tconst value = timestamp + token;\n\t\tconst expected = createHmac('sha256', signingKey).update(value).digest('hex');\n\t\treturn constantTimeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'));\n\t} catch {\n\t\treturn false;\n\t}\n}\n\ninterface MailgunPayload {\n\tsignature?: { timestamp: string; token: string; signature: string };\n\t'event-data'?: MailgunEvent;\n\t[key: string]: unknown;\n}\n\ninterface MailgunEvent {\n\tevent: string;\n\tmessage?: { headers?: { 'message-id'?: string } };\n\t'delivery-status'?: { message?: string };\n\t[key: string]: unknown;\n}\n\nasync function processMailgunEvent(event: MailgunEvent, store: IStoreAdapter): Promise<void> {\n\tconst msgId = event.message?.headers?.['message-id'];\n\tif (!msgId) return;\n\n\tswitch (event.event) {\n\t\tcase 'delivered': await markMessageDelivered(msgId, store); break;\n\t\tcase 'opened': await markMessageOpened(msgId, store); break;\n\t\tcase 'clicked': await markMessageClicked(msgId, store); break;\n\t\tcase 'failed':\n\t\tcase 'bounced': {\n\t\t\tconst reason = event['delivery-status']?.message ?? event.event;\n\t\t\tawait markMessageBounced(msgId, reason, store);\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n// ── Mailtrap (testing only, no signature) ────────────────────────\n\nexport async function handleMailtrapDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n): Promise<Response> {\n\tconst events = (await req.json()) as MailtrapEvent[];\n\tif (!Array.isArray(events)) return Response.json({ ok: true });\n\n\tfor (const ev of events) {\n\t\tconst msgId = ev.message_id;\n\t\tif (!msgId) continue;\n\n\t\tswitch (ev.event) {\n\t\t\tcase 'delivery': await markMessageDelivered(msgId, store); break;\n\t\t\tcase 'open': await markMessageOpened(msgId, store); break;\n\t\t\tcase 'click': await markMessageClicked(msgId, store); break;\n\t\t\tcase 'bounce':\n\t\t\tcase 'soft_bounce':\n\t\t\t\tawait markMessageBounced(msgId, ev.event, store);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn Response.json({ ok: true });\n}\n\ninterface MailtrapEvent {\n\tevent: string;\n\tmessage_id?: string;\n\t[key: string]: unknown;\n}\n\n// ── Utility ───────────────────────────────────────────────────────\n\nfunction parseJson(text: string): unknown {\n\ttry { return JSON.parse(text); } catch { return null; }\n}\n","import type { IFonderieContext, Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport { requireAdminToken } from '@fonderie/core/middlewares';\nimport { VersionConflictError } from '@fonderie/store';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport {\n\tsetTemplate,\n\trollbackTemplate,\n\tlistTemplateRevisions,\n\tgetTemplateEntry,\n\tlistTemplateEntries,\n\tdeleteTemplate,\n} from './admin';\n\n// Bearer-token guard (mirrors @fonderie/config's admin surface). Only registered\n// when a token is configured — no token, no exposed template admin routes.\n// Wrap a handler in the shared admin-token guard (@fonderie/core/middlewares) —\n// one constant-time Bearer check across billing/config/courier.\nfunction guarded(adminToken: string, handler: Middleware): Middleware {\n\tconst guard = requireAdminToken(adminToken);\n\treturn (ctx, next) => guard(ctx, () => handler(ctx, next));\n}\n\nconst actorOf = (ctx: IFonderieContext) => ctx.request.headers.get('x-actor') || 'admin-token';\nconst typeOf = (ctx: IFonderieContext) => ctx.meta.params?.['type'] ?? '';\nconst localeOf = (ctx: IFonderieContext): string | null =>\n\tnew URL(ctx.request.url).searchParams.get('locale');\nconst body = (ctx: IFonderieContext): Record<string, unknown> =>\n\t(ctx.meta['body'] as Record<string, unknown> | undefined) ?? {};\n\nfunction conflictOr(err: unknown): Response {\n\tif (err instanceof VersionConflictError) {\n\t\treturn setApiResponse(HTTP.CONFLICT, 'VERSION_CONFLICT', err.message, {\n\t\t\tcurrentVersion: err.currentVersion,\n\t\t});\n\t}\n\tthrow err;\n}\n\n// Build the template admin route table (registered by CourierModule.install when\n// an adminToken is configured).\nexport function buildTemplateAdminRoutes(\n\tstore: IStoreAdapter,\n\tadminToken: string,\n): Array<[string, string, Middleware]> {\n\tconst g = (h: Middleware): Middleware => guarded(adminToken, h);\n\n\treturn [\n\t\t['GET', '/admin/templates', g(async () => {\n\t\t\treturn setApiResponse(HTTP.OK, 'TEMPLATES_LISTED', 'Templates', await listTemplateEntries(store));\n\t\t})],\n\t\t['GET', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst row = await getTemplateEntry(typeOf(ctx), localeOf(ctx), store);\n\t\t\treturn row\n\t\t\t\t? setApiResponse(HTTP.OK, 'TEMPLATE', 'Template', row)\n\t\t\t\t: setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such template');\n\t\t})],\n\t\t['PUT', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tif (typeof b['text'] !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.text (string) is required');\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst opts: Parameters<typeof setTemplate>[0] = {\n\t\t\t\t\ttype: typeOf(ctx),\n\t\t\t\t\ttext: b['text'],\n\t\t\t\t\tlocale: localeOf(ctx),\n\t\t\t\t\tactor: actorOf(ctx),\n\t\t\t\t};\n\t\t\t\tif (typeof b['subject'] === 'string') opts.subject = b['subject'];\n\t\t\t\tif (typeof b['html'] === 'string') opts.html = b['html'];\n\t\t\t\tif (typeof b['active'] === 'boolean') opts.active = b['active'];\n\t\t\t\tif (typeof b['ifVersion'] === 'number') opts.ifVersion = b['ifVersion'];\n\t\t\t\treturn setApiResponse(HTTP.OK, 'TEMPLATE_SET', 'Template saved', await setTemplate(opts, store));\n\t\t\t} catch (err) {\n\t\t\t\treturn conflictOr(err);\n\t\t\t}\n\t\t})],\n\t\t['DELETE', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst ok = await deleteTemplate(typeOf(ctx), localeOf(ctx), store);\n\t\t\treturn setApiResponse(ok ? HTTP.OK : HTTP.NOT_FOUND, ok ? 'DELETED' : 'NOT_FOUND', ok ? 'Deleted' : 'No such template');\n\t\t})],\n\t\t['GET', '/admin/templates/:type/revisions', g(async (ctx) => {\n\t\t\treturn setApiResponse(HTTP.OK, 'REVISIONS', 'Template revisions', await listTemplateRevisions(typeOf(ctx), localeOf(ctx), store));\n\t\t})],\n\t\t['POST', '/admin/templates/:type/rollback', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tconst toVersion = Number(b['toVersion']);\n\t\t\tif (!Number.isInteger(toVersion)) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.toVersion (int) is required');\n\t\t\t}\n\t\t\tconst row = await rollbackTemplate(\n\t\t\t\t{ type: typeOf(ctx), locale: localeOf(ctx), toVersion, actor: actorOf(ctx) },\n\t\t\t\tstore,\n\t\t\t);\n\t\t\treturn setApiResponse(HTTP.OK, 'ROLLED_BACK', `Rolled back to v${toVersion}`, row);\n\t\t})],\n\t];\n}\n","import type { IStoreAdapter, IVersionedResource } from '@fonderie/store';\nimport { versionedWrite, versionedRollback } from '@fonderie/store';\n\n// Versioned management for email templates, on the shared `@fonderie/store`\n// primitive. Templates are keyed by (type, locale) — a NULL locale is the base,\n// with per-locale overrides on top — and their content is subject/html/text.\n// The resolver reads the current row unchanged; this adds edit + history +\n// rollback + optimistic concurrency on top.\n\nexport interface ITemplateEntry {\n\ttype: string;\n\tlocale: string | null;\n\tsubject: string | null;\n\thtml: string | null;\n\ttext: string;\n\tactive: boolean;\n\tversion: number;\n\tupdatedBy: string | null;\n\tupdatedAt: string;\n}\n\nexport interface ITemplateRevision {\n\ttype: string;\n\tlocale: string | null;\n\tsubject: string | null;\n\thtml: string | null;\n\ttext: string;\n\tversion: number;\n\tactor: string | null;\n\tcreatedAt: string;\n}\n\nconst ENTRY_COLS = `type, locale, subject, html, text, active, version, updated_by AS \"updatedBy\", updated_at AS \"updatedAt\"`;\n\nconst TEMPLATE_RESOURCE: IVersionedResource = {\n\ttable: 'fonderie_courier_templates',\n\trevisions: 'fonderie_courier_template_revisions',\n\tchannel: 'fonderie_courier_templates_changed',\n\tkeyColumns: ['type', 'locale'],\n\tcontentColumns: ['subject', 'html', 'text'],\n\tmetaColumns: ['active'],\n\treturning: ENTRY_COLS,\n};\n\n// Upsert a template (full body — a revision is a full snapshot). Pass `ifVersion`\n// for optimistic concurrency (else `VersionConflictError`). `locale` omitted /\n// null targets the base template.\nexport async function setTemplate(\n\topts: {\n\t\ttype: string;\n\t\ttext: string;\n\t\tlocale?: string | null;\n\t\tsubject?: string | null;\n\t\thtml?: string | null;\n\t\tactive?: boolean;\n\t\tifVersion?: number;\n\t\tactor?: string;\n\t},\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry> {\n\tconst data: Record<string, unknown> = {\n\t\tsubject: opts.subject ?? null,\n\t\thtml: opts.html ?? null,\n\t\ttext: opts.text,\n\t};\n\tif (opts.active !== undefined) data['active'] = opts.active;\n\treturn versionedWrite<ITemplateEntry>(TEMPLATE_RESOURCE, store, {\n\t\tkey: opts.type,\n\t\tscope: opts.locale ?? null,\n\t\tdata,\n\t\t...(opts.ifVersion !== undefined ? { ifVersion: opts.ifVersion } : {}),\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function rollbackTemplate(\n\topts: { type: string; locale?: string | null; toVersion: number; actor?: string },\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry> {\n\treturn versionedRollback<ITemplateEntry>(TEMPLATE_RESOURCE, store, {\n\t\tkey: opts.type,\n\t\tscope: opts.locale ?? null,\n\t\ttoVersion: opts.toVersion,\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function listTemplateRevisions(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<ITemplateRevision[]> {\n\treturn store.query<ITemplateRevision>(\n\t\t`SELECT type, locale, subject, html, text, version, actor, created_at AS \"createdAt\"\n\t\t FROM fonderie_courier_template_revisions\n\t\t WHERE type = $1 AND locale IS NOT DISTINCT FROM $2\n\t\t ORDER BY version DESC`,\n\t\t[type, locale],\n\t);\n}\n\nexport async function getTemplateEntry(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry | null> {\n\tconst [row] = await store.query<ITemplateEntry>(\n\t\t`SELECT ${ENTRY_COLS} FROM fonderie_courier_templates WHERE type = $1 AND locale IS NOT DISTINCT FROM $2`,\n\t\t[type, locale],\n\t);\n\treturn row ?? null;\n}\n\nexport async function listTemplateEntries(store: IStoreAdapter): Promise<ITemplateEntry[]> {\n\treturn store.query<ITemplateEntry>(\n\t\t`SELECT ${ENTRY_COLS} FROM fonderie_courier_templates ORDER BY type, locale NULLS FIRST`,\n\t);\n}\n\nexport async function deleteTemplate(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst rows = await store.query<{ type: string }>(\n\t\t`DELETE FROM fonderie_courier_templates WHERE type = $1 AND locale IS NOT DISTINCT FROM $2 RETURNING type`,\n\t\t[type, locale],\n\t);\n\treturn rows.length > 0;\n}\n","export interface IEmailChannelConfig {\n\tprovider: 'resend' | 'ses' | 'smtp';\n\tfrom: string;\n\tapiKey?: string;\n\tsmtp?: {\n\t\thost: string;\n\t\tport: number;\n\t\tsecure: boolean;\n\t\tuser: string;\n\t\tpass: string;\n\t};\n}\n\nexport interface ISmsChannelConfig {\n\tprovider: 'twilio' | 'vonage';\n\tfrom: string;\n\taccountSid?: string; // twilio\n\tauthToken?: string; // twilio\n\tapiKey?: string; // vonage\n\tapiSecret?: string; // vonage\n}\n\nexport interface IPushChannelConfig {\n\tprovider: 'fcm';\n\tserviceAccount: Record<string, unknown>;\n}\n\nexport const Channel = {\n\tEMAIL: 'email',\n\tSMS: 'sms',\n\tPUSH: 'push',\n} as const satisfies Record<string, 'email' | 'sms' | 'push'>;\n\nexport interface ICourierConfig {\n\t// Which channels handle which message types\n\t// e.g. { 'password-reset': ['email'], 'new-message': ['push', 'sms'] }\n\tchannels: Record<string, Array<'email' | 'sms' | 'push'>>;\n\n\tsms?: ISmsChannelConfig;\n\tpush?: IPushChannelConfig;\n\temail?: IEmailChannelConfig;\n\n\t// When set, exposes Bearer-guarded template admin routes (/admin/templates/*)\n\t// for versioned edit/history/rollback. Requires @fonderie/store (db templates).\n\tadminToken?: string;\n\n\t// Where templates are loaded from\n\t// 'db' requires @fonderie/store to be configured\n\t// 'fs' reads from a local directory\n\ttemplates?: {\n\t\tsource: 'db' | 'fs';\n\t\tdirectory?: string; // for 'fs' source\n\t};\n\n\t// Signing keys for verifying inbound delivery webhook payloads\n\tdelivery?: {\n\t\tsigningKeys?: {\n\t\t\tsendgrid?: string; // HMAC-SHA256 key from SendGrid dashboard\n\t\t\tmailgun?: string; // Mailgun HTTP webhook signing key\n\t\t};\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACGA,oBAAmC;;;ACsBnC,eAAsB,iBACrB,OAQA,OACkB;AAClB,QAAM,CAAC,GAAG,IAAI,MAAM,MAAM;AAAA,IACzB;AAAA;AAAA;AAAA;AAAA,IAIA;AAAA,MACC,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM,UAAU;AAAA,MAChB,MAAM,YAAY;AAAA,MAClB,MAAM,qBAAqB;AAAA,IAC5B;AAAA,EACD;AACA,SAAO,KAAK,MAAM;AACnB;AAEA,eAAsB,gBAAgB,IAAY,OAAqC;AACtF,QAAM,MAAM;AAAA,IACX;AAAA;AAAA;AAAA,IAGA,CAAC,EAAE;AAAA,EACJ;AACD;AAEA,eAAsB,kBACrB,IACA,OACA,OACgB;AAChB,QAAM,MAAM;AAAA,IACX;AAAA;AAAA;AAAA,IAGA,CAAC,IAAI,KAAK;AAAA,EACX;AACD;AAEA,eAAsB,qBACrB,mBACA,OACgB;AAChB,QAAM,MAAM;AAAA,IACX;AAAA;AAAA;AAAA,IAGA,CAAC,iBAAiB;AAAA,EACnB;AACD;AAEA,eAAsB,kBACrB,mBACA,OACgB;AAChB,QAAM,MAAM;AAAA,IACX;AAAA;AAAA;AAAA,IAGA,CAAC,iBAAiB;AAAA,EACnB;AACD;AAEA,eAAsB,mBACrB,mBACA,OACgB;AAChB,QAAM,MAAM;AAAA,IACX;AAAA;AAAA;AAAA,IAGA,CAAC,iBAAiB;AAAA,EACnB;AACD;AAEA,eAAsB,mBACrB,mBACA,QACA,OACgB;AAChB,QAAM,MAAM;AAAA,IACX;AAAA;AAAA;AAAA,IAGA,CAAC,mBAAmB,MAAM;AAAA,EAC3B;AACD;;;ACpHA,SAAS,iBAAiB,SAA0B,SAAyB;AAC5E,MAAI,YAAY,QAAS,QAAO,QAAQ,UAAU,SAAS;AAC3D,MAAI,YAAY,MAAO,QAAO,QAAQ,UAAU,SAAS;AACzD,MAAI,YAAY,OAAQ,QAAO,QAAQ,UAAU,eAAe;AAChE,SAAO,QAAQ,UAAU,SAAS,QAAQ,UAAU,SAAS;AAC9D;AAEO,IAAM,aAAN,MAAiB;AAAA,EAGvB,YACS,QACA,UACA,OACP;AAHO;AACA;AACA;AAAA,EACN;AAAA,EAHM;AAAA,EACA;AAAA,EACA;AAAA,EALD,WAAyC,oBAAI,IAAI;AAAA,EAQzD,gBAAgB,SAAgC;AAC/C,SAAK,SAAS,IAAI,QAAQ,MAAM,OAAO;AACvC,WAAO;AAAA,EACR;AAAA;AAAA;AAAA,EAIA,eAAyB;AACxB,WAAO,CAAC,GAAG,KAAK,SAAS,KAAK,CAAC;AAAA,EAChC;AAAA,EAEA,MAAM,SAAS,SAAyC;AACvD,UAAM,eAAe,KAAK,OAAO,SAAS,QAAQ,IAAI;AAEtD,QAAI,CAAC,gBAAgB,aAAa,WAAW,GAAG;AAC/C,cAAQ,KAAK,sDAAsD,QAAQ,IAAI,EAAE;AACjF;AAAA,IACD;AAEA,UAAM,WAAW,MAAM,KAAK,SAAS,QAAQ,QAAQ,MAAM,QAAQ,MAAM,QAAQ,MAAM;AAEvF,UAAM,QAAQ;AAAA,MACb,aAAa,IAAI,OAAO,SAAS;AAChC,cAAM,UAAU,KAAK,SAAS,IAAI,IAAI;AACtC,YAAI,CAAC,SAAS;AACb,kBAAQ,KAAK,sBAAsB,IAAI,kBAAkB;AACzD;AAAA,QACD;AAGA,cAAM,WAAmD;AAAA,UACxD,aAAa,QAAQ;AAAA,UACrB,SAAS;AAAA,UACT,WAAW,iBAAiB,SAAS,IAAI;AAAA,QAC1C;AACA,YAAI,QAAQ,OAAQ,UAAS,SAAS,QAAQ;AAE9C,cAAM,QAAQ,KAAK,QAChB,MAAM,iBAAiB,UAAU,KAAK,KAAK,EAAE,MAAM,MAAM,EAAE,IAC3D;AAEH,YAAI;AACH,gBAAM,QAAQ,KAAK,SAAS,QAAQ;AAEpC,cAAI,KAAK,SAAS,OAAO;AACxB,4BAAgB,OAAO,KAAK,KAAK,EAAE,MAAM,MAAM,MAAS;AAAA,UACzD;AAAA,QACD,SAAS,KAAK;AACb,gBAAM,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC9D,kBAAQ,MAAM,YAAY,IAAI,oBAAoB,QAAQ,IAAI,KAAK,GAAG;AAEtE,cAAI,KAAK,SAAS,OAAO;AACxB,8BAAkB,OAAO,QAAQ,KAAK,KAAK,EAAE,MAAM,MAAM,MAAS;AAAA,UACnE;AAAA,QACD;AAAA,MACD,CAAC;AAAA,IACF;AAAA,EACD;AACD;;;AC7EO,IAAM,aAAN,MAA4C;AAAA,EAGlD,YAAoB,QAA2B;AAA3B;AAAA,EAA4B;AAAA,EAA5B;AAAA,EAFX,OAAO;AAAA,EAIhB,MAAM,KAAK,SAA0B,UAA4C;AAChF,UAAM,KAAK,QAAQ,UAAU;AAC7B,QAAI,CAAC,IAAI;AACR,cAAQ,KAAK,6DAAwD;AACrE;AAAA,IACD;AAEA,QAAI,KAAK,OAAO,aAAa,UAAU;AACtC,YAAM,KAAK,cAAc,IAAI,SAAS,IAAI;AAAA,IAC3C,WAAW,KAAK,OAAO,aAAa,UAAU;AAC7C,YAAM,KAAK,cAAc,IAAI,SAAS,IAAI;AAAA,IAC3C,OAAO;AACN,cAAQ,KAAK,0BAA0B,KAAK,OAAO,QAAQ,kBAAkB;AAAA,IAC9E;AAAA,EACD;AAAA,EAEA,MAAc,cAAc,IAAY,MAA6B;AACpE,UAAM,EAAE,YAAY,UAAU,IAAI,KAAK;AACvC,QAAI,CAAC,cAAc,CAAC,WAAW;AAC9B,YAAM,IAAI,MAAM,0CAA0C;AAAA,IAC3D;AAEA,UAAM,MAAM,MAAM;AAAA,MACjB,8CAA8C,UAAU;AAAA,MACxD;AAAA,QACC,QAAQ;AAAA,QACR,SAAS;AAAA,UACR,eAAe,SAAS,OAAO,KAAK,GAAG,UAAU,IAAI,SAAS,EAAE,EAAE,SAAS,QAAQ,CAAC;AAAA,UACpF,gBAAgB;AAAA,QACjB;AAAA,QACA,MAAM,IAAI,gBAAgB,EAAE,MAAM,KAAK,OAAO,MAAM,IAAI,IAAI,MAAM,KAAK,CAAC;AAAA,MACzE;AAAA,IACD;AAEA,QAAI,CAAC,IAAI,IAAI;AACZ,YAAMA,QAAO,MAAM,IAAI,KAAK;AAC5B,YAAM,IAAI,MAAM,8BAA8B,IAAI,MAAM,KAAKA,KAAI,EAAE;AAAA,IACpE;AAAA,EACD;AAAA,EAEA,MAAc,cAAc,IAAY,MAA6B;AACpE,UAAM,EAAE,QAAQ,UAAU,IAAI,KAAK;AACnC,QAAI,CAAC,UAAU,CAAC,WAAW;AAC1B,YAAM,IAAI,MAAM,sCAAsC;AAAA,IACvD;AAEA,UAAM,MAAM,MAAM,MAAM,mCAAmC;AAAA,MAC1D,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU;AAAA,QACpB,SAAS;AAAA,QACT,YAAY;AAAA,QACZ,MAAM,KAAK,OAAO;AAAA,QAClB;AAAA,QACA;AAAA,MACD,CAAC;AAAA,IACF,CAAC;AAED,QAAI,CAAC,IAAI,IAAI;AACZ,YAAMA,QAAO,MAAM,IAAI,KAAK;AAC5B,YAAM,IAAI,MAAM,8BAA8B,IAAI,MAAM,KAAKA,KAAI,EAAE;AAAA,IACpE;AAAA,EACD;AACD;;;ACpEO,IAAM,cAAN,MAA6C;AAAA,EAGnD,YAAoB,QAA4B;AAA5B;AAAA,EAA6B;AAAA,EAA7B;AAAA,EAFX,OAAO;AAAA,EAIhB,MAAM,KAAK,SAA0B,UAA4C;AAChF,UAAM,QAAQ,QAAQ,UAAU;AAChC,QAAI,CAAC,OAAO;AACX,cAAQ,KAAK,8DAAyD;AACtE;AAAA,IACD;AAEA,QAAI,KAAK,OAAO,aAAa,OAAO;AACnC,YAAM,KAAK,WAAW,OAAO,QAAQ;AAAA,IACtC;AAAA,EACD;AAAA,EAEA,MAAc,WAAW,OAAe,UAA4C;AACnF,UAAM,SAAS,KAAK,OAAO,eAAe,QAAQ;AAClD,QAAI,CAAC,QAAQ;AACZ,YAAM,IAAI,MAAM,yDAAyD;AAAA,IAC1E;AAEA,UAAM,MAAM,MAAM,MAAM,uCAAuC;AAAA,MAC9D,QAAQ;AAAA,MACR,SAAS;AAAA,QACR,eAAe,OAAO,MAAM;AAAA,QAC5B,gBAAgB;AAAA,MACjB;AAAA,MACA,MAAM,KAAK,UAAU;AAAA,QACpB,IAAI;AAAA,QACJ,cAAc;AAAA,UACb,OAAO,SAAS;AAAA,UAChB,MAAM,SAAS;AAAA,QAChB;AAAA,MACD,CAAC;AAAA,IACF,CAAC;AAED,QAAI,CAAC,IAAI,IAAI;AACZ,YAAMC,QAAO,MAAM,IAAI,KAAK;AAC5B,YAAM,IAAI,MAAM,4BAA4B,IAAI,MAAM,KAAKA,KAAI,EAAE;AAAA,IAClE;AAAA,EACD;AACD;;;AC9CA,wBAAuB;AAKhB,IAAM,eAAN,MAA8C;AAAA,EAIpD,YAAoB,QAA6B;AAA7B;AACnB,QAAI,OAAO,aAAa,UAAU,OAAO,MAAM;AAC9C,WAAK,YAAY,kBAAAC,QAAW,gBAAgB;AAAA,QAC3C,MAAM,OAAO,KAAK;AAAA,QAClB,MAAM,OAAO,KAAK;AAAA,QAClB,QAAQ,OAAO,KAAK;AAAA,QACpB,MAAM,EAAE,MAAM,OAAO,KAAK,MAAM,MAAM,OAAO,KAAK,KAAK;AAAA,MACxD,CAAC;AAAA,IACF;AAAA,EACD;AAAA,EAToB;AAAA,EAHX,OAAO;AAAA,EACR,YAAkE;AAAA,EAa1E,MAAM,KAAK,SAA0B,UAA4C;AAChF,UAAM,KAAK,QAAQ,UAAU;AAC7B,QAAI,CAAC,IAAI;AACR,cAAQ,KAAK,gEAA2D;AACxE;AAAA,IACD;AAEA,QAAI,KAAK,OAAO,aAAa,UAAU;AACtC,YAAM,KAAK,cAAc,IAAI,QAAQ;AAAA,IACtC,WAAW,KAAK,OAAO,aAAa,QAAQ;AAC3C,YAAM,KAAK,YAAY,IAAI,QAAQ;AAAA,IACpC,OAAO;AACN,cAAQ,KAAK,4BAA4B,KAAK,OAAO,QAAQ,kBAAkB;AAAA,IAChF;AAAA,EACD;AAAA,EAEA,MAAc,cAAc,IAAY,UAA4C;AACnF,QAAI,CAAC,KAAK,OAAO,QAAQ;AACxB,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC5C;AAEA,UAAM,MAAM,MAAM,MAAM,iCAAiC;AAAA,MACxD,QAAQ;AAAA,MACR,SAAS;AAAA,QACR,eAAe,UAAU,KAAK,OAAO,MAAM;AAAA,QAC3C,gBAAgB;AAAA,MACjB;AAAA,MACA,MAAM,KAAK,UAAU;AAAA,QACpB,MAAM,KAAK,OAAO;AAAA,QAClB;AAAA,QACA,SAAS,SAAS,WAAW;AAAA,QAC7B,MAAM,SAAS;AAAA,QACf,MAAM,SAAS;AAAA,MAChB,CAAC;AAAA,IACF,CAAC;AAED,QAAI,CAAC,IAAI,IAAI;AACZ,YAAMC,QAAO,MAAM,IAAI,KAAK;AAC5B,YAAM,IAAI,MAAM,gCAAgC,IAAI,MAAM,KAAKA,KAAI,EAAE;AAAA,IACtE;AAAA,EACD;AAAA,EAEA,MAAc,YAAY,IAAY,UAA4C;AACjF,QAAI,CAAC,KAAK,WAAW;AACpB,YAAM,IAAI,MAAM,yDAAoD;AAAA,IACrE;AAEA,UAAM,KAAK,UAAU,SAAS;AAAA,MAC7B,MAAM,KAAK,OAAO;AAAA,MAClB;AAAA,MACA,SAAS,SAAS,WAAW;AAAA,MAC7B,MAAM,SAAS;AAAA,MACf,MAAM,SAAS;AAAA,IAChB,CAAC;AAAA,EACF;AACD;;;AL9DA,IAAAC,sBAAmC;;;AMK5B,IAAM,cAAc;AAAA,EAC1B,OAAO;AAAA,EACP,QAAQ;AAAA;AAAA,EACR,YAAY;AAAA;AAAA,EACZ,aAAa;AAAA;AAAA,EACb,MAAM;AAAA;AAAA,EACN,KAAK;AAAA;AAAA,EACL,OAAO;AAAA;AAAA,EACP,MAAM;AAAA;AAAA,EACN,QAAQ;AAAA;AAAA,EACR,MAAM;AAAA;AACP;AAIA,IAAM,YACL;AACD,IAAM,YAAY;AAIX,IAAM,sBAAsB;AAE5B,IAAM,uBAAuB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sEAWkC,YAAY,MAAM;AAAA,8BAC1D,YAAY,MAAM;AAAA;AAAA;AAAA,iBAG/B,YAAY,IAAI;AAAA,uBACV,YAAY,IAAI;AAAA;AAAA;AAAA;AAAA,6CAIM,YAAY,WAAW;AAAA;AAAA,oCAEhC,SAAS,YAAY,YAAY,GAAG;AAAA;AAAA;AAAA,wBAGhD,SAAS;AAAA,YACrB,YAAY,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA,8BAKG,YAAY,IAAI;AAAA;AAAA;AAAA,wBAGtB,SAAS;AAAA,YACrB,YAAY,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA,iBAKZ,YAAY,MAAM;AAAA,YACvB,YAAY,UAAU;AAAA;AAAA,sBAEZ,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAOT,SAAS;AAAA;AAAA,YAEnB,YAAY,GAAG;AAAA,iBACV,YAAY,MAAM;AAAA,uBACZ,YAAY,IAAI;AAAA;AAAA;AAAA;AAAA,oBAInB,YAAY,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,2HASsF,YAAY,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,iDAK5F,YAAY,IAAI,qBAAqB,YAAY,IAAI;AAAA;AAAA,oEAElC,YAAY,WAAW;AAAA;AAAA,iFAEV,YAAY,GAAG,6BAA6B,YAAY,KAAK;AAAA;AAAA,gFAE9D,YAAY,GAAG;AAAA,EAC7F,mBAAmB;AAAA;AAAA,gFAE2D,YAAY,KAAK;AAAA,sEAC3B,YAAY,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAehF,SAAS,WAAW,UAAkB,aAAqB,sBAA8B;AAC/F,QAAM,UAAU,SAAS,UAAU,EAAE,YAAY;AACjD,MAAI,QAAQ,WAAW,WAAW,KAAK,QAAQ,WAAW,OAAO,GAAG;AACnE,WAAO;AAAA,EACR;AACA,SAAO,WAAW,QAAQ,qBAAqB,QAAQ;AACxD;;;ACzIA,IAAM,cAAc;AAEpB,SAAS,OAAO,UAAkB,MAAuC;AACxE,SAAO,SAAS,QAAQ,kBAAkB,CAAC,GAAG,QAAgB;AAC7D,UAAM,QAAQ,KAAK,GAAG;AACtB,WAAO,UAAU,UAAa,UAAU,OAAO,OAAO,KAAK,IAAI;AAAA,EAChE,CAAC;AACF;AAKA,SAAS,YACR,UACA,YACA,SACA,MACS;AACT,QAAM,UAAU,WAAW,UAAU,UAAU;AAC/C,SAAO,OAAO,SAAS,EAAE,SAAS,WAAW,IAAI,WAAW,IAAI,GAAG,KAAK,CAAC;AAC1E;AAGO,IAAM,qBAAN,MAAsD;AAAA,EAC5D,YAAoB,OAAsB;AAAtB;AAAA,EAAuB;AAAA,EAAvB;AAAA,EAEpB,MAAM,QACL,MACA,MACA,QAC6B;AAC7B,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA;AAAA;AAAA;AAAA,MAQ9B;AAAA;AAAA;AAAA;AAAA;AAAA,MAKA,CAAC,MAAM,UAAU,IAAI;AAAA,IACtB;AAEA,QAAI,CAAC,KAAK;AACT,aAAO,EAAE,MAAM,GAAG,IAAI,KAAK,KAAK,UAAU,IAAI,CAAC,GAAG;AAAA,IACnD;AAEA,UAAM,UAAU,IAAI,UAAU,OAAO,IAAI,SAAS,IAAI,IAAI;AAC1D,UAAM,aAAa,IAAI,OAAO,MAAM,KAAK,OAAO,MAAM,IAAI;AAE1D,WAAO;AAAA,MACN,MAAM,OAAO,IAAI,MAAM,IAAI;AAAA,MAC3B,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,MAC7B,GAAI,IAAI,OAAO,EAAE,MAAM,YAAY,IAAI,MAAM,YAAY,SAAS,IAAI,EAAE,IAAI,CAAC;AAAA,IAC9E;AAAA,EACD;AAAA;AAAA,EAGA,MAAc,OAAO,QAA8C;AAClE,UAAM,CAAC,GAAG,IAAI,MAAM,KAAK,MAAM;AAAA,MAC9B;AAAA;AAAA;AAAA;AAAA;AAAA,MAKA,CAAC,aAAa,UAAU,IAAI;AAAA,IAC7B;AACA,WAAO,KAAK,QAAQ;AAAA,EACrB;AACD;AAGO,IAAM,qBAAN,MAAsD;AAAA,EAC5D,YAAoB,WAAmB;AAAnB;AAAA,EAAoB;AAAA,EAApB;AAAA,EAEpB,MAAM,QACL,MACA,MACA,QAC6B;AAC7B,UAAM,EAAE,SAAS,IAAI,MAAM,OAAO,aAAkB;AACpD,UAAM,EAAE,KAAK,IAAI,MAAM,OAAO,MAAW;AAEzC,UAAM,eAAe,OAAO,SAAyC;AACpE,UAAI;AACH,eAAO,MAAM,SAAS,MAAM,MAAM;AAAA,MACnC,QAAQ;AACP,eAAO;AAAA,MACR;AAAA,IACD;AAGA,UAAM,eAAe,SAAS,GAAG,IAAI,IAAI,MAAM,KAAK;AACpD,UAAM,eAAe,SAAS,GAAG,WAAW,IAAI,MAAM,KAAK;AAE3D,UAAM,CAAC,MAAM,MAAM,SAAS,MAAM,IAAI,MAAM,QAAQ,IAAI;AAAA,MACvD,eACG,aAAa,KAAK,KAAK,WAAW,GAAG,YAAY,MAAM,CAAC,EAAE;AAAA,QAC1D,CAAC,MAAM,KAAK,aAAa,KAAK,KAAK,WAAW,GAAG,IAAI,MAAM,CAAC;AAAA,MAC7D,IACC,aAAa,KAAK,KAAK,WAAW,GAAG,IAAI,MAAM,CAAC;AAAA,MAEnD,eACG,aAAa,KAAK,KAAK,WAAW,GAAG,YAAY,OAAO,CAAC,EAAE;AAAA,QAC3D,CAAC,MAAM,KAAK,aAAa,KAAK,KAAK,WAAW,GAAG,IAAI,OAAO,CAAC;AAAA,MAC9D,IACC,aAAa,KAAK,KAAK,WAAW,GAAG,IAAI,OAAO,CAAC;AAAA,MAEpD,eACG,aAAa,KAAK,KAAK,WAAW,GAAG,YAAY,cAAc,CAAC,EAAE;AAAA,QAClE,CAAC,MAAM,KAAK,aAAa,KAAK,KAAK,WAAW,GAAG,IAAI,cAAc,CAAC;AAAA,MACrE,IACC,aAAa,KAAK,KAAK,WAAW,GAAG,IAAI,cAAc,CAAC;AAAA,MAE3D,eACG,aAAa,KAAK,KAAK,WAAW,GAAG,YAAY,OAAO,CAAC,EAAE;AAAA,QAC3D,CAAC,MAAM,KAAK,aAAa,KAAK,KAAK,WAAW,GAAG,WAAW,OAAO,CAAC;AAAA,MACrE,IACC,aAAa,KAAK,KAAK,WAAW,GAAG,WAAW,OAAO,CAAC;AAAA,IAC5D,CAAC;AAED,UAAM,kBAAkB,UAAU,OAAO,SAAS,IAAI,IAAI;AAE1D,WAAO;AAAA,MACN,MAAM,OAAO,OAAO,MAAM,IAAI,IAAI,GAAG,IAAI,KAAK,KAAK,UAAU,IAAI,CAAC;AAAA,MAClE,GAAI,kBAAkB,EAAE,SAAS,gBAAgB,IAAI,CAAC;AAAA,MACtD,GAAI,OAAO,EAAE,MAAM,YAAY,MAAM,UAAU,QAAW,iBAAiB,IAAI,EAAE,IAAI,CAAC;AAAA,IACvF;AAAA,EACD;AACD;;;ACzIA,IAAM,SAAS;AAUR,SAAS,6BACf,QACA,oBACsB;AACtB,QAAM,aAAa,IAAI,IAAI,kBAAkB;AAC7C,QAAM,OAAO,oBAAI,IAAsB;AAEvC,aAAW,CAAC,MAAM,QAAQ,KAAK,OAAO,QAAQ,OAAO,YAAY,CAAC,CAAC,GAAG;AACrE,eAAW,WAAW,UAAU;AAC/B,UAAI,CAAC,WAAW,IAAI,OAAO,GAAG;AAC7B,cAAM,QAAQ,KAAK,IAAI,OAAO,KAAK,CAAC;AACpC,cAAM,KAAK,IAAI;AACf,aAAK,IAAI,SAAS,KAAK;AAAA,MACxB;AAAA,IACD;AAAA,EACD;AAEA,SAAO,CAAC,GAAG,IAAI,EAAE,IAAI,CAAC,CAAC,SAAS,KAAK,OAAO;AAAA,IAC3C,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,SACC,GAAG,MAAM,MAAM,kCAAkC,OAAO,qBACjD,OAAO,mEACX,MAAM,KAAK,IAAI,CAAC,wBAAwB,OAAO;AAAA,EACpD,EAAE;AACH;AAGO,SAAS,sBACf,QACA,oBACO;AACP,aAAW,WAAW,6BAA6B,QAAQ,kBAAkB,GAAG;AAC/E,YAAQ,KAAK,aAAa,QAAQ,OAAO,EAAE;AAAA,EAC5C;AACD;;;AChDA,yBAA2B;AAE3B,kBAAkC;AAgBlC,eAAsB,uBACrB,KACA,OACA,eACoB;AACpB,MAAI,eAAe;AAClB,UAAM,MAAM,IAAI,QAAQ,IAAI,wCAAwC,KAAK;AACzE,UAAM,KAAM,IAAI,QAAQ,IAAI,wCAAwC,KAAK;AACzE,UAAMC,QAAO,MAAM,IAAI,KAAK;AAE5B,QAAI,CAAC,wBAAwB,eAAe,IAAIA,OAAM,GAAG,GAAG;AAC3D,aAAO,SAAS,KAAK,EAAE,OAAO,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrE;AAEA,UAAMC,UAAS,UAAUD,KAAI;AAC7B,QAAI,CAAC,MAAM,QAAQC,OAAM,EAAG,QAAO,SAAS,KAAK,EAAE,IAAI,KAAK,CAAC;AAE7D,UAAM,sBAAsBA,SAAQ,KAAK;AACzC,WAAO,SAAS,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EAClC;AAEA,QAAM,SAAU,MAAM,IAAI,KAAK;AAC/B,MAAI,CAAC,MAAM,QAAQ,MAAM,EAAG,QAAO,SAAS,KAAK,EAAE,IAAI,KAAK,CAAC;AAE7D,QAAM,sBAAsB,QAAQ,KAAK;AACzC,SAAO,SAAS,KAAK,EAAE,IAAI,KAAK,CAAC;AAClC;AAEA,SAAS,wBACR,QACA,WACAD,OACA,WACU;AACV,MAAI;AACH,UAAM,UAAU,YAAYA;AAC5B,UAAM,eAAW,+BAAW,UAAU,MAAM,EAAE,OAAO,OAAO,EAAE,OAAO,QAAQ;AAC7E,eAAO,+BAAkB,OAAO,KAAK,WAAW,QAAQ,GAAG,OAAO,KAAK,UAAU,QAAQ,CAAC;AAAA,EAC3F,QAAQ;AACP,WAAO;AAAA,EACR;AACD;AASA,eAAe,sBACd,QACA,OACgB;AAChB,aAAW,MAAM,QAAQ;AACxB,UAAM,QAAQ,GAAG,eAAe;AAChC,QAAI,CAAC,SAAS,OAAO,UAAU,SAAU;AAEzC,UAAM,KAAK,MAAM,MAAM,GAAG,EAAE,CAAC,KAAK;AAElC,YAAQ,GAAG,OAAO;AAAA,MACjB,KAAK;AAAa,cAAM,qBAAqB,IAAI,KAAK;AAAG;AAAA,MACzD,KAAK;AAAa,cAAM,kBAAkB,IAAI,KAAK;AAAG;AAAA,MACtD,KAAK;AAAa,cAAM,mBAAmB,IAAI,KAAK;AAAG;AAAA,MACvD,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACJ,cAAM,mBAAmB,IAAI,OAAO,GAAG,QAAQ,MAAM,WAAW,GAAG,QAAQ,IAAI,GAAG,OAAO,KAAK;AAC9F;AAAA,IACF;AAAA,EACD;AACD;AAOA,eAAsB,sBACrB,KACA,OACA,YACoB;AACpB,QAAMA,QAAQ,MAAM,IAAI,KAAK;AAE7B,MAAI,YAAY;AACf,UAAM,EAAE,UAAU,IAAIA;AACtB,QAAI,CAAC,aAAa,CAAC,uBAAuB,YAAY,UAAU,WAAW,UAAU,OAAO,UAAU,SAAS,GAAG;AACjH,aAAO,SAAS,KAAK,EAAE,OAAO,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrE;AAAA,EACD;AAEA,QAAM,QAAQA,MAAK,YAAY;AAC/B,MAAI,OAAO;AACV,UAAM,oBAAoB,OAAO,KAAK;AAAA,EACvC;AAEA,SAAO,SAAS,KAAK,EAAE,IAAI,KAAK,CAAC;AAClC;AAEA,SAAS,uBACR,YACA,WACA,OACA,WACU;AACV,MAAI;AACH,UAAM,QAAQ,YAAY;AAC1B,UAAM,eAAW,+BAAW,UAAU,UAAU,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AAC5E,eAAO,+BAAkB,OAAO,KAAK,UAAU,KAAK,GAAG,OAAO,KAAK,WAAW,KAAK,CAAC;AAAA,EACrF,QAAQ;AACP,WAAO;AAAA,EACR;AACD;AAeA,eAAe,oBAAoB,OAAqB,OAAqC;AAC5F,QAAM,QAAQ,MAAM,SAAS,UAAU,YAAY;AACnD,MAAI,CAAC,MAAO;AAEZ,UAAQ,MAAM,OAAO;AAAA,IACpB,KAAK;AAAa,YAAM,qBAAqB,OAAO,KAAK;AAAG;AAAA,IAC5D,KAAK;AAAa,YAAM,kBAAkB,OAAO,KAAK;AAAG;AAAA,IACzD,KAAK;AAAa,YAAM,mBAAmB,OAAO,KAAK;AAAG;AAAA,IAC1D,KAAK;AAAA,IACL,KAAK,WAAW;AACf,YAAM,SAAS,MAAM,iBAAiB,GAAG,WAAW,MAAM;AAC1D,YAAM,mBAAmB,OAAO,QAAQ,KAAK;AAC7C;AAAA,IACD;AAAA,EACD;AACD;AAIA,eAAsB,uBACrB,KACA,OACoB;AACpB,QAAM,SAAU,MAAM,IAAI,KAAK;AAC/B,MAAI,CAAC,MAAM,QAAQ,MAAM,EAAG,QAAO,SAAS,KAAK,EAAE,IAAI,KAAK,CAAC;AAE7D,aAAW,MAAM,QAAQ;AACxB,UAAM,QAAQ,GAAG;AACjB,QAAI,CAAC,MAAO;AAEZ,YAAQ,GAAG,OAAO;AAAA,MACjB,KAAK;AAAY,cAAM,qBAAqB,OAAO,KAAK;AAAG;AAAA,MAC3D,KAAK;AAAY,cAAM,kBAAkB,OAAO,KAAK;AAAG;AAAA,MACxD,KAAK;AAAY,cAAM,mBAAmB,OAAO,KAAK;AAAG;AAAA,MACzD,KAAK;AAAA,MACL,KAAK;AACJ,cAAM,mBAAmB,OAAO,GAAG,OAAO,KAAK;AAC/C;AAAA,IACF;AAAA,EACD;AAEA,SAAO,SAAS,KAAK,EAAE,IAAI,KAAK,CAAC;AAClC;AAUA,SAAS,UAAU,MAAuB;AACzC,MAAI;AAAE,WAAO,KAAK,MAAM,IAAI;AAAA,EAAG,QAAQ;AAAE,WAAO;AAAA,EAAM;AACvD;;;ACvMA,IAAAE,eAAqC;AACrC,yBAAkC;AAClC,IAAAC,gBAAqC;;;ACFrC,mBAAkD;AA+BlD,IAAM,aAAa;AAEnB,IAAM,oBAAwC;AAAA,EAC7C,OAAO;AAAA,EACP,WAAW;AAAA,EACX,SAAS;AAAA,EACT,YAAY,CAAC,QAAQ,QAAQ;AAAA,EAC7B,gBAAgB,CAAC,WAAW,QAAQ,MAAM;AAAA,EAC1C,aAAa,CAAC,QAAQ;AAAA,EACtB,WAAW;AACZ;AAKA,eAAsB,YACrB,MAUA,OAC0B;AAC1B,QAAM,OAAgC;AAAA,IACrC,SAAS,KAAK,WAAW;AAAA,IACzB,MAAM,KAAK,QAAQ;AAAA,IACnB,MAAM,KAAK;AAAA,EACZ;AACA,MAAI,KAAK,WAAW,OAAW,MAAK,QAAQ,IAAI,KAAK;AACrD,aAAO,6BAA+B,mBAAmB,OAAO;AAAA,IAC/D,KAAK,KAAK;AAAA,IACV,OAAO,KAAK,UAAU;AAAA,IACtB;AAAA,IACA,GAAI,KAAK,cAAc,SAAY,EAAE,WAAW,KAAK,UAAU,IAAI,CAAC;AAAA,IACpE,OAAO,KAAK,SAAS;AAAA,EACtB,CAAC;AACF;AAEA,eAAsB,iBACrB,MACA,OAC0B;AAC1B,aAAO,gCAAkC,mBAAmB,OAAO;AAAA,IAClE,KAAK,KAAK;AAAA,IACV,OAAO,KAAK,UAAU;AAAA,IACtB,WAAW,KAAK;AAAA,IAChB,OAAO,KAAK,SAAS;AAAA,EACtB,CAAC;AACF;AAEA,eAAsB,sBACrB,MACA,QACA,OAC+B;AAC/B,SAAO,MAAM;AAAA,IACZ;AAAA;AAAA;AAAA;AAAA,IAIA,CAAC,MAAM,MAAM;AAAA,EACd;AACD;AAEA,eAAsB,iBACrB,MACA,QACA,OACiC;AACjC,QAAM,CAAC,GAAG,IAAI,MAAM,MAAM;AAAA,IACzB,UAAU,UAAU;AAAA,IACpB,CAAC,MAAM,MAAM;AAAA,EACd;AACA,SAAO,OAAO;AACf;AAEA,eAAsB,oBAAoB,OAAiD;AAC1F,SAAO,MAAM;AAAA,IACZ,UAAU,UAAU;AAAA,EACrB;AACD;AAEA,eAAsB,eACrB,MACA,QACA,OACmB;AACnB,QAAM,OAAO,MAAM,MAAM;AAAA,IACxB;AAAA,IACA,CAAC,MAAM,MAAM;AAAA,EACd;AACA,SAAO,KAAK,SAAS;AACtB;;;AD9GA,SAAS,QAAQ,YAAoB,SAAiC;AACrE,QAAM,YAAQ,sCAAkB,UAAU;AAC1C,SAAO,CAAC,KAAK,SAAS,MAAM,KAAK,MAAM,QAAQ,KAAK,IAAI,CAAC;AAC1D;AAEA,IAAM,UAAU,CAAC,QAA0B,IAAI,QAAQ,QAAQ,IAAI,SAAS,KAAK;AACjF,IAAM,SAAS,CAAC,QAA0B,IAAI,KAAK,SAAS,MAAM,KAAK;AACvE,IAAM,WAAW,CAAC,QACjB,IAAI,IAAI,IAAI,QAAQ,GAAG,EAAE,aAAa,IAAI,QAAQ;AACnD,IAAM,OAAO,CAAC,QACZ,IAAI,KAAK,MAAM,KAA6C,CAAC;AAE/D,SAAS,WAAW,KAAwB;AAC3C,MAAI,eAAe,oCAAsB;AACxC,eAAO,6BAAe,kBAAK,UAAU,oBAAoB,IAAI,SAAS;AAAA,MACrE,gBAAgB,IAAI;AAAA,IACrB,CAAC;AAAA,EACF;AACA,QAAM;AACP;AAIO,SAAS,yBACf,OACA,YACsC;AACtC,QAAM,IAAI,CAAC,MAA8B,QAAQ,YAAY,CAAC;AAE9D,SAAO;AAAA,IACN,CAAC,OAAO,oBAAoB,EAAE,YAAY;AACzC,iBAAO,6BAAe,kBAAK,IAAI,oBAAoB,aAAa,MAAM,oBAAoB,KAAK,CAAC;AAAA,IACjG,CAAC,CAAC;AAAA,IACF,CAAC,OAAO,0BAA0B,EAAE,OAAO,QAAQ;AAClD,YAAM,MAAM,MAAM,iBAAiB,OAAO,GAAG,GAAG,SAAS,GAAG,GAAG,KAAK;AACpE,aAAO,UACJ,6BAAe,kBAAK,IAAI,YAAY,YAAY,GAAG,QACnD,6BAAe,kBAAK,WAAW,aAAa,kBAAkB;AAAA,IAClE,CAAC,CAAC;AAAA,IACF,CAAC,OAAO,0BAA0B,EAAE,OAAO,QAAQ;AAClD,YAAM,IAAI,KAAK,GAAG;AAClB,UAAI,OAAO,EAAE,MAAM,MAAM,UAAU;AAClC,mBAAO,6BAAe,kBAAK,eAAe,WAAW,gCAAgC;AAAA,MACtF;AACA,UAAI;AACH,cAAM,OAA0C;AAAA,UAC/C,MAAM,OAAO,GAAG;AAAA,UAChB,MAAM,EAAE,MAAM;AAAA,UACd,QAAQ,SAAS,GAAG;AAAA,UACpB,OAAO,QAAQ,GAAG;AAAA,QACnB;AACA,YAAI,OAAO,EAAE,SAAS,MAAM,SAAU,MAAK,UAAU,EAAE,SAAS;AAChE,YAAI,OAAO,EAAE,MAAM,MAAM,SAAU,MAAK,OAAO,EAAE,MAAM;AACvD,YAAI,OAAO,EAAE,QAAQ,MAAM,UAAW,MAAK,SAAS,EAAE,QAAQ;AAC9D,YAAI,OAAO,EAAE,WAAW,MAAM,SAAU,MAAK,YAAY,EAAE,WAAW;AACtE,mBAAO,6BAAe,kBAAK,IAAI,gBAAgB,kBAAkB,MAAM,YAAY,MAAM,KAAK,CAAC;AAAA,MAChG,SAAS,KAAK;AACb,eAAO,WAAW,GAAG;AAAA,MACtB;AAAA,IACD,CAAC,CAAC;AAAA,IACF,CAAC,UAAU,0BAA0B,EAAE,OAAO,QAAQ;AACrD,YAAM,KAAK,MAAM,eAAe,OAAO,GAAG,GAAG,SAAS,GAAG,GAAG,KAAK;AACjE,iBAAO,6BAAe,KAAK,kBAAK,KAAK,kBAAK,WAAW,KAAK,YAAY,aAAa,KAAK,YAAY,kBAAkB;AAAA,IACvH,CAAC,CAAC;AAAA,IACF,CAAC,OAAO,oCAAoC,EAAE,OAAO,QAAQ;AAC5D,iBAAO,6BAAe,kBAAK,IAAI,aAAa,sBAAsB,MAAM,sBAAsB,OAAO,GAAG,GAAG,SAAS,GAAG,GAAG,KAAK,CAAC;AAAA,IACjI,CAAC,CAAC;AAAA,IACF,CAAC,QAAQ,mCAAmC,EAAE,OAAO,QAAQ;AAC5D,YAAM,IAAI,KAAK,GAAG;AAClB,YAAM,YAAY,OAAO,EAAE,WAAW,CAAC;AACvC,UAAI,CAAC,OAAO,UAAU,SAAS,GAAG;AACjC,mBAAO,6BAAe,kBAAK,eAAe,WAAW,kCAAkC;AAAA,MACxF;AACA,YAAM,MAAM,MAAM;AAAA,QACjB,EAAE,MAAM,OAAO,GAAG,GAAG,QAAQ,SAAS,GAAG,GAAG,WAAW,OAAO,QAAQ,GAAG,EAAE;AAAA,QAC3E;AAAA,MACD;AACA,iBAAO,6BAAe,kBAAK,IAAI,eAAe,mBAAmB,SAAS,IAAI,GAAG;AAAA,IAClF,CAAC,CAAC;AAAA,EACH;AACD;;;AVhFO,IAAM,gBAAN,MAA+C;AAAA,EAKrD,YACS,QACA,OACR,KACC;AAHO;AACA;AAGR,UAAM,iBAAiB,OAAO,WAAW,UAAU;AACnD,UAAM,WAAW,uBAAuB,gBAAgB,QAAQ,KAAK;AAErE,SAAK,aAAa,IAAI,WAAW,QAAQ,UAAU,KAAK;AAExD,QAAI,OAAO,MAAO,MAAK,WAAW,gBAAgB,IAAI,aAAa,OAAO,KAAK,CAAC;AAChF,QAAI,OAAO,IAAK,MAAK,WAAW,gBAAgB,IAAI,WAAW,OAAO,GAAG,CAAC;AAC1E,QAAI,OAAO,KAAM,MAAK,WAAW,gBAAgB,IAAI,YAAY,OAAO,IAAI,CAAC;AAE7E,SAAK;AAAA,MACJ;AAAA,MACA,OAAO,QAAQ;AACd,cAAM,KAAK,WAAW,SAAS,GAAG;AAAA,MACnC;AAAA,MACA;AAAA,IACD;AAAA,EACD;AAAA,EApBS;AAAA,EACA;AAAA,EANA,OAAO;AAAA,EACP,OAAO,CAAC,kBAAkB;AAAA,EAC1B;AAAA;AAAA,EA0BT,iBAAsC;AACrC,WAAO;AAAA,MACN,GAAG,6BAA6B,KAAK,QAAQ,KAAK,WAAW,aAAa,CAAC;AAAA;AAAA;AAAA,MAG3E,OAAG,wCAAmB,KAAK,OAAO,YAAY,EAAE,QAAQ,KAAK,KAAK,CAAC;AAAA,IACpE;AAAA,EACD;AAAA,EAEA,QAAQ,KAAyB;AAEhC,0BAAsB,KAAK,QAAQ,KAAK,WAAW,aAAa,CAAC;AAEjE,UAAM,QAAQ,KAAK;AACnB,UAAM,cAAc,KAAK,OAAO,UAAU;AAE1C,QAAI;AAAA,MAAS;AAAA,MAAQ;AAAA,MAA8B,CAAC,QACnD,uBAAuB,IAAI,SAAS,OAAQ,aAAa,QAAQ;AAAA,IAClE;AACA,QAAI;AAAA,MAAS;AAAA,MAAQ;AAAA,MAA6B,CAAC,QAClD,sBAAsB,IAAI,SAAS,OAAQ,aAAa,OAAO;AAAA,IAChE;AACA,QAAI;AAAA,MAAS;AAAA,MAAQ;AAAA,MAA8B,CAAC,QACnD,uBAAuB,IAAI,SAAS,KAAM;AAAA,IAC3C;AAIA,QAAI,KAAK,OAAO,YAAY;AAC3B,UAAI,CAAC,OAAO;AACX,cAAM,IAAI,MAAM,8DAA8D;AAAA,MAC/E;AACA,iBAAW,CAAC,QAAQ,MAAM,OAAO,KAAK,yBAAyB,OAAO,KAAK,OAAO,UAAU,GAAG;AAC9F,YAAI,SAAS,QAAQ,MAAM,OAAO;AAAA,MACnC;AAAA,IACD;AAAA,EACD;AACD;AAEA,SAAS,uBACR,QACA,QACA,OACoB;AACpB,MAAI,WAAW,MAAM;AACpB,WAAO,IAAI,mBAAmB,OAAO,WAAW,aAAa,aAAa;AAAA,EAC3E;AAEA,MAAI,CAAC,OAAO;AACX,UAAM,IAAI,MAAM,wDAAwD;AAAA,EACzE;AAEA,SAAO,IAAI,mBAAmB,KAAK;AACpC;;;AY1EO,IAAM,UAAU;AAAA,EACtB,OAAO;AAAA,EACP,KAAK;AAAA,EACL,MAAM;AACP;","names":["body","body","nodemailer","body","import_middlewares","body","events","import_core","import_store"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/module.ts","../src/log.ts","../src/dispatcher.ts","../src/channels/sms.ts","../src/channels/push.ts","../src/channels/email.ts","../src/templates/layout.ts","../src/templates/resolver.ts","../src/config-guard.ts","../src/delivery.ts","../src/templates/admin-routes.ts","../src/templates/admin.ts","../src/config.ts"],"sourcesContent":["export { CourierModule } from './module';\n// Production-readiness — warn on message types routed to a channel with no\n// provider. Runs automatically at boot (CourierModule.install); exported for an\n// app's own preflight.\nexport { validateCourierConfig } from './config-guard';\nexport { handleSendGridDelivery, handleMailgunDelivery, handleMailtrapDelivery } from './delivery';\nexport { Dispatcher } from './dispatcher';\nexport { SmsChannel } from './channels/sms';\nexport { PushChannel } from './channels/push';\nexport { EmailChannel } from './channels/email';\nexport { DBTemplateResolver, FSTemplateResolver, DefaultTemplates, renderFragment } from './templates/resolver';\n// Versioned template management (on the @fonderie/store primitive): edit with\n// optimistic concurrency, revision history, rollback. The resolver is unchanged.\nexport {\n\tsetTemplate,\n\trollbackTemplate,\n\tlistTemplateRevisions,\n\tgetTemplateEntry,\n\tlistTemplateEntries,\n\tdeleteTemplate,\n} from './templates/admin';\nexport { buildTemplateAdminRoutes } from './templates/admin-routes';\nexport type { ITemplateEntry, ITemplateRevision } from './templates/admin';\nexport type { IMessageLog, MessageLogStatus } from './log';\nexport type {\n\tICourierMessage,\n\tICourierChannel,\n\tIRenderedTemplate,\n\tITemplateResolver,\n\tIDefaultTemplate,\n\tDefaultTemplateMap,\n} from './types';\nexport { Channel } from './config';\nexport type {\n\tICourierConfig,\n\tIEmailChannelConfig,\n\tISmsChannelConfig,\n\tIPushChannelConfig,\n} from './config';\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport type { EventBus } from '@fonderie/events';\nimport { NOTIFICATION_EVENT } from '@fonderie/events';\n\nimport type { ITemplateResolver, ICourierMessage } from './types';\nimport type { ICourierConfig } from './config';\n\nimport { Dispatcher } from './dispatcher';\nimport { SmsChannel } from './channels/sms';\nimport { PushChannel } from './channels/push';\nimport { EmailChannel } from './channels/email';\nimport type { IReadinessProblem } from '@fonderie/core';\nimport { validateAdminToken } from '@fonderie/core/middlewares';\nimport { DBTemplateResolver, FSTemplateResolver, DefaultTemplates } from './templates/resolver';\nimport { validateCourierConfig, collectCourierConfigProblems } from './config-guard';\nimport { handleSendGridDelivery, handleMailgunDelivery, handleMailtrapDelivery } from './delivery';\nimport { buildTemplateAdminRoutes } from './templates/admin-routes';\n\nexport class CourierModule implements IFonderieModule {\n\treadonly name = '@fonderie/courier';\n\treadonly deps = ['@fonderie/events'];\n\treadonly dispatcher: Dispatcher;\n\n\tconstructor(\n\t\tprivate config: ICourierConfig,\n\t\tprivate store?: IStoreAdapter,\n\t\tbus?: EventBus,\n\t) {\n\t\tconst templateSource = config.templates?.source ?? 'db';\n\t\tconst resolver = createTemplateResolver(templateSource, config, store);\n\n\t\tthis.dispatcher = new Dispatcher(config, resolver, store);\n\n\t\tif (config.email) this.dispatcher.registerChannel(new EmailChannel(config.email));\n\t\tif (config.sms) this.dispatcher.registerChannel(new SmsChannel(config.sms));\n\t\tif (config.push) this.dispatcher.registerChannel(new PushChannel(config.push));\n\n\t\tbus?.on<ICourierMessage>(\n\t\t\tNOTIFICATION_EVENT,\n\t\t\tasync (msg) => {\n\t\t\t\tawait this.dispatcher.dispatch(msg);\n\t\t\t},\n\t\t\t'courier',\n\t\t);\n\t}\n\n\t// Report config problems for app.checkProductionReadiness() (data, not warn).\n\tcheckReadiness(): IReadinessProblem[] {\n\t\treturn [\n\t\t\t...collectCourierConfigProblems(this.config, this.dispatcher.channelNames()),\n\t\t\t// Shared admin-token strength rule (@fonderie/core/middlewares) — the\n\t\t\t// /admin/templates surface must not be guarded by a weak/placeholder token.\n\t\t\t...validateAdminToken(this.config.adminToken, { module: this.name }),\n\t\t];\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\t// Boot-time preflight: warn if any routed message type has no provider.\n\t\tvalidateCourierConfig(this.config, this.dispatcher.channelNames());\n\n\t\tconst store = this.store;\n\t\tconst signingKeys = this.config.delivery?.signingKeys;\n\n\t\tapp.addRoute('POST', '/courier/delivery/sendgrid', (ctx) =>\n\t\t\thandleSendGridDelivery(ctx.request, store!, signingKeys?.sendgrid),\n\t\t);\n\t\tapp.addRoute('POST', '/courier/delivery/mailgun', (ctx) =>\n\t\t\thandleMailgunDelivery(ctx.request, store!, signingKeys?.mailgun),\n\t\t);\n\t\tapp.addRoute('POST', '/courier/delivery/mailtrap', (ctx) =>\n\t\t\thandleMailtrapDelivery(ctx.request, store!),\n\t\t);\n\n\t\t// Versioned template admin routes — only when a token is configured and a\n\t\t// store is present (db templates). Mirrors @fonderie/config's admin surface.\n\t\tif (this.config.adminToken) {\n\t\t\tif (!store) {\n\t\t\t\tthrow new Error('[courier] adminToken requires @fonderie/store (db templates)');\n\t\t\t}\n\t\t\tfor (const [method, path, handler] of buildTemplateAdminRoutes(store, this.config.adminToken)) {\n\t\t\t\tapp.addRoute(method, path, handler);\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction createTemplateResolver(\n\tsource: 'db' | 'fs',\n\tconfig: ICourierConfig,\n\tstore?: IStoreAdapter,\n): ITemplateResolver {\n\t// Aggregate the module-shipped defaults (one map or an array) into a single\n\t// lookup, mirroring how getMigrationsPath() results are collected at boot.\n\tconst input = config.templates?.defaults;\n\tconst defaults = new DefaultTemplates(input ? (Array.isArray(input) ? input : [input]) : []);\n\n\tif (source === 'fs') {\n\t\treturn new FSTemplateResolver(config.templates?.directory ?? './templates', defaults);\n\t}\n\n\tif (!store) {\n\t\tthrow new Error('[courier] store is required for DB template resolution');\n\t}\n\n\treturn new DBTemplateResolver(store, defaults);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nexport type MessageLogStatus =\n\t| 'pending' | 'sent' | 'failed'\n\t| 'delivered' | 'opened' | 'clicked' | 'bounced' | 'spam';\n\nexport interface IMessageLog {\n\tid: string;\n\tmessageType: string;\n\tchannel: string;\n\trecipient: string;\n\tlocale: string | null;\n\tstatus: MessageLogStatus;\n\terror: string | null;\n\tattempts: number;\n\tprovider: string | null;\n\tproviderMessageId: string | null;\n\topenedAt: string | null;\n\tclickedAt: string | null;\n\tbouncedAt: string | null;\n\tbounceReason: string | null;\n\tcreatedAt: string;\n\tsentAt: string | null;\n}\n\nexport async function insertMessageLog(\n\tentry: {\n\t\tmessageType: string;\n\t\tchannel: string;\n\t\trecipient: string;\n\t\tlocale?: string;\n\t\tprovider?: string;\n\t\tproviderMessageId?: string;\n\t},\n\tstore: IStoreAdapter,\n): Promise<string> {\n\tconst [row] = await store.query<{ id: string }>(\n\t\t`INSERT INTO fonderie_message_log\n\t\t\t(message_type, channel, recipient, locale, provider, provider_message_id)\n\t\t VALUES ($1, $2, $3, $4, $5, $6)\n\t\t RETURNING id`,\n\t\t[\n\t\t\tentry.messageType,\n\t\t\tentry.channel,\n\t\t\tentry.recipient,\n\t\t\tentry.locale ?? null,\n\t\t\tentry.provider ?? null,\n\t\t\tentry.providerMessageId ?? null,\n\t\t],\n\t);\n\treturn row?.id ?? '';\n}\n\nexport async function markMessageSent(id: string, store: IStoreAdapter): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'sent', sent_at = now(), attempts = attempts + 1\n\t\t WHERE id = $1`,\n\t\t[id],\n\t);\n}\n\nexport async function markMessageFailed(\n\tid: string,\n\terror: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'failed', error = $2, attempts = attempts + 1\n\t\t WHERE id = $1`,\n\t\t[id, error],\n\t);\n}\n\nexport async function markMessageDelivered(\n\tproviderMessageId: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'delivered'\n\t\t WHERE provider_message_id = $1`,\n\t\t[providerMessageId],\n\t);\n}\n\nexport async function markMessageOpened(\n\tproviderMessageId: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'opened', opened_at = now()\n\t\t WHERE provider_message_id = $1 AND opened_at IS NULL`,\n\t\t[providerMessageId],\n\t);\n}\n\nexport async function markMessageClicked(\n\tproviderMessageId: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'clicked', clicked_at = now()\n\t\t WHERE provider_message_id = $1 AND clicked_at IS NULL`,\n\t\t[providerMessageId],\n\t);\n}\n\nexport async function markMessageBounced(\n\tproviderMessageId: string,\n\treason: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'bounced', bounced_at = now(), bounce_reason = $2\n\t\t WHERE provider_message_id = $1`,\n\t\t[providerMessageId, reason],\n\t);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ICourierConfig } from './config';\nimport type { ICourierMessage, ICourierChannel, ITemplateResolver } from './types';\nimport { insertMessageLog, markMessageSent, markMessageFailed } from './log';\n\nfunction resolveRecipient(message: ICourierMessage, channel: string): string {\n\tif (channel === 'email') return message.recipient.email ?? '';\n\tif (channel === 'sms') return message.recipient.phone ?? '';\n\tif (channel === 'push') return message.recipient.deviceToken ?? '';\n\treturn message.recipient.email ?? message.recipient.phone ?? '';\n}\n\nexport class Dispatcher {\n\tprivate channels: Map<string, ICourierChannel> = new Map();\n\n\tconstructor(\n\t\tprivate config: ICourierConfig,\n\t\tprivate resolver: ITemplateResolver,\n\t\tprivate store?: IStoreAdapter,\n\t) {}\n\n\tregisterChannel(channel: ICourierChannel): this {\n\t\tthis.channels.set(channel.name, channel);\n\t\treturn this;\n\t}\n\n\t// Names of the currently-registered channels — used by the boot-time config\n\t// guard to detect message types routed to a channel with no provider.\n\tchannelNames(): string[] {\n\t\treturn [...this.channels.keys()];\n\t}\n\n\tasync dispatch(message: ICourierMessage): Promise<void> {\n\t\tconst channelNames = this.config.channels[message.type];\n\n\t\tif (!channelNames || channelNames.length === 0) {\n\t\t\tconsole.warn(`[courier] no channels configured for message type: ${message.type}`);\n\t\t\treturn;\n\t\t}\n\n\t\tconst template = await this.resolver.resolve(message.type, message.data, message.locale);\n\n\t\tawait Promise.allSettled(\n\t\t\tchannelNames.map(async (name) => {\n\t\t\t\tconst channel = this.channels.get(name);\n\t\t\t\tif (!channel) {\n\t\t\t\t\tconsole.warn(`[courier] channel \"${name}\" not registered`);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Insert log entry (fire-and-forget if store unavailable)\n\t\t\t\tconst logEntry: Parameters<typeof insertMessageLog>[0] = {\n\t\t\t\t\tmessageType: message.type,\n\t\t\t\t\tchannel: name,\n\t\t\t\t\trecipient: resolveRecipient(message, name),\n\t\t\t\t};\n\t\t\t\tif (message.locale) logEntry.locale = message.locale;\n\n\t\t\t\tconst logId = this.store\n\t\t\t\t\t? await insertMessageLog(logEntry, this.store).catch(() => '')\n\t\t\t\t\t: '';\n\n\t\t\t\ttry {\n\t\t\t\t\tawait channel.send(message, template);\n\n\t\t\t\t\tif (this.store && logId) {\n\t\t\t\t\t\tmarkMessageSent(logId, this.store).catch(() => undefined);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tconsole.error(`[courier:${name}] failed to send ${message.type}:`, err);\n\n\t\t\t\t\tif (this.store && logId) {\n\t\t\t\t\t\tmarkMessageFailed(logId, errMsg, this.store).catch(() => undefined);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\t}\n}\n","import type { ISmsChannelConfig } from '../config';\nimport type { ICourierChannel, ICourierMessage, IRenderedTemplate } from '../types';\n\nexport class SmsChannel implements ICourierChannel {\n\treadonly name = 'sms';\n\n\tconstructor(private config: ISmsChannelConfig) {}\n\n\tasync send(message: ICourierMessage, template: IRenderedTemplate): Promise<void> {\n\t\tconst to = message.recipient.phone;\n\t\tif (!to) {\n\t\t\tconsole.warn('[courier:sms] no phone number for recipient — skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.config.provider === 'twilio') {\n\t\t\tawait this.sendViaTwilio(to, template.text);\n\t\t} else if (this.config.provider === 'vonage') {\n\t\t\tawait this.sendViaVonage(to, template.text);\n\t\t} else {\n\t\t\tconsole.warn(`[courier:sms] provider ${this.config.provider} not implemented`);\n\t\t}\n\t}\n\n\tprivate async sendViaTwilio(to: string, text: string): Promise<void> {\n\t\tconst { accountSid, authToken } = this.config;\n\t\tif (!accountSid || !authToken) {\n\t\t\tthrow new Error('Twilio accountSid and authToken required');\n\t\t}\n\n\t\tconst res = await fetch(\n\t\t\t`https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`,\n\t\t\t{\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Basic ${Buffer.from(`${accountSid}:${authToken}`).toString('base64')}`,\n\t\t\t\t\t'Content-Type': 'application/x-www-form-urlencoded',\n\t\t\t\t},\n\t\t\t\tbody: new URLSearchParams({ From: this.config.from, To: to, Body: text }),\n\t\t\t},\n\t\t);\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:sms] Twilio error ${res.status}: ${body}`);\n\t\t}\n\t}\n\n\tprivate async sendViaVonage(to: string, text: string): Promise<void> {\n\t\tconst { apiKey, apiSecret } = this.config;\n\t\tif (!apiKey || !apiSecret) {\n\t\t\tthrow new Error('Vonage apiKey and apiSecret required');\n\t\t}\n\n\t\tconst res = await fetch('https://rest.nexmo.com/sms/json', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({\n\t\t\t\tapi_key: apiKey,\n\t\t\t\tapi_secret: apiSecret,\n\t\t\t\tfrom: this.config.from,\n\t\t\t\tto,\n\t\t\t\ttext,\n\t\t\t}),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:sms] Vonage error ${res.status}: ${body}`);\n\t\t}\n\t}\n}\n","import type { IPushChannelConfig } from '../config';\nimport type { ICourierChannel, ICourierMessage, IRenderedTemplate } from '../types';\n\nexport class PushChannel implements ICourierChannel {\n\treadonly name = 'push';\n\n\tconstructor(private config: IPushChannelConfig) {}\n\n\tasync send(message: ICourierMessage, template: IRenderedTemplate): Promise<void> {\n\t\tconst token = message.recipient.deviceToken;\n\t\tif (!token) {\n\t\t\tconsole.warn('[courier:push] no device token for recipient — skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.config.provider === 'fcm') {\n\t\t\tawait this.sendViaFCM(token, template);\n\t\t}\n\t}\n\n\tprivate async sendViaFCM(token: string, template: IRenderedTemplate): Promise<void> {\n\t\tconst apiKey = this.config.serviceAccount['apiKey'] as string | undefined;\n\t\tif (!apiKey) {\n\t\t\tthrow new Error('[courier:push] FCM apiKey is required in serviceAccount');\n\t\t}\n\n\t\tconst res = await fetch('https://fcm.googleapis.com/fcm/send', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\tAuthorization: `key=${apiKey}`,\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tto: token,\n\t\t\t\tnotification: {\n\t\t\t\t\ttitle: template.subject,\n\t\t\t\t\tbody: template.text,\n\t\t\t\t},\n\t\t\t}),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:push] FCM error ${res.status}: ${body}`);\n\t\t}\n\t}\n}\n","import nodemailer from 'nodemailer';\n\nimport type { ICourierChannel, ICourierMessage, IRenderedTemplate } from '../types';\nimport type { IEmailChannelConfig } from '../config';\n\nexport class EmailChannel implements ICourierChannel {\n\treadonly name = 'email';\n\tprivate transport: ReturnType<typeof nodemailer.createTransport> | null = null;\n\n\tconstructor(private config: IEmailChannelConfig) {\n\t\tif (config.provider === 'smtp' && config.smtp) {\n\t\t\tthis.transport = nodemailer.createTransport({\n\t\t\t\thost: config.smtp.host,\n\t\t\t\tport: config.smtp.port,\n\t\t\t\tsecure: config.smtp.secure,\n\t\t\t\tauth: { user: config.smtp.user, pass: config.smtp.pass },\n\t\t\t});\n\t\t}\n\t}\n\n\tasync send(message: ICourierMessage, template: IRenderedTemplate): Promise<void> {\n\t\tconst to = message.recipient.email;\n\t\tif (!to) {\n\t\t\tconsole.warn('[courier:email] no email address for recipient — skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.config.provider === 'resend') {\n\t\t\tawait this.sendViaResend(to, template);\n\t\t} else if (this.config.provider === 'smtp') {\n\t\t\tawait this.sendViaSMTP(to, template);\n\t\t} else {\n\t\t\tconsole.warn(`[courier:email] provider ${this.config.provider} not implemented`);\n\t\t}\n\t}\n\n\tprivate async sendViaResend(to: string, template: IRenderedTemplate): Promise<void> {\n\t\tif (!this.config.apiKey) {\n\t\t\tthrow new Error('Resend apiKey is required');\n\t\t}\n\n\t\tconst res = await fetch('https://api.resend.com/emails', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${this.config.apiKey}`,\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tfrom: this.config.from,\n\t\t\t\tto,\n\t\t\t\tsubject: template.subject ?? '(no subject)',\n\t\t\t\thtml: template.html,\n\t\t\t\ttext: template.text,\n\t\t\t}),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:email] Resend error ${res.status}: ${body}`);\n\t\t}\n\t}\n\n\tprivate async sendViaSMTP(to: string, template: IRenderedTemplate): Promise<void> {\n\t\tif (!this.transport) {\n\t\t\tthrow new Error('SMTP transport not initialised — check smtp config');\n\t\t}\n\n\t\tawait this.transport.sendMail({\n\t\t\tfrom: this.config.from,\n\t\t\tto,\n\t\t\tsubject: template.subject ?? '(no subject)',\n\t\t\thtml: template.html,\n\t\t\ttext: template.text,\n\t\t});\n\t}\n}\n","// Shared, composable email layout — a branded shell that wraps every template\n// body. Templates stored in the DB or on disk are **body fragments**; the\n// resolver injects them into `{{content}}` here, then interpolates `{{vars}}`\n// over the whole. One shell, many bodies — so DB and FS sources render an\n// identical, production-grade frame for free.\n//\n// Cross-client hardening (Outlook/Gmail/Apple Mail) follows the well-worn\n// \"hybrid responsive\" pattern popularised by Cerberus (MIT): a max-width table\n// container, inline styles on the critical structure, and a `<style>` block in\n// `<head>` for the friendly classes and small-screen media query. Founders can\n// override the whole shell by storing a `_layout` template (DB row or\n// `_layout.html` file) in their own source.\n//\n// The theme tokens mirror the product design system (organization/ui/base.css):\n// near-black primary, a mint brand accent, Inter, and the `#fafafa` canvas —\n// retune `EMAIL_THEME` to rebrand every email at once.\n\n// A single small token set — retune these to match your brand.\nexport const EMAIL_THEME = {\n\tbrand: 'Fonderie',\n\taccent: '#171717', // primary action (button) background — the product's primary\n\taccentText: '#ffffff', // text on the accent\n\tbrandAccent: '#171717', // brand highlight (top rule) — monochrome near-black\n\tlink: '#009767', // links — the accessible (darkened) brand teal\n\tink: '#171717', // body copy\n\tmuted: '#5c5c5c', // secondary copy\n\tline: '#e0e0e0', // hairline borders\n\tcanvas: '#fafafa', // page background behind the card\n\tcard: '#ffffff', // the card itself\n} as const;\n\n// The product font stack (Inter first, with system fallbacks for clients that\n// lack it). Shared by every text element so the frame stays consistent.\nconst FONT_SANS =\n\t\"Inter, 'Inter Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif\";\nconst FONT_MONO = \"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace\";\n\n// The `{{content}}` marker is replaced with the body fragment BEFORE variable\n// interpolation, so a body's own `{{firstName}}` etc. still resolve.\nexport const LAYOUT_CONTENT_SLOT = '{{content}}';\n\nexport const DEFAULT_EMAIL_LAYOUT = `<!DOCTYPE html>\n<html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n<head>\n\t<meta charset=\"utf-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"color-scheme\" content=\"light dark\">\n\t<meta name=\"supported-color-schemes\" content=\"light dark\">\n\t<title>{{subject}}</title>\n\t<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->\n\t<style>\n\t\tbody { margin: 0; padding: 0; width: 100% !important; background: ${EMAIL_THEME.canvas}; }\n\t\t.email-body { background: ${EMAIL_THEME.canvas}; }\n\t\t.email-container { width: 100%; max-width: 560px; margin: 0 auto; }\n\t\t.email-card {\n\t\t\tbackground: ${EMAIL_THEME.card};\n\t\t\tborder: 1px solid ${EMAIL_THEME.line};\n\t\t\tborder-radius: 8px;\n\t\t\toverflow: hidden;\n\t\t}\n\t\t.email-accent { height: 3px; background: ${EMAIL_THEME.brandAccent}; font-size: 0; line-height: 0; }\n\t\t.email-header { padding: 28px 32px 0 32px; }\n\t\t.email-brand { font: 800 19px/1 ${FONT_SANS}; color: ${EMAIL_THEME.ink}; letter-spacing: -0.03em; }\n\t\t.email-content {\n\t\t\tpadding: 20px 32px 8px 32px;\n\t\t\tfont: 400 16px/1.7 ${FONT_SANS};\n\t\t\tcolor: ${EMAIL_THEME.ink};\n\t\t\tletter-spacing: -0.01em;\n\t\t}\n\t\t.email-content h1 { margin: 0 0 12px 0; font-size: 22px; line-height: 1.3; font-weight: 700; letter-spacing: -0.03em; }\n\t\t.email-content p { margin: 0 0 16px 0; }\n\t\t.email-content a { color: ${EMAIL_THEME.link}; }\n\t\t.email-footer {\n\t\t\tpadding: 16px 32px 28px 32px;\n\t\t\tfont: 400 13px/1.6 ${FONT_SANS};\n\t\t\tcolor: ${EMAIL_THEME.muted};\n\t\t\tletter-spacing: -0.01em;\n\t\t}\n\t\t.btn {\n\t\t\tdisplay: inline-block;\n\t\t\tbackground: ${EMAIL_THEME.accent};\n\t\t\tcolor: ${EMAIL_THEME.accentText} !important;\n\t\t\ttext-decoration: none;\n\t\t\tfont: 600 16px/1 ${FONT_SANS};\n\t\t\tletter-spacing: -0.01em;\n\t\t\tpadding: 13px 26px;\n\t\t\tborder-radius: 6px;\n\t\t}\n\t\t.pin-code {\n\t\t\tdisplay: inline-block;\n\t\t\tfont: 700 30px/1 ${FONT_MONO};\n\t\t\tletter-spacing: 0.28em;\n\t\t\tcolor: ${EMAIL_THEME.ink};\n\t\t\tbackground: ${EMAIL_THEME.canvas};\n\t\t\tborder: 1px solid ${EMAIL_THEME.line};\n\t\t\tborder-radius: 6px;\n\t\t\tpadding: 14px 22px 14px 30px;\n\t\t}\n\t\t.muted { color: ${EMAIL_THEME.muted}; }\n\t\t@media only screen and (max-width: 599px) {\n\t\t\t.email-header, .email-content, .email-footer { padding-left: 22px !important; padding-right: 22px !important; }\n\t\t\t.pin-code { font-size: 26px !important; letter-spacing: 0.2em !important; }\n\t\t}\n\t</style>\n</head>\n<body class=\"email-body\">\n\t<div style=\"display:none;max-height:0;overflow:hidden;opacity:0;\">{{preheader}}</div>\n\t<table role=\"presentation\" class=\"email-body\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"background:${EMAIL_THEME.canvas};\">\n\t\t<tr>\n\t\t\t<td align=\"center\" style=\"padding: 32px 16px;\">\n\t\t\t\t<table role=\"presentation\" class=\"email-container\" width=\"560\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"max-width:560px;\">\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td class=\"email-card\" style=\"background:${EMAIL_THEME.card};border:1px solid ${EMAIL_THEME.line};border-radius:8px;\">\n\t\t\t\t\t\t\t<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n\t\t\t\t\t\t\t\t<tr><td class=\"email-accent\" style=\"height:3px;background:${EMAIL_THEME.brandAccent};font-size:0;line-height:0;\"> </td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-header\" style=\"padding:28px 32px 0 32px;\">\n\t\t\t\t\t\t\t\t\t<span class=\"email-brand\" style=\"font-weight:800;font-size:19px;color:${EMAIL_THEME.ink};letter-spacing:-0.03em;\">${EMAIL_THEME.brand}</span>\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-content\" style=\"padding:20px 32px 8px 32px;color:${EMAIL_THEME.ink};\">\n${LAYOUT_CONTENT_SLOT}\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-footer\" style=\"padding:16px 32px 28px 32px;color:${EMAIL_THEME.muted};font-size:13px;\">\n\t\t\t\t\t\t\t\t\tYou're receiving this because someone used this address at ${EMAIL_THEME.brand}. If that wasn't you, you can ignore it.\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t</table>\n\t\t\t</td>\n\t\t</tr>\n\t</table>\n</body>\n</html>`;\n\n// Inject a body fragment into a layout shell. If the body is already a full\n// HTML document (a founder chose to store a complete template), it is returned\n// untouched — never double-wrapped.\nexport function wrapLayout(bodyHtml: string, layoutHtml: string = DEFAULT_EMAIL_LAYOUT): string {\n\tconst trimmed = bodyHtml.trimStart().toLowerCase();\n\tif (trimmed.startsWith('<!doctype') || trimmed.startsWith('<html')) {\n\t\treturn bodyHtml;\n\t}\n\treturn layoutHtml.replace(LAYOUT_CONTENT_SLOT, bodyHtml);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ITemplateResolver, IRenderedTemplate, IDefaultTemplate, DefaultTemplateMap } from '../types';\nimport { wrapLayout } from './layout';\n\n// The stored template id for a founder-supplied layout shell (DB row `type` or\n// FS file `_layout.html`). Absent → the built-in DEFAULT_EMAIL_LAYOUT is used.\nconst LAYOUT_TYPE = '_layout';\n\nfunction render(template: string, data: Record<string, unknown>): string {\n\treturn template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) => {\n\t\tconst value = data[key];\n\t\treturn value !== undefined && value !== null ? String(value) : '';\n\t});\n}\n\n// Compose a body fragment into its layout shell, then interpolate variables\n// over the whole. `subject`/`preheader` become available to the shell's title\n// and inbox preview text.\nfunction composeHtml(\n\tbodyHtml: string,\n\tlayoutHtml: string | undefined,\n\tsubject: string | undefined,\n\tdata: Record<string, unknown>,\n): string {\n\tconst wrapped = wrapLayout(bodyHtml, layoutHtml);\n\treturn render(wrapped, { subject: subject ?? '', preheader: '', ...data });\n}\n\n// Render a resolved fragment — a DB row, an FS file set, or a MODULE DEFAULT —\n// into the final template. Factored out so a module default renders\n// byte-identically to a DB row: same {{var}} interpolation, same layout\n// composition. `text` is required; `subject`/`html` are optional (email only).\nexport function renderFragment(\n\tfrag: { subject?: string | null; text: string; html?: string | null },\n\tlayoutHtml: string | undefined,\n\tdata: Record<string, unknown>,\n): IRenderedTemplate {\n\tconst subject = frag.subject ? render(frag.subject, data) : undefined;\n\treturn {\n\t\ttext: render(frag.text, data),\n\t\t...(subject ? { subject } : {}),\n\t\t...(frag.html ? { html: composeHtml(frag.html, layoutHtml, subject, data) } : {}),\n\t};\n}\n\n// The module-shipped default templates an app hands courier, merged into one\n// lookup (aggregated like getMigrationsPath()). Per-key app overrides — a DB row\n// or an FS file — always win over a default; the default wins over the\n// last-resort JSON dump. A later map wins on key collision.\nexport class DefaultTemplates {\n\tprivate readonly map = new Map<string, IDefaultTemplate>();\n\tconstructor(maps: DefaultTemplateMap[] = []) {\n\t\tfor (const m of maps) {\n\t\t\tfor (const [key, tmpl] of Object.entries(m)) this.map.set(key, tmpl);\n\t\t}\n\t}\n\tget(type: string): IDefaultTemplate | undefined {\n\t\treturn this.map.get(type);\n\t}\n\tget size(): number {\n\t\treturn this.map.size;\n\t}\n}\n\n// DB-backed resolver — reads from fonderie_courier_templates with locale fallback\nexport class DBTemplateResolver implements ITemplateResolver {\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate defaults?: DefaultTemplates,\n\t) {}\n\n\tasync resolve(\n\t\ttype: string,\n\t\tdata: Record<string, unknown>,\n\t\tlocale?: string,\n\t): Promise<IRenderedTemplate> {\n\t\tconst [row] = await this.store.query<{\n\t\t\tsubject: string | null;\n\t\t\thtml: string | null;\n\t\t\ttext: string;\n\t\t}>(\n\t\t\t// Serve the exact locale, else the neutral NULL default — never a\n\t\t\t// sibling region (en-CA must not fall back to en-US); the WHERE\n\t\t\t// excludes other locales so legal/jurisdictional copy can't bleed.\n\t\t\t`SELECT subject, html, text\n\t\t\t FROM fonderie_courier_templates\n\t\t\t WHERE type = $1 AND active = true AND (locale = $2 OR locale IS NULL)\n\t\t\t ORDER BY (locale IS NOT DISTINCT FROM $2) DESC\n\t\t\t LIMIT 1`,\n\t\t\t[type, locale ?? null],\n\t\t);\n\n\t\tif (!row) {\n\t\t\t// No app row → fall back to the module default (rendered identically),\n\t\t\t// then to the JSON dump — now only reached for a type neither the app\n\t\t\t// nor any module provides.\n\t\t\tconst def = this.defaults?.get(type);\n\t\t\tif (def) {\n\t\t\t\treturn renderFragment(def, def.html ? await this.layout(locale) : undefined, data);\n\t\t\t}\n\t\t\treturn { text: `${type}: ${JSON.stringify(data)}` };\n\t\t}\n\n\t\tconst layoutHtml = row.html ? await this.layout(locale) : undefined;\n\t\treturn renderFragment(row, layoutHtml, data);\n\t}\n\n\t// Optional founder-supplied layout shell; undefined → built-in default.\n\tprivate async layout(locale?: string): Promise<string | undefined> {\n\t\tconst [row] = await this.store.query<{ html: string | null }>(\n\t\t\t`SELECT html\n\t\t\t FROM fonderie_courier_templates\n\t\t\t WHERE type = $1 AND active = true AND (locale = $2 OR locale IS NULL)\n\t\t\t ORDER BY (locale IS NOT DISTINCT FROM $2) DESC\n\t\t\t LIMIT 1`,\n\t\t\t[LAYOUT_TYPE, locale ?? null],\n\t\t);\n\t\treturn row?.html ?? undefined;\n\t}\n}\n\n// Filesystem resolver — reads {type}.{locale}.txt → {type}.txt with fallback\nexport class FSTemplateResolver implements ITemplateResolver {\n\tconstructor(\n\t\tprivate directory: string,\n\t\tprivate defaults?: DefaultTemplates,\n\t) {}\n\n\tasync resolve(\n\t\ttype: string,\n\t\tdata: Record<string, unknown>,\n\t\tlocale?: string,\n\t): Promise<IRenderedTemplate> {\n\t\tconst { readFile } = await import('node:fs/promises');\n\t\tconst { join } = await import('node:path');\n\n\t\tconst readOptional = async (path: string): Promise<string | null> => {\n\t\t\ttry {\n\t\t\t\treturn await readFile(path, 'utf8');\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t};\n\n\t\t// Per-locale variants take priority over generic variants\n\t\tconst localePrefix = locale ? `${type}.${locale}` : null;\n\t\tconst layoutPrefix = locale ? `${LAYOUT_TYPE}.${locale}` : null;\n\n\t\tconst [text, html, subject, layout] = await Promise.all([\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.txt`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.txt`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.txt`)),\n\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.html`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.html`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.html`)),\n\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.subject.txt`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.subject.txt`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.subject.txt`)),\n\n\t\t\tlayoutPrefix\n\t\t\t\t? readOptional(join(this.directory, `${layoutPrefix}.html`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${LAYOUT_TYPE}.html`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${LAYOUT_TYPE}.html`)),\n\t\t]);\n\n\t\t// App shipped NOTHING for this type (no text/html/subject file) → fall back\n\t\t// to the module default, using the app's own _layout shell if it ships one,\n\t\t// then to the JSON dump. Test file PRESENCE (=== null), not truthiness, so an\n\t\t// app that ships even an empty file keeps per-key control below — mirroring\n\t\t// the DB resolver's row-presence check, not overriding it with a default.\n\t\tif (text === null && html === null && subject === null) {\n\t\t\tconst def = this.defaults?.get(type);\n\t\t\tif (def) {\n\t\t\t\treturn renderFragment(def, def.html ? (layout ?? undefined) : undefined, data);\n\t\t\t}\n\t\t\treturn { text: `${type}: ${JSON.stringify(data)}` };\n\t\t}\n\n\t\tconst renderedSubject = subject ? render(subject, data) : undefined;\n\n\t\treturn {\n\t\t\ttext: text ? render(text, data) : `${type}: ${JSON.stringify(data)}`,\n\t\t\t...(renderedSubject ? { subject: renderedSubject } : {}),\n\t\t\t...(html ? { html: composeHtml(html, layout ?? undefined, renderedSubject, data) } : {}),\n\t\t};\n\t}\n}\n","import type { IReadinessProblem } from '@fonderie/core';\nimport type { ICourierConfig } from './config';\n\nconst MODULE = '@fonderie/courier';\n\n// Pure production-readiness assessment (no side effects) — shared by the\n// boot-time guard and `CourierModule.checkReadiness`. A message type routed to a\n// channel with no registered provider is silently dropped by the dispatcher —\n// with `requireVerification` on, verification / reset emails vanish and users\n// lock themselves out. These are `warning`-severity (channel setups vary\n// legitimately — an app may run in-app only — so this never blocks boot, unlike\n// auth's fatal weak-secret). Checks the channels actually **registered** on the\n// dispatcher, so channels added via `registerChannel` count as present.\nexport function collectCourierConfigProblems(\n\tconfig: ICourierConfig,\n\tregisteredChannels: Iterable<string>,\n): IReadinessProblem[] {\n\tconst registered = new Set(registeredChannels);\n\tconst gaps = new Map<string, string[]>(); // channel → message types routed to it\n\n\tfor (const [type, channels] of Object.entries(config.channels ?? {})) {\n\t\tfor (const channel of channels) {\n\t\t\tif (!registered.has(channel)) {\n\t\t\t\tconst types = gaps.get(channel) ?? [];\n\t\t\t\ttypes.push(type);\n\t\t\t\tgaps.set(channel, types);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn [...gaps].map(([channel, types]) => ({\n\t\tmodule: MODULE,\n\t\tseverity: 'warning' as const,\n\t\tmessage:\n\t\t\t`${types.length} message type(s) route to the '${channel}' channel but ` +\n\t\t\t`no '${channel}' provider is registered — these will be silently dropped: ` +\n\t\t\t`${types.join(', ')}. Configure \\`config.${channel}\\` (or register a channel).`,\n\t}));\n}\n\n// Boot-time preflight, run from `CourierModule.install`. Warn-only.\nexport function validateCourierConfig(\n\tconfig: ICourierConfig,\n\tregisteredChannels: Iterable<string>,\n): void {\n\tfor (const problem of collectCourierConfigProblems(config, registeredChannels)) {\n\t\tconsole.warn(`[courier] ${problem.message}`);\n\t}\n}\n","import { createHmac } from 'node:crypto';\n\nimport { constantTimeEqual } from '@fonderie/core';\n\nimport type { IStoreAdapter } from '@fonderie/store';\nimport {\n\tmarkMessageDelivered,\n\tmarkMessageOpened,\n\tmarkMessageClicked,\n\tmarkMessageBounced,\n} from './log';\n\n// ── SendGrid ──────────────────────────────────────────────────────\n//\n// Verifies the X-Twilio-Email-Event-Webhook-Signature header.\n// Expects an array of event objects in the request body.\n// https://docs.sendgrid.com/for-developers/tracking-events/getting-started-event-webhook-security-features\n\nexport async function handleSendGridDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n\twebhookSecret?: string,\n): Promise<Response> {\n\tif (webhookSecret) {\n\t\tconst sig = req.headers.get('x-twilio-email-event-webhook-signature') ?? '';\n\t\tconst ts = req.headers.get('x-twilio-email-event-webhook-timestamp') ?? '';\n\t\tconst body = await req.text();\n\n\t\tif (!verifySendGridSignature(webhookSecret, ts, body, sig)) {\n\t\t\treturn Response.json({ error: 'INVALID_SIGNATURE' }, { status: 401 });\n\t\t}\n\n\t\tconst events = parseJson(body) as SendGridEvent[] | null;\n\t\tif (!Array.isArray(events)) return Response.json({ ok: true });\n\n\t\tawait processSendGridEvents(events, store);\n\t\treturn Response.json({ ok: true });\n\t}\n\n\tconst events = (await req.json()) as SendGridEvent[];\n\tif (!Array.isArray(events)) return Response.json({ ok: true });\n\n\tawait processSendGridEvents(events, store);\n\treturn Response.json({ ok: true });\n}\n\nfunction verifySendGridSignature(\n\tsecret: string,\n\ttimestamp: string,\n\tbody: string,\n\tsignature: string,\n): boolean {\n\ttry {\n\t\tconst payload = timestamp + body;\n\t\tconst expected = createHmac('sha256', secret).update(payload).digest('base64');\n\t\treturn constantTimeEqual(Buffer.from(signature, 'base64'), Buffer.from(expected, 'base64'));\n\t} catch {\n\t\treturn false;\n\t}\n}\n\ninterface SendGridEvent {\n\tevent: string;\n\tsg_message_id?: string;\n\treason?: string;\n\t[key: string]: unknown;\n}\n\nasync function processSendGridEvents(\n\tevents: SendGridEvent[],\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tfor (const ev of events) {\n\t\tconst msgId = ev['sg_message_id'];\n\t\tif (!msgId || typeof msgId !== 'string') continue;\n\t\t// sg_message_id is \"msgId.filterXxx\" — strip the filter suffix\n\t\tconst id = msgId.split('.')[0] ?? msgId;\n\n\t\tswitch (ev.event) {\n\t\t\tcase 'delivered': await markMessageDelivered(id, store); break;\n\t\t\tcase 'open': await markMessageOpened(id, store); break;\n\t\t\tcase 'click': await markMessageClicked(id, store); break;\n\t\t\tcase 'bounce':\n\t\t\tcase 'blocked':\n\t\t\tcase 'dropped':\n\t\t\t\tawait markMessageBounced(id, typeof ev['reason'] === 'string' ? ev['reason'] : ev.event, store);\n\t\t\t\tbreak;\n\t\t}\n\t}\n}\n\n// ── Mailgun ───────────────────────────────────────────────────────\n//\n// Verifies signature using token + timestamp + signing key (HMAC-SHA256).\n// https://documentation.mailgun.com/docs/mailgun/user-manual/tracking-messages/#securing-webhooks\n\nexport async function handleMailgunDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n\tsigningKey?: string,\n): Promise<Response> {\n\tconst body = (await req.json()) as MailgunPayload;\n\n\tif (signingKey) {\n\t\tconst { signature } = body;\n\t\tif (!signature || !verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {\n\t\t\treturn Response.json({ error: 'INVALID_SIGNATURE' }, { status: 401 });\n\t\t}\n\t}\n\n\tconst event = body['event-data'];\n\tif (event) {\n\t\tawait processMailgunEvent(event, store);\n\t}\n\n\treturn Response.json({ ok: true });\n}\n\nfunction verifyMailgunSignature(\n\tsigningKey: string,\n\ttimestamp: string,\n\ttoken: string,\n\tsignature: string,\n): boolean {\n\ttry {\n\t\tconst value = timestamp + token;\n\t\tconst expected = createHmac('sha256', signingKey).update(value).digest('hex');\n\t\treturn constantTimeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'));\n\t} catch {\n\t\treturn false;\n\t}\n}\n\ninterface MailgunPayload {\n\tsignature?: { timestamp: string; token: string; signature: string };\n\t'event-data'?: MailgunEvent;\n\t[key: string]: unknown;\n}\n\ninterface MailgunEvent {\n\tevent: string;\n\tmessage?: { headers?: { 'message-id'?: string } };\n\t'delivery-status'?: { message?: string };\n\t[key: string]: unknown;\n}\n\nasync function processMailgunEvent(event: MailgunEvent, store: IStoreAdapter): Promise<void> {\n\tconst msgId = event.message?.headers?.['message-id'];\n\tif (!msgId) return;\n\n\tswitch (event.event) {\n\t\tcase 'delivered': await markMessageDelivered(msgId, store); break;\n\t\tcase 'opened': await markMessageOpened(msgId, store); break;\n\t\tcase 'clicked': await markMessageClicked(msgId, store); break;\n\t\tcase 'failed':\n\t\tcase 'bounced': {\n\t\t\tconst reason = event['delivery-status']?.message ?? event.event;\n\t\t\tawait markMessageBounced(msgId, reason, store);\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n// ── Mailtrap (testing only, no signature) ────────────────────────\n\nexport async function handleMailtrapDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n): Promise<Response> {\n\tconst events = (await req.json()) as MailtrapEvent[];\n\tif (!Array.isArray(events)) return Response.json({ ok: true });\n\n\tfor (const ev of events) {\n\t\tconst msgId = ev.message_id;\n\t\tif (!msgId) continue;\n\n\t\tswitch (ev.event) {\n\t\t\tcase 'delivery': await markMessageDelivered(msgId, store); break;\n\t\t\tcase 'open': await markMessageOpened(msgId, store); break;\n\t\t\tcase 'click': await markMessageClicked(msgId, store); break;\n\t\t\tcase 'bounce':\n\t\t\tcase 'soft_bounce':\n\t\t\t\tawait markMessageBounced(msgId, ev.event, store);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn Response.json({ ok: true });\n}\n\ninterface MailtrapEvent {\n\tevent: string;\n\tmessage_id?: string;\n\t[key: string]: unknown;\n}\n\n// ── Utility ───────────────────────────────────────────────────────\n\nfunction parseJson(text: string): unknown {\n\ttry { return JSON.parse(text); } catch { return null; }\n}\n","import type { IFonderieContext, Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport { requireAdminToken } from '@fonderie/core/middlewares';\nimport { VersionConflictError } from '@fonderie/store';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport {\n\tsetTemplate,\n\trollbackTemplate,\n\tlistTemplateRevisions,\n\tgetTemplateEntry,\n\tlistTemplateEntries,\n\tdeleteTemplate,\n} from './admin';\n\n// Bearer-token guard (mirrors @fonderie/config's admin surface). Only registered\n// when a token is configured — no token, no exposed template admin routes.\n// Wrap a handler in the shared admin-token guard (@fonderie/core/middlewares) —\n// one constant-time Bearer check across billing/config/courier.\nfunction guarded(adminToken: string, handler: Middleware): Middleware {\n\tconst guard = requireAdminToken(adminToken);\n\treturn (ctx, next) => guard(ctx, () => handler(ctx, next));\n}\n\nconst actorOf = (ctx: IFonderieContext) => ctx.request.headers.get('x-actor') || 'admin-token';\nconst typeOf = (ctx: IFonderieContext) => ctx.meta.params?.['type'] ?? '';\nconst localeOf = (ctx: IFonderieContext): string | null =>\n\tnew URL(ctx.request.url).searchParams.get('locale');\nconst body = (ctx: IFonderieContext): Record<string, unknown> =>\n\t(ctx.meta['body'] as Record<string, unknown> | undefined) ?? {};\n\nfunction conflictOr(err: unknown): Response {\n\tif (err instanceof VersionConflictError) {\n\t\treturn setApiResponse(HTTP.CONFLICT, 'VERSION_CONFLICT', err.message, {\n\t\t\tcurrentVersion: err.currentVersion,\n\t\t});\n\t}\n\tthrow err;\n}\n\n// Build the template admin route table (registered by CourierModule.install when\n// an adminToken is configured).\nexport function buildTemplateAdminRoutes(\n\tstore: IStoreAdapter,\n\tadminToken: string,\n): Array<[string, string, Middleware]> {\n\tconst g = (h: Middleware): Middleware => guarded(adminToken, h);\n\n\treturn [\n\t\t['GET', '/admin/templates', g(async () => {\n\t\t\treturn setApiResponse(HTTP.OK, 'TEMPLATES_LISTED', 'Templates', await listTemplateEntries(store));\n\t\t})],\n\t\t['GET', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst row = await getTemplateEntry(typeOf(ctx), localeOf(ctx), store);\n\t\t\treturn row\n\t\t\t\t? setApiResponse(HTTP.OK, 'TEMPLATE', 'Template', row)\n\t\t\t\t: setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such template');\n\t\t})],\n\t\t['PUT', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tif (typeof b['text'] !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.text (string) is required');\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst opts: Parameters<typeof setTemplate>[0] = {\n\t\t\t\t\ttype: typeOf(ctx),\n\t\t\t\t\ttext: b['text'],\n\t\t\t\t\tlocale: localeOf(ctx),\n\t\t\t\t\tactor: actorOf(ctx),\n\t\t\t\t};\n\t\t\t\tif (typeof b['subject'] === 'string') opts.subject = b['subject'];\n\t\t\t\tif (typeof b['html'] === 'string') opts.html = b['html'];\n\t\t\t\tif (typeof b['active'] === 'boolean') opts.active = b['active'];\n\t\t\t\tif (typeof b['ifVersion'] === 'number') opts.ifVersion = b['ifVersion'];\n\t\t\t\treturn setApiResponse(HTTP.OK, 'TEMPLATE_SET', 'Template saved', await setTemplate(opts, store));\n\t\t\t} catch (err) {\n\t\t\t\treturn conflictOr(err);\n\t\t\t}\n\t\t})],\n\t\t['DELETE', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst ok = await deleteTemplate(typeOf(ctx), localeOf(ctx), store);\n\t\t\treturn setApiResponse(ok ? HTTP.OK : HTTP.NOT_FOUND, ok ? 'DELETED' : 'NOT_FOUND', ok ? 'Deleted' : 'No such template');\n\t\t})],\n\t\t['GET', '/admin/templates/:type/revisions', g(async (ctx) => {\n\t\t\treturn setApiResponse(HTTP.OK, 'REVISIONS', 'Template revisions', await listTemplateRevisions(typeOf(ctx), localeOf(ctx), store));\n\t\t})],\n\t\t['POST', '/admin/templates/:type/rollback', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tconst toVersion = Number(b['toVersion']);\n\t\t\tif (!Number.isInteger(toVersion)) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.toVersion (int) is required');\n\t\t\t}\n\t\t\tconst row = await rollbackTemplate(\n\t\t\t\t{ type: typeOf(ctx), locale: localeOf(ctx), toVersion, actor: actorOf(ctx) },\n\t\t\t\tstore,\n\t\t\t);\n\t\t\treturn setApiResponse(HTTP.OK, 'ROLLED_BACK', `Rolled back to v${toVersion}`, row);\n\t\t})],\n\t];\n}\n","import type { IStoreAdapter, IVersionedResource } from '@fonderie/store';\nimport { versionedWrite, versionedRollback } from '@fonderie/store';\n\n// Versioned management for email templates, on the shared `@fonderie/store`\n// primitive. Templates are keyed by (type, locale) — a NULL locale is the base,\n// with per-locale overrides on top — and their content is subject/html/text.\n// The resolver reads the current row unchanged; this adds edit + history +\n// rollback + optimistic concurrency on top.\n\nexport interface ITemplateEntry {\n\ttype: string;\n\tlocale: string | null;\n\tsubject: string | null;\n\thtml: string | null;\n\ttext: string;\n\tactive: boolean;\n\tversion: number;\n\tupdatedBy: string | null;\n\tupdatedAt: string;\n}\n\nexport interface ITemplateRevision {\n\ttype: string;\n\tlocale: string | null;\n\tsubject: string | null;\n\thtml: string | null;\n\ttext: string;\n\tversion: number;\n\tactor: string | null;\n\tcreatedAt: string;\n}\n\nconst ENTRY_COLS = `type, locale, subject, html, text, active, version, updated_by AS \"updatedBy\", updated_at AS \"updatedAt\"`;\n\nconst TEMPLATE_RESOURCE: IVersionedResource = {\n\ttable: 'fonderie_courier_templates',\n\trevisions: 'fonderie_courier_template_revisions',\n\tchannel: 'fonderie_courier_templates_changed',\n\tkeyColumns: ['type', 'locale'],\n\tcontentColumns: ['subject', 'html', 'text'],\n\tmetaColumns: ['active'],\n\treturning: ENTRY_COLS,\n};\n\n// Upsert a template (full body — a revision is a full snapshot). Pass `ifVersion`\n// for optimistic concurrency (else `VersionConflictError`). `locale` omitted /\n// null targets the base template.\nexport async function setTemplate(\n\topts: {\n\t\ttype: string;\n\t\ttext: string;\n\t\tlocale?: string | null;\n\t\tsubject?: string | null;\n\t\thtml?: string | null;\n\t\tactive?: boolean;\n\t\tifVersion?: number;\n\t\tactor?: string;\n\t},\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry> {\n\tconst data: Record<string, unknown> = {\n\t\tsubject: opts.subject ?? null,\n\t\thtml: opts.html ?? null,\n\t\ttext: opts.text,\n\t};\n\tif (opts.active !== undefined) data['active'] = opts.active;\n\treturn versionedWrite<ITemplateEntry>(TEMPLATE_RESOURCE, store, {\n\t\tkey: opts.type,\n\t\tscope: opts.locale ?? null,\n\t\tdata,\n\t\t...(opts.ifVersion !== undefined ? { ifVersion: opts.ifVersion } : {}),\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function rollbackTemplate(\n\topts: { type: string; locale?: string | null; toVersion: number; actor?: string },\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry> {\n\treturn versionedRollback<ITemplateEntry>(TEMPLATE_RESOURCE, store, {\n\t\tkey: opts.type,\n\t\tscope: opts.locale ?? null,\n\t\ttoVersion: opts.toVersion,\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function listTemplateRevisions(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<ITemplateRevision[]> {\n\treturn store.query<ITemplateRevision>(\n\t\t`SELECT type, locale, subject, html, text, version, actor, created_at AS \"createdAt\"\n\t\t FROM fonderie_courier_template_revisions\n\t\t WHERE type = $1 AND locale IS NOT DISTINCT FROM $2\n\t\t ORDER BY version DESC`,\n\t\t[type, locale],\n\t);\n}\n\nexport async function getTemplateEntry(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry | null> {\n\tconst [row] = await store.query<ITemplateEntry>(\n\t\t`SELECT ${ENTRY_COLS} FROM fonderie_courier_templates WHERE type = $1 AND locale IS NOT DISTINCT FROM $2`,\n\t\t[type, locale],\n\t);\n\treturn row ?? null;\n}\n\nexport async function listTemplateEntries(store: IStoreAdapter): Promise<ITemplateEntry[]> {\n\treturn store.query<ITemplateEntry>(\n\t\t`SELECT ${ENTRY_COLS} FROM fonderie_courier_templates ORDER BY type, locale NULLS FIRST`,\n\t);\n}\n\nexport async function deleteTemplate(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst rows = await store.query<{ type: string }>(\n\t\t`DELETE FROM fonderie_courier_templates WHERE type = $1 AND locale IS NOT DISTINCT FROM $2 RETURNING type`,\n\t\t[type, locale],\n\t);\n\treturn rows.length > 0;\n}\n","import type { DefaultTemplateMap } from './types';\n\nexport interface IEmailChannelConfig {\n\tprovider: 'resend' | 'ses' | 'smtp';\n\tfrom: string;\n\tapiKey?: string;\n\tsmtp?: {\n\t\thost: string;\n\t\tport: number;\n\t\tsecure: boolean;\n\t\tuser: string;\n\t\tpass: string;\n\t};\n}\n\nexport interface ISmsChannelConfig {\n\tprovider: 'twilio' | 'vonage';\n\tfrom: string;\n\taccountSid?: string; // twilio\n\tauthToken?: string; // twilio\n\tapiKey?: string; // vonage\n\tapiSecret?: string; // vonage\n}\n\nexport interface IPushChannelConfig {\n\tprovider: 'fcm';\n\tserviceAccount: Record<string, unknown>;\n}\n\nexport const Channel = {\n\tEMAIL: 'email',\n\tSMS: 'sms',\n\tPUSH: 'push',\n} as const satisfies Record<string, 'email' | 'sms' | 'push'>;\n\nexport interface ICourierConfig {\n\t// Which channels handle which message types\n\t// e.g. { 'password-reset': ['email'], 'new-message': ['push', 'sms'] }\n\tchannels: Record<string, Array<'email' | 'sms' | 'push'>>;\n\n\tsms?: ISmsChannelConfig;\n\tpush?: IPushChannelConfig;\n\temail?: IEmailChannelConfig;\n\n\t// When set, exposes Bearer-guarded template admin routes (/admin/templates/*)\n\t// for versioned edit/history/rollback. Requires @fonderie/store (db templates).\n\tadminToken?: string;\n\n\t// Where templates are loaded from\n\t// 'db' requires @fonderie/store to be configured\n\t// 'fs' reads from a local directory\n\ttemplates?: {\n\t\tsource: 'db' | 'fs';\n\t\tdirectory?: string; // for 'fs' source\n\t\t// Module-shipped default templates. The app imports each module's\n\t\t// DEFAULT_TEMPLATES (auth, workspaces, billing, …) and passes them here;\n\t\t// the resolver falls back to a default when no app override (DB row / FS\n\t\t// file) exists, before the last-resort JSON dump. Aggregated like\n\t\t// getMigrationsPath() — pass one map or an array of them.\n\t\tdefaults?: DefaultTemplateMap | DefaultTemplateMap[];\n\t};\n\n\t// Signing keys for verifying inbound delivery webhook payloads\n\tdelivery?: {\n\t\tsigningKeys?: {\n\t\t\tsendgrid?: string; // HMAC-SHA256 key from SendGrid dashboard\n\t\t\tmailgun?: string; // Mailgun HTTP webhook signing key\n\t\t};\n\t};\n}\n"],"mappings":"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","names":["body","body","nodemailer","body","import_middlewares","body","events","import_core","import_store"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { IFonderieModule, IReadinessProblem, IFonderieApp, Middleware } from '@fonderie/core';
|
|
2
2
|
import { IStoreAdapter } from '@fonderie/store';
|
|
3
3
|
import { EventBus } from '@fonderie/events';
|
|
4
|
-
import { ITemplateResolver, ICourierChannel, IRenderedTemplate } from './types.cjs';
|
|
4
|
+
import { DefaultTemplateMap, ITemplateResolver, ICourierChannel, IRenderedTemplate } from './types.cjs';
|
|
5
5
|
export { IMessageLog, MessageLogStatus } from './types.cjs';
|
|
6
|
-
import { ICourierMessage } from '@fonderie/core/types';
|
|
7
|
-
export { ICourierMessage } from '@fonderie/core/types';
|
|
6
|
+
import { ICourierMessage, IDefaultTemplate } from '@fonderie/core/types';
|
|
7
|
+
export { ICourierMessage, IDefaultTemplate } from '@fonderie/core/types';
|
|
8
8
|
|
|
9
9
|
interface IEmailChannelConfig {
|
|
10
10
|
provider: 'resend' | 'ses' | 'smtp';
|
|
@@ -44,6 +44,7 @@ interface ICourierConfig {
|
|
|
44
44
|
templates?: {
|
|
45
45
|
source: 'db' | 'fs';
|
|
46
46
|
directory?: string;
|
|
47
|
+
defaults?: DefaultTemplateMap | DefaultTemplateMap[];
|
|
47
48
|
};
|
|
48
49
|
delivery?: {
|
|
49
50
|
signingKeys?: {
|
|
@@ -108,15 +109,28 @@ declare class EmailChannel implements ICourierChannel {
|
|
|
108
109
|
private sendViaSMTP;
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
declare function renderFragment(frag: {
|
|
113
|
+
subject?: string | null;
|
|
114
|
+
text: string;
|
|
115
|
+
html?: string | null;
|
|
116
|
+
}, layoutHtml: string | undefined, data: Record<string, unknown>): IRenderedTemplate;
|
|
117
|
+
declare class DefaultTemplates {
|
|
118
|
+
private readonly map;
|
|
119
|
+
constructor(maps?: DefaultTemplateMap[]);
|
|
120
|
+
get(type: string): IDefaultTemplate | undefined;
|
|
121
|
+
get size(): number;
|
|
122
|
+
}
|
|
111
123
|
declare class DBTemplateResolver implements ITemplateResolver {
|
|
112
124
|
private store;
|
|
113
|
-
|
|
125
|
+
private defaults?;
|
|
126
|
+
constructor(store: IStoreAdapter, defaults?: DefaultTemplates | undefined);
|
|
114
127
|
resolve(type: string, data: Record<string, unknown>, locale?: string): Promise<IRenderedTemplate>;
|
|
115
128
|
private layout;
|
|
116
129
|
}
|
|
117
130
|
declare class FSTemplateResolver implements ITemplateResolver {
|
|
118
131
|
private directory;
|
|
119
|
-
|
|
132
|
+
private defaults?;
|
|
133
|
+
constructor(directory: string, defaults?: DefaultTemplates | undefined);
|
|
120
134
|
resolve(type: string, data: Record<string, unknown>, locale?: string): Promise<IRenderedTemplate>;
|
|
121
135
|
}
|
|
122
136
|
|
|
@@ -164,4 +178,4 @@ declare function deleteTemplate(type: string, locale: string | null, store: ISto
|
|
|
164
178
|
|
|
165
179
|
declare function buildTemplateAdminRoutes(store: IStoreAdapter, adminToken: string): Array<[string, string, Middleware]>;
|
|
166
180
|
|
|
167
|
-
export { Channel, CourierModule, DBTemplateResolver, Dispatcher, EmailChannel, FSTemplateResolver, ICourierChannel, type ICourierConfig, type IEmailChannelConfig, type IPushChannelConfig, IRenderedTemplate, type ISmsChannelConfig, type ITemplateEntry, ITemplateResolver, type ITemplateRevision, PushChannel, SmsChannel, buildTemplateAdminRoutes, deleteTemplate, getTemplateEntry, handleMailgunDelivery, handleMailtrapDelivery, handleSendGridDelivery, listTemplateEntries, listTemplateRevisions, rollbackTemplate, setTemplate, validateCourierConfig };
|
|
181
|
+
export { Channel, CourierModule, DBTemplateResolver, DefaultTemplateMap, DefaultTemplates, Dispatcher, EmailChannel, FSTemplateResolver, ICourierChannel, type ICourierConfig, type IEmailChannelConfig, type IPushChannelConfig, IRenderedTemplate, type ISmsChannelConfig, type ITemplateEntry, ITemplateResolver, type ITemplateRevision, PushChannel, SmsChannel, buildTemplateAdminRoutes, deleteTemplate, getTemplateEntry, handleMailgunDelivery, handleMailtrapDelivery, handleSendGridDelivery, listTemplateEntries, listTemplateRevisions, renderFragment, rollbackTemplate, setTemplate, validateCourierConfig };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { IFonderieModule, IReadinessProblem, IFonderieApp, Middleware } from '@fonderie/core';
|
|
2
2
|
import { IStoreAdapter } from '@fonderie/store';
|
|
3
3
|
import { EventBus } from '@fonderie/events';
|
|
4
|
-
import { ITemplateResolver, ICourierChannel, IRenderedTemplate } from './types.js';
|
|
4
|
+
import { DefaultTemplateMap, ITemplateResolver, ICourierChannel, IRenderedTemplate } from './types.js';
|
|
5
5
|
export { IMessageLog, MessageLogStatus } from './types.js';
|
|
6
|
-
import { ICourierMessage } from '@fonderie/core/types';
|
|
7
|
-
export { ICourierMessage } from '@fonderie/core/types';
|
|
6
|
+
import { ICourierMessage, IDefaultTemplate } from '@fonderie/core/types';
|
|
7
|
+
export { ICourierMessage, IDefaultTemplate } from '@fonderie/core/types';
|
|
8
8
|
|
|
9
9
|
interface IEmailChannelConfig {
|
|
10
10
|
provider: 'resend' | 'ses' | 'smtp';
|
|
@@ -44,6 +44,7 @@ interface ICourierConfig {
|
|
|
44
44
|
templates?: {
|
|
45
45
|
source: 'db' | 'fs';
|
|
46
46
|
directory?: string;
|
|
47
|
+
defaults?: DefaultTemplateMap | DefaultTemplateMap[];
|
|
47
48
|
};
|
|
48
49
|
delivery?: {
|
|
49
50
|
signingKeys?: {
|
|
@@ -108,15 +109,28 @@ declare class EmailChannel implements ICourierChannel {
|
|
|
108
109
|
private sendViaSMTP;
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
declare function renderFragment(frag: {
|
|
113
|
+
subject?: string | null;
|
|
114
|
+
text: string;
|
|
115
|
+
html?: string | null;
|
|
116
|
+
}, layoutHtml: string | undefined, data: Record<string, unknown>): IRenderedTemplate;
|
|
117
|
+
declare class DefaultTemplates {
|
|
118
|
+
private readonly map;
|
|
119
|
+
constructor(maps?: DefaultTemplateMap[]);
|
|
120
|
+
get(type: string): IDefaultTemplate | undefined;
|
|
121
|
+
get size(): number;
|
|
122
|
+
}
|
|
111
123
|
declare class DBTemplateResolver implements ITemplateResolver {
|
|
112
124
|
private store;
|
|
113
|
-
|
|
125
|
+
private defaults?;
|
|
126
|
+
constructor(store: IStoreAdapter, defaults?: DefaultTemplates | undefined);
|
|
114
127
|
resolve(type: string, data: Record<string, unknown>, locale?: string): Promise<IRenderedTemplate>;
|
|
115
128
|
private layout;
|
|
116
129
|
}
|
|
117
130
|
declare class FSTemplateResolver implements ITemplateResolver {
|
|
118
131
|
private directory;
|
|
119
|
-
|
|
132
|
+
private defaults?;
|
|
133
|
+
constructor(directory: string, defaults?: DefaultTemplates | undefined);
|
|
120
134
|
resolve(type: string, data: Record<string, unknown>, locale?: string): Promise<IRenderedTemplate>;
|
|
121
135
|
}
|
|
122
136
|
|
|
@@ -164,4 +178,4 @@ declare function deleteTemplate(type: string, locale: string | null, store: ISto
|
|
|
164
178
|
|
|
165
179
|
declare function buildTemplateAdminRoutes(store: IStoreAdapter, adminToken: string): Array<[string, string, Middleware]>;
|
|
166
180
|
|
|
167
|
-
export { Channel, CourierModule, DBTemplateResolver, Dispatcher, EmailChannel, FSTemplateResolver, ICourierChannel, type ICourierConfig, type IEmailChannelConfig, type IPushChannelConfig, IRenderedTemplate, type ISmsChannelConfig, type ITemplateEntry, ITemplateResolver, type ITemplateRevision, PushChannel, SmsChannel, buildTemplateAdminRoutes, deleteTemplate, getTemplateEntry, handleMailgunDelivery, handleMailtrapDelivery, handleSendGridDelivery, listTemplateEntries, listTemplateRevisions, rollbackTemplate, setTemplate, validateCourierConfig };
|
|
181
|
+
export { Channel, CourierModule, DBTemplateResolver, DefaultTemplateMap, DefaultTemplates, Dispatcher, EmailChannel, FSTemplateResolver, ICourierChannel, type ICourierConfig, type IEmailChannelConfig, type IPushChannelConfig, IRenderedTemplate, type ISmsChannelConfig, type ITemplateEntry, ITemplateResolver, type ITemplateRevision, PushChannel, SmsChannel, buildTemplateAdminRoutes, deleteTemplate, getTemplateEntry, handleMailgunDelivery, handleMailtrapDelivery, handleSendGridDelivery, listTemplateEntries, listTemplateRevisions, renderFragment, rollbackTemplate, setTemplate, validateCourierConfig };
|
package/dist/index.js
CHANGED
|
@@ -449,11 +449,35 @@ function composeHtml(bodyHtml, layoutHtml, subject, data) {
|
|
|
449
449
|
const wrapped = wrapLayout(bodyHtml, layoutHtml);
|
|
450
450
|
return render(wrapped, { subject: subject ?? "", preheader: "", ...data });
|
|
451
451
|
}
|
|
452
|
+
function renderFragment(frag, layoutHtml, data) {
|
|
453
|
+
const subject = frag.subject ? render(frag.subject, data) : void 0;
|
|
454
|
+
return {
|
|
455
|
+
text: render(frag.text, data),
|
|
456
|
+
...subject ? { subject } : {},
|
|
457
|
+
...frag.html ? { html: composeHtml(frag.html, layoutHtml, subject, data) } : {}
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
var DefaultTemplates = class {
|
|
461
|
+
map = /* @__PURE__ */ new Map();
|
|
462
|
+
constructor(maps = []) {
|
|
463
|
+
for (const m of maps) {
|
|
464
|
+
for (const [key, tmpl] of Object.entries(m)) this.map.set(key, tmpl);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
get(type) {
|
|
468
|
+
return this.map.get(type);
|
|
469
|
+
}
|
|
470
|
+
get size() {
|
|
471
|
+
return this.map.size;
|
|
472
|
+
}
|
|
473
|
+
};
|
|
452
474
|
var DBTemplateResolver = class {
|
|
453
|
-
constructor(store) {
|
|
475
|
+
constructor(store, defaults) {
|
|
454
476
|
this.store = store;
|
|
477
|
+
this.defaults = defaults;
|
|
455
478
|
}
|
|
456
479
|
store;
|
|
480
|
+
defaults;
|
|
457
481
|
async resolve(type, data, locale) {
|
|
458
482
|
const [row] = await this.store.query(
|
|
459
483
|
// Serve the exact locale, else the neutral NULL default — never a
|
|
@@ -467,15 +491,14 @@ var DBTemplateResolver = class {
|
|
|
467
491
|
[type, locale ?? null]
|
|
468
492
|
);
|
|
469
493
|
if (!row) {
|
|
494
|
+
const def = this.defaults?.get(type);
|
|
495
|
+
if (def) {
|
|
496
|
+
return renderFragment(def, def.html ? await this.layout(locale) : void 0, data);
|
|
497
|
+
}
|
|
470
498
|
return { text: `${type}: ${JSON.stringify(data)}` };
|
|
471
499
|
}
|
|
472
|
-
const subject = row.subject ? render(row.subject, data) : void 0;
|
|
473
500
|
const layoutHtml = row.html ? await this.layout(locale) : void 0;
|
|
474
|
-
return
|
|
475
|
-
text: render(row.text, data),
|
|
476
|
-
...subject ? { subject } : {},
|
|
477
|
-
...row.html ? { html: composeHtml(row.html, layoutHtml, subject, data) } : {}
|
|
478
|
-
};
|
|
501
|
+
return renderFragment(row, layoutHtml, data);
|
|
479
502
|
}
|
|
480
503
|
// Optional founder-supplied layout shell; undefined → built-in default.
|
|
481
504
|
async layout(locale) {
|
|
@@ -491,10 +514,12 @@ var DBTemplateResolver = class {
|
|
|
491
514
|
}
|
|
492
515
|
};
|
|
493
516
|
var FSTemplateResolver = class {
|
|
494
|
-
constructor(directory) {
|
|
517
|
+
constructor(directory, defaults) {
|
|
495
518
|
this.directory = directory;
|
|
519
|
+
this.defaults = defaults;
|
|
496
520
|
}
|
|
497
521
|
directory;
|
|
522
|
+
defaults;
|
|
498
523
|
async resolve(type, data, locale) {
|
|
499
524
|
const { readFile } = await import("fs/promises");
|
|
500
525
|
const { join } = await import("path");
|
|
@@ -521,6 +546,13 @@ var FSTemplateResolver = class {
|
|
|
521
546
|
(v) => v ?? readOptional(join(this.directory, `${LAYOUT_TYPE}.html`))
|
|
522
547
|
) : readOptional(join(this.directory, `${LAYOUT_TYPE}.html`))
|
|
523
548
|
]);
|
|
549
|
+
if (text === null && html === null && subject === null) {
|
|
550
|
+
const def = this.defaults?.get(type);
|
|
551
|
+
if (def) {
|
|
552
|
+
return renderFragment(def, def.html ? layout ?? void 0 : void 0, data);
|
|
553
|
+
}
|
|
554
|
+
return { text: `${type}: ${JSON.stringify(data)}` };
|
|
555
|
+
}
|
|
524
556
|
const renderedSubject = subject ? render(subject, data) : void 0;
|
|
525
557
|
return {
|
|
526
558
|
text: text ? render(text, data) : `${type}: ${JSON.stringify(data)}`,
|
|
@@ -887,13 +919,15 @@ var CourierModule = class {
|
|
|
887
919
|
}
|
|
888
920
|
};
|
|
889
921
|
function createTemplateResolver(source, config, store) {
|
|
922
|
+
const input = config.templates?.defaults;
|
|
923
|
+
const defaults = new DefaultTemplates(input ? Array.isArray(input) ? input : [input] : []);
|
|
890
924
|
if (source === "fs") {
|
|
891
|
-
return new FSTemplateResolver(config.templates?.directory ?? "./templates");
|
|
925
|
+
return new FSTemplateResolver(config.templates?.directory ?? "./templates", defaults);
|
|
892
926
|
}
|
|
893
927
|
if (!store) {
|
|
894
928
|
throw new Error("[courier] store is required for DB template resolution");
|
|
895
929
|
}
|
|
896
|
-
return new DBTemplateResolver(store);
|
|
930
|
+
return new DBTemplateResolver(store, defaults);
|
|
897
931
|
}
|
|
898
932
|
|
|
899
933
|
// src/config.ts
|
|
@@ -906,6 +940,7 @@ export {
|
|
|
906
940
|
Channel,
|
|
907
941
|
CourierModule,
|
|
908
942
|
DBTemplateResolver,
|
|
943
|
+
DefaultTemplates,
|
|
909
944
|
Dispatcher,
|
|
910
945
|
EmailChannel,
|
|
911
946
|
FSTemplateResolver,
|
|
@@ -919,6 +954,7 @@ export {
|
|
|
919
954
|
handleSendGridDelivery,
|
|
920
955
|
listTemplateEntries,
|
|
921
956
|
listTemplateRevisions,
|
|
957
|
+
renderFragment,
|
|
922
958
|
rollbackTemplate,
|
|
923
959
|
setTemplate,
|
|
924
960
|
validateCourierConfig
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/module.ts","../src/log.ts","../src/dispatcher.ts","../src/channels/sms.ts","../src/channels/push.ts","../src/channels/email.ts","../src/templates/layout.ts","../src/templates/resolver.ts","../src/config-guard.ts","../src/delivery.ts","../src/templates/admin-routes.ts","../src/templates/admin.ts","../src/config.ts"],"sourcesContent":["import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport type { EventBus } from '@fonderie/events';\nimport { NOTIFICATION_EVENT } from '@fonderie/events';\n\nimport type { ITemplateResolver, ICourierMessage } from './types';\nimport type { ICourierConfig } from './config';\n\nimport { Dispatcher } from './dispatcher';\nimport { SmsChannel } from './channels/sms';\nimport { PushChannel } from './channels/push';\nimport { EmailChannel } from './channels/email';\nimport type { IReadinessProblem } from '@fonderie/core';\nimport { validateAdminToken } from '@fonderie/core/middlewares';\nimport { DBTemplateResolver, FSTemplateResolver } from './templates/resolver';\nimport { validateCourierConfig, collectCourierConfigProblems } from './config-guard';\nimport { handleSendGridDelivery, handleMailgunDelivery, handleMailtrapDelivery } from './delivery';\nimport { buildTemplateAdminRoutes } from './templates/admin-routes';\n\nexport class CourierModule implements IFonderieModule {\n\treadonly name = '@fonderie/courier';\n\treadonly deps = ['@fonderie/events'];\n\treadonly dispatcher: Dispatcher;\n\n\tconstructor(\n\t\tprivate config: ICourierConfig,\n\t\tprivate store?: IStoreAdapter,\n\t\tbus?: EventBus,\n\t) {\n\t\tconst templateSource = config.templates?.source ?? 'db';\n\t\tconst resolver = createTemplateResolver(templateSource, config, store);\n\n\t\tthis.dispatcher = new Dispatcher(config, resolver, store);\n\n\t\tif (config.email) this.dispatcher.registerChannel(new EmailChannel(config.email));\n\t\tif (config.sms) this.dispatcher.registerChannel(new SmsChannel(config.sms));\n\t\tif (config.push) this.dispatcher.registerChannel(new PushChannel(config.push));\n\n\t\tbus?.on<ICourierMessage>(\n\t\t\tNOTIFICATION_EVENT,\n\t\t\tasync (msg) => {\n\t\t\t\tawait this.dispatcher.dispatch(msg);\n\t\t\t},\n\t\t\t'courier',\n\t\t);\n\t}\n\n\t// Report config problems for app.checkProductionReadiness() (data, not warn).\n\tcheckReadiness(): IReadinessProblem[] {\n\t\treturn [\n\t\t\t...collectCourierConfigProblems(this.config, this.dispatcher.channelNames()),\n\t\t\t// Shared admin-token strength rule (@fonderie/core/middlewares) — the\n\t\t\t// /admin/templates surface must not be guarded by a weak/placeholder token.\n\t\t\t...validateAdminToken(this.config.adminToken, { module: this.name }),\n\t\t];\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\t// Boot-time preflight: warn if any routed message type has no provider.\n\t\tvalidateCourierConfig(this.config, this.dispatcher.channelNames());\n\n\t\tconst store = this.store;\n\t\tconst signingKeys = this.config.delivery?.signingKeys;\n\n\t\tapp.addRoute('POST', '/courier/delivery/sendgrid', (ctx) =>\n\t\t\thandleSendGridDelivery(ctx.request, store!, signingKeys?.sendgrid),\n\t\t);\n\t\tapp.addRoute('POST', '/courier/delivery/mailgun', (ctx) =>\n\t\t\thandleMailgunDelivery(ctx.request, store!, signingKeys?.mailgun),\n\t\t);\n\t\tapp.addRoute('POST', '/courier/delivery/mailtrap', (ctx) =>\n\t\t\thandleMailtrapDelivery(ctx.request, store!),\n\t\t);\n\n\t\t// Versioned template admin routes — only when a token is configured and a\n\t\t// store is present (db templates). Mirrors @fonderie/config's admin surface.\n\t\tif (this.config.adminToken) {\n\t\t\tif (!store) {\n\t\t\t\tthrow new Error('[courier] adminToken requires @fonderie/store (db templates)');\n\t\t\t}\n\t\t\tfor (const [method, path, handler] of buildTemplateAdminRoutes(store, this.config.adminToken)) {\n\t\t\t\tapp.addRoute(method, path, handler);\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction createTemplateResolver(\n\tsource: 'db' | 'fs',\n\tconfig: ICourierConfig,\n\tstore?: IStoreAdapter,\n): ITemplateResolver {\n\tif (source === 'fs') {\n\t\treturn new FSTemplateResolver(config.templates?.directory ?? './templates');\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);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nexport type MessageLogStatus =\n\t| 'pending' | 'sent' | 'failed'\n\t| 'delivered' | 'opened' | 'clicked' | 'bounced' | 'spam';\n\nexport interface IMessageLog {\n\tid: string;\n\tmessageType: string;\n\tchannel: string;\n\trecipient: string;\n\tlocale: string | null;\n\tstatus: MessageLogStatus;\n\terror: string | null;\n\tattempts: number;\n\tprovider: string | null;\n\tproviderMessageId: string | null;\n\topenedAt: string | null;\n\tclickedAt: string | null;\n\tbouncedAt: string | null;\n\tbounceReason: string | null;\n\tcreatedAt: string;\n\tsentAt: string | null;\n}\n\nexport async function insertMessageLog(\n\tentry: {\n\t\tmessageType: string;\n\t\tchannel: string;\n\t\trecipient: string;\n\t\tlocale?: string;\n\t\tprovider?: string;\n\t\tproviderMessageId?: string;\n\t},\n\tstore: IStoreAdapter,\n): Promise<string> {\n\tconst [row] = await store.query<{ id: string }>(\n\t\t`INSERT INTO fonderie_message_log\n\t\t\t(message_type, channel, recipient, locale, provider, provider_message_id)\n\t\t VALUES ($1, $2, $3, $4, $5, $6)\n\t\t RETURNING id`,\n\t\t[\n\t\t\tentry.messageType,\n\t\t\tentry.channel,\n\t\t\tentry.recipient,\n\t\t\tentry.locale ?? null,\n\t\t\tentry.provider ?? null,\n\t\t\tentry.providerMessageId ?? null,\n\t\t],\n\t);\n\treturn row?.id ?? '';\n}\n\nexport async function markMessageSent(id: string, store: IStoreAdapter): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'sent', sent_at = now(), attempts = attempts + 1\n\t\t WHERE id = $1`,\n\t\t[id],\n\t);\n}\n\nexport async function markMessageFailed(\n\tid: string,\n\terror: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'failed', error = $2, attempts = attempts + 1\n\t\t WHERE id = $1`,\n\t\t[id, error],\n\t);\n}\n\nexport async function markMessageDelivered(\n\tproviderMessageId: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'delivered'\n\t\t WHERE provider_message_id = $1`,\n\t\t[providerMessageId],\n\t);\n}\n\nexport async function markMessageOpened(\n\tproviderMessageId: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'opened', opened_at = now()\n\t\t WHERE provider_message_id = $1 AND opened_at IS NULL`,\n\t\t[providerMessageId],\n\t);\n}\n\nexport async function markMessageClicked(\n\tproviderMessageId: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'clicked', clicked_at = now()\n\t\t WHERE provider_message_id = $1 AND clicked_at IS NULL`,\n\t\t[providerMessageId],\n\t);\n}\n\nexport async function markMessageBounced(\n\tproviderMessageId: string,\n\treason: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'bounced', bounced_at = now(), bounce_reason = $2\n\t\t WHERE provider_message_id = $1`,\n\t\t[providerMessageId, reason],\n\t);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ICourierConfig } from './config';\nimport type { ICourierMessage, ICourierChannel, ITemplateResolver } from './types';\nimport { insertMessageLog, markMessageSent, markMessageFailed } from './log';\n\nfunction resolveRecipient(message: ICourierMessage, channel: string): string {\n\tif (channel === 'email') return message.recipient.email ?? '';\n\tif (channel === 'sms') return message.recipient.phone ?? '';\n\tif (channel === 'push') return message.recipient.deviceToken ?? '';\n\treturn message.recipient.email ?? message.recipient.phone ?? '';\n}\n\nexport class Dispatcher {\n\tprivate channels: Map<string, ICourierChannel> = new Map();\n\n\tconstructor(\n\t\tprivate config: ICourierConfig,\n\t\tprivate resolver: ITemplateResolver,\n\t\tprivate store?: IStoreAdapter,\n\t) {}\n\n\tregisterChannel(channel: ICourierChannel): this {\n\t\tthis.channels.set(channel.name, channel);\n\t\treturn this;\n\t}\n\n\t// Names of the currently-registered channels — used by the boot-time config\n\t// guard to detect message types routed to a channel with no provider.\n\tchannelNames(): string[] {\n\t\treturn [...this.channels.keys()];\n\t}\n\n\tasync dispatch(message: ICourierMessage): Promise<void> {\n\t\tconst channelNames = this.config.channels[message.type];\n\n\t\tif (!channelNames || channelNames.length === 0) {\n\t\t\tconsole.warn(`[courier] no channels configured for message type: ${message.type}`);\n\t\t\treturn;\n\t\t}\n\n\t\tconst template = await this.resolver.resolve(message.type, message.data, message.locale);\n\n\t\tawait Promise.allSettled(\n\t\t\tchannelNames.map(async (name) => {\n\t\t\t\tconst channel = this.channels.get(name);\n\t\t\t\tif (!channel) {\n\t\t\t\t\tconsole.warn(`[courier] channel \"${name}\" not registered`);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Insert log entry (fire-and-forget if store unavailable)\n\t\t\t\tconst logEntry: Parameters<typeof insertMessageLog>[0] = {\n\t\t\t\t\tmessageType: message.type,\n\t\t\t\t\tchannel: name,\n\t\t\t\t\trecipient: resolveRecipient(message, name),\n\t\t\t\t};\n\t\t\t\tif (message.locale) logEntry.locale = message.locale;\n\n\t\t\t\tconst logId = this.store\n\t\t\t\t\t? await insertMessageLog(logEntry, this.store).catch(() => '')\n\t\t\t\t\t: '';\n\n\t\t\t\ttry {\n\t\t\t\t\tawait channel.send(message, template);\n\n\t\t\t\t\tif (this.store && logId) {\n\t\t\t\t\t\tmarkMessageSent(logId, this.store).catch(() => undefined);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tconsole.error(`[courier:${name}] failed to send ${message.type}:`, err);\n\n\t\t\t\t\tif (this.store && logId) {\n\t\t\t\t\t\tmarkMessageFailed(logId, errMsg, this.store).catch(() => undefined);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\t}\n}\n","import type { ISmsChannelConfig } from '../config';\nimport type { ICourierChannel, ICourierMessage, IRenderedTemplate } from '../types';\n\nexport class SmsChannel implements ICourierChannel {\n\treadonly name = 'sms';\n\n\tconstructor(private config: ISmsChannelConfig) {}\n\n\tasync send(message: ICourierMessage, template: IRenderedTemplate): Promise<void> {\n\t\tconst to = message.recipient.phone;\n\t\tif (!to) {\n\t\t\tconsole.warn('[courier:sms] no phone number for recipient — skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.config.provider === 'twilio') {\n\t\t\tawait this.sendViaTwilio(to, template.text);\n\t\t} else if (this.config.provider === 'vonage') {\n\t\t\tawait this.sendViaVonage(to, template.text);\n\t\t} else {\n\t\t\tconsole.warn(`[courier:sms] provider ${this.config.provider} not implemented`);\n\t\t}\n\t}\n\n\tprivate async sendViaTwilio(to: string, text: string): Promise<void> {\n\t\tconst { accountSid, authToken } = this.config;\n\t\tif (!accountSid || !authToken) {\n\t\t\tthrow new Error('Twilio accountSid and authToken required');\n\t\t}\n\n\t\tconst res = await fetch(\n\t\t\t`https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`,\n\t\t\t{\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Basic ${Buffer.from(`${accountSid}:${authToken}`).toString('base64')}`,\n\t\t\t\t\t'Content-Type': 'application/x-www-form-urlencoded',\n\t\t\t\t},\n\t\t\t\tbody: new URLSearchParams({ From: this.config.from, To: to, Body: text }),\n\t\t\t},\n\t\t);\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:sms] Twilio error ${res.status}: ${body}`);\n\t\t}\n\t}\n\n\tprivate async sendViaVonage(to: string, text: string): Promise<void> {\n\t\tconst { apiKey, apiSecret } = this.config;\n\t\tif (!apiKey || !apiSecret) {\n\t\t\tthrow new Error('Vonage apiKey and apiSecret required');\n\t\t}\n\n\t\tconst res = await fetch('https://rest.nexmo.com/sms/json', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({\n\t\t\t\tapi_key: apiKey,\n\t\t\t\tapi_secret: apiSecret,\n\t\t\t\tfrom: this.config.from,\n\t\t\t\tto,\n\t\t\t\ttext,\n\t\t\t}),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:sms] Vonage error ${res.status}: ${body}`);\n\t\t}\n\t}\n}\n","import type { IPushChannelConfig } from '../config';\nimport type { ICourierChannel, ICourierMessage, IRenderedTemplate } from '../types';\n\nexport class PushChannel implements ICourierChannel {\n\treadonly name = 'push';\n\n\tconstructor(private config: IPushChannelConfig) {}\n\n\tasync send(message: ICourierMessage, template: IRenderedTemplate): Promise<void> {\n\t\tconst token = message.recipient.deviceToken;\n\t\tif (!token) {\n\t\t\tconsole.warn('[courier:push] no device token for recipient — skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.config.provider === 'fcm') {\n\t\t\tawait this.sendViaFCM(token, template);\n\t\t}\n\t}\n\n\tprivate async sendViaFCM(token: string, template: IRenderedTemplate): Promise<void> {\n\t\tconst apiKey = this.config.serviceAccount['apiKey'] as string | undefined;\n\t\tif (!apiKey) {\n\t\t\tthrow new Error('[courier:push] FCM apiKey is required in serviceAccount');\n\t\t}\n\n\t\tconst res = await fetch('https://fcm.googleapis.com/fcm/send', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\tAuthorization: `key=${apiKey}`,\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tto: token,\n\t\t\t\tnotification: {\n\t\t\t\t\ttitle: template.subject,\n\t\t\t\t\tbody: template.text,\n\t\t\t\t},\n\t\t\t}),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:push] FCM error ${res.status}: ${body}`);\n\t\t}\n\t}\n}\n","import nodemailer from 'nodemailer';\n\nimport type { ICourierChannel, ICourierMessage, IRenderedTemplate } from '../types';\nimport type { IEmailChannelConfig } from '../config';\n\nexport class EmailChannel implements ICourierChannel {\n\treadonly name = 'email';\n\tprivate transport: ReturnType<typeof nodemailer.createTransport> | null = null;\n\n\tconstructor(private config: IEmailChannelConfig) {\n\t\tif (config.provider === 'smtp' && config.smtp) {\n\t\t\tthis.transport = nodemailer.createTransport({\n\t\t\t\thost: config.smtp.host,\n\t\t\t\tport: config.smtp.port,\n\t\t\t\tsecure: config.smtp.secure,\n\t\t\t\tauth: { user: config.smtp.user, pass: config.smtp.pass },\n\t\t\t});\n\t\t}\n\t}\n\n\tasync send(message: ICourierMessage, template: IRenderedTemplate): Promise<void> {\n\t\tconst to = message.recipient.email;\n\t\tif (!to) {\n\t\t\tconsole.warn('[courier:email] no email address for recipient — skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.config.provider === 'resend') {\n\t\t\tawait this.sendViaResend(to, template);\n\t\t} else if (this.config.provider === 'smtp') {\n\t\t\tawait this.sendViaSMTP(to, template);\n\t\t} else {\n\t\t\tconsole.warn(`[courier:email] provider ${this.config.provider} not implemented`);\n\t\t}\n\t}\n\n\tprivate async sendViaResend(to: string, template: IRenderedTemplate): Promise<void> {\n\t\tif (!this.config.apiKey) {\n\t\t\tthrow new Error('Resend apiKey is required');\n\t\t}\n\n\t\tconst res = await fetch('https://api.resend.com/emails', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${this.config.apiKey}`,\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tfrom: this.config.from,\n\t\t\t\tto,\n\t\t\t\tsubject: template.subject ?? '(no subject)',\n\t\t\t\thtml: template.html,\n\t\t\t\ttext: template.text,\n\t\t\t}),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:email] Resend error ${res.status}: ${body}`);\n\t\t}\n\t}\n\n\tprivate async sendViaSMTP(to: string, template: IRenderedTemplate): Promise<void> {\n\t\tif (!this.transport) {\n\t\t\tthrow new Error('SMTP transport not initialised — check smtp config');\n\t\t}\n\n\t\tawait this.transport.sendMail({\n\t\t\tfrom: this.config.from,\n\t\t\tto,\n\t\t\tsubject: template.subject ?? '(no subject)',\n\t\t\thtml: template.html,\n\t\t\ttext: template.text,\n\t\t});\n\t}\n}\n","// Shared, composable email layout — a branded shell that wraps every template\n// body. Templates stored in the DB or on disk are **body fragments**; the\n// resolver injects them into `{{content}}` here, then interpolates `{{vars}}`\n// over the whole. One shell, many bodies — so DB and FS sources render an\n// identical, production-grade frame for free.\n//\n// Cross-client hardening (Outlook/Gmail/Apple Mail) follows the well-worn\n// \"hybrid responsive\" pattern popularised by Cerberus (MIT): a max-width table\n// container, inline styles on the critical structure, and a `<style>` block in\n// `<head>` for the friendly classes and small-screen media query. Founders can\n// override the whole shell by storing a `_layout` template (DB row or\n// `_layout.html` file) in their own source.\n//\n// The theme tokens mirror the product design system (organization/ui/base.css):\n// near-black primary, a mint brand accent, Inter, and the `#fafafa` canvas —\n// retune `EMAIL_THEME` to rebrand every email at once.\n\n// A single small token set — retune these to match your brand.\nexport const EMAIL_THEME = {\n\tbrand: 'Fonderie',\n\taccent: '#171717', // primary action (button) background — the product's primary\n\taccentText: '#ffffff', // text on the accent\n\tbrandAccent: '#171717', // brand highlight (top rule) — monochrome near-black\n\tlink: '#009767', // links — the accessible (darkened) brand teal\n\tink: '#171717', // body copy\n\tmuted: '#5c5c5c', // secondary copy\n\tline: '#e0e0e0', // hairline borders\n\tcanvas: '#fafafa', // page background behind the card\n\tcard: '#ffffff', // the card itself\n} as const;\n\n// The product font stack (Inter first, with system fallbacks for clients that\n// lack it). Shared by every text element so the frame stays consistent.\nconst FONT_SANS =\n\t\"Inter, 'Inter Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif\";\nconst FONT_MONO = \"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace\";\n\n// The `{{content}}` marker is replaced with the body fragment BEFORE variable\n// interpolation, so a body's own `{{firstName}}` etc. still resolve.\nexport const LAYOUT_CONTENT_SLOT = '{{content}}';\n\nexport const DEFAULT_EMAIL_LAYOUT = `<!DOCTYPE html>\n<html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n<head>\n\t<meta charset=\"utf-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"color-scheme\" content=\"light dark\">\n\t<meta name=\"supported-color-schemes\" content=\"light dark\">\n\t<title>{{subject}}</title>\n\t<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->\n\t<style>\n\t\tbody { margin: 0; padding: 0; width: 100% !important; background: ${EMAIL_THEME.canvas}; }\n\t\t.email-body { background: ${EMAIL_THEME.canvas}; }\n\t\t.email-container { width: 100%; max-width: 560px; margin: 0 auto; }\n\t\t.email-card {\n\t\t\tbackground: ${EMAIL_THEME.card};\n\t\t\tborder: 1px solid ${EMAIL_THEME.line};\n\t\t\tborder-radius: 8px;\n\t\t\toverflow: hidden;\n\t\t}\n\t\t.email-accent { height: 3px; background: ${EMAIL_THEME.brandAccent}; font-size: 0; line-height: 0; }\n\t\t.email-header { padding: 28px 32px 0 32px; }\n\t\t.email-brand { font: 800 19px/1 ${FONT_SANS}; color: ${EMAIL_THEME.ink}; letter-spacing: -0.03em; }\n\t\t.email-content {\n\t\t\tpadding: 20px 32px 8px 32px;\n\t\t\tfont: 400 16px/1.7 ${FONT_SANS};\n\t\t\tcolor: ${EMAIL_THEME.ink};\n\t\t\tletter-spacing: -0.01em;\n\t\t}\n\t\t.email-content h1 { margin: 0 0 12px 0; font-size: 22px; line-height: 1.3; font-weight: 700; letter-spacing: -0.03em; }\n\t\t.email-content p { margin: 0 0 16px 0; }\n\t\t.email-content a { color: ${EMAIL_THEME.link}; }\n\t\t.email-footer {\n\t\t\tpadding: 16px 32px 28px 32px;\n\t\t\tfont: 400 13px/1.6 ${FONT_SANS};\n\t\t\tcolor: ${EMAIL_THEME.muted};\n\t\t\tletter-spacing: -0.01em;\n\t\t}\n\t\t.btn {\n\t\t\tdisplay: inline-block;\n\t\t\tbackground: ${EMAIL_THEME.accent};\n\t\t\tcolor: ${EMAIL_THEME.accentText} !important;\n\t\t\ttext-decoration: none;\n\t\t\tfont: 600 16px/1 ${FONT_SANS};\n\t\t\tletter-spacing: -0.01em;\n\t\t\tpadding: 13px 26px;\n\t\t\tborder-radius: 6px;\n\t\t}\n\t\t.pin-code {\n\t\t\tdisplay: inline-block;\n\t\t\tfont: 700 30px/1 ${FONT_MONO};\n\t\t\tletter-spacing: 0.28em;\n\t\t\tcolor: ${EMAIL_THEME.ink};\n\t\t\tbackground: ${EMAIL_THEME.canvas};\n\t\t\tborder: 1px solid ${EMAIL_THEME.line};\n\t\t\tborder-radius: 6px;\n\t\t\tpadding: 14px 22px 14px 30px;\n\t\t}\n\t\t.muted { color: ${EMAIL_THEME.muted}; }\n\t\t@media only screen and (max-width: 599px) {\n\t\t\t.email-header, .email-content, .email-footer { padding-left: 22px !important; padding-right: 22px !important; }\n\t\t\t.pin-code { font-size: 26px !important; letter-spacing: 0.2em !important; }\n\t\t}\n\t</style>\n</head>\n<body class=\"email-body\">\n\t<div style=\"display:none;max-height:0;overflow:hidden;opacity:0;\">{{preheader}}</div>\n\t<table role=\"presentation\" class=\"email-body\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"background:${EMAIL_THEME.canvas};\">\n\t\t<tr>\n\t\t\t<td align=\"center\" style=\"padding: 32px 16px;\">\n\t\t\t\t<table role=\"presentation\" class=\"email-container\" width=\"560\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"max-width:560px;\">\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td class=\"email-card\" style=\"background:${EMAIL_THEME.card};border:1px solid ${EMAIL_THEME.line};border-radius:8px;\">\n\t\t\t\t\t\t\t<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n\t\t\t\t\t\t\t\t<tr><td class=\"email-accent\" style=\"height:3px;background:${EMAIL_THEME.brandAccent};font-size:0;line-height:0;\"> </td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-header\" style=\"padding:28px 32px 0 32px;\">\n\t\t\t\t\t\t\t\t\t<span class=\"email-brand\" style=\"font-weight:800;font-size:19px;color:${EMAIL_THEME.ink};letter-spacing:-0.03em;\">${EMAIL_THEME.brand}</span>\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-content\" style=\"padding:20px 32px 8px 32px;color:${EMAIL_THEME.ink};\">\n${LAYOUT_CONTENT_SLOT}\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-footer\" style=\"padding:16px 32px 28px 32px;color:${EMAIL_THEME.muted};font-size:13px;\">\n\t\t\t\t\t\t\t\t\tYou're receiving this because someone used this address at ${EMAIL_THEME.brand}. If that wasn't you, you can ignore it.\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t</table>\n\t\t\t</td>\n\t\t</tr>\n\t</table>\n</body>\n</html>`;\n\n// Inject a body fragment into a layout shell. If the body is already a full\n// HTML document (a founder chose to store a complete template), it is returned\n// untouched — never double-wrapped.\nexport function wrapLayout(bodyHtml: string, layoutHtml: string = DEFAULT_EMAIL_LAYOUT): string {\n\tconst trimmed = bodyHtml.trimStart().toLowerCase();\n\tif (trimmed.startsWith('<!doctype') || trimmed.startsWith('<html')) {\n\t\treturn bodyHtml;\n\t}\n\treturn layoutHtml.replace(LAYOUT_CONTENT_SLOT, bodyHtml);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ITemplateResolver, IRenderedTemplate } from '../types';\nimport { wrapLayout } from './layout';\n\n// The stored template id for a founder-supplied layout shell (DB row `type` or\n// FS file `_layout.html`). Absent → the built-in DEFAULT_EMAIL_LAYOUT is used.\nconst LAYOUT_TYPE = '_layout';\n\nfunction render(template: string, data: Record<string, unknown>): string {\n\treturn template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) => {\n\t\tconst value = data[key];\n\t\treturn value !== undefined && value !== null ? String(value) : '';\n\t});\n}\n\n// Compose a body fragment into its layout shell, then interpolate variables\n// over the whole. `subject`/`preheader` become available to the shell's title\n// and inbox preview text.\nfunction composeHtml(\n\tbodyHtml: string,\n\tlayoutHtml: string | undefined,\n\tsubject: string | undefined,\n\tdata: Record<string, unknown>,\n): string {\n\tconst wrapped = wrapLayout(bodyHtml, layoutHtml);\n\treturn render(wrapped, { subject: subject ?? '', preheader: '', ...data });\n}\n\n// DB-backed resolver — reads from fonderie_courier_templates with locale fallback\nexport class DBTemplateResolver implements ITemplateResolver {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync resolve(\n\t\ttype: string,\n\t\tdata: Record<string, unknown>,\n\t\tlocale?: string,\n\t): Promise<IRenderedTemplate> {\n\t\tconst [row] = await this.store.query<{\n\t\t\tsubject: string | null;\n\t\t\thtml: string | null;\n\t\t\ttext: string;\n\t\t}>(\n\t\t\t// Serve the exact locale, else the neutral NULL default — never a\n\t\t\t// sibling region (en-CA must not fall back to en-US); the WHERE\n\t\t\t// excludes other locales so legal/jurisdictional copy can't bleed.\n\t\t\t`SELECT subject, html, text\n\t\t\t FROM fonderie_courier_templates\n\t\t\t WHERE type = $1 AND active = true AND (locale = $2 OR locale IS NULL)\n\t\t\t ORDER BY (locale IS NOT DISTINCT FROM $2) DESC\n\t\t\t LIMIT 1`,\n\t\t\t[type, locale ?? null],\n\t\t);\n\n\t\tif (!row) {\n\t\t\treturn { text: `${type}: ${JSON.stringify(data)}` };\n\t\t}\n\n\t\tconst subject = row.subject ? render(row.subject, data) : undefined;\n\t\tconst layoutHtml = row.html ? await this.layout(locale) : undefined;\n\n\t\treturn {\n\t\t\ttext: render(row.text, data),\n\t\t\t...(subject ? { subject } : {}),\n\t\t\t...(row.html ? { html: composeHtml(row.html, layoutHtml, subject, data) } : {}),\n\t\t};\n\t}\n\n\t// Optional founder-supplied layout shell; undefined → built-in default.\n\tprivate async layout(locale?: string): Promise<string | undefined> {\n\t\tconst [row] = await this.store.query<{ html: string | null }>(\n\t\t\t`SELECT html\n\t\t\t FROM fonderie_courier_templates\n\t\t\t WHERE type = $1 AND active = true AND (locale = $2 OR locale IS NULL)\n\t\t\t ORDER BY (locale IS NOT DISTINCT FROM $2) DESC\n\t\t\t LIMIT 1`,\n\t\t\t[LAYOUT_TYPE, locale ?? null],\n\t\t);\n\t\treturn row?.html ?? undefined;\n\t}\n}\n\n// Filesystem resolver — reads {type}.{locale}.txt → {type}.txt with fallback\nexport class FSTemplateResolver implements ITemplateResolver {\n\tconstructor(private directory: string) {}\n\n\tasync resolve(\n\t\ttype: string,\n\t\tdata: Record<string, unknown>,\n\t\tlocale?: string,\n\t): Promise<IRenderedTemplate> {\n\t\tconst { readFile } = await import('node:fs/promises');\n\t\tconst { join } = await import('node:path');\n\n\t\tconst readOptional = async (path: string): Promise<string | null> => {\n\t\t\ttry {\n\t\t\t\treturn await readFile(path, 'utf8');\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t};\n\n\t\t// Per-locale variants take priority over generic variants\n\t\tconst localePrefix = locale ? `${type}.${locale}` : null;\n\t\tconst layoutPrefix = locale ? `${LAYOUT_TYPE}.${locale}` : null;\n\n\t\tconst [text, html, subject, layout] = await Promise.all([\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.txt`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.txt`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.txt`)),\n\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.html`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.html`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.html`)),\n\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.subject.txt`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.subject.txt`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.subject.txt`)),\n\n\t\t\tlayoutPrefix\n\t\t\t\t? readOptional(join(this.directory, `${layoutPrefix}.html`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${LAYOUT_TYPE}.html`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${LAYOUT_TYPE}.html`)),\n\t\t]);\n\n\t\tconst renderedSubject = subject ? render(subject, data) : undefined;\n\n\t\treturn {\n\t\t\ttext: text ? render(text, data) : `${type}: ${JSON.stringify(data)}`,\n\t\t\t...(renderedSubject ? { subject: renderedSubject } : {}),\n\t\t\t...(html ? { html: composeHtml(html, layout ?? undefined, renderedSubject, data) } : {}),\n\t\t};\n\t}\n}\n","import type { IReadinessProblem } from '@fonderie/core';\nimport type { ICourierConfig } from './config';\n\nconst MODULE = '@fonderie/courier';\n\n// Pure production-readiness assessment (no side effects) — shared by the\n// boot-time guard and `CourierModule.checkReadiness`. A message type routed to a\n// channel with no registered provider is silently dropped by the dispatcher —\n// with `requireVerification` on, verification / reset emails vanish and users\n// lock themselves out. These are `warning`-severity (channel setups vary\n// legitimately — an app may run in-app only — so this never blocks boot, unlike\n// auth's fatal weak-secret). Checks the channels actually **registered** on the\n// dispatcher, so channels added via `registerChannel` count as present.\nexport function collectCourierConfigProblems(\n\tconfig: ICourierConfig,\n\tregisteredChannels: Iterable<string>,\n): IReadinessProblem[] {\n\tconst registered = new Set(registeredChannels);\n\tconst gaps = new Map<string, string[]>(); // channel → message types routed to it\n\n\tfor (const [type, channels] of Object.entries(config.channels ?? {})) {\n\t\tfor (const channel of channels) {\n\t\t\tif (!registered.has(channel)) {\n\t\t\t\tconst types = gaps.get(channel) ?? [];\n\t\t\t\ttypes.push(type);\n\t\t\t\tgaps.set(channel, types);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn [...gaps].map(([channel, types]) => ({\n\t\tmodule: MODULE,\n\t\tseverity: 'warning' as const,\n\t\tmessage:\n\t\t\t`${types.length} message type(s) route to the '${channel}' channel but ` +\n\t\t\t`no '${channel}' provider is registered — these will be silently dropped: ` +\n\t\t\t`${types.join(', ')}. Configure \\`config.${channel}\\` (or register a channel).`,\n\t}));\n}\n\n// Boot-time preflight, run from `CourierModule.install`. Warn-only.\nexport function validateCourierConfig(\n\tconfig: ICourierConfig,\n\tregisteredChannels: Iterable<string>,\n): void {\n\tfor (const problem of collectCourierConfigProblems(config, registeredChannels)) {\n\t\tconsole.warn(`[courier] ${problem.message}`);\n\t}\n}\n","import { createHmac } from 'node:crypto';\n\nimport { constantTimeEqual } from '@fonderie/core';\n\nimport type { IStoreAdapter } from '@fonderie/store';\nimport {\n\tmarkMessageDelivered,\n\tmarkMessageOpened,\n\tmarkMessageClicked,\n\tmarkMessageBounced,\n} from './log';\n\n// ── SendGrid ──────────────────────────────────────────────────────\n//\n// Verifies the X-Twilio-Email-Event-Webhook-Signature header.\n// Expects an array of event objects in the request body.\n// https://docs.sendgrid.com/for-developers/tracking-events/getting-started-event-webhook-security-features\n\nexport async function handleSendGridDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n\twebhookSecret?: string,\n): Promise<Response> {\n\tif (webhookSecret) {\n\t\tconst sig = req.headers.get('x-twilio-email-event-webhook-signature') ?? '';\n\t\tconst ts = req.headers.get('x-twilio-email-event-webhook-timestamp') ?? '';\n\t\tconst body = await req.text();\n\n\t\tif (!verifySendGridSignature(webhookSecret, ts, body, sig)) {\n\t\t\treturn Response.json({ error: 'INVALID_SIGNATURE' }, { status: 401 });\n\t\t}\n\n\t\tconst events = parseJson(body) as SendGridEvent[] | null;\n\t\tif (!Array.isArray(events)) return Response.json({ ok: true });\n\n\t\tawait processSendGridEvents(events, store);\n\t\treturn Response.json({ ok: true });\n\t}\n\n\tconst events = (await req.json()) as SendGridEvent[];\n\tif (!Array.isArray(events)) return Response.json({ ok: true });\n\n\tawait processSendGridEvents(events, store);\n\treturn Response.json({ ok: true });\n}\n\nfunction verifySendGridSignature(\n\tsecret: string,\n\ttimestamp: string,\n\tbody: string,\n\tsignature: string,\n): boolean {\n\ttry {\n\t\tconst payload = timestamp + body;\n\t\tconst expected = createHmac('sha256', secret).update(payload).digest('base64');\n\t\treturn constantTimeEqual(Buffer.from(signature, 'base64'), Buffer.from(expected, 'base64'));\n\t} catch {\n\t\treturn false;\n\t}\n}\n\ninterface SendGridEvent {\n\tevent: string;\n\tsg_message_id?: string;\n\treason?: string;\n\t[key: string]: unknown;\n}\n\nasync function processSendGridEvents(\n\tevents: SendGridEvent[],\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tfor (const ev of events) {\n\t\tconst msgId = ev['sg_message_id'];\n\t\tif (!msgId || typeof msgId !== 'string') continue;\n\t\t// sg_message_id is \"msgId.filterXxx\" — strip the filter suffix\n\t\tconst id = msgId.split('.')[0] ?? msgId;\n\n\t\tswitch (ev.event) {\n\t\t\tcase 'delivered': await markMessageDelivered(id, store); break;\n\t\t\tcase 'open': await markMessageOpened(id, store); break;\n\t\t\tcase 'click': await markMessageClicked(id, store); break;\n\t\t\tcase 'bounce':\n\t\t\tcase 'blocked':\n\t\t\tcase 'dropped':\n\t\t\t\tawait markMessageBounced(id, typeof ev['reason'] === 'string' ? ev['reason'] : ev.event, store);\n\t\t\t\tbreak;\n\t\t}\n\t}\n}\n\n// ── Mailgun ───────────────────────────────────────────────────────\n//\n// Verifies signature using token + timestamp + signing key (HMAC-SHA256).\n// https://documentation.mailgun.com/docs/mailgun/user-manual/tracking-messages/#securing-webhooks\n\nexport async function handleMailgunDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n\tsigningKey?: string,\n): Promise<Response> {\n\tconst body = (await req.json()) as MailgunPayload;\n\n\tif (signingKey) {\n\t\tconst { signature } = body;\n\t\tif (!signature || !verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {\n\t\t\treturn Response.json({ error: 'INVALID_SIGNATURE' }, { status: 401 });\n\t\t}\n\t}\n\n\tconst event = body['event-data'];\n\tif (event) {\n\t\tawait processMailgunEvent(event, store);\n\t}\n\n\treturn Response.json({ ok: true });\n}\n\nfunction verifyMailgunSignature(\n\tsigningKey: string,\n\ttimestamp: string,\n\ttoken: string,\n\tsignature: string,\n): boolean {\n\ttry {\n\t\tconst value = timestamp + token;\n\t\tconst expected = createHmac('sha256', signingKey).update(value).digest('hex');\n\t\treturn constantTimeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'));\n\t} catch {\n\t\treturn false;\n\t}\n}\n\ninterface MailgunPayload {\n\tsignature?: { timestamp: string; token: string; signature: string };\n\t'event-data'?: MailgunEvent;\n\t[key: string]: unknown;\n}\n\ninterface MailgunEvent {\n\tevent: string;\n\tmessage?: { headers?: { 'message-id'?: string } };\n\t'delivery-status'?: { message?: string };\n\t[key: string]: unknown;\n}\n\nasync function processMailgunEvent(event: MailgunEvent, store: IStoreAdapter): Promise<void> {\n\tconst msgId = event.message?.headers?.['message-id'];\n\tif (!msgId) return;\n\n\tswitch (event.event) {\n\t\tcase 'delivered': await markMessageDelivered(msgId, store); break;\n\t\tcase 'opened': await markMessageOpened(msgId, store); break;\n\t\tcase 'clicked': await markMessageClicked(msgId, store); break;\n\t\tcase 'failed':\n\t\tcase 'bounced': {\n\t\t\tconst reason = event['delivery-status']?.message ?? event.event;\n\t\t\tawait markMessageBounced(msgId, reason, store);\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n// ── Mailtrap (testing only, no signature) ────────────────────────\n\nexport async function handleMailtrapDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n): Promise<Response> {\n\tconst events = (await req.json()) as MailtrapEvent[];\n\tif (!Array.isArray(events)) return Response.json({ ok: true });\n\n\tfor (const ev of events) {\n\t\tconst msgId = ev.message_id;\n\t\tif (!msgId) continue;\n\n\t\tswitch (ev.event) {\n\t\t\tcase 'delivery': await markMessageDelivered(msgId, store); break;\n\t\t\tcase 'open': await markMessageOpened(msgId, store); break;\n\t\t\tcase 'click': await markMessageClicked(msgId, store); break;\n\t\t\tcase 'bounce':\n\t\t\tcase 'soft_bounce':\n\t\t\t\tawait markMessageBounced(msgId, ev.event, store);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn Response.json({ ok: true });\n}\n\ninterface MailtrapEvent {\n\tevent: string;\n\tmessage_id?: string;\n\t[key: string]: unknown;\n}\n\n// ── Utility ───────────────────────────────────────────────────────\n\nfunction parseJson(text: string): unknown {\n\ttry { return JSON.parse(text); } catch { return null; }\n}\n","import type { IFonderieContext, Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport { requireAdminToken } from '@fonderie/core/middlewares';\nimport { VersionConflictError } from '@fonderie/store';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport {\n\tsetTemplate,\n\trollbackTemplate,\n\tlistTemplateRevisions,\n\tgetTemplateEntry,\n\tlistTemplateEntries,\n\tdeleteTemplate,\n} from './admin';\n\n// Bearer-token guard (mirrors @fonderie/config's admin surface). Only registered\n// when a token is configured — no token, no exposed template admin routes.\n// Wrap a handler in the shared admin-token guard (@fonderie/core/middlewares) —\n// one constant-time Bearer check across billing/config/courier.\nfunction guarded(adminToken: string, handler: Middleware): Middleware {\n\tconst guard = requireAdminToken(adminToken);\n\treturn (ctx, next) => guard(ctx, () => handler(ctx, next));\n}\n\nconst actorOf = (ctx: IFonderieContext) => ctx.request.headers.get('x-actor') || 'admin-token';\nconst typeOf = (ctx: IFonderieContext) => ctx.meta.params?.['type'] ?? '';\nconst localeOf = (ctx: IFonderieContext): string | null =>\n\tnew URL(ctx.request.url).searchParams.get('locale');\nconst body = (ctx: IFonderieContext): Record<string, unknown> =>\n\t(ctx.meta['body'] as Record<string, unknown> | undefined) ?? {};\n\nfunction conflictOr(err: unknown): Response {\n\tif (err instanceof VersionConflictError) {\n\t\treturn setApiResponse(HTTP.CONFLICT, 'VERSION_CONFLICT', err.message, {\n\t\t\tcurrentVersion: err.currentVersion,\n\t\t});\n\t}\n\tthrow err;\n}\n\n// Build the template admin route table (registered by CourierModule.install when\n// an adminToken is configured).\nexport function buildTemplateAdminRoutes(\n\tstore: IStoreAdapter,\n\tadminToken: string,\n): Array<[string, string, Middleware]> {\n\tconst g = (h: Middleware): Middleware => guarded(adminToken, h);\n\n\treturn [\n\t\t['GET', '/admin/templates', g(async () => {\n\t\t\treturn setApiResponse(HTTP.OK, 'TEMPLATES_LISTED', 'Templates', await listTemplateEntries(store));\n\t\t})],\n\t\t['GET', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst row = await getTemplateEntry(typeOf(ctx), localeOf(ctx), store);\n\t\t\treturn row\n\t\t\t\t? setApiResponse(HTTP.OK, 'TEMPLATE', 'Template', row)\n\t\t\t\t: setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such template');\n\t\t})],\n\t\t['PUT', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tif (typeof b['text'] !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.text (string) is required');\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst opts: Parameters<typeof setTemplate>[0] = {\n\t\t\t\t\ttype: typeOf(ctx),\n\t\t\t\t\ttext: b['text'],\n\t\t\t\t\tlocale: localeOf(ctx),\n\t\t\t\t\tactor: actorOf(ctx),\n\t\t\t\t};\n\t\t\t\tif (typeof b['subject'] === 'string') opts.subject = b['subject'];\n\t\t\t\tif (typeof b['html'] === 'string') opts.html = b['html'];\n\t\t\t\tif (typeof b['active'] === 'boolean') opts.active = b['active'];\n\t\t\t\tif (typeof b['ifVersion'] === 'number') opts.ifVersion = b['ifVersion'];\n\t\t\t\treturn setApiResponse(HTTP.OK, 'TEMPLATE_SET', 'Template saved', await setTemplate(opts, store));\n\t\t\t} catch (err) {\n\t\t\t\treturn conflictOr(err);\n\t\t\t}\n\t\t})],\n\t\t['DELETE', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst ok = await deleteTemplate(typeOf(ctx), localeOf(ctx), store);\n\t\t\treturn setApiResponse(ok ? HTTP.OK : HTTP.NOT_FOUND, ok ? 'DELETED' : 'NOT_FOUND', ok ? 'Deleted' : 'No such template');\n\t\t})],\n\t\t['GET', '/admin/templates/:type/revisions', g(async (ctx) => {\n\t\t\treturn setApiResponse(HTTP.OK, 'REVISIONS', 'Template revisions', await listTemplateRevisions(typeOf(ctx), localeOf(ctx), store));\n\t\t})],\n\t\t['POST', '/admin/templates/:type/rollback', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tconst toVersion = Number(b['toVersion']);\n\t\t\tif (!Number.isInteger(toVersion)) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.toVersion (int) is required');\n\t\t\t}\n\t\t\tconst row = await rollbackTemplate(\n\t\t\t\t{ type: typeOf(ctx), locale: localeOf(ctx), toVersion, actor: actorOf(ctx) },\n\t\t\t\tstore,\n\t\t\t);\n\t\t\treturn setApiResponse(HTTP.OK, 'ROLLED_BACK', `Rolled back to v${toVersion}`, row);\n\t\t})],\n\t];\n}\n","import type { IStoreAdapter, IVersionedResource } from '@fonderie/store';\nimport { versionedWrite, versionedRollback } from '@fonderie/store';\n\n// Versioned management for email templates, on the shared `@fonderie/store`\n// primitive. Templates are keyed by (type, locale) — a NULL locale is the base,\n// with per-locale overrides on top — and their content is subject/html/text.\n// The resolver reads the current row unchanged; this adds edit + history +\n// rollback + optimistic concurrency on top.\n\nexport interface ITemplateEntry {\n\ttype: string;\n\tlocale: string | null;\n\tsubject: string | null;\n\thtml: string | null;\n\ttext: string;\n\tactive: boolean;\n\tversion: number;\n\tupdatedBy: string | null;\n\tupdatedAt: string;\n}\n\nexport interface ITemplateRevision {\n\ttype: string;\n\tlocale: string | null;\n\tsubject: string | null;\n\thtml: string | null;\n\ttext: string;\n\tversion: number;\n\tactor: string | null;\n\tcreatedAt: string;\n}\n\nconst ENTRY_COLS = `type, locale, subject, html, text, active, version, updated_by AS \"updatedBy\", updated_at AS \"updatedAt\"`;\n\nconst TEMPLATE_RESOURCE: IVersionedResource = {\n\ttable: 'fonderie_courier_templates',\n\trevisions: 'fonderie_courier_template_revisions',\n\tchannel: 'fonderie_courier_templates_changed',\n\tkeyColumns: ['type', 'locale'],\n\tcontentColumns: ['subject', 'html', 'text'],\n\tmetaColumns: ['active'],\n\treturning: ENTRY_COLS,\n};\n\n// Upsert a template (full body — a revision is a full snapshot). Pass `ifVersion`\n// for optimistic concurrency (else `VersionConflictError`). `locale` omitted /\n// null targets the base template.\nexport async function setTemplate(\n\topts: {\n\t\ttype: string;\n\t\ttext: string;\n\t\tlocale?: string | null;\n\t\tsubject?: string | null;\n\t\thtml?: string | null;\n\t\tactive?: boolean;\n\t\tifVersion?: number;\n\t\tactor?: string;\n\t},\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry> {\n\tconst data: Record<string, unknown> = {\n\t\tsubject: opts.subject ?? null,\n\t\thtml: opts.html ?? null,\n\t\ttext: opts.text,\n\t};\n\tif (opts.active !== undefined) data['active'] = opts.active;\n\treturn versionedWrite<ITemplateEntry>(TEMPLATE_RESOURCE, store, {\n\t\tkey: opts.type,\n\t\tscope: opts.locale ?? null,\n\t\tdata,\n\t\t...(opts.ifVersion !== undefined ? { ifVersion: opts.ifVersion } : {}),\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function rollbackTemplate(\n\topts: { type: string; locale?: string | null; toVersion: number; actor?: string },\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry> {\n\treturn versionedRollback<ITemplateEntry>(TEMPLATE_RESOURCE, store, {\n\t\tkey: opts.type,\n\t\tscope: opts.locale ?? null,\n\t\ttoVersion: opts.toVersion,\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function listTemplateRevisions(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<ITemplateRevision[]> {\n\treturn store.query<ITemplateRevision>(\n\t\t`SELECT type, locale, subject, html, text, version, actor, created_at AS \"createdAt\"\n\t\t FROM fonderie_courier_template_revisions\n\t\t WHERE type = $1 AND locale IS NOT DISTINCT FROM $2\n\t\t ORDER BY version DESC`,\n\t\t[type, locale],\n\t);\n}\n\nexport async function getTemplateEntry(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry | null> {\n\tconst [row] = await store.query<ITemplateEntry>(\n\t\t`SELECT ${ENTRY_COLS} FROM fonderie_courier_templates WHERE type = $1 AND locale IS NOT DISTINCT FROM $2`,\n\t\t[type, locale],\n\t);\n\treturn row ?? null;\n}\n\nexport async function listTemplateEntries(store: IStoreAdapter): Promise<ITemplateEntry[]> {\n\treturn store.query<ITemplateEntry>(\n\t\t`SELECT ${ENTRY_COLS} FROM fonderie_courier_templates ORDER BY type, locale NULLS FIRST`,\n\t);\n}\n\nexport async function deleteTemplate(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst rows = await store.query<{ type: string }>(\n\t\t`DELETE FROM fonderie_courier_templates WHERE type = $1 AND locale IS NOT DISTINCT FROM $2 RETURNING type`,\n\t\t[type, locale],\n\t);\n\treturn rows.length > 0;\n}\n","export interface IEmailChannelConfig {\n\tprovider: 'resend' | 'ses' | 'smtp';\n\tfrom: string;\n\tapiKey?: string;\n\tsmtp?: {\n\t\thost: string;\n\t\tport: number;\n\t\tsecure: boolean;\n\t\tuser: string;\n\t\tpass: string;\n\t};\n}\n\nexport interface ISmsChannelConfig {\n\tprovider: 'twilio' | 'vonage';\n\tfrom: string;\n\taccountSid?: string; // twilio\n\tauthToken?: string; // twilio\n\tapiKey?: string; // vonage\n\tapiSecret?: string; // vonage\n}\n\nexport interface IPushChannelConfig {\n\tprovider: 'fcm';\n\tserviceAccount: Record<string, unknown>;\n}\n\nexport const Channel = {\n\tEMAIL: 'email',\n\tSMS: 'sms',\n\tPUSH: 'push',\n} as const satisfies Record<string, 'email' | 'sms' | 'push'>;\n\nexport interface ICourierConfig {\n\t// Which channels handle which message types\n\t// e.g. { 'password-reset': ['email'], 'new-message': ['push', 'sms'] }\n\tchannels: Record<string, Array<'email' | 'sms' | 'push'>>;\n\n\tsms?: ISmsChannelConfig;\n\tpush?: IPushChannelConfig;\n\temail?: IEmailChannelConfig;\n\n\t// When set, exposes Bearer-guarded template admin routes (/admin/templates/*)\n\t// for versioned edit/history/rollback. Requires @fonderie/store (db templates).\n\tadminToken?: string;\n\n\t// Where templates are loaded from\n\t// 'db' requires @fonderie/store to be configured\n\t// 'fs' reads from a local directory\n\ttemplates?: {\n\t\tsource: 'db' | 'fs';\n\t\tdirectory?: string; // for 'fs' source\n\t};\n\n\t// Signing keys for verifying inbound delivery webhook payloads\n\tdelivery?: {\n\t\tsigningKeys?: {\n\t\t\tsendgrid?: string; // HMAC-SHA256 key from SendGrid dashboard\n\t\t\tmailgun?: string; // Mailgun HTTP webhook signing key\n\t\t};\n\t};\n}\n"],"mappings":"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","names":["body","body","body","body","events"]}
|
|
1
|
+
{"version":3,"sources":["../src/module.ts","../src/log.ts","../src/dispatcher.ts","../src/channels/sms.ts","../src/channels/push.ts","../src/channels/email.ts","../src/templates/layout.ts","../src/templates/resolver.ts","../src/config-guard.ts","../src/delivery.ts","../src/templates/admin-routes.ts","../src/templates/admin.ts","../src/config.ts"],"sourcesContent":["import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport type { EventBus } from '@fonderie/events';\nimport { NOTIFICATION_EVENT } from '@fonderie/events';\n\nimport type { ITemplateResolver, ICourierMessage } from './types';\nimport type { ICourierConfig } from './config';\n\nimport { Dispatcher } from './dispatcher';\nimport { SmsChannel } from './channels/sms';\nimport { PushChannel } from './channels/push';\nimport { EmailChannel } from './channels/email';\nimport type { IReadinessProblem } from '@fonderie/core';\nimport { validateAdminToken } from '@fonderie/core/middlewares';\nimport { DBTemplateResolver, FSTemplateResolver, DefaultTemplates } from './templates/resolver';\nimport { validateCourierConfig, collectCourierConfigProblems } from './config-guard';\nimport { handleSendGridDelivery, handleMailgunDelivery, handleMailtrapDelivery } from './delivery';\nimport { buildTemplateAdminRoutes } from './templates/admin-routes';\n\nexport class CourierModule implements IFonderieModule {\n\treadonly name = '@fonderie/courier';\n\treadonly deps = ['@fonderie/events'];\n\treadonly dispatcher: Dispatcher;\n\n\tconstructor(\n\t\tprivate config: ICourierConfig,\n\t\tprivate store?: IStoreAdapter,\n\t\tbus?: EventBus,\n\t) {\n\t\tconst templateSource = config.templates?.source ?? 'db';\n\t\tconst resolver = createTemplateResolver(templateSource, config, store);\n\n\t\tthis.dispatcher = new Dispatcher(config, resolver, store);\n\n\t\tif (config.email) this.dispatcher.registerChannel(new EmailChannel(config.email));\n\t\tif (config.sms) this.dispatcher.registerChannel(new SmsChannel(config.sms));\n\t\tif (config.push) this.dispatcher.registerChannel(new PushChannel(config.push));\n\n\t\tbus?.on<ICourierMessage>(\n\t\t\tNOTIFICATION_EVENT,\n\t\t\tasync (msg) => {\n\t\t\t\tawait this.dispatcher.dispatch(msg);\n\t\t\t},\n\t\t\t'courier',\n\t\t);\n\t}\n\n\t// Report config problems for app.checkProductionReadiness() (data, not warn).\n\tcheckReadiness(): IReadinessProblem[] {\n\t\treturn [\n\t\t\t...collectCourierConfigProblems(this.config, this.dispatcher.channelNames()),\n\t\t\t// Shared admin-token strength rule (@fonderie/core/middlewares) — the\n\t\t\t// /admin/templates surface must not be guarded by a weak/placeholder token.\n\t\t\t...validateAdminToken(this.config.adminToken, { module: this.name }),\n\t\t];\n\t}\n\n\tinstall(app: IFonderieApp): void {\n\t\t// Boot-time preflight: warn if any routed message type has no provider.\n\t\tvalidateCourierConfig(this.config, this.dispatcher.channelNames());\n\n\t\tconst store = this.store;\n\t\tconst signingKeys = this.config.delivery?.signingKeys;\n\n\t\tapp.addRoute('POST', '/courier/delivery/sendgrid', (ctx) =>\n\t\t\thandleSendGridDelivery(ctx.request, store!, signingKeys?.sendgrid),\n\t\t);\n\t\tapp.addRoute('POST', '/courier/delivery/mailgun', (ctx) =>\n\t\t\thandleMailgunDelivery(ctx.request, store!, signingKeys?.mailgun),\n\t\t);\n\t\tapp.addRoute('POST', '/courier/delivery/mailtrap', (ctx) =>\n\t\t\thandleMailtrapDelivery(ctx.request, store!),\n\t\t);\n\n\t\t// Versioned template admin routes — only when a token is configured and a\n\t\t// store is present (db templates). Mirrors @fonderie/config's admin surface.\n\t\tif (this.config.adminToken) {\n\t\t\tif (!store) {\n\t\t\t\tthrow new Error('[courier] adminToken requires @fonderie/store (db templates)');\n\t\t\t}\n\t\t\tfor (const [method, path, handler] of buildTemplateAdminRoutes(store, this.config.adminToken)) {\n\t\t\t\tapp.addRoute(method, path, handler);\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunction createTemplateResolver(\n\tsource: 'db' | 'fs',\n\tconfig: ICourierConfig,\n\tstore?: IStoreAdapter,\n): ITemplateResolver {\n\t// Aggregate the module-shipped defaults (one map or an array) into a single\n\t// lookup, mirroring how getMigrationsPath() results are collected at boot.\n\tconst input = config.templates?.defaults;\n\tconst defaults = new DefaultTemplates(input ? (Array.isArray(input) ? input : [input]) : []);\n\n\tif (source === 'fs') {\n\t\treturn new FSTemplateResolver(config.templates?.directory ?? './templates', defaults);\n\t}\n\n\tif (!store) {\n\t\tthrow new Error('[courier] store is required for DB template resolution');\n\t}\n\n\treturn new DBTemplateResolver(store, defaults);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nexport type MessageLogStatus =\n\t| 'pending' | 'sent' | 'failed'\n\t| 'delivered' | 'opened' | 'clicked' | 'bounced' | 'spam';\n\nexport interface IMessageLog {\n\tid: string;\n\tmessageType: string;\n\tchannel: string;\n\trecipient: string;\n\tlocale: string | null;\n\tstatus: MessageLogStatus;\n\terror: string | null;\n\tattempts: number;\n\tprovider: string | null;\n\tproviderMessageId: string | null;\n\topenedAt: string | null;\n\tclickedAt: string | null;\n\tbouncedAt: string | null;\n\tbounceReason: string | null;\n\tcreatedAt: string;\n\tsentAt: string | null;\n}\n\nexport async function insertMessageLog(\n\tentry: {\n\t\tmessageType: string;\n\t\tchannel: string;\n\t\trecipient: string;\n\t\tlocale?: string;\n\t\tprovider?: string;\n\t\tproviderMessageId?: string;\n\t},\n\tstore: IStoreAdapter,\n): Promise<string> {\n\tconst [row] = await store.query<{ id: string }>(\n\t\t`INSERT INTO fonderie_message_log\n\t\t\t(message_type, channel, recipient, locale, provider, provider_message_id)\n\t\t VALUES ($1, $2, $3, $4, $5, $6)\n\t\t RETURNING id`,\n\t\t[\n\t\t\tentry.messageType,\n\t\t\tentry.channel,\n\t\t\tentry.recipient,\n\t\t\tentry.locale ?? null,\n\t\t\tentry.provider ?? null,\n\t\t\tentry.providerMessageId ?? null,\n\t\t],\n\t);\n\treturn row?.id ?? '';\n}\n\nexport async function markMessageSent(id: string, store: IStoreAdapter): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'sent', sent_at = now(), attempts = attempts + 1\n\t\t WHERE id = $1`,\n\t\t[id],\n\t);\n}\n\nexport async function markMessageFailed(\n\tid: string,\n\terror: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'failed', error = $2, attempts = attempts + 1\n\t\t WHERE id = $1`,\n\t\t[id, error],\n\t);\n}\n\nexport async function markMessageDelivered(\n\tproviderMessageId: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'delivered'\n\t\t WHERE provider_message_id = $1`,\n\t\t[providerMessageId],\n\t);\n}\n\nexport async function markMessageOpened(\n\tproviderMessageId: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'opened', opened_at = now()\n\t\t WHERE provider_message_id = $1 AND opened_at IS NULL`,\n\t\t[providerMessageId],\n\t);\n}\n\nexport async function markMessageClicked(\n\tproviderMessageId: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'clicked', clicked_at = now()\n\t\t WHERE provider_message_id = $1 AND clicked_at IS NULL`,\n\t\t[providerMessageId],\n\t);\n}\n\nexport async function markMessageBounced(\n\tproviderMessageId: string,\n\treason: string,\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tawait store.query(\n\t\t`UPDATE fonderie_message_log\n\t\t SET status = 'bounced', bounced_at = now(), bounce_reason = $2\n\t\t WHERE provider_message_id = $1`,\n\t\t[providerMessageId, reason],\n\t);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ICourierConfig } from './config';\nimport type { ICourierMessage, ICourierChannel, ITemplateResolver } from './types';\nimport { insertMessageLog, markMessageSent, markMessageFailed } from './log';\n\nfunction resolveRecipient(message: ICourierMessage, channel: string): string {\n\tif (channel === 'email') return message.recipient.email ?? '';\n\tif (channel === 'sms') return message.recipient.phone ?? '';\n\tif (channel === 'push') return message.recipient.deviceToken ?? '';\n\treturn message.recipient.email ?? message.recipient.phone ?? '';\n}\n\nexport class Dispatcher {\n\tprivate channels: Map<string, ICourierChannel> = new Map();\n\n\tconstructor(\n\t\tprivate config: ICourierConfig,\n\t\tprivate resolver: ITemplateResolver,\n\t\tprivate store?: IStoreAdapter,\n\t) {}\n\n\tregisterChannel(channel: ICourierChannel): this {\n\t\tthis.channels.set(channel.name, channel);\n\t\treturn this;\n\t}\n\n\t// Names of the currently-registered channels — used by the boot-time config\n\t// guard to detect message types routed to a channel with no provider.\n\tchannelNames(): string[] {\n\t\treturn [...this.channels.keys()];\n\t}\n\n\tasync dispatch(message: ICourierMessage): Promise<void> {\n\t\tconst channelNames = this.config.channels[message.type];\n\n\t\tif (!channelNames || channelNames.length === 0) {\n\t\t\tconsole.warn(`[courier] no channels configured for message type: ${message.type}`);\n\t\t\treturn;\n\t\t}\n\n\t\tconst template = await this.resolver.resolve(message.type, message.data, message.locale);\n\n\t\tawait Promise.allSettled(\n\t\t\tchannelNames.map(async (name) => {\n\t\t\t\tconst channel = this.channels.get(name);\n\t\t\t\tif (!channel) {\n\t\t\t\t\tconsole.warn(`[courier] channel \"${name}\" not registered`);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Insert log entry (fire-and-forget if store unavailable)\n\t\t\t\tconst logEntry: Parameters<typeof insertMessageLog>[0] = {\n\t\t\t\t\tmessageType: message.type,\n\t\t\t\t\tchannel: name,\n\t\t\t\t\trecipient: resolveRecipient(message, name),\n\t\t\t\t};\n\t\t\t\tif (message.locale) logEntry.locale = message.locale;\n\n\t\t\t\tconst logId = this.store\n\t\t\t\t\t? await insertMessageLog(logEntry, this.store).catch(() => '')\n\t\t\t\t\t: '';\n\n\t\t\t\ttry {\n\t\t\t\t\tawait channel.send(message, template);\n\n\t\t\t\t\tif (this.store && logId) {\n\t\t\t\t\t\tmarkMessageSent(logId, this.store).catch(() => undefined);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tconsole.error(`[courier:${name}] failed to send ${message.type}:`, err);\n\n\t\t\t\t\tif (this.store && logId) {\n\t\t\t\t\t\tmarkMessageFailed(logId, errMsg, this.store).catch(() => undefined);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\t}\n}\n","import type { ISmsChannelConfig } from '../config';\nimport type { ICourierChannel, ICourierMessage, IRenderedTemplate } from '../types';\n\nexport class SmsChannel implements ICourierChannel {\n\treadonly name = 'sms';\n\n\tconstructor(private config: ISmsChannelConfig) {}\n\n\tasync send(message: ICourierMessage, template: IRenderedTemplate): Promise<void> {\n\t\tconst to = message.recipient.phone;\n\t\tif (!to) {\n\t\t\tconsole.warn('[courier:sms] no phone number for recipient — skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.config.provider === 'twilio') {\n\t\t\tawait this.sendViaTwilio(to, template.text);\n\t\t} else if (this.config.provider === 'vonage') {\n\t\t\tawait this.sendViaVonage(to, template.text);\n\t\t} else {\n\t\t\tconsole.warn(`[courier:sms] provider ${this.config.provider} not implemented`);\n\t\t}\n\t}\n\n\tprivate async sendViaTwilio(to: string, text: string): Promise<void> {\n\t\tconst { accountSid, authToken } = this.config;\n\t\tif (!accountSid || !authToken) {\n\t\t\tthrow new Error('Twilio accountSid and authToken required');\n\t\t}\n\n\t\tconst res = await fetch(\n\t\t\t`https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`,\n\t\t\t{\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Basic ${Buffer.from(`${accountSid}:${authToken}`).toString('base64')}`,\n\t\t\t\t\t'Content-Type': 'application/x-www-form-urlencoded',\n\t\t\t\t},\n\t\t\t\tbody: new URLSearchParams({ From: this.config.from, To: to, Body: text }),\n\t\t\t},\n\t\t);\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:sms] Twilio error ${res.status}: ${body}`);\n\t\t}\n\t}\n\n\tprivate async sendViaVonage(to: string, text: string): Promise<void> {\n\t\tconst { apiKey, apiSecret } = this.config;\n\t\tif (!apiKey || !apiSecret) {\n\t\t\tthrow new Error('Vonage apiKey and apiSecret required');\n\t\t}\n\n\t\tconst res = await fetch('https://rest.nexmo.com/sms/json', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({\n\t\t\t\tapi_key: apiKey,\n\t\t\t\tapi_secret: apiSecret,\n\t\t\t\tfrom: this.config.from,\n\t\t\t\tto,\n\t\t\t\ttext,\n\t\t\t}),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:sms] Vonage error ${res.status}: ${body}`);\n\t\t}\n\t}\n}\n","import type { IPushChannelConfig } from '../config';\nimport type { ICourierChannel, ICourierMessage, IRenderedTemplate } from '../types';\n\nexport class PushChannel implements ICourierChannel {\n\treadonly name = 'push';\n\n\tconstructor(private config: IPushChannelConfig) {}\n\n\tasync send(message: ICourierMessage, template: IRenderedTemplate): Promise<void> {\n\t\tconst token = message.recipient.deviceToken;\n\t\tif (!token) {\n\t\t\tconsole.warn('[courier:push] no device token for recipient — skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.config.provider === 'fcm') {\n\t\t\tawait this.sendViaFCM(token, template);\n\t\t}\n\t}\n\n\tprivate async sendViaFCM(token: string, template: IRenderedTemplate): Promise<void> {\n\t\tconst apiKey = this.config.serviceAccount['apiKey'] as string | undefined;\n\t\tif (!apiKey) {\n\t\t\tthrow new Error('[courier:push] FCM apiKey is required in serviceAccount');\n\t\t}\n\n\t\tconst res = await fetch('https://fcm.googleapis.com/fcm/send', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\tAuthorization: `key=${apiKey}`,\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tto: token,\n\t\t\t\tnotification: {\n\t\t\t\t\ttitle: template.subject,\n\t\t\t\t\tbody: template.text,\n\t\t\t\t},\n\t\t\t}),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:push] FCM error ${res.status}: ${body}`);\n\t\t}\n\t}\n}\n","import nodemailer from 'nodemailer';\n\nimport type { ICourierChannel, ICourierMessage, IRenderedTemplate } from '../types';\nimport type { IEmailChannelConfig } from '../config';\n\nexport class EmailChannel implements ICourierChannel {\n\treadonly name = 'email';\n\tprivate transport: ReturnType<typeof nodemailer.createTransport> | null = null;\n\n\tconstructor(private config: IEmailChannelConfig) {\n\t\tif (config.provider === 'smtp' && config.smtp) {\n\t\t\tthis.transport = nodemailer.createTransport({\n\t\t\t\thost: config.smtp.host,\n\t\t\t\tport: config.smtp.port,\n\t\t\t\tsecure: config.smtp.secure,\n\t\t\t\tauth: { user: config.smtp.user, pass: config.smtp.pass },\n\t\t\t});\n\t\t}\n\t}\n\n\tasync send(message: ICourierMessage, template: IRenderedTemplate): Promise<void> {\n\t\tconst to = message.recipient.email;\n\t\tif (!to) {\n\t\t\tconsole.warn('[courier:email] no email address for recipient — skipping');\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.config.provider === 'resend') {\n\t\t\tawait this.sendViaResend(to, template);\n\t\t} else if (this.config.provider === 'smtp') {\n\t\t\tawait this.sendViaSMTP(to, template);\n\t\t} else {\n\t\t\tconsole.warn(`[courier:email] provider ${this.config.provider} not implemented`);\n\t\t}\n\t}\n\n\tprivate async sendViaResend(to: string, template: IRenderedTemplate): Promise<void> {\n\t\tif (!this.config.apiKey) {\n\t\t\tthrow new Error('Resend apiKey is required');\n\t\t}\n\n\t\tconst res = await fetch('https://api.resend.com/emails', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${this.config.apiKey}`,\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tfrom: this.config.from,\n\t\t\t\tto,\n\t\t\t\tsubject: template.subject ?? '(no subject)',\n\t\t\t\thtml: template.html,\n\t\t\t\ttext: template.text,\n\t\t\t}),\n\t\t});\n\n\t\tif (!res.ok) {\n\t\t\tconst body = await res.text();\n\t\t\tthrow new Error(`[courier:email] Resend error ${res.status}: ${body}`);\n\t\t}\n\t}\n\n\tprivate async sendViaSMTP(to: string, template: IRenderedTemplate): Promise<void> {\n\t\tif (!this.transport) {\n\t\t\tthrow new Error('SMTP transport not initialised — check smtp config');\n\t\t}\n\n\t\tawait this.transport.sendMail({\n\t\t\tfrom: this.config.from,\n\t\t\tto,\n\t\t\tsubject: template.subject ?? '(no subject)',\n\t\t\thtml: template.html,\n\t\t\ttext: template.text,\n\t\t});\n\t}\n}\n","// Shared, composable email layout — a branded shell that wraps every template\n// body. Templates stored in the DB or on disk are **body fragments**; the\n// resolver injects them into `{{content}}` here, then interpolates `{{vars}}`\n// over the whole. One shell, many bodies — so DB and FS sources render an\n// identical, production-grade frame for free.\n//\n// Cross-client hardening (Outlook/Gmail/Apple Mail) follows the well-worn\n// \"hybrid responsive\" pattern popularised by Cerberus (MIT): a max-width table\n// container, inline styles on the critical structure, and a `<style>` block in\n// `<head>` for the friendly classes and small-screen media query. Founders can\n// override the whole shell by storing a `_layout` template (DB row or\n// `_layout.html` file) in their own source.\n//\n// The theme tokens mirror the product design system (organization/ui/base.css):\n// near-black primary, a mint brand accent, Inter, and the `#fafafa` canvas —\n// retune `EMAIL_THEME` to rebrand every email at once.\n\n// A single small token set — retune these to match your brand.\nexport const EMAIL_THEME = {\n\tbrand: 'Fonderie',\n\taccent: '#171717', // primary action (button) background — the product's primary\n\taccentText: '#ffffff', // text on the accent\n\tbrandAccent: '#171717', // brand highlight (top rule) — monochrome near-black\n\tlink: '#009767', // links — the accessible (darkened) brand teal\n\tink: '#171717', // body copy\n\tmuted: '#5c5c5c', // secondary copy\n\tline: '#e0e0e0', // hairline borders\n\tcanvas: '#fafafa', // page background behind the card\n\tcard: '#ffffff', // the card itself\n} as const;\n\n// The product font stack (Inter first, with system fallbacks for clients that\n// lack it). Shared by every text element so the frame stays consistent.\nconst FONT_SANS =\n\t\"Inter, 'Inter Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif\";\nconst FONT_MONO = \"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace\";\n\n// The `{{content}}` marker is replaced with the body fragment BEFORE variable\n// interpolation, so a body's own `{{firstName}}` etc. still resolve.\nexport const LAYOUT_CONTENT_SLOT = '{{content}}';\n\nexport const DEFAULT_EMAIL_LAYOUT = `<!DOCTYPE html>\n<html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n<head>\n\t<meta charset=\"utf-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"color-scheme\" content=\"light dark\">\n\t<meta name=\"supported-color-schemes\" content=\"light dark\">\n\t<title>{{subject}}</title>\n\t<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->\n\t<style>\n\t\tbody { margin: 0; padding: 0; width: 100% !important; background: ${EMAIL_THEME.canvas}; }\n\t\t.email-body { background: ${EMAIL_THEME.canvas}; }\n\t\t.email-container { width: 100%; max-width: 560px; margin: 0 auto; }\n\t\t.email-card {\n\t\t\tbackground: ${EMAIL_THEME.card};\n\t\t\tborder: 1px solid ${EMAIL_THEME.line};\n\t\t\tborder-radius: 8px;\n\t\t\toverflow: hidden;\n\t\t}\n\t\t.email-accent { height: 3px; background: ${EMAIL_THEME.brandAccent}; font-size: 0; line-height: 0; }\n\t\t.email-header { padding: 28px 32px 0 32px; }\n\t\t.email-brand { font: 800 19px/1 ${FONT_SANS}; color: ${EMAIL_THEME.ink}; letter-spacing: -0.03em; }\n\t\t.email-content {\n\t\t\tpadding: 20px 32px 8px 32px;\n\t\t\tfont: 400 16px/1.7 ${FONT_SANS};\n\t\t\tcolor: ${EMAIL_THEME.ink};\n\t\t\tletter-spacing: -0.01em;\n\t\t}\n\t\t.email-content h1 { margin: 0 0 12px 0; font-size: 22px; line-height: 1.3; font-weight: 700; letter-spacing: -0.03em; }\n\t\t.email-content p { margin: 0 0 16px 0; }\n\t\t.email-content a { color: ${EMAIL_THEME.link}; }\n\t\t.email-footer {\n\t\t\tpadding: 16px 32px 28px 32px;\n\t\t\tfont: 400 13px/1.6 ${FONT_SANS};\n\t\t\tcolor: ${EMAIL_THEME.muted};\n\t\t\tletter-spacing: -0.01em;\n\t\t}\n\t\t.btn {\n\t\t\tdisplay: inline-block;\n\t\t\tbackground: ${EMAIL_THEME.accent};\n\t\t\tcolor: ${EMAIL_THEME.accentText} !important;\n\t\t\ttext-decoration: none;\n\t\t\tfont: 600 16px/1 ${FONT_SANS};\n\t\t\tletter-spacing: -0.01em;\n\t\t\tpadding: 13px 26px;\n\t\t\tborder-radius: 6px;\n\t\t}\n\t\t.pin-code {\n\t\t\tdisplay: inline-block;\n\t\t\tfont: 700 30px/1 ${FONT_MONO};\n\t\t\tletter-spacing: 0.28em;\n\t\t\tcolor: ${EMAIL_THEME.ink};\n\t\t\tbackground: ${EMAIL_THEME.canvas};\n\t\t\tborder: 1px solid ${EMAIL_THEME.line};\n\t\t\tborder-radius: 6px;\n\t\t\tpadding: 14px 22px 14px 30px;\n\t\t}\n\t\t.muted { color: ${EMAIL_THEME.muted}; }\n\t\t@media only screen and (max-width: 599px) {\n\t\t\t.email-header, .email-content, .email-footer { padding-left: 22px !important; padding-right: 22px !important; }\n\t\t\t.pin-code { font-size: 26px !important; letter-spacing: 0.2em !important; }\n\t\t}\n\t</style>\n</head>\n<body class=\"email-body\">\n\t<div style=\"display:none;max-height:0;overflow:hidden;opacity:0;\">{{preheader}}</div>\n\t<table role=\"presentation\" class=\"email-body\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"background:${EMAIL_THEME.canvas};\">\n\t\t<tr>\n\t\t\t<td align=\"center\" style=\"padding: 32px 16px;\">\n\t\t\t\t<table role=\"presentation\" class=\"email-container\" width=\"560\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"max-width:560px;\">\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td class=\"email-card\" style=\"background:${EMAIL_THEME.card};border:1px solid ${EMAIL_THEME.line};border-radius:8px;\">\n\t\t\t\t\t\t\t<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n\t\t\t\t\t\t\t\t<tr><td class=\"email-accent\" style=\"height:3px;background:${EMAIL_THEME.brandAccent};font-size:0;line-height:0;\"> </td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-header\" style=\"padding:28px 32px 0 32px;\">\n\t\t\t\t\t\t\t\t\t<span class=\"email-brand\" style=\"font-weight:800;font-size:19px;color:${EMAIL_THEME.ink};letter-spacing:-0.03em;\">${EMAIL_THEME.brand}</span>\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-content\" style=\"padding:20px 32px 8px 32px;color:${EMAIL_THEME.ink};\">\n${LAYOUT_CONTENT_SLOT}\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t\t<tr><td class=\"email-footer\" style=\"padding:16px 32px 28px 32px;color:${EMAIL_THEME.muted};font-size:13px;\">\n\t\t\t\t\t\t\t\t\tYou're receiving this because someone used this address at ${EMAIL_THEME.brand}. If that wasn't you, you can ignore it.\n\t\t\t\t\t\t\t\t</td></tr>\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t</table>\n\t\t\t</td>\n\t\t</tr>\n\t</table>\n</body>\n</html>`;\n\n// Inject a body fragment into a layout shell. If the body is already a full\n// HTML document (a founder chose to store a complete template), it is returned\n// untouched — never double-wrapped.\nexport function wrapLayout(bodyHtml: string, layoutHtml: string = DEFAULT_EMAIL_LAYOUT): string {\n\tconst trimmed = bodyHtml.trimStart().toLowerCase();\n\tif (trimmed.startsWith('<!doctype') || trimmed.startsWith('<html')) {\n\t\treturn bodyHtml;\n\t}\n\treturn layoutHtml.replace(LAYOUT_CONTENT_SLOT, bodyHtml);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { ITemplateResolver, IRenderedTemplate, IDefaultTemplate, DefaultTemplateMap } from '../types';\nimport { wrapLayout } from './layout';\n\n// The stored template id for a founder-supplied layout shell (DB row `type` or\n// FS file `_layout.html`). Absent → the built-in DEFAULT_EMAIL_LAYOUT is used.\nconst LAYOUT_TYPE = '_layout';\n\nfunction render(template: string, data: Record<string, unknown>): string {\n\treturn template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key: string) => {\n\t\tconst value = data[key];\n\t\treturn value !== undefined && value !== null ? String(value) : '';\n\t});\n}\n\n// Compose a body fragment into its layout shell, then interpolate variables\n// over the whole. `subject`/`preheader` become available to the shell's title\n// and inbox preview text.\nfunction composeHtml(\n\tbodyHtml: string,\n\tlayoutHtml: string | undefined,\n\tsubject: string | undefined,\n\tdata: Record<string, unknown>,\n): string {\n\tconst wrapped = wrapLayout(bodyHtml, layoutHtml);\n\treturn render(wrapped, { subject: subject ?? '', preheader: '', ...data });\n}\n\n// Render a resolved fragment — a DB row, an FS file set, or a MODULE DEFAULT —\n// into the final template. Factored out so a module default renders\n// byte-identically to a DB row: same {{var}} interpolation, same layout\n// composition. `text` is required; `subject`/`html` are optional (email only).\nexport function renderFragment(\n\tfrag: { subject?: string | null; text: string; html?: string | null },\n\tlayoutHtml: string | undefined,\n\tdata: Record<string, unknown>,\n): IRenderedTemplate {\n\tconst subject = frag.subject ? render(frag.subject, data) : undefined;\n\treturn {\n\t\ttext: render(frag.text, data),\n\t\t...(subject ? { subject } : {}),\n\t\t...(frag.html ? { html: composeHtml(frag.html, layoutHtml, subject, data) } : {}),\n\t};\n}\n\n// The module-shipped default templates an app hands courier, merged into one\n// lookup (aggregated like getMigrationsPath()). Per-key app overrides — a DB row\n// or an FS file — always win over a default; the default wins over the\n// last-resort JSON dump. A later map wins on key collision.\nexport class DefaultTemplates {\n\tprivate readonly map = new Map<string, IDefaultTemplate>();\n\tconstructor(maps: DefaultTemplateMap[] = []) {\n\t\tfor (const m of maps) {\n\t\t\tfor (const [key, tmpl] of Object.entries(m)) this.map.set(key, tmpl);\n\t\t}\n\t}\n\tget(type: string): IDefaultTemplate | undefined {\n\t\treturn this.map.get(type);\n\t}\n\tget size(): number {\n\t\treturn this.map.size;\n\t}\n}\n\n// DB-backed resolver — reads from fonderie_courier_templates with locale fallback\nexport class DBTemplateResolver implements ITemplateResolver {\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate defaults?: DefaultTemplates,\n\t) {}\n\n\tasync resolve(\n\t\ttype: string,\n\t\tdata: Record<string, unknown>,\n\t\tlocale?: string,\n\t): Promise<IRenderedTemplate> {\n\t\tconst [row] = await this.store.query<{\n\t\t\tsubject: string | null;\n\t\t\thtml: string | null;\n\t\t\ttext: string;\n\t\t}>(\n\t\t\t// Serve the exact locale, else the neutral NULL default — never a\n\t\t\t// sibling region (en-CA must not fall back to en-US); the WHERE\n\t\t\t// excludes other locales so legal/jurisdictional copy can't bleed.\n\t\t\t`SELECT subject, html, text\n\t\t\t FROM fonderie_courier_templates\n\t\t\t WHERE type = $1 AND active = true AND (locale = $2 OR locale IS NULL)\n\t\t\t ORDER BY (locale IS NOT DISTINCT FROM $2) DESC\n\t\t\t LIMIT 1`,\n\t\t\t[type, locale ?? null],\n\t\t);\n\n\t\tif (!row) {\n\t\t\t// No app row → fall back to the module default (rendered identically),\n\t\t\t// then to the JSON dump — now only reached for a type neither the app\n\t\t\t// nor any module provides.\n\t\t\tconst def = this.defaults?.get(type);\n\t\t\tif (def) {\n\t\t\t\treturn renderFragment(def, def.html ? await this.layout(locale) : undefined, data);\n\t\t\t}\n\t\t\treturn { text: `${type}: ${JSON.stringify(data)}` };\n\t\t}\n\n\t\tconst layoutHtml = row.html ? await this.layout(locale) : undefined;\n\t\treturn renderFragment(row, layoutHtml, data);\n\t}\n\n\t// Optional founder-supplied layout shell; undefined → built-in default.\n\tprivate async layout(locale?: string): Promise<string | undefined> {\n\t\tconst [row] = await this.store.query<{ html: string | null }>(\n\t\t\t`SELECT html\n\t\t\t FROM fonderie_courier_templates\n\t\t\t WHERE type = $1 AND active = true AND (locale = $2 OR locale IS NULL)\n\t\t\t ORDER BY (locale IS NOT DISTINCT FROM $2) DESC\n\t\t\t LIMIT 1`,\n\t\t\t[LAYOUT_TYPE, locale ?? null],\n\t\t);\n\t\treturn row?.html ?? undefined;\n\t}\n}\n\n// Filesystem resolver — reads {type}.{locale}.txt → {type}.txt with fallback\nexport class FSTemplateResolver implements ITemplateResolver {\n\tconstructor(\n\t\tprivate directory: string,\n\t\tprivate defaults?: DefaultTemplates,\n\t) {}\n\n\tasync resolve(\n\t\ttype: string,\n\t\tdata: Record<string, unknown>,\n\t\tlocale?: string,\n\t): Promise<IRenderedTemplate> {\n\t\tconst { readFile } = await import('node:fs/promises');\n\t\tconst { join } = await import('node:path');\n\n\t\tconst readOptional = async (path: string): Promise<string | null> => {\n\t\t\ttry {\n\t\t\t\treturn await readFile(path, 'utf8');\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t};\n\n\t\t// Per-locale variants take priority over generic variants\n\t\tconst localePrefix = locale ? `${type}.${locale}` : null;\n\t\tconst layoutPrefix = locale ? `${LAYOUT_TYPE}.${locale}` : null;\n\n\t\tconst [text, html, subject, layout] = await Promise.all([\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.txt`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.txt`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.txt`)),\n\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.html`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.html`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.html`)),\n\n\t\t\tlocalePrefix\n\t\t\t\t? readOptional(join(this.directory, `${localePrefix}.subject.txt`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${type}.subject.txt`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${type}.subject.txt`)),\n\n\t\t\tlayoutPrefix\n\t\t\t\t? readOptional(join(this.directory, `${layoutPrefix}.html`)).then(\n\t\t\t\t\t\t(v) => v ?? readOptional(join(this.directory, `${LAYOUT_TYPE}.html`)),\n\t\t\t\t\t)\n\t\t\t\t: readOptional(join(this.directory, `${LAYOUT_TYPE}.html`)),\n\t\t]);\n\n\t\t// App shipped NOTHING for this type (no text/html/subject file) → fall back\n\t\t// to the module default, using the app's own _layout shell if it ships one,\n\t\t// then to the JSON dump. Test file PRESENCE (=== null), not truthiness, so an\n\t\t// app that ships even an empty file keeps per-key control below — mirroring\n\t\t// the DB resolver's row-presence check, not overriding it with a default.\n\t\tif (text === null && html === null && subject === null) {\n\t\t\tconst def = this.defaults?.get(type);\n\t\t\tif (def) {\n\t\t\t\treturn renderFragment(def, def.html ? (layout ?? undefined) : undefined, data);\n\t\t\t}\n\t\t\treturn { text: `${type}: ${JSON.stringify(data)}` };\n\t\t}\n\n\t\tconst renderedSubject = subject ? render(subject, data) : undefined;\n\n\t\treturn {\n\t\t\ttext: text ? render(text, data) : `${type}: ${JSON.stringify(data)}`,\n\t\t\t...(renderedSubject ? { subject: renderedSubject } : {}),\n\t\t\t...(html ? { html: composeHtml(html, layout ?? undefined, renderedSubject, data) } : {}),\n\t\t};\n\t}\n}\n","import type { IReadinessProblem } from '@fonderie/core';\nimport type { ICourierConfig } from './config';\n\nconst MODULE = '@fonderie/courier';\n\n// Pure production-readiness assessment (no side effects) — shared by the\n// boot-time guard and `CourierModule.checkReadiness`. A message type routed to a\n// channel with no registered provider is silently dropped by the dispatcher —\n// with `requireVerification` on, verification / reset emails vanish and users\n// lock themselves out. These are `warning`-severity (channel setups vary\n// legitimately — an app may run in-app only — so this never blocks boot, unlike\n// auth's fatal weak-secret). Checks the channels actually **registered** on the\n// dispatcher, so channels added via `registerChannel` count as present.\nexport function collectCourierConfigProblems(\n\tconfig: ICourierConfig,\n\tregisteredChannels: Iterable<string>,\n): IReadinessProblem[] {\n\tconst registered = new Set(registeredChannels);\n\tconst gaps = new Map<string, string[]>(); // channel → message types routed to it\n\n\tfor (const [type, channels] of Object.entries(config.channels ?? {})) {\n\t\tfor (const channel of channels) {\n\t\t\tif (!registered.has(channel)) {\n\t\t\t\tconst types = gaps.get(channel) ?? [];\n\t\t\t\ttypes.push(type);\n\t\t\t\tgaps.set(channel, types);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn [...gaps].map(([channel, types]) => ({\n\t\tmodule: MODULE,\n\t\tseverity: 'warning' as const,\n\t\tmessage:\n\t\t\t`${types.length} message type(s) route to the '${channel}' channel but ` +\n\t\t\t`no '${channel}' provider is registered — these will be silently dropped: ` +\n\t\t\t`${types.join(', ')}. Configure \\`config.${channel}\\` (or register a channel).`,\n\t}));\n}\n\n// Boot-time preflight, run from `CourierModule.install`. Warn-only.\nexport function validateCourierConfig(\n\tconfig: ICourierConfig,\n\tregisteredChannels: Iterable<string>,\n): void {\n\tfor (const problem of collectCourierConfigProblems(config, registeredChannels)) {\n\t\tconsole.warn(`[courier] ${problem.message}`);\n\t}\n}\n","import { createHmac } from 'node:crypto';\n\nimport { constantTimeEqual } from '@fonderie/core';\n\nimport type { IStoreAdapter } from '@fonderie/store';\nimport {\n\tmarkMessageDelivered,\n\tmarkMessageOpened,\n\tmarkMessageClicked,\n\tmarkMessageBounced,\n} from './log';\n\n// ── SendGrid ──────────────────────────────────────────────────────\n//\n// Verifies the X-Twilio-Email-Event-Webhook-Signature header.\n// Expects an array of event objects in the request body.\n// https://docs.sendgrid.com/for-developers/tracking-events/getting-started-event-webhook-security-features\n\nexport async function handleSendGridDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n\twebhookSecret?: string,\n): Promise<Response> {\n\tif (webhookSecret) {\n\t\tconst sig = req.headers.get('x-twilio-email-event-webhook-signature') ?? '';\n\t\tconst ts = req.headers.get('x-twilio-email-event-webhook-timestamp') ?? '';\n\t\tconst body = await req.text();\n\n\t\tif (!verifySendGridSignature(webhookSecret, ts, body, sig)) {\n\t\t\treturn Response.json({ error: 'INVALID_SIGNATURE' }, { status: 401 });\n\t\t}\n\n\t\tconst events = parseJson(body) as SendGridEvent[] | null;\n\t\tif (!Array.isArray(events)) return Response.json({ ok: true });\n\n\t\tawait processSendGridEvents(events, store);\n\t\treturn Response.json({ ok: true });\n\t}\n\n\tconst events = (await req.json()) as SendGridEvent[];\n\tif (!Array.isArray(events)) return Response.json({ ok: true });\n\n\tawait processSendGridEvents(events, store);\n\treturn Response.json({ ok: true });\n}\n\nfunction verifySendGridSignature(\n\tsecret: string,\n\ttimestamp: string,\n\tbody: string,\n\tsignature: string,\n): boolean {\n\ttry {\n\t\tconst payload = timestamp + body;\n\t\tconst expected = createHmac('sha256', secret).update(payload).digest('base64');\n\t\treturn constantTimeEqual(Buffer.from(signature, 'base64'), Buffer.from(expected, 'base64'));\n\t} catch {\n\t\treturn false;\n\t}\n}\n\ninterface SendGridEvent {\n\tevent: string;\n\tsg_message_id?: string;\n\treason?: string;\n\t[key: string]: unknown;\n}\n\nasync function processSendGridEvents(\n\tevents: SendGridEvent[],\n\tstore: IStoreAdapter,\n): Promise<void> {\n\tfor (const ev of events) {\n\t\tconst msgId = ev['sg_message_id'];\n\t\tif (!msgId || typeof msgId !== 'string') continue;\n\t\t// sg_message_id is \"msgId.filterXxx\" — strip the filter suffix\n\t\tconst id = msgId.split('.')[0] ?? msgId;\n\n\t\tswitch (ev.event) {\n\t\t\tcase 'delivered': await markMessageDelivered(id, store); break;\n\t\t\tcase 'open': await markMessageOpened(id, store); break;\n\t\t\tcase 'click': await markMessageClicked(id, store); break;\n\t\t\tcase 'bounce':\n\t\t\tcase 'blocked':\n\t\t\tcase 'dropped':\n\t\t\t\tawait markMessageBounced(id, typeof ev['reason'] === 'string' ? ev['reason'] : ev.event, store);\n\t\t\t\tbreak;\n\t\t}\n\t}\n}\n\n// ── Mailgun ───────────────────────────────────────────────────────\n//\n// Verifies signature using token + timestamp + signing key (HMAC-SHA256).\n// https://documentation.mailgun.com/docs/mailgun/user-manual/tracking-messages/#securing-webhooks\n\nexport async function handleMailgunDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n\tsigningKey?: string,\n): Promise<Response> {\n\tconst body = (await req.json()) as MailgunPayload;\n\n\tif (signingKey) {\n\t\tconst { signature } = body;\n\t\tif (!signature || !verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {\n\t\t\treturn Response.json({ error: 'INVALID_SIGNATURE' }, { status: 401 });\n\t\t}\n\t}\n\n\tconst event = body['event-data'];\n\tif (event) {\n\t\tawait processMailgunEvent(event, store);\n\t}\n\n\treturn Response.json({ ok: true });\n}\n\nfunction verifyMailgunSignature(\n\tsigningKey: string,\n\ttimestamp: string,\n\ttoken: string,\n\tsignature: string,\n): boolean {\n\ttry {\n\t\tconst value = timestamp + token;\n\t\tconst expected = createHmac('sha256', signingKey).update(value).digest('hex');\n\t\treturn constantTimeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'));\n\t} catch {\n\t\treturn false;\n\t}\n}\n\ninterface MailgunPayload {\n\tsignature?: { timestamp: string; token: string; signature: string };\n\t'event-data'?: MailgunEvent;\n\t[key: string]: unknown;\n}\n\ninterface MailgunEvent {\n\tevent: string;\n\tmessage?: { headers?: { 'message-id'?: string } };\n\t'delivery-status'?: { message?: string };\n\t[key: string]: unknown;\n}\n\nasync function processMailgunEvent(event: MailgunEvent, store: IStoreAdapter): Promise<void> {\n\tconst msgId = event.message?.headers?.['message-id'];\n\tif (!msgId) return;\n\n\tswitch (event.event) {\n\t\tcase 'delivered': await markMessageDelivered(msgId, store); break;\n\t\tcase 'opened': await markMessageOpened(msgId, store); break;\n\t\tcase 'clicked': await markMessageClicked(msgId, store); break;\n\t\tcase 'failed':\n\t\tcase 'bounced': {\n\t\t\tconst reason = event['delivery-status']?.message ?? event.event;\n\t\t\tawait markMessageBounced(msgId, reason, store);\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n// ── Mailtrap (testing only, no signature) ────────────────────────\n\nexport async function handleMailtrapDelivery(\n\treq: Request,\n\tstore: IStoreAdapter,\n): Promise<Response> {\n\tconst events = (await req.json()) as MailtrapEvent[];\n\tif (!Array.isArray(events)) return Response.json({ ok: true });\n\n\tfor (const ev of events) {\n\t\tconst msgId = ev.message_id;\n\t\tif (!msgId) continue;\n\n\t\tswitch (ev.event) {\n\t\t\tcase 'delivery': await markMessageDelivered(msgId, store); break;\n\t\t\tcase 'open': await markMessageOpened(msgId, store); break;\n\t\t\tcase 'click': await markMessageClicked(msgId, store); break;\n\t\t\tcase 'bounce':\n\t\t\tcase 'soft_bounce':\n\t\t\t\tawait markMessageBounced(msgId, ev.event, store);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn Response.json({ ok: true });\n}\n\ninterface MailtrapEvent {\n\tevent: string;\n\tmessage_id?: string;\n\t[key: string]: unknown;\n}\n\n// ── Utility ───────────────────────────────────────────────────────\n\nfunction parseJson(text: string): unknown {\n\ttry { return JSON.parse(text); } catch { return null; }\n}\n","import type { IFonderieContext, Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport { requireAdminToken } from '@fonderie/core/middlewares';\nimport { VersionConflictError } from '@fonderie/store';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport {\n\tsetTemplate,\n\trollbackTemplate,\n\tlistTemplateRevisions,\n\tgetTemplateEntry,\n\tlistTemplateEntries,\n\tdeleteTemplate,\n} from './admin';\n\n// Bearer-token guard (mirrors @fonderie/config's admin surface). Only registered\n// when a token is configured — no token, no exposed template admin routes.\n// Wrap a handler in the shared admin-token guard (@fonderie/core/middlewares) —\n// one constant-time Bearer check across billing/config/courier.\nfunction guarded(adminToken: string, handler: Middleware): Middleware {\n\tconst guard = requireAdminToken(adminToken);\n\treturn (ctx, next) => guard(ctx, () => handler(ctx, next));\n}\n\nconst actorOf = (ctx: IFonderieContext) => ctx.request.headers.get('x-actor') || 'admin-token';\nconst typeOf = (ctx: IFonderieContext) => ctx.meta.params?.['type'] ?? '';\nconst localeOf = (ctx: IFonderieContext): string | null =>\n\tnew URL(ctx.request.url).searchParams.get('locale');\nconst body = (ctx: IFonderieContext): Record<string, unknown> =>\n\t(ctx.meta['body'] as Record<string, unknown> | undefined) ?? {};\n\nfunction conflictOr(err: unknown): Response {\n\tif (err instanceof VersionConflictError) {\n\t\treturn setApiResponse(HTTP.CONFLICT, 'VERSION_CONFLICT', err.message, {\n\t\t\tcurrentVersion: err.currentVersion,\n\t\t});\n\t}\n\tthrow err;\n}\n\n// Build the template admin route table (registered by CourierModule.install when\n// an adminToken is configured).\nexport function buildTemplateAdminRoutes(\n\tstore: IStoreAdapter,\n\tadminToken: string,\n): Array<[string, string, Middleware]> {\n\tconst g = (h: Middleware): Middleware => guarded(adminToken, h);\n\n\treturn [\n\t\t['GET', '/admin/templates', g(async () => {\n\t\t\treturn setApiResponse(HTTP.OK, 'TEMPLATES_LISTED', 'Templates', await listTemplateEntries(store));\n\t\t})],\n\t\t['GET', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst row = await getTemplateEntry(typeOf(ctx), localeOf(ctx), store);\n\t\t\treturn row\n\t\t\t\t? setApiResponse(HTTP.OK, 'TEMPLATE', 'Template', row)\n\t\t\t\t: setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such template');\n\t\t})],\n\t\t['PUT', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tif (typeof b['text'] !== 'string') {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.text (string) is required');\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst opts: Parameters<typeof setTemplate>[0] = {\n\t\t\t\t\ttype: typeOf(ctx),\n\t\t\t\t\ttext: b['text'],\n\t\t\t\t\tlocale: localeOf(ctx),\n\t\t\t\t\tactor: actorOf(ctx),\n\t\t\t\t};\n\t\t\t\tif (typeof b['subject'] === 'string') opts.subject = b['subject'];\n\t\t\t\tif (typeof b['html'] === 'string') opts.html = b['html'];\n\t\t\t\tif (typeof b['active'] === 'boolean') opts.active = b['active'];\n\t\t\t\tif (typeof b['ifVersion'] === 'number') opts.ifVersion = b['ifVersion'];\n\t\t\t\treturn setApiResponse(HTTP.OK, 'TEMPLATE_SET', 'Template saved', await setTemplate(opts, store));\n\t\t\t} catch (err) {\n\t\t\t\treturn conflictOr(err);\n\t\t\t}\n\t\t})],\n\t\t['DELETE', '/admin/templates/:type', g(async (ctx) => {\n\t\t\tconst ok = await deleteTemplate(typeOf(ctx), localeOf(ctx), store);\n\t\t\treturn setApiResponse(ok ? HTTP.OK : HTTP.NOT_FOUND, ok ? 'DELETED' : 'NOT_FOUND', ok ? 'Deleted' : 'No such template');\n\t\t})],\n\t\t['GET', '/admin/templates/:type/revisions', g(async (ctx) => {\n\t\t\treturn setApiResponse(HTTP.OK, 'REVISIONS', 'Template revisions', await listTemplateRevisions(typeOf(ctx), localeOf(ctx), store));\n\t\t})],\n\t\t['POST', '/admin/templates/:type/rollback', g(async (ctx) => {\n\t\t\tconst b = body(ctx);\n\t\t\tconst toVersion = Number(b['toVersion']);\n\t\t\tif (!Number.isInteger(toVersion)) {\n\t\t\t\treturn setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.toVersion (int) is required');\n\t\t\t}\n\t\t\tconst row = await rollbackTemplate(\n\t\t\t\t{ type: typeOf(ctx), locale: localeOf(ctx), toVersion, actor: actorOf(ctx) },\n\t\t\t\tstore,\n\t\t\t);\n\t\t\treturn setApiResponse(HTTP.OK, 'ROLLED_BACK', `Rolled back to v${toVersion}`, row);\n\t\t})],\n\t];\n}\n","import type { IStoreAdapter, IVersionedResource } from '@fonderie/store';\nimport { versionedWrite, versionedRollback } from '@fonderie/store';\n\n// Versioned management for email templates, on the shared `@fonderie/store`\n// primitive. Templates are keyed by (type, locale) — a NULL locale is the base,\n// with per-locale overrides on top — and their content is subject/html/text.\n// The resolver reads the current row unchanged; this adds edit + history +\n// rollback + optimistic concurrency on top.\n\nexport interface ITemplateEntry {\n\ttype: string;\n\tlocale: string | null;\n\tsubject: string | null;\n\thtml: string | null;\n\ttext: string;\n\tactive: boolean;\n\tversion: number;\n\tupdatedBy: string | null;\n\tupdatedAt: string;\n}\n\nexport interface ITemplateRevision {\n\ttype: string;\n\tlocale: string | null;\n\tsubject: string | null;\n\thtml: string | null;\n\ttext: string;\n\tversion: number;\n\tactor: string | null;\n\tcreatedAt: string;\n}\n\nconst ENTRY_COLS = `type, locale, subject, html, text, active, version, updated_by AS \"updatedBy\", updated_at AS \"updatedAt\"`;\n\nconst TEMPLATE_RESOURCE: IVersionedResource = {\n\ttable: 'fonderie_courier_templates',\n\trevisions: 'fonderie_courier_template_revisions',\n\tchannel: 'fonderie_courier_templates_changed',\n\tkeyColumns: ['type', 'locale'],\n\tcontentColumns: ['subject', 'html', 'text'],\n\tmetaColumns: ['active'],\n\treturning: ENTRY_COLS,\n};\n\n// Upsert a template (full body — a revision is a full snapshot). Pass `ifVersion`\n// for optimistic concurrency (else `VersionConflictError`). `locale` omitted /\n// null targets the base template.\nexport async function setTemplate(\n\topts: {\n\t\ttype: string;\n\t\ttext: string;\n\t\tlocale?: string | null;\n\t\tsubject?: string | null;\n\t\thtml?: string | null;\n\t\tactive?: boolean;\n\t\tifVersion?: number;\n\t\tactor?: string;\n\t},\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry> {\n\tconst data: Record<string, unknown> = {\n\t\tsubject: opts.subject ?? null,\n\t\thtml: opts.html ?? null,\n\t\ttext: opts.text,\n\t};\n\tif (opts.active !== undefined) data['active'] = opts.active;\n\treturn versionedWrite<ITemplateEntry>(TEMPLATE_RESOURCE, store, {\n\t\tkey: opts.type,\n\t\tscope: opts.locale ?? null,\n\t\tdata,\n\t\t...(opts.ifVersion !== undefined ? { ifVersion: opts.ifVersion } : {}),\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function rollbackTemplate(\n\topts: { type: string; locale?: string | null; toVersion: number; actor?: string },\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry> {\n\treturn versionedRollback<ITemplateEntry>(TEMPLATE_RESOURCE, store, {\n\t\tkey: opts.type,\n\t\tscope: opts.locale ?? null,\n\t\ttoVersion: opts.toVersion,\n\t\tactor: opts.actor ?? null,\n\t});\n}\n\nexport async function listTemplateRevisions(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<ITemplateRevision[]> {\n\treturn store.query<ITemplateRevision>(\n\t\t`SELECT type, locale, subject, html, text, version, actor, created_at AS \"createdAt\"\n\t\t FROM fonderie_courier_template_revisions\n\t\t WHERE type = $1 AND locale IS NOT DISTINCT FROM $2\n\t\t ORDER BY version DESC`,\n\t\t[type, locale],\n\t);\n}\n\nexport async function getTemplateEntry(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<ITemplateEntry | null> {\n\tconst [row] = await store.query<ITemplateEntry>(\n\t\t`SELECT ${ENTRY_COLS} FROM fonderie_courier_templates WHERE type = $1 AND locale IS NOT DISTINCT FROM $2`,\n\t\t[type, locale],\n\t);\n\treturn row ?? null;\n}\n\nexport async function listTemplateEntries(store: IStoreAdapter): Promise<ITemplateEntry[]> {\n\treturn store.query<ITemplateEntry>(\n\t\t`SELECT ${ENTRY_COLS} FROM fonderie_courier_templates ORDER BY type, locale NULLS FIRST`,\n\t);\n}\n\nexport async function deleteTemplate(\n\ttype: string,\n\tlocale: string | null,\n\tstore: IStoreAdapter,\n): Promise<boolean> {\n\tconst rows = await store.query<{ type: string }>(\n\t\t`DELETE FROM fonderie_courier_templates WHERE type = $1 AND locale IS NOT DISTINCT FROM $2 RETURNING type`,\n\t\t[type, locale],\n\t);\n\treturn rows.length > 0;\n}\n","import type { DefaultTemplateMap } from './types';\n\nexport interface IEmailChannelConfig {\n\tprovider: 'resend' | 'ses' | 'smtp';\n\tfrom: string;\n\tapiKey?: string;\n\tsmtp?: {\n\t\thost: string;\n\t\tport: number;\n\t\tsecure: boolean;\n\t\tuser: string;\n\t\tpass: string;\n\t};\n}\n\nexport interface ISmsChannelConfig {\n\tprovider: 'twilio' | 'vonage';\n\tfrom: string;\n\taccountSid?: string; // twilio\n\tauthToken?: string; // twilio\n\tapiKey?: string; // vonage\n\tapiSecret?: string; // vonage\n}\n\nexport interface IPushChannelConfig {\n\tprovider: 'fcm';\n\tserviceAccount: Record<string, unknown>;\n}\n\nexport const Channel = {\n\tEMAIL: 'email',\n\tSMS: 'sms',\n\tPUSH: 'push',\n} as const satisfies Record<string, 'email' | 'sms' | 'push'>;\n\nexport interface ICourierConfig {\n\t// Which channels handle which message types\n\t// e.g. { 'password-reset': ['email'], 'new-message': ['push', 'sms'] }\n\tchannels: Record<string, Array<'email' | 'sms' | 'push'>>;\n\n\tsms?: ISmsChannelConfig;\n\tpush?: IPushChannelConfig;\n\temail?: IEmailChannelConfig;\n\n\t// When set, exposes Bearer-guarded template admin routes (/admin/templates/*)\n\t// for versioned edit/history/rollback. Requires @fonderie/store (db templates).\n\tadminToken?: string;\n\n\t// Where templates are loaded from\n\t// 'db' requires @fonderie/store to be configured\n\t// 'fs' reads from a local directory\n\ttemplates?: {\n\t\tsource: 'db' | 'fs';\n\t\tdirectory?: string; // for 'fs' source\n\t\t// Module-shipped default templates. The app imports each module's\n\t\t// DEFAULT_TEMPLATES (auth, workspaces, billing, …) and passes them here;\n\t\t// the resolver falls back to a default when no app override (DB row / FS\n\t\t// file) exists, before the last-resort JSON dump. Aggregated like\n\t\t// getMigrationsPath() — pass one map or an array of them.\n\t\tdefaults?: DefaultTemplateMap | DefaultTemplateMap[];\n\t};\n\n\t// Signing keys for verifying inbound delivery webhook payloads\n\tdelivery?: {\n\t\tsigningKeys?: {\n\t\t\tsendgrid?: string; // HMAC-SHA256 key from SendGrid dashboard\n\t\t\tmailgun?: string; // Mailgun HTTP webhook signing key\n\t\t};\n\t};\n}\n"],"mappings":"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","names":["body","body","body","body","events"]}
|
package/dist/types.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { ICourierMessage } from '@fonderie/core/types';\n\nexport type { ICourierMessage };\n\nexport interface ICourierChannel {\n\tname: string;\n\tsend(message: ICourierMessage, template: IRenderedTemplate): Promise<void>;\n}\n\nexport interface IRenderedTemplate {\n\tsubject?: string; // email only\n\thtml?: string; // email only\n\ttext: string; // all channels\n}\n\nexport interface ITemplateResolver {\n\tresolve(type: string, data: Record<string, unknown>, locale?: string): Promise<IRenderedTemplate>;\n}\n\nexport type { IMessageLog, MessageLogStatus } from './log';\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { ICourierMessage, IDefaultTemplate } from '@fonderie/core/types';\n\nexport type { ICourierMessage, IDefaultTemplate };\n\n// A module ships its built-in copy as Record<ItsMessageKey, IDefaultTemplate>;\n// the app hands courier one (or an array, aggregated like getMigrationsPath())\n// and the resolver falls back to it before the last-resort JSON dump.\nexport type DefaultTemplateMap = Record<string, IDefaultTemplate>;\n\nexport interface ICourierChannel {\n\tname: string;\n\tsend(message: ICourierMessage, template: IRenderedTemplate): Promise<void>;\n}\n\nexport interface IRenderedTemplate {\n\tsubject?: string; // email only\n\thtml?: string; // email only\n\ttext: string; // all channels\n}\n\nexport interface ITemplateResolver {\n\tresolve(type: string, data: Record<string, unknown>, locale?: string): Promise<IRenderedTemplate>;\n}\n\nexport type { IMessageLog, MessageLogStatus } from './log';\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|
package/dist/types.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ICourierMessage } from '@fonderie/core/types';
|
|
2
|
-
export { ICourierMessage } from '@fonderie/core/types';
|
|
1
|
+
import { IDefaultTemplate, ICourierMessage } from '@fonderie/core/types';
|
|
2
|
+
export { ICourierMessage, IDefaultTemplate } from '@fonderie/core/types';
|
|
3
3
|
|
|
4
4
|
type MessageLogStatus = 'pending' | 'sent' | 'failed' | 'delivered' | 'opened' | 'clicked' | 'bounced' | 'spam';
|
|
5
5
|
interface IMessageLog {
|
|
@@ -21,6 +21,7 @@ interface IMessageLog {
|
|
|
21
21
|
sentAt: string | null;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
type DefaultTemplateMap = Record<string, IDefaultTemplate>;
|
|
24
25
|
interface ICourierChannel {
|
|
25
26
|
name: string;
|
|
26
27
|
send(message: ICourierMessage, template: IRenderedTemplate): Promise<void>;
|
|
@@ -34,4 +35,4 @@ interface ITemplateResolver {
|
|
|
34
35
|
resolve(type: string, data: Record<string, unknown>, locale?: string): Promise<IRenderedTemplate>;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
export type { ICourierChannel, IMessageLog, IRenderedTemplate, ITemplateResolver, MessageLogStatus };
|
|
38
|
+
export type { DefaultTemplateMap, ICourierChannel, IMessageLog, IRenderedTemplate, ITemplateResolver, MessageLogStatus };
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ICourierMessage } from '@fonderie/core/types';
|
|
2
|
-
export { ICourierMessage } from '@fonderie/core/types';
|
|
1
|
+
import { IDefaultTemplate, ICourierMessage } from '@fonderie/core/types';
|
|
2
|
+
export { ICourierMessage, IDefaultTemplate } from '@fonderie/core/types';
|
|
3
3
|
|
|
4
4
|
type MessageLogStatus = 'pending' | 'sent' | 'failed' | 'delivered' | 'opened' | 'clicked' | 'bounced' | 'spam';
|
|
5
5
|
interface IMessageLog {
|
|
@@ -21,6 +21,7 @@ interface IMessageLog {
|
|
|
21
21
|
sentAt: string | null;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
type DefaultTemplateMap = Record<string, IDefaultTemplate>;
|
|
24
25
|
interface ICourierChannel {
|
|
25
26
|
name: string;
|
|
26
27
|
send(message: ICourierMessage, template: IRenderedTemplate): Promise<void>;
|
|
@@ -34,4 +35,4 @@ interface ITemplateResolver {
|
|
|
34
35
|
resolve(type: string, data: Record<string, unknown>, locale?: string): Promise<IRenderedTemplate>;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
export type { ICourierChannel, IMessageLog, IRenderedTemplate, ITemplateResolver, MessageLogStatus };
|
|
38
|
+
export type { DefaultTemplateMap, ICourierChannel, IMessageLog, IRenderedTemplate, ITemplateResolver, MessageLogStatus };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fonderie/courier",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "Transactional messaging for SaaS — multi-channel delivery (email, SMS, push) with FS or DB templates, per-message-type channel routing, and a persistent message log.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fonderiejs",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"nodemailer": "^9.0.6"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@fonderie/core": "^0.
|
|
54
|
+
"@fonderie/core": "^0.8.0",
|
|
55
55
|
"@fonderie/store": "^0.2.0",
|
|
56
56
|
"@fonderie/events": "^5.0.0"
|
|
57
57
|
},
|