@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/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, webhookSecret?: string): Promise<Response>;
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, webhookSecret?: string): Promise<Response>;
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
- await this.sendViaResend(to, template);
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, webhookSecret) {
595
- if (webhookSecret) {
596
- const sig = req.headers.get("x-twilio-email-event-webhook-signature") ?? "";
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 events = await req.json();
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(secret, timestamp, body2, signature) {
627
+ function verifySendGridSignature(publicKeyB64, timestamp, body2, signature) {
613
628
  try {
614
- const payload = timestamp + body2;
615
- const expected = createHmac("sha256", secret).update(payload).digest("base64");
616
- return constantTimeEqual(Buffer.from(signature, "base64"), Buffer.from(expected, "base64"));
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
- if (signingKey) {
647
- const { signature } = body2;
648
- if (!signature || !verifyMailgunSignature(signingKey, signature.timestamp, signature.token, signature.signature)) {
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 signingKeys = this.config.delivery?.signingKeys;
896
- app.addRoute(
897
- "POST",
898
- "/courier/delivery/sendgrid",
899
- (ctx) => handleSendGridDelivery(ctx.request, store, signingKeys?.sendgrid)
900
- );
901
- app.addRoute(
902
- "POST",
903
- "/courier/delivery/mailgun",
904
- (ctx) => handleMailgunDelivery(ctx.request, store, signingKeys?.mailgun)
905
- );
906
- app.addRoute(
907
- "POST",
908
- "/courier/delivery/mailtrap",
909
- (ctx) => handleMailtrapDelivery(ctx.request, store)
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)");