@starci/hfs 4.7.32 → 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.
Files changed (473) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +13 -6
  3. package/emit/contracts.mjs +4 -3
  4. package/emit/form-bounds-reader.ts.partial +58 -0
  5. package/emit/form-bounds.mjs +155 -0
  6. package/emit/graphql-input-bounds.mjs +97 -0
  7. package/emit/input-bounds.mjs +44 -0
  8. package/emit/operation-schemas.mjs +137 -0
  9. package/emit/operations-worker.mjs +2 -2
  10. package/emit/operations.mjs +52 -16
  11. package/emit/schema-worker.mjs +17 -4
  12. package/emit/type-schema.mjs +17 -5
  13. package/package.json +9 -2
  14. package/release/gate.mjs +18 -23
  15. package/release/index.mjs +1 -1
  16. package/release/push.mjs +4 -5
  17. package/runtime/engine/admission.mjs +331 -0
  18. package/runtime/engine/canonical-json.mjs +14 -0
  19. package/runtime/engine/db/blob.mjs +380 -0
  20. package/runtime/engine/db/event-compact.mjs +73 -0
  21. package/runtime/engine/db/ledger-open.mjs +38 -0
  22. package/runtime/engine/db/ledger-paths.mjs +87 -0
  23. package/runtime/engine/db/ledger.mjs +1068 -0
  24. package/runtime/engine/db/machine-connection.mjs +150 -0
  25. package/runtime/engine/db/machine-open.mjs +60 -0
  26. package/runtime/engine/db/machine-schema.mjs +105 -0
  27. package/runtime/engine/db/machine.mjs +1317 -0
  28. package/runtime/engine/db/provider-reservations.mjs +127 -0
  29. package/runtime/engine/db/provision-authority.mjs +76 -0
  30. package/runtime/engine/db/provision-lock.mjs +29 -0
  31. package/runtime/engine/db/provision-operations.mjs +146 -0
  32. package/runtime/engine/db/ref-value.mjs +67 -0
  33. package/runtime/engine/db/schema/machine.sql +920 -0
  34. package/runtime/engine/db/schema/runtime.sql +1113 -0
  35. package/runtime/engine/refuse.mjs +11 -0
  36. package/runtime/knowledge/hfs/canon-pins.yaml +65 -9
  37. package/runtime/knowledge/hfs/design-truths.yaml +149 -0
  38. package/runtime/knowledge/hfs/fe-stack.yaml +23 -5
  39. package/runtime/knowledge/hfs/rules.yaml +269 -48
  40. package/runtime/knowledge/hfs/slots.yaml +113 -53
  41. package/runtime/knowledge/patterns/be/api.yaml +46 -20
  42. package/runtime/knowledge/patterns/be/event-bus.yaml +19 -12
  43. package/runtime/knowledge/patterns/be/idempotency.yaml +265 -0
  44. package/runtime/knowledge/patterns/be/jobs.yaml +17 -12
  45. package/runtime/knowledge/patterns/be/projections.yaml +2 -2
  46. package/runtime/knowledge/patterns/be/queues.yaml +57 -22
  47. package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
  48. package/runtime/knowledge/patterns/be/realtime.yaml +326 -27
  49. package/runtime/knowledge/patterns/be/saga.yaml +2 -2
  50. package/runtime/knowledge/sonar-gate.yaml +1 -1
  51. package/runtime/modules/kernel/command-policy.yaml +249 -0
  52. package/runtime/modules/kernel/failure-codes.yaml +211 -52
  53. package/runtime/modules/kernel/hook-input.yaml +6 -0
  54. package/runtime/modules/kernel/protected-zone.yaml +81 -0
  55. package/runtime/modules/kernel/removed-vocabulary.yaml +62 -0
  56. package/runtime/modules/kernel/roles.yaml +405 -0
  57. package/runtime/modules/models/registry.yaml +3 -1
  58. package/runtime/modules/models/runtimes.yaml +2 -0
  59. package/runtime/modules/schemas/application-stacks.schema.yaml +10 -2
  60. package/runtime/packages/cli/src/catalog.generated.mjs +46 -59
  61. package/runtime/packages/grammar/scripts/build-stamp.mjs +98 -0
  62. package/runtime/scripts/agent/codex-app-server.mjs +79 -0
  63. package/runtime/scripts/agent/codex-mirror-source.mjs +34 -0
  64. package/runtime/scripts/agent/host-agents.mjs +80 -0
  65. package/runtime/scripts/agent/launch-trust-policy.mjs +53 -0
  66. package/runtime/scripts/agent/trust-launch.mjs +191 -0
  67. package/runtime/scripts/agent/trust.mjs +523 -0
  68. package/runtime/scripts/api/fs/rename-over.mjs +21 -0
  69. package/runtime/scripts/api/fs/temp-path.mjs +8 -0
  70. package/runtime/scripts/api/fs/write-json-file.mjs +10 -0
  71. package/runtime/scripts/api/git/diff-names.mjs +8 -0
  72. package/runtime/scripts/api/git/head-time.mjs +10 -0
  73. package/runtime/scripts/api/git/index-lock.mjs +78 -0
  74. package/runtime/scripts/api/git/is-ancestor.mjs +5 -0
  75. package/runtime/scripts/api/git/log.mjs +5 -0
  76. package/runtime/scripts/api/git/merge-base-query.mjs +5 -0
  77. package/runtime/scripts/api/git/merge-tree.mjs +5 -0
  78. package/runtime/scripts/api/git/rev-list.mjs +5 -0
  79. package/runtime/scripts/api/git/rev-parse.mjs +13 -0
  80. package/runtime/scripts/api/git/status-query.mjs +5 -0
  81. package/runtime/scripts/api/git/worktree-list-query.mjs +5 -0
  82. package/runtime/scripts/api/node/spawn-node.mjs +6 -0
  83. package/runtime/scripts/api/npm/dist-tag.mjs +8 -0
  84. package/runtime/scripts/api/npm/lib.mjs +32 -0
  85. package/runtime/scripts/api/npm/pack-dry-run.mjs +9 -0
  86. package/runtime/scripts/api/npm/run-npm.mjs +6 -0
  87. package/runtime/scripts/api/npm/view.mjs +30 -0
  88. package/runtime/scripts/api/npm/whoami.mjs +8 -0
  89. package/runtime/scripts/api/process/hide-child-windows.mjs +31 -0
  90. package/runtime/scripts/api/process/kill-tree.mjs +22 -0
  91. package/runtime/scripts/api/process/lib.mjs +125 -0
  92. package/runtime/scripts/api/process/probe-guard-command.mjs +74 -0
  93. package/runtime/scripts/api/process/process-list.mjs +28 -0
  94. package/runtime/scripts/api/process/system-tool.mjs +37 -0
  95. package/runtime/scripts/gates/canon-digest.mjs +81 -0
  96. package/runtime/scripts/gates/canon-pins.mjs +16 -0
  97. package/runtime/scripts/gates/commit-proof.mjs +53 -0
  98. package/runtime/scripts/gates/gate-input.mjs +98 -0
  99. package/runtime/scripts/gates/gate.mjs +669 -0
  100. package/runtime/scripts/gates/grammar-dist.mjs +206 -0
  101. package/runtime/scripts/gates/runtime-package-selection.mjs +27 -0
  102. package/runtime/scripts/gates/test-world-run.mjs +254 -0
  103. package/runtime/scripts/gates/type-impact.mjs +277 -0
  104. package/runtime/scripts/gates/type-program.mjs +113 -0
  105. package/runtime/scripts/guards/assigned-command.mjs +12 -0
  106. package/runtime/scripts/guards/call-rights.mjs +47 -0
  107. package/runtime/scripts/guards/command-guard.mjs +468 -0
  108. package/runtime/scripts/guards/command-policy.mjs +342 -0
  109. package/runtime/scripts/guards/critic-reach.mjs +82 -0
  110. package/runtime/scripts/guards/deps-guard.mjs +150 -0
  111. package/runtime/scripts/guards/env-dump-verdict.mjs +89 -0
  112. package/runtime/scripts/guards/git-pathspec.mjs +225 -0
  113. package/runtime/scripts/guards/git-policy.mjs +355 -0
  114. package/runtime/scripts/guards/guards-root.mjs +27 -0
  115. package/runtime/scripts/guards/hook-install.mjs +343 -0
  116. package/runtime/scripts/guards/hook-io.mjs +76 -0
  117. package/runtime/scripts/guards/hook-verdict.mjs +12 -0
  118. package/runtime/scripts/guards/install-verdict.mjs +41 -0
  119. package/runtime/scripts/guards/kernel-seat.mjs +63 -0
  120. package/runtime/scripts/guards/launch-verdict.mjs +74 -0
  121. package/runtime/scripts/guards/op-starci.mjs +16 -0
  122. package/runtime/scripts/guards/op-write-scope.mjs +27 -0
  123. package/runtime/scripts/guards/orca-self-lifecycle.mjs +39 -0
  124. package/runtime/scripts/guards/process-kill-verdict.mjs +106 -0
  125. package/runtime/scripts/guards/protected-zone.mjs +56 -0
  126. package/runtime/scripts/guards/refusals.mjs +27 -0
  127. package/runtime/scripts/guards/release-definition.mjs +185 -0
  128. package/runtime/scripts/guards/release-push-gate.mjs +72 -0
  129. package/runtime/scripts/guards/release-record.mjs +58 -0
  130. package/runtime/scripts/guards/release-suite-mode.mjs +15 -0
  131. package/runtime/scripts/guards/rights.mjs +268 -0
  132. package/runtime/scripts/guards/seat-tools.mjs +47 -0
  133. package/runtime/scripts/guards/shell-commands.mjs +328 -0
  134. package/runtime/scripts/guards/supervisor-seat.mjs +80 -0
  135. package/runtime/scripts/guards/verdict-process.mjs +81 -0
  136. package/runtime/scripts/hfs/architecture/dead-exports.mjs +10 -10
  137. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +1 -1
  138. package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +53 -0
  139. package/runtime/scripts/hfs/architecture/reachability.mjs +1 -1
  140. package/runtime/scripts/hfs/architecture/sql-owner.mjs +17 -6
  141. package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +105 -0
  142. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +38 -52
  143. package/runtime/scripts/hfs/architecture/supabase-keys.mjs +261 -0
  144. package/runtime/scripts/hfs/architecture/transport-owner.mjs +49 -3
  145. package/runtime/scripts/hfs/architecture/typescript.mjs +1 -1
  146. package/runtime/scripts/hfs/check.mjs +6 -4
  147. package/runtime/scripts/hfs/coverage-scope.mjs +28 -1
  148. package/runtime/scripts/hfs/db-restamp-verb.mjs +54 -14
  149. package/runtime/scripts/hfs/declaration-shape.mjs +4 -4
  150. package/runtime/scripts/hfs/declaration-slots.mjs +14 -3
  151. package/runtime/scripts/hfs/manifest-shape.mjs +10 -6
  152. package/runtime/scripts/hfs/rule-params-shape.mjs +18 -6
  153. package/runtime/scripts/hfs/rules/a15-findings.mjs +4 -1
  154. package/runtime/scripts/hfs/rules/bypass-markers.mjs +22 -7
  155. package/runtime/scripts/hfs/rules/database-migrations.mjs +69 -14
  156. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +9 -7
  157. package/runtime/scripts/hfs/rules/database-queues.mjs +68 -18
  158. package/runtime/scripts/hfs/rules/edition.mjs +56 -12
  159. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -10
  160. package/runtime/scripts/hfs/rules/fe-stack.mjs +12 -7
  161. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +1 -1
  162. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +75 -9
  163. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +1 -1
  164. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +15 -4
  165. package/runtime/scripts/hfs/rules/text-integrity.mjs +73 -0
  166. package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +74 -0
  167. package/runtime/scripts/hfs/runtime-rules/release-notes.mjs +44 -0
  168. package/runtime/scripts/hfs/sealed-members.mjs +3 -3
  169. package/runtime/scripts/hfs/slot-side-problems.mjs +3 -3
  170. package/runtime/scripts/hfs/slots.mjs +2 -1
  171. package/runtime/scripts/hfs/stack-deploy-config.mjs +22 -5
  172. package/runtime/scripts/hfs/stacks-gate.mjs +28 -7
  173. package/runtime/scripts/hfs/view.mjs +2 -1
  174. package/runtime/scripts/lib/app-data.mjs +11 -0
  175. package/runtime/scripts/lib/bypass-markers.mjs +272 -75
  176. package/runtime/scripts/lib/check-format.mjs +7 -0
  177. package/runtime/scripts/lib/cli-arg.mjs +120 -0
  178. package/runtime/scripts/lib/css-rules.mjs +220 -0
  179. package/runtime/scripts/lib/dom-tree.mjs +4 -0
  180. package/runtime/scripts/lib/escape.mjs +16 -0
  181. package/runtime/scripts/lib/event-items.mjs +22 -0
  182. package/runtime/scripts/lib/failure-code-template-errors.mjs +161 -0
  183. package/runtime/scripts/lib/git-dir.mjs +120 -0
  184. package/runtime/scripts/lib/grammar-vendor-parity.mjs +377 -0
  185. package/runtime/scripts/lib/guard-command.mjs +24 -0
  186. package/runtime/scripts/lib/hook-limits.mjs +5 -0
  187. package/runtime/scripts/lib/interrupt-cleanup.mjs +38 -0
  188. package/runtime/scripts/lib/js-lex.mjs +200 -0
  189. package/runtime/scripts/lib/json-formats.mjs +0 -1
  190. package/runtime/scripts/lib/language.mjs +94 -1
  191. package/runtime/scripts/lib/loosening-budgets.mjs +44 -0
  192. package/runtime/scripts/lib/loosening-config.mjs +361 -0
  193. package/runtime/scripts/lib/loosening-state.mjs +223 -0
  194. package/runtime/scripts/lib/menu-parts.mjs +11 -0
  195. package/runtime/scripts/lib/normalize.mjs +25 -0
  196. package/runtime/scripts/lib/op-gate.mjs +15 -0
  197. package/runtime/scripts/lib/redact.mjs +208 -0
  198. package/runtime/scripts/lib/runtime-location.mjs +21 -0
  199. package/runtime/scripts/lib/runtime-package-inventory.mjs +21 -0
  200. package/runtime/scripts/lib/runtime-package-manifests.mjs +84 -0
  201. package/runtime/scripts/lib/runtime-publication-tags.mjs +99 -0
  202. package/runtime/scripts/lib/seat-env.mjs +43 -0
  203. package/runtime/scripts/lib/secret-patterns.mjs +58 -9
  204. package/runtime/scripts/lib/shell-source.mjs +4 -0
  205. package/runtime/scripts/lib/source-phrases.mjs +34 -0
  206. package/runtime/scripts/lib/sqlite.mjs +21 -0
  207. package/runtime/scripts/lib/swarm-stack.mjs +2 -1
  208. package/runtime/scripts/lib/tar-files.mjs +39 -0
  209. package/runtime/scripts/lib/ts-analysis.mjs +10 -0
  210. package/runtime/scripts/lib/worktree-exclude.mjs +35 -0
  211. package/runtime/scripts/lib/worktree-record.mjs +14 -0
  212. package/runtime/scripts/machine/artifact-hold.mjs +52 -0
  213. package/runtime/scripts/machine/ask-receipts.mjs +113 -0
  214. package/runtime/scripts/machine/evidence-store.mjs +223 -0
  215. package/runtime/scripts/machine/home.mjs +216 -0
  216. package/runtime/scripts/machine/host-lock.mjs +184 -0
  217. package/runtime/scripts/machine/lock-recovery.mjs +55 -0
  218. package/runtime/scripts/machine/roles-contract.mjs +90 -0
  219. package/runtime/scripts/machine/runtime-change.mjs +20 -0
  220. package/runtime/scripts/machine/workflow-tree.mjs +122 -0
  221. package/runtime/scripts/reconciler/checklist-items.mjs +7 -0
  222. package/runtime/scripts/reconciler/guard-row.mjs +25 -0
  223. package/runtime/scripts/work/asset-slot.mjs +284 -0
  224. package/runtime/scripts/work/brand/brand-colour.mjs +125 -0
  225. package/runtime/scripts/work/brand/brand-tokens.mjs +167 -0
  226. package/runtime/scripts/work/brand/brand-value-source.mjs +106 -0
  227. package/runtime/scripts/work/brand/brand.mjs +892 -0
  228. package/runtime/scripts/work/draw/draw-dna-anatomy.mjs +60 -0
  229. package/runtime/scripts/work/draw/draw-dna-findings.mjs +256 -0
  230. package/runtime/scripts/work/draw/draw-dna-selector.mjs +38 -0
  231. package/runtime/scripts/work/draw/draw-dna.mjs +483 -0
  232. package/runtime/scripts/work/draw/draw-layer.mjs +305 -0
  233. package/runtime/scripts/work/draw/draw-rationale-page.mjs +237 -0
  234. package/runtime/scripts/work/draw/draw-rationale-ref.mjs +14 -0
  235. package/runtime/scripts/work/draw/draw-rationale.mjs +353 -0
  236. package/runtime/scripts/work/draw/draw-source.mjs +412 -0
  237. package/runtime/scripts/work/draw/draw-taste.mjs +278 -0
  238. package/runtime/scripts/work/draw/draw-typecheck.mjs +72 -0
  239. package/runtime/scripts/work/draw/rationale-findings.mjs +205 -0
  240. package/runtime/scripts/work/draw-grammar.mjs +175 -0
  241. package/runtime/scripts/work/draw-render-page.mjs +188 -0
  242. package/runtime/scripts/work/draw-render.mjs +626 -0
  243. package/runtime/scripts/work/grammar-proposal.mjs +279 -0
  244. package/runtime/scripts/work/png.mjs +237 -0
  245. package/runtime/scripts/work/render-tools.mjs +64 -0
  246. package/runtime/scripts/work/work-io.mjs +119 -0
  247. package/scaffold/add-inbox-lite.mjs +2 -2
  248. package/scaffold/add-plan.mjs +40 -13
  249. package/scaffold/add-queue-lite.mjs +372 -0
  250. package/scaffold/add.mjs +5 -2
  251. package/scaffold/app.mjs +1 -2
  252. package/scaffold/edition-gate.mjs +11 -7
  253. package/scaffold/lite-exports.mjs +10 -0
  254. package/scaffold/pattern-file-slots.mjs +33 -0
  255. package/scaffold/test-world.mjs +1 -1
  256. package/scaffold/yaml-block-edit.mjs +117 -0
  257. package/src/main.mjs +5 -4
  258. package/stack/deploy.mjs +7 -3
  259. package/stack/post-deploy.mjs +48 -0
  260. package/sync/index.mjs +10 -0
  261. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +1 -1
  262. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +1 -1
  263. package/templates/app/codegen-graphql.mjs.partial +59 -12
  264. package/templates/app/hooks/husky/pre-commit +2 -2
  265. package/templates/app/hooks/husky/pre-push +10 -3
  266. package/templates/app/package-scripts/redis-dev.json +3 -0
  267. package/templates/app/package-scripts-lite/package.json +1 -0
  268. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +36 -8
  269. package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
  270. package/templates/app/skeleton/.starcistacks/production/README.md +10 -9
  271. package/templates/app/skeleton/.starcistacks/production/infra/swarm/postgres-init.sh +21 -0
  272. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +1 -1
  273. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +1 -0
  274. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +47 -5
  275. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +5 -3
  276. package/templates/app/skeleton/README.md +2 -0
  277. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +5 -0
  278. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
  279. package/templates/app/skeleton-lite/.starcistacks/production/README.md +1 -1
  280. package/templates/app/skeleton-lite/README.md +1 -1
  281. package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +18 -0
  282. package/templates/be/inbox/platform/event-bus/errors/event-bus.error.ts.tpl +16 -0
  283. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +10 -0
  284. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +1 -1
  285. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +15 -3
  286. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +11 -3
  287. package/templates/be/inbox/platform/event-bus/index.ts.tpl +2 -0
  288. package/templates/be/inbox/platform/event-bus/messages/event-bus.messages.ts.tpl +7 -0
  289. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +2 -2
  290. package/templates/be/inbox/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
  291. package/templates/be/inbox/platform/idempotency/index.ts.tpl +2 -0
  292. package/templates/be/inbox/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
  293. package/templates/be/inbox/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
  294. package/templates/be/inbox/platform/inbox/client-operation.policy.ts.tpl +17 -0
  295. package/templates/be/inbox/platform/inbox/errors/inbox.error.ts.tpl +22 -0
  296. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +21 -0
  297. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +334 -11
  298. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +55 -4
  299. package/templates/be/inbox/platform/inbox/index.ts.tpl +2 -0
  300. package/templates/be/inbox/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
  301. package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
  302. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +26 -0
  303. package/templates/be/inbox/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
  304. package/templates/be/inbox/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
  305. package/templates/be/inbox/platform/migration.sql.tpl +1 -0
  306. package/templates/be/inbox/service.spec.ts.tpl +4 -4
  307. package/templates/be/inbox/service.ts.tpl +3 -1
  308. package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -2
  309. package/templates/be/inbox/webhook-door.ts.tpl +3 -2
  310. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +3 -0
  311. package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +10 -0
  312. package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +2 -2
  313. package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +16 -3
  314. package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +11 -3
  315. package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +139 -50
  316. package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +25 -2
  317. package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +139 -78
  318. package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +10 -6
  319. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +262 -159
  320. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +32 -5
  321. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +2 -0
  322. package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +5 -1
  323. package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +2 -0
  324. package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -4
  325. package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +1 -0
  326. package/templates/be/patterns/idempotency/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
  327. package/templates/be/patterns/idempotency/platform/idempotency/index.ts.tpl +2 -0
  328. package/templates/be/patterns/idempotency/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
  329. package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
  330. package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.ts.tpl +17 -0
  331. package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.spec.ts.tpl +66 -0
  332. package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.ts.tpl +24 -0
  333. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.spec.ts.tpl +60 -0
  334. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.ts.tpl +12 -0
  335. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.spec.ts.tpl +49 -0
  336. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.ts.tpl +12 -0
  337. package/templates/be/patterns/idempotency/platform/inbox/effectful.decorators.ts.tpl +7 -0
  338. package/templates/be/patterns/idempotency/platform/inbox/errors/inbox.error.ts.tpl +22 -0
  339. package/templates/be/patterns/idempotency/platform/inbox/inbox.decorators.ts.tpl +17 -0
  340. package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +7 -0
  341. package/templates/be/patterns/idempotency/platform/inbox/inbox.module.ts.tpl +28 -0
  342. package/templates/be/patterns/idempotency/platform/inbox/inbox.options.ts.tpl +7 -0
  343. package/templates/be/patterns/idempotency/platform/inbox/inbox.port.ts.tpl +44 -0
  344. package/templates/be/patterns/idempotency/platform/inbox/inbox.service.spec.ts.tpl +388 -0
  345. package/templates/be/patterns/idempotency/platform/inbox/inbox.service.ts.tpl +77 -0
  346. package/templates/be/patterns/idempotency/platform/inbox/index.ts.tpl +10 -0
  347. package/templates/be/patterns/idempotency/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
  348. package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +9 -0
  349. package/templates/be/patterns/idempotency/platform/inbox/persistence/entities/inbox-claim.entity.ts.tpl +25 -0
  350. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
  351. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +39 -0
  352. package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts.tpl +21 -0
  353. package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts.tpl +21 -0
  354. package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
  355. package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
  356. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +1 -0
  357. package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +4 -3
  358. package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +2 -0
  359. package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +20 -2
  360. package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +1 -0
  361. package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +4 -0
  362. package/templates/be/patterns/jobs/platform-lite/jobs.index.ts.tpl +10 -0
  363. package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +44 -0
  364. package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +33 -0
  365. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +203 -18
  366. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +78 -22
  367. package/templates/be/patterns/queues/platform/errors/queue.error.ts.tpl +19 -0
  368. package/templates/be/patterns/queues/platform/messages/queue.messages.ts.tpl +13 -0
  369. package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +1 -0
  370. package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +4 -0
  371. package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +2 -0
  372. package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -4
  373. package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +14 -2
  374. package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +5 -2
  375. package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +72 -74
  376. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +1 -1
  377. package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +23 -8
  378. package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +29 -8
  379. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +7 -3
  380. package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +5 -0
  381. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +2 -0
  382. package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +4 -0
  383. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +2 -1
  384. package/templates/be/patterns/queues/platform/queue.port.ts.tpl +1 -1
  385. package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.spec.ts.tpl +417 -0
  386. package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.ts.tpl +172 -0
  387. package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +12 -0
  388. package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +32 -0
  389. package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +148 -0
  390. package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +51 -0
  391. package/templates/be/patterns/queues/platform-lite/queue-transport.port.ts.tpl +163 -0
  392. package/templates/be/patterns/queues/platform-lite/queue.config.ts.tpl +17 -0
  393. package/templates/be/patterns/queues/platform-lite/queue.error.ts.tpl +25 -0
  394. package/templates/be/patterns/queues/platform-lite/queue.index.ts.tpl +8 -0
  395. package/templates/be/patterns/queues/platform-lite/queue.messages.ts.tpl +21 -0
  396. package/templates/be/patterns/queues/platform-lite/queue.module.ts.tpl +74 -0
  397. package/templates/be/patterns/queues/platform-lite/queue.options.ts.tpl +38 -0
  398. package/templates/be/patterns/queues/queue.queue.ts.tpl +2 -2
  399. package/templates/be/patterns/reactors/application.contracts.ts.tpl +2 -0
  400. package/templates/be/patterns/reactors/consumer.ts.tpl +5 -1
  401. package/templates/be/platform/graphql/graphql.contracts.ts +2 -1
  402. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +1 -1
  403. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +6 -1
  404. package/templates/be/skeleton/apps/__app__/src/main.ts +7 -4
  405. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +4 -0
  406. package/templates/be/skeleton/apps/cli/src/main.ts +7 -4
  407. package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -1
  408. package/templates/be/skeleton/src/modules/platform/http/errors/http.error.ts +22 -0
  409. package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.spec.ts +235 -0
  410. package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +85 -0
  411. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.spec.ts +92 -0
  412. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.ts +52 -0
  413. package/templates/be/skeleton/src/modules/platform/http/http.decorators.ts +9 -0
  414. package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +7 -0
  415. package/templates/be/skeleton/src/modules/platform/http/http.module.ts +19 -0
  416. package/templates/be/skeleton/src/modules/platform/http/http.options.ts +2 -0
  417. package/templates/be/skeleton/src/modules/platform/http/http.port.ts +40 -0
  418. package/templates/be/skeleton/src/modules/platform/http/index.ts +7 -0
  419. package/templates/be/skeleton/src/modules/platform/http/messages/http.messages.ts +15 -0
  420. package/templates/be/skeleton/src/modules/platform/ids/ids.decorators.ts +9 -0
  421. package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +7 -0
  422. package/templates/be/skeleton/src/modules/platform/ids/ids.module.ts +19 -0
  423. package/templates/be/skeleton/src/modules/platform/ids/ids.options.ts +2 -0
  424. package/templates/be/skeleton/src/modules/platform/ids/ids.port.ts +5 -0
  425. package/templates/be/skeleton/src/modules/platform/ids/index.ts +4 -0
  426. package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.spec.ts +15 -0
  427. package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.ts +10 -0
  428. package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.spec.ts +59 -0
  429. package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.ts +21 -0
  430. package/templates/be/skeleton/src/modules/platform/logging/correlation.contracts.ts +16 -0
  431. package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.spec.ts +150 -0
  432. package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.ts +42 -0
  433. package/templates/be/skeleton/src/modules/platform/logging/correlation.port.ts +15 -0
  434. package/templates/be/skeleton/src/modules/platform/logging/correlation.service.spec.ts +23 -0
  435. package/templates/be/skeleton/src/modules/platform/logging/correlation.service.ts +28 -0
  436. package/templates/be/skeleton/src/modules/platform/logging/index.ts +14 -1
  437. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +39 -6
  438. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +15 -3
  439. package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +17 -2
  440. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +13 -1
  441. package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +13 -3
  442. package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.spec.ts +69 -0
  443. package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.ts +48 -0
  444. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  445. package/templates/be/skeleton/src/modules/platform/primitives/record.policy.spec.ts +12 -0
  446. package/templates/be/skeleton/src/modules/platform/primitives/record.policy.ts +3 -0
  447. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +6 -1
  448. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +4 -0
  449. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  450. package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +14 -1
  451. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +12 -0
  452. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  453. package/templates/be/test-world/world/test-apps.options.ts.tpl +2 -0
  454. package/templates/be/test-world/world/test-world.config.ts.tpl +1 -0
  455. package/upgrade/check.mjs +12 -135
  456. package/upgrade/file-state.mjs +1 -16
  457. package/upgrade/index.mjs +34 -361
  458. package/runtime/scripts/lib/check-code-loosening.mjs +0 -128
  459. package/runtime/scripts/lib/gate-loosening.mjs +0 -292
  460. package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +0 -89
  461. package/templates/app/upgrade-full/.starcistacks/dev/README.md +0 -16
  462. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +0 -13
  463. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +0 -3
  464. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +0 -12
  465. package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +0 -7
  466. package/templates/be/skeleton-lite/apps/api/src/main.ts +0 -26
  467. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +0 -12
  468. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +0 -7
  469. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +0 -5
  470. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +0 -9
  471. package/templates/be/upgrade-full/apps/cli/src/main.ts +0 -29
  472. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +0 -7
  473. 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>>({ rawBody: RAW_BODY })
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 the transaction `tx` carries; the relay hands it to the broker once the transaction commits. */
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 the transaction `tx`; the event id is the partition key, so the events of one aggregate keep their order. */
46
- async publish(event: BaseEvent, tx: EntityManager): Promise<void> {
47
- const envelope: EventEnvelope = { eventId: event.eventId, eventName: event.eventName, payload: event.payload }
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
- describe("event envelope policies", () => {
24
- describe("readEnvelope", () => {
25
- it("reads the event id and payload of an envelope", () => {
26
- expect(readEnvelope({ eventId: "event-1", payload: { note: "hello" }, ignored: true })).toEqual({
27
- eventId: "event-1",
28
- payload: { note: "hello" },
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
- it.each([
33
- [null, "a non-record"],
34
- [{ eventId: 1, payload: {} }, "a non-text event id"],
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
- describe("readEnvelopeText", () => {
42
- it("parses an envelope and names its event", () => {
43
- const envelope = { eventId: "event-1", eventName: "probe.ping", payload: { note: "hello" } }
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
- expect(readEnvelopeText(JSON.stringify(envelope))).toEqual({
46
- eventName: "probe.ping",
47
- envelope,
48
- cause: null,
49
- })
50
- })
50
+ expect(policy.readEnvelope(envelope)).toBeNull()
51
+ })
52
+ })
51
53
 
52
- it.each([JSON.stringify([]), JSON.stringify({ eventName: 1 })])(
53
- "keeps a parsed JSON value without a text event name unnamed",
54
- (value) => {
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
- it("returns the parse failure for text that is not JSON", () => {
60
- const result = readEnvelopeText("not json")
58
+ const envelope = {
59
+ metadata: { correlationId: "request-1" },
60
+ eventId: "event-1",
61
+ eventName: "probe.ping",
62
+ payload: { note: "hello" },
63
+ }
61
64
 
62
- expect(result.eventName).toBe("")
63
- expect(result.envelope).toBeNull()
64
- expect(result.cause).toBeInstanceOf(SyntaxError)
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
- describe("readEventOf", () => {
69
- it("builds an event after the envelope and its payload are proven", () => {
70
- expect(readEventOf({ eventId: "event-1", payload: { note: "hello" } }, isPingPayload, buildPing)).toEqual(
71
- new PingEvent("event-1", { note: "hello" }),
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
- it("refuses a value that is not an envelope without checking its payload", () => {
76
- let checked = false
77
- const guard = (payload: Record<string, unknown>): payload is PingPayload & Record<string, unknown> => {
78
- checked = true
79
- return isPingPayload(payload)
80
- }
83
+ expect(policy.readEnvelopeText(JSON.stringify(envelope)).correlationId).toBeNull()
84
+ })
85
+ })
81
86
 
82
- expect(readEventOf(null, guard, buildPing)).toBeNull()
83
- expect(checked).toBe(false)
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
- it("refuses an envelope whose payload does not have the event fields", () => {
87
- expect(readEventOf({ eventId: "event-1", payload: {} }, isPingPayload, buildPing)).toBeNull()
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