@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.
- package/dist/__tests__/api/cron-routes.test.d.ts +2 -0
- package/dist/__tests__/api/cron-routes.test.d.ts.map +1 -0
- package/dist/__tests__/api/cron-routes.test.js +67 -0
- package/dist/__tests__/api/cron-routes.test.js.map +1 -0
- package/dist/__tests__/api/health.test.d.ts +2 -0
- package/dist/__tests__/api/health.test.d.ts.map +1 -0
- package/dist/__tests__/api/health.test.js +140 -0
- package/dist/__tests__/api/health.test.js.map +1 -0
- package/dist/__tests__/auth/oauth.test.d.ts +2 -0
- package/dist/__tests__/auth/oauth.test.d.ts.map +1 -0
- package/dist/__tests__/auth/oauth.test.js +406 -0
- package/dist/__tests__/auth/oauth.test.js.map +1 -0
- package/dist/__tests__/auth/password.test.js +82 -3
- package/dist/__tests__/auth/password.test.js.map +1 -1
- package/dist/__tests__/auth/reset.test.d.ts +2 -0
- package/dist/__tests__/auth/reset.test.d.ts.map +1 -0
- package/dist/__tests__/auth/reset.test.js +303 -0
- package/dist/__tests__/auth/reset.test.js.map +1 -0
- package/dist/__tests__/auth/session.test.js +54 -1
- package/dist/__tests__/auth/session.test.js.map +1 -1
- package/dist/__tests__/cron/cron.test.d.ts +2 -0
- package/dist/__tests__/cron/cron.test.d.ts.map +1 -0
- package/dist/__tests__/cron/cron.test.js +262 -0
- package/dist/__tests__/cron/cron.test.js.map +1 -0
- package/dist/__tests__/diagnostics/env.test.d.ts +2 -0
- package/dist/__tests__/diagnostics/env.test.d.ts.map +1 -0
- package/dist/__tests__/diagnostics/env.test.js +119 -0
- package/dist/__tests__/diagnostics/env.test.js.map +1 -0
- package/dist/__tests__/diagnostics/logger.test.d.ts +2 -0
- package/dist/__tests__/diagnostics/logger.test.d.ts.map +1 -0
- package/dist/__tests__/diagnostics/logger.test.js +111 -0
- package/dist/__tests__/diagnostics/logger.test.js.map +1 -0
- package/dist/__tests__/security/encrypted-fields.test.d.ts +2 -0
- package/dist/__tests__/security/encrypted-fields.test.d.ts.map +1 -0
- package/dist/__tests__/security/encrypted-fields.test.js +60 -0
- package/dist/__tests__/security/encrypted-fields.test.js.map +1 -0
- package/dist/__tests__/security/rate-limit.test.js +42 -0
- package/dist/__tests__/security/rate-limit.test.js.map +1 -1
- package/dist/__tests__/security/safe-fetch.test.d.ts +2 -0
- package/dist/__tests__/security/safe-fetch.test.d.ts.map +1 -0
- package/dist/__tests__/security/safe-fetch.test.js +97 -0
- package/dist/__tests__/security/safe-fetch.test.js.map +1 -0
- package/dist/__tests__/security/ssrf.test.d.ts +2 -0
- package/dist/__tests__/security/ssrf.test.d.ts.map +1 -0
- package/dist/__tests__/security/ssrf.test.js +209 -0
- package/dist/__tests__/security/ssrf.test.js.map +1 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +7 -6
- package/dist/actions.js.map +1 -1
- package/dist/api/handler-factory.d.ts.map +1 -1
- package/dist/api/handler-factory.js +15 -6
- package/dist/api/handler-factory.js.map +1 -1
- package/dist/api/handlers.d.ts.map +1 -1
- package/dist/api/handlers.js +165 -26
- package/dist/api/handlers.js.map +1 -1
- package/dist/auth/oauth.d.ts +8 -0
- package/dist/auth/oauth.d.ts.map +1 -1
- package/dist/auth/oauth.js +44 -2
- package/dist/auth/oauth.js.map +1 -1
- package/dist/auth/password.d.ts +35 -2
- package/dist/auth/password.d.ts.map +1 -1
- package/dist/auth/password.js +97 -7
- package/dist/auth/password.js.map +1 -1
- package/dist/auth/reset.d.ts.map +1 -1
- package/dist/auth/reset.js +2 -1
- package/dist/auth/reset.js.map +1 -1
- package/dist/auth/session.d.ts +9 -0
- package/dist/auth/session.d.ts.map +1 -1
- package/dist/auth/session.js +54 -1
- package/dist/auth/session.js.map +1 -1
- package/dist/config/runtime.d.ts +99 -0
- package/dist/config/runtime.d.ts.map +1 -0
- package/dist/config/runtime.js +43 -0
- package/dist/config/runtime.js.map +1 -0
- package/dist/config/types.d.ts +21 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/cron/index.d.ts +72 -0
- package/dist/cron/index.d.ts.map +1 -0
- package/dist/cron/index.js +222 -0
- package/dist/cron/index.js.map +1 -0
- package/dist/diagnostics/env.d.ts +44 -0
- package/dist/diagnostics/env.d.ts.map +1 -0
- package/dist/diagnostics/env.js +293 -0
- package/dist/diagnostics/env.js.map +1 -0
- package/dist/diagnostics/logger.d.ts +38 -0
- package/dist/diagnostics/logger.d.ts.map +1 -0
- package/dist/diagnostics/logger.js +89 -0
- package/dist/diagnostics/logger.js.map +1 -0
- package/dist/page-builder/blocks.d.ts.map +1 -1
- package/dist/page-builder/blocks.js +6 -1
- package/dist/page-builder/blocks.js.map +1 -1
- package/dist/security/audit.d.ts.map +1 -1
- package/dist/security/audit.js +3 -1
- package/dist/security/audit.js.map +1 -1
- package/dist/security/encrypted-fields.d.ts +9 -0
- package/dist/security/encrypted-fields.d.ts.map +1 -1
- package/dist/security/encrypted-fields.js +52 -1
- package/dist/security/encrypted-fields.js.map +1 -1
- package/dist/security/ip-canon.d.ts +71 -0
- package/dist/security/ip-canon.d.ts.map +1 -0
- package/dist/security/ip-canon.js +352 -0
- package/dist/security/ip-canon.js.map +1 -0
- package/dist/security/rate-limit.d.ts +8 -0
- package/dist/security/rate-limit.d.ts.map +1 -1
- package/dist/security/rate-limit.js +81 -3
- package/dist/security/rate-limit.js.map +1 -1
- package/dist/security/safe-fetch.d.ts +30 -8
- package/dist/security/safe-fetch.d.ts.map +1 -1
- package/dist/security/safe-fetch.js +32 -6
- package/dist/security/safe-fetch.js.map +1 -1
- package/dist/security/webhook.d.ts +20 -2
- package/dist/security/webhook.d.ts.map +1 -1
- package/dist/security/webhook.js +100 -30
- package/dist/security/webhook.js.map +1 -1
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ import { createRateLimiter } from '../security/rate-limit.js';
|
|
|
7
7
|
import { getClientIp } from '../security/client-ip.js';
|
|
8
8
|
import { autoSeedAdmin } from '../setup/index.js';
|
|
9
9
|
import { setStorageAdapter } from '../storage/index.js';
|
|
10
|
+
import { getActuateConfig, setActuateConfig, setActuateCoreVersion } from '../config/runtime.js';
|
|
10
11
|
/**
|
|
11
12
|
* Routes that may legitimately receive a state-mutating request without a
|
|
12
13
|
* CSRF token. Matched by exact prefix on the rewritten (CMS-relative) path,
|
|
@@ -22,6 +23,8 @@ import { setStorageAdapter } from '../storage/index.js';
|
|
|
22
23
|
* /auth/oauth/... — OAuth init + callback redirects
|
|
23
24
|
* /setup/create-admin — only reachable on a fresh install
|
|
24
25
|
* /forms/:id/submit — public form submissions (regex below)
|
|
26
|
+
* /cron/... — Vercel Cron / external schedulers; auth is via
|
|
27
|
+
* Authorization: Bearer ${CRON_SECRET} instead
|
|
25
28
|
*/
|
|
26
29
|
const CSRF_EXEMPT_PATHS = [
|
|
27
30
|
'/auth/login',
|
|
@@ -31,6 +34,7 @@ const CSRF_EXEMPT_PATHS = [
|
|
|
31
34
|
'/auth/reset-password',
|
|
32
35
|
'/auth/oauth/',
|
|
33
36
|
'/setup/create-admin',
|
|
37
|
+
'/cron/',
|
|
34
38
|
];
|
|
35
39
|
function isCsrfExemptPath(pathname) {
|
|
36
40
|
if (CSRF_EXEMPT_PATHS.some((p) => pathname === p ||
|
|
@@ -45,11 +49,13 @@ const _require = createRequire(import.meta.url);
|
|
|
45
49
|
const { version: CMS_CORE_VERSION } = _require('../../package.json');
|
|
46
50
|
let cachedGraphQL = null;
|
|
47
51
|
export function handleActuateAPI(options = {}) {
|
|
48
|
-
;
|
|
49
|
-
globalThis.__actuateCoreVersion = CMS_CORE_VERSION;
|
|
52
|
+
setActuateCoreVersion(CMS_CORE_VERSION);
|
|
50
53
|
if (options.config) {
|
|
51
|
-
;
|
|
52
|
-
|
|
54
|
+
// Cast through `unknown` only to satisfy the typed setter; the public
|
|
55
|
+
// signature accepts `unknown` deliberately because consumers may have
|
|
56
|
+
// their own typings. Plugins and downstream code use `getActuateConfig()`
|
|
57
|
+
// which gives them the ActuateCMSConfig shape.
|
|
58
|
+
setActuateConfig(options.config);
|
|
53
59
|
const config = options.config;
|
|
54
60
|
const hooks = [];
|
|
55
61
|
if (Array.isArray(config.plugins)) {
|
|
@@ -175,7 +181,7 @@ export function handleActuateAPI(options = {}) {
|
|
|
175
181
|
});
|
|
176
182
|
}
|
|
177
183
|
}
|
|
178
|
-
const config =
|
|
184
|
+
const config = getActuateConfig();
|
|
179
185
|
if (config) {
|
|
180
186
|
if (!cachedGraphQL) {
|
|
181
187
|
const { createGraphQLHandler } = await import('../graphql/index.js');
|
|
@@ -193,7 +199,10 @@ export function handleActuateAPI(options = {}) {
|
|
|
193
199
|
method: request.method,
|
|
194
200
|
headers: request.headers,
|
|
195
201
|
body: request.body,
|
|
196
|
-
// @ts-
|
|
202
|
+
// @ts-expect-error -- `duplex` is required for streaming bodies but
|
|
203
|
+
// not yet in the lib.dom RequestInit type. Using @ts-expect-error
|
|
204
|
+
// (vs @ts-ignore) ensures this directive itself fails when a future
|
|
205
|
+
// TS lib update adds the property, prompting us to remove the cast.
|
|
197
206
|
duplex: 'half',
|
|
198
207
|
});
|
|
199
208
|
return router.handle(rewrittenRequest);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler-factory.js","sourceRoot":"","sources":["../../src/api/handler-factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACpE,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"handler-factory.js","sourceRoot":"","sources":["../../src/api/handler-factory.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/api/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/api/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAyW5C,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAS9E;AAgKD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAy+IzD"}
|
package/dist/api/handlers.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { listDocuments, getDocument, createDocument, updateDocument, deleteDocument, getGlobal, updateGlobal, } from '../actions.js';
|
|
2
|
-
import { verifyPassword } from '../auth/password.js';
|
|
2
|
+
import { verifyPassword, hashPassword, needsRehash, compareToDummyHash } from '../auth/password.js';
|
|
3
3
|
import { createSession, verifySession, revokeSession } from '../auth/session.js';
|
|
4
4
|
import { createPasswordReset, executePasswordReset } from '../auth/reset.js';
|
|
5
5
|
import { checkSetupRequired, createInitialAdmin } from '../setup/index.js';
|
|
@@ -12,6 +12,7 @@ import { logEvent } from '../security/audit.js';
|
|
|
12
12
|
import { applyFieldAccess } from '../security/access.js';
|
|
13
13
|
import { createPreviewAdapter } from '../preview/index.js';
|
|
14
14
|
import { schedulingCronHandler } from '../scheduling/index.js';
|
|
15
|
+
import { isAuthorizedCronRequest, processCleanup, processSeoScan } from '../cron/index.js';
|
|
15
16
|
import { verifyCaptcha, getCaptchaConfig } from '../security/captcha.js';
|
|
16
17
|
import { checkForUpdates } from '../upgrade/version-check.js';
|
|
17
18
|
import { createUpgradePR } from '../upgrade/upgrade-pr.js';
|
|
@@ -30,6 +31,8 @@ import { enforceSessionLimits } from '../security/session-limits.js';
|
|
|
30
31
|
import { verifyReauth } from '../security/reauth.js';
|
|
31
32
|
import { validateMimeType, checkMagicBytes } from '../security/upload.js';
|
|
32
33
|
import { sanitizeHtml } from '../security/sanitize.js';
|
|
34
|
+
import { getActuateConfig, getActuateCoreVersion } from '../config/runtime.js';
|
|
35
|
+
import { validateEnvShape } from '../diagnostics/env.js';
|
|
33
36
|
// Opaque dynamic import so Turbopack/webpack won't statically analyze the specifier.
|
|
34
37
|
// Returns { put, del, ... } from @vercel/blob when available.
|
|
35
38
|
async function importBlobStorage() {
|
|
@@ -243,9 +246,7 @@ const ALLOWED_SORT_FIELDS = new Set([
|
|
|
243
246
|
let _secretMissing = false;
|
|
244
247
|
let _secretWarningLogged = false;
|
|
245
248
|
function getSessionSecret() {
|
|
246
|
-
const secret = process.env.CMS_SECRET ??
|
|
247
|
-
process.env.CMS_SESSION_SECRET ??
|
|
248
|
-
globalThis.__actuateConfig?.secret;
|
|
249
|
+
const secret = process.env.CMS_SECRET ?? process.env.CMS_SESSION_SECRET ?? getActuateConfig()?.secret;
|
|
249
250
|
if (!secret) {
|
|
250
251
|
_secretMissing = true;
|
|
251
252
|
if (!_secretWarningLogged) {
|
|
@@ -416,9 +417,9 @@ const MAX_CONCURRENT_SESSIONS = 5;
|
|
|
416
417
|
async function enforceSessionLimitsForUser(d, userId) {
|
|
417
418
|
if (!hasModel(d, 'session'))
|
|
418
419
|
return;
|
|
419
|
-
const
|
|
420
|
-
const max = typeof
|
|
421
|
-
?
|
|
420
|
+
const auth = getActuateConfig()?.auth;
|
|
421
|
+
const max = typeof auth?.maxConcurrentSessions === 'number' && auth.maxConcurrentSessions > 0
|
|
422
|
+
? auth.maxConcurrentSessions
|
|
422
423
|
: MAX_CONCURRENT_SESSIONS;
|
|
423
424
|
const active = await d.session.findMany({
|
|
424
425
|
where: { userId, revokedAt: null, expiresAt: { gt: new Date() } },
|
|
@@ -433,7 +434,7 @@ async function enforceSessionLimitsForUser(d, userId) {
|
|
|
433
434
|
}
|
|
434
435
|
}
|
|
435
436
|
function getAdminPath() {
|
|
436
|
-
return
|
|
437
|
+
return process.env.ACTUATE_ADMIN_PATH ?? getActuateConfig()?.admin?.path ?? '/admin';
|
|
437
438
|
}
|
|
438
439
|
class ModelNotAvailableError extends Error {
|
|
439
440
|
model;
|
|
@@ -504,7 +505,7 @@ export function registerCMSRoutes(router) {
|
|
|
504
505
|
// OpenAPI spec
|
|
505
506
|
// ---------------------------------------------------------------------------
|
|
506
507
|
router.get('/openapi.json', async () => {
|
|
507
|
-
const config =
|
|
508
|
+
const config = getActuateConfig();
|
|
508
509
|
if (!config)
|
|
509
510
|
return errorResponse('CMS not configured', 500);
|
|
510
511
|
const spec = generateOpenAPISpec(config);
|
|
@@ -563,10 +564,18 @@ export function registerCMSRoutes(router) {
|
|
|
563
564
|
const user = await d.user.findFirst({
|
|
564
565
|
where: { email: email.toLowerCase().trim() },
|
|
565
566
|
});
|
|
567
|
+
// User-enumeration timing defense (H5): when the email doesn't exist,
|
|
568
|
+
// OR when the account is OAuth-only (no passwordHash), still spend
|
|
569
|
+
// the same ~100-200ms running PBKDF2 against a dummy hash before
|
|
570
|
+
// returning the error. Without this, attackers can distinguish
|
|
571
|
+
// "no such user" / "OAuth-only" / "wrong password" by the response
|
|
572
|
+
// delta, enabling targeted phishing & credential stuffing.
|
|
566
573
|
if (!user) {
|
|
574
|
+
await compareToDummyHash(password);
|
|
567
575
|
return errorResponse('Invalid email or password', 401);
|
|
568
576
|
}
|
|
569
577
|
if (!user.passwordHash) {
|
|
578
|
+
await compareToDummyHash(password);
|
|
570
579
|
return errorResponse('This account uses social login. Please sign in with your OAuth provider.', 400);
|
|
571
580
|
}
|
|
572
581
|
const passwordValid = await verifyPassword(password, user.passwordHash);
|
|
@@ -582,6 +591,20 @@ export function registerCMSRoutes(router) {
|
|
|
582
591
|
if (!user.isActive) {
|
|
583
592
|
return errorResponse('Account is deactivated', 403);
|
|
584
593
|
}
|
|
594
|
+
// H6: opportunistically upgrade stored hashes that use weaker
|
|
595
|
+
// PBKDF2 parameters than the current policy. We have the plaintext
|
|
596
|
+
// here (and we know it's correct) — re-hash and persist. Wrapped in
|
|
597
|
+
// try/catch so a transient DB failure never fails an otherwise-valid
|
|
598
|
+
// login; the user gets in, the upgrade just runs again next time.
|
|
599
|
+
if (needsRehash(user.passwordHash)) {
|
|
600
|
+
try {
|
|
601
|
+
const upgraded = await hashPassword(password);
|
|
602
|
+
await d.user.update({ where: { id: user.id }, data: { passwordHash: upgraded } });
|
|
603
|
+
}
|
|
604
|
+
catch (err) {
|
|
605
|
+
console.warn('[actuate][login] password rehash skipped:', err instanceof Error ? err.message : err);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
585
608
|
if (user.totpEnabled) {
|
|
586
609
|
// Hand back an opaque short-lived token instead of the raw userId.
|
|
587
610
|
// The /auth/totp/login endpoint will verify both this token and a
|
|
@@ -673,7 +696,7 @@ export function registerCMSRoutes(router) {
|
|
|
673
696
|
if (!hasModel(d, 'user'))
|
|
674
697
|
return modelNotAvailable('user');
|
|
675
698
|
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? new URL(request.url).origin;
|
|
676
|
-
const cmsConfig =
|
|
699
|
+
const cmsConfig = getActuateConfig();
|
|
677
700
|
await createPasswordReset(d, email.toLowerCase().trim(), {
|
|
678
701
|
siteUrl,
|
|
679
702
|
platform: cmsConfig?.platform,
|
|
@@ -1054,7 +1077,7 @@ export function registerCMSRoutes(router) {
|
|
|
1054
1077
|
};
|
|
1055
1078
|
const cookies = parseCookieHeader(request.headers.get('cookie') ?? '');
|
|
1056
1079
|
const expectedNonce = cookies['actuate_oauth_nonce'] ?? null;
|
|
1057
|
-
const cmsConfig =
|
|
1080
|
+
const cmsConfig = getActuateConfig();
|
|
1058
1081
|
const allowSelfSignup = cmsConfig?.auth?.oauth?.allowSelfSignup === true;
|
|
1059
1082
|
const result = await handleOAuthCallback(provider, code, stateToken, oauthProviders, secret, db(), { expectedNonce, allowSelfSignup });
|
|
1060
1083
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
@@ -1107,7 +1130,7 @@ export function registerCMSRoutes(router) {
|
|
|
1107
1130
|
locale: url.searchParams.get('locale') ?? undefined,
|
|
1108
1131
|
folderId: url.searchParams.get('folderId') ?? undefined,
|
|
1109
1132
|
}, ctx);
|
|
1110
|
-
const collectionConfig =
|
|
1133
|
+
const collectionConfig = getActuateConfig()?.collections?.[params.slug];
|
|
1111
1134
|
const fields = collectionConfig?.fields;
|
|
1112
1135
|
if (fields && result.docs.length > 0) {
|
|
1113
1136
|
const user = { id: auth.session.userId, role: auth.session.role };
|
|
@@ -1307,7 +1330,21 @@ export function registerCMSRoutes(router) {
|
|
|
1307
1330
|
const auth = await requireAuth(request);
|
|
1308
1331
|
if (auth.error)
|
|
1309
1332
|
return auth.error;
|
|
1310
|
-
|
|
1333
|
+
// Reject *before* buffering the body. We require a valid content-length
|
|
1334
|
+
// header so that chunked / no-length requests (which would otherwise
|
|
1335
|
+
// bypass this gate and allow `request.formData()` to buffer unbounded
|
|
1336
|
+
// memory) are rejected up front. The multipart envelope is always
|
|
1337
|
+
// larger than the underlying file, so checking against MAX_UPLOAD_BYTES
|
|
1338
|
+
// here is a safe upper bound — a 50 MB file cannot fit inside a 50 MB
|
|
1339
|
+
// request body.
|
|
1340
|
+
const contentLengthHeader = request.headers.get('content-length');
|
|
1341
|
+
if (!contentLengthHeader) {
|
|
1342
|
+
return errorResponse('Content-Length header is required for uploads (chunked encoding is not supported)', 411);
|
|
1343
|
+
}
|
|
1344
|
+
const contentLength = parseInt(contentLengthHeader, 10);
|
|
1345
|
+
if (!Number.isFinite(contentLength) || contentLength <= 0) {
|
|
1346
|
+
return errorResponse('Invalid Content-Length header', 400);
|
|
1347
|
+
}
|
|
1311
1348
|
if (contentLength > MAX_UPLOAD_BYTES) {
|
|
1312
1349
|
return errorResponse('File exceeds maximum size of 50MB', 413);
|
|
1313
1350
|
}
|
|
@@ -1320,7 +1357,11 @@ export function registerCMSRoutes(router) {
|
|
|
1320
1357
|
const originalFilename = file.name;
|
|
1321
1358
|
const contentType = file.type;
|
|
1322
1359
|
const originalSize = file.size;
|
|
1323
|
-
|
|
1360
|
+
// Belt-and-braces: even with the header check above, re-validate the
|
|
1361
|
+
// *actual* file size after parsing in case the multipart envelope
|
|
1362
|
+
// disagreed with the header. This must come BEFORE any further
|
|
1363
|
+
// processing (magic byte read, SVG sanitization, blob upload).
|
|
1364
|
+
if (originalSize > MAX_UPLOAD_BYTES) {
|
|
1324
1365
|
return errorResponse('File exceeds maximum size of 50MB', 413);
|
|
1325
1366
|
}
|
|
1326
1367
|
// 1. Block file types that aren't on our allowlist outright.
|
|
@@ -1705,7 +1746,7 @@ export function registerCMSRoutes(router) {
|
|
|
1705
1746
|
const session = await extractSession(request);
|
|
1706
1747
|
if (!session)
|
|
1707
1748
|
return errorResponse('Unauthorized', 401);
|
|
1708
|
-
const coreVersion =
|
|
1749
|
+
const coreVersion = getActuateCoreVersion() ?? '0.1.0';
|
|
1709
1750
|
const info = await checkForUpdates(coreVersion);
|
|
1710
1751
|
const saved = await getUpdateConfig();
|
|
1711
1752
|
return json({
|
|
@@ -1808,7 +1849,7 @@ export function registerCMSRoutes(router) {
|
|
|
1808
1849
|
if (!owner || !repo) {
|
|
1809
1850
|
return errorResponse('GitHub repository not configured. Go to Settings > Updates to add one.', 400);
|
|
1810
1851
|
}
|
|
1811
|
-
const coreVersion =
|
|
1852
|
+
const coreVersion = getActuateCoreVersion() ?? '0.1.0';
|
|
1812
1853
|
const result = await createUpgradePR({
|
|
1813
1854
|
owner,
|
|
1814
1855
|
repo,
|
|
@@ -1893,8 +1934,43 @@ export function registerCMSRoutes(router) {
|
|
|
1893
1934
|
'webhookDeliveryLog',
|
|
1894
1935
|
];
|
|
1895
1936
|
router.get('/health', async () => {
|
|
1896
|
-
const cmsVersion =
|
|
1937
|
+
const cmsVersion = getActuateCoreVersion() ?? '0.0.0';
|
|
1897
1938
|
const models = {};
|
|
1939
|
+
// M6: validate the *shape* of every well-known env var, not just presence.
|
|
1940
|
+
// A 31-char CMS_SECRET or a placeholder CMS_ENCRYPTION_KEY now reports as
|
|
1941
|
+
// a hard error here instead of crashing at the first runtime use.
|
|
1942
|
+
//
|
|
1943
|
+
// Bugbot review (PR #43): CMS_SECRET has *three* legitimate sources —
|
|
1944
|
+
// `process.env.CMS_SECRET`, the legacy `process.env.CMS_SESSION_SECRET`,
|
|
1945
|
+
// and `getActuateConfig()?.secret` (config-file path documented in the
|
|
1946
|
+
// missing-secret warning message itself). `getSessionSecret()` checks all
|
|
1947
|
+
// three; `isSecretMissing()` consequently returns false when any of them
|
|
1948
|
+
// is set. If `validateEnvShape()` only inspected `process.env.CMS_SECRET`,
|
|
1949
|
+
// a config-file deploy would get `secretConfigured: true` AND
|
|
1950
|
+
// `status: "unhealthy"` simultaneously — a contradiction that would trip
|
|
1951
|
+
// monitoring / load-balancer health probes for no real fault. We pass a
|
|
1952
|
+
// wrapped `EnvSource` so the validator sees what the runtime would
|
|
1953
|
+
// actually resolve.
|
|
1954
|
+
const env = validateEnvShape({
|
|
1955
|
+
get(name) {
|
|
1956
|
+
if (name === 'CMS_SECRET') {
|
|
1957
|
+
return (process.env.CMS_SECRET ?? process.env.CMS_SESSION_SECRET ?? getActuateConfig()?.secret);
|
|
1958
|
+
}
|
|
1959
|
+
return process.env[name];
|
|
1960
|
+
},
|
|
1961
|
+
});
|
|
1962
|
+
// Derive overall status from env validation + model availability + DB
|
|
1963
|
+
// connection. `env.errorCount > 0` outranks every model/DB issue because
|
|
1964
|
+
// it represents a *deployment* misconfig the operator must fix before any
|
|
1965
|
+
// request will succeed — pretending the deploy is "degraded" when secrets
|
|
1966
|
+
// are malformed defeats the M6 goal of catching that at /health time.
|
|
1967
|
+
function deriveStatus(dbConnected, allModelsAvailable) {
|
|
1968
|
+
if (env.errorCount > 0)
|
|
1969
|
+
return 'unhealthy';
|
|
1970
|
+
if (allModelsAvailable && dbConnected)
|
|
1971
|
+
return 'healthy';
|
|
1972
|
+
return 'degraded';
|
|
1973
|
+
}
|
|
1898
1974
|
let d;
|
|
1899
1975
|
try {
|
|
1900
1976
|
d = db();
|
|
@@ -1904,9 +1980,10 @@ export function registerCMSRoutes(router) {
|
|
|
1904
1980
|
models[m] = false;
|
|
1905
1981
|
return json({
|
|
1906
1982
|
data: {
|
|
1907
|
-
status:
|
|
1983
|
+
status: deriveStatus(false, false),
|
|
1908
1984
|
version: cmsVersion,
|
|
1909
1985
|
secretConfigured: !isSecretMissing(),
|
|
1986
|
+
env,
|
|
1910
1987
|
models,
|
|
1911
1988
|
databaseConnected: false,
|
|
1912
1989
|
},
|
|
@@ -1932,11 +2009,13 @@ export function registerCMSRoutes(router) {
|
|
|
1932
2009
|
}
|
|
1933
2010
|
}
|
|
1934
2011
|
const allAvailable = Object.values(models).every(Boolean);
|
|
2012
|
+
const status = deriveStatus(dbConnected, allAvailable);
|
|
1935
2013
|
return json({
|
|
1936
2014
|
data: {
|
|
1937
|
-
status
|
|
2015
|
+
status,
|
|
1938
2016
|
version: cmsVersion,
|
|
1939
2017
|
secretConfigured: !isSecretMissing(),
|
|
2018
|
+
env,
|
|
1940
2019
|
models,
|
|
1941
2020
|
databaseConnected: dbConnected,
|
|
1942
2021
|
},
|
|
@@ -2307,9 +2386,9 @@ export function registerCMSRoutes(router) {
|
|
|
2307
2386
|
});
|
|
2308
2387
|
(async () => {
|
|
2309
2388
|
try {
|
|
2310
|
-
const config =
|
|
2311
|
-
const hooks =
|
|
2312
|
-
const formHooks = hooks.filter((h) => h
|
|
2389
|
+
const config = getActuateConfig();
|
|
2390
|
+
const hooks = (config?._pluginHooks ?? []);
|
|
2391
|
+
const formHooks = hooks.filter((h) => h?.event === 'afterCreate:form-submissions');
|
|
2313
2392
|
for (const hook of formHooks) {
|
|
2314
2393
|
await hook.handler({ formId, data: body.fields });
|
|
2315
2394
|
}
|
|
@@ -2382,7 +2461,7 @@ export function registerCMSRoutes(router) {
|
|
|
2382
2461
|
if (!['http:', 'https:'].includes(destUrl.protocol)) {
|
|
2383
2462
|
return errorResponse('Invalid destination URL', 400);
|
|
2384
2463
|
}
|
|
2385
|
-
const cmsConfig =
|
|
2464
|
+
const cmsConfig = getActuateConfig();
|
|
2386
2465
|
const allowed = new Set([
|
|
2387
2466
|
...(Array.isArray(cmsConfig?.redirects?.allowedExternalHosts)
|
|
2388
2467
|
? cmsConfig.redirects.allowedExternalHosts.map((h) => h.toLowerCase())
|
|
@@ -2944,7 +3023,7 @@ export function registerCMSRoutes(router) {
|
|
|
2944
3023
|
// ---------------------------------------------------------------------------
|
|
2945
3024
|
const MAX_RESOLVE_DEPTH = 10;
|
|
2946
3025
|
async function resolveLayout(path, docData, matchedCollection) {
|
|
2947
|
-
const config =
|
|
3026
|
+
const config = getActuateConfig();
|
|
2948
3027
|
const layoutConfig = config?.layout;
|
|
2949
3028
|
if (!layoutConfig?.regions)
|
|
2950
3029
|
return {};
|
|
@@ -3040,7 +3119,7 @@ export function registerCMSRoutes(router) {
|
|
|
3040
3119
|
.replace(/^\/|\/$/g, '')
|
|
3041
3120
|
.split('/')
|
|
3042
3121
|
.filter(Boolean);
|
|
3043
|
-
const configCollections =
|
|
3122
|
+
const configCollections = getActuateConfig()?.collections ?? {};
|
|
3044
3123
|
const collectionDefs = Object.values(configCollections);
|
|
3045
3124
|
let matchedCollection = null;
|
|
3046
3125
|
let docSlug = null;
|
|
@@ -3472,6 +3551,66 @@ export function registerCMSRoutes(router) {
|
|
|
3472
3551
|
return internalError(err, 'scheduling run');
|
|
3473
3552
|
}
|
|
3474
3553
|
});
|
|
3554
|
+
// ---------------------------------------------------------------------------
|
|
3555
|
+
// Cron endpoints — auth via `Authorization: Bearer ${CRON_SECRET}`.
|
|
3556
|
+
//
|
|
3557
|
+
// **HTTP method:** registered as GET because Vercel Cron sends GET requests
|
|
3558
|
+
// (https://vercel.com/docs/cron-jobs). We also register POST aliases so
|
|
3559
|
+
// self-hosted schedulers that POST (k8s CronJob, GH Actions step running
|
|
3560
|
+
// `curl -X POST`, EventBridge HTTP target) keep working without changes.
|
|
3561
|
+
//
|
|
3562
|
+
// Vercel Cron sets the `Authorization: Bearer <CRON_SECRET>` header
|
|
3563
|
+
// automatically when `CRON_SECRET` is defined in the project environment.
|
|
3564
|
+
// When CRON_SECRET is unset, requests are rejected — fail-closed so a
|
|
3565
|
+
// misconfigured deploy never exposes these to the public.
|
|
3566
|
+
//
|
|
3567
|
+
// CSRF is NOT required: these endpoints have no session and the body carries
|
|
3568
|
+
// no admin intent — auth is exclusively via the Bearer token. They are also
|
|
3569
|
+
// listed in `CSRF_EXEMPT_PATHS` in handler-factory.ts so the global CSRF
|
|
3570
|
+
// gate doesn't block them.
|
|
3571
|
+
// ---------------------------------------------------------------------------
|
|
3572
|
+
const cronPublish = async (request) => {
|
|
3573
|
+
try {
|
|
3574
|
+
if (!isAuthorizedCronRequest(request.headers.get('authorization'))) {
|
|
3575
|
+
return errorResponse('Unauthorized', 401);
|
|
3576
|
+
}
|
|
3577
|
+
const result = await schedulingCronHandler(db());
|
|
3578
|
+
return json({ data: result });
|
|
3579
|
+
}
|
|
3580
|
+
catch (err) {
|
|
3581
|
+
return internalError(err, 'cron publish');
|
|
3582
|
+
}
|
|
3583
|
+
};
|
|
3584
|
+
router.get('/cron/publish', cronPublish);
|
|
3585
|
+
router.post('/cron/publish', cronPublish);
|
|
3586
|
+
const cronCleanup = async (request) => {
|
|
3587
|
+
try {
|
|
3588
|
+
if (!isAuthorizedCronRequest(request.headers.get('authorization'))) {
|
|
3589
|
+
return errorResponse('Unauthorized', 401);
|
|
3590
|
+
}
|
|
3591
|
+
const result = await processCleanup(db());
|
|
3592
|
+
return json({ data: result });
|
|
3593
|
+
}
|
|
3594
|
+
catch (err) {
|
|
3595
|
+
return internalError(err, 'cron cleanup');
|
|
3596
|
+
}
|
|
3597
|
+
};
|
|
3598
|
+
router.get('/cron/cleanup', cronCleanup);
|
|
3599
|
+
router.post('/cron/cleanup', cronCleanup);
|
|
3600
|
+
const cronSeoScan = async (request) => {
|
|
3601
|
+
try {
|
|
3602
|
+
if (!isAuthorizedCronRequest(request.headers.get('authorization'))) {
|
|
3603
|
+
return errorResponse('Unauthorized', 401);
|
|
3604
|
+
}
|
|
3605
|
+
const result = await processSeoScan(db());
|
|
3606
|
+
return json({ data: result });
|
|
3607
|
+
}
|
|
3608
|
+
catch (err) {
|
|
3609
|
+
return internalError(err, 'cron seo-scan');
|
|
3610
|
+
}
|
|
3611
|
+
};
|
|
3612
|
+
router.get('/cron/seo-scan', cronSeoScan);
|
|
3613
|
+
router.post('/cron/seo-scan', cronSeoScan);
|
|
3475
3614
|
router.get('/scheduling/calendar', async (request) => {
|
|
3476
3615
|
try {
|
|
3477
3616
|
const auth = await requireAuth(request);
|
|
@@ -3496,7 +3635,7 @@ export function registerCMSRoutes(router) {
|
|
|
3496
3635
|
router.get('/public/globals/:slug', async (_request, params) => {
|
|
3497
3636
|
try {
|
|
3498
3637
|
const slug = params.slug;
|
|
3499
|
-
const globalConfig =
|
|
3638
|
+
const globalConfig = getActuateConfig()?.globals?.[slug];
|
|
3500
3639
|
if (!globalConfig) {
|
|
3501
3640
|
return errorResponse('Global not found', 404);
|
|
3502
3641
|
}
|