@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,596 @@
1
+ import { expect, vi } from 'vitest';
2
+ import fs from 'fs-extra';
3
+ import path from 'node:path';
4
+ import { execFileSync } from 'node:child_process';
5
+ import { Command } from 'commander';
6
+ import JSZip from 'jszip';
7
+ import { v2 as cloudinary } from 'cloudinary';
8
+ import UnderpostFileStorage from '../../src/cli/fs.js';
9
+ import UnderpostRepository from '../../src/cli/repository.js';
10
+ import Downloader from '../../src/server/storage/downloader.js';
11
+ import { program } from '../../src/cli/index.js';
12
+ import { clearTerminalStringColor } from '../../src/client/components/core/CommonJs.js';
13
+
14
+ vi.mock('cloudinary', () => ({
15
+ v2: {
16
+ config: vi.fn(),
17
+ uploader: { upload: vi.fn() },
18
+ api: { resource: vi.fn(), delete_resources: vi.fn() },
19
+ utils: { download_archive_url: vi.fn() },
20
+ },
21
+ }));
22
+ vi.mock('node:child_process', async (importOriginal) => {
23
+ const original = await importOriginal();
24
+ return { ...original, execFileSync: vi.fn(original.execFileSync) };
25
+ });
26
+ vi.mock('../../src/server/runtime/process.js', async (importOriginal) => ({
27
+ ...(await importOriginal()),
28
+ shellExec: vi.fn(() => {
29
+ throw new Error('Storage must not run shell commands.');
30
+ }),
31
+ }));
32
+
33
+ const api = UnderpostFileStorage.API;
34
+ const options = { deployId: 'test' };
35
+ const inventory = {
36
+ 'assets/z.png': { type: 'private', bytes: 3 },
37
+ 'other/b.png': {},
38
+ 'assets/deep/c.png': {},
39
+ 'assets/a.png': {},
40
+ 'assets/gone.png': {},
41
+ };
42
+ let fixture;
43
+ let previousCwd;
44
+ let download;
45
+
46
+ const writeInventory = (storage = inventory, extra = {}) =>
47
+ fs.outputJsonSync(api.resolveManifest({ ...options, ...extra }), storage);
48
+ const readInventory = (extra = {}) => api.readManifest({ ...options, ...extra }).storage;
49
+ const select = (scope, extra = {}) => api.resolveSelection(scope, readInventory(), extra);
50
+ const run = (scope, extra = {}) => api.callback(scope, { ...options, ...extra });
51
+ const git = (...args) => execFileSync('git', args, { encoding: 'utf8' });
52
+ const uploadPaths = () => cloudinary.uploader.upload.mock.calls.map(([key]) => key);
53
+
54
+ beforeEach(() => {
55
+ previousCwd = process.cwd();
56
+ fixture = fs.mkdtempSync('/tmp/engine-fs-storage-');
57
+ process.chdir(fixture);
58
+ for (const file of ['assets/z.png', 'assets/deep/c.png', 'assets/a.png', 'assets/untracked.png', 'other/b.png'])
59
+ fs.outputFileSync(file, 'local');
60
+ writeInventory();
61
+ vi.stubEnv('CLOUDINARY_CLOUD_NAME', 'test-cloud');
62
+ vi.stubEnv('CLOUDINARY_API_KEY', 'test-key');
63
+ vi.stubEnv('CLOUDINARY_API_SECRET', 'test-secret');
64
+ cloudinary.uploader.upload.mockImplementation(async (key) => ({ public_id: key, type: 'private', bytes: 6 }));
65
+ cloudinary.api.delete_resources.mockImplementation(async ([key]) => ({ deleted: { [key]: 'deleted' } }));
66
+ cloudinary.utils.download_archive_url.mockImplementation(({ type }) => `https://example.test/${type}`);
67
+ download = vi.spyOn(Downloader, 'downloadFile').mockImplementation(async (_url, target) => {
68
+ const zip = new JSZip();
69
+ zip.file('remote-asset', 'remote');
70
+ fs.writeFileSync(target, await zip.generateAsync({ type: 'nodebuffer' }));
71
+ });
72
+ for (const name of ['getChangedFiles', 'getDeleteFiles', 'initLocalRepo', 'isInsideWorkTree'])
73
+ vi.spyOn(UnderpostRepository.API, name).mockImplementation(() => {
74
+ throw new Error(`Unexpected Git call: ${name}`);
75
+ });
76
+ execFileSync.mockClear();
77
+ });
78
+
79
+ afterEach(() => {
80
+ vi.restoreAllMocks();
81
+ vi.resetAllMocks();
82
+ vi.unstubAllEnvs();
83
+ process.chdir(previousCwd);
84
+ fs.removeSync(fixture);
85
+ });
86
+
87
+ describe('storage manifest resolution', () => {
88
+ it('uses the default manifest unless a sub-id is explicit', async () => {
89
+ expect(api.resolveManifest(options)).toBe('engine-private/conf/test/storage.json');
90
+ expect(api.resolveManifest({ ...options, storageId: 'assets' })).toBe(
91
+ 'engine-private/conf/test/storage.assets.json',
92
+ );
93
+ writeInventory({ 'custom/file.png': {} }, { storageId: 'assets' });
94
+ const pull = vi.spyOn(api, 'pull').mockResolvedValue('private');
95
+ await run(undefined, { tracked: true, pull: true });
96
+ expect(pull.mock.calls.map(([key]) => key)).toEqual(Object.keys(inventory));
97
+ pull.mockClear();
98
+ await run(undefined, { tracked: true, pull: true, storageId: 'assets' });
99
+ expect(pull.mock.calls.map(([key]) => key)).toEqual(['custom/file.png']);
100
+ });
101
+
102
+ it.each(['', '../assets', '/tmp/a', 'storage.assets.json', 'assets.json', 'a/b', 'a\\b'])(
103
+ 'rejects invalid sub-id %j',
104
+ (storageId) => {
105
+ expect(() => api.resolveManifest({ ...options, storageId })).toThrow('--storage-id');
106
+ },
107
+ );
108
+
109
+ it('requires a deploy id and rejects manifest path overrides', () => {
110
+ expect(() => api.resolveManifest({})).toThrow('--deploy-id');
111
+ expect(() => api.resolveManifest({ deployId: '../test' })).toThrow('--deploy-id');
112
+ expect(() => api.resolveManifest({ ...options, storageFilePath: '/tmp/storage.json' })).toThrow('--storage-id');
113
+ });
114
+
115
+ it('does not create a missing manifest during selection', async () => {
116
+ fs.removeSync('engine-private');
117
+ expect(api.readManifest(options).storage).toEqual({});
118
+ await run(undefined, { tracked: true });
119
+ expect(fs.existsSync('engine-private')).toBe(false);
120
+ expect(cloudinary.config).not.toHaveBeenCalled();
121
+ });
122
+
123
+ it.each([[[]], [null], [{ a: null }], [{ a: { bytes: -1 } }], [{ a: { type: 'unknown' } }], [{ a: {}, './a': {} }]])(
124
+ 'rejects an invalid manifest before remote access: %j',
125
+ async (storage) => {
126
+ writeInventory(storage);
127
+ await expect(run('assets')).rejects.toThrow(/Manifest|manifest/);
128
+ expect(cloudinary.config).not.toHaveBeenCalled();
129
+ },
130
+ );
131
+ });
132
+
133
+ describe('filesystem storage selection', () => {
134
+ it('selects one file without reading its parent', async () => {
135
+ const walk = vi.spyOn(fs, 'readdirSync').mockImplementation(() => {
136
+ throw new Error('Unexpected traversal');
137
+ });
138
+ await run('./assets/a.png');
139
+ expect(uploadPaths()).toEqual(['assets/a.png']);
140
+ expect(walk).not.toHaveBeenCalled();
141
+ expect(execFileSync).not.toHaveBeenCalled();
142
+ });
143
+
144
+ it('selects all files below a directory without Git or --recursive', async () => {
145
+ fs.outputFileSync('assets/.hidden', 'hidden');
146
+ await run('assets');
147
+ expect(uploadPaths().sort()).toEqual([
148
+ 'assets/.hidden',
149
+ 'assets/a.png',
150
+ 'assets/deep/c.png',
151
+ 'assets/untracked.png',
152
+ 'assets/z.png',
153
+ ]);
154
+ expect(execFileSync).not.toHaveBeenCalled();
155
+ expect(cloudinary.api.delete_resources).not.toHaveBeenCalled();
156
+ });
157
+
158
+ it('keeps selection unchanged with --force or --recursive', () => {
159
+ expect(select('assets', { force: true })).toEqual(select('assets'));
160
+ expect(select('assets', { recursive: true })).toEqual(select('assets'));
161
+ });
162
+
163
+ it('normalizes absolute paths and reuses existing manifest keys', () => {
164
+ writeInventory({ './assets/a.png': {} });
165
+ expect(select(path.resolve('assets/a.png'))).toEqual(['./assets/a.png']);
166
+ expect(api.normalizeStoragePath('./assets/deep/../a.png')).toBe('assets/a.png');
167
+ });
168
+
169
+ it('does not follow directory link cycles', () => {
170
+ fs.symlinkSync(path.resolve('assets'), 'assets/loop', 'dir');
171
+ expect(select('assets')).toHaveLength(4);
172
+ });
173
+
174
+ it('requires --tracked to select missing files', async () => {
175
+ await expect(run('assets/gone.png', { rm: true })).rejects.toThrow('ENOENT');
176
+ expect(cloudinary.api.delete_resources).not.toHaveBeenCalled();
177
+ });
178
+ });
179
+
180
+ describe('Git storage selection', () => {
181
+ beforeEach(() => {
182
+ git('init', '-q');
183
+ fs.writeFileSync('.gitignore', 'assets/ignored.png\n');
184
+ fs.outputFileSync('assets/ignored.png', 'ignored');
185
+ fs.outputFileSync('assets/space and\nnewline.png', 'tracked');
186
+ git(
187
+ 'add',
188
+ '--',
189
+ '.gitignore',
190
+ 'assets/a.png',
191
+ 'assets/z.png',
192
+ 'assets/deep/c.png',
193
+ 'assets/space and\nnewline.png',
194
+ );
195
+ git('-c', 'user.name=Storage Test', '-c', 'user.email=storage@example.test', 'commit', '-qm', 'fixture');
196
+ fs.writeFileSync('assets/a.png', 'changed');
197
+ fs.removeSync('assets/z.png');
198
+ execFileSync.mockClear();
199
+ });
200
+
201
+ it('selects unchanged and changed tracked files, excluding ignored and untracked files', async () => {
202
+ await run('assets', { git: true });
203
+ expect(uploadPaths().sort()).toEqual(['assets/a.png', 'assets/deep/c.png', 'assets/space and\nnewline.png']);
204
+ expect(cloudinary.api.delete_resources).not.toHaveBeenCalled();
205
+ expect(readInventory()).toHaveProperty('assets/z.png');
206
+ });
207
+
208
+ it('restricts single files and subdirectories', () => {
209
+ expect(select('assets/untracked.png', { git: true })).toEqual([]);
210
+ expect(select('assets/ignored.png', { git: true })).toEqual([]);
211
+ expect(select('assets/a.png', { git: true })).toEqual(['assets/a.png']);
212
+ expect(select(path.resolve('assets/deep'), { git: true })).toEqual(['assets/deep/c.png']);
213
+ });
214
+
215
+ it.each([{}, { pull: true, force: true }, { rm: true }])(
216
+ 'never changes Git state during CRUD: %j',
217
+ async (operation) => {
218
+ const head = git('rev-parse', 'HEAD');
219
+ const index = fs.readFileSync('.git/index');
220
+ execFileSync.mockClear();
221
+ await run('assets/a.png', { git: true, ...operation });
222
+ expect(execFileSync.mock.calls.map(([command, args]) => [command, ...args])).toEqual([
223
+ ['git', 'rev-parse', '--show-toplevel'],
224
+ ['git', 'ls-files', '--cached', '--full-name', '-z', '--', '.'],
225
+ ]);
226
+ expect(fs.readFileSync('.git/index')).toEqual(index);
227
+ expect(git('rev-parse', 'HEAD')).toBe(head);
228
+ },
229
+ );
230
+
231
+ it('intersects Git paths with manifest ranges and regex filters', () => {
232
+ expect(select('assets', { git: true, fromKey: 'other/b.png', toKey: 'assets/a.png', keyRegex: '\\.png$' })).toEqual(
233
+ ['assets/a.png', 'assets/deep/c.png'],
234
+ );
235
+ });
236
+ });
237
+
238
+ describe('tracked storage selection and filters', () => {
239
+ it('selects only ordered manifest entries without filesystem or Git access', async () => {
240
+ vi.spyOn(fs, 'statSync').mockImplementation(() => {
241
+ throw new Error('Unexpected stat');
242
+ });
243
+ vi.spyOn(fs, 'readdirSync').mockImplementation(() => {
244
+ throw new Error('Unexpected traversal');
245
+ });
246
+ const upload = vi.spyOn(api, 'upload').mockResolvedValue({});
247
+ await run(undefined, { tracked: true });
248
+ expect(upload.mock.calls.map(([key]) => key)).toEqual(Object.keys(inventory));
249
+ expect(execFileSync).not.toHaveBeenCalled();
250
+ });
251
+
252
+ it('scopes missing files using only manifest paths', () => {
253
+ expect(select('assets/gone.png', { tracked: true })).toEqual(['assets/gone.png']);
254
+ expect(select('./assets/', { tracked: true })).toEqual(
255
+ Object.keys(inventory).filter((key) => key.startsWith('assets/')),
256
+ );
257
+ expect(select('asset', { tracked: true })).toEqual([]);
258
+ expect(select('.', { tracked: true })).toEqual(Object.keys(inventory));
259
+ });
260
+
261
+ it.each([
262
+ [{ fromKey: 'assets/deep/c.png' }, ['assets/deep/c.png', 'assets/a.png', 'assets/gone.png']],
263
+ [{ fromKey: 'other/b.png', toKey: 'assets/a.png' }, ['other/b.png', 'assets/deep/c.png', 'assets/a.png']],
264
+ [{ fromKey: 'assets/a.png', toKey: 'assets/a.png' }, ['assets/a.png']],
265
+ [{ toKey: 'other/b.png' }, ['assets/z.png', 'other/b.png']],
266
+ [{ key: 'assets/a.png' }, ['assets/a.png']],
267
+ [{ keyRegex: '^assets/' }, ['assets/z.png', 'assets/deep/c.png', 'assets/a.png', 'assets/gone.png']],
268
+ [{ keyRegex: 'absent' }, []],
269
+ ])('filters in manifest order: %j', (filters, expected) => {
270
+ expect(select(undefined, { tracked: true, ...filters })).toEqual(expected);
271
+ });
272
+
273
+ it('composes scope, range, regex, and exact predicates', () => {
274
+ const filters = { tracked: true, fromKey: 'other/b.png', toKey: 'assets/a.png', keyRegex: '^assets/' };
275
+ expect(select('assets', filters)).toEqual(['assets/deep/c.png', 'assets/a.png']);
276
+ expect(select('assets', { ...filters, key: 'assets/a.png' })).toEqual(['assets/a.png']);
277
+ expect(select('assets/deep/c.png', filters)).toEqual(['assets/deep/c.png']);
278
+ });
279
+
280
+ it('applies manifest predicates to filesystem paths without adding missing files', () => {
281
+ expect(select('assets', { fromKey: 'assets/deep/c.png', keyRegex: '^assets/' })).toEqual([
282
+ 'assets/a.png',
283
+ 'assets/deep/c.png',
284
+ ]);
285
+ expect(select('assets/a.png', { key: 'assets/a.png' })).toEqual(['assets/a.png']);
286
+ });
287
+
288
+ it.each([
289
+ { fromKey: 'missing' },
290
+ { toKey: 'missing' },
291
+ { key: 'missing' },
292
+ { keyRegex: '[' },
293
+ { fromKey: 'assets/a.png', toKey: 'assets/z.png' },
294
+ { key: 'assets/z.png', fromKey: 'assets/a.png' },
295
+ { key: 'assets/a.png', keyRegex: '^other/' },
296
+ { key: 'other/b.png' },
297
+ ])('rejects invalid filters before CRUD: %j', async (filters) => {
298
+ await expect(run('assets', { tracked: true, ...filters })).rejects.toThrow();
299
+ expect(cloudinary.config).not.toHaveBeenCalled();
300
+ });
301
+
302
+ it.each([
303
+ [undefined, {}],
304
+ ['assets', { tracked: true, git: true }],
305
+ ['assets', { rm: true, pull: true }],
306
+ ['assets', { omitUnzip: true }],
307
+ ])('rejects conflicting or incomplete options: %j', async (scope, flags) => {
308
+ await expect(run(scope, flags)).rejects.toThrow();
309
+ expect(cloudinary.config).not.toHaveBeenCalled();
310
+ });
311
+ });
312
+
313
+ describe('storage synchronization', () => {
314
+ it('uploads private raw assets with token access and records remote metadata', async () => {
315
+ await run('assets/a.png');
316
+ expect(cloudinary.uploader.upload).toHaveBeenCalledWith('assets/a.png', {
317
+ public_id: 'assets/a.png',
318
+ resource_type: 'raw',
319
+ type: 'private',
320
+ access_control: [{ access_type: 'token' }],
321
+ overwrite: false,
322
+ });
323
+ expect(readInventory()['assets/a.png']).toEqual({ type: 'private', bytes: 6 });
324
+ expect(fs.statSync('assets/a.png').size).toBe(5);
325
+ expect(cloudinary.config).toHaveBeenCalledWith({
326
+ cloud_name: 'test-cloud',
327
+ api_key: 'test-key',
328
+ api_secret: 'test-secret',
329
+ });
330
+ });
331
+
332
+ it('returns the skip sentinel for a file already present, without reaching the network', async () => {
333
+ const manifest = api.readManifest(options);
334
+ expect(await api.pull('assets/a.png', options, manifest)).to.equal('skipped');
335
+ expect(download).not.toHaveBeenCalled();
336
+ expect(cloudinary.config).not.toHaveBeenCalled();
337
+ });
338
+
339
+ it.each([
340
+ { mode: 'plain', colorEnv: { NO_COLOR: '1' } },
341
+ { mode: 'forced color', colorEnv: { FORCE_COLOR: '1' } },
342
+ { mode: 'CI color', colorEnv: { CI: 'true', GITHUB_ACTIONS: 'true' } },
343
+ ])('reports one skipped-pull count with $mode output', ({ colorEnv }) => {
344
+ fs.outputFileSync('assets/gone.png', 'local');
345
+ const cli = path.resolve(previousCwd, 'bin/index.js');
346
+ const childEnv = { ...process.env };
347
+ delete childEnv.NO_COLOR;
348
+ delete childEnv.FORCE_COLOR;
349
+ Object.assign(childEnv, colorEnv);
350
+ const output = clearTerminalStringColor(
351
+ execFileSync(process.execPath, [cli, 'fs', 'assets', '--deploy-id', 'test', '--pull', '--tracked'], {
352
+ cwd: fixture,
353
+ env: childEnv,
354
+ encoding: 'utf8',
355
+ stdio: ['ignore', 'pipe', 'pipe'],
356
+ }),
357
+ );
358
+
359
+ expect(output).to.not.include('Pull skipped; local file exists');
360
+ const summaries = [...output.matchAll(/Pull skipped files that already exist: (\{[^}]*\})/g)];
361
+ expect(summaries).to.have.lengthOf(1);
362
+ expect(JSON.parse(summaries[0][1])).toEqual({ skipped: 4, selected: 4 });
363
+ });
364
+
365
+ it('uses --force only to set upload overwrite', async () => {
366
+ await run('assets/a.png', { force: true });
367
+ expect(cloudinary.uploader.upload.mock.calls[0][1].overwrite).toBe(true);
368
+ });
369
+
370
+ it('reads remote metadata when overwrite is refused', async () => {
371
+ cloudinary.uploader.upload.mockResolvedValue({ existing: true });
372
+ cloudinary.api.resource.mockResolvedValue({ public_id: 'assets/a.png', type: 'private', bytes: 100 });
373
+ await run('assets/a.png');
374
+ expect(readInventory()['assets/a.png'].bytes).toBe(100);
375
+ expect(cloudinary.api.resource).toHaveBeenCalledWith('assets/a.png', { resource_type: 'raw', type: 'private' });
376
+ });
377
+
378
+ it('requires environment credentials before remote access', async () => {
379
+ vi.stubEnv('CLOUDINARY_API_SECRET', '');
380
+ await expect(run('assets/a.png')).rejects.toThrow('CLOUDINARY_API_SECRET');
381
+ expect(cloudinary.uploader.upload).not.toHaveBeenCalled();
382
+ });
383
+
384
+ it.each([
385
+ { error: 'failed' },
386
+ { public_id: 'wrong', type: 'private', bytes: 4 },
387
+ { public_id: 'assets/a.png', type: 'private' },
388
+ ])('preserves the manifest for an invalid upload response: %j', async (result) => {
389
+ cloudinary.uploader.upload.mockResolvedValue(result);
390
+ await expect(run('assets/a.png')).rejects.toThrow('Invalid Cloudinary');
391
+ expect(readInventory()).toEqual(inventory);
392
+ });
393
+
394
+ it('does not create a manifest after a failed first upload', async () => {
395
+ fs.removeSync('engine-private');
396
+ cloudinary.uploader.upload.mockRejectedValue(new Error('upload failed'));
397
+ await expect(run('assets/a.png')).rejects.toThrow('upload failed');
398
+ expect(fs.existsSync('engine-private')).toBe(false);
399
+ });
400
+
401
+ it('persists each successful upload before a later failure', async () => {
402
+ cloudinary.uploader.upload
403
+ .mockReset()
404
+ .mockResolvedValueOnce({ public_id: 'assets/z.png', type: 'private', bytes: 99 })
405
+ .mockRejectedValueOnce(new Error('upload failed'));
406
+ await expect(run(undefined, { tracked: true })).rejects.toThrow('upload failed');
407
+ expect(readInventory()).toEqual({ ...inventory, 'assets/z.png': { type: 'private', bytes: 99 } });
408
+ });
409
+
410
+ it('reports missing tracked upload files without removing their entries', async () => {
411
+ await expect(run('assets/gone.png', { tracked: true })).rejects.toThrow('ENOENT');
412
+ expect(readInventory()).toEqual(inventory);
413
+ });
414
+
415
+ it('preserves the manifest when an atomic write fails', async () => {
416
+ const before = fs.readFileSync(api.resolveManifest(options), 'utf8');
417
+ vi.spyOn(fs, 'renameSync').mockImplementation(() => {
418
+ throw new Error('rename failed');
419
+ });
420
+ await expect(run('assets/a.png')).rejects.toThrow('rename failed');
421
+ expect(fs.readFileSync(api.resolveManifest(options), 'utf8')).toBe(before);
422
+ expect(fs.readdirSync('engine-private/conf/test')).toEqual(['storage.json']);
423
+ });
424
+
425
+ it('pulls missing assets and corrects metadata after delivery fallback', async () => {
426
+ download.mockRejectedValueOnce(new Error('not found'));
427
+ await run('assets/gone.png', { tracked: true, pull: true });
428
+ expect(fs.readFileSync('assets/gone.png', 'utf8')).toBe('remote');
429
+ expect(download.mock.calls.map(([url]) => url)).toEqual([
430
+ 'https://example.test/upload',
431
+ 'https://example.test/private',
432
+ ]);
433
+ expect(cloudinary.utils.download_archive_url.mock.calls[0][0]).toEqual({
434
+ public_ids: ['assets/gone.png'],
435
+ resource_type: 'raw',
436
+ type: 'upload',
437
+ });
438
+ expect(readInventory()['assets/gone.png']).toEqual({ type: 'private', bytes: 6 });
439
+ expect(execFileSync).not.toHaveBeenCalled();
440
+ });
441
+
442
+ it('preserves existing local content unless --force is set', async () => {
443
+ await run('assets/a.png', { tracked: true, pull: true });
444
+ expect(download).not.toHaveBeenCalled();
445
+ expect(fs.readFileSync('assets/a.png', 'utf8')).toBe('local');
446
+ await run('assets/a.png', { tracked: true, pull: true, force: true });
447
+ expect(fs.readFileSync('assets/a.png', 'utf8')).toBe('remote');
448
+ });
449
+
450
+ it('rejects a directory at a tracked file target', async () => {
451
+ fs.ensureDirSync('assets/gone.png');
452
+ await expect(run('assets/gone.png', { tracked: true, pull: true })).rejects.toThrow('regular file');
453
+ expect(download).not.toHaveBeenCalled();
454
+ });
455
+
456
+ it('keeps delivery corrections across a multi-file pull', async () => {
457
+ writeInventory({ 'assets/first.png': {}, 'assets/second.png': {} });
458
+ download.mockRejectedValueOnce(new Error('not found'));
459
+ await run(undefined, { tracked: true, pull: true });
460
+ expect(readInventory()).toEqual({
461
+ 'assets/first.png': { type: 'private', bytes: 6 },
462
+ 'assets/second.png': { type: 'upload', bytes: 6 },
463
+ });
464
+ });
465
+
466
+ it('keeps existing local content and inventory after failed downloads', async () => {
467
+ download.mockRejectedValue(new Error('download failed'));
468
+ await expect(run('assets/a.png', { pull: true, force: true })).rejects.toThrow('download failed');
469
+ expect(fs.readFileSync('assets/a.png', 'utf8')).toBe('local');
470
+ expect(readInventory()).toEqual(inventory);
471
+ expect(fs.readdirSync('assets').some((name) => name.startsWith('.underpost-fs-'))).toBe(false);
472
+ });
473
+
474
+ it('does not record a pull or replace a file when the archive is invalid', async () => {
475
+ download.mockImplementation(async (_url, target) => fs.writeFileSync(target, 'invalid zip'));
476
+ await expect(run('assets/a.png', { pull: true, force: true })).rejects.toThrow();
477
+ expect(fs.readFileSync('assets/a.png', 'utf8')).toBe('local');
478
+ expect(readInventory()).toEqual(inventory);
479
+ });
480
+
481
+ it('keeps the wrapper archive with --omit-unzip and preserves zip asset names', async () => {
482
+ writeInventory({ 'assets/bundle.zip': {} });
483
+ await run(undefined, { tracked: true, pull: true, omitUnzip: true });
484
+ expect(fs.existsSync('assets/bundle.zip.zip')).toBe(true);
485
+ expect(fs.existsSync('assets/bundle.zip')).toBe(false);
486
+ download.mockClear();
487
+ await run(undefined, { tracked: true, pull: true, omitUnzip: true });
488
+ expect(download).not.toHaveBeenCalled();
489
+ await run(undefined, { tracked: true, pull: true });
490
+ expect(fs.readFileSync('assets/bundle.zip', 'utf8')).toBe('remote');
491
+ });
492
+
493
+ it('requires a manifest entry for pull', async () => {
494
+ await expect(run('assets/untracked.png', { pull: true })).rejects.toThrow('not in the selected manifest');
495
+ expect(download).not.toHaveBeenCalled();
496
+ });
497
+
498
+ it('deletes missing local assets and removes entries after remote confirmation', async () => {
499
+ await run('assets/gone.png', { tracked: true, rm: true });
500
+ expect(readInventory()).not.toHaveProperty('assets/gone.png');
501
+ expect(cloudinary.api.delete_resources.mock.calls).toEqual([
502
+ [['assets/gone.png'], { type: 'upload', resource_type: 'raw' }],
503
+ [['assets/gone.png'], { type: 'private', resource_type: 'raw' }],
504
+ ]);
505
+ expect(execFileSync).not.toHaveBeenCalled();
506
+ });
507
+
508
+ it('preserves local files during remote deletion, including with --force', async () => {
509
+ await run('assets/a.png', { rm: true, force: true });
510
+ expect(fs.readFileSync('assets/a.png', 'utf8')).toBe('local');
511
+ expect(readInventory()).not.toHaveProperty('assets/a.png');
512
+ });
513
+
514
+ it('accepts confirmed remote absence', async () => {
515
+ cloudinary.api.delete_resources.mockImplementation(async ([key]) => ({ deleted: { [key]: 'not_found' } }));
516
+ await run('assets/gone.png', { tracked: true, rm: true });
517
+ expect(readInventory()).not.toHaveProperty('assets/gone.png');
518
+ });
519
+
520
+ it.each([{}, { deleted: { 'assets/a.png': 'error' } }])(
521
+ 'preserves inventory for an unconfirmed delete: %j',
522
+ async (result) => {
523
+ cloudinary.api.delete_resources.mockResolvedValue(result);
524
+ await expect(run('assets/a.png', { rm: true })).rejects.toThrow('Cloudinary delete failed');
525
+ expect(readInventory()).toEqual(inventory);
526
+ },
527
+ );
528
+
529
+ it('preserves inventory after a partial remote delete and supports retry', async () => {
530
+ cloudinary.api.delete_resources
531
+ .mockResolvedValueOnce({ deleted: { 'assets/a.png': 'deleted' } })
532
+ .mockRejectedValueOnce(new Error('delete failed'));
533
+ await expect(run('assets/a.png', { rm: true })).rejects.toThrow('delete failed');
534
+ expect(readInventory()).toEqual(inventory);
535
+ await run('assets/a.png', { rm: true });
536
+ expect(readInventory()).not.toHaveProperty('assets/a.png');
537
+ });
538
+
539
+ it('updates only the selected custom manifest', async () => {
540
+ writeInventory({}, { storageId: 'assets' });
541
+ await run('assets/a.png', { storageId: 'assets' });
542
+ expect(readInventory()).toEqual(inventory);
543
+ expect(readInventory({ storageId: 'assets' })).toEqual({ 'assets/a.png': { type: 'private', bytes: 6 } });
544
+ });
545
+ });
546
+
547
+ describe('storage CLI options', () => {
548
+ const command = () => {
549
+ const registered = program.commands.find((entry) => entry.name() === 'fs');
550
+ const cli = new Command('fs')
551
+ .argument('[path]')
552
+ .exitOverride()
553
+ .configureOutput({ writeErr: () => {} });
554
+ for (const option of registered.options) cli.addOption(option);
555
+ return cli.action(api.callback);
556
+ };
557
+
558
+ it('parses the manifest selector and filters through the registered options', async () => {
559
+ writeInventory({ 'assets/gone.png': {} }, { storageId: 'assets' });
560
+ await command().parseAsync(
561
+ ['--tracked', '--deploy-id', 'test', '--storage-id', 'assets', '--key', 'assets/gone.png', '--pull'],
562
+ { from: 'user' },
563
+ );
564
+ expect(fs.readFileSync('assets/gone.png', 'utf8')).toBe('remote');
565
+ });
566
+
567
+ it('deletes an obsolete bundle part through its exact manifest key without local files', async () => {
568
+ const current = 'build/underpost.net-.zip.part022';
569
+ const obsolete = 'build/underpost.net-.zip.part023';
570
+ const sibling = 'build/underpost.net-peer.zip.part023';
571
+ const storage = { [current]: { type: 'private', bytes: 6 }, [obsolete]: {}, [sibling]: {} };
572
+ writeInventory(storage, { storageId: 'bundle' });
573
+ expect(fs.existsSync('build')).toBe(false);
574
+
575
+ await command().parseAsync(
576
+ ['--tracked', '--key', obsolete, '--deploy-id', 'test', '--storage-id', 'bundle', '--rm'],
577
+ { from: 'user' },
578
+ );
579
+
580
+ expect(cloudinary.api.delete_resources.mock.calls.map(([keys]) => keys)).toEqual([[obsolete], [obsolete]]);
581
+ expect(readInventory({ storageId: 'bundle' })).toEqual({ [current]: storage[current], [sibling]: {} });
582
+ expect(readInventory()).toEqual(inventory);
583
+ expect(fs.existsSync('build')).toBe(false);
584
+ expect(execFileSync).not.toHaveBeenCalled();
585
+ });
586
+
587
+ it('rejects the removed path override and a missing deploy id', async () => {
588
+ await expect(
589
+ command().parseAsync(['assets', '--deploy-id', 'test', '--storage-file-path', '/tmp/other.json'], {
590
+ from: 'user',
591
+ }),
592
+ ).rejects.toThrow('unknown option');
593
+ await expect(command().parseAsync(['assets'], { from: 'user' })).rejects.toThrow('required option');
594
+ expect(cloudinary.config).not.toHaveBeenCalled();
595
+ });
596
+ });