@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
@@ -0,0 +1,502 @@
1
+ 'use strict';
2
+
3
+ import { expect } from 'chai';
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import Underpost from '../../../../src/index.js';
8
+ import { assertEventSchedules, eventSchedule } from '../../../../src/server/ops/event-notification.js';
9
+ import UnderpostEvent, {
10
+ EVENT_E2E,
11
+ assertHubManagementConnection,
12
+ probeDetail,
13
+ eventCooldownKeyFactory,
14
+ eventFirewallCommandsFactory,
15
+ eventServicePortFactory,
16
+ } from '../../../../src/cli/event.js';
17
+ import { UNDERPOST_MONITORING } from '../../../../src/server/ops/monitoring.js';
18
+
19
+ describe('event remediation', () => {
20
+ it('keeps the dispatcher available when WireGuard is down', () => {
21
+ const unit = UnderpostEvent.API.serviceUnitFactory({
22
+ execPath: '/usr/bin/node',
23
+ workingDirectory: '/opt/underpost/engine',
24
+ user: 'root',
25
+ });
26
+ expect(unit).to.include('After=network-online.target');
27
+ expect(unit).to.include('WantedBy=multi-user.target');
28
+ expect(unit).to.include('Environment=PATH=/usr/bin:');
29
+ expect(unit).to.include(`--port ${UNDERPOST_MONITORING.eventWebhook.port}`);
30
+ expect(unit).to.include('Restart=on-failure');
31
+ expect(unit).to.include('StartLimitBurst=5');
32
+ expect(unit).to.not.include('wg-quick@');
33
+ expect(unit).to.not.include('PartOf=');
34
+ });
35
+
36
+ it('applies cooldown independently to each failed spoke', () => {
37
+ const alert = (spoke) => ({ labels: { underpost_spoke: spoke } });
38
+ expect(eventCooldownKeyFactory('wireguard-spoke-down', [alert('homelab-a')])).to.equal(
39
+ 'wireguard-spoke-down:homelab-a',
40
+ );
41
+ expect(eventCooldownKeyFactory('wireguard-spoke-down', [alert('worker-a')])).to.equal(
42
+ 'wireguard-spoke-down:worker-a',
43
+ );
44
+ });
45
+
46
+ it('opens and closes only the configured dispatcher port', () => {
47
+ const port = UNDERPOST_MONITORING.eventWebhook.port;
48
+ expect(eventFirewallCommandsFactory().join('\n')).to.include(`--add-port=${port}/tcp`);
49
+ expect(eventFirewallCommandsFactory({ remove: true }).join('\n')).to.include(`--remove-port=${port}/tcp`);
50
+ });
51
+
52
+ it('reads the previous service port for firewall migration', () => {
53
+ expect(
54
+ eventServicePortFactory('[Service]\nExecStart=/usr/bin/node /home/dd/engine/bin event --serve --port 9099\n'),
55
+ ).to.equal(9099);
56
+ expect(eventServicePortFactory('[Service]\nExecStart=/usr/bin/node app.js\n')).to.equal(0);
57
+ });
58
+
59
+ it('rejects a VPS repair connection that uses the spoke-forward port', () => {
60
+ expect(() =>
61
+ assertHubManagementConnection({
62
+ hubHost: '64.176.25.136',
63
+ sshForwardPort: 2222,
64
+ connection: { user: 'root', host: '64.176.25.136', port: 2222 },
65
+ }),
66
+ ).to.throw('collides with the spoke-forward port');
67
+ });
68
+
69
+ it('refuses to provision detection without a remediation route', () => {
70
+ expect(() =>
71
+ UnderpostEvent.API.assertRemediationReady([
72
+ {
73
+ remediation: [
74
+ {
75
+ role: 'hub',
76
+ via: 'unresolved',
77
+ error: 'no SSH connection is registered for the WireGuard hub',
78
+ },
79
+ ],
80
+ },
81
+ ]),
82
+ ).to.throw('no SSH connection is registered for the WireGuard hub');
83
+
84
+ const definitions = [{ remediation: [{ role: 'spoke', via: 'local' }] }];
85
+ expect(UnderpostEvent.API.assertRemediationReady(definitions)).to.equal(definitions);
86
+ });
87
+
88
+ it('labels dry-run notifications as planned instead of remediated', () => {
89
+ const notification = UnderpostEvent.API.notificationFactory({
90
+ eventId: 'wireguard-spoke-down',
91
+ result: {
92
+ ok: true,
93
+ role: 'spoke',
94
+ condition: 'test',
95
+ targets: [
96
+ {
97
+ role: 'spoke',
98
+ spokeId: 'homelab-a',
99
+ address: '10.0.0.2',
100
+ via: 'local',
101
+ commands: ['node bin wireguard --wireguard-restart --check'],
102
+ ok: true,
103
+ output: '[dry-run] local',
104
+ },
105
+ ],
106
+ },
107
+ });
108
+ expect(notification.subject).to.include('planned');
109
+ expect(notification.text).to.include('no command executed');
110
+ });
111
+ });
112
+
113
+ describe('event end-to-end rehearsal', () => {
114
+ it('loads every shipped scenario, each implementing the contract', async () => {
115
+ // Not every event is rehearsable: a threshold alert has no fault that can be
116
+ // induced on a production node without causing the outage it warns about.
117
+ const shipped = fs
118
+ .readdirSync(EVENT_E2E.scenarioDirectory)
119
+ .filter((name) => name.startsWith('event-e2e-') && name.endsWith('.js'))
120
+ .map((name) => name.slice('event-e2e-'.length, -'.js'.length));
121
+ expect(shipped).to.not.be.empty;
122
+ for (const eventId of shipped) {
123
+ expect(UnderpostEvent.API.EVENTS, eventId).to.have.property(eventId);
124
+ const scenario = await UnderpostEvent.API.e2eScenario(eventId);
125
+ expect(scenario.description, eventId).to.be.a('string');
126
+ for (const method of ['subjects', 'break', 'restore'])
127
+ expect(scenario[method], `${eventId}.${method}`).to.be.a('function');
128
+ }
129
+ });
130
+
131
+ it('reports the path it expected rather than failing on an import', async () => {
132
+ try {
133
+ await UnderpostEvent.API.e2eScenario('not-an-event');
134
+ expect.fail('an unknown event id must not resolve a scenario');
135
+ } catch (error) {
136
+ expect(error.message).to.include(`${EVENT_E2E.scenarioDirectory}/event-e2e-not-an-event.js`);
137
+ }
138
+ });
139
+
140
+ it('refuses the flags that would leave nothing repaired or nothing verified', async () => {
141
+ for (const [options, reason] of [
142
+ [{ dryRun: true }, 'not compatible with --dry-run'],
143
+ [{ notify: false }, 'not compatible with --no-notify'],
144
+ ]) {
145
+ try {
146
+ await UnderpostEvent.API.e2e('wireguard-server-down', options);
147
+ expect.fail(`${reason} was accepted`);
148
+ } catch (error) {
149
+ expect(error.message).to.include(reason);
150
+ }
151
+ }
152
+ });
153
+ });
154
+
155
+ describe('probe observation reporting', () => {
156
+ const down = { target: '10.0.0.3', success: false, read: true };
157
+
158
+ it('reports the elapsed time only when the wait actually succeeded', () => {
159
+ expect(probeDetail({ ok: true, readable: true, elapsedMs: 1200, observations: [down] }, 'answered again')).to.equal(
160
+ 'probes answered again after 1200ms',
161
+ );
162
+ });
163
+
164
+ it('never claims a failed wait answered', () => {
165
+ const detail = probeDetail(
166
+ { ok: false, readable: true, elapsedMs: 180136, observations: [down] },
167
+ 'answered again',
168
+ );
169
+ expect(detail).to.include('never answered again within 180136ms');
170
+ expect(detail).to.include('10.0.0.3 probe_success=0');
171
+ });
172
+
173
+ it('names an unreadable exporter instead of blaming the subject', () => {
174
+ const detail = probeDetail(
175
+ { ok: false, readable: false, elapsedMs: 60082, observations: [{ ...down, read: false, error: 'ECONNREFUSED' }] },
176
+ 'answered again',
177
+ );
178
+ expect(detail).to.include('Blackbox Exporter could not be read');
179
+ expect(detail).to.include('ECONNREFUSED');
180
+ expect(detail).to.not.include('probe_success');
181
+ });
182
+ });
183
+
184
+ describe('public ingress classification', () => {
185
+ const health = (results) => {
186
+ const failing = results.filter((entry) => !entry.ok);
187
+ return {
188
+ state:
189
+ results.length === 0 || failing.length === 0
190
+ ? 'healthy'
191
+ : failing.length === results.length
192
+ ? 'down'
193
+ : 'partial',
194
+ total: results.length,
195
+ healthy: results.length - failing.length,
196
+ failing,
197
+ };
198
+ };
199
+ const probe = (url, statuses) => ({ url, statuses, ok: statuses[statuses.length - 1] === '200' });
200
+
201
+ it('treats the final code of a redirect chain as the answer', () => {
202
+ expect(probe('a', ['302', '200']).ok).to.equal(true);
203
+ expect(probe('b', ['301']).ok).to.equal(false);
204
+ expect(probe('c', ['000']).ok).to.equal(false);
205
+ });
206
+
207
+ it('separates a per-host fault from an edge outage', () => {
208
+ expect(health([probe('a', ['200']), probe('b', ['200'])]).state).to.equal('healthy');
209
+ expect(health([probe('a', ['200']), probe('b', ['000'])]).state).to.equal('partial');
210
+ expect(health([probe('a', ['000']), probe('b', ['503'])]).state).to.equal('down');
211
+ });
212
+
213
+ it('registers the event with an http probe over every published host', () => {
214
+ const event = UnderpostEvent.API.EVENTS['public-ingress-down'];
215
+ expect(event.role).to.equal('ingress');
216
+ expect(event.alert.expr).to.include('underpost_event="public-ingress-down"');
217
+ });
218
+ });
219
+
220
+ describe('event schedule contract', () => {
221
+ const conf = {
222
+ events: {
223
+ 'wireguard-spoke-down': { probeInterval: '30s', alertFor: '2m' },
224
+ 'public-ingress-down': { probeInterval: '10m' },
225
+ 'node-cpu-limit-exceeded': { probeInterval: '1m', alertFor: '5m', threshold: 85 },
226
+ },
227
+ };
228
+
229
+ it('reads every cadence from the one declaration', () => {
230
+ expect(eventSchedule('wireguard-spoke-down', conf)).to.deep.equal({
231
+ probeInterval: '30s',
232
+ alertFor: '2m',
233
+ threshold: '',
234
+ });
235
+ expect(eventSchedule('node-cpu-limit-exceeded', conf)).to.deep.equal({
236
+ probeInterval: '1m',
237
+ alertFor: '5m',
238
+ threshold: '85',
239
+ });
240
+ });
241
+
242
+ it('reports an undeclared event rather than inventing a cadence', () => {
243
+ expect(eventSchedule('absent', conf)).to.deep.equal({ probeInterval: '', alertFor: '', threshold: '' });
244
+ });
245
+
246
+ it('refuses a rule that compares against a threshold the contract never declared', () => {
247
+ expect(() =>
248
+ assertEventSchedules([
249
+ {
250
+ id: 'node-cpu-limit-exceeded',
251
+ alert: { expr: 'cpu > <threshold>' },
252
+ schedule: { probeInterval: '1m', alertFor: '5m', threshold: '' },
253
+ },
254
+ ]),
255
+ ).to.throw('node-cpu-limit-exceeded: threshold');
256
+ });
257
+
258
+ it('refuses to publish a rule whose window the contract never declared', () => {
259
+ expect(() =>
260
+ assertEventSchedules([{ id: 'public-ingress-down', schedule: eventSchedule('public-ingress-down', conf) }]),
261
+ ).to.throw('public-ingress-down: alertFor');
262
+ });
263
+
264
+ it('accepts a fully declared schedule', () => {
265
+ expect(
266
+ assertEventSchedules([{ id: 'wireguard-spoke-down', schedule: eventSchedule('wireguard-spoke-down', conf) }]),
267
+ ).to.have.lengthOf(1);
268
+ });
269
+
270
+ it('keeps the alert window out of the registry so it cannot disagree', () => {
271
+ for (const event of Object.values(UnderpostEvent.API.EVENTS)) expect(event.alert.for).to.equal(undefined);
272
+ });
273
+ });
274
+
275
+ describe('incremental deployment against cluster state', () => {
276
+ let readDeployedEventState;
277
+ const cluster = (ids) => {
278
+ Underpost.monitor.readDeployedEventState = () => ({ readable: true, ids, reason: '' });
279
+ };
280
+ const unreachableCluster = (reason = 'connection refused') => {
281
+ Underpost.monitor.readDeployedEventState = () => ({ readable: false, ids: [], reason });
282
+ };
283
+
284
+ beforeEach(() => {
285
+ readDeployedEventState = Underpost.monitor.readDeployedEventState;
286
+ });
287
+ afterEach(() => {
288
+ Underpost.monitor.readDeployedEventState = readDeployedEventState;
289
+ });
290
+
291
+ it('merges the named event into what the cluster already runs', () => {
292
+ cluster(['public-ingress-down']);
293
+ expect(UnderpostEvent.API.deploySelection('wireguard-spoke-down', {})).to.deep.equal([
294
+ 'public-ingress-down',
295
+ 'wireguard-spoke-down',
296
+ ]);
297
+ });
298
+
299
+ it('is idempotent for an event already deployed', () => {
300
+ cluster(['public-ingress-down']);
301
+ expect(UnderpostEvent.API.deploySelection('public-ingress-down', {})).to.deep.equal(['public-ingress-down']);
302
+ });
303
+
304
+ it('withdraws only the named event', () => {
305
+ cluster(['public-ingress-down', 'wireguard-spoke-down']);
306
+ expect(UnderpostEvent.API.deploySelection('public-ingress-down', {}, true)).to.deep.equal(['wireguard-spoke-down']);
307
+ });
308
+
309
+ it('drops an id the registry no longer declares instead of failing the publish', () => {
310
+ cluster(['public-ingress-down', 'retired-event']);
311
+ expect(UnderpostEvent.API.deploySelection('wireguard-spoke-down', {})).to.not.include('retired-event');
312
+ });
313
+
314
+ it('reports declared, deployed and orphaned events apart', () => {
315
+ cluster(['public-ingress-down', 'retired-event']);
316
+ const status = Object.fromEntries(UnderpostEvent.API.deploymentStatus({}).map((row) => [row.id, row.status]));
317
+ expect(status['public-ingress-down']).to.equal('DEPLOYED');
318
+ expect(status['wireguard-spoke-down']).to.equal('PENDING');
319
+ expect(status['retired-event']).to.equal('OUT_OF_SYNC');
320
+ });
321
+
322
+ it('refuses to publish a set an unreadable cluster would silently reduce to one event', () => {
323
+ unreachableCluster('The connection to the server 127.0.0.1:6443 was refused');
324
+ expect(() => UnderpostEvent.API.deploySelection('wireguard-spoke-down', {})).to.throw(/unreadable/);
325
+ });
326
+
327
+ it('reports an unreadable cluster as unknown rather than as nothing deployed', () => {
328
+ unreachableCluster();
329
+ const status = Object.fromEntries(UnderpostEvent.API.deploymentStatus({}).map((row) => [row.id, row.status]));
330
+ expect(status['public-ingress-down']).to.equal('UNKNOWN');
331
+ expect(status['wireguard-spoke-down']).to.equal('UNKNOWN');
332
+ });
333
+
334
+ it('reads an absent stack as an empty deployed set, not as an unreadable one', () => {
335
+ cluster([]);
336
+ const status = Object.fromEntries(UnderpostEvent.API.deploymentStatus({}).map((row) => [row.id, row.status]));
337
+ expect(status['public-ingress-down']).to.equal('PENDING');
338
+ });
339
+ });
340
+
341
+ describe('planned-maintenance event suspension', () => {
342
+ let readDeployedEventState;
343
+ let syncObservability;
344
+ let directory;
345
+ let stateFile;
346
+ let syncCalls;
347
+
348
+ beforeEach(() => {
349
+ readDeployedEventState = Underpost.monitor.readDeployedEventState;
350
+ syncObservability = Underpost.monitor.syncObservability;
351
+ directory = fs.mkdtempSync(path.join(os.tmpdir(), 'underpost-event-suspension-'));
352
+ stateFile = path.join(directory, 'events.json');
353
+ syncCalls = [];
354
+ Underpost.monitor.readDeployedEventState = () => ({
355
+ readable: true,
356
+ ids: ['wireguard-spoke-down', 'public-ingress-down'],
357
+ reason: '',
358
+ });
359
+ Underpost.monitor.syncObservability = async (options) => {
360
+ syncCalls.push(options);
361
+ return { events: options.events };
362
+ };
363
+ });
364
+
365
+ afterEach(() => {
366
+ Underpost.monitor.readDeployedEventState = readDeployedEventState;
367
+ Underpost.monitor.syncObservability = syncObservability;
368
+ fs.rmSync(directory, { recursive: true, force: true });
369
+ });
370
+
371
+ it('persists the exact deployed set before publishing an empty event set', async () => {
372
+ const state = await UnderpostEvent.API.suspendEvents(stateFile, { namespace: 'default' });
373
+ expect(state.events).to.deep.equal(['public-ingress-down', 'wireguard-spoke-down']);
374
+ expect(JSON.parse(fs.readFileSync(stateFile, 'utf8')).events).to.deep.equal(state.events);
375
+ expect(fs.statSync(stateFile).mode & 0o777).to.equal(0o600);
376
+ expect(syncCalls).to.have.lengthOf(1);
377
+ expect(syncCalls[0].events).to.deep.equal([]);
378
+ expect(syncCalls[0].requireEventReload).to.equal(true);
379
+ });
380
+
381
+ it('restores the saved set and removes state only after resync succeeds', async () => {
382
+ await UnderpostEvent.API.suspendEvents(stateFile, { namespace: 'default' });
383
+ const context = await UnderpostEvent.API.resumeEvents(stateFile);
384
+ expect(context.events).to.deep.equal(['public-ingress-down', 'wireguard-spoke-down']);
385
+ expect(syncCalls[1].events).to.deep.equal(['public-ingress-down', 'wireguard-spoke-down']);
386
+ expect(syncCalls[1].requireEventReload).to.equal(true);
387
+ expect(fs.existsSync(stateFile)).to.equal(false);
388
+ });
389
+
390
+ it('retains recovery state when event resynchronization fails', async () => {
391
+ await UnderpostEvent.API.suspendEvents(stateFile, { namespace: 'default' });
392
+ Underpost.monitor.syncObservability = async () => {
393
+ throw new Error('prometheus unavailable');
394
+ };
395
+ try {
396
+ await UnderpostEvent.API.resumeEvents(stateFile);
397
+ expect.fail('resume should reject a failed observability resync');
398
+ } catch (error) {
399
+ expect(error.message).to.include('prometheus unavailable');
400
+ }
401
+ expect(fs.existsSync(stateFile)).to.equal(true);
402
+ });
403
+
404
+ it('retains the original event set when suspension reconciliation fails', async () => {
405
+ Underpost.monitor.syncObservability = async () => {
406
+ throw new Error('prometheus reload refused');
407
+ };
408
+ try {
409
+ await UnderpostEvent.API.suspendEvents(stateFile, { namespace: 'default' });
410
+ expect.fail('suspend should reject a failed observability reconciliation');
411
+ } catch (error) {
412
+ expect(error.message).to.include('prometheus reload refused');
413
+ }
414
+ expect(JSON.parse(fs.readFileSync(stateFile, 'utf8')).events).to.deep.equal([
415
+ 'public-ingress-down',
416
+ 'wireguard-spoke-down',
417
+ ]);
418
+ });
419
+
420
+ it('does not create state or withdraw alerts when cluster event state is unreadable', async () => {
421
+ Underpost.monitor.readDeployedEventState = () => ({ readable: false, ids: [], reason: 'connection refused' });
422
+ try {
423
+ await UnderpostEvent.API.suspendEvents(stateFile, { namespace: 'default' });
424
+ expect.fail('suspend should reject unreadable cluster state');
425
+ } catch (error) {
426
+ expect(error.message).to.include('unreadable');
427
+ }
428
+ expect(fs.existsSync(stateFile)).to.equal(false);
429
+ expect(syncCalls).to.be.empty;
430
+ });
431
+ });
432
+
433
+ describe('public ingress recovery wait', () => {
434
+ it('polls for the edge to carry traffic again instead of reading the verdict once', () => {
435
+ const source = fs.readFileSync(new URL('../../../../src/cli/event.js', import.meta.url), 'utf8');
436
+ const body = source.slice(source.indexOf('async repairPublicIngress('));
437
+ const method = body.slice(0, body.indexOf('\n },'));
438
+ expect(method).to.include('await Underpost.event.awaitPublicIngressHealth()');
439
+ expect(method).to.not.include('Underpost.event.publicIngressHealth()\n ;');
440
+ });
441
+
442
+ it('gives the edge longer than one probe pass to come back', () => {
443
+ const source = fs.readFileSync(new URL('../../../../src/cli/event.js', import.meta.url), 'utf8');
444
+ const match = /PUBLIC_INGRESS_RECOVERY = \{ timeoutMs: (\d+)/.exec(source);
445
+ expect(Number(match?.[1])).to.be.greaterThan(60000);
446
+ });
447
+ });
448
+
449
+ describe('node threshold events', () => {
450
+ const ids = [
451
+ 'node-cpu-limit-exceeded',
452
+ 'node-memory-limit-exceeded',
453
+ 'hub-bandwidth-limit-exceeded',
454
+ 'node-disk-limit-exceeded',
455
+ 'node-network-traffic-exceeded',
456
+ ];
457
+
458
+ it('registers each with a rule the contract supplies the threshold for', () => {
459
+ for (const id of ids) {
460
+ const event = UnderpostEvent.API.EVENTS[id];
461
+ expect(event, id).to.be.an('object');
462
+ expect(event.alert.expr, id).to.include('<threshold>');
463
+ expect(event.alert.for, id).to.equal(undefined);
464
+ }
465
+ });
466
+
467
+ it('runs no probe, because the rule reads scraped host metrics', () => {
468
+ for (const id of ids) expect(UnderpostEvent.API.EVENTS[id].probes(), id).to.deep.equal([]);
469
+ });
470
+
471
+ it('reads the hub quota from a metric rather than the Vultr API', () => {
472
+ const event = UnderpostEvent.API.EVENTS['hub-bandwidth-limit-exceeded'];
473
+ expect(event.alert.expr).to.include('vultr_bandwidth_used_bytes');
474
+ expect(event.alert.severity).to.equal('critical');
475
+ });
476
+
477
+ it('gathers evidence rather than acting blindly on a threshold', async () => {
478
+ const runCommand = UnderpostEvent.API.runCommand;
479
+ const nodeTargets = UnderpostEvent.API.nodeTargets;
480
+ const calls = [];
481
+ UnderpostEvent.API.nodeTargets = () => [
482
+ { instance: '192.168.1.85', via: 'local', user: '', host: '', nodeName: 'control' },
483
+ { instance: '192.168.1.191', via: 'admin@192.168.1.191:22', user: 'admin', host: '192.168.1.191' },
484
+ ];
485
+ UnderpostEvent.API.runCommand = async (command, options) => {
486
+ calls.push({ command, host: options.host });
487
+ return { ok: true, output: 'evidence' };
488
+ };
489
+ try {
490
+ const result = await UnderpostEvent.API.EVENTS['node-cpu-limit-exceeded'].handler({}, [
491
+ { labels: { instance: '192.168.1.191:9100' } },
492
+ ]);
493
+ expect(result.ok).to.equal(true);
494
+ expect(calls).to.have.lengthOf(1);
495
+ expect(calls[0].host).to.equal('192.168.1.191');
496
+ expect(calls[0].command).to.include('ps aux --sort=-%cpu');
497
+ } finally {
498
+ UnderpostEvent.API.runCommand = runCommand;
499
+ UnderpostEvent.API.nodeTargets = nodeTargets;
500
+ }
501
+ });
502
+ });