@starci/hfs 4.3.0 → 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 (515) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/db/migrate.mjs +361 -0
  3. package/db/post-cutover.mjs +136 -0
  4. package/emit/db-types.mjs +10 -1
  5. package/lint/run.mjs +1 -1
  6. package/package.json +4 -1
  7. package/release/gate.mjs +298 -0
  8. package/release/index.mjs +103 -0
  9. package/release/push.mjs +110 -0
  10. package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
  11. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  12. package/runtime/knowledge/hfs/rules.yaml +3401 -152
  13. package/runtime/knowledge/hfs/slots.yaml +344 -62
  14. package/runtime/knowledge/patterns/be/api.yaml +32 -5
  15. package/runtime/knowledge/patterns/be/cli.yaml +14 -4
  16. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  17. package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
  18. package/runtime/knowledge/patterns/be/projections.yaml +4 -2
  19. package/runtime/knowledge/patterns/be/queues.yaml +11 -3
  20. package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
  21. package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
  22. package/runtime/knowledge/patterns/be/saga.yaml +20 -3
  23. package/runtime/knowledge/patterns/be/webhooks.yaml +23 -4
  24. package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
  25. package/runtime/knowledge/sonar-gate.yaml +1 -1
  26. package/runtime/modules/kernel/failure-codes.yaml +3113 -43
  27. package/runtime/modules/models/runtimes.yaml +1 -1
  28. package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
  29. package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
  30. package/runtime/packages/cli/src/validate-args.mjs +213 -0
  31. package/runtime/scripts/api/git/add.mjs +5 -0
  32. package/runtime/scripts/api/git/init.mjs +7 -0
  33. package/runtime/scripts/api/git/lib.mjs +3 -0
  34. package/runtime/scripts/api/git/tag.mjs +5 -0
  35. package/runtime/scripts/api/node/lib.mjs +20 -0
  36. package/runtime/scripts/api/node/run-node.mjs +6 -0
  37. package/runtime/scripts/api/sops/lib.mjs +41 -0
  38. package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
  39. package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
  40. package/runtime/scripts/api/supabase/database-query.mjs +5 -0
  41. package/runtime/scripts/api/supabase/lib.mjs +44 -0
  42. package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
  43. package/runtime/scripts/hfs/app-wiring.mjs +201 -0
  44. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +1 -8
  45. package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
  46. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  47. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  48. package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
  49. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  50. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  51. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  52. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  53. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
  54. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  55. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  56. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  57. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  58. package/runtime/scripts/hfs/architecture/register-once.mjs +2 -2
  59. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  60. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
  61. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
  62. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  63. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/tiers.mjs +3 -3
  65. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
  66. package/runtime/scripts/hfs/check-results.mjs +28 -0
  67. package/runtime/scripts/hfs/check.mjs +46 -47
  68. package/runtime/scripts/hfs/coverage-from.mjs +70 -0
  69. package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
  70. package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
  71. package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
  72. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  73. package/runtime/scripts/hfs/doc-commands.mjs +367 -0
  74. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  75. package/runtime/scripts/hfs/example-secrets.mjs +158 -0
  76. package/runtime/scripts/hfs/manifest-shape.mjs +67 -7
  77. package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
  78. package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
  79. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  80. package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
  81. package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
  82. package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
  83. package/runtime/scripts/hfs/rules/assets.mjs +79 -0
  84. package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
  85. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
  86. package/runtime/scripts/hfs/rules/contract.mjs +66 -5
  87. package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
  88. package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
  89. package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
  90. package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
  91. package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
  92. package/runtime/scripts/hfs/rules/database-migrations.mjs +286 -7
  93. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
  94. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
  95. package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
  96. package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
  97. package/runtime/scripts/hfs/rules/database.mjs +9 -0
  98. package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
  99. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
  100. package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
  101. package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
  102. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  103. package/runtime/scripts/hfs/rules/edition.mjs +9 -8
  104. package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
  105. package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
  106. package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
  107. package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
  108. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
  109. package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
  110. package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
  111. package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
  112. package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
  113. package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
  114. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
  115. package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
  116. package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
  117. package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
  118. package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
  119. package/runtime/scripts/hfs/rules/saga.mjs +8 -16
  120. package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
  121. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
  122. package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
  123. package/runtime/scripts/hfs/rules/services.mjs +4 -4
  124. package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
  125. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
  126. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
  127. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
  128. package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
  129. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +41 -1
  130. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
  131. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
  132. package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
  133. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
  134. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
  135. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
  136. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
  137. package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
  138. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
  139. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
  140. package/runtime/scripts/hfs/sealed-members.mjs +101 -0
  141. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
  142. package/runtime/scripts/hfs/secret-edit.mjs +170 -0
  143. package/runtime/scripts/hfs/secret-init.mjs +127 -19
  144. package/runtime/scripts/hfs/secret-sops.mjs +38 -6
  145. package/runtime/scripts/hfs/secret-store.mjs +56 -54
  146. package/runtime/scripts/hfs/secret.mjs +34 -5
  147. package/runtime/scripts/hfs/slot-imports.mjs +18 -5
  148. package/runtime/scripts/hfs/slots.mjs +1 -0
  149. package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
  150. package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
  151. package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
  152. package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
  153. package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
  154. package/runtime/scripts/lib/command-tokens.mjs +126 -0
  155. package/runtime/scripts/lib/compose-values.mjs +11 -0
  156. package/runtime/scripts/lib/deployment-findings.mjs +7 -0
  157. package/runtime/scripts/lib/json-clone.mjs +61 -0
  158. package/runtime/scripts/lib/list.mjs +4 -0
  159. package/runtime/scripts/lib/runtime-example.mjs +13 -0
  160. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  161. package/runtime/scripts/lib/sql-ast.mjs +32 -0
  162. package/runtime/scripts/lib/stack-services.mjs +1 -1
  163. package/runtime/scripts/lib/swarm-stack.mjs +5 -18
  164. package/runtime/scripts/lib/trim-end.mjs +8 -0
  165. package/runtime/scripts/lib/ts-analysis.mjs +5 -2
  166. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  167. package/runtime/scripts/lib/workflow-events.mjs +8 -0
  168. package/scaffold/add-app.mjs +77 -59
  169. package/scaffold/add-cli-lite.mjs +14 -6
  170. package/scaffold/add-compose.mjs +79 -0
  171. package/scaffold/add-database-lite.mjs +14 -0
  172. package/scaffold/add-fe-overlay.mjs +122 -0
  173. package/scaffold/add-fe-page.mjs +269 -0
  174. package/scaffold/add-inbox-lite.mjs +74 -42
  175. package/scaffold/add-table.mjs +110 -87
  176. package/scaffold/add.mjs +159 -44
  177. package/scaffold/app.mjs +124 -39
  178. package/scaffold/edition-gate.mjs +3 -3
  179. package/scaffold/lite-exports.mjs +2 -1
  180. package/scaffold/service.mjs +2 -2
  181. package/src/main.mjs +229 -116
  182. package/stack/age-install.mjs +37 -0
  183. package/stack/backup.mjs +100 -0
  184. package/stack/build-args.mjs +42 -0
  185. package/stack/build.mjs +173 -0
  186. package/stack/cli.mjs +130 -0
  187. package/stack/config.mjs +57 -0
  188. package/stack/context.mjs +57 -0
  189. package/stack/converge.mjs +107 -0
  190. package/stack/deploy.mjs +301 -0
  191. package/stack/digest.mjs +16 -0
  192. package/stack/docker-transport.mjs +53 -0
  193. package/stack/edge-alias.mjs +118 -0
  194. package/stack/errors.mjs +21 -0
  195. package/stack/image-scan.mjs +152 -0
  196. package/stack/ingress-create.mjs +114 -0
  197. package/stack/ingress-publish.mjs +273 -0
  198. package/stack/ingress.mjs +706 -0
  199. package/stack/jobs.mjs +51 -0
  200. package/stack/live-restore.mjs +177 -0
  201. package/stack/prepare.mjs +266 -0
  202. package/stack/private-file.mjs +22 -0
  203. package/stack/provision.mjs +90 -0
  204. package/stack/prune.mjs +46 -0
  205. package/stack/release-store.mjs +87 -0
  206. package/stack/remote.mjs +205 -0
  207. package/stack/render.mjs +149 -0
  208. package/stack/retire.mjs +47 -0
  209. package/stack/sealed.mjs +149 -0
  210. package/stack/task.mjs +42 -0
  211. package/stack/verify.mjs +99 -0
  212. package/sync/ci-commands.mjs +13 -0
  213. package/sync/index.mjs +108 -52
  214. package/sync/stack-images.mjs +127 -0
  215. package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
  216. package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
  217. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
  218. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
  219. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
  220. package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
  221. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
  222. package/templates/app/codegen-graphql.mjs.partial +13 -2
  223. package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
  224. package/templates/app/codegen-prelude.mjs.partial +8 -0
  225. package/templates/app/gitignore +3 -0
  226. package/templates/app/gitignore-lite +3 -0
  227. package/templates/app/package-scripts/package.json +1 -0
  228. package/templates/app/package-scripts/supabase.json +7 -0
  229. package/templates/app/package-scripts-lite/package.json +1 -7
  230. package/templates/app/skeleton/.editorconfig +1 -0
  231. package/templates/app/skeleton/.gitattributes +7 -1
  232. package/templates/app/skeleton/.github/dependabot.yml +17 -0
  233. package/templates/app/skeleton/.nvmrc +1 -1
  234. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
  235. package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
  236. package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
  237. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
  238. package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
  239. package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
  240. package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
  241. package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
  242. package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  243. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  244. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  245. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
  246. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  247. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
  248. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
  249. package/templates/app/skeleton/README.md +6 -6
  250. package/templates/app/skeleton/scripts/codegen.mjs +1 -2
  251. package/templates/app/skeleton-lite/.editorconfig +16 -0
  252. package/templates/app/skeleton-lite/.gitattributes +8 -0
  253. package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
  254. package/templates/app/skeleton-lite/.nvmrc +1 -0
  255. package/templates/app/skeleton-lite/.sops.yaml +3 -2
  256. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +178 -1
  257. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
  258. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
  259. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
  260. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
  261. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
  262. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
  263. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
  264. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
  265. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
  266. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
  267. package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
  268. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
  269. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
  270. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  271. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  272. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  273. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
  274. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  275. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
  276. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
  277. package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
  278. package/templates/app/skeleton-lite/README.md +5 -3
  279. package/templates/app/skeleton-lite/supabase/config.toml +8 -1
  280. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
  281. package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
  282. package/templates/be/image/worker/Dockerfile +1 -1
  283. package/templates/be/inbox/database-client.spec.ts.tpl +17 -18
  284. package/templates/be/inbox/database-client.ts.tpl +12 -11
  285. package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
  286. package/templates/be/inbox/event-index.ts.tpl +1 -0
  287. package/templates/be/inbox/events.json.tpl +10 -0
  288. package/templates/be/inbox/migration.sql.tpl +1 -0
  289. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
  290. package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
  291. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
  292. package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
  293. package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
  294. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
  295. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
  296. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
  297. package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
  298. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
  299. package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
  300. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
  301. package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
  302. package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
  303. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
  304. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
  305. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
  306. package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
  307. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
  308. package/templates/be/inbox/platform/migration.sql.tpl +48 -0
  309. package/templates/be/inbox/service.spec.ts.tpl +42 -9
  310. package/templates/be/inbox/service.ts.tpl +23 -8
  311. package/templates/be/inbox/webhook-door.spec.ts.tpl +1 -1
  312. package/templates/be/inbox/webhook-door.ts.tpl +2 -2
  313. package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
  314. package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
  315. package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
  316. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
  317. package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
  318. package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
  319. package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
  320. package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
  321. package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
  322. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
  323. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
  324. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
  325. package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
  326. package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
  327. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
  328. package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
  329. package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
  330. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
  331. package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
  332. package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
  333. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
  334. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
  335. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
  336. package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
  337. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
  338. package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
  339. package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
  340. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
  341. package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
  342. package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
  343. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
  344. package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
  345. package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
  346. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
  347. package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
  348. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
  349. package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
  350. package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
  351. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
  352. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
  353. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
  354. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
  355. package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
  356. package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
  357. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
  358. package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
  359. package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
  360. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
  361. package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
  362. package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
  363. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
  364. package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
  365. package/templates/be/skeleton/apps/__app__/src/app.module.ts +11 -9
  366. package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
  367. package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
  368. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +2 -0
  369. package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
  370. package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
  371. package/templates/be/skeleton/apps/__app__/src/main.ts +15 -1
  372. package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
  373. package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -14
  374. package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
  375. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
  376. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
  377. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
  378. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
  379. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
  380. package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
  381. package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
  382. package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
  383. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
  384. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
  385. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
  386. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
  387. package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
  388. package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
  389. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
  390. package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
  391. package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
  392. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
  393. package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
  394. package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
  395. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
  396. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
  397. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
  398. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
  399. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
  400. package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
  401. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
  402. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
  403. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
  404. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
  405. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
  406. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
  407. package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
  408. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
  409. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
  410. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
  411. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  412. package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
  413. package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
  414. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
  415. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
  416. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
  417. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
  418. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
  419. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
  420. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
  421. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
  422. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
  423. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
  424. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
  425. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
  426. package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
  427. package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
  428. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
  429. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
  430. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
  431. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
  432. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
  433. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
  434. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
  435. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
  436. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
  437. package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
  438. package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
  439. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
  440. package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
  441. package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
  442. package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
  443. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
  444. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
  445. package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
  446. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +14 -18
  447. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
  448. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
  449. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
  450. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +2 -0
  451. package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
  452. package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
  453. package/templates/be/skeleton-lite/apps/api/src/main.ts +2 -3
  454. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
  455. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
  456. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
  457. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
  458. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
  459. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
  460. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
  461. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
  462. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
  463. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
  464. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
  465. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
  466. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
  467. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
  468. package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
  469. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
  470. package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
  471. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
  472. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  473. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
  474. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
  475. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
  476. package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
  477. package/templates/be/table/api/feature.module.ts.tpl +2 -1
  478. package/templates/be/table/api/feature.options.ts.tpl +2 -0
  479. package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
  480. package/templates/be/table/api/graphql.module.ts.tpl +3 -2
  481. package/templates/be/table/api/graphql.options.ts.tpl +2 -0
  482. package/templates/be/table/migration.sql.tpl +1 -0
  483. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
  484. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
  485. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
  486. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
  487. package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
  488. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
  489. package/templates/fe/auth/supabase-client.ts.tpl +34 -0
  490. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
  491. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
  492. package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
  493. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
  494. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
  495. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
  496. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
  497. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
  498. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
  499. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
  500. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
  501. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
  502. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
  503. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
  504. package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
  505. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
  506. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
  507. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
  508. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
  509. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
  510. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
  511. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
  512. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  513. package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
  514. package/upgrade/check.mjs +3 -1
  515. package/upgrade/index.mjs +53 -40
@@ -0,0 +1,269 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { loadSlotManifest, readRepoDeclaration } from "../runtime/scripts/hfs/slots.mjs";
4
+ import { ScaffoldError, pascalOf } from "./service.mjs";
5
+
6
+ const NAME = /^[a-z][a-z0-9-]*$/;
7
+ const RESERVED = /^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])$/i;
8
+ const jsonText = (value) => `${JSON.stringify(value, null, 2)}\n`;
9
+ const objectCatalog = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
10
+
11
+ /** Validate a CLI name before it is used in a path, symbol or catalog key. */
12
+ export function feName(value, label = "name") {
13
+ if (typeof value !== "string" || !NAME.test(value) || value.split("-").includes("") || RESERVED.test(value)) {
14
+ throw new ScaffoldError("HFS_ADD_NAME_INVALID", `${label} must be kebab-case, without a path or a reserved filename`);
15
+ }
16
+ return value;
17
+ }
18
+
19
+ /** An app-relative target whose existing ancestors are real directories, never symlinks. */
20
+ function feTarget(root, relative) {
21
+ const target = path.resolve(root, relative);
22
+ const inside = path.relative(root, target);
23
+ if (!inside || inside.startsWith("..") || path.isAbsolute(inside)) {
24
+ throw new ScaffoldError("HFS_ADD_PATH_UNSAFE", `${relative} must stay inside the app root`);
25
+ }
26
+ let at = root;
27
+ const pieces = inside.split(path.sep);
28
+ for (let index = 0; index < pieces.length; index += 1) {
29
+ at = path.join(at, pieces[index]);
30
+ const stat = fs.lstatSync(at, { throwIfNoEntry: false });
31
+ if (!stat) continue;
32
+ if (stat.isSymbolicLink() || (index < pieces.length - 1 && !stat.isDirectory())) {
33
+ throw new ScaffoldError("HFS_ADD_PATH_UNSAFE", `${relative} has a symlink or a non-directory ancestor`);
34
+ }
35
+ }
36
+ return target;
37
+ }
38
+
39
+ /** Resolve the declared Next app; a multi-app repository must select one explicitly. */
40
+ export function readFeContext({ root, app }) {
41
+ const repoRoot = path.resolve(root);
42
+ const stat = fs.lstatSync(repoRoot, { throwIfNoEntry: false });
43
+ if (!stat?.isDirectory() || stat.isSymbolicLink()) {
44
+ throw new ScaffoldError("HFS_ADD_NOT_AN_APP", "add page and overlay run at a real app root directory");
45
+ }
46
+ const declaration = feTarget(repoRoot, "hfs.json");
47
+ const packageFile = feTarget(repoRoot, "package.json");
48
+ if (!fs.existsSync(declaration) || !fs.existsSync(packageFile)) {
49
+ throw new ScaffoldError("HFS_ADD_NOT_AN_APP", "add page and overlay need hfs.json and package.json at the app root");
50
+ }
51
+ const repo = readRepoDeclaration(loadSlotManifest(), repoRoot);
52
+ const apps = repo.sides?.fe?.apps.filter((candidate) => candidate.kind === "next") ?? [];
53
+ if (!apps.length) throw new ScaffoldError("HFS_ADD_NO_FRONT_END", "this app has no declared Next front end");
54
+ if (app !== undefined) feName(app, "app");
55
+ if (app === undefined && apps.length !== 1) {
56
+ throw new ScaffoldError("HFS_ADD_OPTION_MISSING", "add page and overlay need --app when several Next apps are declared");
57
+ }
58
+ const selected = apps.find((candidate) => candidate.name === (app ?? apps[0].name));
59
+ if (!selected) throw new ScaffoldError("HFS_ADD_APP_UNKNOWN", `front-end app ${app} is not declared`);
60
+ feName(selected.name, "declared app");
61
+ return { root: repoRoot, app: selected.name, repo, source: `fe/apps/${selected.name}/src` };
62
+ }
63
+
64
+ /** Read a required owner file only after checking every path ancestor. */
65
+ export function readFeFile(context, relative) {
66
+ const target = feTarget(context.root, relative);
67
+ if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {
68
+ throw new ScaffoldError("HFS_ADD_WIRE_MISSING", `${relative} is required before this feature can be added`);
69
+ }
70
+ return fs.readFileSync(target, "utf8");
71
+ }
72
+
73
+ /** Refuse an existing owner, including a partially generated one. */
74
+ export function requireNewFeOwner(context, relative) {
75
+ if (fs.existsSync(feTarget(context.root, relative))) {
76
+ throw new ScaffoldError("HFS_ADD_EXISTS", `${relative} already exists; add never replaces an existing owner`);
77
+ }
78
+ }
79
+
80
+ function readCatalogs(context) {
81
+ const directory = `${context.source}/modules/i18n/messages`;
82
+ const target = feTarget(context.root, directory);
83
+ if (!fs.existsSync(target)) throw new ScaffoldError("HFS_ADD_WIRE_MISSING", `${directory} is required`);
84
+ if (!fs.statSync(target).isDirectory()) throw new ScaffoldError("HFS_ADD_I18N_INVALID", `${directory} must be a directory`);
85
+ const files = fs.readdirSync(target).filter((file) => file.endsWith(".json")).sort();
86
+ if (!["vi.json", "en.json"].every((file) => files.includes(file))) {
87
+ throw new ScaffoldError("HFS_ADD_I18N_MISSING", `${directory} needs both vi.json and en.json`);
88
+ }
89
+ return files.map((file) => {
90
+ const relative = `${directory}/${file}`;
91
+ const before = readFeFile(context, relative);
92
+ return { relative, before, locale: file.slice(0, -5), catalog: parseCatalog(before, relative) };
93
+ });
94
+ }
95
+
96
+ function parseCatalog(before, relative) {
97
+ let catalog;
98
+ try { catalog = JSON.parse(before); } catch {
99
+ throw new ScaffoldError("HFS_ADD_I18N_INVALID", `${relative} must contain valid JSON`);
100
+ }
101
+ if (!objectCatalog(catalog)) throw new ScaffoldError("HFS_ADD_I18N_INVALID", `${relative} must contain a catalog object`);
102
+ return catalog;
103
+ }
104
+
105
+ function catalogNamespace(catalogs, app) {
106
+ const first = catalogs[0].catalog;
107
+ const keys = Object.keys(first);
108
+ let namespace = keys.length === 1 ? keys[0] : app;
109
+ if (!Object.hasOwn(first, namespace) && Object.hasOwn(first, "app")) namespace = "app";
110
+ if (!catalogs.every(({ catalog }) => Object.hasOwn(catalog, namespace) && objectCatalog(catalog[namespace]))) {
111
+ throw new ScaffoldError("HFS_ADD_I18N_INVALID", "the app catalogs need a shared object namespace");
112
+ }
113
+ return namespace;
114
+ }
115
+
116
+ function catalogCopy(locale, defaults, messages) {
117
+ const copy = messages?.[locale] ?? defaults[locale];
118
+ const required = Object.keys(defaults.en);
119
+ if (!copy || !required.every((key) => typeof copy[key] === "string" && copy[key].trim())) {
120
+ throw new ScaffoldError("HFS_ADD_I18N_INVALID", `provide the ${required.join(", ")} messages for locale ${locale}`);
121
+ }
122
+ return Object.fromEntries(required.map((key) => [key, copy[key]]));
123
+ }
124
+
125
+ /** Plan matching localized keys in every catalog without changing existing copy. */
126
+ export function planFeCatalogs(context, { group, name, defaults, messages }) {
127
+ const catalogs = readCatalogs(context);
128
+ const namespace = catalogNamespace(catalogs, context.app);
129
+ const updates = catalogs.map(({ catalog, locale, relative, before }) => {
130
+ const owner = catalog[namespace];
131
+ if (owner[group] !== undefined && !objectCatalog(owner[group])) {
132
+ throw new ScaffoldError("HFS_ADD_I18N_INVALID", `${relative}: ${namespace}.${group} must be an object`);
133
+ }
134
+ owner[group] ??= {};
135
+ if (Object.hasOwn(owner[group], name)) {
136
+ throw new ScaffoldError("HFS_ADD_EXISTS", `${relative} already declares ${namespace}.${group}.${name}`);
137
+ }
138
+ owner[group][name] = catalogCopy(locale, defaults, messages);
139
+ return { relative, before, body: jsonText(catalog) };
140
+ });
141
+ return { namespace: `${namespace}.${group}.${name}`, updates };
142
+ }
143
+
144
+ function createParent(root, target, made) {
145
+ const missing = [];
146
+ for (let at = path.dirname(target); at !== root && !fs.existsSync(at); at = path.dirname(at)) missing.push(at);
147
+ for (const directory of missing.reverse()) {
148
+ fs.mkdirSync(directory);
149
+ made.push(directory);
150
+ }
151
+ }
152
+
153
+ function writeCreated(target, body, written) {
154
+ const descriptor = fs.openSync(target, "wx");
155
+ written.push(target);
156
+ try { fs.writeFileSync(descriptor, body); } finally { fs.closeSync(descriptor); }
157
+ }
158
+
159
+ /** Execute a fully checked plan and restore updated owners if a write fails. */
160
+ export function writeFePlan(context, created, updates) {
161
+ for (const file of created) requireNewFeOwner(context, file.relative);
162
+ for (const file of updates) {
163
+ if (readFeFile(context, file.relative) !== file.before) {
164
+ throw new ScaffoldError("HFS_ADD_WIRE_INVALID", `${file.relative} changed while planning the feature`);
165
+ }
166
+ }
167
+ const written = [];
168
+ const updated = [];
169
+ const directories = [];
170
+ try {
171
+ for (const file of created) {
172
+ const target = feTarget(context.root, file.relative);
173
+ createParent(context.root, target, directories);
174
+ writeCreated(target, file.body, written);
175
+ }
176
+ for (const file of updates) {
177
+ updated.push(file);
178
+ fs.writeFileSync(feTarget(context.root, file.relative), file.body);
179
+ }
180
+ } catch (error) {
181
+ for (const file of updated.reverse()) fs.writeFileSync(feTarget(context.root, file.relative), file.before);
182
+ for (const file of written.reverse()) fs.rmSync(file);
183
+ for (const directory of directories.reverse()) fs.rmdirSync(directory);
184
+ throw error;
185
+ }
186
+ return { created: created.map((file) => file.relative), updated: updates.map((file) => file.relative) };
187
+ }
188
+
189
+ function pageSources({ name, namespace, symbol, routeSymbol }) {
190
+ const metadata = `${name.split("-").map((word, index) => index ? pascalOf(word) : word).join("")}Metadata`;
191
+ return {
192
+ metadata,
193
+ entry: `import type { Metadata } from "next"
194
+ import { getTranslations } from "next-intl/server"
195
+ import { ${routeSymbol} } from "@/modules/routes"
196
+ import { ${symbol}Base } from "./component"
197
+
198
+ /** The localized document title and canonical destination of this page. */
199
+ export const ${metadata} = async (): Promise<Metadata> => {
200
+ const t = await getTranslations(${JSON.stringify(namespace)})
201
+ return { title: t("title"), alternates: { canonical: ${routeSymbol} } }
202
+ }
203
+
204
+ /** Resolve the requested locale's copy before drawing this server page. */
205
+ export const ${symbol} = async () => {
206
+ const t = await getTranslations(${JSON.stringify(namespace)})
207
+ return <${symbol}Base props={{ title: t("title") }} />
208
+ }
209
+ `,
210
+ component: `import { Heading } from "@starci/grammar/common"
211
+
212
+ /** The localized words displayed by this page. */
213
+ export interface ${symbol}BaseProps {
214
+ readonly props: { readonly title: string }
215
+ }
216
+
217
+ /** Draw the page using the settled copy supplied by its server owner. */
218
+ export const ${symbol}Base = (props: ${symbol}BaseProps) => (
219
+ <Heading level={1}>{props.props.title}</Heading>
220
+ )
221
+ `,
222
+ };
223
+ }
224
+
225
+ function pageRoute(symbol, metadata) {
226
+ return `import { ${symbol} } from "@/features/pages/${symbol}"
227
+
228
+ /** The locale-aware metadata of this page. */
229
+ export { ${metadata} as generateMetadata } from "@/features/pages/${symbol}"
230
+
231
+ /** Mount the page's public server owner. */
232
+ const Page = () => <${symbol} />
233
+
234
+ export default Page
235
+ `;
236
+ }
237
+
238
+ /** Add a server page, its thin localized route and matching vi/en catalog entries. */
239
+ export function addFePage({ root, app, name, route = name, messages }) {
240
+ feName(name, "page name");
241
+ if (typeof route !== "string" || !route) throw new ScaffoldError("HFS_ADD_OPTION_INVALID", "--route needs one or more kebab-case path segments");
242
+ for (const segment of route.split("/")) feName(segment, "route segment");
243
+ const context = readFeContext({ root, app });
244
+ const symbol = `${pascalOf(name)}Page`;
245
+ const owner = `${context.source}/features/pages/${symbol}`;
246
+ requireNewFeOwner(context, owner);
247
+ const routeFile = `${context.source}/app/[locale]/${route}/page.tsx`;
248
+ requireNewFeOwner(context, routeFile);
249
+ const catalogs = planFeCatalogs(context, {
250
+ group: "pages", name, messages,
251
+ defaults: { vi: { title: "Trang m\u1edbi" }, en: { title: "New page" } },
252
+ });
253
+ const routeSymbol = `${name.replaceAll("-", "_").toUpperCase()}_PAGE_ROUTE`;
254
+ const routesFile = `${context.source}/modules/routes/index.ts`;
255
+ const before = readFeFile(context, routesFile);
256
+ if (before.split(/[^A-Za-z0-9_]/).includes(routeSymbol)) {
257
+ throw new ScaffoldError("HFS_ADD_EXISTS", `${routesFile} already declares ${routeSymbol}`);
258
+ }
259
+ const sources = pageSources({ name, namespace: catalogs.namespace, symbol, routeSymbol });
260
+ const created = [
261
+ { relative: `${owner}/index.tsx`, body: sources.entry },
262
+ { relative: `${owner}/component.tsx`, body: sources.component },
263
+ { relative: routeFile, body: pageRoute(symbol, sources.metadata) },
264
+ ];
265
+ const destination = `/${route}`;
266
+ const routeBody = `${before.trimEnd()}\n\n/** The canonical destination of the ${symbol} owner. */\nexport const ${routeSymbol} = ${JSON.stringify(destination)} as const\n`;
267
+ const updates = [...catalogs.updates, { relative: routesFile, before, body: routeBody }];
268
+ return { ...writeFePlan(context, created, updates), registered: { app: context.app, page: symbol, route: destination } };
269
+ }
@@ -3,6 +3,9 @@ import path from "node:path";
3
3
  import { render, TEMPLATES_DIR } from "../sync/index.mjs";
4
4
  import { ScaffoldError, pascalOf } from "./service.mjs";
5
5
  import { filesBelow } from "./lite-exports.mjs";
6
+ import { jsonText } from "./app.mjs";
7
+ import { byCodeUnit } from "../report/order.mjs";
8
+ import { withSqlIdentifierError } from "./add-database-lite.mjs";
6
9
 
7
10
  const MIGRATION = /^(\d{14})_[a-z][a-z0-9]*(?:-[a-z0-9]+)*\.sql$/;
8
11
  const PLACEHOLDER = /@@([A-Za-z][A-Za-z0-9]*)@@/g;
@@ -50,7 +53,7 @@ function validateProcessingOwner({ root, service, serviceModule, provider }) {
50
53
  );
51
54
  const owner = path.join(root, "be", "src", "modules", "domain", ...capability.split("/"));
52
55
  const candidates = filesBelow(owner).filter((file) =>
53
- file.endsWith(".service.ts") && new RegExp(`\\bexport\\s+class\\s+${service}\\b`).test(fs.readFileSync(file, "utf8")),
56
+ file.endsWith(".service.ts") && new RegExp(String.raw`\bexport\s+class\s+${service}\b`).test(fs.readFileSync(file, "utf8")),
54
57
  );
55
58
  if (candidates.length !== 1)
56
59
  throw new ScaffoldError(
@@ -70,8 +73,10 @@ const databasePatches = (root) => {
70
73
  );
71
74
  let index = fs.readFileSync(indexFile, "utf8").replace(/\r\n/g, "\n");
72
75
  if (!index.includes('export { recordWebhookDelivery } from "./webhook-inbox.client"'))
73
- index = `${index.replace(/\n+$/, "")}\nexport { recordWebhookDelivery } from "./webhook-inbox.client"\n`;
74
- let error = fs.readFileSync(errorFile, "utf8").replace(/\r\n/g, "\n");
76
+ index = `${index.trimEnd()}\nexport { recordWebhookDelivery } from "./webhook-inbox.client"\n`;
77
+ if (!index.split("\n").some((line) => line.startsWith("export { ") && line.includes("DatabaseErrorCode") && line.endsWith(' } from "./errors/database.error"')))
78
+ index = `${index.trimEnd()}\nexport { DatabaseError, DatabaseErrorCode } from "./errors/database.error"\n`;
79
+ let error = withSqlIdentifierError(fs.readFileSync(errorFile, "utf8").replace(/\r\n/g, "\n"));
75
80
  const enumLine =
76
81
  ' WebhookDeliveryIdRequired = "DATABASE_WEBHOOK_DELIVERY_ID_REQUIRED",';
77
82
  if (!error.includes(enumLine)) {
@@ -97,44 +102,58 @@ const databasePatches = (root) => {
97
102
  ];
98
103
  };
99
104
 
100
- const wiredAppModule = ({ root, repo, provider, Provider }) => {
101
- const app = repo.sides.be.apps.find((entry) => entry.kind === "api");
102
- if (!app)
103
- throw new ScaffoldError(
104
- "HFS_ADD_NO_API_OWNER",
105
- "add webhook needs a declared lite api app",
106
- );
107
- const file = path.join(root, "be", "apps", app.name, "src", "app.module.ts");
108
- const relative = `be/apps/${app.name}/src/app.module.ts`;
109
- if (!fs.existsSync(file))
110
- throw new ScaffoldError(
111
- "HFS_ADD_WIRE_MISSING",
112
- `add webhook registers its inbox in ${relative}, which does not exist`,
113
- );
114
- const symbol = `${Provider}InboxModule`;
115
- const importLine = `import { ${symbol} } from "@modules/domain/${provider}-inbox"`;
116
- const lines = fs.readFileSync(file, "utf8").replace(/\r\n/g, "\n").split("\n");
117
- const lastImport = lines.reduce(
118
- (last, line, index) => (line.startsWith("import ") ? index : last),
119
- -1,
120
- );
121
- const imports = lines.findIndex((line) => /^\s*imports: \[$/.test(line));
122
- if (lastImport < 0 || imports < 0)
123
- throw new ScaffoldError(
124
- "HFS_ADD_WIRE_INVALID",
125
- `add webhook could not find the imports array in ${relative}`,
126
- );
127
- if (!lines.includes(importLine)) {
128
- lines.splice(lastImport + 1, 0, importLine);
129
- const shiftedImports = imports > lastImport ? imports + 1 : imports;
130
- const indent = /^(\s*)/.exec(lines[shiftedImports])?.[1] ?? "";
131
- lines.splice(
132
- shiftedImports + 1,
133
- 0,
134
- `${indent} ${symbol}.register({ isGlobal: true }),`,
135
- );
105
+ /** The first webhook installs only the lite inbox claim and transactional event publisher, shared by all providers. */
106
+ const handoffPlatformFiles = (root, values) => {
107
+ const planned = [];
108
+ for (const capability of ["inbox", "event-bus"]) {
109
+ const base = `be/src/modules/platform/${capability}`;
110
+ if (fs.existsSync(path.join(root, base, "index.ts"))) continue;
111
+ const folder = path.join(TEMPLATES_DIR, "be", "inbox", "platform", capability);
112
+ for (const file of filesBelow(folder)) {
113
+ const relative = path.relative(folder, file).replaceAll("\\", "/");
114
+ planned.push({
115
+ relative: `${base}/${relative.replace(/\.tpl$/, "")}`,
116
+ text: template(`platform/${capability}/${relative}`, values),
117
+ });
118
+ }
136
119
  }
137
- return { file, text: `${lines.join("\n").replace(/\n+$/, "")}\n` };
120
+ return planned;
121
+ };
122
+
123
+ /** A provider may already publish other events; retain its barrel and contract when adding the receipt event. */
124
+ const eventFiles = (root, provider, values) => {
125
+ const entries = [
126
+ { relative: `be/src/modules/events/${provider}/index.ts`, text: template("event-index.ts.tpl", values) },
127
+ { relative: `be/contracts/${provider}/events.json`, text: template("events.json.tpl", values) },
128
+ ];
129
+ const planned = [];
130
+ const patches = [];
131
+ for (const entry of entries) {
132
+ const file = path.join(root, ...entry.relative.split("/"));
133
+ if (!fs.existsSync(file)) {
134
+ planned.push(entry);
135
+ continue;
136
+ }
137
+ const previous = fs.readFileSync(file, "utf8").replace(/\r\n/g, "\n");
138
+ if (entry.relative.endsWith("index.ts")) {
139
+ patches.push({ file, text: previous.includes(entry.text.trim()) ? previous : `${previous.trimEnd()}\n${entry.text}` });
140
+ } else {
141
+ const contract = JSON.parse(previous);
142
+ if (contract.schema !== "starci/event-contract@1" || contract.service !== provider || !contract.events || Array.isArray(contract.events) || typeof contract.events !== "object")
143
+ throw new ScaffoldError("HFS_ADD_EVENT_CONTRACT", `${entry.relative} is not the event contract of ${provider}`);
144
+ const additions = JSON.parse(entry.text).events;
145
+ contract.events = { ...contract.events, ...additions };
146
+ patches.push({ file, text: jsonText(contract) });
147
+ }
148
+ }
149
+ return { planned, patches };
150
+ };
151
+
152
+ const eventPatternPatch = (root) => {
153
+ const file = path.join(root, "hfs.json");
154
+ const declaration = JSON.parse(fs.readFileSync(file, "utf8"));
155
+ declaration.sides.be.patterns = [...new Set([...(declaration.sides.be.patterns ?? []), "event-bus"])].sort(byCodeUnit);
156
+ return { file, text: jsonText(declaration) };
138
157
  };
139
158
 
140
159
  /** Writes the lite provider inbox. Processing belongs to a separately added reactor or job, after the record commits. */
@@ -152,7 +171,7 @@ export function addLiteWebhookInbox({
152
171
  const table = `${snakeOf(provider)}_inbox`;
153
172
  const migrationsDir = path.join(root, "supabase", "migrations");
154
173
  const migrations = fs.existsSync(migrationsDir)
155
- ? fs.readdirSync(migrationsDir).filter((file) => MIGRATION.test(file)).sort()
174
+ ? fs.readdirSync(migrationsDir).filter((file) => MIGRATION.test(file)).sort(byCodeUnit)
156
175
  : [];
157
176
  const suffix = `${provider}-inbox.sql`;
158
177
  if (migrations.some((file) => file.endsWith(`_${suffix}`)))
@@ -175,6 +194,7 @@ export function addLiteWebhookInbox({
175
194
  table,
176
195
  };
177
196
  const base = `be/src/modules/domain/${provider}-inbox`;
197
+ const events = eventFiles(root, provider, renderValues);
178
198
  const planned = [
179
199
  {
180
200
  relative: `supabase/migrations/${stamp}_${suffix}`,
@@ -205,7 +225,18 @@ export function addLiteWebhookInbox({
205
225
  relative: `${base}/persistence/${provider}-inbox.sql.ts`,
206
226
  text: template("sql.ts.tpl", renderValues),
207
227
  },
228
+ {
229
+ relative: `be/src/modules/events/${provider}/${provider}-delivery-recorded.event.ts`,
230
+ text: template("delivery-recorded.event.ts.tpl", renderValues),
231
+ },
232
+ ...events.planned,
233
+ ...handoffPlatformFiles(root, renderValues),
208
234
  ];
235
+ const hasHandoffStorage = migrations.some((file) =>
236
+ /create\s+table\s+public\.event_bus\b/i.test(fs.readFileSync(path.join(migrationsDir, file), "utf8")),
237
+ );
238
+ if (!hasHandoffStorage)
239
+ planned[0].text = `${template("platform/migration.sql.tpl", renderValues)}\n${planned[0].text}`;
209
240
  const databaseSql = "be/src/modules/platform/database/database.sql.ts";
210
241
  if (!fs.existsSync(path.join(root, ...databaseSql.split("/")))) {
211
242
  planned.push({
@@ -249,7 +280,8 @@ export function addLiteWebhookInbox({
249
280
  );
250
281
  const patches = [
251
282
  ...databasePatches(root),
252
- wiredAppModule({ root, repo, provider, Provider }),
283
+ ...events.patches,
284
+ eventPatternPatch(root),
253
285
  ];
254
286
  for (const entry of planned) {
255
287
  const target = path.join(root, ...entry.relative.split("/"));