cyberia 3.2.9 → 3.2.12

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 (169) hide show
  1. package/.github/workflows/engine-cyberia.cd.yml +6 -0
  2. package/.github/workflows/npmpkg.ci.yml +1 -0
  3. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  4. package/.github/workflows/release.cd.yml +1 -0
  5. package/.vscode/extensions.json +9 -9
  6. package/.vscode/settings.json +20 -4
  7. package/CHANGELOG.md +213 -1
  8. package/CLI-HELP.md +92 -23
  9. package/README.md +190 -348
  10. package/bin/build.js +24 -8
  11. package/bin/build.template.js +187 -0
  12. package/bin/cyberia.js +229 -52
  13. package/bin/deploy.js +12 -2
  14. package/bin/index.js +229 -52
  15. package/bump.config.js +26 -0
  16. package/conf.js +130 -24
  17. package/deployment.yaml +4 -2
  18. package/hardhat/package-lock.json +113 -144
  19. package/hardhat/package.json +4 -3
  20. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  21. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  22. package/manifests/deployment/dd-cyberia-development/deployment.yaml +4 -2
  23. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  24. package/manifests/deployment/dd-test-development/deployment.yaml +4 -2
  25. package/manifests/kind-config-dev.yaml +8 -0
  26. package/manifests/lxd/lxd-admin-profile.yaml +12 -3
  27. package/manifests/mongodb/pv-pvc.yaml +44 -8
  28. package/manifests/mongodb/statefulset.yaml +55 -68
  29. package/manifests/mongodb-4.4/headless-service.yaml +10 -0
  30. package/manifests/mongodb-4.4/kustomization.yaml +3 -1
  31. package/manifests/mongodb-4.4/mongodb-nodeport.yaml +17 -0
  32. package/manifests/mongodb-4.4/pv-pvc.yaml +10 -14
  33. package/manifests/mongodb-4.4/statefulset.yaml +79 -0
  34. package/manifests/mongodb-4.4/storage-class.yaml +9 -0
  35. package/manifests/valkey/statefulset.yaml +1 -1
  36. package/manifests/valkey/valkey-nodeport.yaml +17 -0
  37. package/package.json +27 -15
  38. package/scripts/ipxe-setup.sh +52 -49
  39. package/scripts/k3s-node-setup.sh +81 -46
  40. package/scripts/lxd-vm-setup.sh +193 -8
  41. package/scripts/maas-nat-firewalld.sh +145 -0
  42. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +38 -33
  43. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +16 -16
  44. package/src/api/core/core.router.js +19 -14
  45. package/src/api/core/core.service.js +5 -5
  46. package/src/api/crypto/crypto.router.js +18 -12
  47. package/src/api/crypto/crypto.service.js +3 -3
  48. package/src/api/cyberia-action/cyberia-action.model.js +1 -1
  49. package/src/api/cyberia-action/cyberia-action.router.js +22 -18
  50. package/src/api/cyberia-action/cyberia-action.service.js +5 -5
  51. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +74 -0
  52. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +99 -0
  53. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +98 -0
  54. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +152 -0
  55. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +25 -20
  56. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +6 -6
  57. package/src/api/cyberia-entity/cyberia-entity.router.js +22 -18
  58. package/src/api/cyberia-entity/cyberia-entity.service.js +5 -5
  59. package/src/api/cyberia-instance/cyberia-fallback-world.js +79 -4
  60. package/src/api/cyberia-instance/cyberia-instance.router.js +57 -52
  61. package/src/api/cyberia-instance/cyberia-instance.service.js +10 -10
  62. package/src/api/cyberia-instance/cyberia-world-generator.js +3 -3
  63. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +14 -48
  64. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +22 -18
  65. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +5 -5
  66. package/src/api/cyberia-map/cyberia-map.router.js +35 -30
  67. package/src/api/cyberia-map/cyberia-map.service.js +7 -7
  68. package/src/api/cyberia-quest/cyberia-quest.model.js +1 -1
  69. package/src/api/cyberia-quest/cyberia-quest.router.js +22 -18
  70. package/src/api/cyberia-quest/cyberia-quest.service.js +5 -5
  71. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +22 -18
  72. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +5 -5
  73. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +451 -0
  74. package/src/api/default/default.router.js +22 -18
  75. package/src/api/default/default.service.js +5 -5
  76. package/src/api/document/document.router.js +28 -23
  77. package/src/api/document/document.service.js +100 -23
  78. package/src/api/file/file.router.js +19 -13
  79. package/src/api/file/file.service.js +9 -7
  80. package/src/api/instance/instance.router.js +29 -24
  81. package/src/api/instance/instance.service.js +6 -6
  82. package/src/api/ipfs/ipfs.router.js +21 -16
  83. package/src/api/ipfs/ipfs.service.js +8 -8
  84. package/src/api/object-layer/object-layer.router.js +512 -507
  85. package/src/api/object-layer/object-layer.service.js +17 -14
  86. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +22 -18
  87. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +5 -5
  88. package/src/api/test/test.router.js +17 -12
  89. package/src/api/types.js +24 -0
  90. package/src/api/user/guest.service.js +5 -4
  91. package/src/api/user/user.router.js +297 -288
  92. package/src/api/user/user.service.js +100 -35
  93. package/src/cli/baremetal.js +132 -101
  94. package/src/cli/cluster.js +700 -232
  95. package/src/cli/db.js +59 -60
  96. package/src/cli/deploy.js +216 -137
  97. package/src/cli/fs.js +13 -3
  98. package/src/cli/index.js +80 -15
  99. package/src/cli/ipfs.js +4 -6
  100. package/src/cli/kubectl.js +4 -1
  101. package/src/cli/lxd.js +1099 -223
  102. package/src/cli/monitor.js +9 -3
  103. package/src/cli/release.js +334 -140
  104. package/src/cli/repository.js +68 -23
  105. package/src/cli/run.js +193 -49
  106. package/src/cli/secrets.js +11 -2
  107. package/src/cli/test.js +9 -3
  108. package/src/client/Default.index.js +9 -3
  109. package/src/client/components/core/Auth.js +5 -0
  110. package/src/client/components/core/ClientEvents.js +76 -0
  111. package/src/client/components/core/EventBus.js +4 -0
  112. package/src/client/components/core/Modal.js +82 -41
  113. package/src/client/components/core/PanelForm.js +56 -52
  114. package/src/client/components/core/Worker.js +162 -363
  115. package/src/client/components/cyberia/MapEngineCyberia.js +1 -1
  116. package/src/client/components/cyberia/SharedDefaultsCyberia.js +330 -0
  117. package/src/client/public/cyberia-docs/ARCHITECTURE.md +50 -410
  118. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +114 -327
  119. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +200 -222
  120. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +203 -185
  121. package/src/client/public/cyberia-docs/CYBERIA.md +259 -0
  122. package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +2 -2
  123. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  124. package/src/client/public/cyberia-docs/UNDERPOST-PLATFORM.md +106 -0
  125. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  126. package/src/client/services/cyberia-client-hints/cyberia-client-hints.service.js +99 -0
  127. package/src/client/ssr/views/CyberiaServerMetrics.js +982 -0
  128. package/src/client/sw/core.sw.js +174 -112
  129. package/src/db/DataBaseProvider.js +115 -15
  130. package/src/db/mariadb/MariaDB.js +2 -1
  131. package/src/db/mongo/MongoBootstrap.js +657 -0
  132. package/src/db/mongo/MongooseDB.js +129 -21
  133. package/src/grpc/cyberia/grpc-server.js +25 -57
  134. package/src/index.js +1 -1
  135. package/src/runtime/cyberia-client/Dockerfile +24 -3
  136. package/src/runtime/cyberia-client/Dockerfile.dev +82 -0
  137. package/src/runtime/cyberia-server/Dockerfile +29 -4
  138. package/src/runtime/cyberia-server/Dockerfile.dev +71 -0
  139. package/src/runtime/express/Express.js +2 -2
  140. package/src/runtime/wp/Wp.js +8 -5
  141. package/src/server/auth.js +2 -2
  142. package/src/server/client-build-docs.js +1 -1
  143. package/src/server/client-build.js +94 -129
  144. package/src/server/conf.js +86 -83
  145. package/src/server/process.js +180 -19
  146. package/src/server/proxy.js +9 -2
  147. package/src/server/runtime.js +1 -1
  148. package/src/server/start.js +17 -5
  149. package/src/server/valkey.js +2 -0
  150. package/src/ws/IoInterface.js +16 -16
  151. package/src/ws/core/channels/core.ws.chat.js +11 -11
  152. package/src/ws/core/channels/core.ws.mailer.js +29 -29
  153. package/src/ws/core/channels/core.ws.stream.js +19 -19
  154. package/src/ws/core/core.ws.connection.js +8 -8
  155. package/src/ws/core/core.ws.server.js +6 -5
  156. package/src/ws/default/channels/default.ws.main.js +10 -10
  157. package/src/ws/default/default.ws.connection.js +4 -4
  158. package/src/ws/default/default.ws.server.js +4 -3
  159. package/bin/file.js +0 -202
  160. package/bin/vs.js +0 -74
  161. package/bin/zed.js +0 -84
  162. package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +0 -574
  163. package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +0 -467
  164. package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
  165. package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
  166. package/src/client/ssr/pages/CyberiaServerMetrics.js +0 -461
  167. /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
  168. /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
  169. /package/src/client/ssr/{pages → views}/Test.js +0 -0
@@ -56,10 +56,11 @@ class WpService {
56
56
  * to `/usr/local/bin/wp` if it is not already present.
57
57
  */
58
58
  static ensureWpCli() {
59
- const existing = shellExec(`PATH="${LAMPP_BIN}:$PATH" which wp 2>/dev/null || true`, {
59
+ const existing = shellExec(`PATH="${LAMPP_BIN}:$PATH" which wp`, {
60
60
  stdout: true,
61
61
  silent: true,
62
62
  disableLog: true,
63
+ silentOnError: true,
63
64
  });
64
65
  if (existing && existing.trim()) return;
65
66
  logger.info('WP-CLI not found — installing to /usr/local/bin/wp');
@@ -76,10 +77,11 @@ class WpService {
76
77
  */
77
78
  static ensureSendmail() {
78
79
  const sendmailPath = '/usr/sbin/sendmail';
79
- const existing = shellExec(`test -x "${sendmailPath}" && echo ok || true`, {
80
+ const existing = shellExec(`test -x "${sendmailPath}" && echo ok`, {
80
81
  stdout: true,
81
82
  silent: true,
82
83
  disableLog: true,
84
+ silentOnError: true,
83
85
  });
84
86
  if (existing && existing.trim() === 'ok') return;
85
87
  logger.info('sendmail stub missing — creating no-op at /usr/sbin/sendmail');
@@ -494,7 +496,7 @@ Thumbs.db
494
496
  * `git clone` yields a fully working site without needing a fresh install.
495
497
  *
496
498
  * Safe to call repeatedly — `git commit` is a no-op when the working tree
497
- * is clean (`|| true` prevents non-zero exit).
499
+ * is clean (`silentOnError: true` swallows the non-zero exit gracefully).
498
500
  *
499
501
  * @param {object} opts
500
502
  * @param {string} opts.siteRoot - Absolute path to the WordPress root.
@@ -514,7 +516,8 @@ Thumbs.db
514
516
 
515
517
  logger.info(`${host}: persisting site to repository`);
516
518
  shellExec(
517
- `cd "${siteRoot}" && git add -A && git commit -m "wp provision ${host} $(date -u +%Y-%m-%dT%H:%M:%SZ)" || true`,
519
+ `cd "${siteRoot}" && git add -A && git commit -m "wp provision ${host} $(date -u +%Y-%m-%dT%H:%M:%SZ)"`,
520
+ { silentOnError: true },
518
521
  );
519
522
  shellExec(`cd "${siteRoot}" && underpost push . ${githubOrg}/${repoName} -f`);
520
523
  logger.info(`${host}: initial commit pushed to ${githubOrg}/${repoName}`);
@@ -627,7 +630,7 @@ if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROT
627
630
 
628
631
  // MariaDB export is handled by the shared db.js backup flow — no duplicate dump here.
629
632
  if (fs.existsSync(path.join(siteRoot, '.git'))) {
630
- shellExec(`cd "${siteRoot}" && git add -A && git commit -m "wp backup $(date -u +%Y-%m-%dT%H:%M:%SZ)" || true`);
633
+ shellExec(`cd "${siteRoot}" && git add -A && git commit -m "wp backup $(date -u +%Y-%m-%dT%H:%M:%SZ)"`, { silentOnError: true });
631
634
  shellExec(`cd "${siteRoot}" && underpost push . ${githubOrg}/${repository.split('/').pop().split('.')[0]}`);
632
635
  logger.info(`backup: git push done for ${siteRoot}`);
633
636
  } else {
@@ -20,7 +20,7 @@ import rateLimit from 'express-rate-limit';
20
20
  import slowDown from 'express-slow-down';
21
21
  import cors from 'cors';
22
22
  import cookieParser from 'cookie-parser';
23
- import { DataBaseProvider } from '../db/DataBaseProvider.js';
23
+ import { DataBaseProviderService } from '../db/DataBaseProvider.js';
24
24
  import { isDevProxyContext } from './conf.js';
25
25
 
26
26
  const logger = loggerFactory(import.meta);
@@ -229,7 +229,7 @@ const authMiddlewareFactory = (options = { host: '', path: '' }) => {
229
229
 
230
230
  // Non-guest verify session exists
231
231
  if (payload.jwtid && payload.role !== 'guest') {
232
- const User = DataBaseProvider.instance[`${payload.host}${payload.path}`].mongoose.models.User;
232
+ const User = DataBaseProviderService.getModel('user', { host: payload.host, path: payload.path });
233
233
  const user = await User.findOne({ _id: payload._id, 'activeSessions._id': payload.jwtid }).lean();
234
234
 
235
235
  if (!user) {
@@ -399,7 +399,7 @@ const buildCoverage = async ({ docs, docsDestination }) => {
399
399
  shellExec(`cd ${coveragePath} && npm run coverage`, { silent: true });
400
400
  } else if (pkg.scripts && pkg.scripts.test) {
401
401
  logger.info('generating coverage via test', coveragePath);
402
- shellExec(`cd ${coveragePath} && npm test`, { silent: true });
402
+ shellExec(`cd ${coveragePath} && npm test`, { silent: true, silentOnError: true });
403
403
  }
404
404
  }
405
405
  }
@@ -724,23 +724,24 @@ const buildClient = async (
724
724
  const ssrPath = path === '/' ? path : `${path}/`;
725
725
  const Render = await ssrFactory();
726
726
 
727
- if (views) {
728
- const jsSrcPath = `./src/client/sw/core.sw.js`;
729
-
730
- const jsPublicPath = `${rootClientPath}/sw.js`;
731
-
732
- if (!(enableLiveRebuild && !options.liveClientBuildPaths.find((p) => p.srcBuildPath === jsSrcPath))) {
733
- const jsSrc = await transformClientJs(jsSrcPath, {
734
- dists,
735
- proxyPath: path,
736
- baseHost,
737
- minify: minifyBuild,
738
- externalizeBareImports: false,
739
- });
740
-
741
- fs.writeFileSync(jsPublicPath, jsSrc, 'utf8');
742
- }
727
+ const swSrcPath = `./src/client/sw/core.sw.js`;
728
+ const swPublicPath = `${rootClientPath}/sw.js`;
729
+ const swShouldRebuild =
730
+ views && !(enableLiveRebuild && !options.liveClientBuildPaths.find((p) => p.srcBuildPath === swSrcPath));
731
+ // Transformed SW JS is held in memory; it gets prepended with renderPayload
732
+ // and written once below, after PRE_CACHED_RESOURCES are known.
733
+ let swTransformedJs = '';
734
+ if (swShouldRebuild) {
735
+ swTransformedJs = await transformClientJs(swSrcPath, {
736
+ dists,
737
+ proxyPath: path,
738
+ baseHost,
739
+ minify: minifyBuild,
740
+ externalizeBareImports: false,
741
+ });
742
+ }
743
743
 
744
+ if (views) {
744
745
  if (
745
746
  !(
746
747
  enableLiveRebuild &&
@@ -750,9 +751,8 @@ const buildClient = async (
750
751
  )
751
752
  )
752
753
  for (const view of views) {
753
- const buildPath = `${
754
- rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath
755
- }${view.path === '/' ? view.path : `${view.path}/`}`;
754
+ const buildPath = `${rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath
755
+ }${view.path === '/' ? view.path : `${view.path}/`}`;
756
756
 
757
757
  if (!fs.existsSync(buildPath)) fs.mkdirSync(buildPath, { recursive: true });
758
758
 
@@ -768,9 +768,8 @@ const buildClient = async (
768
768
  fs.writeFileSync(`${buildPath}${buildId}.js`, jsSrc, 'utf8');
769
769
  const title = metadata.title ? metadata.title : title;
770
770
 
771
- const canonicalURL = `https://${host}${path}${
772
- view.path === '/' ? (path === '/' ? '' : '/') : path === '/' ? `${view.path.slice(1)}/` : `${view.path}/`
773
- }`;
771
+ const canonicalURL = `https://${host}${path}${view.path === '/' ? (path === '/' ? '' : '/') : path === '/' ? `${view.path.slice(1)}/` : `${view.path}/`
772
+ }`;
774
773
 
775
774
  let ssrHeadComponents = ``;
776
775
  let ssrBodyComponents = ``;
@@ -904,12 +903,12 @@ const buildClient = async (
904
903
  `${buildPath}index.html`,
905
904
  minifyBuild
906
905
  ? await minify(htmlSrc, {
907
- minifyCSS: true,
908
- minifyJS: true,
909
- collapseBooleanAttributes: true,
910
- collapseInlineTagWhitespace: true,
911
- collapseWhitespace: true,
912
- })
906
+ minifyCSS: true,
907
+ minifyJS: true,
908
+ collapseBooleanAttributes: true,
909
+ collapseInlineTagWhitespace: true,
910
+ collapseWhitespace: true,
911
+ })
913
912
  : htmlSrc,
914
913
  'utf8',
915
914
  );
@@ -967,119 +966,85 @@ Sitemap: ${sitemapBaseUrl}/sitemap.xml`,
967
966
  }
968
967
 
969
968
  if (client) {
970
- let PRE_CACHED_RESOURCES = [];
971
-
972
- const normalizePrecacheRoutePath = (candidatePath) => {
973
- const routePath =
974
- typeof candidatePath === 'string' && candidatePath.trim().length > 0 ? candidatePath.trim() : '/offline';
975
- const withLeadingSlash = routePath.startsWith('/') ? routePath : `/${routePath}`;
976
- const withoutTrailingSlash = withLeadingSlash.replace(/\/+$/, '');
977
- return withoutTrailingSlash.length > 0 ? withoutTrailingSlash : '/';
978
- };
979
-
980
- const toPrecacheIndexUrl = (routePath) => {
981
- const normalizedRoutePath = normalizePrecacheRoutePath(routePath);
982
- return `${path === '/' ? '' : path}${normalizedRoutePath === '/' ? '' : normalizedRoutePath}/index.html`;
983
- };
984
-
985
- for (const pageType of ['offline', 'pages']) {
986
- if (confSSR[getCapVariableName(client)] && confSSR[getCapVariableName(client)][pageType]) {
987
- for (const page of confSSR[getCapVariableName(client)][pageType]) {
988
- const SsrComponent = await ssrFactory(`./src/client/ssr/${pageType}/${page.client}.js`);
989
-
990
- const htmlSrc = Render({
991
- title: page.title,
992
- ssrPath,
993
- ssrHeadComponents: '<base target="_top">',
994
- ssrBodyComponents: SsrComponent(),
995
- renderPayload: {
996
- apiBaseProxyPath,
997
- apiBaseHost,
998
- apiBasePath: process.env.BASE_API,
999
- version: Underpost.version,
1000
- ...(isDevelopment ? { dev: true } : undefined),
1001
- },
1002
- renderApi: {
1003
- JSONweb,
1004
- },
1005
- });
1006
-
1007
- const buildPath = `${
1008
- rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath
1009
- }${page.path === '/' ? page.path : `${page.path}/`}`;
1010
-
1011
- // Install-time precache is intentionally restricted to SSR offline pages.
1012
- // All other routes/assets are loaded lazily at runtime.
1013
- if (pageType === 'offline') {
1014
- PRE_CACHED_RESOURCES.push(toPrecacheIndexUrl(page.path));
1015
- }
1016
-
1017
- if (!fs.existsSync(buildPath)) fs.mkdirSync(buildPath, { recursive: true });
1018
-
1019
- const buildHtmlPath = `${buildPath}index.html`;
1020
-
1021
- logger.info('ssr page build', buildHtmlPath);
1022
-
1023
- fs.writeFileSync(
1024
- buildHtmlPath,
1025
- minifyBuild
1026
- ? await minify(htmlSrc, {
1027
- minifyCSS: true,
1028
- minifyJS: true,
1029
- collapseBooleanAttributes: true,
1030
- collapseInlineTagWhitespace: true,
1031
- collapseWhitespace: true,
1032
- })
1033
- : htmlSrc,
1034
- 'utf8',
1035
- );
1036
- }
1037
- }
1038
- }
969
+ const proxyPrefix = path === '/' ? '' : path;
970
+ const buildIndexUrl = (routePath) => `${proxyPrefix}${routePath === '/' ? '' : routePath}/index.html`;
971
+
972
+ // SSR views: a single declarative array. The role of each view (regular
973
+ // page vs. offline/maintenance fallback) is expressed by per-entry flags;
974
+ // fallback-flagged views are also precached so the SW can serve them
975
+ // when the network is unreachable.
976
+ const ssrClientConf = confSSR[getCapVariableName(client)] || {};
977
+ const ssrViews = Array.isArray(ssrClientConf.views) ? ssrClientConf.views : [];
978
+ const PRE_CACHED_RESOURCES = [];
979
+ let offlineFallbackUrl = null;
980
+ let maintenanceFallbackUrl = null;
981
+
982
+ for (const view of ssrViews) {
983
+ const SsrComponent = await ssrFactory(`./src/client/ssr/views/${view.client}.js`);
984
+
985
+ const htmlSrc = Render({
986
+ title: view.title,
987
+ ssrPath,
988
+ ssrHeadComponents: '<base target="_top">',
989
+ ssrBodyComponents: SsrComponent(),
990
+ renderPayload: {
991
+ apiBaseProxyPath,
992
+ apiBaseHost,
993
+ apiBasePath: process.env.BASE_API,
994
+ version: Underpost.version,
995
+ ...(isDevelopment ? { dev: true } : undefined),
996
+ },
997
+ renderApi: { JSONweb },
998
+ });
1039
999
 
1040
- {
1041
- const cacheScope = path === '/' ? 'root' : path.replaceAll('/', '_');
1042
- const ssrClientConf = confSSR[getCapVariableName(client)] || {};
1043
- const ssrOfflinePages = Array.isArray(ssrClientConf.offline) ? ssrClientConf.offline : [];
1044
- const normalizeSsrRoutePath = (candidatePath, fallbackPath) => {
1045
- const value =
1046
- typeof candidatePath === 'string' && candidatePath.trim().length > 0
1047
- ? candidatePath.trim()
1048
- : fallbackPath;
1049
- const withLeadingSlash = value.startsWith('/') ? value : `/${value}`;
1050
- const withoutTrailingSlash = withLeadingSlash.replace(/\/+$/, '');
1051
- return withoutTrailingSlash.length > 0 ? withoutTrailingSlash : '/';
1052
- };
1000
+ const buildPath = `${rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath
1001
+ }${view.path === '/' ? view.path : `${view.path}/`}`;
1053
1002
 
1054
- const offlineSsrPage =
1055
- ssrOfflinePages.find(
1056
- (page) =>
1057
- page?.client === 'NoNetworkConnection' ||
1058
- /no\s*network|offline/i.test(`${page?.title || ''} ${page?.client || ''} ${page?.path || ''}`),
1059
- ) || ssrOfflinePages[0];
1003
+ const indexUrl = buildIndexUrl(view.path);
1004
+ if (view.offlineDefault) {
1005
+ offlineFallbackUrl = indexUrl;
1006
+ PRE_CACHED_RESOURCES.push(indexUrl);
1007
+ }
1008
+ if (view.maintenanceDefault) {
1009
+ maintenanceFallbackUrl = indexUrl;
1010
+ PRE_CACHED_RESOURCES.push(indexUrl);
1011
+ }
1060
1012
 
1061
- const maintenanceSsrPage =
1062
- ssrOfflinePages.find(
1063
- (page) =>
1064
- page?.client === 'Maintenance' ||
1065
- /maintenance/i.test(`${page?.title || ''} ${page?.client || ''} ${page?.path || ''}`),
1066
- ) || ssrOfflinePages[1];
1013
+ if (!fs.existsSync(buildPath)) fs.mkdirSync(buildPath, { recursive: true });
1014
+ const buildHtmlPath = `${buildPath}index.html`;
1015
+ logger.info('ssr view build', buildHtmlPath);
1067
1016
 
1068
- const offlinePath = normalizeSsrRoutePath(offlineSsrPage?.path, '/offline');
1069
- const maintenancePath = normalizeSsrRoutePath(maintenanceSsrPage?.path, '/maintenance');
1017
+ fs.writeFileSync(
1018
+ buildHtmlPath,
1019
+ minifyBuild
1020
+ ? await minify(htmlSrc, {
1021
+ minifyCSS: true,
1022
+ minifyJS: true,
1023
+ collapseBooleanAttributes: true,
1024
+ collapseInlineTagWhitespace: true,
1025
+ collapseWhitespace: true,
1026
+ })
1027
+ : htmlSrc,
1028
+ 'utf8',
1029
+ );
1030
+ }
1070
1031
 
1032
+ if (swShouldRebuild) {
1033
+ const cacheScope = path === '/' ? 'root' : path.replaceAll('/', '_');
1071
1034
  const renderPayload = {
1072
1035
  PRE_CACHED_RESOURCES: uniqueArray(PRE_CACHED_RESOURCES),
1073
1036
  PROXY_PATH: path,
1074
- CACHE_PREFIX: `engine-core-v3-${cacheScope}`,
1075
- OFFLINE_PATH: offlinePath,
1076
- MAINTENANCE_PATH: maintenancePath,
1037
+ CACHE_PREFIX: `engine-core-${cacheScope}`,
1038
+ OFFLINE_URL: offlineFallbackUrl || buildIndexUrl('/offline'),
1039
+ MAINTENANCE_URL: maintenanceFallbackUrl || buildIndexUrl('/maintenance'),
1077
1040
  };
1041
+
1042
+ // Single write: prepend the payload prelude to the transformed SW JS.
1078
1043
  fs.writeFileSync(
1079
- `${rootClientPath}/sw.js`,
1044
+ swPublicPath,
1080
1045
  `self.renderPayload = ${JSONweb(renderPayload)};
1081
1046
  self.__WB_DISABLE_DEV_LOGS = true;
1082
- ${fs.readFileSync(`${rootClientPath}/sw.js`, 'utf8')}`,
1047
+ ${swTransformedJs}`,
1083
1048
  'utf8',
1084
1049
  );
1085
1050
  }
@@ -41,6 +41,20 @@ const logger = loggerFactory(import.meta);
41
41
  */
42
42
  const ENV_REF_PREFIX = 'env:';
43
43
 
44
+ /**
45
+ * Resolves a standardized context key from host/path descriptors.
46
+ * The key is used across DB, WS, mailer, and cache registries.
47
+ *
48
+ * @method resolveHostKeyContext
49
+ * @param {{host?: string, path?: string}|string} [context={ host: '', path: '' }] - Context object or prebuilt key.
50
+ * @returns {string} Host key context string.
51
+ * @memberof ServerConfBuilder
52
+ */
53
+ const resolveHostKeyContext = (context = { host: '', path: '' }) => {
54
+ if (typeof context === 'string') return context;
55
+ return `${context.host || ''}${context.path || ''}`;
56
+ };
57
+
44
58
  /**
45
59
  * Recursively walks a configuration object and replaces every string value that
46
60
  * starts with {@link ENV_REF_PREFIX} (`"env:"`) with the corresponding
@@ -320,25 +334,29 @@ const Config = {
320
334
 
321
335
  if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
322
336
 
323
- const envTemplate = fs.existsSync('./.env.example')
337
+ const sharedEnvTemplate = fs.existsSync('./.env.example')
324
338
  ? fs.readFileSync('./.env.example', 'utf8')
325
339
  : fs.existsSync('./.env.production')
326
340
  ? fs.readFileSync('./.env.production', 'utf8')
327
341
  : '';
328
342
 
329
- if (envTemplate) {
330
- const prodEnv = envTemplate.replaceAll('dd-default', deployId);
331
- fs.writeFileSync(`${folder}/.env.production`, prodEnv, 'utf8');
332
- fs.writeFileSync(
333
- `${folder}/.env.development`,
334
- prodEnv.replace('NODE_ENV=production', 'NODE_ENV=development').replace('PORT=3000', 'PORT=4000'),
335
- 'utf8',
336
- );
337
- fs.writeFileSync(
338
- `${folder}/.env.test`,
339
- prodEnv.replace('NODE_ENV=production', 'NODE_ENV=test').replace('PORT=3000', 'PORT=5000'),
340
- 'utf8',
341
- );
343
+ const envTemplates = {
344
+ production: fs.existsSync('./.env.production') ? fs.readFileSync('./.env.production', 'utf8') : sharedEnvTemplate,
345
+ development: fs.existsSync('./.env.development')
346
+ ? fs.readFileSync('./.env.development', 'utf8')
347
+ : sharedEnvTemplate
348
+ ? sharedEnvTemplate.replace('NODE_ENV=production', 'NODE_ENV=development').replace('PORT=3000', 'PORT=4000')
349
+ : '',
350
+ test: fs.existsSync('./.env.test')
351
+ ? fs.readFileSync('./.env.test', 'utf8')
352
+ : sharedEnvTemplate
353
+ ? sharedEnvTemplate.replace('NODE_ENV=production', 'NODE_ENV=test').replace('PORT=3000', 'PORT=5000')
354
+ : '',
355
+ };
356
+
357
+ for (const [envName, envTemplate] of Object.entries(envTemplates)) {
358
+ if (!envTemplate) continue;
359
+ fs.writeFileSync(`${folder}/.env.${envName}`, envTemplate.replaceAll('dd-default', deployId), 'utf8');
342
360
  }
343
361
 
344
362
  fs.writeFileSync(
@@ -1208,7 +1226,11 @@ const validateTemplatePath = (absolutePath = '') => {
1208
1226
  const confSsr = DefaultConf.ssr[ssr];
1209
1227
  const clients = DefaultConf.client.default.services;
1210
1228
 
1211
- if (absolutePath.match('src/api') && !confServer.apis.find((p) => absolutePath.match(`src/api/${p}/`))) {
1229
+ if (
1230
+ absolutePath.match('src/api') &&
1231
+ !absolutePath.match('src/api/types.js') &&
1232
+ !confServer.apis.find((p) => absolutePath.match(`src/api/${p}/`))
1233
+ ) {
1212
1234
  return false;
1213
1235
  }
1214
1236
  if (absolutePath.match('conf.dd-') && absolutePath.match('.js')) return false;
@@ -1250,14 +1272,8 @@ const validateTemplatePath = (absolutePath = '') => {
1250
1272
  return false;
1251
1273
  }
1252
1274
  if (
1253
- absolutePath.match('src/client/ssr/offline') &&
1254
- !confSsr.offline.find((p) => absolutePath.match(`src/client/ssr/offline/${p.client}.js`))
1255
- ) {
1256
- return false;
1257
- }
1258
- if (
1259
- absolutePath.match('src/client/ssr/pages') &&
1260
- !confSsr.pages.find((p) => absolutePath.match(`src/client/ssr/pages/${p.client}.js`))
1275
+ absolutePath.match('src/client/ssr/views') &&
1276
+ !(confSsr.views || []).find((p) => absolutePath.match(`src/client/ssr/views/${p.client}.js`))
1261
1277
  ) {
1262
1278
  return false;
1263
1279
  }
@@ -1279,15 +1295,17 @@ const validateTemplatePath = (absolutePath = '') => {
1279
1295
  /**
1280
1296
  * @method awaitDeployMonitor
1281
1297
  * @description Waits for the deploy monitor.
1282
- * @param {boolean} [init=false] - The init flag.
1298
+ * @param {boolean} [isFinal=false] - If true, logs when the final (non-replica) deployment completes.
1283
1299
  * @param {number} [deltaMs=1000] - The delta ms.
1284
1300
  * @returns {Promise<void>} - The await deploy monitor.
1285
1301
  * @memberof ServerConfBuilder
1286
1302
  */
1287
- const awaitDeployMonitor = async (init = false, deltaMs = 1000) => {
1288
- if (init) Underpost.env.set('await-deploy', new Date().toISOString());
1303
+ const awaitDeployMonitor = async (isFinal = false, deltaMs = 1000) => {
1304
+ Underpost.env.set('await-deploy', new Date().toISOString());
1305
+ if (isFinal) logger.info('Final deployment running (no replica)');
1289
1306
  await timer(deltaMs);
1290
- if (Underpost.env.get('await-deploy')) return await awaitDeployMonitor();
1307
+ if (Underpost.env.get('container-status') === 'error') throw new Error('Container status error');
1308
+ if (Underpost.env.get('await-deploy')) return await awaitDeployMonitor(isFinal, deltaMs);
1291
1309
  };
1292
1310
 
1293
1311
  /**
@@ -1312,59 +1330,6 @@ const mergeFile = async (parts = [], outputFilePath) => {
1312
1330
  });
1313
1331
  };
1314
1332
 
1315
- /**
1316
- * @method rebuildConfFactory
1317
- * @description Rebuilds the conf factory.
1318
- * @param {object} options - The options.
1319
- * @param {string} options.deployId - The deploy ID.
1320
- * @param {string} options.valkey - The valkey.
1321
- * @param {boolean} [options.mongo=false] - The mongo.
1322
- * @returns {object} - The rebuild conf factory.
1323
- * @memberof ServerConfBuilder
1324
- */
1325
- const rebuildConfFactory = ({ deployId, valkey, mongo }) => {
1326
- const confServer = loadReplicas(deployId, loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`));
1327
- const hosts = {};
1328
- for (const host of Object.keys(confServer)) {
1329
- hosts[host] = {};
1330
- for (const path of Object.keys(confServer[host])) {
1331
- if (!confServer[host][path].db) continue;
1332
- const { singleReplica, replicas, db } = confServer[host][path];
1333
- const { provider } = db;
1334
- if (singleReplica) {
1335
- for (const replica of replicas) {
1336
- const deployIdReplica = buildReplicaId({ replica, deployId });
1337
- const confServerReplica = loadConfServerJson(`./engine-private/replica/${deployIdReplica}/conf.server.json`);
1338
- for (const _host of Object.keys(confServerReplica)) {
1339
- for (const _path of Object.keys(confServerReplica[_host])) {
1340
- hosts[host][_path] = { replica: { host, path } };
1341
- confServerReplica[_host][_path].valkey = valkey;
1342
- switch (provider) {
1343
- case 'mongoose':
1344
- confServerReplica[_host][_path].db.host = mongo.host;
1345
- break;
1346
- }
1347
- }
1348
- }
1349
- fs.writeFileSync(
1350
- `./engine-private/replica/${deployIdReplica}/conf.server.json`,
1351
- JSON.stringify(confServerReplica, null, 4),
1352
- 'utf8',
1353
- );
1354
- }
1355
- } else hosts[host][path] = {};
1356
- confServer[host][path].valkey = valkey;
1357
- switch (provider) {
1358
- case 'mongoose':
1359
- confServer[host][path].db.host = mongo.host;
1360
- break;
1361
- }
1362
- }
1363
- }
1364
- fs.writeFileSync(`./engine-private/conf/${deployId}/conf.server.json`, JSON.stringify(confServer, null, 4), 'utf8');
1365
- return { hosts };
1366
- };
1367
-
1368
1333
  /**
1369
1334
  * @method getPathsSSR
1370
1335
  * @description Gets the paths SSR.
@@ -1377,8 +1342,7 @@ const getPathsSSR = (conf) => {
1377
1342
  for (const o of conf.head) paths.push(`src/client/ssr/head/${o}.js`);
1378
1343
  for (const o of conf.body) paths.push(`src/client/ssr/body/${o}.js`);
1379
1344
  for (const o of Object.keys(conf.mailer)) paths.push(`src/client/ssr/mailer/${conf.mailer[o]}.js`);
1380
- for (const o of conf.offline) paths.push(`src/client/ssr/mailer/${o.client}.js`);
1381
- for (const o of conf.pages) paths.push(`src/client/ssr/pages/${o.client}.js`);
1345
+ for (const o of conf.views || []) paths.push(`src/client/ssr/views/${o.client}.js`);
1382
1346
  return paths;
1383
1347
  };
1384
1348
 
@@ -1746,6 +1710,44 @@ const loadConfServerJson = (jsonPath, options) => {
1746
1710
  return options && options.resolve === true ? resolveConfSecrets(raw) : raw;
1747
1711
  };
1748
1712
 
1713
+ /**
1714
+ * Creates and writes the /etc/hosts file for a deployment.
1715
+ * @method etcHostFactory
1716
+ * @param {Array<string>} hosts - List of hosts to be added to the hosts file.
1717
+ * @param {object} options - Options for the hosts file creation.
1718
+ * @param {boolean} options.append - Whether to append to the existing hosts file.
1719
+ * @returns {object} - Object containing the rendered hosts file.
1720
+ * @memberof ServerConfBuilder
1721
+ */
1722
+ const etcHostFactory = (hosts = [], options = { append: false }) => {
1723
+ hosts = hosts.map((host) => {
1724
+ try {
1725
+ if (!host.startsWith('http')) host = `http://${host}`;
1726
+ const hostname = new URL(host).hostname;
1727
+ logger.info('Hostname extract valid', { host, hostname });
1728
+ return hostname;
1729
+ } catch (e) {
1730
+ logger.warn('No hostname extract valid', host);
1731
+ return host;
1732
+ }
1733
+ });
1734
+ const renderHosts = `127.0.0.1 ${hosts.join(
1735
+ ' ',
1736
+ )} localhost localhost.localdomain localhost4 localhost4.localdomain4
1737
+ ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6`;
1738
+
1739
+ if (options && options.append && fs.existsSync(`/etc/hosts`)) {
1740
+ fs.writeFileSync(
1741
+ `/etc/hosts`,
1742
+ fs.readFileSync(`/etc/hosts`, 'utf8') +
1743
+ `
1744
+ ${renderHosts}`,
1745
+ 'utf8',
1746
+ );
1747
+ } else fs.writeFileSync(`/etc/hosts`, renderHosts, 'utf8');
1748
+ return { renderHosts };
1749
+ };
1750
+
1749
1751
  export {
1750
1752
  Config,
1751
1753
  loadConf,
@@ -1774,7 +1776,6 @@ export {
1774
1776
  pathPortAssignmentFactory,
1775
1777
  deployRangePortFactory,
1776
1778
  awaitDeployMonitor,
1777
- rebuildConfFactory,
1778
1779
  buildCliDoc,
1779
1780
  getInstanceContext,
1780
1781
  buildApiConf,
@@ -1784,6 +1785,7 @@ export {
1784
1785
  devProxyHostFactory,
1785
1786
  isTlsDevProxy,
1786
1787
  getTlsHosts,
1788
+ resolveHostKeyContext,
1787
1789
  resolveConfSecrets,
1788
1790
  loadConfServerJson,
1789
1791
  getConfFolder,
@@ -1792,4 +1794,5 @@ export {
1792
1794
  DEFAULT_DEPLOY_ID,
1793
1795
  loadCronDeployEnv,
1794
1796
  cronDeployIdResolve,
1797
+ etcHostFactory,
1795
1798
  };