payload-plugin-newsletter 0.20.7 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -369,6 +369,15 @@ var createSubscribersCollection = (pluginConfig) => {
369
369
  type: "date",
370
370
  hidden: true
371
371
  },
372
+ // External ID for webhook integration
373
+ {
374
+ name: "externalId",
375
+ type: "text",
376
+ admin: {
377
+ description: "ID from email service provider",
378
+ readOnly: true
379
+ }
380
+ },
372
381
  // Subscription status
373
382
  {
374
383
  name: "subscriptionStatus",
@@ -384,6 +393,14 @@ var createSubscribersCollection = (pluginConfig) => {
384
393
  description: "Current subscription status"
385
394
  }
386
395
  },
396
+ {
397
+ name: "subscribedAt",
398
+ type: "date",
399
+ admin: {
400
+ description: "When the user subscribed",
401
+ readOnly: true
402
+ }
403
+ },
387
404
  {
388
405
  name: "unsubscribedAt",
389
406
  type: "date",
@@ -393,6 +410,15 @@ var createSubscribersCollection = (pluginConfig) => {
393
410
  readOnly: true
394
411
  }
395
412
  },
413
+ {
414
+ name: "unsubscribeReason",
415
+ type: "text",
416
+ admin: {
417
+ condition: (data) => data?.subscriptionStatus === "unsubscribed",
418
+ description: "Reason for unsubscribing",
419
+ readOnly: true
420
+ }
421
+ },
396
422
  // Email preferences
397
423
  {
398
424
  name: "emailPreferences",
@@ -725,6 +751,75 @@ var createNewsletterSettingsGlobal = (pluginConfig) => {
725
751
  admin: {
726
752
  description: "Your Broadcast API token"
727
753
  }
754
+ },
755
+ {
756
+ type: "collapsible",
757
+ label: "Webhook Configuration",
758
+ fields: [
759
+ {
760
+ name: "webhookUrl",
761
+ type: "text",
762
+ label: "Webhook URL",
763
+ admin: {
764
+ readOnly: true,
765
+ description: "Copy this URL to your Broadcast webhook settings",
766
+ placeholder: "URL will be generated after save"
767
+ },
768
+ hooks: {
769
+ beforeChange: [
770
+ ({ req }) => {
771
+ const host = req.headers.get("host") || "localhost:3000";
772
+ const protocol = req.headers.get("x-forwarded-proto") || "http";
773
+ const baseUrl = process.env.PAYLOAD_PUBLIC_SERVER_URL || `${protocol}://${host}`;
774
+ return `${baseUrl}/api/newsletter/webhooks/broadcast`;
775
+ }
776
+ ]
777
+ }
778
+ },
779
+ {
780
+ name: "webhookConfiguration",
781
+ type: "ui",
782
+ admin: {
783
+ components: {
784
+ Field: "payload-plugin-newsletter/admin#WebhookConfiguration"
785
+ }
786
+ }
787
+ },
788
+ {
789
+ name: "webhookSecret",
790
+ type: "text",
791
+ label: "Webhook Secret",
792
+ admin: {
793
+ description: "Paste the webhook secret from Broadcast here"
794
+ }
795
+ },
796
+ {
797
+ name: "webhookStatus",
798
+ type: "select",
799
+ label: "Status",
800
+ options: [
801
+ { label: "Not Configured", value: "not_configured" },
802
+ { label: "Configured", value: "configured" },
803
+ { label: "Verified", value: "verified" },
804
+ { label: "Error", value: "error" }
805
+ ],
806
+ defaultValue: "not_configured",
807
+ admin: {
808
+ readOnly: true
809
+ }
810
+ },
811
+ {
812
+ name: "lastWebhookReceived",
813
+ type: "date",
814
+ label: "Last Event Received",
815
+ admin: {
816
+ readOnly: true,
817
+ date: {
818
+ displayFormat: "yyyy-MM-dd HH:mm:ss"
819
+ }
820
+ }
821
+ }
822
+ ]
728
823
  }
729
824
  ]
730
825
  },
@@ -2332,6 +2427,415 @@ var createBroadcastManagementEndpoints = (_config) => {
2332
2427
  return [];
2333
2428
  };
2334
2429
 
2430
+ // src/utils/webhooks/signature.ts
2431
+ import crypto from "crypto";
2432
+ function verifyBroadcastWebhookSignature(payload, signature, timestamp, secret) {
2433
+ try {
2434
+ const now = Math.floor(Date.now() / 1e3);
2435
+ const webhookTimestamp = parseInt(timestamp, 10);
2436
+ if (isNaN(webhookTimestamp)) {
2437
+ console.error("[Webhook Signature] Invalid timestamp format");
2438
+ return false;
2439
+ }
2440
+ if (Math.abs(now - webhookTimestamp) > 300) {
2441
+ console.error("[Webhook Signature] Timestamp too old or in future");
2442
+ return false;
2443
+ }
2444
+ const signatureParts = signature.split(",");
2445
+ if (signatureParts.length !== 2 || signatureParts[0] !== "v1") {
2446
+ console.error("[Webhook Signature] Invalid signature format");
2447
+ return false;
2448
+ }
2449
+ const actualSignature = signatureParts[1];
2450
+ const signedPayload = `${timestamp}.${payload}`;
2451
+ const expectedSignature = crypto.createHmac("sha256", secret).update(signedPayload).digest("base64");
2452
+ const actualBuffer = Buffer.from(actualSignature);
2453
+ const expectedBuffer = Buffer.from(expectedSignature);
2454
+ if (actualBuffer.length !== expectedBuffer.length) {
2455
+ return false;
2456
+ }
2457
+ return crypto.timingSafeEqual(actualBuffer, expectedBuffer);
2458
+ } catch (error) {
2459
+ console.error("[Webhook Signature] Verification error:", error);
2460
+ return false;
2461
+ }
2462
+ }
2463
+
2464
+ // src/types/webhooks.ts
2465
+ var WEBHOOK_EVENT_TYPES = {
2466
+ // Subscriber events
2467
+ SUBSCRIBER_SUBSCRIBED: "subscriber.subscribed",
2468
+ SUBSCRIBER_UNSUBSCRIBED: "subscriber.unsubscribed",
2469
+ // Broadcast events
2470
+ BROADCAST_SCHEDULED: "broadcast.scheduled",
2471
+ BROADCAST_QUEUEING: "broadcast.queueing",
2472
+ BROADCAST_SENDING: "broadcast.sending",
2473
+ BROADCAST_SENT: "broadcast.sent",
2474
+ BROADCAST_FAILED: "broadcast.failed",
2475
+ BROADCAST_PARTIAL_FAILURE: "broadcast.partial_failure",
2476
+ BROADCAST_ABORTED: "broadcast.aborted",
2477
+ BROADCAST_PAUSED: "broadcast.paused"
2478
+ };
2479
+ function isSubscriberEvent(event) {
2480
+ return event.type.startsWith("subscriber.");
2481
+ }
2482
+ function isBroadcastEvent(event) {
2483
+ return event.type.startsWith("broadcast.");
2484
+ }
2485
+ function isHandledEvent(event) {
2486
+ return Object.values(WEBHOOK_EVENT_TYPES).includes(event.type);
2487
+ }
2488
+
2489
+ // src/webhooks/handlers/subscriber.ts
2490
+ async function handleSubscriberEvent(event, req, config) {
2491
+ const { payload } = req;
2492
+ const subscribersSlug = config.subscribersSlug || "subscribers";
2493
+ switch (event.type) {
2494
+ case WEBHOOK_EVENT_TYPES.SUBSCRIBER_SUBSCRIBED:
2495
+ await handleSubscriberSubscribed(event, payload, subscribersSlug);
2496
+ break;
2497
+ case WEBHOOK_EVENT_TYPES.SUBSCRIBER_UNSUBSCRIBED:
2498
+ await handleSubscriberUnsubscribed(event, payload, subscribersSlug);
2499
+ break;
2500
+ default:
2501
+ console.warn("[Subscriber Handler] Unhandled event type:", event.type);
2502
+ }
2503
+ }
2504
+ async function handleSubscriberSubscribed(event, payload, subscribersSlug) {
2505
+ const { data } = event;
2506
+ try {
2507
+ const existing = await payload.find({
2508
+ collection: subscribersSlug,
2509
+ where: {
2510
+ email: {
2511
+ equals: data.email
2512
+ }
2513
+ },
2514
+ limit: 1
2515
+ });
2516
+ if (existing.docs.length > 0) {
2517
+ const subscriber = existing.docs[0];
2518
+ await payload.update({
2519
+ collection: subscribersSlug,
2520
+ id: subscriber.id,
2521
+ data: {
2522
+ subscriptionStatus: "active",
2523
+ subscribedAt: data.subscribed_at,
2524
+ // Store Broadcast ID for future reference
2525
+ externalId: data.id,
2526
+ // Update attributes if provided
2527
+ ...data.attributes && { attributes: data.attributes }
2528
+ }
2529
+ });
2530
+ console.log("[Subscriber Handler] Reactivated subscriber:", data.email);
2531
+ } else {
2532
+ await payload.create({
2533
+ collection: subscribersSlug,
2534
+ data: {
2535
+ email: data.email,
2536
+ name: data.name,
2537
+ subscriptionStatus: "active",
2538
+ subscribedAt: data.subscribed_at,
2539
+ externalId: data.id,
2540
+ attributes: data.attributes || {}
2541
+ }
2542
+ });
2543
+ console.log("[Subscriber Handler] Created new subscriber:", data.email);
2544
+ }
2545
+ } catch (error) {
2546
+ console.error("[Subscriber Handler] Error handling subscribed event:", error);
2547
+ throw error;
2548
+ }
2549
+ }
2550
+ async function handleSubscriberUnsubscribed(event, payload, subscribersSlug) {
2551
+ const { data } = event;
2552
+ try {
2553
+ const existing = await payload.find({
2554
+ collection: subscribersSlug,
2555
+ where: {
2556
+ email: {
2557
+ equals: data.email
2558
+ }
2559
+ },
2560
+ limit: 1
2561
+ });
2562
+ if (existing.docs.length > 0) {
2563
+ const subscriber = existing.docs[0];
2564
+ await payload.update({
2565
+ collection: subscribersSlug,
2566
+ id: subscriber.id,
2567
+ data: {
2568
+ subscriptionStatus: "unsubscribed",
2569
+ unsubscribedAt: data.unsubscribed_at,
2570
+ unsubscribeReason: data.reason
2571
+ }
2572
+ });
2573
+ console.log("[Subscriber Handler] Unsubscribed:", data.email);
2574
+ } else {
2575
+ console.warn("[Subscriber Handler] Subscriber not found for unsubscribe:", data.email);
2576
+ }
2577
+ } catch (error) {
2578
+ console.error("[Subscriber Handler] Error handling unsubscribed event:", error);
2579
+ throw error;
2580
+ }
2581
+ }
2582
+
2583
+ // src/webhooks/handlers/broadcast.ts
2584
+ var STATUS_MAP = {
2585
+ "broadcast.scheduled": "scheduled",
2586
+ "broadcast.queueing": "sending",
2587
+ "broadcast.sending": "sending",
2588
+ "broadcast.sent": "sent",
2589
+ "broadcast.failed": "failed",
2590
+ "broadcast.partial_failure": "sent",
2591
+ // With warning flag
2592
+ "broadcast.aborted": "canceled",
2593
+ "broadcast.paused": "paused"
2594
+ };
2595
+ async function handleBroadcastEvent(event, req, _config) {
2596
+ const { payload } = req;
2597
+ const broadcastsSlug = "broadcasts";
2598
+ const status = STATUS_MAP[event.type];
2599
+ if (!status) {
2600
+ console.warn("[Broadcast Handler] Unknown event type:", event.type);
2601
+ return;
2602
+ }
2603
+ try {
2604
+ await updateBroadcastStatus(event, status, payload, broadcastsSlug);
2605
+ } catch (error) {
2606
+ console.error("[Broadcast Handler] Error handling event:", {
2607
+ type: event.type,
2608
+ error: error instanceof Error ? error.message : "Unknown error"
2609
+ });
2610
+ throw error;
2611
+ }
2612
+ }
2613
+ async function updateBroadcastStatus(event, status, payload, broadcastsSlug) {
2614
+ const { data } = event;
2615
+ const broadcastId = data.broadcast_id;
2616
+ if (!broadcastId) {
2617
+ console.error("[Broadcast Handler] No broadcast_id in event data");
2618
+ return;
2619
+ }
2620
+ try {
2621
+ const existing = await payload.find({
2622
+ collection: broadcastsSlug,
2623
+ where: {
2624
+ externalId: {
2625
+ equals: broadcastId
2626
+ }
2627
+ },
2628
+ limit: 1
2629
+ });
2630
+ if (existing.docs.length === 0) {
2631
+ console.warn("[Broadcast Handler] Broadcast not found:", broadcastId);
2632
+ return;
2633
+ }
2634
+ const broadcast = existing.docs[0];
2635
+ const updateData = {
2636
+ status,
2637
+ lastWebhookEvent: event.type,
2638
+ lastWebhookEventAt: event.occurred_at
2639
+ };
2640
+ switch (event.type) {
2641
+ case WEBHOOK_EVENT_TYPES.BROADCAST_SCHEDULED:
2642
+ updateData.scheduledAt = event.data.scheduled_for;
2643
+ break;
2644
+ case WEBHOOK_EVENT_TYPES.BROADCAST_SENDING: {
2645
+ const sendingEvent = event;
2646
+ updateData.sentCount = sendingEvent.data.sent_count;
2647
+ updateData.totalCount = sendingEvent.data.total_count;
2648
+ break;
2649
+ }
2650
+ case WEBHOOK_EVENT_TYPES.BROADCAST_SENT: {
2651
+ const sentEvent = event;
2652
+ updateData.sentCount = sentEvent.data.sent_count;
2653
+ updateData.sentAt = sentEvent.data.completed_at;
2654
+ updateData.sendingStartedAt = sentEvent.data.started_at;
2655
+ break;
2656
+ }
2657
+ case WEBHOOK_EVENT_TYPES.BROADCAST_FAILED: {
2658
+ const failedEvent = event;
2659
+ updateData.failureReason = failedEvent.data.error;
2660
+ updateData.failedAt = failedEvent.data.failed_at;
2661
+ break;
2662
+ }
2663
+ case WEBHOOK_EVENT_TYPES.BROADCAST_PARTIAL_FAILURE: {
2664
+ const partialFailureEvent = event;
2665
+ updateData.sentCount = partialFailureEvent.data.sent_count;
2666
+ updateData.failedCount = partialFailureEvent.data.failed_count;
2667
+ updateData.totalCount = partialFailureEvent.data.total_count;
2668
+ updateData.hasWarnings = true;
2669
+ break;
2670
+ }
2671
+ case WEBHOOK_EVENT_TYPES.BROADCAST_ABORTED: {
2672
+ const abortedEvent = event;
2673
+ updateData.abortedAt = abortedEvent.data.aborted_at;
2674
+ updateData.abortReason = abortedEvent.data.reason;
2675
+ break;
2676
+ }
2677
+ case WEBHOOK_EVENT_TYPES.BROADCAST_PAUSED: {
2678
+ const pausedEvent = event;
2679
+ updateData.pausedAt = pausedEvent.data.paused_at;
2680
+ updateData.sentCount = pausedEvent.data.sent_count;
2681
+ updateData.remainingCount = pausedEvent.data.remaining_count;
2682
+ break;
2683
+ }
2684
+ }
2685
+ await payload.update({
2686
+ collection: broadcastsSlug,
2687
+ id: broadcast.id,
2688
+ data: updateData
2689
+ });
2690
+ console.log("[Broadcast Handler] Updated broadcast status:", {
2691
+ broadcastId,
2692
+ status,
2693
+ event: event.type
2694
+ });
2695
+ } catch (error) {
2696
+ console.error("[Broadcast Handler] Error updating broadcast:", error);
2697
+ throw error;
2698
+ }
2699
+ }
2700
+
2701
+ // src/webhooks/router.ts
2702
+ async function routeWebhookEvent(event, req, config) {
2703
+ try {
2704
+ if (!isHandledEvent(event)) {
2705
+ console.log("[Webhook Router] Ignoring unhandled event type:", event.type);
2706
+ return;
2707
+ }
2708
+ const handledEvent = event;
2709
+ if (isSubscriberEvent(handledEvent)) {
2710
+ await handleSubscriberEvent(handledEvent, req, config);
2711
+ } else if (isBroadcastEvent(handledEvent)) {
2712
+ await handleBroadcastEvent(handledEvent, req, config);
2713
+ } else {
2714
+ console.warn("[Webhook Router] Event type not routed:", event.type);
2715
+ }
2716
+ } catch (error) {
2717
+ console.error("[Webhook Router] Error routing event:", {
2718
+ type: event.type,
2719
+ error: error instanceof Error ? error.message : "Unknown error"
2720
+ });
2721
+ throw error;
2722
+ }
2723
+ }
2724
+
2725
+ // src/endpoints/webhooks/broadcast.ts
2726
+ var createBroadcastWebhookEndpoint = (config) => {
2727
+ return {
2728
+ path: "/webhooks/broadcast",
2729
+ method: "post",
2730
+ handler: async (req) => {
2731
+ try {
2732
+ const settings = await req.payload.findGlobal({
2733
+ slug: config.settingsSlug || "newsletter-settings"
2734
+ });
2735
+ const webhookSecret = settings?.broadcastSettings?.webhookSecret;
2736
+ if (!webhookSecret) {
2737
+ console.error("[Broadcast Webhook] No webhook secret configured");
2738
+ return Response.json({ error: "Webhook not configured" }, { status: 401 });
2739
+ }
2740
+ const headers = req.headers;
2741
+ const signature = headers.get("x-broadcast-signature");
2742
+ const timestamp = headers.get("x-broadcast-timestamp");
2743
+ if (!signature || !timestamp) {
2744
+ console.error("[Broadcast Webhook] Missing signature or timestamp");
2745
+ return Response.json({ error: "Invalid request" }, { status: 401 });
2746
+ }
2747
+ let rawBodyString;
2748
+ let rawBody;
2749
+ if (typeof req.json === "function") {
2750
+ rawBody = await req.json();
2751
+ rawBodyString = JSON.stringify(rawBody);
2752
+ } else {
2753
+ console.error("[Broadcast Webhook] Request does not support json() method");
2754
+ return Response.json({ error: "Invalid request" }, { status: 400 });
2755
+ }
2756
+ const isValid = verifyBroadcastWebhookSignature(
2757
+ rawBodyString,
2758
+ signature,
2759
+ timestamp,
2760
+ webhookSecret
2761
+ );
2762
+ if (!isValid) {
2763
+ console.error("[Broadcast Webhook] Invalid signature");
2764
+ return Response.json({ error: "Invalid signature" }, { status: 401 });
2765
+ }
2766
+ const data = rawBody;
2767
+ console.log("[Broadcast Webhook] Verified event:", {
2768
+ type: data.type,
2769
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2770
+ });
2771
+ await routeWebhookEvent(data, req, config);
2772
+ await req.payload.updateGlobal({
2773
+ slug: config.settingsSlug || "newsletter-settings",
2774
+ data: {
2775
+ broadcastSettings: {
2776
+ ...settings?.broadcastSettings || {},
2777
+ lastWebhookReceived: (/* @__PURE__ */ new Date()).toISOString(),
2778
+ webhookStatus: "verified"
2779
+ }
2780
+ }
2781
+ });
2782
+ return Response.json({ success: true }, { status: 200 });
2783
+ } catch (error) {
2784
+ console.error("[Broadcast Webhook] Error processing webhook:", error);
2785
+ return Response.json({ success: false }, { status: 200 });
2786
+ }
2787
+ }
2788
+ };
2789
+ };
2790
+
2791
+ // src/endpoints/webhooks/verify.ts
2792
+ var createWebhookVerifyEndpoint = (config) => {
2793
+ return {
2794
+ path: "/webhooks/verify",
2795
+ method: "post",
2796
+ handler: async (req) => {
2797
+ try {
2798
+ const settings = await req.payload.findGlobal({
2799
+ slug: config.settingsSlug || "newsletter-settings"
2800
+ });
2801
+ if (!settings?.broadcastSettings?.webhookSecret) {
2802
+ return Response.json({
2803
+ success: false,
2804
+ error: "Webhook secret not configured"
2805
+ }, { status: 400 });
2806
+ }
2807
+ const apiUrl = settings.broadcastSettings.apiUrl;
2808
+ const token = settings.broadcastSettings.token;
2809
+ if (!apiUrl || !token) {
2810
+ return Response.json({
2811
+ success: false,
2812
+ error: "Broadcast API not configured"
2813
+ }, { status: 400 });
2814
+ }
2815
+ await req.payload.updateGlobal({
2816
+ slug: config.settingsSlug || "newsletter-settings",
2817
+ data: {
2818
+ broadcastSettings: {
2819
+ ...settings.broadcastSettings,
2820
+ webhookStatus: "configured"
2821
+ }
2822
+ }
2823
+ });
2824
+ return Response.json({
2825
+ success: true,
2826
+ message: "Webhook configuration verified. Please configure the webhook in Broadcast."
2827
+ });
2828
+ } catch (error) {
2829
+ console.error("[Webhook Verify] Error:", error);
2830
+ return Response.json({
2831
+ success: false,
2832
+ error: "Failed to verify webhook configuration"
2833
+ }, { status: 500 });
2834
+ }
2835
+ }
2836
+ };
2837
+ };
2838
+
2335
2839
  // src/endpoints/index.ts
2336
2840
  function createNewsletterEndpoints(config) {
2337
2841
  const endpoints = [
@@ -2349,6 +2853,12 @@ function createNewsletterEndpoints(config) {
2349
2853
  );
2350
2854
  }
2351
2855
  endpoints.push(...createBroadcastManagementEndpoints(config));
2856
+ if (config.providers?.broadcast) {
2857
+ endpoints.push(
2858
+ createBroadcastWebhookEndpoint(config),
2859
+ createWebhookVerifyEndpoint(config)
2860
+ );
2861
+ }
2352
2862
  return endpoints;
2353
2863
  }
2354
2864
 
@@ -2521,111 +3031,6 @@ function createMarkdownFieldInternal(config) {
2521
3031
  };
2522
3032
  }
2523
3033
 
2524
- // src/jobs/sync-unsubscribes.ts
2525
- var createUnsubscribeSyncJob = (pluginConfig) => {
2526
- return {
2527
- slug: "sync-unsubscribes",
2528
- label: "Sync Unsubscribes from Email Service",
2529
- handler: async ({ req }) => {
2530
- const subscribersSlug = pluginConfig.subscribersSlug || "subscribers";
2531
- const emailService = req.payload.newsletterEmailService;
2532
- if (!emailService) {
2533
- console.error("Email service not configured");
2534
- return {
2535
- output: {
2536
- syncedCount: 0
2537
- }
2538
- };
2539
- }
2540
- let syncedCount = 0;
2541
- try {
2542
- if (emailService.getProvider() === "broadcast") {
2543
- console.warn("Starting Broadcast unsubscribe sync...");
2544
- const broadcastConfig = pluginConfig.providers?.broadcast;
2545
- if (!broadcastConfig) {
2546
- throw new Error("Broadcast configuration not found");
2547
- }
2548
- const apiUrl = broadcastConfig.apiUrl.replace(/\/$/, "");
2549
- const token = broadcastConfig.token;
2550
- let page = 1;
2551
- let hasMore = true;
2552
- while (hasMore) {
2553
- const response = await fetch(
2554
- `${apiUrl}/api/v1/subscribers.json?page=${page}`,
2555
- {
2556
- headers: {
2557
- "Authorization": `Bearer ${token}`
2558
- }
2559
- }
2560
- );
2561
- if (!response.ok) {
2562
- throw new Error(`Broadcast API error: ${response.status}`);
2563
- }
2564
- const data = await response.json();
2565
- const broadcastSubscribers = data.subscribers || [];
2566
- for (const broadcastSub of broadcastSubscribers) {
2567
- const payloadSubscribers = await req.payload.find({
2568
- collection: subscribersSlug,
2569
- where: {
2570
- email: {
2571
- equals: broadcastSub.email
2572
- }
2573
- },
2574
- limit: 1
2575
- });
2576
- if (payloadSubscribers.docs.length > 0) {
2577
- const payloadSub = payloadSubscribers.docs[0];
2578
- const broadcastUnsubscribed = !broadcastSub.is_active || broadcastSub.unsubscribed_at;
2579
- const payloadUnsubscribed = payloadSub.subscriptionStatus === "unsubscribed";
2580
- if (broadcastUnsubscribed && !payloadUnsubscribed) {
2581
- await req.payload.update({
2582
- collection: subscribersSlug,
2583
- id: payloadSub.id,
2584
- data: {
2585
- subscriptionStatus: "unsubscribed",
2586
- unsubscribedAt: broadcastSub.unsubscribed_at || (/* @__PURE__ */ new Date()).toISOString()
2587
- }
2588
- });
2589
- syncedCount++;
2590
- console.warn(`Unsubscribed: ${broadcastSub.email}`);
2591
- }
2592
- }
2593
- }
2594
- if (data.pagination && data.pagination.current < data.pagination.total_pages) {
2595
- page++;
2596
- } else {
2597
- hasMore = false;
2598
- }
2599
- }
2600
- console.warn(`Broadcast sync complete. Unsubscribed ${syncedCount} contacts.`);
2601
- }
2602
- if (emailService.getProvider() === "resend") {
2603
- console.warn("Starting Resend unsubscribe sync...");
2604
- const resendConfig = pluginConfig.providers?.resend;
2605
- if (!resendConfig) {
2606
- throw new Error("Resend configuration not found");
2607
- }
2608
- console.warn("Resend polling implementation needed - webhooks recommended");
2609
- }
2610
- if (pluginConfig.hooks?.afterUnsubscribeSync) {
2611
- await pluginConfig.hooks.afterUnsubscribeSync({
2612
- req,
2613
- syncedCount
2614
- });
2615
- }
2616
- } catch (error) {
2617
- console.error("Unsubscribe sync error:", error);
2618
- throw error;
2619
- }
2620
- return {
2621
- output: {
2622
- syncedCount
2623
- }
2624
- };
2625
- }
2626
- };
2627
- };
2628
-
2629
3034
  // src/fields/emailContent.ts
2630
3035
  import {
2631
3036
  BoldFeature,
@@ -4165,6 +4570,14 @@ var createBroadcastsCollection = (pluginConfig) => {
4165
4570
  condition: (data) => hasProviders && data?.providerId
4166
4571
  }
4167
4572
  },
4573
+ {
4574
+ name: "externalId",
4575
+ type: "text",
4576
+ admin: {
4577
+ readOnly: true,
4578
+ description: "External ID for webhook integration"
4579
+ }
4580
+ },
4168
4581
  {
4169
4582
  name: "providerData",
4170
4583
  type: "json",
@@ -4173,6 +4586,77 @@ var createBroadcastsCollection = (pluginConfig) => {
4173
4586
  condition: () => false
4174
4587
  // Hidden by default
4175
4588
  }
4589
+ },
4590
+ // Webhook tracking fields
4591
+ {
4592
+ name: "webhookData",
4593
+ type: "group",
4594
+ label: "Webhook Data",
4595
+ admin: {
4596
+ condition: () => false
4597
+ // Hidden by default, used for webhook tracking
4598
+ },
4599
+ fields: [
4600
+ {
4601
+ name: "lastWebhookEvent",
4602
+ type: "text",
4603
+ admin: {
4604
+ readOnly: true
4605
+ }
4606
+ },
4607
+ {
4608
+ name: "lastWebhookEventAt",
4609
+ type: "date",
4610
+ admin: {
4611
+ readOnly: true
4612
+ }
4613
+ },
4614
+ {
4615
+ name: "hasWarnings",
4616
+ type: "checkbox",
4617
+ defaultValue: false
4618
+ },
4619
+ {
4620
+ name: "failureReason",
4621
+ type: "text"
4622
+ },
4623
+ {
4624
+ name: "sentCount",
4625
+ type: "number"
4626
+ },
4627
+ {
4628
+ name: "totalCount",
4629
+ type: "number"
4630
+ },
4631
+ {
4632
+ name: "failedCount",
4633
+ type: "number"
4634
+ },
4635
+ {
4636
+ name: "remainingCount",
4637
+ type: "number"
4638
+ },
4639
+ {
4640
+ name: "sendingStartedAt",
4641
+ type: "date"
4642
+ },
4643
+ {
4644
+ name: "failedAt",
4645
+ type: "date"
4646
+ },
4647
+ {
4648
+ name: "abortedAt",
4649
+ type: "date"
4650
+ },
4651
+ {
4652
+ name: "abortReason",
4653
+ type: "text"
4654
+ },
4655
+ {
4656
+ name: "pausedAt",
4657
+ type: "date"
4658
+ }
4659
+ ]
4176
4660
  }
4177
4661
  ],
4178
4662
  hooks: {
@@ -4796,7 +5280,6 @@ var newsletterPlugin = (pluginConfig) => (incomingConfig) => {
4796
5280
  });
4797
5281
  }
4798
5282
  const endpoints = createNewsletterEndpoints(config);
4799
- const syncJob = config.features?.unsubscribeSync?.enabled ? createUnsubscribeSyncJob(config) : null;
4800
5283
  const modifiedConfig = {
4801
5284
  ...incomingConfig,
4802
5285
  collections,
@@ -4808,31 +5291,7 @@ var newsletterPlugin = (pluginConfig) => (incomingConfig) => {
4808
5291
  ...incomingConfig.endpoints || [],
4809
5292
  ...endpoints
4810
5293
  ],
4811
- jobs: syncJob ? {
4812
- ...incomingConfig.jobs || {},
4813
- tasks: [
4814
- ...incomingConfig.jobs?.tasks || [],
4815
- syncJob
4816
- ],
4817
- // Add cron schedule if specified
4818
- autoRun: config.features?.unsubscribeSync?.schedule ? Array.isArray(incomingConfig.jobs?.autoRun) ? [...incomingConfig.jobs.autoRun, {
4819
- cron: config.features.unsubscribeSync.schedule,
4820
- queue: "newsletter-sync",
4821
- limit: 100
4822
- }] : typeof incomingConfig.jobs?.autoRun === "function" ? async (payload) => {
4823
- const autoRunFn = incomingConfig.jobs.autoRun;
4824
- const existingConfigs = await autoRunFn(payload);
4825
- return [...existingConfigs, {
4826
- cron: config.features.unsubscribeSync.schedule,
4827
- queue: "newsletter-sync",
4828
- limit: 100
4829
- }];
4830
- } : [{
4831
- cron: config.features.unsubscribeSync.schedule,
4832
- queue: "newsletter-sync",
4833
- limit: 100
4834
- }] : incomingConfig.jobs?.autoRun
4835
- } : incomingConfig.jobs,
5294
+ jobs: incomingConfig.jobs,
4836
5295
  onInit: async (payload) => {
4837
5296
  try {
4838
5297
  const settings = await payload.findGlobal({