@underpostnet/cyberia 3.2.90 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/.env.example +16 -0
  2. package/.github/workflows/coverall.cyberia.ci.yml +87 -0
  3. package/.github/workflows/cyberia-client.cd.yml +1 -18
  4. package/.github/workflows/cyberia-server.cd.yml +1 -18
  5. package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
  6. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
  7. package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
  8. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
  9. package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
  10. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
  11. package/.github/workflows/engine-cyberia.cd.yml +3 -78
  12. package/.github/workflows/ghpkg.ci.yml +7 -1
  13. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
  14. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  15. package/.github/workflows/release.cd.yml +1 -9
  16. package/CHANGELOG.md +110 -1
  17. package/CLI-HELP.md +139 -9
  18. package/Dockerfile +1 -1
  19. package/Dockerfile.dev +1 -1
  20. package/Dockerfile.test +1 -1
  21. package/bin/build.js +7 -5
  22. package/bin/cyberia.js +73 -53
  23. package/bin/deploy.js +1 -1
  24. package/bin/index.js +73 -53
  25. package/compose.env +16 -0
  26. package/conf.js +2 -0
  27. package/deploy/dd-cyberia/deploy.sh +42 -0
  28. package/deploy/dd-cyberia/init.sh +63 -0
  29. package/deploy/dd-cyberia/sync-deploy.sh +194 -0
  30. package/deploy/lib/logging.sh +96 -0
  31. package/deploy/pwa-microservices-template/deploy.sh +72 -0
  32. package/deploy/release/deploy.sh +62 -0
  33. package/deployment.yaml +1 -1
  34. package/docker-compose.yml +31 -37
  35. package/hardhat/package-lock.json +9 -9
  36. package/hardhat/package.json +3 -3
  37. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -1
  38. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  39. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +52 -0
  40. package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
  41. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  42. package/package.json +2 -2
  43. package/scripts/audit-selinux.sh +64 -0
  44. package/scripts/coverall-test-cyberia.sh +24 -0
  45. package/scripts/coverall-test.sh +24 -0
  46. package/scripts/gpu-diag.sh +0 -0
  47. package/scripts/ip-info.sh +0 -0
  48. package/scripts/k3s-node-setup.sh +18 -15
  49. package/scripts/kubeadm-node-setup.sh +12 -23
  50. package/scripts/link-local-underpost-cli.sh +0 -0
  51. package/scripts/lxd-vm-setup.sh +0 -0
  52. package/scripts/maas-nat-firewalld.sh +0 -0
  53. package/scripts/nat-iptables.sh +2 -0
  54. package/scripts/rhel-grpc-setup.sh +0 -0
  55. package/scripts/rocky-kickstart.sh +25 -9
  56. package/scripts/test-monitor.sh +1 -1
  57. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +63 -0
  58. package/src/api/cyberia-instance/cyberia-fallback-world.js +8 -1
  59. package/src/api/cyberia-instance/cyberia-instance.controller.js +3 -0
  60. package/src/api/cyberia-instance/cyberia-instance.router.js +13 -0
  61. package/src/api/cyberia-instance/cyberia-instance.service.js +43 -0
  62. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +1 -0
  63. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +16 -3
  64. package/src/api/object-layer/object-layer.model.js +204 -3
  65. package/src/api/object-layer/object-layer.service.js +1 -14
  66. package/src/cli/baremetal.js +1 -2
  67. package/src/cli/cloud-init.js +1 -1
  68. package/src/cli/cluster.js +73 -68
  69. package/src/cli/db.js +9 -2
  70. package/src/cli/deploy.js +21 -5
  71. package/src/cli/docker-compose.js +1 -1
  72. package/src/cli/env.js +1 -1
  73. package/src/cli/image.js +0 -1
  74. package/src/cli/index.js +121 -9
  75. package/src/cli/lxd.js +1 -1
  76. package/src/cli/monitor.js +1 -1
  77. package/src/cli/release.js +57 -22
  78. package/src/cli/repository.js +11 -9
  79. package/src/cli/run.js +36 -9
  80. package/src/cli/ssh.js +198 -77
  81. package/src/cli/system.js +26 -13
  82. package/src/cli/test.js +1 -1
  83. package/src/cli/vultr.js +583 -0
  84. package/src/cli/wireguard.js +2125 -0
  85. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +368 -0
  86. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +33 -0
  87. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +4 -0
  88. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
  89. package/src/client/public/cyberia-docs/ARCHITECTURE.md +17 -3
  90. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +1 -1
  91. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  92. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  93. package/src/client/services/cyberia-instance/cyberia-instance.service.js +40 -0
  94. package/src/client/services/object-layer/object-layer.management.js +4 -4
  95. package/src/client-builder/client-build.js +20 -14
  96. package/src/db/mongo/MongooseDB.js +4 -0
  97. package/src/index.js +25 -1
  98. package/src/projects/cyberia/catalog-cyberia.js +5 -0
  99. package/src/projects/cyberia/generate-saga.js +14 -23
  100. package/src/projects/cyberia/instance-data.js +21 -2
  101. package/src/projects/cyberia/object-layer.js +11 -21
  102. package/src/projects/underpost/catalog-underpost.js +4 -1
  103. package/src/runtime/cyberia-client/Dockerfile +1 -1
  104. package/src/runtime/cyberia-client/Dockerfile.dev +1 -1
  105. package/src/runtime/cyberia-server/Dockerfile +1 -1
  106. package/src/runtime/cyberia-server/Dockerfile.dev +1 -1
  107. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  108. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  109. package/src/runtime/engine-cyberia/Dockerfile.test +1 -1
  110. package/src/runtime/engine-cyberia/compose.env +16 -0
  111. package/src/runtime/engine-cyberia/docker-compose.yml +31 -37
  112. package/src/server/backup.js +1 -1
  113. package/src/server/conf.js +18 -108
  114. package/src/server/cron.js +249 -51
  115. package/src/server/dns.js +100 -6
  116. package/src/server/environment.js +98 -0
  117. package/src/server/forward-proxy.js +549 -0
  118. package/src/server/middlewares.js +56 -1
  119. package/src/server/process.js +0 -1
  120. package/src/server/selinux.js +185 -0
  121. package/src/server/systemd.js +205 -0
  122. package/src/server/underpost-compression.js +186 -0
  123. package/src/server/underpost-gateway.js +20 -10
  124. package/src/server/underpost-ingress.js +18 -2
  125. package/test/cyberia-instance-conf-defaults.test.js +1 -0
  126. package/test/selinux.test.js +71 -0
  127. package/test/underpost-gateway.test.js +41 -0
  128. package/test/underpost-ingress.test.js +52 -0
  129. package/test/wireguard-edge.test.js +1177 -0
@@ -525,6 +525,7 @@ const defaultSitemapXsl = `<?xml version="1.0" encoding="UTF-8"?>
525
525
  * @param {string|number} options.split - Optional zip split size in MB.
526
526
  * @param {boolean} options.fullBuild - Whether to perform a full build.
527
527
  * @param {boolean} options.iconsBuild - Whether to build icons.
528
+ * @param {boolean} options.ssrOnly - Whether to rebuild only views defined in `conf.ssr.json`.
528
529
  * @returns {Promise<void>} - Promise that resolves when the build is complete.
529
530
  * @throws {Error} - If the build fails.
530
531
  * @memberof clientBuild
@@ -548,6 +549,7 @@ const buildClient = async (
548
549
  const packageData = JSON.parse(fs.readFileSync(`./package.json`, 'utf8'));
549
550
  const acmeChallengePath = `/.well-known/acme-challenge`;
550
551
  const publicPath = `./public`;
552
+ const ssrOnly = options.ssrOnly === true;
551
553
 
552
554
  /**
553
555
  * @async
@@ -691,11 +693,11 @@ const buildClient = async (
691
693
  ? `${directory}${acmeChallengePath}`
692
694
  : `${publicPath}/${host}${acmeChallengePath}`;
693
695
 
694
- if (!enableLiveRebuild) buildAcmeChallengePath(acmeChallengeFullPath);
696
+ if (!enableLiveRebuild && !ssrOnly) buildAcmeChallengePath(acmeChallengeFullPath);
695
697
 
696
698
  if (redirect || disabledRebuild) continue;
697
699
 
698
- if (fullBuildEnabled)
700
+ if (fullBuildEnabled && !ssrOnly)
699
701
  await fullBuild({
700
702
  path,
701
703
  logger,
@@ -710,7 +712,7 @@ const buildClient = async (
710
712
  publicCopyNonExistingFiles,
711
713
  });
712
714
 
713
- if (components)
715
+ if (!ssrOnly && components)
714
716
  for (const module of Object.keys(components)) {
715
717
  if (!fs.existsSync(`${rootClientPath}/components/${module}`))
716
718
  fs.mkdirSync(`${rootClientPath}/components/${module}`, { recursive: true });
@@ -733,7 +735,7 @@ const buildClient = async (
733
735
  }
734
736
  }
735
737
 
736
- if (services) {
738
+ if (!ssrOnly && services) {
737
739
  for (const module of services) {
738
740
  if (!fs.existsSync(`${rootClientPath}/services/${module}`))
739
741
  fs.mkdirSync(`${rootClientPath}/services/${module}`, { recursive: true });
@@ -796,7 +798,9 @@ const buildClient = async (
796
798
  const swSrcPath = `./src/client/sw/core.sw.js`;
797
799
  const swPublicPath = `${rootClientPath}/sw.js`;
798
800
  const swShouldRebuild =
799
- views && !(enableLiveRebuild && !options.liveClientBuildPaths.find((p) => p.srcBuildPath === swSrcPath));
801
+ !ssrOnly &&
802
+ views &&
803
+ !(enableLiveRebuild && !options.liveClientBuildPaths.find((p) => p.srcBuildPath === swSrcPath));
800
804
  // Transformed SW JS is held in memory; it gets prepended with renderPayload
801
805
  // and written once below, after PRE_CACHED_RESOURCES are known.
802
806
  let swTransformedJs = '';
@@ -810,13 +814,15 @@ const buildClient = async (
810
814
  });
811
815
  }
812
816
 
813
- if (views) {
814
- if (!(
815
- enableLiveRebuild &&
816
- !options.liveClientBuildPaths.find(
817
- (p) => p.srcBuildPath.startsWith(`./src/client/ssr`) || p.srcBuildPath.slice(-9) === '.index.js',
817
+ if (!ssrOnly && views) {
818
+ if (
819
+ !(
820
+ enableLiveRebuild &&
821
+ !options.liveClientBuildPaths.find(
822
+ (p) => p.srcBuildPath.startsWith(`./src/client/ssr`) || p.srcBuildPath.slice(-9) === '.index.js',
823
+ )
818
824
  )
819
- ))
825
+ )
820
826
  for (const view of views) {
821
827
  const buildPath = `${
822
828
  rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath
@@ -983,7 +989,7 @@ const buildClient = async (
983
989
  );
984
990
  }
985
991
  }
986
- if (!enableLiveRebuild && siteMapLinks.length > 0) {
992
+ if (!ssrOnly && !enableLiveRebuild && siteMapLinks.length > 0) {
987
993
  const hasSitemapTemplate = fs.existsSync(`${rootClientPath}/sitemap`);
988
994
  const sitemapBaseUrl = `https://${host}${path === '/' ? '' : path}`;
989
995
  // Create a stream to write to — omit xslUrl so we can inject a relative href below
@@ -1020,7 +1026,7 @@ Sitemap: ${sitemapBaseUrl}/sitemap.xml`,
1020
1026
  );
1021
1027
  }
1022
1028
 
1023
- if (fullBuildEnabled && docs) {
1029
+ if (!ssrOnly && fullBuildEnabled && docs) {
1024
1030
  await buildDocs({
1025
1031
  host,
1026
1032
  path,
@@ -1128,7 +1134,7 @@ ${swTransformedJs}`,
1128
1134
  );
1129
1135
  }
1130
1136
  }
1131
- if (!enableLiveRebuild && options.buildZip) {
1137
+ if (!ssrOnly && !enableLiveRebuild && options.buildZip) {
1132
1138
  logger.warn('build zip', rootClientPath);
1133
1139
 
1134
1140
  if (!fs.existsSync('./build')) fs.mkdirSync('./build');
@@ -186,6 +186,10 @@ class MongooseDBService {
186
186
  const { ProviderSchema } = await import(`../../api/${api}/${api}.model.js`);
187
187
  const keyModel = getCapVariableName(api); // Assuming this returns a capitalized model name
188
188
  models[keyModel] = conn.model(keyModel, ProviderSchema);
189
+ // Mongoose emits 'error' on the model when an autoIndex build fails; with no
190
+ // listener attached EventEmitter rethrows and takes the whole process down.
191
+ // A stale or conflicting index must degrade to a log, not kill startup.
192
+ models[keyModel].on('error', (error) => logger.error(`${keyModel} index build failed: ${error.message}`));
189
193
  }
190
194
 
191
195
  return models;
package/src/index.js CHANGED
@@ -26,6 +26,8 @@ import UnderpostStatic from './cli/static.js';
26
26
  import UnderpostTest from './cli/test.js';
27
27
  import UnderpostRelease from './cli/release.js';
28
28
  import UnderpostSystemProvisionig from './cli/system.js';
29
+ import UnderpostVultr from './cli/vultr.js';
30
+ import UnderpostWireguard from './cli/wireguard.js';
29
31
 
30
32
  import UnderpostDns from './server/dns.js';
31
33
  import UnderpostBackup from './server/backup.js';
@@ -45,7 +47,7 @@ class Underpost {
45
47
  * @type {String}
46
48
  * @memberof Underpost
47
49
  */
48
- static version = 'v3.2.90';
50
+ static version = 'v3.3.0';
49
51
 
50
52
  /**
51
53
  * Required Node.js major version
@@ -252,6 +254,26 @@ class Underpost {
252
254
  return UnderpostSystemProvisionig.API;
253
255
  }
254
256
 
257
+ /**
258
+ * Edge hub WireGuard/HAProxy cli API
259
+ * @static
260
+ * @type {UnderpostWireguard.API}
261
+ * @memberof Underpost
262
+ */
263
+ static get wireguard() {
264
+ return UnderpostWireguard.API;
265
+ }
266
+
267
+ /**
268
+ * Vultr bandwidth guard cli API
269
+ * @static
270
+ * @type {UnderpostVultr.API}
271
+ * @memberof Underpost
272
+ */
273
+ static get vultr() {
274
+ return UnderpostVultr.API;
275
+ }
276
+
255
277
  /**
256
278
  * Dns server API
257
279
  * @static
@@ -352,6 +374,8 @@ export {
352
374
  UnderpostStartUp,
353
375
  UnderpostRelease,
354
376
  UnderpostTLS,
377
+ UnderpostVultr,
378
+ UnderpostWireguard,
355
379
  };
356
380
 
357
381
  export default Underpost;
@@ -48,6 +48,8 @@ export default {
48
48
  '/.github/workflows/docker-image.cyberia-server.dev.ci.yml',
49
49
  '/.github/workflows/cyberia-client.cd.yml',
50
50
  '/.github/workflows/cyberia-server.cd.yml',
51
+ '/.github/workflows/coverall.cyberia.ci.yml',
52
+ '/scripts/coverall-test-cyberia.sh',
51
53
  ],
52
54
  stripPaths: [
53
55
  './src/projects/cyberia',
@@ -56,8 +58,11 @@ export default {
56
58
  './src/runtime/cyberia-client',
57
59
  './src/runtime/engine-cyberia',
58
60
  './test/cyberia-instance-conf-defaults.test.js',
61
+ './test/cyberia-load.test.js',
62
+ './test/object-layer-item-id.test.js',
59
63
  './test/shape-generator.test.js',
60
64
  './src/client/public/cyberia-docs',
65
+ './scripts/coverall-test-cyberia.sh',
61
66
  'bin/cyberia.js',
62
67
  './hardhat',
63
68
  ],
@@ -18,7 +18,6 @@ import fs from 'fs-extra';
18
18
  import nodePath from 'path';
19
19
  import crypto from 'crypto';
20
20
  import { GeminiClient } from './gemini-client.js';
21
- import { computeSha256 } from './object-layer.js';
22
21
  import { loggerFactory } from '../../server/logger.js';
23
22
 
24
23
  const logger = loggerFactory(import.meta);
@@ -1632,13 +1631,13 @@ function normalizeSagaPayload(raw, { theme }) {
1632
1631
 
1633
1632
  /**
1634
1633
  * Persist normalized object-layer items into the ObjectLayer collection so they
1635
- * are editable in the viewer. Items are created with an empty (`null`) render
1636
- * and an `OFF_CHAIN` ledger; a render/ledger can be set later from
1634
+ * are editable in the viewer. New items get an empty render and an `OFF_CHAIN`
1635
+ * ledger; both can be set later from the object-layer import or from
1637
1636
  * `src/client/components/cyberia/ObjectLayerEngineViewer.js`.
1638
1637
  *
1639
- * Upserts by `data.item.id`. An existing item's render and ledger are PRESERVED
1640
- * (never clobbered back to null) only the textual stats/item fields are
1641
- * refreshed. `sha256` is recomputed over the resulting data.
1638
+ * Upserts through `ObjectLayer.upsertByItemId`, so `data.item.id` stays unique
1639
+ * and an existing item's render, ledger and CIDs are PRESERVED only the
1640
+ * textual stats/item fields are refreshed, and `sha256` is recomputed.
1642
1641
  *
1643
1642
  * @async
1644
1643
  * @param {Object} params
@@ -1647,28 +1646,20 @@ function normalizeSagaPayload(raw, { theme }) {
1647
1646
  * @returns {Promise<number>} Count of items created or updated.
1648
1647
  */
1649
1648
  async function persistObjectLayers({ objectLayers, ObjectLayer }) {
1649
+ await ObjectLayer.ensureUniqueItemIdIndex();
1650
+
1650
1651
  let count = 0;
1651
1652
  for (const ol of objectLayers) {
1652
1653
  const itemId = ol.item?.id;
1653
1654
  if (!itemId) continue;
1654
1655
 
1655
- const existing = await ObjectLayer.findOne({ 'data.item.id': itemId });
1656
-
1657
- // Preserve any render already set via the viewer; default to empty render.
1658
- const hasRender = existing?.data?.render?.cid || existing?.data?.render?.metadataCid;
1659
- const render = hasRender ? existing.data.render : { cid: null, metadataCid: null };
1660
- const ledger = existing?.data?.ledger?.type ? existing.data.ledger : { type: 'OFF_CHAIN', tokenId: '' };
1661
-
1662
- const data = { stats: ol.stats, item: ol.item, ledger, render };
1663
- const sha256 = computeSha256(data);
1664
-
1665
- if (existing) {
1666
- existing.data = data;
1667
- existing.sha256 = sha256;
1668
- await existing.save();
1669
- } else {
1670
- await ObjectLayer.create({ data, sha256, cid: null });
1671
- }
1656
+ // The saga only owns the textual/statistical fields. Ledger and render are
1657
+ // seeded on insert and never refreshed, so a re-import cannot clobber an
1658
+ // on-chain registration or a render produced by the object-layer import.
1659
+ await ObjectLayer.upsertByItemId(
1660
+ { data: { stats: ol.stats, item: ol.item } },
1661
+ { setOnInsert: { cid: null, data: { ledger: { type: 'OFF_CHAIN', tokenId: '' }, render: {} } } },
1662
+ );
1672
1663
  count++;
1673
1664
  }
1674
1665
  return count;
@@ -29,6 +29,7 @@ import {
29
29
  DefaultCyberiaItems,
30
30
  } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
31
31
  import { generateFallbackWorld } from '../../api/cyberia-instance/cyberia-fallback-world.js';
32
+ import { getFallbackDefaultItems } from '../../api/cyberia-instance/cyberia-fallback-default-items.js';
32
33
 
33
34
  const logger = loggerFactory(import.meta);
34
35
 
@@ -421,6 +422,7 @@ function toInstanceConfig(gc) {
421
422
  botAggroRange: gc.botAggroRange ?? fb.botAggroRange,
422
423
  defaultPlayerWidth: gc.defaultPlayerWidth ?? fb.defaultPlayerWidth,
423
424
  defaultPlayerHeight: gc.defaultPlayerHeight ?? fb.defaultPlayerHeight,
425
+ playerBaseSpeed: gc.playerBaseSpeed ?? fb.playerBaseSpeed,
424
426
  playerBaseLifeRegenMin: gc.playerBaseLifeRegenMin ?? fb.playerBaseLifeRegenMin,
425
427
  playerBaseLifeRegenMax: gc.playerBaseLifeRegenMax ?? fb.playerBaseLifeRegenMax,
426
428
  sumStatsLimit: gc.sumStatsLimit ?? fb.sumStatsLimit,
@@ -553,7 +555,7 @@ async function fetchFullInstance(models, requestedInstanceCode) {
553
555
  // ── Fallback: instance not found → return a multi-map procedural world ──
554
556
  if (!inst) {
555
557
  logger.info(`Instance "${instanceCode}" not found — returning fallback world.`);
556
- const world = generateFallbackWorld();
558
+ const world = generateFallbackWorld({ itemIds: getFallbackDefaultItems() });
557
559
  const fallbackConf = buildFallbackConfig();
558
560
 
559
561
  // Collect all objectLayerItemIds from the generated maps so the
@@ -599,6 +601,23 @@ async function fetchFullInstance(models, requestedInstanceCode) {
599
601
 
600
602
  const fallbackConfig = buildFallbackInstanceConfig(fallbackConf, world.instance.itemIds);
601
603
 
604
+ // Opaque version over everything mutable in this payload, mirroring the
605
+ // persisted path below. A constant here would make hot reload a silent
606
+ // no-op: WorldBuilder.ReloadWorld skips the rebuild — and therefore
607
+ // ApplyInstanceConfig — whenever the version is unchanged. The world
608
+ // geometry itself is deterministic (code defaults + seed), so the inputs
609
+ // that can actually differ are the staged default items and the
610
+ // ObjectLayer docs, which are the only things read from the DB here.
611
+ const fallbackVersionParts = ['fallback'];
612
+ for (const entry of world.instance.itemIds || []) {
613
+ fallbackVersionParts.push(`${entry.id}:${entry.defaultPlayerInventory ? 1 : 0}`);
614
+ }
615
+ for (const doc of fallbackOlDocs) fallbackVersionParts.push(String(doc.sha256 || doc._id));
616
+ const fallbackVersion = `fallback-${crypto
617
+ .createHash('sha256')
618
+ .update(fallbackVersionParts.join('|'))
619
+ .digest('hex')}`;
620
+
602
621
  return {
603
622
  instance: toInstanceMsg({ ...world.instance, _id: '', seed: instanceCode }),
604
623
  maps: world.maps.map((m) => ({
@@ -613,7 +632,7 @@ async function fetchFullInstance(models, requestedInstanceCode) {
613
632
  })),
614
633
  objectLayers: fallbackOlDocs.map(toObjectLayerMsg),
615
634
  config: fallbackConfig,
616
- version: 'fallback',
635
+ version: fallbackVersion,
617
636
  // Mission content for the procedural fallback comes straight from
618
637
  // the canonical defaults — no DB, fully self-contained.
619
638
  actions: DefaultCyberiaActions.map(toActionMsg),
@@ -10,9 +10,7 @@ import path from 'path';
10
10
  import { PNG } from 'pngjs';
11
11
  import sharp from 'sharp';
12
12
  import { Jimp, intToRGBA, rgbaToInt } from 'jimp';
13
- import crypto from 'crypto';
14
- import stringify from 'fast-json-stable-stringify';
15
-
13
+ import { computeObjectLayerSha256 } from '../../api/object-layer/object-layer.model.js';
16
14
  import { range } from '../../client/components/core/CommonJs.js';
17
15
  import {
18
16
  generateRandomStats,
@@ -450,7 +448,7 @@ export class ObjectLayerEngine {
450
448
  * @memberof CyberiaObjectLayer
451
449
  */
452
450
  static computeSha256(data) {
453
- return crypto.createHash('sha256').update(stringify(data)).digest('hex');
451
+ return computeObjectLayerSha256(data);
454
452
  }
455
453
 
456
454
  /**
@@ -642,23 +640,15 @@ export class ObjectLayerEngine {
642
640
  }
643
641
 
644
642
  // 4. Atomic create/upsert - ObjectLayer is fully populated with all CIDs
645
- let objectLayer;
646
- const existingByItemId = await ObjectLayer.findOne({ 'data.item.id': objectLayerData.data.item.id });
647
- if (existingByItemId) {
648
- const oldRenderFramesId = existingByItemId.objectLayerRenderFramesId;
649
- logger.info(
650
- `ObjectLayer for item "${objectLayerData.data.item.id}" exists (${existingByItemId._id}), replacing atomically...`,
651
- );
652
- objectLayer = await ObjectLayer.findByIdAndUpdate(existingByItemId._id, objectLayerData, {
653
- returnDocument: 'after',
654
- }).populate('objectLayerRenderFramesId');
655
- if (oldRenderFramesId && !oldRenderFramesId.equals(objectLayerRenderFramesDoc._id)) {
656
- await ObjectLayerRenderFrames.findByIdAndDelete(oldRenderFramesId);
657
- }
658
- } else {
659
- objectLayer = await (await ObjectLayer.create(objectLayerData)).populate('objectLayerRenderFramesId');
660
- logger.info(`ObjectLayer created successfully with id: ${objectLayer._id}`);
643
+ const existingByItemId = await ObjectLayer.findByItemId(objectLayerData.data.item.id);
644
+ const oldRenderFramesId = existingByItemId?.objectLayerRenderFramesId;
645
+
646
+ const objectLayer = await (await ObjectLayer.upsertByItemId(objectLayerData)).populate('objectLayerRenderFramesId');
647
+
648
+ if (oldRenderFramesId && !oldRenderFramesId.equals(objectLayerRenderFramesDoc._id)) {
649
+ await ObjectLayerRenderFrames.findByIdAndDelete(oldRenderFramesId);
661
650
  }
651
+ logger.info(`ObjectLayer for item "${objectLayerData.data.item.id}" persisted with id: ${objectLayer._id}`);
662
652
 
663
653
  return { objectLayer, objectLayerRenderFramesDoc };
664
654
  }
@@ -848,7 +838,7 @@ export class ObjectLayerEngine {
848
838
  * @memberof CyberiaObjectLayer
849
839
  */
850
840
  static async resolveCanonicalCid({ itemId, ObjectLayer, ipfsClient = null, options }) {
851
- const objectLayer = await ObjectLayer.findOne({ 'data.item.id': itemId });
841
+ const objectLayer = await ObjectLayer.findByItemId(itemId);
852
842
  if (!objectLayer) {
853
843
  throw new Error(`ObjectLayer "${itemId}" not found in database`);
854
844
  }
@@ -18,7 +18,10 @@ const TEMPLATE_RESTORE_PATHS = [
18
18
  `./.github/workflows/gitlab.ci.yml`,
19
19
  `./.github/workflows/publish.ci.yml`,
20
20
  `./.github/workflows/release.cd.yml`,
21
- `./src/client/services/user/guest.service.js`,
21
+ `./deploy/lib`,
22
+ './deploy/release',
23
+ './deploy/pwa-microservices-template',
24
+ './src/client/services/user/guest.service.js',
22
25
  './src/api/user/guest.service.js',
23
26
  './src/ws/IoInterface.js',
24
27
  './src/ws/IoServer.js',
@@ -14,7 +14,7 @@ RUN make -f Web.mk clean all BUILD_MODE=${BUILD_MODE} OUTPUT_DIR=bin/
14
14
 
15
15
  # --- Runtime Image ---
16
16
  FROM rockylinux/rockylinux:9 AS runtime
17
- ARG UNDERPOST_VERSION=3.2.90
17
+ ARG UNDERPOST_VERSION=3.3.0
18
18
  ARG NODE_VERSION=24.15.0
19
19
 
20
20
  RUN set -eux; \
@@ -43,7 +43,7 @@ RUN make -f Web.mk all BUILD_MODE=${BUILD_MODE} OUTPUT_DIR=bin/
43
43
  # --- Runtime Image ---
44
44
  FROM rockylinux/rockylinux:9 AS runtime
45
45
 
46
- ARG UNDERPOST_VERSION=3.2.90
46
+ ARG UNDERPOST_VERSION=3.3.0
47
47
  ARG NODE_VERSION=24.15.0
48
48
  RUN set -eux; \
49
49
  dnf -y update; \
@@ -11,7 +11,7 @@ RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o server ./cmd/cyberia-se
11
11
 
12
12
  # --- Runtime Image ---
13
13
  FROM rockylinux/rockylinux:9 AS runtime
14
- ARG UNDERPOST_VERSION=3.2.90
14
+ ARG UNDERPOST_VERSION=3.3.0
15
15
  ARG NODE_VERSION=24.15.0
16
16
 
17
17
  RUN set -eux; \
@@ -25,7 +25,7 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
25
25
  # --- Runtime Image ---
26
26
  FROM rockylinux/rockylinux:9 AS runtime
27
27
 
28
- ARG UNDERPOST_VERSION=3.2.90
28
+ ARG UNDERPOST_VERSION=3.3.0
29
29
  ARG NODE_VERSION=24.15.0
30
30
  RUN set -eux; \
31
31
  dnf -y update; \
@@ -6,7 +6,7 @@
6
6
  # Stage 1 — builder: clone the private deploy, build it, then scrub secrets.
7
7
  # ---------------------------------------------------------------------------
8
8
  FROM rockylinux/rockylinux:9 AS builder
9
- ARG UNDERPOST_VERSION=3.2.90
9
+ ARG UNDERPOST_VERSION=3.3.0
10
10
  # Pin Node to an exact patch: dnf's nodejs:24 module lags (24.14.1) while
11
11
  # underpost's dependencies require >=24.15.0, so install the official binary.
12
12
  ARG NODE_VERSION=24.15.0
@@ -6,7 +6,7 @@
6
6
  # Stage 1 — builder: clone the private deploy, build it, then scrub secrets.
7
7
  # ---------------------------------------------------------------------------
8
8
  FROM rockylinux/rockylinux:9 AS builder
9
- ARG UNDERPOST_VERSION=3.2.90
9
+ ARG UNDERPOST_VERSION=3.3.0
10
10
  # Pin Node to an exact patch: dnf's nodejs:24 module lags (24.14.1) while
11
11
  # underpost's dependencies require >=24.15.0, so install the official binary.
12
12
  ARG NODE_VERSION=24.15.0
@@ -6,7 +6,7 @@
6
6
  # Stage 1 — builder: clone the private deploy, build it, then scrub secrets.
7
7
  # ---------------------------------------------------------------------------
8
8
  FROM rockylinux/rockylinux:9 AS builder
9
- ARG UNDERPOST_VERSION=3.2.90
9
+ ARG UNDERPOST_VERSION=3.3.0
10
10
  # Pin Node to an exact patch: dnf's nodejs:24 module lags (24.14.1) while
11
11
  # underpost's dependencies require >=24.15.0, so install the official binary.
12
12
  ARG NODE_VERSION=24.15.0
@@ -70,6 +70,22 @@ CYBERIA_DEFAULT_INSTANCE=amethyst-strata-expansion
70
70
  CYBERIA_SERVER_APP_ENV=development
71
71
  CYBERIA_SERVER_LOG_LEVEL=
72
72
 
73
+ # --- cyberia-server WebSocket admission and rate limits ----------------------
74
+ # Unset keeps the built-in default. An explicit 0 turns that one limit off.
75
+ # The startup log prints the effective profile ("ws limits: ...").
76
+ #
77
+ # Set CYBERIA_DISABLE_CONNECTION_LIMITS=1 to turn every limit off. The load test
78
+ # (test/cyberia-load.test.js) drives every simulated client from one address, so
79
+ # it needs this. NEVER enable it on a public deploy.
80
+ CYBERIA_DISABLE_CONNECTION_LIMITS=
81
+ CYBERIA_MAX_CONNECTIONS=
82
+ CYBERIA_MAX_CONNECTIONS_PER_IP=
83
+ CYBERIA_CONNECT_RATE_PER_IP=
84
+ CYBERIA_CONNECT_BURST_PER_IP=
85
+ CYBERIA_MESSAGE_RATE=
86
+ CYBERIA_MESSAGE_BURST=
87
+ CYBERIA_MAX_STRIKES=
88
+
73
89
  # --- engine-cyberia runtime env (loaded into the container via env_file) -----
74
90
  # The dd-cyberia engine (engine-private/conf/dd-cyberia/conf.server.json) needs
75
91
  # these to boot: DB auth + per-site DB names, the base PORT (the cyberia host is
@@ -4,36 +4,6 @@
4
4
  # (MongoDB + Valkey + IPFS), then the Cyberia content-authority engine and its
5
5
  # per-instance server/client tiers, fronted by an nginx reverse proxy.
6
6
  #
7
- # Canonical location (custom docker-compose workflow):
8
- # engine-private/conf/dd-cyberia/docker-compose/cyberia/
9
- # ├── docker-compose.yml (this file — the ENTIRE stack, single source)
10
- # ├── compose.env (working env-file, used as-is — not generated)
11
- # ├── nginx.conf (custom per-variant sub-path routing)
12
- # └── mongodb/entrypoint.sh (replica-set bootstrap)
13
- # All bind mounts below are relative to this directory: the CLI runs compose with
14
- # `--project-directory` pinned here (see src/cli/docker-compose.js).
15
- #
16
- # Multi-instance: one engine-cyberia (content + asset authority) feeds three
17
- # cyberia-server + three cyberia-client tiers, one per conf.instances.json
18
- # multiInstance variant (amethyst-strata-expansion "/", FOREST "/FOREST",
19
- # TEST "/TEST"). This custom workflow owns nginx.conf and the services below;
20
- # keep both in sync when its instance topology changes.
21
- #
22
- # Startup is a strict health-gated chain: the stable gateway starts after the
23
- # engine, then each server/client pipeline advances independently:
24
- # mongodb + valkey -> engine runtime -> proxy -> 3x server -> 3x client
25
- #
26
- # Architecture boundaries (see CLAUDE.md "Cyberia architecture boundaries"):
27
- # engine-cyberia — content, validation, persistence, gRPC(:50051)+REST(:4005)
28
- # data services, asset metadata. The authority layer.
29
- # cyberia-server — authoritative real-time simulation / tick processing (Go).
30
- # cyberia-client — rendering, input, prediction, presentation (WASM bundle).
31
- #
32
- # Usage:
33
- # underpost docker-compose --up --deploy-id dd-cyberia --docker-compose-id cyberia
34
- # (or the docker:cyberia:* npm scripts). The engine-cyberia image bakes the
35
- # dd-cyberia build at image-build time, so no private-repo credentials are
36
- # required at container start.
37
7
 
38
8
  name: dd-cyberia
39
9
 
@@ -165,7 +135,7 @@ services:
165
135
  # server starts but rejects connections until seeded). Serves REST/WS on 4005
166
136
  # and the engine gRPC data service on 50051.
167
137
  engine-cyberia-runtime:
168
- image: ${ENGINE_CYBERIA_IMAGE:-underpost/engine-cyberia-dev}:${ENGINE_CYBERIA_TAG:-v3.2.90}
138
+ image: ${ENGINE_CYBERIA_IMAGE:-underpost/engine-cyberia-dev}:${ENGINE_CYBERIA_TAG:-v3.3.0}
169
139
  container_name: dd-cyberia-engine
170
140
  command:
171
141
  - /bin/sh
@@ -220,7 +190,7 @@ services:
220
190
  # gateway on host port 8081, so one port can route every instance path.
221
191
 
222
192
  cyberia-server-runtime:
223
- image: ${CYBERIA_SERVER_IMAGE:-underpost/cyberia-server-dev}:${CYBERIA_SERVER_TAG:-v3.2.90}
193
+ image: ${CYBERIA_SERVER_IMAGE:-underpost/cyberia-server-dev}:${CYBERIA_SERVER_TAG:-v3.3.0}
224
194
  container_name: dd-cyberia-server
225
195
  environment:
226
196
  INSTANCE_CODE: amethyst-strata-expansion
@@ -232,6 +202,14 @@ services:
232
202
  STATIC_DIR: ${CYBERIA_SERVER_STATIC_DIR:-/home/dd/engine/cyberia-server/public}
233
203
  APP_ENV: ${CYBERIA_SERVER_APP_ENV:-development}
234
204
  LOG_LEVEL: ${CYBERIA_SERVER_LOG_LEVEL:-}
205
+ CYBERIA_DISABLE_CONNECTION_LIMITS: ${CYBERIA_DISABLE_CONNECTION_LIMITS:-}
206
+ CYBERIA_MAX_CONNECTIONS: ${CYBERIA_MAX_CONNECTIONS:-}
207
+ CYBERIA_MAX_CONNECTIONS_PER_IP: ${CYBERIA_MAX_CONNECTIONS_PER_IP:-}
208
+ CYBERIA_CONNECT_RATE_PER_IP: ${CYBERIA_CONNECT_RATE_PER_IP:-}
209
+ CYBERIA_CONNECT_BURST_PER_IP: ${CYBERIA_CONNECT_BURST_PER_IP:-}
210
+ CYBERIA_MESSAGE_RATE: ${CYBERIA_MESSAGE_RATE:-}
211
+ CYBERIA_MESSAGE_BURST: ${CYBERIA_MESSAGE_BURST:-}
212
+ CYBERIA_MAX_STRIKES: ${CYBERIA_MAX_STRIKES:-}
235
213
  networks:
236
214
  - dd-cyberia-internal
237
215
  expose:
@@ -248,7 +226,7 @@ services:
248
226
  restart: unless-stopped
249
227
 
250
228
  cyberia-server-forest:
251
- image: ${CYBERIA_SERVER_IMAGE:-underpost/cyberia-server-dev}:${CYBERIA_SERVER_TAG:-v3.2.90}
229
+ image: ${CYBERIA_SERVER_IMAGE:-underpost/cyberia-server-dev}:${CYBERIA_SERVER_TAG:-v3.3.0}
252
230
  container_name: dd-cyberia-server-forest
253
231
  environment:
254
232
  INSTANCE_CODE: FOREST
@@ -260,6 +238,14 @@ services:
260
238
  STATIC_DIR: ${CYBERIA_SERVER_STATIC_DIR:-/home/dd/engine/cyberia-server/public}
261
239
  APP_ENV: ${CYBERIA_SERVER_APP_ENV:-development}
262
240
  LOG_LEVEL: ${CYBERIA_SERVER_LOG_LEVEL:-}
241
+ CYBERIA_DISABLE_CONNECTION_LIMITS: ${CYBERIA_DISABLE_CONNECTION_LIMITS:-}
242
+ CYBERIA_MAX_CONNECTIONS: ${CYBERIA_MAX_CONNECTIONS:-}
243
+ CYBERIA_MAX_CONNECTIONS_PER_IP: ${CYBERIA_MAX_CONNECTIONS_PER_IP:-}
244
+ CYBERIA_CONNECT_RATE_PER_IP: ${CYBERIA_CONNECT_RATE_PER_IP:-}
245
+ CYBERIA_CONNECT_BURST_PER_IP: ${CYBERIA_CONNECT_BURST_PER_IP:-}
246
+ CYBERIA_MESSAGE_RATE: ${CYBERIA_MESSAGE_RATE:-}
247
+ CYBERIA_MESSAGE_BURST: ${CYBERIA_MESSAGE_BURST:-}
248
+ CYBERIA_MAX_STRIKES: ${CYBERIA_MAX_STRIKES:-}
263
249
  networks:
264
250
  - dd-cyberia-internal
265
251
  expose:
@@ -276,7 +262,7 @@ services:
276
262
  restart: unless-stopped
277
263
 
278
264
  cyberia-server-test:
279
- image: ${CYBERIA_SERVER_IMAGE:-underpost/cyberia-server-dev}:${CYBERIA_SERVER_TAG:-v3.2.90}
265
+ image: ${CYBERIA_SERVER_IMAGE:-underpost/cyberia-server-dev}:${CYBERIA_SERVER_TAG:-v3.3.0}
280
266
  container_name: dd-cyberia-server-test
281
267
  environment:
282
268
  INSTANCE_CODE: TEST
@@ -288,6 +274,14 @@ services:
288
274
  STATIC_DIR: ${CYBERIA_SERVER_STATIC_DIR:-/home/dd/engine/cyberia-server/public}
289
275
  APP_ENV: ${CYBERIA_SERVER_APP_ENV:-development}
290
276
  LOG_LEVEL: ${CYBERIA_SERVER_LOG_LEVEL:-}
277
+ CYBERIA_DISABLE_CONNECTION_LIMITS: ${CYBERIA_DISABLE_CONNECTION_LIMITS:-}
278
+ CYBERIA_MAX_CONNECTIONS: ${CYBERIA_MAX_CONNECTIONS:-}
279
+ CYBERIA_MAX_CONNECTIONS_PER_IP: ${CYBERIA_MAX_CONNECTIONS_PER_IP:-}
280
+ CYBERIA_CONNECT_RATE_PER_IP: ${CYBERIA_CONNECT_RATE_PER_IP:-}
281
+ CYBERIA_CONNECT_BURST_PER_IP: ${CYBERIA_CONNECT_BURST_PER_IP:-}
282
+ CYBERIA_MESSAGE_RATE: ${CYBERIA_MESSAGE_RATE:-}
283
+ CYBERIA_MESSAGE_BURST: ${CYBERIA_MESSAGE_BURST:-}
284
+ CYBERIA_MAX_STRIKES: ${CYBERIA_MAX_STRIKES:-}
291
285
  networks:
292
286
  - dd-cyberia-internal
293
287
  expose:
@@ -315,7 +309,7 @@ services:
315
309
  # cyberia-server-test -> cyberia-client-test
316
310
 
317
311
  cyberia-client-runtime:
318
- image: ${CYBERIA_CLIENT_IMAGE:-underpost/cyberia-client-dev}:${CYBERIA_CLIENT_TAG:-v3.2.90}
312
+ image: ${CYBERIA_CLIENT_IMAGE:-underpost/cyberia-client-dev}:${CYBERIA_CLIENT_TAG:-v3.3.0}
319
313
  container_name: dd-cyberia-client
320
314
  environment:
321
315
  CYBERIA_PORT: '8081'
@@ -340,7 +334,7 @@ services:
340
334
  restart: unless-stopped
341
335
 
342
336
  cyberia-client-forest:
343
- image: ${CYBERIA_CLIENT_IMAGE:-underpost/cyberia-client-dev}:${CYBERIA_CLIENT_TAG:-v3.2.90}
337
+ image: ${CYBERIA_CLIENT_IMAGE:-underpost/cyberia-client-dev}:${CYBERIA_CLIENT_TAG:-v3.3.0}
344
338
  container_name: dd-cyberia-client-forest
345
339
  environment:
346
340
  CYBERIA_PORT: '8081'
@@ -365,7 +359,7 @@ services:
365
359
  restart: unless-stopped
366
360
 
367
361
  cyberia-client-test:
368
- image: ${CYBERIA_CLIENT_IMAGE:-underpost/cyberia-client-dev}:${CYBERIA_CLIENT_TAG:-v3.2.90}
362
+ image: ${CYBERIA_CLIENT_IMAGE:-underpost/cyberia-client-dev}:${CYBERIA_CLIENT_TAG:-v3.3.0}
369
363
  container_name: dd-cyberia-client-test
370
364
  environment:
371
365
  CYBERIA_PORT: '8081'
@@ -7,7 +7,7 @@
7
7
  import fs from 'fs-extra';
8
8
  import { loggerFactory } from './logger.js';
9
9
  import Underpost from '../index.js';
10
- import { loadCronDeployEnv } from './conf.js';
10
+ import { loadCronDeployEnv } from './cron.js';
11
11
 
12
12
  const logger = loggerFactory(import.meta);
13
13