@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
@@ -8,6 +8,8 @@ import { DATABASE_ERROR_KINDS, DatabaseModule } from "@modules/platform/database
8
8
  import { ERRORS_MESSAGES, ErrorsModule } from "@modules/platform/errors"
9
9
  import { HTTP_SECURITY_ERROR_KINDS, HTTP_SECURITY_MESSAGES, HttpSecurityModule } from "@modules/platform/http-security"
10
10
  import { I18nModule } from "@modules/platform/i18n"
11
+ import { IdsModule } from "@modules/platform/ids"
12
+ import { HTTP_ERROR_KINDS, HTTP_MESSAGES, HttpModule } from "@modules/platform/http"
11
13
  import { LoggingModule } from "@modules/platform/logging"
12
14
  import { RetryModule } from "@modules/platform/retry"
13
15
  import type { {{appPascal}}Options } from "../{{app}}.options"
@@ -20,14 +22,17 @@ import type { {{appPascal}}Options } from "../{{app}}.options"
20
22
  export const platformModules = (options: {{appPascal}}Options): Array<DynamicModule> => [
21
23
  ClockModule.register({ isGlobal: true }),
22
24
  RetryModule.register({ isGlobal: true }),
25
+ IdsModule.register({ isGlobal: true }),
23
26
  LoggingModule.register({ isGlobal: true }),
27
+ HttpModule.register({ isGlobal: true }),
24
28
  I18nModule.register({
25
29
  isGlobal: true,
26
- bundles: [ERRORS_MESSAGES, HTTP_SECURITY_MESSAGES, IDENTITY_MESSAGES],
30
+ bundles: [HTTP_MESSAGES, ERRORS_MESSAGES, HTTP_SECURITY_MESSAGES, IDENTITY_MESSAGES],
27
31
  }),
28
32
  ErrorsModule.register({
29
33
  isGlobal: true,
30
34
  kinds: [
35
+ HTTP_ERROR_KINDS,
31
36
  CONFIG_ERROR_KINDS,
32
37
  DATABASE_ERROR_KINDS,
33
38
  HTTP_SECURITY_ERROR_KINDS,
@@ -2,7 +2,8 @@ import { NestFactory } from "@nestjs/core"
2
2
  import { SystemClockService } from "@modules/platform/clock"
3
3
  import { EnvSource } from "@modules/platform/config"
4
4
  import { applyHttpSecurity } from "@modules/platform/http-security"
5
- import { createJsonLogger, LoggingLogEvent } from "@modules/platform/logging"
5
+ import { SystemIds } from "@modules/platform/ids"
6
+ import { applyCorrelation, createJsonLogger, createNestLogger, LoggingLogEvent } from "@modules/platform/logging"
6
7
  import { AppModule } from "./app.module"
7
8
  import { read{{appPascal}}Env } from "./{{app}}.env"
8
9
 
@@ -10,17 +11,19 @@ import { read{{appPascal}}Env } from "./{{app}}.env"
10
11
  const bootstrap = async (): Promise<void> => {
11
12
  const env = EnvSource.fromProcess()
12
13
  const options = read{{appPascal}}Env(env)
13
- const app = await NestFactory.create(AppModule.register(options), { rawBody: true })
14
+ const logger = createJsonLogger(new SystemClockService(), new SystemIds())
15
+ const app = await NestFactory.create(AppModule.register(options), { rawBody: true, logger: createNestLogger(logger) })
16
+ applyCorrelation(app)
14
17
  applyHttpSecurity(app, options.httpSecurity, { trustProxy: 1 })
15
18
  app.enableShutdownHooks()
16
19
  await app.listen(options.server.port)
17
- createJsonLogger(new SystemClockService()).info(LoggingLogEvent.ServerStarted, {
20
+ logger.info(LoggingLogEvent.ServerStarted, {
18
21
  service: "{{app}}",
19
22
  port: options.server.port,
20
23
  })
21
24
  }
22
25
 
23
26
  bootstrap().catch((error: unknown) => {
24
- createJsonLogger(new SystemClockService()).error(LoggingLogEvent.StartupFailed, error, { service: "{{app}}" })
27
+ createJsonLogger(new SystemClockService(), new SystemIds()).error(LoggingLogEvent.StartupFailed, error, { service: "{{app}}" })
25
28
  process.exitCode = 1
26
29
  })
@@ -3,6 +3,8 @@ import { noteEntities, noteMigrations } from "@modules/domain/note"
3
3
  import { ClockModule } from "@modules/platform/clock"
4
4
  import { DatabaseModule } from "@modules/platform/database"
5
5
  import type { DatabaseConnectionConfig, DatabaseConnectionOptions } from "@modules/platform/database"
6
+ import { IdsModule } from "@modules/platform/ids"
7
+ import { HTTP_ERROR_KINDS, HTTP_MESSAGES, HttpModule } from "@modules/platform/http"
6
8
  import { LoggingModule } from "@modules/platform/logging"
7
9
  import type { CliAppOptions } from "../cli.options"
8
10
 
@@ -16,7 +18,9 @@ const primaryConnectionOf = (primary: DatabaseConnectionConfig): DatabaseConnect
16
18
  /** The clock, logger and declared database connections shared by CLI commands. */
17
19
  export const platformModules = (options: CliAppOptions): Array<DynamicModule> => [
18
20
  ClockModule.register({ isGlobal: true }),
21
+ IdsModule.register({ isGlobal: true }),
19
22
  LoggingModule.register({ isGlobal: true }),
23
+ HttpModule.register({ isGlobal: true }),
20
24
  DatabaseModule.register({
21
25
  isGlobal: true,
22
26
  connections: [primaryConnectionOf(options.database)],
@@ -2,16 +2,17 @@ import "reflect-metadata"
2
2
  import { CommandFactory } from "nest-commander"
3
3
  import { SystemClockService } from "@modules/platform/clock"
4
4
  import { EnvSource } from "@modules/platform/config"
5
- import { createJsonLogger, LoggingLogEvent } from "@modules/platform/logging"
5
+ import { SystemIds } from "@modules/platform/ids"
6
+ import { createJsonLogger, createNestLogger, LoggingLogEvent } from "@modules/platform/logging"
6
7
  import { AppModule } from "./app.module"
7
8
  import { readCliEnv } from "./cli.env"
8
9
  import { createCliAppOptions } from "./cli.options"
9
10
 
10
11
  /** The command line of the back end: reads the environment once and runs the command its arguments name (`cli migrate run`). */
11
12
  async function bootstrap(): Promise<void> {
12
- const logger = createJsonLogger(new SystemClockService())
13
+ const logger = createJsonLogger(new SystemClockService(), new SystemIds())
13
14
  await CommandFactory.run(AppModule.register(createCliAppOptions(readCliEnv(EnvSource.fromProcess()))), {
14
- logger: false,
15
+ logger: createNestLogger(logger),
15
16
  errorHandler: (error: Error) => {
16
17
  logger.error(LoggingLogEvent.StartupFailed, error, { service: "cli" })
17
18
  process.exit(1)
@@ -20,6 +21,8 @@ async function bootstrap(): Promise<void> {
20
21
  }
21
22
 
22
23
  bootstrap().catch((error: unknown) => {
23
- createJsonLogger(new SystemClockService()).error(LoggingLogEvent.StartupFailed, error, { service: "cli" })
24
+ createJsonLogger(new SystemClockService(), new SystemIds()).error(LoggingLogEvent.StartupFailed, error, {
25
+ service: "cli",
26
+ })
24
27
  process.exit(1)
25
28
  })
@@ -5,7 +5,7 @@ export { MigrationRunnerService } from "./migration-runner.service"
5
5
  export { DEFAULT_SEED_ENV } from "./seed-connections.client"
6
6
  export { SeedRunnerService } from "./seed-runner.service"
7
7
  export type { DatabaseConnectionConfig, DatabaseConnectionOptions } from "./database.options"
8
- export { LIST_ROWS_MAX, sql } from "./database.sql"
8
+ export { BATCH_ROWS, LIST_ROWS_MAX, sql } from "./database.sql"
9
9
  export { DATABASE_ERROR_KINDS } from "./errors/database.error"
10
10
  export { parsePrimaryDatabaseConfig } from "./primary.config"
11
11
  export { InjectPrimaryEntityManager, PRIMARY_ENTITY_MANAGER } from "./primary.decorators"
@@ -0,0 +1,22 @@
1
+ import { DomainError } from "@modules/platform/errors"
2
+ import type { ErrorKind } from "@modules/platform/errors"
3
+
4
+ /** Codes of the http capability: how an outbound call failed before an answer could be used. */
5
+ export enum HttpErrorCode {
6
+ /** The other side did not answer within the deadline. */
7
+ Timeout = "HTTP_TIMEOUT",
8
+ /** The other side could not be reached. */
9
+ Network = "HTTP_NETWORK",
10
+ /** The other side answered with a body that is not JSON. */
11
+ BodyUnreadable = "HTTP_BODY_UNREADABLE",
12
+ }
13
+
14
+ /** How each http code travels. */
15
+ export const HTTP_ERROR_KINDS: Record<HttpErrorCode, ErrorKind> = {
16
+ [HttpErrorCode.Timeout]: "unavailable",
17
+ [HttpErrorCode.Network]: "unavailable",
18
+ [HttpErrorCode.BodyUnreadable]: "internal",
19
+ }
20
+
21
+ /** The one error class of the http capability. */
22
+ export class HttpError extends DomainError<HttpErrorCode> {}
@@ -0,0 +1,235 @@
1
+ import { Test } from "@nestjs/testing"
2
+ import { mock } from "@starci/jest-preset"
3
+ import { HttpErrorCode } from "./errors/http.error"
4
+ import { FetchHttpClient } from "./fetch-http-client.client"
5
+
6
+ /** Asymmetric matchers read as `unknown`: jest types them `any`, which may not flow into a property or a typed argument. */
7
+ const anyOf = (type: unknown): unknown => expect.any(type)
8
+ const objectLike = (shape: Record<string, unknown>): unknown => expect.objectContaining(shape)
9
+
10
+ const build = async (): Promise<FetchHttpClient> => {
11
+ const moduleRef = await Test.createTestingModule({ providers: [FetchHttpClient] }).compile()
12
+ return moduleRef.get(FetchHttpClient)
13
+ }
14
+
15
+ const responseOf = (status = 200) => mock<Response>({ status })
16
+
17
+ afterEach(() => {
18
+ jest.restoreAllMocks()
19
+ })
20
+
21
+ describe("FetchHttpClient JSON requests", () => {
22
+ it("propagates the explicit id once while preserving unrelated headers", async () => {
23
+ const client = await build()
24
+ const response = responseOf()
25
+ response.text.mockResolvedValue("{}")
26
+ const send = jest.spyOn(global, "fetch").mockResolvedValue(response)
27
+
28
+ await client.request({
29
+ correlationId: "request-explicit",
30
+ method: "GET",
31
+ url: "https://service.test/orders",
32
+ headers: {
33
+ "X-Request-ID": "spoofed",
34
+ "x-request-id": "also-spoofed",
35
+ TraceParent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
36
+ accept: "application/json",
37
+ },
38
+ timeoutMs: 2500,
39
+ })
40
+
41
+ expect(send).toHaveBeenCalledWith(
42
+ "https://service.test/orders",
43
+ objectLike({ headers: { accept: "application/json", "x-request-id": "request-explicit" } }),
44
+ )
45
+ })
46
+
47
+ it("sends a JSON request and parses its JSON response", async () => {
48
+ const client = await build()
49
+ const response = responseOf(201)
50
+ response.text.mockResolvedValue('{"created":true}')
51
+ const send = jest.spyOn(global, "fetch").mockResolvedValue(response)
52
+
53
+ await expect(
54
+ client.request({
55
+ correlationId: "request-1",
56
+ method: "POST",
57
+ url: "https://service.test/orders",
58
+ headers: { authorization: "Bearer token" },
59
+ body: { orderId: "order-1" },
60
+ timeoutMs: 2500,
61
+ }),
62
+ ).resolves.toEqual({ status: 201, body: { created: true } })
63
+
64
+ expect(send).toHaveBeenCalledWith("https://service.test/orders", {
65
+ method: "POST",
66
+ headers: { "content-type": "application/json", authorization: "Bearer token", "x-request-id": "request-1" },
67
+ body: JSON.stringify({ orderId: "order-1" }),
68
+ signal: anyOf(AbortSignal),
69
+ })
70
+ })
71
+
72
+ it("returns undefined for an empty response body", async () => {
73
+ const client = await build()
74
+ const response = responseOf(204)
75
+ response.text.mockResolvedValue("")
76
+ jest.spyOn(global, "fetch").mockResolvedValue(response)
77
+
78
+ await expect(
79
+ client.request({
80
+ correlationId: "request-1",
81
+ method: "GET",
82
+ url: "https://service.test/health",
83
+ timeoutMs: 2500,
84
+ }),
85
+ ).resolves.toEqual({ status: 204, body: undefined })
86
+
87
+ expect(fetch).toHaveBeenCalledWith(
88
+ "https://service.test/health",
89
+ objectLike({ headers: { "x-request-id": "request-1" } }),
90
+ )
91
+ expect(jest.mocked(fetch).mock.calls[0]?.[1]).not.toHaveProperty("body")
92
+ })
93
+ })
94
+
95
+ describe("FetchHttpClient form and byte requests", () => {
96
+ it("encodes form fields and combines a caller cancellation signal with the deadline", async () => {
97
+ const client = await build()
98
+ const response = responseOf()
99
+ response.text.mockResolvedValue("{}")
100
+ const send = jest.spyOn(global, "fetch").mockResolvedValue(response)
101
+ const caller = new AbortController()
102
+
103
+ await client.request({
104
+ correlationId: "request-1",
105
+ method: "POST",
106
+ url: "https://service.test/token",
107
+ form: { grant_type: "client_credentials", scope: "orders read" },
108
+ timeoutMs: 2500,
109
+ signal: caller.signal,
110
+ })
111
+
112
+ expect(send).toHaveBeenCalledWith(
113
+ "https://service.test/token",
114
+ objectLike({
115
+ headers: { "content-type": "application/x-www-form-urlencoded", "x-request-id": "request-1" },
116
+ body: "grant_type=client_credentials&scope=orders+read",
117
+ signal: anyOf(AbortSignal),
118
+ }),
119
+ )
120
+ expect(send.mock.calls[0]?.[1]?.signal).not.toBe(caller.signal)
121
+ })
122
+
123
+ it("sends and receives raw bytes", async () => {
124
+ const client = await build()
125
+ const response = responseOf(206)
126
+ response.arrayBuffer.mockResolvedValue(Uint8Array.from([4, 5, 6]).buffer)
127
+ const send = jest.spyOn(global, "fetch").mockResolvedValue(response)
128
+ const bytes = Buffer.from([1, 2, 3])
129
+
130
+ await expect(
131
+ client.request({
132
+ correlationId: "request-1",
133
+ method: "PUT",
134
+ url: "https://service.test/object",
135
+ bytes,
136
+ read: "bytes",
137
+ timeoutMs: 2500,
138
+ }),
139
+ ).resolves.toEqual({ status: 206, body: Buffer.from([4, 5, 6]) })
140
+
141
+ expect(send).toHaveBeenCalledWith(
142
+ "https://service.test/object",
143
+ objectLike({
144
+ headers: { "content-type": "application/octet-stream", "x-request-id": "request-1" },
145
+ body: bytes,
146
+ }),
147
+ )
148
+ })
149
+ })
150
+
151
+ describe("FetchHttpClient response decoding", () => {
152
+ it("reports a response that is not JSON", async () => {
153
+ const client = await build()
154
+ const response = responseOf()
155
+ response.text.mockResolvedValue("not JSON")
156
+ jest.spyOn(global, "fetch").mockResolvedValue(response)
157
+
158
+ await expect(
159
+ client.request({
160
+ correlationId: "request-1",
161
+ method: "GET",
162
+ url: "https://service.test/orders",
163
+ timeoutMs: 2500,
164
+ }),
165
+ ).rejects.toMatchObject({ code: HttpErrorCode.BodyUnreadable, cause: anyOf(SyntaxError) })
166
+ })
167
+ })
168
+
169
+ describe("FetchHttpClient request failures", () => {
170
+ it("maps a failure before headers to a network error", async () => {
171
+ const client = await build()
172
+ jest.spyOn(global, "fetch").mockRejectedValue("connection refused")
173
+
174
+ await expect(
175
+ client.request({
176
+ correlationId: "request-1",
177
+ method: "GET",
178
+ url: "https://service.test/orders",
179
+ timeoutMs: 2500,
180
+ }),
181
+ ).rejects.toMatchObject({ code: HttpErrorCode.Network, cause: "connection refused" })
182
+ })
183
+
184
+ it("maps a deadline failure before headers to a timeout", async () => {
185
+ const client = await build()
186
+ const failure = { name: "TimeoutError" }
187
+ jest.spyOn(global, "fetch").mockRejectedValue(failure)
188
+
189
+ await expect(
190
+ client.request({
191
+ correlationId: "request-1",
192
+ method: "GET",
193
+ url: "https://service.test/orders",
194
+ timeoutMs: 2500,
195
+ }),
196
+ ).rejects.toMatchObject({ code: HttpErrorCode.Timeout, cause: failure })
197
+ })
198
+ })
199
+
200
+ describe("FetchHttpClient response stream failures", () => {
201
+ it("maps a failure while reading a response to a network error", async () => {
202
+ const client = await build()
203
+ const failure = new Error("stream cut")
204
+ const response = responseOf()
205
+ response.text.mockRejectedValue(failure)
206
+ jest.spyOn(global, "fetch").mockResolvedValue(response)
207
+
208
+ await expect(
209
+ client.request({
210
+ correlationId: "request-1",
211
+ method: "GET",
212
+ url: "https://service.test/orders",
213
+ timeoutMs: 2500,
214
+ }),
215
+ ).rejects.toMatchObject({ code: HttpErrorCode.Network, cause: failure })
216
+ })
217
+
218
+ it("maps a deadline while reading bytes to a timeout", async () => {
219
+ const client = await build()
220
+ const failure = { name: "TimeoutError" }
221
+ const response = responseOf()
222
+ response.arrayBuffer.mockRejectedValue(failure)
223
+ jest.spyOn(global, "fetch").mockResolvedValue(response)
224
+
225
+ await expect(
226
+ client.request({
227
+ correlationId: "request-1",
228
+ method: "GET",
229
+ url: "https://service.test/object",
230
+ read: "bytes",
231
+ timeoutMs: 2500,
232
+ }),
233
+ ).rejects.toMatchObject({ code: HttpErrorCode.Timeout, cause: failure })
234
+ })
235
+ })
@@ -0,0 +1,85 @@
1
+ import { Injectable } from "@nestjs/common"
2
+ import { isRecord } from "@modules/platform/primitives"
3
+ import { HttpError, HttpErrorCode } from "./errors/http.error"
4
+ import type { HttpClient, HttpRequest, HttpResponse } from "./http.port"
5
+
6
+ @Injectable()
7
+ /** The HttpClient adapter over the global `fetch`; the only place outside a spec that calls it. */
8
+ export class FetchHttpClient implements HttpClient {
9
+ /** Sends `request` with its deadline and reads the body: parsed JSON, or the raw bytes when the request asks for them. */
10
+ async request(request: HttpRequest): Promise<HttpResponse> {
11
+ const deadline = AbortSignal.timeout(request.timeoutMs)
12
+ const signal = request.signal ? AbortSignal.any([deadline, request.signal]) : deadline
13
+ const response = await this.send(request, signal)
14
+ if (request.read === "bytes")
15
+ return {
16
+ status: response.status,
17
+ body: await this.received(response.arrayBuffer().then((bytes) => Buffer.from(bytes))),
18
+ }
19
+ const text = await this.received(response.text())
20
+ return { status: response.status, body: text === "" ? undefined : this.parse(text) }
21
+ }
22
+
23
+ /** The body as it arrives; a deadline or a cut while it streams is the same HttpError as one before the headers. */
24
+ private async received<T>(reading: Promise<T>): Promise<T> {
25
+ try {
26
+ return await reading
27
+ } catch (cause) {
28
+ throw this.failure(cause)
29
+ }
30
+ }
31
+
32
+ private failure(cause: unknown): HttpError {
33
+ // The deadline rejects with a DOMException from the runtime's own realm: compare its name, never its prototype chain.
34
+ const timedOut = isRecord(cause) && cause.name === "TimeoutError"
35
+ return new HttpError({ code: timedOut ? HttpErrorCode.Timeout : HttpErrorCode.Network, cause })
36
+ }
37
+
38
+ private async send(request: HttpRequest, signal: AbortSignal): Promise<Response> {
39
+ try {
40
+ const body = this.bodyOf(request)
41
+ return await fetch(request.url, {
42
+ method: request.method,
43
+ headers: this.headersOf(request),
44
+ ...(body === undefined ? {} : { body }),
45
+ signal,
46
+ })
47
+ } catch (cause) {
48
+ throw this.failure(cause)
49
+ }
50
+ }
51
+
52
+ /** The explicit id owns correlation; a copied traceparent would take precedence at the receiving logger. */
53
+ private headersOf(request: HttpRequest): Record<string, string> {
54
+ const extra = Object.fromEntries(
55
+ Object.entries(request.headers ?? {}).filter(
56
+ ([name]) => !["x-request-id", "traceparent"].includes(name.toLowerCase()),
57
+ ),
58
+ )
59
+ return { ...this.contentTypeOf(request), ...extra, "x-request-id": request.correlationId }
60
+ }
61
+
62
+ private contentTypeOf(request: HttpRequest): Record<string, string> {
63
+ if (request.form !== undefined) return { "content-type": "application/x-www-form-urlencoded" }
64
+ if (request.bytes !== undefined) return { "content-type": "application/octet-stream" }
65
+ return request.body === undefined ? {} : { "content-type": "application/json" }
66
+ }
67
+
68
+ private bodyOf(request: HttpRequest): string | Buffer | undefined {
69
+ return request.bytes ?? this.textOf(request)
70
+ }
71
+
72
+ /** The text body of a request that carries no bytes: the form encoded, else the JSON body, else none. */
73
+ private textOf(request: HttpRequest): string | undefined {
74
+ if (request.form !== undefined) return new URLSearchParams(request.form).toString()
75
+ return request.body === undefined ? undefined : JSON.stringify(request.body)
76
+ }
77
+
78
+ private parse(text: string): unknown {
79
+ try {
80
+ return JSON.parse(text)
81
+ } catch (cause) {
82
+ throw new HttpError({ code: HttpErrorCode.BodyUnreadable, cause })
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,92 @@
1
+ import { Test } from "@nestjs/testing"
2
+ import { mock } from "@starci/jest-preset"
3
+ import type { HttpClient } from "./http.port"
4
+ import { callGraphql } from "./graphql-call.mapper"
5
+
6
+ const SUBJECT: unique symbol = Symbol("http/graphql-call.mapper")
7
+ const subjectFunctions = () => ({ callGraphql })
8
+
9
+ const build = async () => {
10
+ const moduleRef = await Test.createTestingModule({
11
+ providers: [{ provide: SUBJECT, useFactory: subjectFunctions }],
12
+ }).compile()
13
+ return moduleRef.get<ReturnType<typeof subjectFunctions>>(SUBJECT)
14
+ }
15
+
16
+ describe("callGraphql", () => {
17
+ it("sends the operation and reads its data and string error codes", async () => {
18
+ const subject = await build()
19
+
20
+ const http = mock<HttpClient>()
21
+ http.request.mockResolvedValue({
22
+ status: 200,
23
+ body: {
24
+ data: { order: { id: "order-1" } },
25
+ errors: [
26
+ { extensions: { code: "ORDER_STALE" } },
27
+ { extensions: { code: 7 } },
28
+ { extensions: null },
29
+ "malformed",
30
+ ],
31
+ },
32
+ })
33
+
34
+ await expect(
35
+ subject.callGraphql(http, {
36
+ correlationId: "request-1",
37
+ url: "https://order.test/graphql",
38
+ query: "query Order($id: ID!) { order(id: $id) { id } }",
39
+ variables: { id: "order-1" },
40
+ headers: { authorization: "Bearer token" },
41
+ timeoutMs: 2500,
42
+ }),
43
+ ).resolves.toEqual({ data: { order: { id: "order-1" } }, errorCodes: ["ORDER_STALE"] })
44
+
45
+ expect(http.request).toHaveBeenCalledWith({
46
+ correlationId: "request-1",
47
+ method: "POST",
48
+ url: "https://order.test/graphql",
49
+ headers: { authorization: "Bearer token" },
50
+ body: {
51
+ query: "query Order($id: ID!) { order(id: $id) { id } }",
52
+ variables: { id: "order-1" },
53
+ },
54
+ timeoutMs: 2500,
55
+ })
56
+ })
57
+
58
+ it("returns an empty answer when data and errors do not have GraphQL shapes", async () => {
59
+ const subject = await build()
60
+
61
+ const http = mock<HttpClient>()
62
+ http.request.mockResolvedValue({ status: 200, body: { data: "malformed", errors: {} } })
63
+
64
+ await expect(
65
+ subject.callGraphql(http, {
66
+ correlationId: "request-1",
67
+ url: "https://order.test/graphql",
68
+ query: "query Health { health }",
69
+ timeoutMs: 1000,
70
+ }),
71
+ ).resolves.toEqual({ data: undefined, errorCodes: [] })
72
+ })
73
+
74
+ it.each([undefined, null, "not a response"])(
75
+ "returns null when the response body is not an object (%p)",
76
+ async (body) => {
77
+ const subject = await build()
78
+
79
+ const http = mock<HttpClient>()
80
+ http.request.mockResolvedValue({ status: 502, body })
81
+
82
+ await expect(
83
+ subject.callGraphql(http, {
84
+ correlationId: "request-1",
85
+ url: "https://order.test/graphql",
86
+ query: "query Health { health }",
87
+ timeoutMs: 1000,
88
+ }),
89
+ ).resolves.toBeNull()
90
+ },
91
+ )
92
+ })
@@ -0,0 +1,52 @@
1
+ import { isRecord } from "@modules/platform/primitives"
2
+ import type { HttpClient } from "./http.port"
3
+
4
+ /** One GraphQL operation sent to another service. */
5
+ export interface GraphqlCall {
6
+ /** The correlation id of the request or delivery causing this operation. */
7
+ readonly correlationId: string
8
+ /** The GraphQL endpoint URL. */
9
+ readonly url: string
10
+ /** The operation text. */
11
+ readonly query: string
12
+ /** The operation variables. */
13
+ readonly variables?: Readonly<Record<string, unknown>>
14
+ /** Extra request headers, such as the bearer token to forward. */
15
+ readonly headers?: Readonly<Record<string, string>>
16
+ /** How long to wait for the answer. */
17
+ readonly timeoutMs: number
18
+ }
19
+
20
+ /** What a GraphQL service answered: the data object and the codes of the errors it reported. */
21
+ export interface GraphqlAnswer {
22
+ /** The `data` object, undefined when the operation failed before producing one. */
23
+ readonly data: Readonly<Record<string, unknown>> | undefined
24
+ /** The `extensions.code` of every reported error. */
25
+ readonly errorCodes: ReadonlyArray<string>
26
+ }
27
+
28
+ /** What a GraphQL call yields: the answer, or null when the body is not a GraphQL response at all. */
29
+ export type GraphqlCallResult = GraphqlAnswer | null
30
+
31
+ const codesOf = (errors: unknown): Array<string> =>
32
+ Array.isArray(errors)
33
+ ? errors.flatMap((error): Array<string> => {
34
+ const code: unknown = isRecord(error) && isRecord(error.extensions) ? error.extensions.code : undefined
35
+ return typeof code === "string" ? [code] : []
36
+ })
37
+ : []
38
+
39
+ /** Sends a GraphQL operation and reads the answer; null when the body is not a GraphQL response at all. */
40
+ export const callGraphql = async (http: HttpClient, call: GraphqlCall): Promise<GraphqlCallResult> => {
41
+ const response = await http.request({
42
+ correlationId: call.correlationId,
43
+ method: "POST",
44
+ url: call.url,
45
+ ...(call.headers === undefined ? {} : { headers: call.headers }),
46
+ body: { query: call.query, variables: call.variables },
47
+ timeoutMs: call.timeoutMs,
48
+ })
49
+ const body = response.body
50
+ if (!isRecord(body)) return null
51
+ return { data: isRecord(body.data) ? body.data : undefined, errorCodes: codesOf(body.errors) }
52
+ }
@@ -0,0 +1,9 @@
1
+ import { injector } from "@modules/platform/composition"
2
+ import type { TypedParameterDecorator } from "@modules/platform/composition"
3
+ import type { HttpClient } from "./http.port"
4
+
5
+ /** Token of the HttpClient port. */
6
+ export const HTTP_CLIENT: unique symbol = Symbol("platform.http.client")
7
+
8
+ /** Injects the HttpClient port. Parameter type: HttpClient. */
9
+ export const InjectHttpClient = (): TypedParameterDecorator<HttpClient> => injector<HttpClient>(HTTP_CLIENT)
@@ -0,0 +1,7 @@
1
+ import { ConfigurableModuleBuilder } from "@nestjs/common"
2
+ import type { HttpOptions } from "./http.options"
3
+
4
+ /** The configurable-module base of the http capability; `isGlobal` is decided by the app root. */
5
+ export const { ConfigurableModuleClass, OPTIONS_TYPE } = new ConfigurableModuleBuilder<HttpOptions>()
6
+ .setExtras({ isGlobal: false }, (definition, extras) => ({ ...definition, global: extras.isGlobal }))
7
+ .build()
@@ -0,0 +1,19 @@
1
+ import { Module } from "@nestjs/common"
2
+ import type { DynamicModule } from "@nestjs/common"
3
+ import { FetchHttpClient } from "./fetch-http-client.client"
4
+ import { HTTP_CLIENT } from "./http.decorators"
5
+ import { ConfigurableModuleClass, OPTIONS_TYPE } from "./http.module-definition"
6
+
7
+ @Module({})
8
+ /** Provides the HttpClient port backed by `fetch`. */
9
+ export class HttpModule extends ConfigurableModuleClass {
10
+ /** Registers the capability once per app. */
11
+ static register(options: typeof OPTIONS_TYPE): DynamicModule {
12
+ const base = super.register(options)
13
+ return {
14
+ ...base,
15
+ providers: [...(base.providers ?? []), { provide: HTTP_CLIENT, useClass: FetchHttpClient }],
16
+ exports: [HTTP_CLIENT],
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,2 @@
1
+ /** The http capability takes no options; the empty record keeps `isGlobal` assignable through the configurable module. */
2
+ export type HttpOptions = Record<never, never>