@growth-labs/mailer 0.5.0 → 0.6.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 (45) hide show
  1. package/README.md +28 -0
  2. package/dist/index.d.ts +5 -3
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +14 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/options.d.ts +89 -0
  7. package/dist/options.d.ts.map +1 -1
  8. package/dist/options.js +5 -0
  9. package/dist/options.js.map +1 -1
  10. package/dist/queue/consumer.d.ts +12 -2
  11. package/dist/queue/consumer.d.ts.map +1 -1
  12. package/dist/queue/consumer.js +66 -0
  13. package/dist/queue/consumer.js.map +1 -1
  14. package/dist/schema/sends.d.ts +38 -0
  15. package/dist/schema/sends.d.ts.map +1 -1
  16. package/dist/schema/sends.js +2 -0
  17. package/dist/schema/sends.js.map +1 -1
  18. package/dist/types.d.ts +22 -1
  19. package/dist/types.d.ts.map +1 -1
  20. package/dist/utils/broadcast.d.ts +12 -0
  21. package/dist/utils/broadcast.d.ts.map +1 -1
  22. package/dist/utils/broadcast.js +7 -1
  23. package/dist/utils/broadcast.js.map +1 -1
  24. package/dist/utils/delivery-claim.d.ts +37 -0
  25. package/dist/utils/delivery-claim.d.ts.map +1 -0
  26. package/dist/utils/delivery-claim.js +46 -0
  27. package/dist/utils/delivery-claim.js.map +1 -0
  28. package/dist/utils/send.d.ts.map +1 -1
  29. package/dist/utils/send.js +6 -0
  30. package/dist/utils/send.js.map +1 -1
  31. package/dist/vite-plugin.d.ts +5 -3
  32. package/dist/vite-plugin.d.ts.map +1 -1
  33. package/dist/vite-plugin.js +4 -2
  34. package/dist/vite-plugin.js.map +1 -1
  35. package/migrations/0003_add_delivery_claim_to_email_sends.sql +8 -0
  36. package/package.json +3 -3
  37. package/src/index.ts +22 -4
  38. package/src/options.ts +9 -0
  39. package/src/queue/consumer.ts +116 -2
  40. package/src/schema/sends.ts +2 -0
  41. package/src/types.ts +23 -0
  42. package/src/utils/broadcast.ts +7 -1
  43. package/src/utils/delivery-claim.ts +66 -0
  44. package/src/utils/send.ts +6 -0
  45. package/src/vite-plugin.ts +7 -8
@@ -0,0 +1,37 @@
1
+ import type { drizzle } from 'drizzle-orm/d1';
2
+ type DrizzleDB = ReturnType<typeof drizzle>;
3
+ export type ClaimOutcome =
4
+ /** This invocation owns the send — no prior claim exists. */
5
+ 'claimed'
6
+ /** A prior claim never reached ack (crash/timeout). Reconcile, don't resend. */
7
+ | 'reconcile-in-flight'
8
+ /** The row is already in a terminal state — a duplicate enqueue, no-op. */
9
+ | 'already-terminal'
10
+ /** The row exists but under a different delivery_token — a superseded or replayed message. */
11
+ | 'invalid-authorization'
12
+ /** No row for this trackingId at all — lost or not-yet-visible write. */
13
+ | 'missing';
14
+ /**
15
+ * Atomically claim a queued send for this delivery attempt (WM-01) — the
16
+ * stable per-(campaign, recipient) delivery claim the mailer consumer needs.
17
+ * `trackingId` is unique per recipient/attempt; the update is a
18
+ * compare-and-swap gated on the row still being `queued`, so of two
19
+ * concurrent or sequential claim attempts for the same row only one can
20
+ * succeed — D1 serializes writes, so the loser always observes the winner's
21
+ * new status.
22
+ *
23
+ * `deliveryToken` is minted once at enqueue time and carried in the queue
24
+ * message. Matching it against the row's stored token means a message from
25
+ * a superseded generation of the same trackingId (a fresh resend cycle
26
+ * re-keyed the row, or the message is a stale replay from before a D1
27
+ * restore) can never claim or act on the current row — it reads back as
28
+ * `invalid-authorization`, never `claimed` or `reconcile-in-flight`.
29
+ *
30
+ * An empty `deliveryToken` skips the token match: queue messages already
31
+ * in flight when this column shipped carry no token, and requiring one
32
+ * would strand that one-time backlog instead of letting it drain under the
33
+ * trackingId + status guard alone.
34
+ */
35
+ export declare function claimDelivery(db: DrizzleDB, trackingId: string, deliveryToken: string, now: string): Promise<ClaimOutcome>;
36
+ export {};
37
+ //# sourceMappingURL=delivery-claim.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delivery-claim.d.ts","sourceRoot":"","sources":["../../src/utils/delivery-claim.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAG7C,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAA;AAE3C,MAAM,MAAM,YAAY;AACvB,6DAA6D;AAC3D,SAAS;AACX,gFAAgF;GAC9E,qBAAqB;AACvB,2EAA2E;GACzE,kBAAkB;AACpB,8FAA8F;GAC5F,uBAAuB;AACzB,yEAAyE;GACvE,SAAS,CAAA;AAEZ;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,aAAa,CAClC,EAAE,EAAE,SAAS,EACb,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,GACT,OAAO,CAAC,YAAY,CAAC,CAqBvB"}
@@ -0,0 +1,46 @@
1
+ import { and, eq } from 'drizzle-orm';
2
+ import { emailSends } from '../schema/sends.js';
3
+ /**
4
+ * Atomically claim a queued send for this delivery attempt (WM-01) — the
5
+ * stable per-(campaign, recipient) delivery claim the mailer consumer needs.
6
+ * `trackingId` is unique per recipient/attempt; the update is a
7
+ * compare-and-swap gated on the row still being `queued`, so of two
8
+ * concurrent or sequential claim attempts for the same row only one can
9
+ * succeed — D1 serializes writes, so the loser always observes the winner's
10
+ * new status.
11
+ *
12
+ * `deliveryToken` is minted once at enqueue time and carried in the queue
13
+ * message. Matching it against the row's stored token means a message from
14
+ * a superseded generation of the same trackingId (a fresh resend cycle
15
+ * re-keyed the row, or the message is a stale replay from before a D1
16
+ * restore) can never claim or act on the current row — it reads back as
17
+ * `invalid-authorization`, never `claimed` or `reconcile-in-flight`.
18
+ *
19
+ * An empty `deliveryToken` skips the token match: queue messages already
20
+ * in flight when this column shipped carry no token, and requiring one
21
+ * would strand that one-time backlog instead of letting it drain under the
22
+ * trackingId + status guard alone.
23
+ */
24
+ export async function claimDelivery(db, trackingId, deliveryToken, now) {
25
+ const claimConditions = [eq(emailSends.trackingId, trackingId), eq(emailSends.status, 'queued')];
26
+ if (deliveryToken)
27
+ claimConditions.push(eq(emailSends.deliveryToken, deliveryToken));
28
+ const claimed = await db
29
+ .update(emailSends)
30
+ .set({ status: 'sending', claimedAt: now })
31
+ .where(and(...claimConditions))
32
+ .returning({ id: emailSends.id });
33
+ if (claimed.length > 0)
34
+ return 'claimed';
35
+ const rows = await db.select().from(emailSends).where(eq(emailSends.trackingId, trackingId));
36
+ if (rows.length === 0)
37
+ return 'missing';
38
+ const row = rows[0];
39
+ if (deliveryToken && row.deliveryToken && row.deliveryToken !== deliveryToken) {
40
+ return 'invalid-authorization';
41
+ }
42
+ if (row.status === 'sending')
43
+ return 'reconcile-in-flight';
44
+ return 'already-terminal';
45
+ }
46
+ //# sourceMappingURL=delivery-claim.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delivery-claim.js","sourceRoot":"","sources":["../../src/utils/delivery-claim.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAErC,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAgB/C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAClC,EAAa,EACb,UAAkB,EAClB,aAAqB,EACrB,GAAW;IAEX,MAAM,eAAe,GAAG,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAA;IAChG,IAAI,aAAa;QAAE,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC,CAAA;IAEpF,MAAM,OAAO,GAAG,MAAM,EAAE;SACtB,MAAM,CAAC,UAAU,CAAC;SAClB,GAAG,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;SAC1C,KAAK,CAAC,GAAG,CAAC,GAAG,eAAe,CAAC,CAAC;SAC9B,SAAS,CAAC,EAAE,EAAE,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,CAAA;IAElC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IAExC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC,CAAA;IAC5F,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IAEvC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAqD,CAAA;IACvE,IAAI,aAAa,IAAI,GAAG,CAAC,aAAa,IAAI,GAAG,CAAC,aAAa,KAAK,aAAa,EAAE,CAAC;QAC/E,OAAO,uBAAuB,CAAA;IAC/B,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,qBAAqB,CAAA;IAC1D,OAAO,kBAAkB,CAAA;AAC1B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"send.d.ts","sourceRoot":"","sources":["../../src/utils/send.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAExC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAE1D,OAAO,KAAK,EACX,UAAU,EAGV,gBAAgB,EAEhB,YAAY,EACZ,MAAM,aAAa,CAAA;AAQpB,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAA;AAI3C;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAkD/C,wBAAsB,iBAAiB,CACtC,GAAG,EAAE,SAAS,EACd,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE;IACP,EAAE,EAAE,MAAM,CAAA;IACV,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,YAAY,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC9B,GACC,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CA2DjC;AAoFD,wBAAsB,YAAY,CACjC,GAAG,EAAE,SAAS,EACd,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE;IACP,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,UAAU,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,MAAM,CAAC,EAAE,gBAAgB,CAAA;IACzB,UAAU,CAAC,EAAE,MAAM,CAAA;CACnB,GACC,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAAC,CAqCzD;AAID,wBAAsB,UAAU,CAC/B,GAAG,EAAE,SAAS,EACd,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE;IACP,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,gBAAgB,CAAA;IACzB,UAAU,CAAC,EAAE,MAAM,CAAA;CACnB,GACC,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAAC,CAgCzD;AAID,wBAAsB,gBAAgB,CACrC,EAAE,EAAE,SAAS,EACb,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,GACpC,OAAO,CAAC,IAAI,CAAC,CAKf"}
1
+ {"version":3,"file":"send.d.ts","sourceRoot":"","sources":["../../src/utils/send.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAExC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAE1D,OAAO,KAAK,EACX,UAAU,EAGV,gBAAgB,EAEhB,YAAY,EACZ,MAAM,aAAa,CAAA;AAQpB,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAA;AAI3C;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAkD/C,wBAAsB,iBAAiB,CACtC,GAAG,EAAE,SAAS,EACd,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE;IACP,EAAE,EAAE,MAAM,CAAA;IACV,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,YAAY,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC9B,GACC,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CA8DjC;AAuFD,wBAAsB,YAAY,CACjC,GAAG,EAAE,SAAS,EACd,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE;IACP,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,UAAU,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,MAAM,CAAC,EAAE,gBAAgB,CAAA;IACzB,UAAU,CAAC,EAAE,MAAM,CAAA;CACnB,GACC,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAAC,CAqCzD;AAID,wBAAsB,UAAU,CAC/B,GAAG,EAAE,SAAS,EACd,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE;IACP,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,gBAAgB,CAAA;IACzB,UAAU,CAAC,EAAE,MAAM,CAAA;CACnB,GACC,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAAC,CAgCzD;AAID,wBAAsB,gBAAgB,CACrC,EAAE,EAAE,SAAS,EACb,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,GACpC,OAAO,CAAC,IAAI,CAAC,CAKf"}
@@ -38,6 +38,7 @@ export async function sendTransactional(env, options, params) {
38
38
  const signingSecret = params.subscriberId ? await requireSigningSecret(env, options) : undefined;
39
39
  const { sendsDb, queue } = resolveProducerBindings(env, options);
40
40
  const trackingId = ulid();
41
+ const deliveryToken = ulid();
41
42
  const subscriberId = params.subscriberId ?? ulid();
42
43
  // Render HTML
43
44
  let html;
@@ -68,6 +69,7 @@ export async function sendTransactional(env, options, params) {
68
69
  type: 'transactional',
69
70
  status: 'queued',
70
71
  trackingId,
72
+ deliveryToken,
71
73
  createdAt: new Date().toISOString(),
72
74
  });
73
75
  // Build and enqueue the message
@@ -81,6 +83,7 @@ export async function sendTransactional(env, options, params) {
81
83
  trackingId,
82
84
  unsubscribeToken: '',
83
85
  preferencesToken: '',
86
+ deliveryToken,
84
87
  },
85
88
  ],
86
89
  subject: params.subject,
@@ -105,6 +108,7 @@ async function fanOutToSubscribers(queue, db, sendsDb, options, signingSecret, p
105
108
  const recipients = [];
106
109
  for (const sub of batch) {
107
110
  const trackingId = ulid();
111
+ const deliveryToken = ulid();
108
112
  const unsubscribeToken = await generateToken(signingSecret, {
109
113
  subscriberId: sub.id,
110
114
  action: 'unsubscribe',
@@ -125,6 +129,7 @@ async function fanOutToSubscribers(queue, db, sendsDb, options, signingSecret, p
125
129
  type: params.type,
126
130
  status: 'queued',
127
131
  trackingId,
132
+ deliveryToken,
128
133
  createdAt: new Date().toISOString(),
129
134
  });
130
135
  recipients.push({
@@ -133,6 +138,7 @@ async function fanOutToSubscribers(queue, db, sendsDb, options, signingSecret, p
133
138
  trackingId,
134
139
  unsubscribeToken,
135
140
  preferencesToken,
141
+ deliveryToken,
136
142
  });
137
143
  }
138
144
  const message = {
@@ -1 +1 @@
1
- {"version":3,"file":"send.js","sourceRoot":"","sources":["../../src/utils/send.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"send.js","sourceRoot":"","sources":["../../src/utils/send.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,7 @@
1
1
  import type { Plugin } from 'vite';
2
- import type { ResolvedMailerOptions } from './options.js';
3
- export declare function renderMailerConfigModule(config: ResolvedMailerOptions, command: 'build' | 'serve'): string;
4
- export declare function growthLabsMailerPlugin(config: ResolvedMailerOptions): Plugin;
2
+ import type { ResolvedMailerComponentsOptions, ResolvedMailerOptions } from './options.js';
3
+ type MailerConfig = ResolvedMailerOptions | ResolvedMailerComponentsOptions;
4
+ export declare function renderMailerConfigModule(config: MailerConfig, command: 'build' | 'serve'): string;
5
+ export declare function growthLabsMailerPlugin(config: MailerConfig): Plugin;
6
+ export {};
5
7
  //# sourceMappingURL=vite-plugin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAClC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAKzD,wBAAgB,wBAAwB,CACvC,MAAM,EAAE,qBAAqB,EAC7B,OAAO,EAAE,OAAO,GAAG,OAAO,GACxB,MAAM,CAOR;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAmB5E"}
1
+ {"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAClC,OAAO,KAAK,EAAE,+BAA+B,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAE1F,KAAK,YAAY,GAAG,qBAAqB,GAAG,+BAA+B,CAAA;AAK3E,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,CAOjG;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAmBnE"}
@@ -3,8 +3,10 @@ const RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
3
3
  export function renderMailerConfigModule(config, command) {
4
4
  const serializableConfig = { ...config };
5
5
  if (command !== 'serve') {
6
- delete serializableConfig.turnstileSecretKey;
7
- delete serializableConfig.signingSecret;
6
+ if ('turnstileSecretKey' in serializableConfig)
7
+ delete serializableConfig.turnstileSecretKey;
8
+ if ('signingSecret' in serializableConfig)
9
+ delete serializableConfig.signingSecret;
8
10
  }
9
11
  return `export const config = ${JSON.stringify(serializableConfig)};`;
10
12
  }
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin.js","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AAGA,MAAM,iBAAiB,GAAG,mCAAmC,CAAA;AAC7D,MAAM,0BAA0B,GAAG,KAAK,iBAAiB,EAAE,CAAA;AAE3D,MAAM,UAAU,wBAAwB,CACvC,MAA6B,EAC7B,OAA0B;IAE1B,MAAM,kBAAkB,GAAG,EAAE,GAAG,MAAM,EAAE,CAAA;IACxC,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;QACzB,OAAO,kBAAkB,CAAC,kBAAkB,CAAA;QAC5C,OAAO,kBAAkB,CAAC,aAAa,CAAA;IACxC,CAAC;IACD,OAAO,yBAAyB,IAAI,CAAC,SAAS,CAAC,kBAAkB,CAAC,GAAG,CAAA;AACtE,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,MAA6B;IACnE,IAAI,OAAO,GAAsB,OAAO,CAAA;IAExC,OAAO;QACN,IAAI,EAAE,2BAA2B;QACjC,cAAc,CAAC,cAAc;YAC5B,OAAO,GAAG,cAAc,CAAC,OAAO,CAAA;QACjC,CAAC;QACD,SAAS,CAAC,EAAE;YACX,IAAI,EAAE,KAAK,iBAAiB,EAAE,CAAC;gBAC9B,OAAO,0BAA0B,CAAA;YAClC,CAAC;QACF,CAAC;QACD,IAAI,CAAC,EAAE;YACN,IAAI,EAAE,KAAK,0BAA0B,EAAE,CAAC;gBACvC,OAAO,wBAAwB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;YACjD,CAAC;QACF,CAAC;KACD,CAAA;AACF,CAAC"}
1
+ {"version":3,"file":"vite-plugin.js","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AAKA,MAAM,iBAAiB,GAAG,mCAAmC,CAAA;AAC7D,MAAM,0BAA0B,GAAG,KAAK,iBAAiB,EAAE,CAAA;AAE3D,MAAM,UAAU,wBAAwB,CAAC,MAAoB,EAAE,OAA0B;IACxF,MAAM,kBAAkB,GAAG,EAAE,GAAG,MAAM,EAAE,CAAA;IACxC,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;QACzB,IAAI,oBAAoB,IAAI,kBAAkB;YAAE,OAAO,kBAAkB,CAAC,kBAAkB,CAAA;QAC5F,IAAI,eAAe,IAAI,kBAAkB;YAAE,OAAO,kBAAkB,CAAC,aAAa,CAAA;IACnF,CAAC;IACD,OAAO,yBAAyB,IAAI,CAAC,SAAS,CAAC,kBAAkB,CAAC,GAAG,CAAA;AACtE,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,MAAoB;IAC1D,IAAI,OAAO,GAAsB,OAAO,CAAA;IAExC,OAAO;QACN,IAAI,EAAE,2BAA2B;QACjC,cAAc,CAAC,cAAc;YAC5B,OAAO,GAAG,cAAc,CAAC,OAAO,CAAA;QACjC,CAAC;QACD,SAAS,CAAC,EAAE;YACX,IAAI,EAAE,KAAK,iBAAiB,EAAE,CAAC;gBAC9B,OAAO,0BAA0B,CAAA;YAClC,CAAC;QACF,CAAC;QACD,IAAI,CAAC,EAAE;YACN,IAAI,EAAE,KAAK,0BAA0B,EAAE,CAAC;gBACvC,OAAO,wBAAwB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;YACjD,CAAC;QACF,CAAC;KACD,CAAA;AACF,CAAC"}
@@ -0,0 +1,8 @@
1
+ -- WM-01: send-time suppression recheck + a stable per-recipient delivery
2
+ -- claim. delivery_token is minted once at enqueue time and carried in the
3
+ -- queue message; the consumer's claim update matches on it so a message
4
+ -- superseded by a fresh resend cycle (or replayed from before a restore)
5
+ -- can never be actioned against the current row. claimed_at marks the
6
+ -- in-flight window between claiming the row and reconciling its outcome.
7
+ ALTER TABLE gl_email_sends ADD COLUMN delivery_token TEXT;
8
+ ALTER TABLE gl_email_sends ADD COLUMN claimed_at TEXT;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growth-labs/mailer",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "exports": {
@@ -85,10 +85,10 @@
85
85
  },
86
86
  "devDependencies": {
87
87
  "@cloudflare/workers-types": "^4.20260402.1",
88
- "astro": "^6.4.6",
88
+ "astro": "7.3.1",
89
89
  "drizzle-kit": "^0.30.0",
90
90
  "typescript": "^5.7.0",
91
- "vite": "^7.3.5",
91
+ "vite": "8.0.16",
92
92
  "vitest": "^4.1.8"
93
93
  },
94
94
  "scripts": {
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { fileURLToPath } from 'node:url'
2
2
  import type { AstroIntegration } from 'astro'
3
- import type { MailerOptions, ResolvedMailerOptions } from './options.js'
4
- import { mailerOptionsSchema } from './options.js'
3
+ import type { MailerComponentsOptions, MailerOptions, ResolvedMailerOptions } from './options.js'
4
+ import { mailerComponentsOptionsSchema, mailerOptionsSchema } from './options.js'
5
5
  import { growthLabsMailerPlugin } from './vite-plugin.js'
6
6
 
7
7
  function isSourceEntrypoint(): boolean {
@@ -86,9 +86,27 @@ export default function mailer(userOptions: MailerOptions): AstroIntegration {
86
86
  }
87
87
  }
88
88
 
89
- export type { MailerOptions, ResolvedMailerOptions } from './options.js'
89
+ /** Render package components without installing delivery routes or tracking. */
90
+ export function mailerComponents(userOptions: MailerComponentsOptions): AstroIntegration {
91
+ const options = mailerComponentsOptionsSchema.parse(userOptions)
92
+ return {
93
+ name: '@growth-labs/mailer/components',
94
+ hooks: {
95
+ 'astro:config:setup': ({ updateConfig }) => {
96
+ updateConfig({ vite: { plugins: [growthLabsMailerPlugin(options)] } })
97
+ },
98
+ },
99
+ }
100
+ }
101
+
102
+ export type {
103
+ MailerComponentsOptions,
104
+ MailerOptions,
105
+ ResolvedMailerComponentsOptions,
106
+ ResolvedMailerOptions,
107
+ } from './options.js'
90
108
  // Re-export options and types
91
- export { mailerOptionsSchema } from './options.js'
109
+ export { mailerComponentsOptionsSchema, mailerOptionsSchema } from './options.js'
92
110
  export type {
93
111
  DigestItem,
94
112
  EmailProvider,
package/src/options.ts CHANGED
@@ -90,3 +90,12 @@ export const mailerOptionsSchema = z.object({
90
90
 
91
91
  export type MailerOptions = z.input<typeof mailerOptionsSchema>
92
92
  export type ResolvedMailerOptions = z.output<typeof mailerOptionsSchema>
93
+
94
+ // Only the public configuration read by SubscribeForm. No delivery credentials
95
+ // or Worker bindings are needed when a consumer supplies its own endpoint.
96
+ export const mailerComponentsOptionsSchema = mailerOptionsSchema
97
+ .pick({ turnstileSiteKey: true, topics: true, subscribePath: true })
98
+ .strict()
99
+
100
+ export type MailerComponentsOptions = z.input<typeof mailerComponentsOptionsSchema>
101
+ export type ResolvedMailerComponentsOptions = z.output<typeof mailerComponentsOptionsSchema>
@@ -1,18 +1,82 @@
1
1
  import { drizzle } from 'drizzle-orm/d1'
2
2
  import { probeMailerSendsSchema } from '../_internal/schema-probe.js'
3
- import type { EmailQueueMessage, SiteConfigLookup, SiteMailerConfig } from '../types.js'
3
+ import type {
4
+ EmailQueueMessage,
5
+ QueueRecipient,
6
+ SiteConfigLookup,
7
+ SiteMailerConfig,
8
+ SubscriberStatusLookup,
9
+ } from '../types.js'
4
10
  import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
5
11
  import { updateSendStatus } from '../utils/bounce.js'
12
+ import { SUPPRESSED_STATUSES } from '../utils/broadcast.js'
13
+ import { claimDelivery } from '../utils/delivery-claim.js'
6
14
  import type { CloudflareEmailSender } from '../utils/providers.js'
7
15
  import { CloudflareEmailProvider, sleep } from '../utils/providers.js'
16
+ import { getSubscriberById } from '../utils/subscribers.js'
8
17
 
9
- export type { SiteConfigLookup, SiteMailerConfig } from '../types.js'
18
+ export type { SiteConfigLookup, SiteMailerConfig, SubscriberStatusLookup } from '../types.js'
19
+
20
+ type DrizzleDB = ReturnType<typeof drizzle>
10
21
 
11
22
  export interface MailerConsumerOptions {
12
23
  d1Binding: string
13
24
  senderBinding: string
14
25
  analyticsBinding?: string
15
26
  siteConfigLookup: SiteConfigLookup
27
+ /**
28
+ * Recheck a recipient's current suppression status at send admission
29
+ * (WM-01). Defaults to querying `gl_subscribers` on `env[d1Binding]` —
30
+ * correct for single-tenant sites. A realm consumer whose `d1Binding` D1
31
+ * holds `gl_email_sends` only (subscribers live in each site's own D1)
32
+ * must supply this for the recheck to be effective; unset in that
33
+ * topology, the consumer logs GL_MAILER_SUPPRESSION_CHECK_UNAVAILABLE
34
+ * once per instance and falls back to enqueue-time suppression only.
35
+ */
36
+ subscriberStatusLookup?: SubscriberStatusLookup
37
+ }
38
+
39
+ let subscriberLookupUnavailableLogged = false
40
+
41
+ /**
42
+ * True when the recipient's subscriber row currently carries a suppressed
43
+ * status — rechecked at send admission, not trusted from enqueue time
44
+ * (WM-01). Falls back to a direct `gl_subscribers` query when no
45
+ * `subscriberStatusLookup` is configured; a query failure (table absent —
46
+ * the realm-consumer topology) logs once and fails open rather than
47
+ * silently blocking every send in a deployment this check can't reach.
48
+ */
49
+ async function isCurrentlySuppressed(
50
+ db: DrizzleDB,
51
+ recipient: QueueRecipient,
52
+ siteId: string,
53
+ env: Record<string, unknown>,
54
+ lookup: SubscriberStatusLookup | undefined,
55
+ ): Promise<boolean> {
56
+ if (lookup) {
57
+ const status = await lookup(
58
+ { subscriberId: recipient.subscriberId, siteId, email: recipient.email },
59
+ env,
60
+ )
61
+ return status !== null && SUPPRESSED_STATUSES.has(status)
62
+ }
63
+ try {
64
+ const subscriber = await getSubscriberById(db, recipient.subscriberId)
65
+ return subscriber !== null && SUPPRESSED_STATUSES.has(subscriber.status)
66
+ } catch (err) {
67
+ if (!subscriberLookupUnavailableLogged) {
68
+ subscriberLookupUnavailableLogged = true
69
+ console.error(
70
+ '[mailer] GL_MAILER_SUPPRESSION_CHECK_UNAVAILABLE: cannot recheck suppression at send ' +
71
+ "time — gl_subscribers is not queryable on env.<d1Binding> (a realm consumer's " +
72
+ 'd1Binding D1 holds gl_email_sends only; subscribers live in each site D1). Wire ' +
73
+ 'MailerConsumerOptions.subscriberStatusLookup to close this gap. Sending without a ' +
74
+ 'send-time recheck for this Worker instance. ' +
75
+ (err instanceof Error ? err.message : String(err)),
76
+ )
77
+ }
78
+ return false
79
+ }
16
80
  }
17
81
 
18
82
  /** Effective options for a single message, resolved from `siteConfigLookup`. */
@@ -106,6 +170,56 @@ export async function handleEmailQueue(
106
170
  const effectiveReplyTo = effective.replyTo ?? replyTo
107
171
 
108
172
  for (const recipient of recipients) {
173
+ const claimedAt = new Date().toISOString()
174
+ const claim = await claimDelivery(
175
+ db,
176
+ recipient.trackingId,
177
+ recipient.deliveryToken,
178
+ claimedAt,
179
+ )
180
+
181
+ if (claim === 'missing' || claim === 'invalid-authorization') {
182
+ console.error(
183
+ `[mailer] Refusing to send trackingId=${recipient.trackingId}: ${claim} — stale, ` +
184
+ 'superseded, or a restore-era replay. Not sending, not touching the record.',
185
+ )
186
+ continue
187
+ }
188
+
189
+ if (claim === 'already-terminal') {
190
+ // Duplicate enqueue or redelivery of an already fully-processed
191
+ // send — a no-op, not a resend.
192
+ continue
193
+ }
194
+
195
+ if (claim === 'reconcile-in-flight') {
196
+ // A prior attempt claimed this row and never reached ack (crash or
197
+ // timeout before message.ack()). We cannot know whether the
198
+ // provider call actually completed, but calling it again risks a
199
+ // duplicate real send — reconcile as sent instead of resending.
200
+ await updateSendStatus(db, recipient.trackingId, 'sent', {
201
+ sentAt: new Date().toISOString(),
202
+ })
203
+ continue
204
+ }
205
+
206
+ // claim === 'claimed' — this invocation owns the send. Recheck
207
+ // suppression now: the subscriber's status may have changed since
208
+ // this message was enqueued.
209
+ if (type !== 'transactional') {
210
+ const suppressed = await isCurrentlySuppressed(
211
+ db,
212
+ recipient,
213
+ message.body.siteId,
214
+ env,
215
+ options.subscriberStatusLookup,
216
+ )
217
+ if (suppressed) {
218
+ await updateSendStatus(db, recipient.trackingId, 'suppressed', {})
219
+ continue
220
+ }
221
+ }
222
+
109
223
  let html = htmlTemplate
110
224
  if (type !== 'transactional') {
111
225
  const siteUrl = effective.siteUrl ?? ''
@@ -19,6 +19,8 @@ export const emailSends = sqliteTable(
19
19
  bounceType: text('bounce_type'),
20
20
  complainedAt: text('complained_at'),
21
21
  trackingId: text('tracking_id').notNull().unique(),
22
+ deliveryToken: text('delivery_token'),
23
+ claimedAt: text('claimed_at'),
22
24
  createdAt: text('created_at').notNull(),
23
25
  },
24
26
  (table) => [
package/src/types.ts CHANGED
@@ -82,12 +82,33 @@ export type SiteConfigLookup = (
82
82
  env: Record<string, unknown>,
83
83
  ) => Promise<SiteMailerConfig | null>
84
84
 
85
+ /**
86
+ * Recheck a recipient's current suppression status at send admission
87
+ * (WM-01). The default consumer implementation queries `gl_subscribers` on
88
+ * `env[d1Binding]`, which is correct for single-tenant sites. A realm
89
+ * consumer whose `d1Binding` D1 holds `gl_email_sends` only — subscribers
90
+ * live in each site's own D1 — must supply this to make the recheck
91
+ * effective for real; see MailerConsumerOptions.subscriberStatusLookup.
92
+ */
93
+ export type SubscriberStatusLookup = (
94
+ params: { subscriberId: string; siteId: string; email: string },
95
+ env: Record<string, unknown>,
96
+ ) => Promise<SubscriberStatus | null>
97
+
85
98
  export interface QueueRecipient {
86
99
  email: string
87
100
  subscriberId: string
88
101
  trackingId: string
89
102
  unsubscribeToken: string
90
103
  preferencesToken: string
104
+ /**
105
+ * Minted once at enqueue time and carried through to the consumer (WM-01).
106
+ * The consumer's delivery claim matches this against the current
107
+ * `gl_email_sends` row before sending, so a message superseded by a later
108
+ * resend cycle — or replayed from before a restore — can't act on a row
109
+ * it no longer authorizes.
110
+ */
111
+ deliveryToken: string
91
112
  }
92
113
 
93
114
  // ─── Email send record ───
@@ -113,12 +134,14 @@ export interface EmailSend {
113
134
 
114
135
  export type EmailSendStatus =
115
136
  | 'queued'
137
+ | 'sending'
116
138
  | 'sent'
117
139
  | 'delivered'
118
140
  | 'opened'
119
141
  | 'clicked'
120
142
  | 'bounced'
121
143
  | 'complained'
144
+ | 'suppressed'
122
145
 
123
146
  // ─── Email provider interface ───
124
147
 
@@ -19,8 +19,11 @@ type DrizzleDB = ReturnType<typeof drizzle>
19
19
  * the ONLY sendable status; everything else (including `pending`, i.e. an
20
20
  * unconfirmed double-opt-in subscriber) is suppressed so a one-off blast can
21
21
  * never re-permission or re-mail a suppressed address.
22
+ *
23
+ * Exported so the queue consumer can recheck against the same set at send
24
+ * admission (WM-01) — the same statuses that block enqueue must block send.
22
25
  */
23
- const SUPPRESSED_STATUSES: ReadonlySet<SubscriberStatus> = new Set<SubscriberStatus>([
26
+ export const SUPPRESSED_STATUSES: ReadonlySet<SubscriberStatus> = new Set<SubscriberStatus>([
24
27
  'unsubscribed',
25
28
  'bounced',
26
29
  'complained',
@@ -343,6 +346,7 @@ export async function sendBroadcast(
343
346
  const queueRecipients: QueueRecipient[] = []
344
347
  for (const recipient of fresh) {
345
348
  const trackingId = ulid()
349
+ const deliveryToken = ulid()
346
350
  const [unsubscribeToken, preferencesToken] = await Promise.all([
347
351
  generateToken(signingSecret, {
348
352
  subscriberId: recipient.subscriberId,
@@ -366,6 +370,7 @@ export async function sendBroadcast(
366
370
  type: 'broadcast',
367
371
  status: 'queued',
368
372
  trackingId,
373
+ deliveryToken,
369
374
  createdAt: now,
370
375
  })
371
376
 
@@ -375,6 +380,7 @@ export async function sendBroadcast(
375
380
  trackingId,
376
381
  unsubscribeToken,
377
382
  preferencesToken,
383
+ deliveryToken,
378
384
  })
379
385
  }
380
386
 
@@ -0,0 +1,66 @@
1
+ import { and, eq } from 'drizzle-orm'
2
+ import type { drizzle } from 'drizzle-orm/d1'
3
+ import { emailSends } from '../schema/sends.js'
4
+
5
+ type DrizzleDB = ReturnType<typeof drizzle>
6
+
7
+ export type ClaimOutcome =
8
+ /** This invocation owns the send — no prior claim exists. */
9
+ | 'claimed'
10
+ /** A prior claim never reached ack (crash/timeout). Reconcile, don't resend. */
11
+ | 'reconcile-in-flight'
12
+ /** The row is already in a terminal state — a duplicate enqueue, no-op. */
13
+ | 'already-terminal'
14
+ /** The row exists but under a different delivery_token — a superseded or replayed message. */
15
+ | 'invalid-authorization'
16
+ /** No row for this trackingId at all — lost or not-yet-visible write. */
17
+ | 'missing'
18
+
19
+ /**
20
+ * Atomically claim a queued send for this delivery attempt (WM-01) — the
21
+ * stable per-(campaign, recipient) delivery claim the mailer consumer needs.
22
+ * `trackingId` is unique per recipient/attempt; the update is a
23
+ * compare-and-swap gated on the row still being `queued`, so of two
24
+ * concurrent or sequential claim attempts for the same row only one can
25
+ * succeed — D1 serializes writes, so the loser always observes the winner's
26
+ * new status.
27
+ *
28
+ * `deliveryToken` is minted once at enqueue time and carried in the queue
29
+ * message. Matching it against the row's stored token means a message from
30
+ * a superseded generation of the same trackingId (a fresh resend cycle
31
+ * re-keyed the row, or the message is a stale replay from before a D1
32
+ * restore) can never claim or act on the current row — it reads back as
33
+ * `invalid-authorization`, never `claimed` or `reconcile-in-flight`.
34
+ *
35
+ * An empty `deliveryToken` skips the token match: queue messages already
36
+ * in flight when this column shipped carry no token, and requiring one
37
+ * would strand that one-time backlog instead of letting it drain under the
38
+ * trackingId + status guard alone.
39
+ */
40
+ export async function claimDelivery(
41
+ db: DrizzleDB,
42
+ trackingId: string,
43
+ deliveryToken: string,
44
+ now: string,
45
+ ): Promise<ClaimOutcome> {
46
+ const claimConditions = [eq(emailSends.trackingId, trackingId), eq(emailSends.status, 'queued')]
47
+ if (deliveryToken) claimConditions.push(eq(emailSends.deliveryToken, deliveryToken))
48
+
49
+ const claimed = await db
50
+ .update(emailSends)
51
+ .set({ status: 'sending', claimedAt: now })
52
+ .where(and(...claimConditions))
53
+ .returning({ id: emailSends.id })
54
+
55
+ if (claimed.length > 0) return 'claimed'
56
+
57
+ const rows = await db.select().from(emailSends).where(eq(emailSends.trackingId, trackingId))
58
+ if (rows.length === 0) return 'missing'
59
+
60
+ const row = rows[0] as { status: string; deliveryToken: string | null }
61
+ if (deliveryToken && row.deliveryToken && row.deliveryToken !== deliveryToken) {
62
+ return 'invalid-authorization'
63
+ }
64
+ if (row.status === 'sending') return 'reconcile-in-flight'
65
+ return 'already-terminal'
66
+ }