payload-plugin-newsletter 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/CLAUDE.md +110 -0
  3. package/README.md +25 -3
  4. package/TEST_SUMMARY.md +152 -0
  5. package/dist/.tsbuildinfo +1 -1
  6. package/dist/collections/NewsletterSettings.d.ts.map +1 -1
  7. package/dist/collections/Subscribers.d.ts.map +1 -1
  8. package/dist/src/__tests__/fixtures/newsletter-settings.js +41 -0
  9. package/dist/src/__tests__/fixtures/newsletter-settings.js.map +1 -0
  10. package/dist/src/__tests__/fixtures/subscribers.js +70 -0
  11. package/dist/src/__tests__/fixtures/subscribers.js.map +1 -0
  12. package/dist/src/__tests__/integration/collections/subscriber-hooks.test.js +356 -0
  13. package/dist/src/__tests__/integration/collections/subscriber-hooks.test.js.map +1 -0
  14. package/dist/src/__tests__/integration/endpoints/preferences.test.js +266 -0
  15. package/dist/src/__tests__/integration/endpoints/preferences.test.js.map +1 -0
  16. package/dist/src/__tests__/integration/endpoints/subscribe.test.js +280 -0
  17. package/dist/src/__tests__/integration/endpoints/subscribe.test.js.map +1 -0
  18. package/dist/src/__tests__/integration/endpoints/unsubscribe.test.js +187 -0
  19. package/dist/src/__tests__/integration/endpoints/unsubscribe.test.js.map +1 -0
  20. package/dist/src/__tests__/integration/endpoints/verify-magic-link.test.js +188 -0
  21. package/dist/src/__tests__/integration/endpoints/verify-magic-link.test.js.map +1 -0
  22. package/dist/src/__tests__/mocks/email-providers.js +153 -0
  23. package/dist/src/__tests__/mocks/email-providers.js.map +1 -0
  24. package/dist/src/__tests__/mocks/payload.js +244 -0
  25. package/dist/src/__tests__/mocks/payload.js.map +1 -0
  26. package/dist/src/__tests__/security/csrf-protection.test.js +309 -0
  27. package/dist/src/__tests__/security/csrf-protection.test.js.map +1 -0
  28. package/dist/src/__tests__/security/settings-access.test.js +204 -0
  29. package/dist/src/__tests__/security/settings-access.test.js.map +1 -0
  30. package/dist/src/__tests__/security/subscriber-access.test.js +210 -0
  31. package/dist/src/__tests__/security/subscriber-access.test.js.map +1 -0
  32. package/dist/src/__tests__/security/xss-prevention.test.js +305 -0
  33. package/dist/src/__tests__/security/xss-prevention.test.js.map +1 -0
  34. package/dist/src/__tests__/setup/integration.setup.js +38 -0
  35. package/dist/src/__tests__/setup/integration.setup.js.map +1 -0
  36. package/dist/src/__tests__/setup/unit.setup.js +41 -0
  37. package/dist/src/__tests__/setup/unit.setup.js.map +1 -0
  38. package/dist/src/__tests__/unit/utils/access.test.js +116 -0
  39. package/dist/src/__tests__/unit/utils/access.test.js.map +1 -0
  40. package/dist/src/__tests__/unit/utils/jwt.test.js +238 -0
  41. package/dist/src/__tests__/unit/utils/jwt.test.js.map +1 -0
  42. package/dist/src/collections/NewsletterSettings.js +4 -3
  43. package/dist/src/collections/NewsletterSettings.js.map +1 -1
  44. package/dist/src/collections/Subscribers.js +4 -39
  45. package/dist/src/collections/Subscribers.js.map +1 -1
  46. package/dist/src/types/index.js.map +1 -1
  47. package/dist/src/utils/access.js +80 -0
  48. package/dist/src/utils/access.js.map +1 -0
  49. package/dist/src/utils/validation.js +9 -1
  50. package/dist/src/utils/validation.js.map +1 -1
  51. package/dist/types/index.d.ts +11 -0
  52. package/dist/types/index.d.ts.map +1 -1
  53. package/dist/utils/access.d.ts +15 -0
  54. package/dist/utils/access.d.ts.map +1 -0
  55. package/dist/utils/validation.d.ts.map +1 -1
  56. package/package.json +22 -4
@@ -1 +1 @@
1
- {"version":3,"file":"NewsletterSettings.d.ts","sourceRoot":"","sources":["../../src/collections/NewsletterSettings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAEtD,eAAO,MAAM,kCAAkC,GAC7C,cAAc,sBAAsB,KACnC,gBAiXF,CAAA"}
1
+ {"version":3,"file":"NewsletterSettings.d.ts","sourceRoot":"","sources":["../../src/collections/NewsletterSettings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAGtD,eAAO,MAAM,kCAAkC,GAC7C,cAAc,sBAAsB,KACnC,gBAiXF,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"Subscribers.d.ts","sourceRoot":"","sources":["../../src/collections/Subscribers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAoG,MAAM,SAAS,CAAA;AACjJ,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAEtD,eAAO,MAAM,2BAA2B,GACtC,cAAc,sBAAsB,KACnC,gBAgVF,CAAA"}
1
+ {"version":3,"file":"Subscribers.d.ts","sourceRoot":"","sources":["../../src/collections/Subscribers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAgE,MAAM,SAAS,CAAA;AAC7G,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAGtD,eAAO,MAAM,2BAA2B,GACtC,cAAc,sBAAsB,KACnC,gBA4SF,CAAA"}
@@ -0,0 +1,41 @@
1
+ export const mockNewsletterSettings = {
2
+ id: 'settings-1',
3
+ name: 'Test Configuration',
4
+ active: true,
5
+ provider: 'resend',
6
+ resendSettings: {
7
+ apiKey: 'test-api-key',
8
+ audienceIds: []
9
+ },
10
+ from: {
11
+ email: 'newsletter@example.com',
12
+ name: 'Test Newsletter'
13
+ },
14
+ subscriptionSettings: {
15
+ requireDoubleOptIn: true,
16
+ allowedDomains: [],
17
+ maxSubscribersPerIP: 10
18
+ },
19
+ emailTemplates: {
20
+ welcome: {
21
+ enabled: true,
22
+ subject: 'Welcome to Our Newsletter',
23
+ preheader: 'Thank you for subscribing!'
24
+ },
25
+ magicLink: {
26
+ subject: 'Sign in to {{fromName}}',
27
+ preheader: 'Click the link to access your preferences',
28
+ expirationTime: '7d'
29
+ }
30
+ },
31
+ createdAt: new Date('2024-01-01').toISOString(),
32
+ updatedAt: new Date('2024-01-01').toISOString()
33
+ };
34
+ export const createMockSettings = (overrides = {})=>{
35
+ return {
36
+ ...mockNewsletterSettings,
37
+ ...overrides
38
+ };
39
+ };
40
+
41
+ //# sourceMappingURL=newsletter-settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/__tests__/fixtures/newsletter-settings.ts"],"sourcesContent":["export const mockNewsletterSettings = {\n id: 'settings-1',\n name: 'Test Configuration',\n active: true,\n provider: 'resend',\n resendSettings: {\n apiKey: 'test-api-key',\n audienceIds: [],\n },\n from: {\n email: 'newsletter@example.com',\n name: 'Test Newsletter',\n },\n subscriptionSettings: {\n requireDoubleOptIn: true,\n allowedDomains: [],\n maxSubscribersPerIP: 10,\n },\n emailTemplates: {\n welcome: {\n enabled: true,\n subject: 'Welcome to Our Newsletter',\n preheader: 'Thank you for subscribing!',\n },\n magicLink: {\n subject: 'Sign in to {{fromName}}',\n preheader: 'Click the link to access your preferences',\n expirationTime: '7d',\n },\n },\n createdAt: new Date('2024-01-01').toISOString(),\n updatedAt: new Date('2024-01-01').toISOString(),\n}\n\nexport const createMockSettings = (overrides: any = {}) => {\n return {\n ...mockNewsletterSettings,\n ...overrides,\n }\n}"],"names":["mockNewsletterSettings","id","name","active","provider","resendSettings","apiKey","audienceIds","from","email","subscriptionSettings","requireDoubleOptIn","allowedDomains","maxSubscribersPerIP","emailTemplates","welcome","enabled","subject","preheader","magicLink","expirationTime","createdAt","Date","toISOString","updatedAt","createMockSettings","overrides"],"mappings":"AAAA,OAAO,MAAMA,yBAAyB;IACpCC,IAAI;IACJC,MAAM;IACNC,QAAQ;IACRC,UAAU;IACVC,gBAAgB;QACdC,QAAQ;QACRC,aAAa,EAAE;IACjB;IACAC,MAAM;QACJC,OAAO;QACPP,MAAM;IACR;IACAQ,sBAAsB;QACpBC,oBAAoB;QACpBC,gBAAgB,EAAE;QAClBC,qBAAqB;IACvB;IACAC,gBAAgB;QACdC,SAAS;YACPC,SAAS;YACTC,SAAS;YACTC,WAAW;QACb;QACAC,WAAW;YACTF,SAAS;YACTC,WAAW;YACXE,gBAAgB;QAClB;IACF;IACAC,WAAW,IAAIC,KAAK,cAAcC,WAAW;IAC7CC,WAAW,IAAIF,KAAK,cAAcC,WAAW;AAC/C,EAAC;AAED,OAAO,MAAME,qBAAqB,CAACC,YAAiB,CAAC,CAAC;IACpD,OAAO;QACL,GAAG1B,sBAAsB;QACzB,GAAG0B,SAAS;IACd;AACF,EAAC"}
@@ -0,0 +1,70 @@
1
+ export const mockSubscribers = [
2
+ {
3
+ id: 'sub-1',
4
+ email: 'active@example.com',
5
+ name: 'Active Subscriber',
6
+ locale: 'en',
7
+ subscriptionStatus: 'active',
8
+ emailPreferences: {
9
+ newsletter: true,
10
+ announcements: true
11
+ },
12
+ createdAt: new Date('2024-01-01').toISOString(),
13
+ updatedAt: new Date('2024-01-01').toISOString()
14
+ },
15
+ {
16
+ id: 'sub-2',
17
+ email: 'pending@example.com',
18
+ name: 'Pending Subscriber',
19
+ locale: 'en',
20
+ subscriptionStatus: 'pending',
21
+ emailPreferences: {
22
+ newsletter: true,
23
+ announcements: true
24
+ },
25
+ createdAt: new Date('2024-01-02').toISOString(),
26
+ updatedAt: new Date('2024-01-02').toISOString()
27
+ },
28
+ {
29
+ id: 'sub-3',
30
+ email: 'unsubscribed@example.com',
31
+ name: 'Former Subscriber',
32
+ locale: 'en',
33
+ subscriptionStatus: 'unsubscribed',
34
+ unsubscribedAt: new Date('2024-01-10').toISOString(),
35
+ emailPreferences: {
36
+ newsletter: false,
37
+ announcements: false
38
+ },
39
+ createdAt: new Date('2024-01-03').toISOString(),
40
+ updatedAt: new Date('2024-01-10').toISOString()
41
+ }
42
+ ];
43
+ export const createMockSubscriber = (overrides = {})=>{
44
+ return {
45
+ id: `sub-${Date.now()}`,
46
+ email: 'test@example.com',
47
+ name: 'Test Subscriber',
48
+ locale: 'en',
49
+ subscriptionStatus: 'pending',
50
+ emailPreferences: {
51
+ newsletter: true,
52
+ announcements: true
53
+ },
54
+ createdAt: new Date().toISOString(),
55
+ updatedAt: new Date().toISOString(),
56
+ ...overrides
57
+ };
58
+ };
59
+ export const mockMagicLinkTokenData = {
60
+ subscriberId: 'sub-1',
61
+ email: 'active@example.com',
62
+ action: 'verify'
63
+ };
64
+ export const mockPreferencesTokenData = {
65
+ subscriberId: 'sub-1',
66
+ email: 'active@example.com',
67
+ action: 'preferences'
68
+ };
69
+
70
+ //# sourceMappingURL=subscribers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/__tests__/fixtures/subscribers.ts"],"sourcesContent":["export const mockSubscribers = [\n {\n id: 'sub-1',\n email: 'active@example.com',\n name: 'Active Subscriber',\n locale: 'en',\n subscriptionStatus: 'active',\n emailPreferences: {\n newsletter: true,\n announcements: true,\n },\n createdAt: new Date('2024-01-01').toISOString(),\n updatedAt: new Date('2024-01-01').toISOString(),\n },\n {\n id: 'sub-2',\n email: 'pending@example.com',\n name: 'Pending Subscriber',\n locale: 'en',\n subscriptionStatus: 'pending',\n emailPreferences: {\n newsletter: true,\n announcements: true,\n },\n createdAt: new Date('2024-01-02').toISOString(),\n updatedAt: new Date('2024-01-02').toISOString(),\n },\n {\n id: 'sub-3',\n email: 'unsubscribed@example.com',\n name: 'Former Subscriber',\n locale: 'en',\n subscriptionStatus: 'unsubscribed',\n unsubscribedAt: new Date('2024-01-10').toISOString(),\n emailPreferences: {\n newsletter: false,\n announcements: false,\n },\n createdAt: new Date('2024-01-03').toISOString(),\n updatedAt: new Date('2024-01-10').toISOString(),\n },\n]\n\nexport const createMockSubscriber = (overrides: any = {}) => {\n return {\n id: `sub-${Date.now()}`,\n email: 'test@example.com',\n name: 'Test Subscriber',\n locale: 'en',\n subscriptionStatus: 'pending',\n emailPreferences: {\n newsletter: true,\n announcements: true,\n },\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n ...overrides,\n }\n}\n\nexport const mockMagicLinkTokenData = {\n subscriberId: 'sub-1',\n email: 'active@example.com',\n action: 'verify' as const,\n}\n\nexport const mockPreferencesTokenData = {\n subscriberId: 'sub-1',\n email: 'active@example.com',\n action: 'preferences' as const,\n}"],"names":["mockSubscribers","id","email","name","locale","subscriptionStatus","emailPreferences","newsletter","announcements","createdAt","Date","toISOString","updatedAt","unsubscribedAt","createMockSubscriber","overrides","now","mockMagicLinkTokenData","subscriberId","action","mockPreferencesTokenData"],"mappings":"AAAA,OAAO,MAAMA,kBAAkB;IAC7B;QACEC,IAAI;QACJC,OAAO;QACPC,MAAM;QACNC,QAAQ;QACRC,oBAAoB;QACpBC,kBAAkB;YAChBC,YAAY;YACZC,eAAe;QACjB;QACAC,WAAW,IAAIC,KAAK,cAAcC,WAAW;QAC7CC,WAAW,IAAIF,KAAK,cAAcC,WAAW;IAC/C;IACA;QACEV,IAAI;QACJC,OAAO;QACPC,MAAM;QACNC,QAAQ;QACRC,oBAAoB;QACpBC,kBAAkB;YAChBC,YAAY;YACZC,eAAe;QACjB;QACAC,WAAW,IAAIC,KAAK,cAAcC,WAAW;QAC7CC,WAAW,IAAIF,KAAK,cAAcC,WAAW;IAC/C;IACA;QACEV,IAAI;QACJC,OAAO;QACPC,MAAM;QACNC,QAAQ;QACRC,oBAAoB;QACpBQ,gBAAgB,IAAIH,KAAK,cAAcC,WAAW;QAClDL,kBAAkB;YAChBC,YAAY;YACZC,eAAe;QACjB;QACAC,WAAW,IAAIC,KAAK,cAAcC,WAAW;QAC7CC,WAAW,IAAIF,KAAK,cAAcC,WAAW;IAC/C;CACD,CAAA;AAED,OAAO,MAAMG,uBAAuB,CAACC,YAAiB,CAAC,CAAC;IACtD,OAAO;QACLd,IAAI,CAAC,IAAI,EAAES,KAAKM,GAAG,IAAI;QACvBd,OAAO;QACPC,MAAM;QACNC,QAAQ;QACRC,oBAAoB;QACpBC,kBAAkB;YAChBC,YAAY;YACZC,eAAe;QACjB;QACAC,WAAW,IAAIC,OAAOC,WAAW;QACjCC,WAAW,IAAIF,OAAOC,WAAW;QACjC,GAAGI,SAAS;IACd;AACF,EAAC;AAED,OAAO,MAAME,yBAAyB;IACpCC,cAAc;IACdhB,OAAO;IACPiB,QAAQ;AACV,EAAC;AAED,OAAO,MAAMC,2BAA2B;IACtCF,cAAc;IACdhB,OAAO;IACPiB,QAAQ;AACV,EAAC"}
@@ -0,0 +1,356 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { createPayloadRequestMock, seedCollection, clearCollections, createMockAdminUser } from '../../mocks/payload';
3
+ import { mockSubscribers } from '../../fixtures/subscribers';
4
+ import { createResendMock } from '../../mocks/email-providers';
5
+ // Mock email service
6
+ vi.mock('../../../services/email', ()=>({
7
+ getEmailService: vi.fn()
8
+ }));
9
+ import { getEmailService } from '../../../services/email';
10
+ describe('Subscriber Collection Hooks Security', ()=>{
11
+ let mockReq;
12
+ let mockEmailService;
13
+ const config = {
14
+ subscribersSlug: 'subscribers'
15
+ };
16
+ beforeEach(()=>{
17
+ clearCollections();
18
+ seedCollection('subscribers', mockSubscribers);
19
+ const payloadMock = createPayloadRequestMock();
20
+ mockReq = {
21
+ payload: payloadMock.payload,
22
+ user: null
23
+ };
24
+ mockEmailService = createResendMock();
25
+ getEmailService.mockResolvedValue(mockEmailService);
26
+ vi.clearAllMocks();
27
+ });
28
+ describe('beforeChange Hook', ()=>{
29
+ it('should normalize email addresses', async ()=>{
30
+ const beforeChangeHook = async ({ data, req, operation })=>{
31
+ if (operation === 'create' && data.email) {
32
+ data.email = data.email.trim().toLowerCase();
33
+ }
34
+ return data;
35
+ };
36
+ const result = await beforeChangeHook({
37
+ data: {
38
+ email: ' USER@EXAMPLE.COM ',
39
+ name: 'Test'
40
+ },
41
+ req: mockReq,
42
+ operation: 'create',
43
+ originalDoc: null
44
+ });
45
+ expect(result.email).toBe('user@example.com');
46
+ });
47
+ it('should prevent email changes on update', async ()=>{
48
+ const beforeChangeHook = async ({ data, req, operation, originalDoc })=>{
49
+ if (operation === 'update' && data.email && originalDoc?.email !== data.email) {
50
+ throw new Error('Email cannot be changed');
51
+ }
52
+ return data;
53
+ };
54
+ await expect(beforeChangeHook({
55
+ data: {
56
+ email: 'newemail@example.com'
57
+ },
58
+ req: mockReq,
59
+ operation: 'update',
60
+ originalDoc: {
61
+ email: 'oldemail@example.com'
62
+ }
63
+ })).rejects.toThrow('Email cannot be changed');
64
+ });
65
+ it('should validate subscription status transitions', async ()=>{
66
+ const beforeChangeHook = async ({ data, req, operation, originalDoc })=>{
67
+ if (operation === 'update' && data.subscriptionStatus) {
68
+ const oldStatus = originalDoc?.subscriptionStatus;
69
+ const newStatus = data.subscriptionStatus;
70
+ // Validate allowed transitions
71
+ const allowedTransitions = {
72
+ pending: [
73
+ 'active',
74
+ 'unsubscribed'
75
+ ],
76
+ active: [
77
+ 'unsubscribed'
78
+ ],
79
+ unsubscribed: [
80
+ 'pending'
81
+ ]
82
+ };
83
+ if (!allowedTransitions[oldStatus]?.includes(newStatus)) {
84
+ throw new Error(`Invalid status transition from ${oldStatus} to ${newStatus}`);
85
+ }
86
+ }
87
+ return data;
88
+ };
89
+ // Valid transition
90
+ const result1 = await beforeChangeHook({
91
+ data: {
92
+ subscriptionStatus: 'active'
93
+ },
94
+ req: mockReq,
95
+ operation: 'update',
96
+ originalDoc: {
97
+ subscriptionStatus: 'pending'
98
+ }
99
+ });
100
+ expect(result1.subscriptionStatus).toBe('active');
101
+ // Invalid transition
102
+ await expect(beforeChangeHook({
103
+ data: {
104
+ subscriptionStatus: 'pending'
105
+ },
106
+ req: mockReq,
107
+ operation: 'update',
108
+ originalDoc: {
109
+ subscriptionStatus: 'active'
110
+ }
111
+ })).rejects.toThrow('Invalid status transition');
112
+ });
113
+ it('should set timestamps appropriately', async ()=>{
114
+ const beforeChangeHook = async ({ data, req, operation })=>{
115
+ const now = new Date();
116
+ if (operation === 'update') {
117
+ // Set unsubscribedAt when unsubscribing
118
+ if (data.subscriptionStatus === 'unsubscribed' && !data.unsubscribedAt) {
119
+ data.unsubscribedAt = now;
120
+ } else if (data.subscriptionStatus === 'pending' && data.unsubscribedAt !== undefined) {
121
+ data.unsubscribedAt = null;
122
+ }
123
+ }
124
+ return data;
125
+ };
126
+ // Unsubscribing
127
+ const result1 = await beforeChangeHook({
128
+ data: {
129
+ subscriptionStatus: 'unsubscribed'
130
+ },
131
+ req: mockReq,
132
+ operation: 'update',
133
+ originalDoc: {
134
+ subscriptionStatus: 'active'
135
+ }
136
+ });
137
+ expect(result1.unsubscribedAt).toBeInstanceOf(Date);
138
+ // Re-subscribing
139
+ const result2 = await beforeChangeHook({
140
+ data: {
141
+ subscriptionStatus: 'pending',
142
+ unsubscribedAt: new Date()
143
+ },
144
+ req: mockReq,
145
+ operation: 'update',
146
+ originalDoc: {
147
+ subscriptionStatus: 'unsubscribed'
148
+ }
149
+ });
150
+ expect(result2.unsubscribedAt).toBeNull();
151
+ });
152
+ it('should prevent duplicate emails on create', async ()=>{
153
+ const beforeChangeHook = async ({ data, req, operation })=>{
154
+ if (operation === 'create' && data.email) {
155
+ const existing = await req.payload.find({
156
+ collection: 'subscribers',
157
+ where: {
158
+ email: {
159
+ equals: data.email
160
+ }
161
+ }
162
+ });
163
+ if (existing.docs.length > 0) {
164
+ throw new Error('Email already exists');
165
+ }
166
+ }
167
+ return data;
168
+ };
169
+ seedCollection('subscribers', [
170
+ {
171
+ id: 'existing',
172
+ email: 'existing@example.com',
173
+ subscriptionStatus: 'active'
174
+ }
175
+ ]);
176
+ await expect(beforeChangeHook({
177
+ data: {
178
+ email: 'existing@example.com'
179
+ },
180
+ req: mockReq,
181
+ operation: 'create',
182
+ originalDoc: null
183
+ })).rejects.toThrow('Email already exists');
184
+ });
185
+ });
186
+ describe('afterChange Hook', ()=>{
187
+ it('should sync with email provider on create', async ()=>{
188
+ const afterChangeHook = async ({ doc, req, operation })=>{
189
+ if (operation === 'create') {
190
+ const emailService = await getEmailService({
191
+ provider: 'resend',
192
+ apiKey: 'test-key',
193
+ fromEmail: 'test@example.com',
194
+ fromName: 'Test'
195
+ });
196
+ await emailService.emails.send({
197
+ from: 'test@example.com',
198
+ to: [
199
+ doc.email
200
+ ],
201
+ subject: 'Welcome',
202
+ html: '<p>Welcome!</p>'
203
+ });
204
+ }
205
+ return doc;
206
+ };
207
+ await afterChangeHook({
208
+ doc: {
209
+ id: 'new-sub',
210
+ email: 'new@example.com'
211
+ },
212
+ req: mockReq,
213
+ operation: 'create',
214
+ previousDoc: null
215
+ });
216
+ expect(mockEmailService.emails.send).toHaveBeenCalledWith(expect.objectContaining({
217
+ to: [
218
+ 'new@example.com'
219
+ ]
220
+ }));
221
+ });
222
+ it('should handle email provider sync failures gracefully', async ()=>{
223
+ mockEmailService.emails.send.mockRejectedValueOnce(new Error('Provider error'));
224
+ const afterChangeHook = async ({ doc, req, operation })=>{
225
+ if (operation === 'create') {
226
+ try {
227
+ const emailService = await getEmailService({
228
+ provider: 'resend',
229
+ apiKey: 'test-key',
230
+ fromEmail: 'test@example.com',
231
+ fromName: 'Test'
232
+ });
233
+ await emailService.emails.send({
234
+ from: 'test@example.com',
235
+ to: [
236
+ doc.email
237
+ ],
238
+ subject: 'Welcome',
239
+ html: '<p>Welcome!</p>'
240
+ });
241
+ } catch (error) {
242
+ // Log error but don't fail the operation
243
+ console.error('Failed to sync with email provider:', error);
244
+ // Could update a sync status field
245
+ doc.emailProviderSyncStatus = 'failed';
246
+ }
247
+ }
248
+ return doc;
249
+ };
250
+ const result = await afterChangeHook({
251
+ doc: {
252
+ id: 'new-sub',
253
+ email: 'new@example.com'
254
+ },
255
+ req: mockReq,
256
+ operation: 'create',
257
+ previousDoc: null
258
+ });
259
+ expect(result.emailProviderSyncStatus).toBe('failed');
260
+ });
261
+ it('should clean up magic link tokens after verification', async ()=>{
262
+ const afterChangeHook = async ({ doc, req, operation, previousDoc })=>{
263
+ if (operation === 'update' && previousDoc?.subscriptionStatus === 'pending' && doc.subscriptionStatus === 'active') {
264
+ // Clear any magic link tokens
265
+ await req.payload.update({
266
+ collection: 'subscribers',
267
+ id: doc.id,
268
+ data: {
269
+ magicLinkToken: null,
270
+ magicLinkTokenExpiry: null
271
+ }
272
+ });
273
+ }
274
+ return doc;
275
+ };
276
+ mockReq.payload.update.mockResolvedValueOnce({
277
+ success: true
278
+ });
279
+ await afterChangeHook({
280
+ doc: {
281
+ id: 'sub-123',
282
+ subscriptionStatus: 'active'
283
+ },
284
+ req: mockReq,
285
+ operation: 'update',
286
+ previousDoc: {
287
+ subscriptionStatus: 'pending'
288
+ }
289
+ });
290
+ expect(mockReq.payload.update).toHaveBeenCalledWith(expect.objectContaining({
291
+ data: {
292
+ magicLinkToken: null,
293
+ magicLinkTokenExpiry: null
294
+ }
295
+ }));
296
+ });
297
+ });
298
+ describe('beforeDelete Hook', ()=>{
299
+ it('should prevent deletion of active subscribers by non-admins', async ()=>{
300
+ const beforeDeleteHook = async ({ req, id })=>{
301
+ const subscriber = await req.payload.findByID({
302
+ collection: 'subscribers',
303
+ id
304
+ });
305
+ if (subscriber?.subscriptionStatus === 'active' && !req.user?.roles?.includes('admin')) {
306
+ throw new Error('Only admins can delete active subscribers');
307
+ }
308
+ };
309
+ mockReq.user = {
310
+ id: 'user-123',
311
+ collection: 'users'
312
+ };
313
+ await expect(beforeDeleteHook({
314
+ req: mockReq,
315
+ id: 'sub-1'
316
+ })).rejects.toThrow('Only admins can delete active subscribers');
317
+ // Admin should be allowed
318
+ mockReq.user = createMockAdminUser();
319
+ await expect(beforeDeleteHook({
320
+ req: mockReq,
321
+ id: 'sub-1'
322
+ })).resolves.not.toThrow();
323
+ });
324
+ it('should clean up email provider data on delete', async ()=>{
325
+ const afterDeleteHook = async ({ doc, req })=>{
326
+ try {
327
+ const emailService = await getEmailService({
328
+ provider: 'broadcast',
329
+ apiKey: 'test-key',
330
+ fromEmail: 'test@example.com',
331
+ fromName: 'Test'
332
+ });
333
+ // Remove from email provider
334
+ if (doc.emailProviderId) {
335
+ await emailService.contacts.delete(doc.emailProviderId);
336
+ }
337
+ } catch (error) {
338
+ console.error('Failed to remove from email provider:', error);
339
+ }
340
+ };
341
+ const mockBroadcast = createBroadcastMock();
342
+ getEmailService.mockResolvedValue(mockBroadcast);
343
+ await afterDeleteHook({
344
+ doc: {
345
+ id: 'sub-123',
346
+ email: 'test@example.com',
347
+ emailProviderId: 'contact-123'
348
+ },
349
+ req: mockReq
350
+ });
351
+ expect(mockBroadcast.contacts.delete).toHaveBeenCalledWith('contact-123');
352
+ });
353
+ });
354
+ });
355
+
356
+ //# sourceMappingURL=subscriber-hooks.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../src/__tests__/integration/collections/subscriber-hooks.test.ts"],"sourcesContent":["import { describe, it, expect, beforeEach, vi } from 'vitest'\nimport type { CollectionBeforeChangeHook, CollectionAfterChangeHook } from 'payload'\nimport { createPayloadRequestMock, seedCollection, clearCollections, createMockAdminUser } from '../../mocks/payload'\nimport { mockSubscribers } from '../../fixtures/subscribers'\nimport { createResendMock } from '../../mocks/email-providers'\nimport type { NewsletterPluginConfig } from '../../../types'\n\n// Mock email service\nvi.mock('../../../services/email', () => ({\n getEmailService: vi.fn(),\n}))\n\nimport { getEmailService } from '../../../services/email'\n\ndescribe('Subscriber Collection Hooks Security', () => {\n let mockReq: any\n let mockEmailService: any\n const config: NewsletterPluginConfig = {\n subscribersSlug: 'subscribers',\n }\n\n beforeEach(() => {\n clearCollections()\n seedCollection('subscribers', mockSubscribers)\n \n const payloadMock = createPayloadRequestMock()\n mockReq = {\n payload: payloadMock.payload,\n user: null,\n }\n \n mockEmailService = createResendMock()\n ;(getEmailService as any).mockResolvedValue(mockEmailService)\n \n vi.clearAllMocks()\n })\n\n describe('beforeChange Hook', () => {\n it('should normalize email addresses', async () => {\n const beforeChangeHook: CollectionBeforeChangeHook = async ({ data, req, operation }) => {\n if (operation === 'create' && data.email) {\n data.email = data.email.trim().toLowerCase()\n }\n return data\n }\n\n const result = await beforeChangeHook({\n data: { email: ' USER@EXAMPLE.COM ', name: 'Test' },\n req: mockReq,\n operation: 'create',\n originalDoc: null,\n })\n\n expect(result.email).toBe('user@example.com')\n })\n\n it('should prevent email changes on update', async () => {\n const beforeChangeHook: CollectionBeforeChangeHook = async ({ data, req, operation, originalDoc }) => {\n if (operation === 'update' && data.email && originalDoc?.email !== data.email) {\n throw new Error('Email cannot be changed')\n }\n return data\n }\n\n await expect(\n beforeChangeHook({\n data: { email: 'newemail@example.com' },\n req: mockReq,\n operation: 'update',\n originalDoc: { email: 'oldemail@example.com' },\n })\n ).rejects.toThrow('Email cannot be changed')\n })\n\n it('should validate subscription status transitions', async () => {\n const beforeChangeHook: CollectionBeforeChangeHook = async ({ data, req, operation, originalDoc }) => {\n if (operation === 'update' && data.subscriptionStatus) {\n const oldStatus = originalDoc?.subscriptionStatus\n const newStatus = data.subscriptionStatus\n \n // Validate allowed transitions\n const allowedTransitions: Record<string, string[]> = {\n pending: ['active', 'unsubscribed'],\n active: ['unsubscribed'],\n unsubscribed: ['pending'], // Re-subscription\n }\n \n if (!allowedTransitions[oldStatus]?.includes(newStatus)) {\n throw new Error(`Invalid status transition from ${oldStatus} to ${newStatus}`)\n }\n }\n return data\n }\n\n // Valid transition\n const result1 = await beforeChangeHook({\n data: { subscriptionStatus: 'active' },\n req: mockReq,\n operation: 'update',\n originalDoc: { subscriptionStatus: 'pending' },\n })\n expect(result1.subscriptionStatus).toBe('active')\n\n // Invalid transition\n await expect(\n beforeChangeHook({\n data: { subscriptionStatus: 'pending' },\n req: mockReq,\n operation: 'update',\n originalDoc: { subscriptionStatus: 'active' },\n })\n ).rejects.toThrow('Invalid status transition')\n })\n\n it('should set timestamps appropriately', async () => {\n const beforeChangeHook: CollectionBeforeChangeHook = async ({ data, req, operation }) => {\n const now = new Date()\n \n if (operation === 'update') {\n // Set unsubscribedAt when unsubscribing\n if (data.subscriptionStatus === 'unsubscribed' && !data.unsubscribedAt) {\n data.unsubscribedAt = now\n }\n // Clear unsubscribedAt when re-subscribing\n else if (data.subscriptionStatus === 'pending' && data.unsubscribedAt !== undefined) {\n data.unsubscribedAt = null\n }\n }\n \n return data\n }\n\n // Unsubscribing\n const result1 = await beforeChangeHook({\n data: { subscriptionStatus: 'unsubscribed' },\n req: mockReq,\n operation: 'update',\n originalDoc: { subscriptionStatus: 'active' },\n })\n expect(result1.unsubscribedAt).toBeInstanceOf(Date)\n\n // Re-subscribing\n const result2 = await beforeChangeHook({\n data: { subscriptionStatus: 'pending', unsubscribedAt: new Date() },\n req: mockReq,\n operation: 'update',\n originalDoc: { subscriptionStatus: 'unsubscribed' },\n })\n expect(result2.unsubscribedAt).toBeNull()\n })\n\n it('should prevent duplicate emails on create', async () => {\n const beforeChangeHook: CollectionBeforeChangeHook = async ({ data, req, operation }) => {\n if (operation === 'create' && data.email) {\n const existing = await req.payload.find({\n collection: 'subscribers',\n where: {\n email: { equals: data.email },\n },\n })\n \n if (existing.docs.length > 0) {\n throw new Error('Email already exists')\n }\n }\n return data\n }\n\n seedCollection('subscribers', [{\n id: 'existing',\n email: 'existing@example.com',\n subscriptionStatus: 'active',\n }])\n\n await expect(\n beforeChangeHook({\n data: { email: 'existing@example.com' },\n req: mockReq,\n operation: 'create',\n originalDoc: null,\n })\n ).rejects.toThrow('Email already exists')\n })\n })\n\n describe('afterChange Hook', () => {\n it('should sync with email provider on create', async () => {\n const afterChangeHook: CollectionAfterChangeHook = async ({ doc, req, operation }) => {\n if (operation === 'create') {\n const emailService = await getEmailService({\n provider: 'resend',\n apiKey: 'test-key',\n fromEmail: 'test@example.com',\n fromName: 'Test',\n })\n \n await emailService.emails.send({\n from: 'test@example.com',\n to: [doc.email],\n subject: 'Welcome',\n html: '<p>Welcome!</p>',\n })\n }\n return doc\n }\n\n await afterChangeHook({\n doc: { id: 'new-sub', email: 'new@example.com' },\n req: mockReq,\n operation: 'create',\n previousDoc: null,\n })\n\n expect(mockEmailService.emails.send).toHaveBeenCalledWith(\n expect.objectContaining({\n to: ['new@example.com'],\n })\n )\n })\n\n it('should handle email provider sync failures gracefully', async () => {\n mockEmailService.emails.send.mockRejectedValueOnce(new Error('Provider error'))\n\n const afterChangeHook: CollectionAfterChangeHook = async ({ doc, req, operation }) => {\n if (operation === 'create') {\n try {\n const emailService = await getEmailService({\n provider: 'resend',\n apiKey: 'test-key',\n fromEmail: 'test@example.com',\n fromName: 'Test',\n })\n \n await emailService.emails.send({\n from: 'test@example.com',\n to: [doc.email],\n subject: 'Welcome',\n html: '<p>Welcome!</p>',\n })\n } catch (error) {\n // Log error but don't fail the operation\n console.error('Failed to sync with email provider:', error)\n // Could update a sync status field\n doc.emailProviderSyncStatus = 'failed'\n }\n }\n return doc\n }\n\n const result = await afterChangeHook({\n doc: { id: 'new-sub', email: 'new@example.com' },\n req: mockReq,\n operation: 'create',\n previousDoc: null,\n })\n\n expect(result.emailProviderSyncStatus).toBe('failed')\n })\n\n it('should clean up magic link tokens after verification', async () => {\n const afterChangeHook: CollectionAfterChangeHook = async ({ doc, req, operation, previousDoc }) => {\n if (operation === 'update' && \n previousDoc?.subscriptionStatus === 'pending' && \n doc.subscriptionStatus === 'active') {\n // Clear any magic link tokens\n await req.payload.update({\n collection: 'subscribers',\n id: doc.id,\n data: {\n magicLinkToken: null,\n magicLinkTokenExpiry: null,\n },\n })\n }\n return doc\n }\n\n mockReq.payload.update.mockResolvedValueOnce({ success: true })\n\n await afterChangeHook({\n doc: { id: 'sub-123', subscriptionStatus: 'active' },\n req: mockReq,\n operation: 'update',\n previousDoc: { subscriptionStatus: 'pending' },\n })\n\n expect(mockReq.payload.update).toHaveBeenCalledWith(\n expect.objectContaining({\n data: {\n magicLinkToken: null,\n magicLinkTokenExpiry: null,\n },\n })\n )\n })\n })\n\n describe('beforeDelete Hook', () => {\n it('should prevent deletion of active subscribers by non-admins', async () => {\n const beforeDeleteHook = async ({ req, id }: any) => {\n const subscriber = await req.payload.findByID({\n collection: 'subscribers',\n id,\n })\n \n if (subscriber?.subscriptionStatus === 'active' && !req.user?.roles?.includes('admin')) {\n throw new Error('Only admins can delete active subscribers')\n }\n }\n\n mockReq.user = { id: 'user-123', collection: 'users' }\n\n await expect(\n beforeDeleteHook({\n req: mockReq,\n id: 'sub-1', // Active subscriber\n })\n ).rejects.toThrow('Only admins can delete active subscribers')\n\n // Admin should be allowed\n mockReq.user = createMockAdminUser()\n await expect(\n beforeDeleteHook({\n req: mockReq,\n id: 'sub-1',\n })\n ).resolves.not.toThrow()\n })\n\n it('should clean up email provider data on delete', async () => {\n const afterDeleteHook = async ({ doc, req }: any) => {\n try {\n const emailService = await getEmailService({\n provider: 'broadcast',\n apiKey: 'test-key',\n fromEmail: 'test@example.com',\n fromName: 'Test',\n })\n \n // Remove from email provider\n if (doc.emailProviderId) {\n await emailService.contacts.delete(doc.emailProviderId)\n }\n } catch (error) {\n console.error('Failed to remove from email provider:', error)\n }\n }\n\n const mockBroadcast = createBroadcastMock()\n ;(getEmailService as any).mockResolvedValue(mockBroadcast)\n\n await afterDeleteHook({\n doc: { id: 'sub-123', email: 'test@example.com', emailProviderId: 'contact-123' },\n req: mockReq,\n })\n\n expect(mockBroadcast.contacts.delete).toHaveBeenCalledWith('contact-123')\n })\n })\n})"],"names":["describe","it","expect","beforeEach","vi","createPayloadRequestMock","seedCollection","clearCollections","createMockAdminUser","mockSubscribers","createResendMock","mock","getEmailService","fn","mockReq","mockEmailService","config","subscribersSlug","payloadMock","payload","user","mockResolvedValue","clearAllMocks","beforeChangeHook","data","req","operation","email","trim","toLowerCase","result","name","originalDoc","toBe","Error","rejects","toThrow","subscriptionStatus","oldStatus","newStatus","allowedTransitions","pending","active","unsubscribed","includes","result1","now","Date","unsubscribedAt","undefined","toBeInstanceOf","result2","toBeNull","existing","find","collection","where","equals","docs","length","id","afterChangeHook","doc","emailService","provider","apiKey","fromEmail","fromName","emails","send","from","to","subject","html","previousDoc","toHaveBeenCalledWith","objectContaining","mockRejectedValueOnce","error","console","emailProviderSyncStatus","update","magicLinkToken","magicLinkTokenExpiry","mockResolvedValueOnce","success","beforeDeleteHook","subscriber","findByID","roles","resolves","not","afterDeleteHook","emailProviderId","contacts","delete","mockBroadcast","createBroadcastMock"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,EAAE,EAAEC,MAAM,EAAEC,UAAU,EAAEC,EAAE,QAAQ,SAAQ;AAE7D,SAASC,wBAAwB,EAAEC,cAAc,EAAEC,gBAAgB,EAAEC,mBAAmB,QAAQ,sBAAqB;AACrH,SAASC,eAAe,QAAQ,6BAA4B;AAC5D,SAASC,gBAAgB,QAAQ,8BAA6B;AAG9D,qBAAqB;AACrBN,GAAGO,IAAI,CAAC,2BAA2B,IAAO,CAAA;QACxCC,iBAAiBR,GAAGS,EAAE;IACxB,CAAA;AAEA,SAASD,eAAe,QAAQ,0BAAyB;AAEzDZ,SAAS,wCAAwC;IAC/C,IAAIc;IACJ,IAAIC;IACJ,MAAMC,SAAiC;QACrCC,iBAAiB;IACnB;IAEAd,WAAW;QACTI;QACAD,eAAe,eAAeG;QAE9B,MAAMS,cAAcb;QACpBS,UAAU;YACRK,SAASD,YAAYC,OAAO;YAC5BC,MAAM;QACR;QAEAL,mBAAmBL;QACjBE,gBAAwBS,iBAAiB,CAACN;QAE5CX,GAAGkB,aAAa;IAClB;IAEAtB,SAAS,qBAAqB;QAC5BC,GAAG,oCAAoC;YACrC,MAAMsB,mBAA+C,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAEC,SAAS,EAAE;gBAClF,IAAIA,cAAc,YAAYF,KAAKG,KAAK,EAAE;oBACxCH,KAAKG,KAAK,GAAGH,KAAKG,KAAK,CAACC,IAAI,GAAGC,WAAW;gBAC5C;gBACA,OAAOL;YACT;YAEA,MAAMM,SAAS,MAAMP,iBAAiB;gBACpCC,MAAM;oBAAEG,OAAO;oBAAwBI,MAAM;gBAAO;gBACpDN,KAAKX;gBACLY,WAAW;gBACXM,aAAa;YACf;YAEA9B,OAAO4B,OAAOH,KAAK,EAAEM,IAAI,CAAC;QAC5B;QAEAhC,GAAG,0CAA0C;YAC3C,MAAMsB,mBAA+C,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAEC,SAAS,EAAEM,WAAW,EAAE;gBAC/F,IAAIN,cAAc,YAAYF,KAAKG,KAAK,IAAIK,aAAaL,UAAUH,KAAKG,KAAK,EAAE;oBAC7E,MAAM,IAAIO,MAAM;gBAClB;gBACA,OAAOV;YACT;YAEA,MAAMtB,OACJqB,iBAAiB;gBACfC,MAAM;oBAAEG,OAAO;gBAAuB;gBACtCF,KAAKX;gBACLY,WAAW;gBACXM,aAAa;oBAAEL,OAAO;gBAAuB;YAC/C,IACAQ,OAAO,CAACC,OAAO,CAAC;QACpB;QAEAnC,GAAG,mDAAmD;YACpD,MAAMsB,mBAA+C,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAEC,SAAS,EAAEM,WAAW,EAAE;gBAC/F,IAAIN,cAAc,YAAYF,KAAKa,kBAAkB,EAAE;oBACrD,MAAMC,YAAYN,aAAaK;oBAC/B,MAAME,YAAYf,KAAKa,kBAAkB;oBAEzC,+BAA+B;oBAC/B,MAAMG,qBAA+C;wBACnDC,SAAS;4BAAC;4BAAU;yBAAe;wBACnCC,QAAQ;4BAAC;yBAAe;wBACxBC,cAAc;4BAAC;yBAAU;oBAC3B;oBAEA,IAAI,CAACH,kBAAkB,CAACF,UAAU,EAAEM,SAASL,YAAY;wBACvD,MAAM,IAAIL,MAAM,CAAC,+BAA+B,EAAEI,UAAU,IAAI,EAAEC,WAAW;oBAC/E;gBACF;gBACA,OAAOf;YACT;YAEA,mBAAmB;YACnB,MAAMqB,UAAU,MAAMtB,iBAAiB;gBACrCC,MAAM;oBAAEa,oBAAoB;gBAAS;gBACrCZ,KAAKX;gBACLY,WAAW;gBACXM,aAAa;oBAAEK,oBAAoB;gBAAU;YAC/C;YACAnC,OAAO2C,QAAQR,kBAAkB,EAAEJ,IAAI,CAAC;YAExC,qBAAqB;YACrB,MAAM/B,OACJqB,iBAAiB;gBACfC,MAAM;oBAAEa,oBAAoB;gBAAU;gBACtCZ,KAAKX;gBACLY,WAAW;gBACXM,aAAa;oBAAEK,oBAAoB;gBAAS;YAC9C,IACAF,OAAO,CAACC,OAAO,CAAC;QACpB;QAEAnC,GAAG,uCAAuC;YACxC,MAAMsB,mBAA+C,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAEC,SAAS,EAAE;gBAClF,MAAMoB,MAAM,IAAIC;gBAEhB,IAAIrB,cAAc,UAAU;oBAC1B,wCAAwC;oBACxC,IAAIF,KAAKa,kBAAkB,KAAK,kBAAkB,CAACb,KAAKwB,cAAc,EAAE;wBACtExB,KAAKwB,cAAc,GAAGF;oBACxB,OAEK,IAAItB,KAAKa,kBAAkB,KAAK,aAAab,KAAKwB,cAAc,KAAKC,WAAW;wBACnFzB,KAAKwB,cAAc,GAAG;oBACxB;gBACF;gBAEA,OAAOxB;YACT;YAEA,gBAAgB;YAChB,MAAMqB,UAAU,MAAMtB,iBAAiB;gBACrCC,MAAM;oBAAEa,oBAAoB;gBAAe;gBAC3CZ,KAAKX;gBACLY,WAAW;gBACXM,aAAa;oBAAEK,oBAAoB;gBAAS;YAC9C;YACAnC,OAAO2C,QAAQG,cAAc,EAAEE,cAAc,CAACH;YAE9C,iBAAiB;YACjB,MAAMI,UAAU,MAAM5B,iBAAiB;gBACrCC,MAAM;oBAAEa,oBAAoB;oBAAWW,gBAAgB,IAAID;gBAAO;gBAClEtB,KAAKX;gBACLY,WAAW;gBACXM,aAAa;oBAAEK,oBAAoB;gBAAe;YACpD;YACAnC,OAAOiD,QAAQH,cAAc,EAAEI,QAAQ;QACzC;QAEAnD,GAAG,6CAA6C;YAC9C,MAAMsB,mBAA+C,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAEC,SAAS,EAAE;gBAClF,IAAIA,cAAc,YAAYF,KAAKG,KAAK,EAAE;oBACxC,MAAM0B,WAAW,MAAM5B,IAAIN,OAAO,CAACmC,IAAI,CAAC;wBACtCC,YAAY;wBACZC,OAAO;4BACL7B,OAAO;gCAAE8B,QAAQjC,KAAKG,KAAK;4BAAC;wBAC9B;oBACF;oBAEA,IAAI0B,SAASK,IAAI,CAACC,MAAM,GAAG,GAAG;wBAC5B,MAAM,IAAIzB,MAAM;oBAClB;gBACF;gBACA,OAAOV;YACT;YAEAlB,eAAe,eAAe;gBAAC;oBAC7BsD,IAAI;oBACJjC,OAAO;oBACPU,oBAAoB;gBACtB;aAAE;YAEF,MAAMnC,OACJqB,iBAAiB;gBACfC,MAAM;oBAAEG,OAAO;gBAAuB;gBACtCF,KAAKX;gBACLY,WAAW;gBACXM,aAAa;YACf,IACAG,OAAO,CAACC,OAAO,CAAC;QACpB;IACF;IAEApC,SAAS,oBAAoB;QAC3BC,GAAG,6CAA6C;YAC9C,MAAM4D,kBAA6C,OAAO,EAAEC,GAAG,EAAErC,GAAG,EAAEC,SAAS,EAAE;gBAC/E,IAAIA,cAAc,UAAU;oBAC1B,MAAMqC,eAAe,MAAMnD,gBAAgB;wBACzCoD,UAAU;wBACVC,QAAQ;wBACRC,WAAW;wBACXC,UAAU;oBACZ;oBAEA,MAAMJ,aAAaK,MAAM,CAACC,IAAI,CAAC;wBAC7BC,MAAM;wBACNC,IAAI;4BAACT,IAAInC,KAAK;yBAAC;wBACf6C,SAAS;wBACTC,MAAM;oBACR;gBACF;gBACA,OAAOX;YACT;YAEA,MAAMD,gBAAgB;gBACpBC,KAAK;oBAAEF,IAAI;oBAAWjC,OAAO;gBAAkB;gBAC/CF,KAAKX;gBACLY,WAAW;gBACXgD,aAAa;YACf;YAEAxE,OAAOa,iBAAiBqD,MAAM,CAACC,IAAI,EAAEM,oBAAoB,CACvDzE,OAAO0E,gBAAgB,CAAC;gBACtBL,IAAI;oBAAC;iBAAkB;YACzB;QAEJ;QAEAtE,GAAG,yDAAyD;YAC1Dc,iBAAiBqD,MAAM,CAACC,IAAI,CAACQ,qBAAqB,CAAC,IAAI3C,MAAM;YAE7D,MAAM2B,kBAA6C,OAAO,EAAEC,GAAG,EAAErC,GAAG,EAAEC,SAAS,EAAE;gBAC/E,IAAIA,cAAc,UAAU;oBAC1B,IAAI;wBACF,MAAMqC,eAAe,MAAMnD,gBAAgB;4BACzCoD,UAAU;4BACVC,QAAQ;4BACRC,WAAW;4BACXC,UAAU;wBACZ;wBAEA,MAAMJ,aAAaK,MAAM,CAACC,IAAI,CAAC;4BAC7BC,MAAM;4BACNC,IAAI;gCAACT,IAAInC,KAAK;6BAAC;4BACf6C,SAAS;4BACTC,MAAM;wBACR;oBACF,EAAE,OAAOK,OAAO;wBACd,yCAAyC;wBACzCC,QAAQD,KAAK,CAAC,uCAAuCA;wBACrD,mCAAmC;wBACnChB,IAAIkB,uBAAuB,GAAG;oBAChC;gBACF;gBACA,OAAOlB;YACT;YAEA,MAAMhC,SAAS,MAAM+B,gBAAgB;gBACnCC,KAAK;oBAAEF,IAAI;oBAAWjC,OAAO;gBAAkB;gBAC/CF,KAAKX;gBACLY,WAAW;gBACXgD,aAAa;YACf;YAEAxE,OAAO4B,OAAOkD,uBAAuB,EAAE/C,IAAI,CAAC;QAC9C;QAEAhC,GAAG,wDAAwD;YACzD,MAAM4D,kBAA6C,OAAO,EAAEC,GAAG,EAAErC,GAAG,EAAEC,SAAS,EAAEgD,WAAW,EAAE;gBAC5F,IAAIhD,cAAc,YACdgD,aAAarC,uBAAuB,aACpCyB,IAAIzB,kBAAkB,KAAK,UAAU;oBACvC,8BAA8B;oBAC9B,MAAMZ,IAAIN,OAAO,CAAC8D,MAAM,CAAC;wBACvB1B,YAAY;wBACZK,IAAIE,IAAIF,EAAE;wBACVpC,MAAM;4BACJ0D,gBAAgB;4BAChBC,sBAAsB;wBACxB;oBACF;gBACF;gBACA,OAAOrB;YACT;YAEAhD,QAAQK,OAAO,CAAC8D,MAAM,CAACG,qBAAqB,CAAC;gBAAEC,SAAS;YAAK;YAE7D,MAAMxB,gBAAgB;gBACpBC,KAAK;oBAAEF,IAAI;oBAAWvB,oBAAoB;gBAAS;gBACnDZ,KAAKX;gBACLY,WAAW;gBACXgD,aAAa;oBAAErC,oBAAoB;gBAAU;YAC/C;YAEAnC,OAAOY,QAAQK,OAAO,CAAC8D,MAAM,EAAEN,oBAAoB,CACjDzE,OAAO0E,gBAAgB,CAAC;gBACtBpD,MAAM;oBACJ0D,gBAAgB;oBAChBC,sBAAsB;gBACxB;YACF;QAEJ;IACF;IAEAnF,SAAS,qBAAqB;QAC5BC,GAAG,+DAA+D;YAChE,MAAMqF,mBAAmB,OAAO,EAAE7D,GAAG,EAAEmC,EAAE,EAAO;gBAC9C,MAAM2B,aAAa,MAAM9D,IAAIN,OAAO,CAACqE,QAAQ,CAAC;oBAC5CjC,YAAY;oBACZK;gBACF;gBAEA,IAAI2B,YAAYlD,uBAAuB,YAAY,CAACZ,IAAIL,IAAI,EAAEqE,OAAO7C,SAAS,UAAU;oBACtF,MAAM,IAAIV,MAAM;gBAClB;YACF;YAEApB,QAAQM,IAAI,GAAG;gBAAEwC,IAAI;gBAAYL,YAAY;YAAQ;YAErD,MAAMrD,OACJoF,iBAAiB;gBACf7D,KAAKX;gBACL8C,IAAI;YACN,IACAzB,OAAO,CAACC,OAAO,CAAC;YAElB,0BAA0B;YAC1BtB,QAAQM,IAAI,GAAGZ;YACf,MAAMN,OACJoF,iBAAiB;gBACf7D,KAAKX;gBACL8C,IAAI;YACN,IACA8B,QAAQ,CAACC,GAAG,CAACvD,OAAO;QACxB;QAEAnC,GAAG,iDAAiD;YAClD,MAAM2F,kBAAkB,OAAO,EAAE9B,GAAG,EAAErC,GAAG,EAAO;gBAC9C,IAAI;oBACF,MAAMsC,eAAe,MAAMnD,gBAAgB;wBACzCoD,UAAU;wBACVC,QAAQ;wBACRC,WAAW;wBACXC,UAAU;oBACZ;oBAEA,6BAA6B;oBAC7B,IAAIL,IAAI+B,eAAe,EAAE;wBACvB,MAAM9B,aAAa+B,QAAQ,CAACC,MAAM,CAACjC,IAAI+B,eAAe;oBACxD;gBACF,EAAE,OAAOf,OAAO;oBACdC,QAAQD,KAAK,CAAC,yCAAyCA;gBACzD;YACF;YAEA,MAAMkB,gBAAgBC;YACpBrF,gBAAwBS,iBAAiB,CAAC2E;YAE5C,MAAMJ,gBAAgB;gBACpB9B,KAAK;oBAAEF,IAAI;oBAAWjC,OAAO;oBAAoBkE,iBAAiB;gBAAc;gBAChFpE,KAAKX;YACP;YAEAZ,OAAO8F,cAAcF,QAAQ,CAACC,MAAM,EAAEpB,oBAAoB,CAAC;QAC7D;IACF;AACF"}