@starci/hfs 4.2.17 → 4.4.0
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/CHANGELOG.md +28 -0
- package/README.md +7 -0
- package/db/migrate.mjs +361 -0
- package/db/post-cutover.mjs +136 -0
- package/emit/db-types.mjs +10 -1
- package/lint/run.mjs +1 -1
- package/package.json +4 -1
- package/release/gate.mjs +298 -0
- package/release/index.mjs +103 -0
- package/release/push.mjs +110 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
- package/runtime/knowledge/hfs/rules.yaml +3437 -160
- package/runtime/knowledge/hfs/slots.yaml +430 -92
- package/runtime/knowledge/patterns/be/api.yaml +92 -2
- package/runtime/knowledge/patterns/be/cli.yaml +37 -9
- package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
- package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
- package/runtime/knowledge/patterns/be/projections.yaml +4 -2
- package/runtime/knowledge/patterns/be/queues.yaml +11 -3
- package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
- package/runtime/knowledge/patterns/be/saga.yaml +20 -3
- package/runtime/knowledge/patterns/be/webhooks.yaml +210 -36
- package/runtime/knowledge/patterns/fe/folder.yaml +4 -0
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +3128 -47
- package/runtime/modules/models/runtimes.yaml +1 -1
- package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
- package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
- package/runtime/packages/cli/src/validate-args.mjs +213 -0
- package/runtime/scripts/api/git/add.mjs +5 -0
- package/runtime/scripts/api/git/cat-file.mjs +5 -0
- package/runtime/scripts/api/git/init.mjs +7 -0
- package/runtime/scripts/api/git/lib.mjs +3 -0
- package/runtime/scripts/api/git/tag.mjs +5 -0
- package/runtime/scripts/api/node/lib.mjs +20 -0
- package/runtime/scripts/api/node/run-node.mjs +6 -0
- package/runtime/scripts/api/sops/lib.mjs +41 -0
- package/runtime/scripts/api/sops/resolve-sops.mjs +7 -0
- package/runtime/scripts/api/sops/seal.mjs +1 -1
- package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
- package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
- package/runtime/scripts/api/supabase/database-query.mjs +5 -0
- package/runtime/scripts/api/supabase/lib.mjs +44 -0
- package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
- package/runtime/scripts/hfs/app-wiring.mjs +201 -0
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +103 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +22 -10
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
- package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
- package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
- package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
- package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
- package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
- package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +23 -50
- package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
- package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
- package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +49 -25
- package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
- package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
- package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
- package/runtime/scripts/hfs/architecture/register-once.mjs +15 -7
- package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
- package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
- package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +214 -17
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
- package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
- package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
- package/runtime/scripts/hfs/architecture/tiers.mjs +11 -4
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +9 -13
- package/runtime/scripts/hfs/check-results.mjs +28 -0
- package/runtime/scripts/hfs/check.mjs +63 -72
- package/runtime/scripts/hfs/coverage-from.mjs +70 -0
- package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
- package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
- package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
- package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
- package/runtime/scripts/hfs/doc-commands.mjs +367 -0
- package/runtime/scripts/hfs/edition-slots.mjs +2 -2
- package/runtime/scripts/hfs/example-secrets.mjs +158 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +91 -6
- package/runtime/scripts/hfs/path-findings.mjs +32 -2
- package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
- package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
- package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
- package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
- package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
- package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
- package/runtime/scripts/hfs/rules/assets.mjs +79 -0
- package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
- package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
- package/runtime/scripts/hfs/rules/contract.mjs +66 -5
- package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
- package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
- package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
- package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
- package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +317 -37
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
- package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
- package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
- package/runtime/scripts/hfs/rules/database.mjs +11 -2
- package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
- package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
- package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
- package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
- package/runtime/scripts/hfs/rules/docker.mjs +24 -7
- package/runtime/scripts/hfs/rules/edition.mjs +11 -10
- package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
- package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
- package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
- package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
- package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
- package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
- package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
- package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
- package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
- package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
- package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
- package/runtime/scripts/hfs/rules/proof-commands.mjs +2 -0
- package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
- package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
- package/runtime/scripts/hfs/rules/saga.mjs +8 -16
- package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
- package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
- package/runtime/scripts/hfs/rules/secrets.mjs +26 -0
- package/runtime/scripts/hfs/rules/services.mjs +4 -4
- package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
- package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
- package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
- package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +168 -0
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
- package/runtime/scripts/hfs/rules/stacks.mjs +13 -8
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
- package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
- package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
- package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
- package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
- package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
- package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
- package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
- package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
- package/runtime/scripts/hfs/sealed-members.mjs +101 -0
- package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
- package/runtime/scripts/hfs/secret-document.mjs +97 -0
- package/runtime/scripts/hfs/secret-edit.mjs +170 -0
- package/runtime/scripts/hfs/secret-errors.mjs +20 -0
- package/runtime/scripts/hfs/secret-files.mjs +124 -0
- package/runtime/scripts/hfs/secret-init.mjs +300 -0
- package/runtime/scripts/hfs/secret-output.mjs +100 -0
- package/runtime/scripts/hfs/secret-sops.mjs +130 -0
- package/runtime/scripts/hfs/secret-store.mjs +163 -0
- package/runtime/scripts/hfs/secret.mjs +162 -193
- package/runtime/scripts/hfs/slot-imports.mjs +18 -3
- package/runtime/scripts/hfs/slot-required.mjs +13 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
- package/runtime/scripts/hfs/slots.mjs +1 -0
- package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
- package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
- package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
- package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
- package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
- package/runtime/scripts/hfs/tree-findings.mjs +22 -0
- package/runtime/scripts/lib/command-tokens.mjs +126 -0
- package/runtime/scripts/lib/compose-values.mjs +11 -0
- package/runtime/scripts/lib/deployment-findings.mjs +7 -0
- package/runtime/scripts/lib/json-clone.mjs +61 -0
- package/runtime/scripts/lib/list.mjs +4 -0
- package/runtime/scripts/lib/roots.mjs +59 -0
- package/runtime/scripts/lib/runtime-example.mjs +13 -0
- package/runtime/scripts/lib/secret-patterns.mjs +60 -5
- package/runtime/scripts/lib/sql-ast.mjs +32 -0
- package/runtime/scripts/lib/stack-services.mjs +1 -1
- package/runtime/scripts/lib/swarm-stack.mjs +126 -0
- package/runtime/scripts/lib/trim-end.mjs +8 -0
- package/runtime/scripts/lib/ts-analysis.mjs +23 -0
- package/runtime/scripts/lib/ts-ast.mjs +27 -2
- package/runtime/scripts/lib/workflow-events.mjs +8 -0
- package/scaffold/add-app.mjs +77 -59
- package/scaffold/add-cli-lite.mjs +24 -17
- package/scaffold/add-compose.mjs +79 -0
- package/scaffold/add-database-lite.mjs +14 -0
- package/scaffold/add-fe-overlay.mjs +122 -0
- package/scaffold/add-fe-page.mjs +269 -0
- package/scaffold/add-inbox-lite.mjs +87 -124
- package/scaffold/add-plan.mjs +183 -0
- package/scaffold/add-table.mjs +132 -70
- package/scaffold/add.mjs +186 -204
- package/scaffold/app.mjs +128 -43
- package/scaffold/edition-gate.mjs +3 -3
- package/scaffold/image.mjs +2 -2
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +2 -2
- package/src/main.mjs +238 -119
- package/stack/age-install.mjs +37 -0
- package/stack/backup.mjs +100 -0
- package/stack/build-args.mjs +42 -0
- package/stack/build.mjs +173 -0
- package/stack/cli.mjs +130 -0
- package/stack/config.mjs +57 -0
- package/stack/context.mjs +57 -0
- package/stack/converge.mjs +107 -0
- package/stack/deploy.mjs +301 -0
- package/stack/digest.mjs +16 -0
- package/stack/docker-transport.mjs +53 -0
- package/stack/edge-alias.mjs +118 -0
- package/stack/errors.mjs +21 -0
- package/stack/image-scan.mjs +152 -0
- package/stack/ingress-create.mjs +114 -0
- package/stack/ingress-publish.mjs +273 -0
- package/stack/ingress.mjs +706 -0
- package/stack/jobs.mjs +51 -0
- package/stack/live-restore.mjs +177 -0
- package/stack/prepare.mjs +266 -0
- package/stack/private-file.mjs +22 -0
- package/stack/provision.mjs +90 -0
- package/stack/prune.mjs +46 -0
- package/stack/release-store.mjs +87 -0
- package/stack/remote.mjs +205 -0
- package/stack/render.mjs +149 -0
- package/stack/retire.mjs +47 -0
- package/stack/sealed.mjs +149 -0
- package/stack/task.mjs +42 -0
- package/stack/verify.mjs +99 -0
- package/sync/ci-commands.mjs +13 -0
- package/sync/index.mjs +127 -64
- package/sync/stack-images.mjs +127 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
- package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/codegen-graphql.mjs.partial +13 -2
- package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
- package/templates/app/codegen-prelude.mjs.partial +8 -0
- package/templates/app/gitignore +4 -0
- package/templates/app/gitignore-lite +4 -0
- package/templates/app/hooks/husky/pre-commit +7 -12
- package/templates/app/package-scripts/package.json +1 -0
- package/templates/app/package-scripts/supabase.json +7 -0
- package/templates/app/package-scripts-lite/package.json +1 -7
- package/templates/app/skeleton/.editorconfig +1 -0
- package/templates/app/skeleton/.gitattributes +7 -1
- package/templates/app/skeleton/.github/dependabot.yml +17 -0
- package/templates/app/skeleton/.nvmrc +1 -1
- package/templates/app/skeleton/.sops.yaml +1 -1
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
- package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
- package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
- package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
- package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
- package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/README.md +6 -6
- package/templates/app/skeleton/scripts/codegen.mjs +1 -2
- package/templates/app/skeleton-lite/.editorconfig +16 -0
- package/templates/app/skeleton-lite/.gitattributes +8 -0
- package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
- package/templates/app/skeleton-lite/.nvmrc +1 -0
- package/templates/app/skeleton-lite/.sops.yaml +3 -2
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +193 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
- package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
- package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
- package/templates/app/skeleton-lite/.starciwork/workspace.yaml +9 -0
- package/templates/app/skeleton-lite/README.md +8 -4
- package/templates/app/skeleton-lite/supabase/config.toml +8 -1
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
- package/templates/app/starciwork.gitignore-lite +6 -0
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/inbox/database-client.spec.ts.tpl +22 -28
- package/templates/be/inbox/database-client.ts.tpl +14 -28
- package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
- package/templates/be/inbox/event-index.ts.tpl +1 -0
- package/templates/be/inbox/events.json.tpl +10 -0
- package/templates/be/inbox/migration.sql.tpl +1 -0
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
- package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
- package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
- package/templates/be/inbox/platform/migration.sql.tpl +48 -0
- package/templates/be/inbox/service.spec.ts.tpl +49 -20
- package/templates/be/inbox/service.ts.tpl +27 -16
- package/templates/be/inbox/sql.ts.tpl +2 -2
- package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -3
- package/templates/be/inbox/webhook-door.ts.tpl +4 -3
- package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
- package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
- package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
- package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
- package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
- package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
- package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
- package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
- package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
- package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
- package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
- package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
- package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
- package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
- package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
- package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
- package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
- package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
- package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
- package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
- package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
- package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +3 -3
- package/templates/be/patterns/webhooks/webhook.door.ts.tpl +3 -3
- package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
- package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
- package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
- package/templates/be/skeleton/apps/__app__/src/__app__.env.ts +21 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +1 -1
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +15 -45
- package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +44 -0
- package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
- package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/main.ts +18 -10
- package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
- package/templates/be/skeleton/apps/cli/src/cli.env.ts +13 -0
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +7 -17
- package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
- package/templates/be/skeleton/apps/cli/src/main.ts +3 -2
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
- package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
- package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
- package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
- package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
- package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
- package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +6 -6
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
- package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
- package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
- package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
- package/templates/be/skeleton-lite/apps/api/src/api.env.ts +25 -0
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +1 -1
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +18 -58
- package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +47 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/main.ts +5 -13
- package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +17 -0
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
- package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
- package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/feature.module.ts.tpl +2 -1
- package/templates/be/table/api/feature.options.ts.tpl +2 -0
- package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/graphql.module.ts.tpl +3 -2
- package/templates/be/table/api/graphql.options.ts.tpl +2 -0
- package/templates/be/table/migration.sql.tpl +1 -0
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
- package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
- package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
- package/templates/fe/auth/supabase-client.ts.tpl +34 -0
- package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
- package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
- package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
- package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
- package/upgrade/check.mjs +3 -1
- package/upgrade/file-state.mjs +68 -0
- package/upgrade/index.mjs +58 -94
- package/runtime/scripts/api/git/log.mjs +0 -5
- package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +0 -55
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// secret-init.mjs — initialize the public app rule and an age identity outside Git.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseYaml, stringifyYaml } from "../../engine/yaml.mjs";
|
|
5
|
+
import { secretEnv } from "../../engine/secrets.mjs";
|
|
6
|
+
import { resolveSops } from "../api/sops/resolve-sops.mjs";
|
|
7
|
+
import { selectedAgeRecipient } from "../api/sops/selected-age-recipient.mjs";
|
|
8
|
+
import { resolveRealTool } from "../api/process/resolve-real-tool.mjs";
|
|
9
|
+
import { runProgram } from "../api/process/run-program.mjs";
|
|
10
|
+
import { claimFile } from "../api/fs/claim-file.mjs";
|
|
11
|
+
import { generateAppIdentity } from "./secret-bootstrap-crypto.mjs";
|
|
12
|
+
import { materializeDeclaredPlaceholders } from "./sealed-members.mjs";
|
|
13
|
+
import { defaultSops } from "./secret-sops.mjs";
|
|
14
|
+
import {
|
|
15
|
+
outsideRepository,
|
|
16
|
+
privateDirectory,
|
|
17
|
+
secretRoots,
|
|
18
|
+
} from "./secret-files.mjs";
|
|
19
|
+
|
|
20
|
+
const PATH_RULE = String.raw`\.starcistacks[/\\][^/\\]+[/\\]secrets[/\\][^/\\]+\.enc$`;
|
|
21
|
+
const AGE_IDENTITY_NAME = /^SOPS_AGE_KEY(?:_FILE)?$/i;
|
|
22
|
+
|
|
23
|
+
/** Bootstrap a new app with its own age identity; only its public recipient leaves this owner. */
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
/** Bootstrap a new app with its own age identity; only the public recipient leaves this owner. */
|
|
27
|
+
export function initializeAppIdentity(repoRoot, { write, tools } = {}) {
|
|
28
|
+
const privateDirectory = path.join(repoRoot, ".claude");
|
|
29
|
+
const privateFile = path.join(privateDirectory, "secret.env");
|
|
30
|
+
const directoryStat = fs.lstatSync(privateDirectory, { throwIfNoEntry: false });
|
|
31
|
+
if (directoryStat && (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()))
|
|
32
|
+
throw new Error("the app private identity directory must be an ordinary directory");
|
|
33
|
+
const privateStat = fs.lstatSync(privateFile, { throwIfNoEntry: false });
|
|
34
|
+
if (privateStat && (!privateStat.isFile() || privateStat.isSymbolicLink() || privateStat.nlink !== 1))
|
|
35
|
+
throw new Error("the app private identity must be an ordinary unlinked file");
|
|
36
|
+
let local;
|
|
37
|
+
try { local = privateStat ? (tools?.secretEnv ?? secretEnv)(privateDirectory, {}) : {}; }
|
|
38
|
+
catch { throw new Error("the app private environment could not be loaded safely"); }
|
|
39
|
+
if (Object.keys(local ?? {}).some((name) => AGE_IDENTITY_NAME.test(name)))
|
|
40
|
+
throw new Error("the app already has a private age identity; initialization must not rotate it");
|
|
41
|
+
const config = readInitializationConfig(path.join(repoRoot, ".sops.yaml"));
|
|
42
|
+
if ((config.creation_rules ?? []).some((rule) => rule?.age))
|
|
43
|
+
throw new Error("the app already has an age recipient; initialization must not rotate it");
|
|
44
|
+
const publish = write ?? ((file, content) => {
|
|
45
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
46
|
+
const append = file === privateFile || file === path.join(privateDirectory, ".gitignore");
|
|
47
|
+
fs.writeFileSync(file, content, { mode: 0o600, flag: append ? "a" : "w" });
|
|
48
|
+
});
|
|
49
|
+
const generated = generateAppIdentity();
|
|
50
|
+
let privateContent;
|
|
51
|
+
try {
|
|
52
|
+
const prefix = privateStat ? "\nSOPS_AGE_KEY=" : "SOPS_AGE_KEY=";
|
|
53
|
+
privateContent = Buffer.concat([Buffer.from(prefix), generated.identity, Buffer.from("\n")]);
|
|
54
|
+
publish(path.join(privateDirectory, ".gitignore"), "\nsecret.env\n");
|
|
55
|
+
publish(privateFile, privateContent);
|
|
56
|
+
if (process.platform !== "win32") fs.chmodSync(privateFile, 0o600);
|
|
57
|
+
config.creation_rules = [{ path_regex: PATH_RULE, age: generated.recipient }];
|
|
58
|
+
publish(path.join(repoRoot, ".sops.yaml"), stringifyYaml(config));
|
|
59
|
+
return { recipient: generated.recipient, files: [".claude/.gitignore", ".claude/secret.env", ".sops.yaml"] };
|
|
60
|
+
} finally {
|
|
61
|
+
privateContent?.fill(0);
|
|
62
|
+
generated.identity.fill(0);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Load host-local age selections below the explicit environment, without copying any other credentials. */
|
|
67
|
+
export function ageIdentityEnv(repoRoot, env, tools) {
|
|
68
|
+
const load = tools?.secretEnv ?? secretEnv;
|
|
69
|
+
let resolved = {};
|
|
70
|
+
try {
|
|
71
|
+
resolved = load(path.join(repoRoot, ".claude"), env) ?? {};
|
|
72
|
+
} catch {
|
|
73
|
+
return { ...env };
|
|
74
|
+
}
|
|
75
|
+
const own = new Set(Object.keys(env).map((name) => name.toUpperCase()));
|
|
76
|
+
const local = Object.fromEntries(
|
|
77
|
+
Object.entries(resolved).filter(
|
|
78
|
+
([name]) => AGE_IDENTITY_NAME.test(name) && !own.has(name.toUpperCase()),
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
return { ...env, ...local };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Select the first matching public age rule, including for SOPS's isolated CI identity path. */
|
|
85
|
+
export function configuredRecipients(repoRoot, file) {
|
|
86
|
+
let config;
|
|
87
|
+
try {
|
|
88
|
+
config = parseYaml(
|
|
89
|
+
fs.readFileSync(path.join(repoRoot, ".sops.yaml"), "utf8"),
|
|
90
|
+
);
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error(
|
|
93
|
+
"a valid .sops.yaml is required; run starci app secret init",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const relative = path.relative(repoRoot, file).split(path.sep).join("/");
|
|
97
|
+
if (!Array.isArray(config?.creation_rules))
|
|
98
|
+
throw new Error(
|
|
99
|
+
".sops.yaml requires creation_rules; run starci app secret init",
|
|
100
|
+
);
|
|
101
|
+
const rule = config.creation_rules.find((entry) => {
|
|
102
|
+
try {
|
|
103
|
+
return (
|
|
104
|
+
!entry.path_regex ||
|
|
105
|
+
new RegExp(entry.path_regex).test(relative) ||
|
|
106
|
+
new RegExp(entry.path_regex).test(relative.replaceAll("/", "\\"))
|
|
107
|
+
);
|
|
108
|
+
} catch {
|
|
109
|
+
throw new Error(".sops.yaml has an invalid path_regex");
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
const recipients =
|
|
113
|
+
typeof rule?.age === "string"
|
|
114
|
+
? rule.age.split(",").map((value) => value.trim())
|
|
115
|
+
: [];
|
|
116
|
+
if (
|
|
117
|
+
!recipients.length ||
|
|
118
|
+
recipients.some((value) => !/^age1[a-z0-9]+$/.test(value))
|
|
119
|
+
)
|
|
120
|
+
throw new Error(
|
|
121
|
+
".sops.yaml must supply public age recipients for this secret; run starci app secret init",
|
|
122
|
+
);
|
|
123
|
+
return recipients;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Resolve required tools without exposing subprocess output (which can contain private data). */
|
|
127
|
+
export function secretTools(env, tools = {}) {
|
|
128
|
+
const sops = (tools.resolveSops ?? resolveSops)(env);
|
|
129
|
+
if (!sops)
|
|
130
|
+
throw new Error(
|
|
131
|
+
"sops is not installed; install SOPS and put sops on PATH (Windows: winget install Mozilla.SOPS)",
|
|
132
|
+
);
|
|
133
|
+
const resolve = tools.resolveRealTool ?? resolveRealTool;
|
|
134
|
+
const age = resolve("age", { env });
|
|
135
|
+
const keygen = resolve("age-keygen", { env });
|
|
136
|
+
if (!age || !keygen)
|
|
137
|
+
throw new Error(
|
|
138
|
+
"age or age-keygen is not installed; install age and put age and age-keygen on PATH",
|
|
139
|
+
);
|
|
140
|
+
return { sops, keygen, run: tools.runProgram ?? runProgram };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function readInitializationConfig(configFile) {
|
|
144
|
+
let config = {};
|
|
145
|
+
try {
|
|
146
|
+
if (fs.existsSync(configFile))
|
|
147
|
+
config = parseYaml(fs.readFileSync(configFile, "utf8")) ?? {};
|
|
148
|
+
} catch {
|
|
149
|
+
throw new Error(
|
|
150
|
+
".sops.yaml is invalid; repair it before initializing secrets",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (
|
|
154
|
+
typeof config !== "object" ||
|
|
155
|
+
Array.isArray(config) ||
|
|
156
|
+
(config.creation_rules !== undefined &&
|
|
157
|
+
!Array.isArray(config.creation_rules))
|
|
158
|
+
)
|
|
159
|
+
throw new Error(".sops.yaml must hold a creation_rules list");
|
|
160
|
+
return config;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function existingInitializationRule(rules) {
|
|
164
|
+
const canonical = rules.find(
|
|
165
|
+
(rule) =>
|
|
166
|
+
rule?.path_regex === PATH_RULE &&
|
|
167
|
+
typeof rule.age === "string" &&
|
|
168
|
+
rule.age.trim(),
|
|
169
|
+
);
|
|
170
|
+
return (
|
|
171
|
+
canonical ??
|
|
172
|
+
rules.find((rule) => {
|
|
173
|
+
if (typeof rule?.age !== "string" || !rule.age.trim()) return false;
|
|
174
|
+
try {
|
|
175
|
+
return (
|
|
176
|
+
!rule.path_regex ||
|
|
177
|
+
new RegExp(rule.path_regex).test(
|
|
178
|
+
".starcistacks/production/secrets/app.enc",
|
|
179
|
+
)
|
|
180
|
+
);
|
|
181
|
+
} catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function legacyInitializationRecipient(repoRoot, env, platform, tool) {
|
|
189
|
+
const identity = secretRoots(env, platform).identity;
|
|
190
|
+
outsideRepository(repoRoot, identity);
|
|
191
|
+
privateDirectory(repoRoot, path.dirname(identity));
|
|
192
|
+
const held = claimFile(identity);
|
|
193
|
+
if (!held.ok)
|
|
194
|
+
throw new Error(
|
|
195
|
+
"the age identity is being initialized; retry after that operation settles",
|
|
196
|
+
);
|
|
197
|
+
try {
|
|
198
|
+
if (!fs.existsSync(identity)) {
|
|
199
|
+
const generated = tool.run(tool.keygen, ["-o", identity], {
|
|
200
|
+
env,
|
|
201
|
+
cwd: repoRoot,
|
|
202
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
203
|
+
});
|
|
204
|
+
if (generated.error || generated.status !== 0)
|
|
205
|
+
throw new Error(
|
|
206
|
+
"age-keygen could not create the private identity outside the repository",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const stat = fs.lstatSync(identity);
|
|
210
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1)
|
|
211
|
+
throw new Error("the age identity must be an ordinary private file");
|
|
212
|
+
if (process.platform !== "win32") fs.chmodSync(identity, 0o600);
|
|
213
|
+
const publicKey = tool.run(tool.keygen, ["-y", identity], {
|
|
214
|
+
env,
|
|
215
|
+
cwd: repoRoot,
|
|
216
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
217
|
+
});
|
|
218
|
+
if (publicKey.error || publicKey.status !== 0)
|
|
219
|
+
throw new Error(
|
|
220
|
+
"age-keygen could not read the public recipient from the existing identity",
|
|
221
|
+
);
|
|
222
|
+
const recipients = String(publicKey.stdout ?? "")
|
|
223
|
+
.trim()
|
|
224
|
+
.split(/\s+/);
|
|
225
|
+
if (
|
|
226
|
+
!recipients.length ||
|
|
227
|
+
recipients.some((value) => !/^age1[a-z0-9]+$/.test(value))
|
|
228
|
+
)
|
|
229
|
+
throw new Error("age-keygen returned an invalid public recipient");
|
|
230
|
+
return recipients.join(",");
|
|
231
|
+
} finally {
|
|
232
|
+
held.release();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function initializationRecipient(repoRoot, env, platform, tool, tools) {
|
|
237
|
+
const selectedEnv = ageIdentityEnv(repoRoot, env, tools);
|
|
238
|
+
if (!Object.keys(selectedEnv).some((name) => /^SOPS_AGE_KEY$/i.test(name)))
|
|
239
|
+
return legacyInitializationRecipient(repoRoot, env, platform, tool);
|
|
240
|
+
const result = selectedAgeRecipient({
|
|
241
|
+
env: selectedEnv,
|
|
242
|
+
cwd: repoRoot,
|
|
243
|
+
invocation: {
|
|
244
|
+
runProgram: tool.run,
|
|
245
|
+
resolveRealTool: tools?.resolveRealTool ?? resolveRealTool,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
if (!result.ok)
|
|
249
|
+
throw new Error(`SOPS_AGE_KEY public recipient could not be derived (${result.reason})`);
|
|
250
|
+
return result.publicRecipient;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Reuse an existing matching age rule; initialization must not rotate an app's recipient. */
|
|
254
|
+
export function initializeSecrets(repoRoot, { env, platform, tools, stdout }) {
|
|
255
|
+
const configFile = path.join(repoRoot, ".sops.yaml");
|
|
256
|
+
const config = readInitializationConfig(configFile);
|
|
257
|
+
const rules = config.creation_rules ?? [];
|
|
258
|
+
const existing = existingInitializationRule(rules);
|
|
259
|
+
if (
|
|
260
|
+
existing?.age
|
|
261
|
+
.split(",")
|
|
262
|
+
.some((value) => !/^age1[a-z0-9]+$/.test(value.trim()))
|
|
263
|
+
)
|
|
264
|
+
throw new Error(
|
|
265
|
+
".sops.yaml must name valid public age recipients; initialization never prints private key material",
|
|
266
|
+
);
|
|
267
|
+
const localEnv = ageIdentityEnv(repoRoot, {}, tools);
|
|
268
|
+
const selectedEnv = ageIdentityEnv(repoRoot, existing ? env : { ...env, ...localEnv }, tools);
|
|
269
|
+
if (!existing && !Object.keys(localEnv).some((name) => /^SOPS_AGE_KEY$/i.test(name))) {
|
|
270
|
+
const initialized = initializeAppIdentity(repoRoot, { tools });
|
|
271
|
+
materializeDeclaredPlaceholders(repoRoot, { recipient: initialized.recipient, processEnv: selectedEnv });
|
|
272
|
+
stdout('secrets initialized; public recipient: ' + initialized.recipient + '\n');
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
const tool = secretTools(env, tools);
|
|
276
|
+
const inline = Object.keys(selectedEnv).some((name) => /^SOPS_AGE_KEY$/i.test(name));
|
|
277
|
+
const derived = existing && !inline ? existing.age : initializationRecipient(repoRoot, selectedEnv, platform, tool, tools);
|
|
278
|
+
const recipient = existing?.age ?? derived;
|
|
279
|
+
const exact = rules.findIndex((rule) => rule?.path_regex === PATH_RULE);
|
|
280
|
+
if (exact >= 0) rules[exact] = { ...rules[exact], age: recipient };
|
|
281
|
+
// SOPS uses the first matching rule: keep any existing environment-specific recipients in force.
|
|
282
|
+
else rules.push({ path_regex: PATH_RULE, age: recipient });
|
|
283
|
+
config.creation_rules = rules;
|
|
284
|
+
fs.writeFileSync(
|
|
285
|
+
configFile,
|
|
286
|
+
`# Initialized by starci app secret init; private age identities stay outside Git.\n${stringifyYaml(config)}`,
|
|
287
|
+
);
|
|
288
|
+
materializeDeclaredPlaceholders(repoRoot, {
|
|
289
|
+
recipient,
|
|
290
|
+
recipientsForFile: (file) => configuredRecipients(repoRoot, file),
|
|
291
|
+
processEnv: selectedEnv,
|
|
292
|
+
sops: { seal: (request) => defaultSops(selectedEnv, repoRoot, platform, tools).seal(request) },
|
|
293
|
+
});
|
|
294
|
+
stdout(`secrets initialized; public recipient: ${recipient}\n`);
|
|
295
|
+
if (existing)
|
|
296
|
+
stdout(
|
|
297
|
+
"existing app recipient retained; use its matching age identity for decryption\n",
|
|
298
|
+
);
|
|
299
|
+
return 0;
|
|
300
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// secret-output.mjs - list and materialize secret names and private KEY files.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { byCodeUnit } from "../lib/list.mjs";
|
|
5
|
+
import { materializedDirectory, writeSecretFile } from "./secret-files.mjs";
|
|
6
|
+
import { envelopeOf, validateKey } from "./secret-document.mjs";
|
|
7
|
+
import { fileOf, readMap, sealedFiles } from "./secret-store.mjs";
|
|
8
|
+
import { SecretError, publicStorage } from "./secret-errors.mjs";
|
|
9
|
+
|
|
10
|
+
function validateWindowsKeys(map) {
|
|
11
|
+
const names = new Set();
|
|
12
|
+
for (const key of Object.keys(map)) {
|
|
13
|
+
const normalized = key.toUpperCase();
|
|
14
|
+
if (names.has(normalized))
|
|
15
|
+
throw new SecretError(
|
|
16
|
+
"KEY names differing only by case cannot be materialized on Windows",
|
|
17
|
+
);
|
|
18
|
+
if (/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/.test(normalized))
|
|
19
|
+
throw new SecretError(
|
|
20
|
+
"a KEY uses a reserved Windows file name and cannot be materialized",
|
|
21
|
+
);
|
|
22
|
+
names.add(normalized);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function exportValues(
|
|
27
|
+
map,
|
|
28
|
+
{
|
|
29
|
+
repoRoot,
|
|
30
|
+
environment,
|
|
31
|
+
env,
|
|
32
|
+
platform,
|
|
33
|
+
stdout,
|
|
34
|
+
fileOutput = stdout,
|
|
35
|
+
reveal = false,
|
|
36
|
+
toFile = true,
|
|
37
|
+
},
|
|
38
|
+
) {
|
|
39
|
+
if (toFile && platform === "win32") validateWindowsKeys(map);
|
|
40
|
+
const directory = toFile
|
|
41
|
+
? publicStorage(() =>
|
|
42
|
+
materializedDirectory(repoRoot, environment, env, platform),
|
|
43
|
+
)
|
|
44
|
+
: null;
|
|
45
|
+
for (const key of Object.keys(map).sort(byCodeUnit)) {
|
|
46
|
+
if (toFile) {
|
|
47
|
+
const file = publicStorage(() =>
|
|
48
|
+
writeSecretFile(directory, key, map[key]),
|
|
49
|
+
);
|
|
50
|
+
fileOutput(`${key}_FILE=${file}\n`);
|
|
51
|
+
}
|
|
52
|
+
if (reveal) stdout(`${key}=${map[key]}\n`);
|
|
53
|
+
}
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function listSecrets(directory, stdout) {
|
|
58
|
+
const keys = new Set();
|
|
59
|
+
for (const name of sealedFiles(directory)) {
|
|
60
|
+
for (const item of envelopeOf(
|
|
61
|
+
fs.readFileSync(path.join(directory, name), "utf8"),
|
|
62
|
+
).keys) {
|
|
63
|
+
validateKey(item);
|
|
64
|
+
keys.add(item);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const item of [...keys].sort(byCodeUnit)) stdout(`${item}\n`);
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function materializeSecrets(directory, sops, output) {
|
|
72
|
+
const map = Object.create(null);
|
|
73
|
+
for (const name of sealedFiles(directory)) {
|
|
74
|
+
for (const [item, value] of Object.entries(
|
|
75
|
+
readMap(fileOf(directory, name.slice(0, -4)), sops),
|
|
76
|
+
)) {
|
|
77
|
+
if (Object.hasOwn(map, item))
|
|
78
|
+
throw new SecretError(
|
|
79
|
+
`duplicate KEY ${item} across secret slugs; each materialized KEY needs one owner`,
|
|
80
|
+
);
|
|
81
|
+
map[item] = value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return exportValues(map, output);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getSecret(file, key, sops, opts, output, stderr) {
|
|
88
|
+
let map = readMap(file, sops);
|
|
89
|
+
if (key !== undefined) {
|
|
90
|
+
if (!Object.hasOwn(map, key))
|
|
91
|
+
throw new SecretError(`the secret has no KEY ${key}`);
|
|
92
|
+
map = { [key]: map[key] };
|
|
93
|
+
}
|
|
94
|
+
return exportValues(map, {
|
|
95
|
+
...output,
|
|
96
|
+
fileOutput: stderr,
|
|
97
|
+
reveal: opts.reveal,
|
|
98
|
+
toFile: opts["to-file"] === true,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// secret-sops.mjs - runtime SOPS adapter and age identity selection.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { runProgram } from "../api/process/run-program.mjs";
|
|
5
|
+
import { resolveRealTool } from "../api/process/resolve-real-tool.mjs";
|
|
6
|
+
import { decrypt as sopsDecrypt } from "../api/sops/decrypt.mjs";
|
|
7
|
+
import { seal as sopsSeal } from "../api/sops/seal.mjs";
|
|
8
|
+
import { invocationDir } from "../lib/roots.mjs";
|
|
9
|
+
import { envelopeOf } from "./secret-document.mjs";
|
|
10
|
+
import { ageIdentityEnv, configuredRecipients, secretTools } from "./secret-init.mjs";
|
|
11
|
+
import { outsideRepository, secretRoots } from "./secret-files.mjs";
|
|
12
|
+
import { SecretError, publicStorage } from "./secret-errors.mjs";
|
|
13
|
+
|
|
14
|
+
const sopsInvocation = Object.freeze({ runProgram, resolveRealTool });
|
|
15
|
+
const AGE_IDENTITY_NAME = /^SOPS_AGE_KEY(?:_FILE)?$/i;
|
|
16
|
+
|
|
17
|
+
/** The default sops seam: the runtime's sops api, which finds the binary on this machine. */
|
|
18
|
+
export function defaultSops(
|
|
19
|
+
env = process.env,
|
|
20
|
+
repoRoot = invocationDir(),
|
|
21
|
+
platform = process.platform,
|
|
22
|
+
tools,
|
|
23
|
+
) {
|
|
24
|
+
const { sops: binary } = publicStorage(() => secretTools(env, tools));
|
|
25
|
+
const selectedEnv = ageIdentityEnv(repoRoot, env, tools);
|
|
26
|
+
const invocation = {
|
|
27
|
+
runProgram: tools?.runProgram ?? sopsInvocation.runProgram,
|
|
28
|
+
resolveRealTool: tools?.resolveRealTool ?? sopsInvocation.resolveRealTool,
|
|
29
|
+
};
|
|
30
|
+
const checkIdentity = (file) =>
|
|
31
|
+
publicStorage(() => {
|
|
32
|
+
outsideRepository(repoRoot, file);
|
|
33
|
+
if (fs.existsSync(file)) {
|
|
34
|
+
const stat = fs.lstatSync(file);
|
|
35
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1)
|
|
36
|
+
throw new Error(
|
|
37
|
+
"the age identity must be an ordinary private file without hard links",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
for (const [name, value] of Object.entries(selectedEnv))
|
|
42
|
+
if (/^SOPS_AGE_KEY_FILE$/i.test(name) && value) {
|
|
43
|
+
if (!path.isAbsolute(value))
|
|
44
|
+
throw new SecretError(
|
|
45
|
+
"SOPS_AGE_KEY_FILE must be an absolute path outside the repository",
|
|
46
|
+
);
|
|
47
|
+
checkIdentity(value);
|
|
48
|
+
}
|
|
49
|
+
if (
|
|
50
|
+
!Object.keys(selectedEnv).some((name) => AGE_IDENTITY_NAME.test(name))
|
|
51
|
+
) {
|
|
52
|
+
const identity = secretRoots(env, platform).identity;
|
|
53
|
+
checkIdentity(identity);
|
|
54
|
+
if (fs.existsSync(identity)) selectedEnv.SOPS_AGE_KEY_FILE = identity;
|
|
55
|
+
}
|
|
56
|
+
const refuse = (what, result) => {
|
|
57
|
+
if (result.error?.identityRefusal)
|
|
58
|
+
return Object.assign(new SecretError(result.error.message), {
|
|
59
|
+
identityRefusal: result.error.identityRefusal,
|
|
60
|
+
});
|
|
61
|
+
if (result.error?.code === "SOPS_MISSING")
|
|
62
|
+
return new SecretError(result.error.message);
|
|
63
|
+
return new SecretError(
|
|
64
|
+
`sops could not ${what} (exit ${String(result.status)})`,
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
decrypt(file, format) {
|
|
69
|
+
const result = sopsDecrypt(
|
|
70
|
+
binary,
|
|
71
|
+
["decrypt", "--input-type", format, "--output-type", "dotenv", file],
|
|
72
|
+
{ env: selectedEnv, cwd: repoRoot, invocation },
|
|
73
|
+
);
|
|
74
|
+
if (result.status !== 0)
|
|
75
|
+
throw refuse(
|
|
76
|
+
`decrypt ${path.basename(file)}; is the age identity installed?`,
|
|
77
|
+
result,
|
|
78
|
+
);
|
|
79
|
+
const map = Object.create(null);
|
|
80
|
+
for (const line of String(result.stdout).split(/\r?\n/u)) {
|
|
81
|
+
if (!line || line.startsWith("#")) continue;
|
|
82
|
+
const match = /^([A-Za-z_]\w*)=(.*)$/u.exec(line);
|
|
83
|
+
if (!match || Object.hasOwn(map, match[1]))
|
|
84
|
+
throw new SecretError("sops returned an invalid dotenv document");
|
|
85
|
+
map[match[1]] = match[2].replaceAll(String.raw`\n`, "\n");
|
|
86
|
+
}
|
|
87
|
+
return map;
|
|
88
|
+
},
|
|
89
|
+
/**
|
|
90
|
+
* The ssh key and known_hosts of the deploy custody. A classic sops BINARY envelope (one `data` property) yields the file itself;
|
|
91
|
+
* a JSON envelope under another property name (privateKey, knownHosts, ciPrivateKey) yields the JSON text of its properties,
|
|
92
|
+
* which the stack remote code unwraps by name.
|
|
93
|
+
*/
|
|
94
|
+
decryptBinary(file) {
|
|
95
|
+
const envelope = envelopeOf(fs.readFileSync(file, "utf8"));
|
|
96
|
+
const classic =
|
|
97
|
+
envelope.format === "json" &&
|
|
98
|
+
envelope.keys.length === 1 &&
|
|
99
|
+
envelope.keys[0] === "data";
|
|
100
|
+
const types = classic
|
|
101
|
+
? ["binary", "binary"]
|
|
102
|
+
: [envelope.format, "json"];
|
|
103
|
+
const result = sopsDecrypt(
|
|
104
|
+
binary,
|
|
105
|
+
["decrypt", "--input-type", types[0], "--output-type", types[1], file],
|
|
106
|
+
{ env: selectedEnv, cwd: repoRoot, invocation },
|
|
107
|
+
);
|
|
108
|
+
if (result.status !== 0)
|
|
109
|
+
throw refuse(
|
|
110
|
+
`decrypt ${path.basename(file)}; is the age identity installed?`,
|
|
111
|
+
result,
|
|
112
|
+
);
|
|
113
|
+
return String(result.stdout);
|
|
114
|
+
},
|
|
115
|
+
seal(request) {
|
|
116
|
+
const recipients = request.recipients.length
|
|
117
|
+
? request.recipients
|
|
118
|
+
: publicStorage(() =>
|
|
119
|
+
configuredRecipients(repoRoot, request.filenameOverride),
|
|
120
|
+
);
|
|
121
|
+
const result = sopsSeal(
|
|
122
|
+
binary,
|
|
123
|
+
{ ...request, recipients },
|
|
124
|
+
{ env: selectedEnv, cwd: repoRoot, invocation },
|
|
125
|
+
);
|
|
126
|
+
if (result.status !== 0) throw refuse("seal the secret", result);
|
|
127
|
+
return result.stdout;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// secret-store.mjs - sealed secret paths and atomic named-value updates.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { claimFile } from "../api/fs/claim-file.mjs";
|
|
6
|
+
import { isSopsEnvelope } from "../lib/sops-envelope.mjs";
|
|
7
|
+
import { byCodeUnit } from "../lib/list.mjs";
|
|
8
|
+
import {
|
|
9
|
+
envelopeOf,
|
|
10
|
+
plaintextOf,
|
|
11
|
+
validateKey,
|
|
12
|
+
validateMap,
|
|
13
|
+
} from "./secret-document.mjs";
|
|
14
|
+
import { defaultSops } from "./secret-sops.mjs";
|
|
15
|
+
import { SecretError } from "./secret-errors.mjs";
|
|
16
|
+
|
|
17
|
+
const SLUG = /^[a-z0-9][a-z0-9-]*$/u;
|
|
18
|
+
|
|
19
|
+
/** The directory of the sealed secrets of the app at `repoRoot` for `env`; the one env that has secrets when none is named. */
|
|
20
|
+
export function secretsDirectory(repoRoot, env) {
|
|
21
|
+
const stacks = path.join(repoRoot, ".starcistacks");
|
|
22
|
+
if (env) {
|
|
23
|
+
if (!/^[a-z0-9][a-z0-9-]*$/u.test(env))
|
|
24
|
+
throw new SecretError(`--env ${env} is not an environment name`);
|
|
25
|
+
return path.join(stacks, env, "secrets");
|
|
26
|
+
}
|
|
27
|
+
if (!fs.existsSync(stacks))
|
|
28
|
+
throw new SecretError(
|
|
29
|
+
"this app has no .starcistacks; run starci app secret at the app root, or pass --cwd",
|
|
30
|
+
);
|
|
31
|
+
const withSecrets = fs
|
|
32
|
+
.readdirSync(stacks, { withFileTypes: true })
|
|
33
|
+
.filter(
|
|
34
|
+
(entry) =>
|
|
35
|
+
entry.isDirectory() &&
|
|
36
|
+
fs.existsSync(path.join(stacks, entry.name, "secrets")),
|
|
37
|
+
)
|
|
38
|
+
.map((entry) => entry.name);
|
|
39
|
+
if (withSecrets.length === 1)
|
|
40
|
+
return path.join(stacks, withSecrets[0], "secrets");
|
|
41
|
+
throw new SecretError(
|
|
42
|
+
withSecrets.length === 0
|
|
43
|
+
? "no environment of .starcistacks has a secrets folder; pass --env to create the first secret"
|
|
44
|
+
: `more than one environment has secrets (${withSecrets.join(", ")}); pass --env`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const fileOf = (directory, slug) => {
|
|
49
|
+
if (!SLUG.test(slug ?? ""))
|
|
50
|
+
throw new SecretError(
|
|
51
|
+
`${slug === undefined ? "a secret name" : slug} is not a secret slug: lowercase letters, digits and dashes`,
|
|
52
|
+
);
|
|
53
|
+
return path.join(directory, `${slug}.enc`);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Build and validate a replacement envelope without publishing it. */
|
|
57
|
+
function sealSecretMap({ file, values, age, sops }) {
|
|
58
|
+
let map = Object.create(null);
|
|
59
|
+
let recipients = age;
|
|
60
|
+
if (fs.existsSync(file)) {
|
|
61
|
+
const envelope = envelopeOf(fs.readFileSync(file, "utf8"));
|
|
62
|
+
map = sops.decrypt(file, envelope.format);
|
|
63
|
+
if (!age.length) recipients = envelope.recipients;
|
|
64
|
+
}
|
|
65
|
+
validateMap(map);
|
|
66
|
+
map = { ...map, ...values };
|
|
67
|
+
const sealed = sops.seal({ inputType: "dotenv", plaintext: plaintextOf("dotenv", map), recipients, filenameOverride: file });
|
|
68
|
+
let parsed;
|
|
69
|
+
try { parsed = envelopeOf(sealed); }
|
|
70
|
+
catch { throw new SecretError("sops returned an invalid sealed document"); }
|
|
71
|
+
if (!isSopsEnvelope(sealed) || parsed.keys.length !== Object.keys(map).length || !parsed.keys.every((key) => Object.hasOwn(map, key)))
|
|
72
|
+
throw new SecretError("sops returned an invalid sealed document");
|
|
73
|
+
return sealed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Publish through an explicit file owner, or the canonical atomic replace. */
|
|
77
|
+
function publishSealed(file, sealed, write) {
|
|
78
|
+
if (write) { write(file, sealed); return; }
|
|
79
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
80
|
+
const temporary = path.join(path.dirname(file), "." + path.basename(file) + "-" + randomUUID() + ".tmp");
|
|
81
|
+
try {
|
|
82
|
+
fs.writeFileSync(temporary, sealed, { flag: "wx", mode: 0o600, flush: true });
|
|
83
|
+
fs.renameSync(temporary, file);
|
|
84
|
+
} finally {
|
|
85
|
+
if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function underClaim(file, action) {
|
|
90
|
+
const held = claimFile(path.resolve(file));
|
|
91
|
+
if (!held.ok)
|
|
92
|
+
throw new SecretError("the sealed secret is being updated; retry after that write settles");
|
|
93
|
+
try {
|
|
94
|
+
action();
|
|
95
|
+
} finally { held.release(); }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Replace one existing sealed member with an already validated envelope: same claim and atomic publish as set. */
|
|
99
|
+
export const replaceSealed = (file, sealed) => underClaim(file, () => publishSealed(file, sealed));
|
|
100
|
+
|
|
101
|
+
/** Delete one sealed member under the writer claim. */
|
|
102
|
+
export const deleteSealed = (file) => underClaim(file, () => fs.unlinkSync(file));
|
|
103
|
+
|
|
104
|
+
/** Seal named values; a supplied writer owns its own claim and rollback (the fresh-app scaffold). */
|
|
105
|
+
export function writeSecret({ file, values, age, sops, write }) {
|
|
106
|
+
for (const key of Object.keys(values)) validateKey(key);
|
|
107
|
+
const held = write ? { ok: true, release() {} } : claimFile(path.resolve(file));
|
|
108
|
+
if (!held.ok)
|
|
109
|
+
throw new SecretError("the sealed secret is being updated; retry after that write settles");
|
|
110
|
+
try {
|
|
111
|
+
publishSealed(file, sealSecretMap({ file, values, age, sops }), write);
|
|
112
|
+
} finally { held.release(); }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Update named keys through the same canonical sealed-secret owner used by the CLI. */
|
|
116
|
+
export function setSecretValues(
|
|
117
|
+
repoRoot,
|
|
118
|
+
{ slug, values, env = null },
|
|
119
|
+
{ sops = null, age = [], processEnv = process.env, write } = {},
|
|
120
|
+
) {
|
|
121
|
+
const directory = secretsDirectory(repoRoot, env);
|
|
122
|
+
const file = fileOf(directory, slug);
|
|
123
|
+
if (
|
|
124
|
+
!values ||
|
|
125
|
+
typeof values !== "object" ||
|
|
126
|
+
Array.isArray(values) ||
|
|
127
|
+
!Object.keys(values).length
|
|
128
|
+
)
|
|
129
|
+
throw new SecretError("secret update requires named values");
|
|
130
|
+
for (const value of Object.values(values))
|
|
131
|
+
if (typeof value !== "string" || value === "")
|
|
132
|
+
throw new SecretError("secret values must be nonempty strings");
|
|
133
|
+
writeSecret({
|
|
134
|
+
file,
|
|
135
|
+
values,
|
|
136
|
+
age,
|
|
137
|
+
sops: sops ?? defaultSops(processEnv, repoRoot),
|
|
138
|
+
write,
|
|
139
|
+
});
|
|
140
|
+
return { file, via: "sealed-secret" };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function sealedFiles(directory) {
|
|
144
|
+
return fs.existsSync(directory)
|
|
145
|
+
? fs
|
|
146
|
+
.readdirSync(directory)
|
|
147
|
+
.filter((name) => name.endsWith(".enc"))
|
|
148
|
+
.sort(byCodeUnit)
|
|
149
|
+
: [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function readMap(file, sops) {
|
|
153
|
+
if (!fs.existsSync(file))
|
|
154
|
+
throw new SecretError(
|
|
155
|
+
"the requested secret is not sealed in this environment",
|
|
156
|
+
);
|
|
157
|
+
const map = sops.decrypt(
|
|
158
|
+
file,
|
|
159
|
+
envelopeOf(fs.readFileSync(file, "utf8")).format,
|
|
160
|
+
);
|
|
161
|
+
validateMap(map);
|
|
162
|
+
return map;
|
|
163
|
+
}
|