@starci/hfs 4.0.8 → 4.0.10
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 +9 -0
- package/LICENSE +21 -0
- package/README.md +81 -39
- package/emit/contracts.mjs +56 -9
- package/emit/db-types.mjs +169 -0
- package/emit/events.mjs +41 -0
- package/emit/operations-worker.mjs +1 -1
- package/emit/operations.mjs +2 -2
- package/emit/schema-worker.mjs +1 -1
- package/lint/bound-sys.cjs +50 -0
- package/lint/run.mjs +131 -24
- package/package.json +16 -10
- package/report/sonar.mjs +8 -8
- package/runtime/engine/admission.mjs +308 -0
- package/runtime/engine/canonical-json.mjs +10 -0
- package/runtime/engine/config.mjs +486 -0
- package/runtime/engine/db/blob.mjs +315 -0
- package/runtime/engine/db/ledger-paths.mjs +83 -0
- package/runtime/engine/db/ledger.mjs +1118 -0
- package/runtime/engine/db/machine-connection.mjs +135 -0
- package/runtime/engine/db/machine-schema.mjs +84 -0
- package/runtime/engine/db/machine.mjs +1367 -0
- package/runtime/engine/db/migrations/machine/0001-init.sql +924 -0
- package/runtime/engine/db/migrations/runtime/0001-init.sql +1108 -0
- package/runtime/engine/db/provider-reservations.mjs +101 -0
- package/runtime/engine/digest.mjs +16 -0
- package/runtime/engine/invalid-config.mjs +15 -0
- package/runtime/engine/orca-config.mjs +27 -0
- package/runtime/engine/refuse.mjs +11 -0
- package/runtime/engine/secrets.mjs +130 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +162 -31
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -2
- package/runtime/knowledge/hfs/rules.yaml +2059 -0
- package/runtime/knowledge/hfs/slots.yaml +912 -120
- package/runtime/knowledge/patterns/be/api.yaml +369 -0
- package/runtime/knowledge/patterns/be/cli.yaml +297 -0
- package/runtime/knowledge/patterns/be/domain.yaml +113 -0
- package/runtime/knowledge/patterns/be/event-bus.yaml +285 -0
- package/runtime/knowledge/patterns/be/jobs.yaml +261 -0
- package/runtime/knowledge/patterns/be/projections.yaml +178 -0
- package/runtime/knowledge/patterns/be/queues.yaml +236 -0
- package/runtime/knowledge/patterns/be/reactors.yaml +140 -0
- package/runtime/knowledge/patterns/be/realtime.yaml +222 -0
- package/runtime/knowledge/patterns/be/saga.yaml +248 -0
- package/runtime/knowledge/patterns/be/webhooks.yaml +221 -0
- package/runtime/knowledge/patterns/fe/folder.yaml +131 -25
- package/runtime/knowledge/sonar-gate.yaml +52 -18
- package/runtime/modules/kernel/failure-codes.yaml +611 -51
- package/runtime/modules/models/registry.yaml +553 -0
- package/runtime/modules/models/runtimes.yaml +695 -0
- package/runtime/modules/ops/_labels.yaml +53 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +51 -0
- package/runtime/scripts/api/fs/is-link-like.mjs +25 -0
- package/runtime/scripts/api/fs/lib.mjs +16 -2
- package/runtime/scripts/api/fs/safe-remove.mjs +12 -132
- package/runtime/scripts/api/git/config-get.mjs +10 -0
- package/runtime/scripts/api/git/diff.mjs +5 -0
- package/runtime/scripts/api/git/lib.mjs +18 -26
- package/runtime/scripts/api/git/log.mjs +5 -0
- package/runtime/scripts/api/git/ls-files.mjs +6 -0
- package/runtime/scripts/api/git/ls-tree.mjs +5 -0
- package/runtime/scripts/api/git/merge-base.mjs +9 -0
- package/runtime/scripts/api/git/remote.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse-query.mjs +5 -0
- package/runtime/scripts/api/git/show.mjs +6 -0
- package/runtime/scripts/api/node/lib.mjs +14 -0
- package/runtime/scripts/api/node/spawn-node.mjs +6 -0
- package/runtime/scripts/api/process/lib.mjs +111 -0
- package/runtime/scripts/api/process/owned-process.mjs +78 -0
- package/runtime/scripts/api/process/resolve-real-tool.mjs +46 -0
- package/runtime/scripts/api/process/run-program.mjs +7 -0
- package/runtime/scripts/api/process/stop-owned-process.mjs +9 -0
- package/runtime/scripts/api/sops/decrypt.mjs +9 -12
- package/runtime/scripts/api/sops/lib.mjs +255 -0
- package/runtime/scripts/api/sops/seal.mjs +22 -0
- package/runtime/scripts/connectors/lib.mjs +488 -0
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +220 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +16 -23
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +16 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +3 -1
- package/runtime/scripts/hfs/architecture/clones.mjs +4 -4
- package/runtime/scripts/hfs/architecture/config.mjs +9 -6
- package/runtime/scripts/hfs/architecture/connection-map.mjs +20 -8
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +88 -0
- package/runtime/scripts/hfs/architecture/context-map.mjs +237 -0
- package/runtime/scripts/hfs/architecture/context-owner.mjs +82 -0
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -0
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +60 -0
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +4 -10
- package/runtime/scripts/hfs/architecture/contracts.mjs +33 -162
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +58 -29
- package/runtime/scripts/hfs/architecture/doc-language.mjs +1 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +2 -1
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -2
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +5 -2
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend.mjs +17 -43
- package/runtime/scripts/hfs/architecture/hfs.mjs +24 -39
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +1 -1
- package/runtime/scripts/hfs/architecture/index.mjs +17 -4
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +44 -11
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +27 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +30 -6
- package/runtime/scripts/hfs/architecture/next-data.mjs +22 -63
- package/runtime/scripts/hfs/architecture/owners.mjs +35 -15
- package/runtime/scripts/hfs/architecture/register-once.mjs +2 -1
- package/runtime/scripts/hfs/architecture/registration.mjs +23 -81
- package/runtime/scripts/hfs/architecture/required-files.mjs +20 -12
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +17 -114
- package/runtime/scripts/hfs/architecture/server-action.mjs +24 -0
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +16 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +27 -147
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +59 -18
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +64 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +111 -0
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +131 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +42 -0
- package/runtime/scripts/hfs/architecture/supabase.mjs +434 -0
- package/runtime/scripts/hfs/architecture/surface.mjs +9 -3
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +9 -19
- package/runtime/scripts/hfs/architecture/tiers.mjs +47 -5
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -3
- package/runtime/scripts/hfs/architecture/typescript.mjs +12 -26
- package/runtime/scripts/hfs/architecture.mjs +4 -4
- package/runtime/scripts/hfs/check.mjs +75 -72
- package/runtime/scripts/hfs/coverage-scope.mjs +162 -0
- package/runtime/scripts/hfs/declaration-shape.mjs +62 -0
- package/runtime/scripts/hfs/declaration-slots.mjs +75 -0
- package/runtime/scripts/hfs/edition-slots.mjs +66 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +109 -32
- package/runtime/scripts/hfs/param-names.mjs +18 -0
- package/runtime/scripts/hfs/path-findings.mjs +1 -1
- package/runtime/scripts/hfs/repo-identity.mjs +9 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +50 -0
- package/runtime/scripts/hfs/rules/app-root.mjs +51 -0
- package/runtime/scripts/hfs/rules/cli.mjs +37 -0
- package/runtime/scripts/hfs/rules/contract-compat.mjs +71 -0
- package/runtime/scripts/hfs/rules/contract.mjs +10 -9
- package/runtime/scripts/hfs/rules/database-config.mjs +102 -0
- package/runtime/scripts/hfs/rules/database-constants.mjs +13 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +111 -0
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +233 -0
- package/runtime/scripts/hfs/rules/database-sql.mjs +336 -0
- package/runtime/scripts/hfs/rules/database.mjs +68 -0
- package/runtime/scripts/hfs/rules/deps.mjs +1 -1
- package/runtime/scripts/hfs/rules/docker.mjs +259 -0
- package/runtime/scripts/hfs/rules/edition.mjs +111 -0
- package/runtime/scripts/hfs/rules/event-bus.mjs +153 -0
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -1
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +6 -5
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +9 -9
- package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -6
- package/runtime/scripts/hfs/rules/kinds.mjs +84 -0
- package/runtime/scripts/hfs/rules/lint-suppression.mjs +1 -1
- package/runtime/scripts/hfs/rules/monorepo.mjs +179 -0
- package/runtime/scripts/hfs/rules/peer-integrations.mjs +3 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +11 -21
- package/runtime/scripts/hfs/rules/proof-commands.mjs +2 -2
- package/runtime/scripts/hfs/rules/read.mjs +1 -1
- package/runtime/scripts/hfs/rules/repo-local-checks.mjs +2 -2
- package/runtime/scripts/hfs/rules/saga.mjs +170 -0
- package/runtime/scripts/hfs/rules/secrets.mjs +3 -3
- package/runtime/scripts/hfs/rules/services.mjs +220 -0
- package/runtime/scripts/hfs/rules/spec-placement.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks.mjs +3 -3
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +89 -0
- package/runtime/scripts/hfs/rules/test-topology.mjs +2 -2
- package/runtime/scripts/hfs/secret.mjs +209 -0
- package/runtime/scripts/hfs/slots.mjs +78 -108
- package/runtime/scripts/hfs/sql/pg-parse.mjs +46 -0
- package/runtime/scripts/hfs/test-world-slot.mjs +6 -0
- package/runtime/scripts/hfs/tree.mjs +5 -4
- package/runtime/scripts/hfs/typescript-programs.mjs +0 -5
- package/runtime/scripts/hfs/view.mjs +3 -2
- package/runtime/scripts/lib/clip.mjs +19 -0
- package/runtime/scripts/lib/display-names.mjs +259 -0
- package/runtime/scripts/lib/dockerfile.mjs +124 -0
- package/runtime/scripts/lib/env.mjs +10 -0
- package/runtime/scripts/lib/event-contract.mjs +103 -0
- package/runtime/scripts/lib/example-refs.mjs +158 -0
- package/runtime/scripts/lib/fs-kind.mjs +14 -4
- package/runtime/scripts/lib/git.mjs +36 -0
- package/runtime/scripts/lib/graphql-contract.mjs +1 -1
- package/runtime/scripts/lib/i18n.mjs +86 -0
- package/runtime/scripts/lib/json-schema.mjs +52 -0
- package/runtime/scripts/lib/json.mjs +13 -3
- package/runtime/scripts/lib/language.mjs +24 -5
- package/runtime/scripts/lib/list.mjs +20 -0
- package/runtime/scripts/lib/mutation-fence.mjs +15 -0
- package/runtime/scripts/lib/package-at.mjs +37 -0
- package/runtime/scripts/lib/path-key.mjs +57 -2
- package/runtime/scripts/lib/process-identity.mjs +6 -0
- package/runtime/scripts/lib/read-text.mjs +34 -0
- package/runtime/scripts/lib/read-yaml.mjs +18 -0
- package/runtime/scripts/lib/redact.mjs +161 -0
- package/runtime/scripts/lib/regex.mjs +4 -0
- package/runtime/scripts/lib/same-text.mjs +5 -0
- package/runtime/scripts/lib/secret-patterns.mjs +4 -3
- package/runtime/scripts/lib/sleep-sync.mjs +2 -1
- package/runtime/scripts/lib/sleep.mjs +7 -0
- package/runtime/scripts/lib/sops-envelope.mjs +112 -0
- package/runtime/scripts/lib/source-phrases.mjs +34 -0
- package/runtime/scripts/lib/sqlite.mjs +21 -0
- package/runtime/scripts/lib/stack-services.mjs +7 -31
- package/runtime/scripts/lib/ts-ast.mjs +35 -0
- package/runtime/scripts/lib/walk.mjs +4 -3
- package/scaffold/add-app.mjs +174 -0
- package/scaffold/add-cli-lite.mjs +257 -0
- package/scaffold/add-inbox-lite.mjs +314 -0
- package/scaffold/add-table.mjs +344 -0
- package/scaffold/add.mjs +299 -0
- package/scaffold/app.mjs +358 -69
- package/scaffold/edition-gate.mjs +25 -0
- package/scaffold/image.mjs +19 -0
- package/scaffold/lite-exports.mjs +186 -0
- package/scaffold/service.mjs +32 -26
- package/scaffold/supabase-ports.mjs +26 -0
- package/src/main.mjs +327 -0
- package/sync/cli.mjs +6 -9
- package/sync/format.mjs +1 -1
- package/sync/hygiene.mjs +10 -10
- package/sync/index.mjs +167 -104
- package/sync/managed.mjs +19 -11
- package/sync/sonar-key.mjs +5 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +14 -14
- package/templates/app/ci-workflows/github/workflows/images.yml +37 -0
- package/templates/app/ci-workflows/sonar-steps.yml +9 -0
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +66 -0
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +31 -0
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +37 -0
- package/templates/app/codegen-lite/scripts/codegen.mjs +32 -0
- package/templates/app/docker-ignore/dockerignore +50 -0
- package/templates/app/gitignore-lite +35 -0
- package/templates/app/hooks/husky/pre-commit +3 -8
- package/templates/app/hooks/husky/pre-push +45 -6
- package/templates/app/package-scripts/core.json +8 -0
- package/templates/app/package-scripts/package.json +3 -10
- package/templates/app/package-scripts-lite/package.json +15 -0
- package/templates/app/quality-config/codecov.yml +15 -3
- package/templates/app/quality-config/sonar-base.properties +4 -0
- package/templates/app/quality-config/sonar-project.properties +1 -4
- package/templates/app/quality-config-lite/sonar-project.properties +5 -0
- package/templates/app/skeleton/.starcistacks/dev/seeds/primary-notes.sql +5 -0
- package/templates/app/skeleton/.starciwork/features/system-health/index.yaml +4 -0
- package/templates/app/skeleton/.starciwork/{features/index.yaml → index.yaml} +2 -2
- package/templates/app/skeleton/README.md +7 -5
- package/templates/app/skeleton-lite/.sops.yaml +3 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +5 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/secrets/.gitkeep +1 -0
- package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +55 -0
- package/templates/app/skeleton-lite/README.md +40 -0
- package/templates/app/skeleton-lite/supabase/config.toml +41 -0
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +56 -0
- package/templates/app/skeleton-lite/supabase/seed.sql +0 -0
- package/templates/app/task-graph/turbo.json +23 -0
- package/templates/app/tool-config-lite/prettierignore +2 -0
- package/templates/app/tool-config-lite/prettierrc +1 -0
- package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +38 -0
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +10 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +13 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +3 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +12 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +7 -0
- package/templates/be/common/main-listen.ts.partial +14 -0
- package/templates/be/common/sql-ident.ts.partial +7 -0
- package/templates/be/common/sql-tag.ts.partial +5 -0
- package/templates/be/image/api/Dockerfile +38 -0
- package/templates/be/image/cli/Dockerfile +38 -0
- package/templates/be/image/worker/Dockerfile +36 -0
- package/templates/be/inbox/database-client.ts.tpl +50 -0
- package/templates/be/inbox/index.ts.tpl +2 -0
- package/templates/be/inbox/migration.sql.tpl +29 -0
- package/templates/be/inbox/module-definition.ts.tpl +6 -0
- package/templates/be/inbox/module.ts.tpl +14 -0
- package/templates/be/inbox/options.ts.tpl +2 -0
- package/templates/be/inbox/service.ts.tpl +33 -0
- package/templates/be/inbox/sql.ts.tpl +16 -0
- package/templates/be/inbox/webhook-door.ts.tpl +36 -0
- package/templates/be/patterns/api/application.command.http.ts.tpl +10 -0
- package/templates/be/patterns/api/application.command.ts.tpl +10 -0
- package/templates/be/patterns/api/application.contracts.ts.tpl +11 -0
- package/templates/be/patterns/api/application.handler.http.ts.tpl +22 -0
- package/templates/be/patterns/api/application.handler.ts.tpl +22 -0
- package/templates/be/patterns/api/feature.index.http.ts.tpl +1 -0
- package/templates/be/patterns/api/feature.index.ts.tpl +1 -0
- package/templates/be/patterns/api/feature.module.ts.tpl +6 -0
- package/templates/be/patterns/api/graphql.input.ts.tpl +13 -0
- package/templates/be/patterns/api/graphql.mapper.ts.tpl +9 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +7 -0
- package/templates/be/patterns/api/graphql.resolver.ts.tpl +27 -0
- package/templates/be/patterns/api/graphql.type.ts.tpl +9 -0
- package/templates/be/patterns/api/http.controller.ts.tpl +27 -0
- package/templates/be/patterns/api/http.mapper.ts.tpl +9 -0
- package/templates/be/patterns/api/http.module.ts.tpl +7 -0
- package/templates/be/patterns/api/http.request.ts.tpl +10 -0
- package/templates/be/patterns/api/http.response.ts.tpl +5 -0
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +15 -0
- package/templates/be/patterns/cli/group.cli.ts.tpl +11 -0
- package/templates/be/patterns/cli/group.module.ts.tpl +7 -0
- package/templates/be/patterns/cli/sub.run.cli.spec.ts.tpl +17 -0
- package/templates/be/patterns/cli/sub.run.cli.ts.tpl +15 -0
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +22 -0
- package/templates/be/patterns/event-bus/platform/event-bus.config.ts.tpl +19 -0
- package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +58 -0
- package/templates/be/patterns/event-bus/platform/event-bus.decorators.ts.tpl +46 -0
- package/templates/be/patterns/event-bus/platform/event-bus.log-events.ts.tpl +12 -0
- package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +10 -0
- package/templates/be/patterns/event-bus/platform/event-bus.module.ts.tpl +52 -0
- package/templates/be/patterns/event-bus/platform/event-bus.options.ts.tpl +19 -0
- package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +28 -0
- package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +165 -0
- package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +93 -0
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +90 -0
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +58 -0
- package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +136 -0
- package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +56 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +272 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +128 -0
- package/templates/be/patterns/event-bus/platform/event-transport.port.ts.tpl +180 -0
- package/templates/be/patterns/event-bus/platform/event.policy.spec.ts.tpl +67 -0
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +38 -0
- package/templates/be/patterns/event-bus/platform/index.ts.tpl +11 -0
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.spec.ts.tpl +348 -0
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +241 -0
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +15 -0
- package/templates/be/patterns/event-bus/platform/persistence/connection.ts.tpl +8 -0
- package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +37 -0
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +11 -0
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +12 -0
- package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +26 -0
- package/templates/be/patterns/jobs/job.index.ts.tpl +1 -0
- package/templates/be/patterns/jobs/job.module.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.processor.ts.tpl +20 -0
- package/templates/be/patterns/jobs/job.queue.module.ts.tpl +20 -0
- package/templates/be/patterns/jobs/job.step.ts.tpl +19 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +31 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.ts.tpl +13 -0
- package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +150 -0
- package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +100 -0
- package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +138 -0
- package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +69 -0
- package/templates/be/patterns/jobs/platform/jobs.config.ts.tpl +11 -0
- package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +62 -0
- package/templates/be/patterns/jobs/platform/jobs.decorators.ts.tpl +31 -0
- package/templates/be/patterns/jobs/platform/jobs.error.ts.tpl +22 -0
- package/templates/be/patterns/jobs/platform/jobs.index.ts.tpl +11 -0
- package/templates/be/patterns/jobs/platform/jobs.log-events.ts.tpl +9 -0
- package/templates/be/patterns/jobs/platform/jobs.messages.ts.tpl +15 -0
- package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +10 -0
- package/templates/be/patterns/jobs/platform/jobs.module.ts.tpl +32 -0
- package/templates/be/patterns/jobs/platform/jobs.options.ts.tpl +11 -0
- package/templates/be/patterns/jobs/platform/jobs.port.ts.tpl +36 -0
- package/templates/be/patterns/jobs/platform/persistence.connection.ts.tpl +8 -0
- package/templates/be/patterns/jobs/platform/persistence.job.entity.ts.tpl +53 -0
- package/templates/be/patterns/jobs/platform/persistence.jobs.rows.ts.tpl +13 -0
- package/templates/be/patterns/jobs/platform/persistence.jobs.sql.ts.tpl +33 -0
- package/templates/be/patterns/jobs/platform/persistence.migration.ts.tpl +29 -0
- package/templates/be/patterns/outbox/platform/index.ts.tpl +2 -0
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +96 -0
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +78 -0
- package/templates/be/patterns/outbox/platform/outbox.log-events.ts.tpl +5 -0
- package/templates/be/patterns/projections/migration.ts.tpl +18 -0
- package/templates/be/patterns/projections/persistence.connection.ts.tpl +8 -0
- package/templates/be/patterns/projections/projection.entity.ts.tpl +9 -0
- package/templates/be/patterns/projections/projection.index.ts.tpl +3 -0
- package/templates/be/patterns/projections/projection.module-definition.ts.tpl +7 -0
- package/templates/be/patterns/projections/projection.module.ts.tpl +18 -0
- package/templates/be/patterns/projections/projection.options.ts.tpl +2 -0
- package/templates/be/patterns/projections/projection.projection.spec.ts.tpl +55 -0
- package/templates/be/patterns/projections/projection.projection.ts.tpl +21 -0
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +142 -0
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +81 -0
- package/templates/be/patterns/queues/platform/persistence.connection.ts.tpl +8 -0
- package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +25 -0
- package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +25 -0
- package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +9 -0
- package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +12 -0
- package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +48 -0
- package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +17 -0
- package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +128 -0
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +48 -0
- package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +115 -0
- package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +91 -0
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +45 -0
- package/templates/be/patterns/queues/platform/queue.config.ts.tpl +15 -0
- package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +31 -0
- package/templates/be/patterns/queues/platform/queue.decorators.ts.tpl +45 -0
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +7 -0
- package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +5 -0
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +10 -0
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +62 -0
- package/templates/be/patterns/queues/platform/queue.options.ts.tpl +22 -0
- package/templates/be/patterns/queues/platform/queue.policy.spec.ts.tpl +19 -0
- package/templates/be/patterns/queues/platform/queue.policy.ts.tpl +11 -0
- package/templates/be/patterns/queues/platform/queue.port.ts.tpl +14 -0
- package/templates/be/patterns/queues/queue.index.ts.tpl +3 -0
- package/templates/be/patterns/queues/queue.module.ts.tpl +6 -0
- package/templates/be/patterns/queues/queue.queue.ts.tpl +23 -0
- package/templates/be/patterns/reactors/application.command.ts.tpl +10 -0
- package/templates/be/patterns/reactors/application.contracts.ts.tpl +12 -0
- package/templates/be/patterns/reactors/application.handler.ts.tpl +22 -0
- package/templates/be/patterns/reactors/consumer.ts.tpl +19 -0
- package/templates/be/patterns/reactors/message.module.ts.tpl +20 -0
- package/templates/be/patterns/reactors/reactor.index.ts.tpl +1 -0
- package/templates/be/patterns/reactors/reactor.module.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.index.ts.tpl +1 -0
- package/templates/be/patterns/realtime/channel.input.ts.tpl +12 -0
- package/templates/be/patterns/realtime/channel.subscription.spec.ts.tpl +51 -0
- package/templates/be/patterns/realtime/channel.subscription.ts.tpl +32 -0
- package/templates/be/patterns/realtime/channel.type.ts.tpl +9 -0
- package/templates/be/patterns/saga/saga.compensate.command.ts.tpl +11 -0
- package/templates/be/patterns/saga/saga.compensate.contracts.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.compensate.handler.ts.tpl +22 -0
- package/templates/be/patterns/saga/saga.compensation.ts.tpl +21 -0
- package/templates/be/patterns/saga/saga.complete.command.ts.tpl +11 -0
- package/templates/be/patterns/saga/saga.complete.contracts.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.complete.handler.ts.tpl +22 -0
- package/templates/be/patterns/saga/saga.done.consumer.ts.tpl +21 -0
- package/templates/be/patterns/saga/saga.failed.consumer.ts.tpl +21 -0
- package/templates/be/patterns/saga/saga.index.ts.tpl +1 -0
- package/templates/be/patterns/saga/saga.message.module.ts.tpl +23 -0
- package/templates/be/patterns/saga/saga.module.ts.tpl +20 -0
- package/templates/be/patterns/saga/saga.service.spec.ts.tpl +70 -0
- package/templates/be/patterns/saga/saga.service.ts.tpl +44 -0
- package/templates/be/patterns/saga/saga.state.ts.tpl +14 -0
- package/templates/be/patterns/saga/saga.step.ts.tpl +11 -0
- package/templates/be/patterns/saga/saga.undo.command.ts.tpl +10 -0
- package/templates/be/patterns/saga/saga.undo.contracts.ts.tpl +5 -0
- package/templates/be/patterns/saga/saga.undo.handler.ts.tpl +21 -0
- package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +72 -0
- package/templates/be/patterns/webhooks/webhook.door.ts.tpl +36 -0
- package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +6 -0
- package/templates/be/patterns/webhooks/webhook.index.ts.tpl +1 -0
- package/templates/be/patterns/webhooks/webhook.request.ts.tpl +9 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +3 -0
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +15 -2
- package/templates/be/skeleton/apps/__app__/src/main.ts +7 -15
- package/templates/be/skeleton/apps/cli/src/app.module.ts +30 -0
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +22 -0
- package/templates/be/skeleton/apps/cli/src/main.ts +24 -0
- package/templates/be/skeleton/src/features/cli/cli.module.ts +7 -0
- package/templates/be/skeleton/src/features/cli/index.ts +1 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +17 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +15 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +7 -0
- package/templates/be/skeleton/src/features/cli/migrate/subs/run.cli.spec.ts +17 -0
- package/templates/be/skeleton/src/features/cli/migrate/subs/run.cli.ts +18 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +17 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +15 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +7 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +33 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.ts +34 -0
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +13 -0
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +40 -0
- package/templates/be/skeleton/src/modules/domain/identity/messages/identity.messages.ts +1 -1
- package/templates/be/skeleton/src/modules/domain/note/errors/note.error.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/note/index.ts +3 -0
- package/templates/be/skeleton/src/modules/domain/note/note.contracts.ts +19 -0
- package/templates/be/skeleton/src/modules/domain/note/note.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/domain/note/note.module.ts +19 -0
- package/templates/be/skeleton/src/modules/domain/note/note.options.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/note/note.service.spec.ts +76 -0
- package/templates/be/skeleton/src/modules/domain/note/note.service.ts +37 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/connection.ts +8 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/entities/note.entity.ts +17 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/migrations/1790000000000-init-note.ts +22 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/note.rows.ts +17 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/note.sql.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +20 -0
- package/templates/be/skeleton/src/modules/platform/config/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +18 -0
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +41 -0
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.ts +17 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module.ts +77 -0
- package/templates/be/skeleton/src/modules/platform/database/database.options.ts +30 -0
- package/templates/be/skeleton/src/modules/platform/database/database.port.ts +65 -0
- package/templates/be/skeleton/src/modules/platform/database/database.sql.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/database/errors/database.error.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/database/index.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +30 -0
- package/templates/be/skeleton/src/modules/platform/database/migration-runner.service.spec.ts +84 -0
- package/templates/be/skeleton/src/modules/platform/database/migration-runner.service.ts +23 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.config.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.connection.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +10 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +108 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +75 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.spec.ts +88 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.ts +29 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +48 -0
- package/templates/be/skeleton/src/modules/platform/errors/messages/errors.messages.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +12 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +8 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +14 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +12 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +10 -2
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.spec.ts +63 -0
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +108 -0
- package/templates/be/skeleton/src/modules/platform/http-security/request-validation.service.spec.ts +39 -0
- package/templates/be/skeleton/src/modules/platform/http-security/request-validation.service.ts +25 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +172 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.ts +66 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +3 -1
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +52 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +37 -0
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +81 -0
- package/templates/be/skeleton-lite/apps/api/src/main.ts +34 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +14 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +50 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +26 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +19 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +26 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +7 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +6 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +19 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +10 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.ts +24 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.config.ts +8 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.decorators.ts +17 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts +32 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.options.ts +24 -0
- package/templates/be/skeleton-lite/src/modules/platform/cqrs/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +54 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.options.ts +23 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +10 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.sql.ts +49 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/migration-runner.service.ts +21 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/primary.config.ts +11 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +28 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +25 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +9 -0
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +5 -0
- package/templates/be/table/error.ts.tpl +16 -0
- package/templates/be/table/index.ts.tpl +2 -0
- package/templates/be/table/migration.sql.tpl +51 -0
- package/templates/be/table/module-definition.ts.tpl +7 -0
- package/templates/be/table/module.ts.tpl +14 -0
- package/templates/be/table/options.ts.tpl +2 -0
- package/templates/be/table/rows.ts.tpl +4 -0
- package/templates/be/table/service.ts.tpl +23 -0
- package/templates/be/table/sql.ts.tpl +4 -0
- package/templates/be/tool-config/jest.config.js +3 -1
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +14 -0
- package/templates/be/upgrade-full/apps/cli/src/main.ts +26 -0
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +6 -0
- package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +23 -0
- package/templates/common/gitignore.base +1 -2
- package/templates/fe/app/package.json.tpl +1 -0
- package/templates/fe/app/tsconfig.json +12 -0
- package/templates/fe/common/app-layout.tsx.partial +8 -0
- package/templates/fe/common/site-shell.tsx.partial +9 -0
- package/templates/fe/image/next/Dockerfile +42 -0
- package/templates/fe/skeleton/apps/app/next.config.ts +23 -0
- package/templates/fe/skeleton/apps/app/postcss.config.mjs +7 -0
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/error.tsx +2 -2
- package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +22 -0
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/loading.tsx +2 -2
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/not-found.tsx +2 -2
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +9 -0
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/global-error.tsx +2 -2
- package/templates/fe/skeleton/apps/app/src/app/globals.css +10 -0
- package/templates/fe/skeleton/apps/app/src/features/layouts/AppLayout/index.tsx +18 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppErrorPage/component.tsx +19 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppErrorPage/index.tsx +11 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppGlobalErrorPage/index.tsx +16 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/component.tsx +10 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +15 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppLoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppNotFoundPage/index.tsx +9 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/index.ts +4 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +28 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/request.ts +4 -0
- package/templates/fe/skeleton/apps/app/src/modules/routes/index.ts +4 -0
- package/templates/fe/skeleton/apps/app/src/proxy.ts +1 -0
- package/templates/fe/skeleton/apps/{__app__ → landing}/next.config.ts +4 -2
- package/templates/fe/skeleton/apps/landing/postcss.config.mjs +7 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/error.tsx +10 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/layout.tsx +22 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/loading.tsx +6 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/not-found.tsx +6 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +9 -0
- package/templates/fe/skeleton/apps/landing/src/app/global-error.tsx +10 -0
- package/templates/fe/skeleton/apps/landing/src/app/globals.css +10 -0
- package/templates/fe/skeleton/apps/landing/src/features/layouts/LandingLayout/index.tsx +24 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingErrorPage/component.tsx +18 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingErrorPage/index.tsx +11 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingGlobalErrorPage/index.tsx +16 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingLoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingNotFoundPage/index.tsx +9 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/component.tsx +24 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/index.tsx +16 -0
- package/templates/fe/skeleton/apps/landing/src/modules/config/index.ts +12 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/index.ts +7 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/vi.json +31 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/request.ts +4 -0
- package/templates/fe/skeleton/apps/landing/src/modules/routes/index.ts +4 -0
- package/templates/fe/skeleton/apps/landing/src/proxy.ts +1 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +49 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +24 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +20 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/branches/Providers/index.tsx +23 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/ErrorNotice/index.tsx +14 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/GlobalErrorNotice/index.tsx +25 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/LoadingNotice/index.tsx +9 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/LocaleShell/index.tsx +21 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/NotFoundNotice/index.tsx +19 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/SiteShell/classNames.ts +4 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/SiteShell/index.tsx +19 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +6 -0
- package/templates/fe/skeleton-lite/apps/web/next.config.ts +22 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/error.tsx +10 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +37 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/loading.tsx +6 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/not-found.tsx +6 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/providers.tsx +26 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/auth/callback/route.ts +24 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/global-error.tsx +10 -0
- package/templates/fe/{skeleton/apps/__app__ → skeleton-lite/apps/web}/src/app/globals.css +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/app/health/live/route.ts +5 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/component.tsx +29 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +41 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/component.tsx +17 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/index.tsx +38 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/ErrorNotice/index.tsx +13 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/GlobalErrorNotice/index.tsx +24 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/LoadingNotice/index.tsx +7 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/NotFoundNotice/index.tsx +18 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/SiteShell/classNames.ts +4 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/SiteShell/index.tsx +13 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/layouts/AppLayout/index.tsx +31 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppErrorPage/component.tsx +12 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppErrorPage/index.tsx +10 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppGlobalErrorPage/index.tsx +15 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +14 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +30 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppLoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppNotFoundPage/index.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/brand/brand.css +20 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/brand/index.ts +2 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +20 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/read-session.ts +25 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-in.ts +25 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +17 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/browser.ts +14 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/index.ts +3 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/outcome.ts +59 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/principal.ts +29 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/server.ts +70 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +50 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +8 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +48 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +16 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +14 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +7 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/safe-next-path.ts +26 -0
- package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +43 -0
- package/templates/fe/table/read.ts.tpl +16 -0
- package/templates/fe/table/write.ts.tpl +16 -0
- package/upgrade/check.mjs +137 -0
- package/upgrade/index.mjs +524 -0
- package/bin/hfs.mjs +0 -233
- package/runtime/scripts/api/fs/rmdir-link.mjs +0 -12
- package/runtime/scripts/lib/test-secrets.mjs +0 -116
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +0 -20
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/page.tsx +0 -9
- package/templates/fe/skeleton/apps/__app__/src/components/composites/FailureScreen/index.tsx +0 -28
- package/templates/fe/skeleton/apps/__app__/src/features/layouts/LocaleShell/index.tsx +0 -35
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +0 -19
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +0 -11
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +0 -24
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +0 -14
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +0 -21
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +0 -15
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +0 -23
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +0 -8
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +0 -28
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +0 -8
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/config.ts +0 -8
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/index.ts +0 -2
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages/vi.json +0 -25
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/request.ts +0 -19
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.ts +0 -9
- package/templates/fe/skeleton/apps/__app__/src/modules/routes/index.ts +0 -4
- package/templates/fe/skeleton/apps/__app__/src/proxy.ts +0 -10
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.contracts.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.handler.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.query.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/index.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/system-health.module.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/transport/http/live.controller.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/transport/http/system-health-http.module.ts +0 -0
- /package/templates/fe/skeleton/apps/{__app__ → app}/src/app/health/live/route.ts +0 -0
- /package/templates/fe/skeleton/apps/{__app__ → app}/src/modules/config/index.ts +0 -0
- /package/templates/fe/{skeleton/apps/__app__ → skeleton-lite/apps/web}/postcss.config.mjs +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// services.mjs - the microservice policy of a product that runs more than one service (R163 to R167).
|
|
2
|
+
// A product keeps every back-end service as a Nest app at `be/apps/<service>/` of the one repository, sharing the `be/src` libraries and
|
|
3
|
+
// the one package.json; the services it does not own stay pinned images in the stack. Five tree checks:
|
|
4
|
+
// R163 HFS_SERVICE_PLACEMENT a service root (a Dockerfile, or a package.json other than the app's own and a workspace package's)
|
|
5
|
+
// exists only at be/apps/<service>/ (fe/apps/<app>/Dockerfile is a front end's);
|
|
6
|
+
// R164 HFS_IMAGE_UNPINNED in a multi-service product every component image of application-stacks.yaml is a digest or an exact
|
|
7
|
+
// version (x.y.z), never a missing tag, `latest`, a branch word or a major/minor-only tag;
|
|
8
|
+
// R165 HFS_SERVICE_STACK_DECLARATION each api or worker app of a multi-service product is a `role: service` component of the stack;
|
|
9
|
+
// R166 HFS_EVENT_CONTRACT the async contract is vendored: `be/contracts/<service>/events.json` equals the provider's
|
|
10
|
+
// `apps/<service>/src/events.ts` table (an event may declare `compensates: "<event>"`), every event a
|
|
11
|
+
// consumer's `apps/<app>/src/consumes.ts` names exists in that snapshot at the same version;
|
|
12
|
+
// R167 BE_ASYNC_SPEC_MISSING every consumed event (a consumes.ts entry) is named by an e2e spec through `useTestWorld`; the spec of a
|
|
13
|
+
// saga step (an event whose contract says `compensates`) also names the compensated event, so the whole
|
|
14
|
+
// flow is driven, not the undo alone.
|
|
15
|
+
// "Multi-service" = two or more be apps of kind api or worker. A single-service product is judged by R163 and R166-R167 only.
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { findStackDeclaration } from '../../lib/stack-declaration.mjs';
|
|
19
|
+
import { loadTypescript } from '../../lib/package-at.mjs';
|
|
20
|
+
import { propertyText } from '../../lib/ts-ast.mjs';
|
|
21
|
+
import { folded, readEventClasses, snapshotText } from '../../lib/event-contract.mjs';
|
|
22
|
+
import { found, readText } from './read.mjs';
|
|
23
|
+
|
|
24
|
+
const SERVICE_PLACEMENT = 'HFS_SERVICE_PLACEMENT';
|
|
25
|
+
const IMAGE_UNPINNED = 'HFS_IMAGE_UNPINNED';
|
|
26
|
+
const SERVICE_STACK_DECLARATION = 'HFS_SERVICE_STACK_DECLARATION';
|
|
27
|
+
const EVENT_CONTRACT = 'HFS_EVENT_CONTRACT';
|
|
28
|
+
const ASYNC_SPEC_MISSING = 'BE_ASYNC_SPEC_MISSING';
|
|
29
|
+
|
|
30
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const SERVICE_KINDS = new Set(['api', 'worker']);
|
|
32
|
+
const BE_SERVICE_FILE = /^be\/apps\/([^/]+)\/(.+)$/;
|
|
33
|
+
const DOCKERFILE = /(?:^|\/)Dockerfile(?:\.[^/]+)?$/;
|
|
34
|
+
const WORKSPACE_PACKAGE_JSON = /^fe\/(?:packages|apps)\/[^/]+\/package\.json$/;
|
|
35
|
+
const SNAPSHOT = /^be\/contracts\/([^/]+)\/events\.json$/;
|
|
36
|
+
const DIGEST = /@sha256:[0-9a-f]{64}$/;
|
|
37
|
+
const EXACT_TAG = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
38
|
+
const EVENT_CLASS_FILE = /^be\/src\/modules\/events\/([^/]+)\/[^/]+\.event\.ts$/;
|
|
39
|
+
const CONSUMER_FILE = /^be\/src\/features\/(?:[^/]+\/)+transport\/message\/[^/]+\.consumer\.ts$/;
|
|
40
|
+
const E2E_SPEC = /^be\/src\/tests\/e2e\/[^/]+\/[^/]+\.e2e-spec\.ts$/;
|
|
41
|
+
const USE_TEST_WORLD = /\buseTestWorld\s*\(/;
|
|
42
|
+
|
|
43
|
+
/** The be apps of kind api or worker. */
|
|
44
|
+
const servicesOf = (repo) => (repo.sides?.be?.apps ?? []).filter((app) => SERVICE_KINDS.has(app.kind));
|
|
45
|
+
|
|
46
|
+
/** The TypeScript compiler of the app (else of the runtime), or null. */
|
|
47
|
+
const typescriptFor = (repoRoot) => loadTypescript(repoRoot, HERE);
|
|
48
|
+
|
|
49
|
+
// ------------------------------------------------------------------------------------------------ R163 placement
|
|
50
|
+
|
|
51
|
+
/** Findings of R163: a service root outside `be/apps/<service>/`. */
|
|
52
|
+
function servicePlacementFindings({ files }) {
|
|
53
|
+
const findings = [];
|
|
54
|
+
for (const file of files) {
|
|
55
|
+
if (file.startsWith('.starciwork/') || file.startsWith('.starcistacks/') || file.includes('node_modules/')) continue;
|
|
56
|
+
const dockerfile = DOCKERFILE.test(file) && !BE_SERVICE_FILE.test(file) && !/^fe\/apps\/[^/]+\/Dockerfile(?:\.[^/]+)?$/.test(file) && file !== 'Dockerfile';
|
|
57
|
+
const manifest = path.posix.basename(file) === 'package.json' && file !== 'package.json' && !WORKSPACE_PACKAGE_JSON.test(file);
|
|
58
|
+
const rootDockerfile = file === 'Dockerfile';
|
|
59
|
+
if (!dockerfile && !manifest && !rootDockerfile) continue;
|
|
60
|
+
const what = manifest ? 'a package.json of its own' : 'a Dockerfile';
|
|
61
|
+
findings.push(found(SERVICE_PLACEMENT, file, `${file} gives a folder ${what}, which makes it a service root outside be/apps/<service>/; a back-end service is a Nest app at be/apps/<service>/ (own Dockerfile there) sharing the be/src libraries and the one package.json, never a folder of its own.`));
|
|
62
|
+
}
|
|
63
|
+
return findings;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ------------------------------------------------------------------------------------------------ R164 pinned images
|
|
67
|
+
|
|
68
|
+
/** The image reference with `${VAR:-default}` resolved to its default, or null when it cannot be (a bare `${VAR}`). */
|
|
69
|
+
function resolvedImage(image) {
|
|
70
|
+
let unresolved = false;
|
|
71
|
+
const resolved = image.replace(/\$\{[^}:]+(?::-([^}]*))?\}/g, (_all, fallback) => { if (fallback === undefined) unresolved = true; return fallback ?? ''; });
|
|
72
|
+
return unresolved ? null : resolved;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Why an image is not pinned, or null when it is: a digest, or an exact version tag. */
|
|
76
|
+
function unpinnedReason(image) {
|
|
77
|
+
const resolved = resolvedImage(image);
|
|
78
|
+
if (resolved === null) return 'its reference is an environment variable with no default';
|
|
79
|
+
if (DIGEST.test(resolved)) return null;
|
|
80
|
+
const name = resolved.slice(resolved.lastIndexOf('/') + 1);
|
|
81
|
+
const colon = name.indexOf(':');
|
|
82
|
+
if (colon < 0) return 'it has no tag';
|
|
83
|
+
const tag = name.slice(colon + 1);
|
|
84
|
+
return EXACT_TAG.test(tag) ? null : `its tag \`${tag}\` is not an exact version (x.y.z) or a digest, so it moves`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Findings of R164 over a multi-service product's stack declaration. */
|
|
88
|
+
function imagePinFindings({ repoRoot, repo }) {
|
|
89
|
+
if (servicesOf(repo).length < 2) return [];
|
|
90
|
+
const declaration = findStackDeclaration(repoRoot);
|
|
91
|
+
const components = declaration.doc?.components;
|
|
92
|
+
if (components === null || typeof components !== 'object' || components === undefined) return [];
|
|
93
|
+
const file = '.starcistacks/application-stacks.yaml';
|
|
94
|
+
const findings = [];
|
|
95
|
+
for (const [name, component] of Object.entries(components)) {
|
|
96
|
+
const image = component?.image;
|
|
97
|
+
if (typeof image !== 'string') continue;
|
|
98
|
+
const reason = unpinnedReason(image);
|
|
99
|
+
if (reason !== null) findings.push(found(IMAGE_UNPINNED, file, `${file} components.${name}.image ${image}: ${reason}. A product with more than one service pins every image: an exact version (\`<repo>/<service>:1.2.3\`, \`postgres:16.4\`) or a digest (\`@sha256:...\`).`, { component: name, image }));
|
|
100
|
+
}
|
|
101
|
+
return findings;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ------------------------------------------------------------------------------------------------ R165 stack declaration
|
|
105
|
+
|
|
106
|
+
/** Findings of R165: each service app of a multi-service product is a `role: service` component. */
|
|
107
|
+
function serviceStackFindings({ repoRoot, repo }) {
|
|
108
|
+
const services = servicesOf(repo);
|
|
109
|
+
if (services.length < 2) return [];
|
|
110
|
+
const declaration = findStackDeclaration(repoRoot);
|
|
111
|
+
if (declaration.doc === undefined) return [];
|
|
112
|
+
const components = declaration.doc.components ?? {};
|
|
113
|
+
const file = '.starcistacks/application-stacks.yaml';
|
|
114
|
+
const findings = [];
|
|
115
|
+
for (const app of services) {
|
|
116
|
+
const component = components[app.name];
|
|
117
|
+
if (component === null || typeof component !== 'object') findings.push(found(SERVICE_STACK_DECLARATION, file, `${file} declares no component \`${app.name}\` for the ${app.kind} app be/apps/${app.name}; every service of a multi-service product is a \`role: service\` component (own image \`<repo>/${app.name}:<exact version>\`, built from be/apps/${app.name}/Dockerfile).`, { app: app.name }));
|
|
118
|
+
else if (component.role !== 'service') findings.push(found(SERVICE_STACK_DECLARATION, file, `${file} components.${app.name} is the stack's face of be/apps/${app.name} and must be \`role: service\`, not \`${String(component.role)}\`.`, { app: app.name }));
|
|
119
|
+
}
|
|
120
|
+
return findings;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ------------------------------------------------------------------------------------------------ R166 event contract
|
|
124
|
+
|
|
125
|
+
/** The typed event classes of every service: `[{ service, file, className, name, version, compensates, payload }]` and the problems of the ones that cannot be read. */
|
|
126
|
+
function eventClassesOf({ repoRoot, files, ts }) {
|
|
127
|
+
const classes = [];
|
|
128
|
+
const problems = [];
|
|
129
|
+
for (const file of files.filter((candidate) => EVENT_CLASS_FILE.test(candidate)).sort()) {
|
|
130
|
+
const service = EVENT_CLASS_FILE.exec(file)[1];
|
|
131
|
+
const read = readEventClasses(ts, readText(repoRoot, file) ?? '', file);
|
|
132
|
+
for (const event of read.events) classes.push({ service, file, ...event });
|
|
133
|
+
for (const problem of read.problems) problems.push({ service, file, problem });
|
|
134
|
+
}
|
|
135
|
+
return { classes, problems };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Findings of R166: each service's event classes are the one source of its vendored `events.json`, and a compensating event names a declared event. */
|
|
139
|
+
function eventContractFindings({ repoRoot, files }) {
|
|
140
|
+
const classFiles = files.filter((file) => EVENT_CLASS_FILE.test(file));
|
|
141
|
+
const snapshots = files.filter((file) => SNAPSHOT.test(file));
|
|
142
|
+
if (classFiles.length === 0 && snapshots.length === 0) return [];
|
|
143
|
+
const ts = typescriptFor(repoRoot);
|
|
144
|
+
if (ts === null) return [];
|
|
145
|
+
const tracked = new Set(files);
|
|
146
|
+
const { classes, problems } = eventClassesOf({ repoRoot, files, ts });
|
|
147
|
+
const findings = problems.map(({ service, file, problem }) => found(EVENT_CONTRACT, file, `${file} is not an event class the contract can be emitted from: ${problem}.`, { service }));
|
|
148
|
+
const services = [...new Set(classes.map((event) => event.service))].sort();
|
|
149
|
+
const declared = new Set(classes.map((event) => event.name));
|
|
150
|
+
for (const service of services) {
|
|
151
|
+
const snapshot = `be/contracts/${service}/events.json`;
|
|
152
|
+
const events = classes.filter((event) => event.service === service);
|
|
153
|
+
if (!tracked.has(snapshot)) findings.push(found(EVENT_CONTRACT, snapshot, `${service} declares event classes (be/src/modules/events/${service}/) but ${snapshot} is not committed; run \`npm run contract:emit\` and commit the snapshot, the contract its consumers rely on.`, { service }));
|
|
154
|
+
else if (folded(readText(repoRoot, snapshot) ?? '') !== snapshotText(service, events)) findings.push(found(EVENT_CONTRACT, snapshot, `${snapshot} differs from what the event classes of ${service} emit now; run \`npm run contract:emit\` and commit the result.`, { service, drift: 'stale' }));
|
|
155
|
+
}
|
|
156
|
+
for (const snapshot of snapshots) {
|
|
157
|
+
const service = SNAPSHOT.exec(snapshot)[1];
|
|
158
|
+
if (!services.includes(service) && !problems.some((entry) => entry.service === service)) findings.push(found(EVENT_CONTRACT, snapshot, `${snapshot} is committed but be/src/modules/events/${service}/ declares no event class; delete the snapshot or restore the classes.`, { service, drift: 'left-behind' }));
|
|
159
|
+
}
|
|
160
|
+
for (const event of classes) {
|
|
161
|
+
if (event.compensates !== undefined && !declared.has(event.compensates)) findings.push(found(EVENT_CONTRACT, event.file, `${event.file}: event "${event.name}" compensates "${event.compensates}", which no event class declares; name the event whose step it undoes.`, { service: event.service, event: event.name }));
|
|
162
|
+
}
|
|
163
|
+
return findings;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ------------------------------------------------------------------------------------------------ R167 async specs
|
|
167
|
+
|
|
168
|
+
/** The class an expression names, from `readonly event = <Class>` of a consumer: the identifier, or null. */
|
|
169
|
+
function consumedClassOf(ts, text) {
|
|
170
|
+
return propertyText(ts, text, { file: 'consumer.ts', key: 'event', declaration: true, kind: 'identifier' });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The events the consumers consume: `[{ file, className, service, event }]` from `readonly event = <EventClass>` of every `transport/message/*.consumer.ts`. */
|
|
174
|
+
function consumedEvents({ repoRoot, files, ts }) {
|
|
175
|
+
const { classes } = eventClassesOf({ repoRoot, files, ts });
|
|
176
|
+
const consumed = [];
|
|
177
|
+
for (const file of files.filter((candidate) => CONSUMER_FILE.test(candidate))) {
|
|
178
|
+
const className = consumedClassOf(ts, readText(repoRoot, file) ?? '');
|
|
179
|
+
const event = classes.find((candidate) => candidate.className === className);
|
|
180
|
+
if (event !== undefined) consumed.push({ file, className, service: event.service, event: event.name, compensates: event.compensates });
|
|
181
|
+
}
|
|
182
|
+
return consumed;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Findings of R167: every consumed event has an e2e spec through the world; a saga step's spec also names the event it compensates. */
|
|
186
|
+
function asyncSpecFindings({ repoRoot, files }) {
|
|
187
|
+
const ts = typescriptFor(repoRoot);
|
|
188
|
+
if (ts === null) return [];
|
|
189
|
+
const consumed = consumedEvents({ repoRoot, files, ts });
|
|
190
|
+
if (consumed.length === 0) return [];
|
|
191
|
+
const specs = files
|
|
192
|
+
.filter((file) => E2E_SPEC.test(file))
|
|
193
|
+
.map((file) => ({ file, text: readText(repoRoot, file) ?? '' }))
|
|
194
|
+
.filter(({ text }) => USE_TEST_WORLD.test(text));
|
|
195
|
+
const findings = [];
|
|
196
|
+
for (const { file, className, service, event, compensates } of consumed) {
|
|
197
|
+
const named = specs.filter(({ text }) => text.includes(event));
|
|
198
|
+
if (named.length === 0) {
|
|
199
|
+
findings.push(found(ASYNC_SPEC_MISSING, file, `${file}: the consumer of ${service} event "${event}" (${className}) has no e2e spec; add be/src/tests/e2e/<area>/<name>.e2e-spec.ts, which boots the apps with useTestWorld and names it: publish the event, read the persisted effect back and redeliver it.`, { service, event }));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (typeof compensates === 'string' && !named.some(({ text }) => text.includes(compensates))) {
|
|
203
|
+
findings.push(found(ASYNC_SPEC_MISSING, named[0].file, `${named[0].file} is the spec of the saga step "${event}", which undoes "${compensates}", but never names it; the spec drives the whole flow: the step, the failure, then the compensated state.`, { service, event, compensates }));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return findings;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ------------------------------------------------------------------------------------------------ entry
|
|
210
|
+
|
|
211
|
+
/** Every finding of the microservice policy over the app at `repoRoot`; `files` are its tracked app-relative paths. */
|
|
212
|
+
export function serviceFindings({ repoRoot, files, repo }) {
|
|
213
|
+
return [
|
|
214
|
+
...servicePlacementFindings({ files }),
|
|
215
|
+
...imagePinFindings({ repoRoot, repo }),
|
|
216
|
+
...serviceStackFindings({ repoRoot, repo }),
|
|
217
|
+
...eventContractFindings({ repoRoot, files }),
|
|
218
|
+
...asyncSpecFindings({ repoRoot, files }),
|
|
219
|
+
];
|
|
220
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// finding names the rule rather than only the missing slot. Which folder is a layer is read from the slot manifest, never spelled here.
|
|
8
8
|
import { found } from './read.mjs';
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
const SPEC_PLACEMENT = 'BE_SPEC_PLACEMENT';
|
|
11
11
|
const SPEC_FILE = /(?:\.(?:spec|test)|-spec)\.[cm]?[jt]sx?$/;
|
|
12
12
|
const TEST_SLOT_TESTS = new Set(['unit-beside', 'e2e']);
|
|
13
13
|
|
|
@@ -10,9 +10,9 @@ import { braceVariants, globExpression } from '../../lib/glob.mjs';
|
|
|
10
10
|
import { declaredStack, findStackDeclaration, STACK_ROOT, text } from '../../lib/stack-declaration.mjs';
|
|
11
11
|
import { found } from './read.mjs';
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
const STACKS_SHAPE = 'HFS_STACKS_SHAPE';
|
|
14
|
+
const STACKS_SLOT = 'app.starcistacks';
|
|
15
|
+
const HOST_SONAR_ROOT = '.claude/ext/sonar';
|
|
16
16
|
const SEALED = /\.enc$/;
|
|
17
17
|
const INSIDE_SECRETS = /^[^/]+\/secrets\/[^/]+\.enc$/;
|
|
18
18
|
const RETIRED_ROOT = /^\.?stacks(?:\/|$)/;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// supabase-secrets.mjs - L17 extends the one R06 secret scan for edition lite. It keeps the ordinary
|
|
2
|
+
// HFS_PLAINTEXT_SECRET judgement, then adds the reduced lite custody tree and provider-shaped literals
|
|
3
|
+
// which are meaningful only in the Supabase/FE surface. Findings name shapes, never captured values.
|
|
4
|
+
import { Buffer } from 'node:buffer';
|
|
5
|
+
import { secretFindings } from './secrets.mjs';
|
|
6
|
+
import { found, readText } from './read.mjs';
|
|
7
|
+
|
|
8
|
+
const LITE_SECRET_CUSTODY = 'HFS_LITE_SECRET_CUSTODY';
|
|
9
|
+
|
|
10
|
+
const LOCKFILE = /(^|\/)package-lock\.json$/u;
|
|
11
|
+
const ROOT_ENV = /^\.env(?:\..*)?$/iu;
|
|
12
|
+
const SECRETS_ENV = /^secrets\.env$/iu;
|
|
13
|
+
const SECRETS_DIRECTORY = /^\.secrets(?:\/|$)/iu;
|
|
14
|
+
const SEALED_HOME = /^\.starcistacks\/[^/]+\/secrets\/[^/]+\.enc$/iu;
|
|
15
|
+
|
|
16
|
+
const LITE_SECRET_PATTERNS = Object.freeze([
|
|
17
|
+
{ name: 'supabase-personal-access-token', re: /\bsbp_[A-Za-z0-9_-]{20,}\b/u },
|
|
18
|
+
{ name: 'supabase-publishable-key', re: /\bsb_publishable_[A-Za-z0-9_-]{20,}\b/u },
|
|
19
|
+
{ name: 'supabase-secret-token', re: /\bsb_secret_[A-Za-z0-9_-]{20,}\b/u },
|
|
20
|
+
{ name: 'password-connection-string', re: /\b(?:postgres(?:ql)?|mysql|mariadb|mongodb(?:\+srv)?|redis):\/\/[^:\s/@]+:[^@\s/${}]{8,}@/iu },
|
|
21
|
+
]);
|
|
22
|
+
const JWT_CANDIDATE = /(?<![A-Za-z0-9_-])eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9_-])/gu;
|
|
23
|
+
|
|
24
|
+
const jsonObject = segment => {
|
|
25
|
+
try {
|
|
26
|
+
const value = JSON.parse(Buffer.from(segment, 'base64url').toString('utf8'));
|
|
27
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const validJwt = candidate => {
|
|
34
|
+
const [header, payload, signature, extra] = candidate.split('.');
|
|
35
|
+
if (extra !== undefined || !signature) return false;
|
|
36
|
+
const decodedHeader = jsonObject(header);
|
|
37
|
+
const decodedPayload = jsonObject(payload);
|
|
38
|
+
return typeof decodedHeader?.alg === 'string' && decodedHeader.alg.toLowerCase() !== 'none' && decodedPayload !== null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const liteSecretPatterns = line => {
|
|
42
|
+
const names = LITE_SECRET_PATTERNS.filter(pattern => pattern.re.test(line)).map(pattern => pattern.name);
|
|
43
|
+
if ([...line.matchAll(JWT_CANDIDATE)].some(match => validJwt(match[0]))) names.push('supabase-eyj-service-token');
|
|
44
|
+
return names;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const inSecretFreeTree = (repo, file) => repo.profile === 'fe' || file.startsWith('fe/') || file.startsWith('supabase/');
|
|
48
|
+
|
|
49
|
+
/** Raw L17 findings. The catalog-driven findings filter decides which editions judge them. */
|
|
50
|
+
export function liteSecretCustodyFindings({ repoRoot, files, repo }) {
|
|
51
|
+
const findings = [];
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
if (repo.profile === 'app') {
|
|
54
|
+
if (ROOT_ENV.test(file) || SECRETS_ENV.test(file) || SECRETS_DIRECTORY.test(file)) {
|
|
55
|
+
findings.push(found(LITE_SECRET_CUSTODY, file, `${file} is forbidden at the lite app root; lite keeps no .env*, secrets.env or .secrets/ custody path.`, { pattern: 'lite-forbidden-entry' }));
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (file.endsWith('.enc') && !SEALED_HOME.test(file)) {
|
|
59
|
+
findings.push(found(LITE_SECRET_CUSTODY, file, `${file} is a sealed file outside .starcistacks/<env>/secrets/; lite has exactly one encrypted custody path.`, { pattern: 'sealed-file-home' }));
|
|
60
|
+
}
|
|
61
|
+
} else if (file.endsWith('.enc')) {
|
|
62
|
+
findings.push(found(LITE_SECRET_CUSTODY, file, `${file} is a sealed file inside a side tree; lite keeps *.enc only at the app root under .starcistacks/<env>/secrets/.`, { pattern: 'sealed-file-home' }));
|
|
63
|
+
}
|
|
64
|
+
if (!inSecretFreeTree(repo, file) || LOCKFILE.test(file)) continue;
|
|
65
|
+
const text = readText(repoRoot, file);
|
|
66
|
+
if (text === null) continue;
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
const lines = text.split(/\r?\n/u);
|
|
69
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
70
|
+
for (const pattern of liteSecretPatterns(lines[index])) {
|
|
71
|
+
if (seen.has(pattern)) continue;
|
|
72
|
+
seen.add(pattern);
|
|
73
|
+
findings.push(found(LITE_SECRET_CUSTODY, file, `${file}:${index + 1} holds a provider-shaped secret (${pattern}); lite keeps no secret literal under fe/ or supabase/.`, { line: index + 1, pattern }));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return findings;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Drop-in extension of secretFindings for scripts/hfs/check.mjs. When an L17 finding identifies the same file/line as
|
|
82
|
+
* the generic R06 scanner (a complete JWT, for example), the specific lite code wins so one physical issue is emitted once.
|
|
83
|
+
*/
|
|
84
|
+
export function supabaseSecretFindings(input) {
|
|
85
|
+
const ordinary = secretFindings(input);
|
|
86
|
+
const lite = liteSecretCustodyFindings(input);
|
|
87
|
+
const liteJwtAuthority = (input.repo.edition ?? 'full') === 'lite';
|
|
88
|
+
return [...ordinary.filter(item => !(liteJwtAuthority && item.pattern === 'jwt' && inSecretFreeTree(input.repo, item.path))), ...lite];
|
|
89
|
+
}
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
// - one `jest.config.js` at the root: a second jest config (`jest.config.<x>.js`, `jest.<x>.config.*`, one per app) and a
|
|
5
5
|
// `jest` key in a package.json are refused.
|
|
6
6
|
// `int-spec`, `harness-spec`, the retired test folders and the per-lane configs under src/tests are the architecture machine's
|
|
7
|
-
// (
|
|
7
|
+
// (reported under this same code, BE_TEST_TOPOLOGY); that jest.config.js is the rendered one (projects `unit`, `integration`, `e2e` and
|
|
8
8
|
// `contract`, diagnostics off) is the managed-file check's (HFS_MANAGED_FILE_DRIFT); a spec whose suffix disagrees with its src/tests folder has no slot (HFS_SLOT_UNDECLARED).
|
|
9
9
|
import { found, readJson } from './read.mjs';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const TEST_TOPOLOGY = 'BE_TEST_TOPOLOGY';
|
|
12
12
|
const DOT_TEST = /\.test\.[cm]?[jt]sx?$/;
|
|
13
13
|
const JEST_CONFIG = /(?:^|\/)jest(?:\.[^/]+)*\.config(?:\.[^/]+)*\.(?:[cm]?[jt]s|json)$|(?:^|\/)jest\.config\.[^/]+$/;
|
|
14
14
|
const ROOT_JEST_CONFIG = 'jest.config.js';
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// secret.mjs - `starci app secret list|show|set|gen`: the canon command over an app's sealed secrets.
|
|
2
|
+
// (R06: a secret exists only as a sops envelope there). It is the one way an operator or an agent reads or writes one; a plain
|
|
3
|
+
// `sops` command in a KEYS.md or a runbook is retired. Every sops use goes through scripts/api/sops (decrypt.mjs, seal.mjs):
|
|
4
|
+
// starci app secret list [--env <name>] every secret of the env and the keys it holds (names only, nothing decrypted)
|
|
5
|
+
// starci app secret show <slug> [--key <NAME>] [--env <name>] decrypt one value to stdout
|
|
6
|
+
// starci app secret set <slug> [--key <NAME>] [--age <recipient>]... [--env <name>]
|
|
7
|
+
// the value is read from stdin (never from an argument); a new secret takes its
|
|
8
|
+
// recipients from --age, else from the repository's .sops.yaml
|
|
9
|
+
// starci app secret gen <slug> [--key <NAME>] [--bytes <n>] [--age <recipient>]... [--env <name>]
|
|
10
|
+
// a random value (base64url of n bytes, default 32) sealed without being printed
|
|
11
|
+
// A single-value secret holds its value under the key `data`. Every verb runs at the app root (the folder of hfs.json), or at --cwd.
|
|
12
|
+
// Exit 0 done, 2 a refusal or bad usage (a secret value is never part of a message).
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { runProgram } from '../api/process/run-program.mjs';
|
|
16
|
+
import { resolveRealTool } from '../api/process/resolve-real-tool.mjs';
|
|
17
|
+
const sopsInvocation = Object.freeze({runProgram,resolveRealTool});
|
|
18
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
19
|
+
import { claimManager } from '../connectors/lib.mjs';
|
|
20
|
+
import { sha256 } from '../../engine/digest.mjs';
|
|
21
|
+
import { isSopsEnvelope } from '../lib/sops-envelope.mjs';
|
|
22
|
+
import { decrypt as sopsDecrypt } from '../api/sops/decrypt.mjs';
|
|
23
|
+
import { seal as sopsSeal } from '../api/sops/seal.mjs';
|
|
24
|
+
|
|
25
|
+
class SecretError extends Error {}
|
|
26
|
+
// claimManager is process-owned; refuse a recursive same-process write as well.
|
|
27
|
+
const writingSecrets = new Set();
|
|
28
|
+
|
|
29
|
+
const SLUG = /^[a-z0-9][a-z0-9-]*$/u;
|
|
30
|
+
const KEY = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
31
|
+
const DEFAULT_KEY = 'data';
|
|
32
|
+
const VERBS = new Set(['list', 'show', 'set', 'gen']);
|
|
33
|
+
|
|
34
|
+
/** The envelope format of a sealed secret, read from its text: json, yaml or dotenv. */
|
|
35
|
+
export function formatOf(text) {
|
|
36
|
+
const body = String(text).trimStart();
|
|
37
|
+
if (body.startsWith('{')) return 'json';
|
|
38
|
+
if (/^sops:\s*$/mu.test(text)) return 'yaml';
|
|
39
|
+
return 'dotenv';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The key names a sealed document holds and the age recipients it is sealed to, read without decrypting anything. */
|
|
43
|
+
export function envelopeOf(text) {
|
|
44
|
+
const format = formatOf(text);
|
|
45
|
+
if (format === 'json') {
|
|
46
|
+
const doc = JSON.parse(text);
|
|
47
|
+
return { format, keys: Object.keys(doc).filter((name) => name !== 'sops'), recipients: (doc.sops?.age ?? []).map((entry) => entry.recipient).filter(Boolean) };
|
|
48
|
+
}
|
|
49
|
+
if (format === 'yaml') {
|
|
50
|
+
const keys = [...text.matchAll(/^([A-Za-z_][A-Za-z0-9_]*):/gmu)].map((match) => match[1]).filter((name) => name !== 'sops');
|
|
51
|
+
return { format, keys, recipients: [...text.matchAll(/^\s+-?\s*recipient:\s*(\S+)/gmu)].map((match) => match[1]) };
|
|
52
|
+
}
|
|
53
|
+
const keys = [...text.matchAll(/^([A-Za-z_][A-Za-z0-9_]*)=/gmu)].map((match) => match[1]).filter((name) => !name.startsWith('sops_'));
|
|
54
|
+
return { format, keys, recipients: [...text.matchAll(/^sops_age__list_\d+__map_recipient=(\S+)/gmu)].map((match) => match[1]) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The plaintext document `map` in the given format. A dotenv value cannot hold a line break. */
|
|
58
|
+
export function plaintextOf(format, map) {
|
|
59
|
+
if (format === 'json') return `${JSON.stringify(map)}\n`;
|
|
60
|
+
if (format === 'yaml') return `${Object.entries(map).map(([name, value]) => `${name}: ${JSON.stringify(value)}`).join('\n')}\n`;
|
|
61
|
+
for (const [name, value] of Object.entries(map)) if (/[\r\n]/u.test(value)) throw new SecretError(`${name} holds a line break; a dotenv secret holds one-line values only`);
|
|
62
|
+
return `${Object.entries(map).map(([name, value]) => `${name}=${value}`).join('\n')}\n`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The directory of the sealed secrets of the app at `repoRoot` for `env`; the one env that has secrets when none is named. */
|
|
66
|
+
function secretsDirectory(repoRoot, env) {
|
|
67
|
+
const stacks = path.join(repoRoot, '.starcistacks');
|
|
68
|
+
if (!fs.existsSync(stacks)) throw new SecretError('this app has no .starcistacks; run starci app secret at the app root, or pass --cwd');
|
|
69
|
+
if (env) {
|
|
70
|
+
if (!/^[a-z0-9][a-z0-9-]*$/u.test(env)) throw new SecretError(`--env ${env} is not an environment name`);
|
|
71
|
+
return path.join(stacks, env, 'secrets');
|
|
72
|
+
}
|
|
73
|
+
const withSecrets = fs.readdirSync(stacks, { withFileTypes: true }).filter((entry) => entry.isDirectory() && fs.existsSync(path.join(stacks, entry.name, 'secrets'))).map((entry) => entry.name);
|
|
74
|
+
if (withSecrets.length === 1) return path.join(stacks, withSecrets[0], 'secrets');
|
|
75
|
+
throw new SecretError(withSecrets.length === 0 ? 'no environment of .starcistacks has a secrets folder; pass --env to create the first secret' : `more than one environment has secrets (${withSecrets.join(', ')}); pass --env`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const fileOf = (directory, slug) => {
|
|
79
|
+
if (!SLUG.test(slug ?? '')) throw new SecretError(`${slug === undefined ? 'a secret name' : slug} is not a secret slug: lowercase letters, digits and dashes`);
|
|
80
|
+
return path.join(directory, `${slug}.enc`);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** The default sops seam: the runtime's sops api, which finds the binary on this machine. */
|
|
84
|
+
function defaultSops(env = process.env) {
|
|
85
|
+
const refuse = (what, result) => {
|
|
86
|
+
if (result.error?.identityRefusal) return Object.assign(new SecretError(result.error.message), { identityRefusal: result.error.identityRefusal });
|
|
87
|
+
if (result.error?.code === 'SOPS_MISSING') return new SecretError(result.error.message);
|
|
88
|
+
return new SecretError(`sops could not ${what} (exit ${String(result.status)})`);
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
decrypt(file, format) {
|
|
92
|
+
const result = sopsDecrypt(null, ['decrypt', '--input-type', format, '--output-type', 'json', file], { env, invocation: sopsInvocation });
|
|
93
|
+
if (result.status !== 0) throw refuse(`decrypt ${path.basename(file)}; is the age identity installed?`, result);
|
|
94
|
+
try { return JSON.parse(result.stdout); } catch { throw new SecretError('sops returned an invalid plaintext document'); }
|
|
95
|
+
},
|
|
96
|
+
seal(request) {
|
|
97
|
+
const result = sopsSeal(null, request, { env, invocation: sopsInvocation });
|
|
98
|
+
if (result.status !== 0) throw refuse('seal the secret', result);
|
|
99
|
+
return result.stdout;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parse(argv) {
|
|
105
|
+
const opts = { positional: [], age: [] };
|
|
106
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
107
|
+
const arg = argv[i];
|
|
108
|
+
if (['--cwd', '--env', '--key', '--bytes', '--age'].includes(arg)) {
|
|
109
|
+
if (argv[i + 1] === undefined) throw new SecretError(`${arg} needs a value`);
|
|
110
|
+
if (arg === '--age') opts.age.push(argv[i + 1]); else opts[arg.slice(2)] = argv[i + 1];
|
|
111
|
+
i += 1;
|
|
112
|
+
} else if (arg.startsWith('--')) throw new SecretError(`unknown flag ${arg}`);
|
|
113
|
+
else opts.positional.push(arg);
|
|
114
|
+
}
|
|
115
|
+
return opts;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const readStdin = () => fs.readFileSync(0, 'utf8');
|
|
119
|
+
|
|
120
|
+
/** Seal `key = value` into the secret `slug`: an existing document keeps its other keys and its recipients. */
|
|
121
|
+
function writeSecret({ file, values, age, sops, processEnv = process.env }) {
|
|
122
|
+
for (const key of Object.keys(values)) if (!KEY.test(key)) throw new SecretError(`${key} is not a key name`);
|
|
123
|
+
const absolute = path.resolve(file), target = process.platform === 'win32' ? absolute.toLowerCase() : absolute;
|
|
124
|
+
const name = `sealed-secret-${sha256(target)}`;
|
|
125
|
+
if (writingSecrets.has(name)) throw new SecretError('the sealed secret is being updated; retry after that write settles');
|
|
126
|
+
const held = claimManager(name, { env: processEnv });
|
|
127
|
+
if (!held.ok) throw new SecretError('the sealed secret is being updated; retry after that write settles');
|
|
128
|
+
writingSecrets.add(name);
|
|
129
|
+
try {
|
|
130
|
+
let format = 'json';
|
|
131
|
+
let map = {};
|
|
132
|
+
let recipients = age;
|
|
133
|
+
if (fs.existsSync(file)) {
|
|
134
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
135
|
+
const envelope = envelopeOf(text);
|
|
136
|
+
format = envelope.format;
|
|
137
|
+
map = sops.decrypt(file, format);
|
|
138
|
+
if (!age.length) recipients = envelope.recipients;
|
|
139
|
+
}
|
|
140
|
+
Object.assign(map, values);
|
|
141
|
+
const sealed = sops.seal({ inputType: format, plaintext: plaintextOf(format, map), recipients, filenameOverride: file });
|
|
142
|
+
let parsed;
|
|
143
|
+
try { parsed = envelopeOf(sealed); } catch { throw new SecretError('sops returned an invalid sealed document'); }
|
|
144
|
+
if (!isSopsEnvelope(sealed) || parsed.keys.length !== Object.keys(map).length || !parsed.keys.every(key => Object.hasOwn(map, key))) throw new SecretError('sops returned an invalid sealed document');
|
|
145
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
146
|
+
const temporary = path.join(path.dirname(file), `.${path.basename(file)}-${randomUUID()}.tmp`);
|
|
147
|
+
try {
|
|
148
|
+
fs.writeFileSync(temporary, sealed, { flag: 'wx', mode: 0o600, flush: true });
|
|
149
|
+
fs.renameSync(temporary, file);
|
|
150
|
+
} finally { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); }
|
|
151
|
+
} finally { writingSecrets.delete(name); held.release(); }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Update named keys through the same canonical sealed-secret owner used by the CLI. */
|
|
155
|
+
export function setSecretValues(repoRoot, { slug, values, env = null }, { sops = null, age = [], processEnv = process.env } = {}) {
|
|
156
|
+
const directory = secretsDirectory(repoRoot, env);
|
|
157
|
+
const file = fileOf(directory, slug);
|
|
158
|
+
if (!values || typeof values !== 'object' || Array.isArray(values) || !Object.keys(values).length) throw new SecretError('secret update requires named values');
|
|
159
|
+
for (const value of Object.values(values)) if (typeof value !== 'string' || value === '') throw new SecretError('secret values must be nonempty strings');
|
|
160
|
+
writeSecret({ file, values, age, sops: sops ?? defaultSops(processEnv), processEnv });
|
|
161
|
+
return { file, via: 'sealed-secret' };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** `starci app secret <verb> ...`; `stdin` returns the value `set` seals, `sops` and `random` are test seams. */
|
|
165
|
+
export function secretMain(argv, { stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s), stdin = readStdin, sops = null, random = randomBytes, env = process.env, cwd = process.cwd() } = {}) {
|
|
166
|
+
const [verb, ...rest] = argv;
|
|
167
|
+
try {
|
|
168
|
+
if (!VERBS.has(verb)) throw new SecretError('usage: starci app secret list | show <slug> | set <slug> | gen <slug> [--key NAME] [--env NAME] [--age RECIPIENT] [--bytes N] [--cwd DIR]');
|
|
169
|
+
const opts = parse(rest);
|
|
170
|
+
const repoRoot = path.resolve(cwd, opts.cwd ?? '.');
|
|
171
|
+
const directory = secretsDirectory(repoRoot, opts.env);
|
|
172
|
+
const key = opts.key ?? DEFAULT_KEY;
|
|
173
|
+
if (verb === 'list') {
|
|
174
|
+
if (opts.positional.length) throw new SecretError('starci app secret list takes no name');
|
|
175
|
+
const files = fs.existsSync(directory) ? fs.readdirSync(directory).filter((name) => name.endsWith('.enc')).sort() : [];
|
|
176
|
+
for (const name of files) stdout(`${name.slice(0, -'.enc'.length)} ${envelopeOf(fs.readFileSync(path.join(directory, name), 'utf8')).keys.join(', ')}\n`);
|
|
177
|
+
stdout(`${files.length} secret${files.length === 1 ? '' : 's'} in ${path.relative(repoRoot, directory).split(path.sep).join('/')}\n`);
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
if (opts.positional.length !== 1) throw new SecretError(`starci app secret ${verb} takes one secret name`);
|
|
181
|
+
const file = fileOf(directory, opts.positional[0]);
|
|
182
|
+
const seam = sops ?? defaultSops(env);
|
|
183
|
+
if (verb === 'show') {
|
|
184
|
+
if (!fs.existsSync(file)) throw new SecretError(`${opts.positional[0]} is not sealed in this env`);
|
|
185
|
+
const { format, keys } = envelopeOf(fs.readFileSync(file, 'utf8'));
|
|
186
|
+
if (opts.key === undefined && !keys.includes(DEFAULT_KEY) && keys.length !== 1) throw new SecretError(`${opts.positional[0]} holds ${keys.join(', ')}; name one with --key`);
|
|
187
|
+
const map = seam.decrypt(file, format);
|
|
188
|
+
const name = opts.key ?? (keys.includes(DEFAULT_KEY) ? DEFAULT_KEY : keys[0]);
|
|
189
|
+
if (!Object.hasOwn(map, name)) throw new SecretError(`${opts.positional[0]} has no key ${name}`);
|
|
190
|
+
stdout(`${String(map[name])}\n`);
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
if (verb === 'set') {
|
|
194
|
+
const value = String(stdin()).replace(/\r?\n$/u, '');
|
|
195
|
+
if (value === '') throw new SecretError('the value on stdin is empty');
|
|
196
|
+
writeSecret({ file, values: { [key]: value }, age: opts.age, sops: seam, processEnv: env });
|
|
197
|
+
stdout(`sealed ${opts.positional[0]} (${key})\n`);
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
const bytes = opts.bytes === undefined ? 32 : Number(opts.bytes);
|
|
201
|
+
if (!Number.isInteger(bytes) || bytes < 16 || bytes > 1024) throw new SecretError('--bytes is a whole number from 16 to 1024');
|
|
202
|
+
writeSecret({ file, values: { [key]: random(bytes).toString('base64url') }, age: opts.age, sops: seam, processEnv: env });
|
|
203
|
+
stdout(`generated and sealed ${opts.positional[0]} (${key}, ${bytes} bytes); read it with starci app secret show\n`);
|
|
204
|
+
return 0;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (error instanceof SecretError || error instanceof SyntaxError) { stderr(`starci app secret: ${error.message}\n`); return 2; }
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|