@underpostnet/cyberia 3.2.80 → 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 (162) hide show
  1. package/.env.example +50 -15
  2. package/.github/workflows/coverall.cyberia.ci.yml +87 -0
  3. package/.github/workflows/cyberia-client.cd.yml +2 -7
  4. package/.github/workflows/cyberia-server.cd.yml +2 -7
  5. package/.github/workflows/docker-image.cyberia-client.ci.yml +4 -4
  6. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +4 -4
  7. package/.github/workflows/docker-image.cyberia-server.ci.yml +4 -4
  8. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +4 -4
  9. package/.github/workflows/docker-image.engine-cyberia.ci.yml +3 -3
  10. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +3 -3
  11. package/.github/workflows/engine-cyberia.cd.yml +3 -67
  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 +291 -1
  17. package/CLI-HELP.md +174 -23
  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 +272 -98
  23. package/bin/deploy.js +19 -17
  24. package/bin/index.js +272 -98
  25. package/compose.env +50 -15
  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 -210
  34. package/docker-compose.yml +90 -85
  35. package/hardhat/package-lock.json +139 -131
  36. package/hardhat/package.json +4 -4
  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 -210
  41. package/manifests/deployment/dd-cyberia-development/gateway.yaml +80 -0
  42. package/manifests/deployment/dd-cyberia-development/httproute.yaml +504 -0
  43. package/manifests/deployment/dd-cyberia-development/proxy.yaml +12 -12
  44. package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +0 -82
  45. package/manifests/deployment/dd-cyberia-development/traffic-service.yaml +121 -0
  46. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  47. package/manifests/deployment/playwright/deployment.yaml +1 -1
  48. package/manifests/mongodb/kustomization.yaml +4 -1
  49. package/manifests/mongodb/statefulset.yaml +4 -0
  50. package/manifests/mongodb/storage-class.yaml +9 -2
  51. package/nginx.conf +86 -16
  52. package/package.json +2 -2
  53. package/proxy.yaml +12 -12
  54. package/pv-pvc.yaml +0 -82
  55. package/scripts/audit-selinux.sh +64 -0
  56. package/scripts/coverall-test-cyberia.sh +24 -0
  57. package/scripts/coverall-test.sh +24 -0
  58. package/scripts/gpu-diag.sh +0 -0
  59. package/scripts/ip-info.sh +0 -0
  60. package/scripts/k3s-node-setup.sh +18 -15
  61. package/scripts/kubeadm-node-setup.sh +12 -23
  62. package/scripts/link-local-underpost-cli.sh +0 -0
  63. package/scripts/lxd-vm-setup.sh +0 -0
  64. package/scripts/maas-nat-firewalld.sh +0 -0
  65. package/scripts/nat-iptables.sh +12 -4
  66. package/scripts/rhel-grpc-setup.sh +0 -0
  67. package/scripts/rocky-kickstart.sh +25 -9
  68. package/scripts/test-monitor.sh +4 -3
  69. package/src/api/cyberia-action/cyberia-action.model.js +1 -0
  70. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +63 -0
  71. package/src/api/cyberia-instance/cyberia-fallback-world.js +49 -15
  72. package/src/api/cyberia-instance/cyberia-instance-map.service.js +8 -12
  73. package/src/api/cyberia-instance/cyberia-instance.controller.js +3 -0
  74. package/src/api/cyberia-instance/cyberia-instance.router.js +13 -0
  75. package/src/api/cyberia-instance/cyberia-instance.service.js +43 -0
  76. package/src/api/cyberia-instance/cyberia-portal-connector.js +7 -5
  77. package/src/api/cyberia-instance/cyberia-random-source.js +80 -0
  78. package/src/api/cyberia-instance/cyberia-world-generator.js +4 -3
  79. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +1 -0
  80. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +87 -1
  81. package/src/api/object-layer/object-layer.model.js +204 -3
  82. package/src/api/object-layer/object-layer.service.js +1 -14
  83. package/src/cli/baremetal.js +1 -2
  84. package/src/cli/cloud-init.js +1 -1
  85. package/src/cli/cluster.js +786 -96
  86. package/src/cli/db.js +11 -4
  87. package/src/cli/deploy.js +1698 -177
  88. package/src/cli/docker-compose.js +19 -178
  89. package/src/cli/env.js +1 -1
  90. package/src/cli/image.js +15 -7
  91. package/src/cli/index.js +245 -44
  92. package/src/cli/ipfs.js +82 -11
  93. package/src/cli/lxd.js +1 -1
  94. package/src/cli/monitor.js +2 -2
  95. package/src/cli/release.js +57 -22
  96. package/src/cli/repository.js +12 -10
  97. package/src/cli/run.js +2195 -427
  98. package/src/cli/secrets.js +969 -0
  99. package/src/cli/ssh.js +206 -105
  100. package/src/cli/system.js +26 -13
  101. package/src/cli/test.js +1 -1
  102. package/src/cli/vultr.js +583 -0
  103. package/src/cli/wireguard.js +2125 -0
  104. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +368 -0
  105. package/src/client/components/cyberia/InstanceSelectionView.js +11 -8
  106. package/src/client/components/cyberia/SharedDefaultsCyberia.js +5 -0
  107. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +33 -0
  108. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +4 -0
  109. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
  110. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +106 -39
  111. package/src/client/public/cyberia-docs/ARCHITECTURE.md +35 -3
  112. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +44 -7
  113. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +1 -1
  114. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  115. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  116. package/src/client/services/cyberia-instance/cyberia-instance.service.js +40 -0
  117. package/src/client/services/object-layer/object-layer.management.js +4 -4
  118. package/src/client-builder/client-build.js +102 -13
  119. package/src/client-builder/ssr.js +27 -73
  120. package/src/db/mongo/MongoBootstrap.js +295 -54
  121. package/src/db/mongo/MongooseDB.js +51 -32
  122. package/src/index.js +25 -1
  123. package/src/projects/cyberia/besu-genesis-generator.js +3 -2
  124. package/src/projects/cyberia/catalog-cyberia.js +5 -0
  125. package/src/projects/cyberia/generate-saga.js +14 -23
  126. package/src/projects/cyberia/hot-reload-trigger.js +3 -3
  127. package/src/projects/cyberia/instance-data.js +63 -3
  128. package/src/projects/cyberia/object-layer.js +11 -21
  129. package/src/projects/underpost/catalog-underpost.js +4 -1
  130. package/src/runtime/cyberia-client/Dockerfile +1 -1
  131. package/src/runtime/cyberia-client/Dockerfile.dev +1 -1
  132. package/src/runtime/cyberia-server/Dockerfile +1 -1
  133. package/src/runtime/cyberia-server/Dockerfile.dev +1 -1
  134. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  135. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  136. package/src/runtime/engine-cyberia/Dockerfile.test +1 -1
  137. package/src/runtime/engine-cyberia/compose.env +50 -15
  138. package/src/runtime/engine-cyberia/docker-compose.yml +90 -85
  139. package/src/runtime/engine-cyberia/nginx.conf +86 -16
  140. package/src/server/backup.js +1 -1
  141. package/src/server/conf.js +1216 -168
  142. package/src/server/cri.js +70 -0
  143. package/src/server/cron.js +249 -51
  144. package/src/server/dns.js +100 -6
  145. package/src/server/environment.js +98 -0
  146. package/src/server/forward-proxy.js +549 -0
  147. package/src/server/middlewares.js +56 -1
  148. package/src/server/process.js +0 -1
  149. package/src/server/selinux.js +185 -0
  150. package/src/server/systemd.js +205 -0
  151. package/src/server/underpost-compression.js +186 -0
  152. package/src/server/underpost-gateway.js +1083 -0
  153. package/src/server/underpost-ingress.js +380 -0
  154. package/test/cluster-instances.test.js +435 -0
  155. package/test/cyberia-instance-conf-defaults.test.js +1 -0
  156. package/test/deploy-node-placement.test.js +45 -0
  157. package/test/instance-traffic-plan.test.js +710 -0
  158. package/test/selinux.test.js +71 -0
  159. package/test/sops-secret-store.test.js +612 -0
  160. package/test/underpost-gateway.test.js +510 -0
  161. package/test/underpost-ingress.test.js +305 -0
  162. package/test/wireguard-edge.test.js +1177 -0
@@ -49,22 +49,25 @@ if [ -z "$(ls -A "$ENGINE_ROOT")" ]; then
49
49
  fi
50
50
 
51
51
  # ---------------------------------------------------------------------------
52
- # NVM and Node.js required for `node bin ...` entrypoints
52
+ # System-wide Node.js is required by service units under SELinux.
53
53
  # ---------------------------------------------------------------------------
54
- echo "Installing NVM and Node.js v24.15.0..."
55
-
56
- curl -o- https://cdn.jsdelivr.net/gh/nvm-sh/nvm@v0.40.1/install.sh | bash
57
-
58
- export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
59
- # shellcheck disable=SC1090
60
- [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
54
+ if command -v dnf >/dev/null 2>&1; then
55
+ sudo dnf install -y policycoreutils policycoreutils-python-utils selinux-policy-targeted audit
56
+ fi
61
57
 
62
- nvm install 24.15.0
63
- nvm use 24.15.0
64
- nvm alias default 24.15.0
65
- ln -sf "$(command -v node)" /usr/local/bin/node
66
- ln -sf "$(command -v npm)" /usr/local/bin/npm
67
- ln -sf "$(command -v npx)" /usr/local/bin/npx
58
+ if ! command -v node >/dev/null 2>&1 || ! node --version 2>/dev/null | grep -q '^v24'; then
59
+ echo "Installing system-wide Node.js 24..."
60
+ if command -v dnf >/dev/null 2>&1; then
61
+ curl -fsSL https://rpm.nodesource.com/setup_24.x | sudo bash -
62
+ sudo dnf install -y nodejs
63
+ elif command -v apt-get >/dev/null 2>&1; then
64
+ curl -fsSL https://deb.nodesource.com/setup_24.x | sudo bash -
65
+ sudo apt-get install -y nodejs
66
+ else
67
+ echo "ERROR: a supported system package manager is required for Node.js 24" >&2
68
+ exit 1
69
+ fi
70
+ fi
68
71
 
69
72
  echo "
70
73
  ██╗░░░██╗███╗░░██╗██████╗░███████╗██████╗░██████╗░░█████╗░░██████╗████████╗
@@ -116,7 +119,7 @@ if [ "$ROLE" = "control" ]; then
116
119
  curl -sfL https://get.k3s.io | \
117
120
  K3S_URL="https://${CONTROL_IP}:6443" \
118
121
  K3S_TOKEN="${K3S_TOKEN}" \
119
- sh -s - agent
122
+ sh -s - agent $(if command -v selinuxenabled >/dev/null 2>&1 && selinuxenabled; then printf '%s' '--selinux'; fi)
120
123
 
121
124
  echo ""
122
125
  echo "K3s worker joined https://${CONTROL_IP}:6443 successfully."
@@ -5,8 +5,8 @@ set -euo pipefail
5
5
  # Underpost Kubeadm Node Setup (bare-metal physical node)
6
6
  #
7
7
  # Mirrors scripts/k3s-node-setup.sh but for kubeadm on real hardware: it runs on
8
- # the freshly deployed OS (disk-installed Rocky) after first boot, installs NVM +
9
- # Node.js, ensures the engine source is present, and drives the cluster bring-up
8
+ # the freshly deployed OS (disk-installed Rocky) after first boot, installs
9
+ # system-wide Node.js, ensures the engine source is present, and drives the cluster bring-up
10
10
  # through the local engine entrypoint `node bin cluster` (src/cli/cluster.js) —
11
11
  # reusing its kubeadm + Contour + host-init logic instead of reimplementing it.
12
12
  #
@@ -146,33 +146,22 @@ if [ "$JOIN_ONLY" = "1" ]; then
146
146
  fi
147
147
 
148
148
  # ---------------------------------------------------------------------------
149
- # 0. Base prerequisites — a minimal @core Rocky install lacks tar/xz/git, which
150
- # NVM needs to extract Node.js and the engine clone needs. Install them first.
149
+ # 0. Base prerequisites.
151
150
  # ---------------------------------------------------------------------------
152
- log "Installing base prerequisites (tar, xz, gzip, git, curl)..."
153
- sudo dnf install -y tar xz gzip bzip2 git curl ca-certificates which findutils 2>/dev/null \
154
- || dnf install -y tar xz gzip bzip2 git curl ca-certificates which findutils
151
+ log "Installing base prerequisites..."
152
+ sudo dnf install -y tar xz gzip bzip2 git curl ca-certificates which findutils policycoreutils policycoreutils-python-utils selinux-policy-targeted audit 2>/dev/null \
153
+ || dnf install -y tar xz gzip bzip2 git curl ca-certificates which findutils policycoreutils policycoreutils-python-utils selinux-policy-targeted audit
155
154
 
156
155
  # ---------------------------------------------------------------------------
157
- # 1. NVM and Node.js required for the `node bin ...` entrypoints. Idempotent so
158
- # a retried run does not reinstall Node from scratch.
156
+ # 1. System-wide Node.js. Home-directory runtimes cannot be used by hardened
157
+ # systemd services under SELinux.
159
158
  # ---------------------------------------------------------------------------
160
159
  if command -v node >/dev/null 2>&1 && node --version 2>/dev/null | grep -q '^v24'; then
161
- log "Node.js $(node --version) already installed; skipping NVM setup"
160
+ log "Node.js $(node --version) already installed"
162
161
  else
163
- log "Installing NVM and Node.js v24.15.0..."
164
- curl -o- https://cdn.jsdelivr.net/gh/nvm-sh/nvm@v0.40.1/install.sh | bash
165
-
166
- export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
167
- # shellcheck disable=SC1090
168
- [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
169
-
170
- nvm install 24.15.0
171
- nvm use 24.15.0
172
- nvm alias default 24.15.0
173
- ln -sf "$(command -v node)" /usr/local/bin/node
174
- ln -sf "$(command -v npm)" /usr/local/bin/npm
175
- ln -sf "$(command -v npx)" /usr/local/bin/npx
162
+ log "Installing system-wide Node.js 24..."
163
+ curl -fsSL https://rpm.nodesource.com/setup_24.x | sudo bash -
164
+ sudo dnf install -y nodejs
176
165
  fi
177
166
 
178
167
  echo "
File without changes
File without changes
File without changes
@@ -32,16 +32,24 @@ sudo firewall-cmd --permanent --zone=public --add-service=ssh
32
32
  sudo firewall-cmd --permanent --zone=public --add-service=http
33
33
  sudo firewall-cmd --permanent --zone=public --add-service=https
34
34
 
35
+ # QUIC / HTTP3. The `https` service above covers TCP 443 only, so without this
36
+ # an HTTP/3 gateway advertises `Alt-Svc: h3=":443"` and every client that acts
37
+ # on it silently fails over UDP while HTTP/2 keeps working.
38
+ sudo firewall-cmd --permanent --zone=public --add-port=443/udp
39
+ sudo firewall-cmd --permanent --zone=public --add-port=80/udp
40
+
35
41
  echo "[INFO] Opening Kubernetes control plane ports..."
36
42
 
37
43
  # Kubernetes API Server
38
44
  sudo firewall-cmd --permanent --zone=public --add-port=6443/tcp
45
+ sudo firewall-cmd --permanent --zone=public --add-port=6443/udp
39
46
 
40
47
  # etcd
41
48
  sudo firewall-cmd --permanent --zone=public --add-port=2379-2380/tcp
42
49
 
43
50
  # kubelet API
44
51
  sudo firewall-cmd --permanent --zone=public --add-port=10250/tcp
52
+ sudo firewall-cmd --permanent --zone=public --add-port=10250/udp
45
53
 
46
54
  # kube-scheduler
47
55
  sudo firewall-cmd --permanent --zone=public --add-port=10259/tcp
@@ -98,10 +106,10 @@ sudo firewall-cmd --reload
98
106
  echo
99
107
  echo "[INFO] Sysctl status:"
100
108
  sudo sysctl \
101
- net.ipv4.ip_forward \
102
- net.ipv6.conf.all.forwarding \
103
- net.bridge.bridge-nf-call-iptables \
104
- net.bridge.bridge-nf-call-ip6tables
109
+ net.ipv4.ip_forward \
110
+ net.ipv6.conf.all.forwarding \
111
+ net.bridge.bridge-nf-call-iptables \
112
+ net.bridge.bridge-nf-call-ip6tables
105
113
 
106
114
  echo
107
115
  echo "[INFO] Firewall status:"
File without changes
@@ -168,7 +168,7 @@ PermitEmptyPasswords no
168
168
  PasswordAuthentication ${SSHD_PASSWORD_AUTH}
169
169
  ChallengeResponseAuthentication ${SSHD_KBD_INTERACTIVE}
170
170
  KbdInteractiveAuthentication ${SSHD_KBD_INTERACTIVE}
171
- UsePAM no
171
+ UsePAM yes
172
172
  X11Forwarding no
173
173
  PrintMotd no
174
174
  TCPKeepAlive yes
@@ -585,7 +585,7 @@ fi
585
585
  post_status "installing" "\"detail\":\"installing packages\",\"disk\":\"\$REAL_DISK\""
586
586
 
587
587
  ilog "Installing base packages with dnf --installroot ..."
588
- PKGS="@core kernel grub2-tools openssh-server NetworkManager chrony dracut-config-generic rocky-release"
588
+ PKGS="@core kernel grub2-tools openssh-server NetworkManager chrony dracut-config-generic rocky-release policycoreutils policycoreutils-python-utils selinux-policy-targeted audit sudo"
589
589
  if [ "\$FW" = "uefi" ]; then
590
590
  PKGS="\$PKGS grub2-efi-x64 shim-x64 efibootmgr"
591
591
  else
@@ -622,15 +622,10 @@ ilog "Configuring installed system (hostname, network, ssh, users) ..."
622
622
  echo "\${TARGET_HOSTNAME:-rocky9}" > "\$TARGET_MNT/etc/hostname"
623
623
 
624
624
  chroot "\$TARGET_MNT" /bin/bash -s << CHROOTEOF >> "\$KS_LOG" 2>&1
625
- set +e
625
+ set -e
626
626
  systemctl enable sshd NetworkManager chronyd 2>/dev/null
627
627
 
628
- # SELinux -> permissive in the DEPLOYED OS. Critical: with SELinux enforcing on
629
- # first boot, files written from this installer chroot (authorized_keys, home
630
- # dirs) carry the wrong security context, so sshd drops sessions ("Connection
631
- # reset by peer") and PAM can block console logins. Permissive ignores contexts
632
- # (no relabel reboot needed) and matches what kubeadm/cluster.js expect.
633
- sed -i 's/^SELINUX=.*/SELINUX=permissive/' /etc/selinux/config 2>/dev/null || true
628
+ sed -i 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config
634
629
 
635
630
  # Ensure sshd host keys exist so sshd actually starts on first boot.
636
631
  ssh-keygen -A 2>/dev/null || true
@@ -708,11 +703,19 @@ fi
708
703
  # Key-only SSH on the installed system.
709
704
  mkdir -p /etc/ssh/sshd_config.d
710
705
  cat > /etc/ssh/sshd_config.d/00-underpost.conf <<SSHDEOF
706
+ Port ${SSH_PORT}
711
707
  PermitRootLogin prohibit-password
712
708
  PubkeyAuthentication yes
713
709
  PasswordAuthentication no
710
+ KbdInteractiveAuthentication no
711
+ UsePAM yes
714
712
  SSHDEOF
715
713
 
714
+ if [ "${SSH_PORT}" != "22" ]; then
715
+ semanage port -a -t ssh_port_t -p tcp "${SSH_PORT}" 2>/dev/null || \
716
+ semanage port -m -t ssh_port_t -p tcp "${SSH_PORT}"
717
+ fi
718
+
716
719
  # Create the admin login accounts. create_user <name> writes the authorized key,
717
720
  # wheel membership and passwordless sudo. Passwords are applied after the chroot.
718
721
  create_user() {
@@ -738,11 +741,24 @@ chmod 600 /root/.ssh/authorized_keys
738
741
  create_user "\${ADMIN_USER}"
739
742
  create_user "\${DEPLOY_USER}"
740
743
 
744
+ restorecon -RF /etc/ssh /etc/sudoers.d /root/.ssh /home 2>/dev/null
745
+ touch /.autorelabel
746
+
741
747
  # Build initramfs for the installed kernel.
742
748
  KVER=\\\$(ls /lib/modules | head -1)
743
749
  [ -n "\\\$KVER" ] && dracut -f /boot/initramfs-\\\$KVER.img "\\\$KVER"
744
750
  CHROOTEOF
745
751
 
752
+ CHROOT_RC=\$?
753
+ if [ "\$CHROOT_RC" -ne 0 ]; then
754
+ ilog "FATAL: installed-system configuration failed (rc=\$CHROOT_RC)"
755
+ post_status "failed" "\"detail\":\"installed-system configuration rc=\$CHROOT_RC\""
756
+ for d in dev proc sys run; do umount -l "\$TARGET_MNT/\$d" 2>/dev/null; done
757
+ umount -R "\$TARGET_MNT" 2>/dev/null || true
758
+ rm -f "\$INSTALL_LOCK"
759
+ exit 1
760
+ fi
761
+
746
762
  # Set console passwords AFTER the chroot heredoc, from decoded installer vars.
747
763
  # Primary path: pipe 'user:password' into 'chroot chpasswd' (keeps password
748
764
  # characters out of any heredoc). Fallback: hash with openssl and apply via
@@ -24,7 +24,7 @@ MODE=runtime # runtime | instance
24
24
  ENV=development # development | production
25
25
  DEPLOY_ID=dd-test # deploy id (instance mode: parent of conf.instances.json)
26
26
  INSTANCE_IDS= # instance mode: csv of ids (default: all in conf.instances.json)
27
- IMAGE=underpost/wp:v3.2.80 # runtime mode image (instance mode reads image from conf)
27
+ IMAGE=underpost/wp:v3.3.0 # runtime mode image (instance mode reads image from conf)
28
28
  VERSIONS=green # csv of blue/green versions
29
29
  REPLICAS=1 # replicas per deployment
30
30
  NAMESPACE=default # k8s namespace
@@ -202,7 +202,8 @@ run_runtime_mode() {
202
202
  setup_tls_secrets $(hosts_from "$SERVER_CONF")
203
203
  expose_flags="--tls"
204
204
  fi
205
- [ "$DO_EXPOSE" = true ] && node bin deploy --expose --local-proxy "$DEPLOY_ID" "$ENV" $expose_flags
205
+ [ "$DO_EXPOSE" = true ] && node bin run expose "${DEPLOY_ID}-${ENV}-traffic-service" --namespace "$NAMESPACE" \
206
+ --local-proxy $DEV_FLAG $CLUSTER_FLAG $expose_flags
206
207
  }
207
208
 
208
209
  # ─────────────────────────── instance mode ───────────────────────────
@@ -234,7 +235,7 @@ run_instance_mode() {
234
235
  # Plain HTTP: port-forward each instance service port directly.
235
236
  for id in "${ids[@]}"; do
236
237
  [ -n "$id" ] || continue
237
- node bin deploy --expose "${DEPLOY_ID}-${id}" "$ENV" --namespace "$NAMESPACE" || true
238
+ node bin run expose "${DEPLOY_ID}-${id}-${ENV}-traffic-service" --namespace "$NAMESPACE" $CLUSTER_FLAG || true
238
239
  done
239
240
  fi
240
241
  }
@@ -60,6 +60,7 @@ const CyberiaActionSchema = new Schema(
60
60
  qty: { type: Number, default: 1, min: 1 },
61
61
  },
62
62
  ],
63
+ craftTimeMs: { type: Number, default: 0, min: 0 },
63
64
  },
64
65
  ],
65
66
 
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Fallback-world default items — process-local override store.
3
+ *
4
+ * The procedural fallback world is never persisted, so its default item set
5
+ * has no collection to live in. This module holds the set the FallbackWorldEngine
6
+ * view composed, in memory, for the lifetime of the engine process:
7
+ *
8
+ * - written by POST /api/cyberia-instance/fallback-world/hot-reload
9
+ * - read by fetchFullInstance()'s fallback branch (instance-data.js) and
10
+ * GET /api/cyberia-instance/fallback-world[/default-items]
11
+ *
12
+ * Deliberately NOT persisted: an engine restart drops back to the code defaults,
13
+ * so a stale editor session can never become the permanent shape of the world.
14
+ * This keeps the fallback path's "code defaults are authoritative" invariant —
15
+ * the only thing an operator can layer on top is this explicit, volatile list.
16
+ *
17
+ * @module src/api/cyberia-instance/cyberia-fallback-default-items.js
18
+ */
19
+
20
+ /** @typedef {{ id: string, defaultPlayerInventory: boolean }} FallbackDefaultItem */
21
+
22
+ /** @type {FallbackDefaultItem[]} */
23
+ let defaultItems = [];
24
+
25
+ /**
26
+ * Coerce a caller-supplied list into the canonical `{ id, defaultPlayerInventory }`
27
+ * shape. Accepts the legacy `string[]` form and drops blank/duplicate ids so the
28
+ * world generator never sees a malformed entry.
29
+ *
30
+ * @param {Array<string|FallbackDefaultItem>} entries
31
+ * @returns {FallbackDefaultItem[]}
32
+ */
33
+ function normalizeFallbackDefaultItems(entries) {
34
+ if (!Array.isArray(entries)) return [];
35
+ const seen = new Set();
36
+ const normalized = [];
37
+ for (const entry of entries) {
38
+ const raw = typeof entry === 'string' ? { id: entry } : entry;
39
+ const id = typeof raw?.id === 'string' ? raw.id.trim() : '';
40
+ if (!id || seen.has(id)) continue;
41
+ seen.add(id);
42
+ normalized.push({ id, defaultPlayerInventory: !!raw.defaultPlayerInventory });
43
+ }
44
+ return normalized;
45
+ }
46
+
47
+ /**
48
+ * Current override set. Returns a copy so callers cannot mutate the store.
49
+ * @returns {FallbackDefaultItem[]}
50
+ */
51
+ const getFallbackDefaultItems = () => defaultItems.map((entry) => ({ ...entry }));
52
+
53
+ /**
54
+ * Replace the override set. An empty list clears it, restoring pure code defaults.
55
+ * @param {Array<string|FallbackDefaultItem>} entries
56
+ * @returns {FallbackDefaultItem[]} The stored set.
57
+ */
58
+ function setFallbackDefaultItems(entries) {
59
+ defaultItems = normalizeFallbackDefaultItems(entries);
60
+ return getFallbackDefaultItems();
61
+ }
62
+
63
+ export { getFallbackDefaultItems, setFallbackDefaultItems, normalizeFallbackDefaultItems };
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import { connectPortals } from './cyberia-portal-connector.js';
22
+ import { withSeededRandom } from './cyberia-random-source.js';
22
23
 
23
24
  import {
24
25
  generateObstacles,
@@ -38,6 +39,7 @@ import {
38
39
  DEFAULT_PLAYER_SPAWN,
39
40
  ENTITY_TYPE_DEFAULTS,
40
41
  RESOURCE_ENTITY_TYPE_DEFAULTS,
42
+ DefaultCyberiaActions,
41
43
  DefaultSkillConfig,
42
44
  } from '../cyberia-server-defaults/cyberia-server-defaults.js';
43
45
  import {
@@ -92,6 +94,19 @@ function collectReferencedItemIds() {
92
94
  }
93
95
  }
94
96
  }
97
+ // Provider catalogs: the interact modal draws an icon for every item a
98
+ // vendor sells and every ingredient/output an assembler recipe names, so all
99
+ // of them must resolve to an ObjectLayer.
100
+ for (const action of DefaultCyberiaActions) {
101
+ for (const shopItem of action.shopItems || []) {
102
+ push(shopItem.itemId);
103
+ push(shopItem.priceItemId);
104
+ }
105
+ for (const recipe of action.craftRecipes || []) {
106
+ (recipe.ingredients || []).forEach((i) => push(i.itemId));
107
+ (recipe.outputItems || []).forEach((o) => push(o.itemId));
108
+ }
109
+ }
95
110
  return ids;
96
111
  }
97
112
 
@@ -164,7 +179,7 @@ function generateFallbackMap(mapCode, colors, opts = {}) {
164
179
  const portalEntities = generatePortalEntities(mapDims, colors, { grid });
165
180
 
166
181
  // 6. Bots — placed on walkable cells (avoids obstacles, portals, NPCs)
167
- const bots = generateBots(mapDims, colors, { count: opts.botCount, grid });
182
+ const bots = mapCode === 'fallback-map-0' ? [] : generateBots(mapDims, colors, { count: opts.botCount, grid });
168
183
 
169
184
  // 7. Resources — static exploitable entities placed on walkable cells
170
185
  const resources = generateResources(mapDims, colors, { count: opts.resourceCount, grid });
@@ -217,6 +232,11 @@ function generateFallbackMap(mapCode, colors, opts = {}) {
217
232
  * @param {number} [opts.resourceCount] Resources per map (random if omitted).
218
233
  * @param {number} [opts.staticCount] Static decorators per map (random if omitted).
219
234
  * @param {Array} [opts.colors] Override palette.
235
+ * @param {Array<{id: string, defaultPlayerInventory: boolean}>} [opts.itemIds=[]]
236
+ * Instance-level default items, mirroring `CyberiaInstance.itemIds`. Entries
237
+ * flagged `defaultPlayerInventory` are merged into the player entity default
238
+ * by `applyInstanceDefaultPlayerInventory`; every id is also resolved to an
239
+ * atlas at boot. Layout is unaffected, so this stays outside the world seed.
220
240
  * @returns {{
221
241
  * instance: object,
222
242
  * maps: object[],
@@ -241,6 +261,7 @@ function generateFallbackWorld(opts = {}) {
241
261
  // fallback before atlases load. The C client resolves real colours
242
262
  // through domain/presentation_runtime — it does not read this value.
243
263
  colors = PALETTE,
264
+ itemIds = [],
244
265
  } = opts;
245
266
 
246
267
  // Surface item-id drift loudly on the very first build, so a missing
@@ -262,20 +283,32 @@ function generateFallbackWorld(opts = {}) {
262
283
  mapCodes.push(`fallback-map-${i}`);
263
284
  }
264
285
 
265
- // Generate each map (all randomized independently).
266
- const maps = mapCodes.map((code) =>
267
- generateFallbackMap(code, colors, {
268
- gridSize,
269
- obstacleCount,
270
- foregroundCount,
271
- botCount,
272
- resourceCount,
273
- staticCount,
274
- }),
275
- );
276
-
277
- // Connect maps with portal topology using the portal connector module.
278
- const { portals, topology } = connectPortals(mapCodes, maps);
286
+ // Deterministic layout: seed the shared random source so every call
287
+ // reproduces the SAME world. The instance-map `/static` POIs and the
288
+ // `/preview` node image are built by independent requests (and survive
289
+ // restarts) — they must agree on where every entity sits, so a resource POI
290
+ // lands exactly where the preview draws that resource. The seed folds in the
291
+ // layout-affecting options so a custom count set stays internally consistent.
292
+ const seed = `cyberia-fallback-world-v1:${mapCount}:${gridSize ?? ''}:${obstacleCount ?? ''}:${
293
+ foregroundCount ?? ''
294
+ }:${botCount ?? ''}:${resourceCount ?? ''}:${staticCount ?? ''}`;
295
+
296
+ const { maps, portals, topology } = withSeededRandom(seed, () => {
297
+ // Generate each map (deterministic under the seeded source).
298
+ const genMaps = mapCodes.map((code) =>
299
+ generateFallbackMap(code, colors, {
300
+ gridSize,
301
+ obstacleCount,
302
+ foregroundCount,
303
+ botCount,
304
+ resourceCount,
305
+ staticCount,
306
+ }),
307
+ );
308
+ // Connect maps with portal topology using the portal connector module.
309
+ const { portals: p, topology: t } = connectPortals(mapCodes, genMaps);
310
+ return { maps: genMaps, portals: p, topology: t };
311
+ });
279
312
 
280
313
  // Build the instance shell (never persisted — no mongoId).
281
314
  const instance = {
@@ -287,6 +320,7 @@ function generateFallbackWorld(opts = {}) {
287
320
  portals,
288
321
  topologyMode: 'procedural',
289
322
  playerSpawn: { ...DEFAULT_PLAYER_SPAWN },
323
+ itemIds: itemIds.map((entry) => ({ ...entry })),
290
324
  };
291
325
 
292
326
  return {
@@ -31,8 +31,8 @@ import {
31
31
  DefaultCyberiaActions,
32
32
  DefaultCyberiaQuests,
33
33
  ENTITY_TYPE_DEFAULTS,
34
- RESOURCE_ENTITY_TYPE_DEFAULTS,
35
34
  } from '../cyberia-server-defaults/cyberia-server-defaults.js';
35
+ import { DefaultCyberiaItems } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
36
36
 
37
37
  const logger = loggerFactory(import.meta);
38
38
 
@@ -430,22 +430,18 @@ const resolveInstanceWorld = async (instanceCode, options) => {
430
430
  const instance = await CyberiaInstance.findOne({ code: instanceCode }).lean();
431
431
 
432
432
  if (!instance) {
433
- // Fallback world: topology is regenerated (portal cells may drift from
434
- // the live simulation's world), but map codes and provider POIs come
435
- // from the deterministic canonical defaults.
436
433
  const world = generateFallbackWorld();
437
434
  const mapCodes = new Set(world.instance.cyberiaMapCodes);
438
435
 
439
- // Build synthetic object-layer metadata for the fallback world's resource
440
- // items so resource entities carry a stats-sum readout. The canonical
441
- // resource defaults (wood-1, wood-2) are not persisted to MongoDB in the
442
- // fallback path, so we compute a reasonable stats sum from the item type's
443
- // default stat block (each stat 0-10, 6 stats → ~30 average).
444
- const resourceItemIds = [
445
- ...new Set(RESOURCE_ENTITY_TYPE_DEFAULTS.flatMap((r) => [...(r.liveItemIds || []), ...(r.dropItemIds || [])])),
436
+ const fallbackItemIds = [
437
+ ...new Set(world.maps.flatMap((m) => (m.entities || []).flatMap((e) => e.objectLayerItemIds || []))),
446
438
  ];
439
+ const itemTypeById = Object.fromEntries(DefaultCyberiaItems.map((entry) => [entry.item.id, entry.item.type]));
440
+ const resolved = await resolveObjectLayerMetadata(fallbackItemIds, options);
447
441
  const objectLayerMetadata = Object.fromEntries(
448
- resourceItemIds.map((itemId) => [itemId, { statsSum: 30, type: 'resource' }]),
442
+ Object.entries(resolved).map(([itemId, meta]) => {
443
+ return [itemId, { statsSum: meta.statsSum, type: meta.type }];
444
+ }),
449
445
  );
450
446
 
451
447
  return {
@@ -30,6 +30,9 @@ const CyberiaInstanceController = buildCrudController(CyberiaInstanceService, {
30
30
  portalConnect: serviceHandler(CyberiaInstanceService.portalConnect),
31
31
  // Moderator/admin-triggered world reload on a running cyberia-server.
32
32
  hotReload: serviceHandler(CyberiaInstanceService.hotReload, { errorStatus: 502 }),
33
+ // Fallback-world default items (volatile) + its own reload trigger.
34
+ fallbackDefaultItems: serviceHandler(CyberiaInstanceService.fallbackDefaultItems),
35
+ fallbackHotReload: serviceHandler(CyberiaInstanceService.fallbackHotReload, { errorStatus: 502 }),
33
36
  // Boot transport (REST fallback of the gRPC CyberiaDataService).
34
37
  bootPing: serviceHandler(CyberiaInstanceBootService.ping),
35
38
  bootObjectLayerBatch: serviceHandler(CyberiaInstanceBootService.objectLayerBatch, { errorStatus: 500 }),
@@ -38,6 +38,19 @@ class CyberiaInstanceRouter {
38
38
  async (req, res) => await CyberiaInstanceController.bootFullInstance(req, res, options),
39
39
  );
40
40
  router.get(`/fallback-world`, async (req, res) => await CyberiaInstanceController.fallbackWorld(req, res, options));
41
+ // Fallback-world default items — volatile, process-local (never persisted).
42
+ // The read is open (same as /fallback-world); staging them and triggering a
43
+ // rebuild is elevated privilege, like every other hot-reload path.
44
+ router.get(
45
+ `/fallback-world/default-items`,
46
+ async (req, res) => await CyberiaInstanceController.fallbackDefaultItems(req, res, options),
47
+ );
48
+ router.post(
49
+ `/fallback-world/hot-reload`,
50
+ options.authMiddleware,
51
+ moderatorGuard,
52
+ async (req, res) => await CyberiaInstanceController.fallbackHotReload(req, res, options),
53
+ );
41
54
  // Instance Map — static topology/presence plus dynamic player capability activity.
42
55
  router.get(
43
56
  `/instance-map/:instanceCode/static`,
@@ -7,6 +7,8 @@ import {
7
7
  fallbackListInstance,
8
8
  DEFAULT_FALLBACK_INSTANCE_CODE,
9
9
  } from './cyberia-fallback-world.js';
10
+ import { getFallbackDefaultItems, setFallbackDefaultItems } from './cyberia-fallback-default-items.js';
11
+ import { CYBERIA_INSTANCE_CONF_DEFAULTS } from '../cyberia-server-defaults/cyberia-server-defaults.js';
10
12
  import { triggerHotReload } from '../../projects/cyberia/hot-reload-trigger.js';
11
13
 
12
14
  const logger = loggerFactory(import.meta);
@@ -183,8 +185,49 @@ class CyberiaInstanceService {
183
185
  botCount: q.botCount ? parseInt(q.botCount, 10) : undefined,
184
186
  obstacleCount: q.obstacleCount ? parseInt(q.obstacleCount, 10) : undefined,
185
187
  foregroundCount: q.foregroundCount ? parseInt(q.foregroundCount, 10) : undefined,
188
+ itemIds: getFallbackDefaultItems(),
186
189
  });
187
190
  };
191
+
192
+ /**
193
+ * Current fallback-world default items — the volatile set the
194
+ * FallbackWorldEngine view last pushed, or `[]` when the engine is running on
195
+ * pure code defaults. Lets the view reopen showing what this process serves.
196
+ *
197
+ * `entityDefaultItemIds` are the ids the code defaults already ship in an
198
+ * entity's `defaultObjectLayers`. Selecting one of them and leaving
199
+ * `defaultPlayerInventory` unchecked REMOVES it (see
200
+ * applyInstanceDefaultPlayerInventory), so the editor needs to be able to warn
201
+ * before an operator silently strips the player's starting skin or weapon.
202
+ */
203
+ static fallbackDefaultItems = async () => {
204
+ const entityDefaultItemIds = new Set();
205
+ for (const entityDefault of CYBERIA_INSTANCE_CONF_DEFAULTS.entityDefaults || []) {
206
+ for (const layer of entityDefault.defaultObjectLayers || []) {
207
+ if (layer.itemId) entityDefaultItemIds.add(layer.itemId);
208
+ }
209
+ }
210
+ return { itemIds: getFallbackDefaultItems(), entityDefaultItemIds: [...entityDefaultItemIds].sort() };
211
+ };
212
+
213
+ /**
214
+ * Stage the fallback world's default items, then ask a running cyberia-server
215
+ * to rebuild. The items are not persisted (see cyberia-fallback-default-items):
216
+ * they are staged here first so the rebuild's `getFullInstance` re-fetch picks
217
+ * them up, which is what makes them reach the server at all.
218
+ *
219
+ * `instanceCode` is intentionally left empty — the target world is addressed by
220
+ * `serverUrl` (origin, or origin + variant sub-path), and naming the synthetic
221
+ * `fallback` code would be rejected by any server serving a real instance code.
222
+ */
223
+ static fallbackHotReload = async (req) => {
224
+ const { serverUrl, mode, itemIds } = req.body || {};
225
+ if (!serverUrl) throw new Error('serverUrl is required');
226
+
227
+ const staged = setFallbackDefaultItems(itemIds || []);
228
+ const { transport, result, grpcError } = await triggerHotReload({ serverUrl, instanceCode: '', mode });
229
+ return { transport, itemIds: staged, grpcError, ...result };
230
+ };
188
231
  }
189
232
 
190
233
  export { CyberiaInstanceService };
@@ -20,6 +20,8 @@
20
20
  * @module src/api/cyberia-instance/cyberia-portal-connector
21
21
  */
22
22
 
23
+ import { nextRandom } from './cyberia-random-source.js';
24
+
23
25
  // ── Portal mode constants ────────────────────────────────────────────────────
24
26
 
25
27
  /**
@@ -117,7 +119,7 @@ function buildTopologyFromSubtypes(orderedCodes, portalIndex) {
117
119
 
118
120
  // Target: pick any portal on the target map for landing coordinates
119
121
  const tgtAll = portalIndex[tgtCode] || [];
120
- const tgtEnt = tgtAll.length > 0 ? tgtAll[Math.floor(Math.random() * tgtAll.length)] : null;
122
+ const tgtEnt = tgtAll.length > 0 ? tgtAll[Math.floor(nextRandom() * tgtAll.length)] : null;
121
123
 
122
124
  if (srcEnt) {
123
125
  usedInRing.add(srcEnt);
@@ -139,7 +141,7 @@ function buildTopologyFromSubtypes(orderedCodes, portalIndex) {
139
141
  if (n < 2) return srcCode;
140
142
  let code;
141
143
  do {
142
- code = orderedCodes[Math.floor(Math.random() * n)];
144
+ code = orderedCodes[Math.floor(nextRandom() * n)];
143
145
  } while (code === srcCode && n > 1);
144
146
  return code;
145
147
  };
@@ -151,13 +153,13 @@ function buildTopologyFromSubtypes(orderedCodes, portalIndex) {
151
153
  for (const srcEnt of remaining) {
152
154
  // If the entity already has an explicit subtype, honour it;
153
155
  // otherwise assign a random mode (matching fallback-world behaviour).
154
- const sub = srcEnt.portalSubtype || PORTAL_MODE_LIST[Math.floor(Math.random() * PORTAL_MODE_LIST.length)];
156
+ const sub = srcEnt.portalSubtype || PORTAL_MODE_LIST[Math.floor(nextRandom() * PORTAL_MODE_LIST.length)];
155
157
 
156
158
  switch (sub) {
157
159
  case PORTAL_MODES.INTER_PORTAL: {
158
160
  const tgtCode = otherMap(srcCode);
159
161
  const candidates = portalIndex[tgtCode] || [];
160
- const tgtEnt = candidates.length > 0 ? candidates[Math.floor(Math.random() * candidates.length)] : null;
162
+ const tgtEnt = candidates.length > 0 ? candidates[Math.floor(nextRandom() * candidates.length)] : null;
161
163
  portals.push({
162
164
  sourceMapCode: srcCode,
163
165
  sourceCellX: srcEnt.initCellX ?? 0,
@@ -198,7 +200,7 @@ function buildTopologyFromSubtypes(orderedCodes, portalIndex) {
198
200
  const candidates = allOnMap.filter(
199
201
  (e) => e !== srcEnt && (e.initCellX !== srcEnt.initCellX || e.initCellY !== srcEnt.initCellY),
200
202
  );
201
- const tgtEnt = candidates.length > 0 ? candidates[Math.floor(Math.random() * candidates.length)] : null;
203
+ const tgtEnt = candidates.length > 0 ? candidates[Math.floor(nextRandom() * candidates.length)] : null;
202
204
  portals.push({
203
205
  sourceMapCode: srcCode,
204
206
  sourceCellX: srcEnt.initCellX ?? 0,