@starci/hfs 4.3.0 → 4.4.1
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 +14 -0
- package/README.md +1 -1
- package/db/migrate.mjs +361 -0
- package/db/post-cutover.mjs +136 -0
- package/emit/contracts.mjs +2 -2
- package/emit/db-types.mjs +12 -3
- package/lint/run.mjs +3 -3
- package/package.json +4 -1
- package/release/gate.mjs +298 -0
- package/release/index.mjs +103 -0
- package/release/push.mjs +110 -0
- package/report/sonar.mjs +1 -1
- package/runtime/config.example.yaml +7 -6
- package/runtime/engine/config.mjs +10 -18
- package/runtime/engine/removed-vocabulary.mjs +0 -2
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
- package/runtime/knowledge/hfs/rules.yaml +2855 -163
- package/runtime/knowledge/hfs/slots.yaml +333 -62
- package/runtime/knowledge/patterns/be/api.yaml +32 -5
- package/runtime/knowledge/patterns/be/cli.yaml +14 -4
- 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 +23 -4
- package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +3091 -43
- package/runtime/modules/kernel/removed-vocabulary.yaml +30 -0
- package/runtime/modules/models/runtimes.yaml +5 -7
- package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
- package/runtime/packages/cli/src/catalog.generated.mjs +13769 -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/init.mjs +7 -0
- package/runtime/scripts/api/git/lib.mjs +4 -1
- 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 +42 -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 +1 -8
- package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
- package/runtime/scripts/hfs/architecture/config.mjs +1 -1
- package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
- package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
- 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/index.mjs +1 -1
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
- 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 +2 -2
- package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
- 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 +4 -4
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
- package/runtime/scripts/hfs/architecture.mjs +1 -1
- package/runtime/scripts/hfs/check-results.mjs +28 -0
- package/runtime/scripts/hfs/check.mjs +46 -47
- package/runtime/scripts/hfs/coverage-from.mjs +70 -0
- package/runtime/scripts/hfs/coverage-scope.mjs +1 -1
- 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 +69 -9
- package/runtime/scripts/hfs/param-names.mjs +1 -1
- package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
- package/runtime/scripts/hfs/rule-catalog.mjs +158 -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/cli.mjs +1 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +1 -1
- package/runtime/scripts/hfs/rules/contract.mjs +67 -6
- 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 +286 -7
- 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 +9 -0
- 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 +9 -8
- 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/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 +13 -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 +41 -1
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +2 -2
- 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-edit.mjs +170 -0
- package/runtime/scripts/hfs/secret-init.mjs +127 -19
- package/runtime/scripts/hfs/secret-sops.mjs +38 -6
- package/runtime/scripts/hfs/secret-store.mjs +56 -54
- package/runtime/scripts/hfs/secret.mjs +34 -5
- package/runtime/scripts/hfs/slot-imports.mjs +18 -5
- package/runtime/scripts/hfs/slots.mjs +3 -2
- 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/typescript-programs.mjs +0 -8
- 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/env.mjs +2 -2
- package/runtime/scripts/lib/git.mjs +1 -1
- package/runtime/scripts/lib/i18n.mjs +0 -2
- package/runtime/scripts/lib/json-clone.mjs +61 -0
- package/runtime/scripts/lib/language.mjs +1 -1
- package/runtime/scripts/lib/list.mjs +4 -0
- package/runtime/scripts/lib/roots.mjs +2 -4
- 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 +5 -18
- package/runtime/scripts/lib/trim-end.mjs +8 -0
- package/runtime/scripts/lib/ts-analysis.mjs +5 -2
- 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 +14 -6
- package/scaffold/add-compose.mjs +79 -0
- package/scaffold/add-database-lite.mjs +14 -0
- package/scaffold/add-fe-overlay.mjs +1 -0
- package/scaffold/add-fe-page.mjs +1 -0
- package/scaffold/add-inbox-lite.mjs +74 -42
- package/scaffold/add-table.mjs +110 -87
- package/scaffold/add.mjs +159 -44
- package/scaffold/app.mjs +124 -39
- package/scaffold/edition-gate.mjs +3 -3
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +2 -2
- package/scaffold/supabase-ports.mjs +1 -1
- package/src/main.mjs +229 -116
- 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/format.mjs +1 -1
- package/sync/hygiene.mjs +1 -1
- package/sync/index.mjs +110 -54
- 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 +3 -0
- package/templates/app/gitignore-lite +3 -0
- 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/.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 +178 -1
- 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/README.md +5 -3
- 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/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 +17 -18
- package/templates/be/inbox/database-client.ts.tpl +12 -11
- 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 +42 -9
- package/templates/be/inbox/service.ts.tpl +23 -8
- package/templates/be/inbox/webhook-door.spec.ts.tpl +1 -1
- package/templates/be/inbox/webhook-door.ts.tpl +2 -2
- 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.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.module.ts +11 -9
- 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 +2 -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 +15 -1
- package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -14
- 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/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/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/app.module.ts +14 -18
- 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 +2 -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 +2 -3
- 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/index.mjs +54 -41
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCipheriv, createHash, createHmac, createPrivateKey, createPublicKey,
|
|
3
|
+
diffieHellman, hkdfSync, randomBytes,
|
|
4
|
+
} from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
// Bootstrap only; interoperable with https://c2sp.org/age and getsops/sops.
|
|
7
|
+
// See SOPS aes/cipher.go, sops.go, stores/dotenv/store.go, stores/flatten.go.
|
|
8
|
+
const ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
|
|
9
|
+
const GENERATORS = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
|
10
|
+
const PRIVATE_PREFIX = Buffer.from('302e020100300506032b656e04220420', 'hex');
|
|
11
|
+
const PUBLIC_PREFIX = Buffer.from('302a300506032b656e032100', 'hex');
|
|
12
|
+
|
|
13
|
+
function checksumStep(checksum, value) {
|
|
14
|
+
const top = checksum >>> 25;
|
|
15
|
+
let next = ((checksum & 0x1ffffff) << 5) ^ value;
|
|
16
|
+
for (let bit = 0; bit < 5; bit += 1) {
|
|
17
|
+
if ((top >>> bit) & 1) next ^= GENERATORS[bit];
|
|
18
|
+
}
|
|
19
|
+
return next >>> 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function checksumWords(prefix, words) {
|
|
23
|
+
const codes = [...prefix].map((character) => character.codePointAt(0));
|
|
24
|
+
const expanded = [...codes.map((code) => code >>> 5), 0, ...codes.map((code) => code & 31)];
|
|
25
|
+
return [...expanded, ...words].reduce(checksumStep, 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function convertBits(input, from, to, pad) {
|
|
29
|
+
let accumulator = 0;
|
|
30
|
+
let bits = 0;
|
|
31
|
+
const result = [];
|
|
32
|
+
const mask = (1 << to) - 1;
|
|
33
|
+
for (const value of input) {
|
|
34
|
+
accumulator = ((accumulator << from) | value) & 0xffff;
|
|
35
|
+
bits += from;
|
|
36
|
+
while (bits >= to) {
|
|
37
|
+
bits -= to;
|
|
38
|
+
result.push((accumulator >>> bits) & mask);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (pad && bits) result.push((accumulator << (to - bits)) & mask);
|
|
42
|
+
if (!pad && (bits >= from || ((accumulator << (to - bits)) & mask))) {
|
|
43
|
+
throw new Error('Invalid age recipient encoding');
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function bech32Encode(prefix, bytes) {
|
|
49
|
+
const words = convertBits(bytes, 8, 5, true);
|
|
50
|
+
const checksum = checksumWords(prefix, [...words, 0, 0, 0, 0, 0, 0]) ^ 1;
|
|
51
|
+
const suffix = [];
|
|
52
|
+
for (let index = 0; index < 6; index += 1) suffix.push((checksum >>> (5 * (5 - index))) & 31);
|
|
53
|
+
return prefix + '1' + [...words, ...suffix].map((word) => ALPHABET[word]).join('');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function decodeRecipient(recipient) {
|
|
57
|
+
if (typeof recipient !== 'string' || !recipient.startsWith('age1') || recipient.length !== 62) {
|
|
58
|
+
throw new Error('Bootstrap requires an X25519 age recipient');
|
|
59
|
+
}
|
|
60
|
+
const words = Array.from(recipient.slice(4), (character) => ALPHABET.indexOf(character));
|
|
61
|
+
if (words.includes(-1) || checksumWords('age', words) !== 1) {
|
|
62
|
+
throw new Error('Invalid age recipient checksum');
|
|
63
|
+
}
|
|
64
|
+
const decoded = Buffer.from(convertBits(words.slice(0, -6), 5, 8, false));
|
|
65
|
+
if (decoded.length !== 32) throw new Error('Invalid age recipient length');
|
|
66
|
+
return decoded;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function privateKey(bytes) {
|
|
70
|
+
const encoded = Buffer.concat([PRIVATE_PREFIX, bytes]);
|
|
71
|
+
try {
|
|
72
|
+
return createPrivateKey({ key: encoded, type: 'pkcs8', format: 'der' });
|
|
73
|
+
} finally {
|
|
74
|
+
encoded.fill(0);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function publicBytes(key) {
|
|
79
|
+
return createPublicKey(key).export({ type: 'spki', format: 'der' }).subarray(-32);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Return a fresh app identity in memory. The caller owns custody and zeroing. */
|
|
83
|
+
export function generateAppIdentity() {
|
|
84
|
+
const seed = randomBytes(32);
|
|
85
|
+
try {
|
|
86
|
+
const recipient = bech32Encode('age', publicBytes(privateKey(seed)));
|
|
87
|
+
const identity = Buffer.from(bech32Encode('age-secret-key-', seed).toUpperCase());
|
|
88
|
+
return { identity, recipient };
|
|
89
|
+
} finally {
|
|
90
|
+
seed.fill(0);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function deriveKey(secret, salt, info) {
|
|
95
|
+
return Buffer.from(hkdfSync('sha256', secret, salt, info, 32));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function chachaSeal(plaintext, key, nonce) {
|
|
99
|
+
const cipher = createCipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 });
|
|
100
|
+
return Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function rawBase64(bytes) {
|
|
104
|
+
return bytes.toString('base64').replaceAll('=', '');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function wrapFileKey(fileKey, recipient) {
|
|
108
|
+
const seed = randomBytes(32);
|
|
109
|
+
const ephemeral = privateKey(seed);
|
|
110
|
+
seed.fill(0);
|
|
111
|
+
const share = publicBytes(ephemeral);
|
|
112
|
+
const peer = createPublicKey({ key: Buffer.concat([PUBLIC_PREFIX, recipient]), type: 'spki', format: 'der' });
|
|
113
|
+
const sharedSecret = diffieHellman({ privateKey: ephemeral, publicKey: peer });
|
|
114
|
+
const wrapKey = deriveKey(sharedSecret, Buffer.concat([share, recipient]), 'age-encryption.org/v1/X25519');
|
|
115
|
+
try {
|
|
116
|
+
const body = rawBase64(chachaSeal(fileKey, wrapKey, Buffer.alloc(12)));
|
|
117
|
+
return '-> X25519 ' + rawBase64(share) + '\n' + body + '\n';
|
|
118
|
+
} finally {
|
|
119
|
+
sharedSecret.fill(0);
|
|
120
|
+
wrapKey.fill(0);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function ageSealDataKey(dataKey, recipient) {
|
|
125
|
+
const fileKey = randomBytes(16);
|
|
126
|
+
const nonce = randomBytes(16);
|
|
127
|
+
const headerKey = deriveKey(fileKey, Buffer.alloc(0), 'header');
|
|
128
|
+
const payloadKey = deriveKey(fileKey, nonce, 'payload');
|
|
129
|
+
try {
|
|
130
|
+
const header = 'age-encryption.org/v1\n' + wrapFileKey(fileKey, recipient) + '---';
|
|
131
|
+
const mac = rawBase64(createHmac('sha256', headerKey).update(header).digest());
|
|
132
|
+
const chunkNonce = Buffer.alloc(12);
|
|
133
|
+
chunkNonce[11] = 1;
|
|
134
|
+
const ciphertext = chachaSeal(dataKey, payloadKey, chunkNonce);
|
|
135
|
+
return Buffer.concat([Buffer.from(header + ' ' + mac + '\n'), nonce, ciphertext]);
|
|
136
|
+
} finally {
|
|
137
|
+
fileKey.fill(0);
|
|
138
|
+
headerKey.fill(0);
|
|
139
|
+
payloadKey.fill(0);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function armor(bytes) {
|
|
144
|
+
const encoded = bytes.toString('base64');
|
|
145
|
+
const lines = [];
|
|
146
|
+
for (let offset = 0; offset < encoded.length; offset += 64) lines.push(encoded.slice(offset, offset + 64));
|
|
147
|
+
return ['-----BEGIN AGE ENCRYPTED FILE-----', ...lines, '-----END AGE ENCRYPTED FILE-----', ''].join('\n');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function sopsEncrypt(value, dataKey, additionalData) {
|
|
151
|
+
const iv = randomBytes(32);
|
|
152
|
+
const cipher = createCipheriv('aes-256-gcm', dataKey, iv);
|
|
153
|
+
cipher.setAAD(Buffer.from(additionalData));
|
|
154
|
+
const data = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
|
|
155
|
+
return 'ENC[AES256_GCM,data:' + data.toString('base64') + ',iv:' + iv.toString('base64')
|
|
156
|
+
+ ',tag:' + cipher.getAuthTag().toString('base64') + ',type:str]';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function bootstrapEntries(values) {
|
|
160
|
+
if (!values || typeof values !== 'object' || Array.isArray(values)) {
|
|
161
|
+
throw new Error('Bootstrap values must be a dotenv key/value object');
|
|
162
|
+
}
|
|
163
|
+
const entries = Object.entries(values);
|
|
164
|
+
if (!entries.length) throw new Error('Bootstrap requires at least one value');
|
|
165
|
+
for (const [name, value] of entries) {
|
|
166
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || name.startsWith('sops_') || name.endsWith('_unencrypted')) {
|
|
167
|
+
throw new Error('Bootstrap contains an invalid or reserved dotenv key');
|
|
168
|
+
}
|
|
169
|
+
if (typeof value !== 'string' || !value.length) throw new Error('Bootstrap values must be nonempty strings');
|
|
170
|
+
}
|
|
171
|
+
return entries;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Encrypt bootstrap values to the app's public key; no private key is needed. */
|
|
175
|
+
export function sealBootstrapValues({ values, recipient }) {
|
|
176
|
+
const entries = bootstrapEntries(values);
|
|
177
|
+
const recipientBytes = decodeRecipient(recipient);
|
|
178
|
+
const dataKey = randomBytes(32);
|
|
179
|
+
try {
|
|
180
|
+
const modified = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
181
|
+
const digest = createHash('sha512');
|
|
182
|
+
const lines = entries.map(([name, value]) => {
|
|
183
|
+
digest.update(value, 'utf8');
|
|
184
|
+
return name + '=' + sopsEncrypt(value, dataKey, name + ':');
|
|
185
|
+
});
|
|
186
|
+
const mac = sopsEncrypt(digest.digest('hex').toUpperCase(), dataKey, modified);
|
|
187
|
+
const wrapped = armor(ageSealDataKey(dataKey, recipientBytes)).replaceAll('\n', String.raw`\n`);
|
|
188
|
+
lines.push(
|
|
189
|
+
'sops_age__list_0__map_enc=' + wrapped,
|
|
190
|
+
'sops_age__list_0__map_recipient=' + recipient,
|
|
191
|
+
'sops_lastmodified=' + modified,
|
|
192
|
+
'sops_mac=' + mac,
|
|
193
|
+
'sops_unencrypted_suffix=_unencrypted',
|
|
194
|
+
'sops_version=3.9.4',
|
|
195
|
+
);
|
|
196
|
+
return lines.join('\n') + '\n';
|
|
197
|
+
} finally {
|
|
198
|
+
dataKey.fill(0);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// secret-edit.mjs - app secret unset|rename|remove: edit the key set of one sealed member without ever printing a value.
|
|
2
|
+
// Each verb is a dry run until --execute. The dry run reads only the envelope's public key names and recipients, so it needs no
|
|
3
|
+
// age identity. An executed edit decrypts the member in memory, re-seals it to the SAME recipients in the SAME sops format
|
|
4
|
+
// (the one exception is a binary `data` envelope, which a rename re-seals as a JSON envelope), checks the sealed result and
|
|
5
|
+
// publishes it atomically under the writer claim. Output names environments, members and keys only.
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import { isSopsEnvelope } from "../lib/sops-envelope.mjs";
|
|
8
|
+
import { envelopeOf, plaintextOf, validateKey, validateMap } from "./secret-document.mjs";
|
|
9
|
+
import { deleteSealed, fileOf, replaceSealed } from "./secret-store.mjs";
|
|
10
|
+
import { SecretError } from "./secret-errors.mjs";
|
|
11
|
+
|
|
12
|
+
const BINARY_PROPERTY = "data";
|
|
13
|
+
const NOT_PRESERVED = /\r|\\n/u;
|
|
14
|
+
|
|
15
|
+
const sameNames = (left, right) =>
|
|
16
|
+
left.length === right.length && left.every((name) => right.includes(name));
|
|
17
|
+
|
|
18
|
+
/** True for the sops binary store: a JSON envelope whose only property is `data` (the ssh key and known_hosts members). */
|
|
19
|
+
const isBinaryEnvelope = (envelope) =>
|
|
20
|
+
envelope.format === "json" &&
|
|
21
|
+
envelope.keys.length === 1 &&
|
|
22
|
+
envelope.keys[0] === BINARY_PROPERTY;
|
|
23
|
+
|
|
24
|
+
function inspectMember(file, label) {
|
|
25
|
+
let stat;
|
|
26
|
+
try {
|
|
27
|
+
stat = fs.lstatSync(file);
|
|
28
|
+
} catch {
|
|
29
|
+
throw new SecretError(`${label} is not a sealed member of this environment`);
|
|
30
|
+
}
|
|
31
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
32
|
+
throw new SecretError(`${label} must be an ordinary sealed file`);
|
|
33
|
+
const text = fs.readFileSync(file, "utf8");
|
|
34
|
+
if (!isSopsEnvelope(text))
|
|
35
|
+
throw new SecretError(`${label} is not a sops envelope`);
|
|
36
|
+
return envelopeOf(text);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function requireKey(envelope, key, label) {
|
|
40
|
+
if (!envelope.keys.includes(key))
|
|
41
|
+
throw new SecretError(`${label} has no KEY ${key}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Decrypt the member in memory and check that it holds exactly the keys its envelope declares. */
|
|
45
|
+
function openMap(file, envelope, sops) {
|
|
46
|
+
const map = sops.decrypt(file, envelope.format);
|
|
47
|
+
validateMap(map);
|
|
48
|
+
if (!sameNames(Object.keys(map), envelope.keys))
|
|
49
|
+
throw new SecretError("sops returned keys that differ from the envelope");
|
|
50
|
+
if (
|
|
51
|
+
envelope.format !== "dotenv" &&
|
|
52
|
+
Object.values(map).some((value) => NOT_PRESERVED.test(value))
|
|
53
|
+
)
|
|
54
|
+
throw new SecretError(
|
|
55
|
+
"a value holds a carriage return or literal backslash-n that this edit cannot preserve losslessly",
|
|
56
|
+
);
|
|
57
|
+
return map;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Seal the replacement map to the original recipients and prove the result before anything is published. */
|
|
61
|
+
function sealReplacement({ file, format, map, envelope, sops }) {
|
|
62
|
+
const sealed = sops.seal({
|
|
63
|
+
inputType: format,
|
|
64
|
+
plaintext: plaintextOf(format, map),
|
|
65
|
+
recipients: envelope.recipients,
|
|
66
|
+
filenameOverride: file,
|
|
67
|
+
});
|
|
68
|
+
let parsed;
|
|
69
|
+
try {
|
|
70
|
+
parsed = envelopeOf(sealed);
|
|
71
|
+
} catch {
|
|
72
|
+
throw new SecretError("sops returned an invalid sealed document");
|
|
73
|
+
}
|
|
74
|
+
const recipientsKept =
|
|
75
|
+
!envelope.recipients.length ||
|
|
76
|
+
sameNames(parsed.recipients, envelope.recipients);
|
|
77
|
+
if (
|
|
78
|
+
!isSopsEnvelope(sealed) ||
|
|
79
|
+
parsed.format !== format ||
|
|
80
|
+
!sameNames(parsed.keys, Object.keys(map))
|
|
81
|
+
)
|
|
82
|
+
throw new SecretError("sops returned an invalid sealed document");
|
|
83
|
+
if (!recipientsKept)
|
|
84
|
+
throw new SecretError("sops returned a document sealed to different recipients");
|
|
85
|
+
return sealed;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const keyCount = (count) => `${count} key${count === 1 ? "" : "s"}`;
|
|
89
|
+
|
|
90
|
+
function unsetKey({ file, label, key, execute, sops, stdout }) {
|
|
91
|
+
const envelope = inspectMember(file, label);
|
|
92
|
+
requireKey(envelope, key, label);
|
|
93
|
+
if (envelope.keys.length === 1)
|
|
94
|
+
throw new SecretError(
|
|
95
|
+
`${key} is the last KEY of ${label}; a sealed member cannot be empty, use remove ${label.replace("/", " ")} --force-nonempty to delete it`,
|
|
96
|
+
);
|
|
97
|
+
const remaining = envelope.keys.length - 1;
|
|
98
|
+
if (!execute) {
|
|
99
|
+
stdout(`dry run: would unset ${key} from ${label} (${keyCount(remaining)} remain); add --execute\n`);
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
const seam = sops();
|
|
103
|
+
const map = openMap(file, envelope, seam);
|
|
104
|
+
delete map[key];
|
|
105
|
+
replaceSealed(file, sealReplacement({ file, format: envelope.format, map, envelope, sops: seam }));
|
|
106
|
+
stdout(`unset ${label} (${key}); ${keyCount(remaining)} remain\n`);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The replacement map and format of a rename: the key keeps its position and value; a binary envelope becomes a JSON one. */
|
|
111
|
+
function renamedMap({ file, envelope, from, to, sops }) {
|
|
112
|
+
if (isBinaryEnvelope(envelope)) {
|
|
113
|
+
const value = sops.decryptBinary(file);
|
|
114
|
+
if (typeof value !== "string" || value === "")
|
|
115
|
+
throw new SecretError("the binary envelope holds no content");
|
|
116
|
+
return { format: "json", map: { [to]: value } };
|
|
117
|
+
}
|
|
118
|
+
const map = openMap(file, envelope, sops);
|
|
119
|
+
const entries = Object.entries(map).map(([name, value]) => [name === from ? to : name, value]);
|
|
120
|
+
return { format: envelope.format, map: Object.fromEntries(entries) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renameKey({ file, label, key, to, execute, sops, stdout }) {
|
|
124
|
+
const envelope = inspectMember(file, label);
|
|
125
|
+
requireKey(envelope, key, label);
|
|
126
|
+
if (key === to) throw new SecretError("the new KEY equals the old KEY");
|
|
127
|
+
if (envelope.keys.includes(to))
|
|
128
|
+
throw new SecretError(`${label} already has KEY ${to}`);
|
|
129
|
+
const binary = isBinaryEnvelope(envelope);
|
|
130
|
+
if (!execute) {
|
|
131
|
+
const note = binary ? "; the binary envelope is re-sealed as a JSON envelope" : "";
|
|
132
|
+
stdout(`dry run: would rename ${key} to ${to} in ${label}${note}; add --execute\n`);
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
const seam = sops();
|
|
136
|
+
const { format, map } = renamedMap({ file, envelope, from: key, to, sops: seam });
|
|
137
|
+
replaceSealed(file, sealReplacement({ file, format, map, envelope, sops: seam }));
|
|
138
|
+
stdout(`renamed ${key} to ${to} in ${label}${binary ? " (re-sealed as a JSON envelope)" : ""}\n`);
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function removeMember({ file, label, execute, force, stdout }) {
|
|
143
|
+
const envelope = inspectMember(file, label);
|
|
144
|
+
if (envelope.keys.length && !force)
|
|
145
|
+
throw new SecretError(
|
|
146
|
+
`${label} still holds ${keyCount(envelope.keys.length)} (${envelope.keys.join(", ")}); unset them first, or pass --force-nonempty to delete the whole member`,
|
|
147
|
+
);
|
|
148
|
+
if (!execute) {
|
|
149
|
+
stdout(`dry run: would remove ${label}.enc (${keyCount(envelope.keys.length)}); add --execute\n`);
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
deleteSealed(file);
|
|
153
|
+
stdout(`removed ${label}.enc (${keyCount(envelope.keys.length)})\n`);
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Validate every name of an edit verb before anything touches the file system. */
|
|
158
|
+
export function validateEditNames(verb, names) {
|
|
159
|
+
for (const name of names) validateKey(name);
|
|
160
|
+
if (verb === "remove" && names.length) throw new SecretError("remove takes no KEY");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** unset, rename and remove; `sops` is a thunk so a dry run never needs the sops tool or an age identity. */
|
|
164
|
+
export function editSecret(verb, { directory, environment, slug, names, execute, force, sops, stdout }) {
|
|
165
|
+
const file = fileOf(directory, slug);
|
|
166
|
+
const common = { file, label: `${environment}/${slug}`, execute, sops, stdout };
|
|
167
|
+
if (verb === "unset") return unsetKey({ ...common, key: names[0] });
|
|
168
|
+
if (verb === "rename") return renameKey({ ...common, key: names[0], to: names[1] });
|
|
169
|
+
return removeMember({ ...common, force });
|
|
170
|
+
}
|
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parseYaml, stringifyYaml } from "../../engine/yaml.mjs";
|
|
5
|
+
import { secretEnv } from "../../engine/secrets.mjs";
|
|
5
6
|
import { resolveSops } from "../api/sops/resolve-sops.mjs";
|
|
7
|
+
import { selectedAgeRecipient } from "../api/sops/selected-age-recipient.mjs";
|
|
6
8
|
import { resolveRealTool } from "../api/process/resolve-real-tool.mjs";
|
|
7
9
|
import { runProgram } from "../api/process/run-program.mjs";
|
|
8
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";
|
|
9
14
|
import {
|
|
10
15
|
outsideRepository,
|
|
11
16
|
privateDirectory,
|
|
@@ -13,6 +18,68 @@ import {
|
|
|
13
18
|
} from "./secret-files.mjs";
|
|
14
19
|
|
|
15
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
|
+
}
|
|
16
83
|
|
|
17
84
|
/** Select the first matching public age rule, including for SOPS's isolated CI identity path. */
|
|
18
85
|
export function configuredRecipients(repoRoot, file) {
|
|
@@ -93,19 +160,14 @@ function readInitializationConfig(configFile) {
|
|
|
93
160
|
return config;
|
|
94
161
|
}
|
|
95
162
|
|
|
96
|
-
|
|
97
|
-
export function initializeSecrets(repoRoot, { env, platform, tools, stdout }) {
|
|
98
|
-
const tool = secretTools(env, tools);
|
|
99
|
-
const configFile = path.join(repoRoot, ".sops.yaml");
|
|
100
|
-
const config = readInitializationConfig(configFile);
|
|
101
|
-
const rules = config.creation_rules ?? [];
|
|
163
|
+
function existingInitializationRule(rules) {
|
|
102
164
|
const canonical = rules.find(
|
|
103
165
|
(rule) =>
|
|
104
166
|
rule?.path_regex === PATH_RULE &&
|
|
105
167
|
typeof rule.age === "string" &&
|
|
106
168
|
rule.age.trim(),
|
|
107
169
|
);
|
|
108
|
-
|
|
170
|
+
return (
|
|
109
171
|
canonical ??
|
|
110
172
|
rules.find((rule) => {
|
|
111
173
|
if (typeof rule?.age !== "string" || !rule.age.trim()) return false;
|
|
@@ -119,15 +181,11 @@ export function initializeSecrets(repoRoot, { env, platform, tools, stdout }) {
|
|
|
119
181
|
} catch {
|
|
120
182
|
return false;
|
|
121
183
|
}
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
)
|
|
128
|
-
throw new Error(
|
|
129
|
-
".sops.yaml must name valid public age recipients; initialization never prints private key material",
|
|
130
|
-
);
|
|
184
|
+
})
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function legacyInitializationRecipient(repoRoot, env, platform, tool) {
|
|
131
189
|
const identity = secretRoots(env, platform).identity;
|
|
132
190
|
outsideRepository(repoRoot, identity);
|
|
133
191
|
privateDirectory(repoRoot, path.dirname(identity));
|
|
@@ -136,7 +194,6 @@ export function initializeSecrets(repoRoot, { env, platform, tools, stdout }) {
|
|
|
136
194
|
throw new Error(
|
|
137
195
|
"the age identity is being initialized; retry after that operation settles",
|
|
138
196
|
);
|
|
139
|
-
let recipient;
|
|
140
197
|
try {
|
|
141
198
|
if (!fs.existsSync(identity)) {
|
|
142
199
|
const generated = tool.run(tool.keygen, ["-o", identity], {
|
|
@@ -170,10 +227,55 @@ export function initializeSecrets(repoRoot, { env, platform, tools, stdout }) {
|
|
|
170
227
|
recipients.some((value) => !/^age1[a-z0-9]+$/.test(value))
|
|
171
228
|
)
|
|
172
229
|
throw new Error("age-keygen returned an invalid public recipient");
|
|
173
|
-
|
|
230
|
+
return recipients.join(",");
|
|
174
231
|
} finally {
|
|
175
232
|
held.release();
|
|
176
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;
|
|
177
279
|
const exact = rules.findIndex((rule) => rule?.path_regex === PATH_RULE);
|
|
178
280
|
if (exact >= 0) rules[exact] = { ...rules[exact], age: recipient };
|
|
179
281
|
// SOPS uses the first matching rule: keep any existing environment-specific recipients in force.
|
|
@@ -181,8 +283,14 @@ export function initializeSecrets(repoRoot, { env, platform, tools, stdout }) {
|
|
|
181
283
|
config.creation_rules = rules;
|
|
182
284
|
fs.writeFileSync(
|
|
183
285
|
configFile,
|
|
184
|
-
`# Initialized by starci app secret init; private age identities stay outside
|
|
286
|
+
`# Initialized by starci app secret init; private age identities stay outside Git.\n${stringifyYaml(config)}`,
|
|
185
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
|
+
});
|
|
186
294
|
stdout(`secrets initialized; public recipient: ${recipient}\n`);
|
|
187
295
|
if (existing)
|
|
188
296
|
stdout(
|
|
@@ -6,11 +6,13 @@ import { resolveRealTool } from "../api/process/resolve-real-tool.mjs";
|
|
|
6
6
|
import { decrypt as sopsDecrypt } from "../api/sops/decrypt.mjs";
|
|
7
7
|
import { seal as sopsSeal } from "../api/sops/seal.mjs";
|
|
8
8
|
import { invocationDir } from "../lib/roots.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import { envelopeOf } from "./secret-document.mjs";
|
|
10
|
+
import { ageIdentityEnv, configuredRecipients, secretTools } from "./secret-init.mjs";
|
|
10
11
|
import { outsideRepository, secretRoots } from "./secret-files.mjs";
|
|
11
12
|
import { SecretError, publicStorage } from "./secret-errors.mjs";
|
|
12
13
|
|
|
13
14
|
const sopsInvocation = Object.freeze({ runProgram, resolveRealTool });
|
|
15
|
+
const AGE_IDENTITY_NAME = /^SOPS_AGE_KEY(?:_FILE)?$/i;
|
|
14
16
|
|
|
15
17
|
/** The default sops seam: the runtime's sops api, which finds the binary on this machine. */
|
|
16
18
|
export function defaultSops(
|
|
@@ -20,7 +22,11 @@ export function defaultSops(
|
|
|
20
22
|
tools,
|
|
21
23
|
) {
|
|
22
24
|
const { sops: binary } = publicStorage(() => secretTools(env, tools));
|
|
23
|
-
const selectedEnv =
|
|
25
|
+
const selectedEnv = ageIdentityEnv(repoRoot, env, tools);
|
|
26
|
+
const invocation = {
|
|
27
|
+
runProgram: tools?.runProgram ?? sopsInvocation.runProgram,
|
|
28
|
+
resolveRealTool: tools?.resolveRealTool ?? sopsInvocation.resolveRealTool,
|
|
29
|
+
};
|
|
24
30
|
const checkIdentity = (file) =>
|
|
25
31
|
publicStorage(() => {
|
|
26
32
|
outsideRepository(repoRoot, file);
|
|
@@ -32,7 +38,7 @@ export function defaultSops(
|
|
|
32
38
|
);
|
|
33
39
|
}
|
|
34
40
|
});
|
|
35
|
-
for (const [name, value] of Object.entries(
|
|
41
|
+
for (const [name, value] of Object.entries(selectedEnv))
|
|
36
42
|
if (/^SOPS_AGE_KEY_FILE$/i.test(name) && value) {
|
|
37
43
|
if (!path.isAbsolute(value))
|
|
38
44
|
throw new SecretError(
|
|
@@ -41,7 +47,7 @@ export function defaultSops(
|
|
|
41
47
|
checkIdentity(value);
|
|
42
48
|
}
|
|
43
49
|
if (
|
|
44
|
-
!Object.keys(
|
|
50
|
+
!Object.keys(selectedEnv).some((name) => AGE_IDENTITY_NAME.test(name))
|
|
45
51
|
) {
|
|
46
52
|
const identity = secretRoots(env, platform).identity;
|
|
47
53
|
checkIdentity(identity);
|
|
@@ -63,7 +69,7 @@ export function defaultSops(
|
|
|
63
69
|
const result = sopsDecrypt(
|
|
64
70
|
binary,
|
|
65
71
|
["decrypt", "--input-type", format, "--output-type", "dotenv", file],
|
|
66
|
-
{ env: selectedEnv, cwd: repoRoot, invocation
|
|
72
|
+
{ env: selectedEnv, cwd: repoRoot, invocation },
|
|
67
73
|
);
|
|
68
74
|
if (result.status !== 0)
|
|
69
75
|
throw refuse(
|
|
@@ -80,6 +86,32 @@ export function defaultSops(
|
|
|
80
86
|
}
|
|
81
87
|
return map;
|
|
82
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
|
+
},
|
|
83
115
|
seal(request) {
|
|
84
116
|
const recipients = request.recipients.length
|
|
85
117
|
? request.recipients
|
|
@@ -89,7 +121,7 @@ export function defaultSops(
|
|
|
89
121
|
const result = sopsSeal(
|
|
90
122
|
binary,
|
|
91
123
|
{ ...request, recipients },
|
|
92
|
-
{ env: selectedEnv, cwd: repoRoot, invocation
|
|
124
|
+
{ env: selectedEnv, cwd: repoRoot, invocation },
|
|
93
125
|
);
|
|
94
126
|
if (result.status !== 0) throw refuse("seal the secret", result);
|
|
95
127
|
return result.stdout;
|