@underpostnet/cyberia 3.2.90 → 3.3.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (490) hide show
  1. package/.dockerignore +1 -0
  2. package/.env.example +16 -0
  3. package/.github/workflows/coverall.cyberia.ci.yml +107 -0
  4. package/.github/workflows/cyberia-client.cd.yml +6 -18
  5. package/.github/workflows/cyberia-server.cd.yml +6 -18
  6. package/.github/workflows/docker-image.cyberia-client.ci.yml +8 -5
  7. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +8 -3
  8. package/.github/workflows/docker-image.cyberia-server.ci.yml +8 -5
  9. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +8 -5
  10. package/.github/workflows/docker-image.engine-cyberia.ci.yml +15 -1
  11. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +15 -1
  12. package/.github/workflows/engine-cyberia.cd.yml +6 -78
  13. package/.github/workflows/engine-cyberia.ci.yml +4 -5
  14. package/.github/workflows/ghpkg.ci.yml +42 -21
  15. package/.github/workflows/hardhat.ci.yml +12 -11
  16. package/.github/workflows/npmpkg.ci.yml +17 -15
  17. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
  18. package/.github/workflows/pwa-microservices-template-test.ci.yml +2 -2
  19. package/.github/workflows/release.cd.yml +2 -9
  20. package/.prettierignore +1 -0
  21. package/AGENTS.md +47 -0
  22. package/CHANGELOG.md +581 -2417
  23. package/CLI-HELP.md +416 -127
  24. package/Dockerfile +25 -9
  25. package/Dockerfile.dev +25 -9
  26. package/Dockerfile.test +4 -4
  27. package/README.md +21 -1
  28. package/bin/build.js +70 -43
  29. package/bin/build.template.js +8 -3
  30. package/bin/cyberia.js +372 -135
  31. package/bin/deploy.js +18 -104
  32. package/bin/index.js +372 -135
  33. package/compose.env +16 -0
  34. package/conf.js +93 -4
  35. package/deploy/cyberia-client/deploy.sh +47 -0
  36. package/deploy/cyberia-client/package.sh +18 -0
  37. package/deploy/cyberia-client/state.sh +14 -0
  38. package/deploy/cyberia-server/deploy.sh +47 -0
  39. package/deploy/cyberia-server/package.sh +18 -0
  40. package/deploy/cyberia-server/state.sh +14 -0
  41. package/deploy/dd-cyberia/deploy.sh +22 -0
  42. package/deploy/dd-cyberia/init.sh +55 -0
  43. package/deploy/dd-cyberia/package.sh +17 -0
  44. package/deploy/dd-cyberia/state.sh +14 -0
  45. package/deploy/dd-cyberia/sync-deploy.sh +116 -0
  46. package/deploy/lib/github-actions-logging.sh +594 -0
  47. package/deploy/lib/host.sh +185 -0
  48. package/deploy/lib/state.sh +62 -0
  49. package/deploy/pwa-microservices-template/deploy.sh +45 -0
  50. package/deploy/release/deploy.sh +32 -0
  51. package/deployment.yaml +7 -2
  52. package/docker-compose.yml +31 -37
  53. package/examples/static-page/README.md +19 -19
  54. package/examples/static-page/ssr-components/CustomPage.js +2 -2
  55. package/hardhat/hardhat.config.js +1 -6
  56. package/hardhat/package-lock.json +104 -43
  57. package/hardhat/package.json +3 -3
  58. package/manifests/alertmanager/deployment.yaml +81 -0
  59. package/manifests/blackbox-exporter/deployment.yaml +75 -0
  60. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +36 -7
  61. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +77 -0
  62. package/manifests/deployment/dd-cyberia-development/deployment.yaml +7 -2
  63. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  64. package/manifests/grafana/deployment.yaml +26 -7
  65. package/manifests/grafana/kustomization.yaml +1 -0
  66. package/manifests/grafana/service.yaml +6 -1
  67. package/manifests/mariadb/pv.yaml +1 -0
  68. package/manifests/mongodb/statefulset.yaml +9 -2
  69. package/manifests/mysql/pv-pvc.yaml +14 -1
  70. package/manifests/postgresql/pv.yaml +12 -1
  71. package/manifests/postgresql/pvc.yaml +1 -0
  72. package/manifests/prometheus/deployment.yaml +18 -22
  73. package/package.json +83 -10
  74. package/scripts/audit-selinux.sh +64 -0
  75. package/scripts/event-monitor.sh +56 -0
  76. package/scripts/gpu-diag.sh +0 -0
  77. package/scripts/ip-info.sh +0 -0
  78. package/scripts/k3s-node-setup.sh +30 -14
  79. package/scripts/kubeadm-node-setup.sh +40 -31
  80. package/scripts/lxd-vm-setup.sh +0 -0
  81. package/scripts/maas-nat-firewalld.sh +0 -0
  82. package/scripts/maas-setup.sh +8 -8
  83. package/scripts/nat-iptables.sh +2 -0
  84. package/scripts/rhel-grpc-setup.sh +0 -0
  85. package/scripts/rocky-kickstart.sh +25 -9
  86. package/scripts/selinux-normalize.sh +907 -0
  87. package/scripts/shutdown-machine.sh +843 -0
  88. package/scripts/test-monitor.sh +3 -3
  89. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +1 -1
  90. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +2 -2
  91. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +2 -2
  92. package/src/api/core/core.controller.js +1 -1
  93. package/src/api/core/core.router.js +2 -2
  94. package/src/api/core/core.service.js +2 -2
  95. package/src/api/crypto/crypto.controller.js +1 -1
  96. package/src/api/crypto/crypto.service.js +1 -1
  97. package/src/api/cyberia-action/cyberia-action.controller.js +1 -1
  98. package/src/api/cyberia-action/cyberia-action.router.js +1 -1
  99. package/src/api/cyberia-action/cyberia-action.service.js +2 -2
  100. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -1
  101. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +1 -1
  102. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +2 -2
  103. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +1 -1
  104. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  105. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +2 -2
  106. package/src/api/cyberia-entity/cyberia-entity.controller.js +1 -1
  107. package/src/api/cyberia-entity/cyberia-entity.router.js +1 -1
  108. package/src/api/cyberia-entity/cyberia-entity.service.js +2 -2
  109. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +1 -1
  110. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +1 -1
  111. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +2 -2
  112. package/src/api/cyberia-instance/cyberia-fallback-capture.js +466 -0
  113. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +63 -0
  114. package/src/api/cyberia-instance/cyberia-fallback-world.js +16 -1
  115. package/src/api/cyberia-instance/cyberia-instance-map.service.js +1 -1
  116. package/src/api/cyberia-instance/cyberia-instance.controller.js +4 -1
  117. package/src/api/cyberia-instance/cyberia-instance.router.js +15 -2
  118. package/src/api/cyberia-instance/cyberia-instance.service.js +45 -2
  119. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +1 -1
  120. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +1 -0
  121. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +1 -1
  122. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +2 -2
  123. package/src/api/cyberia-map/cyberia-map.controller.js +1 -1
  124. package/src/api/cyberia-map/cyberia-map.router.js +1 -1
  125. package/src/api/cyberia-map/cyberia-map.service.js +2 -2
  126. package/src/api/cyberia-quest/cyberia-quest.controller.js +1 -1
  127. package/src/api/cyberia-quest/cyberia-quest.router.js +1 -1
  128. package/src/api/cyberia-quest/cyberia-quest.service.js +2 -2
  129. package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +1 -1
  130. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +1 -1
  131. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +2 -2
  132. package/src/api/cyberia-saga/cyberia-saga.controller.js +1 -1
  133. package/src/api/cyberia-saga/cyberia-saga.router.js +1 -1
  134. package/src/api/cyberia-saga/cyberia-saga.service.js +2 -2
  135. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +16 -29
  136. package/src/api/cyberia-skill/cyberia-skill.controller.js +1 -1
  137. package/src/api/cyberia-skill/cyberia-skill.router.js +1 -1
  138. package/src/api/cyberia-skill/cyberia-skill.service.js +2 -2
  139. package/src/api/default/default.controller.js +1 -1
  140. package/src/api/default/default.router.js +1 -1
  141. package/src/api/default/default.service.js +2 -2
  142. package/src/api/document/document.controller.js +1 -1
  143. package/src/api/document/document.router.js +1 -1
  144. package/src/api/document/document.service.js +2 -2
  145. package/src/api/file/file.controller.js +1 -1
  146. package/src/api/file/file.router.js +1 -1
  147. package/src/api/file/file.service.js +2 -2
  148. package/src/api/instance/instance.controller.js +1 -1
  149. package/src/api/instance/instance.router.js +1 -1
  150. package/src/api/instance/instance.service.js +2 -2
  151. package/src/api/ipfs/ipfs.controller.js +1 -1
  152. package/src/api/ipfs/ipfs.router.js +2 -2
  153. package/src/api/ipfs/ipfs.service.js +2 -2
  154. package/src/api/object-layer/object-layer.controller.js +1 -1
  155. package/src/api/object-layer/object-layer.model.js +204 -3
  156. package/src/api/object-layer/object-layer.router.js +2 -2
  157. package/src/api/object-layer/object-layer.service.js +4 -17
  158. package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +1 -1
  159. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +1 -1
  160. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +2 -2
  161. package/src/api/test/test.controller.js +1 -1
  162. package/src/api/test/test.service.js +1 -1
  163. package/src/api/user/guest.service.js +3 -3
  164. package/src/api/user/user.controller.js +1 -1
  165. package/src/api/user/user.router.js +2 -2
  166. package/src/api/user/user.service.js +4 -4
  167. package/src/api.js +5 -9
  168. package/src/cli/app.js +391 -0
  169. package/src/cli/baremetal.js +30 -36
  170. package/src/cli/client.js +306 -0
  171. package/src/cli/cloud-init.js +3 -3
  172. package/src/cli/cluster.js +242 -135
  173. package/src/cli/db.js +126 -67
  174. package/src/cli/deploy.js +158 -60
  175. package/src/cli/docker-compose.js +5 -5
  176. package/src/cli/domains.js +184 -0
  177. package/src/cli/dotenv-store.js +143 -0
  178. package/src/cli/event.js +2281 -0
  179. package/src/cli/fs.js +3 -3
  180. package/src/cli/host.js +672 -0
  181. package/src/cli/image.js +4 -7
  182. package/src/cli/index.js +426 -134
  183. package/src/cli/ipfs.js +34 -30
  184. package/src/cli/kickstart.js +1 -1
  185. package/src/cli/kubectl.js +218 -41
  186. package/src/cli/lxd.js +3 -3
  187. package/src/cli/monitor.js +1359 -26
  188. package/src/cli/package.js +90 -0
  189. package/src/cli/release.js +73 -34
  190. package/src/cli/repository.js +564 -367
  191. package/src/cli/run.js +362 -654
  192. package/src/cli/secrets.js +2158 -964
  193. package/src/cli/ssh.js +651 -331
  194. package/src/cli/state.js +394 -0
  195. package/src/cli/static.js +2 -2
  196. package/src/cli/system.js +26 -13
  197. package/src/cli/test.js +199 -125
  198. package/src/cli/vultr.js +623 -0
  199. package/src/cli/wireguard.js +2930 -0
  200. package/src/client/components/core/CalendarCore.js +53 -96
  201. package/src/client/components/core/Docs.js +258 -211
  202. package/src/client/components/core/FileExplorer.js +0 -21
  203. package/src/client/components/core/PanelForm.js +4 -4
  204. package/src/client/components/core/Repository.js +56 -0
  205. package/src/client/components/core/Translate.js +7 -0
  206. package/src/client/components/core/{FullScreen.js → ViewModeController.js} +42 -39
  207. package/src/client/components/core/Wallet.js +0 -11
  208. package/src/client/components/core/Worker.js +7 -1
  209. package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +2 -2
  210. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +368 -0
  211. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +35 -1
  212. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +4 -0
  213. package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +2 -2
  214. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
  215. package/src/client/components/default/AppShellDefault.js +1 -1
  216. package/src/client/components/default/SettingsDefault.js +2 -2
  217. package/src/client/components/itemledger/SettingsItemledger.js +2 -2
  218. package/src/client/components/underpost/AppShellUnderpost.js +3 -2
  219. package/src/client/components/underpost/SettingsUnderpost.js +2 -2
  220. package/src/client/public/cyberia-docs/ARCHITECTURE.md +17 -3
  221. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +32 -5
  222. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +1 -1
  223. package/src/client/public/cyberia-docs/CYBERIA.md +21 -1
  224. package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +23 -6
  225. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  226. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  227. package/src/client/public/default/android-chrome-144x144.png +0 -0
  228. package/src/client/public/default/android-chrome-192x192.png +0 -0
  229. package/src/client/public/default/android-chrome-256x256.png +0 -0
  230. package/src/client/public/default/android-chrome-36x36.png +0 -0
  231. package/src/client/public/default/android-chrome-384x384.png +0 -0
  232. package/src/client/public/default/android-chrome-48x48.png +0 -0
  233. package/src/client/public/default/android-chrome-512x512.png +0 -0
  234. package/src/client/public/default/android-chrome-72x72.png +0 -0
  235. package/src/client/public/default/android-chrome-96x96.png +0 -0
  236. package/src/client/public/default/apple-touch-icon-1024x1024.png +0 -0
  237. package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
  238. package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
  239. package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
  240. package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
  241. package/src/client/public/default/apple-touch-icon-167x167.png +0 -0
  242. package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
  243. package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
  244. package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
  245. package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
  246. package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
  247. package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
  248. package/src/client/public/default/apple-touch-icon.png +0 -0
  249. package/src/client/public/default/apple-touch-startup-image-1125x2436.png +0 -0
  250. package/src/client/public/default/apple-touch-startup-image-1136x640.png +0 -0
  251. package/src/client/public/default/apple-touch-startup-image-1170x2532.png +0 -0
  252. package/src/client/public/default/apple-touch-startup-image-1179x2556.png +0 -0
  253. package/src/client/public/default/apple-touch-startup-image-1242x2208.png +0 -0
  254. package/src/client/public/default/apple-touch-startup-image-1242x2688.png +0 -0
  255. package/src/client/public/default/apple-touch-startup-image-1284x2778.png +0 -0
  256. package/src/client/public/default/apple-touch-startup-image-1290x2796.png +0 -0
  257. package/src/client/public/default/apple-touch-startup-image-1334x750.png +0 -0
  258. package/src/client/public/default/apple-touch-startup-image-1488x2266.png +0 -0
  259. package/src/client/public/default/apple-touch-startup-image-1536x2048.png +0 -0
  260. package/src/client/public/default/apple-touch-startup-image-1620x2160.png +0 -0
  261. package/src/client/public/default/apple-touch-startup-image-1640x2160.png +0 -0
  262. package/src/client/public/default/apple-touch-startup-image-1668x2224.png +0 -0
  263. package/src/client/public/default/apple-touch-startup-image-1668x2388.png +0 -0
  264. package/src/client/public/default/apple-touch-startup-image-1792x828.png +0 -0
  265. package/src/client/public/default/apple-touch-startup-image-2048x1536.png +0 -0
  266. package/src/client/public/default/apple-touch-startup-image-2048x2732.png +0 -0
  267. package/src/client/public/default/apple-touch-startup-image-2160x1620.png +0 -0
  268. package/src/client/public/default/apple-touch-startup-image-2160x1640.png +0 -0
  269. package/src/client/public/default/apple-touch-startup-image-2208x1242.png +0 -0
  270. package/src/client/public/default/apple-touch-startup-image-2224x1668.png +0 -0
  271. package/src/client/public/default/apple-touch-startup-image-2266x1488.png +0 -0
  272. package/src/client/public/default/apple-touch-startup-image-2388x1668.png +0 -0
  273. package/src/client/public/default/apple-touch-startup-image-2436x1125.png +0 -0
  274. package/src/client/public/default/apple-touch-startup-image-2532x1170.png +0 -0
  275. package/src/client/public/default/apple-touch-startup-image-2556x1179.png +0 -0
  276. package/src/client/public/default/apple-touch-startup-image-2688x1242.png +0 -0
  277. package/src/client/public/default/apple-touch-startup-image-2732x2048.png +0 -0
  278. package/src/client/public/default/apple-touch-startup-image-2778x1284.png +0 -0
  279. package/src/client/public/default/apple-touch-startup-image-2796x1290.png +0 -0
  280. package/src/client/public/default/apple-touch-startup-image-640x1136.png +0 -0
  281. package/src/client/public/default/apple-touch-startup-image-750x1334.png +0 -0
  282. package/src/client/public/default/apple-touch-startup-image-828x1792.png +0 -0
  283. package/src/client/public/default/assets/background/white.jpg +0 -0
  284. package/src/client/public/default/browserconfig.xml +1 -1
  285. package/src/client/public/default/favicon-16x16.png +0 -0
  286. package/src/client/public/default/favicon-32x32.png +0 -0
  287. package/src/client/public/default/favicon-48x48.png +0 -0
  288. package/src/client/public/default/favicon.ico +0 -0
  289. package/src/client/public/default/manifest.webmanifest +5 -5
  290. package/src/client/public/default/mstile-144x144.png +0 -0
  291. package/src/client/public/default/mstile-150x150.png +0 -0
  292. package/src/client/public/default/mstile-310x150.png +0 -0
  293. package/src/client/public/default/mstile-310x310.png +0 -0
  294. package/src/client/public/default/mstile-70x70.png +0 -0
  295. package/src/client/public/default/yandex-browser-50x50.png +0 -0
  296. package/src/client/public/default/yandex-browser-manifest.json +1 -1
  297. package/src/client/services/cyberia-instance/cyberia-instance.service.js +40 -0
  298. package/src/client/services/object-layer/object-layer.management.js +4 -4
  299. package/src/client/ssr/RootDocument.js +1 -1
  300. package/src/client/ssr/body/404.js +1 -1
  301. package/src/client/ssr/body/500.js +1 -1
  302. package/src/client/ssr/body/CacheControl.js +1 -1
  303. package/src/client/ssr/body/CyberiaDefaultSplashScreen.js +1 -1
  304. package/src/client/ssr/body/DefaultSplashScreen.js +1 -1
  305. package/src/client/ssr/body/SwaggerDarkMode.js +1 -1
  306. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +1 -1
  307. package/src/client/ssr/head/CryptokoynScripts.js +1 -1
  308. package/src/client/ssr/head/Css.js +1 -1
  309. package/src/client/ssr/head/CyberiaPortalScripts.js +1 -1
  310. package/src/client/ssr/head/DefaultScripts.js +1 -1
  311. package/src/client/ssr/head/ItemledgerScripts.js +1 -1
  312. package/src/client/ssr/head/Microdata.js +1 -1
  313. package/src/client/ssr/head/Production.js +1 -1
  314. package/src/client/ssr/head/Pwa.js +1 -1
  315. package/src/client/ssr/head/PwaItemledger.js +1 -1
  316. package/src/client/ssr/head/Seo.js +1 -1
  317. package/src/client/ssr/head/UnderpostScripts.js +1 -1
  318. package/src/client/ssr/mailer/DefaultRecoverEmail.js +1 -1
  319. package/src/client/ssr/mailer/DefaultVerifyEmail.js +1 -1
  320. package/src/client/ssr/views/Cyberia404.js +7 -5
  321. package/src/client/ssr/views/CyberiaServerMetrics.js +1 -1
  322. package/src/client/ssr/views/Maintenance.js +1 -1
  323. package/src/client/ssr/views/NoNetworkConnection.js +1 -1
  324. package/src/client/ssr/views/Test.js +1 -1
  325. package/src/client/sw/core.sw.js +20 -14
  326. package/src/client-builder/client-build-docs.js +61 -46
  327. package/src/client-builder/client-build-live.js +2 -2
  328. package/src/client-builder/client-build.js +58 -44
  329. package/src/client-builder/client-dev-server.js +3 -3
  330. package/src/client-builder/client-icons.js +2 -2
  331. package/src/client-builder/ssr.js +3 -3
  332. package/src/client.build.js +3 -6
  333. package/src/client.dev.js +3 -6
  334. package/src/db/DataBaseProvider.js +186 -3
  335. package/src/db/mariadb/MariaDB.js +3 -2
  336. package/src/db/mongo/MongoBootstrap.js +273 -101
  337. package/src/db/mongo/MongooseDB.js +11 -8
  338. package/src/{server/valkey.js → db/valkey/Valkey.js} +5 -4
  339. package/src/grpc/cyberia/grpc-server.js +1 -1
  340. package/src/index.js +103 -23
  341. package/src/mailer/EmailRender.js +2 -2
  342. package/src/mailer/MailerInterceptor.js +87 -0
  343. package/src/mailer/MailerProvider.js +50 -6
  344. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +1 -1
  345. package/src/projects/cyberia/besu-genesis-generator.js +3 -3
  346. package/src/projects/cyberia/catalog-cyberia.js +54 -6
  347. package/src/projects/cyberia/gemini-client.js +1 -1
  348. package/src/projects/cyberia/generate-saga.js +15 -24
  349. package/src/projects/cyberia/hot-reload-trigger.js +1 -1
  350. package/src/projects/cyberia/instance-data.js +22 -3
  351. package/src/projects/cyberia/ipfs-client.js +4 -4
  352. package/src/projects/cyberia/map-preview-generator.js +1 -1
  353. package/src/projects/cyberia/object-layer.js +12 -22
  354. package/src/projects/cyberia/semantic-layer-generator.js +1 -1
  355. package/src/projects/underpost/catalog-underpost.js +13 -2
  356. package/src/proxy.js +4 -7
  357. package/src/runtime/cyberia-client/Dockerfile +9 -4
  358. package/src/runtime/cyberia-client/Dockerfile.dev +8 -2
  359. package/src/runtime/cyberia-server/Dockerfile +9 -4
  360. package/src/runtime/cyberia-server/Dockerfile.dev +8 -2
  361. package/src/runtime/engine-cyberia/Dockerfile +25 -9
  362. package/src/runtime/engine-cyberia/Dockerfile.dev +25 -9
  363. package/src/runtime/engine-cyberia/Dockerfile.test +4 -4
  364. package/src/runtime/engine-cyberia/compose.env +16 -0
  365. package/src/runtime/engine-cyberia/docker-compose.yml +31 -37
  366. package/src/runtime/express/Express.js +32 -27
  367. package/src/runtime/lampp/Lampp.js +22 -70
  368. package/src/runtime/nginx/Nginx.js +1 -1
  369. package/src/runtime/wp/Wp.js +16 -48
  370. package/src/server/{catalog.js → build/catalog.js} +15 -5
  371. package/src/server/build/coverage.js +117 -0
  372. package/src/server/build/execution.js +402 -0
  373. package/src/server/build/package.js +465 -0
  374. package/src/server/build/testing.js +528 -0
  375. package/src/server/{dns.js → network/dns.js} +241 -14
  376. package/src/server/network/forward-proxy.js +541 -0
  377. package/src/server/{middlewares.js → network/middlewares.js} +59 -4
  378. package/src/server/network/node-capability.js +98 -0
  379. package/src/server/{peer.js → network/peer.js} +3 -3
  380. package/src/server/{proxy.js → network/proxy.js} +7 -13
  381. package/src/server/network/router.js +356 -0
  382. package/src/server/{tls.js → network/tls.js} +2 -2
  383. package/src/server/network/underpost-compression.js +186 -0
  384. package/src/server/{underpost-gateway.js → network/underpost-gateway.js} +26 -16
  385. package/src/server/{underpost-ingress.js → network/underpost-ingress.js} +19 -3
  386. package/src/server/{cri.js → ops/cri.js} +2 -2
  387. package/src/server/ops/cron.js +949 -0
  388. package/src/server/ops/event-notification.js +284 -0
  389. package/src/server/{logger.js → ops/logger.js} +108 -23
  390. package/src/server/ops/monitoring.js +1724 -0
  391. package/src/server/ops/systemd.js +259 -0
  392. package/src/server/{conf.js → runtime/conf.js} +521 -520
  393. package/src/server/runtime/config-scope.js +215 -0
  394. package/src/server/runtime/environment.js +149 -0
  395. package/src/server/{process.js → runtime/process.js} +153 -13
  396. package/src/server/{runtime-status.js → runtime/runtime-status.js} +155 -16
  397. package/src/server/{runtime.js → runtime/runtime.js} +12 -8
  398. package/src/server/{start.js → runtime/start.js} +128 -39
  399. package/src/server/{auth.js → security/auth.js} +7 -8
  400. package/src/server/security/container-storage.js +382 -0
  401. package/src/server/{crypto.js → security/crypto.js} +1 -1
  402. package/src/server/security/selinux.js +185 -0
  403. package/src/server/{backup.js → storage/backup.js} +17 -8
  404. package/src/server/{data-query.js → storage/data-query.js} +1 -1
  405. package/src/server/{downloader.js → storage/downloader.js} +2 -2
  406. package/src/server/storage/repository.js +68 -0
  407. package/src/server.js +4 -7
  408. package/src/ws/IoInterface.js +1 -1
  409. package/src/ws/IoServer.js +1 -1
  410. package/src/ws/core/core.ws.connection.js +1 -1
  411. package/src/ws/core/core.ws.emit.js +1 -1
  412. package/src/ws/core/core.ws.server.js +1 -1
  413. package/src/ws/default/default.ws.connection.js +1 -1
  414. package/src/ws/default/default.ws.emit.js +1 -1
  415. package/src/ws/default/default.ws.server.js +1 -1
  416. package/test/e2e/event-e2e-public-ingress-down.js +45 -0
  417. package/test/e2e/event-e2e-wireguard-server-down.js +38 -0
  418. package/test/e2e/event-e2e-wireguard-spoke-down.js +40 -0
  419. package/test/{api.test.js → integration/app/api.test.js} +33 -14
  420. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +30 -0
  421. package/test/{cyberia-instance-conf-defaults.test.js → integration/app/cyberia/cyberia-instance-conf-defaults.test.js} +2 -1
  422. package/test/integration/app/cyberia/cyberia-load.test.js +467 -0
  423. package/test/integration/app/cyberia/fallback-world-capture.test.js +253 -0
  424. package/test/integration/app/cyberia/object-layer-item-id.test.js +327 -0
  425. package/test/{shape-generator.test.js → integration/app/cyberia/shape-generator.test.js} +1 -1
  426. package/test/integration/infra/1-security/config-scope.test.js +305 -0
  427. package/test/integration/infra/1-security/container-storage.test.js +356 -0
  428. package/test/integration/infra/1-security/in-pod-cli-surface.test.js +86 -0
  429. package/test/integration/infra/1-security/secret-onboarding.test.js +930 -0
  430. package/test/integration/infra/1-security/selinux.test.js +69 -0
  431. package/test/{sops-secret-store.test.js → integration/infra/1-security/sops-secret-store.test.js} +415 -61
  432. package/test/integration/infra/1-security/systemd-service.test.js +70 -0
  433. package/test/integration/infra/1-security/underpost-config-secret.test.js +367 -0
  434. package/test/integration/infra/2-network/dns-firewall.test.js +566 -0
  435. package/test/integration/infra/2-network/node-capability.test.js +182 -0
  436. package/test/integration/infra/2-network/wireguard-cli.test.js +1163 -0
  437. package/test/integration/infra/2-network/wireguard-edge.test.js +1731 -0
  438. package/test/{cluster-instances.test.js → integration/infra/3-cluster/cluster-instances.test.js} +11 -15
  439. package/test/{deploy-node-placement.test.js → integration/infra/3-cluster/deploy-node-placement.test.js} +3 -3
  440. package/test/integration/infra/3-cluster/docker-compose-stack.test.js +313 -0
  441. package/test/integration/infra/4-ingress/deploy-routes.test.js +379 -0
  442. package/test/integration/infra/4-ingress/gateway-static-assets.test.js +453 -0
  443. package/test/{instance-traffic-plan.test.js → integration/infra/4-ingress/instance-traffic-plan.test.js} +10 -6
  444. package/test/{underpost-gateway.test.js → integration/infra/4-ingress/underpost-gateway.test.js} +47 -6
  445. package/test/{underpost-ingress.test.js → integration/infra/4-ingress/underpost-ingress.test.js} +56 -4
  446. package/test/integration/infra/5-observability/cron-jobs.test.js +652 -0
  447. package/test/{deploy-monitor.test.js → integration/infra/5-observability/deploy-monitor.test.js} +29 -31
  448. package/test/integration/infra/5-observability/event-notification.test.js +197 -0
  449. package/test/integration/infra/5-observability/event-remediation.test.js +502 -0
  450. package/test/integration/infra/5-observability/event-targets.test.js +1174 -0
  451. package/test/integration/infra/5-observability/monitoring-stack.test.js +902 -0
  452. package/test/support/shell-harness.js +73 -0
  453. package/test/unit/build-template.test.js +97 -0
  454. package/test/unit/build-workflow-offline.test.js +132 -0
  455. package/test/unit/catalog.test.js +71 -0
  456. package/test/unit/client-build-docs.test.js +334 -0
  457. package/test/unit/conf-loading.test.js +303 -0
  458. package/test/unit/conf-resolution.test.js +979 -0
  459. package/test/unit/coverage-artifact.test.js +130 -0
  460. package/test/{crypto.test.js → unit/crypto.test.js} +1 -1
  461. package/test/unit/deploy-env-resolution.test.js +38 -0
  462. package/test/unit/deploy-log-table.test.js +447 -0
  463. package/test/unit/execution-profiles.test.js +219 -0
  464. package/test/unit/fleet-sync-source.test.js +149 -0
  465. package/test/unit/logger-redaction.test.js +112 -0
  466. package/test/unit/package.test.js +403 -0
  467. package/test/unit/prepare-host.test.js +299 -0
  468. package/test/unit/process-environment.test.js +18 -0
  469. package/test/unit/propagation-message.test.js +78 -0
  470. package/test/unit/redeploy-plan.test.js +24 -0
  471. package/test/unit/release-bump.test.js +474 -0
  472. package/test/unit/ssh-output-redaction.test.js +28 -0
  473. package/test/unit/start-options.test.js +46 -0
  474. package/test/unit/test-tiers.test.js +193 -0
  475. package/vitest.config.js +58 -0
  476. package/.nycrc +0 -9
  477. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +0 -48
  478. package/manifests/mongodb/pv-pvc.yaml +0 -59
  479. package/scripts/link-local-underpost-cli.sh +0 -6
  480. package/src/api/user/user.build.js +0 -16
  481. package/src/cli/env.js +0 -177
  482. package/src/client/components/core/Blockchain.js +0 -41
  483. package/src/client/components/core/Blog.js +0 -9
  484. package/src/client/components/core/KeyboardAvoidance.js +0 -145
  485. package/src/client/public/default/assets/background/dark.jpg +0 -0
  486. package/src/client/public/default/assets/background/white0-min.jpg +0 -0
  487. package/src/client/public/default/assets/background/white0.jpg +0 -0
  488. package/src/client/public/doc/favicon.ico +0 -0
  489. package/src/client/public/doc/sitemap +0 -148
  490. package/src/server/cron.js +0 -476
@@ -3,9 +3,12 @@
3
3
  import { expect } from 'chai';
4
4
  import fs from 'fs-extra';
5
5
  import os from 'os';
6
- import Underpost from '../src/index.js';
6
+ import crypto from 'node:crypto';
7
+ import Underpost from '../../../../src/index.js';
8
+ import { classifyCommand, EXECUTION_PROFILES } from '../../../../src/server/build/execution.js';
9
+ import { resolveDeployList } from '../../../../src/server/network/router.js';
7
10
 
8
- const sops = () => Underpost.secret.sops;
11
+ const sops = () => Underpost.secret;
9
12
  // Unique namespace so the fixture can never collide with (or clean up) a real
10
13
  // encrypted store under the gitignored engine-private tree.
11
14
  const TEST_NAMESPACE = 'sops-store-test-ns';
@@ -67,19 +70,27 @@ describe('sops encrypted secret store', () => {
67
70
  './engine-private/secrets/default/mariadb-secret.enc.yaml',
68
71
  );
69
72
  });
73
+
74
+ it('maps Grafana credentials to the dedicated Secret keys', () => {
75
+ expect(sops().managedSecrets()).to.include('grafana-admin');
76
+ expect(sops().seedEnvKeys('grafana-admin')).to.deep.equal({
77
+ 'admin-user': 'GF_SECURITY_ADMIN_USER',
78
+ 'admin-password': 'GF_SECURITY_ADMIN_PASSWORD',
79
+ });
80
+ });
70
81
  });
71
82
 
72
83
  describe('presence detection and origin seed fallback', () => {
73
84
  const storeRoot = './engine-private/secrets';
74
85
  let createdStoreRoot = false;
75
86
 
76
- before(() => {
87
+ beforeAll(() => {
77
88
  createdStoreRoot = !fs.existsSync(storeRoot);
78
89
  fs.ensureDirSync(TEST_DIR);
79
90
  fs.writeFileSync(`${TEST_DIR}/fixture-secret.enc.yaml`, encryptedFixture, 'utf8');
80
91
  });
81
92
 
82
- after(() => {
93
+ afterAll(() => {
83
94
  fs.removeSync(TEST_DIR);
84
95
  // Only reclaim the store root when this suite created it, so a real
85
96
  // encrypted store is never touched.
@@ -121,12 +132,12 @@ describe('sops encrypted secret store', () => {
121
132
  });
122
133
 
123
134
  it('refuses to apply a namespace with no stored manifests', () => {
124
- expect(() => sops().apply('namespace-without-a-store')).to.throw(/No encrypted secrets for namespace/);
135
+ expect(() => sops().applyStore('namespace-without-a-store')).to.throw(/No encrypted secrets for namespace/);
125
136
  });
126
137
  });
127
138
 
128
139
  describe('cluster initialization wiring', () => {
129
- const clusterSource = fs.readFileSync(new URL('../src/cli/cluster.js', import.meta.url), 'utf8');
140
+ const clusterSource = fs.readFileSync(new URL('../../../../src/cli/cluster.js', import.meta.url), 'utf8');
130
141
 
131
142
  for (const [flag, secretName] of [
132
143
  ['mariadb', 'mariadb-secret'],
@@ -137,20 +148,22 @@ describe('sops encrypted secret store', () => {
137
148
  const start = clusterSource.indexOf(`if (options.${flag}) {`);
138
149
  expect(start, `options.${flag} branch not found`).to.be.greaterThan(-1);
139
150
  const branch = clusterSource.slice(start, start + 1200);
140
- expect(branch).to.include(`Underpost.secret.sops.applyIfPresent('${secretName}', options.namespace)`);
141
- expect(branch).to.include(`kubectl create secret generic ${secretName}`);
142
- // The origin seed creation must stay guarded by the negated store lookup,
143
- // never run unconditionally alongside the decrypted apply.
151
+ expect(branch).to.include(`Underpost.secret.applyIfPresent('${secretName}', options.namespace)`);
152
+ // The origin seed projection must stay guarded by the negated store lookup, never run
153
+ // unconditionally alongside the decrypted apply. Its paths come from the workload
154
+ // domain, so the branch names no credential path of its own.
144
155
  expect(branch).to.match(
145
156
  new RegExp(
146
- `if \\(!Underpost\\.secret\\.sops\\.applyIfPresent\\('${secretName}'[\\s\\S]{0,80}kubectl create secret generic ${secretName}`,
157
+ `if \\(!Underpost\\.secret\\.applyIfPresent\\('${secretName}'[\\s\\S]{0,120}` +
158
+ `Underpost\\.secret\\.applyFromOriginSeed\\('${secretName}'`,
147
159
  ),
148
160
  );
161
+ expect(branch).to.not.include('engine-private/');
149
162
  });
150
163
  }
151
164
 
152
165
  it('delegates host tooling install to UnderpostSecret instead of duplicating it', () => {
153
- expect(clusterSource).to.include('Underpost.secret.sops.installTooling()');
166
+ expect(clusterSource).to.include('Underpost.secret.installTooling()');
154
167
  // The binary install logic must live in exactly one place.
155
168
  expect(clusterSource).to.not.include('releases/download/${SOPS_VERSION}');
156
169
  expect(clusterSource).to.not.include('const SOPS_VERSION');
@@ -158,14 +171,14 @@ describe('sops encrypted secret store', () => {
158
171
  });
159
172
 
160
173
  it('pins the sops and age versions in the secrets module', () => {
161
- const secretsSource = fs.readFileSync(new URL('../src/cli/secrets.js', import.meta.url), 'utf8');
174
+ const secretsSource = fs.readFileSync(new URL('../../../../src/cli/secrets.js', import.meta.url), 'utf8');
162
175
  expect(secretsSource).to.match(/const SOPS_VERSION = 'v\d+\.\d+\.\d+';/);
163
176
  expect(secretsSource).to.match(/const AGE_VERSION = 'v\d+\.\d+\.\d+';/);
164
177
  });
165
178
  });
166
179
 
167
180
  describe('decrypt-to-apply pipeline', () => {
168
- const secretsSource = fs.readFileSync(new URL('../src/cli/secrets.js', import.meta.url), 'utf8');
181
+ const secretsSource = fs.readFileSync(new URL('../../../../src/cli/secrets.js', import.meta.url), 'utf8');
169
182
 
170
183
  it('streams decrypted output into kubectl without writing plaintext to disk', () => {
171
184
  expect(secretsSource).to.include('sops --decrypt "${manifestPath}" ');
@@ -182,9 +195,9 @@ describe('sops encrypted secret store', () => {
182
195
  });
183
196
 
184
197
  it('re-keys under pipefail with logging suppressed', () => {
185
- const start = secretsSource.indexOf(' rotate(recipient, options = {}) {');
198
+ const start = secretsSource.indexOf(' rotateRecipient(recipient, options = {}) {');
186
199
  expect(start, 'rotate() not found').to.be.greaterThan(-1);
187
- const body = secretsSource.slice(start, secretsSource.indexOf('\n /**', start));
200
+ const body = secretsSource.slice(start, secretsSource.indexOf('\n /**', start));
188
201
  expect(body).to.include('set -o pipefail');
189
202
  expect(body).to.include('sops --config');
190
203
  expect(body).to.include('updatekeys --yes');
@@ -192,6 +205,320 @@ describe('sops encrypted secret store', () => {
192
205
  });
193
206
  });
194
207
 
208
+ describe('GIT_AUTH_TOKEN rotation', () => {
209
+ const secretsSource = fs.readFileSync(new URL('../../../../src/cli/secrets.js', import.meta.url), 'utf8');
210
+ const saved = {};
211
+
212
+ // `instanceRepos` reads `./engine-private/conf/<deployId>/conf.instances.json`, and
213
+ // engine-private is a private repository absent from a CI checkout. A real deploy's
214
+ // instances are therefore fixture data here: the deploy gets its own directory, that
215
+ // directory is removed whole afterwards, and an existing one is never touched.
216
+ const FIXTURE_DEPLOY = 'dd-fixture-sops';
217
+ const FIXTURE_CONF_DIR = `./engine-private/conf/${FIXTURE_DEPLOY}`;
218
+ const FIXTURE_INSTANCE_REPOS = ['underpostnet/fixture-sops-server', 'underpostnet/fixture-sops-client'];
219
+
220
+ beforeAll(() => {
221
+ if (fs.existsSync(FIXTURE_CONF_DIR))
222
+ throw new Error(`Refusing to write fixtures into an existing deploy: ${FIXTURE_CONF_DIR}`);
223
+ fs.outputJsonSync(`${FIXTURE_CONF_DIR}/conf.instances.json`, [
224
+ { id: 'fixture-server', metadata: { repository: FIXTURE_INSTANCE_REPOS[0] } },
225
+ { id: 'fixture-client', metadata: { repository: FIXTURE_INSTANCE_REPOS[1] } },
226
+ { id: 'fixture-page' },
227
+ ]);
228
+ });
229
+
230
+ afterAll(() => {
231
+ fs.removeSync(FIXTURE_CONF_DIR);
232
+ });
233
+
234
+ beforeEach(() => {
235
+ for (const key of ['ENGINE_SRC_REPO', 'ENGINE_SRC_PRIVATE_REPO', 'GITHUB_USERNAME', 'GIT_AUTH_TOKEN'])
236
+ saved[key] = process.env[key];
237
+ for (const key of Object.keys(saved)) delete process.env[key];
238
+ });
239
+
240
+ afterEach(() => {
241
+ for (const [key, value] of Object.entries(saved))
242
+ if (value === undefined) delete process.env[key];
243
+ else process.env[key] = value;
244
+ });
245
+
246
+ it('targets the private conf repo, both engine sources and the package mirror of a deploy', () => {
247
+ // Same naming `deploy_id_from_repo` resolves in deploy/lib/host.sh, reached through the
248
+ // repository domain rather than re-derived here. dd-core declares no instances, so this is
249
+ // the naming-derived set on its own.
250
+ expect(sops().gitAuthTokenTargets({ deployId: 'dd-core', owner: 'acme' })).to.deep.equal([
251
+ 'acme/engine-core-private',
252
+ 'acme/engine-core',
253
+ 'acme/engine-test-core',
254
+ 'acme/engine-ghpkg-core',
255
+ ]);
256
+ });
257
+
258
+ it('keeps an instance repository under the owner it declares, not the derived one', () => {
259
+ // metadata.repository is an explicit slug, so it outranks the --args owner used for naming.
260
+ expect(sops().gitAuthTokenTargets({ deployId: FIXTURE_DEPLOY, owner: 'acme' })).to.include(
261
+ FIXTURE_INSTANCE_REPOS[0],
262
+ );
263
+ });
264
+
265
+ it('includes the ghpkg package mirror the engine repository publishes into', () => {
266
+ // .github/workflows/ghpkg.ci.yml builds engine-ghpkg-<conf_id>, whose own workflows read
267
+ // the same token; the monorepo has no ghpkg of its own.
268
+ expect(sops().gitAuthTokenTargets({ deployId: 'dd-cyberia', owner: 'acme' })).to.include(
269
+ 'acme/engine-ghpkg-cyberia',
270
+ );
271
+ expect(sops().gitAuthTokenTargets({ owner: 'acme' })).to.not.include('acme/engine-ghpkg');
272
+ });
273
+
274
+ it("includes every repository the deploy's instances are built from", () => {
275
+ // dd-cyberia builds from cyberia-server and cyberia-client; rotating only the engine repos
276
+ // would leave half the deploy on the previous token.
277
+ const declared = Underpost.repo.instanceRepos(FIXTURE_DEPLOY);
278
+ expect(declared, `${FIXTURE_DEPLOY} declares no instance repositories`).to.deep.equal(FIXTURE_INSTANCE_REPOS);
279
+ expect(sops().gitAuthTokenTargets({ deployId: FIXTURE_DEPLOY })).to.include.members(declared);
280
+ });
281
+
282
+ it('contributes nothing for a deploy that declares no instances', () => {
283
+ expect(Underpost.repo.instanceRepos('dd-core')).to.deep.equal([]);
284
+ expect(Underpost.repo.instanceRepos('')).to.deep.equal([]);
285
+ });
286
+
287
+ it('resolves the `template` meta id to the template lineage, not through conf-id naming', () => {
288
+ expect(sops().gitAuthTokenTargets({ deployId: 'template', owner: 'acme' })).to.deep.equal([
289
+ 'acme/pwa-microservices-template',
290
+ 'acme/pwa-microservices-template-ghpkg',
291
+ 'acme/engine',
292
+ ]);
293
+ });
294
+
295
+ it('resolves the same targets from every reference that names the deploy', () => {
296
+ const expected = [
297
+ 'acme/engine-lampp-private',
298
+ 'acme/engine-lampp',
299
+ 'acme/engine-test-lampp',
300
+ 'acme/engine-ghpkg-lampp',
301
+ ];
302
+ for (const reference of ['dd-lampp', 'engine-lampp', 'engine-test-lampp', 'engine-lampp-private'])
303
+ expect(sops().gitAuthTokenTargets({ deployId: reference, owner: 'acme' }), reference).to.deep.equal(expected);
304
+ });
305
+
306
+ it('falls back to the monorepo pair, deduplicated, when no deploy is named', () => {
307
+ expect(sops().gitAuthTokenTargets({ owner: 'acme' })).to.deep.equal(['acme/engine-private', 'acme/engine']);
308
+ });
309
+
310
+ it('takes the deploy and owner from the deploy environment when none is passed', () => {
311
+ process.env.ENGINE_SRC_REPO = 'acme/engine-test-fixture-sops';
312
+ process.env.ENGINE_SRC_PRIVATE_REPO = 'acme/engine-private';
313
+ expect(sops().gitAuthTokenTargets({})).to.deep.equal([
314
+ 'acme/engine-fixture-sops-private',
315
+ 'acme/engine-fixture-sops',
316
+ 'acme/engine-test-fixture-sops',
317
+ 'acme/engine-ghpkg-fixture-sops',
318
+ ...FIXTURE_INSTANCE_REPOS,
319
+ 'acme/engine-private',
320
+ ]);
321
+ });
322
+
323
+ it('fans `dd` out across the route table, one triple per deploy, deduplicated', () => {
324
+ // `dd` is the meta id every runner reads as "all of dd.routes"; the rotation must cover the
325
+ // same fleet the cluster deploys, resolved through the one reader rather than a second parse.
326
+ const routed = resolveDeployList('dd');
327
+ const targets = sops().gitAuthTokenTargets({ deployId: 'dd', owner: 'acme' });
328
+ expect(routed.length, 'no deploy routes to fan out over').to.be.greaterThan(0);
329
+ expect(targets).to.have.lengthOf(new Set(targets).size);
330
+ for (const deployId of routed) {
331
+ const confId = Underpost.repo.confIdFactory(deployId);
332
+ expect(targets, deployId).to.include.members([
333
+ `acme/engine-${confId}-private`,
334
+ `acme/engine-${confId}`,
335
+ `acme/engine-test-${confId}`,
336
+ ]);
337
+ }
338
+ });
339
+
340
+ it('accepts an explicit multi-deploy list, in route-table order', () => {
341
+ expect(sops().gitAuthTokenTargets({ deployId: `${FIXTURE_DEPLOY}|dd-lampp`, owner: 'acme' })).to.deep.equal([
342
+ 'acme/engine-fixture-sops-private',
343
+ 'acme/engine-fixture-sops',
344
+ 'acme/engine-test-fixture-sops',
345
+ 'acme/engine-ghpkg-fixture-sops',
346
+ ...FIXTURE_INSTANCE_REPOS,
347
+ 'acme/engine-lampp-private',
348
+ 'acme/engine-lampp',
349
+ 'acme/engine-test-lampp',
350
+ 'acme/engine-ghpkg-lampp',
351
+ ]);
352
+ });
353
+
354
+ it('unions repositories two deploys share instead of rotating them twice', () => {
355
+ process.env.ENGINE_SRC_PRIVATE_REPO = 'acme/engine-private';
356
+ const targets = sops().gitAuthTokenTargets({ deployId: `${FIXTURE_DEPLOY}|dd-lampp`, owner: 'acme' });
357
+ expect(targets.filter((slug) => slug === 'acme/engine-private')).to.have.lengthOf(1);
358
+ });
359
+
360
+ it('separates extra targets on characters --args does not split on', () => {
361
+ const targets = sops().gitAuthTokenTargets({ owner: 'acme', repos: 'other/one|other/two;other/three' });
362
+ expect(targets).to.include.members(['other/one', 'other/two', 'other/three']);
363
+ });
364
+
365
+ it('drops an unresolvable extra target instead of failing the whole rotation', () => {
366
+ const targets = sops().gitAuthTokenTargets({ owner: 'acme', repos: 'not-a-slug|other/one' });
367
+ expect(targets).to.include('other/one');
368
+ expect(targets).to.not.include('not-a-slug');
369
+ });
370
+
371
+ it('reports the plan without contacting GitHub or writing anything on a dry run', () => {
372
+ // Same reasoning as the 'GIT_AUTH_TOKEN token sources' dry-run test: the reported fields
373
+ // don't depend on gh reachability, so probing is stubbed to keep this test from reaching the
374
+ // real GitHub API through whatever `gh` session happens to be authenticated on this machine.
375
+ const hasBinary = vi.spyOn(sops(), 'hasBinary').mockReturnValue(false);
376
+ try {
377
+ const report = sops().rotateGitAuthToken({ deployId: FIXTURE_DEPLOY, owner: 'acme', dryRun: true });
378
+ expect(report.targets).to.have.lengthOf(4 + FIXTURE_INSTANCE_REPOS.length);
379
+ expect(report.rotated).to.deep.equal([]);
380
+ expect(report.manifest).to.equal('');
381
+ expect(report.tokenSource).to.equal('');
382
+ } finally {
383
+ hasBinary.mockRestore();
384
+ }
385
+ });
386
+
387
+ it('takes a piped token, ahead of an inherited environment holding the outgoing one', () => {
388
+ const start = secretsSource.indexOf(' stageGitAuthToken(stagePath, options = {}) {');
389
+ const body = secretsSource.slice(start, secretsSource.indexOf('\n /**', start));
390
+ expect(body).to.include('stdinIsRedirected()');
391
+ expect(body).to.include("fs.readFileSync(0, 'utf8')");
392
+ expect(body.indexOf("source = 'piped stdin'")).to.be.lessThan(body.indexOf('} else if (inherited) {'));
393
+ });
394
+
395
+ it('reads stdin only when it is piped or redirected, never a terminal or /dev/null', () => {
396
+ // fstat rather than isTTY: `< /dev/null` is not a TTY either, and reading it would strand
397
+ // the rotation on an empty token instead of falling through to the next source.
398
+ expect(secretsSource).to.include('stat.isFIFO() || stat.isFile()');
399
+ });
400
+
401
+ it('names piped stdin as the planned source without consuming it', () => {
402
+ expect(sops().plannedTokenSource({ token: 'ghp_explicit' })).to.equal('--args token');
403
+ expect(sops().plannedTokenSource({})).to.be.a('string').and.not.equal('');
404
+ });
405
+
406
+ it('treats `gh auth status` as advisory, gating on target reachability instead', () => {
407
+ // A logged-in account whose token merely lacks an optional scope still exits non-zero here,
408
+ // so making it the gate blocks a rotation that would have worked.
409
+ const start = secretsSource.indexOf(' rotateGitAuthToken(options = {}) {');
410
+ const body = secretsSource.slice(start, secretsSource.indexOf('\n /**', start));
411
+ expect(body).to.include('if (!ghAuthenticated)');
412
+ expect(body).to.not.match(/if \(!ghAuthenticated\)\s*\n?\s*throw/);
413
+ expect(body).to.include('probed.reachable.length === 0');
414
+ });
415
+
416
+ it('surfaces what gh actually reported instead of a bare "not authenticated"', () => {
417
+ expect(secretsSource).to.include('gh auth status 2>&1');
418
+ expect(secretsSource).to.include('${ghAuthOutput}');
419
+ });
420
+
421
+ it('names a shadowing GH_TOKEN/GITHUB_TOKEN as the likely cause of a dead credential', () => {
422
+ // gh prefers these over the stored login, and `host load` exports GITHUB_TOKEN on every
423
+ // engine node — so a stale one turns a working `gh auth login` into 15 unreachable targets.
424
+ expect(secretsSource).to.include("['GH_TOKEN', 'GITHUB_TOKEN'].filter(");
425
+ expect(secretsSource).to.include('is set here, and gh uses it in preference to the account');
426
+ });
427
+
428
+ it('probes before staging the token, so an unreachable set never prompts for one', () => {
429
+ const start = secretsSource.indexOf(' rotateGitAuthToken(options = {}) {');
430
+ const body = secretsSource.slice(start, secretsSource.indexOf('\n /**', start));
431
+ expect(body.indexOf('probeGitAuthTokenTargets(targets)')).to.be.lessThan(
432
+ body.indexOf('stageGitAuthToken(stagePath, options)'),
433
+ );
434
+ });
435
+
436
+ it('hands the token to gh on stdin, never as a command argument', () => {
437
+ // A token in the command string is a token in the process table and in the command log.
438
+ expect(secretsSource).to.include('gh secret set ${GIT_AUTH_TOKEN_KEY} --repo "${repo}" < "${stagePath}"');
439
+ expect(secretsSource).to.not.include('gh secret set ${GIT_AUTH_TOKEN_KEY} --body');
440
+ });
441
+
442
+ it('runs the gh write under pipefail with the command kept out of the log', () => {
443
+ const start = secretsSource.indexOf(' rotateGitAuthToken(options = {}) {');
444
+ expect(start, 'rotateGitAuthToken() not found').to.be.greaterThan(-1);
445
+ const body = secretsSource.slice(start, secretsSource.indexOf('\n /**', start));
446
+ expect(body).to.include("bash -c 'set -o pipefail; gh secret set");
447
+ expect(body).to.include('disableLog: true');
448
+ expect(body).to.include('shred -u "${stagePath}"');
449
+ });
450
+
451
+ it('stages the token on tmpfs at mode 600 rather than through a shell heredoc', () => {
452
+ expect(secretsSource).to.include("const GIT_AUTH_TOKEN_STAGE_DIR = '/dev/shm/underpost-git-auth';");
453
+ const start = secretsSource.indexOf(' stageGitAuthToken(stagePath, options = {}) {');
454
+ const body = secretsSource.slice(start, secretsSource.indexOf('\n /**', start));
455
+ expect(body).to.include('writeStageFileSync(stagePath');
456
+ // GITHUB_TOKEN is what gh authenticates with — on a rotation that is the outgoing token.
457
+ expect(body).to.not.include('process.env.GITHUB_TOKEN');
458
+ });
459
+
460
+ it('replaces the stored manifest through the validating atomic encrypt path', () => {
461
+ const start = secretsSource.indexOf(" writeGitAuthTokenManifest(token, namespace = 'default') {");
462
+ expect(start, 'writeGitAuthTokenManifest() not found').to.be.greaterThan(-1);
463
+ const body = secretsSource.slice(start, secretsSource.indexOf('\n /**', start));
464
+ expect(body).to.include('Underpost.secret.encrypt(plaintextPath, namespace, { force: true })');
465
+ });
466
+
467
+ it('classifies gh as a network effect so a rotation cannot run under a hermetic profile', () => {
468
+ expect(classifyCommand('gh secret set GIT_AUTH_TOKEN --repo acme/engine')).to.equal('net');
469
+ expect(EXECUTION_PROFILES.HERMETIC_BUILD.permits).to.not.include('net');
470
+ });
471
+
472
+ it('rejects a credential the verb does not rotate, and a rotation with no target', () => {
473
+ expect(() => sops().rotate({ args: { secret: 'AWS_SECRET_ACCESS_KEY' } })).to.throw(
474
+ /GIT_AUTH_TOKEN is the only one it rotates/,
475
+ );
476
+ expect(() => sops().rotate({ args: {} })).to.throw(/rotate requires a target/);
477
+ });
478
+ });
479
+
480
+ describe('GIT_AUTH_TOKEN token sources', () => {
481
+ const saved = {};
482
+
483
+ beforeEach(() => {
484
+ saved.env = process.env.GIT_AUTH_TOKEN;
485
+ delete process.env.GIT_AUTH_TOKEN;
486
+ });
487
+
488
+ afterEach(() => {
489
+ if (saved.env === undefined) delete process.env.GIT_AUTH_TOKEN;
490
+ else process.env.GIT_AUTH_TOKEN = saved.env;
491
+ });
492
+
493
+ it('prefers an explicit token over every implicit source', () => {
494
+ process.env.GIT_AUTH_TOKEN = 'ghp_fromEnvironment';
495
+ expect(sops().plannedTokenSource({ token: 'ghp_explicit' })).to.equal('--args token');
496
+ });
497
+
498
+ it('falls back through the environment to a prompt', () => {
499
+ process.env.GIT_AUTH_TOKEN = 'ghp_fromEnvironment';
500
+ expect(sops().plannedTokenSource({})).to.equal('GIT_AUTH_TOKEN environment');
501
+ delete process.env.GIT_AUTH_TOKEN;
502
+ expect(sops().plannedTokenSource({})).to.match(/interactive prompt|unavailable/);
503
+ });
504
+
505
+ it('does not prompt or write on a dry run', () => {
506
+ // Report fields under dryRun are independent of gh reachability (see rotateGitAuthToken's
507
+ // early return), so probing is stubbed out here rather than left to whatever `gh` happens to
508
+ // be installed and authenticated as on the machine running the suite — otherwise this reaches
509
+ // the real GitHub API for a probe of the fictitious 'acme/dd-cyberia' target.
510
+ const hasBinary = vi.spyOn(sops(), 'hasBinary').mockReturnValue(false);
511
+ try {
512
+ const report = sops().rotateGitAuthToken({ deployId: 'dd-cyberia', owner: 'acme', dryRun: true });
513
+ expect(report.tokenSource).to.equal('');
514
+ expect(report.rotated).to.deep.equal([]);
515
+ expect(report.manifest).to.equal('');
516
+ } finally {
517
+ hasBinary.mockRestore();
518
+ }
519
+ });
520
+ });
521
+
195
522
  describe('creation-rule recipient parsing', () => {
196
523
  const storeRoot = './engine-private/secrets';
197
524
  const confPath = `${storeRoot}/.sops.yaml`;
@@ -203,13 +530,13 @@ describe('sops encrypted secret store', () => {
203
530
  '\n',
204
531
  );
205
532
 
206
- before(() => {
533
+ beforeAll(() => {
207
534
  createdStoreRoot = !fs.existsSync(storeRoot);
208
535
  fs.ensureDirSync(storeRoot);
209
536
  if (fs.existsSync(confPath)) savedConf = fs.readFileSync(confPath, 'utf8');
210
537
  });
211
538
 
212
- after(() => {
539
+ afterAll(() => {
213
540
  if (savedConf !== null) fs.writeFileSync(confPath, savedConf, 'utf8');
214
541
  else fs.removeSync(confPath);
215
542
  if (createdStoreRoot && fs.existsSync(storeRoot) && fs.readdirSync(storeRoot).length === 0)
@@ -238,17 +565,17 @@ describe('sops encrypted secret store', () => {
238
565
 
239
566
  it('rejects a recipient that is not an age public key', () => {
240
567
  fs.writeFileSync(confPath, rule('age1aaa'), 'utf8');
241
- expect(() => sops().rotate('/etc/passwd')).to.throw(/not a valid Age public recipient/i);
242
- expect(() => sops().rotate('')).to.throw(/requires --recipient/);
568
+ expect(() => sops().rotateRecipient('/etc/passwd')).to.throw(/not a valid Age public recipient/i);
569
+ expect(() => sops().rotateRecipient('')).to.throw(/requires --args recipient/);
243
570
  });
244
571
  });
245
572
 
246
573
  describe('emergency purge', () => {
247
574
  it('archives rather than deletes unless forced', () => {
248
- const secretsSource = fs.readFileSync(new URL('../src/cli/secrets.js', import.meta.url), 'utf8');
249
- const start = secretsSource.indexOf(' purge(name, options = {}) {');
575
+ const secretsSource = fs.readFileSync(new URL('../../../../src/cli/secrets.js', import.meta.url), 'utf8');
576
+ const start = secretsSource.indexOf(' purge(name, options = {}) {');
250
577
  expect(start, 'purge() not found').to.be.greaterThan(-1);
251
- const body = secretsSource.slice(start, secretsSource.indexOf('\n /**', start));
578
+ const body = secretsSource.slice(start, secretsSource.indexOf('\n /**', start));
252
579
  expect(body).to.include('kubectl delete secret ${name} -n ${namespace} --ignore-not-found');
253
580
  expect(body).to.include('options.force');
254
581
  expect(body).to.include('fs.moveSync');
@@ -266,22 +593,44 @@ describe('sops encrypted secret store', () => {
266
593
  });
267
594
 
268
595
  describe('CLI surface', () => {
269
- const cliSource = fs.readFileSync(new URL('../src/cli/index.js', import.meta.url), 'utf8');
270
-
271
- it('makes the platform argument optional so --install-tools needs no platform', () => {
272
- expect(cliSource).to.include("argument(\n '[platform]'");
273
- expect(cliSource).to.include('if (options.installTools) return Underpost.secret.sops.installTooling();');
274
- });
275
-
276
- it('exposes the emergency flags', () => {
277
- for (const flag of ['--install-tools', '--rotate', '--recipient <age-public-key>', '--prune-recipients'])
278
- expect(cliSource).to.include(flag);
279
- expect(cliSource).to.include("'--purge <secret-name>'");
596
+ const cliSource = fs.readFileSync(new URL('../../../../src/cli/index.js', import.meta.url), 'utf8');
597
+ const domainsSource = fs.readFileSync(new URL('../../../../src/cli/domains.js', import.meta.url), 'utf8');
598
+
599
+ it('registers all three domains from the one shared factory', () => {
600
+ // Symmetry is structural: there is no per-domain option or action registration to drift.
601
+ for (const name of ["name: 'secret'", "name: 'host'", "name: 'app'"]) expect(cliSource, name).to.include(name);
602
+ expect(cliSource).to.include('registerDomainCommand(program, domain)');
603
+ expect(cliSource.match(/registerDomainCommand\(/g)).to.have.lengthOf(1);
604
+ });
605
+
606
+ it('declares the canonical action and option sets exactly once', () => {
607
+ expect(domainsSource.match(/const DOMAIN_ACTIONS = \[/g)).to.have.lengthOf(1);
608
+ expect(domainsSource.match(/const DOMAIN_OPTIONS = \[/g)).to.have.lengthOf(1);
609
+ for (const action of ['setup', 'load', 'publish', 'apply', 'status', 'rotate', 'clean'])
610
+ expect(domainsSource, action).to.include(`name: '${action}'`);
611
+ for (const flag of ['--env <env>', '--namespace <namespace>', '--args <key=value-list>', '--dry-run', '--force'])
612
+ expect(domainsSource, flag).to.include(flag);
613
+ });
614
+
615
+ it('keeps every legacy single-purpose flag out of the domain surface', () => {
616
+ for (const legacy of [
617
+ '--install-tools',
618
+ '--recipient',
619
+ '--prune-recipients',
620
+ '--purge',
621
+ '--setup',
622
+ '--status',
623
+ '--create-from-env',
624
+ '--from-cron-env',
625
+ '--underpost-config',
626
+ '--global-clean',
627
+ ])
628
+ expect(domainsSource, legacy).to.not.include(legacy);
280
629
  });
281
630
 
282
- it('routes rotate and purge through the selected platform', () => {
283
- expect(cliSource).to.include('Underpost.secret[platform].rotate(options.recipient, options)');
284
- expect(cliSource).to.include('Underpost.secret[platform].purge(options.purge, options)');
631
+ it('rejects an action outside the canonical set instead of silently doing nothing', () => {
632
+ expect(domainsSource).to.include('Unknown ${name} action');
633
+ expect(domainsSource).to.include("does not implement the '${action}' action");
285
634
  });
286
635
  });
287
636
 
@@ -307,12 +656,12 @@ describe('sops encrypted secret store', () => {
307
656
  '',
308
657
  ].join('\n');
309
658
 
310
- before(() => {
659
+ beforeAll(() => {
311
660
  createdStoreRoot = !fs.existsSync(storeRoot);
312
661
  fs.ensureDirSync(dir);
313
662
  });
314
663
 
315
- after(() => {
664
+ afterAll(() => {
316
665
  fs.removeSync(dir);
317
666
  if (createdStoreRoot && fs.existsSync(storeRoot) && fs.readdirSync(storeRoot).length === 0)
318
667
  fs.removeSync(storeRoot);
@@ -435,7 +784,7 @@ describe('sops encrypted secret store', () => {
435
784
  '',
436
785
  ].join('\n');
437
786
 
438
- before(() => {
787
+ beforeAll(() => {
439
788
  createdStoreRoot = !fs.existsSync(storeRoot);
440
789
  fs.ensureDirSync(dir);
441
790
  fs.writeFileSync(`${dir}/mariadb-secret.enc.yaml`, sealedTo('mariadb-secret', FOREIGN), 'utf8');
@@ -444,7 +793,7 @@ describe('sops encrypted secret store', () => {
444
793
  process.env.SOPS_AGE_KEY_FILE = keyPath;
445
794
  });
446
795
 
447
- after(() => {
796
+ afterAll(() => {
448
797
  if (originalKeyFile === undefined) delete process.env.SOPS_AGE_KEY_FILE;
449
798
  else process.env.SOPS_AGE_KEY_FILE = originalKeyFile;
450
799
  fs.removeSync(keyPath);
@@ -481,12 +830,12 @@ describe('sops encrypted secret store', () => {
481
830
  expect(error, 'expected assertDecryptable to throw').to.be.an('error');
482
831
  expect(error.message).to.include(`${NS}/mariadb-secret`);
483
832
  expect(error.message).to.include(FOREIGN);
484
- expect(error.message).to.include('underpost secret sops --rotate');
485
- expect(error.message).to.include('underpost run sops-setup --force');
833
+ expect(error.message).to.include('underpost secret rotate --args recipient=');
834
+ expect(error.message).to.include('underpost secret setup --force');
486
835
  });
487
836
 
488
837
  it('raises the adoption error before any manifest reaches kubectl', () => {
489
- expect(() => sops().apply(NS)).to.throw(/sealed to Age recipients this host does not hold/);
838
+ expect(() => sops().applyStore(NS)).to.throw(/sealed to Age recipients this host does not hold/);
490
839
  });
491
840
 
492
841
  it('raises rather than sliding back to the origin seed path for a present-but-unreadable manifest', () => {
@@ -502,13 +851,13 @@ describe('sops encrypted secret store', () => {
502
851
  let createdStoreRoot = false;
503
852
  let savedConf = null;
504
853
 
505
- before(() => {
854
+ beforeAll(() => {
506
855
  createdStoreRoot = !fs.existsSync(storeRoot);
507
856
  fs.ensureDirSync(storeRoot);
508
857
  if (fs.existsSync(confPath)) savedConf = fs.readFileSync(confPath, 'utf8');
509
858
  });
510
859
 
511
- after(() => {
860
+ afterAll(() => {
512
861
  if (savedConf !== null) fs.writeFileSync(confPath, savedConf, 'utf8');
513
862
  else fs.removeSync(confPath);
514
863
  if (createdStoreRoot && fs.existsSync(storeRoot) && fs.readdirSync(storeRoot).length === 0)
@@ -541,31 +890,36 @@ describe('sops encrypted secret store', () => {
541
890
  });
542
891
 
543
892
  it('registers the host during init so a pulled store cannot be encrypted to write-only', () => {
544
- const secretsSource = fs.readFileSync(new URL('../src/cli/secrets.js', import.meta.url), 'utf8');
545
- const start = secretsSource.indexOf(' init() {');
546
- const body = secretsSource.slice(start, secretsSource.indexOf('\n /**', start));
893
+ const secretsSource = fs.readFileSync(new URL('../../../../src/cli/secrets.js', import.meta.url), 'utf8');
894
+ const start = secretsSource.indexOf(' init() {');
895
+ const body = secretsSource.slice(start, secretsSource.indexOf('\n /**', start));
547
896
  expect(body).to.include('ensureCreationRecipient(recipient)');
548
897
  });
549
898
  });
550
899
 
551
- describe('sops-setup onboarding reports', () => {
552
- const runSource = fs.readFileSync(new URL('../src/cli/run.js', import.meta.url), 'utf8');
553
- const start = runSource.indexOf(" 'sops-setup': (path = '', options = DEFAULT_OPTION) => {");
554
- const body = runSource.slice(start, runSource.indexOf('\n /**', start));
900
+ describe('secret --setup onboarding reports', () => {
901
+ const secretsSource = fs.readFileSync(new URL('../../../../src/cli/secrets.js', import.meta.url), 'utf8');
902
+ const start = secretsSource.indexOf(" setupStore(names = '', options = {}) {");
903
+ const body = secretsSource.slice(start, secretsSource.indexOf('\n /**', start));
555
904
 
556
905
  it('does not report a manifest it cannot decrypt as onboarded', () => {
557
- expect(start, 'sops-setup runner not found').to.be.greaterThan(-1);
558
- expect(body).to.include('Underpost.secret.sops.decryptable(');
906
+ expect(start, 'secret --setup implementation not found').to.be.greaterThan(-1);
907
+ expect(body).to.include('Underpost.secret.decryptable(');
559
908
  expect(body).to.include('sealed to an Age recipient this host does not hold');
560
909
  });
561
910
 
562
911
  it('warns when --force replaces a stored credential with a generated one', () => {
563
912
  expect(body).to.include('generated while replacing the stored manifest');
564
913
  });
914
+
915
+ it('validates and applies only the explicitly requested manifests', () => {
916
+ expect(body).to.include('applySelected(secretNames, namespace');
917
+ expect(body).to.not.include('sops.apply(namespace');
918
+ });
565
919
  });
566
920
 
567
921
  describe('rotation safeguards', () => {
568
- const secretsSource = fs.readFileSync(new URL('../src/cli/secrets.js', import.meta.url), 'utf8');
922
+ const secretsSource = fs.readFileSync(new URL('../../../../src/cli/secrets.js', import.meta.url), 'utf8');
569
923
 
570
924
  it('refuses to revoke recipients without an explicit confirmation', () => {
571
925
  expect(secretsSource).to.include('Refusing to revoke ');
@@ -583,19 +937,19 @@ describe('sops encrypted secret store', () => {
583
937
  it('refuses to plan a rotation the local key could never perform', () => {
584
938
  // updatekeys has to decrypt each data key first, so a host that cannot read the store cannot
585
939
  // rotate it — including on a dry run, where a reported plan would be pure misdirection.
586
- const start = secretsSource.indexOf(' rotate(recipient, options = {}) {');
587
- const body = secretsSource.slice(start, secretsSource.indexOf('\n /**', start));
940
+ const start = secretsSource.indexOf(' rotateRecipient(recipient, options = {}) {');
941
+ const body = secretsSource.slice(start, secretsSource.indexOf('\n /**', start));
588
942
  expect(body).to.include('assertDecryptable(manifests)');
589
943
  expect(body.indexOf('assertDecryptable(manifests)')).to.be.lessThan(body.indexOf('if (options.dryRun)'));
590
944
  });
591
945
  });
592
946
 
593
947
  describe('encrypt write safety', () => {
594
- const secretsSource = fs.readFileSync(new URL('../src/cli/secrets.js', import.meta.url), 'utf8');
948
+ const secretsSource = fs.readFileSync(new URL('../../../../src/cli/secrets.js', import.meta.url), 'utf8');
595
949
 
596
950
  it('stages and moves rather than redirecting straight onto the target', () => {
597
- const start = secretsSource.indexOf(' encrypt(plaintextPath, namespace');
598
- const body = secretsSource.slice(start, secretsSource.indexOf('\n /**', start));
951
+ const start = secretsSource.indexOf(' encrypt(plaintextPath, namespace');
952
+ const body = secretsSource.slice(start, secretsSource.indexOf('\n /**', start));
599
953
  expect(body).to.include('.staged');
600
954
  expect(body).to.include('fs.moveSync');
601
955
  expect(body).to.include('assertManifest');