@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,2 +1,8 @@
1
- /** The identity capability takes no options yet; the empty record keeps `isGlobal` assignable through the configurable module. */
2
- export type IdentityOptions = Record<never, never>
1
+ import type { Type } from "@nestjs/common"
2
+ import type { CredentialAuthenticator } from "./credential.contracts"
3
+
4
+ /** The identity capability's options. */
5
+ export interface IdentityOptions {
6
+ /** The app-owned authenticator of alternate credentials; a credential door fails closed while it is absent. */
7
+ readonly credentialAuthenticator?: Type<CredentialAuthenticator>
8
+ }
@@ -1,4 +1,5 @@
1
1
  export { AuthGuard } from "./auth.guard"
2
+ export { CredentialGuard } from "./credential.guard"
2
3
  export { IDENTITY_ERROR_KINDS } from "./errors/identity.error"
3
4
  export { IDENTITY_MESSAGES } from "./messages/identity.messages"
4
5
  export { PublicReason } from "./identity.contracts"
@@ -21,18 +21,18 @@ export class Secret {
21
21
  }
22
22
 
23
23
  /**
24
- * The only reader of the process environment. Typed readers name the missing or malformed key in the error; a key
25
- * `<KEY>_FILE` supplies `<KEY>` from a file, so secrets can be mounted instead of exported.
24
+ * The only reader of the process environment. Typed readers name the missing or malformed key in the error.
25
+ * A declared `<KEY>_FILE` takes priority over `<KEY>` and supplies its exact file contents, so secrets can be mounted.
26
26
  */
27
27
  export class EnvSource {
28
28
  constructor(private readonly values: Readonly<Record<string, string | undefined>>) {}
29
29
 
30
- /** Reads the process environment once, resolving every `<KEY>_FILE` into `<KEY>`. */
30
+ /** Reads the process environment once; a declared `<KEY>_FILE` must be readable and never falls back to `<KEY>`. */
31
31
  static fromProcess(): EnvSource {
32
32
  const values: Record<string, string | undefined> = { ...process.env }
33
- for (const [key, path] of Object.entries(process.env)) {
34
- if (key.endsWith(FILE_SUFFIX) && path)
35
- values[key.slice(0, -FILE_SUFFIX.length)] = readFileSync(path, "utf8").trim()
33
+ for (const [key, path] of Object.entries(values)) {
34
+ if (key.endsWith(FILE_SUFFIX) && path !== undefined)
35
+ values[key.slice(0, -FILE_SUFFIX.length)] = readFileSync(path, "utf8")
36
36
  }
37
37
  return new EnvSource(values)
38
38
  }
@@ -1,7 +1,10 @@
1
+ import { Test } from "@nestjs/testing"
1
2
  import type { QueryRunner } from "typeorm"
2
3
  import { Secret } from "@modules/platform/config"
3
4
  import type { DatabaseConnectionOptions } from "./database.options"
4
5
  import { openConnectionSource } from "./connection-source.client"
6
+ import { CONNECTION_SOURCE } from "./database.port"
7
+ import type { ConnectionOpener } from "./database.port"
5
8
 
6
9
  class NoteEntity {}
7
10
 
@@ -16,20 +19,24 @@ class CreateNotes {
16
19
  }
17
20
 
18
21
  describe("openConnectionSource", () => {
19
- it("opens an isolated PostgreSQL data source with the connection's entities and migrations", () => {
22
+ it("opens an isolated PostgreSQL data source with the connection's entities and migrations", async () => {
23
+ const moduleRef = await Test.createTestingModule({
24
+ providers: [{ provide: CONNECTION_SOURCE, useFactory: () => ({ open: openConnectionSource }) }],
25
+ }).compile()
26
+ const client = moduleRef.get<ConnectionOpener>(CONNECTION_SOURCE)
20
27
  const connection: DatabaseConnectionOptions = {
21
28
  name: "primary",
22
- url: new Secret("postgres://user:password@database.test:5432/primary"),
29
+ url: new Secret("postgres://database.test:5432/primary"),
23
30
  entities: [NoteEntity],
24
31
  migrations: [CreateNotes],
25
32
  }
26
33
 
27
- const source = openConnectionSource(connection)
34
+ const source = client.open(connection)
28
35
 
29
36
  expect(source).toMatchObject({
30
37
  options: {
31
38
  type: "postgres",
32
- url: "postgres://user:password@database.test:5432/primary",
39
+ url: "postgres://database.test:5432/primary",
33
40
  entities: [NoteEntity],
34
41
  migrations: [CreateNotes],
35
42
  migrationsTableName: "primary_migrations",
@@ -0,0 +1,32 @@
1
+ import { Test } from "@nestjs/testing"
2
+ import { fakeTransaction, mockEntityManager } from "@starci/jest-preset"
3
+ import type { MockEntityManager } from "@starci/jest-preset"
4
+ import { DatabaseProbeService } from "./database-probe.service"
5
+ import { PRIMARY_ENTITY_MANAGER } from "./primary.decorators"
6
+
7
+ const build = async (entityManager: MockEntityManager) => {
8
+ const moduleRef = await Test.createTestingModule({
9
+ providers: [DatabaseProbeService, { provide: PRIMARY_ENTITY_MANAGER, useValue: entityManager }],
10
+ }).compile()
11
+ return moduleRef.get(DatabaseProbeService)
12
+ }
13
+
14
+ describe("DatabaseProbeService", () => {
15
+ it("opens and commits a transaction against the required database", async () => {
16
+ const transaction = fakeTransaction(mockEntityManager())
17
+ const service = await build(transaction.em)
18
+
19
+ await expect(service.probe()).resolves.toBeUndefined()
20
+
21
+ expect(transaction.em.transaction).toHaveBeenCalledTimes(1)
22
+ expect(transaction.commits).toBe(1)
23
+ })
24
+
25
+ it("propagates a failed database transaction", async () => {
26
+ const entityManager = mockEntityManager()
27
+ entityManager.transaction.mockRejectedValue(new Error("connection unavailable"))
28
+ const service = await build(entityManager)
29
+
30
+ await expect(service.probe()).rejects.toThrow("connection unavailable")
31
+ })
32
+ })
@@ -0,0 +1,14 @@
1
+ import { Injectable } from "@nestjs/common"
2
+ import type { EntityManager } from "typeorm"
3
+ import { InjectPrimaryEntityManager } from "./primary.decorators"
4
+
5
+ @Injectable()
6
+ /** Checks that the required primary database accepts and commits a transaction. */
7
+ export class DatabaseProbeService {
8
+ constructor(@InjectPrimaryEntityManager() private readonly entityManager: EntityManager) {}
9
+
10
+ /** Reaches the database and fails when its transaction cannot complete. */
11
+ async probe(): Promise<void> {
12
+ await this.entityManager.transaction(() => Promise.resolve(undefined))
13
+ }
14
+ }
@@ -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
+ }