@growth-labs/mailer 0.1.3 → 0.2.1
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 +27 -5
- package/dist/options.d.ts +4 -65
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +2 -9
- package/dist/options.js.map +1 -1
- package/dist/queue/consumer.d.ts +1 -0
- package/dist/queue/consumer.d.ts.map +1 -1
- package/dist/queue/consumer.js +23 -15
- package/dist/queue/consumer.js.map +1 -1
- package/dist/routes/confirm.d.ts.map +1 -1
- package/dist/routes/confirm.js +15 -4
- package/dist/routes/confirm.js.map +1 -1
- package/dist/routes/subscribe.d.ts.map +1 -1
- package/dist/routes/subscribe.js +20 -4
- package/dist/routes/subscribe.js.map +1 -1
- package/dist/routes/track-click.d.ts.map +1 -1
- package/dist/routes/track-click.js +13 -4
- package/dist/routes/track-click.js.map +1 -1
- package/dist/routes/track-open.d.ts.map +1 -1
- package/dist/routes/track-open.js +13 -4
- package/dist/routes/track-open.js.map +1 -1
- package/dist/routes/unsubscribe.d.ts.map +1 -1
- package/dist/routes/unsubscribe.js +21 -8
- package/dist/routes/unsubscribe.js.map +1 -1
- package/dist/routes/webhook.d.ts.map +1 -1
- package/dist/routes/webhook.js +28 -5
- package/dist/routes/webhook.js.map +1 -1
- package/dist/utils/analytics.d.ts +24 -0
- package/dist/utils/analytics.d.ts.map +1 -0
- package/dist/utils/analytics.js +74 -0
- package/dist/utils/analytics.js.map +1 -0
- package/dist/utils/bindings.d.ts +4 -10
- package/dist/utils/bindings.d.ts.map +1 -1
- package/dist/utils/bindings.js +7 -7
- package/dist/utils/bindings.js.map +1 -1
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/providers.d.ts +0 -10
- package/dist/utils/providers.d.ts.map +1 -1
- package/dist/utils/providers.js +1 -57
- package/dist/utils/providers.js.map +1 -1
- package/package.json +86 -84
- package/src/cloudflare-workers.d.ts +3 -0
- package/src/options.ts +52 -60
- package/src/queue/consumer.ts +28 -19
- package/src/routes/confirm.ts +16 -5
- package/src/routes/preferences.astro +5 -9
- package/src/routes/subscribe.ts +21 -5
- package/src/routes/track-click.ts +14 -8
- package/src/routes/track-open.ts +14 -8
- package/src/routes/unsubscribe.ts +26 -9
- package/src/routes/webhook.ts +30 -10
- package/src/utils/analytics.ts +118 -0
- package/src/utils/bindings.ts +9 -11
- package/src/utils/index.ts +6 -7
- package/src/utils/providers.ts +1 -68
- package/src/virtual.d.ts +1 -2
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
import { env as cloudflareEnv } from 'cloudflare:workers'
|
|
1
2
|
import { config } from 'virtual:growth-labs/mailer/config'
|
|
2
3
|
import type { APIRoute } from 'astro'
|
|
3
4
|
import { drizzle } from 'drizzle-orm/d1'
|
|
4
|
-
import
|
|
5
|
+
import { emitMailerAnalyticsEvent } from '../utils/analytics.js'
|
|
5
6
|
import type { MailerEnv } from '../utils/send.js'
|
|
6
7
|
import { sendTransactional } from '../utils/send.js'
|
|
7
8
|
import { getSubscriberById, unsubscribeSubscriber } from '../utils/subscribers.js'
|
|
8
9
|
import { generateToken, verifyToken } from '../utils/tokens.js'
|
|
9
10
|
import { buildSiteUrl } from '../utils/urls.js'
|
|
10
11
|
|
|
11
|
-
async function processUnsubscribe(
|
|
12
|
+
async function processUnsubscribe(
|
|
13
|
+
token: string,
|
|
14
|
+
context?: Parameters<APIRoute>[0],
|
|
15
|
+
request?: Request,
|
|
16
|
+
) {
|
|
12
17
|
// Verify token
|
|
13
18
|
const payload = await verifyToken(config.signingSecret, token)
|
|
14
19
|
if (!payload || payload.action !== 'unsubscribe') {
|
|
@@ -16,9 +21,9 @@ async function processUnsubscribe(locals: RuntimeLocals, token: string) {
|
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
// Resolve bindings
|
|
19
|
-
const
|
|
20
|
-
const d1 =
|
|
21
|
-
const queue =
|
|
24
|
+
const bindingsEnv = cloudflareEnv as Record<string, unknown>
|
|
25
|
+
const d1 = bindingsEnv[config.d1Binding] as D1Database
|
|
26
|
+
const queue = bindingsEnv[config.queueBinding] as Queue
|
|
22
27
|
const db = drizzle(d1)
|
|
23
28
|
const env: MailerEnv = { DB: d1, QUEUE: queue }
|
|
24
29
|
|
|
@@ -43,6 +48,16 @@ async function processUnsubscribe(locals: RuntimeLocals, token: string) {
|
|
|
43
48
|
},
|
|
44
49
|
})
|
|
45
50
|
|
|
51
|
+
emitMailerAnalyticsEvent(config, bindingsEnv, 'newsletter_unsubscribed', {
|
|
52
|
+
request,
|
|
53
|
+
context,
|
|
54
|
+
contentSlug: subscriber.id,
|
|
55
|
+
label: {
|
|
56
|
+
subscriberId: subscriber.id,
|
|
57
|
+
email: subscriber.email,
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
46
61
|
return {
|
|
47
62
|
success: true,
|
|
48
63
|
subscriberId: payload.subscriberId,
|
|
@@ -50,13 +65,14 @@ async function processUnsubscribe(locals: RuntimeLocals, token: string) {
|
|
|
50
65
|
}
|
|
51
66
|
|
|
52
67
|
// GET: Link from email footer
|
|
53
|
-
export const GET: APIRoute = async (
|
|
68
|
+
export const GET: APIRoute = async (context) => {
|
|
69
|
+
const { url, request } = context
|
|
54
70
|
const token = url.searchParams.get('token')
|
|
55
71
|
if (!token) {
|
|
56
72
|
return Response.json({ error: 'Missing token' }, { status: 400 })
|
|
57
73
|
}
|
|
58
74
|
|
|
59
|
-
const result = await processUnsubscribe(
|
|
75
|
+
const result = await processUnsubscribe(token, context, request)
|
|
60
76
|
if ('error' in result) {
|
|
61
77
|
return Response.json({ error: result.error }, { status: result.status })
|
|
62
78
|
}
|
|
@@ -78,7 +94,8 @@ export const GET: APIRoute = async ({ url, locals }) => {
|
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
// POST: RFC 8058 List-Unsubscribe-Post
|
|
81
|
-
export const POST: APIRoute = async (
|
|
97
|
+
export const POST: APIRoute = async (context) => {
|
|
98
|
+
const { request } = context
|
|
82
99
|
// RFC 8058: body is "List-Unsubscribe=One-Click"
|
|
83
100
|
// Token comes from List-Unsubscribe header URL
|
|
84
101
|
const url = new URL(request.url)
|
|
@@ -87,7 +104,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
87
104
|
return new Response('Missing token', { status: 400 })
|
|
88
105
|
}
|
|
89
106
|
|
|
90
|
-
const result = await processUnsubscribe(
|
|
107
|
+
const result = await processUnsubscribe(token, context, request)
|
|
91
108
|
if ('error' in result) {
|
|
92
109
|
return new Response(result.error, { status: result.status })
|
|
93
110
|
}
|
package/src/routes/webhook.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { env as cloudflareEnv } from 'cloudflare:workers'
|
|
1
2
|
import { config } from 'virtual:growth-labs/mailer/config'
|
|
2
3
|
import type { APIRoute } from 'astro'
|
|
3
4
|
import { drizzle } from 'drizzle-orm/d1'
|
|
5
|
+
import { emitMailerAnalyticsEvent, type MailerAnalyticsEvent } from '../utils/analytics.js'
|
|
4
6
|
import { handleBounce, handleComplaint, handleDelivery } from '../utils/bounce.js'
|
|
5
7
|
|
|
6
8
|
interface WebhookPayload {
|
|
@@ -11,23 +13,19 @@ interface WebhookPayload {
|
|
|
11
13
|
timestamp: string
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
export const POST: APIRoute = async (
|
|
16
|
+
export const POST: APIRoute = async (context) => {
|
|
17
|
+
const { request } = context
|
|
15
18
|
const body = (await request.json()) as WebhookPayload
|
|
16
19
|
|
|
17
20
|
if (!body.type || !body.email) {
|
|
18
21
|
return Response.json({ error: 'Invalid payload' }, { status: 400 })
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
).runtime?.env
|
|
26
|
-
if (!runtimeEnv) {
|
|
27
|
-
return Response.json({ error: 'Runtime not available' }, { status: 500 })
|
|
24
|
+
const bindingsEnv = cloudflareEnv as Record<string, unknown>
|
|
25
|
+
const d1 = bindingsEnv[config.d1Binding] as D1Database
|
|
26
|
+
if (!d1) {
|
|
27
|
+
return Response.json({ error: 'D1 binding not available' }, { status: 500 })
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
const d1 = runtimeEnv[config.d1Binding] as D1Database
|
|
31
29
|
const db = drizzle(d1)
|
|
32
30
|
|
|
33
31
|
switch (body.type) {
|
|
@@ -44,5 +42,27 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
44
42
|
break
|
|
45
43
|
}
|
|
46
44
|
|
|
45
|
+
emitMailerAnalyticsEvent(config, bindingsEnv, eventForWebhookType(body.type), {
|
|
46
|
+
request,
|
|
47
|
+
context,
|
|
48
|
+
contentSlug: body.trackingId ?? body.email,
|
|
49
|
+
label: {
|
|
50
|
+
email: body.email,
|
|
51
|
+
trackingId: body.trackingId,
|
|
52
|
+
bounceType: body.bounceType,
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
47
56
|
return Response.json({ ok: true })
|
|
48
57
|
}
|
|
58
|
+
|
|
59
|
+
function eventForWebhookType(type: WebhookPayload['type']): MailerAnalyticsEvent {
|
|
60
|
+
switch (type) {
|
|
61
|
+
case 'delivery':
|
|
62
|
+
return 'newsletter_delivered'
|
|
63
|
+
case 'bounce':
|
|
64
|
+
return 'newsletter_bounced'
|
|
65
|
+
case 'complaint':
|
|
66
|
+
return 'newsletter_complained'
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { ResolvedMailerOptions } from '../options.js'
|
|
2
|
+
|
|
3
|
+
export type MailerAnalyticsEvent =
|
|
4
|
+
| 'newsletter_subscribed'
|
|
5
|
+
| 'newsletter_confirmed'
|
|
6
|
+
| 'newsletter_unsubscribed'
|
|
7
|
+
| 'newsletter_opened'
|
|
8
|
+
| 'newsletter_clicked'
|
|
9
|
+
| 'newsletter_delivered'
|
|
10
|
+
| 'newsletter_bounced'
|
|
11
|
+
| 'newsletter_complained'
|
|
12
|
+
| 'newsletter_sent'
|
|
13
|
+
| 'newsletter_send_failed'
|
|
14
|
+
|
|
15
|
+
interface AnalyticsBinding {
|
|
16
|
+
writeDataPoint(dataPoint: unknown): Promise<unknown>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface AnalyticsContext {
|
|
20
|
+
locals?: {
|
|
21
|
+
cfContext?: {
|
|
22
|
+
waitUntil(promise: Promise<unknown>): void
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface EmitMailerAnalyticsOptions {
|
|
28
|
+
request?: Request
|
|
29
|
+
context?: AnalyticsContext
|
|
30
|
+
label?: Record<string, unknown>
|
|
31
|
+
contentSlug?: string
|
|
32
|
+
eventValue?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function emitMailerAnalyticsEvent(
|
|
36
|
+
options: ResolvedMailerOptions,
|
|
37
|
+
bindingsEnv: Record<string, unknown>,
|
|
38
|
+
eventName: MailerAnalyticsEvent,
|
|
39
|
+
emitOptions: EmitMailerAnalyticsOptions = {},
|
|
40
|
+
): boolean {
|
|
41
|
+
if (!options.analyticsEnabled) return false
|
|
42
|
+
|
|
43
|
+
const analyticsBinding = bindingsEnv[options.analyticsBinding] as AnalyticsBinding | undefined
|
|
44
|
+
if (!analyticsBinding?.writeDataPoint) return false
|
|
45
|
+
|
|
46
|
+
const dataPoint = buildMailerAnalyticsDataPoint(options, eventName, emitOptions)
|
|
47
|
+
const write = analyticsBinding.writeDataPoint(dataPoint)
|
|
48
|
+
const waitUntil = emitOptions.context?.locals?.cfContext?.waitUntil
|
|
49
|
+
if (waitUntil) {
|
|
50
|
+
waitUntil(write.catch(() => undefined))
|
|
51
|
+
} else {
|
|
52
|
+
void write.catch(() => undefined)
|
|
53
|
+
}
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildMailerAnalyticsDataPoint(
|
|
58
|
+
options: ResolvedMailerOptions,
|
|
59
|
+
eventName: MailerAnalyticsEvent,
|
|
60
|
+
emitOptions: EmitMailerAnalyticsOptions = {},
|
|
61
|
+
): { blobs: string[]; doubles: number[]; indexes: string[] } {
|
|
62
|
+
const url = emitOptions.request ? new URL(emitOptions.request.url) : new URL(options.siteUrl)
|
|
63
|
+
const siteId = siteIdFromUrl(options.siteUrl)
|
|
64
|
+
const label = emitOptions.label ? JSON.stringify(emitOptions.label) : ''
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
blobs: [
|
|
68
|
+
eventName,
|
|
69
|
+
siteId,
|
|
70
|
+
'',
|
|
71
|
+
'',
|
|
72
|
+
url.toString(),
|
|
73
|
+
url.pathname,
|
|
74
|
+
emitOptions.request?.headers.get('referer') ?? '',
|
|
75
|
+
'',
|
|
76
|
+
'',
|
|
77
|
+
'',
|
|
78
|
+
'',
|
|
79
|
+
emitOptions.request?.headers.get('cf-ipcountry') ?? '',
|
|
80
|
+
'',
|
|
81
|
+
'',
|
|
82
|
+
'',
|
|
83
|
+
emitOptions.contentSlug ?? '',
|
|
84
|
+
'newsletter',
|
|
85
|
+
categoryForMailerEvent(eventName),
|
|
86
|
+
label,
|
|
87
|
+
'false',
|
|
88
|
+
],
|
|
89
|
+
doubles: [Date.now(), emitOptions.eventValue ?? 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
90
|
+
indexes: [eventName],
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function siteIdFromUrl(siteUrl: string): string {
|
|
95
|
+
try {
|
|
96
|
+
return new URL(siteUrl).hostname.replace(/^www\./, '')
|
|
97
|
+
} catch {
|
|
98
|
+
return siteUrl
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function categoryForMailerEvent(eventName: MailerAnalyticsEvent): string {
|
|
103
|
+
switch (eventName) {
|
|
104
|
+
case 'newsletter_subscribed':
|
|
105
|
+
case 'newsletter_confirmed':
|
|
106
|
+
return 'conversion'
|
|
107
|
+
case 'newsletter_opened':
|
|
108
|
+
case 'newsletter_clicked':
|
|
109
|
+
return 'interaction'
|
|
110
|
+
case 'newsletter_delivered':
|
|
111
|
+
case 'newsletter_bounced':
|
|
112
|
+
case 'newsletter_complained':
|
|
113
|
+
case 'newsletter_sent':
|
|
114
|
+
case 'newsletter_send_failed':
|
|
115
|
+
case 'newsletter_unsubscribed':
|
|
116
|
+
return 'newsletter'
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/utils/bindings.ts
CHANGED
|
@@ -1,25 +1,23 @@
|
|
|
1
|
+
import { env as cloudflareEnv } from 'cloudflare:workers'
|
|
1
2
|
import { config } from 'virtual:growth-labs/mailer/config'
|
|
2
3
|
import { drizzle } from 'drizzle-orm/d1'
|
|
3
4
|
|
|
4
5
|
type DrizzleDB = ReturnType<typeof drizzle>
|
|
5
6
|
|
|
6
|
-
export interface RuntimeLocals {
|
|
7
|
-
runtime?: { env: Record<string, unknown> }
|
|
8
|
-
}
|
|
9
|
-
|
|
10
7
|
/**
|
|
11
|
-
* Resolve Cloudflare bindings from the
|
|
8
|
+
* Resolve Cloudflare bindings from the Astro 6 Cloudflare Workers env object.
|
|
12
9
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* the virtual config so they stay in sync with the consumer's `astro.config`.
|
|
10
|
+
* Binding names come from the virtual config so they stay in sync with the
|
|
11
|
+
* consumer's `astro.config`.
|
|
16
12
|
*/
|
|
17
|
-
export function resolveBindings(
|
|
13
|
+
export function resolveBindings(
|
|
14
|
+
bindingsEnv: Record<string, unknown> = cloudflareEnv as Record<string, unknown>,
|
|
15
|
+
): {
|
|
18
16
|
db: DrizzleDB
|
|
19
17
|
queue: Queue
|
|
20
18
|
} {
|
|
21
|
-
const d1 =
|
|
22
|
-
const queue =
|
|
19
|
+
const d1 = bindingsEnv[config.d1Binding] as D1Database
|
|
20
|
+
const queue = bindingsEnv[config.queueBinding] as Queue
|
|
23
21
|
|
|
24
22
|
if (!d1) throw new Error(`[mailer] D1 binding "${config.d1Binding}" not found`)
|
|
25
23
|
if (!queue) throw new Error(`[mailer] Queue binding "${config.queueBinding}" not found`)
|
package/src/utils/index.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export {
|
|
2
|
+
buildMailerAnalyticsDataPoint,
|
|
3
|
+
emitMailerAnalyticsEvent,
|
|
4
|
+
type MailerAnalyticsEvent,
|
|
5
|
+
} from './analytics.js'
|
|
1
6
|
export {
|
|
2
7
|
handleBounce,
|
|
3
8
|
handleComplaint,
|
|
@@ -5,13 +10,7 @@ export {
|
|
|
5
10
|
updateSendStatus,
|
|
6
11
|
} from './bounce.js'
|
|
7
12
|
export type { CloudflareEmailSender } from './providers.js'
|
|
8
|
-
export {
|
|
9
|
-
CloudflareEmailProvider,
|
|
10
|
-
getFallbackProvider,
|
|
11
|
-
getProvider,
|
|
12
|
-
ResendFallbackProvider,
|
|
13
|
-
sleep,
|
|
14
|
-
} from './providers.js'
|
|
13
|
+
export { CloudflareEmailProvider, getProvider, sleep } from './providers.js'
|
|
15
14
|
export type { CampaignSchedule, DigestSchedule } from './scheduling.js'
|
|
16
15
|
export {
|
|
17
16
|
executeCampaignSchedule,
|
package/src/utils/providers.ts
CHANGED
|
@@ -44,79 +44,12 @@ export class CloudflareEmailProvider implements EmailProvider {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
// ───
|
|
48
|
-
|
|
49
|
-
export class ResendFallbackProvider implements EmailProvider {
|
|
50
|
-
readonly name = 'resend'
|
|
51
|
-
private readonly apiKey: string
|
|
52
|
-
|
|
53
|
-
constructor(apiKey: string) {
|
|
54
|
-
this.apiKey = apiKey
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async send(email: OutboundEmail): Promise<SendResult> {
|
|
58
|
-
try {
|
|
59
|
-
const response = await fetch('https://api.resend.com/emails', {
|
|
60
|
-
method: 'POST',
|
|
61
|
-
headers: {
|
|
62
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
63
|
-
'Content-Type': 'application/json',
|
|
64
|
-
},
|
|
65
|
-
body: JSON.stringify({
|
|
66
|
-
to: email.to,
|
|
67
|
-
from: email.from,
|
|
68
|
-
reply_to: email.replyTo,
|
|
69
|
-
subject: email.subject,
|
|
70
|
-
html: email.html,
|
|
71
|
-
headers: email.headers,
|
|
72
|
-
}),
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
if (response.ok) {
|
|
76
|
-
const data = (await response.json()) as { id?: string }
|
|
77
|
-
return {
|
|
78
|
-
success: true,
|
|
79
|
-
messageId: data.id,
|
|
80
|
-
retryable: false,
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const retryable = response.status === 429 || response.status >= 500
|
|
85
|
-
const errorText = await response.text().catch(() => 'Unknown error')
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
success: false,
|
|
89
|
-
error: `Resend API ${response.status}: ${errorText}`,
|
|
90
|
-
retryable,
|
|
91
|
-
}
|
|
92
|
-
} catch (err) {
|
|
93
|
-
const message = err instanceof Error ? err.message : String(err)
|
|
94
|
-
return {
|
|
95
|
-
success: false,
|
|
96
|
-
error: message,
|
|
97
|
-
retryable: true,
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ─── Factory functions ───
|
|
47
|
+
// ─── Factory function ───
|
|
104
48
|
|
|
105
49
|
export function getProvider(emailSender: CloudflareEmailSender): EmailProvider {
|
|
106
50
|
return new CloudflareEmailProvider(emailSender)
|
|
107
51
|
}
|
|
108
52
|
|
|
109
|
-
export function getFallbackProvider(options: {
|
|
110
|
-
fallbackProvider: string
|
|
111
|
-
resendApiKey?: string
|
|
112
|
-
}): EmailProvider | null {
|
|
113
|
-
if (options.fallbackProvider === 'resend' && options.resendApiKey) {
|
|
114
|
-
return new ResendFallbackProvider(options.resendApiKey)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return null
|
|
118
|
-
}
|
|
119
|
-
|
|
120
53
|
// ─── Helpers ───
|
|
121
54
|
|
|
122
55
|
export function sleep(ms: number): Promise<void> {
|
package/src/virtual.d.ts
CHANGED
|
@@ -24,9 +24,8 @@ declare module 'virtual:growth-labs/mailer/config' {
|
|
|
24
24
|
accentColor: string
|
|
25
25
|
footerText?: string
|
|
26
26
|
}
|
|
27
|
-
fallbackProvider: 'resend' | 'none'
|
|
28
|
-
resendApiKey?: string
|
|
29
27
|
batchSize: number
|
|
30
28
|
analyticsEnabled: boolean
|
|
29
|
+
analyticsBinding: string
|
|
31
30
|
}
|
|
32
31
|
}
|