@fonderie/courier 3.0.0 → 5.0.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/outcomes.md +26 -0
- package/brain/signatures.md +42 -0
- package/dist/index.cjs +222 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -2
- package/dist/index.d.ts +51 -2
- package/dist/index.js +213 -17
- package/dist/index.js.map +1 -1
- package/dist/migrations/sql/004_template_versioning.sql +26 -0
- package/package.json +5 -5
package/brain/outcomes.md
CHANGED
|
@@ -9,6 +9,19 @@ downloading tarballs.
|
|
|
9
9
|
|
|
10
10
|
## Database tables (after all migrations)
|
|
11
11
|
|
|
12
|
+
### `fonderie_courier_template_revisions`
|
|
13
|
+
|
|
14
|
+
```sql
|
|
15
|
+
type TEXT NOT NULL
|
|
16
|
+
locale TEXT
|
|
17
|
+
subject TEXT
|
|
18
|
+
html TEXT
|
|
19
|
+
text TEXT NOT NULL
|
|
20
|
+
version INT NOT NULL
|
|
21
|
+
actor TEXT
|
|
22
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
23
|
+
```
|
|
24
|
+
|
|
12
25
|
### `fonderie_courier_templates`
|
|
13
26
|
|
|
14
27
|
```sql
|
|
@@ -21,6 +34,8 @@ text TEXT NOT NULL
|
|
|
21
34
|
active BOOLEAN NOT NULL DEFAULT true
|
|
22
35
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
23
36
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
37
|
+
version INT NOT NULL DEFAULT 1
|
|
38
|
+
updated_by TEXT
|
|
24
39
|
-- UNIQUE (type, locale)
|
|
25
40
|
```
|
|
26
41
|
|
|
@@ -48,6 +63,17 @@ bounce_reason TEXT
|
|
|
48
63
|
|
|
49
64
|
Raw SQL ships in `node_modules/@fonderie/courier/dist/migrations/sql/` — read it there if you must; never download tarballs.
|
|
50
65
|
|
|
66
|
+
## HTTP routes registered
|
|
67
|
+
|
|
68
|
+
| Method | Path | Middleware chain (auth / validation / handler) |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| GET | `/admin/templates` | `g(async () => { return setApiResponse(HTTP.OK, 'TEMPLATES_LISTED', 'Templates', await listTemplateEntries(store)); })` |
|
|
71
|
+
| DELETE | `/admin/templates/:type` | `g(async (ctx) => { const ok = await deleteTemplate(typeOf(ctx), localeOf(ctx), store); return setApiResponse(ok ? HTTP.OK : HTTP.NOT_FOUND, ok ? 'DELETED' : 'NOT_FOUND', ok ? 'Deleted' : 'No such template'); })` |
|
|
72
|
+
| GET | `/admin/templates/:type` | `g(async (ctx) => { const row = await getTemplateEntry(typeOf(ctx), localeOf(ctx), store); return row ? setApiResponse(HTTP.OK, 'TEMPLATE', 'Template', row) : setApiResponse(HTTP.NOT_FOUND, 'NOT_FOUND', 'No such template'); })` |
|
|
73
|
+
| PUT | `/admin/templates/:type` | `g(async (ctx) => { const b = body(ctx); if (typeof b['text'] !== 'string') { return setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.text (string) is required'); } try { const opts: Parameters<typeof setTemplate>[0] = { type: typeOf(ctx), text: b['text'], locale: localeOf(ctx), actor: actorOf(ctx), }; if (typeof b['subject'] === 'string') opts.subject = b['subject']; if (typeof b['html'] === 'string') opts.html = b['html']; if (typeof b['active'] === 'boolean') opts.active = b['active']; if (typeof b['ifVersion'] === 'number') opts.ifVersion = b['ifVersion']; return setApiResponse(HTTP.OK, 'TEMPLATE_SET', 'Template saved', await setTemplate(opts, store)); } catch (err) { return conflictOr(err); } })` |
|
|
74
|
+
| GET | `/admin/templates/:type/revisions` | `g(async (ctx) => { return setApiResponse(HTTP.OK, 'REVISIONS', 'Template revisions', await listTemplateRevisions(typeOf(ctx), localeOf(ctx), store)); })` |
|
|
75
|
+
| POST | `/admin/templates/:type/rollback` | `g(async (ctx) => { const b = body(ctx); const toVersion = Number(b['toVersion']); if (!Number.isInteger(toVersion)) { return setApiResponse(HTTP.UNPROCESSABLE, 'INVALID', 'body.toVersion (int) is required'); } const row = await rollbackTemplate( { type: typeOf(ctx), locale: localeOf(ctx), toVersion, actor: actorOf(ctx) }, store, ); return setApiResponse(HTTP.OK, 'ROLLED_BACK', `Rolled back to v${toVersion}`, row); })` |
|
|
76
|
+
|
|
51
77
|
## Migration statements not replayed (verify in raw SQL)
|
|
52
78
|
|
|
53
79
|
- `ELSE`
|
package/brain/signatures.md
CHANGED
|
@@ -11,8 +11,11 @@ new CourierModule(config: ICourierConfig, store?: IStoreAdapter | undefined, bus
|
|
|
11
11
|
.name: "@fonderie/courier"
|
|
12
12
|
.deps: string[]
|
|
13
13
|
.dispatcher: Dispatcher
|
|
14
|
+
.checkReadiness(): IReadinessProblem[]
|
|
14
15
|
.install(app: IFonderieApp): void
|
|
15
16
|
|
|
17
|
+
function validateCourierConfig(config: ICourierConfig, registeredChannels: Iterable<string>): void
|
|
18
|
+
|
|
16
19
|
function handleSendGridDelivery(req: Request, store: IStoreAdapter, webhookSecret?: string | undefined): Promise<Response>
|
|
17
20
|
|
|
18
21
|
function handleMailgunDelivery(req: Request, store: IStoreAdapter, signingKey?: string | undefined): Promise<Response>
|
|
@@ -21,6 +24,7 @@ function handleMailtrapDelivery(req: Request, store: IStoreAdapter): Promise<Res
|
|
|
21
24
|
|
|
22
25
|
new Dispatcher(config: ICourierConfig, resolver: ITemplateResolver, store?: IStoreAdapter | undefined): Dispatcher
|
|
23
26
|
.registerChannel(channel: ICourierChannel): Dispatcher
|
|
27
|
+
.channelNames(): string[]
|
|
24
28
|
.dispatch(message: ICourierMessage): Promise<void>
|
|
25
29
|
|
|
26
30
|
new SmsChannel(config: ISmsChannelConfig): SmsChannel
|
|
@@ -41,6 +45,43 @@ new DBTemplateResolver(store: IStoreAdapter): DBTemplateResolver
|
|
|
41
45
|
new FSTemplateResolver(directory: string): FSTemplateResolver
|
|
42
46
|
.resolve(type: string, data: Record<string, unknown>, locale?: string | undefined): Promise<IRenderedTemplate>
|
|
43
47
|
|
|
48
|
+
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
|
+
|
|
50
|
+
function rollbackTemplate(opts: { type: string; locale?: string | null; toVersion: number; actor?: string; }, store: IStoreAdapter): Promise<ITemplateEntry>
|
|
51
|
+
|
|
52
|
+
function listTemplateRevisions(type: string, locale: string | null, store: IStoreAdapter): Promise<ITemplateRevision[]>
|
|
53
|
+
|
|
54
|
+
function getTemplateEntry(type: string, locale: string | null, store: IStoreAdapter): Promise<ITemplateEntry | null>
|
|
55
|
+
|
|
56
|
+
function listTemplateEntries(store: IStoreAdapter): Promise<ITemplateEntry[]>
|
|
57
|
+
|
|
58
|
+
function deleteTemplate(type: string, locale: string | null, store: IStoreAdapter): Promise<boolean>
|
|
59
|
+
|
|
60
|
+
function buildTemplateAdminRoutes(store: IStoreAdapter, adminToken: string): [string, string, Middleware][]
|
|
61
|
+
|
|
62
|
+
interface ITemplateEntry {
|
|
63
|
+
type: string;
|
|
64
|
+
locale: string | null;
|
|
65
|
+
subject: string | null;
|
|
66
|
+
html: string | null;
|
|
67
|
+
text: string;
|
|
68
|
+
active: boolean;
|
|
69
|
+
version: number;
|
|
70
|
+
updatedBy: string | null;
|
|
71
|
+
updatedAt: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface ITemplateRevision {
|
|
75
|
+
type: string;
|
|
76
|
+
locale: string | null;
|
|
77
|
+
subject: string | null;
|
|
78
|
+
html: string | null;
|
|
79
|
+
text: string;
|
|
80
|
+
version: number;
|
|
81
|
+
actor: string | null;
|
|
82
|
+
createdAt: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
44
85
|
interface IMessageLog {
|
|
45
86
|
id: string;
|
|
46
87
|
messageType: string;
|
|
@@ -95,6 +136,7 @@ interface ICourierConfig {
|
|
|
95
136
|
sms?: ISmsChannelConfig;
|
|
96
137
|
push?: IPushChannelConfig;
|
|
97
138
|
email?: IEmailChannelConfig;
|
|
139
|
+
adminToken?: string;
|
|
98
140
|
templates?: {
|
|
99
141
|
source: 'db' | 'fs';
|
|
100
142
|
directory?: string;
|
package/dist/index.cjs
CHANGED
|
@@ -38,9 +38,17 @@ __export(index_exports, {
|
|
|
38
38
|
FSTemplateResolver: () => FSTemplateResolver,
|
|
39
39
|
PushChannel: () => PushChannel,
|
|
40
40
|
SmsChannel: () => SmsChannel,
|
|
41
|
+
buildTemplateAdminRoutes: () => buildTemplateAdminRoutes,
|
|
42
|
+
deleteTemplate: () => deleteTemplate,
|
|
43
|
+
getTemplateEntry: () => getTemplateEntry,
|
|
41
44
|
handleMailgunDelivery: () => handleMailgunDelivery,
|
|
42
45
|
handleMailtrapDelivery: () => handleMailtrapDelivery,
|
|
43
|
-
handleSendGridDelivery: () => handleSendGridDelivery
|
|
46
|
+
handleSendGridDelivery: () => handleSendGridDelivery,
|
|
47
|
+
listTemplateEntries: () => listTemplateEntries,
|
|
48
|
+
listTemplateRevisions: () => listTemplateRevisions,
|
|
49
|
+
rollbackTemplate: () => rollbackTemplate,
|
|
50
|
+
setTemplate: () => setTemplate,
|
|
51
|
+
validateCourierConfig: () => validateCourierConfig
|
|
44
52
|
});
|
|
45
53
|
module.exports = __toCommonJS(index_exports);
|
|
46
54
|
|
|
@@ -135,6 +143,11 @@ var Dispatcher = class {
|
|
|
135
143
|
this.channels.set(channel.name, channel);
|
|
136
144
|
return this;
|
|
137
145
|
}
|
|
146
|
+
// Names of the currently-registered channels — used by the boot-time config
|
|
147
|
+
// guard to detect message types routed to a channel with no provider.
|
|
148
|
+
channelNames() {
|
|
149
|
+
return [...this.channels.keys()];
|
|
150
|
+
}
|
|
138
151
|
async dispatch(message) {
|
|
139
152
|
const channelNames = this.config.channels[message.type];
|
|
140
153
|
if (!channelNames || channelNames.length === 0) {
|
|
@@ -211,8 +224,8 @@ var SmsChannel = class {
|
|
|
211
224
|
}
|
|
212
225
|
);
|
|
213
226
|
if (!res.ok) {
|
|
214
|
-
const
|
|
215
|
-
throw new Error(`[courier:sms] Twilio error ${res.status}: ${
|
|
227
|
+
const body2 = await res.text();
|
|
228
|
+
throw new Error(`[courier:sms] Twilio error ${res.status}: ${body2}`);
|
|
216
229
|
}
|
|
217
230
|
}
|
|
218
231
|
async sendViaVonage(to, text) {
|
|
@@ -232,8 +245,8 @@ var SmsChannel = class {
|
|
|
232
245
|
})
|
|
233
246
|
});
|
|
234
247
|
if (!res.ok) {
|
|
235
|
-
const
|
|
236
|
-
throw new Error(`[courier:sms] Vonage error ${res.status}: ${
|
|
248
|
+
const body2 = await res.text();
|
|
249
|
+
throw new Error(`[courier:sms] Vonage error ${res.status}: ${body2}`);
|
|
237
250
|
}
|
|
238
251
|
}
|
|
239
252
|
};
|
|
@@ -275,8 +288,8 @@ var PushChannel = class {
|
|
|
275
288
|
})
|
|
276
289
|
});
|
|
277
290
|
if (!res.ok) {
|
|
278
|
-
const
|
|
279
|
-
throw new Error(`[courier:push] FCM error ${res.status}: ${
|
|
291
|
+
const body2 = await res.text();
|
|
292
|
+
throw new Error(`[courier:push] FCM error ${res.status}: ${body2}`);
|
|
280
293
|
}
|
|
281
294
|
}
|
|
282
295
|
};
|
|
@@ -331,8 +344,8 @@ var EmailChannel = class {
|
|
|
331
344
|
})
|
|
332
345
|
});
|
|
333
346
|
if (!res.ok) {
|
|
334
|
-
const
|
|
335
|
-
throw new Error(`[courier:email] Resend error ${res.status}: ${
|
|
347
|
+
const body2 = await res.text();
|
|
348
|
+
throw new Error(`[courier:email] Resend error ${res.status}: ${body2}`);
|
|
336
349
|
}
|
|
337
350
|
}
|
|
338
351
|
async sendViaSMTP(to, template) {
|
|
@@ -568,17 +581,43 @@ var FSTemplateResolver = class {
|
|
|
568
581
|
}
|
|
569
582
|
};
|
|
570
583
|
|
|
584
|
+
// src/config-guard.ts
|
|
585
|
+
var MODULE = "@fonderie/courier";
|
|
586
|
+
function collectCourierConfigProblems(config, registeredChannels) {
|
|
587
|
+
const registered = new Set(registeredChannels);
|
|
588
|
+
const gaps = /* @__PURE__ */ new Map();
|
|
589
|
+
for (const [type, channels] of Object.entries(config.channels ?? {})) {
|
|
590
|
+
for (const channel of channels) {
|
|
591
|
+
if (!registered.has(channel)) {
|
|
592
|
+
const types = gaps.get(channel) ?? [];
|
|
593
|
+
types.push(type);
|
|
594
|
+
gaps.set(channel, types);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return [...gaps].map(([channel, types]) => ({
|
|
599
|
+
module: MODULE,
|
|
600
|
+
severity: "warning",
|
|
601
|
+
message: `${types.length} message type(s) route to the '${channel}' channel but no '${channel}' provider is registered \u2014 these will be silently dropped: ${types.join(", ")}. Configure \`config.${channel}\` (or register a channel).`
|
|
602
|
+
}));
|
|
603
|
+
}
|
|
604
|
+
function validateCourierConfig(config, registeredChannels) {
|
|
605
|
+
for (const problem of collectCourierConfigProblems(config, registeredChannels)) {
|
|
606
|
+
console.warn(`[courier] ${problem.message}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
571
610
|
// src/delivery.ts
|
|
572
611
|
var import_node_crypto = require("crypto");
|
|
573
612
|
async function handleSendGridDelivery(req, store, webhookSecret) {
|
|
574
613
|
if (webhookSecret) {
|
|
575
614
|
const sig = req.headers.get("x-twilio-email-event-webhook-signature") ?? "";
|
|
576
615
|
const ts = req.headers.get("x-twilio-email-event-webhook-timestamp") ?? "";
|
|
577
|
-
const
|
|
578
|
-
if (!verifySendGridSignature(webhookSecret, ts,
|
|
616
|
+
const body2 = await req.text();
|
|
617
|
+
if (!verifySendGridSignature(webhookSecret, ts, body2, sig)) {
|
|
579
618
|
return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
|
|
580
619
|
}
|
|
581
|
-
const events2 = parseJson(
|
|
620
|
+
const events2 = parseJson(body2);
|
|
582
621
|
if (!Array.isArray(events2)) return Response.json({ ok: true });
|
|
583
622
|
await processSendGridEvents(events2, store);
|
|
584
623
|
return Response.json({ ok: true });
|
|
@@ -588,9 +627,9 @@ async function handleSendGridDelivery(req, store, webhookSecret) {
|
|
|
588
627
|
await processSendGridEvents(events, store);
|
|
589
628
|
return Response.json({ ok: true });
|
|
590
629
|
}
|
|
591
|
-
function verifySendGridSignature(secret, timestamp,
|
|
630
|
+
function verifySendGridSignature(secret, timestamp, body2, signature) {
|
|
592
631
|
try {
|
|
593
|
-
const payload = timestamp +
|
|
632
|
+
const payload = timestamp + body2;
|
|
594
633
|
const expected = (0, import_node_crypto.createHmac)("sha256", secret).update(payload).digest("base64");
|
|
595
634
|
const sigBuf = Buffer.from(signature, "base64");
|
|
596
635
|
const expBuf = Buffer.from(expected, "base64");
|
|
@@ -624,14 +663,14 @@ async function processSendGridEvents(events, store) {
|
|
|
624
663
|
}
|
|
625
664
|
}
|
|
626
665
|
async function handleMailgunDelivery(req, store, signingKey) {
|
|
627
|
-
const
|
|
666
|
+
const body2 = await req.json();
|
|
628
667
|
if (signingKey) {
|
|
629
|
-
const { signature } =
|
|
668
|
+
const { signature } = body2;
|
|
630
669
|
if (!signature || !verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {
|
|
631
670
|
return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
|
|
632
671
|
}
|
|
633
672
|
}
|
|
634
|
-
const event =
|
|
673
|
+
const event = body2["event-data"];
|
|
635
674
|
if (event) {
|
|
636
675
|
await processMailgunEvent(event, store);
|
|
637
676
|
}
|
|
@@ -702,6 +741,150 @@ function parseJson(text) {
|
|
|
702
741
|
}
|
|
703
742
|
}
|
|
704
743
|
|
|
744
|
+
// src/templates/admin-routes.ts
|
|
745
|
+
var import_core = require("@fonderie/core");
|
|
746
|
+
var import_store2 = require("@fonderie/store");
|
|
747
|
+
|
|
748
|
+
// src/templates/admin.ts
|
|
749
|
+
var import_store = require("@fonderie/store");
|
|
750
|
+
var ENTRY_COLS = `type, locale, subject, html, text, active, version, updated_by AS "updatedBy", updated_at AS "updatedAt"`;
|
|
751
|
+
var TEMPLATE_RESOURCE = {
|
|
752
|
+
table: "fonderie_courier_templates",
|
|
753
|
+
revisions: "fonderie_courier_template_revisions",
|
|
754
|
+
channel: "fonderie_courier_templates_changed",
|
|
755
|
+
keyColumns: ["type", "locale"],
|
|
756
|
+
contentColumns: ["subject", "html", "text"],
|
|
757
|
+
metaColumns: ["active"],
|
|
758
|
+
returning: ENTRY_COLS
|
|
759
|
+
};
|
|
760
|
+
async function setTemplate(opts, store) {
|
|
761
|
+
const data = {
|
|
762
|
+
subject: opts.subject ?? null,
|
|
763
|
+
html: opts.html ?? null,
|
|
764
|
+
text: opts.text
|
|
765
|
+
};
|
|
766
|
+
if (opts.active !== void 0) data["active"] = opts.active;
|
|
767
|
+
return (0, import_store.versionedWrite)(TEMPLATE_RESOURCE, store, {
|
|
768
|
+
key: opts.type,
|
|
769
|
+
scope: opts.locale ?? null,
|
|
770
|
+
data,
|
|
771
|
+
...opts.ifVersion !== void 0 ? { ifVersion: opts.ifVersion } : {},
|
|
772
|
+
actor: opts.actor ?? null
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
async function rollbackTemplate(opts, store) {
|
|
776
|
+
return (0, import_store.versionedRollback)(TEMPLATE_RESOURCE, store, {
|
|
777
|
+
key: opts.type,
|
|
778
|
+
scope: opts.locale ?? null,
|
|
779
|
+
toVersion: opts.toVersion,
|
|
780
|
+
actor: opts.actor ?? null
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
async function listTemplateRevisions(type, locale, store) {
|
|
784
|
+
return store.query(
|
|
785
|
+
`SELECT type, locale, subject, html, text, version, actor, created_at AS "createdAt"
|
|
786
|
+
FROM fonderie_courier_template_revisions
|
|
787
|
+
WHERE type = $1 AND locale IS NOT DISTINCT FROM $2
|
|
788
|
+
ORDER BY version DESC`,
|
|
789
|
+
[type, locale]
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
async function getTemplateEntry(type, locale, store) {
|
|
793
|
+
const [row] = await store.query(
|
|
794
|
+
`SELECT ${ENTRY_COLS} FROM fonderie_courier_templates WHERE type = $1 AND locale IS NOT DISTINCT FROM $2`,
|
|
795
|
+
[type, locale]
|
|
796
|
+
);
|
|
797
|
+
return row ?? null;
|
|
798
|
+
}
|
|
799
|
+
async function listTemplateEntries(store) {
|
|
800
|
+
return store.query(
|
|
801
|
+
`SELECT ${ENTRY_COLS} FROM fonderie_courier_templates ORDER BY type, locale NULLS FIRST`
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
async function deleteTemplate(type, locale, store) {
|
|
805
|
+
const rows = await store.query(
|
|
806
|
+
`DELETE FROM fonderie_courier_templates WHERE type = $1 AND locale IS NOT DISTINCT FROM $2 RETURNING type`,
|
|
807
|
+
[type, locale]
|
|
808
|
+
);
|
|
809
|
+
return rows.length > 0;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// src/templates/admin-routes.ts
|
|
813
|
+
function guarded(adminToken, handler) {
|
|
814
|
+
return async (ctx, next) => {
|
|
815
|
+
const header = ctx.request.headers.get("authorization") ?? "";
|
|
816
|
+
const token = header.startsWith("Bearer ") ? header.slice(7) : "";
|
|
817
|
+
if (!token || token !== adminToken) {
|
|
818
|
+
return (0, import_core.setApiResponse)(import_core.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Missing or invalid admin token");
|
|
819
|
+
}
|
|
820
|
+
return handler(ctx, next);
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
var actorOf = (ctx) => ctx.request.headers.get("x-actor") || "admin-token";
|
|
824
|
+
var typeOf = (ctx) => ctx.meta.params?.["type"] ?? "";
|
|
825
|
+
var localeOf = (ctx) => new URL(ctx.request.url).searchParams.get("locale");
|
|
826
|
+
var body = (ctx) => ctx.meta["body"] ?? {};
|
|
827
|
+
function conflictOr(err) {
|
|
828
|
+
if (err instanceof import_store2.VersionConflictError) {
|
|
829
|
+
return (0, import_core.setApiResponse)(import_core.HTTP.CONFLICT, "VERSION_CONFLICT", err.message, {
|
|
830
|
+
currentVersion: err.currentVersion
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
throw err;
|
|
834
|
+
}
|
|
835
|
+
function buildTemplateAdminRoutes(store, adminToken) {
|
|
836
|
+
const g = (h) => guarded(adminToken, h);
|
|
837
|
+
return [
|
|
838
|
+
["GET", "/admin/templates", g(async () => {
|
|
839
|
+
return (0, import_core.setApiResponse)(import_core.HTTP.OK, "TEMPLATES_LISTED", "Templates", await listTemplateEntries(store));
|
|
840
|
+
})],
|
|
841
|
+
["GET", "/admin/templates/:type", g(async (ctx) => {
|
|
842
|
+
const row = await getTemplateEntry(typeOf(ctx), localeOf(ctx), store);
|
|
843
|
+
return row ? (0, import_core.setApiResponse)(import_core.HTTP.OK, "TEMPLATE", "Template", row) : (0, import_core.setApiResponse)(import_core.HTTP.NOT_FOUND, "NOT_FOUND", "No such template");
|
|
844
|
+
})],
|
|
845
|
+
["PUT", "/admin/templates/:type", g(async (ctx) => {
|
|
846
|
+
const b = body(ctx);
|
|
847
|
+
if (typeof b["text"] !== "string") {
|
|
848
|
+
return (0, import_core.setApiResponse)(import_core.HTTP.UNPROCESSABLE, "INVALID", "body.text (string) is required");
|
|
849
|
+
}
|
|
850
|
+
try {
|
|
851
|
+
const opts = {
|
|
852
|
+
type: typeOf(ctx),
|
|
853
|
+
text: b["text"],
|
|
854
|
+
locale: localeOf(ctx),
|
|
855
|
+
actor: actorOf(ctx)
|
|
856
|
+
};
|
|
857
|
+
if (typeof b["subject"] === "string") opts.subject = b["subject"];
|
|
858
|
+
if (typeof b["html"] === "string") opts.html = b["html"];
|
|
859
|
+
if (typeof b["active"] === "boolean") opts.active = b["active"];
|
|
860
|
+
if (typeof b["ifVersion"] === "number") opts.ifVersion = b["ifVersion"];
|
|
861
|
+
return (0, import_core.setApiResponse)(import_core.HTTP.OK, "TEMPLATE_SET", "Template saved", await setTemplate(opts, store));
|
|
862
|
+
} catch (err) {
|
|
863
|
+
return conflictOr(err);
|
|
864
|
+
}
|
|
865
|
+
})],
|
|
866
|
+
["DELETE", "/admin/templates/:type", g(async (ctx) => {
|
|
867
|
+
const ok = await deleteTemplate(typeOf(ctx), localeOf(ctx), store);
|
|
868
|
+
return (0, import_core.setApiResponse)(ok ? import_core.HTTP.OK : import_core.HTTP.NOT_FOUND, ok ? "DELETED" : "NOT_FOUND", ok ? "Deleted" : "No such template");
|
|
869
|
+
})],
|
|
870
|
+
["GET", "/admin/templates/:type/revisions", g(async (ctx) => {
|
|
871
|
+
return (0, import_core.setApiResponse)(import_core.HTTP.OK, "REVISIONS", "Template revisions", await listTemplateRevisions(typeOf(ctx), localeOf(ctx), store));
|
|
872
|
+
})],
|
|
873
|
+
["POST", "/admin/templates/:type/rollback", g(async (ctx) => {
|
|
874
|
+
const b = body(ctx);
|
|
875
|
+
const toVersion = Number(b["toVersion"]);
|
|
876
|
+
if (!Number.isInteger(toVersion)) {
|
|
877
|
+
return (0, import_core.setApiResponse)(import_core.HTTP.UNPROCESSABLE, "INVALID", "body.toVersion (int) is required");
|
|
878
|
+
}
|
|
879
|
+
const row = await rollbackTemplate(
|
|
880
|
+
{ type: typeOf(ctx), locale: localeOf(ctx), toVersion, actor: actorOf(ctx) },
|
|
881
|
+
store
|
|
882
|
+
);
|
|
883
|
+
return (0, import_core.setApiResponse)(import_core.HTTP.OK, "ROLLED_BACK", `Rolled back to v${toVersion}`, row);
|
|
884
|
+
})]
|
|
885
|
+
];
|
|
886
|
+
}
|
|
887
|
+
|
|
705
888
|
// src/module.ts
|
|
706
889
|
var CourierModule = class {
|
|
707
890
|
constructor(config, store, bus) {
|
|
@@ -726,7 +909,12 @@ var CourierModule = class {
|
|
|
726
909
|
name = "@fonderie/courier";
|
|
727
910
|
deps = ["@fonderie/events"];
|
|
728
911
|
dispatcher;
|
|
912
|
+
// Report config problems for app.checkProductionReadiness() (data, not warn).
|
|
913
|
+
checkReadiness() {
|
|
914
|
+
return collectCourierConfigProblems(this.config, this.dispatcher.channelNames());
|
|
915
|
+
}
|
|
729
916
|
install(app) {
|
|
917
|
+
validateCourierConfig(this.config, this.dispatcher.channelNames());
|
|
730
918
|
const store = this.store;
|
|
731
919
|
const signingKeys = this.config.delivery?.signingKeys;
|
|
732
920
|
app.addRoute(
|
|
@@ -744,6 +932,14 @@ var CourierModule = class {
|
|
|
744
932
|
"/courier/delivery/mailtrap",
|
|
745
933
|
(ctx) => handleMailtrapDelivery(ctx.request, store)
|
|
746
934
|
);
|
|
935
|
+
if (this.config.adminToken) {
|
|
936
|
+
if (!store) {
|
|
937
|
+
throw new Error("[courier] adminToken requires @fonderie/store (db templates)");
|
|
938
|
+
}
|
|
939
|
+
for (const [method, path, handler] of buildTemplateAdminRoutes(store, this.config.adminToken)) {
|
|
940
|
+
app.addRoute(method, path, handler);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
747
943
|
}
|
|
748
944
|
};
|
|
749
945
|
function createTemplateResolver(source, config, store) {
|
|
@@ -772,8 +968,16 @@ var Channel = {
|
|
|
772
968
|
FSTemplateResolver,
|
|
773
969
|
PushChannel,
|
|
774
970
|
SmsChannel,
|
|
971
|
+
buildTemplateAdminRoutes,
|
|
972
|
+
deleteTemplate,
|
|
973
|
+
getTemplateEntry,
|
|
775
974
|
handleMailgunDelivery,
|
|
776
975
|
handleMailtrapDelivery,
|
|
777
|
-
handleSendGridDelivery
|
|
976
|
+
handleSendGridDelivery,
|
|
977
|
+
listTemplateEntries,
|
|
978
|
+
listTemplateRevisions,
|
|
979
|
+
rollbackTemplate,
|
|
980
|
+
setTemplate,
|
|
981
|
+
validateCourierConfig
|
|
778
982
|
});
|
|
779
983
|
//# sourceMappingURL=index.cjs.map
|