@growth-labs/mailer 0.2.1 → 0.3.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.
Files changed (57) hide show
  1. package/README.md +5 -1
  2. package/dist/_internal/schema-probe.d.ts +30 -0
  3. package/dist/_internal/schema-probe.d.ts.map +1 -0
  4. package/dist/_internal/schema-probe.js +68 -0
  5. package/dist/_internal/schema-probe.js.map +1 -0
  6. package/dist/options.d.ts +58 -0
  7. package/dist/options.d.ts.map +1 -1
  8. package/dist/options.js +22 -0
  9. package/dist/options.js.map +1 -1
  10. package/dist/queue/consumer.d.ts.map +1 -1
  11. package/dist/queue/consumer.js +12 -0
  12. package/dist/queue/consumer.js.map +1 -1
  13. package/dist/routes/confirm.d.ts.map +1 -1
  14. package/dist/routes/confirm.js +5 -0
  15. package/dist/routes/confirm.js.map +1 -1
  16. package/dist/routes/subscribe.d.ts.map +1 -1
  17. package/dist/routes/subscribe.js +6 -0
  18. package/dist/routes/subscribe.js.map +1 -1
  19. package/dist/routes/track-click.d.ts.map +1 -1
  20. package/dist/routes/track-click.js +16 -9
  21. package/dist/routes/track-click.js.map +1 -1
  22. package/dist/routes/track-open.d.ts.map +1 -1
  23. package/dist/routes/track-open.js +15 -9
  24. package/dist/routes/track-open.js.map +1 -1
  25. package/dist/routes/unsubscribe.d.ts.map +1 -1
  26. package/dist/routes/unsubscribe.js +10 -0
  27. package/dist/routes/unsubscribe.js.map +1 -1
  28. package/dist/routes/webhook.d.ts.map +1 -1
  29. package/dist/routes/webhook.js +24 -1
  30. package/dist/routes/webhook.js.map +1 -1
  31. package/dist/utils/analytics.d.ts +1 -1
  32. package/dist/utils/analytics.d.ts.map +1 -1
  33. package/dist/utils/analytics.js +1 -0
  34. package/dist/utils/analytics.js.map +1 -1
  35. package/dist/utils/index.d.ts +1 -0
  36. package/dist/utils/index.d.ts.map +1 -1
  37. package/dist/utils/index.js +1 -0
  38. package/dist/utils/index.js.map +1 -1
  39. package/dist/utils/webhook-signature.d.ts +6 -0
  40. package/dist/utils/webhook-signature.d.ts.map +1 -0
  41. package/dist/utils/webhook-signature.js +59 -0
  42. package/dist/utils/webhook-signature.js.map +1 -0
  43. package/migrations/0001_create_gl_mailer_tables.sql +48 -0
  44. package/package.json +2 -1
  45. package/src/_internal/schema-probe.ts +89 -0
  46. package/src/options.ts +22 -0
  47. package/src/queue/consumer.ts +13 -0
  48. package/src/routes/confirm.ts +6 -0
  49. package/src/routes/subscribe.ts +7 -0
  50. package/src/routes/track-click.ts +21 -14
  51. package/src/routes/track-open.ts +20 -14
  52. package/src/routes/unsubscribe.ts +9 -0
  53. package/src/routes/webhook.ts +25 -1
  54. package/src/utils/analytics.ts +2 -0
  55. package/src/utils/index.ts +1 -0
  56. package/src/utils/webhook-signature.ts +91 -0
  57. package/src/virtual.d.ts +14 -0
@@ -1 +1 @@
1
- {"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/routes/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAE1D,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,wBAAwB,EAA6B,MAAM,uBAAuB,CAAA;AAC3F,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAUlF,MAAM,CAAC,MAAM,IAAI,GAAa,KAAK,EAAE,OAAO,EAAE,EAAE;IAC/C,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IAC3B,MAAM,IAAI,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,CAAmB,CAAA;IAErD,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QAC/B,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACpE,CAAC;IAED,MAAM,WAAW,GAAG,aAAwC,CAAA;IAC5D,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,SAAS,CAAe,CAAA;IACtD,IAAI,CAAC,EAAE,EAAE,CAAC;QACT,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,0BAA0B,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC7E,CAAC;IACD,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAAA;IAEtB,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,UAAU;YACd,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;gBACrB,MAAM,cAAc,CAAC,EAAE,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;YAC1C,CAAC;YACD,MAAK;QACN,KAAK,QAAQ;YACZ,MAAM,YAAY,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,IAAI,MAAM,CAAC,CAAA;YAC7D,MAAK;QACN,KAAK,WAAW;YACf,MAAM,eAAe,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;YACrC,MAAK;IACP,CAAC;IAED,wBAAwB,CAAC,MAAM,EAAE,WAAW,EAAE,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QAC7E,OAAO;QACP,OAAO;QACP,WAAW,EAAE,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,KAAK;QAC1C,KAAK,EAAE;YACN,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,UAAU,EAAE,IAAI,CAAC,UAAU;SAC3B;KACD,CAAC,CAAA;IAEF,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;AACnC,CAAC,CAAA;AAED,SAAS,mBAAmB,CAAC,IAA4B;IACxD,QAAQ,IAAI,EAAE,CAAC;QACd,KAAK,UAAU;YACd,OAAO,sBAAsB,CAAA;QAC9B,KAAK,QAAQ;YACZ,OAAO,oBAAoB,CAAA;QAC5B,KAAK,WAAW;YACf,OAAO,uBAAuB,CAAA;IAChC,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/routes/webhook.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import type { ResolvedMailerOptions } from '../options.js';
2
- export type MailerAnalyticsEvent = 'newsletter_subscribed' | 'newsletter_confirmed' | 'newsletter_unsubscribed' | 'newsletter_opened' | 'newsletter_clicked' | 'newsletter_delivered' | 'newsletter_bounced' | 'newsletter_complained' | 'newsletter_sent' | 'newsletter_send_failed';
2
+ export type MailerAnalyticsEvent = 'newsletter_subscribed' | 'newsletter_confirmed' | 'newsletter_unsubscribed' | 'newsletter_opened' | 'newsletter_clicked' | 'newsletter_webhook_received' | 'newsletter_delivered' | 'newsletter_bounced' | 'newsletter_complained' | 'newsletter_sent' | 'newsletter_send_failed';
3
3
  interface AnalyticsContext {
4
4
  locals?: {
5
5
  cfContext?: {
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../src/utils/analytics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAE1D,MAAM,MAAM,oBAAoB,GAC7B,uBAAuB,GACvB,sBAAsB,GACtB,yBAAyB,GACzB,mBAAmB,GACnB,oBAAoB,GACpB,sBAAsB,GACtB,oBAAoB,GACpB,uBAAuB,GACvB,iBAAiB,GACjB,wBAAwB,CAAA;AAM3B,UAAU,gBAAgB;IACzB,MAAM,CAAC,EAAE;QACR,SAAS,CAAC,EAAE;YACX,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;SAC1C,CAAA;KACD,CAAA;CACD;AAED,UAAU,0BAA0B;IACnC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,gBAAgB,CAAA;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,wBAAwB,CACvC,OAAO,EAAE,qBAAqB,EAC9B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,SAAS,EAAE,oBAAoB,EAC/B,WAAW,GAAE,0BAA+B,GAC1C,OAAO,CAeT;AAED,wBAAgB,6BAA6B,CAC5C,OAAO,EAAE,qBAAqB,EAC9B,SAAS,EAAE,oBAAoB,EAC/B,WAAW,GAAE,0BAA+B,GAC1C;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CA+B3D"}
1
+ {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../src/utils/analytics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAE1D,MAAM,MAAM,oBAAoB,GAC7B,uBAAuB,GACvB,sBAAsB,GACtB,yBAAyB,GACzB,mBAAmB,GACnB,oBAAoB,GACpB,6BAA6B,GAC7B,sBAAsB,GACtB,oBAAoB,GACpB,uBAAuB,GACvB,iBAAiB,GACjB,wBAAwB,CAAA;AAM3B,UAAU,gBAAgB;IACzB,MAAM,CAAC,EAAE;QACR,SAAS,CAAC,EAAE;YACX,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;SAC1C,CAAA;KACD,CAAA;CACD;AAED,UAAU,0BAA0B;IACnC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,gBAAgB,CAAA;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,wBAAwB,CACvC,OAAO,EAAE,qBAAqB,EAC9B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,SAAS,EAAE,oBAAoB,EAC/B,WAAW,GAAE,0BAA+B,GAC1C,OAAO,CAeT;AAED,wBAAgB,6BAA6B,CAC5C,OAAO,EAAE,qBAAqB,EAC9B,SAAS,EAAE,oBAAoB,EAC/B,WAAW,GAAE,0BAA+B,GAC1C;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CA+B3D"}
@@ -62,6 +62,7 @@ function categoryForMailerEvent(eventName) {
62
62
  case 'newsletter_opened':
63
63
  case 'newsletter_clicked':
64
64
  return 'interaction';
65
+ case 'newsletter_webhook_received':
65
66
  case 'newsletter_delivered':
66
67
  case 'newsletter_bounced':
67
68
  case 'newsletter_complained':
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.js","sourceRoot":"","sources":["../../src/utils/analytics.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"analytics.js","sourceRoot":"","sources":["../../src/utils/analytics.ts"],"names":[],"mappings":"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"}
@@ -10,4 +10,5 @@ export { confirmSubscriber, countSubscribers, createSubscriber, getSubscriberBat
10
10
  export { inlineStyles, interpolate, processConditionals, renderDigestItems, renderEmail, } from './templates.js';
11
11
  export { generateToken, verifyToken } from './tokens.js';
12
12
  export { injectTrackingPixel, rewriteLinksForTracking, TRANSPARENT_GIF, } from './tracking.js';
13
+ export { signWebhookPayload, verifyWebhookSignature } from './webhook-signature.js';
13
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,6BAA6B,EAC7B,wBAAwB,EACxB,KAAK,oBAAoB,GACzB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACN,YAAY,EACZ,eAAe,EACf,cAAc,EACd,gBAAgB,GAChB,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAC3D,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAC5E,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACvE,OAAO,EACN,uBAAuB,EACvB,qBAAqB,EACrB,eAAe,EACf,aAAa,EACb,kBAAkB,GAClB,MAAM,iBAAiB,CAAA;AACxB,YAAY,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAC1C,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AACvE,OAAO,EACN,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,GACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACN,YAAY,EACZ,WAAW,EACX,mBAAmB,EACnB,iBAAiB,EACjB,WAAW,GACX,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,EACN,mBAAmB,EACnB,uBAAuB,EACvB,eAAe,GACf,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,6BAA6B,EAC7B,wBAAwB,EACxB,KAAK,oBAAoB,GACzB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACN,YAAY,EACZ,eAAe,EACf,cAAc,EACd,gBAAgB,GAChB,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAC3D,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAC5E,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACvE,OAAO,EACN,uBAAuB,EACvB,qBAAqB,EACrB,eAAe,EACf,aAAa,EACb,kBAAkB,GAClB,MAAM,iBAAiB,CAAA;AACxB,YAAY,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAC1C,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AACvE,OAAO,EACN,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,GACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACN,YAAY,EACZ,WAAW,EACX,mBAAmB,EACnB,iBAAiB,EACjB,WAAW,GACX,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,EACN,mBAAmB,EACnB,uBAAuB,EACvB,eAAe,GACf,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA"}
@@ -7,4 +7,5 @@ export { confirmSubscriber, countSubscribers, createSubscriber, getSubscriberBat
7
7
  export { inlineStyles, interpolate, processConditionals, renderDigestItems, renderEmail, } from './templates.js';
8
8
  export { generateToken, verifyToken } from './tokens.js';
9
9
  export { injectTrackingPixel, rewriteLinksForTracking, TRANSPARENT_GIF, } from './tracking.js';
10
+ export { signWebhookPayload, verifyWebhookSignature } from './webhook-signature.js';
10
11
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,6BAA6B,EAC7B,wBAAwB,GAExB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACN,YAAY,EACZ,eAAe,EACf,cAAc,EACd,gBAAgB,GAChB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAE5E,OAAO,EACN,uBAAuB,EACvB,qBAAqB,EACrB,eAAe,EACf,aAAa,EACb,kBAAkB,GAClB,MAAM,iBAAiB,CAAA;AAExB,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AACvE,OAAO,EACN,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,GACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACN,YAAY,EACZ,WAAW,EACX,mBAAmB,EACnB,iBAAiB,EACjB,WAAW,GACX,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,EACN,mBAAmB,EACnB,uBAAuB,EACvB,eAAe,GACf,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,6BAA6B,EAC7B,wBAAwB,GAExB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACN,YAAY,EACZ,eAAe,EACf,cAAc,EACd,gBAAgB,GAChB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAE5E,OAAO,EACN,uBAAuB,EACvB,qBAAqB,EACrB,eAAe,EACf,aAAa,EACb,kBAAkB,GAClB,MAAM,iBAAiB,CAAA;AAExB,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AACvE,OAAO,EACN,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,GACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACN,YAAY,EACZ,WAAW,EACX,mBAAmB,EACnB,iBAAiB,EACjB,WAAW,GACX,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,EACN,mBAAmB,EACnB,uBAAuB,EACvB,eAAe,GACf,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA"}
@@ -0,0 +1,6 @@
1
+ import type { ResolvedMailerOptions } from '../options.js';
2
+ type WebhookSignatureOptions = ResolvedMailerOptions['webhookSignature'];
3
+ export declare function verifyWebhookSignature(options: WebhookSignatureOptions, request: Request, body: string, nowMs?: number): Promise<boolean>;
4
+ export declare function signWebhookPayload(secret: string, timestamp: string, body: string): Promise<string>;
5
+ export {};
6
+ //# sourceMappingURL=webhook-signature.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-signature.d.ts","sourceRoot":"","sources":["../../src/utils/webhook-signature.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAE1D,KAAK,uBAAuB,GAAG,qBAAqB,CAAC,kBAAkB,CAAC,CAAA;AAExE,wBAAsB,sBAAsB,CAC3C,OAAO,EAAE,uBAAuB,EAChC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,MAAmB,GACxB,OAAO,CAAC,OAAO,CAAC,CAoBlB;AAED,wBAAsB,kBAAkB,CACvC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,CAQjB"}
@@ -0,0 +1,59 @@
1
+ export async function verifyWebhookSignature(options, request, body, nowMs = Date.now()) {
2
+ if (!options.enabled)
3
+ return true;
4
+ const timestamp = request.headers.get(options.timestampHeader);
5
+ const signatureHeader = request.headers.get(options.header);
6
+ if (!timestamp || !signatureHeader)
7
+ return false;
8
+ if (!timestampWithinTolerance(timestamp, options.toleranceSeconds, nowMs))
9
+ return false;
10
+ const signature = signatureFromHeader(signatureHeader);
11
+ if (!signature)
12
+ return false;
13
+ const key = await importWebhookKey(options.secret);
14
+ const signedBody = `${timestamp}.${body}`;
15
+ return crypto.subtle.verify('HMAC', key, fromBase64Url(signature), new TextEncoder().encode(signedBody));
16
+ }
17
+ export async function signWebhookPayload(secret, timestamp, body) {
18
+ const key = await importWebhookKey(secret);
19
+ const signature = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(`${timestamp}.${body}`));
20
+ return toBase64Url(signature);
21
+ }
22
+ function timestampWithinTolerance(timestamp, toleranceSeconds, nowMs) {
23
+ if (toleranceSeconds === 0)
24
+ return true;
25
+ const parsed = Number(timestamp);
26
+ if (!Number.isFinite(parsed))
27
+ return false;
28
+ const timestampMs = parsed > 1_000_000_000_000 ? parsed : parsed * 1000;
29
+ return Math.abs(nowMs - timestampMs) <= toleranceSeconds * 1000;
30
+ }
31
+ function signatureFromHeader(header) {
32
+ const parts = header.split(',').map((part) => part.trim());
33
+ for (const part of parts) {
34
+ if (part.startsWith('v1='))
35
+ return part.slice(3);
36
+ }
37
+ return parts[0] || null;
38
+ }
39
+ async function importWebhookKey(secret) {
40
+ return crypto.subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']);
41
+ }
42
+ function fromBase64Url(value) {
43
+ const padded = value
44
+ .replace(/-/g, '+')
45
+ .replace(/_/g, '/')
46
+ .padEnd(Math.ceil(value.length / 4) * 4, '=');
47
+ const binary = atob(padded);
48
+ const bytes = new Uint8Array(new ArrayBuffer(binary.length));
49
+ for (let i = 0; i < binary.length; i++)
50
+ bytes[i] = binary.charCodeAt(i);
51
+ return bytes;
52
+ }
53
+ function toBase64Url(bytes) {
54
+ let binary = '';
55
+ for (const byte of new Uint8Array(bytes))
56
+ binary += String.fromCharCode(byte);
57
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
58
+ }
59
+ //# sourceMappingURL=webhook-signature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-signature.js","sourceRoot":"","sources":["../../src/utils/webhook-signature.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ -- 0001_create_gl_mailer_tables.sql
2
+ -- @growth-labs/mailer v0.3.0+ schema.
3
+ -- Compatible with `wrangler d1 migrations apply`.
4
+ -- All statements are idempotent (IF NOT EXISTS).
5
+
6
+ CREATE TABLE IF NOT EXISTS gl_subscribers (
7
+ id TEXT PRIMARY KEY,
8
+ email TEXT NOT NULL UNIQUE,
9
+ name TEXT,
10
+ status TEXT NOT NULL DEFAULT 'pending',
11
+ preferences TEXT NOT NULL DEFAULT '[]',
12
+ source TEXT NOT NULL,
13
+ attribution TEXT,
14
+ soft_bounce_count INTEGER NOT NULL DEFAULT 0,
15
+ subscribed_at TEXT NOT NULL,
16
+ confirmed_at TEXT,
17
+ unsubscribed_at TEXT,
18
+ created_at TEXT NOT NULL,
19
+ updated_at TEXT NOT NULL
20
+ );
21
+
22
+ CREATE TABLE IF NOT EXISTS gl_email_sends (
23
+ id TEXT PRIMARY KEY,
24
+ subscriber_id TEXT NOT NULL REFERENCES gl_subscribers(id),
25
+ campaign_id TEXT,
26
+ email TEXT NOT NULL,
27
+ subject TEXT NOT NULL,
28
+ type TEXT NOT NULL,
29
+ status TEXT NOT NULL DEFAULT 'queued',
30
+ sent_at TEXT,
31
+ delivered_at TEXT,
32
+ opened_at TEXT,
33
+ clicked_at TEXT,
34
+ bounced_at TEXT,
35
+ bounce_type TEXT,
36
+ complained_at TEXT,
37
+ tracking_id TEXT NOT NULL UNIQUE,
38
+ created_at TEXT NOT NULL
39
+ );
40
+
41
+ CREATE INDEX IF NOT EXISTS idx_subscribers_status ON gl_subscribers(status);
42
+ CREATE INDEX IF NOT EXISTS idx_subscribers_email ON gl_subscribers(email);
43
+ CREATE INDEX IF NOT EXISTS idx_subscribers_subscribed_at ON gl_subscribers(subscribed_at);
44
+ CREATE INDEX IF NOT EXISTS idx_sends_subscriber ON gl_email_sends(subscriber_id);
45
+ CREATE INDEX IF NOT EXISTS idx_sends_campaign ON gl_email_sends(campaign_id);
46
+ CREATE INDEX IF NOT EXISTS idx_sends_tracking ON gl_email_sends(tracking_id);
47
+ CREATE INDEX IF NOT EXISTS idx_sends_status ON gl_email_sends(status);
48
+ CREATE INDEX IF NOT EXISTS idx_sends_type_created ON gl_email_sends(type, created_at);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growth-labs/mailer",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "exports": {
@@ -50,6 +50,7 @@
50
50
  },
51
51
  "files": [
52
52
  "dist",
53
+ "migrations",
53
54
  "src",
54
55
  "README.md"
55
56
  ],
@@ -0,0 +1,89 @@
1
+ // One-shot D1 schema probe with module-scoped cache. Mailer routes that touch
2
+ // gl_subscribers / gl_email_sends call this before doing D1 work. On miss the
3
+ // route returns 503 with the GL_MAILER_SCHEMA_MISSING code instead of letting
4
+ // drizzle throw and falling into Astro's SSR error template.
5
+ //
6
+ // Does NOT throw. A throw here would recreate the silent-500 cascade that
7
+ // motivates this release. The contract: schema missing → mailer disabled
8
+ // for this Worker's lifetime, affected routes return 503 with a diagnostic
9
+ // body, observability gets one loud error per instance startup.
10
+
11
+ export const GL_MAILER_SCHEMA_MISSING = 'GL_MAILER_SCHEMA_MISSING'
12
+
13
+ export type ProbeResult = { ok: boolean }
14
+
15
+ interface D1PreparedStatement {
16
+ first<T = unknown>(): Promise<T | null>
17
+ }
18
+
19
+ interface D1DatabaseLike {
20
+ prepare(query: string): D1PreparedStatement
21
+ }
22
+
23
+ let _cached: ProbeResult | null = null
24
+
25
+ /** @internal Reset the module-scoped cache — tests only. */
26
+ export function _resetSchemaProbeCache(): void {
27
+ _cached = null
28
+ }
29
+
30
+ /**
31
+ * Probe the configured D1 binding for `gl_subscribers` AND `gl_email_sends`.
32
+ * Returns `{ ok: true }` on success, `{ ok: false }` on failure (either table
33
+ * missing). Caches the result on the first call per Worker instance.
34
+ *
35
+ * Does not throw. A missing binding is treated as `ok: false` and logged —
36
+ * mailer cannot function without D1, so unlike analytics this is loud rather
37
+ * than silent.
38
+ */
39
+ export async function probeMailerSchema(
40
+ db: D1DatabaseLike | undefined,
41
+ d1Binding: string,
42
+ ): Promise<ProbeResult> {
43
+ if (_cached) return _cached
44
+ if (!db) {
45
+ logSchemaMissing(d1Binding, 'D1 binding is not bound')
46
+ _cached = { ok: false }
47
+ return _cached
48
+ }
49
+ try {
50
+ await db.prepare('SELECT 1 FROM gl_subscribers LIMIT 1').first()
51
+ await db.prepare('SELECT 1 FROM gl_email_sends LIMIT 1').first()
52
+ _cached = { ok: true }
53
+ return _cached
54
+ } catch (err) {
55
+ const message = err instanceof Error ? err.message : String(err)
56
+ logSchemaMissing(d1Binding, message)
57
+ _cached = { ok: false }
58
+ return _cached
59
+ }
60
+ }
61
+
62
+ function logSchemaMissing(d1Binding: string, underlying: string): void {
63
+ console.error(
64
+ `[${GL_MAILER_SCHEMA_MISSING}] @growth-labs/mailer: D1 binding "${d1Binding}" is ` +
65
+ 'missing one or both of the gl_subscribers / gl_email_sends tables.\n' +
66
+ 'Remediation:\n' +
67
+ ' 1. Add to wrangler.toml under your [[d1_databases]] block:\n' +
68
+ ' migrations_dir = "node_modules/@growth-labs/mailer/migrations"\n' +
69
+ ` 2. Run: pnpm exec wrangler d1 migrations apply ${d1Binding} --remote\n` +
70
+ 'See packages-docs/mailer-d1-migrations.md for the full guide.\n' +
71
+ 'Mailer routes return 503 until the schema is present. ' +
72
+ `Underlying error: ${underlying}`,
73
+ )
74
+ }
75
+
76
+ /**
77
+ * Helper: build the standard 503 response mailer routes return on schema
78
+ * miss. The body shape is stable so observability dashboards can match on
79
+ * `code === 'GL_MAILER_SCHEMA_MISSING'`.
80
+ */
81
+ export function schemaMissingResponse(): Response {
82
+ return Response.json(
83
+ {
84
+ error: 'Mailer schema is not initialized',
85
+ code: GL_MAILER_SCHEMA_MISSING,
86
+ },
87
+ { status: 503 },
88
+ )
89
+ }
package/src/options.ts CHANGED
@@ -29,6 +29,28 @@ export const mailerOptionsSchema = z.object({
29
29
  unsubscribePath: z.string().default('/api/newsletter/unsubscribe'),
30
30
  preferencesPath: z.string().default('/email/preferences'),
31
31
  webhookPath: z.string().default('/api/email/webhook'),
32
+ webhookSignature: z
33
+ .discriminatedUnion('enabled', [
34
+ z.object({
35
+ enabled: z.literal(false),
36
+ header: z.string().default('x-gl-mailer-signature'),
37
+ timestampHeader: z.string().default('x-gl-mailer-timestamp'),
38
+ toleranceSeconds: z.number().min(0).default(300),
39
+ }),
40
+ z.object({
41
+ enabled: z.literal(true),
42
+ secret: z.string().min(32),
43
+ header: z.string().default('x-gl-mailer-signature'),
44
+ timestampHeader: z.string().default('x-gl-mailer-timestamp'),
45
+ toleranceSeconds: z.number().min(0).default(300),
46
+ }),
47
+ ])
48
+ .default({
49
+ enabled: false,
50
+ header: 'x-gl-mailer-signature',
51
+ timestampHeader: 'x-gl-mailer-timestamp',
52
+ toleranceSeconds: 300,
53
+ }),
32
54
 
33
55
  // ─── Tracking ───
34
56
  trackOpenPath: z.string().default('/api/email/open'),
@@ -1,4 +1,5 @@
1
1
  import { drizzle } from 'drizzle-orm/d1'
2
+ import { probeMailerSchema } from '../_internal/schema-probe.js'
2
3
  import type { ResolvedMailerOptions } from '../options.js'
3
4
  import type { EmailProvider, EmailQueueMessage } from '../types.js'
4
5
  import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
@@ -15,6 +16,18 @@ export async function handleEmailQueue(
15
16
  },
16
17
  options: ResolvedMailerOptions,
17
18
  ): Promise<void> {
19
+ // Schema probe — runs once per Worker instance. On miss the probe logs
20
+ // GL_MAILER_SCHEMA_MISSING and we ack every message in the batch without
21
+ // retry. Re-queueing without the schema would cycle indefinitely and burn
22
+ // Cloudflare Queue retry budget.
23
+ const schemaProbe = await probeMailerSchema(env.DB, options.d1Binding)
24
+ if (!schemaProbe.ok) {
25
+ for (const message of batch.messages) {
26
+ message.ack()
27
+ }
28
+ return
29
+ }
30
+
18
31
  const db = drizzle(env.DB)
19
32
  const provider: EmailProvider = env.EMAIL_SENDER
20
33
  ? new CloudflareEmailProvider(env.EMAIL_SENDER)
@@ -2,6 +2,7 @@ import { env as cloudflareEnv } from 'cloudflare:workers'
2
2
  import { config } from 'virtual:growth-labs/mailer/config'
3
3
  import type { APIRoute } from 'astro'
4
4
  import { drizzle } from 'drizzle-orm/d1'
5
+ import { probeMailerSchema, schemaMissingResponse } from '../_internal/schema-probe.js'
5
6
  import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
6
7
  import type { MailerEnv } from '../utils/send.js'
7
8
  import { sendTransactional } from '../utils/send.js'
@@ -25,6 +26,11 @@ export const GET: APIRoute = async (context) => {
25
26
  const bindingsEnv = cloudflareEnv as Record<string, unknown>
26
27
  const d1 = bindingsEnv[config.d1Binding] as D1Database
27
28
  const queue = bindingsEnv[config.queueBinding] as Queue
29
+
30
+ // Schema probe — return 503 with GL_MAILER_SCHEMA_MISSING on miss.
31
+ const schema = await probeMailerSchema(d1, config.d1Binding)
32
+ if (!schema.ok) return schemaMissingResponse()
33
+
28
34
  const db = drizzle(d1)
29
35
  const env: MailerEnv = { DB: d1, QUEUE: queue }
30
36
 
@@ -2,6 +2,7 @@ import { env as cloudflareEnv } from 'cloudflare:workers'
2
2
  import { config } from 'virtual:growth-labs/mailer/config'
3
3
  import type { APIRoute } from 'astro'
4
4
  import { drizzle } from 'drizzle-orm/d1'
5
+ import { probeMailerSchema, schemaMissingResponse } from '../_internal/schema-probe.js'
5
6
  import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
6
7
  import type { MailerEnv } from '../utils/send.js'
7
8
  import { sendTransactional } from '../utils/send.js'
@@ -53,6 +54,12 @@ export const POST: APIRoute = async (context) => {
53
54
  const bindingsEnv = cloudflareEnv as Record<string, unknown>
54
55
  const d1 = bindingsEnv[config.d1Binding] as D1Database
55
56
  const queue = bindingsEnv[config.queueBinding] as Queue
57
+
58
+ // 5a. Schema probe — runs once per Worker instance. On miss, return 503
59
+ // with GL_MAILER_SCHEMA_MISSING so the consumer's site doesn't 500-cascade.
60
+ const schema = await probeMailerSchema(d1, config.d1Binding)
61
+ if (!schema.ok) return schemaMissingResponse()
62
+
56
63
  const db = drizzle(d1)
57
64
  const env: MailerEnv = { DB: d1, QUEUE: queue }
58
65
 
@@ -3,6 +3,7 @@ import { config } from 'virtual:growth-labs/mailer/config'
3
3
  import type { APIRoute } from 'astro'
4
4
  import { and, eq, inArray } from 'drizzle-orm'
5
5
  import { drizzle } from 'drizzle-orm/d1'
6
+ import { probeMailerSchema } from '../_internal/schema-probe.js'
6
7
  import { emailSends } from '../schema.js'
7
8
  import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
8
9
 
@@ -25,24 +26,30 @@ export const GET: APIRoute = async (context) => {
25
26
  return new Response('Invalid URL', { status: 400 })
26
27
  }
27
28
 
28
- // Update status to 'clicked' only when it hasn't already reached 'clicked'
29
+ // Update status to 'clicked' only when it hasn't already reached 'clicked'.
30
+ // On schema miss the probe logs GL_MAILER_SCHEMA_MISSING and we skip the
31
+ // D1 update — but always still 302 to the destination so we don't break
32
+ // the user's actual click intent.
29
33
  try {
30
34
  const bindingsEnv = cloudflareEnv as Record<string, unknown>
31
35
  if (bindingsEnv) {
32
36
  const d1 = bindingsEnv[config.d1Binding] as D1Database
33
- const db = drizzle(d1)
34
- await db
35
- .update(emailSends)
36
- .set({
37
- status: 'clicked',
38
- clickedAt: new Date().toISOString(),
39
- })
40
- .where(
41
- and(
42
- eq(emailSends.trackingId, trackingId),
43
- inArray(emailSends.status, ['sent', 'delivered', 'opened']),
44
- ),
45
- )
37
+ const schema = await probeMailerSchema(d1, config.d1Binding)
38
+ if (schema.ok) {
39
+ const db = drizzle(d1)
40
+ await db
41
+ .update(emailSends)
42
+ .set({
43
+ status: 'clicked',
44
+ clickedAt: new Date().toISOString(),
45
+ })
46
+ .where(
47
+ and(
48
+ eq(emailSends.trackingId, trackingId),
49
+ inArray(emailSends.status, ['sent', 'delivered', 'opened']),
50
+ ),
51
+ )
52
+ }
46
53
  }
47
54
  } catch {
48
55
  // Never fail the redirect on DB errors
@@ -3,6 +3,7 @@ import { config } from 'virtual:growth-labs/mailer/config'
3
3
  import type { APIRoute } from 'astro'
4
4
  import { and, eq, inArray } from 'drizzle-orm'
5
5
  import { drizzle } from 'drizzle-orm/d1'
6
+ import { probeMailerSchema } from '../_internal/schema-probe.js'
6
7
  import { emailSends } from '../schema.js'
7
8
  import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
8
9
  import { TRANSPARENT_GIF } from '../utils/tracking.js'
@@ -15,24 +16,29 @@ export const GET: APIRoute = async (context) => {
15
16
  }
16
17
 
17
18
  // Update status to 'opened' only when currently 'sent' or 'delivered'
18
- // to avoid downgrading from 'clicked'.
19
+ // to avoid downgrading from 'clicked'. On schema miss, the probe logs
20
+ // GL_MAILER_SCHEMA_MISSING and we skip the D1 work — but always still
21
+ // return the transparent GIF so we don't break email rendering.
19
22
  try {
20
23
  const bindingsEnv = cloudflareEnv as Record<string, unknown>
21
24
  if (bindingsEnv) {
22
25
  const d1 = bindingsEnv[config.d1Binding] as D1Database
23
- const db = drizzle(d1)
24
- await db
25
- .update(emailSends)
26
- .set({
27
- status: 'opened',
28
- openedAt: new Date().toISOString(),
29
- })
30
- .where(
31
- and(
32
- eq(emailSends.trackingId, trackingId),
33
- inArray(emailSends.status, ['sent', 'delivered']),
34
- ),
35
- )
26
+ const schema = await probeMailerSchema(d1, config.d1Binding)
27
+ if (schema.ok) {
28
+ const db = drizzle(d1)
29
+ await db
30
+ .update(emailSends)
31
+ .set({
32
+ status: 'opened',
33
+ openedAt: new Date().toISOString(),
34
+ })
35
+ .where(
36
+ and(
37
+ eq(emailSends.trackingId, trackingId),
38
+ inArray(emailSends.status, ['sent', 'delivered']),
39
+ ),
40
+ )
41
+ }
36
42
  }
37
43
  } catch {
38
44
  // Never fail the pixel response on DB errors
@@ -2,6 +2,7 @@ import { env as cloudflareEnv } from 'cloudflare:workers'
2
2
  import { config } from 'virtual:growth-labs/mailer/config'
3
3
  import type { APIRoute } from 'astro'
4
4
  import { drizzle } from 'drizzle-orm/d1'
5
+ import { probeMailerSchema, schemaMissingResponse } from '../_internal/schema-probe.js'
5
6
  import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
6
7
  import type { MailerEnv } from '../utils/send.js'
7
8
  import { sendTransactional } from '../utils/send.js'
@@ -24,6 +25,12 @@ async function processUnsubscribe(
24
25
  const bindingsEnv = cloudflareEnv as Record<string, unknown>
25
26
  const d1 = bindingsEnv[config.d1Binding] as D1Database
26
27
  const queue = bindingsEnv[config.queueBinding] as Queue
28
+
29
+ // Schema probe — short-circuit with a Response on miss; both GET and POST
30
+ // handlers below forward it unchanged.
31
+ const schema = await probeMailerSchema(d1, config.d1Binding)
32
+ if (!schema.ok) return { schemaMissing: true as const }
33
+
27
34
  const db = drizzle(d1)
28
35
  const env: MailerEnv = { DB: d1, QUEUE: queue }
29
36
 
@@ -73,6 +80,7 @@ export const GET: APIRoute = async (context) => {
73
80
  }
74
81
 
75
82
  const result = await processUnsubscribe(token, context, request)
83
+ if ('schemaMissing' in result) return schemaMissingResponse()
76
84
  if ('error' in result) {
77
85
  return Response.json({ error: result.error }, { status: result.status })
78
86
  }
@@ -105,6 +113,7 @@ export const POST: APIRoute = async (context) => {
105
113
  }
106
114
 
107
115
  const result = await processUnsubscribe(token, context, request)
116
+ if ('schemaMissing' in result) return schemaMissingResponse()
108
117
  if ('error' in result) {
109
118
  return new Response(result.error, { status: result.status })
110
119
  }
@@ -4,6 +4,7 @@ import type { APIRoute } from 'astro'
4
4
  import { drizzle } from 'drizzle-orm/d1'
5
5
  import { emitMailerAnalyticsEvent, type MailerAnalyticsEvent } from '../utils/analytics.js'
6
6
  import { handleBounce, handleComplaint, handleDelivery } from '../utils/bounce.js'
7
+ import { verifyWebhookSignature } from '../utils/webhook-signature.js'
7
8
 
8
9
  interface WebhookPayload {
9
10
  type: 'bounce' | 'complaint' | 'delivery'
@@ -15,7 +16,18 @@ interface WebhookPayload {
15
16
 
16
17
  export const POST: APIRoute = async (context) => {
17
18
  const { request } = context
18
- const body = (await request.json()) as WebhookPayload
19
+ const rawBody = await request.text()
20
+ const signatureValid = await verifyWebhookSignature(config.webhookSignature, request, rawBody)
21
+ if (!signatureValid) {
22
+ return Response.json({ error: 'Invalid webhook signature' }, { status: 401 })
23
+ }
24
+
25
+ let body: WebhookPayload
26
+ try {
27
+ body = JSON.parse(rawBody) as WebhookPayload
28
+ } catch {
29
+ return Response.json({ error: 'Invalid payload' }, { status: 400 })
30
+ }
19
31
 
20
32
  if (!body.type || !body.email) {
21
33
  return Response.json({ error: 'Invalid payload' }, { status: 400 })
@@ -28,6 +40,18 @@ export const POST: APIRoute = async (context) => {
28
40
  }
29
41
  const db = drizzle(d1)
30
42
 
43
+ emitMailerAnalyticsEvent(config, bindingsEnv, 'newsletter_webhook_received', {
44
+ request,
45
+ context,
46
+ contentSlug: body.trackingId ?? body.email,
47
+ label: {
48
+ type: body.type,
49
+ email: body.email,
50
+ trackingId: body.trackingId,
51
+ bounceType: body.bounceType,
52
+ },
53
+ })
54
+
31
55
  switch (body.type) {
32
56
  case 'delivery':
33
57
  if (body.trackingId) {
@@ -6,6 +6,7 @@ export type MailerAnalyticsEvent =
6
6
  | 'newsletter_unsubscribed'
7
7
  | 'newsletter_opened'
8
8
  | 'newsletter_clicked'
9
+ | 'newsletter_webhook_received'
9
10
  | 'newsletter_delivered'
10
11
  | 'newsletter_bounced'
11
12
  | 'newsletter_complained'
@@ -107,6 +108,7 @@ function categoryForMailerEvent(eventName: MailerAnalyticsEvent): string {
107
108
  case 'newsletter_opened':
108
109
  case 'newsletter_clicked':
109
110
  return 'interaction'
111
+ case 'newsletter_webhook_received':
110
112
  case 'newsletter_delivered':
111
113
  case 'newsletter_bounced':
112
114
  case 'newsletter_complained':
@@ -44,3 +44,4 @@ export {
44
44
  rewriteLinksForTracking,
45
45
  TRANSPARENT_GIF,
46
46
  } from './tracking.js'
47
+ export { signWebhookPayload, verifyWebhookSignature } from './webhook-signature.js'