@starci/hfs 4.2.17 → 4.4.0

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 (564) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +7 -0
  3. package/db/migrate.mjs +361 -0
  4. package/db/post-cutover.mjs +136 -0
  5. package/emit/db-types.mjs +10 -1
  6. package/lint/run.mjs +1 -1
  7. package/package.json +4 -1
  8. package/release/gate.mjs +298 -0
  9. package/release/index.mjs +103 -0
  10. package/release/push.mjs +110 -0
  11. package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
  12. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  13. package/runtime/knowledge/hfs/rules.yaml +3437 -160
  14. package/runtime/knowledge/hfs/slots.yaml +430 -92
  15. package/runtime/knowledge/patterns/be/api.yaml +92 -2
  16. package/runtime/knowledge/patterns/be/cli.yaml +37 -9
  17. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  18. package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
  19. package/runtime/knowledge/patterns/be/projections.yaml +4 -2
  20. package/runtime/knowledge/patterns/be/queues.yaml +11 -3
  21. package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
  22. package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
  23. package/runtime/knowledge/patterns/be/saga.yaml +20 -3
  24. package/runtime/knowledge/patterns/be/webhooks.yaml +210 -36
  25. package/runtime/knowledge/patterns/fe/folder.yaml +4 -0
  26. package/runtime/knowledge/sonar-gate.yaml +1 -1
  27. package/runtime/modules/kernel/failure-codes.yaml +3128 -47
  28. package/runtime/modules/models/runtimes.yaml +1 -1
  29. package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
  30. package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
  31. package/runtime/packages/cli/src/validate-args.mjs +213 -0
  32. package/runtime/scripts/api/git/add.mjs +5 -0
  33. package/runtime/scripts/api/git/cat-file.mjs +5 -0
  34. package/runtime/scripts/api/git/init.mjs +7 -0
  35. package/runtime/scripts/api/git/lib.mjs +3 -0
  36. package/runtime/scripts/api/git/tag.mjs +5 -0
  37. package/runtime/scripts/api/node/lib.mjs +20 -0
  38. package/runtime/scripts/api/node/run-node.mjs +6 -0
  39. package/runtime/scripts/api/sops/lib.mjs +41 -0
  40. package/runtime/scripts/api/sops/resolve-sops.mjs +7 -0
  41. package/runtime/scripts/api/sops/seal.mjs +1 -1
  42. package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
  43. package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
  44. package/runtime/scripts/api/supabase/database-query.mjs +5 -0
  45. package/runtime/scripts/api/supabase/lib.mjs +44 -0
  46. package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
  47. package/runtime/scripts/hfs/app-wiring.mjs +201 -0
  48. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +103 -0
  49. package/runtime/scripts/hfs/architecture/backend.mjs +22 -10
  50. package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
  51. package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
  52. package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
  53. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  54. package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
  55. package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
  56. package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
  57. package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
  58. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  59. package/runtime/scripts/hfs/architecture/dead-exports.mjs +23 -50
  60. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  61. package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
  62. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
  63. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  64. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  65. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  66. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
  67. package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
  68. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +49 -25
  69. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  70. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  71. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  72. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  73. package/runtime/scripts/hfs/architecture/register-once.mjs +15 -7
  74. package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
  75. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  76. package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
  77. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +214 -17
  78. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
  79. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  80. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  81. package/runtime/scripts/hfs/architecture/tiers.mjs +11 -4
  82. package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
  83. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +9 -13
  84. package/runtime/scripts/hfs/check-results.mjs +28 -0
  85. package/runtime/scripts/hfs/check.mjs +63 -72
  86. package/runtime/scripts/hfs/coverage-from.mjs +70 -0
  87. package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
  88. package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
  89. package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
  90. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  91. package/runtime/scripts/hfs/doc-commands.mjs +367 -0
  92. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  93. package/runtime/scripts/hfs/example-secrets.mjs +158 -0
  94. package/runtime/scripts/hfs/manifest-shape.mjs +91 -6
  95. package/runtime/scripts/hfs/path-findings.mjs +32 -2
  96. package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
  97. package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
  98. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  99. package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
  100. package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
  101. package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
  102. package/runtime/scripts/hfs/rules/assets.mjs +79 -0
  103. package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
  104. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
  105. package/runtime/scripts/hfs/rules/contract.mjs +66 -5
  106. package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
  107. package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
  108. package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
  109. package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
  110. package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
  111. package/runtime/scripts/hfs/rules/database-migrations.mjs +317 -37
  112. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
  113. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
  114. package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
  115. package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
  116. package/runtime/scripts/hfs/rules/database.mjs +11 -2
  117. package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
  118. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
  119. package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
  120. package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
  121. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  122. package/runtime/scripts/hfs/rules/edition.mjs +11 -10
  123. package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
  124. package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
  125. package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
  126. package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
  127. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
  128. package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
  129. package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
  130. package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
  131. package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
  132. package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
  133. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
  134. package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
  135. package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
  136. package/runtime/scripts/hfs/rules/proof-commands.mjs +2 -0
  137. package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
  138. package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
  139. package/runtime/scripts/hfs/rules/saga.mjs +8 -16
  140. package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
  141. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
  142. package/runtime/scripts/hfs/rules/secrets.mjs +26 -0
  143. package/runtime/scripts/hfs/rules/services.mjs +4 -4
  144. package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
  145. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
  146. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
  147. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
  148. package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
  149. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +168 -0
  150. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
  151. package/runtime/scripts/hfs/rules/stacks.mjs +13 -8
  152. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
  153. package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
  154. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
  155. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
  156. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
  157. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
  158. package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
  159. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
  160. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
  161. package/runtime/scripts/hfs/sealed-members.mjs +101 -0
  162. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
  163. package/runtime/scripts/hfs/secret-document.mjs +97 -0
  164. package/runtime/scripts/hfs/secret-edit.mjs +170 -0
  165. package/runtime/scripts/hfs/secret-errors.mjs +20 -0
  166. package/runtime/scripts/hfs/secret-files.mjs +124 -0
  167. package/runtime/scripts/hfs/secret-init.mjs +300 -0
  168. package/runtime/scripts/hfs/secret-output.mjs +100 -0
  169. package/runtime/scripts/hfs/secret-sops.mjs +130 -0
  170. package/runtime/scripts/hfs/secret-store.mjs +163 -0
  171. package/runtime/scripts/hfs/secret.mjs +162 -193
  172. package/runtime/scripts/hfs/slot-imports.mjs +18 -3
  173. package/runtime/scripts/hfs/slot-required.mjs +13 -0
  174. package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
  175. package/runtime/scripts/hfs/slots.mjs +1 -0
  176. package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
  177. package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
  178. package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
  179. package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
  180. package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
  181. package/runtime/scripts/hfs/tree-findings.mjs +22 -0
  182. package/runtime/scripts/lib/command-tokens.mjs +126 -0
  183. package/runtime/scripts/lib/compose-values.mjs +11 -0
  184. package/runtime/scripts/lib/deployment-findings.mjs +7 -0
  185. package/runtime/scripts/lib/json-clone.mjs +61 -0
  186. package/runtime/scripts/lib/list.mjs +4 -0
  187. package/runtime/scripts/lib/roots.mjs +59 -0
  188. package/runtime/scripts/lib/runtime-example.mjs +13 -0
  189. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  190. package/runtime/scripts/lib/sql-ast.mjs +32 -0
  191. package/runtime/scripts/lib/stack-services.mjs +1 -1
  192. package/runtime/scripts/lib/swarm-stack.mjs +126 -0
  193. package/runtime/scripts/lib/trim-end.mjs +8 -0
  194. package/runtime/scripts/lib/ts-analysis.mjs +23 -0
  195. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  196. package/runtime/scripts/lib/workflow-events.mjs +8 -0
  197. package/scaffold/add-app.mjs +77 -59
  198. package/scaffold/add-cli-lite.mjs +24 -17
  199. package/scaffold/add-compose.mjs +79 -0
  200. package/scaffold/add-database-lite.mjs +14 -0
  201. package/scaffold/add-fe-overlay.mjs +122 -0
  202. package/scaffold/add-fe-page.mjs +269 -0
  203. package/scaffold/add-inbox-lite.mjs +87 -124
  204. package/scaffold/add-plan.mjs +183 -0
  205. package/scaffold/add-table.mjs +132 -70
  206. package/scaffold/add.mjs +186 -204
  207. package/scaffold/app.mjs +128 -43
  208. package/scaffold/edition-gate.mjs +3 -3
  209. package/scaffold/image.mjs +2 -2
  210. package/scaffold/lite-exports.mjs +2 -1
  211. package/scaffold/service.mjs +2 -2
  212. package/src/main.mjs +238 -119
  213. package/stack/age-install.mjs +37 -0
  214. package/stack/backup.mjs +100 -0
  215. package/stack/build-args.mjs +42 -0
  216. package/stack/build.mjs +173 -0
  217. package/stack/cli.mjs +130 -0
  218. package/stack/config.mjs +57 -0
  219. package/stack/context.mjs +57 -0
  220. package/stack/converge.mjs +107 -0
  221. package/stack/deploy.mjs +301 -0
  222. package/stack/digest.mjs +16 -0
  223. package/stack/docker-transport.mjs +53 -0
  224. package/stack/edge-alias.mjs +118 -0
  225. package/stack/errors.mjs +21 -0
  226. package/stack/image-scan.mjs +152 -0
  227. package/stack/ingress-create.mjs +114 -0
  228. package/stack/ingress-publish.mjs +273 -0
  229. package/stack/ingress.mjs +706 -0
  230. package/stack/jobs.mjs +51 -0
  231. package/stack/live-restore.mjs +177 -0
  232. package/stack/prepare.mjs +266 -0
  233. package/stack/private-file.mjs +22 -0
  234. package/stack/provision.mjs +90 -0
  235. package/stack/prune.mjs +46 -0
  236. package/stack/release-store.mjs +87 -0
  237. package/stack/remote.mjs +205 -0
  238. package/stack/render.mjs +149 -0
  239. package/stack/retire.mjs +47 -0
  240. package/stack/sealed.mjs +149 -0
  241. package/stack/task.mjs +42 -0
  242. package/stack/verify.mjs +99 -0
  243. package/sync/ci-commands.mjs +13 -0
  244. package/sync/index.mjs +127 -64
  245. package/sync/stack-images.mjs +127 -0
  246. package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
  247. package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
  248. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
  249. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
  250. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
  251. package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
  252. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
  253. package/templates/app/codegen-graphql.mjs.partial +13 -2
  254. package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
  255. package/templates/app/codegen-prelude.mjs.partial +8 -0
  256. package/templates/app/gitignore +4 -0
  257. package/templates/app/gitignore-lite +4 -0
  258. package/templates/app/hooks/husky/pre-commit +7 -12
  259. package/templates/app/package-scripts/package.json +1 -0
  260. package/templates/app/package-scripts/supabase.json +7 -0
  261. package/templates/app/package-scripts-lite/package.json +1 -7
  262. package/templates/app/skeleton/.editorconfig +1 -0
  263. package/templates/app/skeleton/.gitattributes +7 -1
  264. package/templates/app/skeleton/.github/dependabot.yml +17 -0
  265. package/templates/app/skeleton/.nvmrc +1 -1
  266. package/templates/app/skeleton/.sops.yaml +1 -1
  267. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
  268. package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
  269. package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
  270. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
  271. package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
  272. package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
  273. package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
  274. package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
  275. package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  276. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  277. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  278. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
  279. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  280. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
  281. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
  282. package/templates/app/skeleton/README.md +6 -6
  283. package/templates/app/skeleton/scripts/codegen.mjs +1 -2
  284. package/templates/app/skeleton-lite/.editorconfig +16 -0
  285. package/templates/app/skeleton-lite/.gitattributes +8 -0
  286. package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
  287. package/templates/app/skeleton-lite/.nvmrc +1 -0
  288. package/templates/app/skeleton-lite/.sops.yaml +3 -2
  289. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +193 -0
  290. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
  291. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
  292. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
  293. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
  294. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
  295. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
  296. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
  297. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
  298. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
  299. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
  300. package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
  301. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
  302. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
  303. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  304. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  305. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  306. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
  307. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  308. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
  309. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
  310. package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
  311. package/templates/app/skeleton-lite/.starciwork/workspace.yaml +9 -0
  312. package/templates/app/skeleton-lite/README.md +8 -4
  313. package/templates/app/skeleton-lite/supabase/config.toml +8 -1
  314. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
  315. package/templates/app/starciwork.gitignore-lite +6 -0
  316. package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
  317. package/templates/be/image/worker/Dockerfile +1 -1
  318. package/templates/be/inbox/database-client.spec.ts.tpl +22 -28
  319. package/templates/be/inbox/database-client.ts.tpl +14 -28
  320. package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
  321. package/templates/be/inbox/event-index.ts.tpl +1 -0
  322. package/templates/be/inbox/events.json.tpl +10 -0
  323. package/templates/be/inbox/migration.sql.tpl +1 -0
  324. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
  325. package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
  326. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
  327. package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
  328. package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
  329. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
  330. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
  331. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
  332. package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
  333. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
  334. package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
  335. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
  336. package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
  337. package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
  338. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
  339. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
  340. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
  341. package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
  342. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
  343. package/templates/be/inbox/platform/migration.sql.tpl +48 -0
  344. package/templates/be/inbox/service.spec.ts.tpl +49 -20
  345. package/templates/be/inbox/service.ts.tpl +27 -16
  346. package/templates/be/inbox/sql.ts.tpl +2 -2
  347. package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -3
  348. package/templates/be/inbox/webhook-door.ts.tpl +4 -3
  349. package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
  350. package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
  351. package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
  352. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
  353. package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
  354. package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
  355. package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
  356. package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
  357. package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
  358. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
  359. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
  360. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
  361. package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
  362. package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
  363. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
  364. package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
  365. package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
  366. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
  367. package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
  368. package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
  369. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
  370. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
  371. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
  372. package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
  373. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
  374. package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
  375. package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
  376. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
  377. package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
  378. package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
  379. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
  380. package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
  381. package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
  382. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
  383. package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
  384. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
  385. package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
  386. package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
  387. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
  388. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
  389. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
  390. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
  391. package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
  392. package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
  393. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
  394. package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
  395. package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
  396. package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +3 -3
  397. package/templates/be/patterns/webhooks/webhook.door.ts.tpl +3 -3
  398. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
  399. package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
  400. package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
  401. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
  402. package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
  403. package/templates/be/skeleton/apps/__app__/src/__app__.env.ts +21 -0
  404. package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +1 -1
  405. package/templates/be/skeleton/apps/__app__/src/app.module.ts +15 -45
  406. package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
  407. package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
  408. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +44 -0
  409. package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
  410. package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
  411. package/templates/be/skeleton/apps/__app__/src/main.ts +18 -10
  412. package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
  413. package/templates/be/skeleton/apps/cli/src/cli.env.ts +13 -0
  414. package/templates/be/skeleton/apps/cli/src/cli.options.ts +7 -17
  415. package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
  416. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
  417. package/templates/be/skeleton/apps/cli/src/main.ts +3 -2
  418. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
  419. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
  420. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
  421. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
  422. package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
  423. package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
  424. package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
  425. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
  426. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
  427. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
  428. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
  429. package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
  430. package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
  431. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
  432. package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
  433. package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
  434. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
  435. package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
  436. package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
  437. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
  438. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
  439. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
  440. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
  441. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
  442. package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
  443. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
  444. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
  445. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
  446. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
  447. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
  448. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
  449. package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
  450. package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +6 -6
  451. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
  452. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
  453. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
  454. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  455. package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
  456. package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
  457. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
  458. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
  459. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
  460. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
  461. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
  462. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
  463. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
  464. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
  465. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
  466. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
  467. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
  468. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
  469. package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
  470. package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
  471. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
  472. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
  473. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
  474. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
  475. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
  476. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
  477. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
  478. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
  479. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
  480. package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
  481. package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
  482. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
  483. package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
  484. package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
  485. package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
  486. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
  487. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
  488. package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
  489. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +25 -0
  490. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +1 -1
  491. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +18 -58
  492. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
  493. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
  494. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
  495. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +47 -0
  496. package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
  497. package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
  498. package/templates/be/skeleton-lite/apps/api/src/main.ts +5 -13
  499. package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +17 -0
  500. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
  501. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
  502. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
  503. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
  504. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
  505. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
  506. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
  507. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
  508. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
  509. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
  510. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
  511. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
  512. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
  513. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
  514. package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
  515. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
  516. package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
  517. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
  518. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  519. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
  520. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
  521. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
  522. package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
  523. package/templates/be/table/api/feature.module.ts.tpl +2 -1
  524. package/templates/be/table/api/feature.options.ts.tpl +2 -0
  525. package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
  526. package/templates/be/table/api/graphql.module.ts.tpl +3 -2
  527. package/templates/be/table/api/graphql.options.ts.tpl +2 -0
  528. package/templates/be/table/migration.sql.tpl +1 -0
  529. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
  530. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
  531. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
  532. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
  533. package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
  534. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
  535. package/templates/fe/auth/supabase-client.ts.tpl +34 -0
  536. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
  537. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
  538. package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
  539. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
  540. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
  541. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
  542. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
  543. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
  544. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
  545. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
  546. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
  547. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
  548. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
  549. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
  550. package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
  551. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
  552. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
  553. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
  554. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
  555. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
  556. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
  557. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
  558. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  559. package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
  560. package/upgrade/check.mjs +3 -1
  561. package/upgrade/file-state.mjs +68 -0
  562. package/upgrade/index.mjs +58 -94
  563. package/runtime/scripts/api/git/log.mjs +0 -5
  564. package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +0 -55
@@ -1,7 +1,12 @@
1
1
  import { Module } from "@nestjs/common"
2
2
  import { SystemHealthModule } from "../../system-health.module"
3
3
  import { LiveController } from "./live.controller"
4
+ import { ReadyController } from "./ready.controller"
5
+ import { ConfigurableModuleClass } from "./system-health-http.module-definition"
4
6
 
5
- @Module({ imports: [SystemHealthModule], controllers: [LiveController] })
6
- /** The HTTP transport of the health feature: the liveness door. */
7
- export class SystemHealthHttpModule {}
7
+ @Module({
8
+ imports: [SystemHealthModule.register({})],
9
+ controllers: [LiveController, ReadyController],
10
+ })
11
+ /** The HTTP transport of the health feature: independent liveness and required-dependency readiness doors. */
12
+ export class SystemHealthHttpModule extends ConfigurableModuleClass {}
@@ -0,0 +1,2 @@
1
+ /** The system-health-http module takes no options yet. */
2
+ export type SystemHealthHttpOptions = Record<string, never>
@@ -0,0 +1,6 @@
1
+ import { ConfigurableModuleBuilder } from "@nestjs/common"
2
+ import type { CliOptions } from "./cli.options"
3
+
4
+ /** The configurable-module base of the cli module. */
5
+ export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } =
6
+ new ConfigurableModuleBuilder<CliOptions>().setClassMethodName("register").build()
@@ -1,7 +1,8 @@
1
1
  import { Module } from "@nestjs/common"
2
+ import { ConfigurableModuleClass } from "./cli.module-definition"
2
3
  import { MigrateModule } from "./migrate/migrate.module"
3
4
  import { SeedModule } from "./seed/seed.module"
4
5
 
5
- @Module({ imports: [MigrateModule, SeedModule] })
6
+ @Module({ imports: [MigrateModule.register({}), SeedModule.register({})] })
6
7
  /** The cli feature root: every command group of the back end, compiled only into apps/cli. */
7
- export class CliModule {}
8
+ export class CliModule extends ConfigurableModuleClass {}
@@ -0,0 +1,2 @@
1
+ /** The cli module takes no options yet. */
2
+ export type CliOptions = Record<string, never>
@@ -0,0 +1,6 @@
1
+ import { ConfigurableModuleBuilder } from "@nestjs/common"
2
+ import type { MigrateOptions } from "./migrate.options"
3
+
4
+ /** The configurable-module base of the migrate module. */
5
+ export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } =
6
+ new ConfigurableModuleBuilder<MigrateOptions>().setClassMethodName("register").build()
@@ -1,7 +1,8 @@
1
1
  import { Module } from "@nestjs/common"
2
2
  import { MigrateCli } from "./migrate.cli"
3
+ import { ConfigurableModuleClass } from "./migrate.module-definition"
3
4
  import { RunCli } from "./subs/run.cli"
4
5
 
5
6
  @Module({ providers: [MigrateCli, RunCli] })
6
7
  /** The migrate group: its command and its sub-commands; the connections come from the database capability. */
7
- export class MigrateModule {}
8
+ export class MigrateModule extends ConfigurableModuleClass {}
@@ -0,0 +1,2 @@
1
+ /** The migrate module takes no options yet. */
2
+ export type MigrateOptions = Record<string, never>
@@ -0,0 +1,6 @@
1
+ import { ConfigurableModuleBuilder } from "@nestjs/common"
2
+ import type { SeedOptions } from "./seed.options"
3
+
4
+ /** The configurable-module base of the seed module. */
5
+ export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } =
6
+ new ConfigurableModuleBuilder<SeedOptions>().setClassMethodName("register").build()
@@ -1,7 +1,8 @@
1
1
  import { Module } from "@nestjs/common"
2
2
  import { SeedCli } from "./seed.cli"
3
+ import { ConfigurableModuleClass } from "./seed.module-definition"
3
4
  import { RunSeedsCli } from "./subs/run.cli"
4
5
 
5
6
  @Module({ providers: [SeedCli, RunSeedsCli] })
6
7
  /** The seed group: its command and its sub-commands; the connections and the seed file reader come from the database capability. */
7
- export class SeedModule {}
8
+ export class SeedModule extends ConfigurableModuleClass {}
@@ -0,0 +1,2 @@
1
+ /** The seed module takes no options yet. */
2
+ export type SeedOptions = Record<string, never>
@@ -3,17 +3,17 @@ import { Test } from "@nestjs/testing"
3
3
  import { SeedRunnerService } from "@modules/platform/database"
4
4
  import { RunSeedsCli } from "./run.cli"
5
5
 
6
- const build = async (seeds: SeedRunnerService) => {
6
+ const build = async () => {
7
+ const seeds = mock<SeedRunnerService>()
7
8
  const moduleRef = await Test.createTestingModule({
8
9
  providers: [RunSeedsCli, { provide: SeedRunnerService, useValue: seeds }],
9
10
  }).compile()
10
- return moduleRef.get(RunSeedsCli)
11
+ return { command: moduleRef.get(RunSeedsCli), seeds }
11
12
  }
12
13
 
13
14
  describe("RunSeedsCli", () => {
14
15
  it("delegates the seed run of the named environment to the seed runner once", async () => {
15
- const seeds = mock<SeedRunnerService>()
16
- const command = await build(seeds)
16
+ const { command, seeds } = await build()
17
17
 
18
18
  await command.run([], { env: "staging" })
19
19
 
@@ -22,8 +22,7 @@ describe("RunSeedsCli", () => {
22
22
  })
23
23
 
24
24
  it("delegates with no environment when none is named, and takes --env as given", async () => {
25
- const seeds = mock<SeedRunnerService>()
26
- const command = await build(seeds)
25
+ const { command, seeds } = await build()
27
26
 
28
27
  await command.run([])
29
28
 
@@ -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
  }