@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
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Provides utilities for building, loading, and managing server configurations,
3
3
  * deployment contexts, and service configurations (API, Client, WS).
4
- * @module src/server/conf.js
4
+ * @module src/server/runtime/conf.js
5
5
  * @namespace ServerConfBuilder
6
6
  */
7
7
 
@@ -13,20 +13,30 @@ import {
13
13
  getDirname,
14
14
  newInstance,
15
15
  orderAbc,
16
- orderArrayFromAttrInt,
17
- range,
18
16
  timer,
19
- } from '../client/components/core/CommonJs.js';
17
+ } from '../../client/components/core/CommonJs.js';
20
18
  import * as dir from 'path';
21
19
  import net from 'net';
22
20
  import crypto from 'crypto';
23
21
  import colors from 'colors';
24
- import { loggerFactory } from './logger.js';
25
- import { shellExec } from './process.js';
26
- import { UNDERPOST_GATEWAY, statusPageAssetPathFactory } from './underpost-gateway.js';
27
- import { DefaultConf } from '../../conf.js';
22
+ import { loggerFactory } from '../ops/logger.js';
23
+ import { isOciRuntime, writeEnv } from './environment.js';
24
+ import { configRejectionFactory } from './config-scope.js';
25
+ import { shellArgumentFactory, shellExec } from './process.js';
26
+ import { UNDERPOST_GATEWAY, statusPageAssetPathFactory } from '../network/underpost-gateway.js';
27
+ import { COVERAGE_BUNDLE_DIRECTORY } from '../build/coverage.js';
28
+ import { DefaultConf } from '../../../conf.js';
28
29
  import splitFile from 'split-file';
29
- import Underpost from '../index.js';
30
+ import { readDeployRoutes } from '../network/router.js';
31
+ import Underpost from '../../index.js';
32
+ import {
33
+ clearAwaitDeploy,
34
+ getRuntimeStatus,
35
+ isAwaitingDeploy,
36
+ latchAwaitDeploy,
37
+ RUNTIME_STATUS,
38
+ } from './runtime-status.js';
39
+ import { cronDeployIdResolve } from '../ops/cron.js';
30
40
 
31
41
  colors.enable();
32
42
 
@@ -45,6 +55,13 @@ const logger = loggerFactory(import.meta);
45
55
  */
46
56
  const ENV_REF_PREFIX = 'env:';
47
57
 
58
+ /**
59
+ * Default deploy ID used when no deploy ID is specified.
60
+ * @constant {string}
61
+ * @memberof ServerConfBuilder
62
+ */
63
+ const DEFAULT_DEPLOY_ID = 'dd-default';
64
+
48
65
  /**
49
66
  * Resolves a standardized context key from host/path descriptors.
50
67
  * The key is used across DB, WS, mailer, and cache registries.
@@ -166,67 +183,10 @@ const getConfFolder = (deployId) => {
166
183
  : `./engine-private/conf/${deployId}`;
167
184
  };
168
185
 
169
- /**
170
- * Reads `engine-private/deploy/dd.cron` and returns the deploy-id string,
171
- * or `null` if the file does not exist or is empty.
172
- *
173
- * @method cronDeployIdResolve
174
- * @returns {string|null} The deploy-id from dd.cron, or null.
175
- * @memberof ServerConfBuilder
176
- */
177
- const cronDeployIdResolve = () => {
178
- const cronDeployFile = './engine-private/deploy/dd.cron';
179
- if (fs.existsSync(cronDeployFile)) {
180
- const id = fs.readFileSync(cronDeployFile, 'utf8').trim();
181
- return id || null;
182
- }
183
- return null;
184
- };
185
-
186
- /**
187
- * Loads the deployment-specific `.env` file referenced by `engine-private/deploy/dd.cron`
188
- * into `process.env`. Uses `NODE_ENV` to select the environment variant
189
- * (defaults to `production`).
190
- *
191
- * Safe to call multiple times; subsequent calls are no-ops once the env is loaded.
192
- *
193
- * @method loadCronDeployEnv
194
- * @memberof ServerConfBuilder
195
- */
196
- function loadCronDeployEnv() {
197
- const envName = process.env.NODE_ENV || 'production';
198
-
199
- // 1) Load dd.cron env (takes full precedence)
200
- const cronDeployId = cronDeployIdResolve();
201
- if (cronDeployId) {
202
- const cronEnvPath = `./engine-private/conf/${cronDeployId}/.env.${envName}`;
203
- if (fs.existsSync(cronEnvPath)) {
204
- const cronEnv = dotenv.parse(fs.readFileSync(cronEnvPath, 'utf8'));
205
- process.env = { ...process.env, ...cronEnv };
206
- }
207
- }
208
-
209
- // 2) Load dd.router envs — only keys not already present
210
- const routerDeployFile = './engine-private/deploy/dd.router';
211
- if (fs.existsSync(routerDeployFile)) {
212
- const routerIds = fs.readFileSync(routerDeployFile, 'utf8').trim().split(',');
213
- for (const deployId of routerIds) {
214
- const id = deployId.trim();
215
- if (!id) continue;
216
- const envPath = `./engine-private/conf/${id}/.env.${envName}`;
217
- if (!fs.existsSync(envPath)) continue;
218
- const env = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
219
- for (const key of Object.keys(env)) {
220
- if (!(key in process.env)) process.env[key] = env[key];
221
- }
222
- }
223
- }
224
- }
225
-
226
186
  /**
227
187
  * Resolves the full path to a specific configuration JSON file for a deploy ID.
228
- * For `server` configs in development mode with a subConf, it will prefer the
229
- * dev-specific variant if it exists.
188
+ * For `server` configs it prefers the `dev.<subConf>` variant when one exists: always for a
189
+ * sub-conf the caller named, and only in development for one inherited from `DEPLOY_SUB_CONF`.
230
190
  *
231
191
  * @method getConfFilePath
232
192
  * @param {string} deployId - The deploy ID.
@@ -240,17 +200,28 @@ function loadCronDeployEnv() {
240
200
  * // => './engine-private/conf/dd-myapp/conf.server.json'
241
201
  *
242
202
  * @example
243
- * // In development with subConf 'local':
203
+ * // With an explicitly named subConf, in any environment:
244
204
  * getConfFilePath('dd-myapp', 'server', 'local');
245
205
  * // => './engine-private/conf/dd-myapp/conf.server.dev.local.json' (if it exists)
246
206
  */
247
207
  const getConfFilePath = (deployId, confType, subConf = '') => {
248
208
  const folder = getConfFolder(deployId);
249
- // When no explicit subConf is given, fall back to the env var set by loadConf()
250
- const effectiveSubConf = subConf || process.env.DEPLOY_SUB_CONF || '';
209
+ const explicit = `${subConf ?? ''}`.trim();
210
+ // Without an explicit sub-conf, fall back to the env var loadConf() exported.
211
+ const effectiveSubConf = explicit || process.env.DEPLOY_SUB_CONF || '';
251
212
  if (confType === 'server' && effectiveSubConf) {
252
- const devConfPath = `${folder}/conf.${confType}.dev.${effectiveSubConf}.json`;
253
- if (fs.existsSync(devConfPath)) return devConfPath;
213
+ // An explicitly named sub-conf is a request, not a hint: honor it whatever the ambient
214
+ // environment says. Only the env-var fallback stays gated on development, so a production
215
+ // deploy that merely carries DEPLOY_SUB_CONF is unaffected.
216
+ //
217
+ // The gate used to apply to both, and every `underpost` invocation loads the host
218
+ // configuration store with `override: true` — on a provisioned node that store carries
219
+ // NODE_ENV=production, so the gate was false for every CLI call and a named sub-conf was
220
+ // silently discarded in favour of the base conf.
221
+ if (explicit || process.env.NODE_ENV === 'development') {
222
+ const devConfPath = `${folder}/conf.${confType}.dev.${effectiveSubConf}.json`;
223
+ if (fs.existsSync(devConfPath)) return devConfPath;
224
+ }
254
225
  }
255
226
  return `${folder}/conf.${confType}.json`;
256
227
  };
@@ -280,13 +251,6 @@ const readConfJson = (deployId, confType, options = {}) => {
280
251
  return parsed;
281
252
  };
282
253
 
283
- /**
284
- * Default deploy ID used when no deploy ID is specified.
285
- * @constant {string}
286
- * @memberof ServerConfBuilder
287
- */
288
- const DEFAULT_DEPLOY_ID = 'dd-default';
289
-
290
254
  /**
291
255
  * @class Config
292
256
  * @description Manages the configuration of the server.
@@ -317,7 +281,7 @@ const Config = {
317
281
  deployContext = process.env.DEPLOY_ID;
318
282
  if (!subConf && process.argv[3] && typeof process.argv[3] === 'string') subConf = process.argv[3];
319
283
 
320
- Underpost.env.set('await-deploy', new Date().toISOString());
284
+ latchAwaitDeploy();
321
285
  if (deployContext.startsWith('dd-')) loadConf(deployContext, subConf);
322
286
  if (deployContext === 'proxy') await Config.buildProxy(deployList, subConf);
323
287
  },
@@ -325,16 +289,15 @@ const Config = {
325
289
  * @method deployIdFactory
326
290
  * @description Creates a new deploy ID.
327
291
  * @param {string} [deployId='dd-default']
328
- * @param {object} [options={ subConf: '', cluster: false }] - The options.
292
+ * @param {object} [options={ subConf: '' }] - The options.
329
293
  * @memberof ServerConfBuilder
330
294
  */
331
- deployIdFactory: function (deployId = DEFAULT_DEPLOY_ID, options = { subConf: '', cluster: false }) {
295
+ deployIdFactory: function (deployId = DEFAULT_DEPLOY_ID, options = { subConf: '' }) {
332
296
  if (!deployId.startsWith('dd-')) deployId = `dd-${deployId}`;
333
297
 
334
298
  logger.info('Build deployId', deployId);
335
299
 
336
300
  const folder = `./engine-private/conf/${deployId}`;
337
- const repoName = `engine-${deployId.split('dd-')[1]}`;
338
301
 
339
302
  if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
340
303
 
@@ -386,29 +349,6 @@ const Config = {
386
349
  );
387
350
  }
388
351
 
389
- if (options.cluster === true) {
390
- fs.writeFileSync(
391
- `./.github/workflows/${repoName}.cd.yml`,
392
- fs.readFileSync(`./.github/workflows/engine-test.cd.yml`, 'utf8').replaceAll('test', deployId.split('dd-')[1]),
393
- 'utf8',
394
- );
395
- fs.writeFileSync(
396
- `./.github/workflows/${repoName}.ci.yml`,
397
- fs.readFileSync(`./.github/workflows/engine-test.ci.yml`, 'utf8').replaceAll('test', deployId.split('dd-')[1]),
398
- 'utf8',
399
- );
400
- shellExec(`node bin new --default-conf --deploy-id ${deployId}`);
401
-
402
- if (!fs.existsSync(`./engine-private/deploy/dd.router`))
403
- fs.writeFileSync(`./engine-private/deploy/dd.router`, deployId, 'utf8');
404
- else
405
- fs.writeFileSync(
406
- `./engine-private/deploy/dd.router`,
407
- fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').trim() + `,${deployId}`,
408
- 'utf8',
409
- );
410
- }
411
-
412
352
  return { deployIdFolder: folder, deployId };
413
353
  },
414
354
  /**
@@ -468,17 +408,178 @@ const Config = {
468
408
  * @param {string} [subConf=''] - The sub configuration.
469
409
  * @memberof ServerConfBuilder
470
410
  */
411
+ /**
412
+ * @method deployEnvFilePath
413
+ * @description Resolves a deployment's env file for an environment.
414
+ *
415
+ * A named sub-configuration selects `.env.<env>.<subConf>` when that file exists and degrades to
416
+ * the plain `.env.<env>` when it does not. Single source of this precedence: {@link loadConf}
417
+ * materializes the working tree from it and {@link UnderpostApp} addresses it from the CLI, so
418
+ * both always agree on which file is in effect.
419
+ * @param {string} deployId - Deployment id.
420
+ * @param {string} [env='production'] - Environment selector.
421
+ * @param {string} [subConf] - Sub-configuration name.
422
+ * @returns {string} Path to the deployment env file.
423
+ * @memberof ServerConfBuilder
424
+ */
425
+ const deployEnvFilePath = (deployId, env = 'production', subConf = '') => {
426
+ const base = `${getConfFolder(deployId)}/.env.${env || 'production'}`;
427
+ const name = `${subConf ?? ''}`.trim();
428
+ if (!name) return base;
429
+ const scoped = `${base}.${name}`;
430
+ return fs.existsSync(scoped) ? scoped : base;
431
+ };
432
+
433
+ /**
434
+ * The environments a deployment's configuration actually carries.
435
+ *
436
+ * Read from the tree rather than assumed, because which environments exist is a property of the
437
+ * checkout: a workstation carries every one a developer runs, and a workload carries none at all —
438
+ * its environment is injected. A name is the segment after `.env.` and nothing further, so an
439
+ * overlay (`.env.production.oci`) and a sub-configuration (`.env.development.api`) are the
440
+ * variants of an environment, never environments of their own.
441
+ * @param {string} deployId - Deployment id.
442
+ * @returns {string[]} Environment names present, sorted.
443
+ * @memberof ServerConfBuilder
444
+ */
445
+ const deployEnvNamesFactory = (deployId) => {
446
+ const folder = getConfFolder(deployId);
447
+ if (!fs.existsSync(folder)) return [];
448
+ return fs
449
+ .readdirSync(folder)
450
+ .map((entry) => `${entry}`.match(/^\.env\.([^.]+)$/)?.[1])
451
+ .filter(Boolean)
452
+ .sort();
453
+ };
454
+
455
+ /**
456
+ * Resolves a custom instance's env file for an environment.
457
+ *
458
+ * An instance's runtime environment is its own file, not a sub-configuration of the parent
459
+ * deployment's: `instance-build-manifest` writes it, the instance container sources it at boot,
460
+ * and {@link UnderpostApp} addresses it from the CLI through `--args instance-id=`. Single
461
+ * source of that path, so the three can never disagree.
462
+ * @param {string} deployId - Parent deployment id.
463
+ * @param {string} instanceId - Instance id, as declared in `conf.instances.json`.
464
+ * @param {string} [env='production'] - Environment selector.
465
+ * @returns {string} Path to the instance env file.
466
+ * @memberof ServerConfBuilder
467
+ */
468
+ const instanceEnvFilePath = (deployId, instanceId, env = 'production') =>
469
+ `${getConfFolder(deployId)}/instances/${instanceId}/env/${env || 'production'}.env`;
470
+
471
+ /**
472
+ * Suffix marking a deployment env file's OCI (container) runtime overlay.
473
+ * @constant {string}
474
+ * @memberof ServerConfBuilder
475
+ */
476
+ const OCI_ENV_SUFFIX = '.oci';
477
+
478
+ /**
479
+ * @method deployOciEnvFilePath
480
+ * @description Resolves a deployment's OCI runtime env overlay for an environment.
481
+ *
482
+ * The overlay is per environment, not per sub-configuration: it carries the values that only
483
+ * hold when the deployment runs as a container image (cluster-internal service names, in-cluster
484
+ * database endpoints), which are the same whichever host sub-configuration is in effect.
485
+ * @param {string} deployId - Deployment id.
486
+ * @param {string} [env='production'] - Environment selector.
487
+ * @returns {string} Path to the deployment's OCI env overlay.
488
+ * @memberof ServerConfBuilder
489
+ */
490
+ const deployOciEnvFilePath = (deployId, env = 'production') =>
491
+ `${getConfFolder(deployId)}/.env.${env || 'production'}${OCI_ENV_SUFFIX}`;
492
+
493
+ /**
494
+ * @method envLineKey
495
+ * @description The variable name a dotenv line declares, or `''` for blanks and comments.
496
+ * @param {string} line - One dotenv line.
497
+ * @returns {string} Declared key, or `''`.
498
+ * @memberof ServerConfBuilder
499
+ */
500
+ const envLineKey = (line) => {
501
+ const trimmed = `${line}`.trimStart();
502
+ if (!trimmed || trimmed.startsWith('#')) return '';
503
+ const assign = trimmed.indexOf('=');
504
+ return assign === -1 ? '' : trimmed.slice(0, assign).trim();
505
+ };
506
+
507
+ /**
508
+ * @method ociEnvContentFactory
509
+ * @description Applies an OCI overlay onto base env contents.
510
+ *
511
+ * Textual rather than parse-and-render: base lines the overlay does not redeclare survive
512
+ * verbatim — comments, ordering, and value quoting included — and the overlay is appended whole,
513
+ * so an overridden key appears exactly once with the container value.
514
+ * @param {string} baseContent - The deployment's `.env.<env>` contents.
515
+ * @param {string} overlayContent - The matching `.env.<env>.oci` contents.
516
+ * @returns {string} Overlay-applied env contents.
517
+ * @memberof ServerConfBuilder
518
+ */
519
+ const ociEnvContentFactory = (baseContent, overlayContent) => {
520
+ const overlay = `${overlayContent ?? ''}`.trim();
521
+ if (!overlay) return baseContent;
522
+ const overridden = new Set(overlay.split('\n').map(envLineKey).filter(Boolean));
523
+ const kept = `${baseContent ?? ''}`
524
+ .split('\n')
525
+ .filter((line) => {
526
+ const key = envLineKey(line);
527
+ return !key || !overridden.has(key);
528
+ })
529
+ .join('\n')
530
+ .trimEnd();
531
+ return `${kept}${kept ? '\n' : ''}${overlay}\n`;
532
+ };
533
+
534
+ /**
535
+ * @method deployEnvContentFactory
536
+ * @description Reads a deployment's env file for an environment, with its OCI overlay applied
537
+ * when one exists and the caller is resolving for a container runtime.
538
+ *
539
+ * Single source of the overlay precedence: {@link loadConf} materializes the working tree from
540
+ * it and {@link UnderpostApp} projects the cluster Secret from it, so the file a container reads
541
+ * and the Secret injected into it always carry the same values. Outside a container the base
542
+ * file is returned untouched, which is what keeps a developer host on its local endpoints.
543
+ * @param {string} deployId - Deployment id.
544
+ * @param {string} [env='production'] - Environment selector.
545
+ * @param {string} [subConf=''] - Sub-configuration name.
546
+ * @param {object} [options] - Resolution options.
547
+ * @param {boolean} [options.oci] - Force the overlay on or off; defaults to container detection.
548
+ * @returns {{source: string, overlay: string|null, content: string, values: Object<string,string>}}
549
+ * The base file used, the overlay applied (or `null`), and the resulting contents and values.
550
+ * @memberof ServerConfBuilder
551
+ */
552
+ const deployEnvContentFactory = (deployId, env = 'production', subConf = '', options = {}) => {
553
+ const source = deployEnvFilePath(deployId, env, subConf);
554
+ const baseContent = fs.readFileSync(source, 'utf8');
555
+ const overlayPath = deployOciEnvFilePath(deployId, env);
556
+ const applyOverlay = (options.oci ?? isOciRuntime()) && fs.existsSync(overlayPath);
557
+ if (!applyOverlay) return { source, overlay: null, content: baseContent, values: dotenv.parse(baseContent) };
558
+ const content = ociEnvContentFactory(baseContent, fs.readFileSync(overlayPath, 'utf8'));
559
+ return { source, overlay: overlayPath, content, values: dotenv.parse(content) };
560
+ };
561
+
562
+ /**
563
+ * @method cleanDeployEnvFiles
564
+ * @description Removes the working-tree env files {@link loadConf} materializes.
565
+ * Single source of the file list, shared with the app domain's `clean` action.
566
+ * @returns {Array<string>} The paths that were present and removed.
567
+ * @memberof ServerConfBuilder
568
+ */
569
+ const cleanDeployEnvFiles = () => {
570
+ const targets = ['./.env', './.env.production', './.env.development', './.env.test'];
571
+ const present = targets.filter((target) => fs.existsSync(target));
572
+ for (const target of present) fs.removeSync(target);
573
+ return present;
574
+ };
575
+
471
576
  const loadConf = (deployId = DEFAULT_DEPLOY_ID, subConf) => {
472
577
  if (deployId === 'current') {
473
578
  console.log(process.env.DEPLOY_ID);
474
579
  return;
475
580
  }
476
581
  if (deployId === 'clean') {
477
- const path = '.';
478
- fs.removeSync(`${path}/.env`);
479
- fs.removeSync(`${path}/.env.production`);
480
- fs.removeSync(`${path}/.env.development`);
481
- fs.removeSync(`${path}/.env.test`);
582
+ cleanDeployEnvFiles();
482
583
  return;
483
584
  }
484
585
  const folder = getConfFolder(deployId);
@@ -488,34 +589,58 @@ const loadConf = (deployId = DEFAULT_DEPLOY_ID, subConf) => {
488
589
  if (subConf) process.env.DEPLOY_SUB_CONF = subConf;
489
590
 
490
591
  for (const typeConf of Object.keys(Config.default)) {
491
- let srcConf = fs.readFileSync(`${folder}/conf.${typeConf}.json`, 'utf8');
492
- if (process.env.NODE_ENV === 'development' && typeConf === 'server' && subConf) {
493
- const devConfPath = `${folder}/conf.${typeConf}.dev${subConf ? `.${subConf}` : ''}.json`;
592
+ const confPath = `${folder}/conf.${typeConf}.json`;
593
+ let srcConf;
594
+ if (fs.existsSync(confPath)) srcConf = fs.readFileSync(confPath, 'utf8');
595
+ else {
596
+ Config.default[typeConf] = newInstance(DefaultConf[typeConf]);
597
+ continue;
598
+ }
599
+ // Same precedence as getConfFilePath, so loadConf and every later read agree on which file
600
+ // is in effect: a sub-conf named here was named by the caller, so it is not gated on the
601
+ // ambient environment.
602
+ if (typeConf === 'server' && subConf) {
603
+ const devConfPath = getConfFilePath(deployId, typeConf, subConf);
494
604
  if (fs.existsSync(devConfPath)) srcConf = fs.readFileSync(devConfPath, 'utf8');
495
605
  }
496
606
  let parsed = JSON.parse(srcConf);
497
607
  if (typeConf === 'server') parsed = loadReplicas(deployId, parsed);
498
608
  Config.default[typeConf] = parsed;
499
609
  }
500
- fs.writeFileSync(`./.env.production`, fs.readFileSync(`${folder}/.env.production`, 'utf8'), 'utf8');
501
- fs.writeFileSync(`./.env.development`, fs.readFileSync(`${folder}/.env.development`, 'utf8'), 'utf8');
502
- fs.writeFileSync(`./.env.test`, fs.readFileSync(`${folder}/.env.test`, 'utf8'), 'utf8');
503
- const NODE_ENV = process.env.NODE_ENV || 'development';
504
- if (NODE_ENV) {
505
- const subPathEnv = fs.existsSync(`${folder}/.env.${NODE_ENV}.${subConf}`)
506
- ? `${folder}/.env.${NODE_ENV}.${subConf}`
507
- : `${folder}/.env.${NODE_ENV}`;
508
- fs.writeFileSync(`./.env`, fs.readFileSync(subPathEnv, 'utf8'), 'utf8');
509
- const env = dotenv.parse(fs.readFileSync(subPathEnv, 'utf8'));
510
- process.env = {
511
- ...process.env,
512
- ...env,
513
- };
514
- }
610
+ // Each materialized copy carries its own OCI overlay, so a container that later switches
611
+ // NODE_ENV still reads container values rather than the host endpoints of the base file.
612
+ const available = deployEnvNamesFactory(deployId);
613
+ for (const name of available)
614
+ fs.writeFileSync(`./.env.${name}`, deployEnvContentFactory(deployId, name).content, 'utf8');
615
+
616
+ const envName = process.env.NODE_ENV || 'development';
617
+ // A tree with no environment of its own is a workload: its configuration is injected, and
618
+ // materializing is neither possible nor wanted. A tree that carries environments but not this
619
+ // one is a mistake worth naming — falling through would run the deployment against whichever
620
+ // environment happened to be loaded.
621
+ if (available.length > 0) {
622
+ if (!available.includes(envName))
623
+ throw new Error(
624
+ configRejectionFactory({
625
+ domain: 'app',
626
+ deployId,
627
+ env: envName,
628
+ key: 'NODE_ENV',
629
+ reason: `no environment source; ${getConfFolder(deployId)} carries ${available.join(', ')}`,
630
+ }),
631
+ );
632
+ const { content, values } = deployEnvContentFactory(deployId, envName, subConf);
633
+ fs.writeFileSync(`./.env`, content, 'utf8');
634
+ process.env = { ...process.env, ...values, NODE_ENV: envName };
635
+ } else process.env.NODE_ENV = envName;
515
636
  const originPackageJson = JSON.parse(fs.readFileSync(`./package.json`, 'utf8'));
516
637
  const packageJson = JSON.parse(fs.readFileSync(`${folder}/package.json`, 'utf8'));
517
638
  originPackageJson.scripts.start = packageJson.scripts.start;
518
639
  packageJson.scripts = originPackageJson.scripts;
640
+ // `bin` is engine identity, not deploy identity: it is what `npm link` publishes as the
641
+ // global `underpost` command. Deploy manifests do not carry one, so without this the
642
+ // checkout would stop being linkable the moment its environment was loaded.
643
+ if (originPackageJson.bin) packageJson.bin = originPackageJson.bin;
519
644
  fs.writeFileSync(`./package.json`, JSON.stringify(packageJson, null, 4), 'utf8');
520
645
  return { folder, deployId };
521
646
  };
@@ -886,247 +1011,6 @@ const cloneSrcComponents = async ({ toOptions, fromOptions }) => {
886
1011
  }
887
1012
  };
888
1013
 
889
- /**
890
- * @method buildProxyRouter
891
- * @description Builds the proxy router.
892
- * @memberof ServerConfBuilder
893
- */
894
- const buildProxyRouter = () => {
895
- const confServer = newInstance(Config.default.server);
896
- let currentPort = parseInt(process.env.PORT) + 1;
897
- const proxyRouter = {};
898
- for (const host of Object.keys(confServer)) {
899
- for (const path of Object.keys(confServer[host])) {
900
- if (confServer[host][path].singleReplica) continue;
901
-
902
- if (isDevProxyContext()) confServer[host][path].proxy = [isTlsDevProxy() ? 443 : 80];
903
-
904
- confServer[host][path].port = newInstance(currentPort);
905
- for (const port of confServer[host][path].proxy) {
906
- if (!(port in proxyRouter)) proxyRouter[port] = {};
907
- proxyRouter[port][`${host}${path}`] = {
908
- // target: `http://${host}:${confServer[host][path].port}${path}`,
909
- target: `http://localhost:${confServer[host][path].port}`,
910
- // target: `http://127.0.0.1:${confServer[host][path].port}`,
911
- proxy: confServer[host][path].proxy,
912
- redirect: confServer[host][path].redirect,
913
- host,
914
- path,
915
- };
916
- }
917
- currentPort++;
918
- if (confServer[host][path].peer) {
919
- const peerPath = path === '/' ? `/peer` : `${path}/peer`;
920
- confServer[host][peerPath] = newInstance(confServer[host][path]);
921
- confServer[host][peerPath].port = newInstance(currentPort);
922
- for (const port of confServer[host][path].proxy) {
923
- if (!(port in proxyRouter)) proxyRouter[port] = {};
924
- proxyRouter[port][`${host}${peerPath}`] = {
925
- // target: `http://${host}:${confServer[host][peerPath].port}${peerPath}`,
926
- target: `http://localhost:${confServer[host][peerPath].port}`,
927
- // target: `http://127.0.0.1:${confServer[host][peerPath].port}`,
928
- proxy: confServer[host][peerPath].proxy,
929
- host,
930
- path: peerPath,
931
- };
932
- }
933
- currentPort++;
934
- }
935
- }
936
- }
937
-
938
- return proxyRouter;
939
- };
940
-
941
- /**
942
- * @method pathPortAssignmentFactory
943
- * @description Creates the path port assignment.
944
- * @param {string} deployId - The deploy ID.
945
- * @param {object} router - The router.
946
- * @param {object} confServer - The server configuration.
947
- * @memberof ServerConfBuilder
948
- */
949
- const pathPortAssignmentFactory = async (deployId, router, confServer) => {
950
- const pathPortAssignmentData = {};
951
- for (const host of Object.keys(confServer)) {
952
- const pathPortAssignment = [];
953
- for (const path of Object.keys(confServer[host])) {
954
- const { peer } = confServer[host][path];
955
- if (!router[`${host}${path === '/' ? '' : path}`]) continue;
956
- const port = parseInt(router[`${host}${path === '/' ? '' : path}`].split(':')[2]);
957
- // logger.info('', { host, port, path });
958
- pathPortAssignment.push({
959
- port,
960
- path,
961
- });
962
-
963
- if (peer) {
964
- // logger.info('', { host, port: port + 1, path: '/peer' });
965
- pathPortAssignment.push({
966
- port: port + 1,
967
- path: `${path === '/' ? '' : path}/peer`,
968
- });
969
- }
970
- }
971
- pathPortAssignmentData[host] = pathPortAssignment;
972
- }
973
- if (fs.existsSync(`./engine-private/replica`)) {
974
- const singleReplicas = await fs.readdir(`./engine-private/replica`);
975
- for (let replica of singleReplicas) {
976
- if (replica.startsWith(deployId)) {
977
- const replicaServerConf = loadConfServerJson(`./engine-private/replica/${replica}/conf.server.json`);
978
- for (const host of Object.keys(replicaServerConf)) {
979
- const pathPortAssignment = [];
980
- for (const path of Object.keys(replicaServerConf[host])) {
981
- const { peer } = replicaServerConf[host][path];
982
- if (!router[`${host}${path === '/' ? '' : path}`]) continue;
983
- const port = parseInt(router[`${host}${path === '/' ? '' : path}`].split(':')[2]);
984
- // logger.info('', { host, port, path });
985
- pathPortAssignment.push({
986
- port,
987
- path,
988
- });
989
-
990
- if (peer) {
991
- // logger.info('', { host, port: port + 1, path: '/peer' });
992
- pathPortAssignment.push({
993
- port: port + 1,
994
- path: `${path === '/' ? '' : path}/peer`,
995
- });
996
- }
997
- }
998
- pathPortAssignmentData[host] = pathPortAssignmentData[host].concat(pathPortAssignment);
999
- }
1000
- }
1001
- }
1002
- }
1003
- return pathPortAssignmentData;
1004
- };
1005
-
1006
- /**
1007
- * @method deployRangePortFactory
1008
- * @description Creates the deploy range port factory.
1009
- * @param {object} router - The router.
1010
- * @returns {object} - The deploy range port factory.
1011
- * @memberof ServerConfBuilder
1012
- */
1013
- const deployRangePortFactory = (router) => {
1014
- const ports = Object.values(router).map((p) => parseInt(p.split(':')[2]));
1015
- const fromPort = Math.min(...ports);
1016
- const toPort = Math.max(...ports);
1017
- return { ports, fromPort, toPort };
1018
- };
1019
-
1020
- /**
1021
- * @method buildKindPorts
1022
- * @description Builds the kind ports.
1023
- * @param {number} from - The from port.
1024
- * @param {number} to - The to port.
1025
- * @returns {string} - The kind ports.
1026
- * @memberof ServerConfBuilder
1027
- */
1028
- const buildKindPorts = (from, to) =>
1029
- range(parseInt(from), parseInt(to))
1030
- .map(
1031
- (port) => ` - name: 'tcp-${port}'
1032
- protocol: TCP
1033
- port: ${port}
1034
- targetPort: ${port}
1035
- - name: 'udp-${port}'
1036
- protocol: UDP
1037
- port: ${port}
1038
- targetPort: ${port}
1039
- `,
1040
- )
1041
- .join('\n');
1042
-
1043
- /**
1044
- * @method buildPortProxyRouter
1045
- * @description Builds the port proxy router.
1046
- * @param {object} options - The options.
1047
- * @param {number} [options.port=4000] - The port.
1048
- * @param {object} options.proxyRouter - The proxy router.
1049
- * @param {object} [options.hosts] - The hosts.
1050
- * @param {boolean} [options.orderByPathLength=false] - Whether to order by path length.
1051
- * @param {boolean} [options.devProxyContext=false] - Whether to use dev proxy context.
1052
- * @returns {object} - The port proxy router.
1053
- * @memberof ServerConfBuilder
1054
- */
1055
- const buildPortProxyRouter = (
1056
- options = { port: 4000, proxyRouter, hosts, orderByPathLength: false, devProxyContext: false },
1057
- ) => {
1058
- let { port, proxyRouter, hosts, orderByPathLength } = options;
1059
- hosts = hosts || proxyRouter[port] || {};
1060
-
1061
- const router = {};
1062
- // build router
1063
- Object.keys(hosts).map((hostKey) => {
1064
- let { host, path, target, proxy, peer } = hosts[hostKey];
1065
-
1066
- if (!proxy.includes(port)) {
1067
- logger.warn('Proxy port not set on conf', { port, host, path, proxy, target });
1068
- if (process.env.NODE_ENV === 'production') {
1069
- logger.warn('Omitting host', { host, path, target });
1070
- return;
1071
- }
1072
- }
1073
- // ${process.env.NODE_ENV === 'development' && !isDevProxyContext() ? `:${port}` : ''}
1074
- const absoluteHost = [80, 443].includes(port)
1075
- ? `${host}${path === '/' ? '' : path}`
1076
- : `${host}:${port}${path === '/' ? '' : path}`;
1077
-
1078
- if (absoluteHost in router)
1079
- logger.warn('Overwrite: Absolute host already exists on router', { absoluteHost, target });
1080
-
1081
- if (options.devProxyContext === true) {
1082
- const appDevPort = parseInt(target.split(':')[2]) - process.env.DEV_PROXY_PORT_OFFSET;
1083
- router[absoluteHost] = `http://localhost:${appDevPort}`;
1084
- } else router[absoluteHost] = target;
1085
- }); // order router
1086
-
1087
- if (Object.keys(router).length === 0) return router;
1088
-
1089
- const devApiConfPath = `./engine-private/conf/${process.argv[3]}/conf.server.dev.${process.argv[4]}-dev-api.json`;
1090
- if (options.devProxyContext === true && process.env.NODE_ENV === 'development' && fs.existsSync(devApiConfPath)) {
1091
- const confDevApiServer = JSON.parse(fs.readFileSync(devApiConfPath, 'utf8'));
1092
- let devApiHosts = [];
1093
- let origins = [];
1094
- for (const _host of Object.keys(confDevApiServer))
1095
- for (const _path of Object.keys(confDevApiServer[_host])) {
1096
- if (confDevApiServer[_host][_path].origins && confDevApiServer[_host][_path].origins.length) {
1097
- origins.push(...confDevApiServer[_host][_path].origins);
1098
- if (_path !== 'peer' && devApiHosts.length === 0)
1099
- devApiHosts.push(
1100
- `${_host}${[80, 443].includes(port) && isDevProxyContext() ? '' : `:${port}`}${_path == '/' ? '' : _path}`,
1101
- );
1102
- }
1103
- }
1104
- origins = Array.from(new Set(origins));
1105
- console.log({
1106
- origins,
1107
- devApiHosts,
1108
- });
1109
- for (const devApiHost of devApiHosts) {
1110
- if (devApiHost in router) {
1111
- const target = router[devApiHost];
1112
- delete router[devApiHost];
1113
- router[`${devApiHost}/${process.env.BASE_API}`] = target;
1114
- router[`${devApiHost}/socket.io`] = target;
1115
- for (const origin of origins) router[devApiHost] = origin;
1116
- }
1117
- }
1118
- }
1119
-
1120
- if (orderByPathLength === true) {
1121
- const reOrderRouter = {};
1122
- for (const absoluteHostKey of orderArrayFromAttrInt(Object.keys(router), 'length'))
1123
- reOrderRouter[absoluteHostKey] = router[absoluteHostKey];
1124
- return reOrderRouter;
1125
- }
1126
-
1127
- return router;
1128
- };
1129
-
1130
1014
  /**
1131
1015
  * @method buildReplicaId
1132
1016
  * @description Builds the replica ID.
@@ -1154,17 +1038,7 @@ const getDataDeploy = async (
1154
1038
  disableSyncEnvPort: false,
1155
1039
  },
1156
1040
  ) => {
1157
- let dataDeploy = fs
1158
- .readFileSync(`./engine-private/deploy/dd.router`, 'utf8')
1159
- .split(',')
1160
- .map((deployId) => deployId.trim())
1161
- .filter((deployId) => deployId);
1162
-
1163
- dataDeploy = dataDeploy.map((deployId) => {
1164
- return {
1165
- deployId,
1166
- };
1167
- });
1041
+ let dataDeploy = readDeployRoutes().map((deployId) => ({ deployId }));
1168
1042
 
1169
1043
  if (options && options.buildSingleReplica && fs.existsSync(`./engine-private/replica`))
1170
1044
  fs.removeSync(`./engine-private/replica`);
@@ -1181,7 +1055,7 @@ const getDataDeploy = async (
1181
1055
  for (const path of Object.keys(serverConf[host])) {
1182
1056
  if (!isReplicaDeploy && serverConf[host][path].replicas && serverConf[host][path].singleReplica) {
1183
1057
  if (options && options.buildSingleReplica)
1184
- await Underpost.repo.client(deployObj.deployId, '', host, path, {
1058
+ await Underpost.client.callback(deployObj.deployId, '', host, path, {
1185
1059
  singleReplica: true,
1186
1060
  });
1187
1061
  replicaDataDeploy = replicaDataDeploy.concat(
@@ -1199,7 +1073,7 @@ const getDataDeploy = async (
1199
1073
  }
1200
1074
 
1201
1075
  if (!options.disableSyncEnvPort && options.buildSingleReplica)
1202
- await Underpost.repo.client(undefined, '', '', '', { syncEnvPort: true });
1076
+ await Underpost.client.callback(undefined, '', '', '', { syncEnvPort: true });
1203
1077
 
1204
1078
  logger.info('Deployments configured', buildDataDeploy);
1205
1079
 
@@ -1224,7 +1098,7 @@ const validateTemplatePath = (absolutePath = '') => {
1224
1098
  const clients = DefaultConf.client.default.services;
1225
1099
 
1226
1100
  if (
1227
- absolutePath.match('src/api') &&
1101
+ absolutePath.match('src/api/') &&
1228
1102
  !absolutePath.match('src/api/types.js') &&
1229
1103
  !confServer.apis.find((p) => absolutePath.match(`src/api/${p}/`))
1230
1104
  ) {
@@ -1299,11 +1173,11 @@ const validateTemplatePath = (absolutePath = '') => {
1299
1173
  * @memberof ServerConfBuilder
1300
1174
  */
1301
1175
  const awaitDeployMonitor = async (isFinal = false, deltaMs = 1000, callback = false) => {
1302
- if (!callback) Underpost.env.set('await-deploy', new Date().toISOString());
1176
+ if (!callback) latchAwaitDeploy();
1303
1177
  if (isFinal) logger.info('Final deployment running (no replica)');
1304
1178
  await timer(deltaMs);
1305
- if (Underpost.env.get('container-status') === 'error') return false;
1306
- if (Underpost.env.get('await-deploy')) return await awaitDeployMonitor(false, deltaMs, true);
1179
+ if (getRuntimeStatus() === RUNTIME_STATUS.ERROR) return false;
1180
+ if (isAwaitingDeploy()) return await awaitDeployMonitor(false, deltaMs, true);
1307
1181
  return true;
1308
1182
  };
1309
1183
 
@@ -1431,43 +1305,6 @@ const generateSecurePassword = (length = 16) => {
1431
1305
  return chars.join('');
1432
1306
  };
1433
1307
 
1434
- /**
1435
- * @method getNpmRootPath
1436
- * @description Gets the npm root path.
1437
- * @returns {string} - The npm root path.
1438
- * @memberof ServerConfBuilder
1439
- */
1440
- const getNpmRootPath = () =>
1441
- shellExec(`npm root -g`, {
1442
- stdout: true,
1443
- disableLog: true,
1444
- silent: true,
1445
- }).trim();
1446
-
1447
- /**
1448
- * @method getUnderpostRootPath
1449
- * @description Gets the underpost root path.
1450
- * @returns {string} - The underpost root path.
1451
- * @memberof ServerConfBuilder
1452
- */
1453
- const getUnderpostRootPath = () => `${getNpmRootPath()}/underpost`;
1454
-
1455
- /**
1456
- * @method writeEnv
1457
- * @description Writes the environment variables.
1458
- * @param {string} envPath - The environment path.
1459
- * @param {object} envObj - The environment object.
1460
- * @memberof ServerConfBuilder
1461
- */
1462
- const writeEnv = (envPath, envObj) =>
1463
- fs.writeFileSync(
1464
- envPath,
1465
- Object.keys(envObj)
1466
- .map((key) => `${key}=${envObj[key]}`)
1467
- .join(`\n`),
1468
- 'utf8',
1469
- );
1470
-
1471
1308
  /**
1472
1309
  * @method buildCliDoc
1473
1310
  * @description Scrapes `node bin help` (and `node bin help <command>` for every
@@ -1657,35 +1494,49 @@ const getInstanceContext = async (options = { deployId, singleReplica, replicas,
1657
1494
  * @param {string} options.host - The host.
1658
1495
  * @param {string} options.path - The path.
1659
1496
  * @param {string} options.origin - The origin.
1660
- * @returns {object} - The API configuration.
1497
+ * @returns {Promise<string|undefined>} The derived sub-conf id (`<subConf>-dev-api`), or
1498
+ * undefined when no client origin was given and nothing was derived.
1661
1499
  * @memberof ServerConfBuilder
1662
1500
  */
1663
1501
  const buildApiConf = async (options = { deployId: '', subConf: '', host: '', path: '', origin: '' }) => {
1664
1502
  let { deployId, subConf, host, path, origin } = options;
1665
- if (!deployId) deployId = process.argv[2].trim();
1666
- if (!subConf) subConf = process.argv[3].trim();
1667
- if (process.argv[4]) host = process.argv[4].trim();
1668
- if (process.argv[5]) path = process.argv[5].trim();
1503
+ // The client origin is what selects this mode, so it is resolved before
1504
+ // anything else: `src/api.js` also runs without any of these positionals.
1669
1505
  if (process.argv[6])
1670
1506
  origin = `${process.env.NODE_ENV === 'production' ? 'https' : 'http'}://${process.argv[6].trim()}`;
1671
-
1672
- if (!origin) return;
1507
+ if (!origin) return undefined;
1508
+ if (!deployId) deployId = process.argv[2]?.trim();
1509
+ if (!subConf) subConf = process.argv[3]?.trim();
1510
+ if (process.argv[4]) host = process.argv[4].trim();
1511
+ if (process.argv[5]) path = process.argv[5].trim();
1512
+ if (!deployId || !subConf || !host || !path) {
1513
+ logger.warn('Skipping api conf; a client origin needs deploy-id, sub-conf, host and path', {
1514
+ deployId,
1515
+ subConf,
1516
+ host,
1517
+ path,
1518
+ origin,
1519
+ });
1520
+ return undefined;
1521
+ }
1673
1522
  const confServer = JSON.parse(
1674
1523
  fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.dev.${subConf}.json`, 'utf8'),
1675
1524
  );
1676
1525
  const envObj = dotenv.parse(
1677
1526
  fs.readFileSync(`./engine-private/conf/${deployId}/.env.${process.env.NODE_ENV}`, 'utf8'),
1678
1527
  );
1679
- if (host && path) {
1680
- confServer[host][path].origins = [origin];
1681
- logger.info('Build api conf', { host, path, origin });
1682
- } else return;
1683
- writeEnv(`./engine-private/conf/${deployId}/.env.${process.env.NODE_ENV}.${subConf}-dev-api`, envObj);
1528
+ if (!confServer[host]?.[path])
1529
+ throw new Error(`No ${host}${path} instance in conf.server.dev.${subConf}.json for ${deployId}`);
1530
+ confServer[host][path].origins = [origin];
1531
+ logger.info('Build api conf', { host, path, origin });
1532
+ const devApiSubConf = `${subConf}-dev-api`;
1533
+ writeEnv(`./engine-private/conf/${deployId}/.env.${process.env.NODE_ENV}.${devApiSubConf}`, envObj);
1684
1534
  fs.writeFileSync(
1685
- `./engine-private/conf/${deployId}/conf.server.dev.${subConf}-dev-api.json`,
1535
+ `./engine-private/conf/${deployId}/conf.server.dev.${devApiSubConf}.json`,
1686
1536
  JSON.stringify(confServer, null, 4),
1687
1537
  'utf8',
1688
1538
  );
1539
+ return devApiSubConf;
1689
1540
  };
1690
1541
 
1691
1542
  /**
@@ -1709,12 +1560,19 @@ const buildClientStaticConf = async (
1709
1560
  if (!subConf) subConf = process.argv[3].trim();
1710
1561
  if (!host) host = process.argv[4].trim();
1711
1562
  if (!path) path = process.argv[5].trim();
1712
- const confServer = JSON.parse(
1713
- fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.dev.${subConf}-dev-api.json`, 'utf8'),
1714
- );
1715
- const envObj = dotenv.parse(
1716
- fs.readFileSync(`./engine-private/conf/${deployId}/.env.${process.env.NODE_ENV}.${subConf}-dev-api`, 'utf8'),
1717
- );
1563
+ // The API server derives these two files, and it derives them only when it is
1564
+ // given the client origin. Their absence means that step has not run, which is
1565
+ // a setup order to state rather than an ENOENT to decode.
1566
+ const confPath = `./engine-private/conf/${deployId}/conf.server.dev.${subConf}-dev-api.json`;
1567
+ const envPath = `./engine-private/conf/${deployId}/.env.${process.env.NODE_ENV}.${subConf}-dev-api`;
1568
+ for (const source of [confPath, envPath])
1569
+ if (!fs.existsSync(source))
1570
+ throw new Error(
1571
+ `Missing ${source}. Start the API server first, with the client origin as its last argument: ` +
1572
+ `npm run dev:api ${deployId} ${subConf} ${host} ${path} <client-host:port>`,
1573
+ );
1574
+ const confServer = JSON.parse(fs.readFileSync(confPath, 'utf8'));
1575
+ const envObj = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
1718
1576
  envObj.PORT = parseInt(envObj.PORT);
1719
1577
  const apiBaseHost = devProxy
1720
1578
  ? devProxyHostFactory({ host, tls: isTlsDevProxy() })
@@ -1733,16 +1591,6 @@ const buildClientStaticConf = async (
1733
1591
  );
1734
1592
  };
1735
1593
 
1736
- /**
1737
- * @method isDeployRunnerContext
1738
- * @description Checks if the deploy runner context is valid.
1739
- * @param {string} path - The path.
1740
- * @param {object} options - The options.
1741
- * @returns {boolean} - The deploy runner context.
1742
- * @memberof ServerConfBuilder
1743
- */
1744
- const isDeployRunnerContext = (path, options) => !options.build && path && path !== 'template-deploy';
1745
-
1746
1594
  /**
1747
1595
  * @method isDevProxyContext
1748
1596
  * @description Checks if the dev proxy context is valid.
@@ -1953,7 +1801,7 @@ const dispatchBuildInstanceEnv = ({
1953
1801
  const loadProjectInstanceEnvBuilder = async (deployId) => {
1954
1802
  const match = /^dd-([a-z0-9][a-z0-9-]*)$/.exec(`${deployId || ''}`);
1955
1803
  if (!match) return null;
1956
- const moduleUrl = new URL(`../projects/${match[1]}/instance-data.js`, import.meta.url);
1804
+ const moduleUrl = new URL(`../../projects/${match[1]}/instance-data.js`, import.meta.url);
1957
1805
  if (!fs.existsSync(moduleUrl)) return null;
1958
1806
  const projectModule = await import(moduleUrl.href);
1959
1807
  if (projectModule.buildInstanceEnv === undefined) return null;
@@ -2475,6 +2323,56 @@ const curlStatusChainFactory = (raw = '') => {
2475
2323
  return chain;
2476
2324
  };
2477
2325
 
2326
+ /**
2327
+ * @method publicIngressProbeFactory
2328
+ * @description Probes one public URL the way the traffic report does.
2329
+ *
2330
+ * `-L` follows the real redirect chain, `-v` supplies one response line per hop,
2331
+ * `-i` keeps full response headers available, and `-s` removes only the progress
2332
+ * meter. The body is discarded so a report stays bounded even when a host
2333
+ * returns a large application page. One implementation, because the traffic
2334
+ * table and the ingress event have to agree on what "reachable" means.
2335
+ * @param {string} url - Absolute URL to probe.
2336
+ * @returns {Array<string>} Ordered response codes; `['000']` when nothing answered.
2337
+ * @memberof ServerConfBuilder
2338
+ */
2339
+ const publicIngressProbeFactory = (url) => {
2340
+ if (!url) return ['000'];
2341
+ const argument = shellArgumentFactory(url);
2342
+ return curlStatusChainFactory(
2343
+ shellExec(
2344
+ `curl -L -v -i -s --connect-timeout 3 --max-time 12 --max-redirs 10 ` +
2345
+ `-o /dev/null -w '\nUNDERPOST_CURL_FINAL=%{http_code}\n' ${argument} 2>&1 || true`,
2346
+ { stdout: true, silent: true, silentOnError: true, disableLog: true },
2347
+ ),
2348
+ );
2349
+ };
2350
+
2351
+ /**
2352
+ * @method publicIngressUrlsFactory
2353
+ * @description Every public URL a deploy list publishes, from the same conf the
2354
+ * traffic report reads.
2355
+ * @param {string[]} [deployList] - Deploy ids.
2356
+ * @param {string} [env='production'] - Environment whose routes are published.
2357
+ * @returns {Array<{host: string, path: string, url: string}>} Distinct routable URLs.
2358
+ * @memberof ServerConfBuilder
2359
+ */
2360
+ const publicIngressUrlsFactory = (deployList = [], env = 'production') => {
2361
+ const urls = new Map();
2362
+ for (const deployId of deployList)
2363
+ for (const entry of deployTrafficEntriesFactory({ deployId, env }))
2364
+ for (const routePath of `${entry.path || '/'}`.split(' ').filter(Boolean)) {
2365
+ const path = routePath.startsWith('/') ? routePath : `/${routePath}`;
2366
+ try {
2367
+ const url = new URL(path, `https://${entry.host}`).href;
2368
+ if (!urls.has(url)) urls.set(url, { host: entry.host, path, url });
2369
+ } catch {
2370
+ /* a host the conf cannot turn into a URL is not routable */
2371
+ }
2372
+ }
2373
+ return [...urls.values()];
2374
+ };
2375
+
2478
2376
  /**
2479
2377
  * @method trafficTableRowsFactory
2480
2378
  * @description Resolves the live colour of each routable deployment, optionally
@@ -2557,6 +2455,25 @@ const hostRenderInstancesFactory = ({ declared = [], preserved = [], isDeployed
2557
2455
  const isTrafficServingFactory = ({ liveTraffic = '', hasReadyEndpoints = () => false }) =>
2558
2456
  !!liveTraffic && hasReadyEndpoints(liveTraffic);
2559
2457
 
2458
+ /**
2459
+ * @method redeployPlanFactory
2460
+ * @description The colour a redeploy targets, and how that colour is brought up.
2461
+ *
2462
+ * The inactive colour has no Deployment for most of a blue/green cycle: `run sync`
2463
+ * deletes it before publishing the maintenance route, and a promote leaves only
2464
+ * the colour it routed to. So a `rollout restart` of the target is a NotFound
2465
+ * error on exactly the state blue/green is meant to be in, and its absence has to
2466
+ * select building that colour instead of failing the deploy.
2467
+ * @param {string} [liveTraffic] - Colour currently routed, or empty when none is.
2468
+ * @param {Function} hasDeployment - `(colour) => boolean`.
2469
+ * @returns {{targetTraffic: string, create: boolean}} Colour to deploy, and whether it must be created rather than restarted.
2470
+ * @memberof ServerConfBuilder
2471
+ */
2472
+ const redeployPlanFactory = ({ liveTraffic = '', hasDeployment = () => false }) => {
2473
+ const targetTraffic = nextTrafficFactory(liveTraffic);
2474
+ return { targetTraffic, create: !hasDeployment(targetTraffic) };
2475
+ };
2476
+
2560
2477
  /**
2561
2478
  * @method instanceTrafficPlanFactory
2562
2479
  * @description Resolves, for each instance, the colour routed now and the colour
@@ -3024,32 +2941,14 @@ ${renderHosts}`,
3024
2941
  return { renderHosts, changed: true };
3025
2942
  };
3026
2943
 
3027
- /**
3028
- * Resolves the concrete deploy ids a build or conf-sync run should iterate over.
3029
- *
3030
- * The meta deploy id `dd` fans out to the comma separated ids declared in
3031
- * `engine-private/deploy/dd.router`; any other value is parsed as a comma separated list.
3032
- * Entries are trimmed and empties dropped.
3033
- *
3034
- * @method resolveDeployList
3035
- * @param {string} deployId - A deploy id, a comma separated list, or the `dd` meta id.
3036
- * @returns {string[]} Ordered list of concrete deploy ids.
3037
- * @memberof ServerConfBuilder
3038
- */
3039
- const resolveDeployList = (deployId) =>
3040
- (deployId === 'dd' ? fs.readFileSync('./engine-private/deploy/dd.router', 'utf8') : deployId)
3041
- .split(',')
3042
- .map((id) => id.trim())
3043
- .filter(Boolean);
3044
-
3045
2944
  /**
3046
2945
  * Syncs a single deploy id's private configuration into its dedicated
3047
2946
  * `engine-<suffix>-private` repository and pushes the result.
3048
2947
  *
3049
2948
  * Idempotent and safe to rerun: the private repo is cloned when missing or reset to a clean
3050
- * checkout when present, then the deploy id's `conf` folder, matching `replica` and
3051
- * `itc-scripts` entries, and any caller-supplied `extraPaths` payloads are mirrored. The
3052
- * commit/push step is a no-op when nothing changed (`silentOnError`).
2949
+ * checkout when present, then the deploy id's `conf` folder (its `.env.<env>.oci` runtime
2950
+ * overlays included), matching `replica` entries, and any caller-supplied `extraPaths` payloads
2951
+ * are mirrored. The commit/push step is a no-op when nothing changed (`silentOnError`).
3053
2952
  *
3054
2953
  * @method syncPrivateConf
3055
2954
  * @param {string} deployId - A concrete deploy id (e.g. `dd-cyberia`), not the `dd` meta id.
@@ -3059,10 +2958,10 @@ const resolveDeployList = (deployId) =>
3059
2958
  * @memberof ServerConfBuilder
3060
2959
  */
3061
2960
  const syncPrivateConf = (deployId, extraPaths = []) => {
3062
- const suffix = deployId.split('dd-')[1];
3063
- const privateRepoName = `engine-${suffix}-private`;
2961
+ const privateRepoName = Underpost.repo.privateRepoFactory(deployId);
3064
2962
  const privateGitUri = `${process.env.GITHUB_USERNAME}/${privateRepoName}`;
3065
2963
  const privateRepoPath = `../${privateRepoName}`;
2964
+ const cronDeployId = cronDeployIdResolve();
3066
2965
 
3067
2966
  if (!fs.existsSync(privateRepoPath)) {
3068
2967
  shellExec(`cd .. && underpost clone ${privateGitUri}`, { silent: true });
@@ -3077,14 +2976,16 @@ const syncPrivateConf = (deployId, extraPaths = []) => {
3077
2976
  fs.removeSync(confDest);
3078
2977
  fs.mkdirSync(confDest, { recursive: true });
3079
2978
  fs.copySync(`./engine-private/conf/${deployId}`, confDest);
3080
-
2979
+ // A checkout that declares no cron deploy has no `conf/<cron-id>` to mirror. Without this
2980
+ // the path resolves to `conf/null` and the whole sync fails over an absent optional.
2981
+ if (cronDeployId) fs.copySync(`./engine-private/conf/${cronDeployId}`, `${privateRepoPath}/conf/${cronDeployId}`);
2982
+ fs.copySync(`./engine-private/deploy`, `${privateRepoPath}/deploy`);
3081
2983
  fs.removeSync(`${privateRepoPath}/replica`);
3082
- for (const payloadDir of ['replica', 'itc-scripts']) {
3083
- const srcDir = `./engine-private/${payloadDir}`;
3084
- if (!fs.existsSync(srcDir)) continue;
3085
- for (const entry of fs.readdirSync(srcDir))
3086
- if (entry.match(deployId)) fs.copySync(`${srcDir}/${entry}`, `${privateRepoPath}/${payloadDir}/${entry}`);
3087
- }
2984
+ fs.removeSync(`${privateRepoPath}/logs`);
2985
+ const replicaSrcDir = `./engine-private/replica`;
2986
+ if (fs.existsSync(replicaSrcDir))
2987
+ for (const entry of fs.readdirSync(replicaSrcDir))
2988
+ if (entry.match(deployId)) fs.copySync(`${replicaSrcDir}/${entry}`, `${privateRepoPath}/replica/${entry}`);
3088
2989
 
3089
2990
  for (const extraPath of extraPaths) fs.copySync(`./engine-private/${extraPath}`, `${privateRepoPath}/${extraPath}`);
3090
2991
 
@@ -3117,31 +3018,111 @@ const syncDeployIdSources = (sourceMoves = []) => {
3117
3018
  return true;
3118
3019
  };
3119
3020
 
3021
+ const TEMPLATE_REPOSITORY_NAME = 'pwa-microservices-template';
3022
+
3023
+ /**
3024
+ * @method gitOriginRepositoryName
3025
+ * @description Repository name a checkout's `origin` points at, without the `.git`
3026
+ * suffix, or `null` when the path is not a work tree or declares no origin.
3027
+ * @param {string} repoPath - Local repository path.
3028
+ * @returns {string|null}
3029
+ * @memberof ServerConfBuilder
3030
+ */
3031
+ const gitOriginRepositoryName = (repoPath) => {
3032
+ if (!fs.existsSync(`${repoPath}/.git`)) return null;
3033
+ const url = shellExec(`cd ${repoPath} && git config --get remote.origin.url`, {
3034
+ stdout: true,
3035
+ silent: true,
3036
+ disableLog: true,
3037
+ silentOnError: true,
3038
+ });
3039
+ const name = typeof url === 'string' ? url.trim().split('/').pop() : '';
3040
+ return name ? name.replace(/\.git$/, '') : null;
3041
+ };
3042
+
3043
+ /**
3044
+ * @method ensureTemplateCheckout
3045
+ * @description Guarantees `toPath` is a checkout of the template repository, cloning it
3046
+ * when it is missing or belongs to another repository.
3047
+ *
3048
+ * Publishing steps swap the checkout's `.git` for a product repository's history, so an
3049
+ * assembly that trusted whatever `.git` it found would read another product's identity
3050
+ * back into the base template `package.json`.
3051
+ *
3052
+ * @param {object} params
3053
+ * @param {string} params.toPath - Template checkout path.
3054
+ * @param {string} [params.githubUsername] - Owner of the template repository.
3055
+ * @param {boolean} [params.noClone=false] - Fail instead of cloning a missing/foreign checkout.
3056
+ * @returns {boolean} `true` when the checkout was restored.
3057
+ * @memberof ServerConfBuilder
3058
+ */
3059
+ const ensureTemplateCheckout = ({ toPath, githubUsername, noClone = false }) => {
3060
+ const owner = githubUsername || 'underpostnet';
3061
+ const repository = `${owner}/${TEMPLATE_REPOSITORY_NAME}`;
3062
+ if (gitOriginRepositoryName(toPath) === TEMPLATE_REPOSITORY_NAME) return false;
3063
+ if (noClone) throw new Error(`buildTemplate: ${toPath} is not a ${repository} checkout (--no-clone)`);
3064
+ logger.warn('Restoring template checkout', { toPath, repository });
3065
+
3066
+ // Swap in the clone only once it is complete, so a failed one leaves the checkout as is.
3067
+ const templatePath = dir.resolve(toPath);
3068
+ const stagePath = fs.mkdtempSync(`${dir.dirname(templatePath)}/.${TEMPLATE_REPOSITORY_NAME}-`);
3069
+ try {
3070
+ shellExec(`cd ${stagePath} && node ${dir.resolve('./bin')} clone ${repository}`);
3071
+ const clonePath = `${stagePath}/${TEMPLATE_REPOSITORY_NAME}`;
3072
+ if (gitOriginRepositoryName(clonePath) !== TEMPLATE_REPOSITORY_NAME)
3073
+ throw new Error(`buildTemplate: could not clone ${repository}`);
3074
+ fs.removeSync(templatePath);
3075
+ fs.moveSync(clonePath, templatePath);
3076
+ } finally {
3077
+ fs.removeSync(stagePath);
3078
+ }
3079
+ return true;
3080
+ };
3081
+
3082
+ /**
3083
+ * @method pruneTemplateWorkTree
3084
+ * @description Empties the template checkout of everything a build reconstructs, so a
3085
+ * source renamed or deleted in the engine — or a client bundle belonging to another
3086
+ * deploy id — cannot survive from a previous run into the assembled output.
3087
+ * @param {string} toPath - Template checkout path.
3088
+ * @param {string[]} preservedEntries - Top level entries to keep (git history, installed modules).
3089
+ * @returns {void}
3090
+ * @memberof ServerConfBuilder
3091
+ */
3092
+ const pruneTemplateWorkTree = (toPath, preservedEntries = []) => {
3093
+ for (const entry of fs.readdirSync(toPath)) {
3094
+ if (preservedEntries.includes(entry)) continue;
3095
+ fs.removeSync(`${toPath}/${entry}`);
3096
+ }
3097
+ };
3098
+
3120
3099
  /**
3121
3100
  * Rebuilds the standalone `pwa-microservices-template` from scratch out of the current
3122
3101
  * engine source tree.
3123
3102
  *
3124
- * Clones the template repo next to the engine when missing, otherwise resets it to a clean
3125
- * pristine checkout, then syncs every engine-tracked file the template is allowed to carry
3103
+ * Clones the template repo next to the engine when the checkout is missing or belongs to
3104
+ * another repository, empties it so the result cannot inherit anything from a previous run,
3105
+ * then syncs every engine-tracked file the template is allowed to carry
3126
3106
  * ({@link validateTemplatePath}), strips engine-only + product modules, restores the template's
3127
3107
  * own CI workflows + guest services, and rewrites `package.json` / `package-lock.json` / `README`
3128
3108
  * so the result is a standalone, installable project. Throws on failure; callers own exit codes.
3129
3109
  *
3130
- * Product catalogs are read dynamically ({@link module:src/server/catalog} `loadProductCatalogs`),
3110
+ * Product catalogs are read dynamically ({@link module:src/server/build/catalog.js} `loadProductCatalogs`),
3131
3111
  * so this stays decoupled from — and survives removal of — any product module.
3132
3112
  *
3133
3113
  * @method buildTemplate
3134
3114
  * @param {object} [options]
3135
3115
  * @param {string} [options.srcPath='./'] - Engine source root to sync from.
3136
3116
  * @param {string} [options.toPath='../pwa-microservices-template'] - Template output path.
3117
+ * @param {boolean} [options.noClone=false] - Fail instead of cloning when the checkout is missing or foreign.
3137
3118
  * @returns {Promise<void>}
3138
3119
  * @memberof ServerConfBuilder
3139
3120
  */
3140
- const buildTemplate = async ({ srcPath = './', toPath = '../pwa-microservices-template' } = {}) => {
3121
+ const buildTemplate = async ({ srcPath = './', toPath = '../pwa-microservices-template', noClone = false } = {}) => {
3141
3122
  const walk = (await import('ignore-walk')).default;
3142
- const { TEMPLATE_RESTORE_PATHS, TEMPLATE_KEYWORDS, TEMPLATE_DESCRIPTION } =
3143
- await import('../projects/underpost/catalog-underpost.js');
3144
- const { loadProductCatalogs } = await import('./catalog.js');
3123
+ const { TEMPLATE_RESTORE_PATHS, TEMPLATE_PRESERVED_ENTRIES, TEMPLATE_KEYWORDS, TEMPLATE_DESCRIPTION } =
3124
+ await import('../../projects/underpost/catalog-underpost.js');
3125
+ const { loadProductCatalogs } = await import('../build/catalog.js');
3145
3126
  const githubUsername = process.env.GITHUB_USERNAME;
3146
3127
 
3147
3128
  logger.info('Build template', { srcPath, toPath });
@@ -3154,10 +3135,9 @@ const buildTemplate = async ({ srcPath = './', toPath = '../pwa-microservices-te
3154
3135
  )
3155
3136
  ).filter((p) => !p.startsWith('.git'));
3156
3137
 
3157
- fs.removeSync(`${githubUsername}/pwa-microservices-template`);
3158
- shellExec(`cd .. && node engine/bin clone ${githubUsername}/pwa-microservices-template`);
3159
-
3138
+ ensureTemplateCheckout({ toPath, githubUsername, noClone });
3160
3139
  shellExec(`cd ${toPath} && git config core.filemode false`);
3140
+ pruneTemplateWorkTree(toPath, TEMPLATE_PRESERVED_ENTRIES);
3161
3141
 
3162
3142
  for (const copyPath of sourceFiles) {
3163
3143
  if (copyPath === 'NaN') continue;
@@ -3186,6 +3166,7 @@ const buildTemplate = async ({ srcPath = './', toPath = '../pwa-microservices-te
3186
3166
  }
3187
3167
  shellExec(`rm -rf ${toPath}/.github`);
3188
3168
  shellExec(`rm -rf ${toPath}/manifests/deployment/dd-*`);
3169
+ shellExec(`rm -rf ${toPath}/deploy`);
3189
3170
 
3190
3171
  fs.mkdirSync(`${toPath}/.github/workflows`, { recursive: true });
3191
3172
  for (const restorePath of TEMPLATE_RESTORE_PATHS) {
@@ -3265,7 +3246,8 @@ git add .`);
3265
3246
  * `engine-<idPart>`). A pod started with `underpost start --build --private-test-repo`
3266
3247
  * clones this repo, so work-in-progress engine source can be tested end to end
3267
3248
  * without touching the production source. Mirrors {@link updatePrivateTemplateRepo}
3268
- * but per-deploy-id and against the test repo.
3249
+ * but per-deploy-id and against the test repo, publishing from its own work tree
3250
+ * (`<template parent>/engine-test-<idPart>`) so the template checkout keeps its identity.
3269
3251
  *
3270
3252
  * Assumes the deploy id template has already been assembled at the template path
3271
3253
  * (run `node bin/build <deployId>` first, or use `node bin/build <deployId> --update-private`).
@@ -3275,39 +3257,53 @@ git add .`);
3275
3257
  */
3276
3258
  const updatePrivateEngineTestRepo = async (deployId) => {
3277
3259
  const username = process.env.GITHUB_USERNAME || 'underpostnet';
3278
- const repoName = `engine-test-${deployId.split('-')[1]}`;
3260
+ const repoName = Underpost.repo.engineRepoFactory(deployId, { test: true });
3279
3261
  const templatePath = '/home/dd/pwa-microservices-template';
3280
3262
  if (!fs.existsSync(templatePath))
3281
3263
  throw new Error(`updatePrivateEngineTestRepo: assemble the template first (node bin/build ${deployId})`);
3282
3264
 
3283
- // Detach the assembled working tree from any engine-build git history.
3284
- shellExec(`sudo rm -rf ${templatePath}/.git`);
3285
-
3286
3265
  // Adopt the test repo's existing history when present (so the push is a delta);
3287
3266
  // otherwise publish a fresh history on first push.
3288
- shellExec(`cd /home/dd && sudo rm -rf ./${repoName}.git && underpost clone --bare ${username}/${repoName}`, {
3267
+ const parentPath = dir.dirname(templatePath);
3268
+ shellExec(`cd ${parentPath} && sudo rm -rf ./${repoName}.git && underpost clone --bare ${username}/${repoName}`, {
3289
3269
  silent: true,
3290
3270
  disableLog: true,
3291
3271
  silentOnError: true,
3292
3272
  });
3293
- if (fs.existsSync(`/home/dd/${repoName}.git`)) shellExec(`mv /home/dd/${repoName}.git ${templatePath}/.git`);
3273
+
3274
+ // Publishing from a work tree of its own leaves the template checkout its own git
3275
+ // history, and copying that tree from 0 keeps a stale file out of the published one.
3276
+ const publishPath = `${parentPath}/${repoName}`;
3277
+ shellExec(`sudo rm -rf ${publishPath}`);
3278
+ fs.copySync(templatePath, publishPath, {
3279
+ filter: (src) => {
3280
+ const entries = dir.relative(templatePath, src).split(dir.sep);
3281
+ return !entries.includes('.git') && !entries.includes('node_modules');
3282
+ },
3283
+ });
3284
+ if (fs.existsSync(`${parentPath}/${repoName}.git`)) shellExec(`mv ${parentPath}/${repoName}.git ${publishPath}/.git`);
3294
3285
 
3295
3286
  // `git init` converts the moved bare repo into a normal work-tree repo (bare
3296
3287
  // clones have no work tree, so `git add` would fail), and bootstraps a fresh
3297
3288
  // repo on first publish. Idempotent — mirrors updatePrivateTemplateRepo.
3298
- shellExec(`cd ${templatePath}
3289
+ shellExec(`cd ${publishPath}
3299
3290
  git init
3300
3291
  git config user.name '${username}'
3301
3292
  git config user.email 'development@underpost.net'
3302
- git add .`);
3293
+ git add -A .`);
3303
3294
 
3304
- const hasChanges = shellExec(`node bin cmt ${templatePath} --has-changes`, {
3295
+ // The bundled coverage report is a build output the artifact has to carry: force-added so a
3296
+ // tree-level ignore rule can never silently drop it from the published source.
3297
+ if (fs.existsSync(`${publishPath}/${COVERAGE_BUNDLE_DIRECTORY}`))
3298
+ shellExec(`cd ${publishPath} && git add -A -f ${COVERAGE_BUNDLE_DIRECTORY}`);
3299
+
3300
+ const hasChanges = shellExec(`node bin cmt ${publishPath} --has-changes`, {
3305
3301
  stdout: true,
3306
3302
  silent: true,
3307
3303
  disableLog: true,
3308
3304
  }).trim();
3309
3305
  if (hasChanges === '1')
3310
- shellExec(`cd ${templatePath} && git commit -m 'Update ${repoName}' && underpost push . ${username}/${repoName}`);
3306
+ shellExec(`cd ${publishPath} && git commit -m 'Update ${repoName}' && underpost push . ${username}/${repoName}`);
3311
3307
  else logger.info('No changes to publish', { repoName });
3312
3308
  };
3313
3309
 
@@ -3328,12 +3324,15 @@ git add .`);
3328
3324
  * instance runs — `/etc/hosts` is written in one pass for every host the
3329
3325
  * gateway will serve.
3330
3326
  * @param {Array<string>} deployList - Deploy ids being brought up.
3331
- * @param {string} [instanceList] - `+`-separated instance/template ids.
3327
+ * @param {string} [instanceList] - Comma-separated instance/template ids, as `--instance-id` takes them.
3332
3328
  * @returns {{ byDeployId: Object<string,{ids: Array<string>, hosts: Array<string>}>, unmatched: Array<string> }}
3333
3329
  * Per-deploy selection, and the requested ids no deploy declares.
3334
3330
  */
3335
3331
  const clusterInstancesFactory = (deployList = [], instanceList = '') => {
3336
- const requested = `${instanceList || ''}`.split('+').filter((id) => id.trim());
3332
+ const requested = `${instanceList || ''}`
3333
+ .split(',')
3334
+ .map((id) => id.trim())
3335
+ .filter(Boolean);
3337
3336
  const byDeployId = Object.fromEntries(
3338
3337
  deployList.map((deployId) => {
3339
3338
  const confPath = `./engine-private/conf/${deployId}/conf.instances.json`;
@@ -3358,6 +3357,10 @@ const clusterInstancesFactory = (deployList = [], instanceList = '') => {
3358
3357
  };
3359
3358
 
3360
3359
  export {
3360
+ cleanDeployEnvFiles,
3361
+ deployEnvFilePath,
3362
+ deployEnvNamesFactory,
3363
+ instanceEnvFilePath,
3361
3364
  Config,
3362
3365
  loadConf,
3363
3366
  loadConfInstances,
@@ -3377,28 +3380,19 @@ export {
3377
3380
  addWsConf,
3378
3381
  buildWsSrc,
3379
3382
  cloneSrcComponents,
3380
- buildProxyRouter,
3381
3383
  getDataDeploy,
3382
3384
  validateTemplatePath,
3383
3385
  buildReplicaId,
3384
3386
  mergeFile,
3385
3387
  getPathsSSR,
3386
- buildKindPorts,
3387
- buildPortProxyRouter,
3388
3388
  splitFileFactory,
3389
3389
  generateSecurePassword,
3390
3390
  resolveReplicaCount,
3391
- getNpmRootPath,
3392
- getUnderpostRootPath,
3393
- writeEnv,
3394
- pathPortAssignmentFactory,
3395
- deployRangePortFactory,
3396
3391
  awaitDeployMonitor,
3397
3392
  buildCliDoc,
3398
3393
  getInstanceContext,
3399
3394
  buildApiConf,
3400
3395
  buildClientStaticConf,
3401
- isDeployRunnerContext,
3402
3396
  isDevProxyContext,
3403
3397
  devProxyHostFactory,
3404
3398
  isTlsDevProxy,
@@ -3408,10 +3402,12 @@ export {
3408
3402
  loadConfServerJson,
3409
3403
  getConfFolder,
3410
3404
  getConfFilePath,
3405
+ deployEnvContentFactory,
3406
+ deployOciEnvFilePath,
3407
+ ociEnvContentFactory,
3408
+ OCI_ENV_SUFFIX,
3411
3409
  readConfJson,
3412
3410
  DEFAULT_DEPLOY_ID,
3413
- loadCronDeployEnv,
3414
- cronDeployIdResolve,
3415
3411
  clusterContextFactory,
3416
3412
  clusterTypeFactory,
3417
3413
  exposeTcpPortsFactory,
@@ -3434,10 +3430,13 @@ export {
3434
3430
  trafficProbePathsFactory,
3435
3431
  hostIngressFactsFactory,
3436
3432
  curlStatusChainFactory,
3433
+ publicIngressProbeFactory,
3434
+ publicIngressUrlsFactory,
3437
3435
  hostRenderInstancesFactory,
3438
3436
  instanceTrafficPlanFactory,
3439
3437
  isTrafficServingFactory,
3440
3438
  nextTrafficFactory,
3439
+ redeployPlanFactory,
3441
3440
  schedulableNodeFactory,
3442
3441
  stopPlanFactory,
3443
3442
  trafficFromRoutingInfoFactory,
@@ -3445,9 +3444,11 @@ export {
3445
3444
  resolveEnvScoped,
3446
3445
  sortInstancesByPath,
3447
3446
  waitForPort,
3448
- resolveDeployList,
3449
3447
  syncPrivateConf,
3450
3448
  syncDeployIdSources,
3449
+ gitOriginRepositoryName,
3450
+ ensureTemplateCheckout,
3451
+ pruneTemplateWorkTree,
3451
3452
  buildTemplate,
3452
3453
  updatePrivateTemplateRepo,
3453
3454
  updatePrivateEngineTestRepo,