@underpostnet/cyberia 3.3.0 → 3.3.73

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 (471) hide show
  1. package/.dockerignore +1 -0
  2. package/.github/workflows/coverall.cyberia.ci.yml +34 -14
  3. package/.github/workflows/cyberia-client.cd.yml +5 -0
  4. package/.github/workflows/cyberia-server.cd.yml +5 -0
  5. package/.github/workflows/docker-image.cyberia-client.ci.yml +8 -5
  6. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +8 -3
  7. package/.github/workflows/docker-image.cyberia-server.ci.yml +8 -5
  8. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +8 -5
  9. package/.github/workflows/docker-image.engine-cyberia.ci.yml +15 -1
  10. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +15 -1
  11. package/.github/workflows/engine-cyberia.cd.yml +3 -0
  12. package/.github/workflows/engine-cyberia.ci.yml +4 -5
  13. package/.github/workflows/ghpkg.ci.yml +35 -20
  14. package/.github/workflows/hardhat.ci.yml +12 -11
  15. package/.github/workflows/npmpkg.ci.yml +17 -15
  16. package/.github/workflows/pwa-microservices-template-test.ci.yml +2 -2
  17. package/.github/workflows/release.cd.yml +1 -0
  18. package/.prettierignore +1 -0
  19. package/AGENTS.md +47 -0
  20. package/CHANGELOG.md +565 -2510
  21. package/CLI-HELP.md +306 -147
  22. package/Dockerfile +25 -9
  23. package/Dockerfile.dev +25 -9
  24. package/Dockerfile.test +4 -4
  25. package/README.md +21 -1
  26. package/bin/build.js +66 -41
  27. package/bin/build.template.js +8 -3
  28. package/bin/cyberia.js +308 -91
  29. package/bin/deploy.js +18 -104
  30. package/bin/index.js +308 -91
  31. package/conf.js +91 -4
  32. package/deploy/cyberia-client/deploy.sh +47 -0
  33. package/deploy/cyberia-client/package.sh +18 -0
  34. package/deploy/cyberia-client/state.sh +14 -0
  35. package/deploy/cyberia-server/deploy.sh +47 -0
  36. package/deploy/cyberia-server/package.sh +18 -0
  37. package/deploy/cyberia-server/state.sh +14 -0
  38. package/deploy/dd-cyberia/deploy.sh +6 -26
  39. package/deploy/dd-cyberia/init.sh +35 -43
  40. package/deploy/dd-cyberia/package.sh +17 -0
  41. package/deploy/dd-cyberia/state.sh +14 -0
  42. package/deploy/dd-cyberia/sync-deploy.sh +92 -170
  43. package/deploy/lib/github-actions-logging.sh +594 -0
  44. package/deploy/lib/host.sh +185 -0
  45. package/deploy/lib/state.sh +62 -0
  46. package/deploy/pwa-microservices-template/deploy.sh +12 -39
  47. package/deploy/release/deploy.sh +9 -39
  48. package/deployment.yaml +7 -2
  49. package/docker-compose.yml +7 -7
  50. package/examples/static-page/README.md +19 -19
  51. package/examples/static-page/ssr-components/CustomPage.js +2 -2
  52. package/hardhat/hardhat.config.js +1 -6
  53. package/hardhat/package-lock.json +101 -40
  54. package/hardhat/package.json +2 -2
  55. package/manifests/alertmanager/deployment.yaml +81 -0
  56. package/manifests/blackbox-exporter/deployment.yaml +75 -0
  57. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +32 -7
  58. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +32 -7
  59. package/manifests/deployment/dd-cyberia-development/deployment.yaml +7 -2
  60. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  61. package/manifests/grafana/deployment.yaml +26 -7
  62. package/manifests/grafana/kustomization.yaml +1 -0
  63. package/manifests/grafana/service.yaml +6 -1
  64. package/manifests/mariadb/pv.yaml +1 -0
  65. package/manifests/mongodb/statefulset.yaml +9 -2
  66. package/manifests/mysql/pv-pvc.yaml +14 -1
  67. package/manifests/postgresql/pv.yaml +12 -1
  68. package/manifests/postgresql/pvc.yaml +1 -0
  69. package/manifests/prometheus/deployment.yaml +18 -22
  70. package/package.json +83 -10
  71. package/scripts/event-monitor.sh +56 -0
  72. package/scripts/k3s-node-setup.sh +27 -14
  73. package/scripts/kubeadm-node-setup.sh +31 -11
  74. package/scripts/maas-setup.sh +8 -8
  75. package/scripts/selinux-normalize.sh +907 -0
  76. package/scripts/shutdown-machine.sh +843 -0
  77. package/scripts/test-monitor.sh +3 -3
  78. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +1 -1
  79. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +2 -2
  80. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +2 -2
  81. package/src/api/core/core.controller.js +1 -1
  82. package/src/api/core/core.router.js +2 -2
  83. package/src/api/core/core.service.js +2 -2
  84. package/src/api/crypto/crypto.controller.js +1 -1
  85. package/src/api/crypto/crypto.service.js +1 -1
  86. package/src/api/cyberia-action/cyberia-action.controller.js +1 -1
  87. package/src/api/cyberia-action/cyberia-action.router.js +1 -1
  88. package/src/api/cyberia-action/cyberia-action.service.js +2 -2
  89. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -1
  90. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +1 -1
  91. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +2 -2
  92. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +1 -1
  93. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  94. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +2 -2
  95. package/src/api/cyberia-entity/cyberia-entity.controller.js +1 -1
  96. package/src/api/cyberia-entity/cyberia-entity.router.js +1 -1
  97. package/src/api/cyberia-entity/cyberia-entity.service.js +2 -2
  98. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +1 -1
  99. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +1 -1
  100. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +2 -2
  101. package/src/api/cyberia-instance/cyberia-fallback-capture.js +466 -0
  102. package/src/api/cyberia-instance/cyberia-fallback-world.js +8 -0
  103. package/src/api/cyberia-instance/cyberia-instance-map.service.js +1 -1
  104. package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -1
  105. package/src/api/cyberia-instance/cyberia-instance.router.js +2 -2
  106. package/src/api/cyberia-instance/cyberia-instance.service.js +2 -2
  107. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +1 -1
  108. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +1 -1
  109. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +2 -2
  110. package/src/api/cyberia-map/cyberia-map.controller.js +1 -1
  111. package/src/api/cyberia-map/cyberia-map.router.js +1 -1
  112. package/src/api/cyberia-map/cyberia-map.service.js +2 -2
  113. package/src/api/cyberia-quest/cyberia-quest.controller.js +1 -1
  114. package/src/api/cyberia-quest/cyberia-quest.router.js +1 -1
  115. package/src/api/cyberia-quest/cyberia-quest.service.js +2 -2
  116. package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +1 -1
  117. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +1 -1
  118. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +2 -2
  119. package/src/api/cyberia-saga/cyberia-saga.controller.js +1 -1
  120. package/src/api/cyberia-saga/cyberia-saga.router.js +1 -1
  121. package/src/api/cyberia-saga/cyberia-saga.service.js +2 -2
  122. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +0 -26
  123. package/src/api/cyberia-skill/cyberia-skill.controller.js +1 -1
  124. package/src/api/cyberia-skill/cyberia-skill.router.js +1 -1
  125. package/src/api/cyberia-skill/cyberia-skill.service.js +2 -2
  126. package/src/api/default/default.controller.js +1 -1
  127. package/src/api/default/default.router.js +1 -1
  128. package/src/api/default/default.service.js +2 -2
  129. package/src/api/document/document.controller.js +1 -1
  130. package/src/api/document/document.router.js +1 -1
  131. package/src/api/document/document.service.js +2 -2
  132. package/src/api/file/file.controller.js +1 -1
  133. package/src/api/file/file.router.js +1 -1
  134. package/src/api/file/file.service.js +2 -2
  135. package/src/api/instance/instance.controller.js +1 -1
  136. package/src/api/instance/instance.router.js +1 -1
  137. package/src/api/instance/instance.service.js +2 -2
  138. package/src/api/ipfs/ipfs.controller.js +1 -1
  139. package/src/api/ipfs/ipfs.router.js +2 -2
  140. package/src/api/ipfs/ipfs.service.js +2 -2
  141. package/src/api/object-layer/object-layer.controller.js +1 -1
  142. package/src/api/object-layer/object-layer.router.js +2 -2
  143. package/src/api/object-layer/object-layer.service.js +3 -3
  144. package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +1 -1
  145. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +1 -1
  146. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +2 -2
  147. package/src/api/test/test.controller.js +1 -1
  148. package/src/api/test/test.service.js +1 -1
  149. package/src/api/user/guest.service.js +3 -3
  150. package/src/api/user/user.controller.js +1 -1
  151. package/src/api/user/user.router.js +2 -2
  152. package/src/api/user/user.service.js +4 -4
  153. package/src/api.js +5 -9
  154. package/src/cli/app.js +391 -0
  155. package/src/cli/baremetal.js +30 -35
  156. package/src/cli/client.js +306 -0
  157. package/src/cli/cloud-init.js +3 -3
  158. package/src/cli/cluster.js +184 -82
  159. package/src/cli/db.js +120 -68
  160. package/src/cli/deploy.js +153 -71
  161. package/src/cli/docker-compose.js +5 -5
  162. package/src/cli/domains.js +184 -0
  163. package/src/cli/dotenv-store.js +143 -0
  164. package/src/cli/event.js +2281 -0
  165. package/src/cli/fs.js +3 -3
  166. package/src/cli/host.js +672 -0
  167. package/src/cli/image.js +4 -6
  168. package/src/cli/index.js +322 -142
  169. package/src/cli/ipfs.js +34 -30
  170. package/src/cli/kickstart.js +1 -1
  171. package/src/cli/kubectl.js +218 -41
  172. package/src/cli/lxd.js +3 -3
  173. package/src/cli/monitor.js +1359 -26
  174. package/src/cli/package.js +90 -0
  175. package/src/cli/release.js +16 -12
  176. package/src/cli/repository.js +558 -363
  177. package/src/cli/run.js +331 -650
  178. package/src/cli/secrets.js +2158 -964
  179. package/src/cli/ssh.js +478 -279
  180. package/src/cli/state.js +394 -0
  181. package/src/cli/static.js +2 -2
  182. package/src/cli/system.js +1 -1
  183. package/src/cli/test.js +199 -125
  184. package/src/cli/vultr.js +61 -21
  185. package/src/cli/wireguard.js +1111 -306
  186. package/src/client/components/core/CalendarCore.js +53 -96
  187. package/src/client/components/core/Docs.js +258 -211
  188. package/src/client/components/core/FileExplorer.js +0 -21
  189. package/src/client/components/core/PanelForm.js +4 -4
  190. package/src/client/components/core/Repository.js +56 -0
  191. package/src/client/components/core/Translate.js +7 -0
  192. package/src/client/components/core/{FullScreen.js → ViewModeController.js} +42 -39
  193. package/src/client/components/core/Wallet.js +0 -11
  194. package/src/client/components/core/Worker.js +7 -1
  195. package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +2 -2
  196. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +2 -1
  197. package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +2 -2
  198. package/src/client/components/default/AppShellDefault.js +1 -1
  199. package/src/client/components/default/SettingsDefault.js +2 -2
  200. package/src/client/components/itemledger/SettingsItemledger.js +2 -2
  201. package/src/client/components/underpost/AppShellUnderpost.js +3 -2
  202. package/src/client/components/underpost/SettingsUnderpost.js +2 -2
  203. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +32 -5
  204. package/src/client/public/cyberia-docs/CYBERIA.md +21 -1
  205. package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +23 -6
  206. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  207. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  208. package/src/client/public/default/android-chrome-144x144.png +0 -0
  209. package/src/client/public/default/android-chrome-192x192.png +0 -0
  210. package/src/client/public/default/android-chrome-256x256.png +0 -0
  211. package/src/client/public/default/android-chrome-36x36.png +0 -0
  212. package/src/client/public/default/android-chrome-384x384.png +0 -0
  213. package/src/client/public/default/android-chrome-48x48.png +0 -0
  214. package/src/client/public/default/android-chrome-512x512.png +0 -0
  215. package/src/client/public/default/android-chrome-72x72.png +0 -0
  216. package/src/client/public/default/android-chrome-96x96.png +0 -0
  217. package/src/client/public/default/apple-touch-icon-1024x1024.png +0 -0
  218. package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
  219. package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
  220. package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
  221. package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
  222. package/src/client/public/default/apple-touch-icon-167x167.png +0 -0
  223. package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
  224. package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
  225. package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
  226. package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
  227. package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
  228. package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
  229. package/src/client/public/default/apple-touch-icon.png +0 -0
  230. package/src/client/public/default/apple-touch-startup-image-1125x2436.png +0 -0
  231. package/src/client/public/default/apple-touch-startup-image-1136x640.png +0 -0
  232. package/src/client/public/default/apple-touch-startup-image-1170x2532.png +0 -0
  233. package/src/client/public/default/apple-touch-startup-image-1179x2556.png +0 -0
  234. package/src/client/public/default/apple-touch-startup-image-1242x2208.png +0 -0
  235. package/src/client/public/default/apple-touch-startup-image-1242x2688.png +0 -0
  236. package/src/client/public/default/apple-touch-startup-image-1284x2778.png +0 -0
  237. package/src/client/public/default/apple-touch-startup-image-1290x2796.png +0 -0
  238. package/src/client/public/default/apple-touch-startup-image-1334x750.png +0 -0
  239. package/src/client/public/default/apple-touch-startup-image-1488x2266.png +0 -0
  240. package/src/client/public/default/apple-touch-startup-image-1536x2048.png +0 -0
  241. package/src/client/public/default/apple-touch-startup-image-1620x2160.png +0 -0
  242. package/src/client/public/default/apple-touch-startup-image-1640x2160.png +0 -0
  243. package/src/client/public/default/apple-touch-startup-image-1668x2224.png +0 -0
  244. package/src/client/public/default/apple-touch-startup-image-1668x2388.png +0 -0
  245. package/src/client/public/default/apple-touch-startup-image-1792x828.png +0 -0
  246. package/src/client/public/default/apple-touch-startup-image-2048x1536.png +0 -0
  247. package/src/client/public/default/apple-touch-startup-image-2048x2732.png +0 -0
  248. package/src/client/public/default/apple-touch-startup-image-2160x1620.png +0 -0
  249. package/src/client/public/default/apple-touch-startup-image-2160x1640.png +0 -0
  250. package/src/client/public/default/apple-touch-startup-image-2208x1242.png +0 -0
  251. package/src/client/public/default/apple-touch-startup-image-2224x1668.png +0 -0
  252. package/src/client/public/default/apple-touch-startup-image-2266x1488.png +0 -0
  253. package/src/client/public/default/apple-touch-startup-image-2388x1668.png +0 -0
  254. package/src/client/public/default/apple-touch-startup-image-2436x1125.png +0 -0
  255. package/src/client/public/default/apple-touch-startup-image-2532x1170.png +0 -0
  256. package/src/client/public/default/apple-touch-startup-image-2556x1179.png +0 -0
  257. package/src/client/public/default/apple-touch-startup-image-2688x1242.png +0 -0
  258. package/src/client/public/default/apple-touch-startup-image-2732x2048.png +0 -0
  259. package/src/client/public/default/apple-touch-startup-image-2778x1284.png +0 -0
  260. package/src/client/public/default/apple-touch-startup-image-2796x1290.png +0 -0
  261. package/src/client/public/default/apple-touch-startup-image-640x1136.png +0 -0
  262. package/src/client/public/default/apple-touch-startup-image-750x1334.png +0 -0
  263. package/src/client/public/default/apple-touch-startup-image-828x1792.png +0 -0
  264. package/src/client/public/default/assets/background/white.jpg +0 -0
  265. package/src/client/public/default/browserconfig.xml +1 -1
  266. package/src/client/public/default/favicon-16x16.png +0 -0
  267. package/src/client/public/default/favicon-32x32.png +0 -0
  268. package/src/client/public/default/favicon-48x48.png +0 -0
  269. package/src/client/public/default/favicon.ico +0 -0
  270. package/src/client/public/default/manifest.webmanifest +5 -5
  271. package/src/client/public/default/mstile-144x144.png +0 -0
  272. package/src/client/public/default/mstile-150x150.png +0 -0
  273. package/src/client/public/default/mstile-310x150.png +0 -0
  274. package/src/client/public/default/mstile-310x310.png +0 -0
  275. package/src/client/public/default/mstile-70x70.png +0 -0
  276. package/src/client/public/default/yandex-browser-50x50.png +0 -0
  277. package/src/client/public/default/yandex-browser-manifest.json +1 -1
  278. package/src/client/ssr/RootDocument.js +1 -1
  279. package/src/client/ssr/body/404.js +1 -1
  280. package/src/client/ssr/body/500.js +1 -1
  281. package/src/client/ssr/body/CacheControl.js +1 -1
  282. package/src/client/ssr/body/CyberiaDefaultSplashScreen.js +1 -1
  283. package/src/client/ssr/body/DefaultSplashScreen.js +1 -1
  284. package/src/client/ssr/body/SwaggerDarkMode.js +1 -1
  285. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +1 -1
  286. package/src/client/ssr/head/CryptokoynScripts.js +1 -1
  287. package/src/client/ssr/head/Css.js +1 -1
  288. package/src/client/ssr/head/CyberiaPortalScripts.js +1 -1
  289. package/src/client/ssr/head/DefaultScripts.js +1 -1
  290. package/src/client/ssr/head/ItemledgerScripts.js +1 -1
  291. package/src/client/ssr/head/Microdata.js +1 -1
  292. package/src/client/ssr/head/Production.js +1 -1
  293. package/src/client/ssr/head/Pwa.js +1 -1
  294. package/src/client/ssr/head/PwaItemledger.js +1 -1
  295. package/src/client/ssr/head/Seo.js +1 -1
  296. package/src/client/ssr/head/UnderpostScripts.js +1 -1
  297. package/src/client/ssr/mailer/DefaultRecoverEmail.js +1 -1
  298. package/src/client/ssr/mailer/DefaultVerifyEmail.js +1 -1
  299. package/src/client/ssr/views/Cyberia404.js +7 -5
  300. package/src/client/ssr/views/CyberiaServerMetrics.js +1 -1
  301. package/src/client/ssr/views/Maintenance.js +1 -1
  302. package/src/client/ssr/views/NoNetworkConnection.js +1 -1
  303. package/src/client/ssr/views/Test.js +1 -1
  304. package/src/client/sw/core.sw.js +20 -14
  305. package/src/client-builder/client-build-docs.js +61 -46
  306. package/src/client-builder/client-build-live.js +2 -2
  307. package/src/client-builder/client-build.js +38 -30
  308. package/src/client-builder/client-dev-server.js +3 -3
  309. package/src/client-builder/client-icons.js +2 -2
  310. package/src/client-builder/ssr.js +3 -3
  311. package/src/client.build.js +3 -6
  312. package/src/client.dev.js +3 -6
  313. package/src/db/DataBaseProvider.js +186 -3
  314. package/src/db/mariadb/MariaDB.js +3 -2
  315. package/src/db/mongo/MongoBootstrap.js +273 -101
  316. package/src/db/mongo/MongooseDB.js +7 -8
  317. package/src/{server/valkey.js → db/valkey/Valkey.js} +5 -4
  318. package/src/grpc/cyberia/grpc-server.js +1 -1
  319. package/src/index.js +79 -23
  320. package/src/mailer/EmailRender.js +2 -2
  321. package/src/mailer/MailerInterceptor.js +87 -0
  322. package/src/mailer/MailerProvider.js +50 -6
  323. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +1 -1
  324. package/src/projects/cyberia/besu-genesis-generator.js +3 -3
  325. package/src/projects/cyberia/catalog-cyberia.js +53 -10
  326. package/src/projects/cyberia/gemini-client.js +1 -1
  327. package/src/projects/cyberia/generate-saga.js +1 -1
  328. package/src/projects/cyberia/hot-reload-trigger.js +1 -1
  329. package/src/projects/cyberia/instance-data.js +1 -1
  330. package/src/projects/cyberia/ipfs-client.js +4 -4
  331. package/src/projects/cyberia/map-preview-generator.js +1 -1
  332. package/src/projects/cyberia/object-layer.js +1 -1
  333. package/src/projects/cyberia/semantic-layer-generator.js +1 -1
  334. package/src/projects/underpost/catalog-underpost.js +9 -1
  335. package/src/proxy.js +4 -7
  336. package/src/runtime/cyberia-client/Dockerfile +9 -4
  337. package/src/runtime/cyberia-client/Dockerfile.dev +8 -2
  338. package/src/runtime/cyberia-server/Dockerfile +9 -4
  339. package/src/runtime/cyberia-server/Dockerfile.dev +8 -2
  340. package/src/runtime/engine-cyberia/Dockerfile +25 -9
  341. package/src/runtime/engine-cyberia/Dockerfile.dev +25 -9
  342. package/src/runtime/engine-cyberia/Dockerfile.test +4 -4
  343. package/src/runtime/engine-cyberia/docker-compose.yml +7 -7
  344. package/src/runtime/express/Express.js +32 -27
  345. package/src/runtime/lampp/Lampp.js +22 -70
  346. package/src/runtime/nginx/Nginx.js +1 -1
  347. package/src/runtime/wp/Wp.js +16 -48
  348. package/src/server/{catalog.js → build/catalog.js} +15 -5
  349. package/src/server/build/coverage.js +117 -0
  350. package/src/server/build/execution.js +402 -0
  351. package/src/server/build/package.js +465 -0
  352. package/src/server/build/testing.js +528 -0
  353. package/src/server/{dns.js → network/dns.js} +144 -11
  354. package/src/server/{forward-proxy.js → network/forward-proxy.js} +10 -18
  355. package/src/server/{middlewares.js → network/middlewares.js} +3 -3
  356. package/src/server/network/node-capability.js +98 -0
  357. package/src/server/{peer.js → network/peer.js} +3 -3
  358. package/src/server/{proxy.js → network/proxy.js} +7 -13
  359. package/src/server/network/router.js +356 -0
  360. package/src/server/{tls.js → network/tls.js} +2 -2
  361. package/src/server/{underpost-compression.js → network/underpost-compression.js} +1 -1
  362. package/src/server/{underpost-gateway.js → network/underpost-gateway.js} +7 -7
  363. package/src/server/{underpost-ingress.js → network/underpost-ingress.js} +2 -2
  364. package/src/server/{cri.js → ops/cri.js} +2 -2
  365. package/src/server/{cron.js → ops/cron.js} +349 -74
  366. package/src/server/ops/event-notification.js +284 -0
  367. package/src/server/{logger.js → ops/logger.js} +108 -23
  368. package/src/server/ops/monitoring.js +1724 -0
  369. package/src/server/{systemd.js → ops/systemd.js} +55 -1
  370. package/src/server/{conf.js → runtime/conf.js} +513 -422
  371. package/src/server/runtime/config-scope.js +215 -0
  372. package/src/server/runtime/environment.js +149 -0
  373. package/src/server/{process.js → runtime/process.js} +153 -12
  374. package/src/server/{runtime-status.js → runtime/runtime-status.js} +155 -16
  375. package/src/server/{runtime.js → runtime/runtime.js} +12 -8
  376. package/src/server/{start.js → runtime/start.js} +128 -39
  377. package/src/server/{auth.js → security/auth.js} +7 -8
  378. package/src/server/security/container-storage.js +382 -0
  379. package/src/server/{crypto.js → security/crypto.js} +1 -1
  380. package/src/server/{selinux.js → security/selinux.js} +1 -1
  381. package/src/server/{backup.js → storage/backup.js} +17 -8
  382. package/src/server/{data-query.js → storage/data-query.js} +1 -1
  383. package/src/server/{downloader.js → storage/downloader.js} +2 -2
  384. package/src/server/storage/repository.js +68 -0
  385. package/src/server.js +4 -7
  386. package/src/ws/IoInterface.js +1 -1
  387. package/src/ws/IoServer.js +1 -1
  388. package/src/ws/core/core.ws.connection.js +1 -1
  389. package/src/ws/core/core.ws.emit.js +1 -1
  390. package/src/ws/core/core.ws.server.js +1 -1
  391. package/src/ws/default/default.ws.connection.js +1 -1
  392. package/src/ws/default/default.ws.emit.js +1 -1
  393. package/src/ws/default/default.ws.server.js +1 -1
  394. package/test/e2e/event-e2e-public-ingress-down.js +45 -0
  395. package/test/e2e/event-e2e-wireguard-server-down.js +38 -0
  396. package/test/e2e/event-e2e-wireguard-spoke-down.js +40 -0
  397. package/test/{api.test.js → integration/app/api.test.js} +33 -14
  398. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +30 -0
  399. package/test/{cyberia-instance-conf-defaults.test.js → integration/app/cyberia/cyberia-instance-conf-defaults.test.js} +1 -1
  400. package/test/integration/app/cyberia/cyberia-load.test.js +467 -0
  401. package/test/integration/app/cyberia/fallback-world-capture.test.js +253 -0
  402. package/test/integration/app/cyberia/object-layer-item-id.test.js +327 -0
  403. package/test/{shape-generator.test.js → integration/app/cyberia/shape-generator.test.js} +1 -1
  404. package/test/integration/infra/1-security/config-scope.test.js +305 -0
  405. package/test/integration/infra/1-security/container-storage.test.js +356 -0
  406. package/test/integration/infra/1-security/in-pod-cli-surface.test.js +86 -0
  407. package/test/integration/infra/1-security/secret-onboarding.test.js +930 -0
  408. package/test/{selinux.test.js → integration/infra/1-security/selinux.test.js} +2 -4
  409. package/test/{sops-secret-store.test.js → integration/infra/1-security/sops-secret-store.test.js} +415 -61
  410. package/test/integration/infra/1-security/systemd-service.test.js +70 -0
  411. package/test/integration/infra/1-security/underpost-config-secret.test.js +367 -0
  412. package/test/integration/infra/2-network/dns-firewall.test.js +566 -0
  413. package/test/integration/infra/2-network/node-capability.test.js +182 -0
  414. package/test/integration/infra/2-network/wireguard-cli.test.js +1163 -0
  415. package/test/{wireguard-edge.test.js → integration/infra/2-network/wireguard-edge.test.js} +609 -55
  416. package/test/{cluster-instances.test.js → integration/infra/3-cluster/cluster-instances.test.js} +11 -15
  417. package/test/{deploy-node-placement.test.js → integration/infra/3-cluster/deploy-node-placement.test.js} +3 -3
  418. package/test/integration/infra/3-cluster/docker-compose-stack.test.js +313 -0
  419. package/test/integration/infra/4-ingress/deploy-routes.test.js +379 -0
  420. package/test/integration/infra/4-ingress/gateway-static-assets.test.js +453 -0
  421. package/test/{instance-traffic-plan.test.js → integration/infra/4-ingress/instance-traffic-plan.test.js} +10 -6
  422. package/test/{underpost-gateway.test.js → integration/infra/4-ingress/underpost-gateway.test.js} +6 -6
  423. package/test/{underpost-ingress.test.js → integration/infra/4-ingress/underpost-ingress.test.js} +4 -4
  424. package/test/integration/infra/5-observability/cron-jobs.test.js +652 -0
  425. package/test/{deploy-monitor.test.js → integration/infra/5-observability/deploy-monitor.test.js} +29 -31
  426. package/test/integration/infra/5-observability/event-notification.test.js +197 -0
  427. package/test/integration/infra/5-observability/event-remediation.test.js +502 -0
  428. package/test/integration/infra/5-observability/event-targets.test.js +1174 -0
  429. package/test/integration/infra/5-observability/monitoring-stack.test.js +902 -0
  430. package/test/support/shell-harness.js +73 -0
  431. package/test/unit/build-template.test.js +97 -0
  432. package/test/unit/build-workflow-offline.test.js +132 -0
  433. package/test/unit/catalog.test.js +71 -0
  434. package/test/unit/client-build-docs.test.js +334 -0
  435. package/test/unit/conf-loading.test.js +303 -0
  436. package/test/unit/conf-resolution.test.js +979 -0
  437. package/test/unit/coverage-artifact.test.js +130 -0
  438. package/test/{crypto.test.js → unit/crypto.test.js} +1 -1
  439. package/test/unit/deploy-env-resolution.test.js +38 -0
  440. package/test/unit/deploy-log-table.test.js +447 -0
  441. package/test/unit/execution-profiles.test.js +219 -0
  442. package/test/unit/fleet-sync-source.test.js +149 -0
  443. package/test/unit/logger-redaction.test.js +112 -0
  444. package/test/unit/package.test.js +403 -0
  445. package/test/unit/prepare-host.test.js +299 -0
  446. package/test/unit/process-environment.test.js +18 -0
  447. package/test/unit/propagation-message.test.js +78 -0
  448. package/test/unit/redeploy-plan.test.js +24 -0
  449. package/test/unit/release-bump.test.js +474 -0
  450. package/test/unit/ssh-output-redaction.test.js +28 -0
  451. package/test/unit/start-options.test.js +46 -0
  452. package/test/unit/test-tiers.test.js +193 -0
  453. package/vitest.config.js +58 -0
  454. package/.nycrc +0 -9
  455. package/deploy/lib/logging.sh +0 -96
  456. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +0 -48
  457. package/manifests/mongodb/pv-pvc.yaml +0 -59
  458. package/scripts/coverall-test-cyberia.sh +0 -24
  459. package/scripts/coverall-test.sh +0 -24
  460. package/scripts/link-local-underpost-cli.sh +0 -6
  461. package/src/api/user/user.build.js +0 -16
  462. package/src/cli/env.js +0 -177
  463. package/src/client/components/core/Blockchain.js +0 -41
  464. package/src/client/components/core/Blog.js +0 -9
  465. package/src/client/components/core/KeyboardAvoidance.js +0 -145
  466. package/src/client/public/default/assets/background/dark.jpg +0 -0
  467. package/src/client/public/default/assets/background/white0-min.jpg +0 -0
  468. package/src/client/public/default/assets/background/white0.jpg +0 -0
  469. package/src/client/public/doc/favicon.ico +0 -0
  470. package/src/client/public/doc/sitemap +0 -148
  471. 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']);
700
715
  });
701
716
 
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('');
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');
710
727
  });
711
728
 
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([]);
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=');
743
+ });
744
+
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']);
753
+ });
754
+
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,479 @@ 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 }]);
1175
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('underpost cmt --switch-repo someone/engine --target-branch master');
1417
+ expect(commands).to.include(
1418
+ 'underpost 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('underpost cmt --switch-repo underpostnet/engine --target-branch master');
1428
+ expect(commands).to.include(
1429
+ 'underpost 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('underpost cmt --switch-repo underpostnet/engine --target-branch master');
1440
+ expect(commands).to.include(
1441
+ 'underpost 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('underpost 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
+ 'underpost 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('underpost 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('underpost 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 only when the deploy being synced ships one',
1567
+ () => {
1568
+ process.env.GITHUB_USERNAME = 'someone';
1569
+ const scripts = (repoEngine) =>
1570
+ UnderpostWireguard.API.syncCommands({ repoEngine }).filter((step) => step.command.startsWith('bash '));
1571
+
1572
+ // Both occurrences or neither: the halting one turns a missing script into a failed sync
1573
+ // rather than a missing install.
1574
+ expect(scripts('underpostnet/engine-test-cyberia').map((step) => step.command)).to.deep.equal([
1575
+ 'bash ./deploy/dd-cyberia/package.sh',
1576
+ 'bash ./deploy/dd-cyberia/package.sh',
1577
+ ]);
1578
+ expect(scripts('underpostnet/engine-test-absent')).to.deep.equal([]);
1579
+ expect(scripts('underpostnet/engine')).to.deep.equal([]);
1580
+ },
1581
+ );
1582
+
1583
+ it('neutralizes advisory steps in place and chains the rest', () => {
1584
+ process.env.GITHUB_USERNAME = 'someone';
1585
+ const script = UnderpostWireguard.API.syncScript();
1586
+ for (const step of UnderpostWireguard.API.syncCommands())
1587
+ expect(script, step.command).to.include(step.halt ? `' && ${step.command}` : `' && { ${step.command} || true; }`);
1588
+ });
1589
+ });
1590
+
1591
+ describe('peer locality', () => {
1592
+ const addresses = new Set(['192.168.1.85', '10.0.0.2']);
1593
+
1594
+ it('is this machine only when the peer management address is one of its own', () => {
1595
+ expect(localPeerFactory('192.168.1.85', addresses)).to.equal(true);
1596
+ expect(localPeerFactory(' 192.168.1.85 ', addresses)).to.equal(true);
1597
+ });
1598
+
1599
+ it('is remote for a peer that lives on another host sharing this hostname', () => {
1600
+ expect(localPeerFactory('192.168.1.191', addresses)).to.equal(false);
1601
+ });
1602
+
1603
+ it('fails towards SSH when the peer has no verifiable management address', () => {
1604
+ expect(localPeerFactory('', addresses)).to.equal(false);
1605
+ expect(localPeerFactory('control-plane.lan', addresses)).to.equal(false);
1606
+ });
1607
+ });
1608
+
1609
+ describe('command credential redaction', () => {
1610
+ it('masks a token embedded in a clone URL', () => {
1611
+ expect(redactSensitiveText('git fetch --force "https://x-access-token:ghp_secret@github.com/o/r" main')).to.equal(
1612
+ 'git fetch --force "https://***@github.com/o/r" main',
1613
+ );
1614
+ });
1615
+
1616
+ it('leaves a URL that carries no credential alone', () => {
1617
+ expect(redactSensitiveText('git remote set-url origin "https://github.com/o/r"')).to.equal(
1618
+ 'git remote set-url origin "https://github.com/o/r"',
1619
+ );
1620
+ });
1621
+
1622
+ it('masks token environment assignments in failed command wrappers', () => {
1623
+ expect(redactSensitiveText('GITHUB_TOKEN=ghp_secret GITHUB_USERNAME=operator node bin db')).to.equal(
1624
+ 'GITHUB_TOKEN=[REDACTED] GITHUB_USERNAME=operator node bin db',
1625
+ );
1626
+ });
1627
+
1628
+ it('keeps the secret out of the error a failed command throws', () => {
1629
+ const error = new ShellExecError('git push https://ghp_secret@github.com/o/r', 128, '', '');
1630
+ expect(error.message).to.not.include('ghp_secret');
1631
+ expect(error.cmd).to.not.include('ghp_secret');
1632
+ });
1633
+ });
1634
+
1635
+ describe('shell argument quoting', () => {
1636
+ it('carries a comma-separated command list through as one argument', () => {
1637
+ expect(shellArgumentFactory('cd /home/dd, npm install, npm link')).to.equal("'cd /home/dd, npm install, npm link'");
1638
+ });
1639
+
1640
+ it('closes, escapes and reopens an embedded single quote', () => {
1641
+ expect(shellArgumentFactory(`echo 'hi'`)).to.equal(`'echo '\\''hi'\\'''`);
1642
+ });
1643
+
1644
+ it('leaves double quotes and expansions literal', () => {
1645
+ expect(shellArgumentFactory('echo "$HOME"')).to.equal('\'echo "$HOME"\'');
1646
+ });
1647
+
1648
+ it('quotes an empty value rather than dropping the argument', () => {
1649
+ expect(shellArgumentFactory('')).to.equal("''");
1650
+ expect(shellArgumentFactory(undefined)).to.equal("''");
1651
+ });
1652
+ });
1653
+
1654
+ describe('tunnel firewall zone', () => {
1655
+ const zoneRule = (role, remove = false) =>
1656
+ firewallCommandsFactory({ role, remove }).find((command) => command.includes('-interface=wg0'));
1657
+
1658
+ it('zones the tunnel interface for every role, the hub included', () => {
1659
+ for (const role of ['hub', 'control', 'worker'])
1660
+ expect(zoneRule(role), role).to.include('--zone=trusted --add-interface=wg0');
1661
+ });
1662
+
1663
+ it('withdraws the same interface it added', () => {
1664
+ expect(zoneRule('hub', true)).to.include('--zone=trusted --remove-interface=wg0');
1665
+ });
1666
+
1667
+ it('keeps the hub public ports without losing the zone', () => {
1668
+ const rules = firewallCommandsFactory({ role: 'hub' }).join('\n');
1669
+ expect(rules).to.include('--add-port=51820/udp');
1670
+ expect(rules).to.include('--add-masquerade');
1671
+ expect(rules).to.include('--add-interface=wg0');
1672
+ });
1673
+ });
1674
+
1675
+ describe('probe scheduling', () => {
1676
+ it('gives each interval its own scrape job so cadences do not merge', () => {
1677
+ const groups = probeGroupsFactory([
1678
+ { eventId: 'a', module: 'icmp', interval: '30s', targets: ['10.0.0.1'], labels: {} },
1679
+ { eventId: 'b', module: 'icmp', interval: '30s', targets: ['10.0.0.3'], labels: {} },
1680
+ { eventId: 'c', module: 'http_2xx', interval: '10m', targets: ['https://x/'], labels: {} },
1681
+ ]);
1682
+ expect(groups.map((group) => `${group.module}@${group.interval}`)).to.deep.equal(['icmp@30s', 'http_2xx@10m']);
1683
+ expect(groups[0].groups).to.have.lengthOf(2);
1684
+ });
1685
+
1686
+ it('renders the declared interval as the job scrape period', () => {
1687
+ const conf = prometheusConfFactory({
1688
+ probes: [{ eventId: 'c', module: 'http_2xx', interval: '10m', targets: ['https://x/'], labels: {} }],
1689
+ });
1690
+ expect(conf).to.include("job_name: 'blackbox-http_2xx'");
1691
+ expect(conf).to.include('scrape_interval: 10m');
1692
+ });
1693
+
1694
+ it('omits the period when an event declares none, leaving the global default', () => {
1695
+ const conf = prometheusConfFactory({
1696
+ probes: [{ eventId: 'c', module: 'icmp', interval: '', targets: ['10.0.0.1'], labels: {} }],
1697
+ });
1698
+ expect(conf).to.not.include('scrape_interval: \n');
1699
+ });
1700
+ });
1701
+
1702
+ describe('port-scoped ingress blocking', () => {
1703
+ it('keeps only well-formed TCP ports', () => {
1704
+ expect(Dns.portListFactory('80,443')).to.deep.equal([80, 443]);
1705
+ expect(Dns.portListFactory(' 80 , bad , 443 , 99999 , 0 ')).to.deep.equal([80, 443]);
1706
+ expect(Dns.portListFactory('')).to.deep.equal([]);
1707
+ });
1708
+
1709
+ it('exposes the block and withdraw entry points the scenario calls', () => {
1710
+ for (const method of ['blockIngressPort', 'unblockIngressPort', 'portListFactory'])
1711
+ expect(Dns[method], method).to.be.a('function');
1712
+ });
1713
+
1714
+ it('creates the chain its rules live in before touching it', () => {
1715
+ // `inet filter` exists on a firewalld host with no `input` chain, so every
1716
+ // ingress command fails on a table that looks present.
1717
+ const source = fs.readFileSync(new URL('../../../../src/server/network/dns.js', import.meta.url), 'utf8');
1718
+ for (const method of ['blockAllIngress', 'unblockAllIngress', 'blockIngressPort', 'unblockIngressPort']) {
1719
+ const body = source.slice(source.indexOf(`static ${method}(`));
1720
+ expect(body.slice(0, body.indexOf('\n }')), method).to.include('Dns.ensureIngressChain()');
1721
+ }
1722
+ expect(Dns.ensureIngressChain).to.be.a('function');
1723
+ });
1724
+
1725
+ it('calls the helper on the class that declares it', () => {
1726
+ // The class is `Dns`; `UnderpostDns` is the CLI wrapper and carries no such
1727
+ // static, so naming it here fails only once the command runs on a host.
1728
+ const source = fs.readFileSync(new URL('../../../../src/server/network/dns.js', import.meta.url), 'utf8');
1729
+ expect(source).to.not.include('UnderpostDns.portListFactory');
1176
1730
  });
1177
1731
  });