@underpostnet/cyberia 3.3.0 → 3.3.76

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 (568) hide show
  1. package/.dockerignore +1 -0
  2. package/.env.example +17 -5
  3. package/.github/workflows/coverall.cyberia.ci.yml +39 -14
  4. package/.github/workflows/cyberia-client.cd.yml +5 -0
  5. package/.github/workflows/cyberia-server.cd.yml +5 -0
  6. package/.github/workflows/docker-image.cyberia-client.ci.yml +8 -5
  7. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +8 -3
  8. package/.github/workflows/docker-image.cyberia-server.ci.yml +8 -5
  9. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +8 -5
  10. package/.github/workflows/docker-image.engine-cyberia.ci.yml +15 -1
  11. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +15 -1
  12. package/.github/workflows/engine-cyberia.cd.yml +3 -0
  13. package/.github/workflows/engine-cyberia.ci.yml +4 -5
  14. package/.github/workflows/ghpkg.ci.yml +35 -20
  15. package/.github/workflows/hardhat.ci.yml +12 -11
  16. package/.github/workflows/npmpkg.ci.yml +17 -15
  17. package/.github/workflows/publish.cyberia.ci.yml +44 -0
  18. package/.github/workflows/pwa-microservices-template-test.ci.yml +2 -2
  19. package/.github/workflows/release.cd.yml +1 -0
  20. package/.prettierignore +1 -0
  21. package/AGENTS.md +96 -0
  22. package/CHANGELOG.md +653 -2513
  23. package/CLI-HELP.md +323 -157
  24. package/Dockerfile +25 -9
  25. package/Dockerfile.dev +25 -9
  26. package/Dockerfile.test +5 -5
  27. package/README.md +48 -28
  28. package/bin/build.js +86 -41
  29. package/bin/build.template.js +8 -3
  30. package/bin/cyberia.js +1494 -1403
  31. package/bin/deploy.js +18 -104
  32. package/bin/index.js +1494 -1403
  33. package/compose.env +17 -5
  34. package/conf.js +98 -7
  35. package/deploy/cyberia-client/deploy.sh +46 -0
  36. package/deploy/cyberia-client/package.sh +17 -0
  37. package/deploy/cyberia-client/state.sh +15 -0
  38. package/deploy/cyberia-server/deploy.sh +46 -0
  39. package/deploy/cyberia-server/package.sh +17 -0
  40. package/deploy/cyberia-server/state.sh +15 -0
  41. package/deploy/dd-cyberia/deploy.sh +9 -29
  42. package/deploy/dd-cyberia/init.sh +38 -45
  43. package/deploy/dd-cyberia/package.sh +16 -0
  44. package/deploy/dd-cyberia/state.sh +14 -0
  45. package/deploy/dd-cyberia/sync-deploy.sh +133 -147
  46. package/deploy/lib/config.sh +29 -0
  47. package/deploy/lib/github-actions-logging.sh +610 -0
  48. package/deploy/lib/host.sh +194 -0
  49. package/deploy/lib/state.sh +65 -0
  50. package/deploy/pwa-microservices-template/deploy.sh +17 -42
  51. package/deploy/release/deploy.sh +9 -41
  52. package/deployment.yaml +7 -2
  53. package/docker-compose.yml +23 -17
  54. package/examples/static-page/README.md +19 -19
  55. package/examples/static-page/ssr-components/CustomPage.js +2 -2
  56. package/hardhat/hardhat.config.js +1 -6
  57. package/hardhat/package-lock.json +101 -40
  58. package/hardhat/package.json +2 -2
  59. package/manifests/alertmanager/deployment.yaml +81 -0
  60. package/manifests/blackbox-exporter/deployment.yaml +75 -0
  61. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +32 -7
  62. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +32 -7
  63. package/manifests/deployment/dd-cyberia-development/deployment.yaml +7 -2
  64. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  65. package/manifests/deployment/mongo-express/deployment.yaml +46 -27
  66. package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
  67. package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
  68. package/manifests/deployment/mongo-express/service.yaml +16 -0
  69. package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
  70. package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
  71. package/manifests/grafana/deployment.yaml +26 -7
  72. package/manifests/grafana/kustomization.yaml +1 -0
  73. package/manifests/grafana/service.yaml +6 -1
  74. package/manifests/mariadb/pv.yaml +1 -0
  75. package/manifests/mongodb/statefulset.yaml +9 -2
  76. package/manifests/mysql/pv-pvc.yaml +14 -1
  77. package/manifests/postgresql/pv.yaml +12 -1
  78. package/manifests/postgresql/pvc.yaml +1 -0
  79. package/manifests/prometheus/deployment.yaml +18 -22
  80. package/package.json +87 -12
  81. package/scripts/event-monitor.sh +56 -0
  82. package/scripts/k3s-node-setup.sh +27 -14
  83. package/scripts/kubeadm-node-setup.sh +31 -11
  84. package/scripts/maas-setup.sh +8 -8
  85. package/scripts/selinux-normalize.sh +907 -0
  86. package/scripts/shutdown-machine.sh +843 -0
  87. package/scripts/test-monitor.sh +5 -5
  88. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +15 -10
  89. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
  90. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +7 -2
  91. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +90 -186
  92. package/src/api/core/core.controller.js +1 -1
  93. package/src/api/core/core.router.js +2 -2
  94. package/src/api/core/core.service.js +2 -2
  95. package/src/api/crypto/crypto.controller.js +1 -1
  96. package/src/api/crypto/crypto.service.js +1 -1
  97. package/src/api/cyberia-action/cyberia-action.controller.js +1 -1
  98. package/src/api/cyberia-action/cyberia-action.router.js +3 -3
  99. package/src/api/cyberia-action/cyberia-action.service.js +5 -6
  100. package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
  101. package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
  102. package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
  103. package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
  104. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +2 -4
  105. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
  106. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +13 -35
  107. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +17 -34
  108. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +1 -1
  109. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
  110. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +2 -2
  111. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +2 -2
  112. package/src/api/cyberia-entity/cyberia-entity.controller.js +1 -1
  113. package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
  114. package/src/api/cyberia-entity/cyberia-entity.router.js +1 -1
  115. package/src/api/cyberia-entity/cyberia-entity.service.js +3 -3
  116. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
  117. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
  118. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +17 -2
  119. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +468 -5
  120. package/src/api/cyberia-instance/cyberia-fallback-capture.js +502 -0
  121. package/src/api/cyberia-instance/cyberia-fallback-world.js +29 -40
  122. package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
  123. package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
  124. package/src/api/cyberia-instance/cyberia-instance-map.service.js +35 -142
  125. package/src/api/cyberia-instance/cyberia-instance.controller.js +2 -5
  126. package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
  127. package/src/api/cyberia-instance/cyberia-instance.router.js +5 -18
  128. package/src/api/cyberia-instance/cyberia-instance.service.js +5 -45
  129. package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
  130. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +1 -1
  131. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
  132. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +1 -1
  133. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +76 -3
  134. package/src/api/cyberia-map/cyberia-map.controller.js +1 -1
  135. package/src/api/cyberia-map/cyberia-map.router.js +1 -1
  136. package/src/api/cyberia-map/cyberia-map.service.js +8 -4
  137. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
  138. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
  139. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
  140. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
  141. package/src/api/cyberia-quest/cyberia-quest.controller.js +1 -1
  142. package/src/api/cyberia-quest/cyberia-quest.router.js +4 -4
  143. package/src/api/cyberia-quest/cyberia-quest.service.js +5 -5
  144. package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +1 -1
  145. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
  146. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +1 -1
  147. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +2 -2
  148. package/src/api/cyberia-saga/cyberia-saga.controller.js +1 -1
  149. package/src/api/cyberia-saga/cyberia-saga.router.js +1 -1
  150. package/src/api/cyberia-saga/cyberia-saga.service.js +2 -2
  151. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -214
  152. package/src/api/cyberia-skill/cyberia-skill.controller.js +1 -1
  153. package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
  154. package/src/api/cyberia-skill/cyberia-skill.router.js +1 -1
  155. package/src/api/cyberia-skill/cyberia-skill.service.js +2 -2
  156. package/src/api/default/default.controller.js +1 -1
  157. package/src/api/default/default.router.js +1 -1
  158. package/src/api/default/default.service.js +2 -2
  159. package/src/api/document/document.controller.js +1 -1
  160. package/src/api/document/document.router.js +1 -1
  161. package/src/api/document/document.service.js +2 -2
  162. package/src/api/file/file.controller.js +1 -1
  163. package/src/api/file/file.ref.js +153 -0
  164. package/src/api/file/file.ref.json +11 -2
  165. package/src/api/file/file.router.js +1 -1
  166. package/src/api/file/file.service.js +2 -2
  167. package/src/api/instance/instance.controller.js +1 -1
  168. package/src/api/instance/instance.router.js +1 -1
  169. package/src/api/instance/instance.service.js +2 -2
  170. package/src/api/ipfs/ipfs.controller.js +1 -1
  171. package/src/api/ipfs/ipfs.router.js +2 -2
  172. package/src/api/ipfs/ipfs.service.js +2 -2
  173. package/src/api/object-layer/object-layer.controller.js +1 -1
  174. package/src/api/object-layer/object-layer.model.js +47 -12
  175. package/src/api/object-layer/object-layer.router.js +12 -5
  176. package/src/api/object-layer/object-layer.service.js +116 -123
  177. package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +1 -1
  178. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +1 -1
  179. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +2 -2
  180. package/src/api/test/test.controller.js +1 -1
  181. package/src/api/test/test.service.js +1 -1
  182. package/src/api/user/guest.service.js +3 -3
  183. package/src/api/user/user.controller.js +1 -1
  184. package/src/api/user/user.router.js +2 -2
  185. package/src/api/user/user.service.js +4 -4
  186. package/src/api.js +5 -9
  187. package/src/cli/app.js +402 -0
  188. package/src/cli/baremetal.js +30 -35
  189. package/src/cli/client.js +306 -0
  190. package/src/cli/cloud-init.js +3 -3
  191. package/src/cli/cluster.js +191 -82
  192. package/src/cli/db.js +120 -68
  193. package/src/cli/deploy.js +157 -75
  194. package/src/cli/docker-compose.js +5 -5
  195. package/src/cli/domains.js +184 -0
  196. package/src/cli/dotenv-store.js +143 -0
  197. package/src/cli/event.js +2281 -0
  198. package/src/cli/fs-selection.js +115 -0
  199. package/src/cli/fs.js +159 -343
  200. package/src/cli/host.js +672 -0
  201. package/src/cli/image.js +4 -6
  202. package/src/cli/index.js +343 -151
  203. package/src/cli/ipfs.js +34 -30
  204. package/src/cli/kickstart.js +1 -1
  205. package/src/cli/kubectl.js +218 -41
  206. package/src/cli/lxd.js +3 -3
  207. package/src/cli/monitor.js +1359 -26
  208. package/src/cli/package.js +90 -0
  209. package/src/cli/release.js +16 -12
  210. package/src/cli/repository.js +586 -363
  211. package/src/cli/run.js +398 -843
  212. package/src/cli/secrets.js +2158 -964
  213. package/src/cli/ssh.js +478 -279
  214. package/src/cli/state.js +394 -0
  215. package/src/cli/static.js +2 -2
  216. package/src/cli/system.js +1 -1
  217. package/src/cli/test.js +203 -125
  218. package/src/cli/vultr.js +61 -21
  219. package/src/cli/wireguard.js +1112 -306
  220. package/src/client/components/core/CalendarCore.js +53 -96
  221. package/src/client/components/core/Docs.js +327 -303
  222. package/src/client/components/core/FileExplorer.js +0 -21
  223. package/src/client/components/core/Modal.js +56 -1
  224. package/src/client/components/core/PanelForm.js +4 -4
  225. package/src/client/components/core/Repository.js +56 -0
  226. package/src/client/components/core/Translate.js +7 -0
  227. package/src/client/components/core/{FullScreen.js → ViewModeController.js} +42 -39
  228. package/src/client/components/core/Wallet.js +0 -11
  229. package/src/client/components/core/Worker.js +7 -1
  230. package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +2 -2
  231. package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
  232. package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
  233. package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
  234. package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
  235. package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
  236. package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
  237. package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
  238. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
  239. package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
  240. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +2 -35
  241. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
  242. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
  243. package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +2 -2
  244. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
  245. package/src/client/components/default/AppShellDefault.js +1 -1
  246. package/src/client/components/default/SettingsDefault.js +2 -2
  247. package/src/client/components/itemledger/SettingsItemledger.js +2 -2
  248. package/src/client/components/underpost/AppShellUnderpost.js +3 -2
  249. package/src/client/components/underpost/SettingsUnderpost.js +2 -2
  250. package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
  251. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +313 -30
  252. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
  253. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
  254. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
  255. package/src/client/public/cyberia-docs/CYBERIA.md +48 -28
  256. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
  257. package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +23 -6
  258. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  259. package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
  260. package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
  261. package/src/client/public/default/android-chrome-144x144.png +0 -0
  262. package/src/client/public/default/android-chrome-192x192.png +0 -0
  263. package/src/client/public/default/android-chrome-256x256.png +0 -0
  264. package/src/client/public/default/android-chrome-36x36.png +0 -0
  265. package/src/client/public/default/android-chrome-384x384.png +0 -0
  266. package/src/client/public/default/android-chrome-48x48.png +0 -0
  267. package/src/client/public/default/android-chrome-512x512.png +0 -0
  268. package/src/client/public/default/android-chrome-72x72.png +0 -0
  269. package/src/client/public/default/android-chrome-96x96.png +0 -0
  270. package/src/client/public/default/apple-touch-icon-1024x1024.png +0 -0
  271. package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
  272. package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
  273. package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
  274. package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
  275. package/src/client/public/default/apple-touch-icon-167x167.png +0 -0
  276. package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
  277. package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
  278. package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
  279. package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
  280. package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
  281. package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
  282. package/src/client/public/default/apple-touch-icon.png +0 -0
  283. package/src/client/public/default/apple-touch-startup-image-1125x2436.png +0 -0
  284. package/src/client/public/default/apple-touch-startup-image-1136x640.png +0 -0
  285. package/src/client/public/default/apple-touch-startup-image-1170x2532.png +0 -0
  286. package/src/client/public/default/apple-touch-startup-image-1179x2556.png +0 -0
  287. package/src/client/public/default/apple-touch-startup-image-1242x2208.png +0 -0
  288. package/src/client/public/default/apple-touch-startup-image-1242x2688.png +0 -0
  289. package/src/client/public/default/apple-touch-startup-image-1284x2778.png +0 -0
  290. package/src/client/public/default/apple-touch-startup-image-1290x2796.png +0 -0
  291. package/src/client/public/default/apple-touch-startup-image-1334x750.png +0 -0
  292. package/src/client/public/default/apple-touch-startup-image-1488x2266.png +0 -0
  293. package/src/client/public/default/apple-touch-startup-image-1536x2048.png +0 -0
  294. package/src/client/public/default/apple-touch-startup-image-1620x2160.png +0 -0
  295. package/src/client/public/default/apple-touch-startup-image-1640x2160.png +0 -0
  296. package/src/client/public/default/apple-touch-startup-image-1668x2224.png +0 -0
  297. package/src/client/public/default/apple-touch-startup-image-1668x2388.png +0 -0
  298. package/src/client/public/default/apple-touch-startup-image-1792x828.png +0 -0
  299. package/src/client/public/default/apple-touch-startup-image-2048x1536.png +0 -0
  300. package/src/client/public/default/apple-touch-startup-image-2048x2732.png +0 -0
  301. package/src/client/public/default/apple-touch-startup-image-2160x1620.png +0 -0
  302. package/src/client/public/default/apple-touch-startup-image-2160x1640.png +0 -0
  303. package/src/client/public/default/apple-touch-startup-image-2208x1242.png +0 -0
  304. package/src/client/public/default/apple-touch-startup-image-2224x1668.png +0 -0
  305. package/src/client/public/default/apple-touch-startup-image-2266x1488.png +0 -0
  306. package/src/client/public/default/apple-touch-startup-image-2388x1668.png +0 -0
  307. package/src/client/public/default/apple-touch-startup-image-2436x1125.png +0 -0
  308. package/src/client/public/default/apple-touch-startup-image-2532x1170.png +0 -0
  309. package/src/client/public/default/apple-touch-startup-image-2556x1179.png +0 -0
  310. package/src/client/public/default/apple-touch-startup-image-2688x1242.png +0 -0
  311. package/src/client/public/default/apple-touch-startup-image-2732x2048.png +0 -0
  312. package/src/client/public/default/apple-touch-startup-image-2778x1284.png +0 -0
  313. package/src/client/public/default/apple-touch-startup-image-2796x1290.png +0 -0
  314. package/src/client/public/default/apple-touch-startup-image-640x1136.png +0 -0
  315. package/src/client/public/default/apple-touch-startup-image-750x1334.png +0 -0
  316. package/src/client/public/default/apple-touch-startup-image-828x1792.png +0 -0
  317. package/src/client/public/default/assets/background/white.jpg +0 -0
  318. package/src/client/public/default/browserconfig.xml +1 -1
  319. package/src/client/public/default/favicon-16x16.png +0 -0
  320. package/src/client/public/default/favicon-32x32.png +0 -0
  321. package/src/client/public/default/favicon-48x48.png +0 -0
  322. package/src/client/public/default/favicon.ico +0 -0
  323. package/src/client/public/default/manifest.webmanifest +5 -5
  324. package/src/client/public/default/mstile-144x144.png +0 -0
  325. package/src/client/public/default/mstile-150x150.png +0 -0
  326. package/src/client/public/default/mstile-310x150.png +0 -0
  327. package/src/client/public/default/mstile-310x310.png +0 -0
  328. package/src/client/public/default/mstile-70x70.png +0 -0
  329. package/src/client/public/default/yandex-browser-50x50.png +0 -0
  330. package/src/client/public/default/yandex-browser-manifest.json +1 -1
  331. package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
  332. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
  333. package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
  334. package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
  335. package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
  336. package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
  337. package/src/client/services/object-layer/object-layer.management.js +6 -10
  338. package/src/client/services/object-layer/object-layer.service.js +3 -1
  339. package/src/client/ssr/RootDocument.js +1 -1
  340. package/src/client/ssr/body/404.js +1 -1
  341. package/src/client/ssr/body/500.js +1 -1
  342. package/src/client/ssr/body/CacheControl.js +1 -1
  343. package/src/client/ssr/body/CyberiaDefaultSplashScreen.js +1 -1
  344. package/src/client/ssr/body/DefaultSplashScreen.js +1 -1
  345. package/src/client/ssr/body/SwaggerDarkMode.js +1 -1
  346. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +1 -1
  347. package/src/client/ssr/head/CryptokoynScripts.js +1 -1
  348. package/src/client/ssr/head/Css.js +1 -1
  349. package/src/client/ssr/head/CyberiaPortalScripts.js +1 -1
  350. package/src/client/ssr/head/DefaultScripts.js +1 -1
  351. package/src/client/ssr/head/ItemledgerScripts.js +1 -1
  352. package/src/client/ssr/head/Microdata.js +1 -1
  353. package/src/client/ssr/head/Production.js +1 -1
  354. package/src/client/ssr/head/Pwa.js +1 -1
  355. package/src/client/ssr/head/PwaItemledger.js +1 -1
  356. package/src/client/ssr/head/Seo.js +1 -1
  357. package/src/client/ssr/head/UnderpostScripts.js +1 -1
  358. package/src/client/ssr/mailer/DefaultRecoverEmail.js +1 -1
  359. package/src/client/ssr/mailer/DefaultVerifyEmail.js +1 -1
  360. package/src/client/ssr/views/Cyberia404.js +7 -5
  361. package/src/client/ssr/views/CyberiaServerMetrics.js +1 -1
  362. package/src/client/ssr/views/Maintenance.js +1 -1
  363. package/src/client/ssr/views/NoNetworkConnection.js +1 -1
  364. package/src/client/ssr/views/Test.js +1 -1
  365. package/src/client/sw/core.sw.js +20 -14
  366. package/src/client-builder/client-build-docs.js +92 -77
  367. package/src/client-builder/client-build-live.js +2 -2
  368. package/src/client-builder/client-build.js +105 -74
  369. package/src/client-builder/client-bundle.js +347 -0
  370. package/src/client-builder/client-dev-server.js +3 -3
  371. package/src/client-builder/client-icons.js +2 -2
  372. package/src/client-builder/ssr.js +3 -3
  373. package/src/client.build.js +3 -6
  374. package/src/client.dev.js +3 -6
  375. package/src/db/DataBaseProvider.js +186 -3
  376. package/src/db/mariadb/MariaDB.js +3 -2
  377. package/src/db/mongo/MongoBootstrap.js +273 -101
  378. package/src/db/mongo/MongoExpress.js +187 -0
  379. package/src/db/mongo/MongooseDB.js +7 -8
  380. package/src/{server/valkey.js → db/valkey/Valkey.js} +5 -4
  381. package/src/grpc/cyberia/grpc-server.js +6 -8
  382. package/src/index.js +79 -23
  383. package/src/mailer/EmailRender.js +2 -2
  384. package/src/mailer/MailerInterceptor.js +87 -0
  385. package/src/mailer/MailerProvider.js +50 -6
  386. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +144 -147
  387. package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
  388. package/src/projects/cyberia/besu-genesis-generator.js +8 -17
  389. package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
  390. package/src/projects/cyberia/catalog-cyberia.js +54 -10
  391. package/src/projects/cyberia/gemini-client.js +4 -4
  392. package/src/projects/cyberia/generate-saga.js +5 -12
  393. package/src/projects/cyberia/hot-reload-trigger.js +18 -29
  394. package/src/projects/cyberia/instance-backup.js +210 -0
  395. package/src/projects/cyberia/instance-data.js +178 -258
  396. package/src/projects/cyberia/ipfs-client.js +4 -4
  397. package/src/projects/cyberia/map-preview-generator.js +33 -69
  398. package/src/projects/cyberia/object-layer.js +153 -320
  399. package/src/projects/cyberia/seed-audio.js +139 -0
  400. package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
  401. package/src/projects/cyberia/semantic-layer-generator.js +3 -19
  402. package/src/projects/cyberia/stat-balance.js +81 -0
  403. package/src/projects/cyberia/stat-commands.js +23 -0
  404. package/src/projects/cyberia/stat-contract-generator.js +101 -0
  405. package/src/projects/underpost/catalog-underpost.js +9 -1
  406. package/src/proxy.js +4 -7
  407. package/src/runtime/cyberia-client/Dockerfile +17 -8
  408. package/src/runtime/cyberia-client/Dockerfile.dev +13 -4
  409. package/src/runtime/cyberia-server/Dockerfile +18 -6
  410. package/src/runtime/cyberia-server/Dockerfile.dev +16 -3
  411. package/src/runtime/engine-cyberia/Dockerfile +25 -9
  412. package/src/runtime/engine-cyberia/Dockerfile.dev +25 -9
  413. package/src/runtime/engine-cyberia/Dockerfile.test +5 -5
  414. package/src/runtime/engine-cyberia/compose.env +17 -5
  415. package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
  416. package/src/runtime/express/Express.js +32 -27
  417. package/src/runtime/lampp/Lampp.js +22 -70
  418. package/src/runtime/nginx/Nginx.js +1 -1
  419. package/src/runtime/wp/Wp.js +16 -48
  420. package/src/server/{catalog.js → build/catalog.js} +15 -5
  421. package/src/server/build/coverage.js +188 -0
  422. package/src/server/build/execution.js +402 -0
  423. package/src/server/build/package.js +508 -0
  424. package/src/server/build/testing.js +578 -0
  425. package/src/server/{dns.js → network/dns.js} +144 -11
  426. package/src/server/{forward-proxy.js → network/forward-proxy.js} +10 -18
  427. package/src/server/{middlewares.js → network/middlewares.js} +11 -5
  428. package/src/server/network/node-capability.js +98 -0
  429. package/src/server/{peer.js → network/peer.js} +3 -3
  430. package/src/server/{proxy.js → network/proxy.js} +7 -13
  431. package/src/server/network/router.js +356 -0
  432. package/src/server/{tls.js → network/tls.js} +2 -2
  433. package/src/server/{underpost-compression.js → network/underpost-compression.js} +1 -1
  434. package/src/server/{underpost-gateway.js → network/underpost-gateway.js} +7 -7
  435. package/src/server/{underpost-ingress.js → network/underpost-ingress.js} +2 -2
  436. package/src/server/{cri.js → ops/cri.js} +2 -2
  437. package/src/server/{cron.js → ops/cron.js} +349 -74
  438. package/src/server/ops/event-notification.js +284 -0
  439. package/src/server/{logger.js → ops/logger.js} +143 -23
  440. package/src/server/ops/monitoring.js +1724 -0
  441. package/src/server/{systemd.js → ops/systemd.js} +55 -1
  442. package/src/server/{conf.js → runtime/conf.js} +516 -423
  443. package/src/server/runtime/config-scope.js +215 -0
  444. package/src/server/runtime/environment.js +149 -0
  445. package/src/server/{process.js → runtime/process.js} +153 -12
  446. package/src/server/{runtime-status.js → runtime/runtime-status.js} +155 -16
  447. package/src/server/{runtime.js → runtime/runtime.js} +12 -8
  448. package/src/server/{start.js → runtime/start.js} +128 -39
  449. package/src/server/{auth.js → security/auth.js} +7 -8
  450. package/src/server/security/container-storage.js +382 -0
  451. package/src/server/{crypto.js → security/crypto.js} +1 -1
  452. package/src/server/{selinux.js → security/selinux.js} +1 -1
  453. package/src/server/{backup.js → storage/backup.js} +17 -8
  454. package/src/server/{data-query.js → storage/data-query.js} +17 -1
  455. package/src/server/storage/downloader.js +121 -0
  456. package/src/server/storage/repository.js +68 -0
  457. package/src/server/storage/zip.js +153 -0
  458. package/src/server.js +4 -7
  459. package/src/ws/IoInterface.js +1 -1
  460. package/src/ws/IoServer.js +1 -1
  461. package/src/ws/core/core.ws.connection.js +1 -1
  462. package/src/ws/core/core.ws.emit.js +1 -1
  463. package/src/ws/core/core.ws.server.js +1 -1
  464. package/src/ws/default/default.ws.connection.js +1 -1
  465. package/src/ws/default/default.ws.emit.js +1 -1
  466. package/src/ws/default/default.ws.server.js +1 -1
  467. package/test/e2e/event-e2e-public-ingress-down.js +45 -0
  468. package/test/e2e/event-e2e-wireguard-server-down.js +38 -0
  469. package/test/e2e/event-e2e-wireguard-spoke-down.js +40 -0
  470. package/test/{api.test.js → integration/app/api.test.js} +33 -14
  471. package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
  472. package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
  473. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +64 -0
  474. package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
  475. package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
  476. package/test/{cyberia-instance-conf-defaults.test.js → integration/app/cyberia/cyberia-instance-conf-defaults.test.js} +17 -23
  477. package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
  478. package/test/integration/app/cyberia/cyberia-load.test.js +466 -0
  479. package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
  480. package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
  481. package/test/integration/app/cyberia/fallback-world-capture.test.js +338 -0
  482. package/test/integration/app/cyberia/object-layer-item-id.test.js +327 -0
  483. package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
  484. package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
  485. package/test/integration/app/cyberia/seed-audio.test.js +275 -0
  486. package/test/{shape-generator.test.js → integration/app/cyberia/shape-generator.test.js} +1 -1
  487. package/test/integration/infra/1-security/config-scope.test.js +305 -0
  488. package/test/integration/infra/1-security/container-storage.test.js +356 -0
  489. package/test/integration/infra/1-security/in-pod-cli-surface.test.js +86 -0
  490. package/test/integration/infra/1-security/secret-onboarding.test.js +930 -0
  491. package/test/{selinux.test.js → integration/infra/1-security/selinux.test.js} +2 -4
  492. package/test/{sops-secret-store.test.js → integration/infra/1-security/sops-secret-store.test.js} +415 -61
  493. package/test/integration/infra/1-security/systemd-service.test.js +70 -0
  494. package/test/integration/infra/1-security/underpost-config-secret.test.js +367 -0
  495. package/test/integration/infra/2-network/dns-firewall.test.js +566 -0
  496. package/test/integration/infra/2-network/node-capability.test.js +182 -0
  497. package/test/integration/infra/2-network/wireguard-cli.test.js +1187 -0
  498. package/test/{wireguard-edge.test.js → integration/infra/2-network/wireguard-edge.test.js} +649 -55
  499. package/test/{cluster-instances.test.js → integration/infra/3-cluster/cluster-instances.test.js} +11 -15
  500. package/test/{deploy-node-placement.test.js → integration/infra/3-cluster/deploy-node-placement.test.js} +3 -3
  501. package/test/integration/infra/3-cluster/docker-compose-stack.test.js +313 -0
  502. package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
  503. package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
  504. package/test/integration/infra/4-ingress/deploy-routes.test.js +379 -0
  505. package/test/integration/infra/4-ingress/gateway-static-assets.test.js +453 -0
  506. package/test/{instance-traffic-plan.test.js → integration/infra/4-ingress/instance-traffic-plan.test.js} +10 -6
  507. package/test/{underpost-gateway.test.js → integration/infra/4-ingress/underpost-gateway.test.js} +6 -6
  508. package/test/{underpost-ingress.test.js → integration/infra/4-ingress/underpost-ingress.test.js} +4 -4
  509. package/test/integration/infra/5-observability/cron-jobs.test.js +652 -0
  510. package/test/{deploy-monitor.test.js → integration/infra/5-observability/deploy-monitor.test.js} +29 -31
  511. package/test/integration/infra/5-observability/event-notification.test.js +197 -0
  512. package/test/integration/infra/5-observability/event-remediation.test.js +502 -0
  513. package/test/integration/infra/5-observability/event-targets.test.js +1174 -0
  514. package/test/integration/infra/5-observability/monitoring-stack.test.js +902 -0
  515. package/test/support/shell-harness.js +73 -0
  516. package/test/unit/build-template.test.js +97 -0
  517. package/test/unit/build-workflow-offline.test.js +132 -0
  518. package/test/unit/catalog.test.js +71 -0
  519. package/test/unit/client-build-docs.test.js +361 -0
  520. package/test/unit/client-bundle.test.js +313 -0
  521. package/test/unit/conf-loading.test.js +303 -0
  522. package/test/unit/conf-resolution.test.js +996 -0
  523. package/test/unit/coverage-artifact.test.js +221 -0
  524. package/test/{crypto.test.js → unit/crypto.test.js} +1 -1
  525. package/test/unit/cyberia/instance-backup.test.js +183 -0
  526. package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
  527. package/test/unit/cyberia/publish-workflow.test.js +22 -0
  528. package/test/unit/cyberia/stat-balance.test.js +69 -0
  529. package/test/unit/deploy-env-resolution.test.js +38 -0
  530. package/test/unit/deploy-log-table.test.js +518 -0
  531. package/test/unit/downloader-integrity.test.js +98 -0
  532. package/test/unit/execution-profiles.test.js +219 -0
  533. package/test/unit/file-reference-registry.test.js +141 -0
  534. package/test/unit/fleet-sync-source.test.js +149 -0
  535. package/test/unit/fs-storage-paths.test.js +596 -0
  536. package/test/unit/logger-redaction.test.js +141 -0
  537. package/test/unit/package.test.js +457 -0
  538. package/test/unit/prepare-host.test.js +382 -0
  539. package/test/unit/process-environment.test.js +18 -0
  540. package/test/unit/propagation-message.test.js +78 -0
  541. package/test/unit/redeploy-plan.test.js +24 -0
  542. package/test/unit/release-bump.test.js +474 -0
  543. package/test/unit/ssh-output-redaction.test.js +28 -0
  544. package/test/unit/start-options.test.js +46 -0
  545. package/test/unit/test-tiers.test.js +220 -0
  546. package/test/unit/zip-archive.test.js +142 -0
  547. package/vitest.config.js +66 -0
  548. package/.nycrc +0 -9
  549. package/deploy/lib/logging.sh +0 -96
  550. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +0 -48
  551. package/manifests/mongodb/pv-pvc.yaml +0 -59
  552. package/scripts/coverall-test-cyberia.sh +0 -24
  553. package/scripts/coverall-test.sh +0 -24
  554. package/scripts/link-local-underpost-cli.sh +0 -6
  555. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
  556. package/src/api/user/user.build.js +0 -16
  557. package/src/cli/env.js +0 -177
  558. package/src/client/components/core/Blockchain.js +0 -41
  559. package/src/client/components/core/Blog.js +0 -9
  560. package/src/client/components/core/KeyboardAvoidance.js +0 -145
  561. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
  562. package/src/client/public/default/assets/background/dark.jpg +0 -0
  563. package/src/client/public/default/assets/background/white0-min.jpg +0 -0
  564. package/src/client/public/default/assets/background/white0.jpg +0 -0
  565. package/src/client/public/doc/favicon.ico +0 -0
  566. package/src/client/public/doc/sitemap +0 -148
  567. package/src/server/downloader.js +0 -73
  568. package/src/server/environment.js +0 -98
@@ -3,6 +3,7 @@
3
3
  import { expect } from 'chai';
4
4
  import http from 'node:http';
5
5
  import net from 'node:net';
6
+ import Underpost from '../../../../src/index.js';
6
7
  import {
7
8
  FORWARD_PROXY,
8
9
  fetchViaForwardProxy,
@@ -19,32 +20,46 @@ import {
19
20
  forwardProxyTargetFactory,
20
21
  forwardProxyTunnelTargetFactory,
21
22
  forwardProxyUnitFactory,
22
- } from '../src/server/forward-proxy.js';
23
- import { homeDirectoryPathFactory } from '../src/server/systemd.js';
24
- import {
23
+ } from '../../../../src/server/network/forward-proxy.js';
24
+ import { redactSensitiveText } from '../../../../src/server/ops/logger.js';
25
+ import { ShellExecError, shellArgumentFactory } from '../../../../src/server/runtime/process.js';
26
+ import { probeGroupsFactory, prometheusConfFactory } from '../../../../src/server/ops/monitoring.js';
27
+ import fs from 'node:fs';
28
+ import { Dns } from '../../../../src/server/network/dns.js';
29
+ import { homeDirectoryPathFactory } from '../../../../src/server/ops/systemd.js';
30
+ import UnderpostWireguard, {
31
+ ENGINE_SYNC_STEPS,
25
32
  UNDERPOST_EDGE,
33
+ localPeerFactory,
26
34
  allowedIpsConflictsFactory,
35
+ assertEdgeIdentity,
27
36
  backendNameFactory,
28
37
  defaultPeerFactory,
29
38
  deployListFactory,
39
+ edgeContextFactory,
30
40
  edgeRouteTableFactory,
31
- edgeStateFactory,
41
+ endpointHostFactory,
32
42
  firewallCommandsFactory,
33
43
  haproxyConfFactory,
34
44
  haproxyMapsFactory,
35
45
  hostProxyEntriesFactory,
46
+ hubFactory,
36
47
  instanceProxyEntriesFactory,
37
48
  mergeRouteTablesFactory,
49
+ nodeIdentityFactory,
50
+ nodeNameCandidatesFactory,
38
51
  peerFactory,
39
52
  quicForwardCommandsFactory,
40
53
  redirectHostFactory,
54
+ topologyFactory,
41
55
  tunnelAddressFactory,
42
56
  tunnelNetworkCidrFactory,
57
+ unregisteredPeersFactory,
43
58
  wireguardClientConfFactory,
44
- wireguardClientSettingsFactory,
59
+ wireguardHealthFactory,
45
60
  wireguardServerConfFactory,
46
61
  wireguardStatusFactory,
47
- } from '../src/cli/wireguard.js';
62
+ } from '../../../../src/cli/wireguard.js';
48
63
 
49
64
  // The conf shape the PRD names: two published sites, each fronted by a bare
50
65
  // domain that only redirects to its `www` host.
@@ -94,6 +109,10 @@ const PEERS = [
94
109
  { id: 'homelab-b', address: '10.0.0.3', publicKey: 'BBB=', hosts: ['www.nexodev.org'], instances: ['mmo-server'] },
95
110
  ];
96
111
 
112
+ // Emitted only when this checkout ships the script it names, so the case needs a tree that
113
+ // carries the deploy: the base template restores no deploy id of its own.
114
+ const shipsCyberiaPackageScript = fs.existsSync('./deploy/dd-cyberia/package.sh');
115
+
97
116
  describe('edge hub routing', () => {
98
117
  describe('hostProxyEntriesFactory', () => {
99
118
  it('extracts every domain that declares proxy ports, with its redirect target', () => {
@@ -289,7 +308,7 @@ describe('edge hub routing', () => {
289
308
  });
290
309
 
291
310
  describe('deployListFactory', () => {
292
- it('expands the dd meta id through dd.router', () => {
311
+ it('expands the dd meta id through dd.routes', () => {
293
312
  const list = deployListFactory('dd');
294
313
  expect(list.length).to.be.above(0);
295
314
  for (const id of list) expect(id.startsWith('dd-')).to.equal(true);
@@ -535,15 +554,6 @@ describe('edge hub routing', () => {
535
554
  );
536
555
  expect(client).to.include('PostDown = iptables -D FORWARD -o %i -d 10.0.0.0/24 -j ACCEPT');
537
556
  });
538
-
539
- it('recovers repeatable client settings from an installed config without reading a private key', () => {
540
- expect(wireguardClientSettingsFactory(client)).to.deep.equal({
541
- address: '10.0.0.2/32',
542
- hubPublicKey: 'HUB=',
543
- endpoint: 'vps.example.com:51820',
544
- cidr: '10.0.0.0/24',
545
- });
546
- });
547
557
  });
548
558
 
549
559
  describe('quicForwardCommandsFactory', () => {
@@ -582,41 +592,41 @@ describe('edge hub routing', () => {
582
592
 
583
593
  describe('firewallCommandsFactory', () => {
584
594
  it('opens both transports and the tunnel port on the hub', () => {
585
- const commands = firewallCommandsFactory({ role: 'server' }).join('\n');
595
+ const commands = firewallCommandsFactory({ role: 'hub' }).join('\n');
586
596
  for (const port of ['80/tcp', '443/tcp', '443/udp', '51820/udp']) expect(commands).to.include(port);
587
597
  });
588
598
 
589
599
  // A spoke dials out; it publishes nothing. What it does need is for the
590
600
  // tunnel interface to sit in a zone that permits the forwarded traffic.
591
601
  it('opens nothing on a spoke beyond trusting the tunnel interface', () => {
592
- const commands = firewallCommandsFactory({ role: 'client' }).join('\n');
602
+ const commands = firewallCommandsFactory({ role: 'control' }).join('\n');
593
603
  expect(commands).to.include('--zone=trusted --add-interface=wg0');
594
604
  expect(commands).to.not.include('--add-port');
595
605
  });
596
606
 
597
607
  it('is a no-op where firewalld is not running', () => {
598
- for (const command of firewallCommandsFactory({ role: 'server' }))
608
+ for (const command of firewallCommandsFactory({ role: 'hub' }))
599
609
  expect(command).to.include('systemctl is-active --quiet firewalld');
600
610
  });
601
611
 
602
612
  // A reset that leaves the ports open has not returned the host to zero, and
603
613
  // the only way the two directions cannot drift is sharing one rule list.
604
614
  it('withdraws exactly the rules it opens, for either role', () => {
605
- const opened = firewallCommandsFactory({ role: 'server', listenPort: 51821 });
606
- const withdrawn = firewallCommandsFactory({ role: 'server', listenPort: 51821, remove: true });
615
+ const opened = firewallCommandsFactory({ role: 'hub', listenPort: 51821 });
616
+ const withdrawn = firewallCommandsFactory({ role: 'hub', listenPort: 51821, remove: true });
607
617
  expect(withdrawn).to.have.lengthOf(opened.length);
608
618
  expect(withdrawn.join('\n')).to.equal(opened.join('\n').replace(/--add-/g, '--remove-'));
609
- expect(firewallCommandsFactory({ role: 'client', remove: true }).join('\n')).to.include(
619
+ expect(firewallCommandsFactory({ role: 'worker', remove: true }).join('\n')).to.include(
610
620
  '--zone=trusted --remove-interface=wg0',
611
621
  );
612
622
  });
613
623
  });
614
624
 
615
625
  describe('wireguardStatusFactory', () => {
616
- // One row per spoke carries both halves of its state: what the registry
626
+ // One row per spoke carries both halves of its state: what topology
617
627
  // binds to it, and what the live interface reports — so there is no second
618
628
  // command to list peers.
619
- it('folds link state onto the registry bindings', () => {
629
+ it('folds link state onto topology bindings', () => {
620
630
  const rows = wireguardStatusFactory({
621
631
  peers: PEERS,
622
632
  latestHandshakes: 'AAA=\t1000\nBBB=\t0',
@@ -628,6 +638,8 @@ describe('edge hub routing', () => {
628
638
  {
629
639
  id: 'homelab-a',
630
640
  address: '10.0.0.2',
641
+ managementHost: '',
642
+ publicKey: 'AAA=',
631
643
  allowedIPs: ['10.0.0.2/32', '192.168.10.0/24'],
632
644
  hosts: ['www.dogmadual.com'],
633
645
  instances: [],
@@ -641,6 +653,8 @@ describe('edge hub routing', () => {
641
653
  {
642
654
  id: 'homelab-b',
643
655
  address: '10.0.0.3',
656
+ managementHost: '',
657
+ publicKey: 'BBB=',
644
658
  allowedIPs: ['10.0.0.3/32'],
645
659
  hosts: ['www.nexodev.org'],
646
660
  instances: ['mmo-server'],
@@ -664,8 +678,8 @@ describe('edge hub routing', () => {
664
678
  });
665
679
  });
666
680
 
667
- describe('registry normalization', () => {
668
- it('fills the defaults a hand-written registry may omit', () => {
681
+ describe('topology normalization', () => {
682
+ it('fills the defaults hand-written topology may omit', () => {
669
683
  const peer = peerFactory({ id: 'homelab-a', address: '10.0.0.2' });
670
684
  expect(peer.allowedIPs).to.deep.equal(['10.0.0.2/32']);
671
685
  expect(peer.hosts).to.deep.equal([]);
@@ -673,13 +687,14 @@ describe('edge hub routing', () => {
673
687
  expect(peer.default).to.equal(false);
674
688
  });
675
689
 
676
- // The three routing bindings are the whole registry surface; anything else
690
+ // The three routing bindings are the whole topology surface; anything else
677
691
  // a hand-edited file carries is not a binding the edge honours.
678
692
  it('keeps only the three routing bindings on a peer', () => {
679
693
  const peer = peerFactory({ id: 'a', address: '10.0.0.2', clients: ['legacy'], keepalive: 15 });
680
694
  expect(Object.keys(peer)).to.deep.equal([
681
695
  'id',
682
696
  'address',
697
+ 'managementHost',
683
698
  'publicKey',
684
699
  'allowedIPs',
685
700
  'hosts',
@@ -694,26 +709,101 @@ describe('edge hub routing', () => {
694
709
  ]);
695
710
  });
696
711
 
697
- it('drops registry entries with no id', () => {
698
- const state = edgeStateFactory({ peers: [{ address: '10.0.0.2' }, { id: 'ok', address: '10.0.0.3' }] });
699
- expect(state.peers.map((peer) => peer.id)).to.deep.equal(['ok']);
712
+ it('drops topology peers with no id', () => {
713
+ const hub = hubFactory({ peers: [{ address: '10.0.0.2' }, { id: 'ok', address: '10.0.0.3' }] });
714
+ expect(hub.peers.map((peer) => peer.id)).to.deep.equal(['ok']);
715
+ });
716
+
717
+ it('keeps machine identity fields out of deployment topology', () => {
718
+ const topology = topologyFactory({
719
+ '64.176.25.136': {
720
+ role: 'hub',
721
+ nodeName: 'vultr',
722
+ peerId: 'wrong',
723
+ peers: [],
724
+ },
725
+ });
726
+ expect(topology['64.176.25.136']).to.not.have.keys('role', 'nodeName', 'peerId');
727
+ });
728
+
729
+ it('derives a node runtime context without storing host identity in topology', () => {
730
+ const context = edgeContextFactory({
731
+ topology: {
732
+ '64.176.25.136': {
733
+ publicKey: 'HUB=',
734
+ peers: [{ id: 'homelab-a', address: '10.0.0.2', publicKey: 'SPOKE=' }],
735
+ },
736
+ },
737
+ identity: { nodeName: 'localhost.localdomain', role: 'control', hubHost: '64.176.25.136', peerId: 'homelab-a' },
738
+ });
739
+ expect(context.role).to.equal('control');
740
+ expect(context.peerId).to.equal('homelab-a');
741
+ expect(context.endpoint).to.equal('64.176.25.136:51820');
742
+ expect(context.hubPublicKey).to.equal('HUB=');
700
743
  });
701
744
 
702
- // A spoke rebuilds its interface from `endpoint` + `hubPublicKey`. Without
703
- // the second one recorded, re-running the setup — and every --wireguard-
704
- // reinstall, which ends in one — fails on a flag the operator already gave.
705
- it('records the hub identity a spoke dials, so its setup is repeatable', () => {
706
- const state = edgeStateFactory({ role: 'client', endpoint: 'vps.example.com:51820', hubPublicKey: 'HUB=' });
707
- expect(state.endpoint).to.equal('vps.example.com:51820');
708
- expect(state.hubPublicKey).to.equal('HUB=');
709
- expect(edgeStateFactory().hubPublicKey).to.equal('');
745
+ it('keeps management routing separate from tunnel routing', () => {
746
+ const peer = peerFactory({
747
+ id: 'worker-a',
748
+ address: '10.0.0.3',
749
+ managementHost: '192.168.1.191',
750
+ });
751
+ expect(peer.managementHost).to.equal('192.168.1.191');
752
+ expect(peer.allowedIPs).to.deep.equal(['10.0.0.3/32']);
710
753
  });
711
754
 
712
- it('falls back to the subsystem defaults for an empty registry', () => {
713
- const state = edgeStateFactory();
714
- expect(state.interfaceName).to.equal(UNDERPOST_EDGE.interfaceName);
715
- expect(state.listenPort).to.equal(UNDERPOST_EDGE.listenPort);
716
- expect(state.peers).to.deep.equal([]);
755
+ it('extracts the public host from IPv4, DNS, and IPv6 endpoints', () => {
756
+ expect(endpointHostFactory('64.176.25.136:51820')).to.equal('64.176.25.136');
757
+ expect(endpointHostFactory('edge.example.com:51820')).to.equal('edge.example.com');
758
+ expect(endpointHostFactory('[2001:db8::1]:51820')).to.equal('2001:db8::1');
759
+ });
760
+
761
+ it('requires a fresh hub handshake off the hub', () => {
762
+ const context = {
763
+ role: 'control',
764
+ peerId: 'homelab-a',
765
+ address: '10.0.0.2',
766
+ hubPublicKey: 'HUB=',
767
+ };
768
+ expect(
769
+ wireguardHealthFactory({ context, active: 'active', latestHandshakes: 'HUB=\t1000', now: 1030 }).ok,
770
+ ).to.equal(true);
771
+ expect(
772
+ wireguardHealthFactory({ context, active: 'active', latestHandshakes: 'HUB=\t1000', now: 1200 }).ok,
773
+ ).to.equal(false);
774
+ });
775
+
776
+ it('requires the default routing peer to reconnect on the hub', () => {
777
+ const context = {
778
+ role: 'hub',
779
+ peers: [
780
+ { id: 'homelab-a', address: '10.0.0.2', publicKey: 'DEFAULT=', default: true },
781
+ { id: 'worker-a', address: '10.0.0.3', publicKey: 'WORKER=' },
782
+ ],
783
+ };
784
+ const disconnected = wireguardHealthFactory({ context, active: 'active', latestHandshakes: '', now: 1030 });
785
+ expect(disconnected).to.include({ ok: false, peerId: 'homelab-a', handshakeAgeSeconds: null });
786
+ const connected = wireguardHealthFactory({
787
+ context,
788
+ active: 'active',
789
+ latestHandshakes: 'DEFAULT=\t1000',
790
+ now: 1030,
791
+ });
792
+ expect(connected).to.include({ ok: true, peerId: 'homelab-a', handshakeAgeSeconds: 30 });
793
+ });
794
+
795
+ it('refuses remediation on a different role or peer identity', () => {
796
+ const state = { role: 'control', peerId: 'homelab-a' };
797
+ expect(() => assertEdgeIdentity(state, { expectedRole: 'hub' })).to.throw("expected role 'hub'");
798
+ expect(() => assertEdgeIdentity(state, { expectedRole: 'control', expectedId: 'worker-a' })).to.throw(
799
+ "expected peer 'worker-a'",
800
+ );
801
+ expect(assertEdgeIdentity(state, { expectedRole: 'control', expectedId: 'homelab-a' })).to.deep.equal(state);
802
+ });
803
+
804
+ it('normalizes only supported node roles', () => {
805
+ expect(nodeIdentityFactory({ role: 'control' }).role).to.equal('control');
806
+ expect(nodeIdentityFactory({ role: 'invalid' }).role).to.equal('');
717
807
  });
718
808
  });
719
809
  });
@@ -841,16 +931,16 @@ describe('edge hub forward proxy', () => {
841
931
  // The listener binds the tunnel address alone, and the rule narrows the port
842
932
  // to the tunnel CIDR on top of that — the port is reachable from nowhere else.
843
933
  it('admits the proxy port from the tunnel only, on the hub only', () => {
844
- const hub = firewallCommandsFactory({ role: 'server' }).join('\n');
934
+ const hub = firewallCommandsFactory({ role: 'hub' }).join('\n');
845
935
  expect(hub).to.include(
846
936
  `--add-rich-rule="rule family=ipv4 source address=${UNDERPOST_EDGE.tunnelCidr} port port=${FORWARD_PROXY.port} protocol=tcp accept"`,
847
937
  );
848
938
  expect(hub).to.not.include(`--add-port=${FORWARD_PROXY.port}`);
849
- expect(firewallCommandsFactory({ role: 'client' }).join('\n')).to.not.include('rich-rule');
939
+ expect(firewallCommandsFactory({ role: 'control' }).join('\n')).to.not.include('rich-rule');
850
940
  });
851
941
 
852
942
  it('admits a custom recorded tunnel subnet instead of the default', () => {
853
- expect(firewallCommandsFactory({ role: 'server', tunnelCidr: '10.23.0.0/24' }).join('\n')).to.include(
943
+ expect(firewallCommandsFactory({ role: 'hub', tunnelCidr: '10.23.0.0/24' }).join('\n')).to.include(
854
944
  `source address=10.23.0.0/24 port port=${FORWARD_PROXY.port} protocol=tcp accept`,
855
945
  );
856
946
  expect(tunnelNetworkCidrFactory('10.23.7.9/20')).to.equal('10.23.0.0/20');
@@ -1010,7 +1100,7 @@ describe('edge hub forward proxy', () => {
1010
1100
  let proxy;
1011
1101
  let proxyConfig;
1012
1102
 
1013
- before(async () => {
1103
+ beforeAll(async () => {
1014
1104
  origin = await listen(
1015
1105
  http.createServer((req, res) => {
1016
1106
  res.writeHead(200, { 'content-type': 'application/json' });
@@ -1026,7 +1116,7 @@ describe('edge hub forward proxy', () => {
1026
1116
  proxyConfig = { host: '127.0.0.1', port: port(proxy), apiKey: API_KEY };
1027
1117
  });
1028
1118
 
1029
- after(() => {
1119
+ afterAll(() => {
1030
1120
  for (const server of servers) server.close();
1031
1121
  });
1032
1122
 
@@ -1128,9 +1218,9 @@ describe('edge hub forward proxy', () => {
1128
1218
  const SPOKE = [peerFactory({ id: 'homelab-a', address: '10.0.0.2', publicKey: 'AAA=', default: true })];
1129
1219
 
1130
1220
  it('is closed by default, so no edge exposes SSH it was not told to', () => {
1131
- expect(edgeStateFactory({}).sshForwardPort).to.equal(0);
1132
- expect(edgeStateFactory({ sshForwardPort: 'nonsense' }).sshForwardPort).to.equal(0);
1133
- expect(edgeStateFactory({ sshForwardPort: 2222 }).sshForwardPort).to.equal(2222);
1221
+ expect(hubFactory({}).sshForwardPort).to.equal(0);
1222
+ expect(hubFactory({ sshForwardPort: 'nonsense' }).sshForwardPort).to.equal(0);
1223
+ expect(hubFactory({ sshForwardPort: 2222 }).sshForwardPort).to.equal(2222);
1134
1224
  expect(haproxyConfFactory({ peers: SPOKE, defaultPeerId: 'homelab-a' })).to.not.include('fe_ssh');
1135
1225
  });
1136
1226
 
@@ -1163,15 +1253,519 @@ describe('edge hub forward proxy', () => {
1163
1253
  });
1164
1254
 
1165
1255
  it('opens the port publicly, and withdraws it on teardown', () => {
1166
- const opened = firewallCommandsFactory({ role: 'server', sshForwardPort: 2222 }).join('\n');
1256
+ const opened = firewallCommandsFactory({ role: 'hub', sshForwardPort: 2222 }).join('\n');
1167
1257
  expect(opened).to.include('--add-port=2222/tcp');
1168
- const closed = firewallCommandsFactory({ role: 'server', sshForwardPort: 2222, remove: true }).join('\n');
1258
+ const closed = firewallCommandsFactory({ role: 'hub', sshForwardPort: 2222, remove: true }).join('\n');
1169
1259
  expect(closed).to.include('--remove-port=2222/tcp');
1170
1260
  });
1171
1261
 
1172
1262
  it('opens no SSH port when none is configured, and never on a spoke', () => {
1173
- expect(firewallCommandsFactory({ role: 'server' }).join('\n')).to.not.include('2222');
1174
- expect(firewallCommandsFactory({ role: 'client', sshForwardPort: 2222 }).join('\n')).to.not.include('2222');
1263
+ expect(firewallCommandsFactory({ role: 'hub' }).join('\n')).to.not.include('2222');
1264
+ expect(firewallCommandsFactory({ role: 'worker', sshForwardPort: 2222 }).join('\n')).to.not.include('2222');
1265
+ });
1266
+ });
1267
+
1268
+ // A spoke is identified by the key it presents, not by the name topology
1269
+ // gives it. Reporting the name without the key is what turns "the hub has the
1270
+ // wrong key for this machine" into an unanswerable question.
1271
+ describe('peer identity in --status', () => {
1272
+ it('reports the public key alongside the topology name', () => {
1273
+ const [row] = wireguardStatusFactory({
1274
+ peers: [{ id: 'homelab-a', address: '10.0.0.2', publicKey: 'w1M+9VHZ=' }],
1275
+ latestHandshakes: 'w1M+9VHZ=\t1000',
1276
+ now: 1030,
1277
+ });
1278
+ expect(row.publicKey).to.equal('w1M+9VHZ=');
1279
+ expect(row.id).to.equal('homelab-a');
1280
+ });
1281
+
1282
+ it('carries the key through the off-box view too', () => {
1283
+ const hub = hubFactory({ peers: [{ id: 'a', address: '10.0.0.2', publicKey: 'KEY=' }] });
1284
+ expect(hub.peers[0].publicKey).to.equal('KEY=');
1285
+ });
1286
+ });
1287
+
1288
+ // `wg set` adds a peer and never removes the one it supersedes, so a corrected
1289
+ // --peer-add leaves the previous identity on the wire holding its old
1290
+ // handshake — invisible to every view that is keyed by topology.
1291
+ describe('unregisteredPeersFactory', () => {
1292
+ const PEER = { id: 'homelab-a', address: '10.0.0.2', publicKey: 'CURRENT=' };
1293
+
1294
+ it('reports an identity on the wire that topology does not name', () => {
1295
+ expect(
1296
+ unregisteredPeersFactory({
1297
+ peers: [PEER],
1298
+ latestHandshakes: 'CURRENT=\t900\nSUPERSEDED=\t500',
1299
+ now: 1000,
1300
+ }),
1301
+ ).to.deep.equal([{ publicKey: 'SUPERSEDED=', handshakeAgeSeconds: 500 }]);
1302
+ });
1303
+
1304
+ it('reports a leftover that never handshaked as an unknown age, not as fresh', () => {
1305
+ const [row] = unregisteredPeersFactory({ peers: [PEER], latestHandshakes: 'GHOST=\t0', now: 1000 });
1306
+ expect(row).to.deep.equal({ publicKey: 'GHOST=', handshakeAgeSeconds: null });
1307
+ });
1308
+
1309
+ it('is quiet when the wire matches topology', () => {
1310
+ expect(unregisteredPeersFactory({ peers: [PEER], latestHandshakes: 'CURRENT=\t900', now: 1000 })).to.deep.equal(
1311
+ [],
1312
+ );
1313
+ expect(unregisteredPeersFactory({ peers: [PEER], latestHandshakes: '' })).to.deep.equal([]);
1314
+ });
1315
+ });
1316
+ });
1317
+
1318
+ describe('node document resolution', () => {
1319
+ it('prefers the full hostname, then its short name', () => {
1320
+ expect(nodeNameCandidatesFactory('vultr.guest')).to.deep.equal(['vultr.guest', 'vultr']);
1321
+ expect(nodeNameCandidatesFactory('localhost.localdomain')).to.deep.equal(['localhost.localdomain', 'localhost']);
1322
+ });
1323
+
1324
+ it('offers one candidate when the hostname carries no domain', () => {
1325
+ expect(nodeNameCandidatesFactory('vultr')).to.deep.equal(['vultr']);
1326
+ });
1327
+
1328
+ it('rejects a hostname a node document could not be named after', () => {
1329
+ expect(() => nodeNameCandidatesFactory('bad/name')).to.throw('node names may contain only');
1330
+ });
1331
+ });
1332
+
1333
+ describe('engine sync', () => {
1334
+ const account = process.env.GITHUB_USERNAME;
1335
+ const token = process.env.GITHUB_TOKEN;
1336
+ let getDefaultBranch;
1337
+
1338
+ beforeEach(() => {
1339
+ getDefaultBranch = Underpost.repo.getDefaultBranch;
1340
+ Underpost.repo.getDefaultBranch = (repo) => (repo.endsWith('/engine') ? 'master' : 'main');
1341
+ });
1342
+ afterEach(() => {
1343
+ Underpost.repo.getDefaultBranch = getDefaultBranch;
1344
+ if (account === undefined) delete process.env.GITHUB_USERNAME;
1345
+ else process.env.GITHUB_USERNAME = account;
1346
+ if (token === undefined) delete process.env.GITHUB_TOKEN;
1347
+ else process.env.GITHUB_TOKEN = token;
1348
+ });
1349
+
1350
+ it('accepts a slug or a clone URL for the same repository', () => {
1351
+ for (const reference of [
1352
+ 'underpostnet/engine',
1353
+ 'https://github.com/underpostnet/engine',
1354
+ 'https://github.com/underpostnet/engine.git',
1355
+ 'git@github.com:underpostnet/engine.git',
1356
+ ])
1357
+ expect(Underpost.repo.repoSlugFactory(reference), reference).to.equal('underpostnet/engine');
1358
+ });
1359
+
1360
+ it('keeps GitHub credentials out of Git URLs and command arguments', () => {
1361
+ process.env.GITHUB_TOKEN = 'github-token-fixture';
1362
+ const auth = Underpost.repo.gitAuthFactory('https://x-access-token:stale-token@github.com/underpostnet/engine.git');
1363
+
1364
+ expect(auth.url).to.equal('https://github.com/underpostnet/engine.git');
1365
+ expect(auth.url).to.not.match(/token|@github/);
1366
+ expect(auth.env.GIT_TERMINAL_PROMPT).to.equal('0');
1367
+ expect(auth.env.GIT_CONFIG_KEY_1).to.equal('http.https://github.com/.extraheader');
1368
+ expect(Buffer.from(auth.env.GIT_CONFIG_VALUE_1.split(' ').pop(), 'base64').toString()).to.equal(
1369
+ 'x-access-token:github-token-fixture',
1370
+ );
1371
+ });
1372
+
1373
+ it('refuses a reference that names no owner', () => {
1374
+ expect(() => Underpost.repo.repoSlugFactory('engine')).to.throw('is not an owner/repo reference');
1375
+ });
1376
+
1377
+ it('pairs an engine source with the conf repository of the id they share', () => {
1378
+ process.env.GITHUB_USERNAME = 'someone';
1379
+ expect(Underpost.repo.enginePairFactory()).to.deep.equal({
1380
+ engine: 'someone/engine',
1381
+ enginePrivate: 'someone/engine-private',
1382
+ });
1383
+ for (const engine of ['underpostnet/engine-test-lampp', 'underpostnet/engine-lampp'])
1384
+ expect(Underpost.repo.enginePairFactory({ engine }), engine).to.deep.equal({
1385
+ engine,
1386
+ enginePrivate: 'underpostnet/engine-lampp-private',
1387
+ });
1388
+ });
1389
+
1390
+ it('takes the conf repository owner from the engine, not from the account', () => {
1391
+ process.env.GITHUB_USERNAME = 'someone';
1392
+ expect(Underpost.repo.enginePairFactory({ engine: 'https://github.com/underpostnet/engine.git' })).to.deep.equal({
1393
+ engine: 'underpostnet/engine',
1394
+ enginePrivate: 'underpostnet/engine-private',
1395
+ });
1396
+ });
1397
+
1398
+ it('lets an explicit conf repository override the pairing', () => {
1399
+ process.env.GITHUB_USERNAME = 'someone';
1400
+ expect(
1401
+ Underpost.repo.enginePairFactory({
1402
+ engine: 'underpostnet/engine-test-lampp',
1403
+ enginePrivate: 'https://github.com/underpostnet/engine-core-private.git',
1404
+ }),
1405
+ ).to.deep.equal({
1406
+ engine: 'underpostnet/engine-test-lampp',
1407
+ enginePrivate: 'underpostnet/engine-core-private',
1408
+ });
1409
+ });
1410
+
1411
+ it('binds every repository placeholder to the configured account', () => {
1412
+ process.env.GITHUB_USERNAME = 'someone';
1413
+ const commands = UnderpostWireguard.API.syncCommands().map((step) => step.command);
1414
+ expect(commands.join('\n')).to.not.match(/<[a-z-]+>/);
1415
+ expect(commands.some((command) => command.includes('package.sh'))).to.equal(false);
1416
+ expect(commands).to.include('node bin cmt --switch-repo someone/engine --target-branch master');
1417
+ expect(commands).to.include(
1418
+ 'node bin cmt ./engine-private --switch-repo someone/engine-private --target-branch main',
1419
+ );
1420
+ });
1421
+
1422
+ it('pulls the named engine while the private repository follows the account', () => {
1423
+ process.env.GITHUB_USERNAME = 'someone';
1424
+ const commands = UnderpostWireguard.API.syncCommands({
1425
+ repoEngine: 'https://github.com/underpostnet/engine.git',
1426
+ }).map((step) => step.command);
1427
+ expect(commands).to.include('node bin cmt --switch-repo underpostnet/engine --target-branch master');
1428
+ expect(commands).to.include(
1429
+ 'node bin cmt ./engine-private --switch-repo underpostnet/engine-private --target-branch main',
1430
+ );
1431
+ });
1432
+
1433
+ it('overrides the private repository with --repo-engine-private', () => {
1434
+ process.env.GITHUB_USERNAME = 'someone';
1435
+ const commands = UnderpostWireguard.API.syncCommands({
1436
+ repoEngine: 'https://github.com/underpostnet/engine.git',
1437
+ repoEnginePrivate: 'https://github.com/underposnet/engine-core-private.git',
1438
+ }).map((step) => step.command);
1439
+ expect(commands).to.include('node bin cmt --switch-repo underpostnet/engine --target-branch master');
1440
+ expect(commands).to.include(
1441
+ 'node bin cmt ./engine-private --switch-repo underposnet/engine-core-private --target-branch main',
1442
+ );
1443
+ });
1444
+
1445
+ it('runs only --cmd in place of the sync sequence, no placeholders unresolved', () => {
1446
+ process.env.GITHUB_USERNAME = 'someone';
1447
+ const commands = UnderpostWireguard.API.syncCommands({ cmd: 'uptime, hostname -I' }).map((step) => step.command);
1448
+ expect(commands).to.deep.equal(['uptime', 'hostname -I']);
1449
+ expect(commands.join('\n')).to.not.match(/<[a-z-]+>/);
1450
+ expect(commands).to.not.include('node bin cmt --switch-repo');
1451
+ });
1452
+
1453
+ it('labels a --cmd fleet run as a command, not a sync', () => {
1454
+ process.env.GITHUB_USERNAME = 'someone';
1455
+ const script = UnderpostWireguard.API.syncScript({ cmd: 'uptime, hostname' });
1456
+ expect(script).to.include("echo '[cmd] uptime'");
1457
+ expect(script).to.include("echo '[cmd] hostname'");
1458
+ expect(script).to.not.include('[sync]');
1459
+ });
1460
+
1461
+ it('names the branch the node must fetch instead of letting it guess', () => {
1462
+ process.env.GITHUB_USERNAME = 'someone';
1463
+ Underpost.repo.getDefaultBranch = () => 'trunk';
1464
+ expect(UnderpostWireguard.API.syncCommands().map((step) => step.command)).to.include(
1465
+ 'node bin cmt --switch-repo someone/engine --target-branch trunk',
1466
+ );
1467
+ });
1468
+
1469
+ it('reaches a node once, chaining every declared step in order', () => {
1470
+ process.env.GITHUB_USERNAME = 'someone';
1471
+ const script = UnderpostWireguard.API.syncScript();
1472
+ const commands = UnderpostWireguard.API.syncCommands().map((step) => step.command);
1473
+ expect(script.match(/echo '\[sync\] /g) || []).to.have.lengthOf(commands.length);
1474
+ expect(script).to.not.include(';\n');
1475
+ let cursor = -1;
1476
+ for (const command of commands) {
1477
+ const at = script.indexOf(`echo '[sync] ${redactSensitiveText(command)}'`);
1478
+ expect(at, command).to.be.greaterThan(cursor);
1479
+ cursor = at;
1480
+ }
1481
+ });
1482
+
1483
+ it('redacts a credential from the remote progress line without changing the executed step', () => {
1484
+ process.env.GITHUB_TOKEN = 'github-token-fixture';
1485
+ const script = UnderpostWireguard.API.syncScript();
1486
+
1487
+ expect(script).to.include("echo '[sync] node bin host set GITHUB_TOKEN [REDACTED]'");
1488
+ expect(script).to.include('node bin host set GITHUB_TOKEN github-token-fixture');
1489
+ expect(script.match(/github-token-fixture/g)).to.have.lengthOf(1);
1490
+ });
1491
+
1492
+ it('reconciles the dispatcher for the role of the node it reached', () => {
1493
+ process.env.GITHUB_USERNAME = 'someone';
1494
+ const dispatcher = (nodeRole) =>
1495
+ UnderpostWireguard.API.syncCommands({ nodeRole }).find((step) => step.command.includes('underpost-event'));
1496
+
1497
+ // Only a control node is in a position to answer Alertmanager and repair the tunnel, and
1498
+ // `event --service` refuses to install the dispatcher anywhere else. Regression: `systemctl
1499
+ // restart` reran a generated unit whose ExecStart named a Node binary systemd could not
1500
+ // execute, until it rate-limited; only the generator re-probes that path and rewrites it.
1501
+ expect(dispatcher('control').command).to.include('node bin event --service');
1502
+ expect(dispatcher('control').command).to.not.include('systemctl');
1503
+ for (const nodeRole of ['worker', 'hub'])
1504
+ expect(dispatcher(nodeRole).command, nodeRole).to.include('node bin event --service-stop');
1505
+ expect(dispatcher('')).to.equal(undefined);
1506
+ });
1507
+
1508
+ it('reconciles the hub forward proxy, which runs the checkout the switch replaced', () => {
1509
+ process.env.GITHUB_USERNAME = 'someone';
1510
+ const proxy = (nodeRole) =>
1511
+ UnderpostWireguard.API.syncCommands({ nodeRole }).find((step) => step.command.includes('forward-proxy'));
1512
+
1513
+ expect(proxy('hub').command).to.include('node bin wireguard --forward-proxy-server');
1514
+ expect(proxy('hub').command).to.include('test -f /etc/systemd/system/underpost-forward-proxy.service');
1515
+ expect(proxy('hub').halt).to.equal(false);
1516
+ // The proxy binds the hub's own tunnel address, and `--forward-proxy-server` refuses to
1517
+ // configure itself anywhere else.
1518
+ for (const nodeRole of ['control', 'worker', '']) expect(proxy(nodeRole), nodeRole).to.equal(undefined);
1519
+ });
1520
+
1521
+ it('acts on the unit file rather than on the unit being active', () => {
1522
+ process.env.GITHUB_USERNAME = 'someone';
1523
+ // Regression: guarding on `is-active` skipped exactly the dispatcher that needs the restart,
1524
+ // the one the previous package scope left dead. The unit file is what says a node runs one.
1525
+ const step = ENGINE_SYNC_STEPS.find((entry) => entry.command.includes('<event-service-command>'));
1526
+ expect(step.command).to.include('test -f /etc/systemd/system/underpost-event.service');
1527
+ expect(step.command).to.not.include('is-active');
1528
+ // A node that does not run the dispatcher must not fail its sync over it.
1529
+ expect(step.halt).to.equal(false);
1530
+ const reconcile = UnderpostWireguard.API.syncCommands({ nodeRole: 'control' }).at(-1);
1531
+ expect(UnderpostWireguard.API.syncScript({ nodeRole: 'control' })).to.include(`{ ${reconcile.command} || true; }`);
1532
+ });
1533
+
1534
+ it('records the pair it switched the node onto, after the switch rather than before it', () => {
1535
+ process.env.GITHUB_USERNAME = 'someone';
1536
+ // deploy/lib/host.sh reads these back out of the host configuration store to decide which
1537
+ // source a later `prepare_host` pulls from; without them the node's own deploy scripts
1538
+ // prepare it from their built-in default and undo the switch.
1539
+ const commands = UnderpostWireguard.API.syncCommands({
1540
+ repoEngine: 'underpostnet/engine-test-cyberia',
1541
+ }).map((step) => step.command);
1542
+ const at = (command) => commands.indexOf(command);
1543
+
1544
+ expect(at('node bin host set ENGINE_SRC_REPO underpostnet/engine-test-cyberia')).to.be.greaterThan(
1545
+ at('node bin cmt --switch-repo underpostnet/engine-test-cyberia --target-branch master'),
1546
+ );
1547
+ expect(at('node bin host set ENGINE_SRC_PRIVATE_REPO underpostnet/engine-cyberia-private')).to.be.greaterThan(
1548
+ at('node bin cmt --switch-repo underpostnet/engine-test-cyberia --target-branch master'),
1549
+ );
1550
+ });
1551
+
1552
+ it('republishes the cron manifests from the control node alone', () => {
1553
+ process.env.GITHUB_USERNAME = 'someone';
1554
+ const cron = (nodeRole) =>
1555
+ UnderpostWireguard.API.syncCommands({ nodeRole }).find((step) => step.command.includes('bin cron'));
1556
+
1557
+ // The pod bodies run the checkout the switch replaced, so the manifests are republished — but
1558
+ // only where they can be. Regression: a worker carrying kubectl without cluster access
1559
+ // rewrote the cron deploy's manifests and relabeled host directories before failing.
1560
+ expect(cron('control').command).to.equal('node bin cron --setup-start --git --apply');
1561
+ expect(cron('control').halt).to.equal(false);
1562
+ for (const nodeRole of ['worker', 'hub', '']) expect(cron(nodeRole), nodeRole).to.equal(undefined);
1563
+ });
1564
+
1565
+ it.skipIf(!shipsCyberiaPackageScript)(
1566
+ 'runs a deploy package script when the deploy ships one, and installs the checkout when it does not',
1567
+ () => {
1568
+ process.env.GITHUB_USERNAME = 'someone';
1569
+ const scripts = (repoEngine) =>
1570
+ UnderpostWireguard.API.syncCommands({ repoEngine })
1571
+ .filter((step) => step.command.startsWith('bash '))
1572
+ .map((step) => step.command);
1573
+
1574
+ expect(scripts('underpostnet/engine-test-cyberia')).to.deep.equal(['bash ./deploy/dd-cyberia/package.sh']);
1575
+ expect(scripts('underpostnet/engine-test-absent')).to.deep.equal([]);
1576
+ expect(scripts('underpostnet/engine')).to.deep.equal([]);
1577
+
1578
+ // A sync that belongs to no deploy still lands new source, so the install is never dropped
1579
+ // — it falls back to the checkout's own manifest rather than leaving none.
1580
+ const commands = (repoEngine) => UnderpostWireguard.API.syncCommands({ repoEngine }).map((step) => step.command);
1581
+ expect(commands('underpostnet/engine')).to.include('npm install');
1582
+ expect(commands('underpostnet/engine-test-cyberia')).to.not.include('npm install');
1583
+ },
1584
+ );
1585
+
1586
+ it('installs after the source switch with no CLI step in between', () => {
1587
+ process.env.GITHUB_USERNAME = 'someone';
1588
+ // Regression: the conf switch sat between the source switch and the install, so it ran the
1589
+ // freshly landed source against the previous `node_modules` and died importing a dependency
1590
+ // that switch had just added. Every step here is the node's own CLI, so none of them may
1591
+ // observe a checkout whose packages predate it.
1592
+ for (const repoEngine of ['underpostnet/engine', 'underpostnet/engine-test-cyberia']) {
1593
+ const commands = UnderpostWireguard.API.syncCommands({ repoEngine, nodeRole: 'control' }).map(
1594
+ (step) => step.command,
1595
+ );
1596
+ const switchAt = commands.findIndex(
1597
+ (command) => command.startsWith('node bin cmt --switch-repo') && !command.includes('./engine-private'),
1598
+ );
1599
+ const installAt = commands.findIndex(
1600
+ (command, index) => index > switchAt && (command.startsWith('bash ') || command === 'npm install'),
1601
+ );
1602
+
1603
+ expect(switchAt, repoEngine).to.be.greaterThan(-1);
1604
+ expect(installAt, repoEngine).to.equal(switchAt + 1);
1605
+ // The conf switch has to precede it, since the install reads the manifest that switch lands.
1606
+ expect(
1607
+ commands.findIndex((command) => command.includes('./engine-private --switch-repo')),
1608
+ repoEngine,
1609
+ ).to.be.lessThan(switchAt);
1610
+ }
1611
+ });
1612
+
1613
+ it('restores the checkout manifest before the bootstrap install', () => {
1614
+ process.env.GITHUB_USERNAME = 'someone';
1615
+ // A previous sync left the deploy manifest copied over ./package.json, so installing without
1616
+ // restoring it reinstalls the previous scope and leaves the CLI unable to load new source.
1617
+ const commands = UnderpostWireguard.API.syncCommands().map((step) => step.command);
1618
+ expect(commands[0]).to.equal('git checkout -- package.json; npm install');
1619
+ // Sequenced, not chained: a tree that is not a repository still has to reach the install.
1620
+ expect(commands[0]).to.not.include('&&');
1621
+ });
1622
+
1623
+ it('neutralizes advisory steps in place and chains the rest', () => {
1624
+ process.env.GITHUB_USERNAME = 'someone';
1625
+ const script = UnderpostWireguard.API.syncScript();
1626
+ for (const step of UnderpostWireguard.API.syncCommands())
1627
+ expect(script, step.command).to.include(step.halt ? `' && ${step.command}` : `' && { ${step.command} || true; }`);
1628
+ });
1629
+ });
1630
+
1631
+ describe('peer locality', () => {
1632
+ const addresses = new Set(['192.168.1.85', '10.0.0.2']);
1633
+
1634
+ it('is this machine only when the peer management address is one of its own', () => {
1635
+ expect(localPeerFactory('192.168.1.85', addresses)).to.equal(true);
1636
+ expect(localPeerFactory(' 192.168.1.85 ', addresses)).to.equal(true);
1637
+ });
1638
+
1639
+ it('is remote for a peer that lives on another host sharing this hostname', () => {
1640
+ expect(localPeerFactory('192.168.1.191', addresses)).to.equal(false);
1641
+ });
1642
+
1643
+ it('fails towards SSH when the peer has no verifiable management address', () => {
1644
+ expect(localPeerFactory('', addresses)).to.equal(false);
1645
+ expect(localPeerFactory('control-plane.lan', addresses)).to.equal(false);
1646
+ });
1647
+ });
1648
+
1649
+ describe('command credential redaction', () => {
1650
+ it('masks a token embedded in a clone URL', () => {
1651
+ expect(redactSensitiveText('git fetch --force "https://x-access-token:ghp_secret@github.com/o/r" main')).to.equal(
1652
+ 'git fetch --force "https://***@github.com/o/r" main',
1653
+ );
1654
+ });
1655
+
1656
+ it('leaves a URL that carries no credential alone', () => {
1657
+ expect(redactSensitiveText('git remote set-url origin "https://github.com/o/r"')).to.equal(
1658
+ 'git remote set-url origin "https://github.com/o/r"',
1659
+ );
1660
+ });
1661
+
1662
+ it('masks token environment assignments in failed command wrappers', () => {
1663
+ expect(redactSensitiveText('GITHUB_TOKEN=ghp_secret GITHUB_USERNAME=operator node bin db')).to.equal(
1664
+ 'GITHUB_TOKEN=[REDACTED] GITHUB_USERNAME=operator node bin db',
1665
+ );
1666
+ });
1667
+
1668
+ it('keeps the secret out of the error a failed command throws', () => {
1669
+ const error = new ShellExecError('git push https://ghp_secret@github.com/o/r', 128, '', '');
1670
+ expect(error.message).to.not.include('ghp_secret');
1671
+ expect(error.cmd).to.not.include('ghp_secret');
1672
+ });
1673
+ });
1674
+
1675
+ describe('shell argument quoting', () => {
1676
+ it('carries a comma-separated command list through as one argument', () => {
1677
+ expect(shellArgumentFactory('cd /home/dd, npm install, npm link')).to.equal("'cd /home/dd, npm install, npm link'");
1678
+ });
1679
+
1680
+ it('closes, escapes and reopens an embedded single quote', () => {
1681
+ expect(shellArgumentFactory(`echo 'hi'`)).to.equal(`'echo '\\''hi'\\'''`);
1682
+ });
1683
+
1684
+ it('leaves double quotes and expansions literal', () => {
1685
+ expect(shellArgumentFactory('echo "$HOME"')).to.equal('\'echo "$HOME"\'');
1686
+ });
1687
+
1688
+ it('quotes an empty value rather than dropping the argument', () => {
1689
+ expect(shellArgumentFactory('')).to.equal("''");
1690
+ expect(shellArgumentFactory(undefined)).to.equal("''");
1691
+ });
1692
+ });
1693
+
1694
+ describe('tunnel firewall zone', () => {
1695
+ const zoneRule = (role, remove = false) =>
1696
+ firewallCommandsFactory({ role, remove }).find((command) => command.includes('-interface=wg0'));
1697
+
1698
+ it('zones the tunnel interface for every role, the hub included', () => {
1699
+ for (const role of ['hub', 'control', 'worker'])
1700
+ expect(zoneRule(role), role).to.include('--zone=trusted --add-interface=wg0');
1701
+ });
1702
+
1703
+ it('withdraws the same interface it added', () => {
1704
+ expect(zoneRule('hub', true)).to.include('--zone=trusted --remove-interface=wg0');
1705
+ });
1706
+
1707
+ it('keeps the hub public ports without losing the zone', () => {
1708
+ const rules = firewallCommandsFactory({ role: 'hub' }).join('\n');
1709
+ expect(rules).to.include('--add-port=51820/udp');
1710
+ expect(rules).to.include('--add-masquerade');
1711
+ expect(rules).to.include('--add-interface=wg0');
1712
+ });
1713
+ });
1714
+
1715
+ describe('probe scheduling', () => {
1716
+ it('gives each interval its own scrape job so cadences do not merge', () => {
1717
+ const groups = probeGroupsFactory([
1718
+ { eventId: 'a', module: 'icmp', interval: '30s', targets: ['10.0.0.1'], labels: {} },
1719
+ { eventId: 'b', module: 'icmp', interval: '30s', targets: ['10.0.0.3'], labels: {} },
1720
+ { eventId: 'c', module: 'http_2xx', interval: '10m', targets: ['https://x/'], labels: {} },
1721
+ ]);
1722
+ expect(groups.map((group) => `${group.module}@${group.interval}`)).to.deep.equal(['icmp@30s', 'http_2xx@10m']);
1723
+ expect(groups[0].groups).to.have.lengthOf(2);
1724
+ });
1725
+
1726
+ it('renders the declared interval as the job scrape period', () => {
1727
+ const conf = prometheusConfFactory({
1728
+ probes: [{ eventId: 'c', module: 'http_2xx', interval: '10m', targets: ['https://x/'], labels: {} }],
1175
1729
  });
1730
+ expect(conf).to.include("job_name: 'blackbox-http_2xx'");
1731
+ expect(conf).to.include('scrape_interval: 10m');
1732
+ });
1733
+
1734
+ it('omits the period when an event declares none, leaving the global default', () => {
1735
+ const conf = prometheusConfFactory({
1736
+ probes: [{ eventId: 'c', module: 'icmp', interval: '', targets: ['10.0.0.1'], labels: {} }],
1737
+ });
1738
+ expect(conf).to.not.include('scrape_interval: \n');
1739
+ });
1740
+ });
1741
+
1742
+ describe('port-scoped ingress blocking', () => {
1743
+ it('keeps only well-formed TCP ports', () => {
1744
+ expect(Dns.portListFactory('80,443')).to.deep.equal([80, 443]);
1745
+ expect(Dns.portListFactory(' 80 , bad , 443 , 99999 , 0 ')).to.deep.equal([80, 443]);
1746
+ expect(Dns.portListFactory('')).to.deep.equal([]);
1747
+ });
1748
+
1749
+ it('exposes the block and withdraw entry points the scenario calls', () => {
1750
+ for (const method of ['blockIngressPort', 'unblockIngressPort', 'portListFactory'])
1751
+ expect(Dns[method], method).to.be.a('function');
1752
+ });
1753
+
1754
+ it('creates the chain its rules live in before touching it', () => {
1755
+ // `inet filter` exists on a firewalld host with no `input` chain, so every
1756
+ // ingress command fails on a table that looks present.
1757
+ const source = fs.readFileSync(new URL('../../../../src/server/network/dns.js', import.meta.url), 'utf8');
1758
+ for (const method of ['blockAllIngress', 'unblockAllIngress', 'blockIngressPort', 'unblockIngressPort']) {
1759
+ const body = source.slice(source.indexOf(`static ${method}(`));
1760
+ expect(body.slice(0, body.indexOf('\n }')), method).to.include('Dns.ensureIngressChain()');
1761
+ }
1762
+ expect(Dns.ensureIngressChain).to.be.a('function');
1763
+ });
1764
+
1765
+ it('calls the helper on the class that declares it', () => {
1766
+ // The class is `Dns`; `UnderpostDns` is the CLI wrapper and carries no such
1767
+ // static, so naming it here fails only once the command runs on a host.
1768
+ const source = fs.readFileSync(new URL('../../../../src/server/network/dns.js', import.meta.url), 'utf8');
1769
+ expect(source).to.not.include('UnderpostDns.portListFactory');
1176
1770
  });
1177
1771
  });