@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
@@ -10,6 +10,8 @@ import { ScaffoldError, pascalOf } from "./service.mjs";
10
10
  import { filesBelow, registerLiteDomainService, registerLiteExports, registrationFiles } from "./lite-exports.mjs";
11
11
  import { byCodeUnit } from "../report/order.mjs";
12
12
  import { coreFeApps } from "./app.mjs";
13
+ import { addModuleImport, planComposeGroup } from "./add-compose.mjs";
14
+ import { withSqlIdentifierError } from "./add-database-lite.mjs";
13
15
 
14
16
  const NAME = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
15
17
  const MIGRATION = /^(\d{14})_[a-z][a-z0-9]*(?:-[a-z0-9]+)*\.sql$/;
@@ -163,29 +165,6 @@ function removeEmptyParents(file, stop) {
163
165
  /** The app-root relative path of a file under the app (posix). */
164
166
  const at = (root, relative) => path.join(root, ...relative.split("/"));
165
167
 
166
- /**
167
- * Registers `entry` in the `imports` array of a Nest module file (one import line after the last import, one array entry right
168
- * after `imports: [`), unless `symbol` is already imported. Returns the new text.
169
- */
170
- function wireImport(text, relative, { importLine, symbol, entry, kinds }) {
171
- const lines = text.replace(/\r\n/g, "\n").split("\n");
172
- if (lines.some((line) => line.startsWith("import ") && new RegExp(`\\b${symbol}\\b`).test(line))) return text;
173
- const lastImport = lines.reduce((last, line, index) => (line.startsWith("import ") ? index : last), -1);
174
- if (lastImport < 0 || lines.findIndex((line) => /^\s*imports: \[$/.test(line)) < 0)
175
- throw new ScaffoldError("HFS_ADD_WIRE_INVALID", `add table could not find the imports array in ${relative}`);
176
- lines.splice(lastImport + 1, 0, importLine);
177
- const imports = lines.findIndex((line) => /^\s*imports: \[$/.test(line));
178
- const indent = /^(\s*)/.exec(lines[imports])?.[1] ?? "";
179
- lines.splice(imports + 1, 0, `${indent} ${entry},`);
180
- if (kinds !== undefined) {
181
- const kindsAt = lines.findIndex((line) => /^\s*kinds: \[$/.test(line));
182
- if (kindsAt < 0) throw new ScaffoldError("HFS_ADD_WIRE_INVALID", `add table could not find the error kinds array in ${relative}`);
183
- const kindsIndent = /^(\s*)/.exec(lines[kindsAt])?.[1] ?? "";
184
- lines.splice(kindsAt + 1, 0, `${kindsIndent} ${kinds},`);
185
- }
186
- return `${lines.join("\n").replace(/\n+$/, "")}\n`;
187
- }
188
-
189
168
  /** A line that opens the error kinds array of the errors registration. */
190
169
  const KINDS_LINE = /^\s*kinds: \[$/;
191
170
 
@@ -206,45 +185,33 @@ function kindsHomeOf(root, appRelative) {
206
185
  /** A composition group after `kinds` is listed first in its error kinds array and imported from `specifier`. Returns the new text. */
207
186
  function wireKinds(text, relative, { kinds, specifier }) {
208
187
  const lines = text.replace(/\r\n/g, "\n").split("\n");
209
- if (lines.some((line) => line.startsWith("import ") && new RegExp(`\\b${kinds}\\b`).test(line))) return text;
188
+ if (lines.some((line) => line.startsWith("import ") && new RegExp(String.raw`\b${kinds}\b`).test(line))) return text;
210
189
  const lastImport = lines.reduce((last, line, index) => (line.startsWith("import ") ? index : last), -1);
211
190
  const kindsAt = lines.findIndex((line) => KINDS_LINE.test(line));
212
191
  if (lastImport < 0 || kindsAt < 0) throw new ScaffoldError("HFS_ADD_WIRE_INVALID", `add table could not find the error kinds array in ${relative}`);
213
192
  const kindsIndent = /^(\s*)/.exec(lines[kindsAt])?.[1] ?? "";
214
193
  lines.splice(kindsAt + 1, 0, `${kindsIndent} ${kinds},`);
215
194
  lines.splice(lastImport + 1, 0, `import { ${kinds} } from "${specifier}"`);
216
- return `${lines.join("\n").replace(/\n+$/, "")}\n`;
195
+ return `${lines.join("\n").trimEnd()}\n`;
217
196
  }
218
197
 
219
- /**
220
- * The api app root after the table's domain module is registered: its module, and its error kinds too when the root holds the
221
- * kinds array (`kindsInRoot`); otherwise wireKinds lists them in the composition group that does.
222
- */
223
- const wireDomainModule = (text, relative, name, { kindsInRoot = true } = {}) =>
224
- wireImport(text, relative, {
225
- importLine: kindsInRoot
226
- ? `import { ${snakeOf(name).toUpperCase()}_ERROR_KINDS, ${pascalOf(name)}Module } from "@modules/domain/${name}"`
227
- : `import { ${pascalOf(name)}Module } from "@modules/domain/${name}"`,
228
- symbol: `${pascalOf(name)}Module`,
229
- entry: `${pascalOf(name)}Module.register({ isGlobal: true })`,
230
- ...(kindsInRoot ? { kinds: `${snakeOf(name).toUpperCase()}_ERROR_KINDS` } : {}),
198
+ /** Plans the domain and API area groups of the connection owner, keeping registrations out of app.module.ts. */
199
+ function tableComposition({ root, repo, owner, feature, fe, ownerRelative }) {
200
+ const domain = { importLine: `import { ${pascalOf(feature)}Module } from "@modules/domain/${feature}"`, entry: `${pascalOf(feature)}Module.register({ isGlobal: true })` };
201
+ const groups = [['domain', [domain]]];
202
+ if (fe) groups.push(['api', [
203
+ { importLine: 'import { GraphqlModule } from "@modules/platform/graphql"', entry: 'GraphqlModule.register({ isGlobal: true })' },
204
+ { importLine: `import { ${pascalOf(feature)}GraphqlModule } from "@features/api/${feature}"`, entry: `${pascalOf(feature)}GraphqlModule.register({})` },
205
+ ]]);
206
+ const lines = fs.readFileSync(at(root, ownerRelative), 'utf8').replace(/\r\n/g, '\n').split('\n');
207
+ const patches = groups.map(([area, entries]) => {
208
+ const argument = area === 'domain' && repo.edition === 'lite' ? 'verifyAccessToken' : '';
209
+ addModuleImport(lines, { importLine: `import { ${area}Modules } from "./compose/${area}"`, entry: `...${area}Modules(${argument})`, relative: ownerRelative, noun: 'table' });
210
+ return planComposeGroup({ repoRoot: root, repo, app: owner.name, area, noun: 'table', entries });
231
211
  });
232
-
233
- /** The api app root after the GraphQL server (platform/graphql) and the table's GraphQL transport are registered. */
234
- const wireGraphqlFeature = (text, relative, name) =>
235
- wireImport(
236
- wireImport(text, relative, {
237
- importLine: 'import { GraphqlModule } from "@modules/platform/graphql"',
238
- symbol: "GraphqlModule",
239
- entry: "GraphqlModule.register({ isGlobal: true })",
240
- }),
241
- relative,
242
- {
243
- importLine: `import { ${pascalOf(name)}GraphqlModule } from "@features/api/${name}"`,
244
- symbol: `${pascalOf(name)}GraphqlModule`,
245
- entry: `${pascalOf(name)}GraphqlModule`,
246
- },
247
- );
212
+ patches.push({ relative: ownerRelative, text: lines.join('\n') });
213
+ return patches;
214
+ }
248
215
 
249
216
  /** The platform GraphQL server capability (templates/be/platform/graphql), whole or absent: written once, by the first GraphQL feature. */
250
217
  const GRAPHQL_PLATFORM = "be/src/modules/platform/graphql";
@@ -287,7 +254,7 @@ const GRAPHQL_URL_READER = [
287
254
 
288
255
  /** An app's modules/config text with graphqlUrl appended once. */
289
256
  const withGraphqlUrl = (text) =>
290
- /^export const graphqlUrl\b/m.test(text) ? text : `${text.replace(/\s*$/, "")}\n${GRAPHQL_URL_READER}\n`;
257
+ /^export const graphqlUrl\b/m.test(text) ? text : `${text.trimEnd()}\n${GRAPHQL_URL_READER}\n`;
291
258
 
292
259
  /** The be api feature of a table (owner ruling 2026-10-13, lite = full): get-<singular> and create-<singular> over GraphQL. */
293
260
  const apiFeatureFiles = (values) => {
@@ -296,6 +263,8 @@ const apiFeatureFiles = (values) => {
296
263
  const create = `create-${values.singular}`;
297
264
  return [
298
265
  [`${base}/index.ts`, "feature.index.ts.tpl"],
266
+ [`${base}/${values.name}.options.ts`, "feature.options.ts.tpl"],
267
+ [`${base}/${values.name}.module-definition.ts`, "feature.module-definition.ts.tpl"],
299
268
  [`${base}/${values.name}.module.ts`, "feature.module.ts.tpl"],
300
269
  [`${base}/application/${get}.query.ts`, "get.query.ts.tpl"],
301
270
  [`${base}/application/${get}.handler.ts`, "get.handler.ts.tpl"],
@@ -310,6 +279,8 @@ const apiFeatureFiles = (values) => {
310
279
  [`${base}/transport/graphql/${create}.resolver.ts`, "create.resolver.ts.tpl"],
311
280
  [`${base}/transport/graphql/${create}.mapper.ts`, "create.mapper.ts.tpl"],
312
281
  [`${base}/transport/graphql/dto/${create}.type.ts`, "create.type.ts.tpl"],
282
+ [`${base}/transport/graphql/${values.name}-graphql.options.ts`, "graphql.options.ts.tpl"],
283
+ [`${base}/transport/graphql/${values.name}-graphql.module-definition.ts`, "graphql.module-definition.ts.tpl"],
313
284
  [`${base}/transport/graphql/${values.name}-graphql.module.ts`, "graphql.module.ts.tpl"],
314
285
  ].map(([relative, file]) => ({ relative, body: template(`be/table/api/${file}`, values) }));
315
286
  };
@@ -351,7 +322,7 @@ const feBarrels = (app, values) => {
351
322
  const withExport = (text, line) => {
352
323
  const lines = text.replace(/\r\n/g, "\n").split("\n").filter(Boolean);
353
324
  if (lines.includes(line)) return text;
354
- return `${[...lines, line].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join("\n")}\n`;
325
+ return `${[...lines, line].sort(byCodeUnit).join("\n")}\n`;
355
326
  };
356
327
 
357
328
  /**
@@ -359,13 +330,7 @@ const withExport = (text, line) => {
359
330
  * server capability on first use) and, in every product Next app, its operation files and SWR hooks.
360
331
  * `emitTypes` returns the generated database.types.ts text; false is the explicit `--no-types` seam.
361
332
  */
362
- export function addTable({
363
- root,
364
- name,
365
- fe = false,
366
- emitTypes = emitDbTypes,
367
- now = Date.now,
368
- }) {
333
+ function tableContext({ root, name }) {
369
334
  const requestedName = String(name);
370
335
  if (!NAME.test(requestedName))
371
336
  throw new ScaffoldError(
@@ -384,12 +349,16 @@ export function addTable({
384
349
  );
385
350
  }
386
351
 
352
+ return { root, name, requestedName, feature, table, repo, connection };
353
+ }
354
+
355
+ function nextMigration({ root, feature, requestedName, now }) {
387
356
  const migrationsDir = path.join(root, "supabase", "migrations");
388
357
  const migrations = fs.existsSync(migrationsDir)
389
358
  ? fs
390
359
  .readdirSync(migrationsDir)
391
360
  .filter((file) => MIGRATION.test(file))
392
- .sort()
361
+ .sort(byCodeUnit)
393
362
  : [];
394
363
  if (migrations.some((file) => file.endsWith(`_${feature}.sql`)))
395
364
  throw new ScaffoldError(
@@ -404,6 +373,10 @@ export function addTable({
404
373
  `the current UTC timestamp ${stamp} is not later than the latest migration ${latest}; fix the clock or wait for the next second`,
405
374
  );
406
375
 
376
+ return stamp;
377
+ }
378
+
379
+ function tableValues({ feature, table }) {
407
380
  const singular = singularOf(feature);
408
381
  const values = {
409
382
  name: feature,
@@ -417,12 +390,20 @@ export function addTable({
417
390
  singularCamel: camelOf(singular),
418
391
  singularUpper: snakeOf(singular).toUpperCase(),
419
392
  };
393
+ return values;
394
+ }
395
+
396
+ function productApps(repo, fe) {
420
397
  const apps = fe ? coreFeApps(repo.sides?.fe?.apps ?? []) : [];
421
398
  if (fe && !apps.length)
422
399
  throw new ScaffoldError(
423
400
  "HFS_ADD_NO_FRONT_END",
424
401
  "add table --fe needs at least one declared product Next app",
425
402
  );
403
+ return apps;
404
+ }
405
+
406
+ function tableFiles({ root, repo, feature, stamp, values, fe, apps }) {
426
407
  const api = fe ? ".api" : "";
427
408
  const domain = `be/src/modules/domain/${feature}`;
428
409
  const planned = [
@@ -455,6 +436,10 @@ export function addTable({
455
436
  }
456
437
  }
457
438
 
439
+ return planned;
440
+ }
441
+
442
+ function tableOwner({ root, repo, connection, name, requestedName, planned }) {
458
443
  const clash = planned
459
444
  .filter((file) => fs.existsSync(at(root, file.relative)))
460
445
  .map((file) => file.relative);
@@ -470,8 +455,10 @@ export function addTable({
470
455
  if (!fs.existsSync(at(root, ownerRelative)))
471
456
  throw new ScaffoldError("HFS_ADD_WIRE_MISSING", `add table registers ${name} in ${ownerRelative}, which does not exist`);
472
457
 
473
- // Every existing file this run edits, as it was: restored on any failure, with every created file removed.
474
- const touched = new Map();
458
+ return { owner, ownerRelative };
459
+ }
460
+
461
+ function tableEditor(root, touched) {
475
462
  const edit = (relative, change) => {
476
463
  const file = at(root, relative);
477
464
  if (!touched.has(file)) touched.set(file, fs.existsSync(file) ? fs.readFileSync(file) : null);
@@ -481,38 +468,51 @@ export function addTable({
481
468
  fs.mkdirSync(path.dirname(file), { recursive: true });
482
469
  fs.writeFileSync(file, after);
483
470
  };
484
- const created = [];
485
- try {
486
- for (const file of planned) {
487
- const target = at(root, file.relative);
488
- fs.mkdirSync(path.dirname(target), { recursive: true });
489
- fs.writeFileSync(target, file.body);
490
- created.push(target);
491
- }
492
- if (typeof emitTypes === "function") {
493
- const generated = emitTypes({ root });
494
- if (typeof generated === "string") edit(dbTypesPath, () => generated);
495
- }
496
- const kindsHome = kindsHomeOf(root, ownerRelative);
497
- const kindsInRoot = kindsHome === ownerRelative;
498
- edit(ownerRelative, (text) => wireDomainModule(text, ownerRelative, feature, { kindsInRoot }));
499
- if (!kindsInRoot)
500
- edit(kindsHome, (text) => wireKinds(text, kindsHome, { kinds: `${snakeOf(feature).toUpperCase()}_ERROR_KINDS`, specifier: `@modules/domain/${feature}` }));
471
+ return edit;
472
+ }
473
+
474
+ function writeTableFiles({ root, planned, emitTypes, edit }, created) {
475
+ for (const file of planned) {
476
+ const target = at(root, file.relative);
477
+ fs.mkdirSync(path.dirname(target), { recursive: true });
478
+ fs.writeFileSync(target, file.body);
479
+ created.push(target);
480
+ }
481
+ if (typeof emitTypes === "function") {
482
+ const generated = emitTypes({ root });
483
+ if (typeof generated === "string") edit(dbTypesPath, () => generated);
484
+ }
485
+
486
+ }
487
+
488
+ function tableRegistrations({ root, repo, fe, edit }) {
489
+ const errorFile = "be/src/modules/platform/database/errors/database.error.ts";
490
+ if (repo.edition === "lite" && fs.existsSync(at(root, errorFile))) edit(errorFile, withSqlIdentifierError);
501
491
  const registrations = [
502
492
  ...(repo.edition === "lite" ? ["table"] : []),
503
493
  ...(fe ? ["api", "graphql"] : []),
504
494
  ];
505
495
  for (const generator of registrations) for (const relative of registrationFiles(generator)) edit(relative, (text) => text);
506
496
  for (const generator of registrations) registerLiteExports({ root, generator });
507
- if (fe) {
497
+ }
498
+
499
+ function wireTableFrontends({ root, feature, values, apps, edit }) {
508
500
  registerLiteDomainService({ root, moduleSpecifier: `@modules/domain/${feature}`, service: `${values.Name}Service` });
509
- edit(ownerRelative, (text) => wireGraphqlFeature(text, ownerRelative, feature));
510
501
  for (const app of apps) {
511
502
  for (const [relative, line] of feBarrels(app.name, values)) edit(relative, (text) => withExport(text, line));
512
503
  edit(`fe/apps/${app.name}/src/modules/config/index.ts`, withGraphqlUrl);
513
504
  }
514
- }
515
- } catch (error) {
505
+ }
506
+
507
+ function wireTable({ root, repo, feature, values, ownerRelative, composition, fe, apps, edit }) {
508
+ const kindsHome = kindsHomeOf(root, ownerRelative);
509
+ for (const patch of composition) edit(patch.relative, () => patch.text);
510
+ edit(kindsHome, (text) => wireKinds(text, kindsHome, { kinds: `${snakeOf(feature).toUpperCase()}_ERROR_KINDS`, specifier: `@modules/domain/${feature}` }));
511
+ tableRegistrations({ root, repo, fe, edit });
512
+ if (fe) wireTableFrontends({ root, feature, values, apps, edit });
513
+ }
514
+
515
+ function rollbackTable(root, touched, created) {
516
516
  for (const [file, before] of touched) {
517
517
  if (before === null) fs.rmSync(file, { force: true });
518
518
  else fs.writeFileSync(file, before);
@@ -522,10 +522,33 @@ export function addTable({
522
522
  removeEmptyParents(file, root);
523
523
  }
524
524
  for (const [file, before] of touched) if (before === null) removeEmptyParents(file, root);
525
+ }
526
+
527
+ function applyTable(context) {
528
+ const touched = new Map();
529
+ const created = [];
530
+ const edit = tableEditor(context.root, touched);
531
+ try {
532
+ writeTableFiles({ ...context, edit }, created);
533
+ wireTable({ ...context, edit });
534
+ } catch (error) {
535
+ rollbackTable(context.root, touched, created);
525
536
  throw error;
526
537
  }
538
+ }
539
+
540
+ /** Adds one table through typed domain/API groups and restores every touched byte on failure. */
541
+ export function addTable({ root, name, fe = false, emitTypes = emitDbTypes, now = Date.now }) {
542
+ const context = tableContext({ root, name });
543
+ const stamp = nextMigration({ ...context, now });
544
+ const values = tableValues(context);
545
+ const apps = productApps(context.repo, fe);
546
+ const planned = tableFiles({ ...context, stamp, values, apps, fe });
547
+ const owner = tableOwner({ ...context, planned });
548
+ const composition = tableComposition({ ...context, ...owner, fe });
549
+ applyTable({ ...context, ...owner, planned, composition, apps, values, fe, emitTypes });
527
550
  return {
528
- created: planned.map((file) => file.relative),
551
+ created: [...planned.map((file) => file.relative), ...composition.filter((patch) => patch.exists === false).map((patch) => patch.relative)],
529
552
  types: typeof emitTypes === "function" ? dbTypesPath : "skipped",
530
553
  };
531
554
  }
package/scaffold/add.mjs CHANGED
@@ -16,9 +16,9 @@
16
16
  // `@@vCamel@@` its camelCase, `@@vUpper@@` its UPPER_SNAKE and `@@vSnake@@` its snake_case; the option `--service Class=module` gives `@@service@@` (the class) and `@@serviceModule@@`.
17
17
  import fs from 'node:fs';
18
18
  import path from 'node:path';
19
- import { TEMPLATES_DIR } from '../sync/index.mjs';
20
19
  import { ScaffoldError, pascalOf } from './service.mjs';
21
20
  import { addTable, graphqlPlatformFiles } from './add-table.mjs';
21
+ import { addModuleImport, compositionCode, readComposeGroup, writeComposeGroup } from './add-compose.mjs';
22
22
  import { addApp } from './add-app.mjs';
23
23
  import { ensureLiteCli } from './add-cli-lite.mjs';
24
24
  import { addLiteWebhookInbox } from './add-inbox-lite.mjs';
@@ -59,6 +59,7 @@ function registerFeatureExports({ repoRoot, repo, noun, values }) {
59
59
  }
60
60
 
61
61
  function writeKind({ repoRoot, noun, name, now, manifest, repo, spec, declarationFile, values, forms, beRoot, patterns, planned }) {
62
+ if (noun === 'api' || noun === 'webhook') preflightFeatureComposition({ repoRoot, repo, noun, name });
62
63
  const bootstrapCreated = noun === 'cli' && repo.edition === 'lite' ? ensureLiteCli({ root: repoRoot, manifest, repo }) : [];
63
64
  for (const file of planned) {
64
65
  fs.mkdirSync(path.dirname(file.absolute), { recursive: true });
@@ -66,8 +67,8 @@ function writeKind({ repoRoot, noun, name, now, manifest, repo, spec, declaratio
66
67
  }
67
68
  registerFeatureExports({ repoRoot, repo, noun, values });
68
69
  if (spec.wire) wire({ beRoot, wire: spec.wire, forms, noun, name });
69
- const platformCreated = repo.edition === 'lite' && (noun === 'api' || noun === 'webhook') ? wireLiteFeature({ repoRoot, repo, noun, name }) : [];
70
70
  const inboxCreated = addLiteWebhookInbox({ root: repoRoot, repo, noun, provider: name, values, now });
71
+ const platformCreated = noun === 'api' || noun === 'webhook' ? wireFeature({ repoRoot, repo, noun, name }) : [];
71
72
  const graphqlDoor = noun === 'api' ? ensureGraphqlPeers(repoRoot) : [];
72
73
  const registered = register({ declarationFile, spec: { patterns, trigger: spec.trigger, optionalSlots: noun === 'api' ? [GRAPHQL_CONTRACT_SLOT] : [] } });
73
74
  return { created: [...bootstrapCreated, ...planned.map((file) => `be/${file.target}`), ...platformCreated, ...inboxCreated, ...graphqlDoor], registered };
@@ -91,15 +92,59 @@ function ensureGraphqlPeers(repoRoot) {
91
92
  return ['package.json'];
92
93
  }
93
94
 
94
- /** Adds `importLine` and one `symbol` entry of the `imports` array of a module file's lines, once. */
95
- function addModuleImport(lines, { importLine, entry, relative, noun }) {
96
- if (lines.includes(importLine)) return;
97
- const lastImport = lines.reduce((last, line, index) => line.startsWith('import ') ? index : last, -1);
98
- if (lastImport < 0 || lines.findIndex(line => /^\s*imports: \[$/.test(line)) < 0) throw new ScaffoldError('HFS_ADD_WIRE_INVALID', `add ${noun} could not find the imports array in ${relative}`);
99
- lines.splice(lastImport + 1, 0, importLine);
100
- const imports = lines.findIndex(line => /^\s*imports: \[$/.test(line));
101
- const indent = /^(\s*)/.exec(lines[imports])?.[1] ?? '';
102
- lines.splice(imports + 1, 0, `${indent} ${entry},`);
95
+ /** The skeleton registrations that move into area groups when the first transport is added. */
96
+ function skeletonComposition(lines, edition) {
97
+ const active = compositionCode(lines.join('\n')).split('\n');
98
+ const candidates = [
99
+ { area: 'domain', symbol: 'IdentityModule', source: '@modules/domain/identity', entry: edition === 'lite' ? 'IdentityModule.register({ isGlobal: true, verifyAccessToken })' : 'IdentityModule.register({ isGlobal: true })' },
100
+ { area: 'domain', symbol: 'LivenessModule', source: '@modules/domain/liveness', entry: 'LivenessModule.register({ isGlobal: true })' },
101
+ { area: 'api', symbol: 'SystemHealthHttpModule', source: '@features/api/system-health', entry: 'SystemHealthHttpModule.register({})' },
102
+ { area: 'api', symbol: 'GraphqlModule', source: '@modules/platform/graphql', entry: 'GraphqlModule.register({ isGlobal: true })' },
103
+ ];
104
+ if (edition === 'full') candidates.push({ area: 'domain', symbol: 'NoteModule', source: '@modules/domain/note', entry: 'NoteModule.register({ isGlobal: true })' });
105
+ return candidates.filter(({ entry }) => active.some(line => line.trim() === `${entry},`));
106
+ }
107
+
108
+ /** Existing groups that the root spreads must survive when another area is generated. */
109
+ function referencedComposeAreas(lines) {
110
+ const code = compositionCode(lines.join('\n'));
111
+ return ['domain', 'system', 'api', 'webhooks'].filter((area) => code.includes(`...${area}Modules(`));
112
+ }
113
+
114
+ function featureCompositionRoot({ repoRoot, repo, noun }) {
115
+ const app = repo.sides.be.apps.find(entry => entry.kind === 'api');
116
+ if (!app) throw new ScaffoldError('HFS_ADD_NO_API_OWNER', `add ${noun} needs a declared api app`);
117
+ const relative = `be/apps/${app.name}/src/app.module.ts`;
118
+ const file = path.join(repoRoot, ...relative.split('/'));
119
+ if (!fs.existsSync(file)) throw new ScaffoldError('HFS_ADD_WIRE_MISSING', `add ${noun} registers its transport in ${relative}, which does not exist`);
120
+ return { app, relative, file, lines: fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n').split('\n') };
121
+ }
122
+
123
+ /** Refuse an incompatible root or area group before generating the feature or changing its capability exports. */
124
+ function preflightFeatureComposition({ repoRoot, repo, noun, name }) {
125
+ const { app, relative, lines } = featureCompositionRoot({ repoRoot, repo, noun });
126
+ if (noun === 'webhook' && repo.edition === 'lite') {
127
+ if (!compositionCode(lines.join('\n')).split('\n').some(line => /^\s*imports: \[$/.test(line)))
128
+ throw new ScaffoldError('HFS_ADD_WIRE_INVALID', `add webhook needs a multiline imports array in ${relative}`);
129
+ planLiteWebhookConfig({ repoRoot, app: app.name, provider: name });
130
+ }
131
+ const areas = new Set([noun === 'api' ? 'api' : 'webhooks', ...referencedComposeAreas(lines), ...skeletonComposition(lines, repo.edition).map(entry => entry.area)]);
132
+ for (const area of areas) {
133
+ readComposeGroup({ repoRoot, repo, app: app.name, area, noun });
134
+ const argument = area === 'domain' && repo.edition === 'lite' ? 'verifyAccessToken' : '';
135
+ addModuleImport(lines, { importLine: `import { ${area}Modules } from "./compose/${area}"`, entry: `...${area}Modules(${argument})`, relative, noun });
136
+ }
137
+ }
138
+
139
+ /** Remove only a moved skeleton registration and its named import, keeping guards imported from the same barrel. */
140
+ function removeSkeletonRegistration(lines, { symbol, source, entry }) {
141
+ const entryAt = compositionCode(lines.join('\n')).split('\n').findIndex(line => line.trim() === `${entry},`);
142
+ lines.splice(entryAt, 1);
143
+ const importAt = lines.findIndex(line => line.startsWith('import { ') && line.replace(/;\s*$/, '').endsWith(` } from "${source}"`));
144
+ if (importAt < 0) return;
145
+ const symbols = lines[importAt].slice('import { '.length, lines[importAt].indexOf(' } from ')).split(',').map(value => value.trim()).filter(value => value !== symbol);
146
+ if (symbols.length) lines[importAt] = `import { ${symbols.join(', ')} } from "${source}"`;
147
+ else lines.splice(importAt, 1);
103
148
  }
104
149
 
105
150
  /** The GraphQL server capability (be/src/modules/platform/graphql), written whole by the first GraphQL feature of an app that has none. */
@@ -117,41 +162,62 @@ function ensureGraphqlPlatform(repoRoot) {
117
162
  return written;
118
163
  }
119
164
 
120
- /**
121
- * Lite has one API app; compose each generated transport into that app immediately: an api feature's GraphQL module (with the
122
- * GraphQL server on first use), a webhook's HTTP door.
123
- */
124
- function wireLiteFeature({ repoRoot, repo, noun, name }) {
125
- const app = repo.sides.be.apps.find(entry => entry.kind === 'api');
126
- if (!app) throw new ScaffoldError('HFS_ADD_NO_API_OWNER', `add ${noun} needs a declared lite api app`);
127
- const relative = `be/apps/${app.name}/src/app.module.ts`;
128
- const file = path.join(repoRoot, ...relative.split('/'));
129
- if (!fs.existsSync(file)) throw new ScaffoldError('HFS_ADD_WIRE_MISSING', `add ${noun} registers its transport in ${relative}, which does not exist`);
130
- const lines = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n').split('\n');
131
- const created = [];
165
+ /** The typed area members of one transport, including lite transactional handoff capabilities. */
166
+ function transportComposition(noun, name, edition) {
167
+ const entries = [];
132
168
  if (noun === 'api') {
133
- created.push(...ensureGraphqlPlatform(repoRoot));
134
- addModuleImport(lines, { importLine: 'import { GraphqlModule } from "@modules/platform/graphql"', entry: 'GraphqlModule.register({ isGlobal: true })', relative, noun });
169
+ entries.push({ importLine: 'import { GraphqlModule } from "@modules/platform/graphql"', entry: 'GraphqlModule.register({ isGlobal: true })' });
135
170
  const symbol = `${pascalOf(name)}GraphqlModule`;
136
- addModuleImport(lines, { importLine: `import { ${symbol} } from "@features/api/${name}"`, entry: symbol, relative, noun });
171
+ entries.push({ importLine: `import { ${symbol} } from "@features/api/${name}"`, entry: symbol });
137
172
  } else {
138
173
  const symbol = `${pascalOf(name)}HttpModule`;
139
- addModuleImport(lines, { importLine: `import { ${symbol} } from "@features/webhooks/${name}"`, entry: symbol, relative, noun });
174
+ entries.push({ importLine: `import { ${symbol} } from "@features/webhooks/${name}"`, entry: symbol });
175
+ if (edition === 'lite') {
176
+ const inbox = `${pascalOf(name)}InboxModule`;
177
+ const importLine = `import { ${inbox} } from "@modules/domain/${name}-inbox"`;
178
+ const entry = `${inbox}.register({ isGlobal: true })`;
179
+ entries.push({ importLine, entry });
180
+ for (const platform of ['InboxModule', 'EventBusModule']) {
181
+ const capability = platform === 'InboxModule' ? 'inbox' : 'event-bus';
182
+ entries.push({ importLine: `import { ${platform} } from "@modules/platform/${capability}"`, entry: `${platform}.register({ isGlobal: true })` });
183
+ }
184
+ }
185
+ }
186
+ return entries;
187
+ }
188
+
189
+ /**
190
+ * Compose each generated transport in its declared API owner through an explicitly typed area group. The root only spreads
191
+ * the group; it never acquires a second feature list. Lite's inbox capability belongs to its webhook group as well.
192
+ */
193
+ function wireFeature({ repoRoot, repo, noun, name }) {
194
+ const { app, relative, file, lines } = featureCompositionRoot({ repoRoot, repo, noun });
195
+ const created = [];
196
+ const area = noun === 'api' ? 'api' : 'webhooks';
197
+ const groups = new Map([...referencedComposeAreas(lines), area].map((group) => [group, []]));
198
+ for (const migration of skeletonComposition(lines, repo.edition)) {
199
+ const entries = groups.get(migration.area) ?? [];
200
+ entries.push({ importLine: `import { ${migration.symbol} } from "${migration.source}"`, entry: migration.entry });
201
+ groups.set(migration.area, entries);
202
+ removeSkeletonRegistration(lines, migration);
140
203
  }
141
- fs.writeFileSync(file, `${lines.join('\n').replace(/\n+$/, '')}\n`);
142
- if (noun === 'webhook') wireLiteWebhookConfig({ repoRoot, app: app.name, provider: name });
204
+ const entries = groups.get(area);
205
+ if (noun === 'api') created.push(...ensureGraphqlPlatform(repoRoot));
206
+ entries.push(...transportComposition(noun, name, repo.edition));
207
+ for (const [group, members] of groups) {
208
+ const symbol = `${group}Modules`;
209
+ const argument = group === 'domain' && repo.edition === 'lite' ? 'verifyAccessToken' : '';
210
+ addModuleImport(lines, { importLine: `import { ${symbol} } from "./compose/${group}"`, entry: `...${symbol}(${argument})`, relative, noun });
211
+ created.push(...writeComposeGroup({ repoRoot, repo, app: app.name, area: group, entries: members, noun }));
212
+ }
213
+ fs.writeFileSync(file, `${lines.join('\n').trimEnd()}\n`);
214
+ if (noun === 'webhook' && repo.edition === 'lite') wireLiteWebhookConfig({ repoRoot, app: app.name, provider: name });
143
215
  return created;
144
216
  }
145
217
 
146
- /** Adds one generated webhook provider to the lite API's typed env reader (or an existing inline bootstrap) and enables exact raw-body verification. */
147
- function wireLiteWebhookConfig({ repoRoot, app, provider }) {
148
- const envRelative = `be/apps/${app}/src/${app}.env.ts`;
149
- const relative = fs.existsSync(path.join(repoRoot, ...envRelative.split('/')))
150
- ? envRelative
151
- : `be/apps/${app}/src/main.ts`;
152
- const file = path.join(repoRoot, ...relative.split('/'));
153
- let text = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
154
- const httpSecurityImport = /^import \{ ([^}]+) \} from "@modules\/platform\/http-security"$/m;
218
+ /** Adds the provider config reader to the existing typed HTTP security import. */
219
+ function webhookConfigImport(text, relative) {
220
+ const httpSecurityImport = /^import \{ ([^}]+) \} from "@modules\/platform\/http-security";?$/m;
155
221
  const importMatch = httpSecurityImport.exec(text);
156
222
  if (!importMatch) throw new ScaffoldError('HFS_ADD_WIRE_INVALID', `add webhook could not find the HTTP security import in ${relative}`);
157
223
  const imported = importMatch[1].split(',').map(symbol => symbol.trim()).filter(Boolean);
@@ -160,6 +226,40 @@ function wireLiteWebhookConfig({ repoRoot, app, provider }) {
160
226
  imported.splice(configIndex < 0 ? imported.length : configIndex + 1, 0, 'parseWebhookProviderConfig');
161
227
  text = text.replace(httpSecurityImport, `import { ${imported.join(', ')} } from "@modules/platform/http-security"`);
162
228
  }
229
+ return text;
230
+ }
231
+
232
+ /** Declares a signing key by name and leaves custody/provisioning to the environment owner. */
233
+ function webhookRosterPatches(repoRoot, provider) {
234
+ const stacks = path.join(repoRoot, '.starcistacks');
235
+ if (!fs.existsSync(stacks)) return [];
236
+ const key = formsOf({ provider }).providerUpper + '_WEBHOOK_SECRET';
237
+ const patches = [];
238
+ for (const entry of fs.readdirSync(stacks, { withFileTypes: true })) {
239
+ if (!entry.isDirectory()) continue;
240
+ const file = path.join(stacks, entry.name, 'runtime', 'env', 'KEYS.md');
241
+ if (!fs.existsSync(file)) continue;
242
+ const before = fs.readFileSync(file, 'utf8');
243
+ if (before.includes('\x60' + key + '\x60')) continue;
244
+ const lines = before.replace(/\r\n/g, '\n').split('\n');
245
+ const lastRow = lines.reduce((last, line, index) => line.startsWith('|') ? index : last, -1);
246
+ const row = '| \x60' + key + '\x60 | API signed ' + provider + ' webhook; owner must seal this key and provision its versioned external runtime secret before deployment |';
247
+ lines.splice(lastRow + 1, 0, row);
248
+ patches.push({ file, text: lines.join('\n') });
249
+ }
250
+ return patches;
251
+ }
252
+
253
+ /** Adds one generated webhook provider to the lite API's typed env reader (or an existing inline bootstrap) and enables exact raw-body verification. */
254
+ function planLiteWebhookConfig({ repoRoot, app, provider }) {
255
+ const envRelative = `be/apps/${app}/src/${app}.env.ts`;
256
+ const relative = fs.existsSync(path.join(repoRoot, ...envRelative.split('/')))
257
+ ? envRelative
258
+ : `be/apps/${app}/src/main.ts`;
259
+ const file = path.join(repoRoot, ...relative.split('/'));
260
+ if (!fs.existsSync(file)) throw new ScaffoldError('HFS_ADD_WIRE_MISSING', `add webhook needs ${relative}`);
261
+ let text = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
262
+ text = webhookConfigImport(text, relative);
163
263
  const providerKey = /^[a-z][a-z0-9]*$/.test(provider) ? provider : JSON.stringify(provider);
164
264
  const indent = /^([ \t]*)httpSecurity:/m.exec(text)?.[1] ?? ' ';
165
265
  const providerLine = `${indent} ${providerKey}: parseWebhookProviderConfig(env, ${JSON.stringify(formsOf({ provider }).providerUpper)}),`;
@@ -168,24 +268,39 @@ function wireLiteWebhookConfig({ repoRoot, app, provider }) {
168
268
  if (empty.test(text)) {
169
269
  text = text.replace(empty, ['httpSecurity: {', `${indent} ...parseHttpSecurityConfig(env),`, `${indent} webhooks: {`, providerLine, `${indent} },`, `${indent}}`].join('\n'));
170
270
  } else {
171
- const found = /^\s*webhooks: \{$/m.exec(text);
271
+ const found = /^[ \t]*webhooks: \{$/m.exec(text);
172
272
  if (!found) throw new ScaffoldError('HFS_ADD_WIRE_INVALID', `add webhook could not find httpSecurity options in ${relative}`);
173
273
  const insertAt = found.index + found[0].length;
174
274
  text = `${text.slice(0, insertAt)}\n${providerLine}${text.slice(insertAt)}`;
175
275
  }
176
276
  }
177
277
  text = text.replace('NestFactory.create(AppModule.register(options))', 'NestFactory.create(AppModule.register(options), { rawBody: true })');
178
- fs.writeFileSync(file, text);
278
+ const planned = [{ file, text }];
179
279
  if (relative === envRelative) {
180
280
  const main = path.join(repoRoot, 'be', 'apps', app, 'src', 'main.ts');
281
+ if (!fs.existsSync(main)) throw new ScaffoldError('HFS_ADD_WIRE_MISSING', `add webhook needs be/apps/${app}/src/main.ts`);
181
282
  const entry = fs.readFileSync(main, 'utf8').replace('NestFactory.create(AppModule.register(options))', 'NestFactory.create(AppModule.register(options), { rawBody: true })');
182
- fs.writeFileSync(main, entry);
283
+ planned.push({ file: main, text: entry });
284
+ }
285
+ planned.push(...webhookRosterPatches(repoRoot, provider));
286
+ return planned;
287
+ }
288
+
289
+ /** Applies the preflighted provider configuration and exact raw-body bootstrap option. */
290
+ function wireLiteWebhookConfig(input) {
291
+ const patches = planLiteWebhookConfig(input);
292
+ const originals = new Map(patches.map(({ file }) => [file, fs.readFileSync(file)]));
293
+ try {
294
+ for (const { file, text } of patches) fs.writeFileSync(file, text);
295
+ } catch (error) {
296
+ for (const [file, text] of originals) fs.writeFileSync(file, text);
297
+ throw error;
183
298
  }
184
299
  }
185
300
 
186
301
  /**
187
302
  * Registers the generated member in the static module that lists its siblings (the cli feature root lists each group module): one import
188
- * line after the last import and one more entry of the `imports` array of its `@Module`. Nothing happens when the member is already listed.
303
+ * line after the last import and one more entry of the `imports` array of its `@Module`, written `Symbol.register({})` (every module has a module definition). Nothing happens when the member is already listed.
189
304
  */
190
305
  function wire({ beRoot, wire: target, forms, noun, name }) {
191
306
  const file = path.join(beRoot, ...target.file.split('/'));
@@ -193,13 +308,13 @@ function wire({ beRoot, wire: target, forms, noun, name }) {
193
308
  const fill = (text) => fillKnownForms(text, forms);
194
309
  const symbol = fill(target.symbol);
195
310
  let text = fs.readFileSync(file, 'utf8');
196
- if (new RegExp(`\\b${symbol}\\b`).test(text)) return;
311
+ if (new RegExp(String.raw`\b${symbol}\b`).test(text)) return;
197
312
  const array = /@Module\(\{ imports: \[([^\]]*)\] \}\)/.exec(text);
198
313
  const lines = text.split('\n');
199
314
  const lastImport = lines.map((line) => line.startsWith('import ')).lastIndexOf(true);
200
315
  if (!array || lastImport < 0) throw new ScaffoldError('HFS_ADD_WIRE_FAILED', `${target.file} has no \`@Module({ imports: [...] })\` to register ${symbol} in`);
201
316
  lines.splice(lastImport + 1, 0, `import { ${symbol} } from "${fill(target.import)}"`);
202
- text = lines.join('\n').replace(array[0], `@Module({ imports: [${[array[1].trim(), symbol].filter(Boolean).join(', ')}] })`);
317
+ text = lines.join('\n').replace(array[0], `@Module({ imports: [${[array[1].trim(), `${symbol}.register({})`].filter(Boolean).join(', ')}] })`);
203
318
  fs.writeFileSync(file, text);
204
319
  }
205
320