@starci/hfs 4.7.31 → 4.7.33
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 +12 -0
- package/README.md +13 -6
- package/emit/contracts.mjs +4 -3
- package/emit/form-bounds-reader.ts.partial +58 -0
- package/emit/form-bounds.mjs +155 -0
- package/emit/graphql-input-bounds.mjs +97 -0
- package/emit/input-bounds.mjs +44 -0
- package/emit/operation-schemas.mjs +137 -0
- package/emit/operations-worker.mjs +2 -2
- package/emit/operations.mjs +52 -16
- package/emit/schema-worker.mjs +17 -4
- package/emit/type-schema.mjs +17 -5
- package/package.json +9 -2
- package/release/gate.mjs +18 -23
- package/release/index.mjs +1 -1
- package/release/push.mjs +4 -5
- package/runtime/engine/admission.mjs +331 -0
- package/runtime/engine/canonical-json.mjs +14 -0
- package/runtime/engine/db/blob.mjs +380 -0
- package/runtime/engine/db/event-compact.mjs +73 -0
- package/runtime/engine/db/ledger-open.mjs +38 -0
- package/runtime/engine/db/ledger-paths.mjs +87 -0
- package/runtime/engine/db/ledger.mjs +1068 -0
- package/runtime/engine/db/machine-connection.mjs +150 -0
- package/runtime/engine/db/machine-open.mjs +60 -0
- package/runtime/engine/db/machine-schema.mjs +105 -0
- package/runtime/engine/db/machine.mjs +1317 -0
- package/runtime/engine/db/provider-reservations.mjs +127 -0
- package/runtime/engine/db/provision-authority.mjs +76 -0
- package/runtime/engine/db/provision-lock.mjs +29 -0
- package/runtime/engine/db/provision-operations.mjs +146 -0
- package/runtime/engine/db/ref-value.mjs +67 -0
- package/runtime/engine/db/schema/machine.sql +920 -0
- package/runtime/engine/db/schema/runtime.sql +1113 -0
- package/runtime/engine/refuse.mjs +11 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +65 -9
- package/runtime/knowledge/hfs/design-truths.yaml +149 -0
- package/runtime/knowledge/hfs/fe-stack.yaml +23 -5
- package/runtime/knowledge/hfs/rules.yaml +269 -48
- package/runtime/knowledge/hfs/slots.yaml +113 -53
- package/runtime/knowledge/patterns/be/api.yaml +46 -20
- package/runtime/knowledge/patterns/be/event-bus.yaml +19 -12
- package/runtime/knowledge/patterns/be/idempotency.yaml +265 -0
- package/runtime/knowledge/patterns/be/jobs.yaml +17 -12
- package/runtime/knowledge/patterns/be/projections.yaml +2 -2
- package/runtime/knowledge/patterns/be/queues.yaml +57 -22
- package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +326 -27
- package/runtime/knowledge/patterns/be/saga.yaml +2 -2
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/command-policy.yaml +249 -0
- package/runtime/modules/kernel/failure-codes.yaml +211 -52
- package/runtime/modules/kernel/hook-input.yaml +6 -0
- package/runtime/modules/kernel/protected-zone.yaml +81 -0
- package/runtime/modules/kernel/removed-vocabulary.yaml +62 -0
- package/runtime/modules/kernel/roles.yaml +405 -0
- package/runtime/modules/models/registry.yaml +3 -1
- package/runtime/modules/models/runtimes.yaml +2 -0
- package/runtime/modules/schemas/application-stacks.schema.yaml +10 -2
- package/runtime/packages/cli/src/catalog.generated.mjs +46 -59
- package/runtime/packages/grammar/scripts/build-stamp.mjs +98 -0
- package/runtime/scripts/agent/codex-app-server.mjs +79 -0
- package/runtime/scripts/agent/codex-mirror-source.mjs +34 -0
- package/runtime/scripts/agent/host-agents.mjs +80 -0
- package/runtime/scripts/agent/launch-trust-policy.mjs +53 -0
- package/runtime/scripts/agent/trust-launch.mjs +191 -0
- package/runtime/scripts/agent/trust.mjs +523 -0
- package/runtime/scripts/api/fs/rename-over.mjs +21 -0
- package/runtime/scripts/api/fs/temp-path.mjs +8 -0
- package/runtime/scripts/api/fs/write-json-file.mjs +10 -0
- package/runtime/scripts/api/git/diff-names.mjs +8 -0
- package/runtime/scripts/api/git/head-time.mjs +10 -0
- package/runtime/scripts/api/git/index-lock.mjs +78 -0
- package/runtime/scripts/api/git/is-ancestor.mjs +5 -0
- package/runtime/scripts/api/git/log.mjs +5 -0
- package/runtime/scripts/api/git/merge-base-query.mjs +5 -0
- package/runtime/scripts/api/git/merge-tree.mjs +5 -0
- package/runtime/scripts/api/git/rev-list.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse.mjs +13 -0
- package/runtime/scripts/api/git/status-query.mjs +5 -0
- package/runtime/scripts/api/git/worktree-list-query.mjs +5 -0
- package/runtime/scripts/api/node/spawn-node.mjs +6 -0
- package/runtime/scripts/api/npm/dist-tag.mjs +8 -0
- package/runtime/scripts/api/npm/lib.mjs +32 -0
- package/runtime/scripts/api/npm/pack-dry-run.mjs +9 -0
- package/runtime/scripts/api/npm/run-npm.mjs +6 -0
- package/runtime/scripts/api/npm/view.mjs +30 -0
- package/runtime/scripts/api/npm/whoami.mjs +8 -0
- package/runtime/scripts/api/process/hide-child-windows.mjs +31 -0
- package/runtime/scripts/api/process/kill-tree.mjs +22 -0
- package/runtime/scripts/api/process/lib.mjs +125 -0
- package/runtime/scripts/api/process/probe-guard-command.mjs +74 -0
- package/runtime/scripts/api/process/process-list.mjs +28 -0
- package/runtime/scripts/api/process/system-tool.mjs +37 -0
- package/runtime/scripts/gates/canon-digest.mjs +81 -0
- package/runtime/scripts/gates/canon-pins.mjs +16 -0
- package/runtime/scripts/gates/commit-proof.mjs +53 -0
- package/runtime/scripts/gates/gate-input.mjs +98 -0
- package/runtime/scripts/gates/gate.mjs +669 -0
- package/runtime/scripts/gates/grammar-dist.mjs +206 -0
- package/runtime/scripts/gates/runtime-package-selection.mjs +27 -0
- package/runtime/scripts/gates/test-world-run.mjs +254 -0
- package/runtime/scripts/gates/type-impact.mjs +277 -0
- package/runtime/scripts/gates/type-program.mjs +113 -0
- package/runtime/scripts/guards/assigned-command.mjs +12 -0
- package/runtime/scripts/guards/call-rights.mjs +47 -0
- package/runtime/scripts/guards/command-guard.mjs +468 -0
- package/runtime/scripts/guards/command-policy.mjs +342 -0
- package/runtime/scripts/guards/critic-reach.mjs +82 -0
- package/runtime/scripts/guards/deps-guard.mjs +150 -0
- package/runtime/scripts/guards/env-dump-verdict.mjs +89 -0
- package/runtime/scripts/guards/git-pathspec.mjs +225 -0
- package/runtime/scripts/guards/git-policy.mjs +355 -0
- package/runtime/scripts/guards/guards-root.mjs +27 -0
- package/runtime/scripts/guards/hook-install.mjs +343 -0
- package/runtime/scripts/guards/hook-io.mjs +76 -0
- package/runtime/scripts/guards/hook-verdict.mjs +12 -0
- package/runtime/scripts/guards/install-verdict.mjs +41 -0
- package/runtime/scripts/guards/kernel-seat.mjs +63 -0
- package/runtime/scripts/guards/launch-verdict.mjs +74 -0
- package/runtime/scripts/guards/op-starci.mjs +16 -0
- package/runtime/scripts/guards/op-write-scope.mjs +27 -0
- package/runtime/scripts/guards/orca-self-lifecycle.mjs +39 -0
- package/runtime/scripts/guards/process-kill-verdict.mjs +106 -0
- package/runtime/scripts/guards/protected-zone.mjs +56 -0
- package/runtime/scripts/guards/refusals.mjs +27 -0
- package/runtime/scripts/guards/release-definition.mjs +185 -0
- package/runtime/scripts/guards/release-push-gate.mjs +72 -0
- package/runtime/scripts/guards/release-record.mjs +58 -0
- package/runtime/scripts/guards/release-suite-mode.mjs +15 -0
- package/runtime/scripts/guards/rights.mjs +268 -0
- package/runtime/scripts/guards/seat-tools.mjs +47 -0
- package/runtime/scripts/guards/shell-commands.mjs +328 -0
- package/runtime/scripts/guards/supervisor-seat.mjs +80 -0
- package/runtime/scripts/guards/verdict-process.mjs +81 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +10 -10
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +1 -1
- package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +53 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +1 -1
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +17 -6
- package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +105 -0
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +38 -52
- package/runtime/scripts/hfs/architecture/supabase-keys.mjs +261 -0
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +49 -3
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -1
- package/runtime/scripts/hfs/check.mjs +6 -4
- package/runtime/scripts/hfs/coverage-scope.mjs +28 -1
- package/runtime/scripts/hfs/db-restamp-verb.mjs +54 -14
- package/runtime/scripts/hfs/declaration-shape.mjs +4 -4
- package/runtime/scripts/hfs/declaration-slots.mjs +14 -3
- package/runtime/scripts/hfs/manifest-shape.mjs +10 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +18 -6
- package/runtime/scripts/hfs/rules/a15-findings.mjs +4 -1
- package/runtime/scripts/hfs/rules/bypass-markers.mjs +22 -7
- package/runtime/scripts/hfs/rules/database-migrations.mjs +69 -14
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +9 -7
- package/runtime/scripts/hfs/rules/database-queues.mjs +68 -18
- package/runtime/scripts/hfs/rules/edition.mjs +56 -12
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -10
- package/runtime/scripts/hfs/rules/fe-stack.mjs +12 -7
- package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +75 -9
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +15 -4
- package/runtime/scripts/hfs/rules/text-integrity.mjs +73 -0
- package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +74 -0
- package/runtime/scripts/hfs/runtime-rules/release-notes.mjs +44 -0
- package/runtime/scripts/hfs/sealed-members.mjs +3 -3
- package/runtime/scripts/hfs/slot-side-problems.mjs +3 -3
- package/runtime/scripts/hfs/slots.mjs +2 -1
- package/runtime/scripts/hfs/stack-deploy-config.mjs +22 -5
- package/runtime/scripts/hfs/stacks-gate.mjs +28 -7
- package/runtime/scripts/hfs/view.mjs +2 -1
- package/runtime/scripts/lib/app-data.mjs +11 -0
- package/runtime/scripts/lib/bypass-markers.mjs +272 -75
- package/runtime/scripts/lib/check-format.mjs +7 -0
- package/runtime/scripts/lib/cli-arg.mjs +120 -0
- package/runtime/scripts/lib/css-rules.mjs +220 -0
- package/runtime/scripts/lib/dom-tree.mjs +4 -0
- package/runtime/scripts/lib/escape.mjs +16 -0
- package/runtime/scripts/lib/event-items.mjs +22 -0
- package/runtime/scripts/lib/failure-code-template-errors.mjs +161 -0
- package/runtime/scripts/lib/git-dir.mjs +120 -0
- package/runtime/scripts/lib/grammar-vendor-parity.mjs +377 -0
- package/runtime/scripts/lib/guard-command.mjs +24 -0
- package/runtime/scripts/lib/hook-limits.mjs +5 -0
- package/runtime/scripts/lib/interrupt-cleanup.mjs +38 -0
- package/runtime/scripts/lib/js-lex.mjs +200 -0
- package/runtime/scripts/lib/json-formats.mjs +0 -1
- package/runtime/scripts/lib/language.mjs +94 -1
- package/runtime/scripts/lib/loosening-budgets.mjs +44 -0
- package/runtime/scripts/lib/loosening-config.mjs +361 -0
- package/runtime/scripts/lib/loosening-state.mjs +223 -0
- package/runtime/scripts/lib/menu-parts.mjs +11 -0
- package/runtime/scripts/lib/normalize.mjs +25 -0
- package/runtime/scripts/lib/op-gate.mjs +15 -0
- package/runtime/scripts/lib/redact.mjs +208 -0
- package/runtime/scripts/lib/runtime-location.mjs +21 -0
- package/runtime/scripts/lib/runtime-package-inventory.mjs +21 -0
- package/runtime/scripts/lib/runtime-package-manifests.mjs +84 -0
- package/runtime/scripts/lib/runtime-publication-tags.mjs +99 -0
- package/runtime/scripts/lib/seat-env.mjs +43 -0
- package/runtime/scripts/lib/secret-patterns.mjs +58 -9
- package/runtime/scripts/lib/shell-source.mjs +4 -0
- package/runtime/scripts/lib/source-phrases.mjs +34 -0
- package/runtime/scripts/lib/sqlite.mjs +21 -0
- package/runtime/scripts/lib/swarm-stack.mjs +2 -1
- package/runtime/scripts/lib/tar-files.mjs +39 -0
- package/runtime/scripts/lib/ts-analysis.mjs +10 -0
- package/runtime/scripts/lib/worktree-exclude.mjs +35 -0
- package/runtime/scripts/lib/worktree-record.mjs +14 -0
- package/runtime/scripts/machine/artifact-hold.mjs +52 -0
- package/runtime/scripts/machine/ask-receipts.mjs +113 -0
- package/runtime/scripts/machine/evidence-store.mjs +223 -0
- package/runtime/scripts/machine/home.mjs +216 -0
- package/runtime/scripts/machine/host-lock.mjs +184 -0
- package/runtime/scripts/machine/lock-recovery.mjs +55 -0
- package/runtime/scripts/machine/roles-contract.mjs +90 -0
- package/runtime/scripts/machine/runtime-change.mjs +20 -0
- package/runtime/scripts/machine/workflow-tree.mjs +122 -0
- package/runtime/scripts/reconciler/checklist-items.mjs +7 -0
- package/runtime/scripts/reconciler/guard-row.mjs +25 -0
- package/runtime/scripts/work/asset-slot.mjs +284 -0
- package/runtime/scripts/work/brand/brand-colour.mjs +125 -0
- package/runtime/scripts/work/brand/brand-tokens.mjs +167 -0
- package/runtime/scripts/work/brand/brand-value-source.mjs +106 -0
- package/runtime/scripts/work/brand/brand.mjs +892 -0
- package/runtime/scripts/work/draw/draw-dna-anatomy.mjs +60 -0
- package/runtime/scripts/work/draw/draw-dna-findings.mjs +256 -0
- package/runtime/scripts/work/draw/draw-dna-selector.mjs +38 -0
- package/runtime/scripts/work/draw/draw-dna.mjs +483 -0
- package/runtime/scripts/work/draw/draw-layer.mjs +305 -0
- package/runtime/scripts/work/draw/draw-rationale-page.mjs +237 -0
- package/runtime/scripts/work/draw/draw-rationale-ref.mjs +14 -0
- package/runtime/scripts/work/draw/draw-rationale.mjs +353 -0
- package/runtime/scripts/work/draw/draw-source.mjs +412 -0
- package/runtime/scripts/work/draw/draw-taste.mjs +278 -0
- package/runtime/scripts/work/draw/draw-typecheck.mjs +72 -0
- package/runtime/scripts/work/draw/rationale-findings.mjs +205 -0
- package/runtime/scripts/work/draw-grammar.mjs +175 -0
- package/runtime/scripts/work/draw-render-page.mjs +188 -0
- package/runtime/scripts/work/draw-render.mjs +626 -0
- package/runtime/scripts/work/grammar-proposal.mjs +279 -0
- package/runtime/scripts/work/png.mjs +237 -0
- package/runtime/scripts/work/render-tools.mjs +64 -0
- package/runtime/scripts/work/work-io.mjs +119 -0
- package/scaffold/add-inbox-lite.mjs +2 -2
- package/scaffold/add-plan.mjs +40 -13
- package/scaffold/add-queue-lite.mjs +372 -0
- package/scaffold/add.mjs +5 -2
- package/scaffold/app.mjs +1 -2
- package/scaffold/edition-gate.mjs +11 -7
- package/scaffold/lite-exports.mjs +10 -0
- package/scaffold/pattern-file-slots.mjs +33 -0
- package/scaffold/test-world.mjs +1 -1
- package/scaffold/yaml-block-edit.mjs +117 -0
- package/src/main.mjs +5 -4
- package/stack/deploy.mjs +7 -3
- package/stack/post-deploy.mjs +48 -0
- package/sync/index.mjs +10 -0
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +1 -1
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +1 -1
- package/templates/app/codegen-graphql.mjs.partial +59 -12
- package/templates/app/hooks/husky/pre-commit +2 -2
- package/templates/app/hooks/husky/pre-push +10 -3
- package/templates/app/package-scripts/redis-dev.json +3 -0
- package/templates/app/package-scripts-lite/package.json +1 -0
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +36 -8
- package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
- package/templates/app/skeleton/.starcistacks/production/README.md +10 -9
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/postgres-init.sh +21 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +1 -1
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +1 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +47 -5
- package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +5 -3
- package/templates/app/skeleton/README.md +2 -0
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +5 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +1 -1
- package/templates/app/skeleton-lite/README.md +1 -1
- package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +18 -0
- package/templates/be/inbox/platform/event-bus/errors/event-bus.error.ts.tpl +16 -0
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +10 -0
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +1 -1
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +15 -3
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +11 -3
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/event-bus/messages/event-bus.messages.ts.tpl +7 -0
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +2 -2
- package/templates/be/inbox/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
- package/templates/be/inbox/platform/idempotency/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
- package/templates/be/inbox/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
- package/templates/be/inbox/platform/inbox/client-operation.policy.ts.tpl +17 -0
- package/templates/be/inbox/platform/inbox/errors/inbox.error.ts.tpl +22 -0
- package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +21 -0
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +334 -11
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +55 -4
- package/templates/be/inbox/platform/inbox/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +26 -0
- package/templates/be/inbox/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
- package/templates/be/inbox/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
- package/templates/be/inbox/platform/migration.sql.tpl +1 -0
- package/templates/be/inbox/service.spec.ts.tpl +4 -4
- package/templates/be/inbox/service.ts.tpl +3 -1
- package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -2
- package/templates/be/inbox/webhook-door.ts.tpl +3 -2
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +3 -0
- package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +10 -0
- package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +16 -3
- package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +11 -3
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +139 -50
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +25 -2
- package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +139 -78
- package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +10 -6
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +262 -159
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +32 -5
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +5 -1
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -4
- package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +1 -0
- package/templates/be/patterns/idempotency/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
- package/templates/be/patterns/idempotency/platform/idempotency/index.ts.tpl +2 -0
- package/templates/be/patterns/idempotency/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.ts.tpl +17 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.spec.ts.tpl +66 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.ts.tpl +24 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.spec.ts.tpl +60 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.ts.tpl +12 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.spec.ts.tpl +49 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.ts.tpl +12 -0
- package/templates/be/patterns/idempotency/platform/inbox/effectful.decorators.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/errors/inbox.error.ts.tpl +22 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.decorators.ts.tpl +17 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module.ts.tpl +28 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.options.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.port.ts.tpl +44 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.service.spec.ts.tpl +388 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.service.ts.tpl +77 -0
- package/templates/be/patterns/idempotency/platform/inbox/index.ts.tpl +10 -0
- package/templates/be/patterns/idempotency/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +9 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/entities/inbox-claim.entity.ts.tpl +25 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +39 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts.tpl +21 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts.tpl +21 -0
- package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
- package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +1 -0
- package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +4 -3
- package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +20 -2
- package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +1 -0
- package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +4 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.index.ts.tpl +10 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +44 -0
- package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +33 -0
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +203 -18
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +78 -22
- package/templates/be/patterns/queues/platform/errors/queue.error.ts.tpl +19 -0
- package/templates/be/patterns/queues/platform/messages/queue.messages.ts.tpl +13 -0
- package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +1 -0
- package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +4 -0
- package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +2 -0
- package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -4
- package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +14 -2
- package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +5 -2
- package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +72 -74
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +23 -8
- package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +29 -8
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +7 -3
- package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +5 -0
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +2 -0
- package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +4 -0
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +2 -1
- package/templates/be/patterns/queues/platform/queue.port.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.spec.ts.tpl +417 -0
- package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.ts.tpl +172 -0
- package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +12 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +32 -0
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +148 -0
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +51 -0
- package/templates/be/patterns/queues/platform-lite/queue-transport.port.ts.tpl +163 -0
- package/templates/be/patterns/queues/platform-lite/queue.config.ts.tpl +17 -0
- package/templates/be/patterns/queues/platform-lite/queue.error.ts.tpl +25 -0
- package/templates/be/patterns/queues/platform-lite/queue.index.ts.tpl +8 -0
- package/templates/be/patterns/queues/platform-lite/queue.messages.ts.tpl +21 -0
- package/templates/be/patterns/queues/platform-lite/queue.module.ts.tpl +74 -0
- package/templates/be/patterns/queues/platform-lite/queue.options.ts.tpl +38 -0
- package/templates/be/patterns/queues/queue.queue.ts.tpl +2 -2
- package/templates/be/patterns/reactors/application.contracts.ts.tpl +2 -0
- package/templates/be/patterns/reactors/consumer.ts.tpl +5 -1
- package/templates/be/platform/graphql/graphql.contracts.ts +2 -1
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +1 -1
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +6 -1
- package/templates/be/skeleton/apps/__app__/src/main.ts +7 -4
- package/templates/be/skeleton/apps/cli/src/compose/platform.ts +4 -0
- package/templates/be/skeleton/apps/cli/src/main.ts +7 -4
- package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http/errors/http.error.ts +22 -0
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.spec.ts +235 -0
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +85 -0
- package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.spec.ts +92 -0
- package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.ts +52 -0
- package/templates/be/skeleton/src/modules/platform/http/http.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http/http.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/http/http.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/http/http.port.ts +40 -0
- package/templates/be/skeleton/src/modules/platform/http/index.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http/messages/http.messages.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.port.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/ids/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.spec.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.ts +10 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.spec.ts +59 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.ts +21 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.contracts.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.spec.ts +150 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.ts +42 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.port.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.service.spec.ts +23 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.service.ts +28 -0
- package/templates/be/skeleton/src/modules/platform/logging/index.ts +14 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +39 -6
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +15 -3
- package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +17 -2
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +13 -1
- package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +13 -3
- package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.spec.ts +69 -0
- package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.ts +48 -0
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/record.policy.spec.ts +12 -0
- package/templates/be/skeleton/src/modules/platform/primitives/record.policy.ts +3 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +6 -1
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +14 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/be/test-world/world/test-apps.options.ts.tpl +2 -0
- package/templates/be/test-world/world/test-world.config.ts.tpl +1 -0
- package/upgrade/check.mjs +12 -135
- package/upgrade/file-state.mjs +1 -16
- package/upgrade/index.mjs +34 -361
- package/runtime/scripts/lib/check-code-loosening.mjs +0 -128
- package/runtime/scripts/lib/gate-loosening.mjs +0 -292
- package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +0 -89
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +0 -16
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +0 -13
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +0 -3
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +0 -12
- package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +0 -7
- package/templates/be/skeleton-lite/apps/api/src/main.ts +0 -26
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +0 -12
- package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +0 -7
- package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +0 -5
- package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +0 -9
- package/templates/be/upgrade-full/apps/cli/src/main.ts +0 -29
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +0 -7
- package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +0 -23
|
@@ -5,10 +5,6 @@ schema: starci/application-stacks@1
|
|
|
5
5
|
# A GitHub package.json repository URL also enables the managed block.
|
|
6
6
|
sources:
|
|
7
7
|
- repository: "{{project}}"
|
|
8
|
-
backing:
|
|
9
|
-
primary:
|
|
10
|
-
provider: external-postgres
|
|
11
|
-
managed: true
|
|
12
8
|
components:
|
|
13
9
|
core:
|
|
14
10
|
role: service
|
|
@@ -27,8 +23,10 @@ components:
|
|
|
27
23
|
purpose: The single public entry point; TLS state stays on its declared persistent volume.
|
|
28
24
|
postgres:
|
|
29
25
|
role: stateful
|
|
26
|
+
stateful: true
|
|
27
|
+
volumes: [postgres-data]
|
|
30
28
|
image: postgres:16@sha256:1a6ab3f5345eb6dbe04a1349529caabdb0ab09293a09590fad07b2246bfa4b54
|
|
31
|
-
purpose:
|
|
29
|
+
purpose: PostgreSQL in Docker Linux containers for development and Docker Swarm for production; production credentials use mounted secrets and data stays on its named volume.
|
|
32
30
|
environments:
|
|
33
31
|
dev:
|
|
34
32
|
status: supported
|
|
@@ -51,6 +49,7 @@ environments:
|
|
|
51
49
|
app: { port: 3000 }
|
|
52
50
|
landing: { port: 3002 }
|
|
53
51
|
ingress: { httpPort: 80, httpsPort: 443 }
|
|
52
|
+
postgres: { replicas: 1, port: 5432 }
|
|
54
53
|
runbook: README.md
|
|
55
54
|
ingress:
|
|
56
55
|
component: ingress
|
|
@@ -79,8 +78,8 @@ environments:
|
|
|
79
78
|
retention: Thirty daily snapshots
|
|
80
79
|
owner: Production operator
|
|
81
80
|
restoreProof: .starcistacks/production/infra/swarm/recovery/restore-proof.md
|
|
82
|
-
- component:
|
|
83
|
-
target:
|
|
81
|
+
- component: postgres
|
|
82
|
+
target: Owner-provisioned encrypted off-host database exports and postgres-data volume snapshots
|
|
84
83
|
retention: Thirty daily exports
|
|
85
84
|
owner: Production operator
|
|
86
85
|
restoreProof: .starcistacks/production/infra/swarm/recovery/restore-proof.md
|
|
@@ -92,8 +91,19 @@ environments:
|
|
|
92
91
|
where: secrets/primary-db-url
|
|
93
92
|
recipientPolicy: This app's own age recipient configured in .sops.yaml
|
|
94
93
|
keyCustody: SOPS_AGE_KEY in the host's untracked .claude/secret.env
|
|
94
|
+
- name: postgres-password
|
|
95
|
+
runtimeName: "{{project}}-production-postgres-password-v1"
|
|
96
|
+
version: 1
|
|
97
|
+
where: secrets/postgres-password
|
|
98
|
+
recipientPolicy: This app's own age recipient configured in .sops.yaml
|
|
99
|
+
keyCustody: SOPS_AGE_KEY in the host's untracked .claude/secret.env
|
|
100
|
+
- name: primary-db-password
|
|
101
|
+
runtimeName: "{{project}}-production-primary-db-password-v1"
|
|
102
|
+
version: 1
|
|
103
|
+
where: secrets/primary-db-password
|
|
104
|
+
recipientPolicy: This app's own age recipient configured in .sops.yaml
|
|
105
|
+
keyCustody: SOPS_AGE_KEY in the host's untracked .claude/secret.env
|
|
95
106
|
# What `starci app stack` deploys (verbs: prepare, doctor, deploy, verify, rollback); names and shapes only, never a value.
|
|
96
|
-
# Add secrets.grants once the real sealed members exist under .starcistacks/production/secrets, and a backup block for stateful volumes.
|
|
97
107
|
deploy:
|
|
98
108
|
stack: {{project}}-production
|
|
99
109
|
# The image registry. The canon scaffold writes `public` (the owner's default: images cost nothing there); the value belongs to the app from now on
|
|
@@ -118,6 +128,24 @@ environments:
|
|
|
118
128
|
volumes:
|
|
119
129
|
- {{project}}-production-ingress-data
|
|
120
130
|
- {{project}}-production-ingress-config-data
|
|
131
|
+
- {{project}}-production-postgres-data
|
|
132
|
+
secrets:
|
|
133
|
+
grants:
|
|
134
|
+
- service: core
|
|
135
|
+
filePointers: true
|
|
136
|
+
from: [{member: primary-db-url}]
|
|
137
|
+
- service: postgres
|
|
138
|
+
filePointers: true
|
|
139
|
+
from: [{member: postgres-password}, {member: primary-db-password}]
|
|
140
|
+
configs:
|
|
141
|
+
- name: postgres-init
|
|
142
|
+
file: infra/swarm/postgres-init.sh
|
|
143
|
+
env: POSTGRES_INIT_CONFIG
|
|
144
|
+
# Release tasks: an idempotent write the release owes the data (a catalogue sync, a seed upsert) is declared under tasks and listed in postDeploy.
|
|
145
|
+
# A deploy runs postDeploy after the stack converged and before verify, which stays read-only. Uncomment and name the real command:
|
|
146
|
+
# tasks:
|
|
147
|
+
# sync-catalogue: {service: core, command: [node, dist/cli.js, catalogue, sync]}
|
|
148
|
+
# postDeploy: [sync-catalogue]
|
|
121
149
|
verify:
|
|
122
150
|
http:
|
|
123
151
|
- name: core liveness
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Development stack
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Every development dependency runs in Docker Linux containers. This starter's Docker Compose model contains the core API, the product and landing front ends, and a pinned local PostgreSQL database. Compose is an implementation detail; declared dependencies may also use managed `<dep>:create`, `<dep>:start` and `<dep>:stop` scripts, and local Supabase, when declared, runs through the Supabase CLI. A declared native profile may run app processes locally while dependencies stay in Docker. Trust authentication is confined to this local, unpublished database.
|
|
4
4
|
Starter release images and external secrets must be prepared before starting the container model. Local source development remains available through the app scripts.
|
|
5
5
|
|
|
6
6
|
| Command | Operation |
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# Production stack
|
|
2
2
|
|
|
3
|
-
The tracked model declares
|
|
4
|
-
The production database is external PostgreSQL; the owner supplies its least-privilege connection through sealed custody.
|
|
3
|
+
The app and its dependencies run on Docker Swarm; Supabase, when declared, is a hosted service. The tracked model declares the application services, one digest-pinned Caddy ingress and one digest-pinned PostgreSQL service on an internal backing network, with its data on a named persistent volume and credentials supplied through mounted secrets. On the first boot of an empty data volume, PostgreSQL runs the mounted `postgres-init.sh` config to create the `app_be` role with access to this app's database and schema, without superuser, database-creation, role-creation, replication or RLS-bypass privileges. Existing data volumes skip initialization and retain their roles and credentials.
|
|
5
4
|
The model is a deployment declaration. It contains no owner credentials, real DNS receipt, completed release or restore proof. The public records under `.starcistacks/production/infra/swarm/` mark those owner-run operations as pending. Its all-zero application image digests are illustrative and must be replaced before deployment.
|
|
6
5
|
|
|
7
|
-
The runtime template tracks no encrypted members. Scaffolding generates the declared database
|
|
6
|
+
The runtime template tracks no encrypted members. Scaffolding generates the declared database connection, application password and PostgreSQL administrator password members with placeholder values encrypted to this app's own recipient in `.sops.yaml`. Replace them with `starci app secret set production` using the matching host age identity in untracked local custody. The sealed connection uses role `app_be`, database `{{project}}` and internal endpoint `postgres:5432`; its URI-encoded password matches the separate `primary-db-password` member. The administrator password is distinct. The API receives only its least-privilege connection; only PostgreSQL receives the two password files for initialization. The non-empty application password contains no line breaks. The service has no TTY: initialization creates the role without a password, then feeds the two `psql` `\password` prompts through stdin using Bash's builtin `printf`, so psql encrypts it before sending SQL. No password travels in an initialization command argument or a plaintext SQL statement.
|
|
7
|
+
|
|
8
|
+
Bootstrap references reviewed 2026-10-25: the [official PostgreSQL image initialization and Docker secret contract](https://github.com/docker-library/docs/blob/master/postgres/README.md#initialization-scripts), [PostgreSQL 16 psql password handling and SQL interpolation](https://www.postgresql.org/docs/16/app-psql.html), and its [password command](https://github.com/postgres/postgres/blob/REL_16_STABLE/src/bin/psql/command.c) and [prompt stdin fallback](https://github.com/postgres/postgres/blob/REL_16_STABLE/src/common/sprompt.c). First-release verification must confirm fresh-volume initialization, `app_be` privileges, health-gated startup and unchanged credentials after data-volume reuse; this source template records no live verification result.
|
|
8
9
|
|
|
9
10
|
| Command | Operation |
|
|
10
11
|
| --- | --- |
|
|
11
|
-
| prepare | Owner provisions the Swarm, node label
|
|
12
|
+
| prepare | Owner provisions the Swarm, node label and DNS, seals the matching application connection/password and distinct administrator password, then runs `starci app stack prepare --env production --execute` to create the missing external volumes (the one-time swarm initialization stays an owner-approved step: `--init-swarm` with `--execute`, after the owner's explicit approval). The single up path below starts PostgreSQL and initializes `app_be` automatically on a new empty data volume. Release images are built and pushed by the managed stack-deploy workflow. |
|
|
12
13
|
| doctor | Run `starci app stack doctor --env production` (read-only): docker, swarm, volumes, services, secrets, configs and the release pointers on the intended manager. |
|
|
13
14
|
| up | After preparation and review, run the manual stack-deploy workflow (Actions, deploy stack) on a release tag; it builds the images and runs `starci app stack deploy --env production --image <service>=<repository>@sha256:<digest> --source-sha <sha> --execute`. A dry run is `starci app stack render` or `deploy` without `--execute`. |
|
|
14
15
|
| status | Run `starci app stack doctor --env production`, or `starci app stack converge --env production --since <ISO time>` to inspect desired and running replicas plus update state. |
|
|
@@ -22,16 +23,16 @@ Every item is pending. Swarm initialization, provider URIs, DNS changes and key
|
|
|
22
23
|
|
|
23
24
|
| Area | Required owner action and evidence |
|
|
24
25
|
| --- | --- |
|
|
25
|
-
| Swarm and placement | Initialize the intended manager; label the intended stateful node `{{project}}=production`. Only that node owns the external ingress volumes. |
|
|
26
|
+
| Swarm and placement | Initialize the intended manager; label the intended stateful node `{{project}}=production`. Only that node owns the external ingress and PostgreSQL data volumes. |
|
|
26
27
|
| DNS | Replace the reserved `example.invalid` hosts consistently in the manifest, Caddyfile and public configuration; publish address records for the product, API and landing hosts and retain a provider receipt in `.starcistacks/production/infra/swarm/owner-actions.md`. |
|
|
27
28
|
| TLS | Caddy issues certificates after real DNS resolves. Provision `{{project}}-production-ingress-data` and `{{project}}-production-ingress-config-data` on the selected node; retain an issuance and renewal receipt in `.starcistacks/production/infra/swarm/owner-actions.md`. |
|
|
28
|
-
| Database |
|
|
29
|
-
| Secret custody | Replace
|
|
30
|
-
| External secrets | Materialize the sealed members in owner custody and create `{{project}}-production-primary-db-url-v1`. The API
|
|
29
|
+
| Database | Prepare creates `{{project}}-production-postgres-data` on the declared stateful node; the normal up path starts PostgreSQL and initializes `app_be` on its first empty-volume boot. Seal the matching role, database and password described above before up. Existing databases retain their current roles and credentials; initialization never rewrites them. Record the recovery owner without copying credentials here. |
|
|
30
|
+
| Secret custody | Replace generated placeholders in `.starcistacks/production/secrets/primary-db-url.enc`, `primary-db-password.enc` and `postgres-password.enc` with the matching application connection/password and distinct administrator password; record their key names in the environment roster. The age identity stays only in the host's untracked `.claude/secret.env`. |
|
|
31
|
+
| External secrets | Materialize the sealed members in owner custody and create `{{project}}-production-primary-db-url-v1`, `{{project}}-production-primary-db-password-v1` and `{{project}}-production-postgres-password-v1`. The API and PostgreSQL read their separately granted mounted files. A later credential rotation updates the actual database role under owner authority and rotates matching immutable secrets; changing a mounted file does not rerun first-boot initialization. |
|
|
31
32
|
| Public build configuration | Build the front end with its declared public site and GraphQL URLs. Public browser values stay outside sealed secret custody. |
|
|
32
33
|
| Immutable release | Replace the illustrative all-zero application digests in the manifest, model and `.starcistacks/production/infra/swarm/releases/images.lock.yaml` with the verified release digests. Store each rendered release under `/srv/{{project}}/releases/<digest>`. Retain the last accepted record in `.starcistacks/production/infra/swarm/releases/previous.yaml`, then move the pointer and update `.starcistacks/production/infra/swarm/releases/current.yaml` after verification. |
|
|
33
34
|
| Rollback | Retain the previous model and images, redeploy that model if convergence or health fails, and record the resulting convergence receipt. Database changes follow an expand/contract sequence. |
|
|
34
|
-
| Backup | Provision the manifest's thirty daily encrypted off-host database exports and ingress-volume snapshots. Record the actual destination and encryption custody in `.starcistacks/production/infra/swarm/recovery/restore-proof.md`; the production operator owns both components. |
|
|
35
|
+
| Backup | Provision the manifest's thirty daily encrypted off-host database exports and PostgreSQL and ingress-volume snapshots. Record the actual destination and encryption custody in `.starcistacks/production/infra/swarm/recovery/restore-proof.md`; the production operator owns both components. |
|
|
35
36
|
| Restore proof | Perform a restore into isolated infrastructure, verify the restored database and ingress state, and attach a dated receipt with artifact digests to `.starcistacks/production/infra/swarm/recovery/restore-proof.md`. Its initial record is pending; no restore has been run by scaffolding. |
|
|
36
37
|
|
|
37
38
|
No host scheduler is installed. Scheduled application work belongs to the back-end scheduler or an explicitly declared replicated job.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PostgreSQL runs this config only when its data volume is empty.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
PRIMARY_DB_PASSWORD="$(cat "${PRIMARY_DB_PASSWORD_FILE:?}")"
|
|
6
|
+
if [[ -z "$PRIMARY_DB_PASSWORD" || "$PRIMARY_DB_PASSWORD" == *$'\n'* || "$PRIMARY_DB_PASSWORD" == *$'\r'* ]]; then
|
|
7
|
+
builtin printf '%s\n' 'The application password must be non-empty and contain no line breaks.' >&2
|
|
8
|
+
exit 1
|
|
9
|
+
fi
|
|
10
|
+
psql --no-psqlrc --no-password --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" --set ON_ERROR_STOP=1 <<'SQL'
|
|
11
|
+
\getenv app_database POSTGRES_DB
|
|
12
|
+
CREATE ROLE app_be WITH LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
|
|
13
|
+
REVOKE ALL ON DATABASE :"app_database" FROM PUBLIC;
|
|
14
|
+
GRANT CONNECT ON DATABASE :"app_database" TO app_be;
|
|
15
|
+
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
|
|
16
|
+
GRANT USAGE, CREATE ON SCHEMA public TO app_be;
|
|
17
|
+
SQL
|
|
18
|
+
# The service has no TTY; psql reads both hidden prompts from this pipe and encrypts the password before sending SQL.
|
|
19
|
+
builtin printf '%s\n%s\n' "$PRIMARY_DB_PASSWORD" "$PRIMARY_DB_PASSWORD" |
|
|
20
|
+
psql --no-psqlrc --no-password --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" --set ON_ERROR_STOP=1 --command '\password app_be'
|
|
21
|
+
unset PRIMARY_DB_PASSWORD
|
package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Status: pending owner execution. No backup or restore has been run by scaffolding.
|
|
4
4
|
|
|
5
|
-
The production operator owns recovery of the external
|
|
5
|
+
The production operator owns recovery of the PostgreSQL service's external named volume `{{project}}-production-postgres-data` and both external ingress volumes, `{{project}}-production-ingress-data` and `{{project}}-production-ingress-config-data`. Provision thirty daily encrypted off-host database exports and PostgreSQL volume snapshots. Retain thirty daily encrypted off-host snapshots for both ingress volumes.
|
|
6
6
|
|
|
7
7
|
Before relying on the backup policy, restore a database backup and both volume snapshots into isolated infrastructure. Record the actual off-host destination, retention, encryption custody, backup artifact digests, restoration time and operator. Verify database connectivity and the API readiness endpoint, then verify the restored TLS and ingress configuration. Attach the dated result here without credentials.
|
package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml
CHANGED
|
@@ -7,3 +7,4 @@ images:
|
|
|
7
7
|
app: "{{project}}/app:v0.0.0@sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
|
8
8
|
landing: "{{project}}/landing:v0.0.0@sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
|
9
9
|
ingress: caddy:2.10.2@sha256:c3d7ee5d2b11f9dc54f947f68a734c84e9c9666c92c88a7f30b9cba5da182adb
|
|
10
|
+
postgres: postgres:16@sha256:1a6ab3f5345eb6dbe04a1349529caabdb0ab09293a09590fad07b2246bfa4b54
|
|
@@ -7,11 +7,7 @@ services:
|
|
|
7
7
|
NODE_ENV: production
|
|
8
8
|
PORT: "3001"
|
|
9
9
|
HTTP_SECURITY_ALLOWED_ORIGINS: https://{{project}}.example.invalid
|
|
10
|
-
|
|
11
|
-
secrets:
|
|
12
|
-
- source: primary-db-url
|
|
13
|
-
target: primary-db-url
|
|
14
|
-
networks: [app]
|
|
10
|
+
networks: [app, backing]
|
|
15
11
|
healthcheck:
|
|
16
12
|
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))"]
|
|
17
13
|
interval: 30s
|
|
@@ -91,6 +87,37 @@ services:
|
|
|
91
87
|
restart_policy:
|
|
92
88
|
condition: on-failure
|
|
93
89
|
delay: 5s
|
|
90
|
+
postgres:
|
|
91
|
+
image: postgres:16@sha256:1a6ab3f5345eb6dbe04a1349529caabdb0ab09293a09590fad07b2246bfa4b54
|
|
92
|
+
tty: false
|
|
93
|
+
environment:
|
|
94
|
+
POSTGRES_DB: "{{project}}"
|
|
95
|
+
configs:
|
|
96
|
+
- source: postgres-init
|
|
97
|
+
target: /docker-entrypoint-initdb.d/10-app-role.sh
|
|
98
|
+
mode: 0o555
|
|
99
|
+
volumes: [postgres-data:/var/lib/postgresql/data]
|
|
100
|
+
networks: [backing]
|
|
101
|
+
healthcheck:
|
|
102
|
+
test: ["CMD", "pg_isready", "-h", "127.0.0.1", "-U", "postgres", "-d", "{{project}}"]
|
|
103
|
+
interval: 10s
|
|
104
|
+
timeout: 5s
|
|
105
|
+
retries: 5
|
|
106
|
+
deploy:
|
|
107
|
+
replicas: 1
|
|
108
|
+
placement:
|
|
109
|
+
constraints: ["node.labels.{{project}} == production"]
|
|
110
|
+
update_config:
|
|
111
|
+
parallelism: 1
|
|
112
|
+
order: stop-first
|
|
113
|
+
failure_action: rollback
|
|
114
|
+
monitor: 30s
|
|
115
|
+
rollback_config:
|
|
116
|
+
parallelism: 1
|
|
117
|
+
order: stop-first
|
|
118
|
+
restart_policy:
|
|
119
|
+
condition: on-failure
|
|
120
|
+
delay: 5s
|
|
94
121
|
ingress:
|
|
95
122
|
image: caddy:2.10.2@sha256:c3d7ee5d2b11f9dc54f947f68a734c84e9c9666c92c88a7f30b9cba5da182adb
|
|
96
123
|
configs:
|
|
@@ -126,14 +153,29 @@ services:
|
|
|
126
153
|
networks:
|
|
127
154
|
app:
|
|
128
155
|
driver: overlay
|
|
156
|
+
backing:
|
|
157
|
+
driver: overlay
|
|
158
|
+
internal: true
|
|
129
159
|
secrets:
|
|
130
160
|
primary-db-url:
|
|
131
161
|
external: true
|
|
132
162
|
name: "{{project}}-production-primary-db-url-v1"
|
|
163
|
+
postgres-password:
|
|
164
|
+
external: true
|
|
165
|
+
name: "{{project}}-production-postgres-password-v1"
|
|
166
|
+
primary-db-password:
|
|
167
|
+
external: true
|
|
168
|
+
name: "{{project}}-production-primary-db-password-v1"
|
|
133
169
|
configs:
|
|
134
170
|
ingress-config:
|
|
135
171
|
file: ./Caddyfile
|
|
172
|
+
postgres-init:
|
|
173
|
+
external: true
|
|
174
|
+
name: ${POSTGRES_INIT_CONFIG}
|
|
136
175
|
volumes:
|
|
176
|
+
postgres-data:
|
|
177
|
+
external: true
|
|
178
|
+
name: "{{project}}-production-postgres-data"
|
|
137
179
|
ingress-data:
|
|
138
180
|
external: true
|
|
139
181
|
name: "{{project}}-production-ingress-data"
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
# Production environment key roster
|
|
2
2
|
|
|
3
|
-
This inventory declares names, consumers and mounted file paths. Scaffolding seals
|
|
3
|
+
This inventory declares names, consumers and mounted file paths. Scaffolding seals generated database connection, application password and administrator password placeholders to this app's own recipient. Replace them with real credentials before use; no successful deployment is claimed by scaffolding.
|
|
4
4
|
|
|
5
5
|
| Key | Consumer |
|
|
6
6
|
| --- | --- |
|
|
7
7
|
| `PORT` | Core API listener |
|
|
8
8
|
| `HTTP_SECURITY_ALLOWED_ORIGINS` | Core API origin allowlist |
|
|
9
|
-
| `PRIMARY_DB_URL` | Core API and CLI primary database connection; the API
|
|
9
|
+
| `PRIMARY_DB_URL` | Core API and CLI primary database connection; managed deployment sets the API's `PRIMARY_DB_URL_FILE` to `/run/secrets/PRIMARY_DB_URL` from sealed `secrets/primary-db-url.enc` |
|
|
10
|
+
| `POSTGRES_PASSWORD` | PostgreSQL administrator password; managed deployment sets only PostgreSQL's `POSTGRES_PASSWORD_FILE` to `/run/secrets/POSTGRES_PASSWORD` from sealed `secrets/postgres-password.enc` |
|
|
11
|
+
| `PRIMARY_DB_PASSWORD` | First-boot `app_be` role password; managed deployment sets only PostgreSQL's `PRIMARY_DB_PASSWORD_FILE` to `/run/secrets/PRIMARY_DB_PASSWORD` from sealed `secrets/primary-db-password.enc`; it must match the password in the API's sealed connection URI |
|
|
10
12
|
| `NEXT_PUBLIC_SITE_URL` | Product front-end site URL |
|
|
11
13
|
| `NEXT_PUBLIC_GRAPHQL_URL` | Product front-end GraphQL transport |
|
|
12
14
|
| `NEXT_PUBLIC_APP_URL` | Landing front-end product link |
|
|
13
15
|
|
|
14
|
-
Public front-end configuration is supplied at build time and stays outside sealed custody. Replace the generated private
|
|
16
|
+
Public front-end configuration is supplied at build time and stays outside sealed custody. Replace the generated private placeholders only in the manifest's declared `.starcistacks/production/secrets/primary-db-url.enc`, `primary-db-password.enc` and `postgres-password.enc` members. Their versioned external secrets are `{{project}}-production-primary-db-url-v1`, `{{project}}-production-primary-db-password-v1` and `{{project}}-production-postgres-password-v1`; the API receives its least-privilege connection separately from PostgreSQL's administrator password.
|
|
@@ -22,6 +22,8 @@ Declare `ci.github.repository` and `ci.github.defaultBranch` in `.starcistacks/a
|
|
|
22
22
|
|
|
23
23
|
## Development
|
|
24
24
|
|
|
25
|
+
Every development dependency runs in Docker Linux containers. Local Supabase, when declared, runs through the Supabase CLI; every other declared dependency runs through its managed `<dep>:create`, `<dep>:start` and `<dep>:stop` scripts or the declared container model. This starter uses Compose to run its pinned local PostgreSQL and app services; Compose is an implementation detail. A declared native profile may run the app processes locally while dependencies stay in Docker. In production, the app and its dependencies run on Docker Swarm, except Supabase, which is a hosted service.
|
|
26
|
+
|
|
25
27
|
```sh
|
|
26
28
|
npm install
|
|
27
29
|
npm run typecheck
|
|
@@ -163,6 +163,11 @@ environments:
|
|
|
163
163
|
from:
|
|
164
164
|
- member: primary-db-url
|
|
165
165
|
- member: supabase-service-role-key
|
|
166
|
+
# Release tasks: an idempotent write the release owes the data (a catalogue sync, a seed upsert) is declared under tasks and listed in postDeploy.
|
|
167
|
+
# A deploy runs postDeploy after the stack converged and before verify, which stays read-only. Uncomment and name the real command:
|
|
168
|
+
# tasks:
|
|
169
|
+
# sync-catalogue: {service: api, command: [node, dist/cli.js, catalogue, sync]}
|
|
170
|
+
# postDeploy: [sync-catalogue]
|
|
166
171
|
verify:
|
|
167
172
|
http:
|
|
168
173
|
- name: api liveness
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Development stack
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Every development dependency runs in Docker Linux containers. Local Supabase starts through its CLI and supplies PostgreSQL, pgvector, Auth and Storage. Every other declared dependency, such as Redis, starts through its managed `<dep>:create`, `<dep>:start` and `<dep>:stop` scripts or the declared container model; Compose is an implementation detail. The app services may run in the local Docker Swarm model or through a declared native development profile while dependencies stay in Docker.
|
|
4
4
|
The application image references contain illustrative all-zero digests. Replace them with verified published release digests in the manifest, model and `.starcistacks/dev/infra/swarm/releases/images.lock.yaml` 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.
|
|
5
5
|
|
|
6
6
|
The runtime template tracks no encrypted members. Scaffolding generates the two declared members with placeholder values encrypted to this app's own recipient in `.sops.yaml`. Replace those values with `starci app secret set dev` using the matching host age identity in untracked local custody. Existing generated envelopes are decryptable with that app identity and can be updated in place. Each Docker secret contains the corresponding key's real value, and the API receives it through its declared mounted file.
|
|
7
7
|
|
|
8
8
|
| Command | Operation |
|
|
9
9
|
| --- | --- |
|
|
10
|
-
| 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. Replace the reserved development host consistently in the manifest, Caddyfile and public configuration; resolve it to the node and record this in `.starcistacks/dev/infra/swarm/owner-actions.md`. Create the external development secrets named in the model from real sealed members. |
|
|
10
|
+
| prepare | Run `npm run db:start` at the app root; obtain the local Supabase connection and Auth configuration. For a declared Redis, run `npm run redis:create` once, then `npm run redis:start` when using the native app profile. An owner who chooses local Swarm initializes it, labels its node `{{project}}=dev` and provisions the ingress volumes; the model starts its declared Redis with the app. Replace the reserved development host consistently in the manifest, Caddyfile and public configuration; resolve it to the node and record this in `.starcistacks/dev/infra/swarm/owner-actions.md`. Create the external development secrets named in the model from real sealed members. |
|
|
11
11
|
| doctor | From this directory, run `docker stack config -c infra/swarm/stack.yaml`. Confirm image, secret and local Supabase availability. |
|
|
12
12
|
| up | Run `docker stack deploy -c infra/swarm/stack.yaml {{project}}-dev` from this directory after preparation. |
|
|
13
13
|
| status | Run `docker stack services {{project}}-dev`. |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Production stack
|
|
2
2
|
|
|
3
3
|
The tracked model declares Docker Swarm application services and one digest-pinned Caddy ingress.
|
|
4
|
-
Supabase supplies PostgreSQL, Auth and
|
|
4
|
+
Hosted Supabase supplies PostgreSQL, pgvector, Auth and Storage. The app and its other dependencies run on Docker Swarm. A Redis service runs only when `hfs.json` `sides.be.backing` declares it for a capability that needs it: one stateful replica with a digest-pinned image, a named volume with a backup entry and its password as a sealed member granted as a Swarm secret (Redis runs `--appendonly yes` with the volume at `/data`). The scaffold declares none; `starci app add queue|job` declares Redis and renders it here.
|
|
5
5
|
The model is a deployment declaration. It contains no owner credentials, real DNS receipt, completed release or restore proof. Public records under `.starcistacks/production/infra/swarm/` explicitly mark those owner-run operations as pending.
|
|
6
6
|
Its application image references contain illustrative all-zero digests; they identify no published image and must be replaced before deployment.
|
|
7
7
|
|
|
@@ -24,7 +24,7 @@ Declare `ci.github.repository` and `ci.github.defaultBranch` in `.starcistacks/a
|
|
|
24
24
|
|
|
25
25
|
## Development
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Every development dependency runs in Docker Linux containers. Start local Supabase with `npm run db:start` through the Supabase CLI; start any other declared dependency through its managed `<dep>:create`, `<dep>:start` and `<dep>:stop` scripts or the declared container model. Compose is an implementation detail. Configure the local environment and start the API and web app using the package scripts for the declared development profile. In production, the app and its dependencies run on Docker Swarm, except Supabase, which is a hosted service; Supabase pgvector owns vector search.
|
|
28
28
|
|
|
29
29
|
```sh
|
|
30
30
|
npm install
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- Extend the one shared inbox; provider and broker claims keep null client fields.
|
|
2
|
+
alter table public.inbox
|
|
3
|
+
add column request_fingerprint varchar(64),
|
|
4
|
+
add column outcome jsonb;
|
|
5
|
+
|
|
6
|
+
create index inbox_client_retention_idx on public.inbox (claimed_at, source, event_id)
|
|
7
|
+
where source like 'client:%' and request_fingerprint is not null and outcome is not null;
|
|
8
|
+
|
|
9
|
+
create policy "inbox_app_be_update"
|
|
10
|
+
on public.inbox for update to app_be
|
|
11
|
+
using (source like 'client:%')
|
|
12
|
+
with check (source like 'client:%');
|
|
13
|
+
|
|
14
|
+
create policy "inbox_app_be_delete"
|
|
15
|
+
on public.inbox for delete to app_be
|
|
16
|
+
using (source like 'client:%' and request_fingerprint is not null and outcome is not null);
|
|
17
|
+
|
|
18
|
+
grant update, delete on table public.inbox to app_be;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DomainError } from "@modules/platform/errors"
|
|
2
|
+
import type { ErrorKind } from "@modules/platform/errors"
|
|
3
|
+
|
|
4
|
+
/** Codes of the event-bus capability. */
|
|
5
|
+
export enum EventBusErrorCode {
|
|
6
|
+
/** A publisher supplied an unsafe or unbounded request identity. */
|
|
7
|
+
CorrelationInvalid = "EVENT_BUS_CORRELATION_INVALID",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** How each event-bus code travels. */
|
|
11
|
+
export const EVENT_BUS_ERROR_KINDS: Record<EventBusErrorCode, ErrorKind> = {
|
|
12
|
+
[EventBusErrorCode.CorrelationInvalid]: "invalid",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The one error class of the event-bus capability. */
|
|
16
|
+
export class EventBusError extends DomainError<EventBusErrorCode> {}
|
|
@@ -23,6 +23,8 @@ export interface EventClass<Event extends BaseEvent> {
|
|
|
23
23
|
|
|
24
24
|
/** One committed delivery received by a reactor. */
|
|
25
25
|
export interface EventDelivery<Event extends BaseEvent> {
|
|
26
|
+
/** The request id carried explicitly from intake to delivery. */
|
|
27
|
+
readonly correlationId: string
|
|
26
28
|
/** The consumer's idempotency key. */
|
|
27
29
|
readonly eventId: string
|
|
28
30
|
/** The typed event. */
|
|
@@ -31,8 +33,16 @@ export interface EventDelivery<Event extends BaseEvent> {
|
|
|
31
33
|
readonly attempt: number
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
/** Explicit request context carried independently of the business payload. */
|
|
37
|
+
export interface EventMetadata {
|
|
38
|
+
/** The originating request or promoted legacy event identity. */
|
|
39
|
+
readonly correlationId: string
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
/** The typed event's durable envelope, read only after the intake transaction commits. */
|
|
35
43
|
export interface EventEnvelope {
|
|
44
|
+
/** The originating request context, independent of the business payload. */
|
|
45
|
+
readonly metadata: EventMetadata
|
|
36
46
|
/** The stable event id. */
|
|
37
47
|
readonly eventId: string
|
|
38
48
|
/** The contract's event name. */
|
|
@@ -4,7 +4,7 @@ import type { BaseEvent, EventClass, EventDelivery } from "./event-bus.contracts
|
|
|
4
4
|
/** The publishing side of the lite event bus; the reactor or job owns delivery of committed outbox rows. */
|
|
5
5
|
export interface EventBus {
|
|
6
6
|
/** Writes the typed event in the caller's transaction; a rollback writes no durable event. */
|
|
7
|
-
publish(event: BaseEvent, tx: EntityManager): Promise<void>
|
|
7
|
+
publish(event: BaseEvent, tx: EntityManager, correlationId: string): Promise<void>
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/** A later message transport receives one typed event and forwards its delivery id. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Test } from "@nestjs/testing"
|
|
2
2
|
import { fakeTransaction, mockEntityManager } from "@starci/jest-preset"
|
|
3
|
+
import { EventBusErrorCode } from "./errors/event-bus.error"
|
|
3
4
|
import { BaseEvent } from "./event-bus.contracts"
|
|
4
5
|
import { EventBusService } from "./event-bus.service"
|
|
5
6
|
import { INSERT_OUTBOX_EVENT } from "./persistence/event-bus.sql"
|
|
@@ -16,17 +17,28 @@ describe("EventBusService", () => {
|
|
|
16
17
|
return module.get(EventBusService)
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
it.each(["", "unsafe\nid", "x".repeat(129)])("refuses invalid correlation before SQL: %s", async (correlationId) => {
|
|
21
|
+
const subject = await build()
|
|
22
|
+
const manager = mockEntityManager({ query: [INSERT_OUTBOX_EVENT, []] })
|
|
23
|
+
|
|
24
|
+
await expect(subject.publish(new ReceiptRecordedEvent(), manager, correlationId)).rejects.toMatchObject({
|
|
25
|
+
code: EventBusErrorCode.CorrelationInvalid,
|
|
26
|
+
})
|
|
27
|
+
expect(manager.query).not.toHaveBeenCalled()
|
|
28
|
+
})
|
|
29
|
+
|
|
19
30
|
it("writes the typed envelope through the receipt transaction", async () => {
|
|
20
31
|
const subject = await build()
|
|
21
32
|
const transaction = fakeTransaction(mockEntityManager({ query: [INSERT_OUTBOX_EVENT, []] }))
|
|
22
33
|
const event = new ReceiptRecordedEvent()
|
|
23
34
|
|
|
24
|
-
await transaction.em.transaction((manager) => subject.publish(event, manager))
|
|
35
|
+
await transaction.em.transaction((manager) => subject.publish(event, manager, "request-1"))
|
|
25
36
|
|
|
26
37
|
expect(transaction.em.query).toHaveBeenCalledWith(INSERT_OUTBOX_EVENT, [
|
|
27
38
|
"receipt-1",
|
|
28
39
|
"spec.delivery-recorded",
|
|
29
|
-
JSON.stringify({ eventId: event.eventId, eventName: event.eventName, payload: event.payload }),
|
|
40
|
+
JSON.stringify({ metadata: { correlationId: "request-1" }, eventId: event.eventId, eventName: event.eventName, payload: event.payload }),
|
|
41
|
+
"request-1",
|
|
30
42
|
])
|
|
31
43
|
expect(transaction.commits).toBe(1)
|
|
32
44
|
})
|
|
@@ -37,7 +49,7 @@ describe("EventBusService", () => {
|
|
|
37
49
|
jest.spyOn(manager, "query").mockRejectedValue(new Error("outbox unavailable"))
|
|
38
50
|
const transaction = fakeTransaction(manager)
|
|
39
51
|
|
|
40
|
-
await expect(transaction.em.transaction((tx) => subject.publish(new ReceiptRecordedEvent(), tx))).rejects.toThrow("outbox unavailable")
|
|
52
|
+
await expect(transaction.em.transaction((tx) => subject.publish(new ReceiptRecordedEvent(), tx, "request-1"))).rejects.toThrow("outbox unavailable")
|
|
41
53
|
|
|
42
54
|
expect(transaction.commits).toBe(0)
|
|
43
55
|
expect(transaction.rollbacks).toBe(1)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Injectable } from "@nestjs/common"
|
|
2
2
|
import type { EntityManager } from "typeorm"
|
|
3
|
+
import { isCorrelationId } from "@modules/platform/logging"
|
|
4
|
+
import { EventBusError, EventBusErrorCode } from "./errors/event-bus.error"
|
|
3
5
|
import type { BaseEvent, EventEnvelope } from "./event-bus.contracts"
|
|
4
6
|
import type { EventBus } from "./event-bus.port"
|
|
5
7
|
import { INSERT_OUTBOX_EVENT } from "./persistence/event-bus.sql"
|
|
@@ -8,8 +10,14 @@ import { INSERT_OUTBOX_EVENT } from "./persistence/event-bus.sql"
|
|
|
8
10
|
/** Writes a receipt event into the same transaction as its inbox receipt, without running any consumer. */
|
|
9
11
|
export class EventBusService implements EventBus {
|
|
10
12
|
/** 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
|
-
|
|
13
|
-
|
|
13
|
+
async publish(event: BaseEvent, tx: EntityManager, correlationId: string): Promise<void> {
|
|
14
|
+
if (!isCorrelationId(correlationId)) throw new EventBusError({ code: EventBusErrorCode.CorrelationInvalid })
|
|
15
|
+
const envelope: EventEnvelope = {
|
|
16
|
+
metadata: { correlationId },
|
|
17
|
+
eventId: event.eventId,
|
|
18
|
+
eventName: event.eventName,
|
|
19
|
+
payload: event.payload,
|
|
20
|
+
}
|
|
21
|
+
await tx.query(INSERT_OUTBOX_EVENT, [event.eventId, event.eventName, JSON.stringify(envelope), correlationId])
|
|
14
22
|
}
|
|
15
23
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { EVENT_BUS_ERROR_KINDS } from "./errors/event-bus.error"
|
|
2
|
+
export { EVENT_BUS_MESSAGES } from "./messages/event-bus.messages"
|
|
1
3
|
export { BaseEvent } from "./event-bus.contracts"
|
|
2
4
|
export type { EventClass, EventDelivery, EventEnvelope, ParsedEvent } from "./event-bus.contracts"
|
|
3
5
|
export { EVENT_BUS, InjectEventBus } from "./event-bus.decorators"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MessageBundle } from "@modules/platform/i18n"
|
|
2
|
+
|
|
3
|
+
/** Display text of the event-bus codes, Vietnamese and English. */
|
|
4
|
+
export const EVENT_BUS_MESSAGES: MessageBundle = {
|
|
5
|
+
vi: { "errors.EVENT_BUS_CORRELATION_INVALID": "M? t??ng quan s? ki?n kh?ng h?p l?." },
|
|
6
|
+
en: { "errors.EVENT_BUS_CORRELATION_INVALID": "The event correlation id is invalid." },
|
|
7
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { sql } from "@modules/platform/database"
|
|
2
2
|
|
|
3
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)`
|
|
4
|
+
export const INSERT_OUTBOX_EVENT = sql`INSERT INTO public.event_bus (event_id, event_name, payload, correlation_id)
|
|
5
|
+
VALUES ($1, $2, $3::jsonb, $4)`
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DomainError } from "@modules/platform/errors"
|
|
2
|
+
import type { ErrorKind } from "@modules/platform/errors"
|
|
3
|
+
|
|
4
|
+
/** Stable client-operation refusals shared by transports and durable inbox adapters. */
|
|
5
|
+
export enum IdempotencyErrorCode {
|
|
6
|
+
/** This principal's operation key was already used for a different request. */
|
|
7
|
+
KeyReused = "IDEMPOTENCY_KEY_REUSED",
|
|
8
|
+
/** The key, authenticated scope or JSON data cannot identify a valid operation. */
|
|
9
|
+
RequestInvalid = "IDEMPOTENCY_REQUEST_INVALID",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Transport kinds of the public idempotency refusals. */
|
|
13
|
+
export const IDEMPOTENCY_ERROR_KINDS: Record<IdempotencyErrorCode, ErrorKind> = {
|
|
14
|
+
[IdempotencyErrorCode.KeyReused]: "conflict",
|
|
15
|
+
[IdempotencyErrorCode.RequestInvalid]: "invalid",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The public idempotency error family. */
|
|
19
|
+
export class IdempotencyError extends DomainError<IdempotencyErrorCode> {}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MessageBundle } from "@modules/platform/i18n"
|
|
2
|
+
|
|
3
|
+
/** Display text of the public client-operation refusals. */
|
|
4
|
+
export const IDEMPOTENCY_MESSAGES: MessageBundle = {
|
|
5
|
+
vi: {
|
|
6
|
+
"errors.IDEMPOTENCY_KEY_REUSED": "Khóa thao tác đã được dùng cho nội dung khác.",
|
|
7
|
+
"errors.IDEMPOTENCY_REQUEST_INVALID": "Khóa, phạm vi hoặc dữ liệu thao tác không hợp lệ.",
|
|
8
|
+
},
|
|
9
|
+
en: {
|
|
10
|
+
"errors.IDEMPOTENCY_KEY_REUSED": "This operation key was already used with a different request.",
|
|
11
|
+
"errors.IDEMPOTENCY_REQUEST_INVALID": "The operation key, scope or JSON data is invalid.",
|
|
12
|
+
},
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Test } from "@nestjs/testing"
|
|
2
|
+
import { IdempotencyError, IdempotencyErrorCode } from "@modules/platform/idempotency"
|
|
3
|
+
import { clientSource } from "./client-operation.policy"
|
|
4
|
+
|
|
5
|
+
const CLIENT_SOURCE = Symbol("client-source")
|
|
6
|
+
const build = async () => {
|
|
7
|
+
const moduleRef = await Test.createTestingModule({
|
|
8
|
+
providers: [{ provide: CLIENT_SOURCE, useValue: clientSource }],
|
|
9
|
+
}).compile()
|
|
10
|
+
return moduleRef.get<typeof clientSource>(CLIENT_SOURCE)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const REQUEST = { principalId: "person-1", operation: "checkout.place", idempotencyKey: "key-1", request: {} }
|
|
14
|
+
|
|
15
|
+
describe("clientSource", () => {
|
|
16
|
+
it("binds the exact authenticated principal and operation and accepts a 64-character key", async () => {
|
|
17
|
+
const sourceOf = await build()
|
|
18
|
+
expect(sourceOf({ ...REQUEST, idempotencyKey: "a".repeat(64) })).toBe("client:person-1:checkout.place")
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it.each([
|
|
22
|
+
{ ...REQUEST, principalId: " " },
|
|
23
|
+
{ ...REQUEST, principalId: "a:b" },
|
|
24
|
+
{ ...REQUEST, operation: " " },
|
|
25
|
+
{ ...REQUEST, operation: "a:b" },
|
|
26
|
+
{ ...REQUEST, idempotencyKey: " " },
|
|
27
|
+
{ ...REQUEST, idempotencyKey: "a".repeat(65) },
|
|
28
|
+
])("refuses ambiguous scopes and unbounded or empty keys %p", async (request) => {
|
|
29
|
+
const sourceOf = await build()
|
|
30
|
+
expect(() => sourceOf(request)).toThrow(new IdempotencyError({ code: IdempotencyErrorCode.RequestInvalid }))
|
|
31
|
+
})
|
|
32
|
+
})
|