@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.
- package/.dockerignore +1 -0
- package/.env.example +16 -0
- package/.github/workflows/coverall.cyberia.ci.yml +107 -0
- package/.github/workflows/cyberia-client.cd.yml +6 -18
- package/.github/workflows/cyberia-server.cd.yml +6 -18
- package/.github/workflows/docker-image.cyberia-client.ci.yml +8 -5
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +8 -3
- package/.github/workflows/docker-image.cyberia-server.ci.yml +8 -5
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +8 -5
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +15 -1
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +15 -1
- package/.github/workflows/engine-cyberia.cd.yml +6 -78
- package/.github/workflows/engine-cyberia.ci.yml +4 -5
- package/.github/workflows/ghpkg.ci.yml +42 -21
- package/.github/workflows/hardhat.ci.yml +12 -11
- package/.github/workflows/npmpkg.ci.yml +17 -15
- package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
- package/.github/workflows/pwa-microservices-template-test.ci.yml +2 -2
- package/.github/workflows/release.cd.yml +2 -9
- package/.prettierignore +1 -0
- package/AGENTS.md +47 -0
- package/CHANGELOG.md +581 -2417
- package/CLI-HELP.md +416 -127
- package/Dockerfile +25 -9
- package/Dockerfile.dev +25 -9
- package/Dockerfile.test +4 -4
- package/README.md +21 -1
- package/bin/build.js +70 -43
- package/bin/build.template.js +8 -3
- package/bin/cyberia.js +372 -135
- package/bin/deploy.js +18 -104
- package/bin/index.js +372 -135
- package/compose.env +16 -0
- package/conf.js +93 -4
- package/deploy/cyberia-client/deploy.sh +47 -0
- package/deploy/cyberia-client/package.sh +18 -0
- package/deploy/cyberia-client/state.sh +14 -0
- package/deploy/cyberia-server/deploy.sh +47 -0
- package/deploy/cyberia-server/package.sh +18 -0
- package/deploy/cyberia-server/state.sh +14 -0
- package/deploy/dd-cyberia/deploy.sh +22 -0
- package/deploy/dd-cyberia/init.sh +55 -0
- package/deploy/dd-cyberia/package.sh +17 -0
- package/deploy/dd-cyberia/state.sh +14 -0
- package/deploy/dd-cyberia/sync-deploy.sh +116 -0
- package/deploy/lib/github-actions-logging.sh +594 -0
- package/deploy/lib/host.sh +185 -0
- package/deploy/lib/state.sh +62 -0
- package/deploy/pwa-microservices-template/deploy.sh +45 -0
- package/deploy/release/deploy.sh +32 -0
- package/deployment.yaml +7 -2
- package/docker-compose.yml +31 -37
- package/examples/static-page/README.md +19 -19
- package/examples/static-page/ssr-components/CustomPage.js +2 -2
- package/hardhat/hardhat.config.js +1 -6
- package/hardhat/package-lock.json +104 -43
- package/hardhat/package.json +3 -3
- package/manifests/alertmanager/deployment.yaml +81 -0
- package/manifests/blackbox-exporter/deployment.yaml +75 -0
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +36 -7
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +77 -0
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +7 -2
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/grafana/deployment.yaml +26 -7
- package/manifests/grafana/kustomization.yaml +1 -0
- package/manifests/grafana/service.yaml +6 -1
- package/manifests/mariadb/pv.yaml +1 -0
- package/manifests/mongodb/statefulset.yaml +9 -2
- package/manifests/mysql/pv-pvc.yaml +14 -1
- package/manifests/postgresql/pv.yaml +12 -1
- package/manifests/postgresql/pvc.yaml +1 -0
- package/manifests/prometheus/deployment.yaml +18 -22
- package/package.json +83 -10
- package/scripts/audit-selinux.sh +64 -0
- package/scripts/event-monitor.sh +56 -0
- package/scripts/gpu-diag.sh +0 -0
- package/scripts/ip-info.sh +0 -0
- package/scripts/k3s-node-setup.sh +30 -14
- package/scripts/kubeadm-node-setup.sh +40 -31
- package/scripts/lxd-vm-setup.sh +0 -0
- package/scripts/maas-nat-firewalld.sh +0 -0
- package/scripts/maas-setup.sh +8 -8
- package/scripts/nat-iptables.sh +2 -0
- package/scripts/rhel-grpc-setup.sh +0 -0
- package/scripts/rocky-kickstart.sh +25 -9
- package/scripts/selinux-normalize.sh +907 -0
- package/scripts/shutdown-machine.sh +843 -0
- package/scripts/test-monitor.sh +3 -3
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +1 -1
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +2 -2
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +2 -2
- package/src/api/core/core.controller.js +1 -1
- package/src/api/core/core.router.js +2 -2
- package/src/api/core/core.service.js +2 -2
- package/src/api/crypto/crypto.controller.js +1 -1
- package/src/api/crypto/crypto.service.js +1 -1
- package/src/api/cyberia-action/cyberia-action.controller.js +1 -1
- package/src/api/cyberia-action/cyberia-action.router.js +1 -1
- package/src/api/cyberia-action/cyberia-action.service.js +2 -2
- package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -1
- package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +1 -1
- package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +2 -2
- package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +1 -1
- package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
- package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +2 -2
- package/src/api/cyberia-entity/cyberia-entity.controller.js +1 -1
- package/src/api/cyberia-entity/cyberia-entity.router.js +1 -1
- package/src/api/cyberia-entity/cyberia-entity.service.js +2 -2
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +1 -1
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +1 -1
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +2 -2
- package/src/api/cyberia-instance/cyberia-fallback-capture.js +466 -0
- package/src/api/cyberia-instance/cyberia-fallback-default-items.js +63 -0
- package/src/api/cyberia-instance/cyberia-fallback-world.js +16 -1
- package/src/api/cyberia-instance/cyberia-instance-map.service.js +1 -1
- package/src/api/cyberia-instance/cyberia-instance.controller.js +4 -1
- package/src/api/cyberia-instance/cyberia-instance.router.js +15 -2
- package/src/api/cyberia-instance/cyberia-instance.service.js +45 -2
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +1 -1
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +1 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +1 -1
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +2 -2
- package/src/api/cyberia-map/cyberia-map.controller.js +1 -1
- package/src/api/cyberia-map/cyberia-map.router.js +1 -1
- package/src/api/cyberia-map/cyberia-map.service.js +2 -2
- package/src/api/cyberia-quest/cyberia-quest.controller.js +1 -1
- package/src/api/cyberia-quest/cyberia-quest.router.js +1 -1
- package/src/api/cyberia-quest/cyberia-quest.service.js +2 -2
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +1 -1
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +1 -1
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +2 -2
- package/src/api/cyberia-saga/cyberia-saga.controller.js +1 -1
- package/src/api/cyberia-saga/cyberia-saga.router.js +1 -1
- package/src/api/cyberia-saga/cyberia-saga.service.js +2 -2
- package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +16 -29
- package/src/api/cyberia-skill/cyberia-skill.controller.js +1 -1
- package/src/api/cyberia-skill/cyberia-skill.router.js +1 -1
- package/src/api/cyberia-skill/cyberia-skill.service.js +2 -2
- package/src/api/default/default.controller.js +1 -1
- package/src/api/default/default.router.js +1 -1
- package/src/api/default/default.service.js +2 -2
- package/src/api/document/document.controller.js +1 -1
- package/src/api/document/document.router.js +1 -1
- package/src/api/document/document.service.js +2 -2
- package/src/api/file/file.controller.js +1 -1
- package/src/api/file/file.router.js +1 -1
- package/src/api/file/file.service.js +2 -2
- package/src/api/instance/instance.controller.js +1 -1
- package/src/api/instance/instance.router.js +1 -1
- package/src/api/instance/instance.service.js +2 -2
- package/src/api/ipfs/ipfs.controller.js +1 -1
- package/src/api/ipfs/ipfs.router.js +2 -2
- package/src/api/ipfs/ipfs.service.js +2 -2
- package/src/api/object-layer/object-layer.controller.js +1 -1
- package/src/api/object-layer/object-layer.model.js +204 -3
- package/src/api/object-layer/object-layer.router.js +2 -2
- package/src/api/object-layer/object-layer.service.js +4 -17
- package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +1 -1
- package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +1 -1
- package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +2 -2
- package/src/api/test/test.controller.js +1 -1
- package/src/api/test/test.service.js +1 -1
- package/src/api/user/guest.service.js +3 -3
- package/src/api/user/user.controller.js +1 -1
- package/src/api/user/user.router.js +2 -2
- package/src/api/user/user.service.js +4 -4
- package/src/api.js +5 -9
- package/src/cli/app.js +391 -0
- package/src/cli/baremetal.js +30 -36
- package/src/cli/client.js +306 -0
- package/src/cli/cloud-init.js +3 -3
- package/src/cli/cluster.js +242 -135
- package/src/cli/db.js +126 -67
- package/src/cli/deploy.js +158 -60
- package/src/cli/docker-compose.js +5 -5
- package/src/cli/domains.js +184 -0
- package/src/cli/dotenv-store.js +143 -0
- package/src/cli/event.js +2281 -0
- package/src/cli/fs.js +3 -3
- package/src/cli/host.js +672 -0
- package/src/cli/image.js +4 -7
- package/src/cli/index.js +426 -134
- package/src/cli/ipfs.js +34 -30
- package/src/cli/kickstart.js +1 -1
- package/src/cli/kubectl.js +218 -41
- package/src/cli/lxd.js +3 -3
- package/src/cli/monitor.js +1359 -26
- package/src/cli/package.js +90 -0
- package/src/cli/release.js +73 -34
- package/src/cli/repository.js +564 -367
- package/src/cli/run.js +362 -654
- package/src/cli/secrets.js +2158 -964
- package/src/cli/ssh.js +651 -331
- package/src/cli/state.js +394 -0
- package/src/cli/static.js +2 -2
- package/src/cli/system.js +26 -13
- package/src/cli/test.js +199 -125
- package/src/cli/vultr.js +623 -0
- package/src/cli/wireguard.js +2930 -0
- package/src/client/components/core/CalendarCore.js +53 -96
- package/src/client/components/core/Docs.js +258 -211
- package/src/client/components/core/FileExplorer.js +0 -21
- package/src/client/components/core/PanelForm.js +4 -4
- package/src/client/components/core/Repository.js +56 -0
- package/src/client/components/core/Translate.js +7 -0
- package/src/client/components/core/{FullScreen.js → ViewModeController.js} +42 -39
- package/src/client/components/core/Wallet.js +0 -11
- package/src/client/components/core/Worker.js +7 -1
- package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +2 -2
- package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +368 -0
- package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +35 -1
- package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +4 -0
- package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +2 -2
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
- package/src/client/components/default/AppShellDefault.js +1 -1
- package/src/client/components/default/SettingsDefault.js +2 -2
- package/src/client/components/itemledger/SettingsItemledger.js +2 -2
- package/src/client/components/underpost/AppShellUnderpost.js +3 -2
- package/src/client/components/underpost/SettingsUnderpost.js +2 -2
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +17 -3
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +32 -5
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +1 -1
- package/src/client/public/cyberia-docs/CYBERIA.md +21 -1
- package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +23 -6
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
- package/src/client/public/default/android-chrome-144x144.png +0 -0
- package/src/client/public/default/android-chrome-192x192.png +0 -0
- package/src/client/public/default/android-chrome-256x256.png +0 -0
- package/src/client/public/default/android-chrome-36x36.png +0 -0
- package/src/client/public/default/android-chrome-384x384.png +0 -0
- package/src/client/public/default/android-chrome-48x48.png +0 -0
- package/src/client/public/default/android-chrome-512x512.png +0 -0
- package/src/client/public/default/android-chrome-72x72.png +0 -0
- package/src/client/public/default/android-chrome-96x96.png +0 -0
- package/src/client/public/default/apple-touch-icon-1024x1024.png +0 -0
- package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
- package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
- package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
- package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
- package/src/client/public/default/apple-touch-icon-167x167.png +0 -0
- package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
- package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
- package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
- package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
- package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
- package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
- package/src/client/public/default/apple-touch-icon.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1125x2436.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1136x640.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1170x2532.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1179x2556.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1242x2208.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1242x2688.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1284x2778.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1290x2796.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1334x750.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1488x2266.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1536x2048.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1620x2160.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1640x2160.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1668x2224.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1668x2388.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1792x828.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2048x1536.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2048x2732.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2160x1620.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2160x1640.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2208x1242.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2224x1668.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2266x1488.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2388x1668.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2436x1125.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2532x1170.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2556x1179.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2688x1242.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2732x2048.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2778x1284.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2796x1290.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-640x1136.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-750x1334.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-828x1792.png +0 -0
- package/src/client/public/default/assets/background/white.jpg +0 -0
- package/src/client/public/default/browserconfig.xml +1 -1
- package/src/client/public/default/favicon-16x16.png +0 -0
- package/src/client/public/default/favicon-32x32.png +0 -0
- package/src/client/public/default/favicon-48x48.png +0 -0
- package/src/client/public/default/favicon.ico +0 -0
- package/src/client/public/default/manifest.webmanifest +5 -5
- package/src/client/public/default/mstile-144x144.png +0 -0
- package/src/client/public/default/mstile-150x150.png +0 -0
- package/src/client/public/default/mstile-310x150.png +0 -0
- package/src/client/public/default/mstile-310x310.png +0 -0
- package/src/client/public/default/mstile-70x70.png +0 -0
- package/src/client/public/default/yandex-browser-50x50.png +0 -0
- package/src/client/public/default/yandex-browser-manifest.json +1 -1
- package/src/client/services/cyberia-instance/cyberia-instance.service.js +40 -0
- package/src/client/services/object-layer/object-layer.management.js +4 -4
- package/src/client/ssr/RootDocument.js +1 -1
- package/src/client/ssr/body/404.js +1 -1
- package/src/client/ssr/body/500.js +1 -1
- package/src/client/ssr/body/CacheControl.js +1 -1
- package/src/client/ssr/body/CyberiaDefaultSplashScreen.js +1 -1
- package/src/client/ssr/body/DefaultSplashScreen.js +1 -1
- package/src/client/ssr/body/SwaggerDarkMode.js +1 -1
- package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +1 -1
- package/src/client/ssr/head/CryptokoynScripts.js +1 -1
- package/src/client/ssr/head/Css.js +1 -1
- package/src/client/ssr/head/CyberiaPortalScripts.js +1 -1
- package/src/client/ssr/head/DefaultScripts.js +1 -1
- package/src/client/ssr/head/ItemledgerScripts.js +1 -1
- package/src/client/ssr/head/Microdata.js +1 -1
- package/src/client/ssr/head/Production.js +1 -1
- package/src/client/ssr/head/Pwa.js +1 -1
- package/src/client/ssr/head/PwaItemledger.js +1 -1
- package/src/client/ssr/head/Seo.js +1 -1
- package/src/client/ssr/head/UnderpostScripts.js +1 -1
- package/src/client/ssr/mailer/DefaultRecoverEmail.js +1 -1
- package/src/client/ssr/mailer/DefaultVerifyEmail.js +1 -1
- package/src/client/ssr/views/Cyberia404.js +7 -5
- package/src/client/ssr/views/CyberiaServerMetrics.js +1 -1
- package/src/client/ssr/views/Maintenance.js +1 -1
- package/src/client/ssr/views/NoNetworkConnection.js +1 -1
- package/src/client/ssr/views/Test.js +1 -1
- package/src/client/sw/core.sw.js +20 -14
- package/src/client-builder/client-build-docs.js +61 -46
- package/src/client-builder/client-build-live.js +2 -2
- package/src/client-builder/client-build.js +58 -44
- package/src/client-builder/client-dev-server.js +3 -3
- package/src/client-builder/client-icons.js +2 -2
- package/src/client-builder/ssr.js +3 -3
- package/src/client.build.js +3 -6
- package/src/client.dev.js +3 -6
- package/src/db/DataBaseProvider.js +186 -3
- package/src/db/mariadb/MariaDB.js +3 -2
- package/src/db/mongo/MongoBootstrap.js +273 -101
- package/src/db/mongo/MongooseDB.js +11 -8
- package/src/{server/valkey.js → db/valkey/Valkey.js} +5 -4
- package/src/grpc/cyberia/grpc-server.js +1 -1
- package/src/index.js +103 -23
- package/src/mailer/EmailRender.js +2 -2
- package/src/mailer/MailerInterceptor.js +87 -0
- package/src/mailer/MailerProvider.js +50 -6
- package/src/projects/cyberia/atlas-sprite-sheet-generator.js +1 -1
- package/src/projects/cyberia/besu-genesis-generator.js +3 -3
- package/src/projects/cyberia/catalog-cyberia.js +54 -6
- package/src/projects/cyberia/gemini-client.js +1 -1
- package/src/projects/cyberia/generate-saga.js +15 -24
- package/src/projects/cyberia/hot-reload-trigger.js +1 -1
- package/src/projects/cyberia/instance-data.js +22 -3
- package/src/projects/cyberia/ipfs-client.js +4 -4
- package/src/projects/cyberia/map-preview-generator.js +1 -1
- package/src/projects/cyberia/object-layer.js +12 -22
- package/src/projects/cyberia/semantic-layer-generator.js +1 -1
- package/src/projects/underpost/catalog-underpost.js +13 -2
- package/src/proxy.js +4 -7
- package/src/runtime/cyberia-client/Dockerfile +9 -4
- package/src/runtime/cyberia-client/Dockerfile.dev +8 -2
- package/src/runtime/cyberia-server/Dockerfile +9 -4
- package/src/runtime/cyberia-server/Dockerfile.dev +8 -2
- package/src/runtime/engine-cyberia/Dockerfile +25 -9
- package/src/runtime/engine-cyberia/Dockerfile.dev +25 -9
- package/src/runtime/engine-cyberia/Dockerfile.test +4 -4
- package/src/runtime/engine-cyberia/compose.env +16 -0
- package/src/runtime/engine-cyberia/docker-compose.yml +31 -37
- package/src/runtime/express/Express.js +32 -27
- package/src/runtime/lampp/Lampp.js +22 -70
- package/src/runtime/nginx/Nginx.js +1 -1
- package/src/runtime/wp/Wp.js +16 -48
- package/src/server/{catalog.js → build/catalog.js} +15 -5
- package/src/server/build/coverage.js +117 -0
- package/src/server/build/execution.js +402 -0
- package/src/server/build/package.js +465 -0
- package/src/server/build/testing.js +528 -0
- package/src/server/{dns.js → network/dns.js} +241 -14
- package/src/server/network/forward-proxy.js +541 -0
- package/src/server/{middlewares.js → network/middlewares.js} +59 -4
- package/src/server/network/node-capability.js +98 -0
- package/src/server/{peer.js → network/peer.js} +3 -3
- package/src/server/{proxy.js → network/proxy.js} +7 -13
- package/src/server/network/router.js +356 -0
- package/src/server/{tls.js → network/tls.js} +2 -2
- package/src/server/network/underpost-compression.js +186 -0
- package/src/server/{underpost-gateway.js → network/underpost-gateway.js} +26 -16
- package/src/server/{underpost-ingress.js → network/underpost-ingress.js} +19 -3
- package/src/server/{cri.js → ops/cri.js} +2 -2
- package/src/server/ops/cron.js +949 -0
- package/src/server/ops/event-notification.js +284 -0
- package/src/server/{logger.js → ops/logger.js} +108 -23
- package/src/server/ops/monitoring.js +1724 -0
- package/src/server/ops/systemd.js +259 -0
- package/src/server/{conf.js → runtime/conf.js} +521 -520
- package/src/server/runtime/config-scope.js +215 -0
- package/src/server/runtime/environment.js +149 -0
- package/src/server/{process.js → runtime/process.js} +153 -13
- package/src/server/{runtime-status.js → runtime/runtime-status.js} +155 -16
- package/src/server/{runtime.js → runtime/runtime.js} +12 -8
- package/src/server/{start.js → runtime/start.js} +128 -39
- package/src/server/{auth.js → security/auth.js} +7 -8
- package/src/server/security/container-storage.js +382 -0
- package/src/server/{crypto.js → security/crypto.js} +1 -1
- package/src/server/security/selinux.js +185 -0
- package/src/server/{backup.js → storage/backup.js} +17 -8
- package/src/server/{data-query.js → storage/data-query.js} +1 -1
- package/src/server/{downloader.js → storage/downloader.js} +2 -2
- package/src/server/storage/repository.js +68 -0
- package/src/server.js +4 -7
- package/src/ws/IoInterface.js +1 -1
- package/src/ws/IoServer.js +1 -1
- package/src/ws/core/core.ws.connection.js +1 -1
- package/src/ws/core/core.ws.emit.js +1 -1
- package/src/ws/core/core.ws.server.js +1 -1
- package/src/ws/default/default.ws.connection.js +1 -1
- package/src/ws/default/default.ws.emit.js +1 -1
- package/src/ws/default/default.ws.server.js +1 -1
- package/test/e2e/event-e2e-public-ingress-down.js +45 -0
- package/test/e2e/event-e2e-wireguard-server-down.js +38 -0
- package/test/e2e/event-e2e-wireguard-spoke-down.js +40 -0
- package/test/{api.test.js → integration/app/api.test.js} +33 -14
- package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +30 -0
- package/test/{cyberia-instance-conf-defaults.test.js → integration/app/cyberia/cyberia-instance-conf-defaults.test.js} +2 -1
- package/test/integration/app/cyberia/cyberia-load.test.js +467 -0
- package/test/integration/app/cyberia/fallback-world-capture.test.js +253 -0
- package/test/integration/app/cyberia/object-layer-item-id.test.js +327 -0
- package/test/{shape-generator.test.js → integration/app/cyberia/shape-generator.test.js} +1 -1
- package/test/integration/infra/1-security/config-scope.test.js +305 -0
- package/test/integration/infra/1-security/container-storage.test.js +356 -0
- package/test/integration/infra/1-security/in-pod-cli-surface.test.js +86 -0
- package/test/integration/infra/1-security/secret-onboarding.test.js +930 -0
- package/test/integration/infra/1-security/selinux.test.js +69 -0
- package/test/{sops-secret-store.test.js → integration/infra/1-security/sops-secret-store.test.js} +415 -61
- package/test/integration/infra/1-security/systemd-service.test.js +70 -0
- package/test/integration/infra/1-security/underpost-config-secret.test.js +367 -0
- package/test/integration/infra/2-network/dns-firewall.test.js +566 -0
- package/test/integration/infra/2-network/node-capability.test.js +182 -0
- package/test/integration/infra/2-network/wireguard-cli.test.js +1163 -0
- package/test/integration/infra/2-network/wireguard-edge.test.js +1731 -0
- package/test/{cluster-instances.test.js → integration/infra/3-cluster/cluster-instances.test.js} +11 -15
- package/test/{deploy-node-placement.test.js → integration/infra/3-cluster/deploy-node-placement.test.js} +3 -3
- package/test/integration/infra/3-cluster/docker-compose-stack.test.js +313 -0
- package/test/integration/infra/4-ingress/deploy-routes.test.js +379 -0
- package/test/integration/infra/4-ingress/gateway-static-assets.test.js +453 -0
- package/test/{instance-traffic-plan.test.js → integration/infra/4-ingress/instance-traffic-plan.test.js} +10 -6
- package/test/{underpost-gateway.test.js → integration/infra/4-ingress/underpost-gateway.test.js} +47 -6
- package/test/{underpost-ingress.test.js → integration/infra/4-ingress/underpost-ingress.test.js} +56 -4
- package/test/integration/infra/5-observability/cron-jobs.test.js +652 -0
- package/test/{deploy-monitor.test.js → integration/infra/5-observability/deploy-monitor.test.js} +29 -31
- package/test/integration/infra/5-observability/event-notification.test.js +197 -0
- package/test/integration/infra/5-observability/event-remediation.test.js +502 -0
- package/test/integration/infra/5-observability/event-targets.test.js +1174 -0
- package/test/integration/infra/5-observability/monitoring-stack.test.js +902 -0
- package/test/support/shell-harness.js +73 -0
- package/test/unit/build-template.test.js +97 -0
- package/test/unit/build-workflow-offline.test.js +132 -0
- package/test/unit/catalog.test.js +71 -0
- package/test/unit/client-build-docs.test.js +334 -0
- package/test/unit/conf-loading.test.js +303 -0
- package/test/unit/conf-resolution.test.js +979 -0
- package/test/unit/coverage-artifact.test.js +130 -0
- package/test/{crypto.test.js → unit/crypto.test.js} +1 -1
- package/test/unit/deploy-env-resolution.test.js +38 -0
- package/test/unit/deploy-log-table.test.js +447 -0
- package/test/unit/execution-profiles.test.js +219 -0
- package/test/unit/fleet-sync-source.test.js +149 -0
- package/test/unit/logger-redaction.test.js +112 -0
- package/test/unit/package.test.js +403 -0
- package/test/unit/prepare-host.test.js +299 -0
- package/test/unit/process-environment.test.js +18 -0
- package/test/unit/propagation-message.test.js +78 -0
- package/test/unit/redeploy-plan.test.js +24 -0
- package/test/unit/release-bump.test.js +474 -0
- package/test/unit/ssh-output-redaction.test.js +28 -0
- package/test/unit/start-options.test.js +46 -0
- package/test/unit/test-tiers.test.js +193 -0
- package/vitest.config.js +58 -0
- package/.nycrc +0 -9
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +0 -48
- package/manifests/mongodb/pv-pvc.yaml +0 -59
- package/scripts/link-local-underpost-cli.sh +0 -6
- package/src/api/user/user.build.js +0 -16
- package/src/cli/env.js +0 -177
- package/src/client/components/core/Blockchain.js +0 -41
- package/src/client/components/core/Blog.js +0 -9
- package/src/client/components/core/KeyboardAvoidance.js +0 -145
- package/src/client/public/default/assets/background/dark.jpg +0 -0
- package/src/client/public/default/assets/background/white0-min.jpg +0 -0
- package/src/client/public/default/assets/background/white0.jpg +0 -0
- package/src/client/public/doc/favicon.ico +0 -0
- package/src/client/public/doc/sitemap +0 -148
- package/src/server/cron.js +0 -476
package/test/{sops-secret-store.test.js → integration/infra/1-security/sops-secret-store.test.js}
RENAMED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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().
|
|
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('
|
|
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.
|
|
141
|
-
|
|
142
|
-
//
|
|
143
|
-
//
|
|
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\\.
|
|
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.
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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().
|
|
242
|
-
expect(() => sops().
|
|
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('
|
|
249
|
-
const start = secretsSource.indexOf('
|
|
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
|
|
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('
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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('
|
|
283
|
-
expect(
|
|
284
|
-
expect(
|
|
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
|
-
|
|
659
|
+
beforeAll(() => {
|
|
311
660
|
createdStoreRoot = !fs.existsSync(storeRoot);
|
|
312
661
|
fs.ensureDirSync(dir);
|
|
313
662
|
});
|
|
314
663
|
|
|
315
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
485
|
-
expect(error.message).to.include('underpost
|
|
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().
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
545
|
-
const start = secretsSource.indexOf('
|
|
546
|
-
const body = secretsSource.slice(start, secretsSource.indexOf('\n
|
|
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('
|
|
552
|
-
const
|
|
553
|
-
const start =
|
|
554
|
-
const body =
|
|
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, '
|
|
558
|
-
expect(body).to.include('Underpost.secret.
|
|
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('
|
|
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('
|
|
587
|
-
const body = secretsSource.slice(start, secretsSource.indexOf('\n
|
|
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('
|
|
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('
|
|
598
|
-
const body = secretsSource.slice(start, secretsSource.indexOf('\n
|
|
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');
|