@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
@@ -1,11 +1,11 @@
1
1
  import { sql } from "@modules/platform/database"
2
2
 
3
- /** Writes one event in the outbox ($1 event id, $2 event name, $3 topic, $4 key, $5 envelope, $6 instant). */
4
- export const INSERT_OUTBOX_ROW = sql`INSERT INTO event_outbox (event_id, event_name, topic, message_key, envelope, created_at)
5
- VALUES ($1, $2, $3, $4, $5, $6)`
3
+ /** Writes one event in the outbox ($1 event id, $2 event name, $3 topic, $4 key, $5 envelope, $6 instant, $7 correlation id). */
4
+ export const INSERT_OUTBOX_ROW = sql`INSERT INTO event_outbox (event_id, event_name, topic, message_key, envelope, created_at, correlation_id)
5
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`
6
6
 
7
7
  /** Locks and reads the oldest rows that wait ($1 batch size); a second relay skips the rows this one holds. */
8
- export const SELECT_WAITING_ROWS = sql`SELECT id, topic, message_key, envelope FROM event_outbox
8
+ export const SELECT_WAITING_ROWS = sql`SELECT id, topic, message_key, envelope, correlation_id FROM event_outbox
9
9
  WHERE sent_at IS NULL ORDER BY id LIMIT $1 FOR UPDATE SKIP LOCKED`
10
10
 
11
11
  /** Marks the rows as handed to the broker ($1 ids, $2 instant). */
@@ -9,6 +9,7 @@ export class CreateEventOutbox@@epochMs13@@ implements MigrationInterface {
9
9
  await queryRunner.query(`CREATE TABLE event_outbox (
10
10
  id bigserial PRIMARY KEY,
11
11
  event_id varchar(200) NOT NULL,
12
+ correlation_id varchar(128) NOT NULL DEFAULT pg_catalog.gen_random_uuid()::text,
12
13
  event_name varchar(200) NOT NULL,
13
14
  topic varchar(200) NOT NULL,
14
15
  message_key varchar(200) NOT NULL,
@@ -0,0 +1,19 @@
1
+ import { DomainError } from "@modules/platform/errors"
2
+ import type { ErrorKind } from "@modules/platform/errors"
3
+
4
+ /** Stable client-operation refusals shared by transports and durable inbox adapters. */
5
+ export enum IdempotencyErrorCode {
6
+ /** This principal's operation key was already used for a different request. */
7
+ KeyReused = "IDEMPOTENCY_KEY_REUSED",
8
+ /** The key, authenticated scope or JSON data cannot identify a valid operation. */
9
+ RequestInvalid = "IDEMPOTENCY_REQUEST_INVALID",
10
+ }
11
+
12
+ /** Transport kinds of the public idempotency refusals. */
13
+ export const IDEMPOTENCY_ERROR_KINDS: Record<IdempotencyErrorCode, ErrorKind> = {
14
+ [IdempotencyErrorCode.KeyReused]: "conflict",
15
+ [IdempotencyErrorCode.RequestInvalid]: "invalid",
16
+ }
17
+
18
+ /** The public idempotency error family. */
19
+ export class IdempotencyError extends DomainError<IdempotencyErrorCode> {}
@@ -0,0 +1,2 @@
1
+ export { IdempotencyError, IdempotencyErrorCode, IDEMPOTENCY_ERROR_KINDS } from "./errors/idempotency.error"
2
+ export { IDEMPOTENCY_MESSAGES } from "./messages/idempotency.messages"
@@ -0,0 +1,13 @@
1
+ import type { MessageBundle } from "@modules/platform/i18n"
2
+
3
+ /** Display text of the public client-operation refusals. */
4
+ export const IDEMPOTENCY_MESSAGES: MessageBundle = {
5
+ vi: {
6
+ "errors.IDEMPOTENCY_KEY_REUSED": "Khóa thao tác đã được dùng cho nội dung khác.",
7
+ "errors.IDEMPOTENCY_REQUEST_INVALID": "Khóa, phạm vi hoặc dữ liệu thao tác không hợp lệ.",
8
+ },
9
+ en: {
10
+ "errors.IDEMPOTENCY_KEY_REUSED": "This operation key was already used with a different request.",
11
+ "errors.IDEMPOTENCY_REQUEST_INVALID": "The operation key, scope or JSON data is invalid.",
12
+ },
13
+ }
@@ -0,0 +1,32 @@
1
+ import { Test } from "@nestjs/testing"
2
+ import { IdempotencyError, IdempotencyErrorCode } from "@modules/platform/idempotency"
3
+ import { clientSource } from "./client-operation.policy"
4
+
5
+ const CLIENT_SOURCE = Symbol("client-source")
6
+ const build = async () => {
7
+ const moduleRef = await Test.createTestingModule({
8
+ providers: [{ provide: CLIENT_SOURCE, useValue: clientSource }],
9
+ }).compile()
10
+ return moduleRef.get<typeof clientSource>(CLIENT_SOURCE)
11
+ }
12
+
13
+ const REQUEST = { principalId: "person-1", operation: "checkout.place", idempotencyKey: "key-1", request: {} }
14
+
15
+ describe("clientSource", () => {
16
+ it("binds the exact authenticated principal and operation and accepts a 64-character key", async () => {
17
+ const sourceOf = await build()
18
+ expect(sourceOf({ ...REQUEST, idempotencyKey: "a".repeat(64) })).toBe("client:person-1:checkout.place")
19
+ })
20
+
21
+ it.each([
22
+ { ...REQUEST, principalId: " " },
23
+ { ...REQUEST, principalId: "a:b" },
24
+ { ...REQUEST, operation: " " },
25
+ { ...REQUEST, operation: "a:b" },
26
+ { ...REQUEST, idempotencyKey: " " },
27
+ { ...REQUEST, idempotencyKey: "a".repeat(65) },
28
+ ])("refuses ambiguous scopes and unbounded or empty keys %p", async (request) => {
29
+ const sourceOf = await build()
30
+ expect(() => sourceOf(request)).toThrow(new IdempotencyError({ code: IdempotencyErrorCode.RequestInvalid }))
31
+ })
32
+ })
@@ -0,0 +1,17 @@
1
+ import { IdempotencyError, IdempotencyErrorCode } from "@modules/platform/idempotency"
2
+ import type { ClientOperation } from "./inbox.port"
3
+
4
+ /** Builds the authenticated operation scope after checking that its separators and bounded key are unambiguous. */
5
+ export const clientSource = (request: ClientOperation): string => {
6
+ if (
7
+ !request.principalId.trim() ||
8
+ request.principalId.includes(":") ||
9
+ !request.operation.trim() ||
10
+ request.operation.includes(":") ||
11
+ !request.idempotencyKey.trim() ||
12
+ request.idempotencyKey.length > 64
13
+ ) {
14
+ throw new IdempotencyError({ code: IdempotencyErrorCode.RequestInvalid })
15
+ }
16
+ return `client:${request.principalId}:${request.operation}`
17
+ }
@@ -0,0 +1,66 @@
1
+ import { Test } from "@nestjs/testing"
2
+ import { FakeClock, fakeTransaction, mockEntityManager } from "@starci/jest-preset"
3
+ import { InboxErrorCode } from "./errors/inbox.error"
4
+ import { requestFingerprint } from "./request-fingerprint.policy"
5
+ import { backfillClientOutcome } from "./client-outcome-backfill.client"
6
+ import { BACKFILL_CLIENT_OUTCOME } from "./persistence/inbox.sql"
7
+
8
+ const BACKFILL = Symbol("client-outcome-backfill")
9
+ const build = async () => {
10
+ const moduleRef = await Test.createTestingModule({
11
+ providers: [{ provide: BACKFILL, useValue: backfillClientOutcome }],
12
+ }).compile()
13
+ return moduleRef.get<typeof backfillClientOutcome>(BACKFILL)
14
+ }
15
+
16
+ const OPERATION = {
17
+ principalId: "person-1",
18
+ operation: "checkout.place-order",
19
+ idempotencyKey: "legacy-key",
20
+ request: {},
21
+ }
22
+ const OUTCOME = { kind: "ok", value: { orderId: "old-order", status: "pending" } }
23
+
24
+ describe("backfillClientOutcome", () => {
25
+ it("records the legacy scoped key, original instant, fingerprint and original JSON outcome through the migration transaction", async () => {
26
+ const backfill = await build()
27
+ const manager = mockEntityManager({ query: [BACKFILL_CLIENT_OUTCOME, []] })
28
+ const transaction = fakeTransaction(manager)
29
+ const instant = new FakeClock("2026-01-01T00:00:00.000Z").now()
30
+
31
+ await transaction.em.transaction((tx) => backfill(tx, OPERATION, OUTCOME, instant))
32
+
33
+ expect(transaction.em.query).toHaveBeenCalledWith(BACKFILL_CLIENT_OUTCOME, [
34
+ "client:person-1:checkout.place-order",
35
+ "legacy-key",
36
+ requestFingerprint({}),
37
+ instant,
38
+ '{"value":{"kind":"ok","value":{"orderId":"old-order","status":"pending"}}}',
39
+ ])
40
+ expect(transaction.commits).toBe(1)
41
+ })
42
+
43
+ it("refuses a migration outside its active transaction before recording anything", async () => {
44
+ const backfill = await build()
45
+ const manager = mockEntityManager()
46
+
47
+ await expect(backfill(manager, OPERATION, OUTCOME, new FakeClock().now())).rejects.toMatchObject({
48
+ code: InboxErrorCode.TransactionRequired,
49
+ })
50
+
51
+ expect(manager.query).not.toHaveBeenCalled()
52
+ })
53
+
54
+ it("propagates a failed insert so the domain migration rolls back", async () => {
55
+ const backfill = await build()
56
+ const manager = mockEntityManager()
57
+ const failure = new Error("backfill insert failed")
58
+ manager.query.mockRejectedValueOnce(failure)
59
+ const transaction = fakeTransaction(manager)
60
+
61
+ await expect(
62
+ transaction.em.transaction((tx) => backfill(tx, OPERATION, OUTCOME, new FakeClock().now())),
63
+ ).rejects.toBe(failure)
64
+ expect(transaction.rollbacks).toBe(1)
65
+ })
66
+ })
@@ -0,0 +1,24 @@
1
+ import type { EntityManager } from "typeorm"
2
+ import { clientSource } from "./client-operation.policy"
3
+ import { InboxError, InboxErrorCode } from "./errors/inbox.error"
4
+ import type { ClientOperation } from "./inbox.port"
5
+ import { canonicalJson, requestFingerprint } from "./request-fingerprint.policy"
6
+ import { BACKFILL_CLIENT_OUTCOME } from "./persistence/inbox.sql"
7
+
8
+ /** Transfers a capability's legacy completed operation into the shared inbox during its data migration. */
9
+ export const backfillClientOutcome = async (
10
+ manager: EntityManager,
11
+ operation: ClientOperation,
12
+ outcome: unknown,
13
+ claimedAt: Date,
14
+ ): Promise<void> => {
15
+ if (manager.queryRunner?.isTransactionActive !== true)
16
+ throw new InboxError({ code: InboxErrorCode.TransactionRequired })
17
+ await manager.query(BACKFILL_CLIENT_OUTCOME, [
18
+ clientSource(operation),
19
+ operation.idempotencyKey,
20
+ requestFingerprint(operation.request),
21
+ claimedAt,
22
+ canonicalJson({ value: outcome }),
23
+ ])
24
+ }
@@ -0,0 +1,60 @@
1
+ import { Test } from "@nestjs/testing"
2
+ import { FakeClock, fakeTransaction, mockEntityManager } from "@starci/jest-preset"
3
+ import { InboxErrorCode } from "./errors/inbox.error"
4
+ import { backfillDeliveryClaim } from "./delivery-claim-backfill.client"
5
+ import { CLAIM_EVENT } from "./persistence/inbox.sql"
6
+
7
+ const BACKFILL_DELIVERY = Symbol("delivery-claim-backfill")
8
+ const build = async () => {
9
+ const moduleRef = await Test.createTestingModule({
10
+ providers: [{ provide: BACKFILL_DELIVERY, useValue: backfillDeliveryClaim }],
11
+ }).compile()
12
+ return moduleRef.get<typeof backfillDeliveryClaim>(BACKFILL_DELIVERY)
13
+ }
14
+
15
+ const CLAIM = {
16
+ source: "saga:place-order",
17
+ eventId: "event-1",
18
+ claimedAt: new FakeClock("2026-01-01T00:00:00.000Z").now(),
19
+ }
20
+
21
+ describe("backfillDeliveryClaim", () => {
22
+ it.each([{ rows: [{ event_id: "event-1" }] }, { rows: [] }])(
23
+ "preserves the legacy scope and instant without replacing an existing receipt: %p",
24
+ async ({ rows }) => {
25
+ const backfill = await build()
26
+ const transaction = fakeTransaction(mockEntityManager({ query: [CLAIM_EVENT, rows] }))
27
+
28
+ await expect(transaction.em.transaction((manager) => backfill(manager, CLAIM))).resolves.toBeUndefined()
29
+
30
+ expect(transaction.em.query).toHaveBeenCalledWith(CLAIM_EVENT, [
31
+ CLAIM.source,
32
+ CLAIM.eventId,
33
+ CLAIM.claimedAt,
34
+ ])
35
+ expect(transaction.commits).toBe(1)
36
+ },
37
+ )
38
+
39
+ it("refuses a manager outside an active migration transaction before transferring a claim", async () => {
40
+ const backfill = await build()
41
+ const manager = mockEntityManager()
42
+
43
+ await expect(backfill(manager, CLAIM)).rejects.toMatchObject({ code: InboxErrorCode.TransactionRequired })
44
+
45
+ expect(manager.query).not.toHaveBeenCalled()
46
+ })
47
+
48
+ it("propagates a failed claim statement so the capability migration rolls back", async () => {
49
+ const backfill = await build()
50
+ const manager = mockEntityManager()
51
+ const failure = new Error("delivery claim transfer failed")
52
+ manager.query.mockRejectedValueOnce(failure)
53
+ const transaction = fakeTransaction(manager)
54
+
55
+ await expect(transaction.em.transaction((tx) => backfill(tx, CLAIM))).rejects.toBe(failure)
56
+
57
+ expect(transaction.rollbacks).toBe(1)
58
+ expect(transaction.committedWrites).toEqual([])
59
+ })
60
+ })
@@ -0,0 +1,12 @@
1
+ import type { EntityManager } from "typeorm"
2
+ import { InboxError, InboxErrorCode } from "./errors/inbox.error"
3
+ import type { DeliveryClaimBackfill } from "./inbox.port"
4
+ import { CLAIM_EVENT } from "./persistence/inbox.sql"
5
+
6
+ /** Transfers a legacy delivery claim inside its owning capability's migration transaction. */
7
+ export const backfillDeliveryClaim = async (manager: EntityManager, claim: DeliveryClaimBackfill): Promise<void> => {
8
+ if (manager.queryRunner?.isTransactionActive !== true) {
9
+ throw new InboxError({ code: InboxErrorCode.TransactionRequired })
10
+ }
11
+ await manager.query(CLAIM_EVENT, [claim.source, claim.eventId, claim.claimedAt])
12
+ }
@@ -0,0 +1,49 @@
1
+ import { Test } from "@nestjs/testing"
2
+ import { fakeTransaction, mockEntityManager } from "@starci/jest-preset"
3
+ import { InboxErrorCode } from "./errors/inbox.error"
4
+ import { releaseDeliveryClaim } from "./delivery-claim-release.client"
5
+ import { RELEASE_EVENT } from "./persistence/inbox.sql"
6
+
7
+ const RELEASE_DELIVERY = Symbol("delivery-claim-release")
8
+ const build = async () => {
9
+ const moduleRef = await Test.createTestingModule({
10
+ providers: [{ provide: RELEASE_DELIVERY, useValue: releaseDeliveryClaim }],
11
+ }).compile()
12
+ return moduleRef.get<typeof releaseDeliveryClaim>(RELEASE_DELIVERY)
13
+ }
14
+
15
+ const CLAIM = { source: "saga:place-order", eventId: "event-1" }
16
+
17
+ describe("releaseDeliveryClaim", () => {
18
+ it("uses the protected delivery delete through the migration's transaction", async () => {
19
+ const release = await build()
20
+ const transaction = fakeTransaction(mockEntityManager({ query: [RELEASE_EVENT, []] }))
21
+
22
+ await transaction.em.transaction((manager) => release(manager, CLAIM))
23
+
24
+ expect(transaction.em.query).toHaveBeenCalledWith(RELEASE_EVENT, [CLAIM.source, CLAIM.eventId])
25
+ expect(transaction.commits).toBe(1)
26
+ })
27
+
28
+ it("refuses to release a receipt outside an active migration transaction", async () => {
29
+ const release = await build()
30
+ const manager = mockEntityManager()
31
+
32
+ await expect(release(manager, CLAIM)).rejects.toMatchObject({ code: InboxErrorCode.TransactionRequired })
33
+
34
+ expect(manager.query).not.toHaveBeenCalled()
35
+ })
36
+
37
+ it("propagates a failed delete so restoring and releasing the receipt roll back together", async () => {
38
+ const release = await build()
39
+ const manager = mockEntityManager()
40
+ const failure = new Error("delivery claim release failed")
41
+ manager.query.mockRejectedValueOnce(failure)
42
+ const transaction = fakeTransaction(manager)
43
+
44
+ await expect(transaction.em.transaction((tx) => release(tx, CLAIM))).rejects.toBe(failure)
45
+
46
+ expect(transaction.rollbacks).toBe(1)
47
+ expect(transaction.committedWrites).toEqual([])
48
+ })
49
+ })
@@ -0,0 +1,12 @@
1
+ import type { EntityManager } from "typeorm"
2
+ import { InboxError, InboxErrorCode } from "./errors/inbox.error"
3
+ import type { DeliveryClaim } from "./inbox.port"
4
+ import { RELEASE_EVENT } from "./persistence/inbox.sql"
5
+
6
+ /** Transfers a delivery receipt's authority back after its legacy owner restored the receipt in the same transaction. */
7
+ export const releaseDeliveryClaim = async (manager: EntityManager, claim: DeliveryClaim): Promise<void> => {
8
+ if (manager.queryRunner?.isTransactionActive !== true) {
9
+ throw new InboxError({ code: InboxErrorCode.TransactionRequired })
10
+ }
11
+ await manager.query(RELEASE_EVENT, [claim.source, claim.eventId])
12
+ }
@@ -0,0 +1,7 @@
1
+ import { SetMetadata } from "@nestjs/common"
2
+
3
+ /** Metadata of a client command whose effects require a durable operation key. */
4
+ const EFFECTFUL: unique symbol = Symbol("platform.inbox.effectful")
5
+
6
+ /** Marks an effectful client command for BE_MUTATION_IDEMPOTENT; transaction ownership remains in its domain service. */
7
+ export const Effectful = (): ClassDecorator => SetMetadata(EFFECTFUL, true)
@@ -0,0 +1,22 @@
1
+ import { DomainError } from "@modules/platform/errors"
2
+ import type { ErrorKind } from "@modules/platform/errors"
3
+
4
+ /** Stable refusals and integrity errors of the shared client inbox. */
5
+ export enum InboxErrorCode {
6
+ /** The caller supplied a manager outside an active domain transaction. */
7
+ TransactionRequired = "INBOX_TRANSACTION_REQUIRED",
8
+ /** The effect returned a value that is not plain JSON data, so no outcome can be stored or replayed. */
9
+ OutcomeInvalid = "INBOX_OUTCOME_INVALID",
10
+ /** The atomic claim or outcome write returned no completed row. */
11
+ OutcomeMissing = "INBOX_OUTCOME_MISSING",
12
+ }
13
+
14
+ /** The shared transport mapping of inbox errors. */
15
+ export const INBOX_ERROR_KINDS: Record<InboxErrorCode, ErrorKind> = {
16
+ [InboxErrorCode.OutcomeInvalid]: "internal",
17
+ [InboxErrorCode.OutcomeMissing]: "internal",
18
+ [InboxErrorCode.TransactionRequired]: "internal",
19
+ }
20
+
21
+ /** The error family of the inbox owner. */
22
+ export class InboxError extends DomainError<InboxErrorCode> {}
@@ -0,0 +1,17 @@
1
+ import type { EntityManager } from "typeorm"
2
+ import { injector } from "@modules/platform/composition"
3
+ import type { TypedParameterDecorator } from "@modules/platform/composition"
4
+ import type { Inbox } from "./inbox.port"
5
+
6
+ /** Token of the Inbox port. */
7
+ export const INBOX: unique symbol = Symbol("platform.inbox")
8
+
9
+ /** Token of the entity managers of the connections that hold the claims table, in the order the options name them. */
10
+ export const CLAIM_MANAGERS: unique symbol = Symbol("platform.inbox.claim-managers")
11
+
12
+ /** Injects the Inbox port. Parameter type: Inbox. */
13
+ export const InjectInbox = (): TypedParameterDecorator<Inbox> => injector<Inbox>(INBOX)
14
+
15
+ /** Injects the entity managers of the connections that hold the claims table. Parameter type: a one-manager tuple (`readonly [EntityManager]`). */
16
+ export const InjectClaimManagers = (): TypedParameterDecorator<readonly [EntityManager]> =>
17
+ injector<readonly [EntityManager]>(CLAIM_MANAGERS)
@@ -0,0 +1,7 @@
1
+ import { ConfigurableModuleBuilder } from "@nestjs/common"
2
+ import type { InboxOptions } from "./inbox.options"
3
+
4
+ /** The configurable-module base of the inbox capability; `isGlobal` is decided by the app root. */
5
+ export const { ConfigurableModuleClass, OPTIONS_TYPE } = new ConfigurableModuleBuilder<InboxOptions>()
6
+ .setExtras({ isGlobal: false }, (definition, extras) => ({ ...definition, global: extras.isGlobal }))
7
+ .build()
@@ -0,0 +1,28 @@
1
+ import { Module } from "@nestjs/common"
2
+ import type { DynamicModule } from "@nestjs/common"
3
+ import type { EntityManager } from "typeorm"
4
+ import { INBOX, CLAIM_MANAGERS } from "./inbox.decorators"
5
+ import { ConfigurableModuleClass, OPTIONS_TYPE } from "./inbox.module-definition"
6
+ import { PostgresInbox } from "./inbox.service"
7
+
8
+ @Module({})
9
+ /** Provides the Inbox port over the claims table of the connection the app names. */
10
+ export class InboxModule extends ConfigurableModuleClass {
11
+ /** Registers the capability once per app. */
12
+ static register(options: typeof OPTIONS_TYPE): DynamicModule {
13
+ const base = super.register(options)
14
+ return {
15
+ ...base,
16
+ providers: [
17
+ ...(base.providers ?? []),
18
+ {
19
+ provide: CLAIM_MANAGERS,
20
+ useFactory: (manager: EntityManager) => [manager],
21
+ inject: [options.connection],
22
+ },
23
+ { provide: INBOX, useClass: PostgresInbox },
24
+ ],
25
+ exports: [INBOX],
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,7 @@
1
+ import type { InjectionToken } from "@nestjs/common"
2
+
3
+ /** What the inbox of one app needs: the connection whose claims table it uses, so every service claims on its own database. */
4
+ export interface InboxOptions {
5
+ /** The token of the shared entity manager of the connection that holds the claims table of the app. */
6
+ readonly connection: InjectionToken
7
+ }
@@ -0,0 +1,44 @@
1
+ import type { EntityManager } from "typeorm"
2
+
3
+ /** The authenticated scope and complete normalized body of a client operation; the key is never part of the body. */
4
+ export interface ClientOperation {
5
+ /** The authenticated principal, mapped by the transport rather than supplied by its client. */
6
+ readonly principalId: string
7
+ /** A stable operation name; neither it nor the principal may contain a colon. */
8
+ readonly operation: string
9
+ /** The client's nonempty key, at most 64 characters. */
10
+ readonly idempotencyKey: string
11
+ /** JSON data that identifies every client-controlled input affecting the result. */
12
+ readonly request: unknown
13
+ }
14
+
15
+ /** The original identity of a delivery receipt. */
16
+ export interface DeliveryClaim {
17
+ /** The original publisher or consumer namespace, preserved exactly. */
18
+ readonly source: string
19
+ /** The original delivered event identity. */
20
+ readonly eventId: string
21
+ }
22
+
23
+ /** A delivery receipt transferred from a capability's legacy dedupe store by its data migration. */
24
+ export interface DeliveryClaimBackfill extends DeliveryClaim {
25
+ /** The instant the original receiver claimed the event. */
26
+ readonly claimedAt: Date
27
+ }
28
+
29
+ /** The domain work whose JSON result commits with the client receipt. */
30
+ export interface ClientOperationEffect<T> {
31
+ (): Promise<T>
32
+ }
33
+
34
+ /** Deduplicates deliveries by the source and event id of their consumer. */
35
+ export interface Inbox {
36
+ /** True for the first claim; with `tx`, the claim commits or rolls back with the caller's effect. */
37
+ claim(source: string, eventId: string, tx?: EntityManager): Promise<boolean>
38
+ /** Gives a separately committed claim back after a failed attempt; a transaction-owned claim rolls back with its effect. */
39
+ release(source: string, eventId: string): Promise<void>
40
+ /** Claims, applies and records one JSON outcome inside the caller's transaction; repeats replay it without running the effect. */
41
+ runClient<T>(request: ClientOperation, tx: EntityManager, effect: ClientOperationEffect<T>): Promise<T>
42
+ /** Deletes at most one bounded batch of completed client claims before the cutoff, keeping delivery claims intact. */
43
+ sweepClients(cutoff: Date, tx: EntityManager): Promise<number>
44
+ }