payload-plugin-newsletter 0.3.2 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/CLAUDE.md +31 -19
  3. package/dist/client.cjs +899 -0
  4. package/dist/client.cjs.map +1 -0
  5. package/dist/client.d.cts +52 -0
  6. package/dist/client.d.ts +52 -0
  7. package/dist/client.js +867 -0
  8. package/dist/client.js.map +1 -0
  9. package/dist/components.cjs +899 -0
  10. package/dist/components.cjs.map +1 -0
  11. package/dist/components.d.cts +4 -0
  12. package/dist/components.d.ts +4 -0
  13. package/dist/components.js +867 -0
  14. package/dist/components.js.map +1 -0
  15. package/dist/index.cjs +2004 -0
  16. package/dist/index.cjs.map +1 -0
  17. package/dist/index.d.cts +11 -0
  18. package/dist/index.d.ts +6 -5
  19. package/dist/index.js +1967 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/types.cjs +19 -0
  22. package/dist/types.cjs.map +1 -0
  23. package/dist/{types/index.d.ts → types.d.cts} +19 -17
  24. package/dist/types.d.ts +350 -0
  25. package/dist/types.js +1 -0
  26. package/dist/types.js.map +1 -0
  27. package/package.json +48 -25
  28. package/dist/.tsbuildinfo +0 -1
  29. package/dist/collections/NewsletterSettings.d.ts +0 -4
  30. package/dist/collections/NewsletterSettings.d.ts.map +0 -1
  31. package/dist/collections/Subscribers.d.ts +0 -4
  32. package/dist/collections/Subscribers.d.ts.map +0 -1
  33. package/dist/components/MagicLinkVerify.d.ts +0 -27
  34. package/dist/components/MagicLinkVerify.d.ts.map +0 -1
  35. package/dist/components/NewsletterForm.d.ts +0 -5
  36. package/dist/components/NewsletterForm.d.ts.map +0 -1
  37. package/dist/components/PreferencesForm.d.ts +0 -5
  38. package/dist/components/PreferencesForm.d.ts.map +0 -1
  39. package/dist/components/index.d.ts +0 -5
  40. package/dist/components/index.d.ts.map +0 -1
  41. package/dist/endpoints/index.d.ts +0 -4
  42. package/dist/endpoints/index.d.ts.map +0 -1
  43. package/dist/endpoints/preferences.d.ts +0 -5
  44. package/dist/endpoints/preferences.d.ts.map +0 -1
  45. package/dist/endpoints/subscribe.d.ts +0 -4
  46. package/dist/endpoints/subscribe.d.ts.map +0 -1
  47. package/dist/endpoints/unsubscribe.d.ts +0 -4
  48. package/dist/endpoints/unsubscribe.d.ts.map +0 -1
  49. package/dist/endpoints/verify-magic-link.d.ts +0 -4
  50. package/dist/endpoints/verify-magic-link.d.ts.map +0 -1
  51. package/dist/exports/client.d.ts +0 -6
  52. package/dist/exports/client.d.ts.map +0 -1
  53. package/dist/exports/components.d.ts +0 -2
  54. package/dist/exports/components.d.ts.map +0 -1
  55. package/dist/exports/types.d.ts +0 -2
  56. package/dist/exports/types.d.ts.map +0 -1
  57. package/dist/fields/newsletterScheduling.d.ts +0 -4
  58. package/dist/fields/newsletterScheduling.d.ts.map +0 -1
  59. package/dist/hooks/useNewsletterAuth.d.ts +0 -16
  60. package/dist/hooks/useNewsletterAuth.d.ts.map +0 -1
  61. package/dist/index.d.ts.map +0 -1
  62. package/dist/providers/broadcast.d.ts +0 -19
  63. package/dist/providers/broadcast.d.ts.map +0 -1
  64. package/dist/providers/index.d.ts +0 -23
  65. package/dist/providers/index.d.ts.map +0 -1
  66. package/dist/providers/resend.d.ts +0 -20
  67. package/dist/providers/resend.d.ts.map +0 -1
  68. package/dist/providers/types.d.ts +0 -46
  69. package/dist/providers/types.d.ts.map +0 -1
  70. package/dist/src/__tests__/fixtures/newsletter-settings.js +0 -41
  71. package/dist/src/__tests__/fixtures/newsletter-settings.js.map +0 -1
  72. package/dist/src/__tests__/fixtures/subscribers.js +0 -70
  73. package/dist/src/__tests__/fixtures/subscribers.js.map +0 -1
  74. package/dist/src/__tests__/integration/collections/subscriber-hooks.test.js +0 -356
  75. package/dist/src/__tests__/integration/collections/subscriber-hooks.test.js.map +0 -1
  76. package/dist/src/__tests__/integration/endpoints/preferences.test.js +0 -266
  77. package/dist/src/__tests__/integration/endpoints/preferences.test.js.map +0 -1
  78. package/dist/src/__tests__/integration/endpoints/subscribe.test.js +0 -280
  79. package/dist/src/__tests__/integration/endpoints/subscribe.test.js.map +0 -1
  80. package/dist/src/__tests__/integration/endpoints/unsubscribe.test.js +0 -187
  81. package/dist/src/__tests__/integration/endpoints/unsubscribe.test.js.map +0 -1
  82. package/dist/src/__tests__/integration/endpoints/verify-magic-link.test.js +0 -188
  83. package/dist/src/__tests__/integration/endpoints/verify-magic-link.test.js.map +0 -1
  84. package/dist/src/__tests__/mocks/email-providers.js +0 -153
  85. package/dist/src/__tests__/mocks/email-providers.js.map +0 -1
  86. package/dist/src/__tests__/mocks/payload.js +0 -244
  87. package/dist/src/__tests__/mocks/payload.js.map +0 -1
  88. package/dist/src/__tests__/security/csrf-protection.test.js +0 -309
  89. package/dist/src/__tests__/security/csrf-protection.test.js.map +0 -1
  90. package/dist/src/__tests__/security/settings-access.test.js +0 -204
  91. package/dist/src/__tests__/security/settings-access.test.js.map +0 -1
  92. package/dist/src/__tests__/security/subscriber-access.test.js +0 -210
  93. package/dist/src/__tests__/security/subscriber-access.test.js.map +0 -1
  94. package/dist/src/__tests__/security/xss-prevention.test.js +0 -305
  95. package/dist/src/__tests__/security/xss-prevention.test.js.map +0 -1
  96. package/dist/src/__tests__/setup/integration.setup.js +0 -38
  97. package/dist/src/__tests__/setup/integration.setup.js.map +0 -1
  98. package/dist/src/__tests__/setup/unit.setup.js +0 -41
  99. package/dist/src/__tests__/setup/unit.setup.js.map +0 -1
  100. package/dist/src/__tests__/unit/utils/access.test.js +0 -116
  101. package/dist/src/__tests__/unit/utils/access.test.js.map +0 -1
  102. package/dist/src/__tests__/unit/utils/jwt.test.js +0 -238
  103. package/dist/src/__tests__/unit/utils/jwt.test.js.map +0 -1
  104. package/dist/src/collections/NewsletterSettings.js +0 -390
  105. package/dist/src/collections/NewsletterSettings.js.map +0 -1
  106. package/dist/src/collections/Subscribers.js +0 -309
  107. package/dist/src/collections/Subscribers.js.map +0 -1
  108. package/dist/src/components/MagicLinkVerify.js +0 -180
  109. package/dist/src/components/MagicLinkVerify.js.map +0 -1
  110. package/dist/src/components/NewsletterForm.js +0 -326
  111. package/dist/src/components/NewsletterForm.js.map +0 -1
  112. package/dist/src/components/PreferencesForm.js +0 -524
  113. package/dist/src/components/PreferencesForm.js.map +0 -1
  114. package/dist/src/components/index.js +0 -5
  115. package/dist/src/components/index.js.map +0 -1
  116. package/dist/src/endpoints/index.js +0 -17
  117. package/dist/src/endpoints/index.js.map +0 -1
  118. package/dist/src/endpoints/preferences.js +0 -136
  119. package/dist/src/endpoints/preferences.js.map +0 -1
  120. package/dist/src/endpoints/subscribe.js +0 -151
  121. package/dist/src/endpoints/subscribe.js.map +0 -1
  122. package/dist/src/endpoints/unsubscribe.js +0 -105
  123. package/dist/src/endpoints/unsubscribe.js.map +0 -1
  124. package/dist/src/endpoints/verify-magic-link.js +0 -103
  125. package/dist/src/endpoints/verify-magic-link.js.map +0 -1
  126. package/dist/src/exports/client.js +0 -7
  127. package/dist/src/exports/client.js.map +0 -1
  128. package/dist/src/exports/components.js +0 -6
  129. package/dist/src/exports/components.js.map +0 -1
  130. package/dist/src/exports/types.js +0 -3
  131. package/dist/src/exports/types.js.map +0 -1
  132. package/dist/src/fields/newsletterScheduling.js +0 -195
  133. package/dist/src/fields/newsletterScheduling.js.map +0 -1
  134. package/dist/src/hooks/useNewsletterAuth.js +0 -112
  135. package/dist/src/hooks/useNewsletterAuth.js.map +0 -1
  136. package/dist/src/index.js +0 -130
  137. package/dist/src/index.js.map +0 -1
  138. package/dist/src/providers/broadcast.js +0 -158
  139. package/dist/src/providers/broadcast.js.map +0 -1
  140. package/dist/src/providers/index.js +0 -63
  141. package/dist/src/providers/index.js.map +0 -1
  142. package/dist/src/providers/resend.js +0 -122
  143. package/dist/src/providers/resend.js.map +0 -1
  144. package/dist/src/providers/types.js +0 -12
  145. package/dist/src/providers/types.js.map +0 -1
  146. package/dist/src/templates/BaseTemplate.js +0 -105
  147. package/dist/src/templates/BaseTemplate.js.map +0 -1
  148. package/dist/src/templates/MagicLinkTemplate.js +0 -178
  149. package/dist/src/templates/MagicLinkTemplate.js.map +0 -1
  150. package/dist/src/templates/NewsletterTemplate.js +0 -150
  151. package/dist/src/templates/NewsletterTemplate.js.map +0 -1
  152. package/dist/src/templates/WelcomeTemplate.js +0 -192
  153. package/dist/src/templates/WelcomeTemplate.js.map +0 -1
  154. package/dist/src/templates/index.js +0 -6
  155. package/dist/src/templates/index.js.map +0 -1
  156. package/dist/src/types/index.js +0 -3
  157. package/dist/src/types/index.js.map +0 -1
  158. package/dist/src/utils/access.js +0 -80
  159. package/dist/src/utils/access.js.map +0 -1
  160. package/dist/src/utils/jwt.js +0 -91
  161. package/dist/src/utils/jwt.js.map +0 -1
  162. package/dist/src/utils/validation.js +0 -74
  163. package/dist/src/utils/validation.js.map +0 -1
  164. package/dist/templates/BaseTemplate.d.ts +0 -45
  165. package/dist/templates/BaseTemplate.d.ts.map +0 -1
  166. package/dist/templates/MagicLinkTemplate.d.ts +0 -67
  167. package/dist/templates/MagicLinkTemplate.d.ts.map +0 -1
  168. package/dist/templates/NewsletterTemplate.d.ts +0 -112
  169. package/dist/templates/NewsletterTemplate.d.ts.map +0 -1
  170. package/dist/templates/WelcomeTemplate.d.ts +0 -55
  171. package/dist/templates/WelcomeTemplate.d.ts.map +0 -1
  172. package/dist/templates/index.d.ts +0 -7
  173. package/dist/templates/index.d.ts.map +0 -1
  174. package/dist/types/index.d.ts.map +0 -1
  175. package/dist/utils/access.d.ts +0 -15
  176. package/dist/utils/access.d.ts.map +0 -1
  177. package/dist/utils/jwt.d.ts +0 -32
  178. package/dist/utils/jwt.d.ts.map +0 -1
  179. package/dist/utils/validation.d.ts +0 -25
  180. package/dist/utils/validation.d.ts.map +0 -1
@@ -1,309 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { createPayloadRequestMock, seedCollection, clearCollections } from '../mocks/payload';
3
- import { mockSubscribers } from '../fixtures/subscribers';
4
- describe('CSRF Protection', ()=>{
5
- let mockReq;
6
- let mockRes;
7
- const config = {};
8
- beforeEach(()=>{
9
- clearCollections();
10
- seedCollection('subscribers', mockSubscribers);
11
- const payloadMock = createPayloadRequestMock();
12
- mockReq = {
13
- payload: payloadMock.payload,
14
- body: {},
15
- headers: {},
16
- method: 'POST'
17
- };
18
- mockRes = {
19
- status: vi.fn().mockReturnThis(),
20
- json: vi.fn(),
21
- setHeader: vi.fn(),
22
- end: vi.fn().mockReturnThis()
23
- };
24
- vi.clearAllMocks();
25
- });
26
- describe('Token Validation', ()=>{
27
- it('should validate CSRF tokens on state-changing operations', ()=>{
28
- const validateCSRFToken = (req)=>{
29
- if ([
30
- 'GET',
31
- 'HEAD',
32
- 'OPTIONS'
33
- ].includes(req.method)) {
34
- return true // Safe methods don't need CSRF protection
35
- ;
36
- }
37
- const token = req.headers['x-csrf-token'] || req.body._csrf;
38
- const sessionToken = req.session?.csrfToken;
39
- if (!token || !sessionToken) {
40
- return false;
41
- }
42
- return token === sessionToken;
43
- };
44
- // GET requests should pass without token
45
- mockReq.method = 'GET';
46
- expect(validateCSRFToken(mockReq)).toBe(true);
47
- // POST without token should fail
48
- mockReq.method = 'POST';
49
- mockReq.session = {
50
- csrfToken: 'valid-token'
51
- };
52
- expect(validateCSRFToken(mockReq)).toBe(false);
53
- // POST with valid token in header
54
- mockReq.headers['x-csrf-token'] = 'valid-token';
55
- expect(validateCSRFToken(mockReq)).toBe(true);
56
- // POST with valid token in body
57
- delete mockReq.headers['x-csrf-token'];
58
- mockReq.body._csrf = 'valid-token';
59
- expect(validateCSRFToken(mockReq)).toBe(true);
60
- // POST with invalid token
61
- mockReq.body._csrf = 'invalid-token';
62
- expect(validateCSRFToken(mockReq)).toBe(false);
63
- });
64
- it('should generate secure CSRF tokens', ()=>{
65
- const generateCSRFToken = ()=>{
66
- const array = new Uint8Array(32);
67
- // In real implementation, use crypto.getRandomValues(array)
68
- for(let i = 0; i < array.length; i++){
69
- array[i] = Math.floor(Math.random() * 256);
70
- }
71
- return Buffer.from(array).toString('base64');
72
- };
73
- const token1 = generateCSRFToken();
74
- const token2 = generateCSRFToken();
75
- // Tokens should be unique
76
- expect(token1).not.toBe(token2);
77
- // Tokens should be of sufficient length
78
- expect(token1.length).toBeGreaterThanOrEqual(32);
79
- expect(token2.length).toBeGreaterThanOrEqual(32);
80
- });
81
- });
82
- describe('SameSite Cookie Protection', ()=>{
83
- it('should set SameSite cookie attributes', ()=>{
84
- const setCookie = (res, name, value, options = {})=>{
85
- const cookieOptions = {
86
- httpOnly: true,
87
- secure: process.env.NODE_ENV === 'production',
88
- sameSite: 'strict',
89
- path: '/',
90
- ...options
91
- };
92
- const cookieString = `${name}=${value}; ${Object.entries(cookieOptions).map(([key, val])=>{
93
- if (val === true) return key;
94
- return `${key}=${val}`;
95
- }).join('; ')}`;
96
- res.setHeader('Set-Cookie', cookieString);
97
- };
98
- setCookie(mockRes, 'session', 'session-id-123');
99
- const setCookieHeader = mockRes.setHeader.mock.calls[0][1];
100
- expect(setCookieHeader).toContain('httpOnly');
101
- expect(setCookieHeader).toContain('sameSite=strict');
102
- expect(setCookieHeader).toContain('path=/');
103
- });
104
- it('should validate referer for state-changing requests', ()=>{
105
- const validateReferer = (req, allowedOrigins)=>{
106
- if ([
107
- 'GET',
108
- 'HEAD',
109
- 'OPTIONS'
110
- ].includes(req.method)) {
111
- return true;
112
- }
113
- const referer = req.headers.referer || req.headers.referrer;
114
- if (!referer) {
115
- return false // Reject if no referer on state-changing request
116
- ;
117
- }
118
- try {
119
- const refererUrl = new URL(referer);
120
- return allowedOrigins.includes(refererUrl.origin);
121
- } catch {
122
- return false;
123
- }
124
- };
125
- const allowedOrigins = [
126
- 'https://example.com',
127
- 'https://app.example.com'
128
- ];
129
- // GET requests pass without referer
130
- mockReq.method = 'GET';
131
- expect(validateReferer(mockReq, allowedOrigins)).toBe(true);
132
- // POST without referer fails
133
- mockReq.method = 'POST';
134
- expect(validateReferer(mockReq, allowedOrigins)).toBe(false);
135
- // POST with valid referer passes
136
- mockReq.headers.referer = 'https://example.com/page';
137
- expect(validateReferer(mockReq, allowedOrigins)).toBe(true);
138
- // POST with invalid referer fails
139
- mockReq.headers.referer = 'https://evil.com/page';
140
- expect(validateReferer(mockReq, allowedOrigins)).toBe(false);
141
- });
142
- });
143
- describe('Double Submit Cookie Pattern', ()=>{
144
- it('should implement double submit cookie pattern', ()=>{
145
- const doubleSubmitMiddleware = (req, res)=>{
146
- if ([
147
- 'GET',
148
- 'HEAD',
149
- 'OPTIONS'
150
- ].includes(req.method)) {
151
- return true;
152
- }
153
- // Get token from cookie
154
- const cookieToken = req.cookies?.csrfToken;
155
- // Get token from request (header or body)
156
- const requestToken = req.headers['x-csrf-token'] || req.body._csrf;
157
- if (!cookieToken || !requestToken) {
158
- return false;
159
- }
160
- // Compare tokens
161
- return cookieToken === requestToken;
162
- };
163
- // Set up request with matching tokens
164
- mockReq.cookies = {
165
- csrfToken: 'token-123'
166
- };
167
- mockReq.headers['x-csrf-token'] = 'token-123';
168
- expect(doubleSubmitMiddleware(mockReq, mockRes)).toBe(true);
169
- // Mismatched tokens
170
- mockReq.headers['x-csrf-token'] = 'different-token';
171
- expect(doubleSubmitMiddleware(mockReq, mockRes)).toBe(false);
172
- // Missing cookie token
173
- delete mockReq.cookies.csrfToken;
174
- expect(doubleSubmitMiddleware(mockReq, mockRes)).toBe(false);
175
- });
176
- });
177
- describe('API Endpoint Protection', ()=>{
178
- it('should protect subscribe endpoint from CSRF', async ()=>{
179
- const subscribeHandler = async (req, res)=>{
180
- // Validate CSRF token for subscribe endpoint
181
- if (req.method === 'POST') {
182
- const token = req.headers['x-csrf-token'];
183
- if (!token || token !== req.session?.csrfToken) {
184
- return res.status(403).json({
185
- error: 'Invalid CSRF token'
186
- });
187
- }
188
- }
189
- // Process subscription
190
- return res.status(200).json({
191
- success: true
192
- });
193
- };
194
- // Request without CSRF token
195
- mockReq.session = {
196
- csrfToken: 'valid-token'
197
- };
198
- await subscribeHandler(mockReq, mockRes);
199
- expect(mockRes.status).toHaveBeenCalledWith(403);
200
- // Request with valid CSRF token
201
- mockRes.status.mockClear();
202
- mockReq.headers['x-csrf-token'] = 'valid-token';
203
- await subscribeHandler(mockReq, mockRes);
204
- expect(mockRes.status).toHaveBeenCalledWith(200);
205
- });
206
- it('should protect preferences endpoint from CSRF', async ()=>{
207
- const preferencesHandler = async (req, res)=>{
208
- if (req.method === 'POST' || req.method === 'PUT') {
209
- // Check origin header for API requests
210
- const origin = req.headers.origin;
211
- const allowedOrigins = [
212
- 'https://example.com',
213
- 'https://app.example.com'
214
- ];
215
- if (!origin || !allowedOrigins.includes(origin)) {
216
- return res.status(403).json({
217
- error: 'Cross-origin request blocked'
218
- });
219
- }
220
- }
221
- return res.status(200).json({
222
- success: true
223
- });
224
- };
225
- // POST without origin
226
- mockReq.method = 'POST';
227
- await preferencesHandler(mockReq, mockRes);
228
- expect(mockRes.status).toHaveBeenCalledWith(403);
229
- // POST with invalid origin
230
- mockRes.status.mockClear();
231
- mockReq.headers.origin = 'https://evil.com';
232
- await preferencesHandler(mockReq, mockRes);
233
- expect(mockRes.status).toHaveBeenCalledWith(403);
234
- // POST with valid origin
235
- mockRes.status.mockClear();
236
- mockReq.headers.origin = 'https://example.com';
237
- await preferencesHandler(mockReq, mockRes);
238
- expect(mockRes.status).toHaveBeenCalledWith(200);
239
- });
240
- });
241
- describe('Token Timing Attack Prevention', ()=>{
242
- it('should use constant-time comparison for tokens', ()=>{
243
- const constantTimeCompare = (a, b)=>{
244
- if (a.length !== b.length) {
245
- return false;
246
- }
247
- let result = 0;
248
- for(let i = 0; i < a.length; i++){
249
- result |= a.charCodeAt(i) ^ b.charCodeAt(i);
250
- }
251
- return result === 0;
252
- };
253
- // Same tokens
254
- expect(constantTimeCompare('token123', 'token123')).toBe(true);
255
- // Different tokens
256
- expect(constantTimeCompare('token123', 'token456')).toBe(false);
257
- // Different lengths (early return is ok here)
258
- expect(constantTimeCompare('short', 'muchlongertoken')).toBe(false);
259
- // Measure timing (in real tests, would need more sophisticated timing)
260
- const token1 = 'a'.repeat(32);
261
- const token2 = 'b'.repeat(32);
262
- const start = performance.now();
263
- constantTimeCompare(token1, token2);
264
- const duration = performance.now() - start;
265
- // Should take roughly same time regardless of where difference is
266
- expect(duration).toBeLessThan(1); // Very rough check
267
- });
268
- });
269
- describe('Pre-flight Request Handling', ()=>{
270
- it('should handle OPTIONS requests properly', async ()=>{
271
- const corsHandler = (req, res)=>{
272
- const allowedOrigins = [
273
- 'https://example.com',
274
- 'https://app.example.com'
275
- ];
276
- const origin = req.headers.origin;
277
- if (req.method === 'OPTIONS') {
278
- if (origin && allowedOrigins.includes(origin)) {
279
- res.setHeader('Access-Control-Allow-Origin', origin);
280
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
281
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-CSRF-Token');
282
- res.setHeader('Access-Control-Allow-Credentials', 'true');
283
- res.setHeader('Access-Control-Max-Age', '86400');
284
- }
285
- return res.status(204).end();
286
- }
287
- // Regular request handling
288
- if (origin && allowedOrigins.includes(origin)) {
289
- res.setHeader('Access-Control-Allow-Origin', origin);
290
- res.setHeader('Access-Control-Allow-Credentials', 'true');
291
- }
292
- };
293
- // OPTIONS request from allowed origin
294
- mockReq.method = 'OPTIONS';
295
- mockReq.headers.origin = 'https://example.com';
296
- corsHandler(mockReq, mockRes);
297
- expect(mockRes.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Origin', 'https://example.com');
298
- expect(mockRes.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Headers', expect.stringContaining('X-CSRF-Token'));
299
- expect(mockRes.status).toHaveBeenCalledWith(204);
300
- // OPTIONS request from disallowed origin
301
- mockRes.setHeader.mockClear();
302
- mockReq.headers.origin = 'https://evil.com';
303
- corsHandler(mockReq, mockRes);
304
- expect(mockRes.setHeader).not.toHaveBeenCalledWith('Access-Control-Allow-Origin', 'https://evil.com');
305
- });
306
- });
307
- });
308
-
309
- //# sourceMappingURL=csrf-protection.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../src/__tests__/security/csrf-protection.test.ts"],"sourcesContent":["import { describe, it, expect, beforeEach, vi } from 'vitest'\nimport { createPayloadRequestMock, seedCollection, clearCollections } from '../mocks/payload'\nimport { mockSubscribers } from '../fixtures/subscribers'\nimport type { NewsletterPluginConfig } from '../../types'\n\ndescribe('CSRF Protection', () => {\n let mockReq: any\n let mockRes: any\n const config: NewsletterPluginConfig = {}\n\n beforeEach(() => {\n clearCollections()\n seedCollection('subscribers', mockSubscribers)\n \n const payloadMock = createPayloadRequestMock()\n mockReq = {\n payload: payloadMock.payload,\n body: {},\n headers: {},\n method: 'POST',\n }\n \n mockRes = {\n status: vi.fn().mockReturnThis(),\n json: vi.fn(),\n setHeader: vi.fn(),\n end: vi.fn().mockReturnThis(),\n }\n \n vi.clearAllMocks()\n })\n\n describe('Token Validation', () => {\n it('should validate CSRF tokens on state-changing operations', () => {\n const validateCSRFToken = (req: any): boolean => {\n if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {\n return true // Safe methods don't need CSRF protection\n }\n\n const token = req.headers['x-csrf-token'] || req.body._csrf\n const sessionToken = req.session?.csrfToken\n\n if (!token || !sessionToken) {\n return false\n }\n\n return token === sessionToken\n }\n\n // GET requests should pass without token\n mockReq.method = 'GET'\n expect(validateCSRFToken(mockReq)).toBe(true)\n\n // POST without token should fail\n mockReq.method = 'POST'\n mockReq.session = { csrfToken: 'valid-token' }\n expect(validateCSRFToken(mockReq)).toBe(false)\n\n // POST with valid token in header\n mockReq.headers['x-csrf-token'] = 'valid-token'\n expect(validateCSRFToken(mockReq)).toBe(true)\n\n // POST with valid token in body\n delete mockReq.headers['x-csrf-token']\n mockReq.body._csrf = 'valid-token'\n expect(validateCSRFToken(mockReq)).toBe(true)\n\n // POST with invalid token\n mockReq.body._csrf = 'invalid-token'\n expect(validateCSRFToken(mockReq)).toBe(false)\n })\n\n it('should generate secure CSRF tokens', () => {\n const generateCSRFToken = (): string => {\n const array = new Uint8Array(32)\n // In real implementation, use crypto.getRandomValues(array)\n for (let i = 0; i < array.length; i++) {\n array[i] = Math.floor(Math.random() * 256)\n }\n return Buffer.from(array).toString('base64')\n }\n\n const token1 = generateCSRFToken()\n const token2 = generateCSRFToken()\n\n // Tokens should be unique\n expect(token1).not.toBe(token2)\n \n // Tokens should be of sufficient length\n expect(token1.length).toBeGreaterThanOrEqual(32)\n expect(token2.length).toBeGreaterThanOrEqual(32)\n })\n })\n\n describe('SameSite Cookie Protection', () => {\n it('should set SameSite cookie attributes', () => {\n const setCookie = (res: any, name: string, value: string, options: any = {}) => {\n const cookieOptions = {\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'strict',\n path: '/',\n ...options,\n }\n\n const cookieString = `${name}=${value}; ${Object.entries(cookieOptions)\n .map(([key, val]) => {\n if (val === true) return key\n return `${key}=${val}`\n })\n .join('; ')}`\n\n res.setHeader('Set-Cookie', cookieString)\n }\n\n setCookie(mockRes, 'session', 'session-id-123')\n \n const setCookieHeader = mockRes.setHeader.mock.calls[0][1]\n expect(setCookieHeader).toContain('httpOnly')\n expect(setCookieHeader).toContain('sameSite=strict')\n expect(setCookieHeader).toContain('path=/')\n })\n\n it('should validate referer for state-changing requests', () => {\n const validateReferer = (req: any, allowedOrigins: string[]): boolean => {\n if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {\n return true\n }\n\n const referer = req.headers.referer || req.headers.referrer\n if (!referer) {\n return false // Reject if no referer on state-changing request\n }\n\n try {\n const refererUrl = new URL(referer)\n return allowedOrigins.includes(refererUrl.origin)\n } catch {\n return false\n }\n }\n\n const allowedOrigins = ['https://example.com', 'https://app.example.com']\n\n // GET requests pass without referer\n mockReq.method = 'GET'\n expect(validateReferer(mockReq, allowedOrigins)).toBe(true)\n\n // POST without referer fails\n mockReq.method = 'POST'\n expect(validateReferer(mockReq, allowedOrigins)).toBe(false)\n\n // POST with valid referer passes\n mockReq.headers.referer = 'https://example.com/page'\n expect(validateReferer(mockReq, allowedOrigins)).toBe(true)\n\n // POST with invalid referer fails\n mockReq.headers.referer = 'https://evil.com/page'\n expect(validateReferer(mockReq, allowedOrigins)).toBe(false)\n })\n })\n\n describe('Double Submit Cookie Pattern', () => {\n it('should implement double submit cookie pattern', () => {\n const doubleSubmitMiddleware = (req: any, res: any) => {\n if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {\n return true\n }\n\n // Get token from cookie\n const cookieToken = req.cookies?.csrfToken\n // Get token from request (header or body)\n const requestToken = req.headers['x-csrf-token'] || req.body._csrf\n\n if (!cookieToken || !requestToken) {\n return false\n }\n\n // Compare tokens\n return cookieToken === requestToken\n }\n\n // Set up request with matching tokens\n mockReq.cookies = { csrfToken: 'token-123' }\n mockReq.headers['x-csrf-token'] = 'token-123'\n expect(doubleSubmitMiddleware(mockReq, mockRes)).toBe(true)\n\n // Mismatched tokens\n mockReq.headers['x-csrf-token'] = 'different-token'\n expect(doubleSubmitMiddleware(mockReq, mockRes)).toBe(false)\n\n // Missing cookie token\n delete mockReq.cookies.csrfToken\n expect(doubleSubmitMiddleware(mockReq, mockRes)).toBe(false)\n })\n })\n\n describe('API Endpoint Protection', () => {\n it('should protect subscribe endpoint from CSRF', async () => {\n const subscribeHandler = async (req: any, res: any) => {\n // Validate CSRF token for subscribe endpoint\n if (req.method === 'POST') {\n const token = req.headers['x-csrf-token']\n if (!token || token !== req.session?.csrfToken) {\n return res.status(403).json({\n error: 'Invalid CSRF token',\n })\n }\n }\n\n // Process subscription\n return res.status(200).json({ success: true })\n }\n\n // Request without CSRF token\n mockReq.session = { csrfToken: 'valid-token' }\n await subscribeHandler(mockReq, mockRes)\n expect(mockRes.status).toHaveBeenCalledWith(403)\n\n // Request with valid CSRF token\n mockRes.status.mockClear()\n mockReq.headers['x-csrf-token'] = 'valid-token'\n await subscribeHandler(mockReq, mockRes)\n expect(mockRes.status).toHaveBeenCalledWith(200)\n })\n\n it('should protect preferences endpoint from CSRF', async () => {\n const preferencesHandler = async (req: any, res: any) => {\n if (req.method === 'POST' || req.method === 'PUT') {\n // Check origin header for API requests\n const origin = req.headers.origin\n const allowedOrigins = ['https://example.com', 'https://app.example.com']\n \n if (!origin || !allowedOrigins.includes(origin)) {\n return res.status(403).json({\n error: 'Cross-origin request blocked',\n })\n }\n }\n\n return res.status(200).json({ success: true })\n }\n\n // POST without origin\n mockReq.method = 'POST'\n await preferencesHandler(mockReq, mockRes)\n expect(mockRes.status).toHaveBeenCalledWith(403)\n\n // POST with invalid origin\n mockRes.status.mockClear()\n mockReq.headers.origin = 'https://evil.com'\n await preferencesHandler(mockReq, mockRes)\n expect(mockRes.status).toHaveBeenCalledWith(403)\n\n // POST with valid origin\n mockRes.status.mockClear()\n mockReq.headers.origin = 'https://example.com'\n await preferencesHandler(mockReq, mockRes)\n expect(mockRes.status).toHaveBeenCalledWith(200)\n })\n })\n\n describe('Token Timing Attack Prevention', () => {\n it('should use constant-time comparison for tokens', () => {\n const constantTimeCompare = (a: string, b: string): boolean => {\n if (a.length !== b.length) {\n return false\n }\n\n let result = 0\n for (let i = 0; i < a.length; i++) {\n result |= a.charCodeAt(i) ^ b.charCodeAt(i)\n }\n return result === 0\n }\n\n // Same tokens\n expect(constantTimeCompare('token123', 'token123')).toBe(true)\n \n // Different tokens\n expect(constantTimeCompare('token123', 'token456')).toBe(false)\n \n // Different lengths (early return is ok here)\n expect(constantTimeCompare('short', 'muchlongertoken')).toBe(false)\n\n // Measure timing (in real tests, would need more sophisticated timing)\n const token1 = 'a'.repeat(32)\n const token2 = 'b'.repeat(32)\n const start = performance.now()\n constantTimeCompare(token1, token2)\n const duration = performance.now() - start\n \n // Should take roughly same time regardless of where difference is\n expect(duration).toBeLessThan(1) // Very rough check\n })\n })\n\n describe('Pre-flight Request Handling', () => {\n it('should handle OPTIONS requests properly', async () => {\n const corsHandler = (req: any, res: any) => {\n const allowedOrigins = ['https://example.com', 'https://app.example.com']\n const origin = req.headers.origin\n\n if (req.method === 'OPTIONS') {\n if (origin && allowedOrigins.includes(origin)) {\n res.setHeader('Access-Control-Allow-Origin', origin)\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-CSRF-Token')\n res.setHeader('Access-Control-Allow-Credentials', 'true')\n res.setHeader('Access-Control-Max-Age', '86400')\n }\n return res.status(204).end()\n }\n\n // Regular request handling\n if (origin && allowedOrigins.includes(origin)) {\n res.setHeader('Access-Control-Allow-Origin', origin)\n res.setHeader('Access-Control-Allow-Credentials', 'true')\n }\n }\n\n // OPTIONS request from allowed origin\n mockReq.method = 'OPTIONS'\n mockReq.headers.origin = 'https://example.com'\n corsHandler(mockReq, mockRes)\n \n expect(mockRes.setHeader).toHaveBeenCalledWith(\n 'Access-Control-Allow-Origin',\n 'https://example.com'\n )\n expect(mockRes.setHeader).toHaveBeenCalledWith(\n 'Access-Control-Allow-Headers',\n expect.stringContaining('X-CSRF-Token')\n )\n expect(mockRes.status).toHaveBeenCalledWith(204)\n\n // OPTIONS request from disallowed origin\n mockRes.setHeader.mockClear()\n mockReq.headers.origin = 'https://evil.com'\n corsHandler(mockReq, mockRes)\n \n expect(mockRes.setHeader).not.toHaveBeenCalledWith(\n 'Access-Control-Allow-Origin',\n 'https://evil.com'\n )\n })\n })\n})"],"names":["describe","it","expect","beforeEach","vi","createPayloadRequestMock","seedCollection","clearCollections","mockSubscribers","mockReq","mockRes","config","payloadMock","payload","body","headers","method","status","fn","mockReturnThis","json","setHeader","end","clearAllMocks","validateCSRFToken","req","includes","token","_csrf","sessionToken","session","csrfToken","toBe","generateCSRFToken","array","Uint8Array","i","length","Math","floor","random","Buffer","from","toString","token1","token2","not","toBeGreaterThanOrEqual","setCookie","res","name","value","options","cookieOptions","httpOnly","secure","process","env","NODE_ENV","sameSite","path","cookieString","Object","entries","map","key","val","join","setCookieHeader","mock","calls","toContain","validateReferer","allowedOrigins","referer","referrer","refererUrl","URL","origin","doubleSubmitMiddleware","cookieToken","cookies","requestToken","subscribeHandler","error","success","toHaveBeenCalledWith","mockClear","preferencesHandler","constantTimeCompare","a","b","result","charCodeAt","repeat","start","performance","now","duration","toBeLessThan","corsHandler","stringContaining"],"mappings":"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"}
@@ -1,204 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { createMockUser, createMockAdminUser, createPayloadRequestMock, clearCollections, seedCollection } from '../mocks/payload';
3
- import { mockNewsletterSettings } from '../fixtures/newsletter-settings';
4
- describe('Newsletter Settings Access Control Security', ()=>{
5
- let mockReq;
6
- const mockConfig = {};
7
- beforeEach(()=>{
8
- clearCollections();
9
- seedCollection('newsletter-settings', [
10
- mockNewsletterSettings
11
- ]);
12
- mockReq = createPayloadRequestMock();
13
- });
14
- describe('Read Access', ()=>{
15
- it('should allow public read access for settings validation', async ()=>{
16
- // Unauthenticated user should be able to read settings
17
- // This is necessary for subscription forms to validate
18
- const result = await mockReq.payload.find({
19
- collection: 'newsletter-settings',
20
- overrideAccess: false,
21
- user: null
22
- });
23
- expect(result.docs).toHaveLength(1);
24
- expect(result.docs[0].id).toBe('settings-1');
25
- });
26
- it('should not expose sensitive data in public reads', async ()=>{
27
- const result = await mockReq.payload.find({
28
- collection: 'newsletter-settings',
29
- overrideAccess: false,
30
- user: null
31
- });
32
- const settings = result.docs[0];
33
- // Ensure API keys are not exposed (this would be handled by field access control)
34
- // This test documents the expected behavior
35
- expect(settings.resendSettings.apiKey).toBe('test-api-key'); // In production, this should be hidden
36
- });
37
- });
38
- describe('Write Access', ()=>{
39
- it('should deny create access to unauthenticated users', async ()=>{
40
- await expect(mockReq.payload.create({
41
- collection: 'newsletter-settings',
42
- data: {
43
- name: 'New Settings',
44
- active: false,
45
- provider: 'resend'
46
- },
47
- overrideAccess: false,
48
- user: null
49
- })).rejects.toThrow('Unauthorized');
50
- });
51
- it('should deny update access to regular users', async ()=>{
52
- await expect(mockReq.payload.update({
53
- collection: 'newsletter-settings',
54
- id: 'settings-1',
55
- data: {
56
- name: 'Hacked!'
57
- },
58
- overrideAccess: false,
59
- user: createMockUser()
60
- })).rejects.toThrow('Unauthorized');
61
- });
62
- it('should allow update access to admin users', async ()=>{
63
- const adminUser = createMockAdminUser();
64
- const result = await mockReq.payload.update({
65
- collection: 'newsletter-settings',
66
- id: 'settings-1',
67
- data: {
68
- name: 'Updated by Admin'
69
- },
70
- overrideAccess: false,
71
- user: adminUser
72
- });
73
- expect(result.name).toBe('Updated by Admin');
74
- });
75
- it('should deny delete access to non-admins', async ()=>{
76
- await expect(mockReq.payload.delete({
77
- collection: 'newsletter-settings',
78
- id: 'settings-1',
79
- overrideAccess: false,
80
- user: createMockUser()
81
- })).rejects.toThrow('Unauthorized');
82
- });
83
- it('should prevent synthetic users from modifying settings', async ()=>{
84
- const syntheticUser = {
85
- id: 'sub-123',
86
- email: 'subscriber@example.com',
87
- collection: 'subscribers'
88
- };
89
- await expect(mockReq.payload.update({
90
- collection: 'newsletter-settings',
91
- id: 'settings-1',
92
- data: {
93
- name: 'Hacked by subscriber!'
94
- },
95
- overrideAccess: false,
96
- user: syntheticUser
97
- })).rejects.toThrow('Unauthorized');
98
- });
99
- });
100
- describe('Settings Validation Hook Security', ()=>{
101
- it('should enforce admin check in beforeChange hook', async ()=>{
102
- // The beforeChange hook should verify admin status
103
- // This test documents expected behavior
104
- const adminUser = createMockAdminUser();
105
- // Mock the beforeChange hook behavior
106
- const beforeChangeHook = vi.fn(({ req, operation })=>{
107
- if (operation !== 'read' && !req.user?.roles?.includes('admin')) {
108
- throw new Error('Unauthorized: Only admins can modify newsletter settings');
109
- }
110
- });
111
- // Simulate hook execution
112
- expect(()=>beforeChangeHook({
113
- req: {
114
- user: createMockUser()
115
- },
116
- operation: 'update'
117
- })).toThrow('Unauthorized: Only admins can modify newsletter settings');
118
- expect(()=>beforeChangeHook({
119
- req: {
120
- user: adminUser
121
- },
122
- operation: 'update'
123
- })).not.toThrow();
124
- });
125
- });
126
- describe('API Key Security', ()=>{
127
- it('should protect email provider API keys', async ()=>{
128
- // Test that API keys are properly handled
129
- const settings = await mockReq.payload.findByID({
130
- collection: 'newsletter-settings',
131
- id: 'settings-1',
132
- overrideAccess: false,
133
- user: null
134
- });
135
- // In production, field-level access control should hide API keys
136
- // from non-admin users
137
- expect(settings.resendSettings.apiKey).toBeDefined();
138
- // This test documents that additional field-level security is needed
139
- });
140
- it('should validate API key format on update', async ()=>{
141
- const adminUser = createMockAdminUser();
142
- // Test various invalid API key formats
143
- const invalidApiKeys = [
144
- '',
145
- ' ',
146
- 'key with spaces',
147
- '<script>alert("xss")</script>'
148
- ];
149
- // Current implementation doesn't validate API keys
150
- // This test documents expected validation behavior
151
- for (const invalidKey of invalidApiKeys){
152
- const result = await mockReq.payload.update({
153
- collection: 'newsletter-settings',
154
- id: 'settings-1',
155
- data: {
156
- resendSettings: {
157
- ...mockNewsletterSettings.resendSettings,
158
- apiKey: invalidKey
159
- }
160
- },
161
- overrideAccess: false,
162
- user: adminUser
163
- });
164
- // Should either sanitize or reject invalid keys
165
- expect(result.resendSettings.apiKey).toBe(invalidKey); // Currently allows any value
166
- }
167
- });
168
- });
169
- describe('Settings Singleton Pattern', ()=>{
170
- it('should enforce single settings document', async ()=>{
171
- const adminUser = createMockAdminUser();
172
- // Try to create a second settings document
173
- const secondSettings = await mockReq.payload.create({
174
- collection: 'newsletter-settings',
175
- data: {
176
- name: 'Second Settings',
177
- active: false,
178
- provider: 'resend',
179
- resendSettings: {
180
- apiKey: 'another-key',
181
- audienceIds: []
182
- },
183
- from: {
184
- email: 'another@example.com',
185
- name: 'Another Newsletter'
186
- }
187
- },
188
- overrideAccess: false,
189
- user: adminUser
190
- });
191
- // Currently allows multiple settings documents
192
- // This test documents that singleton enforcement is needed
193
- expect(secondSettings.id).toBeDefined();
194
- const allSettings = await mockReq.payload.find({
195
- collection: 'newsletter-settings',
196
- overrideAccess: false,
197
- user: adminUser
198
- });
199
- expect(allSettings.docs).toHaveLength(2); // Should ideally be 1
200
- });
201
- });
202
- });
203
-
204
- //# sourceMappingURL=settings-access.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../src/__tests__/security/settings-access.test.ts"],"sourcesContent":["import { describe, it, expect, beforeEach, vi } from 'vitest'\nimport type { PayloadRequest } from 'payload'\nimport type { NewsletterPluginConfig } from '../../../types'\nimport { createMockUser, createMockAdminUser, createPayloadRequestMock, clearCollections, seedCollection } from '../mocks/payload'\nimport { mockNewsletterSettings } from '../fixtures/newsletter-settings'\n\ndescribe('Newsletter Settings Access Control Security', () => {\n let mockReq: Partial<PayloadRequest>\n const mockConfig: NewsletterPluginConfig = {}\n\n beforeEach(() => {\n clearCollections()\n seedCollection('newsletter-settings', [mockNewsletterSettings])\n mockReq = createPayloadRequestMock()\n })\n\n describe('Read Access', () => {\n it('should allow public read access for settings validation', async () => {\n // Unauthenticated user should be able to read settings\n // This is necessary for subscription forms to validate\n const result = await mockReq.payload!.find({\n collection: 'newsletter-settings',\n overrideAccess: false,\n user: null,\n })\n \n expect(result.docs).toHaveLength(1)\n expect(result.docs[0].id).toBe('settings-1')\n })\n\n it('should not expose sensitive data in public reads', async () => {\n const result = await mockReq.payload!.find({\n collection: 'newsletter-settings',\n overrideAccess: false,\n user: null,\n })\n \n const settings = result.docs[0]\n // Ensure API keys are not exposed (this would be handled by field access control)\n // This test documents the expected behavior\n expect(settings.resendSettings.apiKey).toBe('test-api-key') // In production, this should be hidden\n })\n })\n\n describe('Write Access', () => {\n it('should deny create access to unauthenticated users', async () => {\n await expect(\n mockReq.payload!.create({\n collection: 'newsletter-settings',\n data: {\n name: 'New Settings',\n active: false,\n provider: 'resend',\n },\n overrideAccess: false,\n user: null,\n })\n ).rejects.toThrow('Unauthorized')\n })\n\n it('should deny update access to regular users', async () => {\n await expect(\n mockReq.payload!.update({\n collection: 'newsletter-settings',\n id: 'settings-1',\n data: {\n name: 'Hacked!',\n },\n overrideAccess: false,\n user: createMockUser(),\n })\n ).rejects.toThrow('Unauthorized')\n })\n\n it('should allow update access to admin users', async () => {\n const adminUser = createMockAdminUser()\n const result = await mockReq.payload!.update({\n collection: 'newsletter-settings',\n id: 'settings-1',\n data: {\n name: 'Updated by Admin',\n },\n overrideAccess: false,\n user: adminUser,\n })\n \n expect(result.name).toBe('Updated by Admin')\n })\n\n it('should deny delete access to non-admins', async () => {\n await expect(\n mockReq.payload!.delete({\n collection: 'newsletter-settings',\n id: 'settings-1',\n overrideAccess: false,\n user: createMockUser(),\n })\n ).rejects.toThrow('Unauthorized')\n })\n\n it('should prevent synthetic users from modifying settings', async () => {\n const syntheticUser = {\n id: 'sub-123',\n email: 'subscriber@example.com',\n collection: 'subscribers', // Synthetic user\n }\n \n await expect(\n mockReq.payload!.update({\n collection: 'newsletter-settings',\n id: 'settings-1',\n data: {\n name: 'Hacked by subscriber!',\n },\n overrideAccess: false,\n user: syntheticUser,\n })\n ).rejects.toThrow('Unauthorized')\n })\n })\n\n describe('Settings Validation Hook Security', () => {\n it('should enforce admin check in beforeChange hook', async () => {\n // The beforeChange hook should verify admin status\n // This test documents expected behavior\n const adminUser = createMockAdminUser()\n \n // Mock the beforeChange hook behavior\n const beforeChangeHook = vi.fn(({ req, operation }) => {\n if (operation !== 'read' && !req.user?.roles?.includes('admin')) {\n throw new Error('Unauthorized: Only admins can modify newsletter settings')\n }\n })\n \n // Simulate hook execution\n expect(() => \n beforeChangeHook({ \n req: { user: createMockUser() }, \n operation: 'update' \n })\n ).toThrow('Unauthorized: Only admins can modify newsletter settings')\n \n expect(() => \n beforeChangeHook({ \n req: { user: adminUser }, \n operation: 'update' \n })\n ).not.toThrow()\n })\n })\n\n describe('API Key Security', () => {\n it('should protect email provider API keys', async () => {\n // Test that API keys are properly handled\n const settings = await mockReq.payload!.findByID({\n collection: 'newsletter-settings',\n id: 'settings-1',\n overrideAccess: false,\n user: null,\n })\n \n // In production, field-level access control should hide API keys\n // from non-admin users\n expect(settings.resendSettings.apiKey).toBeDefined()\n // This test documents that additional field-level security is needed\n })\n\n it('should validate API key format on update', async () => {\n const adminUser = createMockAdminUser()\n \n // Test various invalid API key formats\n const invalidApiKeys = [\n '', // Empty\n ' ', // Whitespace only\n 'key with spaces', // Contains spaces\n '<script>alert(\"xss\")</script>', // XSS attempt\n ]\n \n // Current implementation doesn't validate API keys\n // This test documents expected validation behavior\n for (const invalidKey of invalidApiKeys) {\n const result = await mockReq.payload!.update({\n collection: 'newsletter-settings',\n id: 'settings-1',\n data: {\n resendSettings: {\n ...mockNewsletterSettings.resendSettings,\n apiKey: invalidKey,\n },\n },\n overrideAccess: false,\n user: adminUser,\n })\n \n // Should either sanitize or reject invalid keys\n expect(result.resendSettings.apiKey).toBe(invalidKey) // Currently allows any value\n }\n })\n })\n\n describe('Settings Singleton Pattern', () => {\n it('should enforce single settings document', async () => {\n const adminUser = createMockAdminUser()\n \n // Try to create a second settings document\n const secondSettings = await mockReq.payload!.create({\n collection: 'newsletter-settings',\n data: {\n name: 'Second Settings',\n active: false,\n provider: 'resend',\n resendSettings: {\n apiKey: 'another-key',\n audienceIds: [],\n },\n from: {\n email: 'another@example.com',\n name: 'Another Newsletter',\n },\n },\n overrideAccess: false,\n user: adminUser,\n })\n \n // Currently allows multiple settings documents\n // This test documents that singleton enforcement is needed\n expect(secondSettings.id).toBeDefined()\n \n const allSettings = await mockReq.payload!.find({\n collection: 'newsletter-settings',\n overrideAccess: false,\n user: adminUser,\n })\n \n expect(allSettings.docs).toHaveLength(2) // Should ideally be 1\n })\n })\n})"],"names":["describe","it","expect","beforeEach","vi","createMockUser","createMockAdminUser","createPayloadRequestMock","clearCollections","seedCollection","mockNewsletterSettings","mockReq","mockConfig","result","payload","find","collection","overrideAccess","user","docs","toHaveLength","id","toBe","settings","resendSettings","apiKey","create","data","name","active","provider","rejects","toThrow","update","adminUser","delete","syntheticUser","email","beforeChangeHook","fn","req","operation","roles","includes","Error","not","findByID","toBeDefined","invalidApiKeys","invalidKey","secondSettings","audienceIds","from","allSettings"],"mappings":"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"}