@starci/hfs 4.3.0 → 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 +11 -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 +3401 -152
- package/runtime/knowledge/hfs/slots.yaml +344 -62
- package/runtime/knowledge/patterns/be/api.yaml +32 -5
- package/runtime/knowledge/patterns/be/cli.yaml +14 -4
- package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
- package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
- package/runtime/knowledge/patterns/be/projections.yaml +4 -2
- package/runtime/knowledge/patterns/be/queues.yaml +11 -3
- package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
- package/runtime/knowledge/patterns/be/saga.yaml +20 -3
- package/runtime/knowledge/patterns/be/webhooks.yaml +23 -4
- package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +3113 -43
- 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/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/selected-age-recipient.mjs +7 -0
- package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
- package/runtime/scripts/api/supabase/database-query.mjs +5 -0
- package/runtime/scripts/api/supabase/lib.mjs +44 -0
- package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
- package/runtime/scripts/hfs/app-wiring.mjs +201 -0
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +1 -8
- package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
- package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
- package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
- package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
- package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
- package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
- package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
- package/runtime/scripts/hfs/architecture/register-once.mjs +2 -2
- package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
- package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
- package/runtime/scripts/hfs/architecture/tiers.mjs +3 -3
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
- package/runtime/scripts/hfs/check-results.mjs +28 -0
- package/runtime/scripts/hfs/check.mjs +46 -47
- package/runtime/scripts/hfs/coverage-from.mjs +70 -0
- package/runtime/scripts/hfs/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 +67 -7
- 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 +286 -7
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
- package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
- package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
- package/runtime/scripts/hfs/rules/database.mjs +9 -0
- package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
- package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
- package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
- package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
- package/runtime/scripts/hfs/rules/docker.mjs +24 -7
- package/runtime/scripts/hfs/rules/edition.mjs +9 -8
- package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
- package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
- package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
- package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
- package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
- package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
- package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
- package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
- package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
- package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
- package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
- package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
- package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
- package/runtime/scripts/hfs/rules/saga.mjs +8 -16
- package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
- package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
- package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
- package/runtime/scripts/hfs/rules/services.mjs +4 -4
- package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
- package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
- package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
- package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +41 -1
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +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-edit.mjs +170 -0
- package/runtime/scripts/hfs/secret-init.mjs +127 -19
- package/runtime/scripts/hfs/secret-sops.mjs +38 -6
- package/runtime/scripts/hfs/secret-store.mjs +56 -54
- package/runtime/scripts/hfs/secret.mjs +34 -5
- package/runtime/scripts/hfs/slot-imports.mjs +18 -5
- package/runtime/scripts/hfs/slots.mjs +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/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/runtime-example.mjs +13 -0
- package/runtime/scripts/lib/secret-patterns.mjs +60 -5
- package/runtime/scripts/lib/sql-ast.mjs +32 -0
- package/runtime/scripts/lib/stack-services.mjs +1 -1
- package/runtime/scripts/lib/swarm-stack.mjs +5 -18
- package/runtime/scripts/lib/trim-end.mjs +8 -0
- package/runtime/scripts/lib/ts-analysis.mjs +5 -2
- package/runtime/scripts/lib/ts-ast.mjs +27 -2
- package/runtime/scripts/lib/workflow-events.mjs +8 -0
- package/scaffold/add-app.mjs +77 -59
- package/scaffold/add-cli-lite.mjs +14 -6
- package/scaffold/add-compose.mjs +79 -0
- package/scaffold/add-database-lite.mjs +14 -0
- package/scaffold/add-fe-overlay.mjs +122 -0
- package/scaffold/add-fe-page.mjs +269 -0
- package/scaffold/add-inbox-lite.mjs +74 -42
- package/scaffold/add-table.mjs +110 -87
- package/scaffold/add.mjs +159 -44
- package/scaffold/app.mjs +124 -39
- package/scaffold/edition-gate.mjs +3 -3
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +2 -2
- package/src/main.mjs +229 -116
- package/stack/age-install.mjs +37 -0
- package/stack/backup.mjs +100 -0
- package/stack/build-args.mjs +42 -0
- package/stack/build.mjs +173 -0
- package/stack/cli.mjs +130 -0
- package/stack/config.mjs +57 -0
- package/stack/context.mjs +57 -0
- package/stack/converge.mjs +107 -0
- package/stack/deploy.mjs +301 -0
- package/stack/digest.mjs +16 -0
- package/stack/docker-transport.mjs +53 -0
- package/stack/edge-alias.mjs +118 -0
- package/stack/errors.mjs +21 -0
- package/stack/image-scan.mjs +152 -0
- package/stack/ingress-create.mjs +114 -0
- package/stack/ingress-publish.mjs +273 -0
- package/stack/ingress.mjs +706 -0
- package/stack/jobs.mjs +51 -0
- package/stack/live-restore.mjs +177 -0
- package/stack/prepare.mjs +266 -0
- package/stack/private-file.mjs +22 -0
- package/stack/provision.mjs +90 -0
- package/stack/prune.mjs +46 -0
- package/stack/release-store.mjs +87 -0
- package/stack/remote.mjs +205 -0
- package/stack/render.mjs +149 -0
- package/stack/retire.mjs +47 -0
- package/stack/sealed.mjs +149 -0
- package/stack/task.mjs +42 -0
- package/stack/verify.mjs +99 -0
- package/sync/ci-commands.mjs +13 -0
- package/sync/index.mjs +108 -52
- package/sync/stack-images.mjs +127 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
- package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/codegen-graphql.mjs.partial +13 -2
- package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
- package/templates/app/codegen-prelude.mjs.partial +8 -0
- package/templates/app/gitignore +3 -0
- package/templates/app/gitignore-lite +3 -0
- package/templates/app/package-scripts/package.json +1 -0
- package/templates/app/package-scripts/supabase.json +7 -0
- package/templates/app/package-scripts-lite/package.json +1 -7
- package/templates/app/skeleton/.editorconfig +1 -0
- package/templates/app/skeleton/.gitattributes +7 -1
- package/templates/app/skeleton/.github/dependabot.yml +17 -0
- package/templates/app/skeleton/.nvmrc +1 -1
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
- package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
- package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
- package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
- package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
- package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/README.md +6 -6
- package/templates/app/skeleton/scripts/codegen.mjs +1 -2
- package/templates/app/skeleton-lite/.editorconfig +16 -0
- package/templates/app/skeleton-lite/.gitattributes +8 -0
- package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
- package/templates/app/skeleton-lite/.nvmrc +1 -0
- package/templates/app/skeleton-lite/.sops.yaml +3 -2
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +178 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
- package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
- package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
- package/templates/app/skeleton-lite/README.md +5 -3
- package/templates/app/skeleton-lite/supabase/config.toml +8 -1
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/inbox/database-client.spec.ts.tpl +17 -18
- package/templates/be/inbox/database-client.ts.tpl +12 -11
- package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
- package/templates/be/inbox/event-index.ts.tpl +1 -0
- package/templates/be/inbox/events.json.tpl +10 -0
- package/templates/be/inbox/migration.sql.tpl +1 -0
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
- package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
- package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
- package/templates/be/inbox/platform/migration.sql.tpl +48 -0
- package/templates/be/inbox/service.spec.ts.tpl +42 -9
- package/templates/be/inbox/service.ts.tpl +23 -8
- package/templates/be/inbox/webhook-door.spec.ts.tpl +1 -1
- package/templates/be/inbox/webhook-door.ts.tpl +2 -2
- package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
- package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
- package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
- package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
- package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
- package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
- package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
- package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
- package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
- package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
- package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
- package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
- package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
- package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
- package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
- package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
- package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
- package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
- package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
- package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
- package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
- package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
- package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
- package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +11 -9
- package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +2 -0
- package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
- package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/main.ts +15 -1
- package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -14
- package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
- package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
- package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
- package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
- package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
- package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
- package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
- package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
- package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
- package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +14 -18
- package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +2 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/main.ts +2 -3
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
- package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
- package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/feature.module.ts.tpl +2 -1
- package/templates/be/table/api/feature.options.ts.tpl +2 -0
- package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/graphql.module.ts.tpl +3 -2
- package/templates/be/table/api/graphql.options.ts.tpl +2 -0
- package/templates/be/table/migration.sql.tpl +1 -0
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
- package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
- package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
- package/templates/fe/auth/supabase-client.ts.tpl +34 -0
- package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
- package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
- package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
- package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
- package/upgrade/check.mjs +3 -1
- package/upgrade/index.mjs +53 -40
|
@@ -1,13 +1,65 @@
|
|
|
1
|
+
import { Test } from "@nestjs/testing"
|
|
1
2
|
import { admit } from "./admission.policy"
|
|
2
|
-
import { IdentityErrorCode } from "./errors/identity.error"
|
|
3
|
+
import { IdentityError, IdentityErrorCode } from "./errors/identity.error"
|
|
4
|
+
import type { PublicMetadata } from "./identity.contracts"
|
|
3
5
|
import { PublicReason } from "./identity.contracts"
|
|
4
6
|
|
|
7
|
+
const ADMISSION: unique symbol = Symbol("identity.admission")
|
|
8
|
+
|
|
9
|
+
const runtimeMetadata = (source: string): PublicMetadata => {
|
|
10
|
+
try {
|
|
11
|
+
const metadata: PublicMetadata = JSON.parse(source)
|
|
12
|
+
return metadata
|
|
13
|
+
} catch (cause) {
|
|
14
|
+
throw new IdentityError({ code: IdentityErrorCode.Unauthenticated, cause })
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
5
18
|
describe("admit", () => {
|
|
6
|
-
|
|
7
|
-
|
|
19
|
+
let subject: typeof admit
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
const moduleRef = await Test.createTestingModule({
|
|
23
|
+
providers: [{ provide: ADMISSION, useFactory: () => admit }],
|
|
24
|
+
}).compile()
|
|
25
|
+
subject = moduleRef.get<typeof admit>(ADMISSION)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it.each([PublicReason.Health, PublicReason.AuthHandshake, PublicReason.SignedWebhook, PublicReason.CatalogRead])(
|
|
29
|
+
"admits a door with the canonical public reason %s",
|
|
30
|
+
(reason) => {
|
|
31
|
+
expect(subject({ reason })).toSucceedWith(reason)
|
|
32
|
+
},
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
it("admits a credential door whose principal has the required kind", () => {
|
|
36
|
+
const principal = { kind: "api-key", subject: "key-1", workspaceId: "ws-1" } as const
|
|
37
|
+
|
|
38
|
+
expect(subject(undefined, { required: { kind: "api-key" }, principal })).toSucceedWith(principal)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("refuses a credential door without a principal of the required kind", () => {
|
|
42
|
+
const principal = { kind: "tool-token", subject: "tool-1", workspaceId: "ws-1" } as const
|
|
43
|
+
|
|
44
|
+
expect(subject(undefined, { required: { kind: "api-key" }, principal })).toBeRefused(
|
|
45
|
+
IdentityErrorCode.Unauthenticated,
|
|
46
|
+
)
|
|
47
|
+
expect(subject(undefined, { required: { kind: "api-key" }, principal: undefined })).toBeRefused(
|
|
48
|
+
IdentityErrorCode.Unauthenticated,
|
|
49
|
+
)
|
|
8
50
|
})
|
|
9
51
|
|
|
10
52
|
it("refuses a door that is not public", () => {
|
|
11
|
-
expect(
|
|
53
|
+
expect(subject(undefined)).toBeRefused(IdentityErrorCode.Unauthenticated)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("refuses an unknown public reason supplied as runtime metadata", () => {
|
|
57
|
+
const metadata = runtimeMetadata('{"reason":"tool-token"}')
|
|
58
|
+
expect(subject(metadata)).toBeRefused(IdentityErrorCode.Unauthenticated)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("refuses runtime metadata without a public reason", () => {
|
|
62
|
+
const metadata = runtimeMetadata("{}")
|
|
63
|
+
expect(subject(metadata)).toBeRefused(IdentityErrorCode.Unauthenticated)
|
|
12
64
|
})
|
|
13
65
|
})
|
|
@@ -1,11 +1,42 @@
|
|
|
1
1
|
import { ok, refused } from "@modules/platform/primitives"
|
|
2
2
|
import type { Outcome } from "@modules/platform/primitives"
|
|
3
|
+
import type { CredentialMetadata, CredentialPrincipal } from "./credential.contracts"
|
|
3
4
|
import { IdentityErrorCode } from "./errors/identity.error"
|
|
4
|
-
import type { PublicMetadata
|
|
5
|
+
import type { PublicMetadata } from "./identity.contracts"
|
|
6
|
+
import { PublicReason } from "./identity.contracts"
|
|
7
|
+
|
|
8
|
+
/** What a credential-marked door requires and what `CredentialGuard` established for it. */
|
|
9
|
+
export interface CredentialAdmission {
|
|
10
|
+
/** The `@Credential` metadata of the door. */
|
|
11
|
+
readonly required: CredentialMetadata
|
|
12
|
+
/** The principal `CredentialGuard` set on the request, when the credential authenticated. */
|
|
13
|
+
readonly principal: CredentialPrincipal | undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const admitCredential = ({
|
|
17
|
+
required,
|
|
18
|
+
principal,
|
|
19
|
+
}: CredentialAdmission): Outcome<CredentialPrincipal, IdentityErrorCode> =>
|
|
20
|
+
principal?.kind === required.kind ? ok(principal) : refused(IdentityErrorCode.Unauthenticated)
|
|
5
21
|
|
|
6
22
|
/**
|
|
7
|
-
* The default-deny decision of one door: a door
|
|
8
|
-
* until the design that adds
|
|
23
|
+
* The default-deny decision of one door: a credential-marked door is admitted only with its authenticated credential
|
|
24
|
+
* principal; a door with a canonical public reason is admitted; every other door is refused until the design that adds
|
|
25
|
+
* sign-in establishes a session principal here.
|
|
9
26
|
*/
|
|
10
|
-
export const admit = (
|
|
11
|
-
metadata
|
|
27
|
+
export const admit = (
|
|
28
|
+
metadata: PublicMetadata | undefined,
|
|
29
|
+
credential?: CredentialAdmission,
|
|
30
|
+
): Outcome<PublicReason | CredentialPrincipal, IdentityErrorCode> => {
|
|
31
|
+
if (credential !== undefined) return admitCredential(credential)
|
|
32
|
+
switch (metadata?.reason) {
|
|
33
|
+
case PublicReason.Health:
|
|
34
|
+
case PublicReason.AuthHandshake:
|
|
35
|
+
case PublicReason.SignedWebhook:
|
|
36
|
+
case PublicReason.CatalogRead:
|
|
37
|
+
return ok(metadata.reason)
|
|
38
|
+
case undefined:
|
|
39
|
+
default:
|
|
40
|
+
return refused(IdentityErrorCode.Unauthenticated)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -1,37 +1,68 @@
|
|
|
1
1
|
import type { ExecutionContext } from "@nestjs/common"
|
|
2
|
+
import type { HttpArgumentsHost } from "@nestjs/common/interfaces"
|
|
2
3
|
import { Reflector } from "@nestjs/core"
|
|
3
4
|
import { Test } from "@nestjs/testing"
|
|
4
5
|
import { mock } from "@starci/jest-preset"
|
|
5
6
|
import { AuthGuard } from "./auth.guard"
|
|
7
|
+
import type { CredentialMetadata, CredentialPrincipal } from "./credential.contracts"
|
|
6
8
|
import { IdentityErrorCode } from "./errors/identity.error"
|
|
7
9
|
import type { PublicMetadata } from "./identity.contracts"
|
|
8
10
|
import { PublicReason } from "./identity.contracts"
|
|
11
|
+
import { CREDENTIAL_KEY, PUBLIC_KEY } from "./identity.decorators"
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
interface Door {
|
|
14
|
+
readonly credential?: CredentialMetadata
|
|
15
|
+
readonly metadata?: PublicMetadata
|
|
16
|
+
readonly principal?: CredentialPrincipal
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const build = async ({ credential, metadata }: Door) => {
|
|
11
20
|
const reflector = mock<Reflector>()
|
|
12
|
-
reflector.getAllAndOverride.
|
|
21
|
+
reflector.getAllAndOverride.mockImplementation((key: unknown) =>
|
|
22
|
+
key === CREDENTIAL_KEY ? credential : key === PUBLIC_KEY ? metadata : undefined,
|
|
23
|
+
)
|
|
13
24
|
const moduleRef = await Test.createTestingModule({
|
|
14
25
|
providers: [AuthGuard, { provide: Reflector, useValue: reflector }],
|
|
15
26
|
}).compile()
|
|
16
|
-
return
|
|
27
|
+
return moduleRef.get(AuthGuard)
|
|
17
28
|
}
|
|
18
29
|
|
|
19
|
-
const contextOf = () => {
|
|
30
|
+
const contextOf = (principal?: CredentialPrincipal) => {
|
|
20
31
|
const context = mock<ExecutionContext>()
|
|
21
32
|
context.getHandler.mockReturnValue(() => undefined)
|
|
22
|
-
context.getClass.mockReturnValue(class
|
|
33
|
+
context.getClass.mockReturnValue(class DoorClass {})
|
|
34
|
+
context.getType.mockReturnValue("http")
|
|
35
|
+
const host = mock<HttpArgumentsHost>()
|
|
36
|
+
host.getRequest.mockReturnValue({ credentialPrincipal: principal })
|
|
37
|
+
context.switchToHttp.mockReturnValue(host)
|
|
23
38
|
return context
|
|
24
39
|
}
|
|
25
40
|
|
|
41
|
+
const API_KEY: CredentialPrincipal = { kind: "api-key", subject: "key-1", workspaceId: "ws-1" }
|
|
42
|
+
|
|
26
43
|
describe("AuthGuard", () => {
|
|
27
44
|
it("lets a door through when it states why it is public", async () => {
|
|
28
|
-
const
|
|
45
|
+
const guard = await build({ metadata: { reason: PublicReason.Health } })
|
|
29
46
|
|
|
30
47
|
expect(guard.canActivate(contextOf())).toBe(true)
|
|
31
48
|
})
|
|
32
49
|
|
|
33
|
-
it("refuses every door that is
|
|
34
|
-
const
|
|
50
|
+
it("refuses every door that is neither public nor credential-marked", async () => {
|
|
51
|
+
const guard = await build({})
|
|
52
|
+
|
|
53
|
+
expect(() => guard.canActivate(contextOf())).toThrow(
|
|
54
|
+
expect.objectContaining({ code: IdentityErrorCode.Unauthenticated }),
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("lets a credential door through once the credential principal is set", async () => {
|
|
59
|
+
const guard = await build({ credential: { kind: "api-key" } })
|
|
60
|
+
|
|
61
|
+
expect(guard.canActivate(contextOf(API_KEY))).toBe(true)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("refuses a credential door without a credential principal", async () => {
|
|
65
|
+
const guard = await build({ credential: { kind: "api-key" } })
|
|
35
66
|
|
|
36
67
|
expect(() => guard.canActivate(contextOf())).toThrow(
|
|
37
68
|
expect.objectContaining({ code: IdentityErrorCode.Unauthenticated }),
|
|
@@ -2,24 +2,30 @@ import { Injectable } from "@nestjs/common"
|
|
|
2
2
|
import type { CanActivate, ExecutionContext } from "@nestjs/common"
|
|
3
3
|
import type { Reflector } from "@nestjs/core"
|
|
4
4
|
import { InjectReflector } from "@modules/platform/composition"
|
|
5
|
+
import { requestOf } from "@modules/platform/http-security"
|
|
5
6
|
import { unwrapOutcome } from "@modules/platform/primitives"
|
|
6
7
|
import { admit } from "./admission.policy"
|
|
8
|
+
import type { CredentialMetadata } from "./credential.contracts"
|
|
7
9
|
import { IdentityError } from "./errors/identity.error"
|
|
8
10
|
import type { PublicMetadata } from "./identity.contracts"
|
|
9
|
-
import { PUBLIC_KEY } from "./identity.decorators"
|
|
11
|
+
import { CREDENTIAL_KEY, PUBLIC_KEY } from "./identity.decorators"
|
|
10
12
|
|
|
11
13
|
@Injectable()
|
|
12
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* The fourth app guard and the default-deny gate: a door is open only when it says `@Public({ reason })`, or when it is
|
|
16
|
+
* marked `@Credential` and `CredentialGuard` has set the credential principal. It never calls another guard.
|
|
17
|
+
*/
|
|
13
18
|
export class AuthGuard implements CanActivate {
|
|
14
19
|
constructor(@InjectReflector() private readonly reflector: Reflector) {}
|
|
15
20
|
|
|
16
|
-
/** Lets public doors through and refuses every other door. */
|
|
21
|
+
/** Lets public and credential-authenticated doors through and refuses every other door. */
|
|
17
22
|
canActivate(context: ExecutionContext): boolean {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
const targets = [context.getHandler(), context.getClass()]
|
|
24
|
+
const required = this.reflector.getAllAndOverride<CredentialMetadata | undefined>(CREDENTIAL_KEY, targets)
|
|
25
|
+
const metadata = this.reflector.getAllAndOverride<PublicMetadata | undefined>(PUBLIC_KEY, targets)
|
|
26
|
+
const credential =
|
|
27
|
+
required === undefined ? undefined : { required, principal: requestOf(context).credentialPrincipal }
|
|
28
|
+
unwrapOutcome(admit(metadata, credential), IdentityError)
|
|
23
29
|
return true
|
|
24
30
|
}
|
|
25
31
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** The closed set of alternate credential families a door can accept. */
|
|
2
|
+
export type CredentialKind = "api-key" | "run-token" | "tool-token" | "link-token" | "posting"
|
|
3
|
+
|
|
4
|
+
/** Alternate credentials accepted only by doors marked with `@Credential`. */
|
|
5
|
+
export interface CredentialMetadata {
|
|
6
|
+
/** The credential family this door accepts. */
|
|
7
|
+
readonly kind: CredentialKind
|
|
8
|
+
/** An optional workflow constraint the authenticator may enforce for the credential. */
|
|
9
|
+
readonly workflow?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A credential identity is deliberately separate from a signed-in user. */
|
|
13
|
+
export interface CredentialPrincipal {
|
|
14
|
+
/** The credential family that established this identity. */
|
|
15
|
+
readonly kind: CredentialKind
|
|
16
|
+
/** The key, job, run or link the credential identifies. */
|
|
17
|
+
readonly subject: string
|
|
18
|
+
/** The only workspace this credential can act for. */
|
|
19
|
+
readonly workspaceId: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The port the app binds to the capability that owns credential verification. */
|
|
23
|
+
export interface CredentialAuthenticator {
|
|
24
|
+
/** Authenticates the actual credential, throwing the owning domain's refusal on failure. */
|
|
25
|
+
authenticate(metadata: CredentialMetadata, authorization: string, resourceId?: string): Promise<CredentialPrincipal>
|
|
26
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { ExecutionContext } from "@nestjs/common"
|
|
2
|
+
import type { HttpArgumentsHost } from "@nestjs/common/interfaces"
|
|
3
|
+
import { Reflector } from "@nestjs/core"
|
|
4
|
+
import { Test } from "@nestjs/testing"
|
|
5
|
+
import { mock } from "@starci/jest-preset"
|
|
6
|
+
import { CredentialGuard } from "./credential.guard"
|
|
7
|
+
import type { CredentialAuthenticator, CredentialMetadata, CredentialPrincipal } from "./credential.contracts"
|
|
8
|
+
import { IdentityError, IdentityErrorCode } from "./errors/identity.error"
|
|
9
|
+
import { CREDENTIAL_AUTHENTICATOR } from "./identity.decorators"
|
|
10
|
+
|
|
11
|
+
const PRINCIPAL: CredentialPrincipal = { kind: "tool-token", subject: "tool-1", workspaceId: "ws-1" }
|
|
12
|
+
|
|
13
|
+
interface FakeHeaders {
|
|
14
|
+
readonly authorization?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface FakeParams {
|
|
18
|
+
readonly id?: unknown
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface FakeRequest {
|
|
22
|
+
credentialPrincipal?: CredentialPrincipal
|
|
23
|
+
readonly headers: FakeHeaders
|
|
24
|
+
readonly params?: FakeParams
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const build = async (metadata: CredentialMetadata | undefined, principal: CredentialPrincipal = PRINCIPAL) => {
|
|
28
|
+
const reflector = mock<Reflector>()
|
|
29
|
+
reflector.getAllAndOverride.mockReturnValue(metadata)
|
|
30
|
+
const authenticator = mock<CredentialAuthenticator>()
|
|
31
|
+
authenticator.authenticate.mockResolvedValue(principal)
|
|
32
|
+
const moduleRef = await Test.createTestingModule({
|
|
33
|
+
providers: [
|
|
34
|
+
CredentialGuard,
|
|
35
|
+
{ provide: Reflector, useValue: reflector },
|
|
36
|
+
{ provide: CREDENTIAL_AUTHENTICATOR, useValue: authenticator },
|
|
37
|
+
],
|
|
38
|
+
}).compile()
|
|
39
|
+
return { guard: moduleRef.get(CredentialGuard), authenticator }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const contextOf = (request: FakeRequest) => {
|
|
43
|
+
const context = mock<ExecutionContext>()
|
|
44
|
+
context.getHandler.mockReturnValue(() => undefined)
|
|
45
|
+
context.getClass.mockReturnValue(class DoorClass {})
|
|
46
|
+
context.getType.mockReturnValue("http")
|
|
47
|
+
const host = mock<HttpArgumentsHost>()
|
|
48
|
+
host.getRequest.mockReturnValue(request)
|
|
49
|
+
context.switchToHttp.mockReturnValue(host)
|
|
50
|
+
return context
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const requestOf = (): FakeRequest => ({ headers: { authorization: "Bearer secret" }, params: { id: "run-1" } })
|
|
54
|
+
|
|
55
|
+
describe("CredentialGuard", () => {
|
|
56
|
+
it("passes a door without credential metadata and authenticates nothing", async () => {
|
|
57
|
+
const { guard, authenticator } = await build(undefined)
|
|
58
|
+
const request = { ...requestOf(), credentialPrincipal: PRINCIPAL }
|
|
59
|
+
|
|
60
|
+
await expect(guard.canActivate(contextOf(request))).resolves.toBe(true)
|
|
61
|
+
expect(request.credentialPrincipal).toEqual(PRINCIPAL)
|
|
62
|
+
expect(authenticator.authenticate).not.toHaveBeenCalled()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("authenticates a credential door and sets the credential principal", async () => {
|
|
66
|
+
const { guard, authenticator } = await build({ kind: "tool-token" }, PRINCIPAL)
|
|
67
|
+
const request = requestOf()
|
|
68
|
+
|
|
69
|
+
await expect(guard.canActivate(contextOf(request))).resolves.toBe(true)
|
|
70
|
+
expect(request.credentialPrincipal).toEqual(PRINCIPAL)
|
|
71
|
+
expect(authenticator.authenticate).toHaveBeenCalledWith({ kind: "tool-token" }, "Bearer secret", "run-1")
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("refuses a principal of another credential kind", async () => {
|
|
75
|
+
const { guard } = await build({ kind: "api-key" }, PRINCIPAL)
|
|
76
|
+
const request = requestOf()
|
|
77
|
+
|
|
78
|
+
await expect(guard.canActivate(contextOf(request))).rejects.toMatchObject({
|
|
79
|
+
code: IdentityErrorCode.Unauthenticated,
|
|
80
|
+
})
|
|
81
|
+
expect(request.credentialPrincipal).toBeUndefined()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it.each([
|
|
85
|
+
{ ...PRINCIPAL, subject: "" },
|
|
86
|
+
{ ...PRINCIPAL, workspaceId: "" },
|
|
87
|
+
])("refuses an incomplete credential principal (%o)", async (principal) => {
|
|
88
|
+
const { guard } = await build({ kind: "tool-token" }, principal)
|
|
89
|
+
const request = requestOf()
|
|
90
|
+
|
|
91
|
+
await expect(guard.canActivate(contextOf(request))).rejects.toMatchObject({
|
|
92
|
+
code: IdentityErrorCode.Unauthenticated,
|
|
93
|
+
})
|
|
94
|
+
expect(request.credentialPrincipal).toBeUndefined()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it("clears an earlier credential principal when authentication refuses", async () => {
|
|
98
|
+
const { guard, authenticator } = await build({ kind: "tool-token" }, PRINCIPAL)
|
|
99
|
+
const refusal = new IdentityError({ code: IdentityErrorCode.Unauthenticated })
|
|
100
|
+
authenticator.authenticate.mockRejectedValue(refusal)
|
|
101
|
+
const request = { ...requestOf(), credentialPrincipal: PRINCIPAL }
|
|
102
|
+
|
|
103
|
+
await expect(guard.canActivate(contextOf(request))).rejects.toBe(refusal)
|
|
104
|
+
expect(request.credentialPrincipal).toBeUndefined()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it.each([undefined, { id: 1 }])(
|
|
108
|
+
"passes absent authorization and no string resource id to the authenticator (%o)",
|
|
109
|
+
async (params) => {
|
|
110
|
+
const metadata = { kind: "tool-token", workflow: "checkout" } as const
|
|
111
|
+
const { guard, authenticator } = await build(metadata, PRINCIPAL)
|
|
112
|
+
const request: FakeRequest = { headers: {}, params }
|
|
113
|
+
|
|
114
|
+
await expect(guard.canActivate(contextOf(request))).resolves.toBe(true)
|
|
115
|
+
expect(authenticator.authenticate).toHaveBeenCalledWith(metadata, "", undefined)
|
|
116
|
+
expect(request.credentialPrincipal).toEqual(PRINCIPAL)
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Injectable } from "@nestjs/common"
|
|
2
|
+
import type { CanActivate, ExecutionContext } from "@nestjs/common"
|
|
3
|
+
import type { Reflector } from "@nestjs/core"
|
|
4
|
+
import { InjectReflector } from "@modules/platform/composition"
|
|
5
|
+
import { requestOf } from "@modules/platform/http-security"
|
|
6
|
+
import type { CredentialAuthenticator, CredentialMetadata } from "./credential.contracts"
|
|
7
|
+
import { IdentityError, IdentityErrorCode } from "./errors/identity.error"
|
|
8
|
+
import { CREDENTIAL_KEY, InjectCredentialAuthenticator } from "./identity.decorators"
|
|
9
|
+
|
|
10
|
+
@Injectable()
|
|
11
|
+
/**
|
|
12
|
+
* The third app guard: it acts only on a door marked `@Credential({ kind })`, authenticates the credential through the
|
|
13
|
+
* identity-owned port and sets the request's credential principal. Every other door passes through untouched and
|
|
14
|
+
* `AuthGuard` decides it.
|
|
15
|
+
*/
|
|
16
|
+
export class CredentialGuard implements CanActivate {
|
|
17
|
+
constructor(
|
|
18
|
+
@InjectReflector() private readonly reflector: Reflector,
|
|
19
|
+
@InjectCredentialAuthenticator() private readonly authenticator: CredentialAuthenticator,
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
/** Authenticates a credential door and leaves every other door to `AuthGuard`. */
|
|
23
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
24
|
+
const metadata = this.reflector.getAllAndOverride<CredentialMetadata | undefined>(CREDENTIAL_KEY, [
|
|
25
|
+
context.getHandler(),
|
|
26
|
+
context.getClass(),
|
|
27
|
+
])
|
|
28
|
+
if (metadata === undefined) return true
|
|
29
|
+
const request = requestOf(context)
|
|
30
|
+
request.credentialPrincipal = undefined
|
|
31
|
+
const id: unknown = request.params?.id
|
|
32
|
+
const principal = await this.authenticator.authenticate(
|
|
33
|
+
metadata,
|
|
34
|
+
request.headers.authorization ?? "",
|
|
35
|
+
typeof id === "string" ? id : undefined,
|
|
36
|
+
)
|
|
37
|
+
if (principal.kind !== metadata.kind || !principal.subject || !principal.workspaceId) {
|
|
38
|
+
throw new IdentityError({ code: IdentityErrorCode.Unauthenticated })
|
|
39
|
+
}
|
|
40
|
+
request.credentialPrincipal = principal
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1,7 +1,23 @@
|
|
|
1
|
+
import type { CredentialPrincipal } from "./credential.contracts"
|
|
2
|
+
|
|
3
|
+
declare module "express-serve-static-core" {
|
|
4
|
+
/** The request carries what the credential guard established. */
|
|
5
|
+
interface Request {
|
|
6
|
+
/** The identity `CredentialGuard` authenticated on a door marked `@Credential`; never a session principal. */
|
|
7
|
+
credentialPrincipal?: CredentialPrincipal
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
1
11
|
/** Why a door is open to anonymous callers; the vocabulary is closed. */
|
|
2
12
|
export enum PublicReason {
|
|
3
13
|
/** Liveness and readiness probes. */
|
|
4
14
|
Health = "health",
|
|
15
|
+
/** The authentication protocol's anonymous handshake. */
|
|
16
|
+
AuthHandshake = "auth-handshake",
|
|
17
|
+
/** A webhook whose signature is verified before its domain intake runs. */
|
|
18
|
+
SignedWebhook = "signed-webhook",
|
|
19
|
+
/** A declared public catalog read. */
|
|
20
|
+
CatalogRead = "catalog-read",
|
|
5
21
|
}
|
|
6
22
|
|
|
7
23
|
/** The metadata `@Public` attaches to a door. */
|
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import { SetMetadata } from "@nestjs/common"
|
|
2
|
+
import { injector } from "@modules/platform/composition"
|
|
3
|
+
import type { TypedParameterDecorator } from "@modules/platform/composition"
|
|
4
|
+
import type { CredentialAuthenticator, CredentialMetadata } from "./credential.contracts"
|
|
2
5
|
import type { PublicMetadata } from "./identity.contracts"
|
|
3
6
|
|
|
4
7
|
/** Metadata key of `@Public`. */
|
|
5
8
|
export const PUBLIC_KEY = "domain.identity.public"
|
|
6
9
|
|
|
7
|
-
/** Opens a door to anonymous callers, stating why; every other door needs a signed-in caller. */
|
|
10
|
+
/** Opens a door to anonymous callers, stating why; every other door needs a credential or a signed-in caller. */
|
|
8
11
|
export const Public = (metadata: PublicMetadata): ReturnType<typeof SetMetadata> => SetMetadata(PUBLIC_KEY, metadata)
|
|
12
|
+
|
|
13
|
+
/** Metadata key of `@Credential`. */
|
|
14
|
+
export const CREDENTIAL_KEY: unique symbol = Symbol("domain.identity.credential")
|
|
15
|
+
|
|
16
|
+
/** Token of the identity-owned port that authenticates an API key, run token, tool token or link token. */
|
|
17
|
+
export const CREDENTIAL_AUTHENTICATOR: unique symbol = Symbol("domain.identity.credential-authenticator")
|
|
18
|
+
|
|
19
|
+
/** Injects the credential authenticator identity was registered with. Parameter type: CredentialAuthenticator. */
|
|
20
|
+
export const InjectCredentialAuthenticator = (): TypedParameterDecorator<CredentialAuthenticator> =>
|
|
21
|
+
injector<CredentialAuthenticator>(CREDENTIAL_AUTHENTICATOR)
|
|
22
|
+
|
|
23
|
+
/** Marks a door authenticated by its own credential; `CredentialGuard` authenticates it and `AuthGuard` admits it. */
|
|
24
|
+
export const Credential = (metadata: CredentialMetadata): ReturnType<typeof SetMetadata> =>
|
|
25
|
+
SetMetadata(CREDENTIAL_KEY, metadata)
|
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
import { Module } from "@nestjs/common"
|
|
2
|
-
import type { DynamicModule } from "@nestjs/common"
|
|
2
|
+
import type { DynamicModule, Provider } from "@nestjs/common"
|
|
3
3
|
import { AuthGuard } from "./auth.guard"
|
|
4
|
+
import { CredentialGuard } from "./credential.guard"
|
|
5
|
+
import { IdentityError, IdentityErrorCode } from "./errors/identity.error"
|
|
6
|
+
import { CREDENTIAL_AUTHENTICATOR } from "./identity.decorators"
|
|
4
7
|
import { ConfigurableModuleClass, OPTIONS_TYPE } from "./identity.module-definition"
|
|
5
8
|
|
|
9
|
+
const refusingAuthenticator: Provider = {
|
|
10
|
+
provide: CREDENTIAL_AUTHENTICATOR,
|
|
11
|
+
useValue: {
|
|
12
|
+
authenticate: () => Promise.reject(new IdentityError({ code: IdentityErrorCode.Unauthenticated })),
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
@Module({})
|
|
7
|
-
/** The identity capability: the guard that closes every door by default. The app registers
|
|
17
|
+
/** The identity capability: the credential guard and the guard that closes every door by default. The app registers them as APP_GUARDs after the throttler and the origin guard. */
|
|
8
18
|
export class IdentityModule extends ConfigurableModuleClass {
|
|
9
|
-
/** Registers the capability once per app. */
|
|
19
|
+
/** Registers the capability once per app; the credential authenticator is the port the app binds. */
|
|
10
20
|
static register(options: typeof OPTIONS_TYPE): DynamicModule {
|
|
11
21
|
const base = super.register(options)
|
|
12
|
-
|
|
22
|
+
const authenticator: Provider =
|
|
23
|
+
options.credentialAuthenticator === undefined
|
|
24
|
+
? refusingAuthenticator
|
|
25
|
+
: { provide: CREDENTIAL_AUTHENTICATOR, useClass: options.credentialAuthenticator }
|
|
26
|
+
return {
|
|
27
|
+
...base,
|
|
28
|
+
providers: [...(base.providers ?? []), authenticator, CredentialGuard, AuthGuard],
|
|
29
|
+
exports: [CredentialGuard, CREDENTIAL_AUTHENTICATOR, AuthGuard],
|
|
30
|
+
}
|
|
13
31
|
}
|
|
14
32
|
}
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import type { Type } from "@nestjs/common"
|
|
2
|
+
import type { CredentialAuthenticator } from "./credential.contracts"
|
|
3
|
+
|
|
4
|
+
/** The identity capability's options. */
|
|
5
|
+
export interface IdentityOptions {
|
|
6
|
+
/** The app-owned authenticator of alternate credentials; a credential door fails closed while it is absent. */
|
|
7
|
+
readonly credentialAuthenticator?: Type<CredentialAuthenticator>
|
|
8
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { AuthGuard } from "./auth.guard"
|
|
2
|
+
export { CredentialGuard } from "./credential.guard"
|
|
2
3
|
export { IDENTITY_ERROR_KINDS } from "./errors/identity.error"
|
|
3
4
|
export { IDENTITY_MESSAGES } from "./messages/identity.messages"
|
|
4
5
|
export { PublicReason } from "./identity.contracts"
|
package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { Test } from "@nestjs/testing"
|
|
1
2
|
import type { QueryRunner } from "typeorm"
|
|
2
3
|
import { Secret } from "@modules/platform/config"
|
|
3
4
|
import type { DatabaseConnectionOptions } from "./database.options"
|
|
4
5
|
import { openConnectionSource } from "./connection-source.client"
|
|
6
|
+
import { CONNECTION_SOURCE } from "./database.port"
|
|
7
|
+
import type { ConnectionOpener } from "./database.port"
|
|
5
8
|
|
|
6
9
|
class NoteEntity {}
|
|
7
10
|
|
|
@@ -16,20 +19,24 @@ class CreateNotes {
|
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
describe("openConnectionSource", () => {
|
|
19
|
-
it("opens an isolated PostgreSQL data source with the connection's entities and migrations", () => {
|
|
22
|
+
it("opens an isolated PostgreSQL data source with the connection's entities and migrations", async () => {
|
|
23
|
+
const moduleRef = await Test.createTestingModule({
|
|
24
|
+
providers: [{ provide: CONNECTION_SOURCE, useFactory: () => ({ open: openConnectionSource }) }],
|
|
25
|
+
}).compile()
|
|
26
|
+
const client = moduleRef.get<ConnectionOpener>(CONNECTION_SOURCE)
|
|
20
27
|
const connection: DatabaseConnectionOptions = {
|
|
21
28
|
name: "primary",
|
|
22
|
-
url: new Secret("postgres://
|
|
29
|
+
url: new Secret("postgres://database.test:5432/primary"),
|
|
23
30
|
entities: [NoteEntity],
|
|
24
31
|
migrations: [CreateNotes],
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
const source =
|
|
34
|
+
const source = client.open(connection)
|
|
28
35
|
|
|
29
36
|
expect(source).toMatchObject({
|
|
30
37
|
options: {
|
|
31
38
|
type: "postgres",
|
|
32
|
-
url: "postgres://
|
|
39
|
+
url: "postgres://database.test:5432/primary",
|
|
33
40
|
entities: [NoteEntity],
|
|
34
41
|
migrations: [CreateNotes],
|
|
35
42
|
migrationsTableName: "primary_migrations",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Test } from "@nestjs/testing"
|
|
2
|
+
import { fakeTransaction, mockEntityManager } from "@starci/jest-preset"
|
|
3
|
+
import type { MockEntityManager } from "@starci/jest-preset"
|
|
4
|
+
import { DatabaseProbeService } from "./database-probe.service"
|
|
5
|
+
import { PRIMARY_ENTITY_MANAGER } from "./primary.decorators"
|
|
6
|
+
|
|
7
|
+
const build = async (entityManager: MockEntityManager) => {
|
|
8
|
+
const moduleRef = await Test.createTestingModule({
|
|
9
|
+
providers: [DatabaseProbeService, { provide: PRIMARY_ENTITY_MANAGER, useValue: entityManager }],
|
|
10
|
+
}).compile()
|
|
11
|
+
return moduleRef.get(DatabaseProbeService)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("DatabaseProbeService", () => {
|
|
15
|
+
it("opens and commits a transaction against the required database", async () => {
|
|
16
|
+
const transaction = fakeTransaction(mockEntityManager())
|
|
17
|
+
const service = await build(transaction.em)
|
|
18
|
+
|
|
19
|
+
await expect(service.probe()).resolves.toBeUndefined()
|
|
20
|
+
|
|
21
|
+
expect(transaction.em.transaction).toHaveBeenCalledTimes(1)
|
|
22
|
+
expect(transaction.commits).toBe(1)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("propagates a failed database transaction", async () => {
|
|
26
|
+
const entityManager = mockEntityManager()
|
|
27
|
+
entityManager.transaction.mockRejectedValue(new Error("connection unavailable"))
|
|
28
|
+
const service = await build(entityManager)
|
|
29
|
+
|
|
30
|
+
await expect(service.probe()).rejects.toThrow("connection unavailable")
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Injectable } from "@nestjs/common"
|
|
2
|
+
import type { EntityManager } from "typeorm"
|
|
3
|
+
import { InjectPrimaryEntityManager } from "./primary.decorators"
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
/** Checks that the required primary database accepts and commits a transaction. */
|
|
7
|
+
export class DatabaseProbeService {
|
|
8
|
+
constructor(@InjectPrimaryEntityManager() private readonly entityManager: EntityManager) {}
|
|
9
|
+
|
|
10
|
+
/** Reaches the database and fails when its transaction cannot complete. */
|
|
11
|
+
async probe(): Promise<void> {
|
|
12
|
+
await this.entityManager.transaction(() => Promise.resolve(undefined))
|
|
13
|
+
}
|
|
14
|
+
}
|