@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
package/src/cli/fs.js CHANGED
@@ -5,12 +5,12 @@
5
5
  */
6
6
 
7
7
  import { v2 as cloudinary } from 'cloudinary';
8
- import { loggerFactory } from '../server/logger.js';
8
+ import { loggerFactory } from '../server/ops/logger.js';
9
9
  import AdmZip from 'adm-zip';
10
10
  import * as dir from 'path';
11
11
  import fs from 'fs-extra';
12
- import Downloader from '../server/downloader.js';
13
- import { shellExec } from '../server/process.js';
12
+ import Downloader from '../server/storage/downloader.js';
13
+ import { shellExec } from '../server/runtime/process.js';
14
14
  import Underpost from '../index.js';
15
15
 
16
16
  const logger = loggerFactory(import.meta);
@@ -0,0 +1,672 @@
1
+ /**
2
+ * Host domain: the complete node-level operational environment.
3
+ *
4
+ * Its durable source is the cron deploy's env file — SSH, registry, DNS, mail, cluster and
5
+ * deployment settings — resolved through `engine-private/deploy/dd.cron`, the same id the cron
6
+ * jobs run against, so this is the one configuration the whole cluster shares. Its local runtime
7
+ * is {@link UnderpostHost.API.store}, the host configuration store this domain owns end to end.
8
+ *
9
+ * Implements the canonical domain action set; see {@link UnderpostDomains.DOMAIN_ACTIONS}.
10
+ * @module src/cli/host.js
11
+ * @namespace UnderpostHost
12
+ */
13
+
14
+ import dotenv from 'dotenv';
15
+ import fs from 'fs-extra';
16
+
17
+ import nodePath from 'node:path';
18
+
19
+ import {
20
+ CONFIG_SCOPES,
21
+ classifyConfigKeys,
22
+ configOwnershipFactory,
23
+ configRejectionFactory,
24
+ } from '../server/runtime/config-scope.js';
25
+ import { cronDeployIdResolve } from '../server/ops/cron.js';
26
+ import { domainContextFactory } from './domains.js';
27
+ import { dotenvStoreFactory } from './dotenv-store.js';
28
+ import { getUnderpostRootPath, writeEnv } from '../server/runtime/environment.js';
29
+ import { loggerFactory } from '../server/ops/logger.js';
30
+ import { shellExec } from '../server/runtime/process.js';
31
+ import Underpost from '../index.js';
32
+
33
+ const logger = loggerFactory(import.meta);
34
+
35
+ const UNDERPOST_CONFIG_SECRET = 'underpost-config';
36
+ const DEFAULT_CRON_DEPLOY_ID = 'dd-cron';
37
+ const SCOPES_DIR = './engine-private/deploy/scopes';
38
+
39
+ // Shell- and Kubernetes-critical keys that must never round-trip through a published Secret:
40
+ // an injected PATH overrides the container image's own and breaks coreutils resolution in the
41
+ // pod. NODE_ENV is deliberately exempt — it is the deployment environment this domain exists to
42
+ // carry, and stripping it made every deploy fall back to `development`.
43
+ const RESERVED_ENV_KEYS = new Set([
44
+ 'HOME',
45
+ 'HOSTNAME',
46
+ 'PATH',
47
+ 'TERM',
48
+ 'SHLVL',
49
+ 'PWD',
50
+ '_',
51
+ 'LANG',
52
+ 'LANGUAGE',
53
+ 'LC_ALL',
54
+ 'container',
55
+ 'SHELL',
56
+ 'USER',
57
+ 'LOGNAME',
58
+ 'MAIL',
59
+ 'OLDPWD',
60
+ 'LESSOPEN',
61
+ 'LESSCLOSE',
62
+ 'LS_COLORS',
63
+ 'DISPLAY',
64
+ 'COLORTERM',
65
+ 'EDITOR',
66
+ 'VISUAL',
67
+ 'TERM_PROGRAM',
68
+ 'TERM_PROGRAM_VERSION',
69
+ 'SSH_AUTH_SOCK',
70
+ 'SSH_CLIENT',
71
+ 'SSH_CONNECTION',
72
+ 'SSH_TTY',
73
+ 'XDG_SESSION_ID',
74
+ 'XDG_RUNTIME_DIR',
75
+ 'XDG_DATA_DIRS',
76
+ 'XDG_CONFIG_DIRS',
77
+ 'DBUS_SESSION_BUS_ADDRESS',
78
+ 'GPG_AGENT_INFO',
79
+ 'WINDOWID',
80
+ 'DESKTOP_SESSION',
81
+ 'SESSION_MANAGER',
82
+ 'XAUTHORITY',
83
+ 'WAYLAND_DISPLAY',
84
+ 'which_declare',
85
+ ]);
86
+ const RESERVED_ENV_KEY_PREFIXES = ['KUBERNETES_', 'npm_', 'NODE_'];
87
+ const PRESERVED_ENV_KEYS = new Set(['NODE_ENV']);
88
+
89
+ /**
90
+ * Whether a key must be kept out of a published Secret and out of the host configuration store.
91
+ * @param {string} key - Environment variable name.
92
+ * @returns {boolean} True when the key is reserved.
93
+ * @memberof UnderpostHost
94
+ */
95
+ const isReservedEnvKey = (key) =>
96
+ !PRESERVED_ENV_KEYS.has(key) &&
97
+ (RESERVED_ENV_KEYS.has(key) || RESERVED_ENV_KEY_PREFIXES.some((prefix) => key.startsWith(prefix)));
98
+
99
+ /**
100
+ * @class UnderpostHost
101
+ * @description Host/global infrastructure configuration domain.
102
+ * @memberof UnderpostHost
103
+ */
104
+ class UnderpostHost {
105
+ static API = {
106
+ /**
107
+ * The host configuration store: a single host-scoped dotenv file next to the global installation,
108
+ * holding this node's resolved configuration.
109
+ *
110
+ * This domain's local runtime, so it lives here rather than in a module of its own: `load`
111
+ * rebuilds it from the durable source, `publish` reads it back out, `clean` withdraws it.
112
+ * Key-level access is `underpost host get|set|delete|list`, registered from
113
+ * {@link UnderpostDomains.DOMAIN_STORE_OPERATORS} — a different interaction shape than the
114
+ * canonical seven actions and deliberately outside them, but the same domain.
115
+ *
116
+ * Container runtime status is a different concern with a different lifetime — see
117
+ * {@link UnderpostState}.
118
+ * @type {ReturnType<typeof dotenvStoreFactory>}
119
+ * @memberof UnderpostHost
120
+ */
121
+ store: dotenvStoreFactory({
122
+ path: () => `${getUnderpostRootPath()}/.env`,
123
+ label: 'host configuration',
124
+ }),
125
+
126
+ // ── key-level store operators ───────────────────────────────────────────────────────────
127
+ // Delegates rather than aliases, so the command surface addresses this domain and never the
128
+ // store object directly. Container status is not among them: that key belongs to the state
129
+ // store, whose owner and lifetime are different — see {@link UnderpostState}.
130
+
131
+ /**
132
+ * Reads one host configuration key.
133
+ * @param {string} key - Key to read.
134
+ * @param {*} [value] - Unused; keeps the operator arity uniform.
135
+ * @param {object} [options] - `--plain`, `--copy`, `disableLog`.
136
+ * @returns {string|undefined} Stored value.
137
+ * @memberof UnderpostHost
138
+ */
139
+ get(key, value, options = {}) {
140
+ return UnderpostHost.API.store.get(key, value, options);
141
+ },
142
+
143
+ /**
144
+ * Writes one host configuration key.
145
+ * @param {string} key - Key to write.
146
+ * @param {string} value - Value to write.
147
+ * @memberof UnderpostHost
148
+ */
149
+ set(key, value) {
150
+ return UnderpostHost.API.store.set(key, value);
151
+ },
152
+
153
+ /**
154
+ * Removes one host configuration key.
155
+ * @param {string} key - Key to remove.
156
+ * @memberof UnderpostHost
157
+ */
158
+ delete(key) {
159
+ return UnderpostHost.API.store.delete(key);
160
+ },
161
+
162
+ /**
163
+ * Lists the host configuration store, optionally narrowed by `--filter`.
164
+ * @param {*} [key] - Unused; keeps the operator arity uniform.
165
+ * @param {*} [value] - Unused; keeps the operator arity uniform.
166
+ * @param {object} [options] - `--filter`, `disableLog`.
167
+ * @returns {Object<string, string>} Stored values.
168
+ * @memberof UnderpostHost
169
+ */
170
+ list(key, value, options = {}) {
171
+ return UnderpostHost.API.store.list(key, value, options);
172
+ },
173
+
174
+ /**
175
+ * Resolves the durable source for an environment.
176
+ * @param {string} [env='production'] - Environment selector.
177
+ * @returns {string} Path to the cron deploy's `.env.<env>`.
178
+ * @memberof UnderpostHost
179
+ */
180
+ envPath(env = 'production') {
181
+ return `./engine-private/conf/${cronDeployIdResolve() || DEFAULT_CRON_DEPLOY_ID}/.env.${env || 'production'}`;
182
+ },
183
+
184
+ /**
185
+ * The scoped durable sources present for an environment.
186
+ * @param {string} [env='production'] - Environment selector.
187
+ * @returns {Array<{scope: string, path: string}>} Sources that exist, in scope-table order.
188
+ * @memberof UnderpostHost
189
+ */
190
+ scopedSources(env = 'production') {
191
+ return Object.keys(CONFIG_SCOPES)
192
+ .map((scope) => ({ scope, path: UnderpostHost.API.scopePath(scope, env) }))
193
+ .filter(({ path }) => fs.existsSync(path));
194
+ },
195
+
196
+ /**
197
+ * Parses the host configuration. An absent source reads as `{}` so callers can probe
198
+ * without branching on existence.
199
+ * @param {string} [env='production'] - Environment selector.
200
+ * @returns {Object<string, string>} Parsed host configuration.
201
+ * @memberof UnderpostHost
202
+ */
203
+ read(env = 'production') {
204
+ // The scoped sources are authoritative once they exist. The unsplit file is read only until
205
+ // `setup` has split and verified it, and is retired at that point — a durable source that
206
+ // outlives its replacement is a second answer to the same question.
207
+ const scoped = UnderpostHost.API.scopedSources(env);
208
+ if (scoped.length > 0)
209
+ return scoped.reduce((values, { path }) => ({ ...values, ...dotenv.parse(fs.readFileSync(path, 'utf8')) }), {});
210
+ const envPath = UnderpostHost.API.envPath(env);
211
+ return fs.existsSync(envPath) ? dotenv.parse(fs.readFileSync(envPath, 'utf8')) : {};
212
+ },
213
+
214
+ /**
215
+ * Where this environment's configuration is actually read from, for diagnostics.
216
+ * @param {string} [env='production'] - Environment selector.
217
+ * @returns {string} Scopes directory once split, else the unsplit source.
218
+ * @memberof UnderpostHost
219
+ */
220
+ sourceLabel(env = 'production') {
221
+ const scoped = UnderpostHost.API.scopedSources(env);
222
+ return scoped.length > 0 ? `${SCOPES_DIR}/*.env.${env || 'production'}` : UnderpostHost.API.envPath(env);
223
+ },
224
+
225
+ /**
226
+ * Strips reserved keys from raw env-file content before it becomes a Secret.
227
+ * Blank lines and comments are preserved.
228
+ * @param {string} envFileContent - Raw `.env.<env>` contents.
229
+ * @returns {string} Filtered content.
230
+ * @memberof UnderpostHost
231
+ */
232
+ sanitizeEnvFile(envFileContent) {
233
+ return envFileContent
234
+ .split('\n')
235
+ .filter((line) => {
236
+ const trimmed = line.trimStart();
237
+ if (!trimmed || trimmed.startsWith('#')) return true;
238
+ const key = line.slice(0, line.indexOf('=')).trim();
239
+ return !key || !isReservedEnvKey(key);
240
+ })
241
+ .join('\n');
242
+ },
243
+
244
+ /**
245
+ * Typed read of the Grafana administrator keys. They are host configuration because the
246
+ * admin identity is declared once for the cluster; the secret domain onboards them from here.
247
+ * @param {object} [options] - Options.
248
+ * @param {string} [options.env='production'] - Environment selector.
249
+ * @param {boolean} [options.required=true] - Throw when user or password is missing.
250
+ * @returns {{username: string, password: string, email: string, envPath: string}} Credentials.
251
+ * @memberof UnderpostHost
252
+ */
253
+ grafanaAdmin(options = {}) {
254
+ const env = `${options.env ?? ''}`.trim() || (options.dev === true ? 'development' : 'production');
255
+ const values = UnderpostHost.API.read(env);
256
+ const credentials = {
257
+ username: `${values.GF_SECURITY_ADMIN_USER || ''}`.trim(),
258
+ password: `${values.GF_SECURITY_ADMIN_PASSWORD || ''}`,
259
+ email: `${values.GF_SECURITY_ADMIN_EMAIL || ''}`.trim(),
260
+ envPath: UnderpostHost.API.sourceLabel(env),
261
+ };
262
+ if (options.required !== false) {
263
+ const missing = [
264
+ !credentials.username && 'GF_SECURITY_ADMIN_USER',
265
+ !credentials.password && 'GF_SECURITY_ADMIN_PASSWORD',
266
+ ].filter(Boolean);
267
+ if (missing.length > 0) throw new Error(`[grafana] missing ${missing.join(', ')} in ${credentials.envPath}`);
268
+ }
269
+ return credentials;
270
+ },
271
+
272
+ /**
273
+ * Where a configuration scope's durable source lives.
274
+ *
275
+ * Beside the deploy markers rather than inside a deployment's conf: these are the node's
276
+ * concerns, not any one deployment's, and the deployment that happens to carry them today is
277
+ * an accident of history the split exists to end.
278
+ * @param {string} scope - Scope name.
279
+ * @param {string} [env='production'] - Environment selector.
280
+ * @returns {string} Path to that scope's env file.
281
+ * @memberof UnderpostHost
282
+ */
283
+ scopePath(scope, env = 'production') {
284
+ return `${SCOPES_DIR}/${scope}.env.${env || 'production'}`;
285
+ },
286
+
287
+ /**
288
+ * Splits the host domain's durable source into one file per owning scope.
289
+ *
290
+ * Additive and idempotent: it writes the scoped sources and reports what it found, and never
291
+ * removes the source it read. The legacy file stays authoritative until an operator has
292
+ * compared the two — a migration that deletes the only copy of a fleet's credentials on the
293
+ * strength of a regex is not a migration worth having.
294
+ *
295
+ * Fails closed on a key no scope claims. Guessing an owner is how a provisioning credential
296
+ * ends up in a workload projection, which is the exposure this split exists to close.
297
+ * @param {object} context - Normalized domain context.
298
+ * @returns {{source: string, scopes: Object<string, number>, written: string[]}} What was split.
299
+ * @memberof UnderpostHost
300
+ */
301
+ /**
302
+ * Writes an environment into the scoped durable sources, one file per owning scope.
303
+ *
304
+ * Fails closed on a key no scope claims: guessing an owner is how a provisioning credential
305
+ * ends up in a workload projection, which is the exposure the split exists to close.
306
+ * @param {Object<string, string>} values - Environment to distribute.
307
+ * @param {object} [context] - Normalized domain context.
308
+ * @returns {string[]} Paths written.
309
+ * @memberof UnderpostHost
310
+ */
311
+ writeScopes(values, context = {}) {
312
+ const env = context.env || 'production';
313
+ const { scopes, unclassified } = classifyConfigKeys(values);
314
+ if (unclassified.length > 0)
315
+ throw new Error(
316
+ `[host] ${unclassified.length} key(s) belong to no scope, so their exposure cannot be decided:\n` +
317
+ unclassified
318
+ .map((key) =>
319
+ configRejectionFactory({
320
+ domain: 'host',
321
+ deployId: cronDeployIdResolve() || DEFAULT_CRON_DEPLOY_ID,
322
+ env,
323
+ key,
324
+ reason: 'no ownership rule matches; declare it in CONFIG_OWNERSHIP',
325
+ }),
326
+ )
327
+ .join('\n'),
328
+ );
329
+
330
+ const written = [];
331
+ for (const [scope, keys] of Object.entries(scopes)) {
332
+ if (keys.length === 0) continue;
333
+ const target = UnderpostHost.API.scopePath(scope, env);
334
+ written.push(target);
335
+ if (context.dryRun) continue;
336
+ fs.mkdirpSync(nodePath.dirname(target));
337
+ writeEnv(target, Object.fromEntries(keys.map((key) => [key, values[key]])));
338
+ fs.chmodSync(target, 0o600);
339
+ }
340
+ return written;
341
+ },
342
+
343
+ split(context = {}) {
344
+ context = domainContextFactory(context);
345
+ const env = context.env || 'production';
346
+ const source = UnderpostHost.API.envPath(env);
347
+ if (!fs.existsSync(source)) {
348
+ // Retired, which is the finished state of this migration rather than a fault. Rerunning
349
+ // `setup` on a migrated node has nothing to split, and must not read that as a broken one.
350
+ const scoped = UnderpostHost.API.scopedSources(env);
351
+ if (scoped.length === 0) throw new Error(`[host] configuration source not found: ${source}`);
352
+ logger.info('Host configuration already split by scope', { env, scopes: scoped.length });
353
+ return { source, scopes: {}, written: [] };
354
+ }
355
+
356
+ const values = dotenv.parse(fs.readFileSync(source, 'utf8'));
357
+ const written = UnderpostHost.API.writeScopes(values, { ...context, env });
358
+ const counts = Object.fromEntries(
359
+ Object.entries(classifyConfigKeys(values).scopes).map(([scope, keys]) => [scope, keys.length]),
360
+ );
361
+
362
+ logger.info(
363
+ context.dryRun ? '[dry-run] host configuration would be split by scope' : 'Host configuration split',
364
+ { source, env, scopes: counts, written: written.length },
365
+ );
366
+ return { source, scopes: counts, written };
367
+ },
368
+
369
+ /**
370
+ * Confirms a split is complete: every key of the durable source is present, once, in exactly
371
+ * the scope that owns it. Run before an operator retires the unsplit source.
372
+ * @param {object} context - Normalized domain context.
373
+ * @returns {{ok: boolean, source: string, missing: string[], misplaced: string[]}} Verdict.
374
+ * @memberof UnderpostHost
375
+ */
376
+ verifySplit(context = {}) {
377
+ context = domainContextFactory(context);
378
+ const env = context.env || 'production';
379
+ const source = UnderpostHost.API.envPath(env);
380
+ if (!fs.existsSync(source)) return { ok: true, source, missing: [], misplaced: [] };
381
+
382
+ const values = dotenv.parse(fs.readFileSync(source, 'utf8'));
383
+ const missing = [];
384
+ const misplaced = [];
385
+ for (const key of Object.keys(values)) {
386
+ const ownership = configOwnershipFactory(key);
387
+ if (!ownership) {
388
+ missing.push(key);
389
+ continue;
390
+ }
391
+ const target = UnderpostHost.API.scopePath(ownership.owner, env);
392
+ const split = fs.existsSync(target) ? dotenv.parse(fs.readFileSync(target, 'utf8')) : {};
393
+ if (split[key] === undefined) missing.push(key);
394
+ else if (split[key] !== values[key]) misplaced.push(key);
395
+ }
396
+
397
+ const ok = missing.length === 0 && misplaced.length === 0;
398
+ logger[ok ? 'info' : 'error']('Host configuration split verification', {
399
+ source,
400
+ env,
401
+ keys: Object.keys(values).length,
402
+ missing: missing.length,
403
+ misplaced: misplaced.length,
404
+ ...(ok ? {} : { keysMissing: missing, keysMisplaced: misplaced }),
405
+ });
406
+ if (!ok) process.exitCode = 1;
407
+ return { ok, source, missing, misplaced };
408
+ },
409
+
410
+ /**
411
+ * Whether this environment still has an unsplit source beside its scoped ones.
412
+ * @param {string} [env='production'] - Environment selector.
413
+ * @returns {boolean} True while both layouts exist.
414
+ * @memberof UnderpostHost
415
+ */
416
+ hasDualSource(env = 'production') {
417
+ return UnderpostHost.API.scopedSources(env).length > 0 && fs.existsSync(UnderpostHost.API.envPath(env));
418
+ },
419
+
420
+ /**
421
+ * Removes the unsplit source once the scoped ones fully account for it.
422
+ *
423
+ * The gate is {@link UnderpostHost.verifySplit}, not the presence of the scope files: a split
424
+ * that dropped or altered a key would otherwise retire the only copy of it. Idempotent — an
425
+ * already-retired source is nothing to do, not an error.
426
+ * @param {object} context - Normalized domain context.
427
+ * @returns {{retired: boolean, source: string, reason: string}} What happened, and why.
428
+ * @memberof UnderpostHost
429
+ */
430
+ retireLegacySource(context = {}) {
431
+ context = domainContextFactory(context);
432
+ const source = UnderpostHost.API.envPath(context.env);
433
+ if (!fs.existsSync(source)) return { retired: false, source, reason: 'already retired' };
434
+
435
+ const { ok, missing, misplaced } = UnderpostHost.API.verifySplit(context);
436
+ if (!ok) {
437
+ process.exitCode = 0;
438
+ logger.warn('Unsplit host source kept: the scoped sources do not account for it', {
439
+ source,
440
+ missing: missing.length,
441
+ misplaced: misplaced.length,
442
+ });
443
+ return { retired: false, source, reason: 'verification failed' };
444
+ }
445
+ if (context.dryRun) {
446
+ logger.info('[dry-run] unsplit host source would be retired', { source });
447
+ return { retired: false, source, reason: 'dry-run' };
448
+ }
449
+ fs.removeSync(source);
450
+ logger.info('Unsplit host source retired; the scoped sources are authoritative', { source });
451
+ return { retired: true, source, reason: 'verified' };
452
+ },
453
+
454
+ // ── canonical domain actions ────────────────────────────────────────────────────────────
455
+
456
+ /**
457
+ * Onboards the host domain: confirms the cron deploy resolves and its source exists, then
458
+ * loads it. Idempotent — repeating it re-converges rather than re-provisioning.
459
+ * @param {object} context - Normalized domain context.
460
+ * @returns {{deployId: string, source: string, keys: number}} What was onboarded.
461
+ * @memberof UnderpostHost
462
+ */
463
+ setup(context = {}) {
464
+ context = domainContextFactory(context);
465
+ const deployId = cronDeployIdResolve() || DEFAULT_CRON_DEPLOY_ID;
466
+ const source = UnderpostHost.API.sourceLabel(context.env);
467
+ if (Object.keys(UnderpostHost.API.read(context.env)).length === 0)
468
+ throw new Error(`[host] configuration source not found: ${source}`);
469
+ if (context.dryRun) {
470
+ logger.info('[dry-run] host setup would load', { deployId, source });
471
+ return { deployId, source, keys: Object.keys(UnderpostHost.API.read(context.env)).length };
472
+ }
473
+ const { keys } = UnderpostHost.API.load(context);
474
+ // The scoped sources are what this domain provisions: they are what lets a projection hand a
475
+ // workload its own scope instead of the whole node environment. Split, verify, then retire —
476
+ // the unsplit file is removed only once every key of it is proven present and equal under its
477
+ // owner, so a failed classification leaves the original in place.
478
+ const split = UnderpostHost.API.split(context);
479
+ const retired = UnderpostHost.API.retireLegacySource(context);
480
+ return { deployId, source, keys, scopes: split.scopes, retired };
481
+ },
482
+
483
+ /**
484
+ * Loads the host configuration into the host configuration store.
485
+ *
486
+ * Two sources, one meaning. On a node the env file is on disk. Inside a workload container it
487
+ * is not — `engine-private` is not cloned yet at that point in the boot — and the same
488
+ * configuration arrives as the container environment, injected from the Secret `apply` wrote.
489
+ * Preferring whichever is present makes one action correct in both places.
490
+ *
491
+ * Idempotent: the store is rebuilt from the source, so repeating converges.
492
+ * @param {object} context - Normalized domain context.
493
+ * @returns {{source: string, keys: number}} Where the configuration came from and how much.
494
+ * @memberof UnderpostHost
495
+ */
496
+ load(context = {}) {
497
+ context = domainContextFactory(context);
498
+ const onDisk = UnderpostHost.API.read(context.env);
499
+ const fromFile = Object.keys(onDisk).length > 0;
500
+ const values = fromFile
501
+ ? onDisk
502
+ : Object.fromEntries(Object.entries(process.env).filter(([key]) => !isReservedEnvKey(key)));
503
+ const source = fromFile ? UnderpostHost.API.sourceLabel(context.env) : 'container-env';
504
+ if (context.dryRun) {
505
+ logger.info('[dry-run] host load would replace the host configuration store', {
506
+ source,
507
+ keys: Object.keys(values).length,
508
+ });
509
+ return { source, keys: Object.keys(values).length };
510
+ }
511
+ UnderpostHost.API.store.clean();
512
+ for (const [key, value] of Object.entries(values)) UnderpostHost.API.store.set(key, value);
513
+ logger.info('Host configuration loaded', { source, keys: Object.keys(values).length });
514
+ return { source, keys: Object.keys(values).length };
515
+ },
516
+
517
+ /**
518
+ * Writes the host configuration store back into the durable host configuration source.
519
+ *
520
+ * The inverse of `load`. Writes each key into the scope that owns it — there is no file that
521
+ * takes all of them — so this can never recreate the legacy unsplit source, migrated away from
522
+ * in {@link UnderpostHost.retireLegacySource}. `--force` still governs overwriting an existing
523
+ * scoped source, the same guarantee the single-file version made per file.
524
+ * @param {object} context - Normalized domain context.
525
+ * @returns {{target: string, keys: number}} What was written.
526
+ * @memberof UnderpostHost
527
+ */
528
+ publish(context = {}) {
529
+ context = domainContextFactory(context);
530
+ const env = context.env || 'production';
531
+ const values = UnderpostHost.API.store.list(undefined, undefined, { disableLog: true });
532
+ const keys = Object.keys(values);
533
+ if (keys.length === 0) throw new Error('[host] the host configuration store is empty; nothing to publish');
534
+ if (!context.force) {
535
+ const existing = UnderpostHost.API.scopedSources(env).map(({ path }) => path);
536
+ if (existing.length > 0)
537
+ throw new Error(`[host] ${existing.join(', ')} already exist; re-run with --force to overwrite them`);
538
+ }
539
+ const written = UnderpostHost.API.writeScopes(values, { ...context, env });
540
+ const target = UnderpostHost.API.sourceLabel(env);
541
+ logger.info(context.dryRun ? '[dry-run] host publish would write' : 'Host configuration published', {
542
+ target,
543
+ keys: keys.length,
544
+ scopes: written.length,
545
+ });
546
+ return { target, keys: keys.length };
547
+ },
548
+
549
+ /**
550
+ * Projects the host configuration into the cluster as the `underpost-config` Secret that
551
+ * workloads inject with `envFrom`.
552
+ *
553
+ * Applying it before a deploy is what makes the pod resolve the requested `NODE_ENV`: the
554
+ * container reads it back through `load`, so a stale Secret means a stale environment.
555
+ * Idempotent — delete-then-apply converges on the current source.
556
+ *
557
+ * Reads through {@link UnderpostHost.read}, the one composition of the durable source: scoped
558
+ * files where the migration has run, the legacy file otherwise. Staged on tmpfs rather than
559
+ * beside the source, because the source is no longer necessarily one file to stage beside.
560
+ * @param {object} context - Normalized domain context.
561
+ * @returns {{secret: string, namespace: string, env: string}} What was projected.
562
+ * @memberof UnderpostHost
563
+ */
564
+ apply(context = {}) {
565
+ context = domainContextFactory(context);
566
+ const source = UnderpostHost.API.sourceLabel(context.env);
567
+ const values = UnderpostHost.API.read(context.env);
568
+ if (Object.keys(values).length === 0) throw new Error(`[host] configuration source not found: ${source}`);
569
+ if (context.dryRun) {
570
+ logger.info('[dry-run] host apply would publish the Secret', {
571
+ secret: UNDERPOST_CONFIG_SECRET,
572
+ namespace: context.namespace,
573
+ source,
574
+ });
575
+ return { secret: UNDERPOST_CONFIG_SECRET, namespace: context.namespace, env: context.env };
576
+ }
577
+ // `--from-env-file` turns every KEY=VALUE into a secret key the Deployment injects via
578
+ // `envFrom`, so reserved keys are filtered before the file is handed to kubectl.
579
+ const sanitized = Object.fromEntries(Object.entries(values).filter(([key]) => !isReservedEnvKey(key)));
580
+ const stageDir = '/dev/shm/underpost-host-apply';
581
+ fs.mkdirpSync(stageDir, { mode: 0o700 });
582
+ fs.chmodSync(stageDir, 0o700);
583
+ const sanitizedEnvPath = `${stageDir}/${context.namespace}.env`;
584
+ writeEnv(sanitizedEnvPath, sanitized);
585
+ fs.chmodSync(sanitizedEnvPath, 0o600);
586
+ try {
587
+ shellExec(`kubectl delete secret ${UNDERPOST_CONFIG_SECRET} -n ${context.namespace} --ignore-not-found`);
588
+ shellExec(
589
+ `kubectl create secret generic ${UNDERPOST_CONFIG_SECRET} --from-env-file=${sanitizedEnvPath} --dry-run=client -o yaml | kubectl apply -f - -n ${context.namespace}`,
590
+ );
591
+ } finally {
592
+ fs.removeSync(sanitizedEnvPath);
593
+ }
594
+ logger.info('Host configuration applied', { env: context.env, namespace: context.namespace, source });
595
+ return { secret: UNDERPOST_CONFIG_SECRET, namespace: context.namespace, env: context.env };
596
+ },
597
+
598
+ /**
599
+ * Read-only report: which source is in effect, how many keys it carries, and whether the
600
+ * cluster projection exists.
601
+ * @param {object} context - Normalized domain context.
602
+ * @returns {object} Report.
603
+ * @memberof UnderpostHost
604
+ */
605
+ status(context = {}) {
606
+ context = domainContextFactory(context);
607
+ const source = UnderpostHost.API.sourceLabel(context.env);
608
+ const keys = Object.keys(UnderpostHost.API.read(context.env)).length;
609
+ const legacyPresent = fs.existsSync(UnderpostHost.API.envPath(context.env));
610
+ const projected = shellExec(`kubectl get secret ${UNDERPOST_CONFIG_SECRET} -n ${context.namespace} -o name`, {
611
+ silent: true,
612
+ stdout: true,
613
+ silentOnError: true,
614
+ disableLog: true,
615
+ });
616
+ const report = {
617
+ domain: 'host',
618
+ env: context.env,
619
+ namespace: context.namespace,
620
+ deployId: cronDeployIdResolve() || 'dd-cron',
621
+ source,
622
+ sourcePresent: keys > 0,
623
+ keys,
624
+ dualSource: UnderpostHost.API.hasDualSource(context.env),
625
+ clusterSecret: `${projected ?? ''}`.trim() ? UNDERPOST_CONFIG_SECRET : null,
626
+ };
627
+ logger.info('host status', report);
628
+ if (legacyPresent) UnderpostHost.API.verifySplit(context);
629
+ return report;
630
+ },
631
+
632
+ /**
633
+ * Replaces the live projection: deletes the `underpost-config` Secret and re-applies it, so
634
+ * consumers pick up a source that changed underneath them.
635
+ * @param {object} context - Normalized domain context.
636
+ * @returns {object} The re-applied projection.
637
+ * @memberof UnderpostHost
638
+ */
639
+ rotate(context = {}) {
640
+ context = domainContextFactory(context);
641
+ if (context.dryRun) {
642
+ logger.info('[dry-run] host rotate would re-project the Secret', { namespace: context.namespace });
643
+ return { secret: UNDERPOST_CONFIG_SECRET, namespace: context.namespace, env: context.env };
644
+ }
645
+ shellExec(`kubectl delete secret ${UNDERPOST_CONFIG_SECRET} -n ${context.namespace} --ignore-not-found`);
646
+ return UnderpostHost.API.apply(context);
647
+ },
648
+
649
+ /**
650
+ * Withdraws the host configuration from the local filesystem by clearing the host configuration store.
651
+ * Container runtime state lives in its own store and is untouched. `--force` also removes the
652
+ * ephemeral `engine-private` clone.
653
+ * @param {object} context - Normalized domain context.
654
+ * @returns {{keptKeys: Array<string>, removedPrivateRepo: boolean}} What was withdrawn.
655
+ * @memberof UnderpostHost
656
+ */
657
+ clean(context = {}) {
658
+ context = domainContextFactory(context);
659
+ if (context.dryRun) {
660
+ logger.info('[dry-run] host clean would clear the host configuration store', { force: context.force });
661
+ return { removedPrivateRepo: false };
662
+ }
663
+ UnderpostHost.API.store.clean();
664
+ if (context.force) Underpost.repo.cleanupPrivateEngineRepo();
665
+ logger.info('Host configuration withdrawn', { removedPrivateRepo: context.force });
666
+ return { removedPrivateRepo: context.force };
667
+ },
668
+ };
669
+ }
670
+
671
+ export default UnderpostHost;
672
+ export { isReservedEnvKey, UnderpostHost };