@starci/hfs 4.3.0 → 4.4.1

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 (538) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/db/migrate.mjs +361 -0
  4. package/db/post-cutover.mjs +136 -0
  5. package/emit/contracts.mjs +2 -2
  6. package/emit/db-types.mjs +12 -3
  7. package/lint/run.mjs +3 -3
  8. package/package.json +4 -1
  9. package/release/gate.mjs +298 -0
  10. package/release/index.mjs +103 -0
  11. package/release/push.mjs +110 -0
  12. package/report/sonar.mjs +1 -1
  13. package/runtime/config.example.yaml +7 -6
  14. package/runtime/engine/config.mjs +10 -18
  15. package/runtime/engine/removed-vocabulary.mjs +0 -2
  16. package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
  17. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  18. package/runtime/knowledge/hfs/rules.yaml +2855 -163
  19. package/runtime/knowledge/hfs/slots.yaml +333 -62
  20. package/runtime/knowledge/patterns/be/api.yaml +32 -5
  21. package/runtime/knowledge/patterns/be/cli.yaml +14 -4
  22. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  23. package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
  24. package/runtime/knowledge/patterns/be/projections.yaml +4 -2
  25. package/runtime/knowledge/patterns/be/queues.yaml +11 -3
  26. package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
  27. package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
  28. package/runtime/knowledge/patterns/be/saga.yaml +20 -3
  29. package/runtime/knowledge/patterns/be/webhooks.yaml +23 -4
  30. package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
  31. package/runtime/knowledge/sonar-gate.yaml +1 -1
  32. package/runtime/modules/kernel/failure-codes.yaml +3091 -43
  33. package/runtime/modules/kernel/removed-vocabulary.yaml +30 -0
  34. package/runtime/modules/models/runtimes.yaml +5 -7
  35. package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
  36. package/runtime/packages/cli/src/catalog.generated.mjs +13769 -0
  37. package/runtime/packages/cli/src/validate-args.mjs +213 -0
  38. package/runtime/scripts/api/git/add.mjs +5 -0
  39. package/runtime/scripts/api/git/init.mjs +7 -0
  40. package/runtime/scripts/api/git/lib.mjs +4 -1
  41. package/runtime/scripts/api/git/tag.mjs +5 -0
  42. package/runtime/scripts/api/node/lib.mjs +20 -0
  43. package/runtime/scripts/api/node/run-node.mjs +6 -0
  44. package/runtime/scripts/api/sops/lib.mjs +42 -1
  45. package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
  46. package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
  47. package/runtime/scripts/api/supabase/database-query.mjs +5 -0
  48. package/runtime/scripts/api/supabase/lib.mjs +44 -0
  49. package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
  50. package/runtime/scripts/hfs/app-wiring.mjs +201 -0
  51. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +1 -8
  52. package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
  53. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  54. package/runtime/scripts/hfs/architecture/config.mjs +1 -1
  55. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  56. package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
  57. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  58. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  59. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  60. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  61. package/runtime/scripts/hfs/architecture/index.mjs +1 -1
  62. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
  63. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  64. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  65. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  66. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  67. package/runtime/scripts/hfs/architecture/register-once.mjs +2 -2
  68. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  69. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
  70. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
  71. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  72. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  73. package/runtime/scripts/hfs/architecture/tiers.mjs +4 -4
  74. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
  75. package/runtime/scripts/hfs/architecture.mjs +1 -1
  76. package/runtime/scripts/hfs/check-results.mjs +28 -0
  77. package/runtime/scripts/hfs/check.mjs +46 -47
  78. package/runtime/scripts/hfs/coverage-from.mjs +70 -0
  79. package/runtime/scripts/hfs/coverage-scope.mjs +1 -1
  80. package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
  81. package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
  82. package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
  83. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  84. package/runtime/scripts/hfs/doc-commands.mjs +367 -0
  85. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  86. package/runtime/scripts/hfs/example-secrets.mjs +158 -0
  87. package/runtime/scripts/hfs/manifest-shape.mjs +69 -9
  88. package/runtime/scripts/hfs/param-names.mjs +1 -1
  89. package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
  90. package/runtime/scripts/hfs/rule-catalog.mjs +158 -11
  91. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  92. package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
  93. package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
  94. package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
  95. package/runtime/scripts/hfs/rules/assets.mjs +79 -0
  96. package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
  97. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
  98. package/runtime/scripts/hfs/rules/cli.mjs +1 -1
  99. package/runtime/scripts/hfs/rules/contract-compat.mjs +1 -1
  100. package/runtime/scripts/hfs/rules/contract.mjs +67 -6
  101. package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
  102. package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
  103. package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
  104. package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
  105. package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
  106. package/runtime/scripts/hfs/rules/database-migrations.mjs +286 -7
  107. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
  108. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
  109. package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
  110. package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
  111. package/runtime/scripts/hfs/rules/database.mjs +9 -0
  112. package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
  113. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
  114. package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
  115. package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
  116. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  117. package/runtime/scripts/hfs/rules/edition.mjs +9 -8
  118. package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
  119. package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
  120. package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
  121. package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
  122. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
  123. package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
  124. package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
  125. package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
  126. package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
  127. package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
  128. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
  129. package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
  130. package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
  131. package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
  132. package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
  133. package/runtime/scripts/hfs/rules/saga.mjs +8 -16
  134. package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
  135. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
  136. package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
  137. package/runtime/scripts/hfs/rules/services.mjs +4 -4
  138. package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
  139. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
  140. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
  141. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
  142. package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
  143. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +41 -1
  144. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
  145. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +2 -2
  146. package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
  147. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
  148. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
  149. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
  150. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
  151. package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
  152. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
  153. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
  154. package/runtime/scripts/hfs/sealed-members.mjs +101 -0
  155. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
  156. package/runtime/scripts/hfs/secret-edit.mjs +170 -0
  157. package/runtime/scripts/hfs/secret-init.mjs +127 -19
  158. package/runtime/scripts/hfs/secret-sops.mjs +38 -6
  159. package/runtime/scripts/hfs/secret-store.mjs +56 -54
  160. package/runtime/scripts/hfs/secret.mjs +34 -5
  161. package/runtime/scripts/hfs/slot-imports.mjs +18 -5
  162. package/runtime/scripts/hfs/slots.mjs +3 -2
  163. package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
  164. package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
  165. package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
  166. package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
  167. package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
  168. package/runtime/scripts/hfs/typescript-programs.mjs +0 -8
  169. package/runtime/scripts/lib/command-tokens.mjs +126 -0
  170. package/runtime/scripts/lib/compose-values.mjs +11 -0
  171. package/runtime/scripts/lib/deployment-findings.mjs +7 -0
  172. package/runtime/scripts/lib/env.mjs +2 -2
  173. package/runtime/scripts/lib/git.mjs +1 -1
  174. package/runtime/scripts/lib/i18n.mjs +0 -2
  175. package/runtime/scripts/lib/json-clone.mjs +61 -0
  176. package/runtime/scripts/lib/language.mjs +1 -1
  177. package/runtime/scripts/lib/list.mjs +4 -0
  178. package/runtime/scripts/lib/roots.mjs +2 -4
  179. package/runtime/scripts/lib/runtime-example.mjs +13 -0
  180. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  181. package/runtime/scripts/lib/sql-ast.mjs +32 -0
  182. package/runtime/scripts/lib/stack-services.mjs +1 -1
  183. package/runtime/scripts/lib/swarm-stack.mjs +5 -18
  184. package/runtime/scripts/lib/trim-end.mjs +8 -0
  185. package/runtime/scripts/lib/ts-analysis.mjs +5 -2
  186. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  187. package/runtime/scripts/lib/workflow-events.mjs +8 -0
  188. package/scaffold/add-app.mjs +77 -59
  189. package/scaffold/add-cli-lite.mjs +14 -6
  190. package/scaffold/add-compose.mjs +79 -0
  191. package/scaffold/add-database-lite.mjs +14 -0
  192. package/scaffold/add-fe-overlay.mjs +1 -0
  193. package/scaffold/add-fe-page.mjs +1 -0
  194. package/scaffold/add-inbox-lite.mjs +74 -42
  195. package/scaffold/add-table.mjs +110 -87
  196. package/scaffold/add.mjs +159 -44
  197. package/scaffold/app.mjs +124 -39
  198. package/scaffold/edition-gate.mjs +3 -3
  199. package/scaffold/lite-exports.mjs +2 -1
  200. package/scaffold/service.mjs +2 -2
  201. package/scaffold/supabase-ports.mjs +1 -1
  202. package/src/main.mjs +229 -116
  203. package/stack/age-install.mjs +37 -0
  204. package/stack/backup.mjs +100 -0
  205. package/stack/build-args.mjs +42 -0
  206. package/stack/build.mjs +173 -0
  207. package/stack/cli.mjs +130 -0
  208. package/stack/config.mjs +57 -0
  209. package/stack/context.mjs +57 -0
  210. package/stack/converge.mjs +107 -0
  211. package/stack/deploy.mjs +301 -0
  212. package/stack/digest.mjs +16 -0
  213. package/stack/docker-transport.mjs +53 -0
  214. package/stack/edge-alias.mjs +118 -0
  215. package/stack/errors.mjs +21 -0
  216. package/stack/image-scan.mjs +152 -0
  217. package/stack/ingress-create.mjs +114 -0
  218. package/stack/ingress-publish.mjs +273 -0
  219. package/stack/ingress.mjs +706 -0
  220. package/stack/jobs.mjs +51 -0
  221. package/stack/live-restore.mjs +177 -0
  222. package/stack/prepare.mjs +266 -0
  223. package/stack/private-file.mjs +22 -0
  224. package/stack/provision.mjs +90 -0
  225. package/stack/prune.mjs +46 -0
  226. package/stack/release-store.mjs +87 -0
  227. package/stack/remote.mjs +205 -0
  228. package/stack/render.mjs +149 -0
  229. package/stack/retire.mjs +47 -0
  230. package/stack/sealed.mjs +149 -0
  231. package/stack/task.mjs +42 -0
  232. package/stack/verify.mjs +99 -0
  233. package/sync/ci-commands.mjs +13 -0
  234. package/sync/format.mjs +1 -1
  235. package/sync/hygiene.mjs +1 -1
  236. package/sync/index.mjs +110 -54
  237. package/sync/stack-images.mjs +127 -0
  238. package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
  239. package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
  240. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
  241. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
  242. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
  243. package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
  244. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
  245. package/templates/app/codegen-graphql.mjs.partial +13 -2
  246. package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
  247. package/templates/app/codegen-prelude.mjs.partial +8 -0
  248. package/templates/app/gitignore +3 -0
  249. package/templates/app/gitignore-lite +3 -0
  250. package/templates/app/package-scripts/package.json +1 -0
  251. package/templates/app/package-scripts/supabase.json +7 -0
  252. package/templates/app/package-scripts-lite/package.json +1 -7
  253. package/templates/app/skeleton/.editorconfig +1 -0
  254. package/templates/app/skeleton/.gitattributes +7 -1
  255. package/templates/app/skeleton/.github/dependabot.yml +17 -0
  256. package/templates/app/skeleton/.nvmrc +1 -1
  257. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
  258. package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
  259. package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
  260. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
  261. package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
  262. package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
  263. package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
  264. package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
  265. package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  266. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  267. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  268. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
  269. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  270. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
  271. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
  272. package/templates/app/skeleton/README.md +6 -6
  273. package/templates/app/skeleton/scripts/codegen.mjs +1 -2
  274. package/templates/app/skeleton-lite/.editorconfig +16 -0
  275. package/templates/app/skeleton-lite/.gitattributes +8 -0
  276. package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
  277. package/templates/app/skeleton-lite/.nvmrc +1 -0
  278. package/templates/app/skeleton-lite/.sops.yaml +3 -2
  279. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +178 -1
  280. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
  281. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
  282. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
  283. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
  284. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
  285. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
  286. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
  287. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
  288. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
  289. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
  290. package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
  291. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
  292. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
  293. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  294. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  295. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  296. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
  297. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  298. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
  299. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
  300. package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
  301. package/templates/app/skeleton-lite/README.md +5 -3
  302. package/templates/app/skeleton-lite/supabase/config.toml +8 -1
  303. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
  304. package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
  305. package/templates/be/image/worker/Dockerfile +1 -1
  306. package/templates/be/inbox/database-client.spec.ts.tpl +17 -18
  307. package/templates/be/inbox/database-client.ts.tpl +12 -11
  308. package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
  309. package/templates/be/inbox/event-index.ts.tpl +1 -0
  310. package/templates/be/inbox/events.json.tpl +10 -0
  311. package/templates/be/inbox/migration.sql.tpl +1 -0
  312. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
  313. package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
  314. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
  315. package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
  316. package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
  317. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
  318. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
  319. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
  320. package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
  321. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
  322. package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
  323. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
  324. package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
  325. package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
  326. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
  327. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
  328. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
  329. package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
  330. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
  331. package/templates/be/inbox/platform/migration.sql.tpl +48 -0
  332. package/templates/be/inbox/service.spec.ts.tpl +42 -9
  333. package/templates/be/inbox/service.ts.tpl +23 -8
  334. package/templates/be/inbox/webhook-door.spec.ts.tpl +1 -1
  335. package/templates/be/inbox/webhook-door.ts.tpl +2 -2
  336. package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
  337. package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
  338. package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
  339. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
  340. package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
  341. package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
  342. package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
  343. package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
  344. package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
  345. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
  346. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
  347. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
  348. package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
  349. package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
  350. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
  351. package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
  352. package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
  353. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
  354. package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
  355. package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
  356. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
  357. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
  358. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
  359. package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
  360. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
  361. package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
  362. package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
  363. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
  364. package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
  365. package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
  366. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
  367. package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
  368. package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
  369. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
  370. package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
  371. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
  372. package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
  373. package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
  374. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
  375. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
  376. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
  377. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
  378. package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
  379. package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
  380. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
  381. package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
  382. package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
  383. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
  384. package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
  385. package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
  386. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
  387. package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
  388. package/templates/be/skeleton/apps/__app__/src/app.module.ts +11 -9
  389. package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
  390. package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
  391. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +2 -0
  392. package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
  393. package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
  394. package/templates/be/skeleton/apps/__app__/src/main.ts +15 -1
  395. package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
  396. package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -14
  397. package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
  398. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
  399. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
  400. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
  401. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
  402. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
  403. package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
  404. package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
  405. package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
  406. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
  407. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
  408. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
  409. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
  410. package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
  411. package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
  412. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
  413. package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
  414. package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
  415. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
  416. package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
  417. package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
  418. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
  419. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
  420. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
  421. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
  422. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
  423. package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
  424. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
  425. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
  426. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
  427. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
  428. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
  429. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
  430. package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
  431. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
  432. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
  433. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
  434. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  435. package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
  436. package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
  437. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
  438. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
  439. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
  440. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
  441. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
  442. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
  443. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
  444. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
  445. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
  446. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
  447. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
  448. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
  449. package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
  450. package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
  451. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
  452. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
  453. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
  454. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
  455. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
  456. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
  457. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
  458. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
  459. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
  460. package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
  461. package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
  462. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
  463. package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
  464. package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
  465. package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
  466. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
  467. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
  468. package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
  469. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +14 -18
  470. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
  471. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
  472. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
  473. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +2 -0
  474. package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
  475. package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
  476. package/templates/be/skeleton-lite/apps/api/src/main.ts +2 -3
  477. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
  478. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
  479. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
  480. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
  481. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
  482. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
  483. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
  484. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
  485. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
  486. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
  487. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
  488. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
  489. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
  490. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
  491. package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
  492. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
  493. package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
  494. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
  495. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  496. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
  497. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
  498. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
  499. package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
  500. package/templates/be/table/api/feature.module.ts.tpl +2 -1
  501. package/templates/be/table/api/feature.options.ts.tpl +2 -0
  502. package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
  503. package/templates/be/table/api/graphql.module.ts.tpl +3 -2
  504. package/templates/be/table/api/graphql.options.ts.tpl +2 -0
  505. package/templates/be/table/migration.sql.tpl +1 -0
  506. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
  507. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
  508. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
  509. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
  510. package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
  511. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
  512. package/templates/fe/auth/supabase-client.ts.tpl +34 -0
  513. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
  514. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
  515. package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
  516. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
  517. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
  518. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
  519. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
  520. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
  521. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
  522. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
  523. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
  524. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
  525. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
  526. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
  527. package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
  528. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
  529. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
  530. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
  531. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
  532. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
  533. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
  534. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
  535. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  536. package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
  537. package/upgrade/check.mjs +3 -1
  538. package/upgrade/index.mjs +54 -41
@@ -1,13 +1,65 @@
1
+ import { Test } from "@nestjs/testing"
1
2
  import { admit } from "./admission.policy"
2
- import { IdentityErrorCode } from "./errors/identity.error"
3
+ import { IdentityError, IdentityErrorCode } from "./errors/identity.error"
4
+ import type { PublicMetadata } from "./identity.contracts"
3
5
  import { PublicReason } from "./identity.contracts"
4
6
 
7
+ const ADMISSION: unique symbol = Symbol("identity.admission")
8
+
9
+ const runtimeMetadata = (source: string): PublicMetadata => {
10
+ try {
11
+ const metadata: PublicMetadata = JSON.parse(source)
12
+ return metadata
13
+ } catch (cause) {
14
+ throw new IdentityError({ code: IdentityErrorCode.Unauthenticated, cause })
15
+ }
16
+ }
17
+
5
18
  describe("admit", () => {
6
- it("admits a door that states why it is public", () => {
7
- expect(admit({ reason: PublicReason.Health })).toSucceedWith(PublicReason.Health)
19
+ let subject: typeof admit
20
+
21
+ beforeEach(async () => {
22
+ const moduleRef = await Test.createTestingModule({
23
+ providers: [{ provide: ADMISSION, useFactory: () => admit }],
24
+ }).compile()
25
+ subject = moduleRef.get<typeof admit>(ADMISSION)
26
+ })
27
+
28
+ it.each([PublicReason.Health, PublicReason.AuthHandshake, PublicReason.SignedWebhook, PublicReason.CatalogRead])(
29
+ "admits a door with the canonical public reason %s",
30
+ (reason) => {
31
+ expect(subject({ reason })).toSucceedWith(reason)
32
+ },
33
+ )
34
+
35
+ it("admits a credential door whose principal has the required kind", () => {
36
+ const principal = { kind: "api-key", subject: "key-1", workspaceId: "ws-1" } as const
37
+
38
+ expect(subject(undefined, { required: { kind: "api-key" }, principal })).toSucceedWith(principal)
39
+ })
40
+
41
+ it("refuses a credential door without a principal of the required kind", () => {
42
+ const principal = { kind: "tool-token", subject: "tool-1", workspaceId: "ws-1" } as const
43
+
44
+ expect(subject(undefined, { required: { kind: "api-key" }, principal })).toBeRefused(
45
+ IdentityErrorCode.Unauthenticated,
46
+ )
47
+ expect(subject(undefined, { required: { kind: "api-key" }, principal: undefined })).toBeRefused(
48
+ IdentityErrorCode.Unauthenticated,
49
+ )
8
50
  })
9
51
 
10
52
  it("refuses a door that is not public", () => {
11
- expect(admit(undefined)).toBeRefused(IdentityErrorCode.Unauthenticated)
53
+ expect(subject(undefined)).toBeRefused(IdentityErrorCode.Unauthenticated)
54
+ })
55
+
56
+ it("refuses an unknown public reason supplied as runtime metadata", () => {
57
+ const metadata = runtimeMetadata('{"reason":"tool-token"}')
58
+ expect(subject(metadata)).toBeRefused(IdentityErrorCode.Unauthenticated)
59
+ })
60
+
61
+ it("refuses runtime metadata without a public reason", () => {
62
+ const metadata = runtimeMetadata("{}")
63
+ expect(subject(metadata)).toBeRefused(IdentityErrorCode.Unauthenticated)
12
64
  })
13
65
  })
@@ -1,11 +1,42 @@
1
1
  import { ok, refused } from "@modules/platform/primitives"
2
2
  import type { Outcome } from "@modules/platform/primitives"
3
+ import type { CredentialMetadata, CredentialPrincipal } from "./credential.contracts"
3
4
  import { IdentityErrorCode } from "./errors/identity.error"
4
- import type { PublicMetadata, PublicReason } from "./identity.contracts"
5
+ import type { PublicMetadata } from "./identity.contracts"
6
+ import { PublicReason } from "./identity.contracts"
7
+
8
+ /** What a credential-marked door requires and what `CredentialGuard` established for it. */
9
+ export interface CredentialAdmission {
10
+ /** The `@Credential` metadata of the door. */
11
+ readonly required: CredentialMetadata
12
+ /** The principal `CredentialGuard` set on the request, when the credential authenticated. */
13
+ readonly principal: CredentialPrincipal | undefined
14
+ }
15
+
16
+ const admitCredential = ({
17
+ required,
18
+ principal,
19
+ }: CredentialAdmission): Outcome<CredentialPrincipal, IdentityErrorCode> =>
20
+ principal?.kind === required.kind ? ok(principal) : refused(IdentityErrorCode.Unauthenticated)
5
21
 
6
22
  /**
7
- * The default-deny decision of one door: a door that states why it is public is admitted; every other door is refused
8
- * until the design that adds sign-in establishes a caller here.
23
+ * The default-deny decision of one door: a credential-marked door is admitted only with its authenticated credential
24
+ * principal; a door with a canonical public reason is admitted; every other door is refused until the design that adds
25
+ * sign-in establishes a session principal here.
9
26
  */
10
- export const admit = (metadata: PublicMetadata | undefined): Outcome<PublicReason, IdentityErrorCode> =>
11
- metadata === undefined ? refused(IdentityErrorCode.Unauthenticated) : ok(metadata.reason)
27
+ export const admit = (
28
+ metadata: PublicMetadata | undefined,
29
+ credential?: CredentialAdmission,
30
+ ): Outcome<PublicReason | CredentialPrincipal, IdentityErrorCode> => {
31
+ if (credential !== undefined) return admitCredential(credential)
32
+ switch (metadata?.reason) {
33
+ case PublicReason.Health:
34
+ case PublicReason.AuthHandshake:
35
+ case PublicReason.SignedWebhook:
36
+ case PublicReason.CatalogRead:
37
+ return ok(metadata.reason)
38
+ case undefined:
39
+ default:
40
+ return refused(IdentityErrorCode.Unauthenticated)
41
+ }
42
+ }
@@ -1,37 +1,68 @@
1
1
  import type { ExecutionContext } from "@nestjs/common"
2
+ import type { HttpArgumentsHost } from "@nestjs/common/interfaces"
2
3
  import { Reflector } from "@nestjs/core"
3
4
  import { Test } from "@nestjs/testing"
4
5
  import { mock } from "@starci/jest-preset"
5
6
  import { AuthGuard } from "./auth.guard"
7
+ import type { CredentialMetadata, CredentialPrincipal } from "./credential.contracts"
6
8
  import { IdentityErrorCode } from "./errors/identity.error"
7
9
  import type { PublicMetadata } from "./identity.contracts"
8
10
  import { PublicReason } from "./identity.contracts"
11
+ import { CREDENTIAL_KEY, PUBLIC_KEY } from "./identity.decorators"
9
12
 
10
- const build = async (metadata: PublicMetadata | undefined) => {
13
+ interface Door {
14
+ readonly credential?: CredentialMetadata
15
+ readonly metadata?: PublicMetadata
16
+ readonly principal?: CredentialPrincipal
17
+ }
18
+
19
+ const build = async ({ credential, metadata }: Door) => {
11
20
  const reflector = mock<Reflector>()
12
- reflector.getAllAndOverride.mockReturnValue(metadata)
21
+ reflector.getAllAndOverride.mockImplementation((key: unknown) =>
22
+ key === CREDENTIAL_KEY ? credential : key === PUBLIC_KEY ? metadata : undefined,
23
+ )
13
24
  const moduleRef = await Test.createTestingModule({
14
25
  providers: [AuthGuard, { provide: Reflector, useValue: reflector }],
15
26
  }).compile()
16
- return { guard: moduleRef.get(AuthGuard), reflector }
27
+ return moduleRef.get(AuthGuard)
17
28
  }
18
29
 
19
- const contextOf = () => {
30
+ const contextOf = (principal?: CredentialPrincipal) => {
20
31
  const context = mock<ExecutionContext>()
21
32
  context.getHandler.mockReturnValue(() => undefined)
22
- context.getClass.mockReturnValue(class Door {})
33
+ context.getClass.mockReturnValue(class DoorClass {})
34
+ context.getType.mockReturnValue("http")
35
+ const host = mock<HttpArgumentsHost>()
36
+ host.getRequest.mockReturnValue({ credentialPrincipal: principal })
37
+ context.switchToHttp.mockReturnValue(host)
23
38
  return context
24
39
  }
25
40
 
41
+ const API_KEY: CredentialPrincipal = { kind: "api-key", subject: "key-1", workspaceId: "ws-1" }
42
+
26
43
  describe("AuthGuard", () => {
27
44
  it("lets a door through when it states why it is public", async () => {
28
- const { guard } = await build({ reason: PublicReason.Health })
45
+ const guard = await build({ metadata: { reason: PublicReason.Health } })
29
46
 
30
47
  expect(guard.canActivate(contextOf())).toBe(true)
31
48
  })
32
49
 
33
- it("refuses every door that is not public", async () => {
34
- const { guard } = await build(undefined)
50
+ it("refuses every door that is neither public nor credential-marked", async () => {
51
+ const guard = await build({})
52
+
53
+ expect(() => guard.canActivate(contextOf())).toThrow(
54
+ expect.objectContaining({ code: IdentityErrorCode.Unauthenticated }),
55
+ )
56
+ })
57
+
58
+ it("lets a credential door through once the credential principal is set", async () => {
59
+ const guard = await build({ credential: { kind: "api-key" } })
60
+
61
+ expect(guard.canActivate(contextOf(API_KEY))).toBe(true)
62
+ })
63
+
64
+ it("refuses a credential door without a credential principal", async () => {
65
+ const guard = await build({ credential: { kind: "api-key" } })
35
66
 
36
67
  expect(() => guard.canActivate(contextOf())).toThrow(
37
68
  expect.objectContaining({ code: IdentityErrorCode.Unauthenticated }),
@@ -2,24 +2,30 @@ import { Injectable } from "@nestjs/common"
2
2
  import type { CanActivate, ExecutionContext } from "@nestjs/common"
3
3
  import type { Reflector } from "@nestjs/core"
4
4
  import { InjectReflector } from "@modules/platform/composition"
5
+ import { requestOf } from "@modules/platform/http-security"
5
6
  import { unwrapOutcome } from "@modules/platform/primitives"
6
7
  import { admit } from "./admission.policy"
8
+ import type { CredentialMetadata } from "./credential.contracts"
7
9
  import { IdentityError } from "./errors/identity.error"
8
10
  import type { PublicMetadata } from "./identity.contracts"
9
- import { PUBLIC_KEY } from "./identity.decorators"
11
+ import { CREDENTIAL_KEY, PUBLIC_KEY } from "./identity.decorators"
10
12
 
11
13
  @Injectable()
12
- /** The third app guard and the default-deny gate: a door is open only when it says `@Public({ reason })`. */
14
+ /**
15
+ * The fourth app guard and the default-deny gate: a door is open only when it says `@Public({ reason })`, or when it is
16
+ * marked `@Credential` and `CredentialGuard` has set the credential principal. It never calls another guard.
17
+ */
13
18
  export class AuthGuard implements CanActivate {
14
19
  constructor(@InjectReflector() private readonly reflector: Reflector) {}
15
20
 
16
- /** Lets public doors through and refuses every other door. */
21
+ /** Lets public and credential-authenticated doors through and refuses every other door. */
17
22
  canActivate(context: ExecutionContext): boolean {
18
- const metadata = this.reflector.getAllAndOverride<PublicMetadata | undefined>(PUBLIC_KEY, [
19
- context.getHandler(),
20
- context.getClass(),
21
- ])
22
- unwrapOutcome(admit(metadata), IdentityError)
23
+ const targets = [context.getHandler(), context.getClass()]
24
+ const required = this.reflector.getAllAndOverride<CredentialMetadata | undefined>(CREDENTIAL_KEY, targets)
25
+ const metadata = this.reflector.getAllAndOverride<PublicMetadata | undefined>(PUBLIC_KEY, targets)
26
+ const credential =
27
+ required === undefined ? undefined : { required, principal: requestOf(context).credentialPrincipal }
28
+ unwrapOutcome(admit(metadata, credential), IdentityError)
23
29
  return true
24
30
  }
25
31
  }
@@ -0,0 +1,26 @@
1
+ /** The closed set of alternate credential families a door can accept. */
2
+ export type CredentialKind = "api-key" | "run-token" | "tool-token" | "link-token" | "posting"
3
+
4
+ /** Alternate credentials accepted only by doors marked with `@Credential`. */
5
+ export interface CredentialMetadata {
6
+ /** The credential family this door accepts. */
7
+ readonly kind: CredentialKind
8
+ /** An optional workflow constraint the authenticator may enforce for the credential. */
9
+ readonly workflow?: string
10
+ }
11
+
12
+ /** A credential identity is deliberately separate from a signed-in user. */
13
+ export interface CredentialPrincipal {
14
+ /** The credential family that established this identity. */
15
+ readonly kind: CredentialKind
16
+ /** The key, job, run or link the credential identifies. */
17
+ readonly subject: string
18
+ /** The only workspace this credential can act for. */
19
+ readonly workspaceId: string
20
+ }
21
+
22
+ /** The port the app binds to the capability that owns credential verification. */
23
+ export interface CredentialAuthenticator {
24
+ /** Authenticates the actual credential, throwing the owning domain's refusal on failure. */
25
+ authenticate(metadata: CredentialMetadata, authorization: string, resourceId?: string): Promise<CredentialPrincipal>
26
+ }
@@ -0,0 +1,119 @@
1
+ import type { ExecutionContext } from "@nestjs/common"
2
+ import type { HttpArgumentsHost } from "@nestjs/common/interfaces"
3
+ import { Reflector } from "@nestjs/core"
4
+ import { Test } from "@nestjs/testing"
5
+ import { mock } from "@starci/jest-preset"
6
+ import { CredentialGuard } from "./credential.guard"
7
+ import type { CredentialAuthenticator, CredentialMetadata, CredentialPrincipal } from "./credential.contracts"
8
+ import { IdentityError, IdentityErrorCode } from "./errors/identity.error"
9
+ import { CREDENTIAL_AUTHENTICATOR } from "./identity.decorators"
10
+
11
+ const PRINCIPAL: CredentialPrincipal = { kind: "tool-token", subject: "tool-1", workspaceId: "ws-1" }
12
+
13
+ interface FakeHeaders {
14
+ readonly authorization?: string
15
+ }
16
+
17
+ interface FakeParams {
18
+ readonly id?: unknown
19
+ }
20
+
21
+ interface FakeRequest {
22
+ credentialPrincipal?: CredentialPrincipal
23
+ readonly headers: FakeHeaders
24
+ readonly params?: FakeParams
25
+ }
26
+
27
+ const build = async (metadata: CredentialMetadata | undefined, principal: CredentialPrincipal = PRINCIPAL) => {
28
+ const reflector = mock<Reflector>()
29
+ reflector.getAllAndOverride.mockReturnValue(metadata)
30
+ const authenticator = mock<CredentialAuthenticator>()
31
+ authenticator.authenticate.mockResolvedValue(principal)
32
+ const moduleRef = await Test.createTestingModule({
33
+ providers: [
34
+ CredentialGuard,
35
+ { provide: Reflector, useValue: reflector },
36
+ { provide: CREDENTIAL_AUTHENTICATOR, useValue: authenticator },
37
+ ],
38
+ }).compile()
39
+ return { guard: moduleRef.get(CredentialGuard), authenticator }
40
+ }
41
+
42
+ const contextOf = (request: FakeRequest) => {
43
+ const context = mock<ExecutionContext>()
44
+ context.getHandler.mockReturnValue(() => undefined)
45
+ context.getClass.mockReturnValue(class DoorClass {})
46
+ context.getType.mockReturnValue("http")
47
+ const host = mock<HttpArgumentsHost>()
48
+ host.getRequest.mockReturnValue(request)
49
+ context.switchToHttp.mockReturnValue(host)
50
+ return context
51
+ }
52
+
53
+ const requestOf = (): FakeRequest => ({ headers: { authorization: "Bearer secret" }, params: { id: "run-1" } })
54
+
55
+ describe("CredentialGuard", () => {
56
+ it("passes a door without credential metadata and authenticates nothing", async () => {
57
+ const { guard, authenticator } = await build(undefined)
58
+ const request = { ...requestOf(), credentialPrincipal: PRINCIPAL }
59
+
60
+ await expect(guard.canActivate(contextOf(request))).resolves.toBe(true)
61
+ expect(request.credentialPrincipal).toEqual(PRINCIPAL)
62
+ expect(authenticator.authenticate).not.toHaveBeenCalled()
63
+ })
64
+
65
+ it("authenticates a credential door and sets the credential principal", async () => {
66
+ const { guard, authenticator } = await build({ kind: "tool-token" }, PRINCIPAL)
67
+ const request = requestOf()
68
+
69
+ await expect(guard.canActivate(contextOf(request))).resolves.toBe(true)
70
+ expect(request.credentialPrincipal).toEqual(PRINCIPAL)
71
+ expect(authenticator.authenticate).toHaveBeenCalledWith({ kind: "tool-token" }, "Bearer secret", "run-1")
72
+ })
73
+
74
+ it("refuses a principal of another credential kind", async () => {
75
+ const { guard } = await build({ kind: "api-key" }, PRINCIPAL)
76
+ const request = requestOf()
77
+
78
+ await expect(guard.canActivate(contextOf(request))).rejects.toMatchObject({
79
+ code: IdentityErrorCode.Unauthenticated,
80
+ })
81
+ expect(request.credentialPrincipal).toBeUndefined()
82
+ })
83
+
84
+ it.each([
85
+ { ...PRINCIPAL, subject: "" },
86
+ { ...PRINCIPAL, workspaceId: "" },
87
+ ])("refuses an incomplete credential principal (%o)", async (principal) => {
88
+ const { guard } = await build({ kind: "tool-token" }, principal)
89
+ const request = requestOf()
90
+
91
+ await expect(guard.canActivate(contextOf(request))).rejects.toMatchObject({
92
+ code: IdentityErrorCode.Unauthenticated,
93
+ })
94
+ expect(request.credentialPrincipal).toBeUndefined()
95
+ })
96
+
97
+ it("clears an earlier credential principal when authentication refuses", async () => {
98
+ const { guard, authenticator } = await build({ kind: "tool-token" }, PRINCIPAL)
99
+ const refusal = new IdentityError({ code: IdentityErrorCode.Unauthenticated })
100
+ authenticator.authenticate.mockRejectedValue(refusal)
101
+ const request = { ...requestOf(), credentialPrincipal: PRINCIPAL }
102
+
103
+ await expect(guard.canActivate(contextOf(request))).rejects.toBe(refusal)
104
+ expect(request.credentialPrincipal).toBeUndefined()
105
+ })
106
+
107
+ it.each([undefined, { id: 1 }])(
108
+ "passes absent authorization and no string resource id to the authenticator (%o)",
109
+ async (params) => {
110
+ const metadata = { kind: "tool-token", workflow: "checkout" } as const
111
+ const { guard, authenticator } = await build(metadata, PRINCIPAL)
112
+ const request: FakeRequest = { headers: {}, params }
113
+
114
+ await expect(guard.canActivate(contextOf(request))).resolves.toBe(true)
115
+ expect(authenticator.authenticate).toHaveBeenCalledWith(metadata, "", undefined)
116
+ expect(request.credentialPrincipal).toEqual(PRINCIPAL)
117
+ },
118
+ )
119
+ })
@@ -0,0 +1,43 @@
1
+ import { Injectable } from "@nestjs/common"
2
+ import type { CanActivate, ExecutionContext } from "@nestjs/common"
3
+ import type { Reflector } from "@nestjs/core"
4
+ import { InjectReflector } from "@modules/platform/composition"
5
+ import { requestOf } from "@modules/platform/http-security"
6
+ import type { CredentialAuthenticator, CredentialMetadata } from "./credential.contracts"
7
+ import { IdentityError, IdentityErrorCode } from "./errors/identity.error"
8
+ import { CREDENTIAL_KEY, InjectCredentialAuthenticator } from "./identity.decorators"
9
+
10
+ @Injectable()
11
+ /**
12
+ * The third app guard: it acts only on a door marked `@Credential({ kind })`, authenticates the credential through the
13
+ * identity-owned port and sets the request's credential principal. Every other door passes through untouched and
14
+ * `AuthGuard` decides it.
15
+ */
16
+ export class CredentialGuard implements CanActivate {
17
+ constructor(
18
+ @InjectReflector() private readonly reflector: Reflector,
19
+ @InjectCredentialAuthenticator() private readonly authenticator: CredentialAuthenticator,
20
+ ) {}
21
+
22
+ /** Authenticates a credential door and leaves every other door to `AuthGuard`. */
23
+ async canActivate(context: ExecutionContext): Promise<boolean> {
24
+ const metadata = this.reflector.getAllAndOverride<CredentialMetadata | undefined>(CREDENTIAL_KEY, [
25
+ context.getHandler(),
26
+ context.getClass(),
27
+ ])
28
+ if (metadata === undefined) return true
29
+ const request = requestOf(context)
30
+ request.credentialPrincipal = undefined
31
+ const id: unknown = request.params?.id
32
+ const principal = await this.authenticator.authenticate(
33
+ metadata,
34
+ request.headers.authorization ?? "",
35
+ typeof id === "string" ? id : undefined,
36
+ )
37
+ if (principal.kind !== metadata.kind || !principal.subject || !principal.workspaceId) {
38
+ throw new IdentityError({ code: IdentityErrorCode.Unauthenticated })
39
+ }
40
+ request.credentialPrincipal = principal
41
+ return true
42
+ }
43
+ }
@@ -1,7 +1,23 @@
1
+ import type { CredentialPrincipal } from "./credential.contracts"
2
+
3
+ declare module "express-serve-static-core" {
4
+ /** The request carries what the credential guard established. */
5
+ interface Request {
6
+ /** The identity `CredentialGuard` authenticated on a door marked `@Credential`; never a session principal. */
7
+ credentialPrincipal?: CredentialPrincipal
8
+ }
9
+ }
10
+
1
11
  /** Why a door is open to anonymous callers; the vocabulary is closed. */
2
12
  export enum PublicReason {
3
13
  /** Liveness and readiness probes. */
4
14
  Health = "health",
15
+ /** The authentication protocol's anonymous handshake. */
16
+ AuthHandshake = "auth-handshake",
17
+ /** A webhook whose signature is verified before its domain intake runs. */
18
+ SignedWebhook = "signed-webhook",
19
+ /** A declared public catalog read. */
20
+ CatalogRead = "catalog-read",
5
21
  }
6
22
 
7
23
  /** The metadata `@Public` attaches to a door. */
@@ -1,8 +1,25 @@
1
1
  import { SetMetadata } from "@nestjs/common"
2
+ import { injector } from "@modules/platform/composition"
3
+ import type { TypedParameterDecorator } from "@modules/platform/composition"
4
+ import type { CredentialAuthenticator, CredentialMetadata } from "./credential.contracts"
2
5
  import type { PublicMetadata } from "./identity.contracts"
3
6
 
4
7
  /** Metadata key of `@Public`. */
5
8
  export const PUBLIC_KEY = "domain.identity.public"
6
9
 
7
- /** Opens a door to anonymous callers, stating why; every other door needs a signed-in caller. */
10
+ /** Opens a door to anonymous callers, stating why; every other door needs a credential or a signed-in caller. */
8
11
  export const Public = (metadata: PublicMetadata): ReturnType<typeof SetMetadata> => SetMetadata(PUBLIC_KEY, metadata)
12
+
13
+ /** Metadata key of `@Credential`. */
14
+ export const CREDENTIAL_KEY: unique symbol = Symbol("domain.identity.credential")
15
+
16
+ /** Token of the identity-owned port that authenticates an API key, run token, tool token or link token. */
17
+ export const CREDENTIAL_AUTHENTICATOR: unique symbol = Symbol("domain.identity.credential-authenticator")
18
+
19
+ /** Injects the credential authenticator identity was registered with. Parameter type: CredentialAuthenticator. */
20
+ export const InjectCredentialAuthenticator = (): TypedParameterDecorator<CredentialAuthenticator> =>
21
+ injector<CredentialAuthenticator>(CREDENTIAL_AUTHENTICATOR)
22
+
23
+ /** Marks a door authenticated by its own credential; `CredentialGuard` authenticates it and `AuthGuard` admits it. */
24
+ export const Credential = (metadata: CredentialMetadata): ReturnType<typeof SetMetadata> =>
25
+ SetMetadata(CREDENTIAL_KEY, metadata)
@@ -1,14 +1,32 @@
1
1
  import { Module } from "@nestjs/common"
2
- import type { DynamicModule } from "@nestjs/common"
2
+ import type { DynamicModule, Provider } from "@nestjs/common"
3
3
  import { AuthGuard } from "./auth.guard"
4
+ import { CredentialGuard } from "./credential.guard"
5
+ import { IdentityError, IdentityErrorCode } from "./errors/identity.error"
6
+ import { CREDENTIAL_AUTHENTICATOR } from "./identity.decorators"
4
7
  import { ConfigurableModuleClass, OPTIONS_TYPE } from "./identity.module-definition"
5
8
 
9
+ const refusingAuthenticator: Provider = {
10
+ provide: CREDENTIAL_AUTHENTICATOR,
11
+ useValue: {
12
+ authenticate: () => Promise.reject(new IdentityError({ code: IdentityErrorCode.Unauthenticated })),
13
+ },
14
+ }
15
+
6
16
  @Module({})
7
- /** The identity capability: the guard that closes every door by default. The app registers AuthGuard itself after the throttler and the origin guard. */
17
+ /** The identity capability: the credential guard and the guard that closes every door by default. The app registers them as APP_GUARDs after the throttler and the origin guard. */
8
18
  export class IdentityModule extends ConfigurableModuleClass {
9
- /** Registers the capability once per app. */
19
+ /** Registers the capability once per app; the credential authenticator is the port the app binds. */
10
20
  static register(options: typeof OPTIONS_TYPE): DynamicModule {
11
21
  const base = super.register(options)
12
- return { ...base, providers: [...(base.providers ?? []), AuthGuard], exports: [AuthGuard] }
22
+ const authenticator: Provider =
23
+ options.credentialAuthenticator === undefined
24
+ ? refusingAuthenticator
25
+ : { provide: CREDENTIAL_AUTHENTICATOR, useClass: options.credentialAuthenticator }
26
+ return {
27
+ ...base,
28
+ providers: [...(base.providers ?? []), authenticator, CredentialGuard, AuthGuard],
29
+ exports: [CredentialGuard, CREDENTIAL_AUTHENTICATOR, AuthGuard],
30
+ }
13
31
  }
14
32
  }
@@ -1,2 +1,8 @@
1
- /** The identity capability takes no options yet; the empty record keeps `isGlobal` assignable through the configurable module. */
2
- export type IdentityOptions = Record<never, never>
1
+ import type { Type } from "@nestjs/common"
2
+ import type { CredentialAuthenticator } from "./credential.contracts"
3
+
4
+ /** The identity capability's options. */
5
+ export interface IdentityOptions {
6
+ /** The app-owned authenticator of alternate credentials; a credential door fails closed while it is absent. */
7
+ readonly credentialAuthenticator?: Type<CredentialAuthenticator>
8
+ }
@@ -1,4 +1,5 @@
1
1
  export { AuthGuard } from "./auth.guard"
2
+ export { CredentialGuard } from "./credential.guard"
2
3
  export { IDENTITY_ERROR_KINDS } from "./errors/identity.error"
3
4
  export { IDENTITY_MESSAGES } from "./messages/identity.messages"
4
5
  export { PublicReason } from "./identity.contracts"
@@ -1,7 +1,10 @@
1
+ import { Test } from "@nestjs/testing"
1
2
  import type { QueryRunner } from "typeorm"
2
3
  import { Secret } from "@modules/platform/config"
3
4
  import type { DatabaseConnectionOptions } from "./database.options"
4
5
  import { openConnectionSource } from "./connection-source.client"
6
+ import { CONNECTION_SOURCE } from "./database.port"
7
+ import type { ConnectionOpener } from "./database.port"
5
8
 
6
9
  class NoteEntity {}
7
10
 
@@ -16,20 +19,24 @@ class CreateNotes {
16
19
  }
17
20
 
18
21
  describe("openConnectionSource", () => {
19
- it("opens an isolated PostgreSQL data source with the connection's entities and migrations", () => {
22
+ it("opens an isolated PostgreSQL data source with the connection's entities and migrations", async () => {
23
+ const moduleRef = await Test.createTestingModule({
24
+ providers: [{ provide: CONNECTION_SOURCE, useFactory: () => ({ open: openConnectionSource }) }],
25
+ }).compile()
26
+ const client = moduleRef.get<ConnectionOpener>(CONNECTION_SOURCE)
20
27
  const connection: DatabaseConnectionOptions = {
21
28
  name: "primary",
22
- url: new Secret("postgres://user:password@database.test:5432/primary"),
29
+ url: new Secret("postgres://database.test:5432/primary"),
23
30
  entities: [NoteEntity],
24
31
  migrations: [CreateNotes],
25
32
  }
26
33
 
27
- const source = openConnectionSource(connection)
34
+ const source = client.open(connection)
28
35
 
29
36
  expect(source).toMatchObject({
30
37
  options: {
31
38
  type: "postgres",
32
- url: "postgres://user:password@database.test:5432/primary",
39
+ url: "postgres://database.test:5432/primary",
33
40
  entities: [NoteEntity],
34
41
  migrations: [CreateNotes],
35
42
  migrationsTableName: "primary_migrations",
@@ -0,0 +1,32 @@
1
+ import { Test } from "@nestjs/testing"
2
+ import { fakeTransaction, mockEntityManager } from "@starci/jest-preset"
3
+ import type { MockEntityManager } from "@starci/jest-preset"
4
+ import { DatabaseProbeService } from "./database-probe.service"
5
+ import { PRIMARY_ENTITY_MANAGER } from "./primary.decorators"
6
+
7
+ const build = async (entityManager: MockEntityManager) => {
8
+ const moduleRef = await Test.createTestingModule({
9
+ providers: [DatabaseProbeService, { provide: PRIMARY_ENTITY_MANAGER, useValue: entityManager }],
10
+ }).compile()
11
+ return moduleRef.get(DatabaseProbeService)
12
+ }
13
+
14
+ describe("DatabaseProbeService", () => {
15
+ it("opens and commits a transaction against the required database", async () => {
16
+ const transaction = fakeTransaction(mockEntityManager())
17
+ const service = await build(transaction.em)
18
+
19
+ await expect(service.probe()).resolves.toBeUndefined()
20
+
21
+ expect(transaction.em.transaction).toHaveBeenCalledTimes(1)
22
+ expect(transaction.commits).toBe(1)
23
+ })
24
+
25
+ it("propagates a failed database transaction", async () => {
26
+ const entityManager = mockEntityManager()
27
+ entityManager.transaction.mockRejectedValue(new Error("connection unavailable"))
28
+ const service = await build(entityManager)
29
+
30
+ await expect(service.probe()).rejects.toThrow("connection unavailable")
31
+ })
32
+ })
@@ -0,0 +1,14 @@
1
+ import { Injectable } from "@nestjs/common"
2
+ import type { EntityManager } from "typeorm"
3
+ import { InjectPrimaryEntityManager } from "./primary.decorators"
4
+
5
+ @Injectable()
6
+ /** Checks that the required primary database accepts and commits a transaction. */
7
+ export class DatabaseProbeService {
8
+ constructor(@InjectPrimaryEntityManager() private readonly entityManager: EntityManager) {}
9
+
10
+ /** Reaches the database and fails when its transaction cannot complete. */
11
+ async probe(): Promise<void> {
12
+ await this.entityManager.transaction(() => Promise.resolve(undefined))
13
+ }
14
+ }