@growth-labs/mailer 0.1.3

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 (132) hide show
  1. package/README.md +89 -0
  2. package/dist/components/index.d.ts +3 -0
  3. package/dist/components/index.d.ts.map +1 -0
  4. package/dist/components/index.js +3 -0
  5. package/dist/components/index.js.map +1 -0
  6. package/dist/index.d.ts +14 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +65 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/middleware/tracking.d.ts +3 -0
  11. package/dist/middleware/tracking.d.ts.map +1 -0
  12. package/dist/middleware/tracking.js +13 -0
  13. package/dist/middleware/tracking.js.map +1 -0
  14. package/dist/options.d.ts +160 -0
  15. package/dist/options.d.ts.map +1 -0
  16. package/dist/options.js +51 -0
  17. package/dist/options.js.map +1 -0
  18. package/dist/queue/consumer.d.ts +8 -0
  19. package/dist/queue/consumer.d.ts.map +1 -0
  20. package/dist/queue/consumer.js +83 -0
  21. package/dist/queue/consumer.js.map +1 -0
  22. package/dist/routes/confirm.d.ts +3 -0
  23. package/dist/routes/confirm.d.ts.map +1 -0
  24. package/dist/routes/confirm.js +59 -0
  25. package/dist/routes/confirm.js.map +1 -0
  26. package/dist/routes/subscribe.d.ts +3 -0
  27. package/dist/routes/subscribe.d.ts.map +1 -0
  28. package/dist/routes/subscribe.js +87 -0
  29. package/dist/routes/subscribe.js.map +1 -0
  30. package/dist/routes/track-click.d.ts +3 -0
  31. package/dist/routes/track-click.d.ts.map +1 -0
  32. package/dist/routes/track-click.js +45 -0
  33. package/dist/routes/track-click.js.map +1 -0
  34. package/dist/routes/track-open.d.ts +3 -0
  35. package/dist/routes/track-open.d.ts.map +1 -0
  36. package/dist/routes/track-open.js +40 -0
  37. package/dist/routes/track-open.js.map +1 -0
  38. package/dist/routes/unsubscribe.d.ts +4 -0
  39. package/dist/routes/unsubscribe.d.ts.map +1 -0
  40. package/dist/routes/unsubscribe.js +81 -0
  41. package/dist/routes/unsubscribe.js.map +1 -0
  42. package/dist/routes/webhook.d.ts +3 -0
  43. package/dist/routes/webhook.d.ts.map +1 -0
  44. package/dist/routes/webhook.js +30 -0
  45. package/dist/routes/webhook.js.map +1 -0
  46. package/dist/schema.d.ts +564 -0
  47. package/dist/schema.d.ts.map +1 -0
  48. package/dist/schema.js +47 -0
  49. package/dist/schema.js.map +1 -0
  50. package/dist/types.d.ts +106 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +3 -0
  53. package/dist/types.js.map +1 -0
  54. package/dist/utils/bindings.d.ts +20 -0
  55. package/dist/utils/bindings.d.ts.map +1 -0
  56. package/dist/utils/bindings.js +19 -0
  57. package/dist/utils/bindings.js.map +1 -0
  58. package/dist/utils/bounce.d.ts +29 -0
  59. package/dist/utils/bounce.d.ts.map +1 -0
  60. package/dist/utils/bounce.js +59 -0
  61. package/dist/utils/bounce.js.map +1 -0
  62. package/dist/utils/index.d.ts +12 -0
  63. package/dist/utils/index.d.ts.map +1 -0
  64. package/dist/utils/index.js +9 -0
  65. package/dist/utils/index.js.map +1 -0
  66. package/dist/utils/providers.d.ts +31 -0
  67. package/dist/utils/providers.d.ts.map +1 -0
  68. package/dist/utils/providers.js +109 -0
  69. package/dist/utils/providers.js.map +1 -0
  70. package/dist/utils/scheduling.d.ts +89 -0
  71. package/dist/utils/scheduling.d.ts.map +1 -0
  72. package/dist/utils/scheduling.js +110 -0
  73. package/dist/utils/scheduling.js.map +1 -0
  74. package/dist/utils/send.d.ts +42 -0
  75. package/dist/utils/send.d.ts.map +1 -0
  76. package/dist/utils/send.js +193 -0
  77. package/dist/utils/send.js.map +1 -0
  78. package/dist/utils/subscribers.d.ts +23 -0
  79. package/dist/utils/subscribers.d.ts.map +1 -0
  80. package/dist/utils/subscribers.js +200 -0
  81. package/dist/utils/subscribers.js.map +1 -0
  82. package/dist/utils/templates.d.ts +16 -0
  83. package/dist/utils/templates.d.ts.map +1 -0
  84. package/dist/utils/templates.js +426 -0
  85. package/dist/utils/templates.js.map +1 -0
  86. package/dist/utils/tokens.d.ts +13 -0
  87. package/dist/utils/tokens.d.ts.map +1 -0
  88. package/dist/utils/tokens.js +62 -0
  89. package/dist/utils/tokens.js.map +1 -0
  90. package/dist/utils/tracking.d.ts +26 -0
  91. package/dist/utils/tracking.d.ts.map +1 -0
  92. package/dist/utils/tracking.js +49 -0
  93. package/dist/utils/tracking.js.map +1 -0
  94. package/dist/utils/urls.d.ts +7 -0
  95. package/dist/utils/urls.d.ts.map +1 -0
  96. package/dist/utils/urls.js +34 -0
  97. package/dist/utils/urls.js.map +1 -0
  98. package/dist/vite-plugin.d.ts +4 -0
  99. package/dist/vite-plugin.d.ts.map +1 -0
  100. package/dist/vite-plugin.js +18 -0
  101. package/dist/vite-plugin.js.map +1 -0
  102. package/package.json +85 -0
  103. package/src/astro.d.ts +4 -0
  104. package/src/components/PreferenceCenter.astro +147 -0
  105. package/src/components/SubscribeForm.astro +161 -0
  106. package/src/components/index.ts +2 -0
  107. package/src/index.ts +101 -0
  108. package/src/middleware/tracking.ts +18 -0
  109. package/src/options.ts +65 -0
  110. package/src/queue/consumer.ts +99 -0
  111. package/src/routes/confirm.ts +68 -0
  112. package/src/routes/preferences.astro +137 -0
  113. package/src/routes/subscribe.ts +107 -0
  114. package/src/routes/track-click.ts +57 -0
  115. package/src/routes/track-open.ts +51 -0
  116. package/src/routes/unsubscribe.ts +96 -0
  117. package/src/routes/webhook.ts +48 -0
  118. package/src/schema.ts +56 -0
  119. package/src/types.ts +145 -0
  120. package/src/utils/bindings.ts +28 -0
  121. package/src/utils/bounce.ts +77 -0
  122. package/src/utils/index.ts +47 -0
  123. package/src/utils/providers.ts +141 -0
  124. package/src/utils/scheduling.ts +188 -0
  125. package/src/utils/send.ts +282 -0
  126. package/src/utils/subscribers.ts +277 -0
  127. package/src/utils/templates.ts +459 -0
  128. package/src/utils/tokens.ts +91 -0
  129. package/src/utils/tracking.ts +58 -0
  130. package/src/utils/urls.ts +49 -0
  131. package/src/virtual.d.ts +32 -0
  132. package/src/vite-plugin.ts +21 -0
@@ -0,0 +1,87 @@
1
+ import { config } from 'virtual:growth-labs/mailer/config';
2
+ import { drizzle } from 'drizzle-orm/d1';
3
+ import { sendTransactional } from '../utils/send.js';
4
+ import { createSubscriber } from '../utils/subscribers.js';
5
+ import { generateToken } from '../utils/tokens.js';
6
+ export const POST = async ({ request, locals }) => {
7
+ // 1. Parse request body
8
+ const body = (await request.json());
9
+ // 2. Validate required fields
10
+ if (!body.email || !body.turnstileToken) {
11
+ return Response.json({ error: 'Missing required fields' }, { status: 400 });
12
+ }
13
+ // 3. Basic email format validation
14
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) {
15
+ return Response.json({ error: 'Invalid email format' }, { status: 400 });
16
+ }
17
+ // 4. Validate Turnstile token
18
+ const turnstileResponse = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
19
+ method: 'POST',
20
+ headers: { 'Content-Type': 'application/json' },
21
+ body: JSON.stringify({
22
+ secret: config.turnstileSecretKey,
23
+ response: body.turnstileToken,
24
+ remoteip: request.headers.get('cf-connecting-ip') ?? undefined,
25
+ }),
26
+ });
27
+ const turnstileResult = (await turnstileResponse.json());
28
+ if (!turnstileResult.success) {
29
+ return Response.json({ error: 'Turnstile verification failed' }, { status: 400 });
30
+ }
31
+ // 5. Resolve bindings
32
+ const runtimeEnv = locals.runtime?.env;
33
+ const d1 = runtimeEnv[config.d1Binding];
34
+ const queue = runtimeEnv[config.queueBinding];
35
+ const db = drizzle(d1);
36
+ const env = { DB: d1, QUEUE: queue };
37
+ // 6. Create subscriber
38
+ try {
39
+ const { subscriber, isNew } = await createSubscriber(db, {
40
+ email: body.email,
41
+ name: body.name,
42
+ source: body.source ?? 'form',
43
+ preferences: body.preferences,
44
+ doubleOptIn: config.doubleOptIn,
45
+ });
46
+ // 7. Send confirmation or welcome email
47
+ if (config.doubleOptIn && (isNew || subscriber.status === 'pending')) {
48
+ const confirmToken = await generateToken(config.signingSecret, {
49
+ subscriberId: subscriber.id,
50
+ action: 'confirm',
51
+ });
52
+ const confirmUrl = `${config.siteUrl}${config.confirmPath}?token=${confirmToken}`;
53
+ await sendTransactional(env, config, {
54
+ to: body.email,
55
+ subscriberId: subscriber.id,
56
+ subject: `Confirm your ${config.senderName} subscription`,
57
+ template: 'confirmation',
58
+ data: {
59
+ name: body.name,
60
+ confirmUrl,
61
+ },
62
+ });
63
+ }
64
+ else if (!config.doubleOptIn && isNew) {
65
+ await sendTransactional(env, config, {
66
+ to: body.email,
67
+ subscriberId: subscriber.id,
68
+ subject: `Welcome to ${config.senderName}`,
69
+ template: 'welcome',
70
+ data: { name: body.name },
71
+ });
72
+ }
73
+ // Return success (always 200 to avoid email enumeration)
74
+ return Response.json({
75
+ success: true,
76
+ requiresConfirmation: config.doubleOptIn,
77
+ });
78
+ }
79
+ catch (err) {
80
+ const message = err instanceof Error ? err.message : String(err);
81
+ if (message.includes('bounced') || message.includes('complained')) {
82
+ return Response.json({ error: 'This email address cannot be subscribed' }, { status: 422 });
83
+ }
84
+ throw err;
85
+ }
86
+ };
87
+ //# sourceMappingURL=subscribe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscribe.js","sourceRoot":"","sources":["../../src/routes/subscribe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { APIRoute } from 'astro';
2
+ export declare const GET: APIRoute;
3
+ //# sourceMappingURL=track-click.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"track-click.d.ts","sourceRoot":"","sources":["../../src/routes/track-click.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAKrC,eAAO,MAAM,GAAG,EAAE,QAkDjB,CAAA"}
@@ -0,0 +1,45 @@
1
+ import { config } from 'virtual:growth-labs/mailer/config';
2
+ import { and, eq, inArray } from 'drizzle-orm';
3
+ import { drizzle } from 'drizzle-orm/d1';
4
+ import { emailSends } from '../schema.js';
5
+ export const GET = async ({ params, url, locals }) => {
6
+ const trackingId = params.trackingId;
7
+ const destination = url.searchParams.get('url');
8
+ if (!trackingId || !destination) {
9
+ return new Response('Bad request', { status: 400 });
10
+ }
11
+ // Validate: must be http or https (prevent javascript:, data:, etc.)
12
+ try {
13
+ const destUrl = new URL(destination);
14
+ if (!['http:', 'https:'].includes(destUrl.protocol)) {
15
+ return new Response('Invalid redirect', { status: 400 });
16
+ }
17
+ }
18
+ catch {
19
+ return new Response('Invalid URL', { status: 400 });
20
+ }
21
+ // Update status to 'clicked' only when it hasn't already reached 'clicked'
22
+ try {
23
+ const runtimeEnv = locals.runtime?.env;
24
+ if (runtimeEnv) {
25
+ const d1 = runtimeEnv[config.d1Binding];
26
+ const db = drizzle(d1);
27
+ await db
28
+ .update(emailSends)
29
+ .set({
30
+ status: 'clicked',
31
+ clickedAt: new Date().toISOString(),
32
+ })
33
+ .where(and(eq(emailSends.trackingId, trackingId), inArray(emailSends.status, ['sent', 'delivered', 'opened'])));
34
+ }
35
+ }
36
+ catch {
37
+ // Never fail the redirect on DB errors
38
+ }
39
+ // 302 redirect to original destination
40
+ return new Response(null, {
41
+ status: 302,
42
+ headers: { Location: destination },
43
+ });
44
+ };
45
+ //# sourceMappingURL=track-click.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"track-click.js","sourceRoot":"","sources":["../../src/routes/track-click.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAE1D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAEzC,MAAM,CAAC,MAAM,GAAG,GAAa,KAAK,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE;IAC9D,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAA;IACpC,MAAM,WAAW,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IAE/C,IAAI,CAAC,UAAU,IAAI,CAAC,WAAW,EAAE,CAAC;QACjC,OAAO,IAAI,QAAQ,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACpD,CAAC;IAED,qEAAqE;IACrE,IAAI,CAAC;QACJ,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAA;QACpC,IAAI,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrD,OAAO,IAAI,QAAQ,CAAC,kBAAkB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QACzD,CAAC;IACF,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,QAAQ,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACpD,CAAC;IAED,2EAA2E;IAC3E,IAAI,CAAC;QACJ,MAAM,UAAU,GACf,MAGA,CAAC,OAAO,EAAE,GAAG,CAAA;QACd,IAAI,UAAU,EAAE,CAAC;YAChB,MAAM,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC,SAAS,CAAe,CAAA;YACrD,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAAA;YACtB,MAAM,EAAE;iBACN,MAAM,CAAC,UAAU,CAAC;iBAClB,GAAG,CAAC;gBACJ,MAAM,EAAE,SAAS;gBACjB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACnC,CAAC;iBACD,KAAK,CACL,GAAG,CACF,EAAE,CAAC,UAAU,CAAC,UAAU,EAAE,UAAU,CAAC,EACrC,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC,CAC3D,CACD,CAAA;QACH,CAAC;IACF,CAAC;IAAC,MAAM,CAAC;QACR,uCAAuC;IACxC,CAAC;IAED,uCAAuC;IACvC,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;QACzB,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE;KAClC,CAAC,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { APIRoute } from 'astro';
2
+ export declare const GET: APIRoute;
3
+ //# sourceMappingURL=track-open.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"track-open.d.ts","sourceRoot":"","sources":["../../src/routes/track-open.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAMrC,eAAO,MAAM,GAAG,EAAE,QA2CjB,CAAA"}
@@ -0,0 +1,40 @@
1
+ import { config } from 'virtual:growth-labs/mailer/config';
2
+ import { and, eq, inArray } from 'drizzle-orm';
3
+ import { drizzle } from 'drizzle-orm/d1';
4
+ import { emailSends } from '../schema.js';
5
+ import { TRANSPARENT_GIF } from '../utils/tracking.js';
6
+ export const GET = async ({ params, locals }) => {
7
+ const trackingId = params.trackingId;
8
+ if (!trackingId) {
9
+ return new Response(null, { status: 400 });
10
+ }
11
+ // Update status to 'opened' only when currently 'sent' or 'delivered'
12
+ // to avoid downgrading from 'clicked'.
13
+ try {
14
+ const runtimeEnv = locals.runtime?.env;
15
+ if (runtimeEnv) {
16
+ const d1 = runtimeEnv[config.d1Binding];
17
+ const db = drizzle(d1);
18
+ await db
19
+ .update(emailSends)
20
+ .set({
21
+ status: 'opened',
22
+ openedAt: new Date().toISOString(),
23
+ })
24
+ .where(and(eq(emailSends.trackingId, trackingId), inArray(emailSends.status, ['sent', 'delivered'])));
25
+ }
26
+ }
27
+ catch {
28
+ // Never fail the pixel response on DB errors
29
+ }
30
+ // 1x1 transparent GIF
31
+ return new Response(TRANSPARENT_GIF, {
32
+ status: 200,
33
+ headers: {
34
+ 'Content-Type': 'image/gif',
35
+ 'Cache-Control': 'no-store, no-cache, must-revalidate',
36
+ 'Content-Length': String(TRANSPARENT_GIF.length),
37
+ },
38
+ });
39
+ };
40
+ //# sourceMappingURL=track-open.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"track-open.js","sourceRoot":"","sources":["../../src/routes/track-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAE1D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAEtD,MAAM,CAAC,MAAM,GAAG,GAAa,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE;IACzD,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAA;IACpC,IAAI,CAAC,UAAU,EAAE,CAAC;QACjB,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC3C,CAAC;IAED,sEAAsE;IACtE,uCAAuC;IACvC,IAAI,CAAC;QACJ,MAAM,UAAU,GACf,MAGA,CAAC,OAAO,EAAE,GAAG,CAAA;QACd,IAAI,UAAU,EAAE,CAAC;YAChB,MAAM,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC,SAAS,CAAe,CAAA;YACrD,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAAA;YACtB,MAAM,EAAE;iBACN,MAAM,CAAC,UAAU,CAAC;iBAClB,GAAG,CAAC;gBACJ,MAAM,EAAE,QAAQ;gBAChB,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aAClC,CAAC;iBACD,KAAK,CACL,GAAG,CACF,EAAE,CAAC,UAAU,CAAC,UAAU,EAAE,UAAU,CAAC,EACrC,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CACjD,CACD,CAAA;QACH,CAAC;IACF,CAAC;IAAC,MAAM,CAAC;QACR,6CAA6C;IAC9C,CAAC;IAED,sBAAsB;IACtB,OAAO,IAAI,QAAQ,CAAC,eAAe,EAAE;QACpC,MAAM,EAAE,GAAG;QACX,OAAO,EAAE;YACR,cAAc,EAAE,WAAW;YAC3B,eAAe,EAAE,qCAAqC;YACtD,gBAAgB,EAAE,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC;SAChD;KACD,CAAC,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,4 @@
1
+ import type { APIRoute } from 'astro';
2
+ export declare const GET: APIRoute;
3
+ export declare const POST: APIRoute;
4
+ //# sourceMappingURL=unsubscribe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unsubscribe.d.ts","sourceRoot":"","sources":["../../src/routes/unsubscribe.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAmDrC,eAAO,MAAM,GAAG,EAAE,QAyBjB,CAAA;AAGD,eAAO,MAAM,IAAI,EAAE,QAelB,CAAA"}
@@ -0,0 +1,81 @@
1
+ import { config } from 'virtual:growth-labs/mailer/config';
2
+ import { drizzle } from 'drizzle-orm/d1';
3
+ import { sendTransactional } from '../utils/send.js';
4
+ import { getSubscriberById, unsubscribeSubscriber } from '../utils/subscribers.js';
5
+ import { generateToken, verifyToken } from '../utils/tokens.js';
6
+ import { buildSiteUrl } from '../utils/urls.js';
7
+ async function processUnsubscribe(locals, token) {
8
+ // Verify token
9
+ const payload = await verifyToken(config.signingSecret, token);
10
+ if (!payload || payload.action !== 'unsubscribe') {
11
+ return { error: 'Invalid or expired token', status: 400 };
12
+ }
13
+ // Resolve bindings
14
+ const runtimeEnv = locals.runtime?.env;
15
+ const d1 = runtimeEnv[config.d1Binding];
16
+ const queue = runtimeEnv[config.queueBinding];
17
+ const db = drizzle(d1);
18
+ const env = { DB: d1, QUEUE: queue };
19
+ // Get subscriber
20
+ const subscriber = await getSubscriberById(db, payload.subscriberId);
21
+ if (!subscriber) {
22
+ return { error: 'Subscriber not found', status: 404 };
23
+ }
24
+ // Unsubscribe
25
+ await unsubscribeSubscriber(db, payload.subscriberId);
26
+ // Send confirmation email
27
+ await sendTransactional(env, config, {
28
+ to: subscriber.email,
29
+ subscriberId: subscriber.id,
30
+ subject: `You've been unsubscribed from ${config.senderName}`,
31
+ template: 'unsubscribe-confirm',
32
+ data: {
33
+ name: subscriber.name,
34
+ resubscribeUrl: buildSiteUrl(config.siteUrl, config.subscribePath),
35
+ },
36
+ });
37
+ return {
38
+ success: true,
39
+ subscriberId: payload.subscriberId,
40
+ };
41
+ }
42
+ // GET: Link from email footer
43
+ export const GET = async ({ url, locals }) => {
44
+ const token = url.searchParams.get('token');
45
+ if (!token) {
46
+ return Response.json({ error: 'Missing token' }, { status: 400 });
47
+ }
48
+ const result = await processUnsubscribe(locals, token);
49
+ if ('error' in result) {
50
+ return Response.json({ error: result.error }, { status: result.status });
51
+ }
52
+ // Generate preferences token for the redirect
53
+ const preferencesToken = await generateToken(config.signingSecret, {
54
+ subscriberId: result.subscriberId,
55
+ action: 'preferences',
56
+ });
57
+ const preferencesUrl = buildSiteUrl(config.siteUrl, config.preferencesPath, {
58
+ token: preferencesToken,
59
+ unsubscribed: true,
60
+ });
61
+ return new Response(null, {
62
+ status: 302,
63
+ headers: { Location: preferencesUrl },
64
+ });
65
+ };
66
+ // POST: RFC 8058 List-Unsubscribe-Post
67
+ export const POST = async ({ request, locals }) => {
68
+ // RFC 8058: body is "List-Unsubscribe=One-Click"
69
+ // Token comes from List-Unsubscribe header URL
70
+ const url = new URL(request.url);
71
+ const token = url.searchParams.get('token');
72
+ if (!token) {
73
+ return new Response('Missing token', { status: 400 });
74
+ }
75
+ const result = await processUnsubscribe(locals, token);
76
+ if ('error' in result) {
77
+ return new Response(result.error, { status: result.status });
78
+ }
79
+ return new Response('OK', { status: 200 });
80
+ };
81
+ //# sourceMappingURL=unsubscribe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unsubscribe.js","sourceRoot":"","sources":["../../src/routes/unsubscribe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { APIRoute } from 'astro';
2
+ export declare const POST: APIRoute;
3
+ //# sourceMappingURL=webhook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/routes/webhook.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAYrC,eAAO,MAAM,IAAI,EAAE,QAkClB,CAAA"}
@@ -0,0 +1,30 @@
1
+ import { config } from 'virtual:growth-labs/mailer/config';
2
+ import { drizzle } from 'drizzle-orm/d1';
3
+ import { handleBounce, handleComplaint, handleDelivery } from '../utils/bounce.js';
4
+ export const POST = async ({ request, locals }) => {
5
+ const body = (await request.json());
6
+ if (!body.type || !body.email) {
7
+ return Response.json({ error: 'Invalid payload' }, { status: 400 });
8
+ }
9
+ const runtimeEnv = locals.runtime?.env;
10
+ if (!runtimeEnv) {
11
+ return Response.json({ error: 'Runtime not available' }, { status: 500 });
12
+ }
13
+ const d1 = runtimeEnv[config.d1Binding];
14
+ const db = drizzle(d1);
15
+ switch (body.type) {
16
+ case 'delivery':
17
+ if (body.trackingId) {
18
+ await handleDelivery(db, body.trackingId);
19
+ }
20
+ break;
21
+ case 'bounce':
22
+ await handleBounce(db, body.email, body.bounceType ?? 'hard');
23
+ break;
24
+ case 'complaint':
25
+ await handleComplaint(db, body.email);
26
+ break;
27
+ }
28
+ return Response.json({ ok: true });
29
+ };
30
+ //# sourceMappingURL=webhook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/routes/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAE1D,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAUlF,MAAM,CAAC,MAAM,IAAI,GAAa,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE;IAC3D,MAAM,IAAI,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,CAAmB,CAAA;IAErD,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QAC/B,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IACpE,CAAC;IAED,MAAM,UAAU,GACf,MAGA,CAAC,OAAO,EAAE,GAAG,CAAA;IACd,IAAI,CAAC,UAAU,EAAE,CAAC;QACjB,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC1E,CAAC;IAED,MAAM,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC,SAAS,CAAe,CAAA;IACrD,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAAA;IAEtB,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,UAAU;YACd,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;gBACrB,MAAM,cAAc,CAAC,EAAE,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;YAC1C,CAAC;YACD,MAAK;QACN,KAAK,QAAQ;YACZ,MAAM,YAAY,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,IAAI,MAAM,CAAC,CAAA;YAC7D,MAAK;QACN,KAAK,WAAW;YACf,MAAM,eAAe,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;YACrC,MAAK;IACP,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;AACnC,CAAC,CAAA"}