payload-plugin-newsletter 0.3.2 → 0.4.5

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 (180) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/CLAUDE.md +31 -19
  3. package/dist/client.cjs +899 -0
  4. package/dist/client.cjs.map +1 -0
  5. package/dist/client.d.cts +52 -0
  6. package/dist/client.d.ts +52 -0
  7. package/dist/client.js +867 -0
  8. package/dist/client.js.map +1 -0
  9. package/dist/components.cjs +899 -0
  10. package/dist/components.cjs.map +1 -0
  11. package/dist/components.d.cts +4 -0
  12. package/dist/components.d.ts +4 -0
  13. package/dist/components.js +867 -0
  14. package/dist/components.js.map +1 -0
  15. package/dist/index.cjs +2004 -0
  16. package/dist/index.cjs.map +1 -0
  17. package/dist/index.d.cts +11 -0
  18. package/dist/index.d.ts +6 -5
  19. package/dist/index.js +1967 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/types.cjs +19 -0
  22. package/dist/types.cjs.map +1 -0
  23. package/dist/{types/index.d.ts → types.d.cts} +19 -17
  24. package/dist/types.d.ts +350 -0
  25. package/dist/types.js +1 -0
  26. package/dist/types.js.map +1 -0
  27. package/package.json +48 -25
  28. package/dist/.tsbuildinfo +0 -1
  29. package/dist/collections/NewsletterSettings.d.ts +0 -4
  30. package/dist/collections/NewsletterSettings.d.ts.map +0 -1
  31. package/dist/collections/Subscribers.d.ts +0 -4
  32. package/dist/collections/Subscribers.d.ts.map +0 -1
  33. package/dist/components/MagicLinkVerify.d.ts +0 -27
  34. package/dist/components/MagicLinkVerify.d.ts.map +0 -1
  35. package/dist/components/NewsletterForm.d.ts +0 -5
  36. package/dist/components/NewsletterForm.d.ts.map +0 -1
  37. package/dist/components/PreferencesForm.d.ts +0 -5
  38. package/dist/components/PreferencesForm.d.ts.map +0 -1
  39. package/dist/components/index.d.ts +0 -5
  40. package/dist/components/index.d.ts.map +0 -1
  41. package/dist/endpoints/index.d.ts +0 -4
  42. package/dist/endpoints/index.d.ts.map +0 -1
  43. package/dist/endpoints/preferences.d.ts +0 -5
  44. package/dist/endpoints/preferences.d.ts.map +0 -1
  45. package/dist/endpoints/subscribe.d.ts +0 -4
  46. package/dist/endpoints/subscribe.d.ts.map +0 -1
  47. package/dist/endpoints/unsubscribe.d.ts +0 -4
  48. package/dist/endpoints/unsubscribe.d.ts.map +0 -1
  49. package/dist/endpoints/verify-magic-link.d.ts +0 -4
  50. package/dist/endpoints/verify-magic-link.d.ts.map +0 -1
  51. package/dist/exports/client.d.ts +0 -6
  52. package/dist/exports/client.d.ts.map +0 -1
  53. package/dist/exports/components.d.ts +0 -2
  54. package/dist/exports/components.d.ts.map +0 -1
  55. package/dist/exports/types.d.ts +0 -2
  56. package/dist/exports/types.d.ts.map +0 -1
  57. package/dist/fields/newsletterScheduling.d.ts +0 -4
  58. package/dist/fields/newsletterScheduling.d.ts.map +0 -1
  59. package/dist/hooks/useNewsletterAuth.d.ts +0 -16
  60. package/dist/hooks/useNewsletterAuth.d.ts.map +0 -1
  61. package/dist/index.d.ts.map +0 -1
  62. package/dist/providers/broadcast.d.ts +0 -19
  63. package/dist/providers/broadcast.d.ts.map +0 -1
  64. package/dist/providers/index.d.ts +0 -23
  65. package/dist/providers/index.d.ts.map +0 -1
  66. package/dist/providers/resend.d.ts +0 -20
  67. package/dist/providers/resend.d.ts.map +0 -1
  68. package/dist/providers/types.d.ts +0 -46
  69. package/dist/providers/types.d.ts.map +0 -1
  70. package/dist/src/__tests__/fixtures/newsletter-settings.js +0 -41
  71. package/dist/src/__tests__/fixtures/newsletter-settings.js.map +0 -1
  72. package/dist/src/__tests__/fixtures/subscribers.js +0 -70
  73. package/dist/src/__tests__/fixtures/subscribers.js.map +0 -1
  74. package/dist/src/__tests__/integration/collections/subscriber-hooks.test.js +0 -356
  75. package/dist/src/__tests__/integration/collections/subscriber-hooks.test.js.map +0 -1
  76. package/dist/src/__tests__/integration/endpoints/preferences.test.js +0 -266
  77. package/dist/src/__tests__/integration/endpoints/preferences.test.js.map +0 -1
  78. package/dist/src/__tests__/integration/endpoints/subscribe.test.js +0 -280
  79. package/dist/src/__tests__/integration/endpoints/subscribe.test.js.map +0 -1
  80. package/dist/src/__tests__/integration/endpoints/unsubscribe.test.js +0 -187
  81. package/dist/src/__tests__/integration/endpoints/unsubscribe.test.js.map +0 -1
  82. package/dist/src/__tests__/integration/endpoints/verify-magic-link.test.js +0 -188
  83. package/dist/src/__tests__/integration/endpoints/verify-magic-link.test.js.map +0 -1
  84. package/dist/src/__tests__/mocks/email-providers.js +0 -153
  85. package/dist/src/__tests__/mocks/email-providers.js.map +0 -1
  86. package/dist/src/__tests__/mocks/payload.js +0 -244
  87. package/dist/src/__tests__/mocks/payload.js.map +0 -1
  88. package/dist/src/__tests__/security/csrf-protection.test.js +0 -309
  89. package/dist/src/__tests__/security/csrf-protection.test.js.map +0 -1
  90. package/dist/src/__tests__/security/settings-access.test.js +0 -204
  91. package/dist/src/__tests__/security/settings-access.test.js.map +0 -1
  92. package/dist/src/__tests__/security/subscriber-access.test.js +0 -210
  93. package/dist/src/__tests__/security/subscriber-access.test.js.map +0 -1
  94. package/dist/src/__tests__/security/xss-prevention.test.js +0 -305
  95. package/dist/src/__tests__/security/xss-prevention.test.js.map +0 -1
  96. package/dist/src/__tests__/setup/integration.setup.js +0 -38
  97. package/dist/src/__tests__/setup/integration.setup.js.map +0 -1
  98. package/dist/src/__tests__/setup/unit.setup.js +0 -41
  99. package/dist/src/__tests__/setup/unit.setup.js.map +0 -1
  100. package/dist/src/__tests__/unit/utils/access.test.js +0 -116
  101. package/dist/src/__tests__/unit/utils/access.test.js.map +0 -1
  102. package/dist/src/__tests__/unit/utils/jwt.test.js +0 -238
  103. package/dist/src/__tests__/unit/utils/jwt.test.js.map +0 -1
  104. package/dist/src/collections/NewsletterSettings.js +0 -390
  105. package/dist/src/collections/NewsletterSettings.js.map +0 -1
  106. package/dist/src/collections/Subscribers.js +0 -309
  107. package/dist/src/collections/Subscribers.js.map +0 -1
  108. package/dist/src/components/MagicLinkVerify.js +0 -180
  109. package/dist/src/components/MagicLinkVerify.js.map +0 -1
  110. package/dist/src/components/NewsletterForm.js +0 -326
  111. package/dist/src/components/NewsletterForm.js.map +0 -1
  112. package/dist/src/components/PreferencesForm.js +0 -524
  113. package/dist/src/components/PreferencesForm.js.map +0 -1
  114. package/dist/src/components/index.js +0 -5
  115. package/dist/src/components/index.js.map +0 -1
  116. package/dist/src/endpoints/index.js +0 -17
  117. package/dist/src/endpoints/index.js.map +0 -1
  118. package/dist/src/endpoints/preferences.js +0 -136
  119. package/dist/src/endpoints/preferences.js.map +0 -1
  120. package/dist/src/endpoints/subscribe.js +0 -151
  121. package/dist/src/endpoints/subscribe.js.map +0 -1
  122. package/dist/src/endpoints/unsubscribe.js +0 -105
  123. package/dist/src/endpoints/unsubscribe.js.map +0 -1
  124. package/dist/src/endpoints/verify-magic-link.js +0 -103
  125. package/dist/src/endpoints/verify-magic-link.js.map +0 -1
  126. package/dist/src/exports/client.js +0 -7
  127. package/dist/src/exports/client.js.map +0 -1
  128. package/dist/src/exports/components.js +0 -6
  129. package/dist/src/exports/components.js.map +0 -1
  130. package/dist/src/exports/types.js +0 -3
  131. package/dist/src/exports/types.js.map +0 -1
  132. package/dist/src/fields/newsletterScheduling.js +0 -195
  133. package/dist/src/fields/newsletterScheduling.js.map +0 -1
  134. package/dist/src/hooks/useNewsletterAuth.js +0 -112
  135. package/dist/src/hooks/useNewsletterAuth.js.map +0 -1
  136. package/dist/src/index.js +0 -130
  137. package/dist/src/index.js.map +0 -1
  138. package/dist/src/providers/broadcast.js +0 -158
  139. package/dist/src/providers/broadcast.js.map +0 -1
  140. package/dist/src/providers/index.js +0 -63
  141. package/dist/src/providers/index.js.map +0 -1
  142. package/dist/src/providers/resend.js +0 -122
  143. package/dist/src/providers/resend.js.map +0 -1
  144. package/dist/src/providers/types.js +0 -12
  145. package/dist/src/providers/types.js.map +0 -1
  146. package/dist/src/templates/BaseTemplate.js +0 -105
  147. package/dist/src/templates/BaseTemplate.js.map +0 -1
  148. package/dist/src/templates/MagicLinkTemplate.js +0 -178
  149. package/dist/src/templates/MagicLinkTemplate.js.map +0 -1
  150. package/dist/src/templates/NewsletterTemplate.js +0 -150
  151. package/dist/src/templates/NewsletterTemplate.js.map +0 -1
  152. package/dist/src/templates/WelcomeTemplate.js +0 -192
  153. package/dist/src/templates/WelcomeTemplate.js.map +0 -1
  154. package/dist/src/templates/index.js +0 -6
  155. package/dist/src/templates/index.js.map +0 -1
  156. package/dist/src/types/index.js +0 -3
  157. package/dist/src/types/index.js.map +0 -1
  158. package/dist/src/utils/access.js +0 -80
  159. package/dist/src/utils/access.js.map +0 -1
  160. package/dist/src/utils/jwt.js +0 -91
  161. package/dist/src/utils/jwt.js.map +0 -1
  162. package/dist/src/utils/validation.js +0 -74
  163. package/dist/src/utils/validation.js.map +0 -1
  164. package/dist/templates/BaseTemplate.d.ts +0 -45
  165. package/dist/templates/BaseTemplate.d.ts.map +0 -1
  166. package/dist/templates/MagicLinkTemplate.d.ts +0 -67
  167. package/dist/templates/MagicLinkTemplate.d.ts.map +0 -1
  168. package/dist/templates/NewsletterTemplate.d.ts +0 -112
  169. package/dist/templates/NewsletterTemplate.d.ts.map +0 -1
  170. package/dist/templates/WelcomeTemplate.d.ts +0 -55
  171. package/dist/templates/WelcomeTemplate.d.ts.map +0 -1
  172. package/dist/templates/index.d.ts +0 -7
  173. package/dist/templates/index.d.ts.map +0 -1
  174. package/dist/types/index.d.ts.map +0 -1
  175. package/dist/utils/access.d.ts +0 -15
  176. package/dist/utils/access.d.ts.map +0 -1
  177. package/dist/utils/jwt.d.ts +0 -32
  178. package/dist/utils/jwt.d.ts.map +0 -1
  179. package/dist/utils/validation.d.ts +0 -25
  180. package/dist/utils/validation.d.ts.map +0 -1
package/dist/index.cjs ADDED
@@ -0,0 +1,2004 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ default: () => newsletterPlugin,
34
+ newsletterPlugin: () => newsletterPlugin
35
+ });
36
+ module.exports = __toCommonJS(src_exports);
37
+
38
+ // src/utils/access.ts
39
+ var isAdmin = (user, config) => {
40
+ if (!user || user.collection !== "users") {
41
+ return false;
42
+ }
43
+ if (config?.access?.isAdmin) {
44
+ return config.access.isAdmin(user);
45
+ }
46
+ if (user.roles?.includes("admin")) {
47
+ return true;
48
+ }
49
+ if (user.isAdmin === true) {
50
+ return true;
51
+ }
52
+ if (user.role === "admin") {
53
+ return true;
54
+ }
55
+ if (user.admin === true) {
56
+ return true;
57
+ }
58
+ return false;
59
+ };
60
+ var adminOnly = (config) => ({ req }) => {
61
+ const user = req.user;
62
+ return isAdmin(user, config);
63
+ };
64
+ var adminOrSelf = (config) => ({ req, id }) => {
65
+ const user = req.user;
66
+ if (!user) {
67
+ if (!id) {
68
+ return {
69
+ id: {
70
+ equals: "unauthorized-no-access"
71
+ }
72
+ };
73
+ }
74
+ return false;
75
+ }
76
+ if (isAdmin(user, config)) {
77
+ return true;
78
+ }
79
+ if (user.collection === "subscribers") {
80
+ if (!id) {
81
+ return {
82
+ id: {
83
+ equals: user.id
84
+ }
85
+ };
86
+ }
87
+ return id === user.id;
88
+ }
89
+ if (!id) {
90
+ return {
91
+ id: {
92
+ equals: "unauthorized-no-access"
93
+ }
94
+ };
95
+ }
96
+ return false;
97
+ };
98
+
99
+ // src/collections/Subscribers.ts
100
+ var createSubscribersCollection = (pluginConfig) => {
101
+ const slug = pluginConfig.subscribersSlug || "subscribers";
102
+ const defaultFields = [
103
+ // Core fields
104
+ {
105
+ name: "email",
106
+ type: "email",
107
+ required: true,
108
+ unique: true,
109
+ admin: {
110
+ description: "Subscriber email address"
111
+ }
112
+ },
113
+ {
114
+ name: "name",
115
+ type: "text",
116
+ admin: {
117
+ description: "Subscriber full name"
118
+ }
119
+ },
120
+ {
121
+ name: "locale",
122
+ type: "select",
123
+ options: pluginConfig.i18n?.locales?.map((locale) => ({
124
+ label: locale.toUpperCase(),
125
+ value: locale
126
+ })) || [
127
+ { label: "EN", value: "en" }
128
+ ],
129
+ defaultValue: pluginConfig.i18n?.defaultLocale || "en",
130
+ admin: {
131
+ description: "Preferred language for communications"
132
+ }
133
+ },
134
+ // Authentication fields (hidden from admin UI)
135
+ {
136
+ name: "magicLinkToken",
137
+ type: "text",
138
+ hidden: true
139
+ },
140
+ {
141
+ name: "magicLinkTokenExpiry",
142
+ type: "date",
143
+ hidden: true
144
+ },
145
+ // Subscription status
146
+ {
147
+ name: "subscriptionStatus",
148
+ type: "select",
149
+ options: [
150
+ { label: "Active", value: "active" },
151
+ { label: "Unsubscribed", value: "unsubscribed" },
152
+ { label: "Pending", value: "pending" }
153
+ ],
154
+ defaultValue: "pending",
155
+ required: true,
156
+ admin: {
157
+ description: "Current subscription status"
158
+ }
159
+ },
160
+ {
161
+ name: "unsubscribedAt",
162
+ type: "date",
163
+ admin: {
164
+ condition: (data) => data?.subscriptionStatus === "unsubscribed",
165
+ description: "When the user unsubscribed",
166
+ readOnly: true
167
+ }
168
+ },
169
+ // Email preferences
170
+ {
171
+ name: "emailPreferences",
172
+ type: "group",
173
+ fields: [
174
+ {
175
+ name: "newsletter",
176
+ type: "checkbox",
177
+ defaultValue: true,
178
+ label: "Newsletter",
179
+ admin: {
180
+ description: "Receive regular newsletter updates"
181
+ }
182
+ },
183
+ {
184
+ name: "announcements",
185
+ type: "checkbox",
186
+ defaultValue: true,
187
+ label: "Announcements",
188
+ admin: {
189
+ description: "Receive important announcements"
190
+ }
191
+ }
192
+ ],
193
+ admin: {
194
+ description: "Email communication preferences"
195
+ }
196
+ },
197
+ // Source tracking
198
+ {
199
+ name: "source",
200
+ type: "text",
201
+ admin: {
202
+ description: "Where the subscriber signed up from"
203
+ }
204
+ }
205
+ ];
206
+ if (pluginConfig.features?.utmTracking?.enabled) {
207
+ const utmFields = pluginConfig.features.utmTracking.fields || [
208
+ "source",
209
+ "medium",
210
+ "campaign",
211
+ "content",
212
+ "term"
213
+ ];
214
+ defaultFields.push({
215
+ name: "utmParameters",
216
+ type: "group",
217
+ fields: utmFields.map((field) => ({
218
+ name: field,
219
+ type: "text",
220
+ admin: {
221
+ description: `UTM ${field} parameter`
222
+ }
223
+ })),
224
+ admin: {
225
+ description: "UTM tracking parameters"
226
+ }
227
+ });
228
+ }
229
+ defaultFields.push({
230
+ name: "signupMetadata",
231
+ type: "group",
232
+ fields: [
233
+ {
234
+ name: "ipAddress",
235
+ type: "text",
236
+ admin: {
237
+ readOnly: true
238
+ }
239
+ },
240
+ {
241
+ name: "userAgent",
242
+ type: "text",
243
+ admin: {
244
+ readOnly: true
245
+ }
246
+ },
247
+ {
248
+ name: "referrer",
249
+ type: "text",
250
+ admin: {
251
+ readOnly: true
252
+ }
253
+ },
254
+ {
255
+ name: "signupPage",
256
+ type: "text",
257
+ admin: {
258
+ readOnly: true
259
+ }
260
+ }
261
+ ],
262
+ admin: {
263
+ description: "Technical information about signup"
264
+ }
265
+ });
266
+ if (pluginConfig.features?.leadMagnets?.enabled) {
267
+ defaultFields.push({
268
+ name: "leadMagnet",
269
+ type: "relationship",
270
+ relationTo: pluginConfig.features.leadMagnets.collection || "media",
271
+ admin: {
272
+ description: "Lead magnet downloaded at signup"
273
+ }
274
+ });
275
+ }
276
+ let fields = defaultFields;
277
+ if (pluginConfig.fields?.overrides) {
278
+ fields = pluginConfig.fields.overrides({ defaultFields });
279
+ }
280
+ if (pluginConfig.fields?.additional) {
281
+ fields = [...fields, ...pluginConfig.fields.additional];
282
+ }
283
+ const subscribersCollection = {
284
+ slug,
285
+ labels: {
286
+ singular: "Subscriber",
287
+ plural: "Subscribers"
288
+ },
289
+ admin: {
290
+ useAsTitle: "email",
291
+ defaultColumns: ["email", "name", "subscriptionStatus", "createdAt"],
292
+ group: "Newsletter"
293
+ },
294
+ fields,
295
+ hooks: {
296
+ afterChange: [
297
+ async ({ doc, req, operation, previousDoc }) => {
298
+ if (operation === "create") {
299
+ const emailService = req.payload.newsletterEmailService;
300
+ if (emailService) {
301
+ try {
302
+ await emailService.addContact(doc);
303
+ } catch {
304
+ }
305
+ }
306
+ if (doc.subscriptionStatus === "active" && emailService) {
307
+ try {
308
+ } catch {
309
+ }
310
+ }
311
+ if (pluginConfig.hooks?.afterSubscribe) {
312
+ await pluginConfig.hooks.afterSubscribe({ doc, req });
313
+ }
314
+ }
315
+ if (operation === "update" && previousDoc) {
316
+ const emailService = req.payload.newsletterEmailService;
317
+ if (doc.subscriptionStatus !== previousDoc.subscriptionStatus && emailService) {
318
+ try {
319
+ await emailService.updateContact(doc);
320
+ } catch {
321
+ }
322
+ }
323
+ if (doc.subscriptionStatus === "unsubscribed" && previousDoc.subscriptionStatus !== "unsubscribed") {
324
+ doc.unsubscribedAt = (/* @__PURE__ */ new Date()).toISOString();
325
+ if (pluginConfig.hooks?.afterUnsubscribe) {
326
+ await pluginConfig.hooks.afterUnsubscribe({ doc, req });
327
+ }
328
+ }
329
+ }
330
+ }
331
+ ],
332
+ beforeDelete: [
333
+ async ({ id, req }) => {
334
+ const emailService = req.payload.newsletterEmailService;
335
+ if (emailService) {
336
+ try {
337
+ const doc = await req.payload.findByID({
338
+ collection: slug,
339
+ id
340
+ });
341
+ await emailService.removeContact(doc.email);
342
+ } catch {
343
+ }
344
+ }
345
+ }
346
+ ]
347
+ },
348
+ access: {
349
+ create: () => true,
350
+ // Public can subscribe
351
+ read: adminOrSelf(pluginConfig),
352
+ update: adminOrSelf(pluginConfig),
353
+ delete: adminOnly(pluginConfig)
354
+ },
355
+ timestamps: true
356
+ };
357
+ return subscribersCollection;
358
+ };
359
+
360
+ // src/collections/NewsletterSettings.ts
361
+ var createNewsletterSettingsCollection = (pluginConfig) => {
362
+ const slug = pluginConfig.settingsSlug || "newsletter-settings";
363
+ return {
364
+ slug,
365
+ labels: {
366
+ singular: "Newsletter Setting",
367
+ plural: "Newsletter Settings"
368
+ },
369
+ admin: {
370
+ useAsTitle: "name",
371
+ defaultColumns: ["name", "provider", "active", "updatedAt"],
372
+ group: "Newsletter",
373
+ description: "Configure email provider settings and templates"
374
+ },
375
+ fields: [
376
+ {
377
+ name: "name",
378
+ type: "text",
379
+ label: "Configuration Name",
380
+ required: true,
381
+ admin: {
382
+ description: 'A descriptive name for this configuration (e.g., "Production", "Development", "Marketing Emails")'
383
+ }
384
+ },
385
+ {
386
+ name: "active",
387
+ type: "checkbox",
388
+ label: "Active",
389
+ defaultValue: false,
390
+ admin: {
391
+ description: "Only one configuration can be active at a time"
392
+ }
393
+ },
394
+ {
395
+ type: "tabs",
396
+ tabs: [
397
+ {
398
+ label: "Provider Settings",
399
+ fields: [
400
+ {
401
+ name: "provider",
402
+ type: "select",
403
+ label: "Email Provider",
404
+ required: true,
405
+ options: [
406
+ { label: "Resend", value: "resend" },
407
+ { label: "Broadcast (Self-Hosted)", value: "broadcast" }
408
+ ],
409
+ defaultValue: pluginConfig.providers.default,
410
+ admin: {
411
+ description: "Choose which email service to use"
412
+ }
413
+ },
414
+ {
415
+ name: "resendSettings",
416
+ type: "group",
417
+ label: "Resend Settings",
418
+ admin: {
419
+ condition: (data) => data?.provider === "resend"
420
+ },
421
+ fields: [
422
+ {
423
+ name: "apiKey",
424
+ type: "text",
425
+ label: "API Key",
426
+ required: true,
427
+ admin: {
428
+ description: "Your Resend API key"
429
+ }
430
+ },
431
+ {
432
+ name: "audienceIds",
433
+ type: "array",
434
+ label: "Audience IDs by Locale",
435
+ fields: [
436
+ {
437
+ name: "locale",
438
+ type: "select",
439
+ label: "Locale",
440
+ required: true,
441
+ options: pluginConfig.i18n?.locales?.map((locale) => ({
442
+ label: locale.toUpperCase(),
443
+ value: locale
444
+ })) || [
445
+ { label: "EN", value: "en" }
446
+ ]
447
+ },
448
+ {
449
+ name: "production",
450
+ type: "text",
451
+ label: "Production Audience ID"
452
+ },
453
+ {
454
+ name: "development",
455
+ type: "text",
456
+ label: "Development Audience ID"
457
+ }
458
+ ]
459
+ }
460
+ ]
461
+ },
462
+ {
463
+ name: "broadcastSettings",
464
+ type: "group",
465
+ label: "Broadcast Settings",
466
+ admin: {
467
+ condition: (data) => data?.provider === "broadcast"
468
+ },
469
+ fields: [
470
+ {
471
+ name: "apiUrl",
472
+ type: "text",
473
+ label: "API URL",
474
+ required: true,
475
+ admin: {
476
+ description: "Your Broadcast instance URL"
477
+ }
478
+ },
479
+ {
480
+ name: "productionToken",
481
+ type: "text",
482
+ label: "Production Token",
483
+ admin: {
484
+ description: "Token for production environment"
485
+ }
486
+ },
487
+ {
488
+ name: "developmentToken",
489
+ type: "text",
490
+ label: "Development Token",
491
+ admin: {
492
+ description: "Token for development environment"
493
+ }
494
+ }
495
+ ]
496
+ },
497
+ {
498
+ name: "fromAddress",
499
+ type: "email",
500
+ label: "From Address",
501
+ required: true,
502
+ admin: {
503
+ description: "Default sender email address"
504
+ }
505
+ },
506
+ {
507
+ name: "fromName",
508
+ type: "text",
509
+ label: "From Name",
510
+ required: true,
511
+ admin: {
512
+ description: "Default sender name"
513
+ }
514
+ },
515
+ {
516
+ name: "replyTo",
517
+ type: "email",
518
+ label: "Reply-To Address",
519
+ admin: {
520
+ description: "Optional reply-to email address"
521
+ }
522
+ }
523
+ ]
524
+ },
525
+ {
526
+ label: "Email Templates",
527
+ fields: [
528
+ {
529
+ name: "emailTemplates",
530
+ type: "group",
531
+ label: "Email Templates",
532
+ fields: [
533
+ {
534
+ name: "welcome",
535
+ type: "group",
536
+ label: "Welcome Email",
537
+ fields: [
538
+ {
539
+ name: "enabled",
540
+ type: "checkbox",
541
+ label: "Send Welcome Email",
542
+ defaultValue: true
543
+ },
544
+ {
545
+ name: "subject",
546
+ type: "text",
547
+ label: "Subject Line",
548
+ defaultValue: "Welcome to {{fromName}}!",
549
+ admin: {
550
+ condition: (data) => data?.emailTemplates?.welcome?.enabled
551
+ }
552
+ },
553
+ {
554
+ name: "preheader",
555
+ type: "text",
556
+ label: "Preheader Text",
557
+ admin: {
558
+ condition: (data) => data?.emailTemplates?.welcome?.enabled
559
+ }
560
+ }
561
+ ]
562
+ },
563
+ {
564
+ name: "magicLink",
565
+ type: "group",
566
+ label: "Magic Link Email",
567
+ fields: [
568
+ {
569
+ name: "subject",
570
+ type: "text",
571
+ label: "Subject Line",
572
+ defaultValue: "Sign in to {{fromName}}"
573
+ },
574
+ {
575
+ name: "preheader",
576
+ type: "text",
577
+ label: "Preheader Text",
578
+ defaultValue: "Click the link to access your preferences"
579
+ },
580
+ {
581
+ name: "expirationTime",
582
+ type: "select",
583
+ label: "Link Expiration",
584
+ defaultValue: "7d",
585
+ options: [
586
+ { label: "1 hour", value: "1h" },
587
+ { label: "24 hours", value: "24h" },
588
+ { label: "7 days", value: "7d" },
589
+ { label: "30 days", value: "30d" }
590
+ ]
591
+ }
592
+ ]
593
+ }
594
+ ]
595
+ }
596
+ ]
597
+ },
598
+ {
599
+ label: "Subscription Settings",
600
+ fields: [
601
+ {
602
+ name: "subscriptionSettings",
603
+ type: "group",
604
+ label: "Subscription Settings",
605
+ fields: [
606
+ {
607
+ name: "requireDoubleOptIn",
608
+ type: "checkbox",
609
+ label: "Require Double Opt-In",
610
+ defaultValue: false,
611
+ admin: {
612
+ description: "Require email confirmation before activating subscriptions"
613
+ }
614
+ },
615
+ {
616
+ name: "allowedDomains",
617
+ type: "array",
618
+ label: "Allowed Email Domains",
619
+ admin: {
620
+ description: "Leave empty to allow all domains"
621
+ },
622
+ fields: [
623
+ {
624
+ name: "domain",
625
+ type: "text",
626
+ label: "Domain",
627
+ required: true,
628
+ admin: {
629
+ placeholder: "example.com"
630
+ }
631
+ }
632
+ ]
633
+ },
634
+ {
635
+ name: "maxSubscribersPerIP",
636
+ type: "number",
637
+ label: "Max Subscribers per IP",
638
+ defaultValue: 10,
639
+ min: 1,
640
+ admin: {
641
+ description: "Maximum number of subscriptions allowed from a single IP address"
642
+ }
643
+ }
644
+ ]
645
+ }
646
+ ]
647
+ }
648
+ ]
649
+ }
650
+ ],
651
+ hooks: {
652
+ beforeChange: [
653
+ async ({ data, req, operation }) => {
654
+ if (!req.user || req.user.collection !== "users") {
655
+ throw new Error("Only administrators can modify newsletter settings");
656
+ }
657
+ if (data?.active && operation !== "create") {
658
+ await req.payload.update({
659
+ collection: slug,
660
+ where: {
661
+ id: {
662
+ not_equals: data.id
663
+ }
664
+ },
665
+ data: {
666
+ active: false
667
+ }
668
+ // Keep overrideAccess: true for admin operations after verification
669
+ });
670
+ }
671
+ if (operation === "create" && data?.active) {
672
+ const existingActive = await req.payload.find({
673
+ collection: slug,
674
+ where: {
675
+ active: {
676
+ equals: true
677
+ }
678
+ }
679
+ // Keep overrideAccess: true for admin operations
680
+ });
681
+ if (existingActive.docs.length > 0) {
682
+ for (const doc of existingActive.docs) {
683
+ await req.payload.update({
684
+ collection: slug,
685
+ id: doc.id,
686
+ data: {
687
+ active: false
688
+ }
689
+ // Keep overrideAccess: true for admin operations
690
+ });
691
+ }
692
+ }
693
+ }
694
+ return data;
695
+ }
696
+ ],
697
+ afterChange: [
698
+ async ({ doc, req }) => {
699
+ if (req.payload.newsletterEmailService && doc.active) {
700
+ try {
701
+ console.warn("Newsletter settings updated, reinitializing service...");
702
+ } catch {
703
+ }
704
+ }
705
+ return doc;
706
+ }
707
+ ]
708
+ },
709
+ access: {
710
+ read: () => true,
711
+ // Settings can be read publicly for validation
712
+ create: adminOnly(pluginConfig),
713
+ update: adminOnly(pluginConfig),
714
+ delete: adminOnly(pluginConfig)
715
+ },
716
+ timestamps: true
717
+ };
718
+ };
719
+
720
+ // src/providers/resend.ts
721
+ var import_resend = require("resend");
722
+
723
+ // src/providers/types.ts
724
+ var EmailProviderError = class extends Error {
725
+ constructor(message, provider, originalError) {
726
+ super(message);
727
+ this.name = "EmailProviderError";
728
+ this.provider = provider;
729
+ this.originalError = originalError;
730
+ }
731
+ };
732
+
733
+ // src/providers/resend.ts
734
+ var ResendProvider = class {
735
+ constructor(config) {
736
+ this.client = new import_resend.Resend(config.apiKey);
737
+ this.audienceIds = config.audienceIds || {};
738
+ this.fromAddress = config.fromAddress;
739
+ this.fromName = config.fromName;
740
+ this.isDevelopment = process.env.NODE_ENV !== "production";
741
+ }
742
+ getProvider() {
743
+ return "resend";
744
+ }
745
+ async send(params) {
746
+ try {
747
+ const from = params.from || {
748
+ email: this.fromAddress,
749
+ name: this.fromName
750
+ };
751
+ if (!params.html && !params.text) {
752
+ throw new Error("Either html or text content is required");
753
+ }
754
+ await this.client.emails.send({
755
+ from: `${from.name} <${from.email}>`,
756
+ to: Array.isArray(params.to) ? params.to : [params.to],
757
+ subject: params.subject,
758
+ html: params.html || "",
759
+ text: params.text,
760
+ replyTo: params.replyTo
761
+ });
762
+ } catch (error) {
763
+ throw new EmailProviderError(
764
+ `Failed to send email via Resend: ${error instanceof Error ? error.message : "Unknown error"}`,
765
+ "resend",
766
+ error
767
+ );
768
+ }
769
+ }
770
+ async addContact(contact) {
771
+ try {
772
+ const audienceId = this.getAudienceId(contact.locale);
773
+ if (!audienceId) {
774
+ console.warn(`No audience ID configured for locale: ${contact.locale}`);
775
+ return;
776
+ }
777
+ await this.client.contacts.create({
778
+ email: contact.email,
779
+ firstName: contact.name?.split(" ")[0],
780
+ lastName: contact.name?.split(" ").slice(1).join(" "),
781
+ unsubscribed: contact.subscriptionStatus === "unsubscribed",
782
+ audienceId
783
+ });
784
+ } catch (error) {
785
+ throw new EmailProviderError(
786
+ `Failed to add contact to Resend: ${error instanceof Error ? error.message : "Unknown error"}`,
787
+ "resend",
788
+ error
789
+ );
790
+ }
791
+ }
792
+ async updateContact(contact) {
793
+ try {
794
+ const audienceId = this.getAudienceId(contact.locale);
795
+ if (!audienceId) {
796
+ console.warn(`No audience ID configured for locale: ${contact.locale}`);
797
+ return;
798
+ }
799
+ const contacts = await this.client.contacts.list({ audienceId });
800
+ const existingContact = contacts.data?.data?.find((c) => c.email === contact.email);
801
+ if (existingContact) {
802
+ await this.client.contacts.update({
803
+ id: existingContact.id,
804
+ audienceId,
805
+ firstName: contact.name?.split(" ")[0],
806
+ lastName: contact.name?.split(" ").slice(1).join(" "),
807
+ unsubscribed: contact.subscriptionStatus === "unsubscribed"
808
+ });
809
+ } else {
810
+ await this.addContact(contact);
811
+ }
812
+ } catch (error) {
813
+ throw new EmailProviderError(
814
+ `Failed to update contact in Resend: ${error instanceof Error ? error.message : "Unknown error"}`,
815
+ "resend",
816
+ error
817
+ );
818
+ }
819
+ }
820
+ async removeContact(email) {
821
+ try {
822
+ for (const locale in this.audienceIds) {
823
+ const audienceId = this.getAudienceId(locale);
824
+ if (!audienceId) continue;
825
+ const contacts = await this.client.contacts.list({ audienceId });
826
+ const contact = contacts.data?.data?.find((c) => c.email === email);
827
+ if (contact) {
828
+ await this.client.contacts.update({
829
+ id: contact.id,
830
+ audienceId,
831
+ unsubscribed: true
832
+ });
833
+ break;
834
+ }
835
+ }
836
+ } catch (error) {
837
+ throw new EmailProviderError(
838
+ `Failed to remove contact from Resend: ${error instanceof Error ? error.message : "Unknown error"}`,
839
+ "resend",
840
+ error
841
+ );
842
+ }
843
+ }
844
+ getAudienceId(locale) {
845
+ const localeKey = locale || "en";
846
+ if (!this.audienceIds) return void 0;
847
+ const localeConfig = this.audienceIds[localeKey];
848
+ if (!localeConfig) return void 0;
849
+ const audienceId = this.isDevelopment ? localeConfig.development || localeConfig.production : localeConfig.production || localeConfig.development;
850
+ return audienceId;
851
+ }
852
+ };
853
+
854
+ // src/providers/broadcast.ts
855
+ var BroadcastProvider = class {
856
+ constructor(config) {
857
+ this.apiUrl = config.apiUrl.replace(/\/$/, "");
858
+ this.isDevelopment = process.env.NODE_ENV !== "production";
859
+ this.token = this.isDevelopment ? config.tokens.development || config.tokens.production || "" : config.tokens.production || config.tokens.development || "";
860
+ this.fromAddress = config.fromAddress;
861
+ this.fromName = config.fromName;
862
+ }
863
+ getProvider() {
864
+ return "broadcast";
865
+ }
866
+ async send(params) {
867
+ try {
868
+ const from = params.from || {
869
+ email: this.fromAddress,
870
+ name: this.fromName
871
+ };
872
+ const recipients = Array.isArray(params.to) ? params.to : [params.to];
873
+ const response = await fetch(`${this.apiUrl}/api/v1/emails`, {
874
+ method: "POST",
875
+ headers: {
876
+ "Authorization": `Bearer ${this.token}`,
877
+ "Content-Type": "application/json"
878
+ },
879
+ body: JSON.stringify({
880
+ from_email: from.email,
881
+ from_name: from.name,
882
+ to: recipients,
883
+ subject: params.subject,
884
+ html_body: params.html,
885
+ text_body: params.text,
886
+ reply_to: params.replyTo
887
+ })
888
+ });
889
+ if (!response.ok) {
890
+ const error = await response.text();
891
+ throw new Error(`Broadcast API error: ${response.status} - ${error}`);
892
+ }
893
+ } catch (error) {
894
+ throw new EmailProviderError(
895
+ `Failed to send email via Broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
896
+ "broadcast",
897
+ error
898
+ );
899
+ }
900
+ }
901
+ async addContact(contact) {
902
+ try {
903
+ const response = await fetch(`${this.apiUrl}/api/v1/contacts`, {
904
+ method: "POST",
905
+ headers: {
906
+ "Authorization": `Bearer ${this.token}`,
907
+ "Content-Type": "application/json"
908
+ },
909
+ body: JSON.stringify({
910
+ email: contact.email,
911
+ name: contact.name,
912
+ status: contact.subscriptionStatus === "active" ? "subscribed" : "unsubscribed",
913
+ metadata: {
914
+ locale: contact.locale,
915
+ source: contact.source,
916
+ ...contact.utmParameters
917
+ }
918
+ })
919
+ });
920
+ if (!response.ok) {
921
+ const error = await response.text();
922
+ throw new Error(`Broadcast API error: ${response.status} - ${error}`);
923
+ }
924
+ } catch (error) {
925
+ throw new EmailProviderError(
926
+ `Failed to add contact to Broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
927
+ "broadcast",
928
+ error
929
+ );
930
+ }
931
+ }
932
+ async updateContact(contact) {
933
+ try {
934
+ const searchResponse = await fetch(
935
+ `${this.apiUrl}/api/v1/contacts?email=${encodeURIComponent(contact.email)}`,
936
+ {
937
+ headers: {
938
+ "Authorization": `Bearer ${this.token}`
939
+ }
940
+ }
941
+ );
942
+ if (!searchResponse.ok) {
943
+ await this.addContact(contact);
944
+ return;
945
+ }
946
+ const contacts = await searchResponse.json();
947
+ const existingContact = contacts.data?.[0];
948
+ if (!existingContact) {
949
+ await this.addContact(contact);
950
+ return;
951
+ }
952
+ const response = await fetch(`${this.apiUrl}/api/v1/contacts/${existingContact.id}`, {
953
+ method: "PUT",
954
+ headers: {
955
+ "Authorization": `Bearer ${this.token}`,
956
+ "Content-Type": "application/json"
957
+ },
958
+ body: JSON.stringify({
959
+ email: contact.email,
960
+ name: contact.name,
961
+ status: contact.subscriptionStatus === "active" ? "subscribed" : "unsubscribed",
962
+ metadata: {
963
+ locale: contact.locale,
964
+ source: contact.source,
965
+ ...contact.utmParameters
966
+ }
967
+ })
968
+ });
969
+ if (!response.ok) {
970
+ const error = await response.text();
971
+ throw new Error(`Broadcast API error: ${response.status} - ${error}`);
972
+ }
973
+ } catch (error) {
974
+ throw new EmailProviderError(
975
+ `Failed to update contact in Broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
976
+ "broadcast",
977
+ error
978
+ );
979
+ }
980
+ }
981
+ async removeContact(email) {
982
+ try {
983
+ const searchResponse = await fetch(
984
+ `${this.apiUrl}/api/v1/contacts?email=${encodeURIComponent(email)}`,
985
+ {
986
+ headers: {
987
+ "Authorization": `Bearer ${this.token}`
988
+ }
989
+ }
990
+ );
991
+ if (!searchResponse.ok) {
992
+ return;
993
+ }
994
+ const contacts = await searchResponse.json();
995
+ const contact = contacts.data?.[0];
996
+ if (!contact) {
997
+ return;
998
+ }
999
+ const response = await fetch(`${this.apiUrl}/api/v1/contacts/${contact.id}`, {
1000
+ method: "DELETE",
1001
+ headers: {
1002
+ "Authorization": `Bearer ${this.token}`
1003
+ }
1004
+ });
1005
+ if (!response.ok) {
1006
+ const error = await response.text();
1007
+ throw new Error(`Broadcast API error: ${response.status} - ${error}`);
1008
+ }
1009
+ } catch (error) {
1010
+ throw new EmailProviderError(
1011
+ `Failed to remove contact from Broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
1012
+ "broadcast",
1013
+ error
1014
+ );
1015
+ }
1016
+ }
1017
+ };
1018
+
1019
+ // src/providers/index.ts
1020
+ var EmailService = class {
1021
+ constructor(config) {
1022
+ this.provider = this.createProvider(config);
1023
+ }
1024
+ createProvider(config) {
1025
+ const baseConfig = {
1026
+ fromAddress: config.fromAddress,
1027
+ fromName: config.fromName
1028
+ };
1029
+ switch (config.provider) {
1030
+ case "resend":
1031
+ if (!config.resend) {
1032
+ throw new Error("Resend configuration is required when using Resend provider");
1033
+ }
1034
+ return new ResendProvider({
1035
+ ...config.resend,
1036
+ ...baseConfig
1037
+ });
1038
+ case "broadcast":
1039
+ if (!config.broadcast) {
1040
+ throw new Error("Broadcast configuration is required when using Broadcast provider");
1041
+ }
1042
+ return new BroadcastProvider({
1043
+ ...config.broadcast,
1044
+ ...baseConfig
1045
+ });
1046
+ default:
1047
+ throw new Error(`Unknown email provider: ${config.provider}`);
1048
+ }
1049
+ }
1050
+ async send(params) {
1051
+ return this.provider.send(params);
1052
+ }
1053
+ async addContact(contact) {
1054
+ return this.provider.addContact(contact);
1055
+ }
1056
+ async updateContact(contact) {
1057
+ return this.provider.updateContact(contact);
1058
+ }
1059
+ async removeContact(email) {
1060
+ return this.provider.removeContact(email);
1061
+ }
1062
+ getProvider() {
1063
+ return this.provider.getProvider();
1064
+ }
1065
+ /**
1066
+ * Update the provider configuration
1067
+ * Useful when settings are changed in the admin UI
1068
+ */
1069
+ updateConfig(config) {
1070
+ this.provider = this.createProvider(config);
1071
+ }
1072
+ };
1073
+ function createEmailService(config) {
1074
+ return new EmailService(config);
1075
+ }
1076
+
1077
+ // src/utils/validation.ts
1078
+ var import_isomorphic_dompurify = __toESM(require("isomorphic-dompurify"), 1);
1079
+ function isValidEmail(email) {
1080
+ if (!email || typeof email !== "string") return false;
1081
+ const trimmed = email.trim();
1082
+ if (trimmed.length > 255) return false;
1083
+ if (trimmed.includes("<") || trimmed.includes(">")) return false;
1084
+ if (trimmed.includes("javascript:")) return false;
1085
+ if (trimmed.includes("data:")) return false;
1086
+ const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
1087
+ if (!emailRegex.test(trimmed)) return false;
1088
+ const parts = trimmed.split("@");
1089
+ if (parts.length !== 2) return false;
1090
+ const [localPart, domain] = parts;
1091
+ if (localPart.length > 64 || localPart.length === 0) return false;
1092
+ if (localPart.startsWith(".") || localPart.endsWith(".")) return false;
1093
+ if (domain.startsWith(".") || domain.endsWith(".")) return false;
1094
+ if (domain.includes("..")) return false;
1095
+ if (localPart.includes("..")) return false;
1096
+ return true;
1097
+ }
1098
+ function isDomainAllowed(email, allowedDomains) {
1099
+ if (!isValidEmail(email)) {
1100
+ return false;
1101
+ }
1102
+ if (!allowedDomains || allowedDomains.length === 0) {
1103
+ return true;
1104
+ }
1105
+ const domain = email.split("@")[1]?.toLowerCase();
1106
+ if (!domain) return false;
1107
+ return allowedDomains.some(
1108
+ (allowedDomain) => domain === allowedDomain.toLowerCase()
1109
+ );
1110
+ }
1111
+ function sanitizeInput(input) {
1112
+ if (!input) return "";
1113
+ let cleaned = import_isomorphic_dompurify.default.sanitize(input, {
1114
+ ALLOWED_TAGS: [],
1115
+ ALLOWED_ATTR: [],
1116
+ KEEP_CONTENT: true
1117
+ });
1118
+ cleaned = cleaned.replace(/javascript:/gi, "").replace(/data:/gi, "").replace(/vbscript:/gi, "").replace(/file:\/\//gi, "").replace(/onload/gi, "").replace(/onerror/gi, "").replace(/onclick/gi, "").replace(/onmouseover/gi, "").replace(/alert\(/gi, "").replace(/prompt\(/gi, "").replace(/confirm\(/gi, "").replace(/\|/g, "").replace(/;/g, "").replace(/`/g, "").replace(/&&/g, "").replace(/\$\(/g, "").replace(/\.\./g, "").replace(/\/..\//g, "").replace(/\0/g, "");
1119
+ return cleaned.trim();
1120
+ }
1121
+ function extractUTMParams(searchParams) {
1122
+ const utmParams = {};
1123
+ const utmKeys = ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"];
1124
+ utmKeys.forEach((key) => {
1125
+ const value = searchParams.get(key);
1126
+ if (value) {
1127
+ const shortKey = key.replace("utm_", "");
1128
+ utmParams[shortKey] = value;
1129
+ }
1130
+ });
1131
+ return utmParams;
1132
+ }
1133
+ function isValidSource(source) {
1134
+ if (!source || typeof source !== "string") return false;
1135
+ const allowedSources = [
1136
+ "website",
1137
+ "api",
1138
+ "import",
1139
+ "admin",
1140
+ "signup-form",
1141
+ "magic-link",
1142
+ "preferences",
1143
+ "external"
1144
+ ];
1145
+ return allowedSources.includes(source);
1146
+ }
1147
+ function validateSubscriberData(data) {
1148
+ const errors = [];
1149
+ if (!data.email) {
1150
+ errors.push("Email is required");
1151
+ } else if (!isValidEmail(data.email)) {
1152
+ errors.push("Invalid email format");
1153
+ }
1154
+ if (data.name && data.name.length > 100) {
1155
+ errors.push("Name is too long (max 100 characters)");
1156
+ }
1157
+ if (data.source !== void 0) {
1158
+ if (!data.source || data.source.length === 0) {
1159
+ errors.push("Source cannot be empty");
1160
+ } else if (data.source.length > 50) {
1161
+ errors.push("Source is too long (max 50 characters)");
1162
+ } else if (!isValidSource(data.source)) {
1163
+ errors.push("Invalid source value");
1164
+ }
1165
+ }
1166
+ return {
1167
+ valid: errors.length === 0,
1168
+ errors
1169
+ };
1170
+ }
1171
+
1172
+ // src/endpoints/subscribe.ts
1173
+ var createSubscribeEndpoint = (config) => {
1174
+ return {
1175
+ path: "/newsletter/subscribe",
1176
+ method: "post",
1177
+ handler: async (req, res) => {
1178
+ try {
1179
+ const {
1180
+ email,
1181
+ name,
1182
+ source,
1183
+ preferences,
1184
+ leadMagnet,
1185
+ surveyResponses,
1186
+ metadata = {}
1187
+ } = req.body;
1188
+ const trimmedEmail = email?.trim();
1189
+ const validation = validateSubscriberData({ email: trimmedEmail, name, source });
1190
+ if (!validation.valid) {
1191
+ return res.status(400).json({
1192
+ success: false,
1193
+ errors: validation.errors
1194
+ });
1195
+ }
1196
+ const settingsResult = await req.payload.find({
1197
+ collection: config.settingsSlug || "newsletter-settings",
1198
+ where: {
1199
+ active: {
1200
+ equals: true
1201
+ }
1202
+ },
1203
+ limit: 1,
1204
+ overrideAccess: false
1205
+ // No user context for public endpoint
1206
+ });
1207
+ const settings = settingsResult.docs[0];
1208
+ const allowedDomains = settings?.subscriptionSettings?.allowedDomains?.map((d) => d.domain) || [];
1209
+ if (!isDomainAllowed(trimmedEmail, allowedDomains)) {
1210
+ return res.status(400).json({
1211
+ success: false,
1212
+ error: "Email domain not allowed"
1213
+ });
1214
+ }
1215
+ const existing = await req.payload.find({
1216
+ collection: config.subscribersSlug || "subscribers",
1217
+ where: {
1218
+ email: {
1219
+ equals: trimmedEmail.toLowerCase()
1220
+ }
1221
+ },
1222
+ overrideAccess: true
1223
+ // Need to check for duplicates in public endpoint
1224
+ });
1225
+ if (existing.docs.length > 0) {
1226
+ const subscriber2 = existing.docs[0];
1227
+ if (subscriber2.subscriptionStatus === "unsubscribed") {
1228
+ return res.status(400).json({
1229
+ success: false,
1230
+ error: "This email has been unsubscribed. Please contact support to resubscribe."
1231
+ });
1232
+ }
1233
+ return res.status(400).json({
1234
+ success: false,
1235
+ error: "Already subscribed",
1236
+ subscriber: {
1237
+ id: subscriber2.id,
1238
+ email: subscriber2.email,
1239
+ subscriptionStatus: subscriber2.subscriptionStatus
1240
+ }
1241
+ });
1242
+ }
1243
+ const ipAddress = req.ip || req.connection.remoteAddress;
1244
+ const maxPerIP = settings?.subscriptionSettings?.maxSubscribersPerIP || 10;
1245
+ const ipSubscribers = await req.payload.find({
1246
+ collection: config.subscribersSlug || "subscribers",
1247
+ where: {
1248
+ "signupMetadata.ipAddress": {
1249
+ equals: ipAddress
1250
+ }
1251
+ },
1252
+ overrideAccess: true
1253
+ // Need to check IP limits in public endpoint
1254
+ });
1255
+ if (ipSubscribers.docs.length >= maxPerIP) {
1256
+ return res.status(429).json({
1257
+ success: false,
1258
+ error: "Too many subscriptions from this IP address"
1259
+ });
1260
+ }
1261
+ const referer = req.headers.referer || req.headers.referrer || "";
1262
+ let utmParams = {};
1263
+ if (referer) {
1264
+ try {
1265
+ utmParams = extractUTMParams(new URL(referer).searchParams);
1266
+ } catch {
1267
+ }
1268
+ }
1269
+ const subscriberData = {
1270
+ email: trimmedEmail.toLowerCase(),
1271
+ name: name ? sanitizeInput(name) : void 0,
1272
+ locale: metadata.locale || config.i18n?.defaultLocale || "en",
1273
+ subscriptionStatus: settings?.subscriptionSettings?.requireDoubleOptIn ? "pending" : "active",
1274
+ source: source || "api",
1275
+ emailPreferences: {
1276
+ newsletter: true,
1277
+ announcements: true,
1278
+ ...preferences || {}
1279
+ },
1280
+ signupMetadata: {
1281
+ ipAddress,
1282
+ userAgent: req.headers["user-agent"],
1283
+ referrer: referer,
1284
+ signupPage: metadata.signupPage || referer
1285
+ }
1286
+ };
1287
+ if (config.features?.utmTracking?.enabled && Object.keys(utmParams).length > 0) {
1288
+ subscriberData.utmParameters = utmParams;
1289
+ }
1290
+ if (config.features?.leadMagnets?.enabled && leadMagnet) {
1291
+ subscriberData.leadMagnet = leadMagnet;
1292
+ }
1293
+ const subscriber = await req.payload.create({
1294
+ collection: config.subscribersSlug || "subscribers",
1295
+ data: subscriberData,
1296
+ overrideAccess: true
1297
+ // Public endpoint needs to create subscribers
1298
+ });
1299
+ if (config.features?.surveys?.enabled && surveyResponses) {
1300
+ }
1301
+ if (settings?.subscriptionSettings?.requireDoubleOptIn) {
1302
+ }
1303
+ res.json({
1304
+ success: true,
1305
+ subscriber: {
1306
+ id: subscriber.id,
1307
+ email: subscriber.email,
1308
+ subscriptionStatus: subscriber.subscriptionStatus
1309
+ },
1310
+ message: settings?.subscriptionSettings?.requireDoubleOptIn ? "Please check your email to confirm your subscription" : "Successfully subscribed"
1311
+ });
1312
+ } catch {
1313
+ res.status(500).json({
1314
+ success: false,
1315
+ error: "Failed to subscribe. Please try again."
1316
+ });
1317
+ }
1318
+ }
1319
+ };
1320
+ };
1321
+
1322
+ // src/utils/jwt.ts
1323
+ var import_jsonwebtoken = __toESM(require("jsonwebtoken"), 1);
1324
+ function getJWTSecret() {
1325
+ const secret = process.env.JWT_SECRET || process.env.PAYLOAD_SECRET;
1326
+ if (!secret) {
1327
+ console.warn(
1328
+ "WARNING: No JWT_SECRET or PAYLOAD_SECRET found in environment variables. Magic link authentication will not work properly. Please set JWT_SECRET in your environment."
1329
+ );
1330
+ return "INSECURE_DEVELOPMENT_SECRET_PLEASE_SET_JWT_SECRET";
1331
+ }
1332
+ return secret;
1333
+ }
1334
+ function verifyMagicLinkToken(token) {
1335
+ try {
1336
+ const payload = import_jsonwebtoken.default.verify(token, getJWTSecret(), {
1337
+ issuer: "payload-newsletter-plugin"
1338
+ });
1339
+ if (payload.type !== "magic-link") {
1340
+ throw new Error("Invalid token type");
1341
+ }
1342
+ return payload;
1343
+ } catch (error) {
1344
+ if (error instanceof Error && error.name === "TokenExpiredError") {
1345
+ throw new Error("Magic link has expired. Please request a new one.");
1346
+ }
1347
+ if (error instanceof Error && error.name === "JsonWebTokenError") {
1348
+ throw new Error("Invalid magic link token");
1349
+ }
1350
+ throw error;
1351
+ }
1352
+ }
1353
+ function generateSessionToken(subscriberId, email) {
1354
+ const payload = {
1355
+ subscriberId,
1356
+ email,
1357
+ type: "session"
1358
+ };
1359
+ return import_jsonwebtoken.default.sign(payload, getJWTSecret(), {
1360
+ expiresIn: "30d",
1361
+ issuer: "payload-newsletter-plugin"
1362
+ });
1363
+ }
1364
+ function verifySessionToken(token) {
1365
+ try {
1366
+ const payload = import_jsonwebtoken.default.verify(token, getJWTSecret(), {
1367
+ issuer: "payload-newsletter-plugin"
1368
+ });
1369
+ if (payload.type !== "session") {
1370
+ throw new Error("Invalid token type");
1371
+ }
1372
+ return payload;
1373
+ } catch (error) {
1374
+ if (error instanceof Error && error.name === "TokenExpiredError") {
1375
+ throw new Error("Session has expired. Please sign in again.");
1376
+ }
1377
+ if (error instanceof Error && error.name === "JsonWebTokenError") {
1378
+ throw new Error("Invalid session token");
1379
+ }
1380
+ throw error;
1381
+ }
1382
+ }
1383
+
1384
+ // src/endpoints/verify-magic-link.ts
1385
+ var createVerifyMagicLinkEndpoint = (config) => {
1386
+ return {
1387
+ path: "/newsletter/verify-magic-link",
1388
+ method: "post",
1389
+ handler: async (req, res) => {
1390
+ try {
1391
+ const { token } = req.body;
1392
+ if (!token) {
1393
+ return res.status(400).json({
1394
+ success: false,
1395
+ error: "Token is required"
1396
+ });
1397
+ }
1398
+ let payload;
1399
+ try {
1400
+ payload = verifyMagicLinkToken(token);
1401
+ } catch (error) {
1402
+ return res.status(401).json({
1403
+ success: false,
1404
+ error: error instanceof Error ? error.message : "Invalid token"
1405
+ });
1406
+ }
1407
+ const subscriber = await req.payload.findByID({
1408
+ collection: config.subscribersSlug || "subscribers",
1409
+ id: payload.subscriberId
1410
+ // Keep overrideAccess: true for token verification
1411
+ });
1412
+ if (!subscriber) {
1413
+ return res.status(404).json({
1414
+ success: false,
1415
+ error: "Subscriber not found"
1416
+ });
1417
+ }
1418
+ if (subscriber.email !== payload.email) {
1419
+ return res.status(401).json({
1420
+ success: false,
1421
+ error: "Invalid token"
1422
+ });
1423
+ }
1424
+ if (subscriber.subscriptionStatus === "unsubscribed") {
1425
+ return res.status(403).json({
1426
+ success: false,
1427
+ error: "This email has been unsubscribed"
1428
+ });
1429
+ }
1430
+ const syntheticUser = {
1431
+ collection: "subscribers",
1432
+ id: subscriber.id,
1433
+ email: subscriber.email
1434
+ };
1435
+ if (subscriber.subscriptionStatus === "pending") {
1436
+ await req.payload.update({
1437
+ collection: config.subscribersSlug || "subscribers",
1438
+ id: subscriber.id,
1439
+ data: {
1440
+ subscriptionStatus: "active"
1441
+ },
1442
+ overrideAccess: false,
1443
+ user: syntheticUser
1444
+ });
1445
+ }
1446
+ await req.payload.update({
1447
+ collection: config.subscribersSlug || "subscribers",
1448
+ id: subscriber.id,
1449
+ data: {
1450
+ magicLinkToken: null,
1451
+ magicLinkTokenExpiry: null
1452
+ },
1453
+ overrideAccess: false,
1454
+ user: syntheticUser
1455
+ });
1456
+ const sessionToken = generateSessionToken(
1457
+ String(subscriber.id),
1458
+ subscriber.email
1459
+ );
1460
+ res.json({
1461
+ success: true,
1462
+ sessionToken,
1463
+ subscriber: {
1464
+ id: subscriber.id,
1465
+ email: subscriber.email,
1466
+ name: subscriber.name,
1467
+ locale: subscriber.locale,
1468
+ emailPreferences: subscriber.emailPreferences
1469
+ }
1470
+ });
1471
+ } catch (error) {
1472
+ console.error("Verify magic link error:", error);
1473
+ res.status(500).json({
1474
+ success: false,
1475
+ error: "Failed to verify magic link"
1476
+ });
1477
+ }
1478
+ }
1479
+ };
1480
+ };
1481
+
1482
+ // src/endpoints/preferences.ts
1483
+ var createPreferencesEndpoint = (config) => {
1484
+ return {
1485
+ path: "/newsletter/preferences",
1486
+ method: "get",
1487
+ handler: async (req, res) => {
1488
+ try {
1489
+ const authHeader = req.headers.authorization;
1490
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
1491
+ return res.status(401).json({
1492
+ success: false,
1493
+ error: "Authorization required"
1494
+ });
1495
+ }
1496
+ const token = authHeader.substring(7);
1497
+ let payload;
1498
+ try {
1499
+ payload = verifySessionToken(token);
1500
+ } catch (error) {
1501
+ return res.status(401).json({
1502
+ success: false,
1503
+ error: error instanceof Error ? error.message : "Invalid token"
1504
+ });
1505
+ }
1506
+ const subscriber = await req.payload.findByID({
1507
+ collection: config.subscribersSlug || "subscribers",
1508
+ id: payload.subscriberId,
1509
+ overrideAccess: false,
1510
+ user: {
1511
+ collection: "subscribers",
1512
+ id: payload.subscriberId,
1513
+ email: payload.email
1514
+ }
1515
+ });
1516
+ if (!subscriber) {
1517
+ return res.status(404).json({
1518
+ success: false,
1519
+ error: "Subscriber not found"
1520
+ });
1521
+ }
1522
+ res.json({
1523
+ success: true,
1524
+ subscriber: {
1525
+ id: subscriber.id,
1526
+ email: subscriber.email,
1527
+ name: subscriber.name,
1528
+ locale: subscriber.locale,
1529
+ emailPreferences: subscriber.emailPreferences,
1530
+ subscriptionStatus: subscriber.subscriptionStatus
1531
+ }
1532
+ });
1533
+ } catch (error) {
1534
+ console.error("Get preferences error:", error);
1535
+ res.status(500).json({
1536
+ success: false,
1537
+ error: "Failed to get preferences"
1538
+ });
1539
+ }
1540
+ }
1541
+ };
1542
+ };
1543
+ var createUpdatePreferencesEndpoint = (config) => {
1544
+ return {
1545
+ path: "/newsletter/preferences",
1546
+ method: "post",
1547
+ handler: async (req, res) => {
1548
+ try {
1549
+ const authHeader = req.headers.authorization;
1550
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
1551
+ return res.status(401).json({
1552
+ success: false,
1553
+ error: "Authorization required"
1554
+ });
1555
+ }
1556
+ const token = authHeader.substring(7);
1557
+ let payload;
1558
+ try {
1559
+ payload = verifySessionToken(token);
1560
+ } catch (error) {
1561
+ return res.status(401).json({
1562
+ success: false,
1563
+ error: error instanceof Error ? error.message : "Invalid token"
1564
+ });
1565
+ }
1566
+ const { name, locale, emailPreferences } = req.body;
1567
+ const updateData = {};
1568
+ if (name !== void 0) {
1569
+ updateData.name = name;
1570
+ }
1571
+ if (locale !== void 0) {
1572
+ updateData.locale = locale;
1573
+ }
1574
+ if (emailPreferences !== void 0) {
1575
+ updateData.emailPreferences = emailPreferences;
1576
+ }
1577
+ const subscriber = await req.payload.update({
1578
+ collection: config.subscribersSlug || "subscribers",
1579
+ id: payload.subscriberId,
1580
+ data: updateData,
1581
+ overrideAccess: false,
1582
+ user: {
1583
+ collection: "subscribers",
1584
+ id: payload.subscriberId,
1585
+ email: payload.email
1586
+ }
1587
+ });
1588
+ res.json({
1589
+ success: true,
1590
+ subscriber: {
1591
+ id: subscriber.id,
1592
+ email: subscriber.email,
1593
+ name: subscriber.name,
1594
+ locale: subscriber.locale,
1595
+ emailPreferences: subscriber.emailPreferences,
1596
+ subscriptionStatus: subscriber.subscriptionStatus
1597
+ }
1598
+ });
1599
+ } catch (error) {
1600
+ console.error("Update preferences error:", error);
1601
+ res.status(500).json({
1602
+ success: false,
1603
+ error: "Failed to update preferences"
1604
+ });
1605
+ }
1606
+ }
1607
+ };
1608
+ };
1609
+
1610
+ // src/endpoints/unsubscribe.ts
1611
+ var createUnsubscribeEndpoint = (config) => {
1612
+ return {
1613
+ path: "/newsletter/unsubscribe",
1614
+ method: "post",
1615
+ handler: async (req, res) => {
1616
+ try {
1617
+ const { email, token } = req.body;
1618
+ if (!email && !token) {
1619
+ return res.status(400).json({
1620
+ success: false,
1621
+ error: "Email or token is required"
1622
+ });
1623
+ }
1624
+ let subscriber;
1625
+ if (token) {
1626
+ try {
1627
+ const jwt2 = await import("jsonwebtoken");
1628
+ const payload = jwt2.verify(
1629
+ token,
1630
+ process.env.JWT_SECRET || process.env.PAYLOAD_SECRET || ""
1631
+ );
1632
+ if (payload.type !== "unsubscribe") {
1633
+ throw new Error("Invalid token type");
1634
+ }
1635
+ subscriber = await req.payload.findByID({
1636
+ collection: config.subscribersSlug || "subscribers",
1637
+ id: payload.subscriberId
1638
+ });
1639
+ } catch {
1640
+ return res.status(401).json({
1641
+ success: false,
1642
+ error: "Invalid or expired unsubscribe link"
1643
+ });
1644
+ }
1645
+ } else {
1646
+ if (!isValidEmail(email)) {
1647
+ return res.status(400).json({
1648
+ success: false,
1649
+ error: "Invalid email format"
1650
+ });
1651
+ }
1652
+ const result = await req.payload.find({
1653
+ collection: config.subscribersSlug || "subscribers",
1654
+ where: {
1655
+ email: {
1656
+ equals: email.toLowerCase()
1657
+ }
1658
+ }
1659
+ });
1660
+ if (result.docs.length === 0) {
1661
+ return res.json({
1662
+ success: true,
1663
+ message: "If this email was subscribed, it has been unsubscribed."
1664
+ });
1665
+ }
1666
+ subscriber = result.docs[0];
1667
+ }
1668
+ if (!subscriber) {
1669
+ return res.json({
1670
+ success: true,
1671
+ message: "If this email was subscribed, it has been unsubscribed."
1672
+ });
1673
+ }
1674
+ if (subscriber.subscriptionStatus === "unsubscribed") {
1675
+ return res.json({
1676
+ success: true,
1677
+ message: "Already unsubscribed"
1678
+ });
1679
+ }
1680
+ await req.payload.update({
1681
+ collection: config.subscribersSlug || "subscribers",
1682
+ id: subscriber.id,
1683
+ data: {
1684
+ subscriptionStatus: "unsubscribed",
1685
+ unsubscribedAt: (/* @__PURE__ */ new Date()).toISOString()
1686
+ },
1687
+ overrideAccess: false,
1688
+ user: {
1689
+ collection: "subscribers",
1690
+ id: subscriber.id,
1691
+ email: subscriber.email
1692
+ }
1693
+ });
1694
+ res.json({
1695
+ success: true,
1696
+ message: "Successfully unsubscribed"
1697
+ });
1698
+ } catch (error) {
1699
+ console.error("Unsubscribe error:", error);
1700
+ res.status(500).json({
1701
+ success: false,
1702
+ error: "Failed to unsubscribe. Please try again."
1703
+ });
1704
+ }
1705
+ }
1706
+ };
1707
+ };
1708
+
1709
+ // src/endpoints/index.ts
1710
+ function createNewsletterEndpoints(config) {
1711
+ const endpoints = [
1712
+ createSubscribeEndpoint(config),
1713
+ createUnsubscribeEndpoint(config)
1714
+ ];
1715
+ if (config.auth?.enabled !== false) {
1716
+ endpoints.push(
1717
+ createVerifyMagicLinkEndpoint(config),
1718
+ createPreferencesEndpoint(config),
1719
+ createUpdatePreferencesEndpoint(config)
1720
+ );
1721
+ }
1722
+ return endpoints;
1723
+ }
1724
+
1725
+ // src/fields/newsletterScheduling.ts
1726
+ function createNewsletterSchedulingFields(config) {
1727
+ const groupName = config.features?.newsletterScheduling?.fields?.groupName || "newsletterScheduling";
1728
+ const contentField = config.features?.newsletterScheduling?.fields?.contentField || "content";
1729
+ const createMarkdownField = config.features?.newsletterScheduling?.fields?.createMarkdownField !== false;
1730
+ const fields = [
1731
+ {
1732
+ name: groupName,
1733
+ type: "group",
1734
+ label: "Newsletter Scheduling",
1735
+ admin: {
1736
+ condition: (data, { user }) => user?.collection === "users"
1737
+ // Only show for admin users
1738
+ },
1739
+ fields: [
1740
+ {
1741
+ name: "scheduled",
1742
+ type: "checkbox",
1743
+ label: "Schedule for Newsletter",
1744
+ defaultValue: false,
1745
+ admin: {
1746
+ description: "Schedule this content to be sent as a newsletter"
1747
+ }
1748
+ },
1749
+ {
1750
+ name: "scheduledDate",
1751
+ type: "date",
1752
+ label: "Send Date",
1753
+ required: true,
1754
+ admin: {
1755
+ date: {
1756
+ pickerAppearance: "dayAndTime"
1757
+ },
1758
+ condition: (data) => data?.[groupName]?.scheduled,
1759
+ description: "When to send this newsletter"
1760
+ }
1761
+ },
1762
+ {
1763
+ name: "sentDate",
1764
+ type: "date",
1765
+ label: "Sent Date",
1766
+ admin: {
1767
+ readOnly: true,
1768
+ condition: (data) => data?.[groupName]?.sendStatus === "sent",
1769
+ description: "When this newsletter was sent"
1770
+ }
1771
+ },
1772
+ {
1773
+ name: "sendStatus",
1774
+ type: "select",
1775
+ label: "Status",
1776
+ options: [
1777
+ { label: "Draft", value: "draft" },
1778
+ { label: "Scheduled", value: "scheduled" },
1779
+ { label: "Sending", value: "sending" },
1780
+ { label: "Sent", value: "sent" },
1781
+ { label: "Failed", value: "failed" }
1782
+ ],
1783
+ defaultValue: "draft",
1784
+ admin: {
1785
+ readOnly: true,
1786
+ description: "Current send status"
1787
+ }
1788
+ },
1789
+ {
1790
+ name: "emailSubject",
1791
+ type: "text",
1792
+ label: "Email Subject",
1793
+ required: true,
1794
+ admin: {
1795
+ condition: (data) => data?.[groupName]?.scheduled,
1796
+ description: "Subject line for the newsletter email"
1797
+ }
1798
+ },
1799
+ {
1800
+ name: "preheader",
1801
+ type: "text",
1802
+ label: "Email Preheader",
1803
+ admin: {
1804
+ condition: (data) => data?.[groupName]?.scheduled,
1805
+ description: "Preview text that appears after the subject line"
1806
+ }
1807
+ },
1808
+ {
1809
+ name: "segments",
1810
+ type: "select",
1811
+ label: "Target Segments",
1812
+ hasMany: true,
1813
+ options: [
1814
+ { label: "All Subscribers", value: "all" },
1815
+ ...config.i18n?.locales?.map((locale) => ({
1816
+ label: `${locale.toUpperCase()} Subscribers`,
1817
+ value: locale
1818
+ })) || []
1819
+ ],
1820
+ defaultValue: ["all"],
1821
+ admin: {
1822
+ condition: (data) => data?.[groupName]?.scheduled,
1823
+ description: "Which subscriber segments to send to"
1824
+ }
1825
+ },
1826
+ {
1827
+ name: "testEmails",
1828
+ type: "array",
1829
+ label: "Test Email Recipients",
1830
+ admin: {
1831
+ condition: (data) => data?.[groupName]?.scheduled && data?.[groupName]?.sendStatus === "draft",
1832
+ description: "Send test emails before scheduling"
1833
+ },
1834
+ fields: [
1835
+ {
1836
+ name: "email",
1837
+ type: "email",
1838
+ required: true
1839
+ }
1840
+ ]
1841
+ }
1842
+ ]
1843
+ }
1844
+ ];
1845
+ if (createMarkdownField) {
1846
+ fields.push(createMarkdownFieldInternal({
1847
+ name: `${contentField}Markdown`,
1848
+ richTextField: contentField,
1849
+ label: "Email Content (Markdown)",
1850
+ admin: {
1851
+ position: "sidebar",
1852
+ condition: (data) => Boolean(data?.[contentField] && data?.[groupName]?.scheduled),
1853
+ description: "Markdown version for email rendering",
1854
+ readOnly: true
1855
+ }
1856
+ }));
1857
+ }
1858
+ return fields;
1859
+ }
1860
+ function createMarkdownFieldInternal(config) {
1861
+ return {
1862
+ name: config.name,
1863
+ type: "textarea",
1864
+ label: config.label || "Markdown",
1865
+ admin: {
1866
+ ...config.admin,
1867
+ description: config.admin?.description || "Auto-generated from rich text content"
1868
+ },
1869
+ hooks: {
1870
+ afterRead: [
1871
+ async ({ data }) => {
1872
+ if (data?.[config.richTextField]) {
1873
+ try {
1874
+ const { convertLexicalToMarkdown } = await import("@payloadcms/richtext-lexical");
1875
+ return convertLexicalToMarkdown({
1876
+ data: data[config.richTextField]
1877
+ });
1878
+ } catch {
1879
+ return "";
1880
+ }
1881
+ }
1882
+ return "";
1883
+ }
1884
+ ],
1885
+ beforeChange: [
1886
+ () => {
1887
+ return null;
1888
+ }
1889
+ ]
1890
+ }
1891
+ };
1892
+ }
1893
+
1894
+ // src/index.ts
1895
+ var newsletterPlugin = (pluginConfig) => (incomingConfig) => {
1896
+ const config = {
1897
+ enabled: true,
1898
+ subscribersSlug: "subscribers",
1899
+ settingsSlug: "newsletter-settings",
1900
+ auth: {
1901
+ enabled: true,
1902
+ tokenExpiration: "7d",
1903
+ magicLinkPath: "/newsletter/verify",
1904
+ ...pluginConfig.auth
1905
+ },
1906
+ ...pluginConfig
1907
+ };
1908
+ if (!config.enabled) {
1909
+ return incomingConfig;
1910
+ }
1911
+ const subscribersCollection = createSubscribersCollection(config);
1912
+ const settingsCollection = createNewsletterSettingsCollection(config);
1913
+ let collections = [...incomingConfig.collections || [], subscribersCollection, settingsCollection];
1914
+ if (config.features?.newsletterScheduling?.enabled) {
1915
+ const targetCollections = config.features.newsletterScheduling.collections || "articles";
1916
+ const collectionsToExtend = Array.isArray(targetCollections) ? targetCollections : [targetCollections];
1917
+ const schedulingFields = createNewsletterSchedulingFields(config);
1918
+ collections = collections.map((collection) => {
1919
+ if (collectionsToExtend.includes(collection.slug)) {
1920
+ return {
1921
+ ...collection,
1922
+ fields: [
1923
+ ...collection.fields,
1924
+ ...schedulingFields
1925
+ ]
1926
+ };
1927
+ }
1928
+ return collection;
1929
+ });
1930
+ }
1931
+ const endpoints = createNewsletterEndpoints(config);
1932
+ const modifiedConfig = {
1933
+ ...incomingConfig,
1934
+ collections,
1935
+ globals: [
1936
+ ...incomingConfig.globals || []
1937
+ ],
1938
+ endpoints: [
1939
+ ...incomingConfig.endpoints || [],
1940
+ ...endpoints
1941
+ ],
1942
+ onInit: async (payload) => {
1943
+ try {
1944
+ const settingsResult = await payload.find({
1945
+ collection: config.settingsSlug || "newsletter-settings",
1946
+ where: {
1947
+ active: {
1948
+ equals: true
1949
+ }
1950
+ },
1951
+ limit: 1
1952
+ });
1953
+ const settings = settingsResult.docs[0];
1954
+ let emailServiceConfig;
1955
+ if (settings) {
1956
+ emailServiceConfig = {
1957
+ provider: settings.provider || config.providers.default,
1958
+ fromAddress: settings.fromAddress || config.providers.resend?.fromAddress || config.providers.broadcast?.fromAddress || "noreply@example.com",
1959
+ fromName: settings.fromName || config.providers.resend?.fromName || config.providers.broadcast?.fromName || "Newsletter",
1960
+ replyTo: settings.replyTo,
1961
+ resend: settings.provider === "resend" ? {
1962
+ apiKey: settings.resendSettings?.apiKey || config.providers.resend?.apiKey || "",
1963
+ audienceIds: settings.resendSettings?.audienceIds?.reduce((acc, item) => {
1964
+ acc[item.locale] = {
1965
+ production: item.production,
1966
+ development: item.development
1967
+ };
1968
+ return acc;
1969
+ }, {}) || config.providers.resend?.audienceIds
1970
+ } : config.providers.resend,
1971
+ broadcast: settings.provider === "broadcast" ? {
1972
+ apiUrl: settings.broadcastSettings?.apiUrl || config.providers.broadcast?.apiUrl || "",
1973
+ tokens: {
1974
+ production: settings.broadcastSettings?.productionToken || config.providers.broadcast?.tokens.production,
1975
+ development: settings.broadcastSettings?.developmentToken || config.providers.broadcast?.tokens.development
1976
+ }
1977
+ } : config.providers.broadcast
1978
+ };
1979
+ } else {
1980
+ emailServiceConfig = {
1981
+ provider: config.providers.default,
1982
+ fromAddress: config.providers.resend?.fromAddress || config.providers.broadcast?.fromAddress || "noreply@example.com",
1983
+ fromName: config.providers.resend?.fromName || config.providers.broadcast?.fromName || "Newsletter",
1984
+ resend: config.providers.resend,
1985
+ broadcast: config.providers.broadcast
1986
+ };
1987
+ }
1988
+ payload.newsletterEmailService = createEmailService(emailServiceConfig);
1989
+ console.warn("Newsletter plugin initialized with", payload.newsletterEmailService.getProvider(), "provider");
1990
+ } catch (error) {
1991
+ console.error("Failed to initialize newsletter email service:", error);
1992
+ }
1993
+ if (incomingConfig.onInit) {
1994
+ await incomingConfig.onInit(payload);
1995
+ }
1996
+ }
1997
+ };
1998
+ return modifiedConfig;
1999
+ };
2000
+ // Annotate the CommonJS export names for ESM import in node:
2001
+ 0 && (module.exports = {
2002
+ newsletterPlugin
2003
+ });
2004
+ //# sourceMappingURL=index.cjs.map