@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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Illustrative production model. Replace all-zero application digests with verified published release digests before use.
|
|
2
|
+
version: "3.8"
|
|
3
|
+
services:
|
|
4
|
+
api:
|
|
5
|
+
image: "{{project}}/api:v0.0.0@sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
|
6
|
+
environment:
|
|
7
|
+
NODE_ENV: production
|
|
8
|
+
PORT: "3001"
|
|
9
|
+
HTTP_SECURITY_ALLOWED_ORIGINS: https://{{project}}.example.invalid
|
|
10
|
+
PRIMARY_DB_URL_FILE: /run/secrets/primary-db-url
|
|
11
|
+
PRIMARY_DB_SCHEMA: public
|
|
12
|
+
SUPABASE_URL: https://{{project}}.supabase.co
|
|
13
|
+
SUPABASE_SERVICE_ROLE_KEY_FILE: /run/secrets/supabase-service-role-key
|
|
14
|
+
secrets:
|
|
15
|
+
- source: primary-db-url
|
|
16
|
+
target: primary-db-url
|
|
17
|
+
- source: supabase-service-role-key
|
|
18
|
+
target: supabase-service-role-key
|
|
19
|
+
networks: [app]
|
|
20
|
+
healthcheck:
|
|
21
|
+
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3001/health/live').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
|
22
|
+
interval: 30s
|
|
23
|
+
timeout: 5s
|
|
24
|
+
start_period: 20s
|
|
25
|
+
retries: 3
|
|
26
|
+
deploy:
|
|
27
|
+
replicas: 1
|
|
28
|
+
placement:
|
|
29
|
+
constraints: ["node.labels.{{project}} == production"]
|
|
30
|
+
update_config:
|
|
31
|
+
parallelism: 1
|
|
32
|
+
order: start-first
|
|
33
|
+
failure_action: rollback
|
|
34
|
+
monitor: 30s
|
|
35
|
+
rollback_config:
|
|
36
|
+
parallelism: 1
|
|
37
|
+
order: stop-first
|
|
38
|
+
restart_policy:
|
|
39
|
+
condition: on-failure
|
|
40
|
+
delay: 5s
|
|
41
|
+
web:
|
|
42
|
+
image: "{{project}}/web:v0.0.0@sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
|
43
|
+
environment:
|
|
44
|
+
NODE_ENV: production
|
|
45
|
+
PORT: "3000"
|
|
46
|
+
HOSTNAME: 0.0.0.0
|
|
47
|
+
NEXT_PUBLIC_SITE_URL: https://{{project}}.example.invalid
|
|
48
|
+
networks: [app]
|
|
49
|
+
healthcheck:
|
|
50
|
+
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/health/live').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
|
51
|
+
interval: 30s
|
|
52
|
+
timeout: 5s
|
|
53
|
+
start_period: 20s
|
|
54
|
+
retries: 3
|
|
55
|
+
deploy:
|
|
56
|
+
replicas: 1
|
|
57
|
+
placement:
|
|
58
|
+
constraints: ["node.labels.{{project}} == production"]
|
|
59
|
+
update_config:
|
|
60
|
+
parallelism: 1
|
|
61
|
+
order: start-first
|
|
62
|
+
failure_action: rollback
|
|
63
|
+
monitor: 30s
|
|
64
|
+
rollback_config:
|
|
65
|
+
parallelism: 1
|
|
66
|
+
order: stop-first
|
|
67
|
+
restart_policy:
|
|
68
|
+
condition: on-failure
|
|
69
|
+
delay: 5s
|
|
70
|
+
ingress:
|
|
71
|
+
image: caddy:2.10.2@sha256:c3d7ee5d2b11f9dc54f947f68a734c84e9c9666c92c88a7f30b9cba5da182adb
|
|
72
|
+
configs:
|
|
73
|
+
- source: ingress-config
|
|
74
|
+
target: /etc/caddy/Caddyfile
|
|
75
|
+
ports:
|
|
76
|
+
- { target: 80, published: 80, protocol: tcp, mode: ingress }
|
|
77
|
+
- { target: 443, published: 443, protocol: tcp, mode: ingress }
|
|
78
|
+
volumes:
|
|
79
|
+
- ingress-data:/data
|
|
80
|
+
- ingress-config-data:/config
|
|
81
|
+
networks: [app]
|
|
82
|
+
healthcheck:
|
|
83
|
+
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:2019/config/"]
|
|
84
|
+
interval: 30s
|
|
85
|
+
timeout: 5s
|
|
86
|
+
retries: 3
|
|
87
|
+
deploy:
|
|
88
|
+
replicas: 1
|
|
89
|
+
placement:
|
|
90
|
+
constraints: ["node.labels.{{project}} == production"]
|
|
91
|
+
update_config:
|
|
92
|
+
parallelism: 1
|
|
93
|
+
order: stop-first
|
|
94
|
+
failure_action: rollback
|
|
95
|
+
monitor: 30s
|
|
96
|
+
rollback_config:
|
|
97
|
+
parallelism: 1
|
|
98
|
+
order: stop-first
|
|
99
|
+
restart_policy:
|
|
100
|
+
condition: on-failure
|
|
101
|
+
delay: 5s
|
|
102
|
+
networks:
|
|
103
|
+
app:
|
|
104
|
+
driver: overlay
|
|
105
|
+
secrets:
|
|
106
|
+
primary-db-url:
|
|
107
|
+
external: true
|
|
108
|
+
name: "{{project}}-production-primary-db-url-v1"
|
|
109
|
+
supabase-service-role-key:
|
|
110
|
+
external: true
|
|
111
|
+
name: "{{project}}-production-supabase-service-role-key-v1"
|
|
112
|
+
configs:
|
|
113
|
+
ingress-config:
|
|
114
|
+
file: ./Caddyfile
|
|
115
|
+
volumes:
|
|
116
|
+
ingress-data:
|
|
117
|
+
external: true
|
|
118
|
+
name: "{{project}}-production-ingress-data"
|
|
119
|
+
ingress-config-data:
|
|
120
|
+
external: true
|
|
121
|
+
name: "{{project}}-production-ingress-config-data"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Production environment key roster
|
|
2
|
+
|
|
3
|
+
This inventory declares names, consumers and mounted file paths. Scaffolding generates the two declared encrypted members with placeholder values encrypted to this app's own age recipient. Replace their values through `starci app secret set production` after owner provisioning and before creating external secrets. The runtime template tracks no ciphertext; its specs generate members with ephemeral identities in disposable copies. Scaffolding supplies no deployment credential or successful deployment.
|
|
4
|
+
|
|
5
|
+
| Key | Consumer |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `PORT` | API listener |
|
|
8
|
+
| `HTTP_SECURITY_ALLOWED_ORIGINS` | API origin allowlist |
|
|
9
|
+
| `PRIMARY_DB_URL` | API least-privilege database connection; reads `PRIMARY_DB_URL_FILE` at `/run/secrets/primary-db-url` |
|
|
10
|
+
| `PRIMARY_DB_SCHEMA` | API primary database schema |
|
|
11
|
+
| `SUPABASE_URL` | API Auth and Storage integration |
|
|
12
|
+
| `SUPABASE_SERVICE_ROLE_KEY` | API Auth and Storage integration; reads `SUPABASE_SERVICE_ROLE_KEY_FILE` at `/run/secrets/supabase-service-role-key` |
|
|
13
|
+
| `NEXT_PUBLIC_SITE_URL` | Front-end site URL |
|
|
14
|
+
| `NEXT_PUBLIC_GRAPHQL_URL` | Front-end GraphQL transport |
|
|
15
|
+
| `NEXT_PUBLIC_SUPABASE_URL` | Browser Auth client |
|
|
16
|
+
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Browser Auth client public key |
|
|
17
|
+
| `SUPABASE_ACCESS_TOKEN` | Production database deployment |
|
|
18
|
+
| `SUPABASE_DB_PASSWORD` | Production database deployment |
|
|
19
|
+
| `SUPABASE_PROJECT_ID` | Production database deployment |
|
|
20
|
+
|
|
21
|
+
Public front-end configuration is supplied at build time and stays outside sealed custody. Set real values in `.starcistacks/production/secrets/primary-db-url.enc` and `.starcistacks/production/secrets/supabase-service-role-key.enc` at the declared paths. Versioned external application secrets are `{{project}}-production-primary-db-url-v1` and `{{project}}-production-supabase-service-role-key-v1`. Seal deployment-only credentials separately under `.starcistacks/production/secrets/*.enc`; they never enter an API container.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
schema: work/brand@1
|
|
2
|
+
id: brand
|
|
3
|
+
kind: brand
|
|
4
|
+
state: todo
|
|
5
|
+
rev: 1
|
|
6
|
+
brand:
|
|
7
|
+
identity:
|
|
8
|
+
name: "{{project}}"
|
|
9
|
+
review:
|
|
10
|
+
reviewer: the {{project}} owner
|
|
11
|
+
authority: "The lite scaffold seeds this record so the brand tree exists; the owner settles rev 1 of the brand before the first drawn surface."
|
|
12
|
+
reviewedAt: "2026-10-14T00:00:00Z"
|
|
@@ -4,7 +4,7 @@ One StarCi lite app: a Nest API in `be/`, one Next web app in `fe/`, and Supabas
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
The starter keeps one deployable API and one web app, with the same code structure as a full app: the web app calls the API over GraphQL (`src/modules/api`, SWR hooks under `src/hooks/swr`) and uses Supabase only for Auth in the browser (`src/modules/auth`); every data read and write goes through the API, which verifies the access token.
|
|
7
|
+
The starter keeps one deployable API and one web app, with the same code structure as a full app: the web app calls the API over GraphQL (`fe/apps/web/src/modules/api`, SWR hooks under `fe/apps/web/src/hooks/swr`) and uses Supabase only for Auth in the browser (`fe/apps/web/src/modules/auth`); every data read and write goes through the API, which verifies the access token.
|
|
8
8
|
|
|
9
9
|
## Stack
|
|
10
10
|
|
|
@@ -14,9 +14,9 @@ TypeScript everywhere, NestJS in `be/`, Next.js with next-intl in `fe/`, Supabas
|
|
|
14
14
|
|
|
15
15
|
- `supabase/`: local configuration, forward-only SQL migrations, seed data, and generated database types.
|
|
16
16
|
- `be/`: the API, shared platform capabilities, verified identity admission, and privileged Supabase integrations.
|
|
17
|
-
- `fe/`: the web app; the only Supabase client is the Auth client of `src/modules/auth`, data goes through `src/modules/api`.
|
|
17
|
+
- `fe/`: the web app; the only Supabase client is the Auth client of `fe/apps/web/src/modules/auth`, data goes through `fe/apps/web/src/modules/api`.
|
|
18
18
|
- `.starcistacks/`: value-free environment metadata and encrypted-secret custody.
|
|
19
|
-
- `.starciwork/`: `.gitignore`,
|
|
19
|
+
- `.starciwork/`: `.gitignore`, `.starciwork/workspace.yaml` project metadata and repo roles, and optional brand drawing records, references, and assets.
|
|
20
20
|
|
|
21
21
|
## Development
|
|
22
22
|
|
|
@@ -34,6 +34,8 @@ The API reads `PORT`, `HTTP_SECURITY_ALLOWED_ORIGINS`, `PRIMARY_DB_URL`, `PRIMAR
|
|
|
34
34
|
|
|
35
35
|
`npm test` runs the back end's unit specs with per-file coverage. Optional integration specs run with `npm run test:integration` against the local Supabase stack (`npm run db:start`); a lite app has no e2e world.
|
|
36
36
|
|
|
37
|
+
The public runtime template tracks only sealed member names. Scaffolding creates this app's own age recipient and encrypts generated placeholder values into the declared `.starcistacks/<environment>/secrets/*.enc` members. Keep the matching host age identity in untracked local custody, then use `starci app secret set` to replace each placeholder with an actual value before deployment. Product apps retain sealed member custody. Runtime specs use disposable copies with ephemeral identities and remove them afterwards.
|
|
38
|
+
|
|
37
39
|
## Database changes
|
|
38
40
|
|
|
39
41
|
Create a forward-only migration under `supabase/migrations`, keep each table's RLS policies and grants beside the table, and run `npm run contract:emit` to refresh `supabase/types/database.types.ts`. `starci app add table <name> --fe` adds the table with its GraphQL feature on the API and its operations and SWR hooks in the web app; `npm run codegen:graphql` regenerates the web app's wire types from `be/contracts/<app>/schema.graphql`.
|
|
@@ -3,7 +3,7 @@ project_id = "{{project}}"
|
|
|
3
3
|
[api]
|
|
4
4
|
enabled = true
|
|
5
5
|
port = {{supabasePortApi}}
|
|
6
|
-
schemas = [
|
|
6
|
+
schemas = []
|
|
7
7
|
extra_search_path = ["public", "extensions"]
|
|
8
8
|
max_rows = 1000
|
|
9
9
|
|
|
@@ -25,6 +25,13 @@ port = {{supabasePortInbucket}}
|
|
|
25
25
|
[analytics]
|
|
26
26
|
port = {{supabasePortAnalytics}}
|
|
27
27
|
|
|
28
|
+
[edge_runtime]
|
|
29
|
+
enabled = false
|
|
30
|
+
|
|
31
|
+
[storage]
|
|
32
|
+
enabled = true
|
|
33
|
+
file_size_limit = "50MiB"
|
|
34
|
+
|
|
28
35
|
[auth]
|
|
29
36
|
enabled = true
|
|
30
37
|
site_url = "http://127.0.0.1:3000"
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
do $$
|
|
2
|
+
begin
|
|
3
|
+
if not exists (select 1 from pg_catalog.pg_roles where rolname = 'app_be') then
|
|
4
|
+
create role app_be login noinherit nosuperuser nobypassrls nocreatedb nocreaterole;
|
|
5
|
+
end if;
|
|
6
|
+
end;
|
|
7
|
+
$$;
|
|
8
|
+
|
|
9
|
+
-- The attributes a Supabase migration role may change: SUPERUSER and BYPASSRLS stay as create role set them (the role migration owner is no superuser).
|
|
10
|
+
alter role app_be login noinherit nocreatedb nocreaterole;
|
|
3
11
|
|
|
4
12
|
create schema if not exists private;
|
|
5
13
|
|
|
@@ -24,6 +32,7 @@ create table public.profiles (
|
|
|
24
32
|
);
|
|
25
33
|
|
|
26
34
|
alter table public.profiles enable row level security;
|
|
35
|
+
alter table public.profiles force row level security;
|
|
27
36
|
|
|
28
37
|
create trigger profiles_set_updated_at
|
|
29
38
|
before update on public.profiles
|
|
@@ -34,20 +43,20 @@ create policy profiles_app_be_select
|
|
|
34
43
|
on public.profiles
|
|
35
44
|
for select
|
|
36
45
|
to app_be
|
|
37
|
-
using (
|
|
46
|
+
using (current_user = 'app_be');
|
|
38
47
|
|
|
39
48
|
create policy profiles_app_be_insert
|
|
40
49
|
on public.profiles
|
|
41
50
|
for insert
|
|
42
51
|
to app_be
|
|
43
|
-
with check (
|
|
52
|
+
with check (current_user = 'app_be');
|
|
44
53
|
|
|
45
54
|
create policy profiles_app_be_update
|
|
46
55
|
on public.profiles
|
|
47
56
|
for update
|
|
48
57
|
to app_be
|
|
49
|
-
using (
|
|
50
|
-
with check (
|
|
58
|
+
using (current_user = 'app_be')
|
|
59
|
+
with check (current_user = 'app_be');
|
|
51
60
|
|
|
52
61
|
grant usage on schema public to app_be;
|
|
53
62
|
revoke all on table public.profiles from public, anon, authenticated;
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# Development stack
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The app services may run in a local Docker Swarm; local Supabase is started separately with its CLI and supplies the database and broker. The tracked model contains no self-hosted database.
|
|
4
|
+
Starter release images and external secrets must be prepared before starting the container model. Replace its starter `SUPABASE_URL` with the local Supabase endpoint reachable from the Swarm node, and seal that local database connection and integration credential; a container cannot use the host's loopback address without owner-configured networking. Local source development remains available through the app scripts.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
| Command | Operation |
|
|
7
|
+
| --- | --- |
|
|
8
|
+
| prepare | Run `npm run db:start` at the app root; obtain the local Supabase connection and Auth configuration. An owner who chooses local Swarm initializes it, labels its node `{{project}}=dev` and provisions the ingress volumes. Create the external development secrets named in the model from real sealed members. |
|
|
9
|
+
| doctor | From this directory, run `docker stack config -c infra/swarm/stack.yaml`. Confirm image, secret and local Supabase availability. |
|
|
10
|
+
| up | Run `docker stack deploy -c infra/swarm/stack.yaml {{project}}-dev` from this directory after preparation. |
|
|
11
|
+
| status | Run `docker stack services {{project}}-dev`. |
|
|
12
|
+
| logs | Run `docker service logs {{project}}-dev_api`; never print mounted credentials. |
|
|
13
|
+
| down | Run `docker stack rm {{project}}-dev`. |
|
|
14
|
+
| verification | Confirm the API `/health/live` and `/health/ready` endpoints, then run `npm run lint` from the app root. |
|
|
15
|
+
|
|
16
|
+
Keep real sealed members under `.starcistacks/dev/secrets/*.enc`, add them to the manifest after sealing and inventory their key names in the environment record. The scaffold creates no fake secret or successful proof.
|
|
@@ -32,5 +32,5 @@ RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force
|
|
|
32
32
|
COPY --from=build /app/be/dist be/dist
|
|
33
33
|
|
|
34
34
|
USER node
|
|
35
|
-
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD
|
|
35
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD ["node", "-e", "const fs=require('node:fs');const args=fs.readFileSync('/proc/1/cmdline','utf8').split(String.fromCharCode(0));process.exit(args.includes('be/dist/apps/{{app}}/src/main.js')?0:1)"]
|
|
36
36
|
CMD ["node", "be/dist/apps/{{app}}/src/main.js"]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mockEntityManager } from "@starci/jest-preset"
|
|
2
2
|
import type { EntityManager } from "typeorm"
|
|
3
3
|
import { sql } from "./database.sql"
|
|
4
4
|
import { DatabaseErrorCode } from "./errors/database.error"
|
|
@@ -8,35 +8,34 @@ import { recordWebhookDelivery } from "./webhook-inbox.client"
|
|
|
8
8
|
const INSERT = sql`SELECT id FROM record_spec_inbox($1, $2, $3::jsonb)`
|
|
9
9
|
const PAYLOAD = { id: "delivery-1" }
|
|
10
10
|
|
|
11
|
-
const record = (
|
|
12
|
-
recordWebhookDelivery({
|
|
11
|
+
const record = (manager: EntityManager, deliveryId: string | undefined) =>
|
|
12
|
+
recordWebhookDelivery({ manager, insert: INSERT, provider: "spec", deliveryId, payload: PAYLOAD })
|
|
13
13
|
|
|
14
14
|
describe("recordWebhookDelivery", () => {
|
|
15
|
-
it("records a new delivery
|
|
16
|
-
const
|
|
15
|
+
it("records a new delivery through the caller's transaction manager", async () => {
|
|
16
|
+
const manager = mockEntityManager({ query: [INSERT, [{ id: "inbox-1" }]] })
|
|
17
17
|
|
|
18
|
-
await record(
|
|
18
|
+
const receiptId = await record(manager, " delivery-1 ")
|
|
19
19
|
|
|
20
|
-
expect(
|
|
21
|
-
expect(
|
|
22
|
-
expect(
|
|
20
|
+
expect(manager.query).toHaveBeenCalledTimes(1)
|
|
21
|
+
expect(manager.query).toHaveBeenCalledWith(INSERT, ["spec", "delivery-1", JSON.stringify(PAYLOAD)])
|
|
22
|
+
expect(receiptId).toBe("inbox-1")
|
|
23
23
|
})
|
|
24
24
|
|
|
25
25
|
it("acknowledges a duplicate without any completion or business write", async () => {
|
|
26
|
-
const
|
|
26
|
+
const manager = mockEntityManager({ query: [INSERT, []] })
|
|
27
27
|
|
|
28
|
-
await record(
|
|
28
|
+
const receiptId = await record(manager, "delivery-1")
|
|
29
29
|
|
|
30
|
-
expect(
|
|
31
|
-
expect(
|
|
30
|
+
expect(manager.query).toHaveBeenCalledTimes(1)
|
|
31
|
+
expect(receiptId).toBeUndefined()
|
|
32
32
|
})
|
|
33
33
|
|
|
34
|
-
it.each([["no", undefined], ["a blank", " "]])("refuses %s delivery id before
|
|
35
|
-
const
|
|
34
|
+
it.each([["no", undefined], ["a blank", " "]])("refuses %s delivery id before writing a receipt", async (_label, deliveryId) => {
|
|
35
|
+
const manager = mockEntityManager()
|
|
36
36
|
|
|
37
|
-
await expect(record(
|
|
37
|
+
await expect(record(manager, deliveryId)).rejects.toMatchObject({ code: DatabaseErrorCode.WebhookDeliveryIdRequired })
|
|
38
38
|
|
|
39
|
-
expect(
|
|
40
|
-
expect(transaction.em.query).not.toHaveBeenCalled()
|
|
39
|
+
expect(manager.query).not.toHaveBeenCalled()
|
|
41
40
|
})
|
|
42
41
|
})
|
|
@@ -3,33 +3,34 @@ import { DatabaseError, DatabaseErrorCode } from "./errors/database.error"
|
|
|
3
3
|
import type { SqlText } from "./database.sql"
|
|
4
4
|
|
|
5
5
|
interface RecordWebhookDelivery {
|
|
6
|
-
readonly
|
|
6
|
+
readonly manager: EntityManager
|
|
7
7
|
readonly insert: SqlText
|
|
8
8
|
readonly provider: string
|
|
9
9
|
readonly deliveryId: string | undefined
|
|
10
10
|
readonly payload: object
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
type WebhookReceiptId = string | undefined
|
|
14
|
+
|
|
13
15
|
/**
|
|
14
|
-
* Records one pending delivery
|
|
16
|
+
* Records one pending delivery using the intake's transaction manager and returns its durable receipt identity.
|
|
15
17
|
* Business processing and the completion marker belong to the reactor or job that consumes the committed row.
|
|
16
18
|
*/
|
|
17
19
|
export const recordWebhookDelivery = async ({
|
|
18
|
-
|
|
20
|
+
manager,
|
|
19
21
|
insert,
|
|
20
22
|
provider,
|
|
21
23
|
deliveryId,
|
|
22
24
|
payload,
|
|
23
|
-
}: RecordWebhookDelivery): Promise<
|
|
25
|
+
}: RecordWebhookDelivery): Promise<WebhookReceiptId> => {
|
|
24
26
|
const normalizedDeliveryId = deliveryId?.trim()
|
|
25
27
|
if (!normalizedDeliveryId) {
|
|
26
28
|
throw new DatabaseError({ code: DatabaseErrorCode.WebhookDeliveryIdRequired })
|
|
27
29
|
}
|
|
28
|
-
await
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
})
|
|
30
|
+
const rows: Array<{ readonly id: string }> = await manager.query(insert, [
|
|
31
|
+
provider,
|
|
32
|
+
normalizedDeliveryId,
|
|
33
|
+
JSON.stringify(payload),
|
|
34
|
+
])
|
|
35
|
+
return rows[0]?.id
|
|
35
36
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { BaseEvent } from "@modules/platform/event-bus"
|
|
2
|
+
import type { ParsedEvent } from "@modules/platform/event-bus"
|
|
3
|
+
|
|
4
|
+
/** A durable receipt reference; processing reads the provider payload after the intake commits. */
|
|
5
|
+
export interface @@Provider@@DeliveryRecordedPayload {
|
|
6
|
+
readonly receiptId: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Version 1 of `@@provider@@.delivery-recorded`, published once by the provider inbox intake. */
|
|
10
|
+
export class @@Provider@@DeliveryRecordedEvent extends BaseEvent {
|
|
11
|
+
static readonly eventName = "@@provider@@.delivery-recorded"
|
|
12
|
+
static readonly version = 1
|
|
13
|
+
|
|
14
|
+
readonly eventName = @@Provider@@DeliveryRecordedEvent.eventName
|
|
15
|
+
|
|
16
|
+
private constructor(
|
|
17
|
+
readonly eventId: string,
|
|
18
|
+
readonly payload: @@Provider@@DeliveryRecordedPayload,
|
|
19
|
+
) {
|
|
20
|
+
super()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The receipt identity is stable across relay retries and distinct from a consumer's inbox source. */
|
|
24
|
+
static create(payload: @@Provider@@DeliveryRecordedPayload): @@Provider@@DeliveryRecordedEvent {
|
|
25
|
+
return new @@Provider@@DeliveryRecordedEvent(payload.receiptId, payload)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Reads an outbox envelope; malformed deliveries never reach a processing consumer. */
|
|
29
|
+
static parse(envelope: unknown): ParsedEvent<@@Provider@@DeliveryRecordedEvent> {
|
|
30
|
+
if (typeof envelope !== "object" || envelope === null || !("eventId" in envelope) || !("payload" in envelope)) return null
|
|
31
|
+
const { eventId, payload } = envelope
|
|
32
|
+
if (typeof eventId !== "string" || !eventId.trim() || typeof payload !== "object" || payload === null || !("receiptId" in payload)) return null
|
|
33
|
+
if (typeof payload.receiptId !== "string" || !payload.receiptId.trim()) return null
|
|
34
|
+
return new @@Provider@@DeliveryRecordedEvent(eventId, { receiptId: payload.receiptId })
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { @@Provider@@DeliveryRecordedEvent } from "./@@provider@@-delivery-recorded.event"
|
|
@@ -9,6 +9,7 @@ create table public.@@table@@ (
|
|
|
9
9
|
);
|
|
10
10
|
|
|
11
11
|
alter table public.@@table@@ enable row level security;
|
|
12
|
+
alter table public.@@table@@ force row level security;
|
|
12
13
|
|
|
13
14
|
-- The dedicated app_be PostgreSQL role is the only application path. With no anon or authenticated policy,
|
|
14
15
|
-- RLS denies those PostgREST roles by default; the service-role JWT is deliberately not granted here.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** The base of each typed event in modules/events: its stable identity, name and payload. */
|
|
2
|
+
export abstract class BaseEvent {
|
|
3
|
+
/** A receiver uses this stable identity to deduplicate redelivery. */
|
|
4
|
+
abstract readonly eventId: string
|
|
5
|
+
/** The event name listed by the service's vendored event contract. */
|
|
6
|
+
abstract readonly eventName: string
|
|
7
|
+
/** The fact that a reactor or job processes after commit. */
|
|
8
|
+
abstract readonly payload: object
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Parsing either returns the event or refuses a mismatched envelope. */
|
|
12
|
+
export type ParsedEvent<Event extends BaseEvent> = Event | null
|
|
13
|
+
|
|
14
|
+
/** The class side of a typed event, shared by publishers and later consumers. */
|
|
15
|
+
export interface EventClass<Event extends BaseEvent> {
|
|
16
|
+
/** The contract's event name. */
|
|
17
|
+
readonly eventName: string
|
|
18
|
+
/** The contract's payload version. */
|
|
19
|
+
readonly version: number
|
|
20
|
+
/** Reads the event from its durable envelope. */
|
|
21
|
+
parse(envelope: unknown): ParsedEvent<Event>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One committed delivery received by a reactor. */
|
|
25
|
+
export interface EventDelivery<Event extends BaseEvent> {
|
|
26
|
+
/** The consumer's idempotency key. */
|
|
27
|
+
readonly eventId: string
|
|
28
|
+
/** The typed event. */
|
|
29
|
+
readonly event: Event
|
|
30
|
+
/** The number of attempts, including this one. */
|
|
31
|
+
readonly attempt: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The typed event's durable envelope, read only after the intake transaction commits. */
|
|
35
|
+
export interface EventEnvelope {
|
|
36
|
+
/** The stable event id. */
|
|
37
|
+
readonly eventId: string
|
|
38
|
+
/** The contract's event name. */
|
|
39
|
+
readonly eventName: string
|
|
40
|
+
/** The data of the fact. */
|
|
41
|
+
readonly payload: object
|
|
42
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { injector } from "@modules/platform/composition"
|
|
2
|
+
import type { TypedParameterDecorator } from "@modules/platform/composition"
|
|
3
|
+
import type { EventBus } from "./event-bus.port"
|
|
4
|
+
|
|
5
|
+
/** Token of the transactional EventBus publisher. */
|
|
6
|
+
export const EVENT_BUS: unique symbol = Symbol("platform.event-bus")
|
|
7
|
+
|
|
8
|
+
/** Injects the EventBus port. Parameter type: EventBus. */
|
|
9
|
+
export const InjectEventBus = (): TypedParameterDecorator<EventBus> => injector<EventBus>(EVENT_BUS)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ConfigurableModuleBuilder } from "@nestjs/common"
|
|
2
|
+
import type { EventBusOptions } from "./event-bus.options"
|
|
3
|
+
|
|
4
|
+
export const { ConfigurableModuleClass, OPTIONS_TYPE } = new ConfigurableModuleBuilder<EventBusOptions>()
|
|
5
|
+
.setExtras({ isGlobal: false }, (definition, extras) => ({ ...definition, global: extras.isGlobal }))
|
|
6
|
+
.build()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Module } from "@nestjs/common"
|
|
2
|
+
import type { DynamicModule } from "@nestjs/common"
|
|
3
|
+
import { EVENT_BUS } from "./event-bus.decorators"
|
|
4
|
+
import { ConfigurableModuleClass, OPTIONS_TYPE } from "./event-bus.module-definition"
|
|
5
|
+
import { EventBusService } from "./event-bus.service"
|
|
6
|
+
|
|
7
|
+
@Module({})
|
|
8
|
+
/** Provides the transactional lite EventBus publisher, shared by domain intake services. */
|
|
9
|
+
export class EventBusModule extends ConfigurableModuleClass {
|
|
10
|
+
/** Registers the publishing capability once per app. */
|
|
11
|
+
static register(options: typeof OPTIONS_TYPE): DynamicModule {
|
|
12
|
+
const base = super.register(options)
|
|
13
|
+
return {
|
|
14
|
+
...base,
|
|
15
|
+
providers: [...(base.providers ?? []), { provide: EVENT_BUS, useClass: EventBusService }],
|
|
16
|
+
exports: [EVENT_BUS],
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EntityManager } from "typeorm"
|
|
2
|
+
import type { BaseEvent, EventClass, EventDelivery } from "./event-bus.contracts"
|
|
3
|
+
|
|
4
|
+
/** The publishing side of the lite event bus; the reactor or job owns delivery of committed outbox rows. */
|
|
5
|
+
export interface EventBus {
|
|
6
|
+
/** Writes the typed event in the caller's transaction; a rollback writes no durable event. */
|
|
7
|
+
publish(event: BaseEvent, tx: EntityManager): Promise<void>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A later message transport receives one typed event and forwards its delivery id. */
|
|
11
|
+
export interface EventConsumer<Event extends BaseEvent> {
|
|
12
|
+
/** The event class the consumer reads. */
|
|
13
|
+
readonly event: EventClass<Event>
|
|
14
|
+
/** Applies one delivery after commit; a throw leaves the delivery available for retry. */
|
|
15
|
+
handle(delivery: EventDelivery<Event>): Promise<void>
|
|
16
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Test } from "@nestjs/testing"
|
|
2
|
+
import { fakeTransaction, mockEntityManager } from "@starci/jest-preset"
|
|
3
|
+
import { BaseEvent } from "./event-bus.contracts"
|
|
4
|
+
import { EventBusService } from "./event-bus.service"
|
|
5
|
+
import { INSERT_OUTBOX_EVENT } from "./persistence/event-bus.sql"
|
|
6
|
+
|
|
7
|
+
class ReceiptRecordedEvent extends BaseEvent {
|
|
8
|
+
readonly eventId = "receipt-1"
|
|
9
|
+
readonly eventName = "spec.delivery-recorded"
|
|
10
|
+
readonly payload = { receiptId: "receipt-1" }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("EventBusService", () => {
|
|
14
|
+
const build = async () => {
|
|
15
|
+
const module = await Test.createTestingModule({ providers: [EventBusService] }).compile()
|
|
16
|
+
return module.get(EventBusService)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
it("writes the typed envelope through the receipt transaction", async () => {
|
|
20
|
+
const subject = await build()
|
|
21
|
+
const transaction = fakeTransaction(mockEntityManager({ query: [INSERT_OUTBOX_EVENT, []] }))
|
|
22
|
+
const event = new ReceiptRecordedEvent()
|
|
23
|
+
|
|
24
|
+
await transaction.em.transaction((manager) => subject.publish(event, manager))
|
|
25
|
+
|
|
26
|
+
expect(transaction.em.query).toHaveBeenCalledWith(INSERT_OUTBOX_EVENT, [
|
|
27
|
+
"receipt-1",
|
|
28
|
+
"spec.delivery-recorded",
|
|
29
|
+
JSON.stringify({ eventId: event.eventId, eventName: event.eventName, payload: event.payload }),
|
|
30
|
+
])
|
|
31
|
+
expect(transaction.commits).toBe(1)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("propagates an outbox failure so the receipt transaction rolls back", async () => {
|
|
35
|
+
const subject = await build()
|
|
36
|
+
const manager = mockEntityManager({ query: [INSERT_OUTBOX_EVENT, []] })
|
|
37
|
+
jest.spyOn(manager, "query").mockRejectedValue(new Error("outbox unavailable"))
|
|
38
|
+
const transaction = fakeTransaction(manager)
|
|
39
|
+
|
|
40
|
+
await expect(transaction.em.transaction((tx) => subject.publish(new ReceiptRecordedEvent(), tx))).rejects.toThrow("outbox unavailable")
|
|
41
|
+
|
|
42
|
+
expect(transaction.commits).toBe(0)
|
|
43
|
+
expect(transaction.rollbacks).toBe(1)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Injectable } from "@nestjs/common"
|
|
2
|
+
import type { EntityManager } from "typeorm"
|
|
3
|
+
import type { BaseEvent, EventEnvelope } from "./event-bus.contracts"
|
|
4
|
+
import type { EventBus } from "./event-bus.port"
|
|
5
|
+
import { INSERT_OUTBOX_EVENT } from "./persistence/event-bus.sql"
|
|
6
|
+
|
|
7
|
+
@Injectable()
|
|
8
|
+
/** Writes a receipt event into the same transaction as its inbox receipt, without running any consumer. */
|
|
9
|
+
export class EventBusService implements EventBus {
|
|
10
|
+
/** The outbox commits or rolls back with the receipt; only a later reactor or job processes it. */
|
|
11
|
+
async publish(event: BaseEvent, tx: EntityManager): Promise<void> {
|
|
12
|
+
const envelope: EventEnvelope = { eventId: event.eventId, eventName: event.eventName, payload: event.payload }
|
|
13
|
+
await tx.query(INSERT_OUTBOX_EVENT, [event.eventId, event.eventName, JSON.stringify(envelope)])
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { BaseEvent } from "./event-bus.contracts"
|
|
2
|
+
export type { EventClass, EventDelivery, EventEnvelope, ParsedEvent } from "./event-bus.contracts"
|
|
3
|
+
export { EVENT_BUS, InjectEventBus } from "./event-bus.decorators"
|
|
4
|
+
export { EventBusModule } from "./event-bus.module"
|
|
5
|
+
export type { EventBus, EventConsumer } from "./event-bus.port"
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { sql } from "@modules/platform/database"
|
|
2
|
+
|
|
3
|
+
/** Writes only the typed envelope; a reactor or job reads the committed row afterward. */
|
|
4
|
+
export const INSERT_OUTBOX_EVENT = sql`INSERT INTO public.event_bus (event_id, event_name, payload)
|
|
5
|
+
VALUES ($1, $2, $3::jsonb)`
|