@fonderie/courier 5.2.2 → 7.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/signatures.md +4 -3
- package/dist/index.cjs +96 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +98 -50
- package/dist/index.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +5 -2
- package/dist/types.d.ts +5 -2
- package/package.json +3 -3
package/brain/signatures.md
CHANGED
|
@@ -16,7 +16,7 @@ new CourierModule(config: ICourierConfig, store?: IStoreAdapter | undefined, bus
|
|
|
16
16
|
|
|
17
17
|
function validateCourierConfig(config: ICourierConfig, registeredChannels: Iterable<string>): void
|
|
18
18
|
|
|
19
|
-
function handleSendGridDelivery(req: Request, store: IStoreAdapter,
|
|
19
|
+
function handleSendGridDelivery(req: Request, store: IStoreAdapter, publicKey?: string | undefined): Promise<Response>
|
|
20
20
|
|
|
21
21
|
function handleMailgunDelivery(req: Request, store: IStoreAdapter, signingKey?: string | undefined): Promise<Response>
|
|
22
22
|
|
|
@@ -37,7 +37,7 @@ new PushChannel(config: IPushChannelConfig): PushChannel
|
|
|
37
37
|
|
|
38
38
|
new EmailChannel(config: IEmailChannelConfig): EmailChannel
|
|
39
39
|
.name: "email"
|
|
40
|
-
.send(message: ICourierMessage, template: IRenderedTemplate): Promise<void>
|
|
40
|
+
.send(message: ICourierMessage, template: IRenderedTemplate): Promise<void | ISendResult>
|
|
41
41
|
|
|
42
42
|
new DBTemplateResolver(store: IStoreAdapter, defaults?: DefaultTemplates | undefined): DBTemplateResolver
|
|
43
43
|
.resolve(type: string, data: Record<string, unknown>, locale?: string | undefined): Promise<IRenderedTemplate>
|
|
@@ -122,7 +122,7 @@ interface ICourierMessage {
|
|
|
122
122
|
|
|
123
123
|
interface ICourierChannel {
|
|
124
124
|
name: string;
|
|
125
|
-
send(message: ICourierMessage, template: IRenderedTemplate): Promise<void>;
|
|
125
|
+
send(message: ICourierMessage, template: IRenderedTemplate): Promise<ISendResult | void>;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
interface IRenderedTemplate {
|
|
@@ -161,6 +161,7 @@ interface ICourierConfig {
|
|
|
161
161
|
sendgrid?: string;
|
|
162
162
|
mailgun?: string;
|
|
163
163
|
};
|
|
164
|
+
allowUnverifiedMailtrap?: boolean;
|
|
164
165
|
};
|
|
165
166
|
}
|
|
166
167
|
|
package/dist/index.cjs
CHANGED
|
@@ -75,6 +75,14 @@ async function insertMessageLog(entry, store) {
|
|
|
75
75
|
);
|
|
76
76
|
return row?.id ?? "";
|
|
77
77
|
}
|
|
78
|
+
async function setMessageProviderId(id, providerMessageId, store) {
|
|
79
|
+
await store.query(
|
|
80
|
+
`UPDATE fonderie_message_log
|
|
81
|
+
SET provider_message_id = $2
|
|
82
|
+
WHERE id = $1`,
|
|
83
|
+
[id, providerMessageId]
|
|
84
|
+
);
|
|
85
|
+
}
|
|
78
86
|
async function markMessageSent(id, store) {
|
|
79
87
|
await store.query(
|
|
80
88
|
`UPDATE fonderie_message_log
|
|
@@ -172,8 +180,13 @@ var Dispatcher = class {
|
|
|
172
180
|
if (message.locale) logEntry.locale = message.locale;
|
|
173
181
|
const logId = this.store ? await insertMessageLog(logEntry, this.store).catch(() => "") : "";
|
|
174
182
|
try {
|
|
175
|
-
await channel.send(message, template);
|
|
183
|
+
const result = await channel.send(message, template);
|
|
176
184
|
if (this.store && logId) {
|
|
185
|
+
if (result?.providerMessageId) {
|
|
186
|
+
await setMessageProviderId(logId, result.providerMessageId, this.store).catch(
|
|
187
|
+
() => void 0
|
|
188
|
+
);
|
|
189
|
+
}
|
|
177
190
|
markMessageSent(logId, this.store).catch(() => void 0);
|
|
178
191
|
}
|
|
179
192
|
} catch (err) {
|
|
@@ -320,12 +333,12 @@ var EmailChannel = class {
|
|
|
320
333
|
return;
|
|
321
334
|
}
|
|
322
335
|
if (this.config.provider === "resend") {
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
console.warn(`[courier:email] provider ${this.config.provider} not implemented`);
|
|
336
|
+
return this.sendViaResend(to, template);
|
|
337
|
+
}
|
|
338
|
+
if (this.config.provider === "smtp") {
|
|
339
|
+
return this.sendViaSMTP(to, template);
|
|
328
340
|
}
|
|
341
|
+
console.warn(`[courier:email] provider ${this.config.provider} not implemented`);
|
|
329
342
|
}
|
|
330
343
|
async sendViaResend(to, template) {
|
|
331
344
|
if (!this.config.apiKey) {
|
|
@@ -349,18 +362,23 @@ var EmailChannel = class {
|
|
|
349
362
|
const body2 = await res.text();
|
|
350
363
|
throw new Error(`[courier:email] Resend error ${res.status}: ${body2}`);
|
|
351
364
|
}
|
|
365
|
+
const data = await res.json().catch(() => null);
|
|
366
|
+
return typeof data?.id === "string" ? { providerMessageId: data.id } : {};
|
|
352
367
|
}
|
|
353
368
|
async sendViaSMTP(to, template) {
|
|
354
369
|
if (!this.transport) {
|
|
355
370
|
throw new Error("SMTP transport not initialised \u2014 check smtp config");
|
|
356
371
|
}
|
|
357
|
-
await this.transport.sendMail({
|
|
372
|
+
const info = await this.transport.sendMail({
|
|
358
373
|
from: this.config.from,
|
|
359
374
|
to,
|
|
360
375
|
subject: template.subject ?? "(no subject)",
|
|
361
376
|
text: template.text,
|
|
362
377
|
...template.html !== void 0 ? { html: template.html } : {}
|
|
363
378
|
});
|
|
379
|
+
const raw = typeof info?.messageId === "string" ? info.messageId : "";
|
|
380
|
+
const providerMessageId = raw.replace(/^<|>$/g, "");
|
|
381
|
+
return providerMessageId ? { providerMessageId } : {};
|
|
364
382
|
}
|
|
365
383
|
};
|
|
366
384
|
|
|
@@ -495,15 +513,20 @@ function wrapLayout(bodyHtml, layoutHtml = DEFAULT_EMAIL_LAYOUT) {
|
|
|
495
513
|
|
|
496
514
|
// src/templates/resolver.ts
|
|
497
515
|
var LAYOUT_TYPE = "_layout";
|
|
498
|
-
function
|
|
516
|
+
function escapeHtml(value) {
|
|
517
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
518
|
+
}
|
|
519
|
+
function render(template, data, opts = {}) {
|
|
499
520
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
500
521
|
const value = data[key];
|
|
501
|
-
|
|
522
|
+
if (value === void 0 || value === null) return "";
|
|
523
|
+
const s = String(value);
|
|
524
|
+
return opts.escapeHtml ? escapeHtml(s) : s;
|
|
502
525
|
});
|
|
503
526
|
}
|
|
504
527
|
function composeHtml(bodyHtml, layoutHtml, subject, data) {
|
|
505
528
|
const wrapped = wrapLayout(bodyHtml, layoutHtml);
|
|
506
|
-
return render(wrapped, { subject: subject ?? "", preheader: "", ...data });
|
|
529
|
+
return render(wrapped, { subject: subject ?? "", preheader: "", ...data }, { escapeHtml: true });
|
|
507
530
|
}
|
|
508
531
|
function renderFragment(frag, layoutHtml, data) {
|
|
509
532
|
const subject = frag.subject ? render(frag.subject, data) : void 0;
|
|
@@ -647,29 +670,43 @@ function validateCourierConfig(config, registeredChannels) {
|
|
|
647
670
|
// src/delivery.ts
|
|
648
671
|
var import_node_crypto = require("crypto");
|
|
649
672
|
var import_core = require("@fonderie/core");
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
if (!Array.isArray(events2)) return Response.json({ ok: true });
|
|
660
|
-
await processSendGridEvents(events2, store);
|
|
661
|
-
return Response.json({ ok: true });
|
|
673
|
+
var TIMESTAMP_TOLERANCE_S = 5 * 60;
|
|
674
|
+
function isFreshTimestamp(unixSeconds) {
|
|
675
|
+
const ts = Number(unixSeconds);
|
|
676
|
+
if (!Number.isFinite(ts)) return false;
|
|
677
|
+
return Math.abs(Date.now() / 1e3 - ts) <= TIMESTAMP_TOLERANCE_S;
|
|
678
|
+
}
|
|
679
|
+
async function handleSendGridDelivery(req, store, publicKey) {
|
|
680
|
+
if (!publicKey) {
|
|
681
|
+
return Response.json({ error: "VERIFICATION_NOT_CONFIGURED" }, { status: 401 });
|
|
662
682
|
}
|
|
663
|
-
const
|
|
683
|
+
const sig = req.headers.get("x-twilio-email-event-webhook-signature") ?? "";
|
|
684
|
+
const ts = req.headers.get("x-twilio-email-event-webhook-timestamp") ?? "";
|
|
685
|
+
const body2 = await req.text();
|
|
686
|
+
if (!isFreshTimestamp(ts)) {
|
|
687
|
+
return Response.json({ error: "STALE_TIMESTAMP" }, { status: 401 });
|
|
688
|
+
}
|
|
689
|
+
if (!verifySendGridSignature(publicKey, ts, body2, sig)) {
|
|
690
|
+
return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
|
|
691
|
+
}
|
|
692
|
+
const events = parseJson(body2);
|
|
664
693
|
if (!Array.isArray(events)) return Response.json({ ok: true });
|
|
665
694
|
await processSendGridEvents(events, store);
|
|
666
695
|
return Response.json({ ok: true });
|
|
667
696
|
}
|
|
668
|
-
function verifySendGridSignature(
|
|
697
|
+
function verifySendGridSignature(publicKeyB64, timestamp, body2, signature) {
|
|
669
698
|
try {
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
699
|
+
const key = (0, import_node_crypto.createPublicKey)({
|
|
700
|
+
key: Buffer.from(publicKeyB64, "base64"),
|
|
701
|
+
format: "der",
|
|
702
|
+
type: "spki"
|
|
703
|
+
});
|
|
704
|
+
return (0, import_node_crypto.verify)(
|
|
705
|
+
"sha256",
|
|
706
|
+
Buffer.from(timestamp + body2),
|
|
707
|
+
key,
|
|
708
|
+
Buffer.from(signature, "base64")
|
|
709
|
+
);
|
|
673
710
|
} catch {
|
|
674
711
|
return false;
|
|
675
712
|
}
|
|
@@ -698,12 +735,16 @@ async function processSendGridEvents(events, store) {
|
|
|
698
735
|
}
|
|
699
736
|
}
|
|
700
737
|
async function handleMailgunDelivery(req, store, signingKey) {
|
|
738
|
+
if (!signingKey) {
|
|
739
|
+
return Response.json({ error: "VERIFICATION_NOT_CONFIGURED" }, { status: 401 });
|
|
740
|
+
}
|
|
701
741
|
const body2 = await req.json();
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
742
|
+
const { signature } = body2;
|
|
743
|
+
if (!signature || !isFreshTimestamp(signature.timestamp)) {
|
|
744
|
+
return Response.json({ error: "STALE_TIMESTAMP" }, { status: 401 });
|
|
745
|
+
}
|
|
746
|
+
if (!verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {
|
|
747
|
+
return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
|
|
707
748
|
}
|
|
708
749
|
const event = body2["event-data"];
|
|
709
750
|
if (event) {
|
|
@@ -948,22 +989,29 @@ var CourierModule = class {
|
|
|
948
989
|
install(app) {
|
|
949
990
|
validateCourierConfig(this.config, this.dispatcher.channelNames());
|
|
950
991
|
const store = this.store;
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
992
|
+
const delivery = this.config.delivery;
|
|
993
|
+
const signingKeys = delivery?.signingKeys;
|
|
994
|
+
if (signingKeys?.sendgrid) {
|
|
995
|
+
app.addRoute(
|
|
996
|
+
"POST",
|
|
997
|
+
"/courier/delivery/sendgrid",
|
|
998
|
+
(ctx) => handleSendGridDelivery(ctx.request, store, signingKeys.sendgrid)
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
if (signingKeys?.mailgun) {
|
|
1002
|
+
app.addRoute(
|
|
1003
|
+
"POST",
|
|
1004
|
+
"/courier/delivery/mailgun",
|
|
1005
|
+
(ctx) => handleMailgunDelivery(ctx.request, store, signingKeys.mailgun)
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
if (delivery?.allowUnverifiedMailtrap) {
|
|
1009
|
+
app.addRoute(
|
|
1010
|
+
"POST",
|
|
1011
|
+
"/courier/delivery/mailtrap",
|
|
1012
|
+
(ctx) => handleMailtrapDelivery(ctx.request, store)
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
967
1015
|
if (this.config.adminToken) {
|
|
968
1016
|
if (!store) {
|
|
969
1017
|
throw new Error("[courier] adminToken requires @fonderie/store (db templates)");
|