@reldens/cms 0.36.0 → 0.38.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/README.md +172 -0
- package/admin/reldens-admin-client.js +4 -0
- package/lib/manager.js +11 -11
- package/lib/template-engine/asset-transformer.js +41 -54
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -292,6 +292,178 @@ const cms = new Manager({
|
|
|
292
292
|
// - Configures template reloading
|
|
293
293
|
```
|
|
294
294
|
|
|
295
|
+
### Development Mode Detection
|
|
296
|
+
|
|
297
|
+
The CMS automatically detects development environments based on domain patterns. Domain mapping keys are **no longer automatically treated as development domains**.
|
|
298
|
+
|
|
299
|
+
**Default Development Patterns:**
|
|
300
|
+
```javascript
|
|
301
|
+
const patterns = [
|
|
302
|
+
'localhost',
|
|
303
|
+
'127.0.0.1',
|
|
304
|
+
'.local', // Domains ending with .local
|
|
305
|
+
'.test', // Domains ending with .test
|
|
306
|
+
'.dev', // Domains ending with .dev
|
|
307
|
+
'.acc', // Domains ending with .acc
|
|
308
|
+
'.staging', // Domains ending with .staging
|
|
309
|
+
'local.', // Domains starting with local.
|
|
310
|
+
'test.', // Domains starting with test.
|
|
311
|
+
'dev.', // Domains starting with dev.
|
|
312
|
+
'acc.', // Domains starting with acc.
|
|
313
|
+
'staging.' // Domains starting with staging.
|
|
314
|
+
];
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Override Development Patterns:**
|
|
318
|
+
```javascript
|
|
319
|
+
const cms = new Manager({
|
|
320
|
+
// Only these patterns will trigger development mode
|
|
321
|
+
developmentPatterns: [
|
|
322
|
+
'localhost',
|
|
323
|
+
'127.0.0.1',
|
|
324
|
+
'.local'
|
|
325
|
+
],
|
|
326
|
+
domainMapping: {
|
|
327
|
+
// These are just aliases - NOT automatically development
|
|
328
|
+
'www.example.com': 'example.com',
|
|
329
|
+
'new.example.com': 'example.com'
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Important Notes:**
|
|
335
|
+
- The bug in pattern matching where domains with common substrings (e.g., "reldens" in both "acc.reldens.com" and "reldens.new") incorrectly triggered development mode has been fixed.
|
|
336
|
+
- Domain patterns now only match at the start or end of domains, not arbitrary positions.
|
|
337
|
+
- Override `developmentPatterns` in production to prevent staging/acc domains from enabling development mode.
|
|
338
|
+
|
|
339
|
+
### Security Configuration
|
|
340
|
+
|
|
341
|
+
Configure Content Security Policy and security headers through the app server config:
|
|
342
|
+
|
|
343
|
+
#### External Domains for CSP
|
|
344
|
+
|
|
345
|
+
When configuring external domains for CSP directives, keys can be in either kebab-case or camelCase format:
|
|
346
|
+
|
|
347
|
+
```javascript
|
|
348
|
+
const cms = new Manager({
|
|
349
|
+
appServerConfig: {
|
|
350
|
+
developmentExternalDomains: {
|
|
351
|
+
// Both formats work - choose whichever you prefer
|
|
352
|
+
'scriptSrc': ['https://cdn.example.com'], // camelCase
|
|
353
|
+
'script-src': ['https://analytics.example.com'], // kebab-case (auto-converted)
|
|
354
|
+
'styleSrc': ['https://fonts.googleapis.com'], // camelCase
|
|
355
|
+
'font-src': ['https://fonts.gstatic.com'] // kebab-case (auto-converted)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The system automatically:
|
|
362
|
+
- Converts kebab-case keys to camelCase (e.g., `'script-src'` → `scriptSrc`)
|
|
363
|
+
- Adds domains to both the base directive and the `-elem` variant (e.g., `scriptSrc` and `scriptSrcElem`)
|
|
364
|
+
|
|
365
|
+
#### CSP Directive Merging vs Override
|
|
366
|
+
|
|
367
|
+
By default, custom CSP directives are **merged** with security defaults:
|
|
368
|
+
|
|
369
|
+
```javascript
|
|
370
|
+
const cms = new Manager({
|
|
371
|
+
appServerConfig: {
|
|
372
|
+
helmetConfig: {
|
|
373
|
+
contentSecurityPolicy: {
|
|
374
|
+
// Default: merge with base directives
|
|
375
|
+
directives: {
|
|
376
|
+
scriptSrc: ['https://cdn.example.com']
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Result: default scriptSrc values + 'https://cdn.example.com'
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**Default Base Directives:**
|
|
387
|
+
```javascript
|
|
388
|
+
{
|
|
389
|
+
defaultSrc: ["'self'"],
|
|
390
|
+
scriptSrc: ["'self'"],
|
|
391
|
+
scriptSrcElem: ["'self'"],
|
|
392
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
393
|
+
styleSrcElem: ["'self'", "'unsafe-inline'"],
|
|
394
|
+
imgSrc: ["'self'", "data:", "https:"],
|
|
395
|
+
fontSrc: ["'self'"],
|
|
396
|
+
connectSrc: ["'self'"],
|
|
397
|
+
frameAncestors: ["'none'"],
|
|
398
|
+
baseUri: ["'self'"],
|
|
399
|
+
formAction: ["'self'"]
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
To **completely replace** the default directives, use `overrideDirectives: true`:
|
|
404
|
+
|
|
405
|
+
```javascript
|
|
406
|
+
const cms = new Manager({
|
|
407
|
+
appServerConfig: {
|
|
408
|
+
helmetConfig: {
|
|
409
|
+
contentSecurityPolicy: {
|
|
410
|
+
overrideDirectives: true, // Replace defaults entirely
|
|
411
|
+
directives: {
|
|
412
|
+
defaultSrc: ["'self'"],
|
|
413
|
+
scriptSrc: ["'self'", "https://trusted-cdn.com"],
|
|
414
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
415
|
+
imgSrc: ["'self'", "data:", "https:"],
|
|
416
|
+
fontSrc: ["'self'"],
|
|
417
|
+
connectSrc: ["'self'"],
|
|
418
|
+
frameAncestors: ["'none'"],
|
|
419
|
+
baseUri: ["'self'"],
|
|
420
|
+
formAction: ["'self'"]
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
#### Additional Helmet Security Headers
|
|
429
|
+
|
|
430
|
+
Configure other security headers through `helmetConfig`:
|
|
431
|
+
|
|
432
|
+
```javascript
|
|
433
|
+
const cms = new Manager({
|
|
434
|
+
appServerConfig: {
|
|
435
|
+
helmetConfig: {
|
|
436
|
+
// HTTP Strict Transport Security
|
|
437
|
+
hsts: {
|
|
438
|
+
maxAge: 31536000, // 1 year in seconds
|
|
439
|
+
includeSubDomains: true,
|
|
440
|
+
preload: true
|
|
441
|
+
},
|
|
442
|
+
// Cross-Origin-Opener-Policy
|
|
443
|
+
crossOriginOpenerPolicy: {
|
|
444
|
+
policy: "same-origin"
|
|
445
|
+
},
|
|
446
|
+
// Cross-Origin-Resource-Policy
|
|
447
|
+
crossOriginResourcePolicy: {
|
|
448
|
+
policy: "same-origin"
|
|
449
|
+
},
|
|
450
|
+
// Cross-Origin-Embedder-Policy
|
|
451
|
+
crossOriginEmbedderPolicy: {
|
|
452
|
+
policy: "require-corp"
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
**Note:** In development mode, CSP and HSTS are automatically disabled to ease development. Security headers are only enforced when the CMS is not in development mode.
|
|
460
|
+
|
|
461
|
+
**Trusted Types:** To enable Trusted Types for enhanced XSS protection, add to CSP directives:
|
|
462
|
+
```javascript
|
|
463
|
+
requireTrustedTypesFor: ["'script'"]
|
|
464
|
+
```
|
|
465
|
+
However, this requires updating all JavaScript code to use the Trusted Types API.
|
|
466
|
+
|
|
295
467
|
## Dynamic Forms System
|
|
296
468
|
|
|
297
469
|
### Basic Form Usage
|
package/lib/manager.js
CHANGED
|
@@ -236,13 +236,11 @@ class Manager
|
|
|
236
236
|
|
|
237
237
|
buildAppServerConfiguration()
|
|
238
238
|
{
|
|
239
|
-
let useHelmet =
|
|
240
|
-
|
|
241
|
-
useHelmet = false;
|
|
242
|
-
}
|
|
239
|
+
let useHelmet = this.isInstalled();
|
|
240
|
+
let useHttps = this.config.host.startsWith('https://');
|
|
243
241
|
let baseConfig = {
|
|
244
242
|
port: this.config.port,
|
|
245
|
-
useHttps
|
|
243
|
+
useHttps,
|
|
246
244
|
useHelmet,
|
|
247
245
|
domainMapping: this.domainMapping || {},
|
|
248
246
|
defaultDomain: this.defaultDomain,
|
|
@@ -254,12 +252,14 @@ class Manager
|
|
|
254
252
|
};
|
|
255
253
|
let appServerConfig = Object.assign({}, baseConfig, this.appServerConfig);
|
|
256
254
|
if(this.domainMapping && 'object' === typeof this.domainMapping){
|
|
255
|
+
this.appServerFactory.setDomainMapping(this.domainMapping);
|
|
257
256
|
this.validateCdnMappingsInDevelopment();
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
257
|
+
if(!useHttps){
|
|
258
|
+
let mappingKeys = Object.keys(this.domainMapping);
|
|
259
|
+
for(let domain of mappingKeys){
|
|
260
|
+
this.appServerFactory.addDevelopmentDomain(domain);
|
|
261
|
+
}
|
|
261
262
|
}
|
|
262
|
-
this.appServerFactory.setDomainMapping(this.domainMapping);
|
|
263
263
|
}
|
|
264
264
|
if(sc.isArray(this.domains) && 0 < this.domains.length){
|
|
265
265
|
for(let domain of this.domains){
|
|
@@ -278,7 +278,7 @@ class Manager
|
|
|
278
278
|
return;
|
|
279
279
|
}
|
|
280
280
|
if(!sc.isObject(this.developmentExternalDomains) || sc.isArray(this.developmentExternalDomains)){
|
|
281
|
-
Logger.
|
|
281
|
+
Logger.info('CDN mappings configured but developmentExternalDomains not provided. '
|
|
282
282
|
+'CDN assets may fail in development mode due to CORS. '
|
|
283
283
|
+'Add CDN domains to developmentExternalDomains configuration.');
|
|
284
284
|
return;
|
|
@@ -292,7 +292,7 @@ class Manager
|
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
294
|
if(0 < missingCDNs.length){
|
|
295
|
-
Logger.
|
|
295
|
+
Logger.info('CDN domains not found in developmentExternalDomains: '+missingCDNs.join(', ')
|
|
296
296
|
+'. Add them to avoid CORS issues in development mode.');
|
|
297
297
|
}
|
|
298
298
|
}
|
|
@@ -1,54 +1,41 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* Reldens - CMS - AssetTransformer
|
|
4
|
-
*
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const { sc } = require('@reldens/utils');
|
|
8
|
-
|
|
9
|
-
class AssetTransformer
|
|
10
|
-
{
|
|
11
|
-
|
|
12
|
-
async transform(template, domain, req, systemVariables)
|
|
13
|
-
{
|
|
14
|
-
if(!template){
|
|
15
|
-
return template;
|
|
16
|
-
}
|
|
17
|
-
let currentRequest = sc.get(systemVariables, 'currentRequest', {});
|
|
18
|
-
let assetPattern = /\[asset\(([^)]+)\)\]/g;
|
|
19
|
-
let matches = [...template.matchAll(assetPattern)];
|
|
20
|
-
for(let i = matches.length - 1; i >= 0; i--){
|
|
21
|
-
let match = matches[i];
|
|
22
|
-
let assetPath = match[1].replace(/['"]/g, '');
|
|
23
|
-
let absoluteUrl = this.buildAssetUrl(assetPath, currentRequest);
|
|
24
|
-
template = template.substring(0, match.index) +
|
|
25
|
-
absoluteUrl +
|
|
26
|
-
template.substring(match.index + match[0].length);
|
|
27
|
-
}
|
|
28
|
-
return template;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
buildAssetUrl(assetPath, currentRequest)
|
|
32
|
-
{
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
return '';
|
|
44
|
-
}
|
|
45
|
-
let normalizedPath = assetPath.startsWith('/') ? assetPath : '/'+assetPath;
|
|
46
|
-
if(assetUrl){
|
|
47
|
-
return assetUrl+normalizedPath;
|
|
48
|
-
}
|
|
49
|
-
return publicUrl+'/assets'+normalizedPath;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
module.exports.AssetTransformer = AssetTransformer;
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Reldens - CMS - AssetTransformer
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { sc } = require('@reldens/utils');
|
|
8
|
+
|
|
9
|
+
class AssetTransformer
|
|
10
|
+
{
|
|
11
|
+
|
|
12
|
+
async transform(template, domain, req, systemVariables)
|
|
13
|
+
{
|
|
14
|
+
if(!template){
|
|
15
|
+
return template;
|
|
16
|
+
}
|
|
17
|
+
let currentRequest = sc.get(systemVariables, 'currentRequest', {});
|
|
18
|
+
let assetPattern = /\[asset\(([^)]+)\)\]/g;
|
|
19
|
+
let matches = [...template.matchAll(assetPattern)];
|
|
20
|
+
for(let i = matches.length - 1; i >= 0; i--){
|
|
21
|
+
let match = matches[i];
|
|
22
|
+
let assetPath = match[1].replace(/['"]/g, '');
|
|
23
|
+
let absoluteUrl = this.buildAssetUrl(assetPath, currentRequest);
|
|
24
|
+
template = template.substring(0, match.index) +
|
|
25
|
+
absoluteUrl +
|
|
26
|
+
template.substring(match.index + match[0].length);
|
|
27
|
+
}
|
|
28
|
+
return template;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
buildAssetUrl(assetPath, currentRequest)
|
|
32
|
+
{
|
|
33
|
+
if(!assetPath || assetPath.startsWith('http')){
|
|
34
|
+
return assetPath;
|
|
35
|
+
}
|
|
36
|
+
return sc.get(currentRequest, 'assetUrl', '')+'/assets'+(assetPath.startsWith('/') ? assetPath : '/'+assetPath);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports.AssetTransformer = AssetTransformer;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reldens/cms",
|
|
3
3
|
"scope": "@reldens",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.38.0",
|
|
5
5
|
"description": "Reldens - CMS",
|
|
6
6
|
"author": "Damian A. Pastorini",
|
|
7
7
|
"license": "MIT",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"url": "https://github.com/damian-pastorini/reldens-cms/issues"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@reldens/server-utils": "^0.
|
|
37
|
-
"@reldens/storage": "^0.
|
|
36
|
+
"@reldens/server-utils": "^0.37.0",
|
|
37
|
+
"@reldens/storage": "^0.75.0",
|
|
38
38
|
"@reldens/utils": "^0.53.0",
|
|
39
39
|
"dotenv": "17.2.3",
|
|
40
40
|
"mustache": "4.2.0"
|