@growth-labs/mailer 0.2.0 → 0.2.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.
Files changed (62) hide show
  1. package/README.md +32 -4
  2. package/dist/options.d.ts +61 -0
  3. package/dist/options.d.ts.map +1 -1
  4. package/dist/options.js +23 -0
  5. package/dist/options.js.map +1 -1
  6. package/dist/queue/consumer.d.ts +1 -0
  7. package/dist/queue/consumer.d.ts.map +1 -1
  8. package/dist/queue/consumer.js +22 -0
  9. package/dist/queue/consumer.js.map +1 -1
  10. package/dist/routes/confirm.d.ts.map +1 -1
  11. package/dist/routes/confirm.js +15 -4
  12. package/dist/routes/confirm.js.map +1 -1
  13. package/dist/routes/subscribe.d.ts.map +1 -1
  14. package/dist/routes/subscribe.js +20 -4
  15. package/dist/routes/subscribe.js.map +1 -1
  16. package/dist/routes/track-click.d.ts.map +1 -1
  17. package/dist/routes/track-click.js +13 -4
  18. package/dist/routes/track-click.js.map +1 -1
  19. package/dist/routes/track-open.d.ts.map +1 -1
  20. package/dist/routes/track-open.js +13 -4
  21. package/dist/routes/track-open.js.map +1 -1
  22. package/dist/routes/unsubscribe.d.ts.map +1 -1
  23. package/dist/routes/unsubscribe.js +21 -8
  24. package/dist/routes/unsubscribe.js.map +1 -1
  25. package/dist/routes/webhook.d.ts.map +1 -1
  26. package/dist/routes/webhook.js +52 -6
  27. package/dist/routes/webhook.js.map +1 -1
  28. package/dist/utils/analytics.d.ts +24 -0
  29. package/dist/utils/analytics.d.ts.map +1 -0
  30. package/dist/utils/analytics.js +75 -0
  31. package/dist/utils/analytics.js.map +1 -0
  32. package/dist/utils/bindings.d.ts +4 -10
  33. package/dist/utils/bindings.d.ts.map +1 -1
  34. package/dist/utils/bindings.js +7 -7
  35. package/dist/utils/bindings.js.map +1 -1
  36. package/dist/utils/index.d.ts +3 -1
  37. package/dist/utils/index.d.ts.map +1 -1
  38. package/dist/utils/index.js +3 -1
  39. package/dist/utils/index.js.map +1 -1
  40. package/dist/utils/providers.js +1 -1
  41. package/dist/utils/providers.js.map +1 -1
  42. package/dist/utils/webhook-signature.d.ts +6 -0
  43. package/dist/utils/webhook-signature.d.ts.map +1 -0
  44. package/dist/utils/webhook-signature.js +59 -0
  45. package/dist/utils/webhook-signature.js.map +1 -0
  46. package/package.json +7 -1
  47. package/src/cloudflare-workers.d.ts +3 -0
  48. package/src/options.ts +23 -0
  49. package/src/queue/consumer.ts +27 -1
  50. package/src/routes/confirm.ts +16 -5
  51. package/src/routes/preferences.astro +5 -9
  52. package/src/routes/subscribe.ts +21 -5
  53. package/src/routes/track-click.ts +14 -8
  54. package/src/routes/track-open.ts +14 -8
  55. package/src/routes/unsubscribe.ts +26 -9
  56. package/src/routes/webhook.ts +55 -11
  57. package/src/utils/analytics.ts +120 -0
  58. package/src/utils/bindings.ts +9 -11
  59. package/src/utils/index.ts +7 -5
  60. package/src/utils/providers.ts +1 -1
  61. package/src/utils/webhook-signature.ts +91 -0
  62. package/src/virtual.d.ts +15 -0
package/README.md CHANGED
@@ -6,6 +6,14 @@ Queue-based email sending engine for Astro + Cloudflare. Newsletter subscription
6
6
 
7
7
  If you only need immediate transactional delivery from a Worker with no subscriber state or queueing, use `@growth-labs/email` instead.
8
8
 
9
+ Astro 6 note: injected routes read D1 and Queue bindings from `cloudflare:workers` `env`; no `locals.runtime` shim is required.
10
+
11
+ ## When to use this vs `@growth-labs/email`
12
+
13
+ Use `@growth-labs/mailer` (this package) for newsletter and campaign
14
+ workflows. Use `@growth-labs/email` for transactional one-off sends.
15
+ See the `@growth-labs/email` README for the full distinction.
16
+
9
17
  ## Config
10
18
 
11
19
  ```typescript
@@ -30,6 +38,11 @@ mailer({
30
38
  footerText: '© FEDweek. All rights reserved.',
31
39
  },
32
40
  analyticsEnabled: true, // Emit email events to @growth-labs/analytics
41
+ analyticsBinding: 'ANALYTICS', // WAE binding used when analytics is enabled
42
+ webhookSignature: {
43
+ enabled: true,
44
+ secret: import.meta.env.MAILER_WEBHOOK_SECRET,
45
+ },
33
46
  })
34
47
  ```
35
48
 
@@ -40,7 +53,7 @@ mailer({
40
53
  - `GET /api/newsletter/confirm?token=...` — Double opt-in confirmation
41
54
  - `GET /api/newsletter/unsubscribe?token=...` — One-click unsubscribe
42
55
  - `GET/POST /email/preferences?token=...` — Preference center page
43
- - `POST /api/newsletter/webhook` — ESP webhook receiver (delivery, bounce, complaint)
56
+ - `POST /api/email/webhook` — ESP webhook receiver (delivery, bounce, complaint), optional HMAC signature verification
44
57
  - `GET /api/email/open/:trackingId` — Open tracking pixel
45
58
  - `GET /api/email/click/:trackingId` — Click tracking redirect
46
59
 
@@ -67,21 +80,36 @@ queue = "email-sends"
67
80
  [[queues.consumers]]
68
81
  queue = "email-sends"
69
82
  max_batch_size = 10
83
+
84
+ [[analytics_engine_datasets]]
85
+ binding = "ANALYTICS"
86
+ dataset = "your_analytics_dataset"
70
87
  ```
71
88
 
72
89
  ## Email Sending Flow
73
90
 
74
91
  1. Consumer calls `sendTransactional()`, `sendCampaign()`, or `sendDigest()` → message enqueued
75
- 2. Queue consumer dequeues → sends via the Cloudflare Email Sending binding
92
+ 2. Queue consumer dequeues → sends via Cloudflare Email Sending with retry on retryable failures
76
93
  3. Status updated in `gl_email_sends`: queued → sent → delivered/bounced
77
94
  4. Open/click tracking updates status further
78
95
 
79
96
  ## Key Patterns
80
97
 
81
98
  - Virtual module: `virtual:growth-labs/mailer/config`
99
+ - Runtime bindings: `import { env } from 'cloudflare:workers'`
82
100
  - Status never downgrades (sent → delivered → opened → clicked)
83
101
  - List-Unsubscribe header on every email (RFC 8058)
84
102
  - Turnstile on subscribe endpoint (+ IP rate limiting as backup)
85
- - Cloudflare Email Sending is the only outbound provider
86
- - WAE events via `@growth-labs/analytics` writeDataPoint() — fire-and-forget, non-blocking
103
+ - Provider failover logged for observability
104
+ - WAE events via the configured Analytics Engine binding — fire-and-forget, non-blocking
87
105
  - `.astro` component files ship as source, not compiled
106
+
107
+ ## Analytics Events
108
+
109
+ When `analyticsEnabled` is true and the configured `analyticsBinding`
110
+ exists, the package writes WAE data points for
111
+ `newsletter_subscribed`, `newsletter_confirmed`,
112
+ `newsletter_unsubscribed`, `newsletter_opened`,
113
+ `newsletter_clicked`, `newsletter_delivered`,
114
+ `newsletter_bounced`, `newsletter_complained`,
115
+ `newsletter_sent`, and `newsletter_send_failed`.
package/dist/options.d.ts CHANGED
@@ -15,6 +15,40 @@ export declare const mailerOptionsSchema: z.ZodObject<{
15
15
  unsubscribePath: z.ZodDefault<z.ZodString>;
16
16
  preferencesPath: z.ZodDefault<z.ZodString>;
17
17
  webhookPath: z.ZodDefault<z.ZodString>;
18
+ webhookSignature: z.ZodDefault<z.ZodDiscriminatedUnion<"enabled", [z.ZodObject<{
19
+ enabled: z.ZodLiteral<false>;
20
+ header: z.ZodDefault<z.ZodString>;
21
+ timestampHeader: z.ZodDefault<z.ZodString>;
22
+ toleranceSeconds: z.ZodDefault<z.ZodNumber>;
23
+ }, "strip", z.ZodTypeAny, {
24
+ enabled: false;
25
+ header: string;
26
+ timestampHeader: string;
27
+ toleranceSeconds: number;
28
+ }, {
29
+ enabled: false;
30
+ header?: string | undefined;
31
+ timestampHeader?: string | undefined;
32
+ toleranceSeconds?: number | undefined;
33
+ }>, z.ZodObject<{
34
+ enabled: z.ZodLiteral<true>;
35
+ secret: z.ZodString;
36
+ header: z.ZodDefault<z.ZodString>;
37
+ timestampHeader: z.ZodDefault<z.ZodString>;
38
+ toleranceSeconds: z.ZodDefault<z.ZodNumber>;
39
+ }, "strip", z.ZodTypeAny, {
40
+ enabled: true;
41
+ header: string;
42
+ timestampHeader: string;
43
+ toleranceSeconds: number;
44
+ secret: string;
45
+ }, {
46
+ enabled: true;
47
+ secret: string;
48
+ header?: string | undefined;
49
+ timestampHeader?: string | undefined;
50
+ toleranceSeconds?: number | undefined;
51
+ }>]>>;
18
52
  trackOpenPath: z.ZodDefault<z.ZodString>;
19
53
  trackClickPath: z.ZodDefault<z.ZodString>;
20
54
  siteUrl: z.ZodString;
@@ -36,6 +70,7 @@ export declare const mailerOptionsSchema: z.ZodObject<{
36
70
  }>>;
37
71
  batchSize: z.ZodDefault<z.ZodNumber>;
38
72
  analyticsEnabled: z.ZodDefault<z.ZodBoolean>;
73
+ analyticsBinding: z.ZodDefault<z.ZodString>;
39
74
  }, "strip", z.ZodTypeAny, {
40
75
  senderName: string;
41
76
  fromAddress: string;
@@ -50,6 +85,18 @@ export declare const mailerOptionsSchema: z.ZodObject<{
50
85
  unsubscribePath: string;
51
86
  preferencesPath: string;
52
87
  webhookPath: string;
88
+ webhookSignature: {
89
+ enabled: false;
90
+ header: string;
91
+ timestampHeader: string;
92
+ toleranceSeconds: number;
93
+ } | {
94
+ enabled: true;
95
+ header: string;
96
+ timestampHeader: string;
97
+ toleranceSeconds: number;
98
+ secret: string;
99
+ };
53
100
  trackOpenPath: string;
54
101
  trackClickPath: string;
55
102
  siteUrl: string;
@@ -61,6 +108,7 @@ export declare const mailerOptionsSchema: z.ZodObject<{
61
108
  };
62
109
  batchSize: number;
63
110
  analyticsEnabled: boolean;
111
+ analyticsBinding: string;
64
112
  replyTo?: string | undefined;
65
113
  topics?: string[] | undefined;
66
114
  }, {
@@ -80,6 +128,18 @@ export declare const mailerOptionsSchema: z.ZodObject<{
80
128
  unsubscribePath?: string | undefined;
81
129
  preferencesPath?: string | undefined;
82
130
  webhookPath?: string | undefined;
131
+ webhookSignature?: {
132
+ enabled: false;
133
+ header?: string | undefined;
134
+ timestampHeader?: string | undefined;
135
+ toleranceSeconds?: number | undefined;
136
+ } | {
137
+ enabled: true;
138
+ secret: string;
139
+ header?: string | undefined;
140
+ timestampHeader?: string | undefined;
141
+ toleranceSeconds?: number | undefined;
142
+ } | undefined;
83
143
  trackOpenPath?: string | undefined;
84
144
  trackClickPath?: string | undefined;
85
145
  brand?: {
@@ -90,6 +150,7 @@ export declare const mailerOptionsSchema: z.ZodObject<{
90
150
  } | undefined;
91
151
  batchSize?: number | undefined;
92
152
  analyticsEnabled?: boolean | undefined;
153
+ analyticsBinding?: string | undefined;
93
154
  }>;
94
155
  export type MailerOptions = z.input<typeof mailerOptionsSchema>;
95
156
  export type ResolvedMailerOptions = z.output<typeof mailerOptionsSchema>;
@@ -1 +1 @@
1
- {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkD9B,CAAA;AAEF,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAC/D,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAA"}
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyE9B,CAAA;AAEF,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAC/D,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAA"}
package/dist/options.js CHANGED
@@ -22,6 +22,28 @@ export const mailerOptionsSchema = z.object({
22
22
  unsubscribePath: z.string().default('/api/newsletter/unsubscribe'),
23
23
  preferencesPath: z.string().default('/email/preferences'),
24
24
  webhookPath: z.string().default('/api/email/webhook'),
25
+ webhookSignature: z
26
+ .discriminatedUnion('enabled', [
27
+ z.object({
28
+ enabled: z.literal(false),
29
+ header: z.string().default('x-gl-mailer-signature'),
30
+ timestampHeader: z.string().default('x-gl-mailer-timestamp'),
31
+ toleranceSeconds: z.number().min(0).default(300),
32
+ }),
33
+ z.object({
34
+ enabled: z.literal(true),
35
+ secret: z.string().min(32),
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
+ ])
41
+ .default({
42
+ enabled: false,
43
+ header: 'x-gl-mailer-signature',
44
+ timestampHeader: 'x-gl-mailer-timestamp',
45
+ toleranceSeconds: 300,
46
+ }),
25
47
  // ─── Tracking ───
26
48
  trackOpenPath: z.string().default('/api/email/open'),
27
49
  trackClickPath: z.string().default('/api/email/click'),
@@ -39,5 +61,6 @@ export const mailerOptionsSchema = z.object({
39
61
  batchSize: z.number().min(1).max(500).default(100),
40
62
  // ─── Optional peer: analytics ───
41
63
  analyticsEnabled: z.boolean().default(false),
64
+ analyticsBinding: z.string().default('ANALYTICS'),
42
65
  });
43
66
  //# sourceMappingURL=options.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,mBAAmB;IACnB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;IACtB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;IAE/B,mBAAmB;IACnB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE;IAEtC,8BAA8B;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC;IACxC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,aAAa,CAAC;IAE/C,qCAAqC;IACrC,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE;IAC5B,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE;IAE9B,+BAA+B;IAC/B,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IACtC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IAEtC,wBAAwB;IACxB,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE;IAEzB,iBAAiB;IACjB,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,2BAA2B,CAAC;IAC9D,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,yBAAyB,CAAC;IAC1D,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,6BAA6B,CAAC;IAClE,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,oBAAoB,CAAC;IACzD,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,oBAAoB,CAAC;IAErD,mBAAmB;IACnB,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,iBAAiB,CAAC;IACpD,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,kBAAkB,CAAC;IACtD,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IAEzB,4CAA4C;IAC5C,KAAK,EAAE,CAAC;SACN,MAAM,CAAC;QACP,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;QACpC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC;QAC3C,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC;QAC1C,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KACjC,CAAC;SACD,OAAO,CAAC,EAAE,CAAC;IAEb,yBAAyB;IACzB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC;IAElD,mCAAmC;IACnC,gBAAgB,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;CAC5C,CAAC,CAAA"}
1
+ {"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"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"}
@@ -4,5 +4,6 @@ import type { CloudflareEmailSender } from '../utils/providers.js';
4
4
  export declare function handleEmailQueue(batch: MessageBatch<EmailQueueMessage>, env: {
5
5
  DB: D1Database;
6
6
  EMAIL_SENDER?: CloudflareEmailSender;
7
+ [key: string]: unknown;
7
8
  }, options: ResolvedMailerOptions): Promise<void>;
8
9
  //# sourceMappingURL=consumer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"consumer.d.ts","sourceRoot":"","sources":["../../src/queue/consumer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAC1D,OAAO,KAAK,EAAiB,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAEnE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAGlE,wBAAsB,gBAAgB,CACrC,KAAK,EAAE,YAAY,CAAC,iBAAiB,CAAC,EACtC,GAAG,EAAE;IAAE,EAAE,EAAE,UAAU,CAAC;IAAC,YAAY,CAAC,EAAE,qBAAqB,CAAA;CAAE,EAC7D,OAAO,EAAE,qBAAqB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAsEf"}
1
+ {"version":3,"file":"consumer.d.ts","sourceRoot":"","sources":["../../src/queue/consumer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAC1D,OAAO,KAAK,EAAiB,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAGnE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAGlE,wBAAsB,gBAAgB,CACrC,KAAK,EAAE,YAAY,CAAC,iBAAiB,CAAC,EACtC,GAAG,EAAE;IACJ,EAAE,EAAE,UAAU,CAAA;IACd,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACtB,EACD,OAAO,EAAE,qBAAqB,GAC5B,OAAO,CAAC,IAAI,CAAC,CA2Ff"}
@@ -1,4 +1,5 @@
1
1
  import { drizzle } from 'drizzle-orm/d1';
2
+ import { emitMailerAnalyticsEvent } from '../utils/analytics.js';
2
3
  import { updateSendStatus } from '../utils/bounce.js';
3
4
  import { CloudflareEmailProvider, sleep } from '../utils/providers.js';
4
5
  export async function handleEmailQueue(batch, env, options) {
@@ -54,12 +55,33 @@ export async function handleEmailQueue(batch, env, options) {
54
55
  await updateSendStatus(db, recipient.trackingId, 'sent', {
55
56
  sentAt: new Date().toISOString(),
56
57
  });
58
+ emitMailerAnalyticsEvent(options, env, 'newsletter_sent', {
59
+ contentSlug: recipient.trackingId,
60
+ label: {
61
+ trackingId: recipient.trackingId,
62
+ subscriberId: recipient.subscriberId,
63
+ email: recipient.email,
64
+ campaignId: message.body.campaignId,
65
+ type,
66
+ },
67
+ });
57
68
  }
58
69
  else {
59
70
  await updateSendStatus(db, recipient.trackingId, 'bounced', {
60
71
  bouncedAt: new Date().toISOString(),
61
72
  bounceType: 'hard',
62
73
  });
74
+ emitMailerAnalyticsEvent(options, env, 'newsletter_send_failed', {
75
+ contentSlug: recipient.trackingId,
76
+ label: {
77
+ trackingId: recipient.trackingId,
78
+ subscriberId: recipient.subscriberId,
79
+ email: recipient.email,
80
+ campaignId: message.body.campaignId,
81
+ type,
82
+ error: result.error,
83
+ },
84
+ });
63
85
  console.error(`[mailer] Send failed for ${recipient.email}: ${result.error}`);
64
86
  }
65
87
  }
@@ -1 +1 @@
1
- {"version":3,"file":"consumer.js","sourceRoot":"","sources":["../../src/queue/consumer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"consumer.js","sourceRoot":"","sources":["../../src/queue/consumer.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"confirm.d.ts","sourceRoot":"","sources":["../../src/routes/confirm.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAQrC,eAAO,MAAM,GAAG,EAAE,QA0DjB,CAAA"}
1
+ {"version":3,"file":"confirm.d.ts","sourceRoot":"","sources":["../../src/routes/confirm.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAQrC,eAAO,MAAM,GAAG,EAAE,QAoEjB,CAAA"}
@@ -1,9 +1,12 @@
1
+ import { env as cloudflareEnv } from 'cloudflare:workers';
1
2
  import { config } from 'virtual:growth-labs/mailer/config';
2
3
  import { drizzle } from 'drizzle-orm/d1';
4
+ import { emitMailerAnalyticsEvent } from '../utils/analytics.js';
3
5
  import { sendTransactional } from '../utils/send.js';
4
6
  import { confirmSubscriber, getSubscriberById } from '../utils/subscribers.js';
5
7
  import { verifyToken } from '../utils/tokens.js';
6
- export const GET = async ({ url, locals }) => {
8
+ export const GET = async (context) => {
9
+ const { url } = context;
7
10
  const token = url.searchParams.get('token');
8
11
  if (!token) {
9
12
  return Response.json({ error: 'Missing token' }, { status: 400 });
@@ -14,9 +17,9 @@ export const GET = async ({ url, locals }) => {
14
17
  return Response.json({ error: 'Invalid or expired token' }, { status: 400 });
15
18
  }
16
19
  // Resolve bindings
17
- const runtimeEnv = locals.runtime?.env;
18
- const d1 = runtimeEnv[config.d1Binding];
19
- const queue = runtimeEnv[config.queueBinding];
20
+ const bindingsEnv = cloudflareEnv;
21
+ const d1 = bindingsEnv[config.d1Binding];
22
+ const queue = bindingsEnv[config.queueBinding];
20
23
  const db = drizzle(d1);
21
24
  const env = { DB: d1, QUEUE: queue };
22
25
  // Get subscriber to check status
@@ -48,6 +51,14 @@ export const GET = async ({ url, locals }) => {
48
51
  template: 'welcome',
49
52
  data: { name: subscriber.name },
50
53
  });
54
+ emitMailerAnalyticsEvent(config, bindingsEnv, 'newsletter_confirmed', {
55
+ context,
56
+ contentSlug: subscriber.id,
57
+ label: {
58
+ subscriberId: subscriber.id,
59
+ email: subscriber.email,
60
+ },
61
+ });
51
62
  // Redirect to site with confirmed flag
52
63
  return new Response(null, {
53
64
  status: 302,
@@ -1 +1 @@
1
- {"version":3,"file":"confirm.js","sourceRoot":"","sources":["../../src/routes/confirm.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"confirm.js","sourceRoot":"","sources":["../../src/routes/confirm.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"subscribe.d.ts","sourceRoot":"","sources":["../../src/routes/subscribe.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAQrC,eAAO,MAAM,IAAI,EAAE,QAiGlB,CAAA"}
1
+ {"version":3,"file":"subscribe.d.ts","sourceRoot":"","sources":["../../src/routes/subscribe.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAQrC,eAAO,MAAM,IAAI,EAAE,QAgHlB,CAAA"}
@@ -1,9 +1,12 @@
1
+ import { env as cloudflareEnv } from 'cloudflare:workers';
1
2
  import { config } from 'virtual:growth-labs/mailer/config';
2
3
  import { drizzle } from 'drizzle-orm/d1';
4
+ import { emitMailerAnalyticsEvent } from '../utils/analytics.js';
3
5
  import { sendTransactional } from '../utils/send.js';
4
6
  import { createSubscriber } from '../utils/subscribers.js';
5
7
  import { generateToken } from '../utils/tokens.js';
6
- export const POST = async ({ request, locals }) => {
8
+ export const POST = async (context) => {
9
+ const { request } = context;
7
10
  // 1. Parse request body
8
11
  const body = (await request.json());
9
12
  // 2. Validate required fields
@@ -29,9 +32,9 @@ export const POST = async ({ request, locals }) => {
29
32
  return Response.json({ error: 'Turnstile verification failed' }, { status: 400 });
30
33
  }
31
34
  // 5. Resolve bindings
32
- const runtimeEnv = locals.runtime?.env;
33
- const d1 = runtimeEnv[config.d1Binding];
34
- const queue = runtimeEnv[config.queueBinding];
35
+ const bindingsEnv = cloudflareEnv;
36
+ const d1 = bindingsEnv[config.d1Binding];
37
+ const queue = bindingsEnv[config.queueBinding];
35
38
  const db = drizzle(d1);
36
39
  const env = { DB: d1, QUEUE: queue };
37
40
  // 6. Create subscriber
@@ -70,6 +73,19 @@ export const POST = async ({ request, locals }) => {
70
73
  data: { name: body.name },
71
74
  });
72
75
  }
76
+ emitMailerAnalyticsEvent(config, bindingsEnv, 'newsletter_subscribed', {
77
+ request,
78
+ context,
79
+ contentSlug: subscriber.id,
80
+ label: {
81
+ subscriberId: subscriber.id,
82
+ email: subscriber.email,
83
+ source: body.source ?? 'form',
84
+ preferences: body.preferences ?? [],
85
+ requiresConfirmation: config.doubleOptIn,
86
+ isNew,
87
+ },
88
+ });
73
89
  // Return success (always 200 to avoid email enumeration)
74
90
  return Response.json({
75
91
  success: true,
@@ -1 +1 @@
1
- {"version":3,"file":"subscribe.js","sourceRoot":"","sources":["../../src/routes/subscribe.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"subscribe.js","sourceRoot":"","sources":["../../src/routes/subscribe.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"track-click.d.ts","sourceRoot":"","sources":["../../src/routes/track-click.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAKrC,eAAO,MAAM,GAAG,EAAE,QAkDjB,CAAA"}
1
+ {"version":3,"file":"track-click.d.ts","sourceRoot":"","sources":["../../src/routes/track-click.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAMrC,eAAO,MAAM,GAAG,EAAE,QAsDjB,CAAA"}
@@ -1,8 +1,11 @@
1
+ import { env as cloudflareEnv } from 'cloudflare:workers';
1
2
  import { config } from 'virtual:growth-labs/mailer/config';
2
3
  import { and, eq, inArray } from 'drizzle-orm';
3
4
  import { drizzle } from 'drizzle-orm/d1';
4
5
  import { emailSends } from '../schema.js';
5
- export const GET = async ({ params, url, locals }) => {
6
+ import { emitMailerAnalyticsEvent } from '../utils/analytics.js';
7
+ export const GET = async (context) => {
8
+ const { params, request, url } = context;
6
9
  const trackingId = params.trackingId;
7
10
  const destination = url.searchParams.get('url');
8
11
  if (!trackingId || !destination) {
@@ -20,9 +23,9 @@ export const GET = async ({ params, url, locals }) => {
20
23
  }
21
24
  // Update status to 'clicked' only when it hasn't already reached 'clicked'
22
25
  try {
23
- const runtimeEnv = locals.runtime?.env;
24
- if (runtimeEnv) {
25
- const d1 = runtimeEnv[config.d1Binding];
26
+ const bindingsEnv = cloudflareEnv;
27
+ if (bindingsEnv) {
28
+ const d1 = bindingsEnv[config.d1Binding];
26
29
  const db = drizzle(d1);
27
30
  await db
28
31
  .update(emailSends)
@@ -36,6 +39,12 @@ export const GET = async ({ params, url, locals }) => {
36
39
  catch {
37
40
  // Never fail the redirect on DB errors
38
41
  }
42
+ emitMailerAnalyticsEvent(config, cloudflareEnv, 'newsletter_clicked', {
43
+ request,
44
+ context,
45
+ contentSlug: trackingId,
46
+ label: { trackingId, destination },
47
+ });
39
48
  // 302 redirect to original destination
40
49
  return new Response(null, {
41
50
  status: 302,
@@ -1 +1 @@
1
- {"version":3,"file":"track-click.js","sourceRoot":"","sources":["../../src/routes/track-click.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAE1D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAEzC,MAAM,CAAC,MAAM,GAAG,GAAa,KAAK,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE;IAC9D,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAA;IACpC,MAAM,WAAW,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IAE/C,IAAI,CAAC,UAAU,IAAI,CAAC,WAAW,EAAE,CAAC;QACjC,OAAO,IAAI,QAAQ,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACpD,CAAC;IAED,qEAAqE;IACrE,IAAI,CAAC;QACJ,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAA;QACpC,IAAI,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrD,OAAO,IAAI,QAAQ,CAAC,kBAAkB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QACzD,CAAC;IACF,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,QAAQ,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACpD,CAAC;IAED,2EAA2E;IAC3E,IAAI,CAAC;QACJ,MAAM,UAAU,GACf,MAGA,CAAC,OAAO,EAAE,GAAG,CAAA;QACd,IAAI,UAAU,EAAE,CAAC;YAChB,MAAM,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC,SAAS,CAAe,CAAA;YACrD,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAAA;YACtB,MAAM,EAAE;iBACN,MAAM,CAAC,UAAU,CAAC;iBAClB,GAAG,CAAC;gBACJ,MAAM,EAAE,SAAS;gBACjB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACnC,CAAC;iBACD,KAAK,CACL,GAAG,CACF,EAAE,CAAC,UAAU,CAAC,UAAU,EAAE,UAAU,CAAC,EACrC,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC,CAC3D,CACD,CAAA;QACH,CAAC;IACF,CAAC;IAAC,MAAM,CAAC;QACR,uCAAuC;IACxC,CAAC;IAED,uCAAuC;IACvC,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;QACzB,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE;KAClC,CAAC,CAAA;AACH,CAAC,CAAA"}
1
+ {"version":3,"file":"track-click.js","sourceRoot":"","sources":["../../src/routes/track-click.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAE1D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AAEhE,MAAM,CAAC,MAAM,GAAG,GAAa,KAAK,EAAE,OAAO,EAAE,EAAE;IAC9C,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,OAAO,CAAA;IACxC,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAA;IACpC,MAAM,WAAW,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IAE/C,IAAI,CAAC,UAAU,IAAI,CAAC,WAAW,EAAE,CAAC;QACjC,OAAO,IAAI,QAAQ,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACpD,CAAC;IAED,qEAAqE;IACrE,IAAI,CAAC;QACJ,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAA;QACpC,IAAI,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrD,OAAO,IAAI,QAAQ,CAAC,kBAAkB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QACzD,CAAC;IACF,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,QAAQ,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACpD,CAAC;IAED,2EAA2E;IAC3E,IAAI,CAAC;QACJ,MAAM,WAAW,GAAG,aAAwC,CAAA;QAC5D,IAAI,WAAW,EAAE,CAAC;YACjB,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,SAAS,CAAe,CAAA;YACtD,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAAA;YACtB,MAAM,EAAE;iBACN,MAAM,CAAC,UAAU,CAAC;iBAClB,GAAG,CAAC;gBACJ,MAAM,EAAE,SAAS;gBACjB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACnC,CAAC;iBACD,KAAK,CACL,GAAG,CACF,EAAE,CAAC,UAAU,CAAC,UAAU,EAAE,UAAU,CAAC,EACrC,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC,CAC3D,CACD,CAAA;QACH,CAAC;IACF,CAAC;IAAC,MAAM,CAAC;QACR,uCAAuC;IACxC,CAAC;IAED,wBAAwB,CAAC,MAAM,EAAE,aAAwC,EAAE,oBAAoB,EAAE;QAChG,OAAO;QACP,OAAO;QACP,WAAW,EAAE,UAAU;QACvB,KAAK,EAAE,EAAE,UAAU,EAAE,WAAW,EAAE;KAClC,CAAC,CAAA;IAEF,uCAAuC;IACvC,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;QACzB,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE;KAClC,CAAC,CAAA;AACH,CAAC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"track-open.d.ts","sourceRoot":"","sources":["../../src/routes/track-open.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAMrC,eAAO,MAAM,GAAG,EAAE,QA2CjB,CAAA"}
1
+ {"version":3,"file":"track-open.d.ts","sourceRoot":"","sources":["../../src/routes/track-open.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAOrC,eAAO,MAAM,GAAG,EAAE,QA+CjB,CAAA"}
@@ -1,9 +1,12 @@
1
+ import { env as cloudflareEnv } from 'cloudflare:workers';
1
2
  import { config } from 'virtual:growth-labs/mailer/config';
2
3
  import { and, eq, inArray } from 'drizzle-orm';
3
4
  import { drizzle } from 'drizzle-orm/d1';
4
5
  import { emailSends } from '../schema.js';
6
+ import { emitMailerAnalyticsEvent } from '../utils/analytics.js';
5
7
  import { TRANSPARENT_GIF } from '../utils/tracking.js';
6
- export const GET = async ({ params, locals }) => {
8
+ export const GET = async (context) => {
9
+ const { params, request } = context;
7
10
  const trackingId = params.trackingId;
8
11
  if (!trackingId) {
9
12
  return new Response(null, { status: 400 });
@@ -11,9 +14,9 @@ export const GET = async ({ params, locals }) => {
11
14
  // Update status to 'opened' only when currently 'sent' or 'delivered'
12
15
  // to avoid downgrading from 'clicked'.
13
16
  try {
14
- const runtimeEnv = locals.runtime?.env;
15
- if (runtimeEnv) {
16
- const d1 = runtimeEnv[config.d1Binding];
17
+ const bindingsEnv = cloudflareEnv;
18
+ if (bindingsEnv) {
19
+ const d1 = bindingsEnv[config.d1Binding];
17
20
  const db = drizzle(d1);
18
21
  await db
19
22
  .update(emailSends)
@@ -27,6 +30,12 @@ export const GET = async ({ params, locals }) => {
27
30
  catch {
28
31
  // Never fail the pixel response on DB errors
29
32
  }
33
+ emitMailerAnalyticsEvent(config, cloudflareEnv, 'newsletter_opened', {
34
+ request,
35
+ context,
36
+ contentSlug: trackingId,
37
+ label: { trackingId },
38
+ });
30
39
  // 1x1 transparent GIF
31
40
  return new Response(TRANSPARENT_GIF, {
32
41
  status: 200,
@@ -1 +1 @@
1
- {"version":3,"file":"track-open.js","sourceRoot":"","sources":["../../src/routes/track-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAE1D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAEtD,MAAM,CAAC,MAAM,GAAG,GAAa,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE;IACzD,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAA;IACpC,IAAI,CAAC,UAAU,EAAE,CAAC;QACjB,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC3C,CAAC;IAED,sEAAsE;IACtE,uCAAuC;IACvC,IAAI,CAAC;QACJ,MAAM,UAAU,GACf,MAGA,CAAC,OAAO,EAAE,GAAG,CAAA;QACd,IAAI,UAAU,EAAE,CAAC;YAChB,MAAM,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC,SAAS,CAAe,CAAA;YACrD,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAAA;YACtB,MAAM,EAAE;iBACN,MAAM,CAAC,UAAU,CAAC;iBAClB,GAAG,CAAC;gBACJ,MAAM,EAAE,QAAQ;gBAChB,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aAClC,CAAC;iBACD,KAAK,CACL,GAAG,CACF,EAAE,CAAC,UAAU,CAAC,UAAU,EAAE,UAAU,CAAC,EACrC,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CACjD,CACD,CAAA;QACH,CAAC;IACF,CAAC;IAAC,MAAM,CAAC;QACR,6CAA6C;IAC9C,CAAC;IAED,sBAAsB;IACtB,OAAO,IAAI,QAAQ,CAAC,eAAe,EAAE;QACpC,MAAM,EAAE,GAAG;QACX,OAAO,EAAE;YACR,cAAc,EAAE,WAAW;YAC3B,eAAe,EAAE,qCAAqC;YACtD,gBAAgB,EAAE,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC;SAChD;KACD,CAAC,CAAA;AACH,CAAC,CAAA"}
1
+ {"version":3,"file":"track-open.js","sourceRoot":"","sources":["../../src/routes/track-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAE1D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AAChE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAEtD,MAAM,CAAC,MAAM,GAAG,GAAa,KAAK,EAAE,OAAO,EAAE,EAAE;IAC9C,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IACnC,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAA;IACpC,IAAI,CAAC,UAAU,EAAE,CAAC;QACjB,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC3C,CAAC;IAED,sEAAsE;IACtE,uCAAuC;IACvC,IAAI,CAAC;QACJ,MAAM,WAAW,GAAG,aAAwC,CAAA;QAC5D,IAAI,WAAW,EAAE,CAAC;YACjB,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,SAAS,CAAe,CAAA;YACtD,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAAA;YACtB,MAAM,EAAE;iBACN,MAAM,CAAC,UAAU,CAAC;iBAClB,GAAG,CAAC;gBACJ,MAAM,EAAE,QAAQ;gBAChB,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aAClC,CAAC;iBACD,KAAK,CACL,GAAG,CACF,EAAE,CAAC,UAAU,CAAC,UAAU,EAAE,UAAU,CAAC,EACrC,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CACjD,CACD,CAAA;QACH,CAAC;IACF,CAAC;IAAC,MAAM,CAAC;QACR,6CAA6C;IAC9C,CAAC;IAED,wBAAwB,CAAC,MAAM,EAAE,aAAwC,EAAE,mBAAmB,EAAE;QAC/F,OAAO;QACP,OAAO;QACP,WAAW,EAAE,UAAU;QACvB,KAAK,EAAE,EAAE,UAAU,EAAE;KACrB,CAAC,CAAA;IAEF,sBAAsB;IACtB,OAAO,IAAI,QAAQ,CAAC,eAAe,EAAE;QACpC,MAAM,EAAE,GAAG;QACX,OAAO,EAAE;YACR,cAAc,EAAE,WAAW;YAC3B,eAAe,EAAE,qCAAqC;YACtD,gBAAgB,EAAE,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC;SAChD;KACD,CAAC,CAAA;AACH,CAAC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"unsubscribe.d.ts","sourceRoot":"","sources":["../../src/routes/unsubscribe.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAmDrC,eAAO,MAAM,GAAG,EAAE,QAyBjB,CAAA;AAGD,eAAO,MAAM,IAAI,EAAE,QAelB,CAAA"}
1
+ {"version":3,"file":"unsubscribe.d.ts","sourceRoot":"","sources":["../../src/routes/unsubscribe.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAiErC,eAAO,MAAM,GAAG,EAAE,QA0BjB,CAAA;AAGD,eAAO,MAAM,IAAI,EAAE,QAgBlB,CAAA"}
@@ -1,19 +1,21 @@
1
+ import { env as cloudflareEnv } from 'cloudflare:workers';
1
2
  import { config } from 'virtual:growth-labs/mailer/config';
2
3
  import { drizzle } from 'drizzle-orm/d1';
4
+ import { emitMailerAnalyticsEvent } from '../utils/analytics.js';
3
5
  import { sendTransactional } from '../utils/send.js';
4
6
  import { getSubscriberById, unsubscribeSubscriber } from '../utils/subscribers.js';
5
7
  import { generateToken, verifyToken } from '../utils/tokens.js';
6
8
  import { buildSiteUrl } from '../utils/urls.js';
7
- async function processUnsubscribe(locals, token) {
9
+ async function processUnsubscribe(token, context, request) {
8
10
  // Verify token
9
11
  const payload = await verifyToken(config.signingSecret, token);
10
12
  if (!payload || payload.action !== 'unsubscribe') {
11
13
  return { error: 'Invalid or expired token', status: 400 };
12
14
  }
13
15
  // Resolve bindings
14
- const runtimeEnv = locals.runtime?.env;
15
- const d1 = runtimeEnv[config.d1Binding];
16
- const queue = runtimeEnv[config.queueBinding];
16
+ const bindingsEnv = cloudflareEnv;
17
+ const d1 = bindingsEnv[config.d1Binding];
18
+ const queue = bindingsEnv[config.queueBinding];
17
19
  const db = drizzle(d1);
18
20
  const env = { DB: d1, QUEUE: queue };
19
21
  // Get subscriber
@@ -34,18 +36,28 @@ async function processUnsubscribe(locals, token) {
34
36
  resubscribeUrl: buildSiteUrl(config.siteUrl, config.subscribePath),
35
37
  },
36
38
  });
39
+ emitMailerAnalyticsEvent(config, bindingsEnv, 'newsletter_unsubscribed', {
40
+ request,
41
+ context,
42
+ contentSlug: subscriber.id,
43
+ label: {
44
+ subscriberId: subscriber.id,
45
+ email: subscriber.email,
46
+ },
47
+ });
37
48
  return {
38
49
  success: true,
39
50
  subscriberId: payload.subscriberId,
40
51
  };
41
52
  }
42
53
  // GET: Link from email footer
43
- export const GET = async ({ url, locals }) => {
54
+ export const GET = async (context) => {
55
+ const { url, request } = context;
44
56
  const token = url.searchParams.get('token');
45
57
  if (!token) {
46
58
  return Response.json({ error: 'Missing token' }, { status: 400 });
47
59
  }
48
- const result = await processUnsubscribe(locals, token);
60
+ const result = await processUnsubscribe(token, context, request);
49
61
  if ('error' in result) {
50
62
  return Response.json({ error: result.error }, { status: result.status });
51
63
  }
@@ -64,7 +76,8 @@ export const GET = async ({ url, locals }) => {
64
76
  });
65
77
  };
66
78
  // POST: RFC 8058 List-Unsubscribe-Post
67
- export const POST = async ({ request, locals }) => {
79
+ export const POST = async (context) => {
80
+ const { request } = context;
68
81
  // RFC 8058: body is "List-Unsubscribe=One-Click"
69
82
  // Token comes from List-Unsubscribe header URL
70
83
  const url = new URL(request.url);
@@ -72,7 +85,7 @@ export const POST = async ({ request, locals }) => {
72
85
  if (!token) {
73
86
  return new Response('Missing token', { status: 400 });
74
87
  }
75
- const result = await processUnsubscribe(locals, token);
88
+ const result = await processUnsubscribe(token, context, request);
76
89
  if ('error' in result) {
77
90
  return new Response(result.error, { status: result.status });
78
91
  }