@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
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Test } from "@nestjs/testing"
|
|
2
|
+
import { IdempotencyError, IdempotencyErrorCode } from "@modules/platform/idempotency"
|
|
3
|
+
import { requestFingerprint } from "./request-fingerprint.policy"
|
|
4
|
+
|
|
5
|
+
const FINGERPRINT = Symbol("fingerprint")
|
|
6
|
+
const build = async () => {
|
|
7
|
+
const moduleRef = await Test.createTestingModule({
|
|
8
|
+
providers: [{ provide: FINGERPRINT, useValue: requestFingerprint }],
|
|
9
|
+
}).compile()
|
|
10
|
+
return moduleRef.get<typeof requestFingerprint>(FINGERPRINT)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("requestFingerprint", () => {
|
|
14
|
+
it("hashes nested objects independently of property insertion order", async () => {
|
|
15
|
+
const fingerprint = await build()
|
|
16
|
+
|
|
17
|
+
expect(fingerprint({ personId: "person-1", body: { quantity: 2, productId: "product-1" } })).toBe(
|
|
18
|
+
fingerprint({ body: { productId: "product-1", quantity: 2 }, personId: "person-1" }),
|
|
19
|
+
)
|
|
20
|
+
expect(fingerprint({ a: 1 })).toMatch(/^[a-f0-9]{64}$/)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("keeps binary UTF-16 key order for non-ASCII and supplementary Unicode keys", async () => {
|
|
24
|
+
const fingerprint = await build()
|
|
25
|
+
const body = { "\uE000": 5, "\u{1F600}": 4, "\u00E9": 1, a: 3, Z: 2 }
|
|
26
|
+
|
|
27
|
+
expect(fingerprint(body)).toBe(fingerprint({ Z: 2, a: 3, "\u00E9": 1, "\u{1F600}": 4, "\uE000": 5 }))
|
|
28
|
+
expect(fingerprint(body)).toBe("6eb3469044da5c7df68404ba05b90306384fc773ad605d4de06321a2092ff365")
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("preserves array order, JSON types and nested values", async () => {
|
|
32
|
+
const fingerprint = await build()
|
|
33
|
+
|
|
34
|
+
expect(fingerprint([1, 2])).not.toBe(fingerprint([2, 1]))
|
|
35
|
+
expect(fingerprint({ value: 1 })).not.toBe(fingerprint({ value: "1" }))
|
|
36
|
+
expect(fingerprint({ body: { quantity: 2 } })).not.toBe(fingerprint({ body: { quantity: 3 } }))
|
|
37
|
+
expect(new Set([null, true, false, "", 0].map(fingerprint)).size).toBe(5)
|
|
38
|
+
expect(fingerprint([])).not.toBe(fingerprint({}))
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("accepts an object without a prototype and repeated noncyclic references", async () => {
|
|
42
|
+
const fingerprint = await build()
|
|
43
|
+
const shared = { quantity: 2 }
|
|
44
|
+
const withoutPrototype: unknown = Object.assign(Object.create(null), { quantity: 2 })
|
|
45
|
+
|
|
46
|
+
expect(fingerprint(withoutPrototype)).toBe(fingerprint(shared))
|
|
47
|
+
expect(fingerprint([shared, shared])).toBe(fingerprint([{ quantity: 2 }, { quantity: 2 }]))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it.each([
|
|
51
|
+
[undefined],
|
|
52
|
+
[() => 1],
|
|
53
|
+
[Symbol("unsupported")],
|
|
54
|
+
[BigInt(1)],
|
|
55
|
+
[Number.NaN],
|
|
56
|
+
[Number.POSITIVE_INFINITY],
|
|
57
|
+
[Number.NEGATIVE_INFINITY],
|
|
58
|
+
[{ missing: undefined }],
|
|
59
|
+
[[undefined]],
|
|
60
|
+
[Array(1)],
|
|
61
|
+
[new Date("2026-10-24T00:00:00.000Z")],
|
|
62
|
+
[new Map([["quantity", 2]])],
|
|
63
|
+
])("refuses values that cannot represent a JSON request: %p", async (request: unknown) => {
|
|
64
|
+
const fingerprint = await build()
|
|
65
|
+
|
|
66
|
+
expect(() => fingerprint(request)).toThrow(new IdempotencyError({ code: IdempotencyErrorCode.RequestInvalid }))
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it("refuses cyclic requests before a claim can be written", async () => {
|
|
70
|
+
const fingerprint = await build()
|
|
71
|
+
const cyclic: Record<string, unknown> = { quantity: 2 }
|
|
72
|
+
cyclic.body = cyclic
|
|
73
|
+
|
|
74
|
+
expect(() => fingerprint(cyclic)).toThrow(new IdempotencyError({ code: IdempotencyErrorCode.RequestInvalid }))
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import { IdempotencyError, IdempotencyErrorCode } from "@modules/platform/idempotency"
|
|
3
|
+
|
|
4
|
+
/** Serializes JSON without depending on object insertion order; array order remains part of the request. */
|
|
5
|
+
const serializeJson = (value: unknown, ancestors: Set<object>): string => {
|
|
6
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value)
|
|
7
|
+
if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value)
|
|
8
|
+
if (typeof value !== "object" || ancestors.has(value))
|
|
9
|
+
throw new IdempotencyError({ code: IdempotencyErrorCode.RequestInvalid })
|
|
10
|
+
ancestors.add(value)
|
|
11
|
+
let serialized: string
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
serialized = `[${Array.from(value, (item: unknown) => serializeJson(item, ancestors)).join(",")}]`
|
|
14
|
+
} else {
|
|
15
|
+
const prototype: unknown = Object.getPrototypeOf(value)
|
|
16
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
17
|
+
throw new IdempotencyError({ code: IdempotencyErrorCode.RequestInvalid })
|
|
18
|
+
}
|
|
19
|
+
serialized = `{${Object.keys(value)
|
|
20
|
+
.sort((left, right) => Number(left > right) - Number(left < right))
|
|
21
|
+
.map((key) => {
|
|
22
|
+
const item: unknown = Reflect.get(value, key)
|
|
23
|
+
return `${JSON.stringify(key)}:${serializeJson(item, ancestors)}`
|
|
24
|
+
})
|
|
25
|
+
.join(",")}}`
|
|
26
|
+
}
|
|
27
|
+
ancestors.delete(value)
|
|
28
|
+
return serialized
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Returns a stable JSON representation for both the request digest and the stored outcome. */
|
|
32
|
+
export const canonicalJson = (value: unknown): string => serializeJson(value, new Set())
|
|
33
|
+
|
|
34
|
+
/** Hashes the validated client request, so a reused key cannot silently apply a different body. */
|
|
35
|
+
export const requestFingerprint = (request: unknown): string =>
|
|
36
|
+
createHash("sha256").update(canonicalJson(request)).digest("hex")
|
|
@@ -21,6 +21,7 @@ revoke all on table public.inbox from public, anon, authenticated;
|
|
|
21
21
|
grant select, insert on table public.inbox to app_be;
|
|
22
22
|
|
|
23
23
|
create table public.event_bus (
|
|
24
|
+
correlation_id varchar(128) not null,
|
|
24
25
|
event_id text primary key check (event_id <> ''),
|
|
25
26
|
event_name text not null check (event_name <> ''),
|
|
26
27
|
payload jsonb not null,
|
|
@@ -31,7 +31,7 @@ describe("@@Provider@@InboxService", () => {
|
|
|
31
31
|
it("claims, records and publishes a typed event in the same transaction", async () => {
|
|
32
32
|
const { inbox, claims, events, entityManager, transaction } = await build(true)
|
|
33
33
|
|
|
34
|
-
await inbox.record@@Provider@@Delivery({ eventId: "delivery-1", delivery: DELIVERY })
|
|
34
|
+
await inbox.record@@Provider@@Delivery({ eventId: "delivery-1", delivery: DELIVERY, correlationId: "request-1" })
|
|
35
35
|
|
|
36
36
|
expect(entityManager.query).toHaveBeenCalledTimes(1)
|
|
37
37
|
expect(entityManager.query).toHaveBeenCalledWith(INSERT_@@providerUpper@@_INBOX, ["@@provider@@", "delivery-1", JSON.stringify(DELIVERY)])
|
|
@@ -45,7 +45,7 @@ describe("@@Provider@@InboxService", () => {
|
|
|
45
45
|
it("acknowledges a duplicate @@provider@@ delivery without further work", async () => {
|
|
46
46
|
const { inbox, events, entityManager, transaction } = await build(false)
|
|
47
47
|
|
|
48
|
-
await inbox.record@@Provider@@Delivery({ eventId: "delivery-1", delivery: DELIVERY })
|
|
48
|
+
await inbox.record@@Provider@@Delivery({ eventId: "delivery-1", delivery: DELIVERY, correlationId: "request-1" })
|
|
49
49
|
|
|
50
50
|
expect(entityManager.query).not.toHaveBeenCalled()
|
|
51
51
|
expect(events.events).toEqual([])
|
|
@@ -55,7 +55,7 @@ describe("@@Provider@@InboxService", () => {
|
|
|
55
55
|
it("does not publish for an already recorded receipt", async () => {
|
|
56
56
|
const { inbox, events, transaction } = await build(true, [])
|
|
57
57
|
|
|
58
|
-
await inbox.record@@Provider@@Delivery({ eventId: "delivery-1", delivery: DELIVERY })
|
|
58
|
+
await inbox.record@@Provider@@Delivery({ eventId: "delivery-1", delivery: DELIVERY, correlationId: "request-1" })
|
|
59
59
|
|
|
60
60
|
expect(events.events).toEqual([])
|
|
61
61
|
expect(transaction.commits).toBe(1)
|
|
@@ -66,7 +66,7 @@ describe("@@Provider@@InboxService", () => {
|
|
|
66
66
|
const failure = new Error("outbox unavailable")
|
|
67
67
|
events.failNext("publish", failure)
|
|
68
68
|
|
|
69
|
-
await expect(inbox.record@@Provider@@Delivery({ eventId: "delivery-1", delivery: DELIVERY })).rejects.toBe(failure)
|
|
69
|
+
await expect(inbox.record@@Provider@@Delivery({ eventId: "delivery-1", delivery: DELIVERY, correlationId: "request-1" })).rejects.toBe(failure)
|
|
70
70
|
|
|
71
71
|
expect(transaction.rollbacks).toBe(1)
|
|
72
72
|
expect(transaction.commits).toBe(0)
|
|
@@ -13,6 +13,8 @@ interface @@Provider@@Delivery {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
interface @@Provider@@DeliveryRequest {
|
|
16
|
+
/** The request identity carried into the event envelope. */
|
|
17
|
+
readonly correlationId: string
|
|
16
18
|
readonly eventId: string | undefined
|
|
17
19
|
readonly delivery: @@Provider@@Delivery
|
|
18
20
|
}
|
|
@@ -38,7 +40,7 @@ export class @@Provider@@InboxService {
|
|
|
38
40
|
payload: request.delivery,
|
|
39
41
|
})
|
|
40
42
|
if (receiptId === undefined) return
|
|
41
|
-
await this.eventBus.publish(@@Provider@@DeliveryRecordedEvent.create({ receiptId }), manager)
|
|
43
|
+
await this.eventBus.publish(@@Provider@@DeliveryRecordedEvent.create({ receiptId }), manager, request.correlationId)
|
|
42
44
|
})
|
|
43
45
|
}
|
|
44
46
|
}
|
|
@@ -7,6 +7,7 @@ import { @@Provider@@InboxService } from "@modules/domain/@@provider@@-inbox"
|
|
|
7
7
|
import { Secret } from "@modules/platform/config"
|
|
8
8
|
import { HttpSecurityModule, WebhookSignatureService } from "@modules/platform/http-security"
|
|
9
9
|
import type { HttpSecurityOptions } from "@modules/platform/http-security"
|
|
10
|
+
import type { CorrelationRequest } from "@modules/platform/logging"
|
|
10
11
|
import { @@Event@@Request } from "./dto/@@event@@.request"
|
|
11
12
|
import { @@Provider@@Webhook } from "./@@provider@@.webhook"
|
|
12
13
|
|
|
@@ -24,7 +25,7 @@ const deliveryOf = (): @@Event@@Request => {
|
|
|
24
25
|
|
|
25
26
|
const DELIVERY = deliveryOf()
|
|
26
27
|
const RAW_BODY = Buffer.from(JSON.stringify(DELIVERY))
|
|
27
|
-
const REQUEST = mock<RawBodyRequest<Request
|
|
28
|
+
const REQUEST = mock<RawBodyRequest<Request> & CorrelationRequest>({ rawBody: RAW_BODY, correlationId: "request-1" })
|
|
28
29
|
|
|
29
30
|
const OPTIONS: HttpSecurityOptions = {
|
|
30
31
|
allowedOrigins: [],
|
|
@@ -58,7 +59,7 @@ describe("@@Provider@@Webhook", () => {
|
|
|
58
59
|
await door.receive(REQUEST, sign(RAW_BODY, SIGNED_AT), SIGNED_AT, DELIVERY_ID, DELIVERY)
|
|
59
60
|
|
|
60
61
|
expect(deliveries.record@@Provider@@Delivery).toHaveBeenCalledTimes(1)
|
|
61
|
-
expect(deliveries.record@@Provider@@Delivery).toHaveBeenCalledWith({ eventId: DELIVERY_ID, delivery: DELIVERY })
|
|
62
|
+
expect(deliveries.record@@Provider@@Delivery).toHaveBeenCalledWith({ eventId: DELIVERY_ID, delivery: DELIVERY, correlationId: "request-1" })
|
|
62
63
|
})
|
|
63
64
|
|
|
64
65
|
it("never reaches the inbox when the signature is not the provider's", async () => {
|
|
@@ -5,6 +5,7 @@ import { Public, PublicReason } from "@modules/domain/identity"
|
|
|
5
5
|
import { @@Provider@@InboxService } from "@modules/domain/@@provider@@-inbox"
|
|
6
6
|
import { InjectWebhookSignature, RateLimit, RateTier } from "@modules/platform/http-security"
|
|
7
7
|
import type { WebhookSignatureService } from "@modules/platform/http-security"
|
|
8
|
+
import type { CorrelationRequest } from "@modules/platform/logging"
|
|
8
9
|
import { @@Event@@Request } from "./dto/@@event@@.request"
|
|
9
10
|
|
|
10
11
|
@Controller("webhooks/@@provider@@")
|
|
@@ -25,13 +26,13 @@ export class @@Provider@@Webhook {
|
|
|
25
26
|
@Public({ reason: PublicReason.SignedWebhook })
|
|
26
27
|
@RateLimit(RateTier.Strict)
|
|
27
28
|
async receive(
|
|
28
|
-
@Req() request: RawBodyRequest<Request
|
|
29
|
+
@Req() request: RawBodyRequest<Request> & CorrelationRequest,
|
|
29
30
|
@Headers("x-@@provider@@-signature") signature: string | undefined,
|
|
30
31
|
@Headers("x-@@provider@@-timestamp") timestamp: string | undefined,
|
|
31
32
|
@Headers("x-@@provider@@-delivery-id") deliveryId: string | undefined,
|
|
32
33
|
@Body() delivery: @@Event@@Request,
|
|
33
34
|
): Promise<void> {
|
|
34
35
|
this.signature.verify({ provider: "@@provider@@", rawBody: request.rawBody, signature, timestamp })
|
|
35
|
-
await this.deliveries.record@@Provider@@Delivery({ eventId: deliveryId, delivery })
|
|
36
|
+
await this.deliveries.record@@Provider@@Delivery({ eventId: deliveryId, delivery, correlationId: request.correlationId })
|
|
36
37
|
}
|
|
37
38
|
}
|
|
@@ -3,6 +3,8 @@ import type { ErrorKind } from "@modules/platform/errors"
|
|
|
3
3
|
|
|
4
4
|
/** Codes of the event-bus capability. */
|
|
5
5
|
export enum EventBusErrorCode {
|
|
6
|
+
/** A publisher supplied an unsafe or unbounded request identity. */
|
|
7
|
+
CorrelationInvalid = "EVENT_BUS_CORRELATION_INVALID",
|
|
6
8
|
/** The broker did not answer a call within its deadline, or refused it. */
|
|
7
9
|
BrokerUnavailable = "EVENT_BUS_BROKER_UNAVAILABLE",
|
|
8
10
|
/** A dead letter id does not name a message the dead-letter topic holds. */
|
|
@@ -11,6 +13,7 @@ export enum EventBusErrorCode {
|
|
|
11
13
|
|
|
12
14
|
/** How each event-bus code travels. */
|
|
13
15
|
export const EVENT_BUS_ERROR_KINDS: Record<EventBusErrorCode, ErrorKind> = {
|
|
16
|
+
[EventBusErrorCode.CorrelationInvalid]: "invalid",
|
|
14
17
|
[EventBusErrorCode.BrokerUnavailable]: "unavailable",
|
|
15
18
|
[EventBusErrorCode.DeadLetterUnknown]: "not-found",
|
|
16
19
|
}
|
|
@@ -25,6 +25,8 @@ export interface EventClass<Event extends BaseEvent> {
|
|
|
25
25
|
|
|
26
26
|
/** An event as a consumer receives it. */
|
|
27
27
|
export interface EventDelivery<Event extends BaseEvent> {
|
|
28
|
+
/** The originating request id, carried explicitly across the broker. */
|
|
29
|
+
readonly correlationId: string
|
|
28
30
|
/** The stable event id: the dedupe key of the receiver. */
|
|
29
31
|
readonly eventId: string
|
|
30
32
|
/** The event. */
|
|
@@ -47,8 +49,16 @@ export interface EventDeadLetter {
|
|
|
47
49
|
readonly attempts: number
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
/** Explicit request context carried independently of the business payload. */
|
|
53
|
+
export interface EventMetadata {
|
|
54
|
+
/** The originating request or promoted legacy event identity. */
|
|
55
|
+
readonly correlationId: string
|
|
56
|
+
}
|
|
57
|
+
|
|
50
58
|
/** The envelope of an event on the wire: what the outbox row stores and a consumer reads back. */
|
|
51
59
|
export interface EventEnvelope {
|
|
60
|
+
/** Request context that belongs to transport metadata rather than the business payload. */
|
|
61
|
+
readonly metadata: EventMetadata
|
|
52
62
|
/** The stable event id. */
|
|
53
63
|
readonly eventId: string
|
|
54
64
|
/** The event name. */
|
|
@@ -3,8 +3,8 @@ import type { BaseEvent, EventClass, EventDeadLetter, EventDelivery } from "./ev
|
|
|
3
3
|
|
|
4
4
|
/** The event bus of a service: events leave it through `publish`, called by a domain service in the transaction of the change they report. */
|
|
5
5
|
export interface EventBus {
|
|
6
|
-
/** Writes the event in the outbox of
|
|
7
|
-
publish(event: BaseEvent, tx: EntityManager): Promise<void>
|
|
6
|
+
/** Writes the event and explicit request id in the outbox of `tx`; the relay sends them after commit. */
|
|
7
|
+
publish(event: BaseEvent, tx: EntityManager, correlationId: string): Promise<void>
|
|
8
8
|
/** How many deliveries of the class wait on the retry topic for their backoff to pass. */
|
|
9
9
|
pendingRetries(event: EventClass<BaseEvent>): Promise<number>
|
|
10
10
|
/** The events of the class that ran out of attempts. */
|
|
@@ -46,7 +46,7 @@ class PingEvent extends BaseEvent {
|
|
|
46
46
|
|
|
47
47
|
const DLQ = "run-1.events.probe.dlq"
|
|
48
48
|
|
|
49
|
-
const envelope = (eventName: string, eventId: string) => JSON.stringify({ eventId, eventName, payload: {} })
|
|
49
|
+
const envelope = (eventName: string, eventId: string) => JSON.stringify({ metadata: { correlationId: `request-${eventId}` }, eventId, eventName, payload: {} })
|
|
50
50
|
|
|
51
51
|
const letter = (offset: string, eventName: string, headers: Record<string, string>): InboundMessage => ({
|
|
52
52
|
topic: DLQ,
|
|
@@ -72,19 +72,32 @@ const build = async () => {
|
|
|
72
72
|
|
|
73
73
|
describe("EventBusService", () => {
|
|
74
74
|
describe("publish", () => {
|
|
75
|
+
it.each(["", "unsafe\nid", "x".repeat(129)])("refuses an invalid correlation id before SQL: %s", async (correlationId) => {
|
|
76
|
+
const tx = mockEntityManager({ query: [INSERT_OUTBOX_ROW, []] })
|
|
77
|
+
const { bus, transport } = await build()
|
|
78
|
+
|
|
79
|
+
await expect(bus.publish(new PingEvent("e-1", { note: "hi" }), tx, correlationId)).rejects.toMatchObject({
|
|
80
|
+
code: EventBusErrorCode.CorrelationInvalid,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
expect(tx.query).not.toHaveBeenCalled()
|
|
84
|
+
expect(transport.send).not.toHaveBeenCalled()
|
|
85
|
+
})
|
|
86
|
+
|
|
75
87
|
it("writes the envelope as an outbox row of the caller transaction, keyed by the event id on the topic of the service", async () => {
|
|
76
88
|
const tx = mockEntityManager({ query: [INSERT_OUTBOX_ROW, []] })
|
|
77
89
|
const { bus, transport } = await build()
|
|
78
90
|
|
|
79
|
-
await bus.publish(new PingEvent("e-1", { note: "hi" }), tx)
|
|
91
|
+
await bus.publish(new PingEvent("e-1", { note: "hi" }), tx, "request-1")
|
|
80
92
|
|
|
81
93
|
expect(tx.query).toHaveBeenCalledWith(INSERT_OUTBOX_ROW, [
|
|
82
94
|
"e-1",
|
|
83
95
|
"probe.ping",
|
|
84
96
|
"run-1.events.probe",
|
|
85
97
|
"e-1",
|
|
86
|
-
JSON.stringify({ eventId: "e-1", eventName: "probe.ping", payload: { note: "hi" } }),
|
|
98
|
+
JSON.stringify({ metadata: { correlationId: "request-1" }, eventId: "e-1", eventName: "probe.ping", payload: { note: "hi" } }),
|
|
87
99
|
new Date(AT),
|
|
100
|
+
"request-1",
|
|
88
101
|
])
|
|
89
102
|
expect(transport.send).not.toHaveBeenCalled()
|
|
90
103
|
})
|
|
@@ -2,6 +2,7 @@ import { Injectable } from "@nestjs/common"
|
|
|
2
2
|
import type { EntityManager } from "typeorm"
|
|
3
3
|
import { InjectClock } from "@modules/platform/clock"
|
|
4
4
|
import type { Clock } from "@modules/platform/clock"
|
|
5
|
+
import { isCorrelationId } from "@modules/platform/logging"
|
|
5
6
|
import { EventBusError, EventBusErrorCode } from "./errors/event-bus.error"
|
|
6
7
|
import type { BaseEvent, EventClass, EventDeadLetter, EventEnvelope } from "./event-bus.contracts"
|
|
7
8
|
import { InjectEventBusOptions, InjectEventTransport } from "./event-bus.decorators"
|
|
@@ -42,9 +43,15 @@ export class EventBusService implements EventBus {
|
|
|
42
43
|
@InjectClock() private readonly clock: Clock,
|
|
43
44
|
) {}
|
|
44
45
|
|
|
45
|
-
/** Writes the event in the outbox of
|
|
46
|
-
async publish(event: BaseEvent, tx: EntityManager): Promise<void> {
|
|
47
|
-
|
|
46
|
+
/** Writes the event and explicit request id in the outbox of `tx`; the event id is the partition key. */
|
|
47
|
+
async publish(event: BaseEvent, tx: EntityManager, correlationId: string): Promise<void> {
|
|
48
|
+
if (!isCorrelationId(correlationId)) throw new EventBusError({ code: EventBusErrorCode.CorrelationInvalid })
|
|
49
|
+
const envelope: EventEnvelope = {
|
|
50
|
+
metadata: { correlationId },
|
|
51
|
+
eventId: event.eventId,
|
|
52
|
+
eventName: event.eventName,
|
|
53
|
+
payload: event.payload,
|
|
54
|
+
}
|
|
48
55
|
await tx.query(INSERT_OUTBOX_ROW, [
|
|
49
56
|
event.eventId,
|
|
50
57
|
event.eventName,
|
|
@@ -52,6 +59,7 @@ export class EventBusService implements EventBus {
|
|
|
52
59
|
event.eventId,
|
|
53
60
|
JSON.stringify(envelope),
|
|
54
61
|
this.clock.now(),
|
|
62
|
+
correlationId,
|
|
55
63
|
])
|
|
56
64
|
}
|
|
57
65
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Test } from "@nestjs/testing"
|
|
1
2
|
import { BaseEvent } from "./event-bus.contracts"
|
|
2
3
|
import { readEnvelope, readEnvelopeText, readEventOf } from "./event-envelope.policy"
|
|
3
4
|
|
|
@@ -20,71 +21,159 @@ const isPingPayload = (payload: Record<string, unknown>): payload is PingPayload
|
|
|
20
21
|
typeof payload.note === "string"
|
|
21
22
|
const buildPing = (eventId: string, payload: PingPayload): PingEvent => new PingEvent(eventId, payload)
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
const POLICY = Symbol("policy")
|
|
25
|
+
const policyFunctions = { readEnvelope, readEnvelopeText, readEventOf }
|
|
26
|
+
const build = async () => {
|
|
27
|
+
const moduleRef = await Test.createTestingModule({
|
|
28
|
+
providers: [{ provide: POLICY, useFactory: () => policyFunctions }],
|
|
29
|
+
}).compile()
|
|
30
|
+
return moduleRef.get<typeof policyFunctions>(POLICY)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("event envelope policies readEnvelope", () => {
|
|
34
|
+
it("reads the event id and payload of an envelope", async () => {
|
|
35
|
+
const policy = await build()
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
[{ eventId: "event-1", payload: 1 }, "a non-record payload"],
|
|
36
|
-
])("refuses %s because it is %s", (envelope: unknown, _why: string) => {
|
|
37
|
-
expect(readEnvelope(envelope)).toBeNull()
|
|
37
|
+
expect(policy.readEnvelope({ eventId: "event-1", payload: { note: "hello" }, ignored: true })).toEqual({
|
|
38
|
+
eventId: "event-1",
|
|
39
|
+
payload: { note: "hello" },
|
|
38
40
|
})
|
|
39
41
|
})
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
it.each([
|
|
44
|
+
[null, "a non-record"],
|
|
45
|
+
[{ eventId: 1, payload: {} }, "a non-text event id"],
|
|
46
|
+
[{ eventId: "event-1", payload: 1 }, "a non-record payload"],
|
|
47
|
+
])("refuses %s because it is %s", async (envelope: unknown, _why: string) => {
|
|
48
|
+
const policy = await build()
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
cause: null,
|
|
49
|
-
})
|
|
50
|
-
})
|
|
50
|
+
expect(policy.readEnvelope(envelope)).toBeNull()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
expect(readEnvelopeText(value)).toMatchObject({ eventName: "", cause: null })
|
|
56
|
-
},
|
|
57
|
-
)
|
|
54
|
+
describe("event envelope policies current correlation metadata", () => {
|
|
55
|
+
it("parses an envelope and names its event", async () => {
|
|
56
|
+
const policy = await build()
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
const envelope = {
|
|
59
|
+
metadata: { correlationId: "request-1" },
|
|
60
|
+
eventId: "event-1",
|
|
61
|
+
eventName: "probe.ping",
|
|
62
|
+
payload: { note: "hello" },
|
|
63
|
+
}
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
expect(policy.readEnvelopeText(JSON.stringify(envelope))).toEqual({
|
|
66
|
+
eventName: "probe.ping",
|
|
67
|
+
correlationId: "request-1",
|
|
68
|
+
envelope,
|
|
69
|
+
cause: null,
|
|
65
70
|
})
|
|
66
71
|
})
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
73
|
+
it.each([
|
|
74
|
+
{},
|
|
75
|
+
{ metadata: {} },
|
|
76
|
+
{ metadata: { correlationId: 1 } },
|
|
77
|
+
{ metadata: { correlationId: "" } },
|
|
78
|
+
{ metadata: { correlationId: "request\nspoof" } },
|
|
79
|
+
{ metadata: { correlationId: "a".repeat(129) } },
|
|
80
|
+
])("refuses missing or unsafe correlation metadata", async (envelope) => {
|
|
81
|
+
const policy = await build()
|
|
74
82
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
checked = true
|
|
79
|
-
return isPingPayload(payload)
|
|
80
|
-
}
|
|
83
|
+
expect(policy.readEnvelopeText(JSON.stringify(envelope)).correlationId).toBeNull()
|
|
84
|
+
})
|
|
85
|
+
})
|
|
81
86
|
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
describe("event envelope policies legacy promotion", () => {
|
|
88
|
+
it("promotes a legacy envelope at its safe event id without changing business fields", async () => {
|
|
89
|
+
const policy = await build()
|
|
90
|
+
const legacy = { eventId: "legacy-1", eventName: "probe.ping", payload: { note: "hello" }, causeId: "prior" }
|
|
91
|
+
|
|
92
|
+
expect(policy.readEnvelopeText(JSON.stringify(legacy))).toEqual({
|
|
93
|
+
eventName: "probe.ping",
|
|
94
|
+
correlationId: "legacy-1",
|
|
95
|
+
envelope: { ...legacy, metadata: { correlationId: "legacy-1" } },
|
|
96
|
+
cause: null,
|
|
84
97
|
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it.each([
|
|
101
|
+
{ eventId: "unsafe\nid", eventName: "probe.ping", payload: {} },
|
|
102
|
+
{ eventId: "legacy-1", eventName: 1, payload: {} },
|
|
103
|
+
{ eventId: "legacy-1", eventName: "", payload: {} },
|
|
104
|
+
{ eventId: "legacy-1", eventName: "probe.ping", payload: null },
|
|
105
|
+
{ eventId: "legacy-1", eventName: "probe.ping", payload: [] },
|
|
106
|
+
{ eventId: "legacy-1", eventName: "probe.ping", payload: {}, metadata: null },
|
|
107
|
+
{ eventId: "legacy-1", eventName: "probe.ping", payload: {}, metadata: {} },
|
|
108
|
+
])("rejects an unsafe or ambiguous legacy envelope", async (envelope) => {
|
|
109
|
+
const policy = await build()
|
|
110
|
+
|
|
111
|
+
expect(policy.readEnvelopeText(JSON.stringify(envelope)).correlationId).toBeNull()
|
|
112
|
+
})
|
|
113
|
+
})
|
|
85
114
|
|
|
86
|
-
|
|
87
|
-
|
|
115
|
+
describe("event envelope policies JSON shape and decoding", () => {
|
|
116
|
+
it.each([
|
|
117
|
+
{ value: "null", envelope: null },
|
|
118
|
+
{ value: "42", envelope: 42 },
|
|
119
|
+
{ value: '"provider text"', envelope: "provider text" },
|
|
120
|
+
{ value: "false", envelope: false },
|
|
121
|
+
])("refuses valid JSON without an envelope record: $value", async ({ value, envelope }) => {
|
|
122
|
+
const policy = await build()
|
|
123
|
+
|
|
124
|
+
expect(policy.readEnvelopeText(value)).toEqual({
|
|
125
|
+
eventName: "",
|
|
126
|
+
correlationId: null,
|
|
127
|
+
envelope,
|
|
128
|
+
cause: null,
|
|
88
129
|
})
|
|
89
130
|
})
|
|
131
|
+
|
|
132
|
+
it.each([JSON.stringify([]), JSON.stringify({ eventName: 1 })])(
|
|
133
|
+
"keeps a parsed JSON value without a text event name unnamed",
|
|
134
|
+
async (value) => {
|
|
135
|
+
const policy = await build()
|
|
136
|
+
|
|
137
|
+
expect(policy.readEnvelopeText(value)).toMatchObject({ eventName: "", cause: null })
|
|
138
|
+
},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
it("returns the parse failure for text that is not JSON", async () => {
|
|
142
|
+
const policy = await build()
|
|
143
|
+
|
|
144
|
+
const result = policy.readEnvelopeText("not json")
|
|
145
|
+
|
|
146
|
+
expect(result.eventName).toBe("")
|
|
147
|
+
expect(result.envelope).toBeNull()
|
|
148
|
+
expect(result.cause).toBeInstanceOf(SyntaxError)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe("event envelope policies readEventOf", () => {
|
|
153
|
+
it("builds an event after the envelope and its payload are proven", async () => {
|
|
154
|
+
const policy = await build()
|
|
155
|
+
|
|
156
|
+
expect(
|
|
157
|
+
policy.readEventOf({ eventId: "event-1", payload: { note: "hello" } }, isPingPayload, buildPing),
|
|
158
|
+
).toEqual(new PingEvent("event-1", { note: "hello" }))
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it("refuses a value that is not an envelope without checking its payload", async () => {
|
|
162
|
+
const policy = await build()
|
|
163
|
+
|
|
164
|
+
let checked = false
|
|
165
|
+
const guard = (payload: Record<string, unknown>): payload is PingPayload & Record<string, unknown> => {
|
|
166
|
+
checked = true
|
|
167
|
+
return isPingPayload(payload)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
expect(policy.readEventOf(null, guard, buildPing)).toBeNull()
|
|
171
|
+
expect(checked).toBe(false)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it("refuses an envelope whose payload does not have the event fields", async () => {
|
|
175
|
+
const policy = await build()
|
|
176
|
+
|
|
177
|
+
expect(policy.readEventOf({ eventId: "event-1", payload: {} }, isPingPayload, buildPing)).toBeNull()
|
|
178
|
+
})
|
|
90
179
|
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BaseEvent, ParsedEvent } from "./event-bus.contracts"
|
|
2
2
|
import { isRecord } from "@modules/platform/primitives"
|
|
3
|
+
import { isCorrelationId } from "@modules/platform/logging"
|
|
3
4
|
|
|
4
5
|
/** What a received envelope carries once its shape is checked: the event id and the payload record. */
|
|
5
6
|
export interface ReadEnvelope {
|
|
@@ -14,6 +15,8 @@ export type ReadEnvelopeResult = ReadEnvelope | null
|
|
|
14
15
|
|
|
15
16
|
/** What reading the text of a message answers: the event name it carries (empty when none), the parsed value, and the failure when the text is not JSON. */
|
|
16
17
|
export interface ReadEnvelopeText {
|
|
18
|
+
/** The explicit request context, or null when metadata is missing or malformed. */
|
|
19
|
+
readonly correlationId: string | null
|
|
17
20
|
/** The event name of the envelope, or an empty text when the value is not an envelope. */
|
|
18
21
|
readonly eventName: string
|
|
19
22
|
/** The parsed value, or null when the text is not JSON. */
|
|
@@ -28,14 +31,34 @@ export const readEnvelope = (envelope: unknown): ReadEnvelopeResult =>
|
|
|
28
31
|
? { eventId: envelope.eventId, payload: envelope.payload }
|
|
29
32
|
: null
|
|
30
33
|
|
|
34
|
+
/** Reads current metadata, or promotes an unambiguous legacy envelope rooted at its stable event id. */
|
|
35
|
+
const readCorrelation = (envelope: unknown): Pick<ReadEnvelopeText, "correlationId" | "envelope"> => {
|
|
36
|
+
if (!isRecord(envelope)) return { correlationId: null, envelope }
|
|
37
|
+
const metadata = envelope.metadata
|
|
38
|
+
if (isRecord(metadata) && isCorrelationId(metadata.correlationId))
|
|
39
|
+
return { correlationId: metadata.correlationId, envelope }
|
|
40
|
+
if (
|
|
41
|
+
"metadata" in envelope ||
|
|
42
|
+
!isCorrelationId(envelope.eventId) ||
|
|
43
|
+
typeof envelope.eventName !== "string" ||
|
|
44
|
+
envelope.eventName.length === 0 ||
|
|
45
|
+
!isRecord(envelope.payload) ||
|
|
46
|
+
Array.isArray(envelope.payload)
|
|
47
|
+
) return { correlationId: null, envelope }
|
|
48
|
+
return {
|
|
49
|
+
correlationId: envelope.eventId,
|
|
50
|
+
envelope: { ...envelope, metadata: { correlationId: envelope.eventId } },
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
31
54
|
/** Parses the text of a message and names the event it carries; a text that is not JSON has no name and carries the cause. */
|
|
32
55
|
export const readEnvelopeText = (value: string): ReadEnvelopeText => {
|
|
33
56
|
try {
|
|
34
57
|
const envelope: unknown = JSON.parse(value)
|
|
35
58
|
const eventName = isRecord(envelope) && typeof envelope.eventName === "string" ? envelope.eventName : ""
|
|
36
|
-
return { eventName, envelope, cause: null }
|
|
59
|
+
return { eventName, ...readCorrelation(envelope), cause: null }
|
|
37
60
|
} catch (cause) {
|
|
38
|
-
return { eventName: "", envelope: null, cause }
|
|
61
|
+
return { eventName: "", envelope: null, correlationId: null, cause }
|
|
39
62
|
}
|
|
40
63
|
}
|
|
41
64
|
|