@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.
- package/README.md +28 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -2
- package/dist/index.js.map +1 -1
- package/dist/options.d.ts +89 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +5 -0
- package/dist/options.js.map +1 -1
- package/dist/queue/consumer.d.ts +12 -2
- package/dist/queue/consumer.d.ts.map +1 -1
- package/dist/queue/consumer.js +66 -0
- package/dist/queue/consumer.js.map +1 -1
- package/dist/schema/sends.d.ts +38 -0
- package/dist/schema/sends.d.ts.map +1 -1
- package/dist/schema/sends.js +2 -0
- package/dist/schema/sends.js.map +1 -1
- package/dist/types.d.ts +22 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/broadcast.d.ts +12 -0
- package/dist/utils/broadcast.d.ts.map +1 -1
- package/dist/utils/broadcast.js +7 -1
- package/dist/utils/broadcast.js.map +1 -1
- package/dist/utils/delivery-claim.d.ts +37 -0
- package/dist/utils/delivery-claim.d.ts.map +1 -0
- package/dist/utils/delivery-claim.js +46 -0
- package/dist/utils/delivery-claim.js.map +1 -0
- package/dist/utils/send.d.ts.map +1 -1
- package/dist/utils/send.js +6 -0
- package/dist/utils/send.js.map +1 -1
- package/dist/vite-plugin.d.ts +5 -3
- package/dist/vite-plugin.d.ts.map +1 -1
- package/dist/vite-plugin.js +4 -2
- package/dist/vite-plugin.js.map +1 -1
- package/migrations/0003_add_delivery_claim_to_email_sends.sql +8 -0
- package/package.json +3 -3
- package/src/index.ts +22 -4
- package/src/options.ts +9 -0
- package/src/queue/consumer.ts +116 -2
- package/src/schema/sends.ts +2 -0
- package/src/types.ts +23 -0
- package/src/utils/broadcast.ts +7 -1
- package/src/utils/delivery-claim.ts +66 -0
- package/src/utils/send.ts +6 -0
- 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"}
|
package/dist/utils/send.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/utils/send.js
CHANGED
|
@@ -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 = {
|
package/dist/utils/send.js.map
CHANGED
|
@@ -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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"}
|
package/dist/vite-plugin.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
|
-
import type { ResolvedMailerOptions } from './options.js';
|
|
3
|
-
|
|
4
|
-
export declare function
|
|
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;
|
|
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"}
|
package/dist/vite-plugin.js
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
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
|
}
|
package/dist/vite-plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite-plugin.js","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"
|
|
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.
|
|
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": "
|
|
88
|
+
"astro": "7.3.1",
|
|
89
89
|
"drizzle-kit": "^0.30.0",
|
|
90
90
|
"typescript": "^5.7.0",
|
|
91
|
-
"vite": "
|
|
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
|
-
|
|
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>
|
package/src/queue/consumer.ts
CHANGED
|
@@ -1,18 +1,82 @@
|
|
|
1
1
|
import { drizzle } from 'drizzle-orm/d1'
|
|
2
2
|
import { probeMailerSendsSchema } from '../_internal/schema-probe.js'
|
|
3
|
-
import type {
|
|
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 ?? ''
|
package/src/schema/sends.ts
CHANGED
|
@@ -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
|
|
package/src/utils/broadcast.ts
CHANGED
|
@@ -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
|
+
}
|