@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 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
@@ -4,6 +4,10 @@
4
4
  *
5
5
  */
6
6
 
7
+ if(window.trustedTypes?.createPolicy){
8
+ trustedTypes.createPolicy('default', {createHTML: s => s});
9
+ }
10
+
7
11
  window.addEventListener('DOMContentLoaded', () => {
8
12
 
9
13
  // helpers:
package/lib/manager.js CHANGED
@@ -236,13 +236,11 @@ class Manager
236
236
 
237
237
  buildAppServerConfiguration()
238
238
  {
239
- let useHelmet = true;
240
- if(!this.isInstalled()){
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: this.config.host.startsWith('https://'),
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
- let mappingKeys = Object.keys(this.domainMapping);
259
- for(let domain of mappingKeys){
260
- this.appServerFactory.addDevelopmentDomain(domain);
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.warning('CDN mappings configured but developmentExternalDomains not provided. '
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.warning('CDN domains not found in developmentExternalDomains: '+missingCDNs.join(', ')
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
- // if the path is a url, we will not transform it:
34
- if(assetPath && assetPath.startsWith('http')){
35
- return assetPath;
36
- }
37
- let assetUrl = sc.get(currentRequest, 'assetUrl', '');
38
- let publicUrl = sc.get(currentRequest, 'publicUrl', '');
39
- if(!assetPath){
40
- if(assetUrl){
41
- return assetUrl;
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.36.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.36.0",
37
- "@reldens/storage": "^0.73.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"