@underpostnet/cyberia 3.3.0 → 3.3.76

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 (568) hide show
  1. package/.dockerignore +1 -0
  2. package/.env.example +17 -5
  3. package/.github/workflows/coverall.cyberia.ci.yml +39 -14
  4. package/.github/workflows/cyberia-client.cd.yml +5 -0
  5. package/.github/workflows/cyberia-server.cd.yml +5 -0
  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 +3 -0
  13. package/.github/workflows/engine-cyberia.ci.yml +4 -5
  14. package/.github/workflows/ghpkg.ci.yml +35 -20
  15. package/.github/workflows/hardhat.ci.yml +12 -11
  16. package/.github/workflows/npmpkg.ci.yml +17 -15
  17. package/.github/workflows/publish.cyberia.ci.yml +44 -0
  18. package/.github/workflows/pwa-microservices-template-test.ci.yml +2 -2
  19. package/.github/workflows/release.cd.yml +1 -0
  20. package/.prettierignore +1 -0
  21. package/AGENTS.md +96 -0
  22. package/CHANGELOG.md +653 -2513
  23. package/CLI-HELP.md +323 -157
  24. package/Dockerfile +25 -9
  25. package/Dockerfile.dev +25 -9
  26. package/Dockerfile.test +5 -5
  27. package/README.md +48 -28
  28. package/bin/build.js +86 -41
  29. package/bin/build.template.js +8 -3
  30. package/bin/cyberia.js +1494 -1403
  31. package/bin/deploy.js +18 -104
  32. package/bin/index.js +1494 -1403
  33. package/compose.env +17 -5
  34. package/conf.js +98 -7
  35. package/deploy/cyberia-client/deploy.sh +46 -0
  36. package/deploy/cyberia-client/package.sh +17 -0
  37. package/deploy/cyberia-client/state.sh +15 -0
  38. package/deploy/cyberia-server/deploy.sh +46 -0
  39. package/deploy/cyberia-server/package.sh +17 -0
  40. package/deploy/cyberia-server/state.sh +15 -0
  41. package/deploy/dd-cyberia/deploy.sh +9 -29
  42. package/deploy/dd-cyberia/init.sh +38 -45
  43. package/deploy/dd-cyberia/package.sh +16 -0
  44. package/deploy/dd-cyberia/state.sh +14 -0
  45. package/deploy/dd-cyberia/sync-deploy.sh +133 -147
  46. package/deploy/lib/config.sh +29 -0
  47. package/deploy/lib/github-actions-logging.sh +610 -0
  48. package/deploy/lib/host.sh +194 -0
  49. package/deploy/lib/state.sh +65 -0
  50. package/deploy/pwa-microservices-template/deploy.sh +17 -42
  51. package/deploy/release/deploy.sh +9 -41
  52. package/deployment.yaml +7 -2
  53. package/docker-compose.yml +23 -17
  54. package/examples/static-page/README.md +19 -19
  55. package/examples/static-page/ssr-components/CustomPage.js +2 -2
  56. package/hardhat/hardhat.config.js +1 -6
  57. package/hardhat/package-lock.json +101 -40
  58. package/hardhat/package.json +2 -2
  59. package/manifests/alertmanager/deployment.yaml +81 -0
  60. package/manifests/blackbox-exporter/deployment.yaml +75 -0
  61. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +32 -7
  62. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +32 -7
  63. package/manifests/deployment/dd-cyberia-development/deployment.yaml +7 -2
  64. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  65. package/manifests/deployment/mongo-express/deployment.yaml +46 -27
  66. package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
  67. package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
  68. package/manifests/deployment/mongo-express/service.yaml +16 -0
  69. package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
  70. package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
  71. package/manifests/grafana/deployment.yaml +26 -7
  72. package/manifests/grafana/kustomization.yaml +1 -0
  73. package/manifests/grafana/service.yaml +6 -1
  74. package/manifests/mariadb/pv.yaml +1 -0
  75. package/manifests/mongodb/statefulset.yaml +9 -2
  76. package/manifests/mysql/pv-pvc.yaml +14 -1
  77. package/manifests/postgresql/pv.yaml +12 -1
  78. package/manifests/postgresql/pvc.yaml +1 -0
  79. package/manifests/prometheus/deployment.yaml +18 -22
  80. package/package.json +87 -12
  81. package/scripts/event-monitor.sh +56 -0
  82. package/scripts/k3s-node-setup.sh +27 -14
  83. package/scripts/kubeadm-node-setup.sh +31 -11
  84. package/scripts/maas-setup.sh +8 -8
  85. package/scripts/selinux-normalize.sh +907 -0
  86. package/scripts/shutdown-machine.sh +843 -0
  87. package/scripts/test-monitor.sh +5 -5
  88. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +15 -10
  89. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
  90. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +7 -2
  91. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +90 -186
  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 +3 -3
  99. package/src/api/cyberia-action/cyberia-action.service.js +5 -6
  100. package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
  101. package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
  102. package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
  103. package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
  104. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +2 -4
  105. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
  106. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +13 -35
  107. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +17 -34
  108. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +1 -1
  109. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
  110. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +2 -2
  111. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +2 -2
  112. package/src/api/cyberia-entity/cyberia-entity.controller.js +1 -1
  113. package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
  114. package/src/api/cyberia-entity/cyberia-entity.router.js +1 -1
  115. package/src/api/cyberia-entity/cyberia-entity.service.js +3 -3
  116. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
  117. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
  118. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +17 -2
  119. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +468 -5
  120. package/src/api/cyberia-instance/cyberia-fallback-capture.js +502 -0
  121. package/src/api/cyberia-instance/cyberia-fallback-world.js +29 -40
  122. package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
  123. package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
  124. package/src/api/cyberia-instance/cyberia-instance-map.service.js +35 -142
  125. package/src/api/cyberia-instance/cyberia-instance.controller.js +2 -5
  126. package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
  127. package/src/api/cyberia-instance/cyberia-instance.router.js +5 -18
  128. package/src/api/cyberia-instance/cyberia-instance.service.js +5 -45
  129. package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
  130. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +1 -1
  131. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
  132. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +1 -1
  133. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +76 -3
  134. package/src/api/cyberia-map/cyberia-map.controller.js +1 -1
  135. package/src/api/cyberia-map/cyberia-map.router.js +1 -1
  136. package/src/api/cyberia-map/cyberia-map.service.js +8 -4
  137. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
  138. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
  139. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
  140. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
  141. package/src/api/cyberia-quest/cyberia-quest.controller.js +1 -1
  142. package/src/api/cyberia-quest/cyberia-quest.router.js +4 -4
  143. package/src/api/cyberia-quest/cyberia-quest.service.js +5 -5
  144. package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +1 -1
  145. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
  146. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +1 -1
  147. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +2 -2
  148. package/src/api/cyberia-saga/cyberia-saga.controller.js +1 -1
  149. package/src/api/cyberia-saga/cyberia-saga.router.js +1 -1
  150. package/src/api/cyberia-saga/cyberia-saga.service.js +2 -2
  151. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -214
  152. package/src/api/cyberia-skill/cyberia-skill.controller.js +1 -1
  153. package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
  154. package/src/api/cyberia-skill/cyberia-skill.router.js +1 -1
  155. package/src/api/cyberia-skill/cyberia-skill.service.js +2 -2
  156. package/src/api/default/default.controller.js +1 -1
  157. package/src/api/default/default.router.js +1 -1
  158. package/src/api/default/default.service.js +2 -2
  159. package/src/api/document/document.controller.js +1 -1
  160. package/src/api/document/document.router.js +1 -1
  161. package/src/api/document/document.service.js +2 -2
  162. package/src/api/file/file.controller.js +1 -1
  163. package/src/api/file/file.ref.js +153 -0
  164. package/src/api/file/file.ref.json +11 -2
  165. package/src/api/file/file.router.js +1 -1
  166. package/src/api/file/file.service.js +2 -2
  167. package/src/api/instance/instance.controller.js +1 -1
  168. package/src/api/instance/instance.router.js +1 -1
  169. package/src/api/instance/instance.service.js +2 -2
  170. package/src/api/ipfs/ipfs.controller.js +1 -1
  171. package/src/api/ipfs/ipfs.router.js +2 -2
  172. package/src/api/ipfs/ipfs.service.js +2 -2
  173. package/src/api/object-layer/object-layer.controller.js +1 -1
  174. package/src/api/object-layer/object-layer.model.js +47 -12
  175. package/src/api/object-layer/object-layer.router.js +12 -5
  176. package/src/api/object-layer/object-layer.service.js +116 -123
  177. package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +1 -1
  178. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +1 -1
  179. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +2 -2
  180. package/src/api/test/test.controller.js +1 -1
  181. package/src/api/test/test.service.js +1 -1
  182. package/src/api/user/guest.service.js +3 -3
  183. package/src/api/user/user.controller.js +1 -1
  184. package/src/api/user/user.router.js +2 -2
  185. package/src/api/user/user.service.js +4 -4
  186. package/src/api.js +5 -9
  187. package/src/cli/app.js +402 -0
  188. package/src/cli/baremetal.js +30 -35
  189. package/src/cli/client.js +306 -0
  190. package/src/cli/cloud-init.js +3 -3
  191. package/src/cli/cluster.js +191 -82
  192. package/src/cli/db.js +120 -68
  193. package/src/cli/deploy.js +157 -75
  194. package/src/cli/docker-compose.js +5 -5
  195. package/src/cli/domains.js +184 -0
  196. package/src/cli/dotenv-store.js +143 -0
  197. package/src/cli/event.js +2281 -0
  198. package/src/cli/fs-selection.js +115 -0
  199. package/src/cli/fs.js +159 -343
  200. package/src/cli/host.js +672 -0
  201. package/src/cli/image.js +4 -6
  202. package/src/cli/index.js +343 -151
  203. package/src/cli/ipfs.js +34 -30
  204. package/src/cli/kickstart.js +1 -1
  205. package/src/cli/kubectl.js +218 -41
  206. package/src/cli/lxd.js +3 -3
  207. package/src/cli/monitor.js +1359 -26
  208. package/src/cli/package.js +90 -0
  209. package/src/cli/release.js +16 -12
  210. package/src/cli/repository.js +586 -363
  211. package/src/cli/run.js +398 -843
  212. package/src/cli/secrets.js +2158 -964
  213. package/src/cli/ssh.js +478 -279
  214. package/src/cli/state.js +394 -0
  215. package/src/cli/static.js +2 -2
  216. package/src/cli/system.js +1 -1
  217. package/src/cli/test.js +203 -125
  218. package/src/cli/vultr.js +61 -21
  219. package/src/cli/wireguard.js +1112 -306
  220. package/src/client/components/core/CalendarCore.js +53 -96
  221. package/src/client/components/core/Docs.js +327 -303
  222. package/src/client/components/core/FileExplorer.js +0 -21
  223. package/src/client/components/core/Modal.js +56 -1
  224. package/src/client/components/core/PanelForm.js +4 -4
  225. package/src/client/components/core/Repository.js +56 -0
  226. package/src/client/components/core/Translate.js +7 -0
  227. package/src/client/components/core/{FullScreen.js → ViewModeController.js} +42 -39
  228. package/src/client/components/core/Wallet.js +0 -11
  229. package/src/client/components/core/Worker.js +7 -1
  230. package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +2 -2
  231. package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
  232. package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
  233. package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
  234. package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
  235. package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
  236. package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
  237. package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
  238. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
  239. package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
  240. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +2 -35
  241. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
  242. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
  243. package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +2 -2
  244. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
  245. package/src/client/components/default/AppShellDefault.js +1 -1
  246. package/src/client/components/default/SettingsDefault.js +2 -2
  247. package/src/client/components/itemledger/SettingsItemledger.js +2 -2
  248. package/src/client/components/underpost/AppShellUnderpost.js +3 -2
  249. package/src/client/components/underpost/SettingsUnderpost.js +2 -2
  250. package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
  251. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +313 -30
  252. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
  253. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
  254. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
  255. package/src/client/public/cyberia-docs/CYBERIA.md +48 -28
  256. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
  257. package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +23 -6
  258. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  259. package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
  260. package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
  261. package/src/client/public/default/android-chrome-144x144.png +0 -0
  262. package/src/client/public/default/android-chrome-192x192.png +0 -0
  263. package/src/client/public/default/android-chrome-256x256.png +0 -0
  264. package/src/client/public/default/android-chrome-36x36.png +0 -0
  265. package/src/client/public/default/android-chrome-384x384.png +0 -0
  266. package/src/client/public/default/android-chrome-48x48.png +0 -0
  267. package/src/client/public/default/android-chrome-512x512.png +0 -0
  268. package/src/client/public/default/android-chrome-72x72.png +0 -0
  269. package/src/client/public/default/android-chrome-96x96.png +0 -0
  270. package/src/client/public/default/apple-touch-icon-1024x1024.png +0 -0
  271. package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
  272. package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
  273. package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
  274. package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
  275. package/src/client/public/default/apple-touch-icon-167x167.png +0 -0
  276. package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
  277. package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
  278. package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
  279. package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
  280. package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
  281. package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
  282. package/src/client/public/default/apple-touch-icon.png +0 -0
  283. package/src/client/public/default/apple-touch-startup-image-1125x2436.png +0 -0
  284. package/src/client/public/default/apple-touch-startup-image-1136x640.png +0 -0
  285. package/src/client/public/default/apple-touch-startup-image-1170x2532.png +0 -0
  286. package/src/client/public/default/apple-touch-startup-image-1179x2556.png +0 -0
  287. package/src/client/public/default/apple-touch-startup-image-1242x2208.png +0 -0
  288. package/src/client/public/default/apple-touch-startup-image-1242x2688.png +0 -0
  289. package/src/client/public/default/apple-touch-startup-image-1284x2778.png +0 -0
  290. package/src/client/public/default/apple-touch-startup-image-1290x2796.png +0 -0
  291. package/src/client/public/default/apple-touch-startup-image-1334x750.png +0 -0
  292. package/src/client/public/default/apple-touch-startup-image-1488x2266.png +0 -0
  293. package/src/client/public/default/apple-touch-startup-image-1536x2048.png +0 -0
  294. package/src/client/public/default/apple-touch-startup-image-1620x2160.png +0 -0
  295. package/src/client/public/default/apple-touch-startup-image-1640x2160.png +0 -0
  296. package/src/client/public/default/apple-touch-startup-image-1668x2224.png +0 -0
  297. package/src/client/public/default/apple-touch-startup-image-1668x2388.png +0 -0
  298. package/src/client/public/default/apple-touch-startup-image-1792x828.png +0 -0
  299. package/src/client/public/default/apple-touch-startup-image-2048x1536.png +0 -0
  300. package/src/client/public/default/apple-touch-startup-image-2048x2732.png +0 -0
  301. package/src/client/public/default/apple-touch-startup-image-2160x1620.png +0 -0
  302. package/src/client/public/default/apple-touch-startup-image-2160x1640.png +0 -0
  303. package/src/client/public/default/apple-touch-startup-image-2208x1242.png +0 -0
  304. package/src/client/public/default/apple-touch-startup-image-2224x1668.png +0 -0
  305. package/src/client/public/default/apple-touch-startup-image-2266x1488.png +0 -0
  306. package/src/client/public/default/apple-touch-startup-image-2388x1668.png +0 -0
  307. package/src/client/public/default/apple-touch-startup-image-2436x1125.png +0 -0
  308. package/src/client/public/default/apple-touch-startup-image-2532x1170.png +0 -0
  309. package/src/client/public/default/apple-touch-startup-image-2556x1179.png +0 -0
  310. package/src/client/public/default/apple-touch-startup-image-2688x1242.png +0 -0
  311. package/src/client/public/default/apple-touch-startup-image-2732x2048.png +0 -0
  312. package/src/client/public/default/apple-touch-startup-image-2778x1284.png +0 -0
  313. package/src/client/public/default/apple-touch-startup-image-2796x1290.png +0 -0
  314. package/src/client/public/default/apple-touch-startup-image-640x1136.png +0 -0
  315. package/src/client/public/default/apple-touch-startup-image-750x1334.png +0 -0
  316. package/src/client/public/default/apple-touch-startup-image-828x1792.png +0 -0
  317. package/src/client/public/default/assets/background/white.jpg +0 -0
  318. package/src/client/public/default/browserconfig.xml +1 -1
  319. package/src/client/public/default/favicon-16x16.png +0 -0
  320. package/src/client/public/default/favicon-32x32.png +0 -0
  321. package/src/client/public/default/favicon-48x48.png +0 -0
  322. package/src/client/public/default/favicon.ico +0 -0
  323. package/src/client/public/default/manifest.webmanifest +5 -5
  324. package/src/client/public/default/mstile-144x144.png +0 -0
  325. package/src/client/public/default/mstile-150x150.png +0 -0
  326. package/src/client/public/default/mstile-310x150.png +0 -0
  327. package/src/client/public/default/mstile-310x310.png +0 -0
  328. package/src/client/public/default/mstile-70x70.png +0 -0
  329. package/src/client/public/default/yandex-browser-50x50.png +0 -0
  330. package/src/client/public/default/yandex-browser-manifest.json +1 -1
  331. package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
  332. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
  333. package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
  334. package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
  335. package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
  336. package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
  337. package/src/client/services/object-layer/object-layer.management.js +6 -10
  338. package/src/client/services/object-layer/object-layer.service.js +3 -1
  339. package/src/client/ssr/RootDocument.js +1 -1
  340. package/src/client/ssr/body/404.js +1 -1
  341. package/src/client/ssr/body/500.js +1 -1
  342. package/src/client/ssr/body/CacheControl.js +1 -1
  343. package/src/client/ssr/body/CyberiaDefaultSplashScreen.js +1 -1
  344. package/src/client/ssr/body/DefaultSplashScreen.js +1 -1
  345. package/src/client/ssr/body/SwaggerDarkMode.js +1 -1
  346. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +1 -1
  347. package/src/client/ssr/head/CryptokoynScripts.js +1 -1
  348. package/src/client/ssr/head/Css.js +1 -1
  349. package/src/client/ssr/head/CyberiaPortalScripts.js +1 -1
  350. package/src/client/ssr/head/DefaultScripts.js +1 -1
  351. package/src/client/ssr/head/ItemledgerScripts.js +1 -1
  352. package/src/client/ssr/head/Microdata.js +1 -1
  353. package/src/client/ssr/head/Production.js +1 -1
  354. package/src/client/ssr/head/Pwa.js +1 -1
  355. package/src/client/ssr/head/PwaItemledger.js +1 -1
  356. package/src/client/ssr/head/Seo.js +1 -1
  357. package/src/client/ssr/head/UnderpostScripts.js +1 -1
  358. package/src/client/ssr/mailer/DefaultRecoverEmail.js +1 -1
  359. package/src/client/ssr/mailer/DefaultVerifyEmail.js +1 -1
  360. package/src/client/ssr/views/Cyberia404.js +7 -5
  361. package/src/client/ssr/views/CyberiaServerMetrics.js +1 -1
  362. package/src/client/ssr/views/Maintenance.js +1 -1
  363. package/src/client/ssr/views/NoNetworkConnection.js +1 -1
  364. package/src/client/ssr/views/Test.js +1 -1
  365. package/src/client/sw/core.sw.js +20 -14
  366. package/src/client-builder/client-build-docs.js +92 -77
  367. package/src/client-builder/client-build-live.js +2 -2
  368. package/src/client-builder/client-build.js +105 -74
  369. package/src/client-builder/client-bundle.js +347 -0
  370. package/src/client-builder/client-dev-server.js +3 -3
  371. package/src/client-builder/client-icons.js +2 -2
  372. package/src/client-builder/ssr.js +3 -3
  373. package/src/client.build.js +3 -6
  374. package/src/client.dev.js +3 -6
  375. package/src/db/DataBaseProvider.js +186 -3
  376. package/src/db/mariadb/MariaDB.js +3 -2
  377. package/src/db/mongo/MongoBootstrap.js +273 -101
  378. package/src/db/mongo/MongoExpress.js +187 -0
  379. package/src/db/mongo/MongooseDB.js +7 -8
  380. package/src/{server/valkey.js → db/valkey/Valkey.js} +5 -4
  381. package/src/grpc/cyberia/grpc-server.js +6 -8
  382. package/src/index.js +79 -23
  383. package/src/mailer/EmailRender.js +2 -2
  384. package/src/mailer/MailerInterceptor.js +87 -0
  385. package/src/mailer/MailerProvider.js +50 -6
  386. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +144 -147
  387. package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
  388. package/src/projects/cyberia/besu-genesis-generator.js +8 -17
  389. package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
  390. package/src/projects/cyberia/catalog-cyberia.js +54 -10
  391. package/src/projects/cyberia/gemini-client.js +4 -4
  392. package/src/projects/cyberia/generate-saga.js +5 -12
  393. package/src/projects/cyberia/hot-reload-trigger.js +18 -29
  394. package/src/projects/cyberia/instance-backup.js +210 -0
  395. package/src/projects/cyberia/instance-data.js +178 -258
  396. package/src/projects/cyberia/ipfs-client.js +4 -4
  397. package/src/projects/cyberia/map-preview-generator.js +33 -69
  398. package/src/projects/cyberia/object-layer.js +153 -320
  399. package/src/projects/cyberia/seed-audio.js +139 -0
  400. package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
  401. package/src/projects/cyberia/semantic-layer-generator.js +3 -19
  402. package/src/projects/cyberia/stat-balance.js +81 -0
  403. package/src/projects/cyberia/stat-commands.js +23 -0
  404. package/src/projects/cyberia/stat-contract-generator.js +101 -0
  405. package/src/projects/underpost/catalog-underpost.js +9 -1
  406. package/src/proxy.js +4 -7
  407. package/src/runtime/cyberia-client/Dockerfile +17 -8
  408. package/src/runtime/cyberia-client/Dockerfile.dev +13 -4
  409. package/src/runtime/cyberia-server/Dockerfile +18 -6
  410. package/src/runtime/cyberia-server/Dockerfile.dev +16 -3
  411. package/src/runtime/engine-cyberia/Dockerfile +25 -9
  412. package/src/runtime/engine-cyberia/Dockerfile.dev +25 -9
  413. package/src/runtime/engine-cyberia/Dockerfile.test +5 -5
  414. package/src/runtime/engine-cyberia/compose.env +17 -5
  415. package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
  416. package/src/runtime/express/Express.js +32 -27
  417. package/src/runtime/lampp/Lampp.js +22 -70
  418. package/src/runtime/nginx/Nginx.js +1 -1
  419. package/src/runtime/wp/Wp.js +16 -48
  420. package/src/server/{catalog.js → build/catalog.js} +15 -5
  421. package/src/server/build/coverage.js +188 -0
  422. package/src/server/build/execution.js +402 -0
  423. package/src/server/build/package.js +508 -0
  424. package/src/server/build/testing.js +578 -0
  425. package/src/server/{dns.js → network/dns.js} +144 -11
  426. package/src/server/{forward-proxy.js → network/forward-proxy.js} +10 -18
  427. package/src/server/{middlewares.js → network/middlewares.js} +11 -5
  428. package/src/server/network/node-capability.js +98 -0
  429. package/src/server/{peer.js → network/peer.js} +3 -3
  430. package/src/server/{proxy.js → network/proxy.js} +7 -13
  431. package/src/server/network/router.js +356 -0
  432. package/src/server/{tls.js → network/tls.js} +2 -2
  433. package/src/server/{underpost-compression.js → network/underpost-compression.js} +1 -1
  434. package/src/server/{underpost-gateway.js → network/underpost-gateway.js} +7 -7
  435. package/src/server/{underpost-ingress.js → network/underpost-ingress.js} +2 -2
  436. package/src/server/{cri.js → ops/cri.js} +2 -2
  437. package/src/server/{cron.js → ops/cron.js} +349 -74
  438. package/src/server/ops/event-notification.js +284 -0
  439. package/src/server/{logger.js → ops/logger.js} +143 -23
  440. package/src/server/ops/monitoring.js +1724 -0
  441. package/src/server/{systemd.js → ops/systemd.js} +55 -1
  442. package/src/server/{conf.js → runtime/conf.js} +516 -423
  443. package/src/server/runtime/config-scope.js +215 -0
  444. package/src/server/runtime/environment.js +149 -0
  445. package/src/server/{process.js → runtime/process.js} +153 -12
  446. package/src/server/{runtime-status.js → runtime/runtime-status.js} +155 -16
  447. package/src/server/{runtime.js → runtime/runtime.js} +12 -8
  448. package/src/server/{start.js → runtime/start.js} +128 -39
  449. package/src/server/{auth.js → security/auth.js} +7 -8
  450. package/src/server/security/container-storage.js +382 -0
  451. package/src/server/{crypto.js → security/crypto.js} +1 -1
  452. package/src/server/{selinux.js → security/selinux.js} +1 -1
  453. package/src/server/{backup.js → storage/backup.js} +17 -8
  454. package/src/server/{data-query.js → storage/data-query.js} +17 -1
  455. package/src/server/storage/downloader.js +121 -0
  456. package/src/server/storage/repository.js +68 -0
  457. package/src/server/storage/zip.js +153 -0
  458. package/src/server.js +4 -7
  459. package/src/ws/IoInterface.js +1 -1
  460. package/src/ws/IoServer.js +1 -1
  461. package/src/ws/core/core.ws.connection.js +1 -1
  462. package/src/ws/core/core.ws.emit.js +1 -1
  463. package/src/ws/core/core.ws.server.js +1 -1
  464. package/src/ws/default/default.ws.connection.js +1 -1
  465. package/src/ws/default/default.ws.emit.js +1 -1
  466. package/src/ws/default/default.ws.server.js +1 -1
  467. package/test/e2e/event-e2e-public-ingress-down.js +45 -0
  468. package/test/e2e/event-e2e-wireguard-server-down.js +38 -0
  469. package/test/e2e/event-e2e-wireguard-spoke-down.js +40 -0
  470. package/test/{api.test.js → integration/app/api.test.js} +33 -14
  471. package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
  472. package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
  473. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +64 -0
  474. package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
  475. package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
  476. package/test/{cyberia-instance-conf-defaults.test.js → integration/app/cyberia/cyberia-instance-conf-defaults.test.js} +17 -23
  477. package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
  478. package/test/integration/app/cyberia/cyberia-load.test.js +466 -0
  479. package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
  480. package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
  481. package/test/integration/app/cyberia/fallback-world-capture.test.js +338 -0
  482. package/test/integration/app/cyberia/object-layer-item-id.test.js +327 -0
  483. package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
  484. package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
  485. package/test/integration/app/cyberia/seed-audio.test.js +275 -0
  486. package/test/{shape-generator.test.js → integration/app/cyberia/shape-generator.test.js} +1 -1
  487. package/test/integration/infra/1-security/config-scope.test.js +305 -0
  488. package/test/integration/infra/1-security/container-storage.test.js +356 -0
  489. package/test/integration/infra/1-security/in-pod-cli-surface.test.js +86 -0
  490. package/test/integration/infra/1-security/secret-onboarding.test.js +930 -0
  491. package/test/{selinux.test.js → integration/infra/1-security/selinux.test.js} +2 -4
  492. package/test/{sops-secret-store.test.js → integration/infra/1-security/sops-secret-store.test.js} +415 -61
  493. package/test/integration/infra/1-security/systemd-service.test.js +70 -0
  494. package/test/integration/infra/1-security/underpost-config-secret.test.js +367 -0
  495. package/test/integration/infra/2-network/dns-firewall.test.js +566 -0
  496. package/test/integration/infra/2-network/node-capability.test.js +182 -0
  497. package/test/integration/infra/2-network/wireguard-cli.test.js +1187 -0
  498. package/test/{wireguard-edge.test.js → integration/infra/2-network/wireguard-edge.test.js} +649 -55
  499. package/test/{cluster-instances.test.js → integration/infra/3-cluster/cluster-instances.test.js} +11 -15
  500. package/test/{deploy-node-placement.test.js → integration/infra/3-cluster/deploy-node-placement.test.js} +3 -3
  501. package/test/integration/infra/3-cluster/docker-compose-stack.test.js +313 -0
  502. package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
  503. package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
  504. package/test/integration/infra/4-ingress/deploy-routes.test.js +379 -0
  505. package/test/integration/infra/4-ingress/gateway-static-assets.test.js +453 -0
  506. package/test/{instance-traffic-plan.test.js → integration/infra/4-ingress/instance-traffic-plan.test.js} +10 -6
  507. package/test/{underpost-gateway.test.js → integration/infra/4-ingress/underpost-gateway.test.js} +6 -6
  508. package/test/{underpost-ingress.test.js → integration/infra/4-ingress/underpost-ingress.test.js} +4 -4
  509. package/test/integration/infra/5-observability/cron-jobs.test.js +652 -0
  510. package/test/{deploy-monitor.test.js → integration/infra/5-observability/deploy-monitor.test.js} +29 -31
  511. package/test/integration/infra/5-observability/event-notification.test.js +197 -0
  512. package/test/integration/infra/5-observability/event-remediation.test.js +502 -0
  513. package/test/integration/infra/5-observability/event-targets.test.js +1174 -0
  514. package/test/integration/infra/5-observability/monitoring-stack.test.js +902 -0
  515. package/test/support/shell-harness.js +73 -0
  516. package/test/unit/build-template.test.js +97 -0
  517. package/test/unit/build-workflow-offline.test.js +132 -0
  518. package/test/unit/catalog.test.js +71 -0
  519. package/test/unit/client-build-docs.test.js +361 -0
  520. package/test/unit/client-bundle.test.js +313 -0
  521. package/test/unit/conf-loading.test.js +303 -0
  522. package/test/unit/conf-resolution.test.js +996 -0
  523. package/test/unit/coverage-artifact.test.js +221 -0
  524. package/test/{crypto.test.js → unit/crypto.test.js} +1 -1
  525. package/test/unit/cyberia/instance-backup.test.js +183 -0
  526. package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
  527. package/test/unit/cyberia/publish-workflow.test.js +22 -0
  528. package/test/unit/cyberia/stat-balance.test.js +69 -0
  529. package/test/unit/deploy-env-resolution.test.js +38 -0
  530. package/test/unit/deploy-log-table.test.js +518 -0
  531. package/test/unit/downloader-integrity.test.js +98 -0
  532. package/test/unit/execution-profiles.test.js +219 -0
  533. package/test/unit/file-reference-registry.test.js +141 -0
  534. package/test/unit/fleet-sync-source.test.js +149 -0
  535. package/test/unit/fs-storage-paths.test.js +596 -0
  536. package/test/unit/logger-redaction.test.js +141 -0
  537. package/test/unit/package.test.js +457 -0
  538. package/test/unit/prepare-host.test.js +382 -0
  539. package/test/unit/process-environment.test.js +18 -0
  540. package/test/unit/propagation-message.test.js +78 -0
  541. package/test/unit/redeploy-plan.test.js +24 -0
  542. package/test/unit/release-bump.test.js +474 -0
  543. package/test/unit/ssh-output-redaction.test.js +28 -0
  544. package/test/unit/start-options.test.js +46 -0
  545. package/test/unit/test-tiers.test.js +220 -0
  546. package/test/unit/zip-archive.test.js +142 -0
  547. package/vitest.config.js +66 -0
  548. package/.nycrc +0 -9
  549. package/deploy/lib/logging.sh +0 -96
  550. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +0 -48
  551. package/manifests/mongodb/pv-pvc.yaml +0 -59
  552. package/scripts/coverall-test-cyberia.sh +0 -24
  553. package/scripts/coverall-test.sh +0 -24
  554. package/scripts/link-local-underpost-cli.sh +0 -6
  555. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
  556. package/src/api/user/user.build.js +0 -16
  557. package/src/cli/env.js +0 -177
  558. package/src/client/components/core/Blockchain.js +0 -41
  559. package/src/client/components/core/Blog.js +0 -9
  560. package/src/client/components/core/KeyboardAvoidance.js +0 -145
  561. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
  562. package/src/client/public/default/assets/background/dark.jpg +0 -0
  563. package/src/client/public/default/assets/background/white0-min.jpg +0 -0
  564. package/src/client/public/default/assets/background/white0.jpg +0 -0
  565. package/src/client/public/doc/favicon.ico +0 -0
  566. package/src/client/public/doc/sitemap +0 -148
  567. package/src/server/downloader.js +0 -73
  568. package/src/server/environment.js +0 -98
@@ -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,21 +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 { writeEnv } from './environment.js';
26
- import { shellExec } from './process.js';
27
- import { UNDERPOST_GATEWAY, statusPageAssetPathFactory } from './underpost-gateway.js';
28
- 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';
29
29
  import splitFile from 'split-file';
30
- 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';
31
40
 
32
41
  colors.enable();
33
42
 
@@ -176,8 +185,8 @@ const getConfFolder = (deployId) => {
176
185
 
177
186
  /**
178
187
  * Resolves the full path to a specific configuration JSON file for a deploy ID.
179
- * For `server` configs in development mode with a subConf, it will prefer the
180
- * 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`.
181
190
  *
182
191
  * @method getConfFilePath
183
192
  * @param {string} deployId - The deploy ID.
@@ -191,17 +200,28 @@ const getConfFolder = (deployId) => {
191
200
  * // => './engine-private/conf/dd-myapp/conf.server.json'
192
201
  *
193
202
  * @example
194
- * // In development with subConf 'local':
203
+ * // With an explicitly named subConf, in any environment:
195
204
  * getConfFilePath('dd-myapp', 'server', 'local');
196
205
  * // => './engine-private/conf/dd-myapp/conf.server.dev.local.json' (if it exists)
197
206
  */
198
207
  const getConfFilePath = (deployId, confType, subConf = '') => {
199
208
  const folder = getConfFolder(deployId);
200
- // When no explicit subConf is given, fall back to the env var set by loadConf()
201
- 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 || '';
202
212
  if (confType === 'server' && effectiveSubConf) {
203
- const devConfPath = `${folder}/conf.${confType}.dev.${effectiveSubConf}.json`;
204
- 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
+ }
205
225
  }
206
226
  return `${folder}/conf.${confType}.json`;
207
227
  };
@@ -261,7 +281,7 @@ const Config = {
261
281
  deployContext = process.env.DEPLOY_ID;
262
282
  if (!subConf && process.argv[3] && typeof process.argv[3] === 'string') subConf = process.argv[3];
263
283
 
264
- Underpost.env.set('await-deploy', new Date().toISOString());
284
+ latchAwaitDeploy();
265
285
  if (deployContext.startsWith('dd-')) loadConf(deployContext, subConf);
266
286
  if (deployContext === 'proxy') await Config.buildProxy(deployList, subConf);
267
287
  },
@@ -269,16 +289,15 @@ const Config = {
269
289
  * @method deployIdFactory
270
290
  * @description Creates a new deploy ID.
271
291
  * @param {string} [deployId='dd-default']
272
- * @param {object} [options={ subConf: '', cluster: false }] - The options.
292
+ * @param {object} [options={ subConf: '' }] - The options.
273
293
  * @memberof ServerConfBuilder
274
294
  */
275
- deployIdFactory: function (deployId = DEFAULT_DEPLOY_ID, options = { subConf: '', cluster: false }) {
295
+ deployIdFactory: function (deployId = DEFAULT_DEPLOY_ID, options = { subConf: '' }) {
276
296
  if (!deployId.startsWith('dd-')) deployId = `dd-${deployId}`;
277
297
 
278
298
  logger.info('Build deployId', deployId);
279
299
 
280
300
  const folder = `./engine-private/conf/${deployId}`;
281
- const repoName = `engine-${deployId.split('dd-')[1]}`;
282
301
 
283
302
  if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
284
303
 
@@ -330,29 +349,6 @@ const Config = {
330
349
  );
331
350
  }
332
351
 
333
- if (options.cluster === true) {
334
- fs.writeFileSync(
335
- `./.github/workflows/${repoName}.cd.yml`,
336
- fs.readFileSync(`./.github/workflows/engine-test.cd.yml`, 'utf8').replaceAll('test', deployId.split('dd-')[1]),
337
- 'utf8',
338
- );
339
- fs.writeFileSync(
340
- `./.github/workflows/${repoName}.ci.yml`,
341
- fs.readFileSync(`./.github/workflows/engine-test.ci.yml`, 'utf8').replaceAll('test', deployId.split('dd-')[1]),
342
- 'utf8',
343
- );
344
- shellExec(`node bin new --default-conf --deploy-id ${deployId}`);
345
-
346
- if (!fs.existsSync(`./engine-private/deploy/dd.router`))
347
- fs.writeFileSync(`./engine-private/deploy/dd.router`, deployId, 'utf8');
348
- else
349
- fs.writeFileSync(
350
- `./engine-private/deploy/dd.router`,
351
- fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').trim() + `,${deployId}`,
352
- 'utf8',
353
- );
354
- }
355
-
356
352
  return { deployIdFolder: folder, deployId };
357
353
  },
358
354
  /**
@@ -412,17 +408,178 @@ const Config = {
412
408
  * @param {string} [subConf=''] - The sub configuration.
413
409
  * @memberof ServerConfBuilder
414
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
+
415
576
  const loadConf = (deployId = DEFAULT_DEPLOY_ID, subConf) => {
416
577
  if (deployId === 'current') {
417
578
  console.log(process.env.DEPLOY_ID);
418
579
  return;
419
580
  }
420
581
  if (deployId === 'clean') {
421
- const path = '.';
422
- fs.removeSync(`${path}/.env`);
423
- fs.removeSync(`${path}/.env.production`);
424
- fs.removeSync(`${path}/.env.development`);
425
- fs.removeSync(`${path}/.env.test`);
582
+ cleanDeployEnvFiles();
426
583
  return;
427
584
  }
428
585
  const folder = getConfFolder(deployId);
@@ -432,34 +589,58 @@ const loadConf = (deployId = DEFAULT_DEPLOY_ID, subConf) => {
432
589
  if (subConf) process.env.DEPLOY_SUB_CONF = subConf;
433
590
 
434
591
  for (const typeConf of Object.keys(Config.default)) {
435
- let srcConf = fs.readFileSync(`${folder}/conf.${typeConf}.json`, 'utf8');
436
- if (process.env.NODE_ENV === 'development' && typeConf === 'server' && subConf) {
437
- 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);
438
604
  if (fs.existsSync(devConfPath)) srcConf = fs.readFileSync(devConfPath, 'utf8');
439
605
  }
440
606
  let parsed = JSON.parse(srcConf);
441
607
  if (typeConf === 'server') parsed = loadReplicas(deployId, parsed);
442
608
  Config.default[typeConf] = parsed;
443
609
  }
444
- fs.writeFileSync(`./.env.production`, fs.readFileSync(`${folder}/.env.production`, 'utf8'), 'utf8');
445
- fs.writeFileSync(`./.env.development`, fs.readFileSync(`${folder}/.env.development`, 'utf8'), 'utf8');
446
- fs.writeFileSync(`./.env.test`, fs.readFileSync(`${folder}/.env.test`, 'utf8'), 'utf8');
447
- const NODE_ENV = process.env.NODE_ENV || 'development';
448
- if (NODE_ENV) {
449
- const subPathEnv = fs.existsSync(`${folder}/.env.${NODE_ENV}.${subConf}`)
450
- ? `${folder}/.env.${NODE_ENV}.${subConf}`
451
- : `${folder}/.env.${NODE_ENV}`;
452
- fs.writeFileSync(`./.env`, fs.readFileSync(subPathEnv, 'utf8'), 'utf8');
453
- const env = dotenv.parse(fs.readFileSync(subPathEnv, 'utf8'));
454
- process.env = {
455
- ...process.env,
456
- ...env,
457
- };
458
- }
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;
459
636
  const originPackageJson = JSON.parse(fs.readFileSync(`./package.json`, 'utf8'));
460
637
  const packageJson = JSON.parse(fs.readFileSync(`${folder}/package.json`, 'utf8'));
461
638
  originPackageJson.scripts.start = packageJson.scripts.start;
462
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;
463
644
  fs.writeFileSync(`./package.json`, JSON.stringify(packageJson, null, 4), 'utf8');
464
645
  return { folder, deployId };
465
646
  };
@@ -830,247 +1011,6 @@ const cloneSrcComponents = async ({ toOptions, fromOptions }) => {
830
1011
  }
831
1012
  };
832
1013
 
833
- /**
834
- * @method buildProxyRouter
835
- * @description Builds the proxy router.
836
- * @memberof ServerConfBuilder
837
- */
838
- const buildProxyRouter = () => {
839
- const confServer = newInstance(Config.default.server);
840
- let currentPort = parseInt(process.env.PORT) + 1;
841
- const proxyRouter = {};
842
- for (const host of Object.keys(confServer)) {
843
- for (const path of Object.keys(confServer[host])) {
844
- if (confServer[host][path].singleReplica) continue;
845
-
846
- if (isDevProxyContext()) confServer[host][path].proxy = [isTlsDevProxy() ? 443 : 80];
847
-
848
- confServer[host][path].port = newInstance(currentPort);
849
- for (const port of confServer[host][path].proxy) {
850
- if (!(port in proxyRouter)) proxyRouter[port] = {};
851
- proxyRouter[port][`${host}${path}`] = {
852
- // target: `http://${host}:${confServer[host][path].port}${path}`,
853
- target: `http://localhost:${confServer[host][path].port}`,
854
- // target: `http://127.0.0.1:${confServer[host][path].port}`,
855
- proxy: confServer[host][path].proxy,
856
- redirect: confServer[host][path].redirect,
857
- host,
858
- path,
859
- };
860
- }
861
- currentPort++;
862
- if (confServer[host][path].peer) {
863
- const peerPath = path === '/' ? `/peer` : `${path}/peer`;
864
- confServer[host][peerPath] = newInstance(confServer[host][path]);
865
- confServer[host][peerPath].port = newInstance(currentPort);
866
- for (const port of confServer[host][path].proxy) {
867
- if (!(port in proxyRouter)) proxyRouter[port] = {};
868
- proxyRouter[port][`${host}${peerPath}`] = {
869
- // target: `http://${host}:${confServer[host][peerPath].port}${peerPath}`,
870
- target: `http://localhost:${confServer[host][peerPath].port}`,
871
- // target: `http://127.0.0.1:${confServer[host][peerPath].port}`,
872
- proxy: confServer[host][peerPath].proxy,
873
- host,
874
- path: peerPath,
875
- };
876
- }
877
- currentPort++;
878
- }
879
- }
880
- }
881
-
882
- return proxyRouter;
883
- };
884
-
885
- /**
886
- * @method pathPortAssignmentFactory
887
- * @description Creates the path port assignment.
888
- * @param {string} deployId - The deploy ID.
889
- * @param {object} router - The router.
890
- * @param {object} confServer - The server configuration.
891
- * @memberof ServerConfBuilder
892
- */
893
- const pathPortAssignmentFactory = async (deployId, router, confServer) => {
894
- const pathPortAssignmentData = {};
895
- for (const host of Object.keys(confServer)) {
896
- const pathPortAssignment = [];
897
- for (const path of Object.keys(confServer[host])) {
898
- const { peer } = confServer[host][path];
899
- if (!router[`${host}${path === '/' ? '' : path}`]) continue;
900
- const port = parseInt(router[`${host}${path === '/' ? '' : path}`].split(':')[2]);
901
- // logger.info('', { host, port, path });
902
- pathPortAssignment.push({
903
- port,
904
- path,
905
- });
906
-
907
- if (peer) {
908
- // logger.info('', { host, port: port + 1, path: '/peer' });
909
- pathPortAssignment.push({
910
- port: port + 1,
911
- path: `${path === '/' ? '' : path}/peer`,
912
- });
913
- }
914
- }
915
- pathPortAssignmentData[host] = pathPortAssignment;
916
- }
917
- if (fs.existsSync(`./engine-private/replica`)) {
918
- const singleReplicas = await fs.readdir(`./engine-private/replica`);
919
- for (let replica of singleReplicas) {
920
- if (replica.startsWith(deployId)) {
921
- const replicaServerConf = loadConfServerJson(`./engine-private/replica/${replica}/conf.server.json`);
922
- for (const host of Object.keys(replicaServerConf)) {
923
- const pathPortAssignment = [];
924
- for (const path of Object.keys(replicaServerConf[host])) {
925
- const { peer } = replicaServerConf[host][path];
926
- if (!router[`${host}${path === '/' ? '' : path}`]) continue;
927
- const port = parseInt(router[`${host}${path === '/' ? '' : path}`].split(':')[2]);
928
- // logger.info('', { host, port, path });
929
- pathPortAssignment.push({
930
- port,
931
- path,
932
- });
933
-
934
- if (peer) {
935
- // logger.info('', { host, port: port + 1, path: '/peer' });
936
- pathPortAssignment.push({
937
- port: port + 1,
938
- path: `${path === '/' ? '' : path}/peer`,
939
- });
940
- }
941
- }
942
- pathPortAssignmentData[host] = pathPortAssignmentData[host].concat(pathPortAssignment);
943
- }
944
- }
945
- }
946
- }
947
- return pathPortAssignmentData;
948
- };
949
-
950
- /**
951
- * @method deployRangePortFactory
952
- * @description Creates the deploy range port factory.
953
- * @param {object} router - The router.
954
- * @returns {object} - The deploy range port factory.
955
- * @memberof ServerConfBuilder
956
- */
957
- const deployRangePortFactory = (router) => {
958
- const ports = Object.values(router).map((p) => parseInt(p.split(':')[2]));
959
- const fromPort = Math.min(...ports);
960
- const toPort = Math.max(...ports);
961
- return { ports, fromPort, toPort };
962
- };
963
-
964
- /**
965
- * @method buildKindPorts
966
- * @description Builds the kind ports.
967
- * @param {number} from - The from port.
968
- * @param {number} to - The to port.
969
- * @returns {string} - The kind ports.
970
- * @memberof ServerConfBuilder
971
- */
972
- const buildKindPorts = (from, to) =>
973
- range(parseInt(from), parseInt(to))
974
- .map(
975
- (port) => ` - name: 'tcp-${port}'
976
- protocol: TCP
977
- port: ${port}
978
- targetPort: ${port}
979
- - name: 'udp-${port}'
980
- protocol: UDP
981
- port: ${port}
982
- targetPort: ${port}
983
- `,
984
- )
985
- .join('\n');
986
-
987
- /**
988
- * @method buildPortProxyRouter
989
- * @description Builds the port proxy router.
990
- * @param {object} options - The options.
991
- * @param {number} [options.port=4000] - The port.
992
- * @param {object} options.proxyRouter - The proxy router.
993
- * @param {object} [options.hosts] - The hosts.
994
- * @param {boolean} [options.orderByPathLength=false] - Whether to order by path length.
995
- * @param {boolean} [options.devProxyContext=false] - Whether to use dev proxy context.
996
- * @returns {object} - The port proxy router.
997
- * @memberof ServerConfBuilder
998
- */
999
- const buildPortProxyRouter = (
1000
- options = { port: 4000, proxyRouter, hosts, orderByPathLength: false, devProxyContext: false },
1001
- ) => {
1002
- let { port, proxyRouter, hosts, orderByPathLength } = options;
1003
- hosts = hosts || proxyRouter[port] || {};
1004
-
1005
- const router = {};
1006
- // build router
1007
- Object.keys(hosts).map((hostKey) => {
1008
- let { host, path, target, proxy, peer } = hosts[hostKey];
1009
-
1010
- if (!proxy.includes(port)) {
1011
- logger.warn('Proxy port not set on conf', { port, host, path, proxy, target });
1012
- if (process.env.NODE_ENV === 'production') {
1013
- logger.warn('Omitting host', { host, path, target });
1014
- return;
1015
- }
1016
- }
1017
- // ${process.env.NODE_ENV === 'development' && !isDevProxyContext() ? `:${port}` : ''}
1018
- const absoluteHost = [80, 443].includes(port)
1019
- ? `${host}${path === '/' ? '' : path}`
1020
- : `${host}:${port}${path === '/' ? '' : path}`;
1021
-
1022
- if (absoluteHost in router)
1023
- logger.warn('Overwrite: Absolute host already exists on router', { absoluteHost, target });
1024
-
1025
- if (options.devProxyContext === true) {
1026
- const appDevPort = parseInt(target.split(':')[2]) - process.env.DEV_PROXY_PORT_OFFSET;
1027
- router[absoluteHost] = `http://localhost:${appDevPort}`;
1028
- } else router[absoluteHost] = target;
1029
- }); // order router
1030
-
1031
- if (Object.keys(router).length === 0) return router;
1032
-
1033
- const devApiConfPath = `./engine-private/conf/${process.argv[3]}/conf.server.dev.${process.argv[4]}-dev-api.json`;
1034
- if (options.devProxyContext === true && process.env.NODE_ENV === 'development' && fs.existsSync(devApiConfPath)) {
1035
- const confDevApiServer = JSON.parse(fs.readFileSync(devApiConfPath, 'utf8'));
1036
- let devApiHosts = [];
1037
- let origins = [];
1038
- for (const _host of Object.keys(confDevApiServer))
1039
- for (const _path of Object.keys(confDevApiServer[_host])) {
1040
- if (confDevApiServer[_host][_path].origins && confDevApiServer[_host][_path].origins.length) {
1041
- origins.push(...confDevApiServer[_host][_path].origins);
1042
- if (_path !== 'peer' && devApiHosts.length === 0)
1043
- devApiHosts.push(
1044
- `${_host}${[80, 443].includes(port) && isDevProxyContext() ? '' : `:${port}`}${_path == '/' ? '' : _path}`,
1045
- );
1046
- }
1047
- }
1048
- origins = Array.from(new Set(origins));
1049
- console.log({
1050
- origins,
1051
- devApiHosts,
1052
- });
1053
- for (const devApiHost of devApiHosts) {
1054
- if (devApiHost in router) {
1055
- const target = router[devApiHost];
1056
- delete router[devApiHost];
1057
- router[`${devApiHost}/${process.env.BASE_API}`] = target;
1058
- router[`${devApiHost}/socket.io`] = target;
1059
- for (const origin of origins) router[devApiHost] = origin;
1060
- }
1061
- }
1062
- }
1063
-
1064
- if (orderByPathLength === true) {
1065
- const reOrderRouter = {};
1066
- for (const absoluteHostKey of orderArrayFromAttrInt(Object.keys(router), 'length'))
1067
- reOrderRouter[absoluteHostKey] = router[absoluteHostKey];
1068
- return reOrderRouter;
1069
- }
1070
-
1071
- return router;
1072
- };
1073
-
1074
1014
  /**
1075
1015
  * @method buildReplicaId
1076
1016
  * @description Builds the replica ID.
@@ -1098,17 +1038,7 @@ const getDataDeploy = async (
1098
1038
  disableSyncEnvPort: false,
1099
1039
  },
1100
1040
  ) => {
1101
- let dataDeploy = fs
1102
- .readFileSync(`./engine-private/deploy/dd.router`, 'utf8')
1103
- .split(',')
1104
- .map((deployId) => deployId.trim())
1105
- .filter((deployId) => deployId);
1106
-
1107
- dataDeploy = dataDeploy.map((deployId) => {
1108
- return {
1109
- deployId,
1110
- };
1111
- });
1041
+ let dataDeploy = readDeployRoutes().map((deployId) => ({ deployId }));
1112
1042
 
1113
1043
  if (options && options.buildSingleReplica && fs.existsSync(`./engine-private/replica`))
1114
1044
  fs.removeSync(`./engine-private/replica`);
@@ -1125,7 +1055,7 @@ const getDataDeploy = async (
1125
1055
  for (const path of Object.keys(serverConf[host])) {
1126
1056
  if (!isReplicaDeploy && serverConf[host][path].replicas && serverConf[host][path].singleReplica) {
1127
1057
  if (options && options.buildSingleReplica)
1128
- await Underpost.repo.client(deployObj.deployId, '', host, path, {
1058
+ await Underpost.client.callback(deployObj.deployId, '', host, path, {
1129
1059
  singleReplica: true,
1130
1060
  });
1131
1061
  replicaDataDeploy = replicaDataDeploy.concat(
@@ -1143,7 +1073,7 @@ const getDataDeploy = async (
1143
1073
  }
1144
1074
 
1145
1075
  if (!options.disableSyncEnvPort && options.buildSingleReplica)
1146
- await Underpost.repo.client(undefined, '', '', '', { syncEnvPort: true });
1076
+ await Underpost.client.callback(undefined, '', '', '', { syncEnvPort: true });
1147
1077
 
1148
1078
  logger.info('Deployments configured', buildDataDeploy);
1149
1079
 
@@ -1168,7 +1098,7 @@ const validateTemplatePath = (absolutePath = '') => {
1168
1098
  const clients = DefaultConf.client.default.services;
1169
1099
 
1170
1100
  if (
1171
- absolutePath.match('src/api') &&
1101
+ absolutePath.match('src/api/') &&
1172
1102
  !absolutePath.match('src/api/types.js') &&
1173
1103
  !confServer.apis.find((p) => absolutePath.match(`src/api/${p}/`))
1174
1104
  ) {
@@ -1243,11 +1173,11 @@ const validateTemplatePath = (absolutePath = '') => {
1243
1173
  * @memberof ServerConfBuilder
1244
1174
  */
1245
1175
  const awaitDeployMonitor = async (isFinal = false, deltaMs = 1000, callback = false) => {
1246
- if (!callback) Underpost.env.set('await-deploy', new Date().toISOString());
1176
+ if (!callback) latchAwaitDeploy();
1247
1177
  if (isFinal) logger.info('Final deployment running (no replica)');
1248
1178
  await timer(deltaMs);
1249
- if (Underpost.env.get('container-status') === 'error') return false;
1250
- 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);
1251
1181
  return true;
1252
1182
  };
1253
1183
 
@@ -1564,35 +1494,49 @@ const getInstanceContext = async (options = { deployId, singleReplica, replicas,
1564
1494
  * @param {string} options.host - The host.
1565
1495
  * @param {string} options.path - The path.
1566
1496
  * @param {string} options.origin - The origin.
1567
- * @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.
1568
1499
  * @memberof ServerConfBuilder
1569
1500
  */
1570
1501
  const buildApiConf = async (options = { deployId: '', subConf: '', host: '', path: '', origin: '' }) => {
1571
1502
  let { deployId, subConf, host, path, origin } = options;
1572
- if (!deployId) deployId = process.argv[2].trim();
1573
- if (!subConf) subConf = process.argv[3].trim();
1574
- if (process.argv[4]) host = process.argv[4].trim();
1575
- 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.
1576
1505
  if (process.argv[6])
1577
1506
  origin = `${process.env.NODE_ENV === 'production' ? 'https' : 'http'}://${process.argv[6].trim()}`;
1578
-
1579
- 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
+ }
1580
1522
  const confServer = JSON.parse(
1581
1523
  fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.dev.${subConf}.json`, 'utf8'),
1582
1524
  );
1583
1525
  const envObj = dotenv.parse(
1584
1526
  fs.readFileSync(`./engine-private/conf/${deployId}/.env.${process.env.NODE_ENV}`, 'utf8'),
1585
1527
  );
1586
- if (host && path) {
1587
- confServer[host][path].origins = [origin];
1588
- logger.info('Build api conf', { host, path, origin });
1589
- } else return;
1590
- 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);
1591
1534
  fs.writeFileSync(
1592
- `./engine-private/conf/${deployId}/conf.server.dev.${subConf}-dev-api.json`,
1535
+ `./engine-private/conf/${deployId}/conf.server.dev.${devApiSubConf}.json`,
1593
1536
  JSON.stringify(confServer, null, 4),
1594
1537
  'utf8',
1595
1538
  );
1539
+ return devApiSubConf;
1596
1540
  };
1597
1541
 
1598
1542
  /**
@@ -1616,12 +1560,19 @@ const buildClientStaticConf = async (
1616
1560
  if (!subConf) subConf = process.argv[3].trim();
1617
1561
  if (!host) host = process.argv[4].trim();
1618
1562
  if (!path) path = process.argv[5].trim();
1619
- const confServer = JSON.parse(
1620
- fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.dev.${subConf}-dev-api.json`, 'utf8'),
1621
- );
1622
- const envObj = dotenv.parse(
1623
- fs.readFileSync(`./engine-private/conf/${deployId}/.env.${process.env.NODE_ENV}.${subConf}-dev-api`, 'utf8'),
1624
- );
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'));
1625
1576
  envObj.PORT = parseInt(envObj.PORT);
1626
1577
  const apiBaseHost = devProxy
1627
1578
  ? devProxyHostFactory({ host, tls: isTlsDevProxy() })
@@ -1640,16 +1591,6 @@ const buildClientStaticConf = async (
1640
1591
  );
1641
1592
  };
1642
1593
 
1643
- /**
1644
- * @method isDeployRunnerContext
1645
- * @description Checks if the deploy runner context is valid.
1646
- * @param {string} path - The path.
1647
- * @param {object} options - The options.
1648
- * @returns {boolean} - The deploy runner context.
1649
- * @memberof ServerConfBuilder
1650
- */
1651
- const isDeployRunnerContext = (path, options) => !options.build && path && path !== 'template-deploy';
1652
-
1653
1594
  /**
1654
1595
  * @method isDevProxyContext
1655
1596
  * @description Checks if the dev proxy context is valid.
@@ -1860,7 +1801,7 @@ const dispatchBuildInstanceEnv = ({
1860
1801
  const loadProjectInstanceEnvBuilder = async (deployId) => {
1861
1802
  const match = /^dd-([a-z0-9][a-z0-9-]*)$/.exec(`${deployId || ''}`);
1862
1803
  if (!match) return null;
1863
- 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);
1864
1805
  if (!fs.existsSync(moduleUrl)) return null;
1865
1806
  const projectModule = await import(moduleUrl.href);
1866
1807
  if (projectModule.buildInstanceEnv === undefined) return null;
@@ -2382,6 +2323,56 @@ const curlStatusChainFactory = (raw = '') => {
2382
2323
  return chain;
2383
2324
  };
2384
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
+
2385
2376
  /**
2386
2377
  * @method trafficTableRowsFactory
2387
2378
  * @description Resolves the live colour of each routable deployment, optionally
@@ -2464,6 +2455,25 @@ const hostRenderInstancesFactory = ({ declared = [], preserved = [], isDeployed
2464
2455
  const isTrafficServingFactory = ({ liveTraffic = '', hasReadyEndpoints = () => false }) =>
2465
2456
  !!liveTraffic && hasReadyEndpoints(liveTraffic);
2466
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
+
2467
2477
  /**
2468
2478
  * @method instanceTrafficPlanFactory
2469
2479
  * @description Resolves, for each instance, the colour routed now and the colour
@@ -2931,39 +2941,14 @@ ${renderHosts}`,
2931
2941
  return { renderHosts, changed: true };
2932
2942
  };
2933
2943
 
2934
- /**
2935
- * Resolves the concrete deploy ids a build or conf-sync run should iterate over.
2936
- *
2937
- * The meta deploy id `dd` fans out to the comma separated ids declared in
2938
- * `engine-private/deploy/dd.router`; when that file is absent (e.g. the private
2939
- * repository is not checked out) it falls back to {@link ServerConfBuilder.DEFAULT_DEPLOY_ID}.
2940
- * Any other value is parsed as a comma separated list.
2941
- * Entries are trimmed and empties dropped.
2942
- *
2943
- * @method resolveDeployList
2944
- * @param {string} deployId - A deploy id, a comma separated list, or the `dd` meta id.
2945
- * @returns {string[]} Ordered list of concrete deploy ids.
2946
- * @memberof ServerConfBuilder
2947
- */
2948
- const resolveDeployList = (deployId) =>
2949
- (deployId === 'dd'
2950
- ? fs.existsSync('./engine-private/deploy/dd.router')
2951
- ? fs.readFileSync('./engine-private/deploy/dd.router', 'utf8')
2952
- : DEFAULT_DEPLOY_ID
2953
- : deployId
2954
- )
2955
- .split(',')
2956
- .map((id) => id.trim())
2957
- .filter(Boolean);
2958
-
2959
2944
  /**
2960
2945
  * Syncs a single deploy id's private configuration into its dedicated
2961
2946
  * `engine-<suffix>-private` repository and pushes the result.
2962
2947
  *
2963
2948
  * Idempotent and safe to rerun: the private repo is cloned when missing or reset to a clean
2964
- * checkout when present, then the deploy id's `conf` folder, matching `replica` and
2965
- * `itc-scripts` entries, and any caller-supplied `extraPaths` payloads are mirrored. The
2966
- * 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`).
2967
2952
  *
2968
2953
  * @method syncPrivateConf
2969
2954
  * @param {string} deployId - A concrete deploy id (e.g. `dd-cyberia`), not the `dd` meta id.
@@ -2973,10 +2958,10 @@ const resolveDeployList = (deployId) =>
2973
2958
  * @memberof ServerConfBuilder
2974
2959
  */
2975
2960
  const syncPrivateConf = (deployId, extraPaths = []) => {
2976
- const suffix = deployId.split('dd-')[1];
2977
- const privateRepoName = `engine-${suffix}-private`;
2961
+ const privateRepoName = Underpost.repo.privateRepoFactory(deployId);
2978
2962
  const privateGitUri = `${process.env.GITHUB_USERNAME}/${privateRepoName}`;
2979
2963
  const privateRepoPath = `../${privateRepoName}`;
2964
+ const cronDeployId = cronDeployIdResolve();
2980
2965
 
2981
2966
  if (!fs.existsSync(privateRepoPath)) {
2982
2967
  shellExec(`cd .. && underpost clone ${privateGitUri}`, { silent: true });
@@ -2991,17 +2976,21 @@ const syncPrivateConf = (deployId, extraPaths = []) => {
2991
2976
  fs.removeSync(confDest);
2992
2977
  fs.mkdirSync(confDest, { recursive: true });
2993
2978
  fs.copySync(`./engine-private/conf/${deployId}`, confDest);
2994
-
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`);
2995
2983
  fs.removeSync(`${privateRepoPath}/replica`);
2996
- for (const payloadDir of ['replica', 'itc-scripts']) {
2997
- const srcDir = `./engine-private/${payloadDir}`;
2998
- if (!fs.existsSync(srcDir)) continue;
2999
- for (const entry of fs.readdirSync(srcDir))
3000
- if (entry.match(deployId)) fs.copySync(`${srcDir}/${entry}`, `${privateRepoPath}/${payloadDir}/${entry}`);
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}`);
2989
+ for (const extraPath of extraPaths) {
2990
+ fs.removeSync(`${privateRepoPath}/${extraPath}`);
2991
+ fs.copySync(`./engine-private/${extraPath}`, `${privateRepoPath}/${extraPath}`);
3001
2992
  }
3002
2993
 
3003
- for (const extraPath of extraPaths) fs.copySync(`./engine-private/${extraPath}`, `${privateRepoPath}/${extraPath}`);
3004
-
3005
2994
  shellExec(
3006
2995
  `cd ${privateRepoPath}` +
3007
2996
  ` && git add .` +
@@ -3031,31 +3020,111 @@ const syncDeployIdSources = (sourceMoves = []) => {
3031
3020
  return true;
3032
3021
  };
3033
3022
 
3023
+ const TEMPLATE_REPOSITORY_NAME = 'pwa-microservices-template';
3024
+
3025
+ /**
3026
+ * @method gitOriginRepositoryName
3027
+ * @description Repository name a checkout's `origin` points at, without the `.git`
3028
+ * suffix, or `null` when the path is not a work tree or declares no origin.
3029
+ * @param {string} repoPath - Local repository path.
3030
+ * @returns {string|null}
3031
+ * @memberof ServerConfBuilder
3032
+ */
3033
+ const gitOriginRepositoryName = (repoPath) => {
3034
+ if (!fs.existsSync(`${repoPath}/.git`)) return null;
3035
+ const url = shellExec(`cd ${repoPath} && git config --get remote.origin.url`, {
3036
+ stdout: true,
3037
+ silent: true,
3038
+ disableLog: true,
3039
+ silentOnError: true,
3040
+ });
3041
+ const name = typeof url === 'string' ? url.trim().split('/').pop() : '';
3042
+ return name ? name.replace(/\.git$/, '') : null;
3043
+ };
3044
+
3045
+ /**
3046
+ * @method ensureTemplateCheckout
3047
+ * @description Guarantees `toPath` is a checkout of the template repository, cloning it
3048
+ * when it is missing or belongs to another repository.
3049
+ *
3050
+ * Publishing steps swap the checkout's `.git` for a product repository's history, so an
3051
+ * assembly that trusted whatever `.git` it found would read another product's identity
3052
+ * back into the base template `package.json`.
3053
+ *
3054
+ * @param {object} params
3055
+ * @param {string} params.toPath - Template checkout path.
3056
+ * @param {string} [params.githubUsername] - Owner of the template repository.
3057
+ * @param {boolean} [params.noClone=false] - Fail instead of cloning a missing/foreign checkout.
3058
+ * @returns {boolean} `true` when the checkout was restored.
3059
+ * @memberof ServerConfBuilder
3060
+ */
3061
+ const ensureTemplateCheckout = ({ toPath, githubUsername, noClone = false }) => {
3062
+ const owner = githubUsername || 'underpostnet';
3063
+ const repository = `${owner}/${TEMPLATE_REPOSITORY_NAME}`;
3064
+ if (gitOriginRepositoryName(toPath) === TEMPLATE_REPOSITORY_NAME) return false;
3065
+ if (noClone) throw new Error(`buildTemplate: ${toPath} is not a ${repository} checkout (--no-clone)`);
3066
+ logger.warn('Restoring template checkout', { toPath, repository });
3067
+
3068
+ // Swap in the clone only once it is complete, so a failed one leaves the checkout as is.
3069
+ const templatePath = dir.resolve(toPath);
3070
+ const stagePath = fs.mkdtempSync(`${dir.dirname(templatePath)}/.${TEMPLATE_REPOSITORY_NAME}-`);
3071
+ try {
3072
+ shellExec(`cd ${stagePath} && node ${dir.resolve('./bin')} clone ${repository}`);
3073
+ const clonePath = `${stagePath}/${TEMPLATE_REPOSITORY_NAME}`;
3074
+ if (gitOriginRepositoryName(clonePath) !== TEMPLATE_REPOSITORY_NAME)
3075
+ throw new Error(`buildTemplate: could not clone ${repository}`);
3076
+ fs.removeSync(templatePath);
3077
+ fs.moveSync(clonePath, templatePath);
3078
+ } finally {
3079
+ fs.removeSync(stagePath);
3080
+ }
3081
+ return true;
3082
+ };
3083
+
3084
+ /**
3085
+ * @method pruneTemplateWorkTree
3086
+ * @description Empties the template checkout of everything a build reconstructs, so a
3087
+ * source renamed or deleted in the engine — or a client bundle belonging to another
3088
+ * deploy id — cannot survive from a previous run into the assembled output.
3089
+ * @param {string} toPath - Template checkout path.
3090
+ * @param {string[]} preservedEntries - Top level entries to keep (git history, installed modules).
3091
+ * @returns {void}
3092
+ * @memberof ServerConfBuilder
3093
+ */
3094
+ const pruneTemplateWorkTree = (toPath, preservedEntries = []) => {
3095
+ for (const entry of fs.readdirSync(toPath)) {
3096
+ if (preservedEntries.includes(entry)) continue;
3097
+ fs.removeSync(`${toPath}/${entry}`);
3098
+ }
3099
+ };
3100
+
3034
3101
  /**
3035
3102
  * Rebuilds the standalone `pwa-microservices-template` from scratch out of the current
3036
3103
  * engine source tree.
3037
3104
  *
3038
- * Clones the template repo next to the engine when missing, otherwise resets it to a clean
3039
- * pristine checkout, then syncs every engine-tracked file the template is allowed to carry
3105
+ * Clones the template repo next to the engine when the checkout is missing or belongs to
3106
+ * another repository, empties it so the result cannot inherit anything from a previous run,
3107
+ * then syncs every engine-tracked file the template is allowed to carry
3040
3108
  * ({@link validateTemplatePath}), strips engine-only + product modules, restores the template's
3041
3109
  * own CI workflows + guest services, and rewrites `package.json` / `package-lock.json` / `README`
3042
3110
  * so the result is a standalone, installable project. Throws on failure; callers own exit codes.
3043
3111
  *
3044
- * Product catalogs are read dynamically ({@link module:src/server/catalog} `loadProductCatalogs`),
3112
+ * Product catalogs are read dynamically ({@link module:src/server/build/catalog.js} `loadProductCatalogs`),
3045
3113
  * so this stays decoupled from — and survives removal of — any product module.
3046
3114
  *
3047
3115
  * @method buildTemplate
3048
3116
  * @param {object} [options]
3049
3117
  * @param {string} [options.srcPath='./'] - Engine source root to sync from.
3050
3118
  * @param {string} [options.toPath='../pwa-microservices-template'] - Template output path.
3119
+ * @param {boolean} [options.noClone=false] - Fail instead of cloning when the checkout is missing or foreign.
3051
3120
  * @returns {Promise<void>}
3052
3121
  * @memberof ServerConfBuilder
3053
3122
  */
3054
- const buildTemplate = async ({ srcPath = './', toPath = '../pwa-microservices-template' } = {}) => {
3123
+ const buildTemplate = async ({ srcPath = './', toPath = '../pwa-microservices-template', noClone = false } = {}) => {
3055
3124
  const walk = (await import('ignore-walk')).default;
3056
- const { TEMPLATE_RESTORE_PATHS, TEMPLATE_KEYWORDS, TEMPLATE_DESCRIPTION } =
3057
- await import('../projects/underpost/catalog-underpost.js');
3058
- const { loadProductCatalogs } = await import('./catalog.js');
3125
+ const { TEMPLATE_RESTORE_PATHS, TEMPLATE_PRESERVED_ENTRIES, TEMPLATE_KEYWORDS, TEMPLATE_DESCRIPTION } =
3126
+ await import('../../projects/underpost/catalog-underpost.js');
3127
+ const { loadProductCatalogs } = await import('../build/catalog.js');
3059
3128
  const githubUsername = process.env.GITHUB_USERNAME;
3060
3129
 
3061
3130
  logger.info('Build template', { srcPath, toPath });
@@ -3068,10 +3137,9 @@ const buildTemplate = async ({ srcPath = './', toPath = '../pwa-microservices-te
3068
3137
  )
3069
3138
  ).filter((p) => !p.startsWith('.git'));
3070
3139
 
3071
- fs.removeSync(`${githubUsername}/pwa-microservices-template`);
3072
- shellExec(`cd .. && node engine/bin clone ${githubUsername}/pwa-microservices-template`);
3073
-
3140
+ ensureTemplateCheckout({ toPath, githubUsername, noClone });
3074
3141
  shellExec(`cd ${toPath} && git config core.filemode false`);
3142
+ pruneTemplateWorkTree(toPath, TEMPLATE_PRESERVED_ENTRIES);
3075
3143
 
3076
3144
  for (const copyPath of sourceFiles) {
3077
3145
  if (copyPath === 'NaN') continue;
@@ -3180,7 +3248,8 @@ git add .`);
3180
3248
  * `engine-<idPart>`). A pod started with `underpost start --build --private-test-repo`
3181
3249
  * clones this repo, so work-in-progress engine source can be tested end to end
3182
3250
  * without touching the production source. Mirrors {@link updatePrivateTemplateRepo}
3183
- * but per-deploy-id and against the test repo.
3251
+ * but per-deploy-id and against the test repo, publishing from its own work tree
3252
+ * (`<template parent>/engine-test-<idPart>`) so the template checkout keeps its identity.
3184
3253
  *
3185
3254
  * Assumes the deploy id template has already been assembled at the template path
3186
3255
  * (run `node bin/build <deployId>` first, or use `node bin/build <deployId> --update-private`).
@@ -3190,39 +3259,53 @@ git add .`);
3190
3259
  */
3191
3260
  const updatePrivateEngineTestRepo = async (deployId) => {
3192
3261
  const username = process.env.GITHUB_USERNAME || 'underpostnet';
3193
- const repoName = `engine-test-${deployId.split('-')[1]}`;
3262
+ const repoName = Underpost.repo.engineRepoFactory(deployId, { test: true });
3194
3263
  const templatePath = '/home/dd/pwa-microservices-template';
3195
3264
  if (!fs.existsSync(templatePath))
3196
3265
  throw new Error(`updatePrivateEngineTestRepo: assemble the template first (node bin/build ${deployId})`);
3197
3266
 
3198
- // Detach the assembled working tree from any engine-build git history.
3199
- shellExec(`sudo rm -rf ${templatePath}/.git`);
3200
-
3201
3267
  // Adopt the test repo's existing history when present (so the push is a delta);
3202
3268
  // otherwise publish a fresh history on first push.
3203
- shellExec(`cd /home/dd && sudo rm -rf ./${repoName}.git && underpost clone --bare ${username}/${repoName}`, {
3269
+ const parentPath = dir.dirname(templatePath);
3270
+ shellExec(`cd ${parentPath} && sudo rm -rf ./${repoName}.git && underpost clone --bare ${username}/${repoName}`, {
3204
3271
  silent: true,
3205
3272
  disableLog: true,
3206
3273
  silentOnError: true,
3207
3274
  });
3208
- if (fs.existsSync(`/home/dd/${repoName}.git`)) shellExec(`mv /home/dd/${repoName}.git ${templatePath}/.git`);
3275
+
3276
+ // Publishing from a work tree of its own leaves the template checkout its own git
3277
+ // history, and copying that tree from 0 keeps a stale file out of the published one.
3278
+ const publishPath = `${parentPath}/${repoName}`;
3279
+ shellExec(`sudo rm -rf ${publishPath}`);
3280
+ fs.copySync(templatePath, publishPath, {
3281
+ filter: (src) => {
3282
+ const entries = dir.relative(templatePath, src).split(dir.sep);
3283
+ return !entries.includes('.git') && !entries.includes('node_modules');
3284
+ },
3285
+ });
3286
+ if (fs.existsSync(`${parentPath}/${repoName}.git`)) shellExec(`mv ${parentPath}/${repoName}.git ${publishPath}/.git`);
3209
3287
 
3210
3288
  // `git init` converts the moved bare repo into a normal work-tree repo (bare
3211
3289
  // clones have no work tree, so `git add` would fail), and bootstraps a fresh
3212
3290
  // repo on first publish. Idempotent — mirrors updatePrivateTemplateRepo.
3213
- shellExec(`cd ${templatePath}
3291
+ shellExec(`cd ${publishPath}
3214
3292
  git init
3215
3293
  git config user.name '${username}'
3216
3294
  git config user.email 'development@underpost.net'
3217
- git add .`);
3295
+ git add -A .`);
3218
3296
 
3219
- const hasChanges = shellExec(`node bin cmt ${templatePath} --has-changes`, {
3297
+ // The bundled coverage report is a build output the artifact has to carry: force-added so a
3298
+ // tree-level ignore rule can never silently drop it from the published source.
3299
+ if (fs.existsSync(`${publishPath}/${COVERAGE_BUNDLE_DIRECTORY}`))
3300
+ shellExec(`cd ${publishPath} && git add -A -f ${COVERAGE_BUNDLE_DIRECTORY}`);
3301
+
3302
+ const hasChanges = shellExec(`node bin cmt ${publishPath} --has-changes`, {
3220
3303
  stdout: true,
3221
3304
  silent: true,
3222
3305
  disableLog: true,
3223
3306
  }).trim();
3224
3307
  if (hasChanges === '1')
3225
- shellExec(`cd ${templatePath} && git commit -m 'Update ${repoName}' && underpost push . ${username}/${repoName}`);
3308
+ shellExec(`cd ${publishPath} && git commit -m 'Update ${repoName}' && underpost push . ${username}/${repoName}`);
3226
3309
  else logger.info('No changes to publish', { repoName });
3227
3310
  };
3228
3311
 
@@ -3243,12 +3326,15 @@ git add .`);
3243
3326
  * instance runs — `/etc/hosts` is written in one pass for every host the
3244
3327
  * gateway will serve.
3245
3328
  * @param {Array<string>} deployList - Deploy ids being brought up.
3246
- * @param {string} [instanceList] - `+`-separated instance/template ids.
3329
+ * @param {string} [instanceList] - Comma-separated instance/template ids, as `--instance-id` takes them.
3247
3330
  * @returns {{ byDeployId: Object<string,{ids: Array<string>, hosts: Array<string>}>, unmatched: Array<string> }}
3248
3331
  * Per-deploy selection, and the requested ids no deploy declares.
3249
3332
  */
3250
3333
  const clusterInstancesFactory = (deployList = [], instanceList = '') => {
3251
- const requested = `${instanceList || ''}`.split('+').filter((id) => id.trim());
3334
+ const requested = `${instanceList || ''}`
3335
+ .split(',')
3336
+ .map((id) => id.trim())
3337
+ .filter(Boolean);
3252
3338
  const byDeployId = Object.fromEntries(
3253
3339
  deployList.map((deployId) => {
3254
3340
  const confPath = `./engine-private/conf/${deployId}/conf.instances.json`;
@@ -3273,6 +3359,10 @@ const clusterInstancesFactory = (deployList = [], instanceList = '') => {
3273
3359
  };
3274
3360
 
3275
3361
  export {
3362
+ cleanDeployEnvFiles,
3363
+ deployEnvFilePath,
3364
+ deployEnvNamesFactory,
3365
+ instanceEnvFilePath,
3276
3366
  Config,
3277
3367
  loadConf,
3278
3368
  loadConfInstances,
@@ -3292,25 +3382,19 @@ export {
3292
3382
  addWsConf,
3293
3383
  buildWsSrc,
3294
3384
  cloneSrcComponents,
3295
- buildProxyRouter,
3296
3385
  getDataDeploy,
3297
3386
  validateTemplatePath,
3298
3387
  buildReplicaId,
3299
3388
  mergeFile,
3300
3389
  getPathsSSR,
3301
- buildKindPorts,
3302
- buildPortProxyRouter,
3303
3390
  splitFileFactory,
3304
3391
  generateSecurePassword,
3305
3392
  resolveReplicaCount,
3306
- pathPortAssignmentFactory,
3307
- deployRangePortFactory,
3308
3393
  awaitDeployMonitor,
3309
3394
  buildCliDoc,
3310
3395
  getInstanceContext,
3311
3396
  buildApiConf,
3312
3397
  buildClientStaticConf,
3313
- isDeployRunnerContext,
3314
3398
  isDevProxyContext,
3315
3399
  devProxyHostFactory,
3316
3400
  isTlsDevProxy,
@@ -3320,6 +3404,10 @@ export {
3320
3404
  loadConfServerJson,
3321
3405
  getConfFolder,
3322
3406
  getConfFilePath,
3407
+ deployEnvContentFactory,
3408
+ deployOciEnvFilePath,
3409
+ ociEnvContentFactory,
3410
+ OCI_ENV_SUFFIX,
3323
3411
  readConfJson,
3324
3412
  DEFAULT_DEPLOY_ID,
3325
3413
  clusterContextFactory,
@@ -3344,10 +3432,13 @@ export {
3344
3432
  trafficProbePathsFactory,
3345
3433
  hostIngressFactsFactory,
3346
3434
  curlStatusChainFactory,
3435
+ publicIngressProbeFactory,
3436
+ publicIngressUrlsFactory,
3347
3437
  hostRenderInstancesFactory,
3348
3438
  instanceTrafficPlanFactory,
3349
3439
  isTrafficServingFactory,
3350
3440
  nextTrafficFactory,
3441
+ redeployPlanFactory,
3351
3442
  schedulableNodeFactory,
3352
3443
  stopPlanFactory,
3353
3444
  trafficFromRoutingInfoFactory,
@@ -3355,9 +3446,11 @@ export {
3355
3446
  resolveEnvScoped,
3356
3447
  sortInstancesByPath,
3357
3448
  waitForPort,
3358
- resolveDeployList,
3359
3449
  syncPrivateConf,
3360
3450
  syncDeployIdSources,
3451
+ gitOriginRepositoryName,
3452
+ ensureTemplateCheckout,
3453
+ pruneTemplateWorkTree,
3361
3454
  buildTemplate,
3362
3455
  updatePrivateTemplateRepo,
3363
3456
  updatePrivateEngineTestRepo,