mailery 0.16.0 → 0.16.2

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
@@ -190,7 +190,15 @@ type WebhookToleranceOption = number | false;
190
190
 
191
191
  interface SendGridProviderOptions {
192
192
  apiKey: string;
193
- /** ECDSA public key (PEM) from SendGrid → Settings → Mail Settings → Signed Event Webhook. */
193
+ /**
194
+ * ECDSA public key from SendGrid → Settings → Mail Settings → Signed Event
195
+ * Webhook. Either form SendGrid hands out is accepted: the single-line
196
+ * base64 string its dashboard shows and its API returns as `public_key`
197
+ * (what `mailery setup-sendgrid` prints), or the same key as a PEM block.
198
+ * A PEM whose newlines were escaped to `\n` for an .env line is unescaped.
199
+ * Anything that does not parse as a public key throws at construction —
200
+ * a key that silently verifies nothing is the failure this replaces.
201
+ */
194
202
  webhookVerificationKey?: string;
195
203
  /**
196
204
  * Replay window for the signed webhook timestamp, in seconds.
@@ -213,6 +221,8 @@ declare class SendGridProvider implements MailProvider {
213
221
  readonly sendRatePerSecond: number;
214
222
  /** Resolved replay window in seconds; `0` means the check is disabled. */
215
223
  readonly webhookToleranceSeconds: number;
224
+ /** The verification key as PEM, whatever shape it was configured in. */
225
+ private readonly verificationKeyPem;
216
226
  constructor(opts: SendGridProviderOptions);
217
227
  send(args: SendArgs): Promise<SendResult>;
218
228
  verifyWebhook(rawBody: Buffer, headers: Record<string, string>): Promise<boolean>;
package/dist/index.d.ts CHANGED
@@ -190,7 +190,15 @@ type WebhookToleranceOption = number | false;
190
190
 
191
191
  interface SendGridProviderOptions {
192
192
  apiKey: string;
193
- /** ECDSA public key (PEM) from SendGrid → Settings → Mail Settings → Signed Event Webhook. */
193
+ /**
194
+ * ECDSA public key from SendGrid → Settings → Mail Settings → Signed Event
195
+ * Webhook. Either form SendGrid hands out is accepted: the single-line
196
+ * base64 string its dashboard shows and its API returns as `public_key`
197
+ * (what `mailery setup-sendgrid` prints), or the same key as a PEM block.
198
+ * A PEM whose newlines were escaped to `\n` for an .env line is unescaped.
199
+ * Anything that does not parse as a public key throws at construction —
200
+ * a key that silently verifies nothing is the failure this replaces.
201
+ */
194
202
  webhookVerificationKey?: string;
195
203
  /**
196
204
  * Replay window for the signed webhook timestamp, in seconds.
@@ -213,6 +221,8 @@ declare class SendGridProvider implements MailProvider {
213
221
  readonly sendRatePerSecond: number;
214
222
  /** Resolved replay window in seconds; `0` means the check is disabled. */
215
223
  readonly webhookToleranceSeconds: number;
224
+ /** The verification key as PEM, whatever shape it was configured in. */
225
+ private readonly verificationKeyPem;
216
226
  constructor(opts: SendGridProviderOptions);
217
227
  send(args: SendArgs): Promise<SendResult>;
218
228
  verifyWebhook(rawBody: Buffer, headers: Record<string, string>): Promise<boolean>;
package/dist/index.js CHANGED
@@ -268,15 +268,49 @@ var init_webhook_tolerance = __esm({
268
268
  // src/server/providers/sendgrid.ts
269
269
  var sendgrid_exports = {};
270
270
  __export(sendgrid_exports, {
271
- SendGridProvider: () => SendGridProvider
271
+ SendGridProvider: () => SendGridProvider,
272
+ normalizeSendGridMessageId: () => normalizeSendGridMessageId,
273
+ normalizeWebhookVerificationKey: () => normalizeWebhookVerificationKey
272
274
  });
275
+ function normalizeWebhookVerificationKey(input) {
276
+ let key = String(input ?? "").trim().replace(/^["']|["']$/g, "");
277
+ if (key.includes("\\n")) key = key.replace(/\\n/g, "\n");
278
+ let pem;
279
+ if (key.includes(PEM_HEADER)) {
280
+ pem = key;
281
+ } else {
282
+ const b64 = key.replace(/\s+/g, "");
283
+ if (!b64 || !/^[A-Za-z0-9+/=]+$/.test(b64)) {
284
+ throw new Error(
285
+ "SendGridProvider: webhookVerificationKey is neither a PEM public key nor a base64 SPKI key. Copy the Verification Key from SendGrid \u2192 Settings \u2192 Mail Settings \u2192 Signed Event Webhook."
286
+ );
287
+ }
288
+ pem = `${PEM_HEADER}
289
+ ${b64.match(/.{1,64}/g).join("\n")}
290
+ ${PEM_FOOTER}
291
+ `;
292
+ }
293
+ try {
294
+ crypto2.createPublicKey(pem);
295
+ } catch (err) {
296
+ throw new Error(
297
+ `SendGridProvider: webhookVerificationKey did not parse as a public key (${String(err?.message ?? err)}). Copy the Verification Key from SendGrid \u2192 Settings \u2192 Mail Settings \u2192 Signed Event Webhook.`
298
+ );
299
+ }
300
+ return pem;
301
+ }
302
+ function normalizeSendGridMessageId(raw) {
303
+ const id = String(raw ?? "");
304
+ const marker = /\.(?:filter|recvd)\S*$/i.exec(id);
305
+ return marker ? id.slice(0, marker.index) : id;
306
+ }
273
307
  function normalizeSendGridEvent(e) {
274
308
  const type = mapEventType(e.event);
275
309
  if (!type) return null;
276
310
  return {
277
311
  type,
278
312
  providerEventId: String(e.sg_event_id ?? e["smtp-id"] ?? `${e.event}-${e.timestamp}-${e.email}`),
279
- providerMessageId: String(e.sg_message_id ?? e["smtp-id"] ?? ""),
313
+ providerMessageId: e.sg_message_id != null ? normalizeSendGridMessageId(e.sg_message_id) : String(e["smtp-id"] ?? ""),
280
314
  email: String(e.email ?? "").toLowerCase(),
281
315
  occurredAt: new Date(Number(e.timestamp) * 1e3),
282
316
  details: {
@@ -308,24 +342,29 @@ function mapEventType(sgEvent) {
308
342
  return null;
309
343
  }
310
344
  }
311
- var SG_SIG_HEADER, SG_TS_HEADER, SendGridProvider;
345
+ var SG_SIG_HEADER, SG_TS_HEADER, PEM_HEADER, PEM_FOOTER, SendGridProvider;
312
346
  var init_sendgrid = __esm({
313
347
  "src/server/providers/sendgrid.ts"() {
314
348
  init_webhook_tolerance();
315
349
  SG_SIG_HEADER = "x-twilio-email-event-webhook-signature";
316
350
  SG_TS_HEADER = "x-twilio-email-event-webhook-timestamp";
351
+ PEM_HEADER = "-----BEGIN PUBLIC KEY-----";
352
+ PEM_FOOTER = "-----END PUBLIC KEY-----";
317
353
  SendGridProvider = class {
318
354
  constructor(opts) {
319
355
  this.opts = opts;
320
356
  sgMail.setApiKey(opts.apiKey);
321
357
  this.sendRatePerSecond = opts.sendRatePerSecond ?? 10;
322
358
  this.webhookToleranceSeconds = resolveWebhookToleranceSeconds(opts.webhookToleranceSeconds);
359
+ this.verificationKeyPem = opts.webhookVerificationKey ? normalizeWebhookVerificationKey(opts.webhookVerificationKey) : null;
323
360
  }
324
361
  opts;
325
362
  name = "sendgrid";
326
363
  sendRatePerSecond;
327
364
  /** Resolved replay window in seconds; `0` means the check is disabled. */
328
365
  webhookToleranceSeconds;
366
+ /** The verification key as PEM, whatever shape it was configured in. */
367
+ verificationKeyPem;
329
368
  async send(args) {
330
369
  const msg = {
331
370
  to: args.to,
@@ -357,7 +396,7 @@ var init_sendgrid = __esm({
357
396
  };
358
397
  }
359
398
  async verifyWebhook(rawBody, headers) {
360
- if (!this.opts.webhookVerificationKey) return false;
399
+ if (!this.verificationKeyPem) return false;
361
400
  const sig = headers[SG_SIG_HEADER];
362
401
  const ts = headers[SG_TS_HEADER];
363
402
  if (!sig || !ts) return false;
@@ -365,7 +404,7 @@ var init_sendgrid = __esm({
365
404
  try {
366
405
  const verifier = crypto2.createVerify("sha256");
367
406
  verifier.update(payload);
368
- if (!verifier.verify(this.opts.webhookVerificationKey, sig, "base64")) return false;
407
+ if (!verifier.verify(this.verificationKeyPem, sig, "base64")) return false;
369
408
  } catch {
370
409
  return false;
371
410
  }
@@ -1708,11 +1747,20 @@ async function processWebhookBacklog(ctx, opts = {}) {
1708
1747
  }
1709
1748
  }
1710
1749
  }
1750
+ async function findSendForEvent(event, ctx) {
1751
+ if (event.providerMessageId) {
1752
+ const byId = await ctx.collections.sends.findOne({ providerMessageId: event.providerMessageId });
1753
+ if (byId) return byId;
1754
+ }
1755
+ if (!event.email) return null;
1756
+ return ctx.collections.sends.findOne({ emailAtSend: event.email, sentAt: { $ne: null } }, { sort: { sentAt: -1 } });
1757
+ }
1758
+ function webhookEventsForMessageId(providerMessageId) {
1759
+ const escaped = providerMessageId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1760
+ return { $or: [{ providerMessageId }, { providerMessageId: { $regex: `^${escaped}\\.` } }] };
1761
+ }
1711
1762
  async function applyWebhookEvent(event, ctx) {
1712
- const send = await ctx.collections.sends.findOne(
1713
- event.providerMessageId ? { $or: [{ providerMessageId: event.providerMessageId }, { emailAtSend: event.email }] } : { emailAtSend: event.email },
1714
- { sort: { queuedAt: -1 } }
1715
- );
1763
+ const send = await findSendForEvent(event, ctx);
1716
1764
  switch (event.type) {
1717
1765
  case "delivered":
1718
1766
  if (send) {
@@ -6466,7 +6514,7 @@ function createAdminApiRouter(mailer, opts = {}) {
6466
6514
  if (!ObjectId.isValid(id)) return res.status(400).json({ error: "bad_id" });
6467
6515
  const send = await c.sends.findOne({ _id: new ObjectId(id) });
6468
6516
  if (!send) return res.status(404).json({ error: "not_found" });
6469
- const events = send.providerMessageId ? await c.webhookEvents.find({ providerMessageId: send.providerMessageId }).sort({ receivedAt: -1 }).limit(100).toArray() : [];
6517
+ const events = send.providerMessageId ? await c.webhookEvents.find(webhookEventsForMessageId(send.providerMessageId)).sort({ receivedAt: -1 }).limit(100).toArray() : [];
6470
6518
  return res.json({ send, webhookEvents: events });
6471
6519
  })
6472
6520
  );
@@ -8205,7 +8253,7 @@ function unitToMs2(value, unit) {
8205
8253
  }
8206
8254
 
8207
8255
  // src/server/api/agent.ts
8208
- var VERSION = "0.16.0" ;
8256
+ var VERSION = "0.16.2" ;
8209
8257
  var MIN_AGENT_TOKEN_LENGTH = 24;
8210
8258
  function createAgentRouter(mailer, opts) {
8211
8259
  if (!opts || !Array.isArray(opts.tokens) || opts.tokens.length === 0) {
@@ -8408,7 +8456,7 @@ function createAgentRouter(mailer, opts) {
8408
8456
  if (reached || Date.now() - started >= timeoutMs) break;
8409
8457
  await sleep2(1e3);
8410
8458
  }
8411
- const webhookEvents = send.providerMessageId ? await c.webhookEvents.find({ providerMessageId: send.providerMessageId }).sort({ receivedAt: 1 }).limit(100).toArray() : [];
8459
+ const webhookEvents = send.providerMessageId ? await c.webhookEvents.find(webhookEventsForMessageId(send.providerMessageId)).sort({ receivedAt: 1 }).limit(100).toArray() : [];
8412
8460
  res.json({ reached, target, waitedMs: Date.now() - started, send: sendSummary(send), webhookEvents });
8413
8461
  })
8414
8462
  );
@@ -9905,7 +9953,7 @@ var DEDUPE_POLICIES = [
9905
9953
  ];
9906
9954
 
9907
9955
  // src/server/index.ts
9908
- var VERSION2 = "0.16.0" ;
9956
+ var VERSION2 = "0.16.2" ;
9909
9957
 
9910
9958
  export { DEDUPE_POLICIES, DEFAULT_BOT_UA_RE, FLOW_STEP_KINDS, FlowOperationError, MIN_AGENT_TOKEN_LENGTH, Mailer, MongoContactAdapter, NullProvider, PREDICATE_KINDS, RESERVED_VAR_KEYS, SEGMENT_FILTER_KINDS, SendGridProvider, TRACKING_SIG_LENGTH, VERSION2 as VERSION, applyTracking, applyWebhookEvent, armFlow, compileMailyTemplate, compileTemplate, computeDeliveryTime, createAdminApiRouter, createAdminRouter, createAgentRouter, createPublicRouter, defaultFlowStep, defaultPredicate, defaultSegmentFilter, defineVars, derivePlaintext, disarmFlow, dispatchSend, drainPendingUnsubscribes, ensureIndexes, gateFlow, getCollections, isCanaryGate, predicateKind, processNewlyFiredEventTriggers, processOneRunStep, referencedPaths, renderForContact, renderTemplate, runTick, sendgridInboundParser, sha256Hex, signTrackingToken, signUnsubscribeToken, simulateFlow, stampWatermarkIfNull, sweepStrandedFlowRuns, ungateFlow, validateSenderDomain, varsJsonSchema, verifyTemplate, verifyTrackingToken, verifyUnsubscribeToken };
9911
9959
  //# sourceMappingURL=index.js.map