@mobilizehub/payload-plugin 0.1.0 → 0.3.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/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/resend-adapter.d.ts +34 -0
- package/dist/adapters/resend-adapter.js +219 -0
- package/dist/adapters/resend-adapter.js.map +1 -0
- package/dist/collections/broadcasts/generateBroadcastsCollection.d.ts +3 -0
- package/dist/collections/broadcasts/generateBroadcastsCollection.js +241 -0
- package/dist/collections/broadcasts/generateBroadcastsCollection.js.map +1 -0
- package/dist/collections/emails/generateEmailsCollection.d.ts +3 -0
- package/dist/collections/emails/generateEmailsCollection.js +204 -0
- package/dist/collections/emails/generateEmailsCollection.js.map +1 -0
- package/dist/collections/emails/hooks/sync-status-from-activity.d.ts +5 -0
- package/dist/collections/emails/hooks/sync-status-from-activity.js +64 -0
- package/dist/collections/emails/hooks/sync-status-from-activity.js.map +1 -0
- package/dist/collections/pages/generatePagesCollection.d.ts +3 -0
- package/dist/collections/pages/generatePagesCollection.js +77 -0
- package/dist/collections/pages/generatePagesCollection.js.map +1 -0
- package/dist/collections/unsubscribe-tokens/generateUnsubscribeTokens.d.ts +2 -0
- package/dist/collections/unsubscribe-tokens/generateUnsubscribeTokens.js +48 -0
- package/dist/collections/unsubscribe-tokens/generateUnsubscribeTokens.js.map +1 -0
- package/dist/components/broadcast-metrics-card.d.ts +7 -0
- package/dist/components/broadcast-metrics-card.js +159 -0
- package/dist/components/broadcast-metrics-card.js.map +1 -0
- package/dist/components/broadcast-send-modal.d.ts +9 -0
- package/dist/components/broadcast-send-modal.js +51 -0
- package/dist/components/broadcast-send-modal.js.map +1 -0
- package/dist/components/broadcast-send-test-drawer.d.ts +7 -0
- package/dist/components/broadcast-send-test-drawer.js +154 -0
- package/dist/components/broadcast-send-test-drawer.js.map +1 -0
- package/dist/components/email-activity.d.ts +4 -0
- package/dist/components/email-activity.js +359 -0
- package/dist/components/email-activity.js.map +1 -0
- package/dist/components/email-preview.d.ts +2 -0
- package/dist/components/email-preview.js +95 -0
- package/dist/components/email-preview.js.map +1 -0
- package/dist/endpoints/sendBroadcastHandler.d.ts +9 -0
- package/dist/endpoints/sendBroadcastHandler.js +107 -0
- package/dist/endpoints/sendBroadcastHandler.js.map +1 -0
- package/dist/endpoints/sendTestBroadcastHandler.d.ts +10 -0
- package/dist/endpoints/sendTestBroadcastHandler.js +143 -0
- package/dist/endpoints/sendTestBroadcastHandler.js.map +1 -0
- package/dist/endpoints/unsubscribeHandler.d.ts +9 -0
- package/dist/endpoints/unsubscribeHandler.js +153 -0
- package/dist/endpoints/unsubscribeHandler.js.map +1 -0
- package/dist/exports/client.d.ts +3 -1
- package/dist/exports/client.js +3 -0
- package/dist/exports/client.js.map +1 -1
- package/dist/exports/rsc.d.ts +2 -1
- package/dist/exports/rsc.js +2 -0
- package/dist/exports/rsc.js.map +1 -1
- package/dist/fields/name.d.ts +5 -0
- package/dist/fields/name.js +12 -0
- package/dist/fields/name.js.map +1 -0
- package/dist/fields/publishedAt.d.ts +5 -0
- package/dist/fields/publishedAt.js +16 -0
- package/dist/fields/publishedAt.js.map +1 -0
- package/dist/fields/slug.d.ts +7 -0
- package/dist/fields/slug.js +47 -0
- package/dist/fields/slug.js.map +1 -0
- package/dist/fields/status.d.ts +5 -0
- package/dist/fields/status.js +25 -0
- package/dist/fields/status.js.map +1 -0
- package/dist/index.js +48 -3
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +3 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/unsubscribe.d.ts +6 -0
- package/dist/react/unsubscribe.js +16 -0
- package/dist/react/unsubscribe.js.map +1 -0
- package/dist/tasks/sendBroadcastsTask.d.ts +11 -0
- package/dist/tasks/sendBroadcastsTask.js +196 -0
- package/dist/tasks/sendBroadcastsTask.js.map +1 -0
- package/dist/tasks/sendEmailTask.d.ts +9 -0
- package/dist/tasks/sendEmailTask.js +167 -0
- package/dist/tasks/sendEmailTask.js.map +1 -0
- package/dist/types/index.d.ts +133 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/api-response.d.ts +72 -0
- package/dist/utils/api-response.js +66 -0
- package/dist/utils/api-response.js.map +1 -0
- package/dist/utils/email.d.ts +36 -0
- package/dist/utils/email.js +40 -0
- package/dist/utils/email.js.map +1 -0
- package/dist/utils/lexical.d.ts +13 -0
- package/dist/utils/lexical.js +27 -0
- package/dist/utils/lexical.js.map +1 -0
- package/dist/utils/unsubscribe-token.d.ts +67 -0
- package/dist/utils/unsubscribe-token.js +103 -0
- package/dist/utils/unsubscribe-token.js.map +1 -0
- package/package.json +20 -9
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { resendAdapter } from './resend-adapter.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/index.ts"],"sourcesContent":["export { resendAdapter } from './resend-adapter.js'\n"],"names":["resendAdapter"],"mappings":"AAAA,SAASA,aAAa,QAAQ,sBAAqB"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { EmailAdapter } from '../types/index.js';
|
|
2
|
+
type ResendAdapterOptions = {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
defaultFromAddress: string;
|
|
5
|
+
defaultFromName: string;
|
|
6
|
+
render: ReturnType<EmailAdapter>['render'];
|
|
7
|
+
webhookSecret: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Resend email adapter for MobilizeHub
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { resendAdapter } from '@mobilizehub/payload-plugin/adapters'
|
|
15
|
+
*
|
|
16
|
+
* const emailAdapter = resendAdapter({
|
|
17
|
+
* apiKey: process.env.RESEND_API_KEY!,
|
|
18
|
+
* webhookSecret: process.env.RESEND_WEBHOOK_SECRET!,
|
|
19
|
+
* defaultFromAddress: 'noreply@example.com',
|
|
20
|
+
* defaultFromName: 'My App',
|
|
21
|
+
* render: ({ html }) => html,
|
|
22
|
+
* })
|
|
23
|
+
*
|
|
24
|
+
* // In your Payload config:
|
|
25
|
+
* plugins: [
|
|
26
|
+
* mobilizehub({
|
|
27
|
+
* email: emailAdapter,
|
|
28
|
+
* // ...
|
|
29
|
+
* }),
|
|
30
|
+
* ]
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare const resendAdapter: (opts: ResendAdapterOptions) => EmailAdapter;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { formatFromAddress } from '../utils/email.js';
|
|
3
|
+
const RESEND_API_URL = 'https://api.resend.com/emails';
|
|
4
|
+
const WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS = 5 * 60 // 5 minutes
|
|
5
|
+
;
|
|
6
|
+
const WEBHOOK_EVENT_TO_ACTIVITY = {
|
|
7
|
+
'email.bounced': 'bounced',
|
|
8
|
+
'email.clicked': 'clicked',
|
|
9
|
+
'email.complained': 'complained',
|
|
10
|
+
'email.delivered': 'delivered',
|
|
11
|
+
'email.delivery_delayed': 'delivery_delayed',
|
|
12
|
+
'email.failed': 'failed',
|
|
13
|
+
'email.opened': 'opened',
|
|
14
|
+
'email.received': 'received',
|
|
15
|
+
'email.sent': 'sent'
|
|
16
|
+
};
|
|
17
|
+
async function sendResendEmail(apiKey, message, idempotencyKey) {
|
|
18
|
+
const response = await fetch(RESEND_API_URL, {
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
from: message.from,
|
|
21
|
+
html: message.html,
|
|
22
|
+
subject: message.subject,
|
|
23
|
+
to: message.to
|
|
24
|
+
}),
|
|
25
|
+
headers: {
|
|
26
|
+
Authorization: `Bearer ${apiKey}`,
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
'Idempotency-Key': idempotencyKey || ''
|
|
29
|
+
},
|
|
30
|
+
method: 'POST'
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
const errorText = await response.text();
|
|
34
|
+
throw new Error(`Resend API error: ${response.status} - ${errorText}`);
|
|
35
|
+
}
|
|
36
|
+
const data = await response.json();
|
|
37
|
+
return {
|
|
38
|
+
providerId: data.id
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function extractSvixHeaders(req) {
|
|
42
|
+
const id = req.headers.get('svix-id');
|
|
43
|
+
const timestamp = req.headers.get('svix-timestamp');
|
|
44
|
+
const signature = req.headers.get('svix-signature');
|
|
45
|
+
if (!id || !timestamp || !signature) {
|
|
46
|
+
throw new Error('Missing required Svix headers');
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
signature,
|
|
51
|
+
timestamp
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function verifyTimestamp(timestamp) {
|
|
55
|
+
const timestampSeconds = parseInt(timestamp, 10);
|
|
56
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
57
|
+
if (Math.abs(nowSeconds - timestampSeconds) > WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS) {
|
|
58
|
+
throw new Error('Webhook timestamp is too old');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function computeExpectedSignature(svixId, svixTimestamp, body, webhookSecret) {
|
|
62
|
+
const signedContent = `${svixId}.${svixTimestamp}.${body}`;
|
|
63
|
+
// Resend webhook secrets are prefixed with "whsec_" and base64 encoded
|
|
64
|
+
const secretParts = webhookSecret.split('_');
|
|
65
|
+
const secretBase64 = secretParts.length > 1 ? secretParts[1] : secretParts[0];
|
|
66
|
+
const secretBytes = Buffer.from(secretBase64, 'base64');
|
|
67
|
+
return crypto.createHmac('sha256', secretBytes).update(signedContent).digest('base64');
|
|
68
|
+
}
|
|
69
|
+
function verifySignature(svixSignature, expectedSignature) {
|
|
70
|
+
// Svix signatures can be multiple, space-delimited with version prefix (e.g., "v1,<sig>")
|
|
71
|
+
const signatures = svixSignature.split(' ').map((sig)=>{
|
|
72
|
+
const [version, signature] = sig.split(',');
|
|
73
|
+
return {
|
|
74
|
+
signature,
|
|
75
|
+
version
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
return signatures.some(({ signature, version })=>{
|
|
79
|
+
if (version !== 'v1') {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
return crypto.timingSafeEqual(Buffer.from(expectedSignature), Buffer.from(signature));
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async function verifyWebhookSignature(req, webhookSecret) {
|
|
90
|
+
const headers = extractSvixHeaders(req);
|
|
91
|
+
verifyTimestamp(headers.timestamp);
|
|
92
|
+
if (!req.text) {
|
|
93
|
+
throw new Error('Request does not have a text body method');
|
|
94
|
+
}
|
|
95
|
+
const body = await req.text();
|
|
96
|
+
if (!body) {
|
|
97
|
+
throw new Error('No body in request');
|
|
98
|
+
}
|
|
99
|
+
const expectedSignature = computeExpectedSignature(headers.id, headers.timestamp, body, webhookSecret);
|
|
100
|
+
if (!verifySignature(headers.signature, expectedSignature)) {
|
|
101
|
+
throw new Error('Invalid webhook signature');
|
|
102
|
+
}
|
|
103
|
+
return JSON.parse(body);
|
|
104
|
+
}
|
|
105
|
+
async function findEmailByProviderId(payload, providerId) {
|
|
106
|
+
const result = await payload.find({
|
|
107
|
+
collection: 'emails',
|
|
108
|
+
limit: 1,
|
|
109
|
+
where: {
|
|
110
|
+
providerId: {
|
|
111
|
+
equals: providerId
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return result.docs[0] || null;
|
|
116
|
+
}
|
|
117
|
+
async function addEmailActivity(payload, emailId, currentActivity, activityType) {
|
|
118
|
+
const updatedActivity = [
|
|
119
|
+
...currentActivity || [],
|
|
120
|
+
{
|
|
121
|
+
type: activityType,
|
|
122
|
+
timestamp: new Date().toISOString()
|
|
123
|
+
}
|
|
124
|
+
];
|
|
125
|
+
await payload.update({
|
|
126
|
+
id: emailId,
|
|
127
|
+
collection: 'emails',
|
|
128
|
+
data: {
|
|
129
|
+
activity: updatedActivity
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async function handleWebhookEvent(payload, eventType, emailProviderId, logger) {
|
|
134
|
+
const activityType = WEBHOOK_EVENT_TO_ACTIVITY[eventType];
|
|
135
|
+
if (!activityType) {
|
|
136
|
+
logger.warn(`Unhandled webhook event type: ${eventType}`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const email = await findEmailByProviderId(payload, emailProviderId);
|
|
140
|
+
if (!email) {
|
|
141
|
+
logger.error(`No email record found for provider ID: ${emailProviderId}`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
await addEmailActivity(payload, email.id, email.activity, activityType);
|
|
145
|
+
// Log based on severity
|
|
146
|
+
const warnEvents = [
|
|
147
|
+
'email.bounced',
|
|
148
|
+
'email.complained',
|
|
149
|
+
'email.delivery_delayed'
|
|
150
|
+
];
|
|
151
|
+
const errorEvents = [
|
|
152
|
+
'email.failed'
|
|
153
|
+
];
|
|
154
|
+
if (errorEvents.includes(eventType)) {
|
|
155
|
+
logger.error(`Email ${activityType}: ${emailProviderId}`);
|
|
156
|
+
} else if (warnEvents.includes(eventType)) {
|
|
157
|
+
logger.warn(`Email ${activityType}: ${emailProviderId}`);
|
|
158
|
+
} else {
|
|
159
|
+
logger.info(`Email ${activityType}: ${emailProviderId}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Resend email adapter for MobilizeHub
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```typescript
|
|
167
|
+
* import { resendAdapter } from '@mobilizehub/payload-plugin/adapters'
|
|
168
|
+
*
|
|
169
|
+
* const emailAdapter = resendAdapter({
|
|
170
|
+
* apiKey: process.env.RESEND_API_KEY!,
|
|
171
|
+
* webhookSecret: process.env.RESEND_WEBHOOK_SECRET!,
|
|
172
|
+
* defaultFromAddress: 'noreply@example.com',
|
|
173
|
+
* defaultFromName: 'My App',
|
|
174
|
+
* render: ({ html }) => html,
|
|
175
|
+
* })
|
|
176
|
+
*
|
|
177
|
+
* // In your Payload config:
|
|
178
|
+
* plugins: [
|
|
179
|
+
* mobilizehub({
|
|
180
|
+
* email: emailAdapter,
|
|
181
|
+
* // ...
|
|
182
|
+
* }),
|
|
183
|
+
* ]
|
|
184
|
+
* ```
|
|
185
|
+
*/ export const resendAdapter = (opts)=>{
|
|
186
|
+
return ()=>({
|
|
187
|
+
name: 'resend-mobilizehub-adapter',
|
|
188
|
+
defaultFromAddress: opts.defaultFromAddress,
|
|
189
|
+
defaultFromName: opts.defaultFromName,
|
|
190
|
+
render: opts.render,
|
|
191
|
+
sendEmail: async (message)=>{
|
|
192
|
+
const fromAddress = message.from || formatFromAddress(opts.defaultFromName, opts.defaultFromAddress);
|
|
193
|
+
return sendResendEmail(opts.apiKey, {
|
|
194
|
+
from: fromAddress,
|
|
195
|
+
html: message.html,
|
|
196
|
+
subject: message.subject,
|
|
197
|
+
to: message.to
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
webhookHandler: async (req)=>{
|
|
201
|
+
const { payload } = req;
|
|
202
|
+
const logger = payload.logger;
|
|
203
|
+
try {
|
|
204
|
+
const webhookPayload = await verifyWebhookSignature(req, opts.webhookSecret);
|
|
205
|
+
const emailProviderId = webhookPayload.data.email_id;
|
|
206
|
+
if (!emailProviderId) {
|
|
207
|
+
logger.warn(`Webhook event ${webhookPayload.type} has no email_id`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
await handleWebhookEvent(payload, webhookPayload.type, emailProviderId, logger);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
logger.error(`Resend webhook error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
//# sourceMappingURL=resend-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/resend-adapter.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\n\nimport crypto from 'crypto'\n\nimport type { EmailActivityType, EmailAdapter, EmailMessage } from '../types/index.js'\n\nimport { formatFromAddress } from '../utils/email.js'\n\ntype ResendAdapterOptions = {\n apiKey: string\n defaultFromAddress: string\n defaultFromName: string\n render: ReturnType<EmailAdapter>['render']\n webhookSecret: string\n}\n\ntype ResendWebhookPayload = {\n created_at: string\n data: {\n [key: string]: unknown\n email_id?: string\n }\n type: string\n}\n\nconst RESEND_API_URL = 'https://api.resend.com/emails'\nconst WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS = 5 * 60 // 5 minutes\n\nconst WEBHOOK_EVENT_TO_ACTIVITY: Record<string, EmailActivityType> = {\n 'email.bounced': 'bounced',\n 'email.clicked': 'clicked',\n 'email.complained': 'complained',\n 'email.delivered': 'delivered',\n 'email.delivery_delayed': 'delivery_delayed',\n 'email.failed': 'failed',\n 'email.opened': 'opened',\n 'email.received': 'received',\n 'email.sent': 'sent',\n}\n\nasync function sendResendEmail(\n apiKey: string,\n message: EmailMessage,\n idempotencyKey?: string,\n): Promise<{ providerId: string }> {\n const response = await fetch(RESEND_API_URL, {\n body: JSON.stringify({\n from: message.from,\n html: message.html,\n subject: message.subject,\n to: message.to,\n }),\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n 'Idempotency-Key': idempotencyKey || '',\n },\n method: 'POST',\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(`Resend API error: ${response.status} - ${errorText}`)\n }\n\n const data = (await response.json()) as { id: string }\n\n return { providerId: data.id }\n}\n\ntype SvixHeaders = {\n id: string\n signature: string\n timestamp: string\n}\n\nfunction extractSvixHeaders(req: PayloadRequest): SvixHeaders {\n const id = req.headers.get('svix-id')\n const timestamp = req.headers.get('svix-timestamp')\n const signature = req.headers.get('svix-signature')\n\n if (!id || !timestamp || !signature) {\n throw new Error('Missing required Svix headers')\n }\n\n return { id, signature, timestamp }\n}\n\nfunction verifyTimestamp(timestamp: string): void {\n const timestampSeconds = parseInt(timestamp, 10)\n const nowSeconds = Math.floor(Date.now() / 1000)\n\n if (Math.abs(nowSeconds - timestampSeconds) > WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS) {\n throw new Error('Webhook timestamp is too old')\n }\n}\n\nfunction computeExpectedSignature(\n svixId: string,\n svixTimestamp: string,\n body: string,\n webhookSecret: string,\n): string {\n const signedContent = `${svixId}.${svixTimestamp}.${body}`\n\n // Resend webhook secrets are prefixed with \"whsec_\" and base64 encoded\n const secretParts = webhookSecret.split('_')\n const secretBase64 = secretParts.length > 1 ? secretParts[1] : secretParts[0]\n const secretBytes = Buffer.from(secretBase64, 'base64')\n\n return crypto.createHmac('sha256', secretBytes).update(signedContent).digest('base64')\n}\n\nfunction verifySignature(svixSignature: string, expectedSignature: string): boolean {\n // Svix signatures can be multiple, space-delimited with version prefix (e.g., \"v1,<sig>\")\n const signatures = svixSignature.split(' ').map((sig) => {\n const [version, signature] = sig.split(',')\n return { signature, version }\n })\n\n return signatures.some(({ signature, version }) => {\n if (version !== 'v1') {\n return false\n }\n\n try {\n return crypto.timingSafeEqual(Buffer.from(expectedSignature), Buffer.from(signature))\n } catch {\n return false\n }\n })\n}\n\nasync function verifyWebhookSignature(\n req: PayloadRequest,\n webhookSecret: string,\n): Promise<ResendWebhookPayload> {\n const headers = extractSvixHeaders(req)\n\n verifyTimestamp(headers.timestamp)\n\n if (!req.text) {\n throw new Error('Request does not have a text body method')\n }\n\n const body = await req.text()\n\n if (!body) {\n throw new Error('No body in request')\n }\n\n const expectedSignature = computeExpectedSignature(\n headers.id,\n headers.timestamp,\n body,\n webhookSecret,\n )\n\n if (!verifySignature(headers.signature, expectedSignature)) {\n throw new Error('Invalid webhook signature')\n }\n\n return JSON.parse(body) as ResendWebhookPayload\n}\n\nasync function findEmailByProviderId(\n payload: Payload,\n providerId: string,\n): Promise<{ activity?: unknown[]; id: number | string } | null> {\n const result = await payload.find({\n collection: 'emails',\n limit: 1,\n where: {\n providerId: { equals: providerId },\n },\n })\n\n return (result.docs[0] as { activity?: unknown[]; id: number | string }) || null\n}\n\nasync function addEmailActivity(\n payload: Payload,\n emailId: number | string,\n currentActivity: undefined | unknown[],\n activityType: EmailActivityType,\n): Promise<void> {\n const updatedActivity = [\n ...(currentActivity || []),\n {\n type: activityType,\n timestamp: new Date().toISOString(),\n },\n ]\n\n await payload.update({\n id: emailId,\n collection: 'emails',\n data: {\n activity: updatedActivity,\n },\n })\n}\n\nasync function handleWebhookEvent(\n payload: Payload,\n eventType: string,\n emailProviderId: string,\n logger: Payload['logger'],\n): Promise<void> {\n const activityType = WEBHOOK_EVENT_TO_ACTIVITY[eventType]\n\n if (!activityType) {\n logger.warn(`Unhandled webhook event type: ${eventType}`)\n return\n }\n\n const email = await findEmailByProviderId(payload, emailProviderId)\n\n if (!email) {\n logger.error(`No email record found for provider ID: ${emailProviderId}`)\n return\n }\n\n await addEmailActivity(payload, email.id, email.activity, activityType)\n\n // Log based on severity\n const warnEvents = ['email.bounced', 'email.complained', 'email.delivery_delayed']\n const errorEvents = ['email.failed']\n\n if (errorEvents.includes(eventType)) {\n logger.error(`Email ${activityType}: ${emailProviderId}`)\n } else if (warnEvents.includes(eventType)) {\n logger.warn(`Email ${activityType}: ${emailProviderId}`)\n } else {\n logger.info(`Email ${activityType}: ${emailProviderId}`)\n }\n}\n\n/**\n * Resend email adapter for MobilizeHub\n *\n * @example\n * ```typescript\n * import { resendAdapter } from '@mobilizehub/payload-plugin/adapters'\n *\n * const emailAdapter = resendAdapter({\n * apiKey: process.env.RESEND_API_KEY!,\n * webhookSecret: process.env.RESEND_WEBHOOK_SECRET!,\n * defaultFromAddress: 'noreply@example.com',\n * defaultFromName: 'My App',\n * render: ({ html }) => html,\n * })\n *\n * // In your Payload config:\n * plugins: [\n * mobilizehub({\n * email: emailAdapter,\n * // ...\n * }),\n * ]\n * ```\n */\nexport const resendAdapter = (opts: ResendAdapterOptions): EmailAdapter => {\n return () => ({\n name: 'resend-mobilizehub-adapter',\n defaultFromAddress: opts.defaultFromAddress,\n defaultFromName: opts.defaultFromName,\n render: opts.render,\n\n sendEmail: async (message) => {\n const fromAddress =\n message.from || formatFromAddress(opts.defaultFromName, opts.defaultFromAddress)\n\n return sendResendEmail(opts.apiKey, {\n from: fromAddress,\n html: message.html,\n subject: message.subject,\n to: message.to,\n })\n },\n\n webhookHandler: async (req) => {\n const { payload } = req\n const logger = payload.logger\n\n try {\n const webhookPayload = await verifyWebhookSignature(req, opts.webhookSecret)\n\n const emailProviderId = webhookPayload.data.email_id\n\n if (!emailProviderId) {\n logger.warn(`Webhook event ${webhookPayload.type} has no email_id`)\n return\n }\n\n await handleWebhookEvent(payload, webhookPayload.type, emailProviderId, logger)\n } catch (error) {\n logger.error(\n `Resend webhook error: ${error instanceof Error ? error.message : 'Unknown error'}`,\n )\n throw error\n }\n },\n })\n}\n"],"names":["crypto","formatFromAddress","RESEND_API_URL","WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS","WEBHOOK_EVENT_TO_ACTIVITY","sendResendEmail","apiKey","message","idempotencyKey","response","fetch","body","JSON","stringify","from","html","subject","to","headers","Authorization","method","ok","errorText","text","Error","status","data","json","providerId","id","extractSvixHeaders","req","get","timestamp","signature","verifyTimestamp","timestampSeconds","parseInt","nowSeconds","Math","floor","Date","now","abs","computeExpectedSignature","svixId","svixTimestamp","webhookSecret","signedContent","secretParts","split","secretBase64","length","secretBytes","Buffer","createHmac","update","digest","verifySignature","svixSignature","expectedSignature","signatures","map","sig","version","some","timingSafeEqual","verifyWebhookSignature","parse","findEmailByProviderId","payload","result","find","collection","limit","where","equals","docs","addEmailActivity","emailId","currentActivity","activityType","updatedActivity","type","toISOString","activity","handleWebhookEvent","eventType","emailProviderId","logger","warn","email","error","warnEvents","errorEvents","includes","info","resendAdapter","opts","name","defaultFromAddress","defaultFromName","render","sendEmail","fromAddress","webhookHandler","webhookPayload","email_id"],"mappings":"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"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { authenticated } from '../../access/authenticated.js';
|
|
2
|
+
export const generateBroadcastsCollection = (broadcastsConfig)=>{
|
|
3
|
+
const defaultFields = [
|
|
4
|
+
{
|
|
5
|
+
name: 'status',
|
|
6
|
+
type: 'select',
|
|
7
|
+
admin: {
|
|
8
|
+
position: 'sidebar',
|
|
9
|
+
readOnly: true
|
|
10
|
+
},
|
|
11
|
+
defaultValue: 'draft',
|
|
12
|
+
options: [
|
|
13
|
+
{
|
|
14
|
+
label: 'Draft',
|
|
15
|
+
value: 'draft'
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
label: 'Sending',
|
|
19
|
+
value: 'sending'
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
label: "Failed",
|
|
23
|
+
value: 'failed'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
label: 'Sent',
|
|
27
|
+
value: 'sent'
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'tabs',
|
|
33
|
+
tabs: [
|
|
34
|
+
{
|
|
35
|
+
fields: [
|
|
36
|
+
{
|
|
37
|
+
name: 'name',
|
|
38
|
+
type: 'text',
|
|
39
|
+
access: {
|
|
40
|
+
update: ({ doc })=>doc?.status === 'draft'
|
|
41
|
+
},
|
|
42
|
+
admin: {
|
|
43
|
+
description: 'This is for internal reference only.'
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'fromName',
|
|
48
|
+
type: 'text',
|
|
49
|
+
defaultValue: ({ req })=>broadcastsConfig.email(req).defaultFromName || '',
|
|
50
|
+
label: 'Name',
|
|
51
|
+
required: true
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'fromAddress',
|
|
55
|
+
type: 'text',
|
|
56
|
+
admin: {
|
|
57
|
+
description: 'The from address is set in the email configuration.',
|
|
58
|
+
readOnly: true
|
|
59
|
+
},
|
|
60
|
+
defaultValue: ({ req })=>broadcastsConfig.email(req).defaultFromAddress || '',
|
|
61
|
+
label: 'Address',
|
|
62
|
+
required: true
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'to',
|
|
66
|
+
type: 'radio',
|
|
67
|
+
admin: {
|
|
68
|
+
description: 'Choose how to segment the recipients of this broadcast.',
|
|
69
|
+
layout: 'horizontal'
|
|
70
|
+
},
|
|
71
|
+
defaultValue: 'all',
|
|
72
|
+
options: [
|
|
73
|
+
{
|
|
74
|
+
label: 'All',
|
|
75
|
+
value: 'all'
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
label: 'By Tags',
|
|
79
|
+
value: 'tags'
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'tags',
|
|
85
|
+
type: 'relationship',
|
|
86
|
+
admin: {
|
|
87
|
+
condition: (_, siblingData)=>siblingData?.to === 'tags',
|
|
88
|
+
description: 'Select one or more tags to send this broadcast to contacts with any of the selected tags who have opted in to receive emails.'
|
|
89
|
+
},
|
|
90
|
+
hasMany: true,
|
|
91
|
+
minRows: 1,
|
|
92
|
+
relationTo: 'tags'
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'replyTo',
|
|
96
|
+
type: 'text',
|
|
97
|
+
localized: true
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'subject',
|
|
101
|
+
type: 'text',
|
|
102
|
+
localized: true,
|
|
103
|
+
required: true
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'previewText',
|
|
107
|
+
type: 'text',
|
|
108
|
+
localized: true
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
label: 'Settings'
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
fields: [
|
|
115
|
+
{
|
|
116
|
+
name: 'content',
|
|
117
|
+
type: 'richText',
|
|
118
|
+
label: 'Content',
|
|
119
|
+
localized: true
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
label: 'Content'
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
admin: {
|
|
126
|
+
condition: (_, siblingData)=>siblingData?.status !== 'draft'
|
|
127
|
+
},
|
|
128
|
+
fields: [
|
|
129
|
+
{
|
|
130
|
+
name: 'metricsTable',
|
|
131
|
+
type: 'ui',
|
|
132
|
+
admin: {
|
|
133
|
+
components: {
|
|
134
|
+
Field: '@mobilizehub/payload-plugin/rsc#MetricsCards'
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'emails',
|
|
140
|
+
type: 'join',
|
|
141
|
+
admin: {
|
|
142
|
+
condition: (_, siblingData)=>siblingData?.status !== 'draft'
|
|
143
|
+
},
|
|
144
|
+
collection: 'emails',
|
|
145
|
+
on: 'broadcast'
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
label: 'Metrics'
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'meta',
|
|
154
|
+
type: 'group',
|
|
155
|
+
admin: {
|
|
156
|
+
hidden: true
|
|
157
|
+
},
|
|
158
|
+
fields: [
|
|
159
|
+
{
|
|
160
|
+
name: 'contactsCount',
|
|
161
|
+
type: 'number',
|
|
162
|
+
defaultValue: 0
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'processedCount',
|
|
166
|
+
type: 'number',
|
|
167
|
+
defaultValue: 0
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'lastProcessedContactId',
|
|
171
|
+
type: 'number',
|
|
172
|
+
admin: {
|
|
173
|
+
description: 'Used for cursor-based pagination during batch processing'
|
|
174
|
+
},
|
|
175
|
+
defaultValue: 0
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
];
|
|
180
|
+
const config = {
|
|
181
|
+
...broadcastsConfig.broadcastsOverrides || {},
|
|
182
|
+
slug: broadcastsConfig.broadcastsOverrides?.slug || 'broadcasts',
|
|
183
|
+
access: {
|
|
184
|
+
read: authenticated,
|
|
185
|
+
update: ()=>{
|
|
186
|
+
return {
|
|
187
|
+
status: {
|
|
188
|
+
equals: 'draft'
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
...broadcastsConfig.broadcastsOverrides?.access || {}
|
|
193
|
+
},
|
|
194
|
+
admin: {
|
|
195
|
+
components: {
|
|
196
|
+
edit: {
|
|
197
|
+
beforeDocumentControls: [
|
|
198
|
+
{
|
|
199
|
+
clientProps: {
|
|
200
|
+
buttonLabel: 'Send'
|
|
201
|
+
},
|
|
202
|
+
path: '@mobilizehub/payload-plugin/client#SendBroadcastModal'
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
clientProps: {
|
|
206
|
+
buttonLabel: 'Test'
|
|
207
|
+
},
|
|
208
|
+
path: '@mobilizehub/payload-plugin/client#SendTestBroadcastDrawer'
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
hidden: broadcastsConfig.broadcastsOverrides?.admin?.hidden || false,
|
|
214
|
+
useAsTitle: broadcastsConfig.broadcastsOverrides?.admin?.useAsTitle || 'name',
|
|
215
|
+
...broadcastsConfig.broadcastsOverrides?.admin || {}
|
|
216
|
+
},
|
|
217
|
+
fields: broadcastsConfig.broadcastsOverrides?.fields ? broadcastsConfig.broadcastsOverrides.fields({
|
|
218
|
+
defaultFields
|
|
219
|
+
}) : defaultFields,
|
|
220
|
+
hooks: {
|
|
221
|
+
...broadcastsConfig.broadcastsOverrides?.hooks || {},
|
|
222
|
+
beforeChange: [
|
|
223
|
+
...broadcastsConfig.broadcastsOverrides?.hooks?.beforeChange || [],
|
|
224
|
+
({ data, operation })=>{
|
|
225
|
+
// When duplicating, check if this is a creation of a published doc
|
|
226
|
+
if (operation === 'create' && data.status !== 'draft') {
|
|
227
|
+
// Force status to draft for all newly created documents
|
|
228
|
+
return {
|
|
229
|
+
...data,
|
|
230
|
+
status: 'draft'
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return data;
|
|
234
|
+
}
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
return config;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
//# sourceMappingURL=generateBroadcastsCollection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/collections/broadcasts/generateBroadcastsCollection.ts"],"sourcesContent":["import type { CollectionConfig, Field } from 'payload'\n\nimport type { MobilizehubPluginConfig } from '../../types/index.js'\n\nimport { authenticated } from '../../access/authenticated.js'\n\nexport const generateBroadcastsCollection = (broadcastsConfig: MobilizehubPluginConfig) => {\n const defaultFields: Field[] = [\n {\n name: 'status',\n type: 'select',\n admin: {\n position: 'sidebar',\n readOnly: true,\n },\n defaultValue: 'draft',\n options: [\n {\n label: 'Draft',\n value: 'draft',\n },\n {\n label: 'Sending',\n value: 'sending',\n },\n {\n label: \"Failed\",\n value: 'failed',\n },\n {\n label: 'Sent',\n value: 'sent',\n },\n ],\n },\n {\n type: 'tabs',\n tabs: [\n {\n fields: [\n {\n name: 'name',\n type: 'text',\n access: {\n update: ({ doc }) => doc?.status === 'draft',\n },\n admin: {\n description: 'This is for internal reference only.',\n },\n },\n {\n name: 'fromName',\n type: 'text',\n defaultValue: ({ req }) => broadcastsConfig.email(req).defaultFromName || '',\n label: 'Name',\n required: true,\n },\n {\n name: 'fromAddress',\n type: 'text',\n admin: {\n description: 'The from address is set in the email configuration.',\n readOnly: true,\n },\n defaultValue: ({ req }) => broadcastsConfig.email(req).defaultFromAddress || '',\n label: 'Address',\n required: true,\n },\n {\n name: 'to',\n type: 'radio',\n admin: {\n description: 'Choose how to segment the recipients of this broadcast.',\n layout: 'horizontal',\n },\n defaultValue: 'all',\n options: [\n {\n label: 'All',\n value: 'all',\n },\n {\n label: 'By Tags',\n value: 'tags',\n },\n ],\n },\n {\n name: 'tags',\n type: 'relationship',\n admin: {\n condition: (_, siblingData) => siblingData?.to === 'tags',\n description:\n 'Select one or more tags to send this broadcast to contacts with any of the selected tags who have opted in to receive emails.',\n },\n hasMany: true,\n minRows: 1,\n relationTo: 'tags',\n },\n {\n name: 'replyTo',\n type: 'text',\n localized: true,\n },\n {\n name: 'subject',\n type: 'text',\n localized: true,\n required: true,\n },\n {\n name: 'previewText',\n type: 'text',\n localized: true,\n },\n ],\n label: 'Settings',\n },\n {\n fields: [\n {\n name: 'content',\n type: 'richText',\n label: 'Content',\n localized: true,\n },\n ],\n label: 'Content',\n },\n {\n admin: {\n condition: (_, siblingData) => siblingData?.status !== 'draft',\n },\n fields: [\n {\n name: 'metricsTable',\n type: 'ui',\n admin: {\n components: {\n Field: '@mobilizehub/payload-plugin/rsc#MetricsCards',\n },\n },\n },\n {\n name: 'emails',\n type: 'join',\n admin: {\n condition: (_, siblingData) => siblingData?.status !== 'draft',\n },\n collection: 'emails',\n on: 'broadcast',\n },\n ],\n label: 'Metrics',\n },\n ],\n },\n {\n name: 'meta',\n type: 'group',\n admin: {\n hidden: true,\n },\n fields: [\n {\n name: 'contactsCount',\n type: 'number',\n defaultValue: 0,\n },\n {\n name: 'processedCount',\n type: 'number',\n defaultValue: 0,\n },\n {\n name: 'lastProcessedContactId',\n type: 'number',\n admin: {\n description: 'Used for cursor-based pagination during batch processing',\n },\n defaultValue: 0,\n },\n ],\n },\n ]\n\n const config: CollectionConfig = {\n ...(broadcastsConfig.broadcastsOverrides || {}),\n slug: broadcastsConfig.broadcastsOverrides?.slug || 'broadcasts',\n access: {\n read: authenticated,\n update: () => {\n return {\n status: { equals: 'draft' },\n }\n },\n ...(broadcastsConfig.broadcastsOverrides?.access || {}),\n },\n admin: {\n components: {\n edit: {\n beforeDocumentControls: [\n {\n clientProps: {\n buttonLabel: 'Send',\n },\n path: '@mobilizehub/payload-plugin/client#SendBroadcastModal',\n },\n {\n clientProps: {\n buttonLabel: 'Test',\n },\n path: '@mobilizehub/payload-plugin/client#SendTestBroadcastDrawer',\n },\n ],\n },\n },\n hidden: broadcastsConfig.broadcastsOverrides?.admin?.hidden || false,\n useAsTitle: broadcastsConfig.broadcastsOverrides?.admin?.useAsTitle || 'name',\n ...(broadcastsConfig.broadcastsOverrides?.admin || {}),\n },\n fields: broadcastsConfig.broadcastsOverrides?.fields\n ? broadcastsConfig.broadcastsOverrides.fields({ defaultFields })\n : defaultFields,\n hooks: {\n ...(broadcastsConfig.broadcastsOverrides?.hooks || {}),\n beforeChange: [\n ...(broadcastsConfig.broadcastsOverrides?.hooks?.beforeChange || []),\n ({ data, operation }) => {\n // When duplicating, check if this is a creation of a published doc\n if (operation === 'create' && data.status !== 'draft') {\n // Force status to draft for all newly created documents\n return {\n ...data,\n status: 'draft',\n }\n }\n return data\n },\n ],\n },\n }\n\n return config\n}\n"],"names":["authenticated","generateBroadcastsCollection","broadcastsConfig","defaultFields","name","type","admin","position","readOnly","defaultValue","options","label","value","tabs","fields","access","update","doc","status","description","req","email","defaultFromName","required","defaultFromAddress","layout","condition","_","siblingData","to","hasMany","minRows","relationTo","localized","components","Field","collection","on","hidden","config","broadcastsOverrides","slug","read","equals","edit","beforeDocumentControls","clientProps","buttonLabel","path","useAsTitle","hooks","beforeChange","data","operation"],"mappings":"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"}
|