@actuate-media/cms-core 0.11.2 → 0.13.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.
Files changed (115) hide show
  1. package/dist/__tests__/api/cron-routes.test.d.ts +2 -0
  2. package/dist/__tests__/api/cron-routes.test.d.ts.map +1 -0
  3. package/dist/__tests__/api/cron-routes.test.js +67 -0
  4. package/dist/__tests__/api/cron-routes.test.js.map +1 -0
  5. package/dist/__tests__/api/health.test.d.ts +2 -0
  6. package/dist/__tests__/api/health.test.d.ts.map +1 -0
  7. package/dist/__tests__/api/health.test.js +140 -0
  8. package/dist/__tests__/api/health.test.js.map +1 -0
  9. package/dist/__tests__/auth/oauth.test.d.ts +2 -0
  10. package/dist/__tests__/auth/oauth.test.d.ts.map +1 -0
  11. package/dist/__tests__/auth/oauth.test.js +406 -0
  12. package/dist/__tests__/auth/oauth.test.js.map +1 -0
  13. package/dist/__tests__/auth/password.test.js +82 -3
  14. package/dist/__tests__/auth/password.test.js.map +1 -1
  15. package/dist/__tests__/auth/reset.test.d.ts +2 -0
  16. package/dist/__tests__/auth/reset.test.d.ts.map +1 -0
  17. package/dist/__tests__/auth/reset.test.js +303 -0
  18. package/dist/__tests__/auth/reset.test.js.map +1 -0
  19. package/dist/__tests__/auth/session.test.js +54 -1
  20. package/dist/__tests__/auth/session.test.js.map +1 -1
  21. package/dist/__tests__/cron/cron.test.d.ts +2 -0
  22. package/dist/__tests__/cron/cron.test.d.ts.map +1 -0
  23. package/dist/__tests__/cron/cron.test.js +262 -0
  24. package/dist/__tests__/cron/cron.test.js.map +1 -0
  25. package/dist/__tests__/diagnostics/env.test.d.ts +2 -0
  26. package/dist/__tests__/diagnostics/env.test.d.ts.map +1 -0
  27. package/dist/__tests__/diagnostics/env.test.js +119 -0
  28. package/dist/__tests__/diagnostics/env.test.js.map +1 -0
  29. package/dist/__tests__/diagnostics/logger.test.d.ts +2 -0
  30. package/dist/__tests__/diagnostics/logger.test.d.ts.map +1 -0
  31. package/dist/__tests__/diagnostics/logger.test.js +111 -0
  32. package/dist/__tests__/diagnostics/logger.test.js.map +1 -0
  33. package/dist/__tests__/security/encrypted-fields.test.d.ts +2 -0
  34. package/dist/__tests__/security/encrypted-fields.test.d.ts.map +1 -0
  35. package/dist/__tests__/security/encrypted-fields.test.js +60 -0
  36. package/dist/__tests__/security/encrypted-fields.test.js.map +1 -0
  37. package/dist/__tests__/security/rate-limit.test.js +42 -0
  38. package/dist/__tests__/security/rate-limit.test.js.map +1 -1
  39. package/dist/__tests__/security/safe-fetch.test.d.ts +2 -0
  40. package/dist/__tests__/security/safe-fetch.test.d.ts.map +1 -0
  41. package/dist/__tests__/security/safe-fetch.test.js +97 -0
  42. package/dist/__tests__/security/safe-fetch.test.js.map +1 -0
  43. package/dist/__tests__/security/ssrf.test.d.ts +2 -0
  44. package/dist/__tests__/security/ssrf.test.d.ts.map +1 -0
  45. package/dist/__tests__/security/ssrf.test.js +209 -0
  46. package/dist/__tests__/security/ssrf.test.js.map +1 -0
  47. package/dist/actions.d.ts.map +1 -1
  48. package/dist/actions.js +7 -6
  49. package/dist/actions.js.map +1 -1
  50. package/dist/api/handler-factory.d.ts.map +1 -1
  51. package/dist/api/handler-factory.js +15 -6
  52. package/dist/api/handler-factory.js.map +1 -1
  53. package/dist/api/handlers.d.ts.map +1 -1
  54. package/dist/api/handlers.js +165 -26
  55. package/dist/api/handlers.js.map +1 -1
  56. package/dist/auth/oauth.d.ts +8 -0
  57. package/dist/auth/oauth.d.ts.map +1 -1
  58. package/dist/auth/oauth.js +44 -2
  59. package/dist/auth/oauth.js.map +1 -1
  60. package/dist/auth/password.d.ts +35 -2
  61. package/dist/auth/password.d.ts.map +1 -1
  62. package/dist/auth/password.js +97 -7
  63. package/dist/auth/password.js.map +1 -1
  64. package/dist/auth/reset.d.ts.map +1 -1
  65. package/dist/auth/reset.js +2 -1
  66. package/dist/auth/reset.js.map +1 -1
  67. package/dist/auth/session.d.ts +9 -0
  68. package/dist/auth/session.d.ts.map +1 -1
  69. package/dist/auth/session.js +54 -1
  70. package/dist/auth/session.js.map +1 -1
  71. package/dist/config/runtime.d.ts +99 -0
  72. package/dist/config/runtime.d.ts.map +1 -0
  73. package/dist/config/runtime.js +43 -0
  74. package/dist/config/runtime.js.map +1 -0
  75. package/dist/config/types.d.ts +21 -0
  76. package/dist/config/types.d.ts.map +1 -1
  77. package/dist/cron/index.d.ts +72 -0
  78. package/dist/cron/index.d.ts.map +1 -0
  79. package/dist/cron/index.js +222 -0
  80. package/dist/cron/index.js.map +1 -0
  81. package/dist/diagnostics/env.d.ts +44 -0
  82. package/dist/diagnostics/env.d.ts.map +1 -0
  83. package/dist/diagnostics/env.js +293 -0
  84. package/dist/diagnostics/env.js.map +1 -0
  85. package/dist/diagnostics/logger.d.ts +38 -0
  86. package/dist/diagnostics/logger.d.ts.map +1 -0
  87. package/dist/diagnostics/logger.js +89 -0
  88. package/dist/diagnostics/logger.js.map +1 -0
  89. package/dist/page-builder/blocks.d.ts.map +1 -1
  90. package/dist/page-builder/blocks.js +6 -1
  91. package/dist/page-builder/blocks.js.map +1 -1
  92. package/dist/security/audit.d.ts.map +1 -1
  93. package/dist/security/audit.js +3 -1
  94. package/dist/security/audit.js.map +1 -1
  95. package/dist/security/encrypted-fields.d.ts +9 -0
  96. package/dist/security/encrypted-fields.d.ts.map +1 -1
  97. package/dist/security/encrypted-fields.js +52 -1
  98. package/dist/security/encrypted-fields.js.map +1 -1
  99. package/dist/security/ip-canon.d.ts +71 -0
  100. package/dist/security/ip-canon.d.ts.map +1 -0
  101. package/dist/security/ip-canon.js +352 -0
  102. package/dist/security/ip-canon.js.map +1 -0
  103. package/dist/security/rate-limit.d.ts +8 -0
  104. package/dist/security/rate-limit.d.ts.map +1 -1
  105. package/dist/security/rate-limit.js +81 -3
  106. package/dist/security/rate-limit.js.map +1 -1
  107. package/dist/security/safe-fetch.d.ts +30 -8
  108. package/dist/security/safe-fetch.d.ts.map +1 -1
  109. package/dist/security/safe-fetch.js +32 -6
  110. package/dist/security/safe-fetch.js.map +1 -1
  111. package/dist/security/webhook.d.ts +20 -2
  112. package/dist/security/webhook.d.ts.map +1 -1
  113. package/dist/security/webhook.js +100 -30
  114. package/dist/security/webhook.js.map +1 -1
  115. package/package.json +1 -1
@@ -0,0 +1,262 @@
1
+ import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
2
+ import { isAuthorizedCronRequest, processCleanup, processSeoScan } from '../../cron/index.js';
3
+ describe('cron auth', () => {
4
+ const originalSecret = process.env.CRON_SECRET;
5
+ afterEach(() => {
6
+ if (originalSecret === undefined)
7
+ delete process.env.CRON_SECRET;
8
+ else
9
+ process.env.CRON_SECRET = originalSecret;
10
+ });
11
+ it('rejects when CRON_SECRET is unset (fail-closed)', () => {
12
+ delete process.env.CRON_SECRET;
13
+ expect(isAuthorizedCronRequest('Bearer anything')).toBe(false);
14
+ });
15
+ it('rejects when CRON_SECRET is the empty string (fail-closed)', () => {
16
+ process.env.CRON_SECRET = '';
17
+ expect(isAuthorizedCronRequest('Bearer anything')).toBe(false);
18
+ });
19
+ it('rejects null/undefined header', () => {
20
+ process.env.CRON_SECRET = 'abc123';
21
+ expect(isAuthorizedCronRequest(null)).toBe(false);
22
+ expect(isAuthorizedCronRequest(undefined)).toBe(false);
23
+ });
24
+ it('rejects wrong secret', () => {
25
+ process.env.CRON_SECRET = 'correct-secret';
26
+ expect(isAuthorizedCronRequest('Bearer wrong-secret')).toBe(false);
27
+ });
28
+ it('rejects partially-correct secret of equal length (constant-time)', () => {
29
+ process.env.CRON_SECRET = 'secret-with-fixed-length';
30
+ expect(isAuthorizedCronRequest('Bearer secret-with-fixex-length')).toBe(false);
31
+ });
32
+ it('accepts correct Bearer token', () => {
33
+ process.env.CRON_SECRET = 'correct-secret-value';
34
+ expect(isAuthorizedCronRequest('Bearer correct-secret-value')).toBe(true);
35
+ });
36
+ it('accepts bare secret (for self-hosted schedulers without Bearer prefix)', () => {
37
+ process.env.CRON_SECRET = 'correct-secret-value';
38
+ expect(isAuthorizedCronRequest('correct-secret-value')).toBe(true);
39
+ });
40
+ // Bugbot review #6 (PR #40): the prior `if (a.length !== b.length) return
41
+ // false` early exit leaked the secret length through response timing.
42
+ // The HMAC-based comparison must reject mismatched lengths just as
43
+ // reliably without revealing length information through different code
44
+ // paths.
45
+ it.each([
46
+ ['shorter than secret', 'short'],
47
+ ['longer than secret', 'a-much-longer-string-than-the-secret-value-itself'],
48
+ ['empty', ''],
49
+ ['one char short', 'correct-secret-valu'],
50
+ ['one char long', 'correct-secret-value-x'],
51
+ ])('rejects header %s when length differs from secret', (_label, attempt) => {
52
+ process.env.CRON_SECRET = 'correct-secret-value';
53
+ expect(isAuthorizedCronRequest(`Bearer ${attempt}`)).toBe(false);
54
+ });
55
+ });
56
+ describe('processCleanup', () => {
57
+ let now;
58
+ beforeEach(() => {
59
+ now = Date.now();
60
+ vi.useFakeTimers();
61
+ vi.setSystemTime(now);
62
+ });
63
+ afterEach(() => {
64
+ vi.useRealTimers();
65
+ });
66
+ it('returns zeros when db is empty / has no models', async () => {
67
+ const result = await processCleanup({});
68
+ expect(result).toEqual({
69
+ sessionsDeleted: 0,
70
+ auditLogsDeleted: 0,
71
+ documentsDeleted: 0,
72
+ passwordResetTokensDeleted: 0,
73
+ });
74
+ });
75
+ it('deletes expired/revoked sessions older than the retention window', async () => {
76
+ const deleteMany = vi.fn().mockResolvedValue({ count: 3 });
77
+ const db = { session: { deleteMany } };
78
+ const result = await processCleanup(db, { sessionRetentionMs: 1000 });
79
+ expect(result.sessionsDeleted).toBe(3);
80
+ expect(deleteMany).toHaveBeenCalledTimes(1);
81
+ const where = deleteMany.mock.calls[0][0].where;
82
+ expect(where.OR[0].revokedAt.lt.getTime()).toBe(now - 1000);
83
+ expect(where.OR[1].expiresAt.lt.getTime()).toBe(now - 1000);
84
+ });
85
+ it('continues other cleanups when one model fails', async () => {
86
+ const db = {
87
+ session: { deleteMany: vi.fn().mockRejectedValue(new Error('boom')) },
88
+ auditLog: { deleteMany: vi.fn().mockResolvedValue({ count: 5 }) },
89
+ document: { deleteMany: vi.fn().mockResolvedValue({ count: 2 }) },
90
+ };
91
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => { });
92
+ const result = await processCleanup(db);
93
+ expect(result.sessionsDeleted).toBe(0);
94
+ expect(result.auditLogsDeleted).toBe(5);
95
+ expect(result.documentsDeleted).toBe(2);
96
+ expect(warn).toHaveBeenCalled();
97
+ warn.mockRestore();
98
+ });
99
+ it('uses default retention windows when none provided', async () => {
100
+ const deleteMany = vi.fn().mockResolvedValue({ count: 0 });
101
+ const db = { auditLog: { deleteMany } };
102
+ await processCleanup(db);
103
+ const cutoff = deleteMany.mock.calls[0][0].where.timestamp.lt.getTime();
104
+ // Default audit log retention is 90 days.
105
+ expect(cutoff).toBe(now - 90 * 24 * 60 * 60 * 1000);
106
+ });
107
+ // Bugbot review (PR #40, post-fix re-scan): `modelExists` used
108
+ // `typeof db[name] === 'object'` which is true for `null` because of the
109
+ // historical `typeof null === 'object'` quirk. With `{ session: null }` the
110
+ // guard returned true and the subsequent `db.session.deleteMany(...)` call
111
+ // threw a TypeError that the outer try/catch papered over.
112
+ it('treats explicitly-null model delegates as missing (regression)', async () => {
113
+ const db = {
114
+ session: null,
115
+ auditLog: null,
116
+ document: null,
117
+ passwordResetToken: null,
118
+ };
119
+ const result = await processCleanup(db);
120
+ expect(result).toEqual({
121
+ sessionsDeleted: 0,
122
+ auditLogsDeleted: 0,
123
+ documentsDeleted: 0,
124
+ passwordResetTokensDeleted: 0,
125
+ });
126
+ });
127
+ });
128
+ describe('processSeoScan', () => {
129
+ it('returns empty result when document model is missing', async () => {
130
+ const result = await processSeoScan({});
131
+ expect(result).toEqual({ total: 0, pagesWithIssues: 0, totalProblems: 0, issues: [] });
132
+ });
133
+ it('flags missing meta title, description, canonical, schema', async () => {
134
+ const findMany = vi.fn().mockResolvedValue([
135
+ {
136
+ id: 'doc1',
137
+ title: 'Hello',
138
+ slug: 'hello',
139
+ collection: 'pages',
140
+ data: {},
141
+ plainText: 'x'.repeat(500),
142
+ },
143
+ ]);
144
+ const db = { document: { findMany } };
145
+ const result = await processSeoScan(db);
146
+ expect(result.total).toBe(1);
147
+ expect(result.pagesWithIssues).toBe(1);
148
+ const problems = result.issues[0].problems;
149
+ expect(problems).toContain('Missing meta title');
150
+ expect(problems).toContain('Missing meta description');
151
+ expect(problems).toContain('No canonical URL set');
152
+ expect(problems).toContain('No Schema.org type');
153
+ });
154
+ it('respects maxDocuments bound', async () => {
155
+ const findMany = vi.fn().mockResolvedValue([]);
156
+ const db = { document: { findMany } };
157
+ await processSeoScan(db, { maxDocuments: 100 });
158
+ expect(findMany.mock.calls[0][0].take).toBe(100);
159
+ });
160
+ it('does not flag well-formed documents', async () => {
161
+ const db = {
162
+ document: {
163
+ findMany: vi.fn().mockResolvedValue([
164
+ {
165
+ id: 'doc1',
166
+ title: 'Hello',
167
+ slug: 'hello',
168
+ collection: 'pages',
169
+ data: {
170
+ metaTitle: 'Hello',
171
+ metaDescription: 'A page',
172
+ canonical: 'https://example.com/hello',
173
+ schemaType: 'Article',
174
+ body: '<h1>Hello</h1><img alt="ok" src="x">'.padEnd(400, ' '),
175
+ },
176
+ plainText: 'x'.repeat(500),
177
+ },
178
+ ]),
179
+ },
180
+ };
181
+ const result = await processSeoScan(db);
182
+ expect(result.pagesWithIssues).toBe(0);
183
+ expect(result.totalProblems).toBe(0);
184
+ });
185
+ // Bugbot review (PR #40, post-fix re-scan): the `<h1>` heading detection
186
+ // used `content.includes('<h1')` which is case-sensitive, so documents with
187
+ // valid uppercase `<H1>` headings (or `<H1 class="...">`) were falsely
188
+ // flagged as missing. Now matches the case-insensitive `<img>` detection
189
+ // for consistency with the HTML spec.
190
+ it.each([
191
+ ['lowercase <h1>', '<h1>Hello</h1>'],
192
+ ['uppercase <H1>', '<H1>Hello</H1>'],
193
+ ['mixed-case <H1 class>', '<H1 class="hero">Hello</H1>'],
194
+ ['attribute on lowercase', '<h1 id="x">Hello</h1>'],
195
+ ])('does not flag %s as missing H1 (regression)', async (_label, h1Markup) => {
196
+ const db = {
197
+ document: {
198
+ findMany: vi.fn().mockResolvedValue([
199
+ {
200
+ id: 'doc1',
201
+ title: 'Hello',
202
+ slug: 'hello',
203
+ collection: 'pages',
204
+ data: {
205
+ metaTitle: 'Hello',
206
+ metaDescription: 'A page',
207
+ canonical: 'https://example.com/hello',
208
+ schemaType: 'Article',
209
+ body: `${h1Markup}<img alt="ok" src="x">`.padEnd(400, ' '),
210
+ },
211
+ plainText: 'x'.repeat(500),
212
+ },
213
+ ]),
214
+ },
215
+ };
216
+ const result = await processSeoScan(db);
217
+ expect(result.issues[0]?.problems ?? []).not.toContain('No H1 heading found in content');
218
+ });
219
+ // Bugbot review (PR #40, post-fix re-scan #2): the `<img>` regex was
220
+ // case-insensitive (`/gi`) but the `alt=` substring check inside the filter
221
+ // was case-sensitive (`img.includes('alt=')`), so `<IMG ALT="text">` matched
222
+ // the regex and was then falsely counted as missing alt text. Same class of
223
+ // bug as the H1 case-sensitivity fix above.
224
+ it.each([
225
+ ['lowercase img + lowercase alt', '<img src="x" alt="ok">', 0],
226
+ ['uppercase IMG + uppercase ALT', '<IMG SRC="x" ALT="ok">', 0],
227
+ ['mixed-case Img + mixed-case Alt', '<Img src="x" Alt="ok">', 0],
228
+ ['lowercase img with no alt is flagged', '<img src="x">', 1],
229
+ ['uppercase IMG with no alt is flagged', '<IMG SRC="x">', 1],
230
+ ])('alt-text scan is case-insensitive — %s', async (_label, imgMarkup, expectedMissing) => {
231
+ const db = {
232
+ document: {
233
+ findMany: vi.fn().mockResolvedValue([
234
+ {
235
+ id: 'doc1',
236
+ title: 'Hello',
237
+ slug: 'hello',
238
+ collection: 'pages',
239
+ data: {
240
+ metaTitle: 'Hello',
241
+ metaDescription: 'A page',
242
+ canonical: 'https://example.com/hello',
243
+ schemaType: 'Article',
244
+ body: `<h1>Hello</h1>${imgMarkup}`.padEnd(400, ' '),
245
+ },
246
+ plainText: 'x'.repeat(500),
247
+ },
248
+ ]),
249
+ },
250
+ };
251
+ const result = await processSeoScan(db);
252
+ const problems = result.issues[0]?.problems ?? [];
253
+ const altMessages = problems.filter((p) => p.endsWith('image(s) missing alt text'));
254
+ if (expectedMissing === 0) {
255
+ expect(altMessages).toHaveLength(0);
256
+ }
257
+ else {
258
+ expect(altMessages).toContain(`${expectedMissing} image(s) missing alt text`);
259
+ }
260
+ });
261
+ });
262
+ //# sourceMappingURL=cron.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron.test.js","sourceRoot":"","sources":["../../../src/__tests__/cron/cron.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=env.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/diagnostics/env.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,119 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { validateEnvShape } from '../../diagnostics/env.js';
3
+ function envFrom(values) {
4
+ return { get: (name) => values[name] };
5
+ }
6
+ describe('validateEnvShape', () => {
7
+ it('returns ok=false when CMS_SECRET is missing', () => {
8
+ const r = validateEnvShape(envFrom({ DATABASE_URL: 'postgres://x' }));
9
+ expect(r.ok).toBe(false);
10
+ const secret = r.checks.find((c) => c.name === 'CMS_SECRET');
11
+ expect(secret?.status).toBe('missing');
12
+ });
13
+ it('flags a CMS_SECRET shorter than 32 chars as an error', () => {
14
+ const r = validateEnvShape(envFrom({
15
+ CMS_SECRET: 'too-short',
16
+ DATABASE_URL: 'postgres://x',
17
+ }));
18
+ expect(r.ok).toBe(false);
19
+ expect(r.checks.find((c) => c.name === 'CMS_SECRET')?.status).toBe('error');
20
+ });
21
+ it('flags placeholder-looking CMS_SECRET values', () => {
22
+ const r = validateEnvShape(envFrom({
23
+ CMS_SECRET: 'change-me-in-prod-this-is-a-placeholder-string-xx',
24
+ DATABASE_URL: 'postgres://x',
25
+ }));
26
+ expect(r.checks.find((c) => c.name === 'CMS_SECRET')?.status).toBe('error');
27
+ });
28
+ it('flags a CMS_ENCRYPTION_KEY that is not 64 hex chars', () => {
29
+ const r = validateEnvShape(envFrom({
30
+ CMS_SECRET: 'a'.repeat(40),
31
+ DATABASE_URL: 'postgres://x',
32
+ CMS_ENCRYPTION_KEY: 'aes256-local-dev-key-change-in-prod',
33
+ }));
34
+ expect(r.ok).toBe(false);
35
+ const enc = r.checks.find((c) => c.name === 'CMS_ENCRYPTION_KEY');
36
+ expect(enc?.status).toBe('error');
37
+ });
38
+ it('accepts a valid 64-hex-char CMS_ENCRYPTION_KEY', () => {
39
+ const r = validateEnvShape(envFrom({
40
+ CMS_SECRET: 'a'.repeat(40),
41
+ DATABASE_URL: 'postgres://x',
42
+ CMS_ENCRYPTION_KEY: 'a1b2'.repeat(16), // 64 hex chars
43
+ }));
44
+ expect(r.checks.find((c) => c.name === 'CMS_ENCRYPTION_KEY')?.status).toBe('ok');
45
+ });
46
+ it('flags non-hex chars in the encryption key', () => {
47
+ const r = validateEnvShape(envFrom({
48
+ CMS_SECRET: 'a'.repeat(40),
49
+ DATABASE_URL: 'postgres://x',
50
+ CMS_ENCRYPTION_KEY: 'z'.repeat(64), // wrong alphabet
51
+ }));
52
+ expect(r.checks.find((c) => c.name === 'CMS_ENCRYPTION_KEY')?.status).toBe('error');
53
+ });
54
+ it('flags an invalid Upstash URL when only one of URL/token is set', () => {
55
+ const r = validateEnvShape(envFrom({
56
+ CMS_SECRET: 'a'.repeat(40),
57
+ DATABASE_URL: 'postgres://x',
58
+ UPSTASH_REDIS_REST_URL: 'https://r.upstash.io',
59
+ // no token
60
+ }));
61
+ expect(r.checks.find((c) => c.name === 'UPSTASH_REDIS_REST_TOKEN')?.status).toBe('error');
62
+ });
63
+ it('flags a CRON_SECRET that is too short', () => {
64
+ const r = validateEnvShape(envFrom({
65
+ CMS_SECRET: 'a'.repeat(40),
66
+ DATABASE_URL: 'postgres://x',
67
+ CRON_SECRET: 'short',
68
+ }));
69
+ expect(r.checks.find((c) => c.name === 'CRON_SECRET')?.status).toBe('error');
70
+ });
71
+ it('returns ok=true when every required var is well-formed', () => {
72
+ const r = validateEnvShape(envFrom({
73
+ CMS_SECRET: 'a'.repeat(40),
74
+ DATABASE_URL: 'postgres://user:pass@host:5432/db',
75
+ CMS_ENCRYPTION_KEY: 'a1b2'.repeat(16),
76
+ CRON_SECRET: 'a-real-cron-secret-of-sufficient-length',
77
+ BLOB_READ_WRITE_TOKEN: 'vercel_blob_rw_xxx',
78
+ RESEND_API_KEY: 're_xxx',
79
+ }));
80
+ expect(r.ok).toBe(true);
81
+ expect(r.errorCount).toBe(0);
82
+ });
83
+ // Bugbot review (PR #41): the unauthenticated /health endpoint surfaces
84
+ // these messages, and the previous "ok" wording (`Configured (42 chars).`)
85
+ // leaked exact secret lengths to anyone who could reach the URL — narrowing
86
+ // brute-force search space with no operational benefit. Lengths must not
87
+ // appear in any "ok" message.
88
+ it.each([
89
+ [
90
+ 'CMS_SECRET',
91
+ {
92
+ CMS_SECRET: 'a'.repeat(42),
93
+ DATABASE_URL: 'postgres://x',
94
+ },
95
+ ],
96
+ [
97
+ 'CMS_ENCRYPTION_KEY',
98
+ {
99
+ CMS_SECRET: 'a'.repeat(40),
100
+ DATABASE_URL: 'postgres://x',
101
+ CMS_ENCRYPTION_KEY: 'a1b2'.repeat(16),
102
+ },
103
+ ],
104
+ [
105
+ 'CRON_SECRET',
106
+ {
107
+ CMS_SECRET: 'a'.repeat(40),
108
+ DATABASE_URL: 'postgres://x',
109
+ CRON_SECRET: 'a-real-cron-secret-of-sufficient-length',
110
+ },
111
+ ],
112
+ ])('does not include exact length in the "ok" message for %s', (name, env) => {
113
+ const r = validateEnvShape(envFrom(env));
114
+ const check = r.checks.find((c) => c.name === name);
115
+ expect(check?.status).toBe('ok');
116
+ expect(check?.message ?? '').not.toMatch(/\d+\s*chars?/i);
117
+ });
118
+ });
119
+ //# sourceMappingURL=env.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.test.js","sourceRoot":"","sources":["../../../src/__tests__/diagnostics/env.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=logger.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/diagnostics/logger.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,111 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { createLogger } from '../../diagnostics/logger.js';
3
+ describe('createLogger', () => {
4
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
5
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
6
+ const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => { });
7
+ const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => { });
8
+ const originalLevel = process.env.ACTUATE_LOG_LEVEL;
9
+ const originalFormat = process.env.ACTUATE_LOG_FORMAT;
10
+ const originalNodeEnv = process.env.NODE_ENV;
11
+ beforeEach(() => {
12
+ errorSpy.mockClear();
13
+ warnSpy.mockClear();
14
+ infoSpy.mockClear();
15
+ debugSpy.mockClear();
16
+ });
17
+ afterEach(() => {
18
+ if (originalLevel === undefined)
19
+ delete process.env.ACTUATE_LOG_LEVEL;
20
+ else
21
+ process.env.ACTUATE_LOG_LEVEL = originalLevel;
22
+ if (originalFormat === undefined)
23
+ delete process.env.ACTUATE_LOG_FORMAT;
24
+ else
25
+ process.env.ACTUATE_LOG_FORMAT = originalFormat;
26
+ if (originalNodeEnv === undefined)
27
+ delete process.env.NODE_ENV;
28
+ else
29
+ process.env.NODE_ENV = originalNodeEnv;
30
+ });
31
+ it('emits all levels by default in non-production', () => {
32
+ delete process.env.NODE_ENV;
33
+ delete process.env.ACTUATE_LOG_LEVEL;
34
+ const log = createLogger('test');
35
+ log.error('e');
36
+ log.warn('w');
37
+ log.info('i');
38
+ log.debug('d');
39
+ expect(errorSpy).toHaveBeenCalled();
40
+ expect(warnSpy).toHaveBeenCalled();
41
+ expect(infoSpy).toHaveBeenCalled();
42
+ // info-level default does NOT include debug
43
+ expect(debugSpy).not.toHaveBeenCalled();
44
+ });
45
+ it('silences info+debug when level=warn', () => {
46
+ process.env.ACTUATE_LOG_LEVEL = 'warn';
47
+ const log = createLogger('test');
48
+ log.error('e');
49
+ log.warn('w');
50
+ log.info('i');
51
+ log.debug('d');
52
+ expect(errorSpy).toHaveBeenCalled();
53
+ expect(warnSpy).toHaveBeenCalled();
54
+ expect(infoSpy).not.toHaveBeenCalled();
55
+ expect(debugSpy).not.toHaveBeenCalled();
56
+ });
57
+ it('only emits errors when level=error', () => {
58
+ process.env.ACTUATE_LOG_LEVEL = 'error';
59
+ const log = createLogger('test');
60
+ log.error('e');
61
+ log.warn('w');
62
+ log.info('i');
63
+ expect(errorSpy).toHaveBeenCalled();
64
+ expect(warnSpy).not.toHaveBeenCalled();
65
+ expect(infoSpy).not.toHaveBeenCalled();
66
+ });
67
+ it('emits nothing when level=silent', () => {
68
+ process.env.ACTUATE_LOG_LEVEL = 'silent';
69
+ const log = createLogger('test');
70
+ log.error('e');
71
+ log.warn('w');
72
+ expect(errorSpy).not.toHaveBeenCalled();
73
+ expect(warnSpy).not.toHaveBeenCalled();
74
+ });
75
+ it('defaults to warn in NODE_ENV=production', () => {
76
+ process.env.NODE_ENV = 'production';
77
+ delete process.env.ACTUATE_LOG_LEVEL;
78
+ const log = createLogger('test');
79
+ log.error('e');
80
+ log.warn('w');
81
+ log.info('i');
82
+ expect(errorSpy).toHaveBeenCalled();
83
+ expect(warnSpy).toHaveBeenCalled();
84
+ expect(infoSpy).not.toHaveBeenCalled();
85
+ });
86
+ it('emits one-line JSON when format=json', () => {
87
+ process.env.ACTUATE_LOG_LEVEL = 'info';
88
+ process.env.ACTUATE_LOG_FORMAT = 'json';
89
+ const log = createLogger('rate-limit');
90
+ log.error('boom', { reason: 'upstream-down', retry: 3 });
91
+ expect(errorSpy).toHaveBeenCalledOnce();
92
+ const line = errorSpy.mock.calls[0][0];
93
+ expect(typeof line).toBe('string');
94
+ const parsed = JSON.parse(line);
95
+ expect(parsed.level).toBe('error');
96
+ expect(parsed.scope).toBe('rate-limit');
97
+ expect(parsed.msg).toBe('boom');
98
+ expect(parsed.details).toEqual({ reason: 'upstream-down', retry: 3 });
99
+ expect(typeof parsed.ts).toBe('string');
100
+ });
101
+ it('uses [actuate][scope] prefix in text mode', () => {
102
+ process.env.ACTUATE_LOG_LEVEL = 'info';
103
+ delete process.env.ACTUATE_LOG_FORMAT;
104
+ const log = createLogger('rate-limit');
105
+ log.warn('hi');
106
+ expect(warnSpy).toHaveBeenCalledOnce();
107
+ expect(warnSpy.mock.calls[0][0]).toBe('[actuate][rate-limit]');
108
+ expect(warnSpy.mock.calls[0][1]).toBe('hi');
109
+ });
110
+ });
111
+ //# sourceMappingURL=logger.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.test.js","sourceRoot":"","sources":["../../../src/__tests__/diagnostics/logger.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=encrypted-fields.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encrypted-fields.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/security/encrypted-fields.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { encryptField, decryptField, InvalidEncryptionKeyError, } from '../../security/encrypted-fields.js';
3
+ const VALID_KEY = 'a'.repeat(64); // 64 hex chars = 32 bytes
4
+ const ALT_KEY = 'b'.repeat(64);
5
+ describe('encrypted-fields key validation', () => {
6
+ it('rejects empty key', async () => {
7
+ await expect(encryptField('hello', '')).rejects.toBeInstanceOf(InvalidEncryptionKeyError);
8
+ });
9
+ it('rejects undefined / non-string key', async () => {
10
+ await expect(encryptField('hello', undefined)).rejects.toBeInstanceOf(InvalidEncryptionKeyError);
11
+ });
12
+ it('rejects too-short key', async () => {
13
+ await expect(encryptField('hello', 'a'.repeat(32))).rejects.toBeInstanceOf(InvalidEncryptionKeyError);
14
+ });
15
+ it('rejects too-long key', async () => {
16
+ await expect(encryptField('hello', 'a'.repeat(128))).rejects.toBeInstanceOf(InvalidEncryptionKeyError);
17
+ });
18
+ it('rejects non-hex characters', async () => {
19
+ await expect(encryptField('hello', 'z'.repeat(63) + 'a')).rejects.toBeInstanceOf(InvalidEncryptionKeyError);
20
+ });
21
+ it('rejects the placeholder dev key from the .env example', async () => {
22
+ // "aes256-local-dev-key-change-in-prod" is 35 chars, not 64, and contains '-'.
23
+ await expect(encryptField('hello', 'aes256-local-dev-key-change-in-prod')).rejects.toBeInstanceOf(InvalidEncryptionKeyError);
24
+ });
25
+ it('error message points operators at the random-bytes generator', async () => {
26
+ try {
27
+ await encryptField('hello', 'short');
28
+ throw new Error('should have thrown');
29
+ }
30
+ catch (err) {
31
+ expect(err).toBeInstanceOf(InvalidEncryptionKeyError);
32
+ expect(err.message).toContain('randomBytes(32)');
33
+ }
34
+ });
35
+ });
36
+ describe('encrypted-fields round-trip', () => {
37
+ it('encrypts and decrypts with the same key', async () => {
38
+ const plaintext = 'hello, world — with unicode ✓';
39
+ const encrypted = await encryptField(plaintext, VALID_KEY);
40
+ expect(encrypted).not.toBe(plaintext);
41
+ const decrypted = await decryptField(encrypted, VALID_KEY);
42
+ expect(decrypted).toBe(plaintext);
43
+ });
44
+ it('produces different ciphertext on every call (unique IV)', async () => {
45
+ const a = await encryptField('same input', VALID_KEY);
46
+ const b = await encryptField('same input', VALID_KEY);
47
+ expect(a).not.toBe(b);
48
+ });
49
+ it('rejects ciphertext encrypted with a different key', async () => {
50
+ const encrypted = await encryptField('secret', VALID_KEY);
51
+ await expect(decryptField(encrypted, ALT_KEY)).rejects.toThrow();
52
+ });
53
+ it('rejects tampered ciphertext (AES-GCM auth tag enforces integrity)', async () => {
54
+ const encrypted = await encryptField('secret', VALID_KEY);
55
+ // Flip one byte at the end (inside the auth tag region)
56
+ const tampered = encrypted.slice(0, -2) + (encrypted.slice(-2) === 'ff' ? '00' : 'ff');
57
+ await expect(decryptField(tampered, VALID_KEY)).rejects.toThrow();
58
+ });
59
+ });
60
+ //# sourceMappingURL=encrypted-fields.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encrypted-fields.test.js","sourceRoot":"","sources":["../../../src/__tests__/security/encrypted-fields.test.ts"],"names":[],"mappings":"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"}