@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
@@ -2,6 +2,7 @@ import { Module } from "@nestjs/common"
2
2
  import type { DynamicModule } from "@nestjs/common"
3
3
  import { TypeOrmModule, getDataSourceToken } from "@nestjs/typeorm"
4
4
  import type { DataSource } from "typeorm"
5
+ import { DatabaseProbeService } from "./database-probe.service"
5
6
  import { openConnectionSource } from "./connection-source.client"
6
7
  import { CONNECTION_SOURCE, DATABASE_OPTIONS, READ_SEED_FILES } from "./database.port"
7
8
  import { ConfigurableModuleClass, OPTIONS_TYPE } from "./database.module-definition"
@@ -9,7 +10,7 @@ import { MigrationRunnerService } from "./migration-runner.service"
9
10
  import { readSeedFiles } from "./seed-connections.client"
10
11
  import { SeedRunnerService } from "./seed-runner.service"
11
12
  import { PRIMARY_CONNECTION } from "./primary.connection"
12
- import { PRIMARY_ENTITY_MANAGER } from "./primary.decorators"
13
+ import { DATABASE_PROBE, PRIMARY_ENTITY_MANAGER } from "./primary.decorators"
13
14
 
14
15
  /** The token each declared connection provides its shared EntityManager under. */
15
16
  type EntityManagerToken = typeof PRIMARY_ENTITY_MANAGER
@@ -39,6 +40,9 @@ export class DatabaseModule extends ConfigurableModuleClass {
39
40
  },
40
41
  ]
41
42
  })
43
+ const probes = options.connections.some((connection) => connection.name === PRIMARY_CONNECTION)
44
+ ? [{ provide: DATABASE_PROBE, useClass: DatabaseProbeService }]
45
+ : []
42
46
  return {
43
47
  ...base,
44
48
  imports: [
@@ -59,6 +63,7 @@ export class DatabaseModule extends ConfigurableModuleClass {
59
63
  ...managers,
60
64
  MigrationRunnerService,
61
65
  SeedRunnerService,
66
+ ...probes,
62
67
  {
63
68
  provide: CONNECTION_SOURCE,
64
69
  useValue: { open: openConnectionSource },
@@ -70,6 +75,7 @@ export class DatabaseModule extends ConfigurableModuleClass {
70
75
  CONNECTION_SOURCE,
71
76
  MigrationRunnerService,
72
77
  SeedRunnerService,
78
+ ...probes.map((probe) => probe.provide),
73
79
  ...managers.map((manager) => manager.provide),
74
80
  ],
75
81
  }
@@ -1,4 +1,6 @@
1
1
  export { DatabaseModule } from "./database.module"
2
+ export { DATABASE_PROBE, InjectDatabaseProbe } from "./primary.decorators"
3
+ export { DatabaseProbeService } from "./database-probe.service"
2
4
  export { MigrationRunnerService } from "./migration-runner.service"
3
5
  export { DEFAULT_SEED_ENV } from "./seed-connections.client"
4
6
  export { SeedRunnerService } from "./seed-runner.service"
@@ -1,6 +1,7 @@
1
1
  import { injector } from "@modules/platform/composition"
2
2
  import type { TypedParameterDecorator } from "@modules/platform/composition"
3
3
  import type { EntityManager } from "typeorm"
4
+ import type { DatabaseProbeService } from "./database-probe.service"
4
5
 
5
6
  /** Token of the shared EntityManager of the `primary` database; the database module provides it for the connection it opens. */
6
7
  export const PRIMARY_ENTITY_MANAGER: unique symbol = Symbol("platform.database.primary-entity-manager")
@@ -8,3 +9,10 @@ export const PRIMARY_ENTITY_MANAGER: unique symbol = Symbol("platform.database.p
8
9
  /** Injects the shared EntityManager of the `primary` database. Parameter type: EntityManager. */
9
10
  export const InjectPrimaryEntityManager = (): TypedParameterDecorator<EntityManager> =>
10
11
  injector<EntityManager>(PRIMARY_ENTITY_MANAGER)
12
+
13
+ /** The required primary database connectivity probe provided by the database capability. */
14
+ export const DATABASE_PROBE: unique symbol = Symbol("platform.database.probe")
15
+
16
+ /** Injects the primary database connectivity probe. Parameter type: DatabaseProbeService. */
17
+ export const InjectDatabaseProbe = (): TypedParameterDecorator<DatabaseProbeService> =>
18
+ injector<DatabaseProbeService>(DATABASE_PROBE)
@@ -1,6 +1,7 @@
1
1
  import { mkdtemp, rm, writeFile } from "node:fs/promises"
2
2
  import { tmpdir } from "node:os"
3
3
  import { join } from "node:path"
4
+ import { Test } from "@nestjs/testing"
4
5
  import { mock } from "@starci/jest-preset"
5
6
  import { Secret } from "@modules/platform/config"
6
7
  import type { DatabaseConnectionOptions } from "./database.options"
@@ -9,6 +10,19 @@ import { seedText } from "./database.sql"
9
10
  import { readSeedFiles, seedConnections, seedDirectoryOf } from "./seed-connections.client"
10
11
  import type { SeedFile } from "./seed-connections.client"
11
12
 
13
+ const SEED_CLIENT = Symbol("seed-connections")
14
+
15
+ const setup = async () => {
16
+ const moduleRef = await Test.createTestingModule({
17
+ providers: [{ provide: SEED_CLIENT, useFactory: () => ({ readSeedFiles, seedConnections, seedDirectoryOf }) }],
18
+ }).compile()
19
+ return moduleRef.get<{
20
+ readonly readSeedFiles: typeof readSeedFiles
21
+ readonly seedConnections: typeof seedConnections
22
+ readonly seedDirectoryOf: typeof seedDirectoryOf
23
+ }>(SEED_CLIENT)
24
+ }
25
+
12
26
  const connection = (name: string): DatabaseConnectionOptions => ({
13
27
  name,
14
28
  url: new Secret(`postgres://localhost:5432/${name}`),
@@ -22,13 +36,15 @@ const file = (name: string, text: string): SeedFile => ({
22
36
  })
23
37
 
24
38
  describe("seedDirectoryOf", () => {
25
- it("is the seeds folder of the environment under .starcistacks, relative to the app root", () => {
26
- expect(seedDirectoryOf("dev")).toBe(join(".starcistacks", "dev", "seeds"))
39
+ it("is the seeds folder of the environment under .starcistacks, relative to the app root", async () => {
40
+ const client = await setup()
41
+ expect(client.seedDirectoryOf("dev")).toBe(join(".starcistacks", "dev", "seeds"))
27
42
  })
28
43
  })
29
44
 
30
45
  describe("seedConnections", () => {
31
46
  it("runs the files of each connection in the given order and never opens a connection without a file", async () => {
47
+ const client = await setup()
32
48
  const primary = mock<ConnectionSource>()
33
49
  const archive = mock<ConnectionSource>()
34
50
  const sources = new Map([
@@ -40,7 +56,7 @@ describe("seedConnections", () => {
40
56
  (target: DatabaseConnectionOptions) => sources.get(target.name) ?? mock<ConnectionSource>(),
41
57
  )
42
58
 
43
- const report = await seedConnections(
59
+ const report = await client.seedConnections(
44
60
  [connection("primary"), connection("archive")],
45
61
  [file("primary-a.sql", "SELECT 1"), file("primary-b.sql", "SELECT 2")],
46
62
  opener,
@@ -57,9 +73,10 @@ describe("seedConnections", () => {
57
73
  })
58
74
 
59
75
  it("answers the files that name no connection as unmatched", async () => {
76
+ const client = await setup()
60
77
  const opener = mock<ConnectionOpener>()
61
78
 
62
- const report = await seedConnections(
79
+ const report = await client.seedConnections(
63
80
  [connection("primary")],
64
81
  [file("legacy-users.sql", "INSERT INTO users DEFAULT VALUES")],
65
82
  opener,
@@ -70,13 +87,14 @@ describe("seedConnections", () => {
70
87
  })
71
88
 
72
89
  it("destroys the data source of a connection whose seed fails, and fails", async () => {
90
+ const client = await setup()
73
91
  const failing = mock<ConnectionSource>()
74
92
  failing.query.mockRejectedValue(new TypeError("relation does not exist"))
75
93
  const opener = mock<ConnectionOpener>()
76
94
  opener.open.mockReturnValue(failing)
77
95
 
78
96
  await expect(
79
- seedConnections([connection("primary")], [file("primary-a.sql", "SELECT 1")], opener),
97
+ client.seedConnections([connection("primary")], [file("primary-a.sql", "SELECT 1")], opener),
80
98
  ).rejects.toThrow("relation does not exist")
81
99
  expect(failing.destroy).toHaveBeenCalledTimes(1)
82
100
  })
@@ -94,11 +112,12 @@ describe("readSeedFiles", () => {
94
112
  })
95
113
 
96
114
  it("answers every sql file of the directory sorted by name with its statements, and nothing else", async () => {
115
+ const client = await setup()
97
116
  await writeFile(join(directory, "primary-b.sql"), "SELECT 2")
98
117
  await writeFile(join(directory, "primary-a.sql"), "SELECT 1")
99
118
  await writeFile(join(directory, "README.md"), "not a seed")
100
119
 
101
- const files = await readSeedFiles(directory)
120
+ const files = await client.readSeedFiles(directory)
102
121
 
103
122
  expect(files).toEqual([
104
123
  { name: "primary-a.sql", text: "SELECT 1" },
@@ -1,19 +1,29 @@
1
1
  import type { ArgumentsHost } from "@nestjs/common"
2
+ import { Test } from "@nestjs/testing"
2
3
  import { mock } from "@starci/jest-preset"
3
4
  import type { Request, Response } from "express"
5
+ import { REQUEST_LOCALE } from "@modules/platform/i18n"
4
6
  import type { RequestLocale } from "@modules/platform/i18n"
7
+ import { ERRORS_SERVICE } from "./errors.decorators"
5
8
  import { ErrorsFilter } from "./errors.filter"
6
9
  import type { ErrorsService } from "./errors.service"
7
10
 
8
- const build = () => {
11
+ const build = async () => {
9
12
  const errors = mock<ErrorsService>()
10
13
  const requestLocale = mock<RequestLocale>()
11
- return { filter: new ErrorsFilter(errors, requestLocale), errors, requestLocale }
14
+ const moduleRef = await Test.createTestingModule({
15
+ providers: [
16
+ ErrorsFilter,
17
+ { provide: ERRORS_SERVICE, useValue: errors },
18
+ { provide: REQUEST_LOCALE, useValue: requestLocale },
19
+ ],
20
+ }).compile()
21
+ return { filter: moduleRef.get(ErrorsFilter), errors, requestLocale }
12
22
  }
13
23
 
14
24
  describe("ErrorsFilter", () => {
15
- it("hands a failure in a GraphQL context back untouched for the GraphQL error formatter", () => {
16
- const { filter, errors } = build()
25
+ it("hands a failure in a GraphQL context back untouched for the GraphQL error formatter", async () => {
26
+ const { filter, errors } = await build()
17
27
  const failure = new Error("resolver failed")
18
28
  const host = mock<ArgumentsHost>()
19
29
  host.getType.mockReturnValue("graphql")
@@ -23,8 +33,8 @@ describe("ErrorsFilter", () => {
23
33
  expect(host.switchToHttp).not.toHaveBeenCalled()
24
34
  })
25
35
 
26
- it("writes a localized description and its status to an HTTP response", () => {
27
- const { filter, errors, requestLocale } = build()
36
+ it("writes a localized description and its status to an HTTP response", async () => {
37
+ const { filter, errors, requestLocale } = await build()
28
38
  const failure = new Error("note missing")
29
39
  const request = mock<Request>({ headers: { "accept-language": "en-US,en;q=0.9" } })
30
40
  const response = mock<Response>()
@@ -0,0 +1,130 @@
1
+ import type { INestApplication } from "@nestjs/common"
2
+ import { Test } from "@nestjs/testing"
3
+ import type { Express, NextFunction, Request, Response } from "express"
4
+ import { mock } from "@starci/jest-preset"
5
+ import { applyHttpSecurity, securityHeaders, validateAllowedOrigins } from "./edge-security.service"
6
+ import type { HttpSecurityOptions } from "./http-security.options"
7
+
8
+ const HTTP_EDGE = Symbol("HTTP_EDGE")
9
+
10
+ const buildEdge = async () => {
11
+ const moduleRef = await Test.createTestingModule({
12
+ providers: [{ provide: HTTP_EDGE, useValue: { applyHttpSecurity, securityHeaders, validateAllowedOrigins } }],
13
+ }).compile()
14
+ return moduleRef.get<typeof import("./edge-security.service")>(HTTP_EDGE)
15
+ }
16
+
17
+ const options: HttpSecurityOptions = {
18
+ allowedOrigins: ["https://web.example"],
19
+ rateLimit: { windowMs: 1, defaultLimit: 1, strictLimit: 1 },
20
+ webhooks: {},
21
+ }
22
+
23
+ const appWithServer = () => {
24
+ const server = mock<Express>()
25
+ const adapter = mock<ReturnType<INestApplication["getHttpAdapter"]>>()
26
+ adapter.getInstance.mockReturnValue(server)
27
+ const app = mock<INestApplication>()
28
+ app.getHttpAdapter.mockReturnValue(adapter)
29
+ return { app, server }
30
+ }
31
+
32
+ describe("HTTP edge origin validation", () => {
33
+ it("keeps exact HTTPS origins and admits HTTP only when explicitly enabled", async () => {
34
+ const edge = await buildEdge()
35
+ expect(edge.validateAllowedOrigins(["https://web.example", "https://admin.example:8443"], false)).toEqual([
36
+ "https://web.example",
37
+ "https://admin.example:8443",
38
+ ])
39
+ expect(edge.validateAllowedOrigins(["http://localhost:3000"], true)).toEqual(["http://localhost:3000"])
40
+ })
41
+
42
+ it.each([
43
+ "*",
44
+ "https://*.example",
45
+ "https://web.example/",
46
+ "https://web.example/path",
47
+ "https://user:pass@web.example",
48
+ "https://web.example?query=1",
49
+ "https://web.example#hash",
50
+ "not-an-origin",
51
+ "",
52
+ "ftp://web.example",
53
+ ])("rejects wildcard, malformed and non-origin allowlist entry %s at boot", async (origin) => {
54
+ const edge = await buildEdge()
55
+ expect(() => edge.validateAllowedOrigins([origin], true)).toThrow(
56
+ expect.objectContaining({ code: "HTTP_SECURITY_REQUEST_INVALID" }),
57
+ )
58
+ })
59
+
60
+ it("rejects an empty allowlist and HTTP origins without development permission", async () => {
61
+ const edge = await buildEdge()
62
+ expect(() => edge.validateAllowedOrigins([], false)).toThrow(
63
+ expect.objectContaining({ code: "HTTP_SECURITY_REQUEST_INVALID" }),
64
+ )
65
+ expect(() => edge.validateAllowedOrigins(["http://web.example"], false)).toThrow(
66
+ expect.objectContaining({ code: "HTTP_SECURITY_REQUEST_INVALID" }),
67
+ )
68
+ })
69
+ })
70
+
71
+ describe("HTTP edge installation", () => {
72
+ it("sets every API security header and continues the request", async () => {
73
+ const edge = await buildEdge()
74
+ const response = mock<Response>()
75
+ const next: jest.MockedFunction<NextFunction> = jest.fn()
76
+ edge.securityHeaders(mock<Request>(), response, next)
77
+ expect(response.setHeader.mock.calls).toEqual([
78
+ ["Strict-Transport-Security", "max-age=31536000; includeSubDomains"],
79
+ ["X-Content-Type-Options", "nosniff"],
80
+ ["X-Frame-Options", "DENY"],
81
+ ["Referrer-Policy", "no-referrer"],
82
+ ["Content-Security-Policy", "default-src 'none'; base-uri 'none'; frame-ancestors 'none'"],
83
+ ])
84
+ expect(next).toHaveBeenCalledTimes(1)
85
+ })
86
+
87
+ it("installs headers before CORS, removes x-powered-by and trusts the declared ingress hop count", async () => {
88
+ const edge = await buildEdge()
89
+ const { app, server } = appWithServer()
90
+ edge.applyHttpSecurity(app, { ...options, allowHttpOrigins: false }, { trustProxy: 1 })
91
+ expect(server.disable).toHaveBeenCalledWith("x-powered-by")
92
+ expect(server.set).toHaveBeenCalledWith("trust proxy", 1)
93
+ expect(app.use).toHaveBeenCalledWith(edge.securityHeaders)
94
+ expect(app.enableCors).toHaveBeenCalledWith({ origin: ["https://web.example"] })
95
+ expect(app.use.mock.invocationCallOrder[0]).toBeLessThan(app.enableCors.mock.invocationCallOrder[0] ?? 0)
96
+ })
97
+
98
+ it("defaults manually supplied options to HTTPS and accepts explicit zero proxy hops", async () => {
99
+ const edge = await buildEdge()
100
+ const { app, server } = appWithServer()
101
+ edge.applyHttpSecurity(app, options, { trustProxy: 0 })
102
+ expect(server.set).toHaveBeenCalledWith("trust proxy", 0)
103
+ expect(() =>
104
+ edge.applyHttpSecurity(app, { ...options, allowedOrigins: ["http://web.example"] }, { trustProxy: 1 }),
105
+ ).toThrow()
106
+ })
107
+
108
+ it("admits development HTTP origins when explicitly enabled", async () => {
109
+ const edge = await buildEdge()
110
+ const { app } = appWithServer()
111
+ edge.applyHttpSecurity(
112
+ app,
113
+ { ...options, allowedOrigins: ["http://localhost:3000"], allowHttpOrigins: true },
114
+ { trustProxy: 1 },
115
+ )
116
+ expect(app.enableCors).toHaveBeenCalledWith({ origin: ["http://localhost:3000"] })
117
+ })
118
+
119
+ it.each([-1, 0.5, Number.NaN, Number.MAX_SAFE_INTEGER + 1])(
120
+ "rejects unsafe proxy hop count %s",
121
+ async (trustProxy) => {
122
+ const edge = await buildEdge()
123
+ const { app } = appWithServer()
124
+ expect(() => edge.applyHttpSecurity(app, options, { trustProxy })).toThrow(
125
+ expect.objectContaining({ code: "HTTP_SECURITY_REQUEST_INVALID" }),
126
+ )
127
+ expect(app.enableCors).not.toHaveBeenCalled()
128
+ },
129
+ )
130
+ })
@@ -0,0 +1,58 @@
1
+ import type { INestApplication } from "@nestjs/common"
2
+ import type { Express, RequestHandler } from "express"
3
+ import { HttpSecurityError, HttpSecurityErrorCode } from "./errors/http-security.error"
4
+ import type { HttpSecurityOptions } from "./http-security.options"
5
+
6
+ /** The declared ingress hop count and the environment in which HTTP origins may be used. */
7
+ export interface HttpEdgeOptions {
8
+ /** Exact trusted proxy hops; the standard API stack has one ingress in front of it. */
9
+ readonly trustProxy: number
10
+ }
11
+
12
+ /** Validates exact browser origins at boot, before CORS can admit a request. */
13
+ export const validateAllowedOrigins = (origins: ReadonlyArray<string>, allowHttpOrigins: boolean): Array<string> => {
14
+ if (origins.length === 0)
15
+ throw new HttpSecurityError({
16
+ code: HttpSecurityErrorCode.RequestInvalid,
17
+ params: { fields: "HTTP_SECURITY_ALLOWED_ORIGINS" },
18
+ })
19
+ return origins.map((origin) => {
20
+ const parsed = URL.canParse(origin) ? new URL(origin) : undefined
21
+ if (
22
+ parsed === undefined ||
23
+ origin.includes("*") ||
24
+ parsed.origin !== origin ||
25
+ (parsed.protocol !== "https:" && !(allowHttpOrigins && parsed.protocol === "http:"))
26
+ )
27
+ throw new HttpSecurityError({
28
+ code: HttpSecurityErrorCode.RequestInvalid,
29
+ params: { fields: "HTTP_SECURITY_ALLOWED_ORIGINS" },
30
+ })
31
+ return origin
32
+ })
33
+ }
34
+
35
+ /** Security headers of the API's data and probe responses, including errors. */
36
+ export const securityHeaders: RequestHandler = (_request, response, next) => {
37
+ response.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
38
+ response.setHeader("X-Content-Type-Options", "nosniff")
39
+ response.setHeader("X-Frame-Options", "DENY")
40
+ response.setHeader("Referrer-Policy", "no-referrer")
41
+ response.setHeader("Content-Security-Policy", "default-src 'none'; base-uri 'none'; frame-ancestors 'none'")
42
+ next()
43
+ }
44
+
45
+ /** Installs the API edge policy once: validated CORS, security headers and a declared numeric ingress hop count. */
46
+ export const applyHttpSecurity = (app: INestApplication, options: HttpSecurityOptions, edge: HttpEdgeOptions): void => {
47
+ const origins = validateAllowedOrigins(options.allowedOrigins, options.allowHttpOrigins === true)
48
+ if (!Number.isSafeInteger(edge.trustProxy) || edge.trustProxy < 0)
49
+ throw new HttpSecurityError({
50
+ code: HttpSecurityErrorCode.RequestInvalid,
51
+ params: { fields: "HTTP_SECURITY_TRUST_PROXY" },
52
+ })
53
+ const server: Express = app.getHttpAdapter().getInstance()
54
+ server.disable("x-powered-by")
55
+ server.set("trust proxy", edge.trustProxy)
56
+ app.use(securityHeaders)
57
+ app.enableCors({ origin: origins })
58
+ }
@@ -3,6 +3,8 @@ import type { ErrorKind } from "@modules/platform/errors"
3
3
 
4
4
  /** Codes of the http-security capability. */
5
5
  export enum HttpSecurityErrorCode {
6
+ /** The required primary database failed or did not answer before the readiness deadline. */
7
+ RequiredDependencyUnavailable = "HTTP_SECURITY_REQUIRED_DEPENDENCY_UNAVAILABLE",
6
8
  /** A state-changing request came from an origin that is not on the allowlist. */
7
9
  OriginRejected = "HTTP_SECURITY_ORIGIN_REJECTED",
8
10
  /** The caller sent more requests than its tier allows in the current window. */
@@ -19,6 +21,7 @@ export enum HttpSecurityErrorCode {
19
21
 
20
22
  /** How each http-security code travels. */
21
23
  export const HTTP_SECURITY_ERROR_KINDS: Record<HttpSecurityErrorCode, ErrorKind> = {
24
+ [HttpSecurityErrorCode.RequiredDependencyUnavailable]: "unavailable",
22
25
  [HttpSecurityErrorCode.OriginRejected]: "forbidden",
23
26
  [HttpSecurityErrorCode.RateLimited]: "rate-limited",
24
27
  [HttpSecurityErrorCode.RequestInvalid]: "invalid",
@@ -1,10 +1,21 @@
1
1
  import type { ExecutionContext } from "@nestjs/common"
2
2
  import type { Request } from "express"
3
+ import { Test } from "@nestjs/testing"
3
4
  import { mock } from "@starci/jest-preset"
4
5
  import { requestOf } from "./execution-request.mapper"
5
6
 
7
+ const REQUEST_MAPPER = Symbol("REQUEST_MAPPER")
8
+
9
+ const build = async () => {
10
+ const moduleRef = await Test.createTestingModule({
11
+ providers: [{ provide: REQUEST_MAPPER, useValue: requestOf }],
12
+ }).compile()
13
+ return moduleRef.get<typeof requestOf>(REQUEST_MAPPER)
14
+ }
15
+
6
16
  describe("requestOf", () => {
7
- it("reads the HTTP request behind an HTTP execution context", () => {
17
+ it("reads the HTTP request behind an HTTP execution context", async () => {
18
+ const mapRequest = await build()
8
19
  const request = mock<Request>()
9
20
  const http = mock<ReturnType<ExecutionContext["switchToHttp"]>>()
10
21
  http.getRequest.mockReturnValue(request)
@@ -12,17 +23,18 @@ describe("requestOf", () => {
12
23
  context.getType.mockReturnValue("http")
13
24
  context.switchToHttp.mockReturnValue(http)
14
25
 
15
- expect(requestOf(context)).toBe(request)
26
+ expect(mapRequest(context)).toBe(request)
16
27
  expect(context.getArgs).not.toHaveBeenCalled()
17
28
  })
18
29
 
19
- it("reads the request of the GraphQL context behind a resolver's execution context", () => {
30
+ it("reads the request of the GraphQL context behind a resolver's execution context", async () => {
31
+ const mapRequest = await build()
20
32
  const request = mock<Request>()
21
33
  const context = mock<ExecutionContext>()
22
34
  context.getType.mockReturnValue("graphql")
23
35
  context.getArgs.mockReturnValue([undefined, {}, { req: request }, undefined])
24
36
 
25
- expect(requestOf(context)).toBe(request)
37
+ expect(mapRequest(context)).toBe(request)
26
38
  expect(context.switchToHttp).not.toHaveBeenCalled()
27
39
  })
28
40
  })
@@ -1,5 +1,6 @@
1
1
  import type { EnvSource } from "@modules/platform/config"
2
2
  import type { HttpSecurityOptions, WebhookProviderOptions } from "./http-security.options"
3
+ import { validateAllowedOrigins } from "./edge-security.service"
3
4
 
4
5
  /** Reads one webhook provider's required signing secret and bounded replay window. */
5
6
  export const parseWebhookProviderConfig = (env: EnvSource, prefix: string): WebhookProviderOptions => ({
@@ -8,16 +9,21 @@ export const parseWebhookProviderConfig = (env: EnvSource, prefix: string): Webh
8
9
  })
9
10
 
10
11
  /** Reads the http-security options: the origin allowlist is required, the rate limits are tunables with literal defaults. */
11
- export const parseHttpSecurityConfig = (env: EnvSource): HttpSecurityOptions => ({
12
- allowedOrigins: env
12
+ export const parseHttpSecurityConfig = (env: EnvSource): HttpSecurityOptions => {
13
+ const mode = env.optional("NODE_ENV") ?? "development"
14
+ const allowHttpOrigins = mode === "development" || mode === "test"
15
+ const origins = env
13
16
  .string("HTTP_SECURITY_ALLOWED_ORIGINS")
14
17
  .split(",")
15
18
  .map((origin) => origin.trim())
16
- .filter(Boolean),
17
- rateLimit: {
18
- windowMs: env.duration("HTTP_SECURITY_RATE_WINDOW", 60_000),
19
- defaultLimit: env.int("HTTP_SECURITY_RATE_DEFAULT_LIMIT", 600),
20
- strictLimit: env.int("HTTP_SECURITY_RATE_STRICT_LIMIT", 30),
21
- },
22
- webhooks: {},
23
- })
19
+ return {
20
+ allowedOrigins: validateAllowedOrigins(origins, allowHttpOrigins),
21
+ allowHttpOrigins,
22
+ rateLimit: {
23
+ windowMs: env.duration("HTTP_SECURITY_RATE_WINDOW", 60_000),
24
+ defaultLimit: env.int("HTTP_SECURITY_RATE_DEFAULT_LIMIT", 600),
25
+ strictLimit: env.int("HTTP_SECURITY_RATE_STRICT_LIMIT", 30),
26
+ },
27
+ webhooks: {},
28
+ }
29
+ }
@@ -3,6 +3,7 @@ import type { DynamicModule } from "@nestjs/common"
3
3
  import { APP_PIPE } from "@nestjs/core"
4
4
  import { WEBHOOK_SIGNATURE } from "./http-security.decorators"
5
5
  import { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } from "./http-security.module-definition"
6
+ import { ReadinessService } from "./readiness.service"
6
7
  import { RequestValidationService } from "./request-validation.service"
7
8
  import { WebhookSignatureService } from "./webhook-signature.service"
8
9
 
@@ -18,9 +19,10 @@ export class HttpSecurityModule extends ConfigurableModuleClass {
18
19
  ...(base.providers ?? []),
19
20
  { provide: APP_PIPE, useClass: RequestValidationService },
20
21
  WebhookSignatureService,
22
+ ReadinessService,
21
23
  { provide: WEBHOOK_SIGNATURE, useExisting: WebhookSignatureService },
22
24
  ],
23
- exports: [MODULE_OPTIONS_TOKEN, WebhookSignatureService, WEBHOOK_SIGNATURE],
25
+ exports: [MODULE_OPTIONS_TOKEN, WebhookSignatureService, WEBHOOK_SIGNATURE, ReadinessService],
24
26
  }
25
27
  }
26
28
  }
@@ -22,6 +22,8 @@ export interface WebhookProviderOptions {
22
22
  export interface HttpSecurityOptions {
23
23
  /** The origins allowed to send state-changing requests from a browser. */
24
24
  readonly allowedOrigins: ReadonlyArray<string>
25
+ /** Plain HTTP origins are admitted only by a development or test environment reader. */
26
+ readonly allowHttpOrigins?: boolean
25
27
  /** The rate limit tiers. */
26
28
  readonly rateLimit: RateLimitOptions
27
29
  /** Signed-webhook providers keyed by their generated route name. */
@@ -4,4 +4,9 @@ export { HttpSecurityModule } from "./http-security.module"
4
4
  export type { HttpSecurityOptions } from "./http-security.options"
5
5
  export { HTTP_SECURITY_MESSAGES } from "./messages/http-security.messages"
6
6
  export { OriginGuard } from "./origin.guard"
7
+ export { requestOf } from "./execution-request.mapper"
7
8
  export { RateLimitGuard } from "./rate-limit.guard"
9
+ export { applyHttpSecurity } from "./edge-security.service"
10
+ export { ReadinessService } from "./readiness.service"
11
+ export { InjectReadiness } from "./readiness.decorators"
12
+ export type { ReadinessReport } from "./readiness.contracts"
@@ -3,14 +3,21 @@ import type { MessageBundle } from "@modules/platform/i18n"
3
3
  /** Display text of the http-security codes, Vietnamese and English. */
4
4
  export const HTTP_SECURITY_MESSAGES: MessageBundle = {
5
5
  vi: {
6
- "errors.HTTP_SECURITY_ORIGIN_REJECTED": "Ngu\u1ed3n g\u1eedi y\u00eau c\u1ea7u kh\u00f4ng \u0111\u01b0\u1ee3c ph\u00e9p.",
7
- "errors.HTTP_SECURITY_RATE_LIMITED": "B\u1ea1n g\u1eedi qu\u00e1 nhi\u1ec1u y\u00eau c\u1ea7u, h\u00e3y th\u1eed l\u1ea1i sau.",
8
- "errors.HTTP_SECURITY_REQUEST_INVALID": "D\u1eef li\u1ec7u g\u1eedi l\u00ean kh\u00f4ng h\u1ee3p l\u1ec7: {{fields}}.",
6
+ "errors.HTTP_SECURITY_REQUIRED_DEPENDENCY_UNAVAILABLE": "C\u01a1 s\u1edf d\u1eef li\u1ec7u b\u1eaft bu\u1ed9c hi\u1ec7n kh\u00f4ng kh\u1ea3 d\u1ee5ng.",
7
+ "errors.HTTP_SECURITY_ORIGIN_REJECTED":
8
+ "Ngu\u1ed3n g\u1eedi y\u00eau c\u1ea7u kh\u00f4ng \u0111\u01b0\u1ee3c ph\u00e9p.",
9
+ "errors.HTTP_SECURITY_RATE_LIMITED":
10
+ "B\u1ea1n g\u1eedi qu\u00e1 nhi\u1ec1u y\u00eau c\u1ea7u, h\u00e3y th\u1eed l\u1ea1i sau.",
11
+ "errors.HTTP_SECURITY_REQUEST_INVALID":
12
+ "D\u1eef li\u1ec7u g\u1eedi l\u00ean kh\u00f4ng h\u1ee3p l\u1ec7: {{fields}}.",
9
13
  "errors.HTTP_SECURITY_WEBHOOK_SIGNATURE_INVALID": "Ch\u1eef k\u00fd webhook kh\u00f4ng h\u1ee3p l\u1ec7.",
10
- "errors.HTTP_SECURITY_WEBHOOK_REPLAYED": "Webhook n\u1eb1m ngo\u00e0i c\u1eeda s\u1ed5 th\u1eddi gian cho ph\u00e9p.",
11
- "errors.HTTP_SECURITY_WEBHOOK_PROVIDER_UNKNOWN": "Nh\u00e0 cung c\u1ea5p webhook {{provider}} ch\u01b0a \u0111\u01b0\u1ee3c c\u1ea5u h\u00ecnh.",
14
+ "errors.HTTP_SECURITY_WEBHOOK_REPLAYED":
15
+ "Webhook n\u1eb1m ngo\u00e0i c\u1eeda s\u1ed5 th\u1eddi gian cho ph\u00e9p.",
16
+ "errors.HTTP_SECURITY_WEBHOOK_PROVIDER_UNKNOWN":
17
+ "Nh\u00e0 cung c\u1ea5p webhook {{provider}} ch\u01b0a \u0111\u01b0\u1ee3c c\u1ea5u h\u00ecnh.",
12
18
  },
13
19
  en: {
20
+ "errors.HTTP_SECURITY_REQUIRED_DEPENDENCY_UNAVAILABLE": "The required database is unavailable.",
14
21
  "errors.HTTP_SECURITY_ORIGIN_REJECTED": "The origin of the request is not allowed.",
15
22
  "errors.HTTP_SECURITY_RATE_LIMITED": "Too many requests, try again later.",
16
23
  "errors.HTTP_SECURITY_REQUEST_INVALID": "The submitted data is invalid: {{fields}}.",
@@ -17,7 +17,9 @@ const options: HttpSecurityOptions = {
17
17
  webhooks: {},
18
18
  }
19
19
 
20
- const build = async (reflector: Reflector, clock: FakeClock) => {
20
+ const build = async (clock: FakeClock, tier?: RateTier) => {
21
+ const reflector = mock<Reflector>()
22
+ reflector.getAllAndOverride.mockReturnValue(tier)
21
23
  const moduleRef = await Test.createTestingModule({
22
24
  providers: [
23
25
  RateLimitGuard,
@@ -26,7 +28,7 @@ const build = async (reflector: Reflector, clock: FakeClock) => {
26
28
  { provide: HTTP_SECURITY_OPTIONS, useValue: options },
27
29
  ],
28
30
  }).compile()
29
- return moduleRef.get(RateLimitGuard)
31
+ return { guard: moduleRef.get(RateLimitGuard), reflector }
30
32
  }
31
33
 
32
34
  /** An execution context of one POST from `ip` on the door; the handler and the class carry the tier metadata. */
@@ -47,17 +49,20 @@ describe("RateLimit", () => {
47
49
  it("marks a door with the tier it declares, and the guard counts it under that tier's limit", async () => {
48
50
  class Door {}
49
51
  RateLimit(RateTier.Strict)(Door)
50
- const guard = await build(new Reflector(), new FakeClock(AT))
51
- const { context } = contextOf("127.0.0.1", () => undefined, Door)
52
+ const { guard, reflector } = await build(new FakeClock(AT), RateTier.Strict)
53
+ const handler = () => undefined
54
+ const { context } = contextOf("127.0.0.1", handler, Door)
52
55
 
56
+ expect(Reflect.getMetadata("platform.http-security.rate-tier", Door)).toBe(RateTier.Strict)
53
57
  expect(guard.canActivate(context)).toBe(true)
54
58
  expect(() => guard.canActivate(context)).toThrow(RATE_LIMITED)
59
+ expect(reflector.getAllAndOverride).toHaveBeenCalledWith("platform.http-security.rate-tier", [handler, Door])
55
60
  })
56
61
  })
57
62
 
58
63
  describe("RateLimitGuard", () => {
59
64
  it("counts an unmarked door under the default limit", async () => {
60
- const guard = await build(mock<Reflector>(), new FakeClock(AT))
65
+ const { guard } = await build(new FakeClock(AT))
61
66
  const { context } = contextOf("127.0.0.1")
62
67
 
63
68
  expect(guard.canActivate(context)).toBe(true)
@@ -67,7 +72,7 @@ describe("RateLimitGuard", () => {
67
72
 
68
73
  it("opens a fresh window for a caller whose bucket expired", async () => {
69
74
  const clock = new FakeClock(AT)
70
- const guard = await build(mock<Reflector>(), clock)
75
+ const { guard } = await build(clock)
71
76
  const { context } = contextOf("127.0.0.1")
72
77
 
73
78
  guard.canActivate(context)
@@ -80,7 +85,7 @@ describe("RateLimitGuard", () => {
80
85
  })
81
86
 
82
87
  it("counts a request without an address under the unknown bucket", async () => {
83
- const guard = await build(mock<Reflector>(), new FakeClock(AT))
88
+ const { guard } = await build(new FakeClock(AT))
84
89
  const { context } = contextOf(undefined)
85
90
 
86
91
  guard.canActivate(context)
@@ -90,7 +95,7 @@ describe("RateLimitGuard", () => {
90
95
 
91
96
  it("prunes the expired buckets when the map outgrows the bucket cap", async () => {
92
97
  const clock = new FakeClock(AT)
93
- const guard = await build(mock<Reflector>(), clock)
98
+ const { guard } = await build(clock)
94
99
  const { context, setIp } = contextOf("0.0.0.0")
95
100
 
96
101
  for (let index = 0; index <= 10_000; index += 1) {
@@ -0,0 +1,5 @@
1
+ /** The required primary database answered before the readiness deadline. */
2
+ export interface ReadinessReport {
3
+ /** Whether this process can serve requests requiring the database. */
4
+ readonly ok: true
5
+ }