@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
|
@@ -2,6 +2,7 @@ import { Module } from "@nestjs/common"
|
|
|
2
2
|
import type { DynamicModule } from "@nestjs/common"
|
|
3
3
|
import { TypeOrmModule, getDataSourceToken } from "@nestjs/typeorm"
|
|
4
4
|
import type { DataSource } from "typeorm"
|
|
5
|
+
import { DatabaseProbeService } from "./database-probe.service"
|
|
5
6
|
import { openConnectionSource } from "./connection-source.client"
|
|
6
7
|
import { CONNECTION_SOURCE, DATABASE_OPTIONS, READ_SEED_FILES } from "./database.port"
|
|
7
8
|
import { ConfigurableModuleClass, OPTIONS_TYPE } from "./database.module-definition"
|
|
@@ -9,7 +10,7 @@ import { MigrationRunnerService } from "./migration-runner.service"
|
|
|
9
10
|
import { readSeedFiles } from "./seed-connections.client"
|
|
10
11
|
import { SeedRunnerService } from "./seed-runner.service"
|
|
11
12
|
import { PRIMARY_CONNECTION } from "./primary.connection"
|
|
12
|
-
import { PRIMARY_ENTITY_MANAGER } from "./primary.decorators"
|
|
13
|
+
import { DATABASE_PROBE, PRIMARY_ENTITY_MANAGER } from "./primary.decorators"
|
|
13
14
|
|
|
14
15
|
/** The token each declared connection provides its shared EntityManager under. */
|
|
15
16
|
type EntityManagerToken = typeof PRIMARY_ENTITY_MANAGER
|
|
@@ -39,6 +40,9 @@ export class DatabaseModule extends ConfigurableModuleClass {
|
|
|
39
40
|
},
|
|
40
41
|
]
|
|
41
42
|
})
|
|
43
|
+
const probes = options.connections.some((connection) => connection.name === PRIMARY_CONNECTION)
|
|
44
|
+
? [{ provide: DATABASE_PROBE, useClass: DatabaseProbeService }]
|
|
45
|
+
: []
|
|
42
46
|
return {
|
|
43
47
|
...base,
|
|
44
48
|
imports: [
|
|
@@ -59,6 +63,7 @@ export class DatabaseModule extends ConfigurableModuleClass {
|
|
|
59
63
|
...managers,
|
|
60
64
|
MigrationRunnerService,
|
|
61
65
|
SeedRunnerService,
|
|
66
|
+
...probes,
|
|
62
67
|
{
|
|
63
68
|
provide: CONNECTION_SOURCE,
|
|
64
69
|
useValue: { open: openConnectionSource },
|
|
@@ -70,6 +75,7 @@ export class DatabaseModule extends ConfigurableModuleClass {
|
|
|
70
75
|
CONNECTION_SOURCE,
|
|
71
76
|
MigrationRunnerService,
|
|
72
77
|
SeedRunnerService,
|
|
78
|
+
...probes.map((probe) => probe.provide),
|
|
73
79
|
...managers.map((manager) => manager.provide),
|
|
74
80
|
],
|
|
75
81
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { DatabaseModule } from "./database.module"
|
|
2
|
+
export { DATABASE_PROBE, InjectDatabaseProbe } from "./primary.decorators"
|
|
3
|
+
export { DatabaseProbeService } from "./database-probe.service"
|
|
2
4
|
export { MigrationRunnerService } from "./migration-runner.service"
|
|
3
5
|
export { DEFAULT_SEED_ENV } from "./seed-connections.client"
|
|
4
6
|
export { SeedRunnerService } from "./seed-runner.service"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { injector } from "@modules/platform/composition"
|
|
2
2
|
import type { TypedParameterDecorator } from "@modules/platform/composition"
|
|
3
3
|
import type { EntityManager } from "typeorm"
|
|
4
|
+
import type { DatabaseProbeService } from "./database-probe.service"
|
|
4
5
|
|
|
5
6
|
/** Token of the shared EntityManager of the `primary` database; the database module provides it for the connection it opens. */
|
|
6
7
|
export const PRIMARY_ENTITY_MANAGER: unique symbol = Symbol("platform.database.primary-entity-manager")
|
|
@@ -8,3 +9,10 @@ export const PRIMARY_ENTITY_MANAGER: unique symbol = Symbol("platform.database.p
|
|
|
8
9
|
/** Injects the shared EntityManager of the `primary` database. Parameter type: EntityManager. */
|
|
9
10
|
export const InjectPrimaryEntityManager = (): TypedParameterDecorator<EntityManager> =>
|
|
10
11
|
injector<EntityManager>(PRIMARY_ENTITY_MANAGER)
|
|
12
|
+
|
|
13
|
+
/** The required primary database connectivity probe provided by the database capability. */
|
|
14
|
+
export const DATABASE_PROBE: unique symbol = Symbol("platform.database.probe")
|
|
15
|
+
|
|
16
|
+
/** Injects the primary database connectivity probe. Parameter type: DatabaseProbeService. */
|
|
17
|
+
export const InjectDatabaseProbe = (): TypedParameterDecorator<DatabaseProbeService> =>
|
|
18
|
+
injector<DatabaseProbeService>(DATABASE_PROBE)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdtemp, rm, writeFile } from "node:fs/promises"
|
|
2
2
|
import { tmpdir } from "node:os"
|
|
3
3
|
import { join } from "node:path"
|
|
4
|
+
import { Test } from "@nestjs/testing"
|
|
4
5
|
import { mock } from "@starci/jest-preset"
|
|
5
6
|
import { Secret } from "@modules/platform/config"
|
|
6
7
|
import type { DatabaseConnectionOptions } from "./database.options"
|
|
@@ -9,6 +10,19 @@ import { seedText } from "./database.sql"
|
|
|
9
10
|
import { readSeedFiles, seedConnections, seedDirectoryOf } from "./seed-connections.client"
|
|
10
11
|
import type { SeedFile } from "./seed-connections.client"
|
|
11
12
|
|
|
13
|
+
const SEED_CLIENT = Symbol("seed-connections")
|
|
14
|
+
|
|
15
|
+
const setup = async () => {
|
|
16
|
+
const moduleRef = await Test.createTestingModule({
|
|
17
|
+
providers: [{ provide: SEED_CLIENT, useFactory: () => ({ readSeedFiles, seedConnections, seedDirectoryOf }) }],
|
|
18
|
+
}).compile()
|
|
19
|
+
return moduleRef.get<{
|
|
20
|
+
readonly readSeedFiles: typeof readSeedFiles
|
|
21
|
+
readonly seedConnections: typeof seedConnections
|
|
22
|
+
readonly seedDirectoryOf: typeof seedDirectoryOf
|
|
23
|
+
}>(SEED_CLIENT)
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
const connection = (name: string): DatabaseConnectionOptions => ({
|
|
13
27
|
name,
|
|
14
28
|
url: new Secret(`postgres://localhost:5432/${name}`),
|
|
@@ -22,13 +36,15 @@ const file = (name: string, text: string): SeedFile => ({
|
|
|
22
36
|
})
|
|
23
37
|
|
|
24
38
|
describe("seedDirectoryOf", () => {
|
|
25
|
-
it("is the seeds folder of the environment under .starcistacks, relative to the app root", () => {
|
|
26
|
-
|
|
39
|
+
it("is the seeds folder of the environment under .starcistacks, relative to the app root", async () => {
|
|
40
|
+
const client = await setup()
|
|
41
|
+
expect(client.seedDirectoryOf("dev")).toBe(join(".starcistacks", "dev", "seeds"))
|
|
27
42
|
})
|
|
28
43
|
})
|
|
29
44
|
|
|
30
45
|
describe("seedConnections", () => {
|
|
31
46
|
it("runs the files of each connection in the given order and never opens a connection without a file", async () => {
|
|
47
|
+
const client = await setup()
|
|
32
48
|
const primary = mock<ConnectionSource>()
|
|
33
49
|
const archive = mock<ConnectionSource>()
|
|
34
50
|
const sources = new Map([
|
|
@@ -40,7 +56,7 @@ describe("seedConnections", () => {
|
|
|
40
56
|
(target: DatabaseConnectionOptions) => sources.get(target.name) ?? mock<ConnectionSource>(),
|
|
41
57
|
)
|
|
42
58
|
|
|
43
|
-
const report = await seedConnections(
|
|
59
|
+
const report = await client.seedConnections(
|
|
44
60
|
[connection("primary"), connection("archive")],
|
|
45
61
|
[file("primary-a.sql", "SELECT 1"), file("primary-b.sql", "SELECT 2")],
|
|
46
62
|
opener,
|
|
@@ -57,9 +73,10 @@ describe("seedConnections", () => {
|
|
|
57
73
|
})
|
|
58
74
|
|
|
59
75
|
it("answers the files that name no connection as unmatched", async () => {
|
|
76
|
+
const client = await setup()
|
|
60
77
|
const opener = mock<ConnectionOpener>()
|
|
61
78
|
|
|
62
|
-
const report = await seedConnections(
|
|
79
|
+
const report = await client.seedConnections(
|
|
63
80
|
[connection("primary")],
|
|
64
81
|
[file("legacy-users.sql", "INSERT INTO users DEFAULT VALUES")],
|
|
65
82
|
opener,
|
|
@@ -70,13 +87,14 @@ describe("seedConnections", () => {
|
|
|
70
87
|
})
|
|
71
88
|
|
|
72
89
|
it("destroys the data source of a connection whose seed fails, and fails", async () => {
|
|
90
|
+
const client = await setup()
|
|
73
91
|
const failing = mock<ConnectionSource>()
|
|
74
92
|
failing.query.mockRejectedValue(new TypeError("relation does not exist"))
|
|
75
93
|
const opener = mock<ConnectionOpener>()
|
|
76
94
|
opener.open.mockReturnValue(failing)
|
|
77
95
|
|
|
78
96
|
await expect(
|
|
79
|
-
seedConnections([connection("primary")], [file("primary-a.sql", "SELECT 1")], opener),
|
|
97
|
+
client.seedConnections([connection("primary")], [file("primary-a.sql", "SELECT 1")], opener),
|
|
80
98
|
).rejects.toThrow("relation does not exist")
|
|
81
99
|
expect(failing.destroy).toHaveBeenCalledTimes(1)
|
|
82
100
|
})
|
|
@@ -94,11 +112,12 @@ describe("readSeedFiles", () => {
|
|
|
94
112
|
})
|
|
95
113
|
|
|
96
114
|
it("answers every sql file of the directory sorted by name with its statements, and nothing else", async () => {
|
|
115
|
+
const client = await setup()
|
|
97
116
|
await writeFile(join(directory, "primary-b.sql"), "SELECT 2")
|
|
98
117
|
await writeFile(join(directory, "primary-a.sql"), "SELECT 1")
|
|
99
118
|
await writeFile(join(directory, "README.md"), "not a seed")
|
|
100
119
|
|
|
101
|
-
const files = await readSeedFiles(directory)
|
|
120
|
+
const files = await client.readSeedFiles(directory)
|
|
102
121
|
|
|
103
122
|
expect(files).toEqual([
|
|
104
123
|
{ name: "primary-a.sql", text: "SELECT 1" },
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
import type { ArgumentsHost } from "@nestjs/common"
|
|
2
|
+
import { Test } from "@nestjs/testing"
|
|
2
3
|
import { mock } from "@starci/jest-preset"
|
|
3
4
|
import type { Request, Response } from "express"
|
|
5
|
+
import { REQUEST_LOCALE } from "@modules/platform/i18n"
|
|
4
6
|
import type { RequestLocale } from "@modules/platform/i18n"
|
|
7
|
+
import { ERRORS_SERVICE } from "./errors.decorators"
|
|
5
8
|
import { ErrorsFilter } from "./errors.filter"
|
|
6
9
|
import type { ErrorsService } from "./errors.service"
|
|
7
10
|
|
|
8
|
-
const build = () => {
|
|
11
|
+
const build = async () => {
|
|
9
12
|
const errors = mock<ErrorsService>()
|
|
10
13
|
const requestLocale = mock<RequestLocale>()
|
|
11
|
-
|
|
14
|
+
const moduleRef = await Test.createTestingModule({
|
|
15
|
+
providers: [
|
|
16
|
+
ErrorsFilter,
|
|
17
|
+
{ provide: ERRORS_SERVICE, useValue: errors },
|
|
18
|
+
{ provide: REQUEST_LOCALE, useValue: requestLocale },
|
|
19
|
+
],
|
|
20
|
+
}).compile()
|
|
21
|
+
return { filter: moduleRef.get(ErrorsFilter), errors, requestLocale }
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
describe("ErrorsFilter", () => {
|
|
15
|
-
it("hands a failure in a GraphQL context back untouched for the GraphQL error formatter", () => {
|
|
16
|
-
const { filter, errors } = build()
|
|
25
|
+
it("hands a failure in a GraphQL context back untouched for the GraphQL error formatter", async () => {
|
|
26
|
+
const { filter, errors } = await build()
|
|
17
27
|
const failure = new Error("resolver failed")
|
|
18
28
|
const host = mock<ArgumentsHost>()
|
|
19
29
|
host.getType.mockReturnValue("graphql")
|
|
@@ -23,8 +33,8 @@ describe("ErrorsFilter", () => {
|
|
|
23
33
|
expect(host.switchToHttp).not.toHaveBeenCalled()
|
|
24
34
|
})
|
|
25
35
|
|
|
26
|
-
it("writes a localized description and its status to an HTTP response", () => {
|
|
27
|
-
const { filter, errors, requestLocale } = build()
|
|
36
|
+
it("writes a localized description and its status to an HTTP response", async () => {
|
|
37
|
+
const { filter, errors, requestLocale } = await build()
|
|
28
38
|
const failure = new Error("note missing")
|
|
29
39
|
const request = mock<Request>({ headers: { "accept-language": "en-US,en;q=0.9" } })
|
|
30
40
|
const response = mock<Response>()
|
package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { INestApplication } from "@nestjs/common"
|
|
2
|
+
import { Test } from "@nestjs/testing"
|
|
3
|
+
import type { Express, NextFunction, Request, Response } from "express"
|
|
4
|
+
import { mock } from "@starci/jest-preset"
|
|
5
|
+
import { applyHttpSecurity, securityHeaders, validateAllowedOrigins } from "./edge-security.service"
|
|
6
|
+
import type { HttpSecurityOptions } from "./http-security.options"
|
|
7
|
+
|
|
8
|
+
const HTTP_EDGE = Symbol("HTTP_EDGE")
|
|
9
|
+
|
|
10
|
+
const buildEdge = async () => {
|
|
11
|
+
const moduleRef = await Test.createTestingModule({
|
|
12
|
+
providers: [{ provide: HTTP_EDGE, useValue: { applyHttpSecurity, securityHeaders, validateAllowedOrigins } }],
|
|
13
|
+
}).compile()
|
|
14
|
+
return moduleRef.get<typeof import("./edge-security.service")>(HTTP_EDGE)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const options: HttpSecurityOptions = {
|
|
18
|
+
allowedOrigins: ["https://web.example"],
|
|
19
|
+
rateLimit: { windowMs: 1, defaultLimit: 1, strictLimit: 1 },
|
|
20
|
+
webhooks: {},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const appWithServer = () => {
|
|
24
|
+
const server = mock<Express>()
|
|
25
|
+
const adapter = mock<ReturnType<INestApplication["getHttpAdapter"]>>()
|
|
26
|
+
adapter.getInstance.mockReturnValue(server)
|
|
27
|
+
const app = mock<INestApplication>()
|
|
28
|
+
app.getHttpAdapter.mockReturnValue(adapter)
|
|
29
|
+
return { app, server }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("HTTP edge origin validation", () => {
|
|
33
|
+
it("keeps exact HTTPS origins and admits HTTP only when explicitly enabled", async () => {
|
|
34
|
+
const edge = await buildEdge()
|
|
35
|
+
expect(edge.validateAllowedOrigins(["https://web.example", "https://admin.example:8443"], false)).toEqual([
|
|
36
|
+
"https://web.example",
|
|
37
|
+
"https://admin.example:8443",
|
|
38
|
+
])
|
|
39
|
+
expect(edge.validateAllowedOrigins(["http://localhost:3000"], true)).toEqual(["http://localhost:3000"])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it.each([
|
|
43
|
+
"*",
|
|
44
|
+
"https://*.example",
|
|
45
|
+
"https://web.example/",
|
|
46
|
+
"https://web.example/path",
|
|
47
|
+
"https://user:pass@web.example",
|
|
48
|
+
"https://web.example?query=1",
|
|
49
|
+
"https://web.example#hash",
|
|
50
|
+
"not-an-origin",
|
|
51
|
+
"",
|
|
52
|
+
"ftp://web.example",
|
|
53
|
+
])("rejects wildcard, malformed and non-origin allowlist entry %s at boot", async (origin) => {
|
|
54
|
+
const edge = await buildEdge()
|
|
55
|
+
expect(() => edge.validateAllowedOrigins([origin], true)).toThrow(
|
|
56
|
+
expect.objectContaining({ code: "HTTP_SECURITY_REQUEST_INVALID" }),
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("rejects an empty allowlist and HTTP origins without development permission", async () => {
|
|
61
|
+
const edge = await buildEdge()
|
|
62
|
+
expect(() => edge.validateAllowedOrigins([], false)).toThrow(
|
|
63
|
+
expect.objectContaining({ code: "HTTP_SECURITY_REQUEST_INVALID" }),
|
|
64
|
+
)
|
|
65
|
+
expect(() => edge.validateAllowedOrigins(["http://web.example"], false)).toThrow(
|
|
66
|
+
expect.objectContaining({ code: "HTTP_SECURITY_REQUEST_INVALID" }),
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe("HTTP edge installation", () => {
|
|
72
|
+
it("sets every API security header and continues the request", async () => {
|
|
73
|
+
const edge = await buildEdge()
|
|
74
|
+
const response = mock<Response>()
|
|
75
|
+
const next: jest.MockedFunction<NextFunction> = jest.fn()
|
|
76
|
+
edge.securityHeaders(mock<Request>(), response, next)
|
|
77
|
+
expect(response.setHeader.mock.calls).toEqual([
|
|
78
|
+
["Strict-Transport-Security", "max-age=31536000; includeSubDomains"],
|
|
79
|
+
["X-Content-Type-Options", "nosniff"],
|
|
80
|
+
["X-Frame-Options", "DENY"],
|
|
81
|
+
["Referrer-Policy", "no-referrer"],
|
|
82
|
+
["Content-Security-Policy", "default-src 'none'; base-uri 'none'; frame-ancestors 'none'"],
|
|
83
|
+
])
|
|
84
|
+
expect(next).toHaveBeenCalledTimes(1)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it("installs headers before CORS, removes x-powered-by and trusts the declared ingress hop count", async () => {
|
|
88
|
+
const edge = await buildEdge()
|
|
89
|
+
const { app, server } = appWithServer()
|
|
90
|
+
edge.applyHttpSecurity(app, { ...options, allowHttpOrigins: false }, { trustProxy: 1 })
|
|
91
|
+
expect(server.disable).toHaveBeenCalledWith("x-powered-by")
|
|
92
|
+
expect(server.set).toHaveBeenCalledWith("trust proxy", 1)
|
|
93
|
+
expect(app.use).toHaveBeenCalledWith(edge.securityHeaders)
|
|
94
|
+
expect(app.enableCors).toHaveBeenCalledWith({ origin: ["https://web.example"] })
|
|
95
|
+
expect(app.use.mock.invocationCallOrder[0]).toBeLessThan(app.enableCors.mock.invocationCallOrder[0] ?? 0)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it("defaults manually supplied options to HTTPS and accepts explicit zero proxy hops", async () => {
|
|
99
|
+
const edge = await buildEdge()
|
|
100
|
+
const { app, server } = appWithServer()
|
|
101
|
+
edge.applyHttpSecurity(app, options, { trustProxy: 0 })
|
|
102
|
+
expect(server.set).toHaveBeenCalledWith("trust proxy", 0)
|
|
103
|
+
expect(() =>
|
|
104
|
+
edge.applyHttpSecurity(app, { ...options, allowedOrigins: ["http://web.example"] }, { trustProxy: 1 }),
|
|
105
|
+
).toThrow()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it("admits development HTTP origins when explicitly enabled", async () => {
|
|
109
|
+
const edge = await buildEdge()
|
|
110
|
+
const { app } = appWithServer()
|
|
111
|
+
edge.applyHttpSecurity(
|
|
112
|
+
app,
|
|
113
|
+
{ ...options, allowedOrigins: ["http://localhost:3000"], allowHttpOrigins: true },
|
|
114
|
+
{ trustProxy: 1 },
|
|
115
|
+
)
|
|
116
|
+
expect(app.enableCors).toHaveBeenCalledWith({ origin: ["http://localhost:3000"] })
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it.each([-1, 0.5, Number.NaN, Number.MAX_SAFE_INTEGER + 1])(
|
|
120
|
+
"rejects unsafe proxy hop count %s",
|
|
121
|
+
async (trustProxy) => {
|
|
122
|
+
const edge = await buildEdge()
|
|
123
|
+
const { app } = appWithServer()
|
|
124
|
+
expect(() => edge.applyHttpSecurity(app, options, { trustProxy })).toThrow(
|
|
125
|
+
expect.objectContaining({ code: "HTTP_SECURITY_REQUEST_INVALID" }),
|
|
126
|
+
)
|
|
127
|
+
expect(app.enableCors).not.toHaveBeenCalled()
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { INestApplication } from "@nestjs/common"
|
|
2
|
+
import type { Express, RequestHandler } from "express"
|
|
3
|
+
import { HttpSecurityError, HttpSecurityErrorCode } from "./errors/http-security.error"
|
|
4
|
+
import type { HttpSecurityOptions } from "./http-security.options"
|
|
5
|
+
|
|
6
|
+
/** The declared ingress hop count and the environment in which HTTP origins may be used. */
|
|
7
|
+
export interface HttpEdgeOptions {
|
|
8
|
+
/** Exact trusted proxy hops; the standard API stack has one ingress in front of it. */
|
|
9
|
+
readonly trustProxy: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Validates exact browser origins at boot, before CORS can admit a request. */
|
|
13
|
+
export const validateAllowedOrigins = (origins: ReadonlyArray<string>, allowHttpOrigins: boolean): Array<string> => {
|
|
14
|
+
if (origins.length === 0)
|
|
15
|
+
throw new HttpSecurityError({
|
|
16
|
+
code: HttpSecurityErrorCode.RequestInvalid,
|
|
17
|
+
params: { fields: "HTTP_SECURITY_ALLOWED_ORIGINS" },
|
|
18
|
+
})
|
|
19
|
+
return origins.map((origin) => {
|
|
20
|
+
const parsed = URL.canParse(origin) ? new URL(origin) : undefined
|
|
21
|
+
if (
|
|
22
|
+
parsed === undefined ||
|
|
23
|
+
origin.includes("*") ||
|
|
24
|
+
parsed.origin !== origin ||
|
|
25
|
+
(parsed.protocol !== "https:" && !(allowHttpOrigins && parsed.protocol === "http:"))
|
|
26
|
+
)
|
|
27
|
+
throw new HttpSecurityError({
|
|
28
|
+
code: HttpSecurityErrorCode.RequestInvalid,
|
|
29
|
+
params: { fields: "HTTP_SECURITY_ALLOWED_ORIGINS" },
|
|
30
|
+
})
|
|
31
|
+
return origin
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Security headers of the API's data and probe responses, including errors. */
|
|
36
|
+
export const securityHeaders: RequestHandler = (_request, response, next) => {
|
|
37
|
+
response.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
|
38
|
+
response.setHeader("X-Content-Type-Options", "nosniff")
|
|
39
|
+
response.setHeader("X-Frame-Options", "DENY")
|
|
40
|
+
response.setHeader("Referrer-Policy", "no-referrer")
|
|
41
|
+
response.setHeader("Content-Security-Policy", "default-src 'none'; base-uri 'none'; frame-ancestors 'none'")
|
|
42
|
+
next()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Installs the API edge policy once: validated CORS, security headers and a declared numeric ingress hop count. */
|
|
46
|
+
export const applyHttpSecurity = (app: INestApplication, options: HttpSecurityOptions, edge: HttpEdgeOptions): void => {
|
|
47
|
+
const origins = validateAllowedOrigins(options.allowedOrigins, options.allowHttpOrigins === true)
|
|
48
|
+
if (!Number.isSafeInteger(edge.trustProxy) || edge.trustProxy < 0)
|
|
49
|
+
throw new HttpSecurityError({
|
|
50
|
+
code: HttpSecurityErrorCode.RequestInvalid,
|
|
51
|
+
params: { fields: "HTTP_SECURITY_TRUST_PROXY" },
|
|
52
|
+
})
|
|
53
|
+
const server: Express = app.getHttpAdapter().getInstance()
|
|
54
|
+
server.disable("x-powered-by")
|
|
55
|
+
server.set("trust proxy", edge.trustProxy)
|
|
56
|
+
app.use(securityHeaders)
|
|
57
|
+
app.enableCors({ origin: origins })
|
|
58
|
+
}
|
package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { ErrorKind } from "@modules/platform/errors"
|
|
|
3
3
|
|
|
4
4
|
/** Codes of the http-security capability. */
|
|
5
5
|
export enum HttpSecurityErrorCode {
|
|
6
|
+
/** The required primary database failed or did not answer before the readiness deadline. */
|
|
7
|
+
RequiredDependencyUnavailable = "HTTP_SECURITY_REQUIRED_DEPENDENCY_UNAVAILABLE",
|
|
6
8
|
/** A state-changing request came from an origin that is not on the allowlist. */
|
|
7
9
|
OriginRejected = "HTTP_SECURITY_ORIGIN_REJECTED",
|
|
8
10
|
/** The caller sent more requests than its tier allows in the current window. */
|
|
@@ -19,6 +21,7 @@ export enum HttpSecurityErrorCode {
|
|
|
19
21
|
|
|
20
22
|
/** How each http-security code travels. */
|
|
21
23
|
export const HTTP_SECURITY_ERROR_KINDS: Record<HttpSecurityErrorCode, ErrorKind> = {
|
|
24
|
+
[HttpSecurityErrorCode.RequiredDependencyUnavailable]: "unavailable",
|
|
22
25
|
[HttpSecurityErrorCode.OriginRejected]: "forbidden",
|
|
23
26
|
[HttpSecurityErrorCode.RateLimited]: "rate-limited",
|
|
24
27
|
[HttpSecurityErrorCode.RequestInvalid]: "invalid",
|
package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import type { ExecutionContext } from "@nestjs/common"
|
|
2
2
|
import type { Request } from "express"
|
|
3
|
+
import { Test } from "@nestjs/testing"
|
|
3
4
|
import { mock } from "@starci/jest-preset"
|
|
4
5
|
import { requestOf } from "./execution-request.mapper"
|
|
5
6
|
|
|
7
|
+
const REQUEST_MAPPER = Symbol("REQUEST_MAPPER")
|
|
8
|
+
|
|
9
|
+
const build = async () => {
|
|
10
|
+
const moduleRef = await Test.createTestingModule({
|
|
11
|
+
providers: [{ provide: REQUEST_MAPPER, useValue: requestOf }],
|
|
12
|
+
}).compile()
|
|
13
|
+
return moduleRef.get<typeof requestOf>(REQUEST_MAPPER)
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
describe("requestOf", () => {
|
|
7
|
-
it("reads the HTTP request behind an HTTP execution context", () => {
|
|
17
|
+
it("reads the HTTP request behind an HTTP execution context", async () => {
|
|
18
|
+
const mapRequest = await build()
|
|
8
19
|
const request = mock<Request>()
|
|
9
20
|
const http = mock<ReturnType<ExecutionContext["switchToHttp"]>>()
|
|
10
21
|
http.getRequest.mockReturnValue(request)
|
|
@@ -12,17 +23,18 @@ describe("requestOf", () => {
|
|
|
12
23
|
context.getType.mockReturnValue("http")
|
|
13
24
|
context.switchToHttp.mockReturnValue(http)
|
|
14
25
|
|
|
15
|
-
expect(
|
|
26
|
+
expect(mapRequest(context)).toBe(request)
|
|
16
27
|
expect(context.getArgs).not.toHaveBeenCalled()
|
|
17
28
|
})
|
|
18
29
|
|
|
19
|
-
it("reads the request of the GraphQL context behind a resolver's execution context", () => {
|
|
30
|
+
it("reads the request of the GraphQL context behind a resolver's execution context", async () => {
|
|
31
|
+
const mapRequest = await build()
|
|
20
32
|
const request = mock<Request>()
|
|
21
33
|
const context = mock<ExecutionContext>()
|
|
22
34
|
context.getType.mockReturnValue("graphql")
|
|
23
35
|
context.getArgs.mockReturnValue([undefined, {}, { req: request }, undefined])
|
|
24
36
|
|
|
25
|
-
expect(
|
|
37
|
+
expect(mapRequest(context)).toBe(request)
|
|
26
38
|
expect(context.switchToHttp).not.toHaveBeenCalled()
|
|
27
39
|
})
|
|
28
40
|
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EnvSource } from "@modules/platform/config"
|
|
2
2
|
import type { HttpSecurityOptions, WebhookProviderOptions } from "./http-security.options"
|
|
3
|
+
import { validateAllowedOrigins } from "./edge-security.service"
|
|
3
4
|
|
|
4
5
|
/** Reads one webhook provider's required signing secret and bounded replay window. */
|
|
5
6
|
export const parseWebhookProviderConfig = (env: EnvSource, prefix: string): WebhookProviderOptions => ({
|
|
@@ -8,16 +9,21 @@ export const parseWebhookProviderConfig = (env: EnvSource, prefix: string): Webh
|
|
|
8
9
|
})
|
|
9
10
|
|
|
10
11
|
/** Reads the http-security options: the origin allowlist is required, the rate limits are tunables with literal defaults. */
|
|
11
|
-
export const parseHttpSecurityConfig = (env: EnvSource): HttpSecurityOptions =>
|
|
12
|
-
|
|
12
|
+
export const parseHttpSecurityConfig = (env: EnvSource): HttpSecurityOptions => {
|
|
13
|
+
const mode = env.optional("NODE_ENV") ?? "development"
|
|
14
|
+
const allowHttpOrigins = mode === "development" || mode === "test"
|
|
15
|
+
const origins = env
|
|
13
16
|
.string("HTTP_SECURITY_ALLOWED_ORIGINS")
|
|
14
17
|
.split(",")
|
|
15
18
|
.map((origin) => origin.trim())
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
19
|
+
return {
|
|
20
|
+
allowedOrigins: validateAllowedOrigins(origins, allowHttpOrigins),
|
|
21
|
+
allowHttpOrigins,
|
|
22
|
+
rateLimit: {
|
|
23
|
+
windowMs: env.duration("HTTP_SECURITY_RATE_WINDOW", 60_000),
|
|
24
|
+
defaultLimit: env.int("HTTP_SECURITY_RATE_DEFAULT_LIMIT", 600),
|
|
25
|
+
strictLimit: env.int("HTTP_SECURITY_RATE_STRICT_LIMIT", 30),
|
|
26
|
+
},
|
|
27
|
+
webhooks: {},
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { DynamicModule } from "@nestjs/common"
|
|
|
3
3
|
import { APP_PIPE } from "@nestjs/core"
|
|
4
4
|
import { WEBHOOK_SIGNATURE } from "./http-security.decorators"
|
|
5
5
|
import { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } from "./http-security.module-definition"
|
|
6
|
+
import { ReadinessService } from "./readiness.service"
|
|
6
7
|
import { RequestValidationService } from "./request-validation.service"
|
|
7
8
|
import { WebhookSignatureService } from "./webhook-signature.service"
|
|
8
9
|
|
|
@@ -18,9 +19,10 @@ export class HttpSecurityModule extends ConfigurableModuleClass {
|
|
|
18
19
|
...(base.providers ?? []),
|
|
19
20
|
{ provide: APP_PIPE, useClass: RequestValidationService },
|
|
20
21
|
WebhookSignatureService,
|
|
22
|
+
ReadinessService,
|
|
21
23
|
{ provide: WEBHOOK_SIGNATURE, useExisting: WebhookSignatureService },
|
|
22
24
|
],
|
|
23
|
-
exports: [MODULE_OPTIONS_TOKEN, WebhookSignatureService, WEBHOOK_SIGNATURE],
|
|
25
|
+
exports: [MODULE_OPTIONS_TOKEN, WebhookSignatureService, WEBHOOK_SIGNATURE, ReadinessService],
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
}
|
|
@@ -22,6 +22,8 @@ export interface WebhookProviderOptions {
|
|
|
22
22
|
export interface HttpSecurityOptions {
|
|
23
23
|
/** The origins allowed to send state-changing requests from a browser. */
|
|
24
24
|
readonly allowedOrigins: ReadonlyArray<string>
|
|
25
|
+
/** Plain HTTP origins are admitted only by a development or test environment reader. */
|
|
26
|
+
readonly allowHttpOrigins?: boolean
|
|
25
27
|
/** The rate limit tiers. */
|
|
26
28
|
readonly rateLimit: RateLimitOptions
|
|
27
29
|
/** Signed-webhook providers keyed by their generated route name. */
|
|
@@ -4,4 +4,9 @@ export { HttpSecurityModule } from "./http-security.module"
|
|
|
4
4
|
export type { HttpSecurityOptions } from "./http-security.options"
|
|
5
5
|
export { HTTP_SECURITY_MESSAGES } from "./messages/http-security.messages"
|
|
6
6
|
export { OriginGuard } from "./origin.guard"
|
|
7
|
+
export { requestOf } from "./execution-request.mapper"
|
|
7
8
|
export { RateLimitGuard } from "./rate-limit.guard"
|
|
9
|
+
export { applyHttpSecurity } from "./edge-security.service"
|
|
10
|
+
export { ReadinessService } from "./readiness.service"
|
|
11
|
+
export { InjectReadiness } from "./readiness.decorators"
|
|
12
|
+
export type { ReadinessReport } from "./readiness.contracts"
|
package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts
CHANGED
|
@@ -3,14 +3,21 @@ import type { MessageBundle } from "@modules/platform/i18n"
|
|
|
3
3
|
/** Display text of the http-security codes, Vietnamese and English. */
|
|
4
4
|
export const HTTP_SECURITY_MESSAGES: MessageBundle = {
|
|
5
5
|
vi: {
|
|
6
|
-
"errors.
|
|
7
|
-
"errors.
|
|
8
|
-
|
|
6
|
+
"errors.HTTP_SECURITY_REQUIRED_DEPENDENCY_UNAVAILABLE": "C\u01a1 s\u1edf d\u1eef li\u1ec7u b\u1eaft bu\u1ed9c hi\u1ec7n kh\u00f4ng kh\u1ea3 d\u1ee5ng.",
|
|
7
|
+
"errors.HTTP_SECURITY_ORIGIN_REJECTED":
|
|
8
|
+
"Ngu\u1ed3n g\u1eedi y\u00eau c\u1ea7u kh\u00f4ng \u0111\u01b0\u1ee3c ph\u00e9p.",
|
|
9
|
+
"errors.HTTP_SECURITY_RATE_LIMITED":
|
|
10
|
+
"B\u1ea1n g\u1eedi qu\u00e1 nhi\u1ec1u y\u00eau c\u1ea7u, h\u00e3y th\u1eed l\u1ea1i sau.",
|
|
11
|
+
"errors.HTTP_SECURITY_REQUEST_INVALID":
|
|
12
|
+
"D\u1eef li\u1ec7u g\u1eedi l\u00ean kh\u00f4ng h\u1ee3p l\u1ec7: {{fields}}.",
|
|
9
13
|
"errors.HTTP_SECURITY_WEBHOOK_SIGNATURE_INVALID": "Ch\u1eef k\u00fd webhook kh\u00f4ng h\u1ee3p l\u1ec7.",
|
|
10
|
-
"errors.HTTP_SECURITY_WEBHOOK_REPLAYED":
|
|
11
|
-
|
|
14
|
+
"errors.HTTP_SECURITY_WEBHOOK_REPLAYED":
|
|
15
|
+
"Webhook n\u1eb1m ngo\u00e0i c\u1eeda s\u1ed5 th\u1eddi gian cho ph\u00e9p.",
|
|
16
|
+
"errors.HTTP_SECURITY_WEBHOOK_PROVIDER_UNKNOWN":
|
|
17
|
+
"Nh\u00e0 cung c\u1ea5p webhook {{provider}} ch\u01b0a \u0111\u01b0\u1ee3c c\u1ea5u h\u00ecnh.",
|
|
12
18
|
},
|
|
13
19
|
en: {
|
|
20
|
+
"errors.HTTP_SECURITY_REQUIRED_DEPENDENCY_UNAVAILABLE": "The required database is unavailable.",
|
|
14
21
|
"errors.HTTP_SECURITY_ORIGIN_REJECTED": "The origin of the request is not allowed.",
|
|
15
22
|
"errors.HTTP_SECURITY_RATE_LIMITED": "Too many requests, try again later.",
|
|
16
23
|
"errors.HTTP_SECURITY_REQUEST_INVALID": "The submitted data is invalid: {{fields}}.",
|
|
@@ -17,7 +17,9 @@ const options: HttpSecurityOptions = {
|
|
|
17
17
|
webhooks: {},
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const build = async (
|
|
20
|
+
const build = async (clock: FakeClock, tier?: RateTier) => {
|
|
21
|
+
const reflector = mock<Reflector>()
|
|
22
|
+
reflector.getAllAndOverride.mockReturnValue(tier)
|
|
21
23
|
const moduleRef = await Test.createTestingModule({
|
|
22
24
|
providers: [
|
|
23
25
|
RateLimitGuard,
|
|
@@ -26,7 +28,7 @@ const build = async (reflector: Reflector, clock: FakeClock) => {
|
|
|
26
28
|
{ provide: HTTP_SECURITY_OPTIONS, useValue: options },
|
|
27
29
|
],
|
|
28
30
|
}).compile()
|
|
29
|
-
return moduleRef.get(RateLimitGuard)
|
|
31
|
+
return { guard: moduleRef.get(RateLimitGuard), reflector }
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/** An execution context of one POST from `ip` on the door; the handler and the class carry the tier metadata. */
|
|
@@ -47,17 +49,20 @@ describe("RateLimit", () => {
|
|
|
47
49
|
it("marks a door with the tier it declares, and the guard counts it under that tier's limit", async () => {
|
|
48
50
|
class Door {}
|
|
49
51
|
RateLimit(RateTier.Strict)(Door)
|
|
50
|
-
const guard = await build(new
|
|
51
|
-
const
|
|
52
|
+
const { guard, reflector } = await build(new FakeClock(AT), RateTier.Strict)
|
|
53
|
+
const handler = () => undefined
|
|
54
|
+
const { context } = contextOf("127.0.0.1", handler, Door)
|
|
52
55
|
|
|
56
|
+
expect(Reflect.getMetadata("platform.http-security.rate-tier", Door)).toBe(RateTier.Strict)
|
|
53
57
|
expect(guard.canActivate(context)).toBe(true)
|
|
54
58
|
expect(() => guard.canActivate(context)).toThrow(RATE_LIMITED)
|
|
59
|
+
expect(reflector.getAllAndOverride).toHaveBeenCalledWith("platform.http-security.rate-tier", [handler, Door])
|
|
55
60
|
})
|
|
56
61
|
})
|
|
57
62
|
|
|
58
63
|
describe("RateLimitGuard", () => {
|
|
59
64
|
it("counts an unmarked door under the default limit", async () => {
|
|
60
|
-
const guard = await build(
|
|
65
|
+
const { guard } = await build(new FakeClock(AT))
|
|
61
66
|
const { context } = contextOf("127.0.0.1")
|
|
62
67
|
|
|
63
68
|
expect(guard.canActivate(context)).toBe(true)
|
|
@@ -67,7 +72,7 @@ describe("RateLimitGuard", () => {
|
|
|
67
72
|
|
|
68
73
|
it("opens a fresh window for a caller whose bucket expired", async () => {
|
|
69
74
|
const clock = new FakeClock(AT)
|
|
70
|
-
const guard = await build(
|
|
75
|
+
const { guard } = await build(clock)
|
|
71
76
|
const { context } = contextOf("127.0.0.1")
|
|
72
77
|
|
|
73
78
|
guard.canActivate(context)
|
|
@@ -80,7 +85,7 @@ describe("RateLimitGuard", () => {
|
|
|
80
85
|
})
|
|
81
86
|
|
|
82
87
|
it("counts a request without an address under the unknown bucket", async () => {
|
|
83
|
-
const guard = await build(
|
|
88
|
+
const { guard } = await build(new FakeClock(AT))
|
|
84
89
|
const { context } = contextOf(undefined)
|
|
85
90
|
|
|
86
91
|
guard.canActivate(context)
|
|
@@ -90,7 +95,7 @@ describe("RateLimitGuard", () => {
|
|
|
90
95
|
|
|
91
96
|
it("prunes the expired buckets when the map outgrows the bucket cap", async () => {
|
|
92
97
|
const clock = new FakeClock(AT)
|
|
93
|
-
const guard = await build(
|
|
98
|
+
const { guard } = await build(clock)
|
|
94
99
|
const { context, setIp } = contextOf("0.0.0.0")
|
|
95
100
|
|
|
96
101
|
for (let index = 0; index <= 10_000; index += 1) {
|