@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
@@ -0,0 +1,361 @@
1
+ 'use strict';
2
+
3
+ import { expect } from 'chai';
4
+ import fs from 'fs-extra';
5
+ import shell from 'shelljs';
6
+ import { COVERAGE_BUNDLE_DIRECTORY } from '../../src/server/build/coverage.js';
7
+ import { buildCoverage, buildDocs, buildSwaggerUiOptions } from '../../src/client-builder/client-build-docs.js';
8
+
9
+ // swagger-autogen is a code generator that walks the real router files and
10
+ // writes into ./public. The doc it is handed is what this module is responsible
11
+ // for, so the generator itself is replaced and its arguments are asserted; it
12
+ // writes the document a test stages as its output, so the post-process has one.
13
+ const generated = vi.hoisted(() => []);
14
+ const staged = vi.hoisted(() => ({ swaggerJson: null }));
15
+ vi.mock('swagger-autogen', async () => {
16
+ const fs = (await import('fs-extra')).default;
17
+ return {
18
+ default: (options) => async (outputFile, routes, doc) => {
19
+ generated.push({ options, outputFile, routes, doc });
20
+ if (staged.swaggerJson) fs.outputJsonSync(outputFile, staged.swaggerJson);
21
+ },
22
+ };
23
+ });
24
+
25
+ describe('client coverage build', () => {
26
+ let fixturePath;
27
+ let cwd;
28
+
29
+ beforeEach(() => {
30
+ fixturePath = fs.mkdtempSync('/tmp/engine-coverage-');
31
+ // Suite reports resolve against the engine root, so the fixture stands in for it.
32
+ cwd = process.cwd();
33
+ process.chdir(fixturePath);
34
+ });
35
+
36
+ afterEach(() => {
37
+ process.chdir(cwd);
38
+ fs.removeSync(fixturePath);
39
+ vi.restoreAllMocks();
40
+ });
41
+
42
+ it('publishes each declared report at its own coverage route', async () => {
43
+ fs.outputFileSync(`${fixturePath}/coverage/cyberia/index.html`, '<!doctype html><title>cyberia</title>');
44
+ fs.outputFileSync(`${fixturePath}/hardhat/coverage/html/index.html`, '<!doctype html><title>hardhat</title>');
45
+ fs.outputFileSync(`${fixturePath}/hardhat/coverage/html/base.css`, 'body {}');
46
+ const docsDestination = `${fixturePath}/public/docs/`;
47
+
48
+ await buildCoverage({
49
+ docs: {
50
+ coverage: [
51
+ { id: 'cyberia', suite: 'cyberia' },
52
+ { id: 'hardhat', path: `${fixturePath}/hardhat` },
53
+ ],
54
+ },
55
+ docsDestination,
56
+ });
57
+
58
+ expect(fs.readFileSync(`${docsDestination}coverage/cyberia/index.html`, 'utf8')).to.include('cyberia');
59
+ expect(fs.readFileSync(`${docsDestination}coverage/hardhat/index.html`, 'utf8')).to.include('hardhat');
60
+ expect(fs.existsSync(`${docsDestination}coverage/hardhat/base.css`)).to.equal(true);
61
+ expect(fs.existsSync(`${docsDestination}coverage/hardhat/html`)).to.equal(false);
62
+ });
63
+
64
+ it('drops a previously published report the conf no longer declares', async () => {
65
+ fs.outputFileSync(`${fixturePath}/out/coverage/legacy/index.html`, '<!doctype html>');
66
+ fs.outputFileSync(`${fixturePath}/coverage/unit/index.html`, '<!doctype html><title>unit</title>');
67
+
68
+ await buildCoverage({
69
+ docs: { coverage: [{ id: 'engine', suite: 'unit' }] },
70
+ docsDestination: `${fixturePath}/out/`,
71
+ });
72
+
73
+ expect(fs.existsSync(`${fixturePath}/out/coverage/legacy`)).to.equal(false);
74
+ expect(fs.existsSync(`${fixturePath}/out/coverage/engine/index.html`)).to.equal(true);
75
+ });
76
+
77
+ it("publishes the run a report names, never another selection's", async () => {
78
+ fs.outputFileSync(`${fixturePath}/coverage/unit-infra-app-cyberia/index.html`, '<!doctype html><title>all</title>');
79
+
80
+ await buildCoverage({
81
+ docs: { coverage: [{ id: 'engine', suite: 'unit,infra,app' }] },
82
+ docsDestination: `${fixturePath}/out/`,
83
+ });
84
+
85
+ const page = fs.readFileSync(`${fixturePath}/out/coverage/engine/index.html`, 'utf8');
86
+ expect(page).to.include('Coverage report unavailable');
87
+ expect(page).to.include('node bin test unit,infra,app');
88
+ });
89
+
90
+ it('publishes the unavailable page for a coverage directory with no HTML index', async () => {
91
+ fs.outputFileSync(`${fixturePath}/hardhat/coverage/lcov.info`, 'TN:\n');
92
+
93
+ await buildCoverage({
94
+ docs: { coverage: [{ id: 'hardhat', path: `${fixturePath}/hardhat` }] },
95
+ docsDestination: `${fixturePath}/out/`,
96
+ });
97
+
98
+ expect(fs.readFileSync(`${fixturePath}/out/coverage/hardhat/index.html`, 'utf8')).to.include(
99
+ 'Coverage report unavailable',
100
+ );
101
+ });
102
+
103
+ it('publishes the report an assembled deploy artifact bundled', async () => {
104
+ fs.outputFileSync(
105
+ `${fixturePath}/${COVERAGE_BUNDLE_DIRECTORY}/engine/index.html`,
106
+ '<!doctype html><title>bundled</title>',
107
+ );
108
+
109
+ await buildCoverage({
110
+ docs: { coverage: [{ id: 'engine', suite: 'unit' }] },
111
+ docsDestination: `${fixturePath}/out/`,
112
+ });
113
+
114
+ expect(fs.readFileSync(`${fixturePath}/out/coverage/engine/index.html`, 'utf8')).to.include('bundled');
115
+ });
116
+
117
+ it('prefers a freshly generated report over the bundled artifact', async () => {
118
+ fs.outputFileSync(`${fixturePath}/coverage/unit/index.html`, '<!doctype html><title>fresh</title>');
119
+ fs.outputFileSync(
120
+ `${fixturePath}/${COVERAGE_BUNDLE_DIRECTORY}/engine/index.html`,
121
+ '<!doctype html><title>bundled</title>',
122
+ );
123
+
124
+ await buildCoverage({
125
+ docs: { coverage: [{ id: 'engine', suite: 'unit' }] },
126
+ docsDestination: `${fixturePath}/out/`,
127
+ });
128
+
129
+ expect(fs.readFileSync(`${fixturePath}/out/coverage/engine/index.html`, 'utf8')).to.include('fresh');
130
+ });
131
+
132
+ // Regression: the build used to shell out to `npm test` when no report was present. Inside a
133
+ // pod that spent minutes of the build phase on a test runner, and the suite's expected
134
+ // non-zero exits latched `container-status=error`, failing a healthy rollout.
135
+ it('never runs a test runner to produce a missing report', async () => {
136
+ fs.outputJsonSync(`${fixturePath}/package.json`, {
137
+ scripts: { coverage: 'vitest run --coverage', test: 'vitest run' },
138
+ });
139
+ const exec = vi.spyOn(shell, 'exec');
140
+
141
+ await buildCoverage({
142
+ docs: { coverage: [{ id: 'engine', suite: 'unit' }] },
143
+ docsDestination: `${fixturePath}/out/`,
144
+ });
145
+
146
+ expect(exec.mock.calls.length).to.equal(0);
147
+ expect(fs.readFileSync(`${fixturePath}/out/coverage/engine/index.html`, 'utf8')).to.include(
148
+ 'Coverage report unavailable',
149
+ );
150
+ });
151
+
152
+ it('publishes nothing for a deploy that declares no coverage report', async () => {
153
+ await buildCoverage({ docs: {}, docsDestination: `${fixturePath}/out/` });
154
+
155
+ expect(fs.existsSync(`${fixturePath}/out`)).to.equal(false);
156
+ });
157
+ });
158
+
159
+ describe('docs build', () => {
160
+ let fixturePath;
161
+ let publicRoot;
162
+
163
+ const PACKAGE_DATA = { version: 'v1.2.3' };
164
+ const HOST = 'docs.fixture.test';
165
+
166
+ const runBuildDocs = async (overrides = {}) =>
167
+ buildDocs({
168
+ host: HOST,
169
+ path: '/',
170
+ port: 4000,
171
+ metadata: { title: 'Fixture API', description: 'fixture docs' },
172
+ apis: ['user', 'object-layer', 'unknown-api'],
173
+ publicClientId: 'fixture',
174
+ rootClientPath: `${fixturePath}/client`,
175
+ packageData: PACKAGE_DATA,
176
+ docs: { jsJsonPath: `${fixturePath}/typedoc.json` },
177
+ ...overrides,
178
+ });
179
+
180
+ beforeEach(() => {
181
+ generated.length = 0;
182
+ fixturePath = fs.mkdtempSync('/tmp/engine-docs-');
183
+ publicRoot = `./public/${HOST}`;
184
+ if (fs.existsSync(publicRoot)) throw new Error(`Refusing to write fixtures into an existing site: ${publicRoot}`);
185
+ staged.swaggerJson = null;
186
+ });
187
+
188
+ afterEach(() => {
189
+ vi.restoreAllMocks();
190
+ fs.removeSync(fixturePath);
191
+ fs.removeSync(publicRoot);
192
+ fs.removeSync('.typedoc.tmp.json');
193
+ });
194
+
195
+ it('renders the typedoc runtime config without mutating the base config on disk', async () => {
196
+ const baseConfig = { tsconfig: './tsconfig.docs.json', name: 'Base', entryPoints: ['./src'] };
197
+ fs.outputJsonSync(`${fixturePath}/typedoc.json`, baseConfig);
198
+ fs.outputFileSync(`${fixturePath}/reference-a.md`, '# a');
199
+ let tmpConfig;
200
+ vi.spyOn(shell, 'exec').mockImplementation(() => {
201
+ tmpConfig = JSON.parse(fs.readFileSync('.typedoc.tmp.json', 'utf8'));
202
+ return { code: 0, stdout: '', stderr: '' };
203
+ });
204
+
205
+ await runBuildDocs({
206
+ docs: {
207
+ jsJsonPath: `${fixturePath}/typedoc.json`,
208
+ references: [`${fixturePath}/reference-a.md`, `${fixturePath}/missing.md`],
209
+ },
210
+ });
211
+
212
+ expect(tmpConfig.name).to.equal('Fixture API');
213
+ expect(tmpConfig.out).to.equal(`./public/${HOST}/docs/engine/1.2.3/`);
214
+ expect(tmpConfig.favicon).to.equal(`./public/${HOST}/favicon.ico`);
215
+ expect(tmpConfig.tsconfig.startsWith('/')).to.equal(true);
216
+ expect(tmpConfig.projectDocuments).to.deep.equal([`${fixturePath}/reference-a.md`]);
217
+ expect(fs.readJsonSync(`${fixturePath}/typedoc.json`)).to.deep.equal(baseConfig);
218
+ expect(fs.existsSync('.typedoc.tmp.json')).to.equal(false);
219
+ });
220
+
221
+ it('keeps the base name and drops the documents key when nothing resolves', async () => {
222
+ fs.outputJsonSync(`${fixturePath}/typedoc.json`, { tsconfig: './tsconfig.docs.json', name: 'Base' });
223
+ let tmpConfig;
224
+ vi.spyOn(shell, 'exec').mockImplementation(() => {
225
+ tmpConfig = JSON.parse(fs.readFileSync('.typedoc.tmp.json', 'utf8'));
226
+ return { code: 0, stdout: '', stderr: '' };
227
+ });
228
+
229
+ await runBuildDocs({ metadata: {}, docs: { jsJsonPath: `${fixturePath}/typedoc.json`, references: [] } });
230
+
231
+ expect(tmpConfig.name).to.equal('Base');
232
+ expect(tmpConfig).not.to.have.property('projectDocuments');
233
+ });
234
+
235
+ it('skips the typedoc build when the deploy declares no config', async () => {
236
+ const exec = vi.spyOn(shell, 'exec');
237
+ await runBuildDocs({ docs: { jsJsonPath: `${fixturePath}/absent.json` } });
238
+ expect(exec.mock.calls.length).to.equal(0);
239
+ });
240
+
241
+ it('documents only the routers it has schemas for, at the deploy version', async () => {
242
+ const exec = vi.spyOn(shell, 'exec').mockReturnValue({ code: 0, stdout: '', stderr: '' });
243
+ await runBuildDocs();
244
+
245
+ expect(exec.mock.calls.length).to.equal(0);
246
+ expect(generated.length).to.equal(1);
247
+ const [{ options, outputFile, routes, doc }] = generated;
248
+ expect(options).to.deep.equal({ openapi: '3.0.0' });
249
+ expect(outputFile).to.equal(`./public/${HOST}/swagger-output.json`);
250
+ expect(routes).to.deep.equal(['./src/api/user/user.router.js', './src/api/object-layer/object-layer.router.js']);
251
+ expect(doc.info).to.deep.equal({ version: 'v1.2.3', title: 'Fixture API', description: 'fixture docs' });
252
+ expect(doc.components.securitySchemes.bearerAuth).to.deep.equal({ type: 'http', scheme: 'bearer' });
253
+ });
254
+
255
+ it('falls back to a generic title and empty description without metadata', async () => {
256
+ await runBuildDocs({ metadata: {}, docs: { jsJsonPath: `${fixturePath}/absent.json` } });
257
+ expect(generated[0].doc.info.title).to.equal('REST API');
258
+ expect(generated[0].doc.info.description).to.equal('');
259
+ });
260
+
261
+ it('points the server at localhost in development and at the host in production', async () => {
262
+ const previousEnv = process.env.NODE_ENV;
263
+ const previousApi = process.env.BASE_API;
264
+ process.env.BASE_API = 'api';
265
+ try {
266
+ process.env.NODE_ENV = 'development';
267
+ await runBuildDocs({ path: '/store', docs: { jsJsonPath: `${fixturePath}/absent.json` } });
268
+ expect(generated[0].doc.servers[0].url).to.equal('http://localhost:4000/store/api');
269
+ expect(generated[0].outputFile).to.equal(`./public/${HOST}/store/swagger-output.json`);
270
+
271
+ generated.length = 0;
272
+ process.env.NODE_ENV = 'production';
273
+ await runBuildDocs({ docs: { jsJsonPath: `${fixturePath}/absent.json` } });
274
+ expect(generated[0].doc.servers[0].url).to.equal(`https://${HOST}/api`);
275
+ } finally {
276
+ process.env.NODE_ENV = previousEnv;
277
+ if (previousApi === undefined) delete process.env.BASE_API;
278
+ else process.env.BASE_API = previousApi;
279
+ }
280
+ });
281
+
282
+ it('points the server at the API runtime when the client and the API are split', async () => {
283
+ const previousEnv = process.env.NODE_ENV;
284
+ const previousApi = process.env.BASE_API;
285
+ process.env.BASE_API = 'api';
286
+ try {
287
+ process.env.NODE_ENV = 'development';
288
+ await runBuildDocs({
289
+ apiBaseHost: 'localhost:4017',
290
+ apiBaseProxyPath: '/',
291
+ docs: { jsJsonPath: `${fixturePath}/absent.json` },
292
+ });
293
+ expect(generated[0].doc.servers[0].url).to.equal('http://localhost:4017/api');
294
+
295
+ generated.length = 0;
296
+ process.env.NODE_ENV = 'production';
297
+ await runBuildDocs({
298
+ path: '/store',
299
+ apiBaseHost: 'api.fixture.test',
300
+ apiBaseProxyPath: '/store',
301
+ docs: { jsJsonPath: `${fixturePath}/absent.json` },
302
+ });
303
+ expect(generated[0].doc.servers[0].url).to.equal('https://api.fixture.test/store/api');
304
+ } finally {
305
+ process.env.NODE_ENV = previousEnv;
306
+ if (previousApi === undefined) delete process.env.BASE_API;
307
+ else process.env.BASE_API = previousApi;
308
+ }
309
+ });
310
+
311
+ // swagger-autogen has no OAS-3 requestBody support, so the generated document
312
+ // is patched afterwards. The patch has to survive an operation the generator
313
+ // did not emit, and has to strip the OAS-2 `in: body` parameter it did.
314
+ it('injects the request bodies into the generated document', async () => {
315
+ const outputFile = `./public/${HOST}/swagger-output.json`;
316
+ staged.swaggerJson = {
317
+ paths: {
318
+ '/user': {
319
+ post: {
320
+ parameters: [
321
+ { in: 'body', name: 'obj' },
322
+ { in: 'query', name: 'q' },
323
+ ],
324
+ },
325
+ },
326
+ '/user/{id}': { get: {} },
327
+ },
328
+ };
329
+ await runBuildDocs({ docs: { jsJsonPath: `${fixturePath}/absent.json` } });
330
+
331
+ const patched = fs.readJsonSync(outputFile);
332
+ expect(patched.paths['/user'].post.requestBody.content['application/json'].schema.$ref).to.equal(
333
+ '#/components/schemas/userRequest',
334
+ );
335
+ expect(patched.paths['/user'].post.parameters).to.deep.equal([{ in: 'query', name: 'q' }]);
336
+ expect(patched.paths['/user/{id}'].get).not.to.have.property('requestBody');
337
+ });
338
+
339
+ it('leaves the generated document alone when no operation matches', async () => {
340
+ const outputFile = `./public/${HOST}/swagger-output.json`;
341
+ staged.swaggerJson = { paths: { '/health': { get: {} } } };
342
+ await runBuildDocs({ docs: { jsJsonPath: `${fixturePath}/absent.json` } });
343
+ expect(fs.readJsonSync(outputFile)).to.deep.equal({ paths: { '/health': { get: {} } } });
344
+ });
345
+
346
+ // Regression: the spec was generated on a timer the build never awaited, so a bundle zipped
347
+ // right after the build shipped without it and the pod restoring it served no api-docs.
348
+ it('has written the spec by the time the build returns', async () => {
349
+ staged.swaggerJson = { paths: {} };
350
+ await runBuildDocs({ docs: { jsJsonPath: `${fixturePath}/absent.json` } });
351
+ expect(fs.existsSync(`./public/${HOST}/swagger-output.json`)).to.equal(true);
352
+ });
353
+ });
354
+
355
+ describe('swagger UI options', () => {
356
+ it('renders the dark mode toggle css and script from the SSR component', async () => {
357
+ const { customCss, customJsStr } = await buildSwaggerUiOptions();
358
+ expect(customCss).to.be.a('string').and.not.equal('');
359
+ expect(customJsStr).to.be.a('string').and.not.equal('');
360
+ });
361
+ });
@@ -0,0 +1,313 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @module client-bundle.test
5
+ * @description Covers the client bundle transport: the route set push and pull agree on, the
6
+ * uniform host filter and split option both runners parse, and the pull path that merges split
7
+ * parts, extracts them in-process, and installs the result under the served public directory.
8
+ *
9
+ * The shell boundary is mocked so the deterministic parts — route selection, artifact
10
+ * resolution, and the local zip round trip — run for real.
11
+ *
12
+ * Uses 'chai' for assertions.
13
+ */
14
+
15
+ import { expect } from 'chai';
16
+ import fs from 'fs-extra';
17
+ import { vi } from 'vitest';
18
+
19
+ const shellCommands = [];
20
+ vi.mock('../../src/server/runtime/process.js', async (importOriginal) => ({
21
+ ...(await importOriginal()),
22
+ shellExec: (command) => {
23
+ shellCommands.push(command);
24
+ return '';
25
+ },
26
+ }));
27
+
28
+ const {
29
+ CLIENT_BUNDLE_DIRECTORY,
30
+ clientBundleTrackedKeys,
31
+ clientBundleManifestKey,
32
+ clientBundleArtifactPaths,
33
+ clientBundleEntriesFactory,
34
+ clientBundleHostFilter,
35
+ clientBundleSplitFlag,
36
+ clientBundleStorageFilePath,
37
+ pullClientBundle,
38
+ pushClientBundle,
39
+ } = await import('../../src/client-builder/client-bundle.js');
40
+ const { zipFromLocalFiles } = await import('../../src/server/storage/zip.js');
41
+ const { program } = await import('../../src/cli/index.js');
42
+
43
+ const CONF_SERVER = {
44
+ 'a.com': {
45
+ '/': { client: 'A' },
46
+ '/peer': { client: 'P' },
47
+ '/old': { redirect: '/' },
48
+ '/frozen': { disabledRebuild: true },
49
+ '/multi': { client: 'M', replicas: ['/multi-1', '/multi-2'] },
50
+ '/single': { client: 'S', singleReplica: true, replicas: ['/single-1'] },
51
+ },
52
+ 'b.com': { '/': { client: 'B' } },
53
+ };
54
+
55
+ describe('client bundle runner input', () => {
56
+ it('parses the comma-separated host filter both runners share', () => {
57
+ expect(clientBundleHostFilter(' a.com , b.com ,, ')).to.deep.equal(['a.com', 'b.com']);
58
+ expect(clientBundleHostFilter('')).to.deep.equal([]);
59
+ expect(clientBundleHostFilter(undefined)).to.deep.equal([]);
60
+ });
61
+
62
+ it('resolves the split option, defaulting an unusable value rather than failing the build', () => {
63
+ expect(clientBundleSplitFlag(undefined)).to.equal('--split 8');
64
+ expect(clientBundleSplitFlag('16')).to.equal('--split 16');
65
+ expect(clientBundleSplitFlag('none')).to.equal('');
66
+ expect(clientBundleSplitFlag('0')).to.equal('--split 8');
67
+ expect(clientBundleSplitFlag('abc')).to.equal('--split 8');
68
+ });
69
+
70
+ it('names one storage manifest per deployment', () => {
71
+ expect(clientBundleStorageFilePath('dd-test')).to.equal('engine-private/conf/dd-test/storage.bundle.json');
72
+ });
73
+ });
74
+
75
+ describe('client bundle route selection', () => {
76
+ let fixturePath;
77
+ let previousCwd;
78
+
79
+ beforeEach(() => {
80
+ previousCwd = process.cwd();
81
+ fixturePath = fs.mkdtempSync('/tmp/engine-client-bundle-');
82
+ process.chdir(fixturePath);
83
+ fs.mkdirSync('engine-private/conf/dd-test', { recursive: true });
84
+ fs.writeFileSync('engine-private/conf/dd-test/conf.server.json', JSON.stringify(CONF_SERVER), 'utf8');
85
+ shellCommands.length = 0;
86
+ });
87
+
88
+ afterEach(() => {
89
+ process.chdir(previousCwd);
90
+ fs.removeSync(fixturePath);
91
+ });
92
+
93
+ const buildIds = (options) => clientBundleEntriesFactory(options).map((entry) => entry.buildId);
94
+
95
+ it('selects every route the client build emits a bundle for, replicas included', () => {
96
+ expect(buildIds({ deployId: 'dd-test' })).to.deep.equal([
97
+ 'a.com-',
98
+ 'a.com-peer',
99
+ 'a.com-multi',
100
+ 'a.com-multi-1',
101
+ 'a.com-multi-2',
102
+ 'b.com-',
103
+ ]);
104
+ });
105
+
106
+ it('skips the routes the client build skips', () => {
107
+ const selected = buildIds({ deployId: 'dd-test' });
108
+ expect(selected).to.not.include('a.com-old');
109
+ expect(selected).to.not.include('a.com-frozen');
110
+ expect(selected).to.not.include('a.com-single');
111
+ expect(selected).to.not.include('a.com-single-1');
112
+ });
113
+
114
+ it('maps a route to its served public directory', () => {
115
+ const entries = clientBundleEntriesFactory({ deployId: 'dd-test', hosts: ['a.com'] });
116
+ const byId = Object.fromEntries(entries.map((entry) => [entry.buildId, entry.publicPath]));
117
+ expect(byId['a.com-']).to.equal('public/a.com');
118
+ expect(byId['a.com-peer']).to.equal('public/a.com/peer');
119
+ });
120
+
121
+ it('applies the host filter, and resolves nothing for an unknown host or deployment', () => {
122
+ expect(buildIds({ deployId: 'dd-test', hosts: ['b.com'] })).to.deep.equal(['b.com-']);
123
+ expect(buildIds({ deployId: 'dd-test', hosts: ['absent.com'] })).to.deep.equal([]);
124
+ expect(buildIds({ deployId: 'dd-absent' })).to.deep.equal([]);
125
+ });
126
+
127
+ it('resolves split parts ahead of a single zip, and nothing when neither is present', () => {
128
+ fs.mkdirSync(CLIENT_BUNDLE_DIRECTORY, { recursive: true });
129
+ for (const part of ['001', '002']) fs.writeFileSync(`${CLIENT_BUNDLE_DIRECTORY}/a.com-.zip.part${part}`, 'x');
130
+ fs.writeFileSync(`${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip`, 'y');
131
+
132
+ expect(clientBundleArtifactPaths(`${CLIENT_BUNDLE_DIRECTORY}/a.com-.zip`).artifactPaths).to.have.lengthOf(2);
133
+ expect(clientBundleArtifactPaths(`${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip`).artifactPaths).to.deep.equal([
134
+ `${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip`,
135
+ ]);
136
+ expect(clientBundleArtifactPaths(`${CLIENT_BUNDLE_DIRECTORY}/absent-.zip`).artifactPaths).to.deep.equal([]);
137
+ expect(clientBundleArtifactPaths('./no-such-dir/x.zip').artifactPaths).to.deep.equal([]);
138
+ });
139
+
140
+ it('uploads every artifact of every resolved route, and reports the routes with none', () => {
141
+ fs.mkdirSync(CLIENT_BUNDLE_DIRECTORY, { recursive: true });
142
+ for (const part of ['001', '002']) fs.writeFileSync(`${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip.part${part}`, 'x');
143
+
144
+ const result = pushClientBundle({ deployId: 'dd-test', hosts: ['b.com'], split: '4' });
145
+
146
+ expect(result).to.deep.equal({ pushed: 1, skipped: 0 });
147
+ expect(shellCommands.some((command) => command.includes('--build-zip --split 4'))).to.equal(true);
148
+ const uploads = shellCommands.filter((command) => command.includes(' fs '));
149
+ expect(uploads).to.have.lengthOf(2);
150
+ expect(uploads.every((command) => command.includes('--storage-id bundle') && command.includes('--force'))).to.equal(
151
+ true,
152
+ );
153
+
154
+ shellCommands.length = 0;
155
+ expect(pushClientBundle({ deployId: 'dd-test', hosts: ['a.com'] })).to.deep.equal({ pushed: 0, skipped: 5 });
156
+ });
157
+
158
+ it('shells out only to subcommands the CLI still registers', async () => {
159
+ // Regression: both directions called `underpost env`, a deprecated alias removed when the
160
+ // env and config commands were folded into the host configuration store. Nothing failed at
161
+ // import time, so bundle mode stayed broken until a deployment ran it.
162
+ fs.mkdirSync(CLIENT_BUNDLE_DIRECTORY, { recursive: true });
163
+ fs.writeFileSync('bundled.html', '<h1>bundled</h1>', 'utf8');
164
+ fs.writeFileSync(
165
+ `${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip`,
166
+ await zipFromLocalFiles([{ localPath: 'bundled.html', entryName: 'index.html' }]),
167
+ );
168
+ pushClientBundle({ deployId: 'dd-test', hosts: ['b.com'] });
169
+ await pullClientBundle({ deployId: 'dd-test', hosts: ['b.com'] });
170
+
171
+ const pull = shellCommands.find((command) => command.includes(' fs ') && command.includes('--pull'));
172
+ expect(pull).to.include('--tracked').and.to.include('--storage-id bundle');
173
+ expect(pull).not.to.include('--storage-file-path');
174
+
175
+ const registered = program.commands.map((command) => command.name());
176
+ const invoked = [
177
+ ...new Set(
178
+ shellCommands
179
+ .map((command) => /(?:^|\s)(?:node\s+\S*bin\S*|underpost)\s+([a-z-]+)/.exec(command)?.[1])
180
+ .filter(Boolean),
181
+ ),
182
+ ];
183
+
184
+ expect(invoked).to.not.be.empty;
185
+ for (const subcommand of invoked) expect(registered, subcommand).to.include(subcommand);
186
+ });
187
+
188
+ it('untracks bundle parts a smaller build no longer produces', async () => {
189
+ const manifest = {
190
+ 'build/b.com-.zip.part001': { bytes: 1 },
191
+ 'build/b.com-.zip.part002': { bytes: 1 },
192
+ 'build/b.com-.zip.part003': { bytes: 1 },
193
+ 'build/other.com-.zip.part003': { bytes: 1 },
194
+ };
195
+ expect(clientBundleTrackedKeys(manifest, 'b.com-')).to.deep.equal([
196
+ 'build/b.com-.zip.part001',
197
+ 'build/b.com-.zip.part002',
198
+ 'build/b.com-.zip.part003',
199
+ ]);
200
+
201
+ fs.mkdirSync(CLIENT_BUNDLE_DIRECTORY, { recursive: true });
202
+ for (const part of ['001', '002']) fs.writeFileSync(`${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip.part${part}`, 'x');
203
+ fs.writeFileSync('engine-private/conf/dd-test/storage.bundle.json', JSON.stringify(manifest), 'utf8');
204
+
205
+ shellCommands.length = 0;
206
+ expect(fs.existsSync('build/b.com-.zip.part003')).to.equal(false);
207
+ expect(pushClientBundle({ deployId: 'dd-test', hosts: ['b.com'] })).to.deep.equal({ pushed: 1, skipped: 0 });
208
+
209
+ const removals = shellCommands.filter((command) => command.includes(' --rm'));
210
+ expect(removals, 'exactly the part this build did not produce').to.have.lengthOf(1);
211
+ expect(removals[0]).to.include(" fs --tracked --key 'build/b.com-.zip.part003'");
212
+ expect(removals[0]).to.include('--deploy-id dd-test --storage-id bundle --rm');
213
+ expect(removals[0]).to.not.include('other.com');
214
+ const uploads = shellCommands.filter((command) => command.includes(' fs ') && command.includes('--force'));
215
+ expect(uploads).to.have.lengthOf(2);
216
+ expect(shellCommands.indexOf(removals[0])).to.be.greaterThan(shellCommands.indexOf(uploads[1]));
217
+ });
218
+
219
+ it('merges, extracts, and installs a split bundle into the public directory', async () => {
220
+ fs.mkdirSync('source/nested', { recursive: true });
221
+ fs.writeFileSync('source/index.html', '<h1>bundle</h1>'.repeat(100), 'utf8');
222
+ fs.writeFileSync('source/nested/app.js', 'console.log(1);'.repeat(100), 'utf8');
223
+ const archive = await zipFromLocalFiles([
224
+ { localPath: 'source/index.html', entryName: 'index.html' },
225
+ { localPath: 'source/nested/app.js', entryName: 'nested/app.js' },
226
+ ]);
227
+
228
+ fs.mkdirSync(CLIENT_BUNDLE_DIRECTORY, { recursive: true });
229
+ const partSize = Math.ceil(archive.length / 3);
230
+ for (let offset = 0, index = 1; offset < archive.length; offset += partSize, index++) {
231
+ const name = `${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip.part${String(index).padStart(3, '0')}`;
232
+ fs.writeFileSync(name, archive.subarray(offset, offset + partSize));
233
+ }
234
+
235
+ const result = await pullClientBundle({ deployId: 'dd-test', hosts: ['b.com'] });
236
+
237
+ expect(result).to.deep.equal({ pulled: 1, skipped: 0 });
238
+ expect(fs.readFileSync('public/b.com/nested/app.js', 'utf8')).to.equal(
239
+ fs.readFileSync('source/nested/app.js', 'utf8'),
240
+ );
241
+ expect(fs.existsSync(`${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip`)).to.equal(false);
242
+ expect(fs.readdirSync(CLIENT_BUNDLE_DIRECTORY).filter((name) => name.includes('.part'))).to.deep.equal([]);
243
+ });
244
+
245
+ it('maps a pulled part back to the storage key that produced it', () => {
246
+ // A pull keeping the downloaded archive writes `<key>.zip`; an unwrapped part is the key.
247
+ expect(clientBundleManifestKey('build/a.com-.zip.part001.zip')).to.equal('build/a.com-.zip.part001');
248
+ expect(clientBundleManifestKey('./build/a.com-.zip.part001.zip')).to.equal('build/a.com-.zip.part001');
249
+ expect(clientBundleManifestKey('build/a.com-.zip.part001')).to.equal('build/a.com-.zip.part001');
250
+ });
251
+
252
+ it('refuses a bundle that reassembles to a different size than was pushed', async () => {
253
+ // Regression: one part's upload failed silently, so the store kept the previous build's copy.
254
+ // The parts merged without complaint and only the assembled archive was corrupt, 190MB later.
255
+ fs.writeFileSync('source.html', '<h1>bundle</h1>'.repeat(100), 'utf8');
256
+ const archive = await zipFromLocalFiles([{ localPath: 'source.html', entryName: 'index.html' }]);
257
+ fs.mkdirSync(CLIENT_BUNDLE_DIRECTORY, { recursive: true });
258
+
259
+ const partSize = Math.ceil(archive.length / 2);
260
+ const partPaths = [];
261
+ for (let offset = 0, index = 1; offset < archive.length; offset += partSize, index++) {
262
+ const name = `${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip.part${String(index).padStart(3, '0')}`;
263
+ fs.writeFileSync(name, archive.subarray(offset, offset + partSize));
264
+ partPaths.push(name.replace(/^\.\//, ''));
265
+ }
266
+
267
+ // The manifest records a larger second part, as a build whose last part never replaced the
268
+ // previous one would.
269
+ fs.writeFileSync(
270
+ 'engine-private/conf/dd-test/storage.bundle.json',
271
+ JSON.stringify({
272
+ [partPaths[0]]: { bytes: fs.statSync(partPaths[0]).size },
273
+ [partPaths[1]]: { bytes: fs.statSync(partPaths[1]).size + 4096 },
274
+ }),
275
+ 'utf8',
276
+ );
277
+
278
+ let thrown;
279
+ await pullClientBundle({ deployId: 'dd-test', hosts: ['b.com'] }).catch((error) => (thrown = error));
280
+
281
+ expect(thrown?.message).to.match(/reassembled to \d+ bytes, expected \d+/);
282
+ expect(fs.existsSync('public/b.com'), 'a bundle that failed verification must not be installed').to.equal(false);
283
+ });
284
+
285
+ it('installs a bundle whose parts match the pushed sizes', async () => {
286
+ fs.writeFileSync('source.html', '<h1>bundle</h1>'.repeat(100), 'utf8');
287
+ const archive = await zipFromLocalFiles([{ localPath: 'source.html', entryName: 'index.html' }]);
288
+ fs.mkdirSync(CLIENT_BUNDLE_DIRECTORY, { recursive: true });
289
+
290
+ const partSize = Math.ceil(archive.length / 2);
291
+ const manifest = {};
292
+ for (let offset = 0, index = 1; offset < archive.length; offset += partSize, index++) {
293
+ const name = `${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip.part${String(index).padStart(3, '0')}`;
294
+ fs.writeFileSync(name, archive.subarray(offset, offset + partSize));
295
+ manifest[name.replace(/^\.\//, '')] = { bytes: fs.statSync(name).size };
296
+ }
297
+ fs.writeFileSync('engine-private/conf/dd-test/storage.bundle.json', JSON.stringify(manifest), 'utf8');
298
+
299
+ expect(await pullClientBundle({ deployId: 'dd-test', hosts: ['b.com'] })).to.deep.equal({ pulled: 1, skipped: 0 });
300
+ expect(fs.existsSync('public/b.com/index.html')).to.equal(true);
301
+ });
302
+
303
+ it('skips a route whose bundle never arrived rather than clearing its public directory', async () => {
304
+ fs.mkdirSync('public/b.com', { recursive: true });
305
+ fs.writeFileSync('public/b.com/index.html', 'live', 'utf8');
306
+
307
+ const result = await pullClientBundle({ deployId: 'dd-test', hosts: ['b.com'] });
308
+
309
+ expect(result).to.deep.equal({ pulled: 0, skipped: 1 });
310
+ expect(fs.readFileSync('public/b.com/index.html', 'utf8')).to.equal('live');
311
+ expect(shellCommands.some((command) => command.startsWith('sudo rm -rf'))).to.equal(false);
312
+ });
313
+ });