@starci/hfs 4.7.31 → 4.7.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (473) hide show
  1. package/CHANGELOG.md +12 -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
@@ -5,10 +5,6 @@ schema: starci/application-stacks@1
5
5
  # A GitHub package.json repository URL also enables the managed block.
6
6
  sources:
7
7
  - repository: "{{project}}"
8
- backing:
9
- primary:
10
- provider: external-postgres
11
- managed: true
12
8
  components:
13
9
  core:
14
10
  role: service
@@ -27,8 +23,10 @@ components:
27
23
  purpose: The single public entry point; TLS state stays on its declared persistent volume.
28
24
  postgres:
29
25
  role: stateful
26
+ stateful: true
27
+ volumes: [postgres-data]
30
28
  image: postgres:16@sha256:1a6ab3f5345eb6dbe04a1349529caabdb0ab09293a09590fad07b2246bfa4b54
31
- purpose: The full development database; production uses an owner-provisioned external PostgreSQL connection.
29
+ purpose: PostgreSQL in Docker Linux containers for development and Docker Swarm for production; production credentials use mounted secrets and data stays on its named volume.
32
30
  environments:
33
31
  dev:
34
32
  status: supported
@@ -51,6 +49,7 @@ environments:
51
49
  app: { port: 3000 }
52
50
  landing: { port: 3002 }
53
51
  ingress: { httpPort: 80, httpsPort: 443 }
52
+ postgres: { replicas: 1, port: 5432 }
54
53
  runbook: README.md
55
54
  ingress:
56
55
  component: ingress
@@ -79,8 +78,8 @@ environments:
79
78
  retention: Thirty daily snapshots
80
79
  owner: Production operator
81
80
  restoreProof: .starcistacks/production/infra/swarm/recovery/restore-proof.md
82
- - component: primary
83
- target: Provider recovery plus owner-provisioned encrypted off-host database exports
81
+ - component: postgres
82
+ target: Owner-provisioned encrypted off-host database exports and postgres-data volume snapshots
84
83
  retention: Thirty daily exports
85
84
  owner: Production operator
86
85
  restoreProof: .starcistacks/production/infra/swarm/recovery/restore-proof.md
@@ -92,8 +91,19 @@ environments:
92
91
  where: secrets/primary-db-url
93
92
  recipientPolicy: This app's own age recipient configured in .sops.yaml
94
93
  keyCustody: SOPS_AGE_KEY in the host's untracked .claude/secret.env
94
+ - name: postgres-password
95
+ runtimeName: "{{project}}-production-postgres-password-v1"
96
+ version: 1
97
+ where: secrets/postgres-password
98
+ recipientPolicy: This app's own age recipient configured in .sops.yaml
99
+ keyCustody: SOPS_AGE_KEY in the host's untracked .claude/secret.env
100
+ - name: primary-db-password
101
+ runtimeName: "{{project}}-production-primary-db-password-v1"
102
+ version: 1
103
+ where: secrets/primary-db-password
104
+ recipientPolicy: This app's own age recipient configured in .sops.yaml
105
+ keyCustody: SOPS_AGE_KEY in the host's untracked .claude/secret.env
95
106
  # What `starci app stack` deploys (verbs: prepare, doctor, deploy, verify, rollback); names and shapes only, never a value.
96
- # Add secrets.grants once the real sealed members exist under .starcistacks/production/secrets, and a backup block for stateful volumes.
97
107
  deploy:
98
108
  stack: {{project}}-production
99
109
  # The image registry. The canon scaffold writes `public` (the owner's default: images cost nothing there); the value belongs to the app from now on
@@ -118,6 +128,24 @@ environments:
118
128
  volumes:
119
129
  - {{project}}-production-ingress-data
120
130
  - {{project}}-production-ingress-config-data
131
+ - {{project}}-production-postgres-data
132
+ secrets:
133
+ grants:
134
+ - service: core
135
+ filePointers: true
136
+ from: [{member: primary-db-url}]
137
+ - service: postgres
138
+ filePointers: true
139
+ from: [{member: postgres-password}, {member: primary-db-password}]
140
+ configs:
141
+ - name: postgres-init
142
+ file: infra/swarm/postgres-init.sh
143
+ env: POSTGRES_INIT_CONFIG
144
+ # Release tasks: an idempotent write the release owes the data (a catalogue sync, a seed upsert) is declared under tasks and listed in postDeploy.
145
+ # A deploy runs postDeploy after the stack converged and before verify, which stays read-only. Uncomment and name the real command:
146
+ # tasks:
147
+ # sync-catalogue: {service: core, command: [node, dist/cli.js, catalogue, sync]}
148
+ # postDeploy: [sync-catalogue]
121
149
  verify:
122
150
  http:
123
151
  - name: core liveness
@@ -1,6 +1,6 @@
1
1
  # Development stack
2
2
 
3
- The development Docker Compose model contains the core API, the product and landing front ends, and a pinned local PostgreSQL database. Trust authentication is confined to this local, unpublished database.
3
+ Every development dependency runs in Docker Linux containers. This starter's Docker Compose model contains the core API, the product and landing front ends, and a pinned local PostgreSQL database. Compose is an implementation detail; declared dependencies may also use managed `<dep>:create`, `<dep>:start` and `<dep>:stop` scripts, and local Supabase, when declared, runs through the Supabase CLI. A declared native profile may run app processes locally while dependencies stay in Docker. Trust authentication is confined to this local, unpublished database.
4
4
  Starter release images and external secrets must be prepared before starting the container model. Local source development remains available through the app scripts.
5
5
 
6
6
  | Command | Operation |
@@ -1,14 +1,15 @@
1
1
  # Production stack
2
2
 
3
- The tracked model declares Docker Swarm application services and one digest-pinned Caddy ingress.
4
- The production database is external PostgreSQL; the owner supplies its least-privilege connection through sealed custody.
3
+ The app and its dependencies run on Docker Swarm; Supabase, when declared, is a hosted service. The tracked model declares the application services, one digest-pinned Caddy ingress and one digest-pinned PostgreSQL service on an internal backing network, with its data on a named persistent volume and credentials supplied through mounted secrets. On the first boot of an empty data volume, PostgreSQL runs the mounted `postgres-init.sh` config to create the `app_be` role with access to this app's database and schema, without superuser, database-creation, role-creation, replication or RLS-bypass privileges. Existing data volumes skip initialization and retain their roles and credentials.
5
4
  The model is a deployment declaration. It contains no owner credentials, real DNS receipt, completed release or restore proof. The public records under `.starcistacks/production/infra/swarm/` mark those owner-run operations as pending. Its all-zero application image digests are illustrative and must be replaced before deployment.
6
5
 
7
- The runtime template tracks no encrypted members. Scaffolding generates the declared database member with a placeholder value encrypted to this app's own recipient in `.sops.yaml`. Replace it with `starci app secret set production` using the matching host age identity in untracked local custody. Each Docker secret contains its corresponding key's real value; the API receives it through the declared mounted file.
6
+ The runtime template tracks no encrypted members. Scaffolding generates the declared database connection, application password and PostgreSQL administrator password members with placeholder values encrypted to this app's own recipient in `.sops.yaml`. Replace them with `starci app secret set production` using the matching host age identity in untracked local custody. The sealed connection uses role `app_be`, database `{{project}}` and internal endpoint `postgres:5432`; its URI-encoded password matches the separate `primary-db-password` member. The administrator password is distinct. The API receives only its least-privilege connection; only PostgreSQL receives the two password files for initialization. The non-empty application password contains no line breaks. The service has no TTY: initialization creates the role without a password, then feeds the two `psql` `\password` prompts through stdin using Bash's builtin `printf`, so psql encrypts it before sending SQL. No password travels in an initialization command argument or a plaintext SQL statement.
7
+
8
+ Bootstrap references reviewed 2026-10-25: the [official PostgreSQL image initialization and Docker secret contract](https://github.com/docker-library/docs/blob/master/postgres/README.md#initialization-scripts), [PostgreSQL 16 psql password handling and SQL interpolation](https://www.postgresql.org/docs/16/app-psql.html), and its [password command](https://github.com/postgres/postgres/blob/REL_16_STABLE/src/bin/psql/command.c) and [prompt stdin fallback](https://github.com/postgres/postgres/blob/REL_16_STABLE/src/common/sprompt.c). First-release verification must confirm fresh-volume initialization, `app_be` privileges, health-gated startup and unchanged credentials after data-volume reuse; this source template records no live verification result.
8
9
 
9
10
  | Command | Operation |
10
11
  | --- | --- |
11
- | prepare | Owner provisions the Swarm, node label, database, DNS and real sealed secret members, then runs `starci app stack prepare --env production --execute` to create the missing external volumes (the one-time swarm initialization stays an owner-approved step: `--init-swarm` with `--execute`, after the owner's explicit approval). Release images are built and pushed by the managed the managed stack-deploy workflow. |
12
+ | prepare | Owner provisions the Swarm, node label and DNS, seals the matching application connection/password and distinct administrator password, then runs `starci app stack prepare --env production --execute` to create the missing external volumes (the one-time swarm initialization stays an owner-approved step: `--init-swarm` with `--execute`, after the owner's explicit approval). The single up path below starts PostgreSQL and initializes `app_be` automatically on a new empty data volume. Release images are built and pushed by the managed stack-deploy workflow. |
12
13
  | doctor | Run `starci app stack doctor --env production` (read-only): docker, swarm, volumes, services, secrets, configs and the release pointers on the intended manager. |
13
14
  | up | After preparation and review, run the manual stack-deploy workflow (Actions, deploy stack) on a release tag; it builds the images and runs `starci app stack deploy --env production --image <service>=<repository>@sha256:<digest> --source-sha <sha> --execute`. A dry run is `starci app stack render` or `deploy` without `--execute`. |
14
15
  | status | Run `starci app stack doctor --env production`, or `starci app stack converge --env production --since <ISO time>` to inspect desired and running replicas plus update state. |
@@ -22,16 +23,16 @@ Every item is pending. Swarm initialization, provider URIs, DNS changes and key
22
23
 
23
24
  | Area | Required owner action and evidence |
24
25
  | --- | --- |
25
- | Swarm and placement | Initialize the intended manager; label the intended stateful node `{{project}}=production`. Only that node owns the external ingress volumes. |
26
+ | Swarm and placement | Initialize the intended manager; label the intended stateful node `{{project}}=production`. Only that node owns the external ingress and PostgreSQL data volumes. |
26
27
  | DNS | Replace the reserved `example.invalid` hosts consistently in the manifest, Caddyfile and public configuration; publish address records for the product, API and landing hosts and retain a provider receipt in `.starcistacks/production/infra/swarm/owner-actions.md`. |
27
28
  | TLS | Caddy issues certificates after real DNS resolves. Provision `{{project}}-production-ingress-data` and `{{project}}-production-ingress-config-data` on the selected node; retain an issuance and renewal receipt in `.starcistacks/production/infra/swarm/owner-actions.md`. |
28
- | Database | Provision external PostgreSQL and its least-privilege application role. Record the provider URI and recovery owner without copying credentials here. |
29
- | Secret custody | Replace the generated placeholder with the real value in the manifest's declared `.starcistacks/production/secrets/primary-db-url.enc`; record its key in `.starcistacks/production/runtime/env/KEYS.md`. The age identity stays only in the host's untracked `.claude/secret.env`. |
30
- | External secrets | Materialize the sealed members in owner custody and create `{{project}}-production-primary-db-url-v1`. The API reads mounted files; rotation creates new versioned names before updating the model. |
29
+ | Database | Prepare creates `{{project}}-production-postgres-data` on the declared stateful node; the normal up path starts PostgreSQL and initializes `app_be` on its first empty-volume boot. Seal the matching role, database and password described above before up. Existing databases retain their current roles and credentials; initialization never rewrites them. Record the recovery owner without copying credentials here. |
30
+ | Secret custody | Replace generated placeholders in `.starcistacks/production/secrets/primary-db-url.enc`, `primary-db-password.enc` and `postgres-password.enc` with the matching application connection/password and distinct administrator password; record their key names in the environment roster. The age identity stays only in the host's untracked `.claude/secret.env`. |
31
+ | External secrets | Materialize the sealed members in owner custody and create `{{project}}-production-primary-db-url-v1`, `{{project}}-production-primary-db-password-v1` and `{{project}}-production-postgres-password-v1`. The API and PostgreSQL read their separately granted mounted files. A later credential rotation updates the actual database role under owner authority and rotates matching immutable secrets; changing a mounted file does not rerun first-boot initialization. |
31
32
  | Public build configuration | Build the front end with its declared public site and GraphQL URLs. Public browser values stay outside sealed secret custody. |
32
33
  | Immutable release | Replace the illustrative all-zero application digests in the manifest, model and `.starcistacks/production/infra/swarm/releases/images.lock.yaml` with the verified release digests. Store each rendered release under `/srv/{{project}}/releases/<digest>`. Retain the last accepted record in `.starcistacks/production/infra/swarm/releases/previous.yaml`, then move the pointer and update `.starcistacks/production/infra/swarm/releases/current.yaml` after verification. |
33
34
  | Rollback | Retain the previous model and images, redeploy that model if convergence or health fails, and record the resulting convergence receipt. Database changes follow an expand/contract sequence. |
34
- | Backup | Provision the manifest's thirty daily encrypted off-host database exports and ingress-volume snapshots. Record the actual destination and encryption custody in `.starcistacks/production/infra/swarm/recovery/restore-proof.md`; the production operator owns both components. |
35
+ | Backup | Provision the manifest's thirty daily encrypted off-host database exports and PostgreSQL and ingress-volume snapshots. Record the actual destination and encryption custody in `.starcistacks/production/infra/swarm/recovery/restore-proof.md`; the production operator owns both components. |
35
36
  | Restore proof | Perform a restore into isolated infrastructure, verify the restored database and ingress state, and attach a dated receipt with artifact digests to `.starcistacks/production/infra/swarm/recovery/restore-proof.md`. Its initial record is pending; no restore has been run by scaffolding. |
36
37
 
37
38
  No host scheduler is installed. Scheduled application work belongs to the back-end scheduler or an explicitly declared replicated job.
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+ # PostgreSQL runs this config only when its data volume is empty.
3
+ set -euo pipefail
4
+
5
+ PRIMARY_DB_PASSWORD="$(cat "${PRIMARY_DB_PASSWORD_FILE:?}")"
6
+ if [[ -z "$PRIMARY_DB_PASSWORD" || "$PRIMARY_DB_PASSWORD" == *$'\n'* || "$PRIMARY_DB_PASSWORD" == *$'\r'* ]]; then
7
+ builtin printf '%s\n' 'The application password must be non-empty and contain no line breaks.' >&2
8
+ exit 1
9
+ fi
10
+ psql --no-psqlrc --no-password --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" --set ON_ERROR_STOP=1 <<'SQL'
11
+ \getenv app_database POSTGRES_DB
12
+ CREATE ROLE app_be WITH LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
13
+ REVOKE ALL ON DATABASE :"app_database" FROM PUBLIC;
14
+ GRANT CONNECT ON DATABASE :"app_database" TO app_be;
15
+ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
16
+ GRANT USAGE, CREATE ON SCHEMA public TO app_be;
17
+ SQL
18
+ # The service has no TTY; psql reads both hidden prompts from this pipe and encrypts the password before sending SQL.
19
+ builtin printf '%s\n%s\n' "$PRIMARY_DB_PASSWORD" "$PRIMARY_DB_PASSWORD" |
20
+ psql --no-psqlrc --no-password --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" --set ON_ERROR_STOP=1 --command '\password app_be'
21
+ unset PRIMARY_DB_PASSWORD
@@ -2,6 +2,6 @@
2
2
 
3
3
  Status: pending owner execution. No backup or restore has been run by scaffolding.
4
4
 
5
- The production operator owns recovery of the external PostgreSQL database and both external ingress volumes, `{{project}}-production-ingress-data` and `{{project}}-production-ingress-config-data`. Provision provider recovery and thirty daily encrypted off-host database exports. Retain thirty daily encrypted off-host snapshots for both ingress volumes.
5
+ The production operator owns recovery of the PostgreSQL service's external named volume `{{project}}-production-postgres-data` and both external ingress volumes, `{{project}}-production-ingress-data` and `{{project}}-production-ingress-config-data`. Provision thirty daily encrypted off-host database exports and PostgreSQL volume snapshots. Retain thirty daily encrypted off-host snapshots for both ingress volumes.
6
6
 
7
7
  Before relying on the backup policy, restore a database backup and both volume snapshots into isolated infrastructure. Record the actual off-host destination, retention, encryption custody, backup artifact digests, restoration time and operator. Verify database connectivity and the API readiness endpoint, then verify the restored TLS and ingress configuration. Attach the dated result here without credentials.
@@ -7,3 +7,4 @@ images:
7
7
  app: "{{project}}/app:v0.0.0@sha256:0000000000000000000000000000000000000000000000000000000000000000"
8
8
  landing: "{{project}}/landing:v0.0.0@sha256:0000000000000000000000000000000000000000000000000000000000000000"
9
9
  ingress: caddy:2.10.2@sha256:c3d7ee5d2b11f9dc54f947f68a734c84e9c9666c92c88a7f30b9cba5da182adb
10
+ postgres: postgres:16@sha256:1a6ab3f5345eb6dbe04a1349529caabdb0ab09293a09590fad07b2246bfa4b54
@@ -7,11 +7,7 @@ services:
7
7
  NODE_ENV: production
8
8
  PORT: "3001"
9
9
  HTTP_SECURITY_ALLOWED_ORIGINS: https://{{project}}.example.invalid
10
- PRIMARY_DB_URL_FILE: /run/secrets/primary-db-url
11
- secrets:
12
- - source: primary-db-url
13
- target: primary-db-url
14
- networks: [app]
10
+ networks: [app, backing]
15
11
  healthcheck:
16
12
  test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3001/health/live').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
17
13
  interval: 30s
@@ -91,6 +87,37 @@ services:
91
87
  restart_policy:
92
88
  condition: on-failure
93
89
  delay: 5s
90
+ postgres:
91
+ image: postgres:16@sha256:1a6ab3f5345eb6dbe04a1349529caabdb0ab09293a09590fad07b2246bfa4b54
92
+ tty: false
93
+ environment:
94
+ POSTGRES_DB: "{{project}}"
95
+ configs:
96
+ - source: postgres-init
97
+ target: /docker-entrypoint-initdb.d/10-app-role.sh
98
+ mode: 0o555
99
+ volumes: [postgres-data:/var/lib/postgresql/data]
100
+ networks: [backing]
101
+ healthcheck:
102
+ test: ["CMD", "pg_isready", "-h", "127.0.0.1", "-U", "postgres", "-d", "{{project}}"]
103
+ interval: 10s
104
+ timeout: 5s
105
+ retries: 5
106
+ deploy:
107
+ replicas: 1
108
+ placement:
109
+ constraints: ["node.labels.{{project}} == production"]
110
+ update_config:
111
+ parallelism: 1
112
+ order: stop-first
113
+ failure_action: rollback
114
+ monitor: 30s
115
+ rollback_config:
116
+ parallelism: 1
117
+ order: stop-first
118
+ restart_policy:
119
+ condition: on-failure
120
+ delay: 5s
94
121
  ingress:
95
122
  image: caddy:2.10.2@sha256:c3d7ee5d2b11f9dc54f947f68a734c84e9c9666c92c88a7f30b9cba5da182adb
96
123
  configs:
@@ -126,14 +153,29 @@ services:
126
153
  networks:
127
154
  app:
128
155
  driver: overlay
156
+ backing:
157
+ driver: overlay
158
+ internal: true
129
159
  secrets:
130
160
  primary-db-url:
131
161
  external: true
132
162
  name: "{{project}}-production-primary-db-url-v1"
163
+ postgres-password:
164
+ external: true
165
+ name: "{{project}}-production-postgres-password-v1"
166
+ primary-db-password:
167
+ external: true
168
+ name: "{{project}}-production-primary-db-password-v1"
133
169
  configs:
134
170
  ingress-config:
135
171
  file: ./Caddyfile
172
+ postgres-init:
173
+ external: true
174
+ name: ${POSTGRES_INIT_CONFIG}
136
175
  volumes:
176
+ postgres-data:
177
+ external: true
178
+ name: "{{project}}-production-postgres-data"
137
179
  ingress-data:
138
180
  external: true
139
181
  name: "{{project}}-production-ingress-data"
@@ -1,14 +1,16 @@
1
1
  # Production environment key roster
2
2
 
3
- This inventory declares names, consumers and mounted file paths. Scaffolding seals a generated database placeholder to this app's own recipient. Replace it with the real credential before use; no successful deployment is claimed by scaffolding.
3
+ This inventory declares names, consumers and mounted file paths. Scaffolding seals generated database connection, application password and administrator password placeholders to this app's own recipient. Replace them with real credentials before use; no successful deployment is claimed by scaffolding.
4
4
 
5
5
  | Key | Consumer |
6
6
  | --- | --- |
7
7
  | `PORT` | Core API listener |
8
8
  | `HTTP_SECURITY_ALLOWED_ORIGINS` | Core API origin allowlist |
9
- | `PRIMARY_DB_URL` | Core API and CLI primary database connection; the API reads `PRIMARY_DB_URL_FILE` at `/run/secrets/primary-db-url` |
9
+ | `PRIMARY_DB_URL` | Core API and CLI primary database connection; managed deployment sets the API's `PRIMARY_DB_URL_FILE` to `/run/secrets/PRIMARY_DB_URL` from sealed `secrets/primary-db-url.enc` |
10
+ | `POSTGRES_PASSWORD` | PostgreSQL administrator password; managed deployment sets only PostgreSQL's `POSTGRES_PASSWORD_FILE` to `/run/secrets/POSTGRES_PASSWORD` from sealed `secrets/postgres-password.enc` |
11
+ | `PRIMARY_DB_PASSWORD` | First-boot `app_be` role password; managed deployment sets only PostgreSQL's `PRIMARY_DB_PASSWORD_FILE` to `/run/secrets/PRIMARY_DB_PASSWORD` from sealed `secrets/primary-db-password.enc`; it must match the password in the API's sealed connection URI |
10
12
  | `NEXT_PUBLIC_SITE_URL` | Product front-end site URL |
11
13
  | `NEXT_PUBLIC_GRAPHQL_URL` | Product front-end GraphQL transport |
12
14
  | `NEXT_PUBLIC_APP_URL` | Landing front-end product link |
13
15
 
14
- Public front-end configuration is supplied at build time and stays outside sealed custody. Replace the generated private placeholder only in `.starcistacks/production/secrets/primary-db-url.enc`, already declared by the manifest. The versioned external database secret is `{{project}}-production-primary-db-url-v1`.
16
+ Public front-end configuration is supplied at build time and stays outside sealed custody. Replace the generated private placeholders only in the manifest's declared `.starcistacks/production/secrets/primary-db-url.enc`, `primary-db-password.enc` and `postgres-password.enc` members. Their versioned external secrets are `{{project}}-production-primary-db-url-v1`, `{{project}}-production-primary-db-password-v1` and `{{project}}-production-postgres-password-v1`; the API receives its least-privilege connection separately from PostgreSQL's administrator password.
@@ -22,6 +22,8 @@ Declare `ci.github.repository` and `ci.github.defaultBranch` in `.starcistacks/a
22
22
 
23
23
  ## Development
24
24
 
25
+ Every development dependency runs in Docker Linux containers. Local Supabase, when declared, runs through the Supabase CLI; every other declared dependency runs through its managed `<dep>:create`, `<dep>:start` and `<dep>:stop` scripts or the declared container model. This starter uses Compose to run its pinned local PostgreSQL and app services; Compose is an implementation detail. A declared native profile may run the app processes locally while dependencies stay in Docker. In production, the app and its dependencies run on Docker Swarm, except Supabase, which is a hosted service.
26
+
25
27
  ```sh
26
28
  npm install
27
29
  npm run typecheck
@@ -163,6 +163,11 @@ environments:
163
163
  from:
164
164
  - member: primary-db-url
165
165
  - member: supabase-service-role-key
166
+ # Release tasks: an idempotent write the release owes the data (a catalogue sync, a seed upsert) is declared under tasks and listed in postDeploy.
167
+ # A deploy runs postDeploy after the stack converged and before verify, which stays read-only. Uncomment and name the real command:
168
+ # tasks:
169
+ # sync-catalogue: {service: api, command: [node, dist/cli.js, catalogue, sync]}
170
+ # postDeploy: [sync-catalogue]
166
171
  verify:
167
172
  http:
168
173
  - name: api liveness
@@ -1,13 +1,13 @@
1
1
  # Development stack
2
2
 
3
- The app services may run in a local Docker Swarm; local Supabase is started separately with its CLI and supplies the database and broker. The tracked model contains no self-hosted database.
3
+ Every development dependency runs in Docker Linux containers. Local Supabase starts through its CLI and supplies PostgreSQL, pgvector, Auth and Storage. Every other declared dependency, such as Redis, starts through its managed `<dep>:create`, `<dep>:start` and `<dep>:stop` scripts or the declared container model; Compose is an implementation detail. The app services may run in the local Docker Swarm model or through a declared native development profile while dependencies stay in Docker.
4
4
  The application image references contain illustrative all-zero digests. Replace them with verified published release digests in the manifest, model and `.starcistacks/dev/infra/swarm/releases/images.lock.yaml` before starting the container model. Replace its starter `SUPABASE_URL` with the local Supabase endpoint reachable from the Swarm node, and seal that local database connection and integration credential; a container cannot use the host's loopback address without owner-configured networking. Local source development remains available through the app scripts.
5
5
 
6
6
  The runtime template tracks no encrypted members. Scaffolding generates the two declared members with placeholder values encrypted to this app's own recipient in `.sops.yaml`. Replace those values with `starci app secret set dev` using the matching host age identity in untracked local custody. Existing generated envelopes are decryptable with that app identity and can be updated in place. Each Docker secret contains the corresponding key's real value, and the API receives it through its declared mounted file.
7
7
 
8
8
  | Command | Operation |
9
9
  | --- | --- |
10
- | prepare | Run `npm run db:start` at the app root; obtain the local Supabase connection and Auth configuration. An owner who chooses local Swarm initializes it, labels its node `{{project}}=dev` and provisions the ingress volumes. Replace the reserved development host consistently in the manifest, Caddyfile and public configuration; resolve it to the node and record this in `.starcistacks/dev/infra/swarm/owner-actions.md`. Create the external development secrets named in the model from real sealed members. |
10
+ | prepare | Run `npm run db:start` at the app root; obtain the local Supabase connection and Auth configuration. For a declared Redis, run `npm run redis:create` once, then `npm run redis:start` when using the native app profile. An owner who chooses local Swarm initializes it, labels its node `{{project}}=dev` and provisions the ingress volumes; the model starts its declared Redis with the app. Replace the reserved development host consistently in the manifest, Caddyfile and public configuration; resolve it to the node and record this in `.starcistacks/dev/infra/swarm/owner-actions.md`. Create the external development secrets named in the model from real sealed members. |
11
11
  | doctor | From this directory, run `docker stack config -c infra/swarm/stack.yaml`. Confirm image, secret and local Supabase availability. |
12
12
  | up | Run `docker stack deploy -c infra/swarm/stack.yaml {{project}}-dev` from this directory after preparation. |
13
13
  | status | Run `docker stack services {{project}}-dev`. |
@@ -1,7 +1,7 @@
1
1
  # Production stack
2
2
 
3
3
  The tracked model declares Docker Swarm application services and one digest-pinned Caddy ingress.
4
- Supabase supplies PostgreSQL, Auth and pgmq. This stack runs no PostgreSQL, Redis or MongoDB service.
4
+ Hosted Supabase supplies PostgreSQL, pgvector, Auth and Storage. The app and its other dependencies run on Docker Swarm. A Redis service runs only when `hfs.json` `sides.be.backing` declares it for a capability that needs it: one stateful replica with a digest-pinned image, a named volume with a backup entry and its password as a sealed member granted as a Swarm secret (Redis runs `--appendonly yes` with the volume at `/data`). The scaffold declares none; `starci app add queue|job` declares Redis and renders it here.
5
5
  The model is a deployment declaration. It contains no owner credentials, real DNS receipt, completed release or restore proof. Public records under `.starcistacks/production/infra/swarm/` explicitly mark those owner-run operations as pending.
6
6
  Its application image references contain illustrative all-zero digests; they identify no published image and must be replaced before deployment.
7
7
 
@@ -24,7 +24,7 @@ Declare `ci.github.repository` and `ci.github.defaultBranch` in `.starcistacks/a
24
24
 
25
25
  ## Development
26
26
 
27
- Start the Docker engine, then run `npm run db:start` to start local Supabase in Docker containers through the Supabase CLI. Configure the local environment and start the API and web app separately using the package scripts for the declared development profile.
27
+ Every development dependency runs in Docker Linux containers. Start local Supabase with `npm run db:start` through the Supabase CLI; start any other declared dependency through its managed `<dep>:create`, `<dep>:start` and `<dep>:stop` scripts or the declared container model. Compose is an implementation detail. Configure the local environment and start the API and web app using the package scripts for the declared development profile. In production, the app and its dependencies run on Docker Swarm, except Supabase, which is a hosted service; Supabase pgvector owns vector search.
28
28
 
29
29
  ```sh
30
30
  npm install
@@ -0,0 +1,18 @@
1
+ -- Extend the one shared inbox; provider and broker claims keep null client fields.
2
+ alter table public.inbox
3
+ add column request_fingerprint varchar(64),
4
+ add column outcome jsonb;
5
+
6
+ create index inbox_client_retention_idx on public.inbox (claimed_at, source, event_id)
7
+ where source like 'client:%' and request_fingerprint is not null and outcome is not null;
8
+
9
+ create policy "inbox_app_be_update"
10
+ on public.inbox for update to app_be
11
+ using (source like 'client:%')
12
+ with check (source like 'client:%');
13
+
14
+ create policy "inbox_app_be_delete"
15
+ on public.inbox for delete to app_be
16
+ using (source like 'client:%' and request_fingerprint is not null and outcome is not null);
17
+
18
+ grant update, delete on table public.inbox to app_be;
@@ -0,0 +1,16 @@
1
+ import { DomainError } from "@modules/platform/errors"
2
+ import type { ErrorKind } from "@modules/platform/errors"
3
+
4
+ /** Codes of the event-bus capability. */
5
+ export enum EventBusErrorCode {
6
+ /** A publisher supplied an unsafe or unbounded request identity. */
7
+ CorrelationInvalid = "EVENT_BUS_CORRELATION_INVALID",
8
+ }
9
+
10
+ /** How each event-bus code travels. */
11
+ export const EVENT_BUS_ERROR_KINDS: Record<EventBusErrorCode, ErrorKind> = {
12
+ [EventBusErrorCode.CorrelationInvalid]: "invalid",
13
+ }
14
+
15
+ /** The one error class of the event-bus capability. */
16
+ export class EventBusError extends DomainError<EventBusErrorCode> {}
@@ -23,6 +23,8 @@ export interface EventClass<Event extends BaseEvent> {
23
23
 
24
24
  /** One committed delivery received by a reactor. */
25
25
  export interface EventDelivery<Event extends BaseEvent> {
26
+ /** The request id carried explicitly from intake to delivery. */
27
+ readonly correlationId: string
26
28
  /** The consumer's idempotency key. */
27
29
  readonly eventId: string
28
30
  /** The typed event. */
@@ -31,8 +33,16 @@ export interface EventDelivery<Event extends BaseEvent> {
31
33
  readonly attempt: number
32
34
  }
33
35
 
36
+ /** Explicit request context carried independently of the business payload. */
37
+ export interface EventMetadata {
38
+ /** The originating request or promoted legacy event identity. */
39
+ readonly correlationId: string
40
+ }
41
+
34
42
  /** The typed event's durable envelope, read only after the intake transaction commits. */
35
43
  export interface EventEnvelope {
44
+ /** The originating request context, independent of the business payload. */
45
+ readonly metadata: EventMetadata
36
46
  /** The stable event id. */
37
47
  readonly eventId: string
38
48
  /** The contract's event name. */
@@ -4,7 +4,7 @@ import type { BaseEvent, EventClass, EventDelivery } from "./event-bus.contracts
4
4
  /** The publishing side of the lite event bus; the reactor or job owns delivery of committed outbox rows. */
5
5
  export interface EventBus {
6
6
  /** Writes the typed event in the caller's transaction; a rollback writes no durable event. */
7
- publish(event: BaseEvent, tx: EntityManager): Promise<void>
7
+ publish(event: BaseEvent, tx: EntityManager, correlationId: string): Promise<void>
8
8
  }
9
9
 
10
10
  /** A later message transport receives one typed event and forwards its delivery id. */
@@ -1,5 +1,6 @@
1
1
  import { Test } from "@nestjs/testing"
2
2
  import { fakeTransaction, mockEntityManager } from "@starci/jest-preset"
3
+ import { EventBusErrorCode } from "./errors/event-bus.error"
3
4
  import { BaseEvent } from "./event-bus.contracts"
4
5
  import { EventBusService } from "./event-bus.service"
5
6
  import { INSERT_OUTBOX_EVENT } from "./persistence/event-bus.sql"
@@ -16,17 +17,28 @@ describe("EventBusService", () => {
16
17
  return module.get(EventBusService)
17
18
  }
18
19
 
20
+ it.each(["", "unsafe\nid", "x".repeat(129)])("refuses invalid correlation before SQL: %s", async (correlationId) => {
21
+ const subject = await build()
22
+ const manager = mockEntityManager({ query: [INSERT_OUTBOX_EVENT, []] })
23
+
24
+ await expect(subject.publish(new ReceiptRecordedEvent(), manager, correlationId)).rejects.toMatchObject({
25
+ code: EventBusErrorCode.CorrelationInvalid,
26
+ })
27
+ expect(manager.query).not.toHaveBeenCalled()
28
+ })
29
+
19
30
  it("writes the typed envelope through the receipt transaction", async () => {
20
31
  const subject = await build()
21
32
  const transaction = fakeTransaction(mockEntityManager({ query: [INSERT_OUTBOX_EVENT, []] }))
22
33
  const event = new ReceiptRecordedEvent()
23
34
 
24
- await transaction.em.transaction((manager) => subject.publish(event, manager))
35
+ await transaction.em.transaction((manager) => subject.publish(event, manager, "request-1"))
25
36
 
26
37
  expect(transaction.em.query).toHaveBeenCalledWith(INSERT_OUTBOX_EVENT, [
27
38
  "receipt-1",
28
39
  "spec.delivery-recorded",
29
- JSON.stringify({ eventId: event.eventId, eventName: event.eventName, payload: event.payload }),
40
+ JSON.stringify({ metadata: { correlationId: "request-1" }, eventId: event.eventId, eventName: event.eventName, payload: event.payload }),
41
+ "request-1",
30
42
  ])
31
43
  expect(transaction.commits).toBe(1)
32
44
  })
@@ -37,7 +49,7 @@ describe("EventBusService", () => {
37
49
  jest.spyOn(manager, "query").mockRejectedValue(new Error("outbox unavailable"))
38
50
  const transaction = fakeTransaction(manager)
39
51
 
40
- await expect(transaction.em.transaction((tx) => subject.publish(new ReceiptRecordedEvent(), tx))).rejects.toThrow("outbox unavailable")
52
+ await expect(transaction.em.transaction((tx) => subject.publish(new ReceiptRecordedEvent(), tx, "request-1"))).rejects.toThrow("outbox unavailable")
41
53
 
42
54
  expect(transaction.commits).toBe(0)
43
55
  expect(transaction.rollbacks).toBe(1)
@@ -1,5 +1,7 @@
1
1
  import { Injectable } from "@nestjs/common"
2
2
  import type { EntityManager } from "typeorm"
3
+ import { isCorrelationId } from "@modules/platform/logging"
4
+ import { EventBusError, EventBusErrorCode } from "./errors/event-bus.error"
3
5
  import type { BaseEvent, EventEnvelope } from "./event-bus.contracts"
4
6
  import type { EventBus } from "./event-bus.port"
5
7
  import { INSERT_OUTBOX_EVENT } from "./persistence/event-bus.sql"
@@ -8,8 +10,14 @@ import { INSERT_OUTBOX_EVENT } from "./persistence/event-bus.sql"
8
10
  /** Writes a receipt event into the same transaction as its inbox receipt, without running any consumer. */
9
11
  export class EventBusService implements EventBus {
10
12
  /** The outbox commits or rolls back with the receipt; only a later reactor or job processes it. */
11
- async publish(event: BaseEvent, tx: EntityManager): Promise<void> {
12
- const envelope: EventEnvelope = { eventId: event.eventId, eventName: event.eventName, payload: event.payload }
13
- await tx.query(INSERT_OUTBOX_EVENT, [event.eventId, event.eventName, JSON.stringify(envelope)])
13
+ async publish(event: BaseEvent, tx: EntityManager, correlationId: string): Promise<void> {
14
+ if (!isCorrelationId(correlationId)) throw new EventBusError({ code: EventBusErrorCode.CorrelationInvalid })
15
+ const envelope: EventEnvelope = {
16
+ metadata: { correlationId },
17
+ eventId: event.eventId,
18
+ eventName: event.eventName,
19
+ payload: event.payload,
20
+ }
21
+ await tx.query(INSERT_OUTBOX_EVENT, [event.eventId, event.eventName, JSON.stringify(envelope), correlationId])
14
22
  }
15
23
  }
@@ -1,3 +1,5 @@
1
+ export { EVENT_BUS_ERROR_KINDS } from "./errors/event-bus.error"
2
+ export { EVENT_BUS_MESSAGES } from "./messages/event-bus.messages"
1
3
  export { BaseEvent } from "./event-bus.contracts"
2
4
  export type { EventClass, EventDelivery, EventEnvelope, ParsedEvent } from "./event-bus.contracts"
3
5
  export { EVENT_BUS, InjectEventBus } from "./event-bus.decorators"
@@ -0,0 +1,7 @@
1
+ import type { MessageBundle } from "@modules/platform/i18n"
2
+
3
+ /** Display text of the event-bus codes, Vietnamese and English. */
4
+ export const EVENT_BUS_MESSAGES: MessageBundle = {
5
+ vi: { "errors.EVENT_BUS_CORRELATION_INVALID": "M? t??ng quan s? ki?n kh?ng h?p l?." },
6
+ en: { "errors.EVENT_BUS_CORRELATION_INVALID": "The event correlation id is invalid." },
7
+ }
@@ -1,5 +1,5 @@
1
1
  import { sql } from "@modules/platform/database"
2
2
 
3
3
  /** Writes only the typed envelope; a reactor or job reads the committed row afterward. */
4
- export const INSERT_OUTBOX_EVENT = sql`INSERT INTO public.event_bus (event_id, event_name, payload)
5
- VALUES ($1, $2, $3::jsonb)`
4
+ export const INSERT_OUTBOX_EVENT = sql`INSERT INTO public.event_bus (event_id, event_name, payload, correlation_id)
5
+ VALUES ($1, $2, $3::jsonb, $4)`
@@ -0,0 +1,19 @@
1
+ import { DomainError } from "@modules/platform/errors"
2
+ import type { ErrorKind } from "@modules/platform/errors"
3
+
4
+ /** Stable client-operation refusals shared by transports and durable inbox adapters. */
5
+ export enum IdempotencyErrorCode {
6
+ /** This principal's operation key was already used for a different request. */
7
+ KeyReused = "IDEMPOTENCY_KEY_REUSED",
8
+ /** The key, authenticated scope or JSON data cannot identify a valid operation. */
9
+ RequestInvalid = "IDEMPOTENCY_REQUEST_INVALID",
10
+ }
11
+
12
+ /** Transport kinds of the public idempotency refusals. */
13
+ export const IDEMPOTENCY_ERROR_KINDS: Record<IdempotencyErrorCode, ErrorKind> = {
14
+ [IdempotencyErrorCode.KeyReused]: "conflict",
15
+ [IdempotencyErrorCode.RequestInvalid]: "invalid",
16
+ }
17
+
18
+ /** The public idempotency error family. */
19
+ export class IdempotencyError extends DomainError<IdempotencyErrorCode> {}
@@ -0,0 +1,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
+ })