@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,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,97 @@
|
|
|
1
|
+
// secret-document.mjs - envelope inspection, plaintext serialization and named-value validation.
|
|
2
|
+
import { SecretError } from "./secret-errors.mjs";
|
|
3
|
+
|
|
4
|
+
const KEY = /^[A-Za-z_]\w*$/u;
|
|
5
|
+
const WHITESPACE = String.raw`\s`;
|
|
6
|
+
const REQUIRED_INDENT = `${WHITESPACE}+`;
|
|
7
|
+
const OPTIONAL_DASH = "-?";
|
|
8
|
+
const OPTIONAL_SPACING = `${WHITESPACE}*`;
|
|
9
|
+
const RECIPIENT_NAME = "recipient:";
|
|
10
|
+
const RECIPIENT_VALUE = String.raw`(\S+)`;
|
|
11
|
+
const RECIPIENT_LINE = new RegExp(
|
|
12
|
+
`^${REQUIRED_INDENT}${OPTIONAL_DASH}${OPTIONAL_SPACING}${RECIPIENT_NAME}${OPTIONAL_SPACING}${RECIPIENT_VALUE}`,
|
|
13
|
+
"gmu",
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
/** The envelope format of a sealed secret, read from its text: json, yaml or dotenv. */
|
|
17
|
+
export function formatOf(text) {
|
|
18
|
+
const body = String(text).trimStart();
|
|
19
|
+
if (body.startsWith("{")) return "json";
|
|
20
|
+
if (/^sops:\s*$/mu.test(text)) return "yaml";
|
|
21
|
+
return "dotenv";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The key names a sealed document holds and the age recipients it is sealed to, read without decrypting anything. */
|
|
25
|
+
export function envelopeOf(text) {
|
|
26
|
+
const format = formatOf(text);
|
|
27
|
+
if (format === "json") {
|
|
28
|
+
const doc = JSON.parse(text);
|
|
29
|
+
return {
|
|
30
|
+
format,
|
|
31
|
+
keys: Object.keys(doc).filter((name) => name !== "sops"),
|
|
32
|
+
recipients: (doc.sops?.age ?? [])
|
|
33
|
+
.map((entry) => entry.recipient)
|
|
34
|
+
.filter(Boolean),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (format === "yaml") {
|
|
38
|
+
const keys = [...text.matchAll(/^([A-Za-z_]\w*):/gmu)]
|
|
39
|
+
.map((match) => match[1])
|
|
40
|
+
.filter((name) => name !== "sops");
|
|
41
|
+
return {
|
|
42
|
+
format,
|
|
43
|
+
keys,
|
|
44
|
+
recipients: [...text.matchAll(RECIPIENT_LINE)].map((match) => match[1]),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const keys = [...text.matchAll(/^([A-Za-z_]\w*)=/gmu)]
|
|
48
|
+
.map((match) => match[1])
|
|
49
|
+
.filter((name) => !name.startsWith("sops_"));
|
|
50
|
+
return {
|
|
51
|
+
format,
|
|
52
|
+
keys,
|
|
53
|
+
recipients: [
|
|
54
|
+
...text.matchAll(/^sops_age__list_\d+__map_recipient=(\S+)/gmu),
|
|
55
|
+
].map((match) => match[1]),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** SOPS dotenv escapes LF as backslash-n; literal backslash-n has no distinct representation. */
|
|
60
|
+
export function plaintextOf(format, map) {
|
|
61
|
+
if (format === "json") return `${JSON.stringify(map)}\n`;
|
|
62
|
+
if (format === "yaml")
|
|
63
|
+
return (
|
|
64
|
+
Object.entries(map)
|
|
65
|
+
.map(([name, value]) => name + ": " + JSON.stringify(value))
|
|
66
|
+
.join("\n") + "\n"
|
|
67
|
+
);
|
|
68
|
+
for (const value of Object.values(map))
|
|
69
|
+
if (/\r|\\n/u.test(value))
|
|
70
|
+
throw new SecretError(
|
|
71
|
+
"dotenv cannot preserve a carriage return or literal backslash-n; use an LF line break instead",
|
|
72
|
+
);
|
|
73
|
+
return (
|
|
74
|
+
Object.entries(map)
|
|
75
|
+
.map(
|
|
76
|
+
([name, value]) => name + "=" + value.replaceAll("\n", String.raw`\n`),
|
|
77
|
+
)
|
|
78
|
+
.join("\n") + "\n"
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function validateKey(key) {
|
|
83
|
+
if (!KEY.test(key ?? "") || key.startsWith("sops_"))
|
|
84
|
+
throw new SecretError(
|
|
85
|
+
"KEY must be an environment variable name outside the reserved sops_ namespace",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function validateMap(map) {
|
|
90
|
+
if (!map || typeof map !== "object" || Array.isArray(map))
|
|
91
|
+
throw new SecretError("sops returned an invalid secret map");
|
|
92
|
+
for (const [key, value] of Object.entries(map)) {
|
|
93
|
+
validateKey(key);
|
|
94
|
+
if (typeof value !== "string")
|
|
95
|
+
throw new SecretError("secret values must be strings");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// secret-errors.mjs - sanitized public errors shared by app secret operations.
|
|
2
|
+
|
|
3
|
+
export class SecretError extends Error {}
|
|
4
|
+
|
|
5
|
+
export const publicStorage = (action) => {
|
|
6
|
+
try {
|
|
7
|
+
return action();
|
|
8
|
+
} catch (error) {
|
|
9
|
+
throw new SecretError(
|
|
10
|
+
error.code ? `private storage failed (${error.code})` : error.message,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function secretErrorMessage(error) {
|
|
16
|
+
if (error instanceof SecretError) return error.message;
|
|
17
|
+
if (error instanceof SyntaxError) return "invalid secret document";
|
|
18
|
+
const code = error.code ? ` (${error.code})` : "";
|
|
19
|
+
return `operation failed${code}; no secret values were printed`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// secret-files.mjs — private files for app secrets, always outside the repository.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
/** The platform's SOPS age identity and StarCi local data roots. */
|
|
8
|
+
export function secretRoots(env = process.env, platform = process.platform) {
|
|
9
|
+
const home = env.USERPROFILE || env.HOME || os.homedir();
|
|
10
|
+
const support = path.join(home, "Library", "Application Support");
|
|
11
|
+
let config;
|
|
12
|
+
if (platform === "win32")
|
|
13
|
+
config = env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
14
|
+
else if (platform === "darwin") config = support;
|
|
15
|
+
else config = env.XDG_CONFIG_HOME || path.join(home, ".config");
|
|
16
|
+
let local;
|
|
17
|
+
if (platform === "win32")
|
|
18
|
+
local = env.LOCALAPPDATA || path.join(home, "AppData", "Local");
|
|
19
|
+
else if (platform === "darwin") local = support;
|
|
20
|
+
else local = env.XDG_DATA_HOME || path.join(home, ".local", "share");
|
|
21
|
+
return {
|
|
22
|
+
identity: path.resolve(config, "sops", "age", "keys.txt"),
|
|
23
|
+
local: path.resolve(local, "StarCi", "secrets"),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const inside = (root, target) => {
|
|
28
|
+
const relative = path.relative(root, target);
|
|
29
|
+
return (
|
|
30
|
+
relative === "" ||
|
|
31
|
+
(!relative.startsWith(`..${path.sep}`) &&
|
|
32
|
+
relative !== ".." &&
|
|
33
|
+
!path.isAbsolute(relative))
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Reject repository custody, including a path spelled through a symlink or junction. */
|
|
38
|
+
export function outsideRepository(repoRoot, target) {
|
|
39
|
+
const root = fs.realpathSync(repoRoot);
|
|
40
|
+
let ancestor = path.resolve(target);
|
|
41
|
+
while (!fs.existsSync(ancestor)) {
|
|
42
|
+
const parent = path.dirname(ancestor);
|
|
43
|
+
if (parent === ancestor)
|
|
44
|
+
throw new Error("cannot resolve the private storage root");
|
|
45
|
+
ancestor = parent;
|
|
46
|
+
}
|
|
47
|
+
const resolved = path.resolve(
|
|
48
|
+
fs.realpathSync(ancestor),
|
|
49
|
+
path.relative(ancestor, path.resolve(target)),
|
|
50
|
+
);
|
|
51
|
+
if (
|
|
52
|
+
inside(root, resolved) ||
|
|
53
|
+
inside(path.resolve(repoRoot), path.resolve(target))
|
|
54
|
+
)
|
|
55
|
+
throw new Error("private storage must be outside the repository");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Create private directories without following a symlink at any storage component. */
|
|
59
|
+
export function privateDirectory(repoRoot, directory) {
|
|
60
|
+
outsideRepository(repoRoot, directory);
|
|
61
|
+
const missing = [];
|
|
62
|
+
for (let at = directory; !fs.existsSync(at); at = path.dirname(at))
|
|
63
|
+
missing.push(at);
|
|
64
|
+
for (const at of missing.reverse()) fs.mkdirSync(at, { mode: 0o700 });
|
|
65
|
+
for (let at = directory; ; at = path.dirname(at)) {
|
|
66
|
+
const stat = fs.lstatSync(at);
|
|
67
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
68
|
+
throw new Error("private storage must use ordinary directories");
|
|
69
|
+
if (path.dirname(at) === at) break;
|
|
70
|
+
}
|
|
71
|
+
outsideRepository(repoRoot, directory);
|
|
72
|
+
if (process.platform !== "win32") fs.chmodSync(directory, 0o700);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The declared project owns a separate environment directory on the local machine. */
|
|
76
|
+
export function materializedDirectory(repoRoot, environment, env, platform) {
|
|
77
|
+
let project;
|
|
78
|
+
const declaration = path.join(repoRoot, "hfs.json");
|
|
79
|
+
if (fs.existsSync(declaration))
|
|
80
|
+
project = JSON.parse(fs.readFileSync(declaration, "utf8")).project;
|
|
81
|
+
if (!project && fs.existsSync(path.join(repoRoot, "package.json")))
|
|
82
|
+
project = JSON.parse(
|
|
83
|
+
fs.readFileSync(path.join(repoRoot, "package.json"), "utf8"),
|
|
84
|
+
).name;
|
|
85
|
+
project ||= path.basename(repoRoot);
|
|
86
|
+
if (
|
|
87
|
+
typeof project !== "string" ||
|
|
88
|
+
!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(project) ||
|
|
89
|
+
project === ".."
|
|
90
|
+
)
|
|
91
|
+
throw new Error("the project name must be a safe local directory name");
|
|
92
|
+
const directory = path.join(
|
|
93
|
+
secretRoots(env, platform).local,
|
|
94
|
+
project,
|
|
95
|
+
environment,
|
|
96
|
+
);
|
|
97
|
+
privateDirectory(repoRoot, directory);
|
|
98
|
+
return directory;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Atomically publish one KEY file; an existing link or hard link is never overwritten. */
|
|
102
|
+
export function writeSecretFile(directory, key, value) {
|
|
103
|
+
const file = path.join(directory, key);
|
|
104
|
+
try {
|
|
105
|
+
const stat = fs.lstatSync(file);
|
|
106
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1)
|
|
107
|
+
throw new Error("a private KEY file must be an ordinary file");
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error.code !== "ENOENT") throw error;
|
|
110
|
+
}
|
|
111
|
+
const temporary = path.join(directory, `.${key}-${randomUUID()}.tmp`);
|
|
112
|
+
try {
|
|
113
|
+
fs.writeFileSync(temporary, value, {
|
|
114
|
+
flag: "wx",
|
|
115
|
+
mode: 0o600,
|
|
116
|
+
flush: true,
|
|
117
|
+
});
|
|
118
|
+
fs.renameSync(temporary, file);
|
|
119
|
+
if (process.platform !== "win32") fs.chmodSync(file, 0o600);
|
|
120
|
+
} finally {
|
|
121
|
+
if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
|
|
122
|
+
}
|
|
123
|
+
return file;
|
|
124
|
+
}
|