payload-plugin-newsletter
Version:
Complete newsletter management plugin for Payload CMS with subscriber management, magic link authentication, and email service integration
4,912 lines • 163 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/types/newsletter.ts
var NewsletterProviderError;
var init_newsletter = __esm({
"src/types/newsletter.ts"() {
"use strict";
NewsletterProviderError = class extends Error {
constructor(message, code, provider, details) {
super(message);
this.code = code;
this.provider = provider;
this.details = details;
this.name = "NewsletterProviderError";
}
};
}
});
// src/types/broadcast.ts
var BroadcastProviderError;
var init_broadcast = __esm({
"src/types/broadcast.ts"() {
"use strict";
init_newsletter();
BroadcastProviderError = class extends Error {
constructor(message, code, provider, details) {
super(message);
this.code = code;
this.provider = provider;
this.details = details;
this.name = "BroadcastProviderError";
}
};
}
});
// src/types/channel.ts
var init_channel = __esm({
"src/types/channel.ts"() {
"use strict";
}
});
// src/types/providers.ts
var BaseBroadcastProvider;
var init_providers = __esm({
"src/types/providers.ts"() {
"use strict";
init_broadcast();
init_newsletter();
BaseBroadcastProvider = class {
constructor(config) {
this.config = config;
}
/**
* Schedule a broadcast - default implementation throws not supported
*/
async schedule(_id, _scheduledAt) {
const capabilities = this.getCapabilities();
if (!capabilities.supportsScheduling) {
throw new BroadcastProviderError(
"Scheduling is not supported by this provider",
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
this.name
);
}
throw new Error("Method not implemented");
}
/**
* Cancel scheduled broadcast - default implementation throws not supported
*/
async cancelSchedule(_id) {
const capabilities = this.getCapabilities();
if (!capabilities.supportsScheduling) {
throw new BroadcastProviderError(
"Scheduling is not supported by this provider",
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
this.name
);
}
throw new Error("Method not implemented");
}
/**
* Get analytics - default implementation returns zeros
*/
async getAnalytics(_id) {
const capabilities = this.getCapabilities();
if (!capabilities.supportsAnalytics) {
throw new BroadcastProviderError(
"Analytics are not supported by this provider",
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
this.name
);
}
return {
sent: 0,
delivered: 0,
opened: 0,
clicked: 0,
bounced: 0,
complained: 0,
unsubscribed: 0
};
}
/**
* Helper method to validate required fields
*/
validateRequiredFields(data, fields) {
const missing = fields.filter((field) => !data[field]);
if (missing.length > 0) {
throw new BroadcastProviderError(
`Missing required fields: ${missing.join(", ")}`,
"VALIDATION_ERROR" /* VALIDATION_ERROR */,
this.name
);
}
}
/**
* Helper method to check if a status transition is allowed
*/
canEditInStatus(status) {
const capabilities = this.getCapabilities();
return capabilities.editableStatuses.includes(status);
}
/**
* Helper to build pagination response
*/
buildListResponse(items, total, options = {}) {
const limit = options.limit || 20;
const offset = options.offset || 0;
return {
items,
total,
limit,
offset,
hasMore: offset + items.length < total
};
}
};
}
});
// src/types/index.ts
var init_types = __esm({
"src/types/index.ts"() {
"use strict";
init_broadcast();
init_channel();
init_providers();
init_newsletter();
}
});
// src/providers/broadcast/broadcast.ts
var broadcast_exports = {};
__export(broadcast_exports, {
BroadcastApiProvider: () => BroadcastApiProvider
});
var BroadcastApiProvider;
var init_broadcast2 = __esm({
"src/providers/broadcast/broadcast.ts"() {
"use strict";
init_types();
BroadcastApiProvider = class extends BaseBroadcastProvider {
constructor(config) {
super(config);
this.name = "broadcast";
this.apiUrl = config.apiUrl.replace(/\/$/, "");
this.isDevelopment = process.env.NODE_ENV !== "production";
this.token = this.isDevelopment ? config.tokens.development || config.tokens.production || "" : config.tokens.production || config.tokens.development || "";
if (!this.token) {
throw new BroadcastProviderError(
"Broadcast API token is required",
"CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */,
this.name
);
}
}
// Channel Management Methods
async listChannels(options) {
try {
const params = new URLSearchParams();
if (options?.limit) params.append("limit", options.limit.toString());
if (options?.offset) params.append("offset", options.offset.toString());
const response = await fetch(`${this.apiUrl}/api/v1/channels?${params}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json"
}
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
}
const data = await response.json();
const channels = data.data.map((channel) => this.transformChannelFromApi(channel));
return {
channels,
total: data.total || channels.length,
limit: options?.limit || 20,
offset: options?.offset || 0
};
} catch (error) {
throw new BroadcastProviderError(
`Failed to list channels: ${error instanceof Error ? error.message : "Unknown error"}`,
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
this.name,
error
);
}
}
async getChannel(id) {
try {
const response = await fetch(`${this.apiUrl}/api/v1/channels/${id}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json"
}
});
if (!response.ok) {
if (response.status === 404) {
throw new BroadcastProviderError(
`Channel not found: ${id}`,
"CHANNEL_NOT_FOUND" /* CHANNEL_NOT_FOUND */,
this.name
);
}
const error = await response.text();
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
}
const channel = await response.json();
return this.transformChannelFromApi(channel);
} catch (error) {
if (error instanceof BroadcastProviderError) throw error;
throw new BroadcastProviderError(
`Failed to get channel: ${error instanceof Error ? error.message : "Unknown error"}`,
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
this.name,
error
);
}
}
async createChannel(data) {
try {
const response = await fetch(`${this.apiUrl}/api/v1/channels`, {
method: "POST",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
channel: {
name: data.name,
description: data.description,
from: data.fromName,
address: data.fromEmail,
reply_to: data.replyTo
}
})
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
}
const channel = await response.json();
return this.transformChannelFromApi(channel);
} catch (error) {
throw new BroadcastProviderError(
`Failed to create channel: ${error instanceof Error ? error.message : "Unknown error"}`,
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
this.name,
error
);
}
}
async updateChannel(id, data) {
try {
const updateData = { channel: {} };
if (data.name !== void 0) updateData.channel.name = data.name;
if (data.description !== void 0) updateData.channel.description = data.description;
if (data.fromName !== void 0) updateData.channel.from = data.fromName;
if (data.fromEmail !== void 0) updateData.channel.address = data.fromEmail;
if (data.replyTo !== void 0) updateData.channel.reply_to = data.replyTo;
const response = await fetch(`${this.apiUrl}/api/v1/channels/${id}`, {
method: "PATCH",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json"
},
body: JSON.stringify(updateData)
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
}
const channel = await response.json();
return this.transformChannelFromApi(channel);
} catch (error) {
throw new BroadcastProviderError(
`Failed to update channel: ${error instanceof Error ? error.message : "Unknown error"}`,
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
this.name,
error
);
}
}
async deleteChannel(id) {
try {
const response = await fetch(`${this.apiUrl}/api/v1/channels/${id}`, {
method: "DELETE",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json"
}
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
}
} catch (error) {
throw new BroadcastProviderError(
`Failed to delete channel: ${error instanceof Error ? error.message : "Unknown error"}`,
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
this.name,
error
);
}
}
// Broadcast Management Methods
async list(options) {
try {
const params = new URLSearchParams();
if (options?.limit) params.append("limit", options.limit.toString());
if (options?.offset) params.append("offset", options.offset.toString());
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts?${params}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json"
}
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
}
const data = await response.json();
const broadcasts = data.data.map((broadcast) => this.transformBroadcastFromApi(broadcast));
return this.buildListResponse(broadcasts, data.total, options);
} catch (error) {
throw new BroadcastProviderError(
`Failed to list broadcasts: ${error instanceof Error ? error.message : "Unknown error"}`,
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
this.name,
error
);
}
}
async get(id) {
try {
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json"
}
});
if (!response.ok) {
if (response.status === 404) {
throw new BroadcastProviderError(
`Broadcast not found: ${id}`,
"NOT_FOUND" /* NOT_FOUND */,
this.name
);
}
const error = await response.text();
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
}
const broadcast = await response.json();
return this.transformBroadcastFromApi(broadcast);
} catch (error) {
if (error instanceof BroadcastProviderError) throw error;
throw new BroadcastProviderError(
`Failed to get broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
this.name,
error
);
}
}
async create(data) {
try {
this.validateRequiredFields(data, ["channelId", "name", "subject", "content"]);
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts`, {
method: "POST",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
broadcast: {
channel_id: parseInt(data.channelId),
// Broadcast API uses numeric IDs
name: data.name,
subject: data.subject,
preheader: data.preheader,
body: data.content,
html_body: true,
track_opens: data.trackOpens ?? true,
track_clicks: data.trackClicks ?? true,
reply_to: data.replyTo,
segment_ids: data.audienceIds
}
})
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
}
const result = await response.json();
return this.get(result.id.toString());
} catch (error) {
if (error instanceof BroadcastProviderError) throw error;
throw new BroadcastProviderError(
`Failed to create broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
this.name,
error
);
}
}
async update(id, data) {
try {
const existing = await this.get(id);
if (!this.canEditInStatus(existing.status)) {
throw new BroadcastProviderError(
`Cannot update broadcast in status: ${existing.status}`,
"INVALID_STATUS" /* INVALID_STATUS */,
this.name
);
}
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
method: "PATCH",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
broadcast: {
name: data.name,
subject: data.subject,
preheader: data.preheader,
body: data.content,
track_opens: data.trackOpens,
track_clicks: data.trackClicks,
reply_to: data.replyTo,
segment_ids: data.audienceIds
}
})
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
}
const broadcast = await response.json();
return this.transformBroadcastFromApi(broadcast);
} catch (error) {
if (error instanceof BroadcastProviderError) throw error;
throw new BroadcastProviderError(
`Failed to update broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
this.name,
error
);
}
}
async delete(id) {
try {
const existing = await this.get(id);
if (!this.canEditInStatus(existing.status)) {
throw new BroadcastProviderError(
`Cannot delete broadcast in status: ${existing.status}`,
"INVALID_STATUS" /* INVALID_STATUS */,
this.name
);
}
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
method: "DELETE",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json"
}
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
}
} catch (error) {
if (error instanceof BroadcastProviderError) throw error;
throw new BroadcastProviderError(
`Failed to delete broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
this.name,
error
);
}
}
async send(id, options) {
try {
if (options?.testMode && options.testRecipients?.length) {
throw new BroadcastProviderError(
"Test send is not yet implemented for Broadcast provider",
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
this.name
);
}
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}/send_broadcast`, {
method: "POST",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
segment_ids: options?.audienceIds
})
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
}
const result = await response.json();
return this.get(result.id.toString());
} catch (error) {
if (error instanceof BroadcastProviderError) throw error;
throw new BroadcastProviderError(
`Failed to send broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
this.name,
error
);
}
}
async schedule(id, scheduledAt) {
try {
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
method: "PATCH",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
broadcast: {
scheduled_send_at: scheduledAt.toISOString(),
// TODO: Handle timezone properly
scheduled_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
}
})
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
}
const broadcast = await response.json();
return this.transformBroadcastFromApi(broadcast);
} catch (error) {
throw new BroadcastProviderError(
`Failed to schedule broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
this.name,
error
);
}
}
async cancelSchedule(id) {
try {
const response = await fetch(`${this.apiUrl}/api/v1/broadcasts/${id}`, {
method: "PATCH",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
broadcast: {
scheduled_send_at: null,
scheduled_timezone: null
}
})
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
}
const broadcast = await response.json();
return this.transformBroadcastFromApi(broadcast);
} catch (error) {
throw new BroadcastProviderError(
`Failed to cancel scheduled broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
this.name,
error
);
}
}
async getAnalytics(_id) {
throw new BroadcastProviderError(
"Analytics API not yet implemented for Broadcast provider",
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
this.name
);
}
getCapabilities() {
return {
supportsScheduling: true,
supportsSegmentation: true,
supportsAnalytics: false,
// Not documented yet
supportsABTesting: false,
supportsTemplates: false,
supportsPersonalization: true,
supportsMultipleChannels: true,
supportsChannelSegmentation: true,
editableStatuses: ["draft" /* DRAFT */, "scheduled" /* SCHEDULED */],
supportedContentTypes: ["html", "text"]
};
}
async validateConfiguration() {
try {
await this.list({ limit: 1 });
return true;
} catch {
return false;
}
}
transformBroadcastFromApi(broadcast) {
return {
id: broadcast.id.toString(),
channelId: "1",
// TODO: Get from API response when available
name: broadcast.name,
subject: broadcast.subject,
preheader: broadcast.preheader,
content: broadcast.body,
status: this.mapBroadcastStatus(broadcast.status),
trackOpens: broadcast.track_opens,
trackClicks: broadcast.track_clicks,
replyTo: broadcast.reply_to,
recipientCount: broadcast.total_recipients,
sentAt: broadcast.sent_at ? new Date(broadcast.sent_at) : void 0,
scheduledAt: broadcast.scheduled_send_at ? new Date(broadcast.scheduled_send_at) : void 0,
createdAt: new Date(broadcast.created_at),
updatedAt: new Date(broadcast.updated_at),
providerData: { broadcast },
providerId: broadcast.id.toString(),
providerType: "broadcast"
};
}
transformChannelFromApi(channel) {
return {
id: channel.id.toString(),
name: channel.name,
description: channel.description,
fromName: channel.from,
fromEmail: channel.address,
replyTo: channel.reply_to,
providerId: channel.id.toString(),
providerType: "broadcast",
subscriberCount: channel.total_active_subscribers,
active: true,
// Broadcast API doesn't have an active field
createdAt: new Date(channel.created_at),
updatedAt: new Date(channel.updated_at)
};
}
mapBroadcastStatus(status) {
const statusMap = {
"draft": "draft" /* DRAFT */,
"scheduled": "scheduled" /* SCHEDULED */,
"queueing": "sending" /* SENDING */,
"sending": "sending" /* SENDING */,
"sent": "sent" /* SENT */,
"failed": "failed" /* FAILED */,
"partial_failure": "failed" /* FAILED */,
"paused": "paused" /* PAUSED */,
"aborted": "canceled" /* CANCELED */
};
return statusMap[status] || "draft" /* DRAFT */;
}
};
}
});
// src/providers/resend/broadcast.ts
var broadcast_exports2 = {};
__export(broadcast_exports2, {
ResendBroadcastProvider: () => ResendBroadcastProvider
});
var import_resend3, ResendBroadcastProvider;
var init_broadcast3 = __esm({
"src/providers/resend/broadcast.ts"() {
"use strict";
import_resend3 = require("resend");
init_types();
ResendBroadcastProvider = class extends BaseBroadcastProvider {
constructor(config) {
super(config);
this.name = "resend";
this.client = new import_resend3.Resend(config.apiKey);
this.audienceIds = config.audienceIds || {};
this.isDevelopment = process.env.NODE_ENV !== "production";
if (!config.apiKey) {
throw new BroadcastProviderError(
"Resend API key is required",
"CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */,
this.name
);
}
}
// Channel Management Methods (map to Resend Audiences)
async listChannels(options) {
try {
const response = await this.client.audiences.list();
const channels = response.data?.data?.map((audience) => ({
id: audience.id,
name: audience.name,
description: void 0,
// Resend doesn't have description
fromName: this.config.fromName || "",
fromEmail: this.config.fromEmail || "",
replyTo: this.config.replyTo,
providerId: audience.id,
providerType: "resend",
subscriberCount: void 0,
// Not available in list API
active: true,
createdAt: new Date(audience.created_at),
updatedAt: new Date(audience.created_at)
// No updated_at in Resend
})) || [];
return {
channels,
total: channels.length,
limit: options?.limit || 100,
offset: options?.offset || 0
};
} catch (error) {
throw new BroadcastProviderError(
`Failed to list channels (audiences): ${error instanceof Error ? error.message : "Unknown error"}`,
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
this.name,
error
);
}
}
async getChannel(id) {
try {
const response = await this.client.audiences.get(id);
if (!response.data) {
throw new BroadcastProviderError(
`Channel (audience) not found: ${id}`,
"CHANNEL_NOT_FOUND" /* CHANNEL_NOT_FOUND */,
this.name
);
}
return {
id: response.data.id,
name: response.data.name,
description: void 0,
fromName: this.config.fromName || "",
fromEmail: this.config.fromEmail || "",
replyTo: this.config.replyTo,
providerId: response.data.id,
providerType: "resend",
subscriberCount: void 0,
// Not available
active: true,
createdAt: new Date(response.data.created_at),
updatedAt: new Date(response.data.created_at)
};
} catch (error) {
if (error instanceof BroadcastProviderError) throw error;
throw new BroadcastProviderError(
`Failed to get channel (audience): ${error instanceof Error ? error.message : "Unknown error"}`,
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
this.name,
error
);
}
}
async createChannel(data) {
try {
const response = await this.client.audiences.create({
name: data.name
});
if (!response.data) {
throw new Error("Failed to create audience");
}
return {
id: response.data.id,
name: response.data.name,
description: data.description,
fromName: data.fromName,
fromEmail: data.fromEmail,
replyTo: data.replyTo,
providerId: response.data.id,
providerType: "resend",
subscriberCount: 0,
active: true,
createdAt: /* @__PURE__ */ new Date(),
updatedAt: /* @__PURE__ */ new Date()
};
} catch (error) {
throw new BroadcastProviderError(
`Failed to create channel (audience): ${error instanceof Error ? error.message : "Unknown error"}`,
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
this.name,
error
);
}
}
async updateChannel(_id, _data) {
throw new BroadcastProviderError(
"Updating channels (audiences) is not supported by Resend API",
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
this.name
);
}
async deleteChannel(id) {
try {
await this.client.audiences.remove(id);
} catch (error) {
throw new BroadcastProviderError(
`Failed to delete channel (audience): ${error instanceof Error ? error.message : "Unknown error"}`,
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
this.name,
error
);
}
}
// Broadcast Management Methods
async list(_options) {
throw new BroadcastProviderError(
"Listing broadcasts is not currently supported by Resend API. This feature may be available in the dashboard only.",
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
this.name
);
}
async get(_id) {
throw new BroadcastProviderError(
"Getting individual broadcasts is not currently supported by Resend API. This feature may be available in the dashboard only.",
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
this.name
);
}
async create(data) {
try {
this.validateRequiredFields(data, ["channelId", "name", "subject", "content"]);
const locale = "en";
const audienceConfig = this.audienceIds?.[locale];
const audienceId = this.isDevelopment ? audienceConfig?.development || audienceConfig?.production : audienceConfig?.production || audienceConfig?.development;
if (!audienceId && data.audienceIds?.length) {
}
const resendClient = this.client;
if (resendClient.broadcasts?.create) {
const broadcast = await resendClient.broadcasts.create({
name: data.name,
subject: data.subject,
from: `${this.config.fromName || "Newsletter"} <${this.config.fromEmail || "noreply@example.com"}>`,
reply_to: data.replyTo,
audience_id: audienceId || data.audienceIds?.[0],
content: {
html: data.content
// TODO: Handle plain text version
}
});
return this.transformResendToBroadcast(broadcast);
}
throw new BroadcastProviderError(
"Creating broadcasts via API is not currently supported. Please check if Resend has released their Broadcasts API.",
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
this.name
);
} catch (error) {
if (error instanceof BroadcastProviderError) throw error;
throw new BroadcastProviderError(
`Failed to create broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
this.name,
error
);
}
}
async update(_id, _data) {
throw new BroadcastProviderError(
"Updating broadcasts is not currently supported by Resend API. Note: Resend broadcasts can only be edited where they were created.",
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
this.name
);
}
async delete(_id) {
throw new BroadcastProviderError(
"Deleting broadcasts is not currently supported by Resend API.",
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
this.name
);
}
async send(id, options) {
try {
const resendClient = this.client;
if (resendClient.broadcasts?.send) {
await resendClient.broadcasts.send(id, {
audience_id: options?.audienceIds?.[0]
// TODO: Handle test mode if supported
});
return {
id,
channelId: options?.audienceIds?.[0] || "1",
name: "Unknown",
subject: "Unknown",
content: "",
status: "sending" /* SENDING */,
trackOpens: true,
trackClicks: true,
createdAt: /* @__PURE__ */ new Date(),
updatedAt: /* @__PURE__ */ new Date(),
providerType: "resend"
};
}
throw new BroadcastProviderError(
"Sending broadcasts via API is not currently supported. Please check if Resend has released their Broadcasts API.",
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
this.name
);
} catch (error) {
if (error instanceof BroadcastProviderError) throw error;
throw new BroadcastProviderError(
`Failed to send broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
"PROVIDER_ERROR" /* PROVIDER_ERROR */,
this.name,
error
);
}
}
async schedule(_id, _scheduledAt) {
throw new BroadcastProviderError(
"Scheduling broadcasts is not supported by Resend",
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
this.name
);
}
async getAnalytics(_id) {
throw new BroadcastProviderError(
"Getting broadcast analytics via API is not currently supported. Analytics may be available in the Resend dashboard.",
"NOT_SUPPORTED" /* NOT_SUPPORTED */,
this.name
);
}
getCapabilities() {
return {
supportsScheduling: false,
// Not documented
supportsSegmentation: true,
// Via Audiences
supportsAnalytics: true,
// Available in dashboard, API unclear
supportsABTesting: false,
supportsTemplates: false,
// Not clear from docs
supportsPersonalization: true,
// Via merge tags
supportsMultipleChannels: true,
// Via multiple audiences
supportsChannelSegmentation: false,
// Not within a single audience
editableStatuses: [],
// Unclear which statuses can be edited
supportedContentTypes: ["html"]
// React components via SDK
};
}
async validateConfiguration() {
try {
const resendClient = this.client;
if (resendClient.audiences?.list) {
await resendClient.audiences.list({ limit: 1 });
return true;
}
await this.client.emails.send({
from: "onboarding@resend.dev",
to: "delivered@resend.dev",
subject: "Configuration Test",
html: "<p>Testing configuration</p>"
});
return true;
} catch {
return false;
}
}
/**
* Transform Resend broadcast to our Broadcast type
* NOTE: This is speculative based on what the API might return
*/
transformResendToBroadcast(broadcast) {
return {
id: broadcast.id,
channelId: broadcast.audience_id || "1",
// Map audience_id to channelId
name: broadcast.name || "Untitled",
subject: broadcast.subject,
preheader: broadcast.preheader,
content: broadcast.content?.html || broadcast.html || "",
status: this.mapResendStatus(broadcast.status),
trackOpens: true,
// Resend tracks by default
trackClicks: true,
// Resend tracks by default
replyTo: broadcast.reply_to,
recipientCount: broadcast.recipient_count,
sentAt: broadcast.sent_at ? new Date(broadcast.sent_at) : void 0,
scheduledAt: broadcast.scheduled_at ? new Date(broadcast.scheduled_at) : void 0,
createdAt: new Date(broadcast.created_at || Date.now()),
updatedAt: new Date(broadcast.updated_at || Date.now()),
providerData: { broadcast },
providerId: broadcast.id,
providerType: "resend"
};
}
mapResendStatus(status) {
if (!status) return "draft" /* DRAFT */;
const statusMap = {
"draft": "draft" /* DRAFT */,
"scheduled": "scheduled" /* SCHEDULED */,
"sending": "sending" /* SENDING */,
"sent": "sent" /* SENT */,
"failed": "failed" /* FAILED */
};
return statusMap[status.toLowerCase()] || "draft" /* DRAFT */;
}
};
}
});
// src/index.ts
var src_exports = {};
__export(src_exports, {
default: () => newsletterPlugin,
getServerSideAuth: () => getServerSideAuth,
getTokenFromRequest: () => getTokenFromRequest,
isAuthenticated: () => isAuthenticated,
newsletterPlugin: () => newsletterPlugin,
requireAuth: () => requireAuth,
verifyToken: () => verifyToken
});
module.exports = __toCommonJS(src_exports);
// src/utils/access.ts
var isAdmin = (user, config) => {
if (!user || user.collection !== "users") {
return false;
}
if (config?.access?.isAdmin) {
return config.access.isAdmin(user);
}
if (user.roles?.includes("admin")) {
return true;
}
if (user.isAdmin === true) {
return true;
}
if (user.role === "admin") {
return true;
}
if (user.admin === true) {
return true;
}
return false;
};
var adminOnly = (config) => ({ req }) => {
const user = req.user;
return isAdmin(user, config);
};
var adminOrSelf = (config) => ({ req, id }) => {
const user = req.user;
if (!user) {
if (!id) {
return {
id: {
equals: "unauthorized-no-access"
}
};
}
return false;
}
if (isAdmin(user, config)) {
return true;
}
if (user.collection === "subscribers") {
if (!id) {
return {
id: {
equals: user.id
}
};
}
return id === user.id;
}
if (!id) {
return {
id: {
equals: "unauthorized-no-access"
}
};
}
return false;
};
// src/emails/render.tsx
var import_render = require("@react-email/render");
// src/emails/MagicLink.tsx
var import_components = require("@react-email/components");
// src/emails/styles.ts
var styles = {
main: {
backgroundColor: "#f6f9fc",
fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif'
},
container: {
backgroundColor: "#ffffff",
border: "1px solid #f0f0f0",
borderRadius: "5px",
margin: "0 auto",
padding: "45px",
marginBottom: "64px",
maxWidth: "500px"
},
heading: {
fontSize: "24px",
letterSpacing: "-0.5px",
lineHeight: "1.3",
fontWeight: "600",
color: "#484848",
margin: "0 0 20px",
padding: "0"
},
text: {
fontSize: "16px",
lineHeight: "26px",
fontWeight: "400",
color: "#484848",
margin: "16px 0"
},
button: {
backgroundColor: "#000000",
borderRadius: "5px",
color: "#fff",
fontSize: "16px",
fontWeight: "bold",
textDecoration: "none",
textAlign: "center",
display: "block",
width: "100%",
padding: "14px 20px",
margin: "30px 0"
},
link: {
color: "#2754C5",
fontSize: "14px",
textDecoration: "underline",
wordBreak: "break-all"
},
hr: {
borderColor: "#e6ebf1",
margin: "30px 0"
},
footer: {
fontSize: "14px",
lineHeight: "24px",
color: "#9ca2ac",
textAlign: "center",
margin: "0"
},
code: {
display: "inline-block",
padding: "16px",
width: "100%",
backgroundColor: "#f4f4f4",
borderRadius: "5px",
border: "1px solid #eee",
fontSize: "14px",
fontFamily: "monospace",
textAlign: "center",
margin: "24px 0"
}
};
// src/emails/MagicLink.tsx
var import_jsx_runtime = require("react/jsx-runtime");
var MagicLinkEmail = ({
magicLink,
email,
siteName = "Newsletter",
expiresIn = "24 hours"
}) => {
const previewText = `Sign in to ${siteName}`;
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Html, { children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Head, {}),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Preview, { children: previewText }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Body, { style: styles.main, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Container, { style: styles.container, children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Text, { style: styles.heading, children: [
"Sign in to ",
siteName
] }),
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Text, { style: styles.text, children: [
"Hi ",
email.split("@")[0],
","
] }),
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Text, { style: styles.text, children: [
"We received a request to sign in to your ",
siteName,
" account. Click the button below to complete your sign in:"
] }),
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Button, { href: magicLink, style: styles.button, children: [
"Sign in to ",
siteName
] }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Text, { style: styles.text, children: "Or copy and paste this URL into your browser:" }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: styles.code, children: magicLink }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Hr, { style: styles.hr }),
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Text, { style: styles.footer, children: [
"This link will expire in ",
expiresIn,
". If you didn't request this email, you can safely ignore it."
] })
] }) })
] });
};
// src/emails/Welcome.tsx
var import_components2 = require("@react-email/components");
var import_jsx_runtime2 = require("react/jsx-runtime");
var WelcomeEmail = ({
email,
siteName = "Newsletter",
preferencesUrl
}) => {
const previewText = `Welcome to ${siteName}!`;
const firstName = email.split("@")[0];
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Html, { children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Head, {}),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Preview, { children: previewText }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Body, { style: styles.main, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Container, { style: styles.container, children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Text, { style: styles.heading, children: [
"Welcome to ",
siteName,
"! \u{1F389}"
] }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Text, { style: styles.text, children: [
"Hi ",
firstName,
","
] }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Text, { style: styles.text, children: [
"Thanks for subscribing to ",
siteName,
"! We're excited to have you as part of our community."
] }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Text, { style: styles.text, children: "You'll receive our newsletter based on your preferences. Speaking of which, you can update your preferences anytime:" }),
preferencesUrl && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Button, { href: preferencesUrl, style: styles.button, children: "Manage Preferences" }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Text, { style: styles.text, children: "Here's what you can expect from us:" }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Text, { style: styles.text, children: [
"\u2022 Regular updates based on your chosen frequency",
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("br", {}),
"\u2022 Content tailored to your interests",
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("br", {}),
"\u2022 Easy unsubscribe options in every email",
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("br", {}),
"\u2022 Your privacy respected always"
] }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Hr, { style: styles.hr }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Text, { style: styles.footer, children: "If you have any questions, feel free to reply to this email. We're here to help!" })
] }) })
] });
};
// src/emails/SignIn.tsx
var import_jsx_runtime3 = require("react/jsx-runtime");
var SignInEmail = (props) => {
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MagicLinkEmail, { ...props });
};
// src/emails/render.tsx
var import_jsx_runtime4 = require("react/jsx-runtime");
async function renderEmail(template, data) {
try {
switch (template) {
case "magic-link": {
const magicLinkData = data;
return (0, import_render.render)(
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
MagicLinkEmail,
{
magicLink: magicLinkData.magicLink || magicLinkData.verificationUrl || magicLinkData.magicLinkUrl || "",
email: magicLinkData.email || "",
siteName: magicLinkData.siteName,
expiresIn: magicLinkData.expiresIn
}
)
);
}
case "signin": {
const signinData = data;
return (0, import_render.render)(
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
SignInEmail,
{
magicLink: signinData.magicLink || signinData.verificationUrl || signinData.magicLinkUrl || "",
email: signinData.email || "",
siteName: signinData.siteName,
expiresIn: signinData.expiresIn
}
)
);
}
case "welcome": {
const welcomeData = data;
return (0, import_render.render)(
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
WelcomeEmail,
{
email: welcomeData.email || "",
siteName: welcomeData.siteName,
preferencesUrl: welcomeData.preferencesUrl
}
)
);
}
default:
throw new Error(`Unknown email template: ${template}`);
}
} catch (error) {
console.error(`Failed to render email template ${template}:`, error);
throw error;
}
}
// src/collections/Subscribers.ts
var createSubscribersCollection = (pluginConfig) => {
const slug = pluginConfig.subscribersSlug || "subscribers";
const defaultFields = [
// Core fields
{
name: "email",
type: "email",
required: true,
unique: true,
admin: {
description: "Subscriber email address"
}
},
{
name: "name",
type: "text",
admin: {
description: "Subscriber full name"
}
},
{
name: "locale",
type: "select",
options: pluginConfig.i18n?.locales?.map((locale) => ({
label: locale.toUpperCase(),
value: locale
})) || [
{ label: "EN", value: "en" }
],
defaultValue: pluginConfig.i18n?.defaultLocale || "en",
admin: {
description: "Preferred language for communications"
}
},
// Authentication fields (hidden from admin UI)
{
name: "magicLinkToken",
type: "text",
hidden: true
},
{
name: "magicLinkTokenExpiry",
type: "date",
hidden: true
},
// Subscription status
{
name: "subscriptionStatus",
type: "select",
options: [
{ label: "Active", value: "active" },
{ label: "Unsubscribed", value: "unsubscribed" },
{ label: "Pending", value: "pending" }
],
defaultValue: "pending",
required: true,
admin: {
description: "Current subscription status"
}
},
{
name: "unsubscribedAt",
type: "date",
admin: {
condition: (data) => data?.subscriptionStatus === "unsubscribed",
description: "When the user unsubscribed",
readOnly: true
}
},
// Email preferences
{
name: "emailPreferences",
type: "group",
fields: [
{
name: "newsletter",
type: "checkbox",
defaultValue: true,
label: "Newsletter",
admin: {
description: "Receive regular newsletter updates"
}
},
{
name: "announcements",
type: "checkbox",
defaultValue: true,
label: "Announcements",
admin: {
description: "Receive important announcements"
}
}
],
admin: {
description: "Email communication preferences"
}
},
// Source tracking
{
name: "source",
type: "text",
admin: {
description: "Where the subscriber signed up from"
}
}
];
if (pluginConfig.features?.utmTracking?.enabled) {
const utmFields = pluginConfig.features.utmTracking.fields || [
"source",
"medium",
"campaign",
"content",
"term"
];
defaultFields.push({
name: "utmParameters",
type: "group",
fields: utmFields.map((field) => ({
name: field,
type: "text",
admin: {
description: `UTM ${field} parameter`
}
})),
admin: {
description: "UTM tracking parameters"
}
});
}
defaultFields.push({
name: "signupMetadata",
type: "group",
fields: [
{
name: "ipAddress",
type: "text",
admin: {
readOnly: true
}
},
{
name: "userAgent",
type: "text",
admin: {
readOnly: true
}
},
{
name: "referrer",
type: "text",
admin: {
readOnly: true
}
},
{
name: "signupPage",
type: "text",
admin: {
readOnly: true
}
}
],
admin: {
description: "Technical information about signup"
}
});
if (pluginConfig.features?.leadMagnets?.enabled) {
defaultFields.push({
name: "leadMagnet",
type: "relationship",
relationTo: pluginConfig.features.leadMagnets.collection || "media",
admin: {
description: "Lead magnet downloaded at signup"
}
});
}
let fields = defaultFields;
if (pluginConfig.fields?.overrides) {
fields = pluginConfig.fields.overrides({ defaultFields });
}
if (pluginConfig.fields?.additional) {
fields = [...fields, ...pluginConfig.fields.additional];
}
const subscribersCollection = {
slug,
labels: {
singular: "Subscriber",
plural: "Subscribers"
},
admin: {
useAsTitle: "email",
defaultColumns: ["email", "name", "subscriptionStatus", "createdAt"],
group: "Newsletter"
},
fields,
hooks: {
afterChange: [
async ({ doc, req, operation, previousDoc }) => {
if (operation === "create") {
const emailService = req.payload.newsletterEmailService;
if (emailService) {
try {
await emailService.addContact(doc);
} catch {
}
}
if (doc.subscriptionStatus === "active" && emailService) {
try {
const settings = await req.payload.findGlobal({
slug: pluginConfig.settingsSlug || "newsletter-settings"
});
const serverURL = req.payload.config.serverURL || process.env.PAYLOAD_PUBLIC_SERVER_URL || "";
const html = await renderEmail("welcome", {
email: doc.email,
siteName: settings?.brandSettings?.siteName || "Newsletter",
preferencesUrl: `${serverURL}/account/preferences`
// This could be customized
});
await emailService.send({
to: doc.email,
subject: settings?.brandSettings?.siteName ? `Welcome to ${settings.brandSettings.siteName}!` : "Welcome!",
html
});
console.warn(`Welcome email sent to: ${doc.email}`);
} catch (error) {
console.error("Failed to send welcome email:", error);
}
}
if (pluginConfig.hooks?.afterSubscribe) {
await pluginConfig.hooks.afterSubscribe({ doc, req });
}
}
if (operation === "update" && previousDoc) {
const emailService = req.payload.newsletterEmailService;
if (doc.subscriptionStatus !== previousDoc.subscriptionStatus && emailService) {
try {
await emailService.updateContact(doc);
} catch {
}
}
if (doc.subscriptionStatus === "unsubscribed" && previousDoc.subscriptionStatus !== "unsubscribed") {
doc.unsubscribedAt = (/* @__PURE__ */ new Date()).toISOString();
if (pluginConfig.hooks?.afterUnsubscribe) {
await pluginConfig.hooks.afterUnsubscribe({ doc, req });
}
}
}
}
],
beforeDelete: [
async ({ id, req }) => {
const emailService = req.payload.newsletterEmailService;
if (emailService) {
try {
const doc = await req.payload.findByID({
collection: slug,
id
});
await emailService.removeContact(doc.email);
} catch {
}
}
}
]
},
access: {
create: () => true,
// Public can subscribe
read: adminOrSelf(pluginConfig),
update: adminOrSelf(pluginConfig),
delete: adminOnly(pluginConfig)
},
timestamps: true
};
return subscribersCollection;
};
// src/globals/NewsletterSettings.ts
var createNewsletterSettingsGlobal = (pluginConfig) => {
const slug = pluginConfig.settingsSlug || "newsletter-settings";
return {
slug,
label: "Newsletter Settings",
admin: {
group: "Newsletter",
description: "Configure email provider settings and templates"
},
fields: [
{
type: "tabs",
tabs: [
{
label: "Provider Settings",
fields: [
{
name: "provider",
type: "select",
label: "Email Provider",
required: true,
options: [
{ label: "Resend", value: "resend" },
{ label: "Broadcast (Self-Hosted)", value: "broadcast" }
],
defaultValue: pluginConfig.providers.default,
admin: {
description: "Choose which email service to use"
}
},
{
name: "resendSettings",
type: "group",
label: "Resend Settings",
admin: {
condition: (data) => data?.provider === "resend"
},
fields: [
{
name: "apiKey",
type: "text",
label: "API Key",
required: true,
admin: {
description: "Your Resend API key"
}
},
{
name: "audienceIds",
type: "array",
label: "Audience IDs by Locale",
fields: [
{
name: "locale",
type: "select",
label: "Locale",
required: true,
options: pluginConfig.i18n?.locales?.map((locale) => ({
label: locale.toUpperCase(),
value: locale
})) || [
{ label: "EN", value: "en" }
]
},
{
name: "production",
type: "text",
label: "Production Audience ID"
},
{
name: "development",
type: "text",
label: "Development Audience ID"
}
]
}
]
},
{
name: "broadcastSettings",
type: "group",
label: "Broadcast Settings",
admin: {
condition: (data) => data?.provider === "broadcast"
},
fields: [
{
name: "apiUrl",
type: "text",
label: "API URL",
required: true,
admin: {
description: "Your Broadcast instance URL"
}
},
{
name: "productionToken",
type: "text",
label: "Production Token",
admin: {
description: "Token for production environment"
}
},
{
name: "developmentToken",
type: "text",
label: "Development Token",
admin: {
description: "Token for development environment"
}
}
]
},
{
name: "fromAddress",
type: "email",
label: "From Address",
required: true,
admin: {
description: "Default sender email address"
}
},
{
name: "fromName",
type: "text",
label: "From Name",
required: true,
admin: {
description: "Default sender name"
}
},
{
name: "replyTo",
type: "email",
label: "Reply-To Address",
admin: {
description: "Optional reply-to email address"
}
}
]
},
{
label: "Email Templates",
fields: [
{
name: "emailTemplates",
type: "group",
label: "Email Templates",
fields: [
{
name: "welcome",
type: "group",
label: "Welcome Email",
fields: [
{
name: "enabled",
type: "checkbox",
label: "Send Welcome Email",
defaultValue: true
},
{
name: "subject",
type: "text",
label: "Subject Line",
defaultValue: "Welcome to {{fromName}}!",
admin: {
condition: (data) => data?.emailTemplates?.welcome?.enabled
}
},
{
name: "preheader",
type: "text",
label: "Preheader Text",
admin: {
condition: (data) => data?.emailTemplates?.welcome?.enabled
}
}
]
},
{
name: "magicLink",
type: "group",
label: "Magic Link Email",
fields: [
{
name: "subject",
type: "text",
label: "Subject Line",
defaultValue: "Sign in to {{fromName}}"
},
{
name: "preheader",
type: "text",
label: "Preheader Text",
defaultValue: "Click the link to access your preferences"
},
{
name: "expirationTime",
type: "select",
label: "Link Expiration",
defaultValue: "7d",
options: [
{ label: "1 hour", value: "1h" },
{ label: "24 hours", value: "24h" },
{ label: "7 days", value: "7d" },
{ label: "30 days", value: "30d" }
]
}
]
}
]
}
]
},
{
label: "Subscription Settings",
fields: [
{
name: "subscriptionSettings",
type: "group",
label: "Subscription Settings",
fields: [
{
name: "requireDoubleOptIn",
type: "checkbox",
label: "Require Double Opt-In",
defaultValue: false,
admin: {
description: "Require email confirmation before activating subscriptions"
}
},
{
name: "allowedDomains",
type: "array",
label: "Allowed Email Domains",
admin: {
description: "Leave empty to allow all domains"
},
fields: [
{
name: "domain",
type: "text",
label: "Domain",
required: true,
admin: {
placeholder: "example.com"
}
}
]
},
{
name: "maxSubscribersPerIP",
type: "number",
label: "Max Subscribers per IP",
defaultValue: 10,
min: 1,
admin: {
description: "Maximum number of subscriptions allowed from a single IP address"
}
}
]
}
]
}
]
}
],
hooks: {
beforeChange: [
async ({ data, req }) => {
if (!req.user || req.user.collection !== "users") {
throw new Error("Only administrators can modify newsletter settings");
}
return data;
}
],
afterChange: [
async ({ doc, req }) => {
if (req.payload.newsletterEmailService) {
try {
console.warn("Newsletter settings updated, reinitializing service...");
} catch {
}
}
return doc;
}
]
},
access: {
read: () => true,
// Settings can be read publicly for validation
update: adminOnly(pluginConfig)
}
};
};
// src/providers/resend.ts
var import_resend = require("resend");
// src/providers/types.ts
var EmailProviderError = class extends Error {
constructor(message, provider, originalError) {
super(message);
this.name = "EmailProviderError";
this.provider = provider;
this.originalError = originalError;
}
};
// src/providers/resend.ts
var ResendProvider = class {
constructor(config) {
this.client = new import_resend.Resend(config.apiKey);
this.audienceIds = config.audienceIds || {};
this.fromAddress = config.fromAddress;
this.fromName = config.fromName;
this.isDevelopment = process.env.NODE_ENV !== "production";
}
getProvider() {
return "resend";
}
async send(params) {
try {
const from = params.from || {
email: this.fromAddress,
name: this.fromName
};
if (!params.html && !params.text) {
throw new Error("Either html or text content is required");
}
await this.client.emails.send({
from: `${from.name} <${from.email}>`,
to: Array.isArray(params.to) ? params.to : [params.to],
subject: params.subject,
html: params.html || "",
text: params.text,
replyTo: params.replyTo
});
} catch (error) {
throw new EmailProviderError(
`Failed to send email via Resend: ${error instanceof Error ? error.message : "Unknown error"}`,
"resend",
error
);
}
}
async addContact(contact) {
try {
const audienceId = this.getAudienceId(contact.locale);
if (!audienceId) {
console.warn(`No audience ID configured for locale: ${contact.locale}`);
return;
}
await this.client.contacts.create({
email: contact.email,
firstName: contact.name?.split(" ")[0],
lastName: contact.name?.split(" ").slice(1).join(" "),
unsubscribed: contact.subscriptionStatus === "unsubscribed",
audienceId
});
} catch (error) {
throw new EmailProviderError(
`Failed to add contact to Resend: ${error instanceof Error ? error.message : "Unknown error"}`,
"resend",
error
);
}
}
async updateContact(contact) {
try {
const audienceId = this.getAudienceId(contact.locale);
if (!audienceId) {
console.warn(`No audience ID configured for locale: ${contact.locale}`);
return;
}
const contacts = await this.client.contacts.list({ audienceId });
const existingContact = contacts.data?.data?.find((c) => c.email === contact.email);
if (existingContact) {
await this.client.contacts.update({
id: existingContact.id,
audienceId,
firstName: contact.name?.split(" ")[0],
lastName: contact.name?.split(" ").slice(1).join(" "),
unsubscribed: contact.subscriptionStatus === "unsubscribed"
});
} else {
await this.addContact(contact);
}
} catch (error) {
throw new EmailProviderError(
`Failed to update contact in Resend: ${error instanceof Error ? error.message : "Unknown error"}`,
"resend",
error
);
}
}
async removeContact(email) {
try {
for (const locale in this.audienceIds) {
const audienceId = this.getAudienceId(locale);
if (!audienceId) continue;
const contacts = await this.client.contacts.list({ audienceId });
const contact = contacts.data?.data?.find((c) => c.email === email);
if (contact) {
await this.client.contacts.update({
id: contact.id,
audienceId,
unsubscribed: true
});
break;
}
}
} catch (error) {
throw new EmailProviderError(
`Failed to remove contact from Resend: ${error instanceof Error ? error.message : "Unknown error"}`,
"resend",
error
);
}
}
getAudienceId(locale) {
const localeKey = locale || "en";
if (!this.audienceIds) return void 0;
const localeConfig = this.audienceIds[localeKey];
if (!localeConfig) return void 0;
const audienceId = this.isDevelopment ? localeConfig.development || localeConfig.production : localeConfig.production || localeConfig.development;
return audienceId;
}
};
// src/providers/broadcast.ts
var BroadcastProvider = class {
constructor(config) {
this.apiUrl = config.apiUrl.replace(/\/$/, "");
this.isDevelopment = process.env.NODE_ENV !== "production";
this.token = this.isDevelopment ? config.tokens.development || config.tokens.production || "" : config.tokens.production || config.tokens.development || "";
this.fromAddress = config.fromAddress;
this.fromName = config.fromName;
}
getProvider() {
return "broadcast";
}
async send(params) {
try {
const from = params.from || {
email: this.fromAddress,
name: this.fromName
};
const recipients = Array.isArray(params.to) ? params.to : [params.to];
const response = await fetch(`${this.apiUrl}/api/v1/emails`, {
method: "POST",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
from_email: from.email,
from_name: from.name,
to: recipients,
subject: params.subject,
html_body: params.html,
text_body: params.text,
reply_to: params.replyTo
})
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
}
} catch (error) {
throw new EmailProviderError(
`Failed to send email via Broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
"broadcast",
error
);
}
}
async addContact(contact) {
try {
const [firstName, ...lastNameParts] = (contact.name || "").split(" ");
const lastName = lastNameParts.join(" ");
const response = await fetch(`${this.apiUrl}/api/v1/subscribers.json`, {
method: "POST",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
subscriber: {
email: contact.email,
first_name: firstName || void 0,
last_name: lastName || void 0,
tags: [`lang:${contact.locale || "en"}`],
is_active: contact.subscriptionStatus === "active",
source: contact.source
}
})
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
}
} catch (error) {
throw new EmailProviderError(
`Failed to add contact to Broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
"broadcast",
error
);
}
}
async updateContact(contact) {
try {
const searchResponse = await fetch(
`${this.apiUrl}/api/v1/subscribers/find.json?email=${encodeURIComponent(contact.email)}`,
{
headers: {
"Authorization": `Bearer ${this.token}`
}
}
);
if (!searchResponse.ok) {
await this.addContact(contact);
return;
}
const existingContact = await searchResponse.json();
if (!existingContact || !existingContact.id) {
await this.addContact(contact);
return;
}
const [firstName, ...lastNameParts] = (contact.name || "").split(" ");
const lastName = lastNameParts.join(" ");
const response = await fetch(`${this.apiUrl}/api/v1/subscribers.json`, {
method: "PATCH",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
email: contact.email,
subscriber: {
first_name: firstName || void 0,
last_name: lastName || void 0,
tags: [`lang:${contact.locale || "en"}`],
is_active: contact.subscriptionStatus === "active",
source: contact.source
}
})
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
}
} catch (error) {
throw new EmailProviderError(
`Failed to update contact in Broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
"broadcast",
error
);
}
}
async removeContact(email) {
try {
const searchResponse = await fetch(
`${this.apiUrl}/api/v1/subscribers/find.json?email=${encodeURIComponent(email)}`,
{
headers: {
"Authorization": `Bearer ${this.token}`
}
}
);
if (!searchResponse.ok) {
return;
}
const contact = await searchResponse.json();
if (!contact || !contact.id) {
return;
}
const response = await fetch(`${this.apiUrl}/api/v1/subscribers/deactivate.json`, {
method: "POST",
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ email })
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
}
} catch (error) {
throw new EmailProviderError(
`Failed to remove contact from Broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
"broadcast",
error
);
}
}
};
// src/providers/index.ts
var EmailService = class {
constructor(config) {
this.provider = this.createProvider(config);
}
createProvider(config) {
const baseConfig = {
fromAddress: config.fromAddress,
fromName: config.fromName
};
switch (config.provider) {
case "resend":
if (!config.resend) {
throw new Error("Resend configuration is required when using Resend provider");
}
return new ResendProvider({
...config.resend,
...baseConfig
});
case "broadcast":
if (!config.broadcast) {
throw new Error("Broadcast configuration is required when using Broadcast provider");
}
return new BroadcastProvider({
...config.broadcast,
...baseConfig
});
default:
throw new Error(`Unknown email provider: ${config.provider}`);
}
}
async send(params) {
return this.provider.send(params);
}
async addContact(contact) {
return this.provider.addContact(contact);
}
async updateContact(contact) {
return this.provider.updateContact(contact);
}
async removeContact(email) {
return this.provider.removeContact(email);
}
getProvider() {
return this.provider.getProvider();
}
/**
* Update the provider configuration
* Useful when settings are changed in the admin UI
*/
updateConfig(config) {
this.provider = this.createProvider(config);
}
};
function createEmailService(config) {
return new EmailService(config);
}
// src/utils/validation.ts
var import_isomorphic_dompurify = __toESM(require("isomorphic-dompurify"), 1);
function isValidEmail(email) {
if (!email || typeof email !== "string") return false;
const trimmed = email.trim();
if (trimmed.length > 255) return false;
if (trimmed.includes("<") || trimmed.includes(">")) return false;
if (trimmed.includes("javascript:")) return false;
if (trimmed.includes("data:")) return false;
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!emailRegex.test(trimmed)) return false;
const parts = trimmed.split("@");
if (parts.length !== 2) return false;
const [localPart, domain] = parts;
if (localPart.length > 64 || localPart.length === 0) return false;
if (localPart.startsWith(".") || localPart.endsWith(".")) return false;
if (domain.startsWith(".") || domain.endsWith(".")) return false;
if (domain.includes("..")) return false;
if (localPart.includes("..")) return false;
return true;
}
function isDomainAllowed(email, allowedDomains) {
if (!isValidEmail(email)) {
return false;
}
if (!allowedDomains || allowedDomains.length === 0) {
return true;
}
const domain = email.split("@")[1]?.toLowerCase();
if (!domain) return false;
return allowedDomains.some(
(allowedDomain) => domain === allowedDomain.toLowerCase()
);
}
function sanitizeInput(input) {
if (!input) return "";
let cleaned = import_isomorphic_dompurify.default.sanitize(input, {
ALLOWED_TAGS: [],
ALLOWED_ATTR: [],
KEEP_CONTENT: true
});
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, "");
return cleaned.trim();
}
function extractUTMParams(searchParams) {
const utmParams = {};
const utmKeys = ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"];
utmKeys.forEach((key) => {
const value = searchParams.get(key);
if (value) {
const shortKey = key.replace("utm_", "");
utmParams[shortKey] = value;
}
});
return utmParams;
}
function isValidSource(source) {
if (!source || typeof source !== "string") return false;
const allowedSources = [
"website",
"api",
"import",
"admin",
"signup-form",
"magic-link",
"preferences",
"external"
];
return allowedSources.includes(source);
}
function validateSubscriberData(data) {
const errors = [];
if (!data.email) {
errors.push("Email is required");
} else if (!isValidEmail(data.email)) {
errors.push("Invalid email format");
}
if (data.name && data.name.length > 100) {
errors.push("Name is too long (max 100 characters)");
}
if (data.source !== void 0) {
if (!data.source || data.source.length === 0) {
errors.push("Source cannot be empty");
} else if (data.source.length > 50) {
errors.push("Source is too long (max 50 characters)");
} else if (!isValidSource(data.source)) {
errors.push("Invalid source value");
}
}
return {
valid: errors.length === 0,
errors
};
}
// src/utils/jwt.ts
var import_jsonwebtoken = __toESM(require("jsonwebtoken"), 1);
function getJWTSecret() {
const secret = process.env.JWT_SECRET || process.env.PAYLOAD_SECRET;
if (!secret) {
console.warn(
"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."
);
return "INSECURE_DEVELOPMENT_SECRET_PLEASE_SET_JWT_SECRET";
}
return secret;
}
function generateMagicLinkToken(subscriberId, email, config) {
const payload = {
subscriberId,
email,
type: "magic-link"
};
const expiresIn = config.auth?.tokenExpiration || "7d";
return import_jsonwebtoken.default.sign(payload, getJWTSecret(), {
expiresIn,
issuer: "payload-newsletter-plugin"
});
}
function verifyMagicLinkToken(token) {
try {
const payload = import_jsonwebtoken.default.verify(token, getJWTSecret(), {
issuer: "payload-newsletter-plugin"
});
if (payload.type !== "magic-link") {
throw new Error("Invalid token type");
}
return payload;
} catch (error) {
if (error instanceof Error && error.name === "TokenExpiredError") {
throw new Error("Magic link has expired. Please request a new one.");
}
if (error instanceof Error && error.name === "JsonWebTokenError") {
throw new Error("Invalid magic link token");
}
throw error;
}
}
function generateSessionToken(subscriberId, email) {
const payload = {
subscriberId,
email,
type: "session"
};
return import_jsonwebtoken.default.sign(payload, getJWTSecret(), {
expiresIn: "30d",
issuer: "payload-newsletter-plugin"
});
}
function verifySessionToken(token) {
try {
const payload = import_jsonwebtoken.default.verify(token, getJWTSecret(), {
issuer: "payload-newsletter-plugin"
});
if (payload.type !== "session") {
throw new Error("Invalid token type");
}
return payload;
} catch (error) {
if (error instanceof Error && error.name === "TokenExpiredError") {
throw new Error("Session has expired. Please sign in again.");
}
if (error instanceof Error && error.name === "JsonWebTokenError") {
throw new Error("Invalid session token");
}
throw error;
}
}
function generateMagicLinkURL(token, baseURL, config) {
const path = config.auth?.magicLinkPath || "/newsletter/verify";
const url = new URL(path, baseURL);
url.searchParams.set("token", token);
return url.toString();
}
// src/endpoints/subscribe.ts
var createSubscribeEndpoint = (config) => {
return {
path: "/newsletter/subscribe",
method: "post",
handler: async (req) => {
try {
const data = await req.json();
const {
email,
name,
source,
preferences,
leadMagnet,
surveyResponses,
metadata = {}
} = data;
const trimmedEmail = email?.trim();
const validation = validateSubscriberData({ email: trimmedEmail, name, source });
if (!validation.valid) {
return Response.json({
success: false,
errors: validation.errors
}, { status: 400 });
}
const settings = await req.payload.findGlobal({
slug: config.settingsSlug || "newsletter-settings",
overrideAccess: false
// No user context for public endpoint
});
const allowedDomains = settings?.subscriptionSettings?.allowedDomains?.map((d) => d.domain) || [];
if (!isDomainAllowed(trimmedEmail, allowedDomains)) {
return Response.json({
success: false,
error: "Email domain not allowed"
}, { status: 400 });
}
const existing = await req.payload.find({
collection: config.subscribersSlug || "subscribers",
where: {
email: {
equals: trimmedEmail.toLowerCase()
}
},
overrideAccess: true
// Need to check for duplicates in public endpoint
});
if (existing.docs.length > 0) {
const subscriber2 = existing.docs[0];
if (subscriber2.subscriptionStatus === "unsubscribed") {
return Response.json({
success: false,
error: "This email has been unsubscribed. Please contact support to resubscribe."
}, { status: 400 });
}
return Response.json({
success: false,
error: "Already subscribed",
subscriber: {
id: subscriber2.id,
email: subscriber2.email,
subscriptionStatus: subscriber2.subscriptionStatus
}
}, { status: 400 });
}
const ipAddress = req.ip || req.connection?.remoteAddress;
const maxPerIP = settings?.subscriptionSettings?.maxSubscribersPerIP || 10;
const ipSubscribers = await req.payload.find({
collection: config.subscribersSlug || "subscribers",
where: {
"signupMetadata.ipAddress": {
equals: ipAddress
}
},
overrideAccess: true
// Need to check IP limits in public endpoint
});
if (ipSubscribers.docs.length >= maxPerIP) {
return Response.json({
success: false,
error: "Too many subscriptions from this IP address"
}, { status: 429 });
}
const referer = req.headers.get("referer") || req.headers.get("referrer") || "";
let utmParams = {};
if (referer) {
try {
utmParams = extractUTMParams(new URL(referer).searchParams);
} catch {
}
}
const subscriberData = {
email: trimmedEmail.toLowerCase(),
name: name ? sanitizeInput(name) : void 0,
locale: metadata.locale || config.i18n?.defaultLocale || "en",
subscriptionStatus: settings?.subscriptionSettings?.requireDoubleOptIn ? "pending" : "active",
source: source || "api",
emailPreferences: {
newsletter: true,
announcements: true,
...preferences || {}
},
signupMetadata: {
ipAddress,
userAgent: req.headers.get("user-agent") || void 0,
referrer: referer,
signupPage: metadata.signupPage || referer
}
};
if (config.features?.utmTracking?.enabled && Object.keys(utmParams).length > 0) {
subscriberData.utmParameters = utmParams;
}
if (config.features?.leadMagnets?.enabled && leadMagnet) {
subscriberData.leadMagnet = leadMagnet;
}
const subscriber = await req.payload.create({
collection: config.subscribersSlug || "subscribers",
data: subscriberData,
overrideAccess: true
// Public endpoint needs to create subscribers
});
if (config.features?.surveys?.enabled && surveyResponses) {
}
if (settings?.subscriptionSettings?.requireDoubleOptIn) {
try {
const token = generateMagicLinkToken(
String(subscriber.id),
subscriber.email,
config
);
const serverURL = req.payload.config.serverURL || process.env.PAYLOAD_PUBLIC_SERVER_URL || "";
const magicLinkURL = generateMagicLinkURL(token, serverURL, config);
const emailService = req.payload.newsletterEmailService;
if (emailService) {
const html = await renderEmail("magic-link", {
magicLink: magicLinkURL,
email: subscriber.email,
siteName: settings?.brandSettings?.siteName || "Newsletter",
expiresIn: config.auth?.tokenExpiration || "7d"
});
await emailService.send({
to: subscriber.email,
subject: settings?.brandSettings?.siteName ? `Verify your email for ${settings.brandSettings.siteName}` : "Verify your email",
html
});
} else {
console.warn("Email service not initialized, cannot send magic link");
}
} catch (error) {
console.error("Failed to send magic link email:", error);
}
}
return Response.json({
success: true,
subscriber: {
id: subscriber.id,
email: subscriber.email,
subscriptionStatus: subscriber.subscriptionStatus
},
message: settings?.subscriptionSettings?.requireDoubleOptIn ? "Please check your email to confirm your subscription" : "Successfully subscribed"
});
} catch {
return Response.json({
success: false,
error: "Failed to subscribe. Please try again."
}, { status: 500 });
}
}
};
};
// src/endpoints/verify-magic-link.ts
var createVerifyMagicLinkEndpoint = (config) => {
return {
path: "/newsletter/verify-magic-link",
method: "post",
handler: async (req) => {
try {
const data = await req.json();
const { token } = data;
if (!token) {
return Response.json({
success: false,
error: "Token is required"
}, { status: 400 });
}
let payload;
try {
payload = verifyMagicLinkToken(token);
} catch (error) {
return Response.json({
success: false,
error: error instanceof Error ? error.message : "Invalid token"
}, { status: 401 });
}
const subscriber = await req.payload.findByID({
collection: config.subscribersSlug || "subscribers",
id: payload.subscriberId
// Keep overrideAccess: true for token verification
});
if (!subscriber) {
return Response.json({
success: false,
error: "Subscriber not found"
}, { status: 404 });
}
if (subscriber.email !== payload.email) {
return Response.json({
success: false,
error: "Invalid token"
}, { status: 401 });
}
if (subscriber.subscriptionStatus === "unsubscribed") {
return Response.json({
success: false,
error: "This email has been unsubscribed"
}, { status: 403 });
}
const syntheticUser = {
collection: "subscribers",
id: subscriber.id,
email: subscriber.email
};
let isNewlyActivated = false;
if (subscriber.subscriptionStatus === "pending") {
await req.payload.update({
collection: config.subscribersSlug || "subscribers",
id: subscriber.id,
data: {
subscriptionStatus: "active"
},
overrideAccess: false,
user: syntheticUser
});
isNewlyActivated = true;
}
await req.payload.update({
collection: config.subscribersSlug || "subscribers",
id: subscriber.id,
data: {
magicLinkToken: null,
magicLinkTokenExpiry: null
},
overrideAccess: false,
user: syntheticUser
});
const sessionToken = generateSessionToken(
String(subscriber.id),
subscriber.email
);
if (isNewlyActivated) {
try {
const emailService = req.payload.newsletterEmailService;
if (emailService) {
const settings = await req.payload.findGlobal({
slug: config.settingsSlug || "newsletter-settings"
});
const serverURL = req.payload.config.serverURL || process.env.PAYLOAD_PUBLIC_SERVER_URL || "";
const html = await renderEmail("welcome", {
email: subscriber.email,
siteName: settings?.brandSettings?.siteName || "Newsletter",
preferencesUrl: `${serverURL}/account/preferences`
// This could be customized
});
await emailService.send({
to: subscriber.email,
subject: settings?.brandSettings?.siteName ? `Welcome to ${settings.brandSettings.siteName}!` : "Welcome!",
html
});
} else {
console.warn("Email service not initialized, cannot send welcome email");
}
} catch (error) {
console.error("Failed to send welcome email:", error);
}
}
const headers = new Headers();
headers.append("Set-Cookie", `newsletter-auth=${sessionToken}; HttpOnly; Secure=${process.env.NODE_ENV === "production"}; SameSite=Lax; Path=/; Max-Age=${30 * 24 * 60 * 60}`);
return Response.json({
success: true,
sessionToken,
subscriber: {
id: subscriber.id,
email: subscriber.email,
name: subscriber.name,
locale: subscriber.locale,
emailPreferences: subscriber.emailPreferences
}
}, { headers });
} catch (error) {
console.error("Verify magic link error:", error);
return Response.json({
success: false,
error: "Failed to verify magic link"
}, { status: 500 });
}
}
};
};
// src/endpoints/preferences.ts
var createPreferencesEndpoint = (config) => {
return {
path: "/newsletter/preferences",
method: "get",
handler: async (req) => {
try {
const authHeader = req.headers.get("authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return Response.json({
success: false,
error: "Authorization required"
}, { status: 401 });
}
const token = authHeader.substring(7);
let payload;
try {
payload = verifySessionToken(token);
} catch (error) {
return Response.json({
success: false,
error: error instanceof Error ? error.message : "Invalid token"
}, { status: 401 });
}
const subscriber = await req.payload.findByID({
collection: config.subscribersSlug || "subscribers",
id: payload.subscriberId,
overrideAccess: false,
user: {
collection: "subscribers",
id: payload.subscriberId,
email: payload.email
}
});
if (!subscriber) {
return Response.json({
success: false,
error: "Subscriber not found"
}, { status: 404 });
}
return Response.json({
success: true,
subscriber: {
id: subscriber.id,
email: subscriber.email,
name: subscriber.name,
locale: subscriber.locale,
emailPreferences: subscriber.emailPreferences,
subscriptionStatus: subscriber.subscriptionStatus
}
});
} catch (error) {
console.error("Get preferences error:", error);
return Response.json({
success: false,
error: "Failed to get preferences"
}, { status: 500 });
}
}
};
};
var createUpdatePreferencesEndpoint = (config) => {
return {
path: "/newsletter/preferences",
method: "post",
handler: async (req) => {
try {
const authHeader = req.headers.get("authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return Response.json({
success: false,
error: "Authorization required"
}, { status: 401 });
}
const token = authHeader.substring(7);
let payload;
try {
payload = verifySessionToken(token);
} catch (error) {
return Response.json({
success: false,
error: error instanceof Error ? error.message : "Invalid token"
}, { status: 401 });
}
const data = await req.json();
const { name, locale, emailPreferences } = data;
const updateData = {};
if (name !== void 0) {
updateData.name = name;
}
if (locale !== void 0) {
updateData.locale = locale;
}
if (emailPreferences !== void 0) {
updateData.emailPreferences = emailPreferences;
}
const subscriber = await req.payload.update({
collection: config.subscribersSlug || "subscribers",
id: payload.subscriberId,
data: updateData,
overrideAccess: false,
user: {
collection: "subscribers",
id: payload.subscriberId,
email: payload.email
}
});
return Response.json({
success: true,
subscriber: {
id: subscriber.id,
email: subscriber.email,
name: subscriber.name,
locale: subscriber.locale,
emailPreferences: subscriber.emailPreferences,
subscriptionStatus: subscriber.subscriptionStatus
}
});
} catch (error) {
console.error("Update preferences error:", error);
return Response.json({
success: false,
error: "Failed to update preferences"
}, { status: 500 });
}
}
};
};
// src/endpoints/unsubscribe.ts
var createUnsubscribeEndpoint = (config) => {
return {
path: "/newsletter/unsubscribe",
method: "post",
handler: async (req) => {
try {
const data = await req.json();
const { email, token } = data;
if (!email && !token) {
return Response.json({
success: false,
error: "Email or token is required"
}, { status: 400 });
}
let subscriber;
if (token) {
try {
const jwt3 = await import("jsonwebtoken");
const payload = jwt3.verify(
token,
process.env.JWT_SECRET || process.env.PAYLOAD_SECRET || ""
);
if (payload.type !== "unsubscribe") {
throw new Error("Invalid token type");
}
subscriber = await req.payload.findByID({
collection: config.subscribersSlug || "subscribers",
id: payload.subscriberId
});
} catch {
return Response.json({
success: false,
error: "Invalid or expired unsubscribe link"
}, { status: 401 });
}
} else {
if (!email || !isValidEmail(email)) {
return Response.json({
success: false,
error: "Invalid email format"
}, { status: 400 });
}
const result = await req.payload.find({
collection: config.subscribersSlug || "subscribers",
where: {
email: {
equals: email.toLowerCase()
}
}
});
if (result.docs.length === 0) {
return Response.json({
success: true,
message: "If this email was subscribed, it has been unsubscribed."
});
}
subscriber = result.docs[0];
}
if (!subscriber) {
return Response.json({
success: true,
message: "If this email was subscribed, it has been unsubscribed."
});
}
if (subscriber.subscriptionStatus === "unsubscribed") {
return Response.json({
success: true,
message: "Already unsubscribed"
});
}
await req.payload.update({
collection: config.subscribersSlug || "subscribers",
id: subscriber.id,
data: {
subscriptionStatus: "unsubscribed",
unsubscribedAt: (/* @__PURE__ */ new Date()).toISOString()
},
overrideAccess: false,
user: {
collection: "subscribers",
id: subscriber.id,
email: subscriber.email
}
});
return Response.json({
success: true,
message: "Successfully unsubscribed"
});
} catch (error) {
console.error("Unsubscribe error:", error);
return Response.json({
success: false,
error: "Failed to unsubscribe. Please try again."
}, { status: 500 });
}
}
};
};
// src/utils/rate-limiter.ts
var RateLimiter = class {
constructor(options) {
this.attempts = /* @__PURE__ */ new Map();
this.options = options;
}
async checkLimit(key) {
const now = Date.now();
const record = this.attempts.get(key);
if (!record || record.resetTime < now) {
this.attempts.set(key, {
count: 1,
resetTime: now + this.options.windowMs
});
return true;
}
if (record.count >= this.options.maxAttempts) {
return false;
}
record.count++;
return true;
}
async incrementAttempt(key) {
const now = Date.now();
const record = this.attempts.get(key);
if (!record || record.resetTime < now) {
this.attempts.set(key, {
count: 1,
resetTime: now + this.options.windowMs
});
} else {
record.count++;
}
}
async reset(key) {
this.attempts.delete(key);
}
async resetAll() {
this.attempts.clear();
}
};
// src/endpoints/signin.ts
var signinRateLimiter = new RateLimiter({
maxAttempts: 5,
windowMs: 15 * 60 * 1e3,
// 15 minutes
prefix: "signin"
});
var createSigninEndpoint = (config) => {
return {
path: "/newsletter/signin",
method: "post",
handler: async (req) => {
try {
const data = await req.json();
const { email } = data;
const validation = validateSubscriberData({ email });
if (!validation.valid) {
return Response.json({
success: false,
errors: validation.errors
}, { status: 400 });
}
const rateLimitKey = `signin:${email.toLowerCase()}`;
const allowed = await signinRateLimiter.checkLimit(rateLimitKey);
if (!allowed) {
return Response.json({
success: false,
error: "Too many sign-in attempts. Please try again later."
}, { status: 429 });
}
const result = await req.payload.find({
collection: config.subscribersSlug || "subscribers",
where: {
email: { equals: email.toLowerCase() },
subscriptionStatus: { equals: "active" }
},
limit: 1,
overrideAccess: true
// Need to check subscriber exists
});
if (result.docs.length === 0) {
return Response.json({
success: false,
error: "Email not found. Please subscribe first."
}, { status: 404 });
}
const subscriber = result.docs[0];
const token = generateMagicLinkToken(
String(subscriber.id),
subscriber.email,
config
);
const serverURL = req.payload.config.serverURL || process.env.PAYLOAD_PUBLIC_SERVER_URL || "";
const magicLinkURL = generateMagicLinkURL(token, serverURL, config);
const emailService = req.payload.newsletterEmailService;
if (emailService) {
const settings = await req.payload.findGlobal({
slug: config.settingsSlug || "newsletter-settings"
});
const html = await renderEmail("signin", {
magicLink: magicLinkURL,
email: subscriber.email,
siteName: settings?.brandSettings?.siteName || "Newsletter",
expiresIn: config.auth?.tokenExpiration || "7d"
});
await emailService.send({
to: subscriber.email,
subject: settings?.brandSettings?.siteName ? `Sign in to ${settings.brandSettings.siteName}` : "Sign in to your account",
html
});
} else {
console.warn("Email service not initialized, cannot send sign-in link");
}
return Response.json({
success: true,
message: "Check your email for the sign-in link"
});
} catch (error) {
console.error("Sign-in error:", error);
return Response.json({
success: false,
error: "Failed to process sign-in request"
}, { status: 500 });
}
}
};
};
// src/endpoints/me.ts
var createMeEndpoint = (config) => {
return {
path: "/newsletter/me",
method: "get",
handler: async (req) => {
try {
const cookieHeader = req.headers.get("cookie") || "";
const cookies = Object.fromEntries(
cookieHeader.split("; ").map((c) => {
const [key, ...value] = c.split("=");
return [key, value.join("=")];
})
);
const token = cookies["newsletter-auth"];
if (!token) {
return Response.json({
success: false,
error: "Not authenticated"
}, { status: 401 });
}
let payload;
try {
payload = verifySessionToken(token);
} catch {
return Response.json({
success: false,
error: "Invalid or expired session"
}, { status: 401 });
}
const subscriber = await req.payload.findByID({
collection: config.subscribersSlug || "subscribers",
id: payload.subscriberId,
overrideAccess: true
// Need to get subscriber data
});
if (!subscriber || subscriber.subscriptionStatus !== "active") {
return Response.json({
success: false,
error: "Not authenticated"
}, { status: 401 });
}
return Response.json({
success: true,
subscriber: {
id: subscriber.id,
email: subscriber.email,
name: subscriber.name,
status: subscriber.subscriptionStatus,
preferences: {
frequency: subscriber.emailPreferences?.frequency,
categories: subscriber.emailPreferences?.categories
},
createdAt: subscriber.createdAt,
updatedAt: subscriber.updatedAt
}
});
} catch (error) {
console.error("Me endpoint error:", error);
return Response.json({
success: false,
error: "Internal server error"
}, { status: 500 });
}
}
};
};
// src/endpoints/signout.ts
var createSignoutEndpoint = (_config) => {
return {
path: "/newsletter/signout",
method: "post",
handler: (_req) => {
try {
const headers = new Headers();
headers.append("Set-Cookie", `newsletter-auth=; HttpOnly; Secure=${process.env.NODE_ENV === "production"}; SameSite=Lax; Path=/; Max-Age=0`);
return Response.json({
success: true,
message: "Signed out successfully"
}, { headers });
} catch (error) {
console.error("Signout error:", error);
return Response.json({
success: false,
error: "Failed to sign out"
}, { status: 500 });
}
}
};
};
// src/endpoints/broadcasts/send.ts
init_types();
// src/utils/auth.ts
async function getAuthenticatedUser(req) {
try {
const me = await req.payload.find({
collection: "users",
where: {
id: {
equals: "me"
// Special value in Payload to get current user
}
},
limit: 1,
depth: 0
});
return me.docs[0] || null;
} catch {
return null;
}
}
async function requireAdmin(req, config) {
const user = await getAuthenticatedUser(req);
if (!user) {
return {
authorized: false,
error: "Authentication required"
};
}
if (!isAdmin(user, config)) {
return {
authorized: false,
error: "Admin access required"
};
}
return {
authorized: true,
user
};
}
// src/endpoints/broadcasts/send.ts
var createSendBroadcastEndpoint = (config, collectionSlug) => {
return {
path: `/${collectionSlug}/:id/send`,
method: "post",
handler: async (req) => {
try {
const auth = await requireAdmin(req, config);
if (!auth.authorized) {
return Response.json({
success: false,
error: auth.error
}, { status: 401 });
}
const broadcastProvider = req.payload.newsletterProvider;
if (!broadcastProvider) {
return Response.json({
success: false,
error: "Broadcast management is not enabled"
}, { status: 400 });
}
const url = new URL(req.url || "", `http://localhost`);
const pathParts = url.pathname.split("/");
const id = pathParts[pathParts.length - 2];
if (!id) {
return Response.json({
success: false,
error: "Broadcast ID is required"
}, { status: 400 });
}
const data = await (req.json?.() || Promise.resolve({}));
const broadcastDoc = await req.payload.findByID({
collection: collectionSlug,
id,
user: auth.user
});
if (!broadcastDoc || !broadcastDoc.providerId) {
return Response.json({
success: false,
error: "Broadcast not found or not synced with provider"
}, { status: 404 });
}
const broadcast = await broadcastProvider.send(broadcastDoc.providerId, data);
await req.payload.update({
collection: collectionSlug,
id,
data: {
status: "sending" /* SENDING */,
sentAt: (/* @__PURE__ */ new Date()).toISOString()
},
user: auth.user
});
return Response.json({
success: true,
message: "Broadcast sent successfully",
broadcast
});
} catch (error) {
console.error("Failed to send broadcast:", error);
if (error instanceof NewsletterProviderError) {
return Response.json({
success: false,
error: error.message,
code: error.code
}, { status: error.code === "NOT_SUPPORTED" ? 501 : 500 });
}
return Response.json({
success: false,
error: "Failed to send broadcast"
}, { status: 500 });
}
}
};
};
// src/endpoints/broadcasts/schedule.ts
init_types();
var createScheduleBroadcastEndpoint = (config, collectionSlug) => {
return {
path: `/${collectionSlug}/:id/schedule`,
method: "post",
handler: async (req) => {
try {
const auth = await requireAdmin(req, config);
if (!auth.authorized) {
return Response.json({
success: false,
error: auth.error
}, { status: 401 });
}
const broadcastProvider = req.payload.newsletterProvider;
if (!broadcastProvider) {
return Response.json({
success: false,
error: "Broadcast management is not enabled"
}, { status: 400 });
}
const url = new URL(req.url || "", `http://localhost`);
const pathParts = url.pathname.split("/");
const id = pathParts[pathParts.length - 2];
if (!id) {
return Response.json({
success: false,
error: "Broadcast ID is required"
}, { status: 400 });
}
const data = await (req.json?.() || Promise.resolve({}));
const { scheduledAt } = data;
if (!scheduledAt) {
return Response.json({
success: false,
error: "scheduledAt is required"
}, { status: 400 });
}
const scheduledDate = new Date(scheduledAt);
if (isNaN(scheduledDate.getTime())) {
return Response.json({
success: false,
error: "Invalid scheduledAt date"
}, { status: 400 });
}
if (scheduledDate <= /* @__PURE__ */ new Date()) {
return Response.json({
success: false,
error: "scheduledAt must be in the future"
}, { status: 400 });
}
const broadcastDoc = await req.payload.findByID({
collection: collectionSlug,
id,
user: auth.user
});
if (!broadcastDoc || !broadcastDoc.providerId) {
return Response.json({
success: false,
error: "Broadcast not found or not synced with provider"
}, { status: 404 });
}
const broadcast = await broadcastProvider.schedule(broadcastDoc.providerId, scheduledDate);
await req.payload.update({
collection: collectionSlug,
id,
data: {
status: "scheduled" /* SCHEDULED */,
scheduledAt: scheduledDate.toISOString()
},
user: auth.user
});
return Response.json({
success: true,
message: `Broadcast scheduled for ${scheduledDate.toISOString()}`,
broadcast
});
} catch (error) {
console.error("Failed to schedule broadcast:", error);
if (error instanceof NewsletterProviderError) {
return Response.json({
success: false,
error: error.message,
code: error.code
}, { status: error.code === "NOT_SUPPORTED" ? 501 : 500 });
}
return Response.json({
success: false,
error: "Failed to schedule broadcast"
}, { status: 500 });
}
}
};
};
// src/utils/emailSafeHtml.ts
var import_isomorphic_dompurify2 = __toESM(require("isomorphic-dompurify"), 1);
var EMAIL_SAFE_CONFIG = {
ALLOWED_TAGS: [
"p",
"br",
"strong",
"b",
"em",
"i",
"u",
"strike",
"s",
"span",
"a",
"h1",
"h2",
"h3",
"ul",
"ol",
"li",
"blockquote",
"hr"
],
ALLOWED_ATTR: ["href", "style", "target", "rel", "align"],
ALLOWED_STYLES: {
"*": [
"color",
"background-color",
"font-size",
"font-weight",
"font-style",
"text-decoration",
"text-align",
"margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
"padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
"line-height",
"border-left",
"border-left-width",
"border-left-style",
"border-left-color"
]
},
FORBID_TAGS: ["script", "style", "iframe", "object", "embed", "form", "input"],
FORBID_ATTR: ["class", "id", "onclick", "onload", "onerror"]
};
async function convertToEmailSafeHtml(editorState, options) {
const rawHtml = await lexicalToEmailHtml(editorState);
const sanitizedHtml = import_isomorphic_dompurify2.default.sanitize(rawHtml, EMAIL_SAFE_CONFIG);
if (options?.wrapInTemplate) {
return wrapInEmailTemplate(sanitizedHtml, options.preheader);
}
return sanitizedHtml;
}
async function lexicalToEmailHtml(editorState) {
const { root } = editorState;
if (!root || !root.children) {
return "";
}
const html = root.children.map((node) => convertNode(node)).join("");
return html;
}
function convertNode(node) {
switch (node.type) {
case "paragraph":
return convertParagraph(node);
case "heading":
return convertHeading(node);
case "list":
return convertList(node);
case "listitem":
return convertListItem(node);
case "blockquote":
return convertBlockquote(node);
case "text":
return convertText(node);
case "link":
return convertLink(node);
case "linebreak":
return "<br>";
default:
if (node.children) {
return node.children.map(convertNode).join("");
}
return "";
}
}
function convertParagraph(node) {
const align = getAlignment(node.format);
const children = node.children?.map(convertNode).join("") || "";
if (!children.trim()) {
return '<p style="margin: 0 0 16px 0; min-height: 1em;"> </p>';
}
return `<p style="margin: 0 0 16px 0; text-align: ${align};">${children}</p>`;
}
function convertHeading(node) {
const tag = node.tag || "h1";
const align = getAlignment(node.format);
const children = node.children?.map(convertNode).join("") || "";
const styles2 = {
h1: "font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;",
h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
h3: "font-size: 20px; font-weight: 600; margin: 0 0 12px 0; line-height: 1.4;"
};
const style = `${styles2[tag] || styles2.h3} text-align: ${align};`;
return `<${tag} style="${style}">${children}</${tag}>`;
}
function convertList(node) {
const tag = node.listType === "number" ? "ol" : "ul";
const children = node.children?.map(convertNode).join("") || "";
const style = tag === "ul" ? "margin: 0 0 16px 0; padding-left: 24px; list-style-type: disc;" : "margin: 0 0 16px 0; padding-left: 24px; list-style-type: decimal;";
return `<${tag} style="${style}">${children}</${tag}>`;
}
function convertListItem(node) {
const children = node.children?.map(convertNode).join("") || "";
return `<li style="margin: 0 0 8px 0;">${children}</li>`;
}
function convertBlockquote(node) {
const children = node.children?.map(convertNode).join("") || "";
const style = "margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;";
return `<blockquote style="${style}">${children}</blockquote>`;
}
function convertText(node) {
let text = escapeHtml(node.text || "");
if (node.format & 1) {
text = `<strong>${text}</strong>`;
}
if (node.format & 2) {
text = `<em>${text}</em>`;
}
if (node.format & 8) {
text = `<u>${text}</u>`;
}
if (node.format & 4) {
text = `<strike>${text}</strike>`;
}
return text;
}
function convertLink(node) {
const children = node.children?.map(convertNode).join("") || "";
const url = node.fields?.url || "#";
return `<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer" style="color: #2563eb; text-decoration: underline;">${children}</a>`;
}
function getAlignment(format) {
if (!format) return "left";
if (format & 2) return "center";
if (format & 3) return "right";
if (format & 4) return "justify";
return "left";
}
function escapeHtml(text) {
const map = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'"
};
return text.replace(/[&<>"']/g, (m) => map[m]);
}
function wrapInEmailTemplate(content, preheader) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email</title>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; font-size: 16px; line-height: 1.5; color: #333333; background-color: #f3f4f6;">
${preheader ? `<div style="display: none; max-height: 0; overflow: hidden;">${escapeHtml(preheader)}</div>` : ""}
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="margin: 0; padding: 0;">
<tr>
<td align="center" style="padding: 20px 0;">
<table role="presentation" cellpadding="0" cellspacing="0" width="600" style="margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden;">
<tr>
<td style="padding: 40px 30px;">
${content}
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>`;
}
// src/endpoints/broadcasts/test.ts
var createTestBroadcastEndpoint = (config, collectionSlug) => {
return {
path: `/${collectionSlug}/:id/test`,
method: "post",
handler: async (req) => {
try {
const auth = await requireAdmin(req, config);
if (!auth.authorized) {
return Response.json({
success: false,
error: auth.error
}, { status: 401 });
}
const url = new URL(req.url || "", `http://localhost`);
const pathParts = url.pathname.split("/");
const id = pathParts[pathParts.length - 2];
if (!id) {
return Response.json({
success: false,
error: "Broadcast ID is required"
}, { status: 400 });
}
const data = await (req.json?.() || Promise.resolve({}));
const testEmail = data.email || auth.user.email;
if (!testEmail) {
return Response.json({
success: false,
error: "No email address available for test send"
}, { status: 400 });
}
const broadcast = await req.payload.findByID({
collection: collectionSlug,
id,
user: auth.user
});
if (!broadcast) {
return Response.json({
success: false,
error: "Broadcast not found"
}, { status: 404 });
}
const channel = await req.payload.findByID({
collection: "channels",
id: typeof broadcast.channel === "string" ? broadcast.channel : broadcast.channel.id,
user: auth.user
});
const htmlContent = await convertToEmailSafeHtml(broadcast.content, {
wrapInTemplate: true,
preheader: broadcast.preheader
});
const emailService = req.payload.newsletterEmailService;
if (!emailService) {
return Response.json({
success: false,
error: "Email service is not configured"
}, { status: 500 });
}
await emailService.send({
to: testEmail,
from: channel?.fromEmail || config.providers.resend?.fromAddress || "noreply@example.com",
fromName: channel?.fromName || config.providers.resend?.fromName || "Newsletter",
replyTo: broadcast.settings?.replyTo || channel?.replyTo,
subject: `[TEST] ${broadcast.subject}`,
html: htmlContent,
trackOpens: false,
trackClicks: false
});
return Response.json({
success: true,
message: `Test email sent to ${testEmail}`
});
} catch (error) {
console.error("Failed to send test broadcast:", error);
return Response.json({
success: false,
error: "Failed to send test email"
}, { status: 500 });
}
}
};
};
// src/endpoints/broadcasts/index.ts
var createBroadcastManagementEndpoints = (config) => {
if (!config.features?.newsletterManagement?.enabled) {
return [];
}
const collectionSlug = config.features.newsletterManagement.collections?.broadcasts || "broadcasts";
return [
createSendBroadcastEndpoint(config, collectionSlug),
createScheduleBroadcastEndpoint(config, collectionSlug),
createTestBroadcastEndpoint(config, collectionSlug)
];
};
// src/endpoints/index.ts
function createNewsletterEndpoints(config) {
const endpoints = [
createSubscribeEndpoint(config),
createUnsubscribeEndpoint(config)
];
if (config.auth?.enabled !== false) {
endpoints.push(
createVerifyMagicLinkEndpoint(config),
createPreferencesEndpoint(config),
createUpdatePreferencesEndpoint(config),
createSigninEndpoint(config),
createMeEndpoint(config),
createSignoutEndpoint(config)
);
}
endpoints.push(...createBroadcastManagementEndpoints(config));
return endpoints;
}
// src/fields/newsletterScheduling.ts
function createNewsletterSchedulingFields(config) {
const groupName = config.features?.newsletterScheduling?.fields?.groupName || "newsletterScheduling";
const contentField = config.features?.newsletterScheduling?.fields?.contentField || "content";
const createMarkdownField = config.features?.newsletterScheduling?.fields?.createMarkdownField !== false;
const fields = [
{
name: groupName,
type: "group",
label: "Newsletter Scheduling",
admin: {
condition: (data, { user }) => user?.collection === "users"
// Only show for admin users
},
fields: [
{
name: "scheduled",
type: "checkbox",
label: "Schedule for Newsletter",
defaultValue: false,
admin: {
description: "Schedule this content to be sent as a newsletter"
}
},
{
name: "scheduledDate",
type: "date",
label: "Send Date",
required: true,
admin: {
date: {
pickerAppearance: "dayAndTime"
},
condition: (data) => data?.[groupName]?.scheduled,
description: "When to send this newsletter"
}
},
{
name: "sentDate",
type: "date",
label: "Sent Date",
admin: {
readOnly: true,
condition: (data) => data?.[groupName]?.sendStatus === "sent",
description: "When this newsletter was sent"
}
},
{
name: "sendStatus",
type: "select",
label: "Status",
options: [
{ label: "Draft", value: "draft" },
{ label: "Scheduled", value: "scheduled" },
{ label: "Sending", value: "sending" },
{ label: "Sent", value: "sent" },
{ label: "Failed", value: "failed" }
],
defaultValue: "draft",
admin: {
readOnly: true,
description: "Current send status"
}
},
{
name: "emailSubject",
type: "text",
label: "Email Subject",
required: true,
admin: {
condition: (data) => data?.[groupName]?.scheduled,
description: "Subject line for the newsletter email"
}
},
{
name: "preheader",
type: "text",
label: "Email Preheader",
admin: {
condition: (data) => data?.[groupName]?.scheduled,
description: "Preview text that appears after the subject line"
}
},
{
name: "segments",
type: "select",
label: "Target Segments",
hasMany: true,
options: [
{ label: "All Subscribers", value: "all" },
...config.i18n?.locales?.map((locale) => ({
label: `${locale.toUpperCase()} Subscribers`,
value: locale
})) || []
],
defaultValue: ["all"],
admin: {
condition: (data) => data?.[groupName]?.scheduled,
description: "Which subscriber segments to send to"
}
},
{
name: "testEmails",
type: "array",
label: "Test Email Recipients",
admin: {
condition: (data) => data?.[groupName]?.scheduled && data?.[groupName]?.sendStatus === "draft",
description: "Send test emails before scheduling"
},
fields: [
{
name: "email",
type: "email",
required: true
}
]
}
]
}
];
if (createMarkdownField) {
fields.push(createMarkdownFieldInternal({
name: `${contentField}Markdown`,
richTextField: contentField,
label: "Email Content (Markdown)",
admin: {
position: "sidebar",
condition: (data) => Boolean(data?.[contentField] && data?.[groupName]?.scheduled),
description: "Markdown version for email rendering",
readOnly: true
}
}));
}
return fields;
}
function createMarkdownFieldInternal(config) {
return {
name: config.name,
type: "textarea",
label: config.label || "Markdown",
admin: {
...config.admin,
description: config.admin?.description || "Auto-generated from rich text content"
},
hooks: {
afterRead: [
async ({ data }) => {
if (data?.[config.richTextField]) {
try {
const { convertLexicalToMarkdown } = await import("@payloadcms/richtext-lexical");
return convertLexicalToMarkdown({
data: data[config.richTextField]
});
} catch {
return "";
}
}
return "";
}
],
beforeChange: [
() => {
return null;
}
]
}
};
}
// src/jobs/sync-unsubscribes.ts
var createUnsubscribeSyncJob = (pluginConfig) => {
return {
slug: "sync-unsubscribes",
label: "Sync Unsubscribes from Email Service",
handler: async ({ req }) => {
const subscribersSlug = pluginConfig.subscribersSlug || "subscribers";
const emailService = req.payload.newsletterEmailService;
if (!emailService) {
console.error("Email service not configured");
return {
output: {
syncedCount: 0
}
};
}
let syncedCount = 0;
try {
if (emailService.getProvider() === "broadcast") {
console.warn("Starting Broadcast unsubscribe sync...");
const broadcastConfig = pluginConfig.providers?.broadcast;
if (!broadcastConfig) {
throw new Error("Broadcast configuration not found");
}
const apiUrl = broadcastConfig.apiUrl.replace(/\/$/, "");
const token = process.env.NODE_ENV === "production" ? broadcastConfig.tokens.production : broadcastConfig.tokens.development;
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(
`${apiUrl}/api/v1/subscribers.json?page=${page}`,
{
headers: {
"Authorization": `Bearer ${token}`
}
}
);
if (!response.ok) {
throw new Error(`Broadcast API error: ${response.status}`);
}
const data = await response.json();
const broadcastSubscribers = data.subscribers || [];
for (const broadcastSub of broadcastSubscribers) {
const payloadSubscribers = await req.payload.find({
collection: subscribersSlug,
where: {
email: {
equals: broadcastSub.email
}
},
limit: 1
});
if (payloadSubscribers.docs.length > 0) {
const payloadSub = payloadSubscribers.docs[0];
const broadcastUnsubscribed = !broadcastSub.is_active || broadcastSub.unsubscribed_at;
const payloadUnsubscribed = payloadSub.subscriptionStatus === "unsubscribed";
if (broadcastUnsubscribed && !payloadUnsubscribed) {
await req.payload.update({
collection: subscribersSlug,
id: payloadSub.id,
data: {
subscriptionStatus: "unsubscribed",
unsubscribedAt: broadcastSub.unsubscribed_at || (/* @__PURE__ */ new Date()).toISOString()
}
});
syncedCount++;
console.warn(`Unsubscribed: ${broadcastSub.email}`);
}
}
}
if (data.pagination && data.pagination.current < data.pagination.total_pages) {
page++;
} else {
hasMore = false;
}
}
console.warn(`Broadcast sync complete. Unsubscribed ${syncedCount} contacts.`);
}
if (emailService.getProvider() === "resend") {
console.warn("Starting Resend unsubscribe sync...");
const resendConfig = pluginConfig.providers?.resend;
if (!resendConfig) {
throw new Error("Resend configuration not found");
}
console.warn("Resend polling implementation needed - webhooks recommended");
}
if (pluginConfig.hooks?.afterUnsubscribeSync) {
await pluginConfig.hooks.afterUnsubscribeSync({
req,
syncedCount
});
}
} catch (error) {
console.error("Unsubscribe sync error:", error);
throw error;
}
return {
output: {
syncedCount
}
};
}
};
};
// src/collections/Channels.ts
var createChannelsCollection = (pluginConfig) => {
const hasProviders = !!(pluginConfig.providers?.broadcast || pluginConfig.providers?.resend);
return {
slug: "channels",
labels: {
singular: "Channel",
plural: "Channels"
},
admin: {
useAsTitle: "name",
description: "Newsletter channels/publications that can send broadcasts",
defaultColumns: ["name", "fromEmail", "subscriberCount", "active"]
},
fields: [
{
name: "name",
type: "text",
required: true,
admin: {
description: "The name of this newsletter channel"
}
},
{
name: "description",
type: "textarea",
admin: {
description: "A brief description of what this channel is about"
}
},
{
name: "fromName",
type: "text",
required: true,
admin: {
description: "The sender name that appears in emails"
}
},
{
name: "fromEmail",
type: "email",
required: true,
admin: {
description: "The sender email address"
}
},
{
name: "replyTo",
type: "email",
admin: {
description: "Reply-to email address (optional)"
}
},
{
name: "providerType",
type: "select",
required: true,
options: [
...pluginConfig.providers?.broadcast ? [{ label: "Broadcast", value: "broadcast" }] : [],
...pluginConfig.providers?.resend ? [{ label: "Resend", value: "resend" }] : []
],
admin: {
description: "Which email provider manages this channel"
}
},
{
name: "providerId",
type: "text",
admin: {
readOnly: true,
description: "ID from the email provider",
condition: (data) => hasProviders && data?.providerId
}
},
{
name: "subscriberCount",
type: "number",
admin: {
readOnly: true,
description: "Number of active subscribers"
},
defaultValue: 0
},
{
name: "active",
type: "checkbox",
defaultValue: true,
admin: {
description: "Whether this channel is currently active"
}
},
{
name: "settings",
type: "group",
fields: [
{
name: "defaultTrackOpens",
type: "checkbox",
defaultValue: true,
admin: {
description: "Track email opens by default for broadcasts in this channel"
}
},
{
name: "defaultTrackClicks",
type: "checkbox",
defaultValue: true,
admin: {
description: "Track link clicks by default for broadcasts in this channel"
}
},
{
name: "requireDoubleOptIn",
type: "checkbox",
defaultValue: false,
admin: {
description: "Require double opt-in for new subscribers"
}
}
]
}
],
hooks: {
// Sync with provider on create
afterChange: [
async ({ doc, operation, req }) => {
if (!hasProviders || operation !== "create") return doc;
try {
const provider = await getProvider(doc.providerType, pluginConfig);
if (!provider) return doc;
const providerChannel = await provider.createChannel({
name: doc.name,
description: doc.description,
fromName: doc.fromName,
fromEmail: doc.fromEmail,
replyTo: doc.replyTo
});
await req.payload.update({
collection: "channels",
id: doc.id,
data: {
providerId: providerChannel.id,
subscriberCount: providerChannel.subscriberCount || 0
},
req
});
return {
...doc,
providerId: providerChannel.id,
subscriberCount: providerChannel.subscriberCount || 0
};
} catch (error) {
req.payload.logger.error("Failed to create channel in provider:", error);
return doc;
}
}
],
// Sync updates with provider
beforeChange: [
async ({ data, originalDoc, operation, req }) => {
if (!hasProviders || !originalDoc?.providerId || operation !== "update") return data;
try {
const provider = await getProvider(originalDoc.providerType, pluginConfig);
if (!provider) return data;
const updates = {};
if (data.name !== originalDoc.name) updates.name = data.name;
if (data.description !== originalDoc.description) updates.description = data.description;
if (data.fromName !== originalDoc.fromName) updates.fromName = data.fromName;
if (data.fromEmail !== originalDoc.fromEmail) updates.fromEmail = data.fromEmail;
if (data.replyTo !== originalDoc.replyTo) updates.replyTo = data.replyTo;
if (Object.keys(updates).length > 0) {
await provider.updateChannel(originalDoc.providerId, updates);
}
} catch (error) {
req.payload.logger.error("Failed to update channel in provider:", error);
}
return data;
}
],
// Handle deletion
afterDelete: [
async ({ doc, req }) => {
if (!hasProviders || !doc?.providerId) return doc;
try {
const provider = await getProvider(doc.providerType, pluginConfig);
if (!provider) return doc;
await provider.deleteChannel(doc.providerId);
} catch (error) {
req.payload.logger.error("Failed to delete channel from provider:", error);
}
return doc;
}
]
}
};
};
async function getProvider(providerType, config) {
if (providerType === "broadcast") {
const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
const providerConfig = config.providers?.broadcast;
return providerConfig ? new BroadcastApiProvider2(providerConfig) : null;
}
if (providerType === "resend") {
const { ResendBroadcastProvider: ResendBroadcastProvider2 } = await Promise.resolve().then(() => (init_broadcast3(), broadcast_exports2));
const providerConfig = config.providers?.resend;
return providerConfig ? new ResendBroadcastProvider2(providerConfig) : null;
}
return null;
}
// src/collections/Broadcasts.ts
init_types();
// src/fields/emailContent.ts
var import_richtext_lexical = require("@payloadcms/richtext-lexical");
var emailSafeFeatures = [
// Basic text formatting
(0, import_richtext_lexical.BoldFeature)(),
(0, import_richtext_lexical.ItalicFeature)(),
(0, import_richtext_lexical.UnderlineFeature)(),
(0, import_richtext_lexical.StrikethroughFeature)(),
// Links with simple configuration
(0, import_richtext_lexical.LinkFeature)({
fields: [{
name: "url",
type: "text",
required: true,
admin: {
description: "Enter the full URL (including https://)"
}
}]
}),
// Lists
(0, import_richtext_lexical.OrderedListFeature)(),
(0, import_richtext_lexical.UnorderedListFeature)(),
// Headings - limited to h1, h2, h3 for email compatibility
(0, import_richtext_lexical.HeadingFeature)({
enabledHeadingSizes: ["h1", "h2", "h3"]
}),
// Basic paragraph and alignment
(0, import_richtext_lexical.ParagraphFeature)(),
(0, import_richtext_lexical.AlignFeature)(),
// Blockquotes
(0, import_richtext_lexical.BlockquoteFeature)()
];
var createEmailContentField = (overrides) => {
return {
name: "content",
type: "richText",
required: true,
editor: (0, import_richtext_lexical.lexicalEditor)({
features: emailSafeFeatures
}),
admin: {
description: "Email content with limited formatting for compatibility",
...overrides?.admin
},
...overrides
};
};
// src/collections/Broadcasts.ts
var createBroadcastsCollection = (pluginConfig) => {
const hasProviders = !!(pluginConfig.providers?.broadcast || pluginConfig.providers?.resend);
return {
slug: "broadcasts",
labels: {
singular: "Broadcast",
plural: "Broadcasts"
},
admin: {
useAsTitle: "name",
description: "Individual email campaigns sent to subscribers",
defaultColumns: ["name", "subject", "status", "channel", "sentAt", "actions"]
},
fields: [
{
name: "channel",
type: "relationship",
relationTo: "channels",
required: true,
admin: {
description: "The channel this broadcast belongs to"
}
},
{
name: "name",
type: "text",
required: true,
admin: {
description: "Internal name for this broadcast"
}
},
{
name: "subject",
type: "text",
required: true,
admin: {
description: "Email subject line"
}
},
{
name: "preheader",
type: "text",
admin: {
description: "Preview text shown in email clients"
}
},
createEmailContentField({
admin: {
description: "Email content"
}
}),
{
name: "emailPreview",
type: "ui",
admin: {
components: {
Field: "/src/components/Broadcasts/EmailPreviewField"
}
}
},
{
name: "status",
type: "select",
required: true,
defaultValue: "draft" /* DRAFT */,
options: [
{ label: "Draft", value: "draft" /* DRAFT */ },
{ label: "Scheduled", value: "scheduled" /* SCHEDULED */ },
{ label: "Sending", value: "sending" /* SENDING */ },
{ label: "Sent", value: "sent" /* SENT */ },
{ label: "Failed", value: "failed" /* FAILED */ },
{ label: "Paused", value: "paused" /* PAUSED */ },
{ label: "Canceled", value: "canceled" /* CANCELED */ }
],
admin: {
readOnly: true,
components: {
Cell: "/src/components/Broadcasts/StatusBadge"
}
}
},
{
name: "settings",
type: "group",
fields: [
{
name: "trackOpens",
type: "checkbox",
defaultValue: true,
admin: {
description: "Track when recipients open this email"
}
},
{
name: "trackClicks",
type: "checkbox",
defaultValue: true,
admin: {
description: "Track when recipients click links"
}
},
{
name: "replyTo",
type: "email",
admin: {
description: "Override the channel reply-to address for this broadcast"
}
}
]
},
{
name: "audienceIds",
type: "array",
fields: [
{
name: "audienceId",
type: "text",
required: true
}
],
admin: {
description: "Target specific audience segments",
condition: () => {
return hasProviders;
}
}
},
{
name: "analytics",
type: "group",
admin: {
readOnly: true,
condition: (data) => data?.status === "sent" /* SENT */
},
fields: [
{
name: "recipientCount",
type: "number",
defaultValue: 0
},
{
name: "sent",
type: "number",
defaultValue: 0
},
{
name: "delivered",
type: "number",
defaultValue: 0
},
{
name: "opened",
type: "number",
defaultValue: 0
},
{
name: "clicked",
type: "number",
defaultValue: 0
},
{
name: "bounced",
type: "number",
defaultValue: 0
},
{
name: "complained",
type: "number",
defaultValue: 0
},
{
name: "unsubscribed",
type: "number",
defaultValue: 0
}
]
},
{
name: "sentAt",
type: "date",
admin: {
readOnly: true,
date: {
displayFormat: "MMM d, yyyy h:mm a"
}
}
},
{
name: "scheduledAt",
type: "date",
admin: {
condition: (data) => data?.status === "scheduled" /* SCHEDULED */,
date: {
displayFormat: "MMM d, yyyy h:mm a"
}
}
},
{
name: "providerId",
type: "text",
admin: {
readOnly: true,
description: "ID from the email provider",
condition: (data) => hasProviders && data?.providerId
}
},
{
name: "providerData",
type: "json",
admin: {
readOnly: true,
condition: () => false
// Hidden by default
}
},
// UI Field for custom actions in list view
{
name: "actions",
type: "ui",
admin: {
components: {
Cell: "/src/components/Broadcasts/ActionsCell",
Field: "/src/components/Broadcasts/EmptyField"
},
disableListColumn: false
}
}
],
hooks: {
// Sync with provider on create
afterChange: [
async ({ doc, operation, req }) => {
if (!hasProviders || operation !== "create") return doc;
try {
const channel = await req.payload.findByID({
collection: "channels",
id: typeof doc.channel === "string" ? doc.channel : doc.channel.id,
req
});
const provider = await getProvider2(channel.providerType, pluginConfig);
if (!provider) return doc;
const htmlContent = await convertToEmailSafeHtml(doc.content);
const providerBroadcast = await provider.create({
channelId: channel.providerId || channel.id,
name: doc.name,
subject: doc.subject,
preheader: doc.preheader,
content: htmlContent,
trackOpens: doc.settings?.trackOpens,
trackClicks: doc.settings?.trackClicks,
replyTo: doc.settings?.replyTo,
audienceIds: doc.audienceIds?.map((a) => a.audienceId)
});
await req.payload.update({
collection: "broadcasts",
id: doc.id,
data: {
providerId: providerBroadcast.id,
providerData: providerBroadcast.providerData
},
req
});
return {
...doc,
providerId: providerBroadcast.id,
providerData: providerBroadcast.providerData
};
} catch (error) {
req.payload.logger.error("Failed to create broadcast in provider:", error);
return doc;
}
}
],
// Sync updates with provider
beforeChange: [
async ({ data, originalDoc, operation, req }) => {
if (!hasProviders || !originalDoc?.providerId || operation !== "update") return data;
try {
const channelId = data.channel || originalDoc.channel;
const channel = await req.payload.findByID({
collection: "channels",
id: typeof channelId === "string" ? channelId : channelId.id,
req
});
const provider = await getProvider2(channel.providerType, pluginConfig);
if (!provider) return data;
const capabilities = provider.getCapabilities();
if (!capabilities.editableStatuses.includes(originalDoc.status)) {
return data;
}
const updates = {};
if (data.name !== originalDoc.name) updates.name = data.name;
if (data.subject !== originalDoc.subject) updates.subject = data.subject;
if (data.preheader !== originalDoc.preheader) updates.preheader = data.preheader;
if (data.content !== originalDoc.content) {
updates.content = await convertToEmailSafeHtml(data.content);
}
if (data.settings?.trackOpens !== originalDoc.settings?.trackOpens) {
updates.trackOpens = data.settings.trackOpens;
}
if (data.settings?.trackClicks !== originalDoc.settings?.trackClicks) {
updates.trackClicks = data.settings.trackClicks;
}
if (data.settings?.replyTo !== originalDoc.settings?.replyTo) {
updates.replyTo = data.settings.replyTo;
}
if (JSON.stringify(data.audienceIds) !== JSON.stringify(originalDoc.audienceIds)) {
updates.audienceIds = data.audienceIds?.map((a) => a.audienceId);
}
if (Object.keys(updates).length > 0) {
await provider.update(originalDoc.providerId, updates);
}
} catch (error) {
req.payload.logger.error("Failed to update broadcast in provider:", error);
}
return data;
}
],
// Handle deletion
afterDelete: [
async ({ doc, req }) => {
if (!hasProviders || !doc?.providerId) return doc;
try {
const channel = await req.payload.findByID({
collection: "channels",
id: typeof doc.channel === "string" ? doc.channel : doc.channel.id,
req
});
const provider = await getProvider2(channel.providerType, pluginConfig);
if (!provider) return doc;
const capabilities = provider.getCapabilities();
if (capabilities.editableStatuses.includes(doc.status)) {
await provider.delete(doc.providerId);
}
} catch (error) {
req.payload.logger.error("Failed to delete broadcast from provider:", error);
}
return doc;
}
]
}
};
};
async function getProvider2(providerType, config) {
if (providerType === "broadcast") {
const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
const providerConfig = config.providers?.broadcast;
return providerConfig ? new BroadcastApiProvider2(providerConfig) : null;
}
if (providerType === "resend") {
const { ResendBroadcastProvider: ResendBroadcastProvider2 } = await Promise.resolve().then(() => (init_broadcast3(), broadcast_exports2));
const providerConfig = config.providers?.resend;
return providerConfig ? new ResendBroadcastProvider2(providerConfig) : null;
}
return null;
}
// src/index.ts
init_broadcast2();
init_broadcast3();
// src/utilities/session.ts
var import_jsonwebtoken2 = __toESM(require("jsonwebtoken"), 1);
var getTokenFromRequest = (req) => {
const cookies = req.cookies || req.headers?.cookie;
if (!cookies) return null;
if (typeof cookies === "string") {
const parsed = cookies.split(";").reduce((acc, cookie) => {
const [key, value] = cookie.trim().split("=");
acc[key] = value;
return acc;
}, {});
return parsed["newsletter-auth"] || null;
}
return cookies["newsletter-auth"] || null;
};
var verifyToken = (token, secret) => {
try {
const decoded = import_jsonwebtoken2.default.verify(token, secret);
return decoded;
} catch {
return null;
}
};
var getServerSideAuth = async (context, secret) => {
const token = getTokenFromRequest(context.req);
if (!token) {
return { subscriber: null, isAuthenticated: false };
}
const payloadSecret = secret || process.env.PAYLOAD_SECRET;
if (!payloadSecret) {
console.error("No secret provided for token verification");
return { subscriber: null, isAuthenticated: false };
}
const decoded = verifyToken(token, payloadSecret);
if (!decoded) {
return { subscriber: null, isAuthenticated: false };
}
return {
subscriber: decoded,
isAuthenticated: true
};
};
var requireAuth = (gssp) => {
return async (context) => {
const { isAuthenticated: isAuthenticated2, subscriber } = await getServerSideAuth(context);
if (!isAuthenticated2) {
return {
redirect: {
destination: "/auth/signin",
permanent: false
}
};
}
if (gssp) {
const result = await gssp(context);
return {
...result,
props: {
...result.props,
subscriber
}
};
}
return {
props: {
subscriber
}
};
};
};
var isAuthenticated = (req, secret) => {
const token = getTokenFromRequest(req);
if (!token) return false;
const decoded = verifyToken(token, secret);
return !!decoded;
};
// src/index.ts
var newsletterPlugin = (pluginConfig) => (incomingConfig) => {
const config = {
enabled: true,
subscribersSlug: "subscribers",
settingsSlug: "newsletter-settings",
auth: {
enabled: true,
tokenExpiration: "7d",
magicLinkPath: "/newsletter/verify",
...pluginConfig.auth
},
...pluginConfig
};
if (!config.enabled) {
return incomingConfig;
}
const subscribersCollection = createSubscribersCollection(config);
const settingsGlobal = createNewsletterSettingsGlobal(config);
let collections = [...incomingConfig.collections || [], subscribersCollection];
if (config.features?.newsletterManagement?.enabled) {
const channelsCollection = createChannelsCollection(config);
const broadcastsCollection = createBroadcastsCollection(config);
collections.push(channelsCollection, broadcastsCollection);
}
if (config.features?.newsletterScheduling?.enabled) {
const targetCollections = config.features.newsletterScheduling.collections || "articles";
const collectionsToExtend = Array.isArray(targetCollections) ? targetCollections : [targetCollections];
const schedulingFields = createNewsletterSchedulingFields(config);
collections = collections.map((collection) => {
if (collectionsToExtend.includes(collection.slug)) {
return {
...collection,
fields: [
...collection.fields,
...schedulingFields
]
};
}
return collection;
});
}
const endpoints = createNewsletterEndpoints(config);
const syncJob = config.features?.unsubscribeSync?.enabled ? createUnsubscribeSyncJob(config) : null;
const modifiedConfig = {
...incomingConfig,
collections,
globals: [
...incomingConfig.globals || [],
settingsGlobal
],
endpoints: [
...incomingConfig.endpoints || [],
...endpoints
],
jobs: syncJob ? {
...incomingConfig.jobs || {},
tasks: [
...incomingConfig.jobs?.tasks || [],
syncJob
],
// Add cron schedule if specified
autoRun: config.features?.unsubscribeSync?.schedule ? Array.isArray(incomingConfig.jobs?.autoRun) ? [...incomingConfig.jobs.autoRun, {
cron: config.features.unsubscribeSync.schedule,
queue: "newsletter-sync",
limit: 100
}] : typeof incomingConfig.jobs?.autoRun === "function" ? async (payload) => {
const autoRunFn = incomingConfig.jobs.autoRun;
const existingConfigs = await autoRunFn(payload);
return [...existingConfigs, {
cron: config.features.unsubscribeSync.schedule,
queue: "newsletter-sync",
limit: 100
}];
} : [{
cron: config.features.unsubscribeSync.schedule,
queue: "newsletter-sync",
limit: 100
}] : incomingConfig.jobs?.autoRun
} : incomingConfig.jobs,
onInit: async (payload) => {
try {
const settings = await payload.findGlobal({
slug: config.settingsSlug || "newsletter-settings"
});
let emailServiceConfig;
if (settings) {
emailServiceConfig = {
provider: settings.provider || config.providers.default,
fromAddress: settings.fromAddress || config.providers.resend?.fromAddress || config.providers.broadcast?.fromAddress || "noreply@example.com",
fromName: settings.fromName || config.providers.resend?.fromName || config.providers.broadcast?.fromName || "Newsletter",
replyTo: settings.replyTo,
resend: settings.provider === "resend" ? {
apiKey: settings.resendSettings?.apiKey || config.providers.resend?.apiKey || "",
audienceIds: settings.resendSettings?.audienceIds?.reduce((acc, item) => {
acc[item.locale] = {
production: item.production,
development: item.development
};
return acc;
}, {}) || config.providers.resend?.audienceIds
} : config.providers.resend,
broadcast: settings.provider === "broadcast" ? {
apiUrl: settings.broadcastSettings?.apiUrl || config.providers.broadcast?.apiUrl || "",
tokens: {
production: settings.broadcastSettings?.productionToken || config.providers.broadcast?.tokens.production,
development: settings.broadcastSettings?.developmentToken || config.providers.broadcast?.tokens.development
}
} : config.providers.broadcast
};
} else {
emailServiceConfig = {
provider: config.providers.default,
fromAddress: config.providers.resend?.fromAddress || config.providers.broadcast?.fromAddress || "noreply@example.com",
fromName: config.providers.resend?.fromName || config.providers.broadcast?.fromName || "Newsletter",
resend: config.providers.resend,
broadcast: config.providers.broadcast
};
}
payload.newsletterEmailService = createEmailService(emailServiceConfig);
console.warn("Newsletter plugin initialized with", payload.newsletterEmailService.getProvider(), "provider");
if (config.features?.newsletterManagement?.enabled) {
try {
let broadcastProvider;
if (config.features.newsletterManagement.provider) {
broadcastProvider = config.features.newsletterManagement.provider;
} else {
const providerType = emailServiceConfig.provider || config.providers.default;
if (providerType === "broadcast" && emailServiceConfig.broadcast) {
broadcastProvider = new BroadcastApiProvider(emailServiceConfig.broadcast);
} else if (providerType === "resend" && emailServiceConfig.resend) {
broadcastProvider = new ResendBroadcastProvider(emailServiceConfig.resend);
} else {
throw new Error(`Unsupported broadcast provider: ${providerType}`);
}
}
const payloadWithProvider = payload;
payloadWithProvider.broadcastProvider = broadcastProvider;
payloadWithProvider.newsletterProvider = broadcastProvider;
console.warn("Broadcast management initialized with", broadcastProvider.name, "provider");
} catch (error) {
console.error("Failed to initialize broadcast management provider:", error);
}
}
} catch (error) {
console.error("Failed to initialize newsletter email service:", error);
}
if (incomingConfig.onInit) {
await incomingConfig.onInit(payload);
}
}
};
return modifiedConfig;
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
getServerSideAuth,
getTokenFromRequest,
isAuthenticated,
newsletterPlugin,
requireAuth,
verifyToken
});
//# sourceMappingURL=index.cjs.map