@underpostnet/cyberia 3.2.90 → 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 (490) hide show
  1. package/.dockerignore +1 -0
  2. package/.env.example +16 -0
  3. package/.github/workflows/coverall.cyberia.ci.yml +107 -0
  4. package/.github/workflows/cyberia-client.cd.yml +6 -18
  5. package/.github/workflows/cyberia-server.cd.yml +6 -18
  6. package/.github/workflows/docker-image.cyberia-client.ci.yml +8 -5
  7. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +8 -3
  8. package/.github/workflows/docker-image.cyberia-server.ci.yml +8 -5
  9. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +8 -5
  10. package/.github/workflows/docker-image.engine-cyberia.ci.yml +15 -1
  11. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +15 -1
  12. package/.github/workflows/engine-cyberia.cd.yml +6 -78
  13. package/.github/workflows/engine-cyberia.ci.yml +4 -5
  14. package/.github/workflows/ghpkg.ci.yml +42 -21
  15. package/.github/workflows/hardhat.ci.yml +12 -11
  16. package/.github/workflows/npmpkg.ci.yml +17 -15
  17. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
  18. package/.github/workflows/pwa-microservices-template-test.ci.yml +2 -2
  19. package/.github/workflows/release.cd.yml +2 -9
  20. package/.prettierignore +1 -0
  21. package/AGENTS.md +47 -0
  22. package/CHANGELOG.md +581 -2417
  23. package/CLI-HELP.md +416 -127
  24. package/Dockerfile +25 -9
  25. package/Dockerfile.dev +25 -9
  26. package/Dockerfile.test +4 -4
  27. package/README.md +21 -1
  28. package/bin/build.js +70 -43
  29. package/bin/build.template.js +8 -3
  30. package/bin/cyberia.js +372 -135
  31. package/bin/deploy.js +18 -104
  32. package/bin/index.js +372 -135
  33. package/compose.env +16 -0
  34. package/conf.js +93 -4
  35. package/deploy/cyberia-client/deploy.sh +47 -0
  36. package/deploy/cyberia-client/package.sh +18 -0
  37. package/deploy/cyberia-client/state.sh +14 -0
  38. package/deploy/cyberia-server/deploy.sh +47 -0
  39. package/deploy/cyberia-server/package.sh +18 -0
  40. package/deploy/cyberia-server/state.sh +14 -0
  41. package/deploy/dd-cyberia/deploy.sh +22 -0
  42. package/deploy/dd-cyberia/init.sh +55 -0
  43. package/deploy/dd-cyberia/package.sh +17 -0
  44. package/deploy/dd-cyberia/state.sh +14 -0
  45. package/deploy/dd-cyberia/sync-deploy.sh +116 -0
  46. package/deploy/lib/github-actions-logging.sh +594 -0
  47. package/deploy/lib/host.sh +185 -0
  48. package/deploy/lib/state.sh +62 -0
  49. package/deploy/pwa-microservices-template/deploy.sh +45 -0
  50. package/deploy/release/deploy.sh +32 -0
  51. package/deployment.yaml +7 -2
  52. package/docker-compose.yml +31 -37
  53. package/examples/static-page/README.md +19 -19
  54. package/examples/static-page/ssr-components/CustomPage.js +2 -2
  55. package/hardhat/hardhat.config.js +1 -6
  56. package/hardhat/package-lock.json +104 -43
  57. package/hardhat/package.json +3 -3
  58. package/manifests/alertmanager/deployment.yaml +81 -0
  59. package/manifests/blackbox-exporter/deployment.yaml +75 -0
  60. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +36 -7
  61. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +77 -0
  62. package/manifests/deployment/dd-cyberia-development/deployment.yaml +7 -2
  63. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  64. package/manifests/grafana/deployment.yaml +26 -7
  65. package/manifests/grafana/kustomization.yaml +1 -0
  66. package/manifests/grafana/service.yaml +6 -1
  67. package/manifests/mariadb/pv.yaml +1 -0
  68. package/manifests/mongodb/statefulset.yaml +9 -2
  69. package/manifests/mysql/pv-pvc.yaml +14 -1
  70. package/manifests/postgresql/pv.yaml +12 -1
  71. package/manifests/postgresql/pvc.yaml +1 -0
  72. package/manifests/prometheus/deployment.yaml +18 -22
  73. package/package.json +83 -10
  74. package/scripts/audit-selinux.sh +64 -0
  75. package/scripts/event-monitor.sh +56 -0
  76. package/scripts/gpu-diag.sh +0 -0
  77. package/scripts/ip-info.sh +0 -0
  78. package/scripts/k3s-node-setup.sh +30 -14
  79. package/scripts/kubeadm-node-setup.sh +40 -31
  80. package/scripts/lxd-vm-setup.sh +0 -0
  81. package/scripts/maas-nat-firewalld.sh +0 -0
  82. package/scripts/maas-setup.sh +8 -8
  83. package/scripts/nat-iptables.sh +2 -0
  84. package/scripts/rhel-grpc-setup.sh +0 -0
  85. package/scripts/rocky-kickstart.sh +25 -9
  86. package/scripts/selinux-normalize.sh +907 -0
  87. package/scripts/shutdown-machine.sh +843 -0
  88. package/scripts/test-monitor.sh +3 -3
  89. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +1 -1
  90. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +2 -2
  91. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +2 -2
  92. package/src/api/core/core.controller.js +1 -1
  93. package/src/api/core/core.router.js +2 -2
  94. package/src/api/core/core.service.js +2 -2
  95. package/src/api/crypto/crypto.controller.js +1 -1
  96. package/src/api/crypto/crypto.service.js +1 -1
  97. package/src/api/cyberia-action/cyberia-action.controller.js +1 -1
  98. package/src/api/cyberia-action/cyberia-action.router.js +1 -1
  99. package/src/api/cyberia-action/cyberia-action.service.js +2 -2
  100. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -1
  101. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +1 -1
  102. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +2 -2
  103. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +1 -1
  104. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  105. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +2 -2
  106. package/src/api/cyberia-entity/cyberia-entity.controller.js +1 -1
  107. package/src/api/cyberia-entity/cyberia-entity.router.js +1 -1
  108. package/src/api/cyberia-entity/cyberia-entity.service.js +2 -2
  109. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +1 -1
  110. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +1 -1
  111. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +2 -2
  112. package/src/api/cyberia-instance/cyberia-fallback-capture.js +466 -0
  113. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +63 -0
  114. package/src/api/cyberia-instance/cyberia-fallback-world.js +16 -1
  115. package/src/api/cyberia-instance/cyberia-instance-map.service.js +1 -1
  116. package/src/api/cyberia-instance/cyberia-instance.controller.js +4 -1
  117. package/src/api/cyberia-instance/cyberia-instance.router.js +15 -2
  118. package/src/api/cyberia-instance/cyberia-instance.service.js +45 -2
  119. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +1 -1
  120. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +1 -0
  121. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +1 -1
  122. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +2 -2
  123. package/src/api/cyberia-map/cyberia-map.controller.js +1 -1
  124. package/src/api/cyberia-map/cyberia-map.router.js +1 -1
  125. package/src/api/cyberia-map/cyberia-map.service.js +2 -2
  126. package/src/api/cyberia-quest/cyberia-quest.controller.js +1 -1
  127. package/src/api/cyberia-quest/cyberia-quest.router.js +1 -1
  128. package/src/api/cyberia-quest/cyberia-quest.service.js +2 -2
  129. package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +1 -1
  130. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +1 -1
  131. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +2 -2
  132. package/src/api/cyberia-saga/cyberia-saga.controller.js +1 -1
  133. package/src/api/cyberia-saga/cyberia-saga.router.js +1 -1
  134. package/src/api/cyberia-saga/cyberia-saga.service.js +2 -2
  135. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +16 -29
  136. package/src/api/cyberia-skill/cyberia-skill.controller.js +1 -1
  137. package/src/api/cyberia-skill/cyberia-skill.router.js +1 -1
  138. package/src/api/cyberia-skill/cyberia-skill.service.js +2 -2
  139. package/src/api/default/default.controller.js +1 -1
  140. package/src/api/default/default.router.js +1 -1
  141. package/src/api/default/default.service.js +2 -2
  142. package/src/api/document/document.controller.js +1 -1
  143. package/src/api/document/document.router.js +1 -1
  144. package/src/api/document/document.service.js +2 -2
  145. package/src/api/file/file.controller.js +1 -1
  146. package/src/api/file/file.router.js +1 -1
  147. package/src/api/file/file.service.js +2 -2
  148. package/src/api/instance/instance.controller.js +1 -1
  149. package/src/api/instance/instance.router.js +1 -1
  150. package/src/api/instance/instance.service.js +2 -2
  151. package/src/api/ipfs/ipfs.controller.js +1 -1
  152. package/src/api/ipfs/ipfs.router.js +2 -2
  153. package/src/api/ipfs/ipfs.service.js +2 -2
  154. package/src/api/object-layer/object-layer.controller.js +1 -1
  155. package/src/api/object-layer/object-layer.model.js +204 -3
  156. package/src/api/object-layer/object-layer.router.js +2 -2
  157. package/src/api/object-layer/object-layer.service.js +4 -17
  158. package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +1 -1
  159. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +1 -1
  160. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +2 -2
  161. package/src/api/test/test.controller.js +1 -1
  162. package/src/api/test/test.service.js +1 -1
  163. package/src/api/user/guest.service.js +3 -3
  164. package/src/api/user/user.controller.js +1 -1
  165. package/src/api/user/user.router.js +2 -2
  166. package/src/api/user/user.service.js +4 -4
  167. package/src/api.js +5 -9
  168. package/src/cli/app.js +391 -0
  169. package/src/cli/baremetal.js +30 -36
  170. package/src/cli/client.js +306 -0
  171. package/src/cli/cloud-init.js +3 -3
  172. package/src/cli/cluster.js +242 -135
  173. package/src/cli/db.js +126 -67
  174. package/src/cli/deploy.js +158 -60
  175. package/src/cli/docker-compose.js +5 -5
  176. package/src/cli/domains.js +184 -0
  177. package/src/cli/dotenv-store.js +143 -0
  178. package/src/cli/event.js +2281 -0
  179. package/src/cli/fs.js +3 -3
  180. package/src/cli/host.js +672 -0
  181. package/src/cli/image.js +4 -7
  182. package/src/cli/index.js +426 -134
  183. package/src/cli/ipfs.js +34 -30
  184. package/src/cli/kickstart.js +1 -1
  185. package/src/cli/kubectl.js +218 -41
  186. package/src/cli/lxd.js +3 -3
  187. package/src/cli/monitor.js +1359 -26
  188. package/src/cli/package.js +90 -0
  189. package/src/cli/release.js +73 -34
  190. package/src/cli/repository.js +564 -367
  191. package/src/cli/run.js +362 -654
  192. package/src/cli/secrets.js +2158 -964
  193. package/src/cli/ssh.js +651 -331
  194. package/src/cli/state.js +394 -0
  195. package/src/cli/static.js +2 -2
  196. package/src/cli/system.js +26 -13
  197. package/src/cli/test.js +199 -125
  198. package/src/cli/vultr.js +623 -0
  199. package/src/cli/wireguard.js +2930 -0
  200. package/src/client/components/core/CalendarCore.js +53 -96
  201. package/src/client/components/core/Docs.js +258 -211
  202. package/src/client/components/core/FileExplorer.js +0 -21
  203. package/src/client/components/core/PanelForm.js +4 -4
  204. package/src/client/components/core/Repository.js +56 -0
  205. package/src/client/components/core/Translate.js +7 -0
  206. package/src/client/components/core/{FullScreen.js → ViewModeController.js} +42 -39
  207. package/src/client/components/core/Wallet.js +0 -11
  208. package/src/client/components/core/Worker.js +7 -1
  209. package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +2 -2
  210. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +368 -0
  211. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +35 -1
  212. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +4 -0
  213. package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +2 -2
  214. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
  215. package/src/client/components/default/AppShellDefault.js +1 -1
  216. package/src/client/components/default/SettingsDefault.js +2 -2
  217. package/src/client/components/itemledger/SettingsItemledger.js +2 -2
  218. package/src/client/components/underpost/AppShellUnderpost.js +3 -2
  219. package/src/client/components/underpost/SettingsUnderpost.js +2 -2
  220. package/src/client/public/cyberia-docs/ARCHITECTURE.md +17 -3
  221. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +32 -5
  222. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +1 -1
  223. package/src/client/public/cyberia-docs/CYBERIA.md +21 -1
  224. package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +23 -6
  225. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  226. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  227. package/src/client/public/default/android-chrome-144x144.png +0 -0
  228. package/src/client/public/default/android-chrome-192x192.png +0 -0
  229. package/src/client/public/default/android-chrome-256x256.png +0 -0
  230. package/src/client/public/default/android-chrome-36x36.png +0 -0
  231. package/src/client/public/default/android-chrome-384x384.png +0 -0
  232. package/src/client/public/default/android-chrome-48x48.png +0 -0
  233. package/src/client/public/default/android-chrome-512x512.png +0 -0
  234. package/src/client/public/default/android-chrome-72x72.png +0 -0
  235. package/src/client/public/default/android-chrome-96x96.png +0 -0
  236. package/src/client/public/default/apple-touch-icon-1024x1024.png +0 -0
  237. package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
  238. package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
  239. package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
  240. package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
  241. package/src/client/public/default/apple-touch-icon-167x167.png +0 -0
  242. package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
  243. package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
  244. package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
  245. package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
  246. package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
  247. package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
  248. package/src/client/public/default/apple-touch-icon.png +0 -0
  249. package/src/client/public/default/apple-touch-startup-image-1125x2436.png +0 -0
  250. package/src/client/public/default/apple-touch-startup-image-1136x640.png +0 -0
  251. package/src/client/public/default/apple-touch-startup-image-1170x2532.png +0 -0
  252. package/src/client/public/default/apple-touch-startup-image-1179x2556.png +0 -0
  253. package/src/client/public/default/apple-touch-startup-image-1242x2208.png +0 -0
  254. package/src/client/public/default/apple-touch-startup-image-1242x2688.png +0 -0
  255. package/src/client/public/default/apple-touch-startup-image-1284x2778.png +0 -0
  256. package/src/client/public/default/apple-touch-startup-image-1290x2796.png +0 -0
  257. package/src/client/public/default/apple-touch-startup-image-1334x750.png +0 -0
  258. package/src/client/public/default/apple-touch-startup-image-1488x2266.png +0 -0
  259. package/src/client/public/default/apple-touch-startup-image-1536x2048.png +0 -0
  260. package/src/client/public/default/apple-touch-startup-image-1620x2160.png +0 -0
  261. package/src/client/public/default/apple-touch-startup-image-1640x2160.png +0 -0
  262. package/src/client/public/default/apple-touch-startup-image-1668x2224.png +0 -0
  263. package/src/client/public/default/apple-touch-startup-image-1668x2388.png +0 -0
  264. package/src/client/public/default/apple-touch-startup-image-1792x828.png +0 -0
  265. package/src/client/public/default/apple-touch-startup-image-2048x1536.png +0 -0
  266. package/src/client/public/default/apple-touch-startup-image-2048x2732.png +0 -0
  267. package/src/client/public/default/apple-touch-startup-image-2160x1620.png +0 -0
  268. package/src/client/public/default/apple-touch-startup-image-2160x1640.png +0 -0
  269. package/src/client/public/default/apple-touch-startup-image-2208x1242.png +0 -0
  270. package/src/client/public/default/apple-touch-startup-image-2224x1668.png +0 -0
  271. package/src/client/public/default/apple-touch-startup-image-2266x1488.png +0 -0
  272. package/src/client/public/default/apple-touch-startup-image-2388x1668.png +0 -0
  273. package/src/client/public/default/apple-touch-startup-image-2436x1125.png +0 -0
  274. package/src/client/public/default/apple-touch-startup-image-2532x1170.png +0 -0
  275. package/src/client/public/default/apple-touch-startup-image-2556x1179.png +0 -0
  276. package/src/client/public/default/apple-touch-startup-image-2688x1242.png +0 -0
  277. package/src/client/public/default/apple-touch-startup-image-2732x2048.png +0 -0
  278. package/src/client/public/default/apple-touch-startup-image-2778x1284.png +0 -0
  279. package/src/client/public/default/apple-touch-startup-image-2796x1290.png +0 -0
  280. package/src/client/public/default/apple-touch-startup-image-640x1136.png +0 -0
  281. package/src/client/public/default/apple-touch-startup-image-750x1334.png +0 -0
  282. package/src/client/public/default/apple-touch-startup-image-828x1792.png +0 -0
  283. package/src/client/public/default/assets/background/white.jpg +0 -0
  284. package/src/client/public/default/browserconfig.xml +1 -1
  285. package/src/client/public/default/favicon-16x16.png +0 -0
  286. package/src/client/public/default/favicon-32x32.png +0 -0
  287. package/src/client/public/default/favicon-48x48.png +0 -0
  288. package/src/client/public/default/favicon.ico +0 -0
  289. package/src/client/public/default/manifest.webmanifest +5 -5
  290. package/src/client/public/default/mstile-144x144.png +0 -0
  291. package/src/client/public/default/mstile-150x150.png +0 -0
  292. package/src/client/public/default/mstile-310x150.png +0 -0
  293. package/src/client/public/default/mstile-310x310.png +0 -0
  294. package/src/client/public/default/mstile-70x70.png +0 -0
  295. package/src/client/public/default/yandex-browser-50x50.png +0 -0
  296. package/src/client/public/default/yandex-browser-manifest.json +1 -1
  297. package/src/client/services/cyberia-instance/cyberia-instance.service.js +40 -0
  298. package/src/client/services/object-layer/object-layer.management.js +4 -4
  299. package/src/client/ssr/RootDocument.js +1 -1
  300. package/src/client/ssr/body/404.js +1 -1
  301. package/src/client/ssr/body/500.js +1 -1
  302. package/src/client/ssr/body/CacheControl.js +1 -1
  303. package/src/client/ssr/body/CyberiaDefaultSplashScreen.js +1 -1
  304. package/src/client/ssr/body/DefaultSplashScreen.js +1 -1
  305. package/src/client/ssr/body/SwaggerDarkMode.js +1 -1
  306. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +1 -1
  307. package/src/client/ssr/head/CryptokoynScripts.js +1 -1
  308. package/src/client/ssr/head/Css.js +1 -1
  309. package/src/client/ssr/head/CyberiaPortalScripts.js +1 -1
  310. package/src/client/ssr/head/DefaultScripts.js +1 -1
  311. package/src/client/ssr/head/ItemledgerScripts.js +1 -1
  312. package/src/client/ssr/head/Microdata.js +1 -1
  313. package/src/client/ssr/head/Production.js +1 -1
  314. package/src/client/ssr/head/Pwa.js +1 -1
  315. package/src/client/ssr/head/PwaItemledger.js +1 -1
  316. package/src/client/ssr/head/Seo.js +1 -1
  317. package/src/client/ssr/head/UnderpostScripts.js +1 -1
  318. package/src/client/ssr/mailer/DefaultRecoverEmail.js +1 -1
  319. package/src/client/ssr/mailer/DefaultVerifyEmail.js +1 -1
  320. package/src/client/ssr/views/Cyberia404.js +7 -5
  321. package/src/client/ssr/views/CyberiaServerMetrics.js +1 -1
  322. package/src/client/ssr/views/Maintenance.js +1 -1
  323. package/src/client/ssr/views/NoNetworkConnection.js +1 -1
  324. package/src/client/ssr/views/Test.js +1 -1
  325. package/src/client/sw/core.sw.js +20 -14
  326. package/src/client-builder/client-build-docs.js +61 -46
  327. package/src/client-builder/client-build-live.js +2 -2
  328. package/src/client-builder/client-build.js +58 -44
  329. package/src/client-builder/client-dev-server.js +3 -3
  330. package/src/client-builder/client-icons.js +2 -2
  331. package/src/client-builder/ssr.js +3 -3
  332. package/src/client.build.js +3 -6
  333. package/src/client.dev.js +3 -6
  334. package/src/db/DataBaseProvider.js +186 -3
  335. package/src/db/mariadb/MariaDB.js +3 -2
  336. package/src/db/mongo/MongoBootstrap.js +273 -101
  337. package/src/db/mongo/MongooseDB.js +11 -8
  338. package/src/{server/valkey.js → db/valkey/Valkey.js} +5 -4
  339. package/src/grpc/cyberia/grpc-server.js +1 -1
  340. package/src/index.js +103 -23
  341. package/src/mailer/EmailRender.js +2 -2
  342. package/src/mailer/MailerInterceptor.js +87 -0
  343. package/src/mailer/MailerProvider.js +50 -6
  344. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +1 -1
  345. package/src/projects/cyberia/besu-genesis-generator.js +3 -3
  346. package/src/projects/cyberia/catalog-cyberia.js +54 -6
  347. package/src/projects/cyberia/gemini-client.js +1 -1
  348. package/src/projects/cyberia/generate-saga.js +15 -24
  349. package/src/projects/cyberia/hot-reload-trigger.js +1 -1
  350. package/src/projects/cyberia/instance-data.js +22 -3
  351. package/src/projects/cyberia/ipfs-client.js +4 -4
  352. package/src/projects/cyberia/map-preview-generator.js +1 -1
  353. package/src/projects/cyberia/object-layer.js +12 -22
  354. package/src/projects/cyberia/semantic-layer-generator.js +1 -1
  355. package/src/projects/underpost/catalog-underpost.js +13 -2
  356. package/src/proxy.js +4 -7
  357. package/src/runtime/cyberia-client/Dockerfile +9 -4
  358. package/src/runtime/cyberia-client/Dockerfile.dev +8 -2
  359. package/src/runtime/cyberia-server/Dockerfile +9 -4
  360. package/src/runtime/cyberia-server/Dockerfile.dev +8 -2
  361. package/src/runtime/engine-cyberia/Dockerfile +25 -9
  362. package/src/runtime/engine-cyberia/Dockerfile.dev +25 -9
  363. package/src/runtime/engine-cyberia/Dockerfile.test +4 -4
  364. package/src/runtime/engine-cyberia/compose.env +16 -0
  365. package/src/runtime/engine-cyberia/docker-compose.yml +31 -37
  366. package/src/runtime/express/Express.js +32 -27
  367. package/src/runtime/lampp/Lampp.js +22 -70
  368. package/src/runtime/nginx/Nginx.js +1 -1
  369. package/src/runtime/wp/Wp.js +16 -48
  370. package/src/server/{catalog.js → build/catalog.js} +15 -5
  371. package/src/server/build/coverage.js +117 -0
  372. package/src/server/build/execution.js +402 -0
  373. package/src/server/build/package.js +465 -0
  374. package/src/server/build/testing.js +528 -0
  375. package/src/server/{dns.js → network/dns.js} +241 -14
  376. package/src/server/network/forward-proxy.js +541 -0
  377. package/src/server/{middlewares.js → network/middlewares.js} +59 -4
  378. package/src/server/network/node-capability.js +98 -0
  379. package/src/server/{peer.js → network/peer.js} +3 -3
  380. package/src/server/{proxy.js → network/proxy.js} +7 -13
  381. package/src/server/network/router.js +356 -0
  382. package/src/server/{tls.js → network/tls.js} +2 -2
  383. package/src/server/network/underpost-compression.js +186 -0
  384. package/src/server/{underpost-gateway.js → network/underpost-gateway.js} +26 -16
  385. package/src/server/{underpost-ingress.js → network/underpost-ingress.js} +19 -3
  386. package/src/server/{cri.js → ops/cri.js} +2 -2
  387. package/src/server/ops/cron.js +949 -0
  388. package/src/server/ops/event-notification.js +284 -0
  389. package/src/server/{logger.js → ops/logger.js} +108 -23
  390. package/src/server/ops/monitoring.js +1724 -0
  391. package/src/server/ops/systemd.js +259 -0
  392. package/src/server/{conf.js → runtime/conf.js} +521 -520
  393. package/src/server/runtime/config-scope.js +215 -0
  394. package/src/server/runtime/environment.js +149 -0
  395. package/src/server/{process.js → runtime/process.js} +153 -13
  396. package/src/server/{runtime-status.js → runtime/runtime-status.js} +155 -16
  397. package/src/server/{runtime.js → runtime/runtime.js} +12 -8
  398. package/src/server/{start.js → runtime/start.js} +128 -39
  399. package/src/server/{auth.js → security/auth.js} +7 -8
  400. package/src/server/security/container-storage.js +382 -0
  401. package/src/server/{crypto.js → security/crypto.js} +1 -1
  402. package/src/server/security/selinux.js +185 -0
  403. package/src/server/{backup.js → storage/backup.js} +17 -8
  404. package/src/server/{data-query.js → storage/data-query.js} +1 -1
  405. package/src/server/{downloader.js → storage/downloader.js} +2 -2
  406. package/src/server/storage/repository.js +68 -0
  407. package/src/server.js +4 -7
  408. package/src/ws/IoInterface.js +1 -1
  409. package/src/ws/IoServer.js +1 -1
  410. package/src/ws/core/core.ws.connection.js +1 -1
  411. package/src/ws/core/core.ws.emit.js +1 -1
  412. package/src/ws/core/core.ws.server.js +1 -1
  413. package/src/ws/default/default.ws.connection.js +1 -1
  414. package/src/ws/default/default.ws.emit.js +1 -1
  415. package/src/ws/default/default.ws.server.js +1 -1
  416. package/test/e2e/event-e2e-public-ingress-down.js +45 -0
  417. package/test/e2e/event-e2e-wireguard-server-down.js +38 -0
  418. package/test/e2e/event-e2e-wireguard-spoke-down.js +40 -0
  419. package/test/{api.test.js → integration/app/api.test.js} +33 -14
  420. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +30 -0
  421. package/test/{cyberia-instance-conf-defaults.test.js → integration/app/cyberia/cyberia-instance-conf-defaults.test.js} +2 -1
  422. package/test/integration/app/cyberia/cyberia-load.test.js +467 -0
  423. package/test/integration/app/cyberia/fallback-world-capture.test.js +253 -0
  424. package/test/integration/app/cyberia/object-layer-item-id.test.js +327 -0
  425. package/test/{shape-generator.test.js → integration/app/cyberia/shape-generator.test.js} +1 -1
  426. package/test/integration/infra/1-security/config-scope.test.js +305 -0
  427. package/test/integration/infra/1-security/container-storage.test.js +356 -0
  428. package/test/integration/infra/1-security/in-pod-cli-surface.test.js +86 -0
  429. package/test/integration/infra/1-security/secret-onboarding.test.js +930 -0
  430. package/test/integration/infra/1-security/selinux.test.js +69 -0
  431. package/test/{sops-secret-store.test.js → integration/infra/1-security/sops-secret-store.test.js} +415 -61
  432. package/test/integration/infra/1-security/systemd-service.test.js +70 -0
  433. package/test/integration/infra/1-security/underpost-config-secret.test.js +367 -0
  434. package/test/integration/infra/2-network/dns-firewall.test.js +566 -0
  435. package/test/integration/infra/2-network/node-capability.test.js +182 -0
  436. package/test/integration/infra/2-network/wireguard-cli.test.js +1163 -0
  437. package/test/integration/infra/2-network/wireguard-edge.test.js +1731 -0
  438. package/test/{cluster-instances.test.js → integration/infra/3-cluster/cluster-instances.test.js} +11 -15
  439. package/test/{deploy-node-placement.test.js → integration/infra/3-cluster/deploy-node-placement.test.js} +3 -3
  440. package/test/integration/infra/3-cluster/docker-compose-stack.test.js +313 -0
  441. package/test/integration/infra/4-ingress/deploy-routes.test.js +379 -0
  442. package/test/integration/infra/4-ingress/gateway-static-assets.test.js +453 -0
  443. package/test/{instance-traffic-plan.test.js → integration/infra/4-ingress/instance-traffic-plan.test.js} +10 -6
  444. package/test/{underpost-gateway.test.js → integration/infra/4-ingress/underpost-gateway.test.js} +47 -6
  445. package/test/{underpost-ingress.test.js → integration/infra/4-ingress/underpost-ingress.test.js} +56 -4
  446. package/test/integration/infra/5-observability/cron-jobs.test.js +652 -0
  447. package/test/{deploy-monitor.test.js → integration/infra/5-observability/deploy-monitor.test.js} +29 -31
  448. package/test/integration/infra/5-observability/event-notification.test.js +197 -0
  449. package/test/integration/infra/5-observability/event-remediation.test.js +502 -0
  450. package/test/integration/infra/5-observability/event-targets.test.js +1174 -0
  451. package/test/integration/infra/5-observability/monitoring-stack.test.js +902 -0
  452. package/test/support/shell-harness.js +73 -0
  453. package/test/unit/build-template.test.js +97 -0
  454. package/test/unit/build-workflow-offline.test.js +132 -0
  455. package/test/unit/catalog.test.js +71 -0
  456. package/test/unit/client-build-docs.test.js +334 -0
  457. package/test/unit/conf-loading.test.js +303 -0
  458. package/test/unit/conf-resolution.test.js +979 -0
  459. package/test/unit/coverage-artifact.test.js +130 -0
  460. package/test/{crypto.test.js → unit/crypto.test.js} +1 -1
  461. package/test/unit/deploy-env-resolution.test.js +38 -0
  462. package/test/unit/deploy-log-table.test.js +447 -0
  463. package/test/unit/execution-profiles.test.js +219 -0
  464. package/test/unit/fleet-sync-source.test.js +149 -0
  465. package/test/unit/logger-redaction.test.js +112 -0
  466. package/test/unit/package.test.js +403 -0
  467. package/test/unit/prepare-host.test.js +299 -0
  468. package/test/unit/process-environment.test.js +18 -0
  469. package/test/unit/propagation-message.test.js +78 -0
  470. package/test/unit/redeploy-plan.test.js +24 -0
  471. package/test/unit/release-bump.test.js +474 -0
  472. package/test/unit/ssh-output-redaction.test.js +28 -0
  473. package/test/unit/start-options.test.js +46 -0
  474. package/test/unit/test-tiers.test.js +193 -0
  475. package/vitest.config.js +58 -0
  476. package/.nycrc +0 -9
  477. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +0 -48
  478. package/manifests/mongodb/pv-pvc.yaml +0 -59
  479. package/scripts/link-local-underpost-cli.sh +0 -6
  480. package/src/api/user/user.build.js +0 -16
  481. package/src/cli/env.js +0 -177
  482. package/src/client/components/core/Blockchain.js +0 -41
  483. package/src/client/components/core/Blog.js +0 -9
  484. package/src/client/components/core/KeyboardAvoidance.js +0 -145
  485. package/src/client/public/default/assets/background/dark.jpg +0 -0
  486. package/src/client/public/default/assets/background/white0-min.jpg +0 -0
  487. package/src/client/public/default/assets/background/white0.jpg +0 -0
  488. package/src/client/public/doc/favicon.ico +0 -0
  489. package/src/client/public/doc/sitemap +0 -148
  490. package/src/server/cron.js +0 -476
@@ -0,0 +1,623 @@
1
+ /**
2
+ * Vultr bandwidth guard for the edge hub.
3
+ *
4
+ * The edge VPS is the only machine in the topology that pays for traffic: every
5
+ * byte a client receives leaves through it, and Vultr bills the overage per GB
6
+ * once the plan's monthly transfer quota is spent. Nothing in the request path
7
+ * knows how much of that quota is left — HAProxy forwards bytes it never
8
+ * counts, and the spokes behind the tunnel cannot see the meter at all.
9
+ *
10
+ * This module is that meter. It reads the instance's consumption from the Vultr
11
+ * API, compares it against the plan's quota, and — once a configured fraction of
12
+ * it is gone — reaches the VPS over SSH and drops its egress with
13
+ * {@link module:src/server/network/dns.js}'s `blockAllEgress`.
14
+ *
15
+ * That last step is deliberately blunt: it takes every hostname behind the hub
16
+ * offline. It is the cheaper failure. An overage accrues silently and without a
17
+ * ceiling, while a blocked edge is loud, immediate, and reversible with one
18
+ * command. `blockAllEgress` keeps established and related connections, so a new
19
+ * inbound SSH session still completes its handshake and the host stays
20
+ * reachable to undo it.
21
+ *
22
+ * The enforcement is latched in the host store rather than re-applied every run,
23
+ * so a cron firing every ten minutes does not re-open an SSH session to a host
24
+ * that is already blocked.
25
+ *
26
+ * @module src/cli/vultr.js
27
+ * @namespace UnderpostVultr
28
+ */
29
+
30
+ import axios from 'axios';
31
+ import { environmentValueFactory } from '../server/runtime/environment.js';
32
+ import { FORWARD_PROXY, fetchViaForwardProxy } from '../server/network/forward-proxy.js';
33
+ import fs from 'fs-extra';
34
+ import nodePath from 'node:path';
35
+ import { loggerFactory } from '../server/ops/logger.js';
36
+ import { UNDERPOST_MONITORING } from '../server/ops/monitoring.js';
37
+ import Underpost from '../index.js';
38
+
39
+ const logger = loggerFactory(import.meta);
40
+
41
+ /**
42
+ * @constant UNDERPOST_VULTR
43
+ * @description Fixed identity of the bandwidth guard: API surface, defaults, and
44
+ * the key the enforcement state is latched under.
45
+ * @memberof UnderpostVultr
46
+ */
47
+ const UNDERPOST_VULTR = {
48
+ apiBaseUrl: 'https://api.vultr.com/v2',
49
+ // Vultr quotes plan `bandwidth` in GB and usage in bytes, so one of the two
50
+ // has to be converted. GB here is binary, matching how the quota is metered.
51
+ bytesPerGB: 1024 * 1024 * 1024,
52
+ defaultThreshold: 0.8,
53
+ // The documented maximum; fewer pages means fewer round trips before the
54
+ // instance's plan is found.
55
+ plansPerPage: 500,
56
+ // A cursor loop bounded so a malformed `meta.links.next` cannot spin forever.
57
+ maxPlanPages: 20,
58
+ requestTimeoutMs: 20000,
59
+ defaultSshUser: 'root',
60
+ defaultSshPort: 22,
61
+ remoteEnginePath: '/home/dd/engine',
62
+ // latched in the host store, which the CronJob mounts from the host, so the
63
+ // decision survives the container that made it.
64
+ latchKey: 'VULTR_EGRESS_BLOCKED_AT',
65
+ env: {
66
+ apiKey: 'VULTR_API_KEY',
67
+ instanceId: 'VULTR_INSTANCE_ID',
68
+ threshold: 'VULTR_BANDWIDTH_THRESHOLD',
69
+ host: ['VULTR_VPS_IP', 'DEFAULT_SSH_HOST'],
70
+ user: ['VULTR_SSH_USER', 'DEFAULT_SSH_USER'],
71
+ keyPath: ['VULTR_SSH_KEY_PATH', 'DEFAULT_SSH_KEY_PATH'],
72
+ port: ['VULTR_SSH_PORT', 'DEFAULT_SSH_PORT'],
73
+ // The edge hub's forward proxy, named once in its canonical module so the two
74
+ // ends of it cannot disagree about the variable that configures it.
75
+ forwardProxyApiKey: FORWARD_PROXY.env.apiKey,
76
+ forwardProxyHost: FORWARD_PROXY.env.host,
77
+ forwardProxyPort: FORWARD_PROXY.env.port,
78
+ },
79
+ };
80
+
81
+ /**
82
+ * @method envFactory
83
+ * @description First non-empty value among a list of keys.
84
+ *
85
+ * Resolution is {@link module:src/server/runtime/environment.js.environmentValueFactory}'s — the process
86
+ * environment, then the deploy env `underpost app load --env <environment>`
87
+ * selects into `./.env`, then the host store — rather than a second
88
+ * implementation of it, because the three callers differ: a CronJob container has
89
+ * its deploy env loaded into `process.env` by `loadCronDeployEnv`, an operator
90
+ * preparing a manual run has `./.env`, and an operator who ran `underpost host
91
+ * set` has the host store. Never logged — one of the keys this resolves is an API
92
+ * key.
93
+ * @param {string|Array<string>} keys - Environment variable name, or names in precedence order.
94
+ * @returns {string} The resolved value, or an empty string.
95
+ * @memberof UnderpostVultr
96
+ */
97
+ const envFactory = (keys) => {
98
+ for (const key of Array.isArray(keys) ? keys : [keys]) {
99
+ const value = environmentValueFactory(key);
100
+ if (value) return value;
101
+ }
102
+ return '';
103
+ };
104
+
105
+ /**
106
+ * @method thresholdFactory
107
+ * @description Normalizes the configured trigger fraction.
108
+ *
109
+ * `80` and `0.80` are the same intent expressed two ways, and the first one
110
+ * silently never fires — a guard that never fires is worse than no guard, since
111
+ * it reads as protection. Both are accepted and resolve to the same ratio.
112
+ * @param {string|number} [raw] - Configured value.
113
+ * @param {number} [fallback] - Ratio used when nothing usable is configured.
114
+ * @returns {number} Ratio in `(0, 1]`.
115
+ * @memberof UnderpostVultr
116
+ */
117
+ const thresholdFactory = (raw, fallback = UNDERPOST_VULTR.defaultThreshold) => {
118
+ const parsed = parseFloat(`${raw ?? ''}`.trim());
119
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
120
+ const ratio = parsed > 1 ? parsed / 100 : parsed;
121
+ return ratio > 1 ? 1 : ratio;
122
+ };
123
+
124
+ /**
125
+ * @method billingMonthFactory
126
+ * @description The `YYYY-MM` prefix the daily buckets of the current cycle carry.
127
+ *
128
+ * UTC, because the endpoint's day boundaries are UTC. A host in a negative
129
+ * offset would otherwise drop the current day for part of its evening.
130
+ * @param {Date} [now] - Reference instant.
131
+ * @returns {string} `YYYY-MM`.
132
+ * @memberof UnderpostVultr
133
+ */
134
+ const billingMonthFactory = (now = new Date()) =>
135
+ `${now.getUTCFullYear()}-${`${now.getUTCMonth() + 1}`.padStart(2, '0')}`;
136
+
137
+ /**
138
+ * @method bandwidthTotalsFactory
139
+ * @description Folds the endpoint's daily buckets into one consumption figure.
140
+ *
141
+ * Scoped to the current month by default rather than summing every key
142
+ * returned. The response is a rolling window that can still carry the tail of
143
+ * the previous cycle, and those bytes are against a quota that has already
144
+ * reset — counting them reports a host as over budget on the first days of a
145
+ * month when it has barely spent anything.
146
+ *
147
+ * Both directions are returned separately because which of them is billable is
148
+ * a property of the account's plan, not of this code: `total` is the
149
+ * conservative reading and trips first, `outgoing` is the one that maps to
150
+ * egress alone.
151
+ * @param {object} [bandwidth] - `bandwidth` object from the Vultr response.
152
+ * @param {string} [month] - `YYYY-MM` to scope to; empty sums every bucket.
153
+ * @returns {{totalBytes: number, incomingBytes: number, outgoingBytes: number, days: number, dates: Array<string>}} Consumption for the window.
154
+ * @memberof UnderpostVultr
155
+ */
156
+ const bandwidthTotalsFactory = ({ bandwidth = {}, month = billingMonthFactory() } = {}) => {
157
+ const dates = Object.keys(bandwidth || {})
158
+ .filter((date) => !month || `${date}`.startsWith(month))
159
+ .sort();
160
+ let incomingBytes = 0;
161
+ let outgoingBytes = 0;
162
+ for (const date of dates) {
163
+ const bucket = bandwidth[date] || {};
164
+ incomingBytes += Number(bucket.incoming_bytes) || 0;
165
+ outgoingBytes += Number(bucket.outgoing_bytes) || 0;
166
+ }
167
+ return { totalBytes: incomingBytes + outgoingBytes, incomingBytes, outgoingBytes, days: dates.length, dates };
168
+ };
169
+
170
+ /**
171
+ * @method quotaStateFactory
172
+ * @description The consumption decision: quota, effective trigger, and whether
173
+ * it has been crossed.
174
+ *
175
+ * A plan with no quota (`bandwidth: 0`, which Vultr uses for unmetered plans)
176
+ * yields `exceeded: false` and is reported as such rather than dividing by zero
177
+ * and blocking a host that cannot run out.
178
+ * @param {number} consumedBytes - Bytes counted for the cycle.
179
+ * @param {number} planBandwidthGB - Plan quota in GB.
180
+ * @param {number} [threshold] - Trigger fraction of the quota.
181
+ * @returns {{maxBytes: number, limitBytes: number, consumedBytes: number, ratio: number, exceeded: boolean, metered: boolean}} Decision inputs and outcome.
182
+ * @memberof UnderpostVultr
183
+ */
184
+ const quotaStateFactory = ({
185
+ consumedBytes = 0,
186
+ planBandwidthGB = 0,
187
+ threshold = UNDERPOST_VULTR.defaultThreshold,
188
+ }) => {
189
+ const maxBytes = Math.max(0, Number(planBandwidthGB) || 0) * UNDERPOST_VULTR.bytesPerGB;
190
+ const limitBytes = maxBytes * threshold;
191
+ const metered = maxBytes > 0;
192
+ return {
193
+ maxBytes,
194
+ limitBytes,
195
+ consumedBytes,
196
+ ratio: metered ? consumedBytes / maxBytes : 0,
197
+ exceeded: metered && consumedBytes >= limitBytes,
198
+ metered,
199
+ };
200
+ };
201
+
202
+ /**
203
+ * @method formatBytes
204
+ * @description Byte count as GB, for log lines an operator has to read against
205
+ * a plan quota that is quoted in GB.
206
+ * @param {number} bytes - Byte count.
207
+ * @returns {string} e.g. `812.40 GB`.
208
+ * @memberof UnderpostVultr
209
+ */
210
+ const formatBytes = (bytes) => `${((Number(bytes) || 0) / UNDERPOST_VULTR.bytesPerGB).toFixed(2)} GB`;
211
+
212
+ /**
213
+ * @method vultrUrlFactory
214
+ * @description One absolute API URL, with its query already encoded.
215
+ *
216
+ * Built here rather than left to the transport because the two transports below
217
+ * take a query differently — axios takes `params`, the proxy client takes a URL —
218
+ * and a request that differs by transport is a bug waiting for the day the
219
+ * fallback is used.
220
+ * @param {string} path - Path below `/v2`.
221
+ * @param {object} [params] - Query parameters.
222
+ * @returns {string} Absolute URL.
223
+ * @memberof UnderpostVultr
224
+ */
225
+ const vultrUrlFactory = ({ path, params = {} }) => {
226
+ const url = new URL(`${UNDERPOST_VULTR.apiBaseUrl}${path}`);
227
+ for (const [key, value] of Object.entries(params))
228
+ if (value !== undefined && value !== null && `${value}` !== '') url.searchParams.set(key, `${value}`);
229
+ return url.href;
230
+ };
231
+
232
+ /**
233
+ * @method vultrGet
234
+ * @description The transport: through the edge hub's forward proxy when one is
235
+ * configured, straight out otherwise.
236
+ *
237
+ * The proxy is the point of this indirection. Vultr's API sees the address the
238
+ * request came from, and this job usually runs in a CronJob inside a homelab
239
+ * cluster — so a direct call arrives from a residential ISP address, while a
240
+ * proxied one arrives from the very VPS the job is metering. An API key scoped to
241
+ * the edge's address only works over the proxy.
242
+ *
243
+ * Non-2xx answers are returned rather than thrown, so both transports report a
244
+ * status the same way; only a transport failure throws.
245
+ * @param {string} url - Absolute URL.
246
+ * @param {string} apiKey - Vultr API key.
247
+ * @param {object} [proxy] - Forward proxy endpoint; ignored when it carries no key.
248
+ * @returns {Promise<{status: number, data: object}>} Status and parsed body.
249
+ * @memberof UnderpostVultr
250
+ */
251
+ const vultrGet = async ({ url, apiKey, proxy }) => {
252
+ const headers = { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' };
253
+ if (!proxy?.apiKey) {
254
+ const response = await axios.get(url, {
255
+ headers,
256
+ timeout: UNDERPOST_VULTR.requestTimeoutMs,
257
+ validateStatus: () => true,
258
+ });
259
+ return { status: response.status, data: response.data };
260
+ }
261
+ const { status, body } = await fetchViaForwardProxy(url, {
262
+ headers,
263
+ timeout: UNDERPOST_VULTR.requestTimeoutMs,
264
+ proxy,
265
+ });
266
+ try {
267
+ return { status, data: body ? JSON.parse(body) : {} };
268
+ } catch {
269
+ return { status, data: {} };
270
+ }
271
+ };
272
+
273
+ /**
274
+ * @method vultrRequest
275
+ * @description One authenticated Vultr API call.
276
+ *
277
+ * Errors are re-thrown with Vultr's own message and without the request config,
278
+ * because an axios error carries the `Authorization` header it was sent with and
279
+ * this module's failures are logged.
280
+ * @param {string} apiKey - Vultr API key.
281
+ * @param {string} path - Path below `/v2`.
282
+ * @param {object} [params] - Query parameters.
283
+ * @param {object} [proxy] - Forward proxy endpoint from {@link UnderpostVultr.resolveConfig}.
284
+ * @returns {Promise<object>} Response body.
285
+ * @throws {Error} With the API's status and message, never the credentials.
286
+ * @memberof UnderpostVultr
287
+ */
288
+ const vultrRequest = async ({ apiKey, path, params = {}, proxy = null }) => {
289
+ let status = 0;
290
+ let data = {};
291
+ try {
292
+ ({ status, data } = await vultrGet({ url: vultrUrlFactory({ path, params }), apiKey, proxy }));
293
+ } catch (error) {
294
+ const responseStatus = error?.response?.status;
295
+ const detail = error?.response?.data?.error || error?.message || 'request failed';
296
+ const code = error?.code || error?.cause?.code;
297
+ const proxyHint =
298
+ proxy?.apiKey && ['EHOSTUNREACH', 'ENETUNREACH', 'ECONNREFUSED', 'ETIMEDOUT'].includes(code)
299
+ ? '; verify wg0 on the node; if only pods fail, re-run --wireguard-setup and restart wg0; if the host also fails, re-run --forward-proxy-server on the hub'
300
+ : '';
301
+ throw new Error(
302
+ `[vultr] GET /v2${path} failed${responseStatus ? ` (${responseStatus})` : ''}: ${detail}${proxyHint}`,
303
+ );
304
+ }
305
+ if (status < 200 || status >= 300)
306
+ throw new Error(`[vultr] GET /v2${path} failed (${status}): ${data?.error || 'request failed'}`);
307
+ return data;
308
+ };
309
+
310
+ /**
311
+ * @method planBandwidthGBFactory
312
+ * @description The monthly transfer quota, in GB, of the plan an instance runs.
313
+ *
314
+ * `/v2/plans` is paginated and the catalogue is longer than one page, so the
315
+ * cursor is followed rather than the first page searched — a plan that happens
316
+ * to sort past the page boundary would otherwise read as "not found" and, on a
317
+ * naive implementation, as a quota of zero.
318
+ * @param {string} apiKey - Vultr API key.
319
+ * @param {string} planId - Plan id from the instance record.
320
+ * @param {object} [proxy] - Forward proxy endpoint.
321
+ * @returns {Promise<number>} Quota in GB.
322
+ * @throws {Error} When the plan is absent from the catalogue.
323
+ * @memberof UnderpostVultr
324
+ */
325
+ const planBandwidthGBFactory = async ({ apiKey, planId, proxy = null }) => {
326
+ let cursor = '';
327
+ for (let page = 0; page < UNDERPOST_VULTR.maxPlanPages; page++) {
328
+ const data = await vultrRequest({
329
+ apiKey,
330
+ path: '/plans',
331
+ params: { type: 'all', per_page: UNDERPOST_VULTR.plansPerPage, ...(cursor ? { cursor } : {}) },
332
+ proxy,
333
+ });
334
+ const match = (data?.plans || []).find((plan) => plan?.id === planId);
335
+ if (match) return Number(match.bandwidth) || 0;
336
+ cursor = `${data?.meta?.links?.next || ''}`.trim();
337
+ if (!cursor) break;
338
+ }
339
+ throw new Error(`[vultr] Plan ${planId} was not found in the plan catalogue; cannot resolve its bandwidth quota`);
340
+ };
341
+
342
+ /**
343
+ * @class UnderpostVultr
344
+ * @description Bandwidth accounting for the edge VPS, and the egress cut-off it
345
+ * triggers.
346
+ * @memberof UnderpostVultr
347
+ */
348
+ class UnderpostVultr {
349
+ static API = {
350
+ /**
351
+ * @method callback
352
+ * @description CLI and cron entry point.
353
+ *
354
+ * Takes the same `(deployList, options)` shape every other
355
+ * {@link UnderpostCron} job does, so `underpost cron default vultr` dispatches
356
+ * to it unchanged. The deploy list is not used to select an instance — the
357
+ * edge hub is one machine for the whole cluster, exactly as its WireGuard
358
+ * WireGuard topology is — but it is logged so a run is attributable.
359
+ * @param {string} [deployList] - Comma-separated deploy ids, from the cron dispatcher.
360
+ * @param {object} [options] - CLI flags.
361
+ * @returns {Promise<object>} Result from {@link UnderpostVultr.checkBandwidth}.
362
+ * @memberof UnderpostVultr
363
+ */
364
+ callback: async function (deployList = 'default', options = {}) {
365
+ return await UnderpostVultr.API.checkBandwidth({ ...options, deployList });
366
+ },
367
+
368
+ /**
369
+ * @method publishBandwidthMetrics
370
+ * @description Publishes the measured quota so the cluster can read it
371
+ * without calling Vultr.
372
+ *
373
+ * The API is rate limited and needs a credential, neither of which belongs
374
+ * on an alert evaluation path, so the guard — which already holds the
375
+ * numbers — writes them where the host collector picks them up, and to the
376
+ * host configuration store for anything that reads configuration rather than metrics.
377
+ * The file is renamed into place because the collector may read it mid-write.
378
+ * @param {{consumedBytes: number, maxBytes: number}} state - Measured quota state.
379
+ * @returns {boolean} True when the metrics file was written.
380
+ * @memberof UnderpostVultr
381
+ */
382
+ publishBandwidthMetrics({ consumedBytes = 0, maxBytes = 0 } = {}) {
383
+ Underpost.host.store.set('VULTR_BANDWIDTH_USAGE_BYTES', `${consumedBytes}`);
384
+ Underpost.host.store.set('VULTR_BANDWIDTH_LIMIT_BYTES', `${maxBytes}`);
385
+
386
+ const target = `${UNDERPOST_MONITORING.nodeExporter.textfileDirectory}/vultr_bandwidth.prom`;
387
+ const content =
388
+ '# HELP vultr_bandwidth_used_bytes Monthly bandwidth consumed by the edge hub.\n' +
389
+ '# TYPE vultr_bandwidth_used_bytes gauge\n' +
390
+ `vultr_bandwidth_used_bytes ${consumedBytes}\n` +
391
+ '# HELP vultr_bandwidth_limit_bytes Monthly bandwidth quota of the edge hub plan.\n' +
392
+ '# TYPE vultr_bandwidth_limit_bytes gauge\n' +
393
+ `vultr_bandwidth_limit_bytes ${maxBytes}\n`;
394
+ try {
395
+ fs.mkdirpSync(nodePath.dirname(target));
396
+ fs.writeFileSync(`${target}.tmp`, content, 'utf8');
397
+ fs.renameSync(`${target}.tmp`, target);
398
+ return true;
399
+ } catch (error) {
400
+ logger.warn('Bandwidth metrics not published to the host collector', { target, error: `${error.message}` });
401
+ return false;
402
+ }
403
+ },
404
+
405
+ /**
406
+ * @method resolveConfig
407
+ * @description Every input the guard needs, resolved once.
408
+ *
409
+ * CLI flags win over the environment so a one-off run can target another
410
+ * instance without editing any `.env`; the environment is the standing
411
+ * configuration. Single source of truth for the resolution, so no other
412
+ * method re-reads `process.env`.
413
+ * @param {object} [options] - CLI flags.
414
+ * @returns {object} Resolved configuration; `apiKey` is present but must never be logged.
415
+ * @memberof UnderpostVultr
416
+ */
417
+ resolveConfig(options = {}) {
418
+ return {
419
+ apiKey: `${options.apiKey || ''}`.trim() || envFactory(UNDERPOST_VULTR.env.apiKey),
420
+ instanceId: `${options.instanceId || ''}`.trim() || envFactory(UNDERPOST_VULTR.env.instanceId),
421
+ threshold: thresholdFactory(options.threshold || envFactory(UNDERPOST_VULTR.env.threshold)),
422
+ host: `${options.host || ''}`.trim() || envFactory(UNDERPOST_VULTR.env.host),
423
+ user: `${options.user || ''}`.trim() || envFactory(UNDERPOST_VULTR.env.user) || UNDERPOST_VULTR.defaultSshUser,
424
+ keyPath: Underpost.ssh.keyPathFactory(
425
+ `${options.keyPath || ''}`.trim() || envFactory(UNDERPOST_VULTR.env.keyPath),
426
+ ),
427
+ port: Number(options.port || envFactory(UNDERPOST_VULTR.env.port)) || UNDERPOST_VULTR.defaultSshPort,
428
+ // `total` is the conservative reading and trips first; `outgoing` counts
429
+ // egress alone, which is what a plan billing outbound-only meters.
430
+ metric: `${options.metric || 'total'}`.trim() === 'outgoing' ? 'outgoing' : 'total',
431
+ month: options.allDates === true ? '' : `${options.month || ''}`.trim() || billingMonthFactory(),
432
+ dryRun: options.dryRun === true,
433
+ force: options.force === true,
434
+ autoUnblock: options.autoUnblock === true,
435
+ // Resolved through the same env precedence as everything else, so the
436
+ // cron's deploy env can enable the proxy without a flag. An unset key
437
+ // means no proxy, and the API is called directly.
438
+ forwardProxy: {
439
+ apiKey: envFactory(UNDERPOST_VULTR.env.forwardProxyApiKey),
440
+ host: envFactory(UNDERPOST_VULTR.env.forwardProxyHost),
441
+ port: envFactory(UNDERPOST_VULTR.env.forwardProxyPort),
442
+ },
443
+ };
444
+ },
445
+
446
+ /**
447
+ * @method checkBandwidth
448
+ * @description Reads the instance's consumption for the cycle and enforces
449
+ * the threshold.
450
+ *
451
+ * Three calls, in the only order that works: the instance record names its
452
+ * plan, the plan carries the quota, and the bandwidth endpoint carries the
453
+ * consumption. Nothing is enforced until all three have answered — a guard
454
+ * that blocked an edge on a failed API call would be an outage caused by the
455
+ * thing meant to prevent one.
456
+ * @param {object} [options] - CLI flags; see {@link UnderpostVultr.resolveConfig}.
457
+ * @returns {Promise<{instanceId: string, plan: string, metric: string, month: string, consumedBytes: number, limitBytes: number, maxBytes: number, ratio: number, exceeded: boolean, enforced: boolean, latched: boolean}>} What was measured and what was done about it.
458
+ * @throws {Error} When credentials are missing or the API cannot be read.
459
+ * @memberof UnderpostVultr
460
+ */
461
+ checkBandwidth: async function (options = {}) {
462
+ const config = UnderpostVultr.API.resolveConfig(options);
463
+ if (!config.apiKey) throw new Error(`[vultr] ${UNDERPOST_VULTR.env.apiKey} is not set`);
464
+ if (!config.instanceId) throw new Error(`[vultr] ${UNDERPOST_VULTR.env.instanceId} is not set`);
465
+
466
+ const proxy = config.forwardProxy;
467
+ const instance = (await vultrRequest({ apiKey: config.apiKey, path: `/instances/${config.instanceId}`, proxy }))
468
+ ?.instance;
469
+ const planId = `${instance?.plan || ''}`.trim();
470
+ if (!planId) throw new Error(`[vultr] Instance ${config.instanceId} returned no plan id`);
471
+
472
+ const planBandwidthGB = await planBandwidthGBFactory({ apiKey: config.apiKey, planId, proxy });
473
+ const { bandwidth } = await vultrRequest({
474
+ apiKey: config.apiKey,
475
+ path: `/instances/${config.instanceId}/bandwidth`,
476
+ proxy,
477
+ });
478
+
479
+ const totals = bandwidthTotalsFactory({ bandwidth, month: config.month });
480
+ const consumedBytes = config.metric === 'outgoing' ? totals.outgoingBytes : totals.totalBytes;
481
+ const state = quotaStateFactory({ consumedBytes, planBandwidthGB, threshold: config.threshold });
482
+ UnderpostVultr.API.publishBandwidthMetrics(state);
483
+ const latchedAt =
484
+ `${Underpost.host.store.get(UNDERPOST_VULTR.latchKey, undefined, { disableLog: true }) ?? ''}`.trim();
485
+
486
+ const summary = {
487
+ instanceId: config.instanceId,
488
+ plan: planId,
489
+ metric: config.metric,
490
+ month: config.month || 'all-dates',
491
+ days: totals.days,
492
+ consumed: formatBytes(consumedBytes),
493
+ incoming: formatBytes(totals.incomingBytes),
494
+ outgoing: formatBytes(totals.outgoingBytes),
495
+ triggerLimit: formatBytes(state.limitBytes),
496
+ planQuota: formatBytes(state.maxBytes),
497
+ usedPercent: `${(state.ratio * 100).toFixed(1)}%`,
498
+ threshold: `${(config.threshold * 100).toFixed(0)}%`,
499
+ // Which address Vultr saw the reads come from, which is the difference
500
+ // between a key scoped to the edge VPS working and failing.
501
+ via: proxy.apiKey ? `forward-proxy ${proxy.host || FORWARD_PROXY.defaultHost}` : 'direct',
502
+ };
503
+
504
+ if (!state.metered) {
505
+ logger.info('Vultr plan reports no metered bandwidth quota; nothing to enforce', summary);
506
+ return { ...state, ...summary, enforced: false, latched: !!latchedAt };
507
+ }
508
+
509
+ if (!state.exceeded) {
510
+ logger.info('Vultr bandwidth within budget', summary);
511
+ // A latch that outlives the cycle it was set in would suppress the next
512
+ // real trigger, so it is cleared as soon as usage is back under the
513
+ // threshold — but the host stays blocked until someone says otherwise.
514
+ if (latchedAt) {
515
+ if (!config.dryRun) Underpost.host.store.delete(UNDERPOST_VULTR.latchKey);
516
+ if (config.autoUnblock) await UnderpostVultr.API.setEdgeEgress({ config, blocked: false });
517
+ else
518
+ logger.warn('Edge egress is still blocked from a previous cycle; unblock it when you are ready', {
519
+ host: config.host,
520
+ blockedAt: latchedAt,
521
+ next: `underpost ip --unblock-all-egress (on ${config.host || 'the edge VPS'})`,
522
+ });
523
+ }
524
+ return { ...state, ...summary, enforced: false, latched: false };
525
+ }
526
+
527
+ logger.warn('Vultr bandwidth threshold reached; edge egress will be blocked', summary);
528
+
529
+ if (latchedAt && !config.force) {
530
+ logger.info('Edge egress was already blocked for this cycle; not re-applying', {
531
+ host: config.host,
532
+ blockedAt: latchedAt,
533
+ next: 'pass --force to re-apply',
534
+ });
535
+ return { ...state, ...summary, enforced: false, latched: true };
536
+ }
537
+
538
+ const enforced = await UnderpostVultr.API.setEdgeEgress({ config, blocked: true });
539
+ if (enforced && !config.dryRun) Underpost.host.store.set(UNDERPOST_VULTR.latchKey, new Date().toISOString());
540
+ return { ...state, ...summary, enforced, latched: enforced };
541
+ },
542
+
543
+ /**
544
+ * @method setEdgeEgress
545
+ * @description Blocks or restores outbound traffic on the edge VPS over SSH.
546
+ *
547
+ * The command is run on the VPS rather than here because the nftables rules
548
+ * belong to that host — this process usually runs in a CronJob container
549
+ * inside a spoke cluster, on the far side of the tunnel the rules govern.
550
+ *
551
+ * `underpost ip` is preferred when the CLI is installed globally and the
552
+ * checked-out engine is the fallback, so a VPS provisioned either way is
553
+ * reachable. The command already elevates internally, so it is not wrapped
554
+ * in `sudo` here.
555
+ * @param {object} config - Resolved configuration from {@link UnderpostVultr.resolveConfig}.
556
+ * @param {boolean} blocked - True to block egress, false to restore it.
557
+ * @returns {Promise<boolean>} True when the remote command succeeded.
558
+ * @memberof UnderpostVultr
559
+ */
560
+ setEdgeEgress: async function ({ config, blocked }) {
561
+ const flag = blocked ? '--block-all-egress' : '--unblock-all-egress';
562
+ if (!config.host) {
563
+ logger.error('No edge host configured; cannot reach the VPS to change its egress', {
564
+ set: UNDERPOST_VULTR.env.host.join(' or '),
565
+ });
566
+ return false;
567
+ }
568
+ const command = [
569
+ 'set -e',
570
+ 'if command -v underpost >/dev/null 2>&1; then',
571
+ ` underpost ip ${flag}`,
572
+ 'else',
573
+ ` cd ${UNDERPOST_VULTR.remoteEnginePath} && node bin ip ${flag}`,
574
+ 'fi',
575
+ ].join('\n');
576
+
577
+ if (config.dryRun) {
578
+ logger.info('[dry-run] would run on the edge VPS', {
579
+ target: `${config.user}@${config.host}:${config.port}`,
580
+ command: `underpost ip ${flag}`,
581
+ });
582
+ return false;
583
+ }
584
+
585
+ const result = await Underpost.ssh.sshExecBatch({
586
+ host: config.host,
587
+ port: config.port,
588
+ user: config.user,
589
+ keyPath: config.keyPath,
590
+ command,
591
+ });
592
+ if (!result.ok) {
593
+ logger.error(`Failed to run 'underpost ip ${flag}' on the edge VPS`, {
594
+ target: `${config.user}@${config.host}:${config.port}`,
595
+ code: result.code,
596
+ stderr: `${result.stderr || ''}`.slice(-400),
597
+ });
598
+ return false;
599
+ }
600
+ if (blocked)
601
+ logger.warn('Edge egress blocked; every hostname behind the hub is now offline', {
602
+ host: config.host,
603
+ restore: `underpost ip --unblock-all-egress (on ${config.host})`,
604
+ });
605
+ else logger.info('Edge egress restored', { host: config.host });
606
+ return true;
607
+ },
608
+ };
609
+ }
610
+
611
+ export default UnderpostVultr;
612
+
613
+ export {
614
+ UNDERPOST_VULTR,
615
+ bandwidthTotalsFactory,
616
+ billingMonthFactory,
617
+ envFactory,
618
+ formatBytes,
619
+ planBandwidthGBFactory,
620
+ quotaStateFactory,
621
+ thresholdFactory,
622
+ vultrUrlFactory,
623
+ };