@fonderie/courier 5.2.2 → 6.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 +76 -45
- 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 +77 -46
- 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 +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 { DefaultTemplateMap, ITemplateResolver, ICourierChannel, IRenderedTemplate } from './types.cjs';
|
|
4
|
+
import { DefaultTemplateMap, ITemplateResolver, ICourierChannel, IRenderedTemplate, ISendResult } from './types.cjs';
|
|
5
5
|
export { IMessageLog, MessageLogStatus } from './types.cjs';
|
|
6
6
|
import { ICourierMessage, IDefaultTemplate } from '@fonderie/core/types';
|
|
7
7
|
export { ICourierMessage, IDefaultTemplate } from '@fonderie/core/types';
|
|
@@ -51,6 +51,7 @@ interface ICourierConfig {
|
|
|
51
51
|
sendgrid?: string;
|
|
52
52
|
mailgun?: string;
|
|
53
53
|
};
|
|
54
|
+
allowUnverifiedMailtrap?: boolean;
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -78,7 +79,7 @@ declare class CourierModule implements IFonderieModule {
|
|
|
78
79
|
|
|
79
80
|
declare function validateCourierConfig(config: ICourierConfig, registeredChannels: Iterable<string>): void;
|
|
80
81
|
|
|
81
|
-
declare function handleSendGridDelivery(req: Request, store: IStoreAdapter,
|
|
82
|
+
declare function handleSendGridDelivery(req: Request, store: IStoreAdapter, publicKey?: string): Promise<Response>;
|
|
82
83
|
declare function handleMailgunDelivery(req: Request, store: IStoreAdapter, signingKey?: string): Promise<Response>;
|
|
83
84
|
declare function handleMailtrapDelivery(req: Request, store: IStoreAdapter): Promise<Response>;
|
|
84
85
|
|
|
@@ -104,7 +105,7 @@ declare class EmailChannel implements ICourierChannel {
|
|
|
104
105
|
readonly name = "email";
|
|
105
106
|
private transport;
|
|
106
107
|
constructor(config: IEmailChannelConfig);
|
|
107
|
-
send(message: ICourierMessage, template: IRenderedTemplate): Promise<void>;
|
|
108
|
+
send(message: ICourierMessage, template: IRenderedTemplate): Promise<ISendResult | void>;
|
|
108
109
|
private sendViaResend;
|
|
109
110
|
private sendViaSMTP;
|
|
110
111
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 { DefaultTemplateMap, ITemplateResolver, ICourierChannel, IRenderedTemplate } from './types.js';
|
|
4
|
+
import { DefaultTemplateMap, ITemplateResolver, ICourierChannel, IRenderedTemplate, ISendResult } from './types.js';
|
|
5
5
|
export { IMessageLog, MessageLogStatus } from './types.js';
|
|
6
6
|
import { ICourierMessage, IDefaultTemplate } from '@fonderie/core/types';
|
|
7
7
|
export { ICourierMessage, IDefaultTemplate } from '@fonderie/core/types';
|
|
@@ -51,6 +51,7 @@ interface ICourierConfig {
|
|
|
51
51
|
sendgrid?: string;
|
|
52
52
|
mailgun?: string;
|
|
53
53
|
};
|
|
54
|
+
allowUnverifiedMailtrap?: boolean;
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -78,7 +79,7 @@ declare class CourierModule implements IFonderieModule {
|
|
|
78
79
|
|
|
79
80
|
declare function validateCourierConfig(config: ICourierConfig, registeredChannels: Iterable<string>): void;
|
|
80
81
|
|
|
81
|
-
declare function handleSendGridDelivery(req: Request, store: IStoreAdapter,
|
|
82
|
+
declare function handleSendGridDelivery(req: Request, store: IStoreAdapter, publicKey?: string): Promise<Response>;
|
|
82
83
|
declare function handleMailgunDelivery(req: Request, store: IStoreAdapter, signingKey?: string): Promise<Response>;
|
|
83
84
|
declare function handleMailtrapDelivery(req: Request, store: IStoreAdapter): Promise<Response>;
|
|
84
85
|
|
|
@@ -104,7 +105,7 @@ declare class EmailChannel implements ICourierChannel {
|
|
|
104
105
|
readonly name = "email";
|
|
105
106
|
private transport;
|
|
106
107
|
constructor(config: IEmailChannelConfig);
|
|
107
|
-
send(message: ICourierMessage, template: IRenderedTemplate): Promise<void>;
|
|
108
|
+
send(message: ICourierMessage, template: IRenderedTemplate): Promise<ISendResult | void>;
|
|
108
109
|
private sendViaResend;
|
|
109
110
|
private sendViaSMTP;
|
|
110
111
|
}
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,14 @@ async function insertMessageLog(entry, store) {
|
|
|
19
19
|
);
|
|
20
20
|
return row?.id ?? "";
|
|
21
21
|
}
|
|
22
|
+
async function setMessageProviderId(id, providerMessageId, store) {
|
|
23
|
+
await store.query(
|
|
24
|
+
`UPDATE fonderie_message_log
|
|
25
|
+
SET provider_message_id = $2
|
|
26
|
+
WHERE id = $1`,
|
|
27
|
+
[id, providerMessageId]
|
|
28
|
+
);
|
|
29
|
+
}
|
|
22
30
|
async function markMessageSent(id, store) {
|
|
23
31
|
await store.query(
|
|
24
32
|
`UPDATE fonderie_message_log
|
|
@@ -116,8 +124,13 @@ var Dispatcher = class {
|
|
|
116
124
|
if (message.locale) logEntry.locale = message.locale;
|
|
117
125
|
const logId = this.store ? await insertMessageLog(logEntry, this.store).catch(() => "") : "";
|
|
118
126
|
try {
|
|
119
|
-
await channel.send(message, template);
|
|
127
|
+
const result = await channel.send(message, template);
|
|
120
128
|
if (this.store && logId) {
|
|
129
|
+
if (result?.providerMessageId) {
|
|
130
|
+
await setMessageProviderId(logId, result.providerMessageId, this.store).catch(
|
|
131
|
+
() => void 0
|
|
132
|
+
);
|
|
133
|
+
}
|
|
121
134
|
markMessageSent(logId, this.store).catch(() => void 0);
|
|
122
135
|
}
|
|
123
136
|
} catch (err) {
|
|
@@ -264,12 +277,12 @@ var EmailChannel = class {
|
|
|
264
277
|
return;
|
|
265
278
|
}
|
|
266
279
|
if (this.config.provider === "resend") {
|
|
267
|
-
|
|
268
|
-
} else if (this.config.provider === "smtp") {
|
|
269
|
-
await this.sendViaSMTP(to, template);
|
|
270
|
-
} else {
|
|
271
|
-
console.warn(`[courier:email] provider ${this.config.provider} not implemented`);
|
|
280
|
+
return this.sendViaResend(to, template);
|
|
272
281
|
}
|
|
282
|
+
if (this.config.provider === "smtp") {
|
|
283
|
+
return this.sendViaSMTP(to, template);
|
|
284
|
+
}
|
|
285
|
+
console.warn(`[courier:email] provider ${this.config.provider} not implemented`);
|
|
273
286
|
}
|
|
274
287
|
async sendViaResend(to, template) {
|
|
275
288
|
if (!this.config.apiKey) {
|
|
@@ -293,18 +306,23 @@ var EmailChannel = class {
|
|
|
293
306
|
const body2 = await res.text();
|
|
294
307
|
throw new Error(`[courier:email] Resend error ${res.status}: ${body2}`);
|
|
295
308
|
}
|
|
309
|
+
const data = await res.json().catch(() => null);
|
|
310
|
+
return typeof data?.id === "string" ? { providerMessageId: data.id } : {};
|
|
296
311
|
}
|
|
297
312
|
async sendViaSMTP(to, template) {
|
|
298
313
|
if (!this.transport) {
|
|
299
314
|
throw new Error("SMTP transport not initialised \u2014 check smtp config");
|
|
300
315
|
}
|
|
301
|
-
await this.transport.sendMail({
|
|
316
|
+
const info = await this.transport.sendMail({
|
|
302
317
|
from: this.config.from,
|
|
303
318
|
to,
|
|
304
319
|
subject: template.subject ?? "(no subject)",
|
|
305
320
|
text: template.text,
|
|
306
321
|
...template.html !== void 0 ? { html: template.html } : {}
|
|
307
322
|
});
|
|
323
|
+
const raw = typeof info?.messageId === "string" ? info.messageId : "";
|
|
324
|
+
const providerMessageId = raw.replace(/^<|>$/g, "");
|
|
325
|
+
return providerMessageId ? { providerMessageId } : {};
|
|
308
326
|
}
|
|
309
327
|
};
|
|
310
328
|
|
|
@@ -589,31 +607,36 @@ function validateCourierConfig(config, registeredChannels) {
|
|
|
589
607
|
}
|
|
590
608
|
|
|
591
609
|
// src/delivery.ts
|
|
592
|
-
import { createHmac } from "crypto";
|
|
610
|
+
import { createHmac, createPublicKey, verify as cryptoVerify } from "crypto";
|
|
593
611
|
import { constantTimeEqual } from "@fonderie/core";
|
|
594
|
-
async function handleSendGridDelivery(req, store,
|
|
595
|
-
if (
|
|
596
|
-
|
|
597
|
-
const ts = req.headers.get("x-twilio-email-event-webhook-timestamp") ?? "";
|
|
598
|
-
const body2 = await req.text();
|
|
599
|
-
if (!verifySendGridSignature(webhookSecret, ts, body2, sig)) {
|
|
600
|
-
return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
|
|
601
|
-
}
|
|
602
|
-
const events2 = parseJson(body2);
|
|
603
|
-
if (!Array.isArray(events2)) return Response.json({ ok: true });
|
|
604
|
-
await processSendGridEvents(events2, store);
|
|
605
|
-
return Response.json({ ok: true });
|
|
612
|
+
async function handleSendGridDelivery(req, store, publicKey) {
|
|
613
|
+
if (!publicKey) {
|
|
614
|
+
return Response.json({ error: "VERIFICATION_NOT_CONFIGURED" }, { status: 401 });
|
|
606
615
|
}
|
|
607
|
-
const
|
|
616
|
+
const sig = req.headers.get("x-twilio-email-event-webhook-signature") ?? "";
|
|
617
|
+
const ts = req.headers.get("x-twilio-email-event-webhook-timestamp") ?? "";
|
|
618
|
+
const body2 = await req.text();
|
|
619
|
+
if (!verifySendGridSignature(publicKey, ts, body2, sig)) {
|
|
620
|
+
return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
|
|
621
|
+
}
|
|
622
|
+
const events = parseJson(body2);
|
|
608
623
|
if (!Array.isArray(events)) return Response.json({ ok: true });
|
|
609
624
|
await processSendGridEvents(events, store);
|
|
610
625
|
return Response.json({ ok: true });
|
|
611
626
|
}
|
|
612
|
-
function verifySendGridSignature(
|
|
627
|
+
function verifySendGridSignature(publicKeyB64, timestamp, body2, signature) {
|
|
613
628
|
try {
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
629
|
+
const key = createPublicKey({
|
|
630
|
+
key: Buffer.from(publicKeyB64, "base64"),
|
|
631
|
+
format: "der",
|
|
632
|
+
type: "spki"
|
|
633
|
+
});
|
|
634
|
+
return cryptoVerify(
|
|
635
|
+
"sha256",
|
|
636
|
+
Buffer.from(timestamp + body2),
|
|
637
|
+
key,
|
|
638
|
+
Buffer.from(signature, "base64")
|
|
639
|
+
);
|
|
617
640
|
} catch {
|
|
618
641
|
return false;
|
|
619
642
|
}
|
|
@@ -642,12 +665,13 @@ async function processSendGridEvents(events, store) {
|
|
|
642
665
|
}
|
|
643
666
|
}
|
|
644
667
|
async function handleMailgunDelivery(req, store, signingKey) {
|
|
668
|
+
if (!signingKey) {
|
|
669
|
+
return Response.json({ error: "VERIFICATION_NOT_CONFIGURED" }, { status: 401 });
|
|
670
|
+
}
|
|
645
671
|
const body2 = await req.json();
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
|
|
650
|
-
}
|
|
672
|
+
const { signature } = body2;
|
|
673
|
+
if (!signature || !verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {
|
|
674
|
+
return Response.json({ error: "INVALID_SIGNATURE" }, { status: 401 });
|
|
651
675
|
}
|
|
652
676
|
const event = body2["event-data"];
|
|
653
677
|
if (event) {
|
|
@@ -892,22 +916,29 @@ var CourierModule = class {
|
|
|
892
916
|
install(app) {
|
|
893
917
|
validateCourierConfig(this.config, this.dispatcher.channelNames());
|
|
894
918
|
const store = this.store;
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
919
|
+
const delivery = this.config.delivery;
|
|
920
|
+
const signingKeys = delivery?.signingKeys;
|
|
921
|
+
if (signingKeys?.sendgrid) {
|
|
922
|
+
app.addRoute(
|
|
923
|
+
"POST",
|
|
924
|
+
"/courier/delivery/sendgrid",
|
|
925
|
+
(ctx) => handleSendGridDelivery(ctx.request, store, signingKeys.sendgrid)
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
if (signingKeys?.mailgun) {
|
|
929
|
+
app.addRoute(
|
|
930
|
+
"POST",
|
|
931
|
+
"/courier/delivery/mailgun",
|
|
932
|
+
(ctx) => handleMailgunDelivery(ctx.request, store, signingKeys.mailgun)
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
if (delivery?.allowUnverifiedMailtrap) {
|
|
936
|
+
app.addRoute(
|
|
937
|
+
"POST",
|
|
938
|
+
"/courier/delivery/mailtrap",
|
|
939
|
+
(ctx) => handleMailtrapDelivery(ctx.request, store)
|
|
940
|
+
);
|
|
941
|
+
}
|
|
911
942
|
if (this.config.adminToken) {
|
|
912
943
|
if (!store) {
|
|
913
944
|
throw new Error("[courier] adminToken requires @fonderie/store (db templates)");
|