@growth-labs/mailer 0.4.11 → 0.5.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 +18 -2
- package/dist/options.d.ts +12 -6
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +4 -2
- package/dist/options.js.map +1 -1
- package/dist/routes/confirm.d.ts.map +1 -1
- package/dist/routes/confirm.js +6 -2
- package/dist/routes/confirm.js.map +1 -1
- package/dist/routes/subscribe.d.ts.map +1 -1
- package/dist/routes/subscribe.js +11 -3
- package/dist/routes/subscribe.js.map +1 -1
- package/dist/routes/unsubscribe.d.ts.map +1 -1
- package/dist/routes/unsubscribe.js +14 -3
- package/dist/routes/unsubscribe.js.map +1 -1
- package/dist/utils/broadcast.d.ts.map +1 -1
- package/dist/utils/broadcast.js +10 -2
- package/dist/utils/broadcast.js.map +1 -1
- package/dist/utils/runtime-secrets.d.ts +25 -0
- package/dist/utils/runtime-secrets.d.ts.map +1 -0
- package/dist/utils/runtime-secrets.js +96 -0
- package/dist/utils/runtime-secrets.js.map +1 -0
- package/dist/utils/send.d.ts.map +1 -1
- package/dist/utils/send.js +18 -6
- package/dist/utils/send.js.map +1 -1
- package/dist/utils/tokens.d.ts +3 -1
- package/dist/utils/tokens.d.ts.map +1 -1
- package/dist/utils/tokens.js +14 -5
- package/dist/utils/tokens.js.map +1 -1
- package/dist/utils/urls.d.ts +1 -1
- package/dist/utils/urls.d.ts.map +1 -1
- package/dist/utils/urls.js +9 -3
- package/dist/utils/urls.js.map +1 -1
- package/dist/vite-plugin.d.ts +1 -0
- package/dist/vite-plugin.d.ts.map +1 -1
- package/dist/vite-plugin.js +13 -1
- package/dist/vite-plugin.js.map +1 -1
- package/package.json +4 -4
- package/src/options.ts +4 -2
- package/src/routes/confirm.ts +9 -2
- package/src/routes/preferences.astro +12 -3
- package/src/routes/subscribe.ts +15 -3
- package/src/routes/unsubscribe.ts +16 -3
- package/src/utils/broadcast.ts +9 -2
- package/src/utils/runtime-secrets.ts +144 -0
- package/src/utils/send.ts +21 -5
- package/src/utils/tokens.ts +16 -5
- package/src/utils/urls.ts +9 -6
- package/src/virtual.d.ts +4 -2
- package/src/vite-plugin.ts +18 -1
package/dist/utils/send.js
CHANGED
|
@@ -2,11 +2,18 @@ import { eq } from 'drizzle-orm';
|
|
|
2
2
|
import { drizzle } from 'drizzle-orm/d1';
|
|
3
3
|
import { ulid } from 'ulidx';
|
|
4
4
|
import { emailSends } from '../schema/sends.js';
|
|
5
|
+
import { resolveSigningSecret, siteHost } from './runtime-secrets.js';
|
|
5
6
|
import { getSubscriberBatch } from './subscribers.js';
|
|
6
7
|
import { renderDigestItems, renderEmail } from './templates.js';
|
|
7
8
|
import { generateToken } from './tokens.js';
|
|
8
9
|
import { injectTrackingPixel, rewriteLinksForTracking } from './tracking.js';
|
|
9
10
|
import { buildSubscriberManageUrls } from './urls.js';
|
|
11
|
+
async function requireSigningSecret(env, options) {
|
|
12
|
+
const result = await resolveSigningSecret(env, options, options.siteUrl);
|
|
13
|
+
if (!result.ok)
|
|
14
|
+
throw new Error(result.code);
|
|
15
|
+
return result.secret;
|
|
16
|
+
}
|
|
10
17
|
function resolveProducerBindings(env, options) {
|
|
11
18
|
const d1 = env[options.d1Binding];
|
|
12
19
|
if (!d1) {
|
|
@@ -28,13 +35,14 @@ function resolveProducerBindings(env, options) {
|
|
|
28
35
|
}
|
|
29
36
|
// ─── sendTransactional ───
|
|
30
37
|
export async function sendTransactional(env, options, params) {
|
|
38
|
+
const signingSecret = params.subscriberId ? await requireSigningSecret(env, options) : undefined;
|
|
31
39
|
const { sendsDb, queue } = resolveProducerBindings(env, options);
|
|
32
40
|
const trackingId = ulid();
|
|
33
41
|
const subscriberId = params.subscriberId ?? ulid();
|
|
34
42
|
// Render HTML
|
|
35
43
|
let html;
|
|
36
44
|
if (params.template) {
|
|
37
|
-
const manageUrls = await buildSubscriberManageUrls(options, params.subscriberId);
|
|
45
|
+
const manageUrls = await buildSubscriberManageUrls(options, params.subscriberId, signingSecret);
|
|
38
46
|
html = renderEmail(params.template, {
|
|
39
47
|
...params.data,
|
|
40
48
|
senderName: options.senderName,
|
|
@@ -84,7 +92,7 @@ export async function sendTransactional(env, options, params) {
|
|
|
84
92
|
return { trackingId };
|
|
85
93
|
}
|
|
86
94
|
// ─── Fan-out helper (shared by campaign + digest) ───
|
|
87
|
-
async function fanOutToSubscribers(queue, db, sendsDb, options, params) {
|
|
95
|
+
async function fanOutToSubscribers(queue, db, sendsDb, options, signingSecret, params) {
|
|
88
96
|
let cursor = null;
|
|
89
97
|
let recipientCount = 0;
|
|
90
98
|
const filter = params.filter ?? { status: ['active'] };
|
|
@@ -97,13 +105,15 @@ async function fanOutToSubscribers(queue, db, sendsDb, options, params) {
|
|
|
97
105
|
const recipients = [];
|
|
98
106
|
for (const sub of batch) {
|
|
99
107
|
const trackingId = ulid();
|
|
100
|
-
const unsubscribeToken = await generateToken(
|
|
108
|
+
const unsubscribeToken = await generateToken(signingSecret, {
|
|
101
109
|
subscriberId: sub.id,
|
|
102
110
|
action: 'unsubscribe',
|
|
111
|
+
host: siteHost(options.siteUrl),
|
|
103
112
|
});
|
|
104
|
-
const preferencesToken = await generateToken(
|
|
113
|
+
const preferencesToken = await generateToken(signingSecret, {
|
|
105
114
|
subscriberId: sub.id,
|
|
106
115
|
action: 'preferences',
|
|
116
|
+
host: siteHost(options.siteUrl),
|
|
107
117
|
});
|
|
108
118
|
await sendsDb.insert(emailSends).values({
|
|
109
119
|
id: ulid(),
|
|
@@ -144,6 +154,7 @@ async function fanOutToSubscribers(queue, db, sendsDb, options, params) {
|
|
|
144
154
|
// ─── sendCampaign ───
|
|
145
155
|
export async function sendCampaign(env, options, params) {
|
|
146
156
|
const campaignId = params.campaignId ?? ulid();
|
|
157
|
+
const signingSecret = await requireSigningSecret(env, options);
|
|
147
158
|
const { db, sendsDb, queue } = resolveProducerBindings(env, options);
|
|
148
159
|
// Render HTML
|
|
149
160
|
let html;
|
|
@@ -168,7 +179,7 @@ export async function sendCampaign(env, options, params) {
|
|
|
168
179
|
const trackClickUrl = `${options.siteUrl}${options.trackClickPath}`;
|
|
169
180
|
html = injectTrackingPixel(html, trackOpenUrl);
|
|
170
181
|
html = rewriteLinksForTracking(html, trackClickUrl);
|
|
171
|
-
const recipientCount = await fanOutToSubscribers(queue, db, sendsDb, options, {
|
|
182
|
+
const recipientCount = await fanOutToSubscribers(queue, db, sendsDb, options, signingSecret, {
|
|
172
183
|
subject: params.subject,
|
|
173
184
|
html,
|
|
174
185
|
campaignId,
|
|
@@ -180,6 +191,7 @@ export async function sendCampaign(env, options, params) {
|
|
|
180
191
|
// ─── sendDigest ───
|
|
181
192
|
export async function sendDigest(env, options, params) {
|
|
182
193
|
const campaignId = params.campaignId ?? ulid();
|
|
194
|
+
const signingSecret = await requireSigningSecret(env, options);
|
|
183
195
|
const { db, sendsDb, queue } = resolveProducerBindings(env, options);
|
|
184
196
|
// Render digest items then full template
|
|
185
197
|
const itemsHtml = renderDigestItems(params.items);
|
|
@@ -197,7 +209,7 @@ export async function sendDigest(env, options, params) {
|
|
|
197
209
|
const trackClickUrl = `${options.siteUrl}${options.trackClickPath}`;
|
|
198
210
|
html = injectTrackingPixel(html, trackOpenUrl);
|
|
199
211
|
html = rewriteLinksForTracking(html, trackClickUrl);
|
|
200
|
-
const recipientCount = await fanOutToSubscribers(queue, db, sendsDb, options, {
|
|
212
|
+
const recipientCount = await fanOutToSubscribers(queue, db, sendsDb, options, signingSecret, {
|
|
201
213
|
subject: params.subject,
|
|
202
214
|
html,
|
|
203
215
|
campaignId,
|
package/dist/utils/send.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"send.js","sourceRoot":"","sources":["../../src/utils/send.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAChC,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAE5B,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAS/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AAC5E,OAAO,EAAE,yBAAyB,EAAE,MAAM,WAAW,CAAA;AAsBrD,SAAS,uBAAuB,CAC/B,GAAc,EACd,OAA8B;IAE9B,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,SAAS,CAA2B,CAAA;IAC3D,IAAI,CAAC,EAAE,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CACd,gBAAgB,OAAO,CAAC,SAAS,iBAAiB;YACjD,qFAAqF,CACtF,CAAA;IACF,CAAC;IACD,MAAM,gBAAgB,GAAG,OAAO,CAAC,cAAc,IAAI,OAAO,CAAC,SAAS,CAAA;IACpE,MAAM,OAAO,GAAG,GAAG,CAAC,gBAAgB,CAA2B,CAAA;IAC/D,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CACd,gBAAgB,gBAAgB,iBAAiB;YAChD,0FAA0F,CAC3F,CAAA;IACF,CAAC;IACD,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,YAAY,CAAsB,CAAA;IAC5D,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACd,gBAAgB,OAAO,CAAC,YAAY,iBAAiB;YACpD,wFAAwF,CACzF,CAAA;IACF,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,CAAA;AAC7D,CAAC;AAED,4BAA4B;AAE5B,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACtC,GAAc,EACd,OAA8B,EAC9B,MAOC;IAED,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,uBAAuB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;IAChE,MAAM,UAAU,GAAG,IAAI,EAAE,CAAA;IACzB,MAAM,YAAY,GAAG,MAAM,CAAC,YAAY,IAAI,IAAI,EAAE,CAAA;IAElD,cAAc;IACd,IAAI,IAAY,CAAA;IAChB,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACrB,MAAM,UAAU,GAAG,MAAM,yBAAyB,CAAC,OAAO,EAAE,MAAM,CAAC,YAAY,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"send.js","sourceRoot":"","sources":["../../src/utils/send.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/tokens.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
interface TokenPayload {
|
|
2
2
|
subscriberId: string;
|
|
3
3
|
action: 'confirm' | 'unsubscribe' | 'preferences';
|
|
4
|
+
host: string;
|
|
4
5
|
exp?: number;
|
|
5
6
|
}
|
|
6
7
|
export declare function generateToken(secret: string, payload: TokenPayload): Promise<string>;
|
|
7
|
-
export declare function verifyToken(secret: string, token: string): Promise<{
|
|
8
|
+
export declare function verifyToken(secret: string, token: string, expectedHost: string): Promise<{
|
|
8
9
|
subscriberId: string;
|
|
9
10
|
action: string;
|
|
11
|
+
host: string;
|
|
10
12
|
exp?: number;
|
|
11
13
|
} | null>;
|
|
12
14
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/utils/tokens.ts"],"names":[],"mappings":"AA0CA,UAAU,YAAY;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,SAAS,GAAG,aAAa,GAAG,aAAa,CAAA;IACjD,GAAG,CAAC,EAAE,MAAM,CAAA;CACZ;AAID,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/utils/tokens.ts"],"names":[],"mappings":"AA0CA,UAAU,YAAY;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,SAAS,GAAG,aAAa,GAAG,aAAa,CAAA;IACjD,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;CACZ;AAID,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAc1F;AAED,wBAAsB,WAAW,CAChC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,GAClB,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA8BtF"}
|
package/dist/utils/tokens.js
CHANGED
|
@@ -30,6 +30,9 @@ async function hmacVerify(secret, data, signature) {
|
|
|
30
30
|
}
|
|
31
31
|
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
32
32
|
export async function generateToken(secret, payload) {
|
|
33
|
+
if (typeof payload.host !== 'string' || !payload.host.trim()) {
|
|
34
|
+
throw new Error('GL_MAILER_TOKEN_HOST_REQUIRED');
|
|
35
|
+
}
|
|
33
36
|
const finalPayload = { ...payload };
|
|
34
37
|
if (finalPayload.action === 'confirm' && finalPayload.exp === undefined) {
|
|
35
38
|
finalPayload.exp = Date.now() + SEVEN_DAYS_MS;
|
|
@@ -38,7 +41,9 @@ export async function generateToken(secret, payload) {
|
|
|
38
41
|
const signature = await hmacSign(secret, payloadB64);
|
|
39
42
|
return `${payloadB64}.${signature}`;
|
|
40
43
|
}
|
|
41
|
-
export async function verifyToken(secret, token) {
|
|
44
|
+
export async function verifyToken(secret, token, expectedHost) {
|
|
45
|
+
if (typeof expectedHost !== 'string' || !expectedHost.trim())
|
|
46
|
+
return null;
|
|
42
47
|
const dotIndex = token.indexOf('.');
|
|
43
48
|
if (dotIndex === -1)
|
|
44
49
|
return null;
|
|
@@ -46,14 +51,18 @@ export async function verifyToken(secret, token) {
|
|
|
46
51
|
const signature = token.slice(dotIndex + 1);
|
|
47
52
|
if (!payloadB64 || !signature)
|
|
48
53
|
return null;
|
|
49
|
-
const valid = await hmacVerify(secret, payloadB64, signature);
|
|
50
|
-
if (!valid)
|
|
51
|
-
return null;
|
|
52
54
|
try {
|
|
55
|
+
const valid = await hmacVerify(secret, payloadB64, signature);
|
|
56
|
+
if (!valid)
|
|
57
|
+
return null;
|
|
53
58
|
const decoded = JSON.parse(new TextDecoder().decode(fromBase64Url(payloadB64)));
|
|
54
59
|
if (decoded.exp !== undefined && decoded.exp < Date.now())
|
|
55
60
|
return null;
|
|
56
|
-
|
|
61
|
+
if (typeof decoded.host !== 'string' || !decoded.host.trim())
|
|
62
|
+
return null;
|
|
63
|
+
if (decoded.host !== expectedHost)
|
|
64
|
+
return null;
|
|
65
|
+
return { ...decoded, host: decoded.host };
|
|
57
66
|
}
|
|
58
67
|
catch {
|
|
59
68
|
return null;
|
package/dist/utils/tokens.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/utils/tokens.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAA;AAEjC,SAAS,WAAW,CAAC,MAAgC;IACpD,MAAM,KAAK,GAAG,MAAM,YAAY,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAA;IAC5E,OAAO,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,KAAK,CAAC,CAAC;SACxC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;AACrB,CAAC;AAED,SAAS,aAAa,CAAC,GAAW;IACjC,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IACjG,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAA;IAC3B,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;IAChC,CAAC;IACD,OAAO,KAAK,CAAA;AACb,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,MAAc;IACtC,OAAO,MAAM,CAAC,MAAM,CAAC,SAAS,CAC7B,KAAK,EACL,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EACtB,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,EACjC,KAAK,EACL,CAAC,MAAM,EAAE,QAAQ,CAAC,CAClB,CAAA;AACF,CAAC;AAED,KAAK,UAAU,QAAQ,CAAC,MAAc,EAAE,IAAY;IACnD,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,CAAA;IACnC,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;IAC7E,OAAO,WAAW,CAAC,SAAS,CAAC,CAAA;AAC9B,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,MAAc,EAAE,IAAY,EAAE,SAAiB;IACxE,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,CAAA;IACnC,MAAM,QAAQ,GAAG,aAAa,CAAC,SAAS,CAAC,CAAA;IACzC,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,QAAQ,CAAC,MAAqB,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;AAC/F,CAAC;
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/utils/tokens.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/urls.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ResolvedMailerOptions } from '../options.js';
|
|
2
2
|
export declare function buildSiteUrl(siteUrl: string, path: string, searchParams?: Record<string, string | boolean | undefined>): string;
|
|
3
|
-
export declare function buildSubscriberManageUrls(options: Pick<ResolvedMailerOptions, 'preferencesPath' | 'siteUrl' | '
|
|
3
|
+
export declare function buildSubscriberManageUrls(options: Pick<ResolvedMailerOptions, 'preferencesPath' | 'siteUrl' | 'unsubscribePath'>, subscriberId?: string, signingSecret?: string): Promise<{
|
|
4
4
|
preferencesUrl: string;
|
|
5
5
|
unsubscribeUrl: string;
|
|
6
6
|
}>;
|
package/dist/utils/urls.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"urls.d.ts","sourceRoot":"","sources":["../../src/utils/urls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"urls.d.ts","sourceRoot":"","sources":["../../src/utils/urls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAI1D,wBAAgB,YAAY,CAC3B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,GACzD,MAAM,CASR;AAED,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,IAAI,CAAC,qBAAqB,EAAE,iBAAiB,GAAG,SAAS,GAAG,iBAAiB,CAAC,EACvF,YAAY,CAAC,EAAE,MAAM,EACrB,aAAa,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAAC,CA4B7D"}
|
package/dist/utils/urls.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { siteHost } from './runtime-secrets.js';
|
|
1
2
|
import { generateToken } from './tokens.js';
|
|
2
3
|
export function buildSiteUrl(siteUrl, path, searchParams) {
|
|
3
4
|
const url = new URL(path, siteUrl);
|
|
@@ -8,18 +9,23 @@ export function buildSiteUrl(siteUrl, path, searchParams) {
|
|
|
8
9
|
}
|
|
9
10
|
return url.toString();
|
|
10
11
|
}
|
|
11
|
-
export async function buildSubscriberManageUrls(options, subscriberId) {
|
|
12
|
+
export async function buildSubscriberManageUrls(options, subscriberId, signingSecret) {
|
|
12
13
|
if (!subscriberId) {
|
|
13
14
|
return { preferencesUrl: '', unsubscribeUrl: '' };
|
|
14
15
|
}
|
|
16
|
+
if (!signingSecret)
|
|
17
|
+
throw new Error('GL_MAILER_SIGNING_UNCONFIGURED');
|
|
18
|
+
const host = siteHost(options.siteUrl);
|
|
15
19
|
const [unsubscribeToken, preferencesToken] = await Promise.all([
|
|
16
|
-
generateToken(
|
|
20
|
+
generateToken(signingSecret, {
|
|
17
21
|
subscriberId,
|
|
18
22
|
action: 'unsubscribe',
|
|
23
|
+
host,
|
|
19
24
|
}),
|
|
20
|
-
generateToken(
|
|
25
|
+
generateToken(signingSecret, {
|
|
21
26
|
subscriberId,
|
|
22
27
|
action: 'preferences',
|
|
28
|
+
host,
|
|
23
29
|
}),
|
|
24
30
|
]);
|
|
25
31
|
return {
|
package/dist/utils/urls.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"urls.js","sourceRoot":"","sources":["../../src/utils/urls.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3C,MAAM,UAAU,YAAY,CAC3B,OAAe,EACf,IAAY,EACZ,YAA2D;IAE3D,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IAElC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC,EAAE,CAAC;QAC/D,IAAI,KAAK,KAAK,SAAS;YAAE,SAAQ;QACjC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;IAC9E,CAAC;IAED,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAA;AACtB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC9C,
|
|
1
|
+
{"version":3,"file":"urls.js","sourceRoot":"","sources":["../../src/utils/urls.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3C,MAAM,UAAU,YAAY,CAC3B,OAAe,EACf,IAAY,EACZ,YAA2D;IAE3D,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IAElC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC,EAAE,CAAC;QAC/D,IAAI,KAAK,KAAK,SAAS;YAAE,SAAQ;QACjC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;IAC9E,CAAC;IAED,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAA;AACtB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC9C,OAAuF,EACvF,YAAqB,EACrB,aAAsB;IAEtB,IAAI,CAAC,YAAY,EAAE,CAAC;QACnB,OAAO,EAAE,cAAc,EAAE,EAAE,EAAE,cAAc,EAAE,EAAE,EAAE,CAAA;IAClD,CAAC;IAED,IAAI,CAAC,aAAa;QAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAA;IACrE,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;IACtC,MAAM,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC9D,aAAa,CAAC,aAAa,EAAE;YAC5B,YAAY;YACZ,MAAM,EAAE,aAAa;YACrB,IAAI;SACJ,CAAC;QACF,aAAa,CAAC,aAAa,EAAE;YAC5B,YAAY;YACZ,MAAM,EAAE,aAAa;YACrB,IAAI;SACJ,CAAC;KACF,CAAC,CAAA;IAEF,OAAO;QACN,cAAc,EAAE,YAAY,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,eAAe,EAAE;YACtE,KAAK,EAAE,gBAAgB;SACvB,CAAC;QACF,cAAc,EAAE,YAAY,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,eAAe,EAAE;YACtE,KAAK,EAAE,gBAAgB;SACvB,CAAC;KACF,CAAA;AACF,CAAC"}
|
package/dist/vite-plugin.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
2
|
import type { ResolvedMailerOptions } from './options.js';
|
|
3
|
+
export declare function renderMailerConfigModule(config: ResolvedMailerOptions, command: 'build' | 'serve'): string;
|
|
3
4
|
export declare function growthLabsMailerPlugin(config: ResolvedMailerOptions): Plugin;
|
|
4
5
|
//# 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,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,
|
|
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"}
|
package/dist/vite-plugin.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
const VIRTUAL_MODULE_ID = 'virtual:growth-labs/mailer/config';
|
|
2
2
|
const RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
|
|
3
|
+
export function renderMailerConfigModule(config, command) {
|
|
4
|
+
const serializableConfig = { ...config };
|
|
5
|
+
if (command !== 'serve') {
|
|
6
|
+
delete serializableConfig.turnstileSecretKey;
|
|
7
|
+
delete serializableConfig.signingSecret;
|
|
8
|
+
}
|
|
9
|
+
return `export const config = ${JSON.stringify(serializableConfig)};`;
|
|
10
|
+
}
|
|
3
11
|
export function growthLabsMailerPlugin(config) {
|
|
12
|
+
let command = 'build';
|
|
4
13
|
return {
|
|
5
14
|
name: 'growth-labs-mailer-config',
|
|
15
|
+
configResolved(resolvedConfig) {
|
|
16
|
+
command = resolvedConfig.command;
|
|
17
|
+
},
|
|
6
18
|
resolveId(id) {
|
|
7
19
|
if (id === VIRTUAL_MODULE_ID) {
|
|
8
20
|
return RESOLVED_VIRTUAL_MODULE_ID;
|
|
@@ -10,7 +22,7 @@ export function growthLabsMailerPlugin(config) {
|
|
|
10
22
|
},
|
|
11
23
|
load(id) {
|
|
12
24
|
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
|
|
13
|
-
return
|
|
25
|
+
return renderMailerConfigModule(config, command);
|
|
14
26
|
}
|
|
15
27
|
},
|
|
16
28
|
};
|
package/dist/vite-plugin.js.map
CHANGED
|
@@ -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,sBAAsB,CAAC,MAA6B;IACnE,OAAO;QACN,IAAI,EAAE,2BAA2B;QACjC,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,
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growth-labs/mailer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"registry": "https://registry.npmjs.org/"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
|
-
"astro": "^6.1.10"
|
|
74
|
+
"astro": "^6.1.10 || ^7.0.0"
|
|
75
75
|
},
|
|
76
76
|
"peerDependenciesMeta": {
|
|
77
77
|
"@growth-labs/analytics": {
|
|
@@ -85,10 +85,10 @@
|
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
87
|
"@cloudflare/workers-types": "^4.20260402.1",
|
|
88
|
-
"astro": "^6.
|
|
88
|
+
"astro": "^6.4.6",
|
|
89
89
|
"drizzle-kit": "^0.30.0",
|
|
90
90
|
"typescript": "^5.7.0",
|
|
91
|
-
"vite": "^7.3.
|
|
91
|
+
"vite": "^7.3.5",
|
|
92
92
|
"vitest": "^4.1.8"
|
|
93
93
|
},
|
|
94
94
|
"scripts": {
|
package/src/options.ts
CHANGED
|
@@ -25,14 +25,16 @@ export const mailerOptionsSchema = z.object({
|
|
|
25
25
|
|
|
26
26
|
// ─── Turnstile (subscribe form) ───
|
|
27
27
|
turnstileSiteKey: z.string(),
|
|
28
|
-
|
|
28
|
+
turnstileSecretBinding: z.string().min(1).default('TURNSTILE_SECRET_KEY'),
|
|
29
|
+
turnstileSecretKey: z.string().optional(),
|
|
29
30
|
|
|
30
31
|
// ─── Subscriber lifecycle ───
|
|
31
32
|
doubleOptIn: z.boolean().default(true),
|
|
32
33
|
topics: z.array(z.string()).optional(),
|
|
33
34
|
|
|
34
35
|
// ─── Token signing ───
|
|
35
|
-
|
|
36
|
+
signingSecretBinding: z.string().min(1).default('MAILER_SIGNING_SECRET'),
|
|
37
|
+
signingSecret: z.string().optional(),
|
|
36
38
|
|
|
37
39
|
// ─── Routes ───
|
|
38
40
|
subscribePath: z.string().default('/api/newsletter/subscribe'),
|
package/src/routes/confirm.ts
CHANGED
|
@@ -4,6 +4,11 @@ import type { APIRoute } from 'astro'
|
|
|
4
4
|
import { drizzle } from 'drizzle-orm/d1'
|
|
5
5
|
import { probeMailerSchema, schemaMissingResponse } from '../_internal/schema-probe.js'
|
|
6
6
|
import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
|
|
7
|
+
import {
|
|
8
|
+
resolveSigningSecret,
|
|
9
|
+
runtimeSecretErrorResponse,
|
|
10
|
+
siteHost,
|
|
11
|
+
} from '../utils/runtime-secrets.js'
|
|
7
12
|
import { sendTransactional } from '../utils/send.js'
|
|
8
13
|
import { confirmSubscriber, getSubscriberById } from '../utils/subscribers.js'
|
|
9
14
|
import { verifyToken } from '../utils/tokens.js'
|
|
@@ -14,15 +19,17 @@ export const GET: APIRoute = async (context) => {
|
|
|
14
19
|
if (!token) {
|
|
15
20
|
return Response.json({ error: 'Missing token' }, { status: 400 })
|
|
16
21
|
}
|
|
22
|
+
const bindingsEnv = cloudflareEnv as Record<string, unknown>
|
|
23
|
+
const signingSecret = await resolveSigningSecret(bindingsEnv, config, url.toString())
|
|
24
|
+
if (!signingSecret.ok) return runtimeSecretErrorResponse(signingSecret)
|
|
17
25
|
|
|
18
26
|
// Verify token
|
|
19
|
-
const payload = await verifyToken(
|
|
27
|
+
const payload = await verifyToken(signingSecret.secret, token, siteHost(config.siteUrl))
|
|
20
28
|
if (!payload || payload.action !== 'confirm') {
|
|
21
29
|
return Response.json({ error: 'Invalid or expired token' }, { status: 400 })
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
// Resolve bindings
|
|
25
|
-
const bindingsEnv = cloudflareEnv as Record<string, unknown>
|
|
26
33
|
const d1 = bindingsEnv[config.d1Binding] as D1Database
|
|
27
34
|
|
|
28
35
|
// Schema probe — return 503 with GL_MAILER_SCHEMA_MISSING on miss.
|
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
unsubscribeSubscriber,
|
|
9
9
|
updatePreferences,
|
|
10
10
|
} from "../utils/subscribers.js";
|
|
11
|
+
import {
|
|
12
|
+
resolveSigningSecret,
|
|
13
|
+
runtimeSecretErrorResponse,
|
|
14
|
+
siteHost,
|
|
15
|
+
} from "../utils/runtime-secrets.js";
|
|
11
16
|
import { generateToken, verifyToken } from "../utils/tokens.js";
|
|
12
17
|
import { buildSiteUrl } from "../utils/urls.js";
|
|
13
18
|
|
|
@@ -27,15 +32,20 @@ if (!token) {
|
|
|
27
32
|
return new Response("Missing token", { status: 400 });
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
const
|
|
35
|
+
const bindingsEnv = cloudflareEnv as Record<string, unknown>;
|
|
36
|
+
const signingSecret = await resolveSigningSecret(bindingsEnv, config, Astro.request.url);
|
|
37
|
+
if (!signingSecret.ok) return runtimeSecretErrorResponse(signingSecret);
|
|
38
|
+
|
|
39
|
+
const payload = await verifyToken(signingSecret.secret, token, siteHost(config.siteUrl));
|
|
31
40
|
if (!payload || !["preferences", "unsubscribe"].includes(payload.action)) {
|
|
32
41
|
return new Response("Invalid or expired token", { status: 400 });
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
if (Astro.request.method === "GET" && payload.action === "unsubscribe") {
|
|
36
|
-
const preferencesToken = await generateToken(
|
|
45
|
+
const preferencesToken = await generateToken(signingSecret.secret, {
|
|
37
46
|
subscriberId: payload.subscriberId,
|
|
38
47
|
action: "preferences",
|
|
48
|
+
host: siteHost(config.siteUrl),
|
|
39
49
|
});
|
|
40
50
|
|
|
41
51
|
return Astro.redirect(
|
|
@@ -51,7 +61,6 @@ if (Astro.request.method === "POST" && payload.action !== "preferences") {
|
|
|
51
61
|
return new Response("Invalid or expired token", { status: 400 });
|
|
52
62
|
}
|
|
53
63
|
|
|
54
|
-
const bindingsEnv = cloudflareEnv as Record<string, unknown>;
|
|
55
64
|
const d1 = bindingsEnv[config.d1Binding] as D1Database;
|
|
56
65
|
if (!d1) {
|
|
57
66
|
return new Response("D1 binding not available", { status: 500 });
|
package/src/routes/subscribe.ts
CHANGED
|
@@ -4,6 +4,12 @@ import type { APIRoute } from 'astro'
|
|
|
4
4
|
import { drizzle } from 'drizzle-orm/d1'
|
|
5
5
|
import { probeMailerSchema, schemaMissingResponse } from '../_internal/schema-probe.js'
|
|
6
6
|
import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
|
|
7
|
+
import {
|
|
8
|
+
resolveSigningSecret,
|
|
9
|
+
resolveTurnstileSecret,
|
|
10
|
+
runtimeSecretErrorResponse,
|
|
11
|
+
siteHost,
|
|
12
|
+
} from '../utils/runtime-secrets.js'
|
|
7
13
|
import { sendTransactional } from '../utils/send.js'
|
|
8
14
|
import { createSubscriber } from '../utils/subscribers.js'
|
|
9
15
|
import { generateToken } from '../utils/tokens.js'
|
|
@@ -29,6 +35,12 @@ export const POST: APIRoute = async (context) => {
|
|
|
29
35
|
return Response.json({ error: 'Invalid email format' }, { status: 400 })
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
const bindingsEnv = cloudflareEnv as Record<string, unknown>
|
|
39
|
+
const turnstileSecret = await resolveTurnstileSecret(bindingsEnv, config, request.url)
|
|
40
|
+
if (!turnstileSecret.ok) return runtimeSecretErrorResponse(turnstileSecret)
|
|
41
|
+
const signingSecret = await resolveSigningSecret(bindingsEnv, config, request.url)
|
|
42
|
+
if (!signingSecret.ok) return runtimeSecretErrorResponse(signingSecret)
|
|
43
|
+
|
|
32
44
|
// 4. Validate Turnstile token
|
|
33
45
|
const turnstileResponse = await fetch(
|
|
34
46
|
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
|
|
@@ -36,7 +48,7 @@ export const POST: APIRoute = async (context) => {
|
|
|
36
48
|
method: 'POST',
|
|
37
49
|
headers: { 'Content-Type': 'application/json' },
|
|
38
50
|
body: JSON.stringify({
|
|
39
|
-
secret:
|
|
51
|
+
secret: turnstileSecret.secret,
|
|
40
52
|
response: body.turnstileToken,
|
|
41
53
|
remoteip: request.headers.get('cf-connecting-ip') ?? undefined,
|
|
42
54
|
}),
|
|
@@ -50,7 +62,6 @@ export const POST: APIRoute = async (context) => {
|
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
// 5. Resolve bindings
|
|
53
|
-
const bindingsEnv = cloudflareEnv as Record<string, unknown>
|
|
54
65
|
const d1 = bindingsEnv[config.d1Binding] as D1Database
|
|
55
66
|
|
|
56
67
|
// 5a. Schema probe — runs once per Worker instance. On miss, return 503
|
|
@@ -72,9 +83,10 @@ export const POST: APIRoute = async (context) => {
|
|
|
72
83
|
|
|
73
84
|
// 7. Send confirmation or welcome email
|
|
74
85
|
if (config.doubleOptIn && (isNew || subscriber.status === 'pending')) {
|
|
75
|
-
const confirmToken = await generateToken(
|
|
86
|
+
const confirmToken = await generateToken(signingSecret.secret, {
|
|
76
87
|
subscriberId: subscriber.id,
|
|
77
88
|
action: 'confirm',
|
|
89
|
+
host: siteHost(config.siteUrl),
|
|
78
90
|
})
|
|
79
91
|
const confirmUrl = `${config.siteUrl}${config.confirmPath}?token=${confirmToken}`
|
|
80
92
|
|
|
@@ -4,6 +4,11 @@ import type { APIRoute } from 'astro'
|
|
|
4
4
|
import { drizzle } from 'drizzle-orm/d1'
|
|
5
5
|
import { probeMailerSchema, schemaMissingResponse } from '../_internal/schema-probe.js'
|
|
6
6
|
import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
|
|
7
|
+
import {
|
|
8
|
+
resolveSigningSecret,
|
|
9
|
+
runtimeSecretErrorResponse,
|
|
10
|
+
siteHost,
|
|
11
|
+
} from '../utils/runtime-secrets.js'
|
|
7
12
|
import { sendTransactional } from '../utils/send.js'
|
|
8
13
|
import { getSubscriberById, unsubscribeSubscriber } from '../utils/subscribers.js'
|
|
9
14
|
import { generateToken, verifyToken } from '../utils/tokens.js'
|
|
@@ -14,14 +19,18 @@ async function processUnsubscribe(
|
|
|
14
19
|
context?: Parameters<APIRoute>[0],
|
|
15
20
|
request?: Request,
|
|
16
21
|
) {
|
|
22
|
+
const bindingsEnv = cloudflareEnv as Record<string, unknown>
|
|
23
|
+
const requestUrl = request?.url ?? context?.url.toString() ?? config.siteUrl
|
|
24
|
+
const signingSecret = await resolveSigningSecret(bindingsEnv, config, requestUrl)
|
|
25
|
+
if (!signingSecret.ok) return { secretFailure: signingSecret }
|
|
26
|
+
|
|
17
27
|
// Verify token
|
|
18
|
-
const payload = await verifyToken(
|
|
28
|
+
const payload = await verifyToken(signingSecret.secret, token, siteHost(config.siteUrl))
|
|
19
29
|
if (!payload || payload.action !== 'unsubscribe') {
|
|
20
30
|
return { error: 'Invalid or expired token', status: 400 }
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
// Resolve bindings
|
|
24
|
-
const bindingsEnv = cloudflareEnv as Record<string, unknown>
|
|
25
34
|
const d1 = bindingsEnv[config.d1Binding] as D1Database
|
|
26
35
|
|
|
27
36
|
// Schema probe — short-circuit with a Response on miss; both GET and POST
|
|
@@ -66,6 +75,7 @@ async function processUnsubscribe(
|
|
|
66
75
|
return {
|
|
67
76
|
success: true,
|
|
68
77
|
subscriberId: payload.subscriberId,
|
|
78
|
+
signingSecret: signingSecret.secret,
|
|
69
79
|
}
|
|
70
80
|
}
|
|
71
81
|
|
|
@@ -78,15 +88,17 @@ export const GET: APIRoute = async (context) => {
|
|
|
78
88
|
}
|
|
79
89
|
|
|
80
90
|
const result = await processUnsubscribe(token, context, request)
|
|
91
|
+
if (result.secretFailure) return runtimeSecretErrorResponse(result.secretFailure)
|
|
81
92
|
if ('schemaMissing' in result) return schemaMissingResponse()
|
|
82
93
|
if ('error' in result) {
|
|
83
94
|
return Response.json({ error: result.error }, { status: result.status })
|
|
84
95
|
}
|
|
85
96
|
|
|
86
97
|
// Generate preferences token for the redirect
|
|
87
|
-
const preferencesToken = await generateToken(
|
|
98
|
+
const preferencesToken = await generateToken(result.signingSecret, {
|
|
88
99
|
subscriberId: result.subscriberId,
|
|
89
100
|
action: 'preferences',
|
|
101
|
+
host: siteHost(config.siteUrl),
|
|
90
102
|
})
|
|
91
103
|
const preferencesUrl = buildSiteUrl(config.siteUrl, config.preferencesPath, {
|
|
92
104
|
token: preferencesToken,
|
|
@@ -111,6 +123,7 @@ export const POST: APIRoute = async (context) => {
|
|
|
111
123
|
}
|
|
112
124
|
|
|
113
125
|
const result = await processUnsubscribe(token, context, request)
|
|
126
|
+
if (result.secretFailure) return runtimeSecretErrorResponse(result.secretFailure)
|
|
114
127
|
if ('schemaMissing' in result) return schemaMissingResponse()
|
|
115
128
|
if ('error' in result) {
|
|
116
129
|
return new Response(result.error, { status: result.status })
|
package/src/utils/broadcast.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { emailSends } from '../schema/sends.js'
|
|
|
6
6
|
import { subscribers } from '../schema/subscribers.js'
|
|
7
7
|
import type { EmailQueueMessage, QueueRecipient, SubscriberStatus } from '../types.js'
|
|
8
8
|
import { sleep } from './providers.js'
|
|
9
|
+
import { resolveSigningSecret, siteHost } from './runtime-secrets.js'
|
|
9
10
|
import type { MailerEnv } from './send.js'
|
|
10
11
|
import { generateToken } from './tokens.js'
|
|
11
12
|
import { injectTrackingPixel, rewriteLinksForTracking } from './tracking.js'
|
|
@@ -330,6 +331,10 @@ export async function sendBroadcast(
|
|
|
330
331
|
dryRun: false,
|
|
331
332
|
}
|
|
332
333
|
}
|
|
334
|
+
const signingSecretResult = await resolveSigningSecret(env, options, options.siteUrl)
|
|
335
|
+
if (!signingSecretResult.ok) throw new Error(signingSecretResult.code)
|
|
336
|
+
const signingSecret = signingSecretResult.secret
|
|
337
|
+
const host = siteHost(options.siteUrl)
|
|
333
338
|
|
|
334
339
|
const html = prepareBroadcastHtml(params.html, options)
|
|
335
340
|
const now = new Date().toISOString()
|
|
@@ -339,13 +344,15 @@ export async function sendBroadcast(
|
|
|
339
344
|
for (const recipient of fresh) {
|
|
340
345
|
const trackingId = ulid()
|
|
341
346
|
const [unsubscribeToken, preferencesToken] = await Promise.all([
|
|
342
|
-
generateToken(
|
|
347
|
+
generateToken(signingSecret, {
|
|
343
348
|
subscriberId: recipient.subscriberId,
|
|
344
349
|
action: 'unsubscribe',
|
|
350
|
+
host,
|
|
345
351
|
}),
|
|
346
|
-
generateToken(
|
|
352
|
+
generateToken(signingSecret, {
|
|
347
353
|
subscriberId: recipient.subscriberId,
|
|
348
354
|
action: 'preferences',
|
|
355
|
+
host,
|
|
349
356
|
}),
|
|
350
357
|
])
|
|
351
358
|
|