@starci/hfs 4.2.17 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (564) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +7 -0
  3. package/db/migrate.mjs +361 -0
  4. package/db/post-cutover.mjs +136 -0
  5. package/emit/db-types.mjs +10 -1
  6. package/lint/run.mjs +1 -1
  7. package/package.json +4 -1
  8. package/release/gate.mjs +298 -0
  9. package/release/index.mjs +103 -0
  10. package/release/push.mjs +110 -0
  11. package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
  12. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  13. package/runtime/knowledge/hfs/rules.yaml +3437 -160
  14. package/runtime/knowledge/hfs/slots.yaml +430 -92
  15. package/runtime/knowledge/patterns/be/api.yaml +92 -2
  16. package/runtime/knowledge/patterns/be/cli.yaml +37 -9
  17. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  18. package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
  19. package/runtime/knowledge/patterns/be/projections.yaml +4 -2
  20. package/runtime/knowledge/patterns/be/queues.yaml +11 -3
  21. package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
  22. package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
  23. package/runtime/knowledge/patterns/be/saga.yaml +20 -3
  24. package/runtime/knowledge/patterns/be/webhooks.yaml +210 -36
  25. package/runtime/knowledge/patterns/fe/folder.yaml +4 -0
  26. package/runtime/knowledge/sonar-gate.yaml +1 -1
  27. package/runtime/modules/kernel/failure-codes.yaml +3128 -47
  28. package/runtime/modules/models/runtimes.yaml +1 -1
  29. package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
  30. package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
  31. package/runtime/packages/cli/src/validate-args.mjs +213 -0
  32. package/runtime/scripts/api/git/add.mjs +5 -0
  33. package/runtime/scripts/api/git/cat-file.mjs +5 -0
  34. package/runtime/scripts/api/git/init.mjs +7 -0
  35. package/runtime/scripts/api/git/lib.mjs +3 -0
  36. package/runtime/scripts/api/git/tag.mjs +5 -0
  37. package/runtime/scripts/api/node/lib.mjs +20 -0
  38. package/runtime/scripts/api/node/run-node.mjs +6 -0
  39. package/runtime/scripts/api/sops/lib.mjs +41 -0
  40. package/runtime/scripts/api/sops/resolve-sops.mjs +7 -0
  41. package/runtime/scripts/api/sops/seal.mjs +1 -1
  42. package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
  43. package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
  44. package/runtime/scripts/api/supabase/database-query.mjs +5 -0
  45. package/runtime/scripts/api/supabase/lib.mjs +44 -0
  46. package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
  47. package/runtime/scripts/hfs/app-wiring.mjs +201 -0
  48. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +103 -0
  49. package/runtime/scripts/hfs/architecture/backend.mjs +22 -10
  50. package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
  51. package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
  52. package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
  53. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  54. package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
  55. package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
  56. package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
  57. package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
  58. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  59. package/runtime/scripts/hfs/architecture/dead-exports.mjs +23 -50
  60. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  61. package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
  62. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
  63. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  64. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  65. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  66. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
  67. package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
  68. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +49 -25
  69. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  70. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  71. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  72. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  73. package/runtime/scripts/hfs/architecture/register-once.mjs +15 -7
  74. package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
  75. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  76. package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
  77. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +214 -17
  78. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
  79. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  80. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  81. package/runtime/scripts/hfs/architecture/tiers.mjs +11 -4
  82. package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
  83. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +9 -13
  84. package/runtime/scripts/hfs/check-results.mjs +28 -0
  85. package/runtime/scripts/hfs/check.mjs +63 -72
  86. package/runtime/scripts/hfs/coverage-from.mjs +70 -0
  87. package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
  88. package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
  89. package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
  90. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  91. package/runtime/scripts/hfs/doc-commands.mjs +367 -0
  92. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  93. package/runtime/scripts/hfs/example-secrets.mjs +158 -0
  94. package/runtime/scripts/hfs/manifest-shape.mjs +91 -6
  95. package/runtime/scripts/hfs/path-findings.mjs +32 -2
  96. package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
  97. package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
  98. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  99. package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
  100. package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
  101. package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
  102. package/runtime/scripts/hfs/rules/assets.mjs +79 -0
  103. package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
  104. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
  105. package/runtime/scripts/hfs/rules/contract.mjs +66 -5
  106. package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
  107. package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
  108. package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
  109. package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
  110. package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
  111. package/runtime/scripts/hfs/rules/database-migrations.mjs +317 -37
  112. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
  113. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
  114. package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
  115. package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
  116. package/runtime/scripts/hfs/rules/database.mjs +11 -2
  117. package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
  118. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
  119. package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
  120. package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
  121. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  122. package/runtime/scripts/hfs/rules/edition.mjs +11 -10
  123. package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
  124. package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
  125. package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
  126. package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
  127. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
  128. package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
  129. package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
  130. package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
  131. package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
  132. package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
  133. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
  134. package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
  135. package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
  136. package/runtime/scripts/hfs/rules/proof-commands.mjs +2 -0
  137. package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
  138. package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
  139. package/runtime/scripts/hfs/rules/saga.mjs +8 -16
  140. package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
  141. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
  142. package/runtime/scripts/hfs/rules/secrets.mjs +26 -0
  143. package/runtime/scripts/hfs/rules/services.mjs +4 -4
  144. package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
  145. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
  146. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
  147. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
  148. package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
  149. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +168 -0
  150. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
  151. package/runtime/scripts/hfs/rules/stacks.mjs +13 -8
  152. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
  153. package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
  154. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
  155. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
  156. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
  157. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
  158. package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
  159. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
  160. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
  161. package/runtime/scripts/hfs/sealed-members.mjs +101 -0
  162. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
  163. package/runtime/scripts/hfs/secret-document.mjs +97 -0
  164. package/runtime/scripts/hfs/secret-edit.mjs +170 -0
  165. package/runtime/scripts/hfs/secret-errors.mjs +20 -0
  166. package/runtime/scripts/hfs/secret-files.mjs +124 -0
  167. package/runtime/scripts/hfs/secret-init.mjs +300 -0
  168. package/runtime/scripts/hfs/secret-output.mjs +100 -0
  169. package/runtime/scripts/hfs/secret-sops.mjs +130 -0
  170. package/runtime/scripts/hfs/secret-store.mjs +163 -0
  171. package/runtime/scripts/hfs/secret.mjs +162 -193
  172. package/runtime/scripts/hfs/slot-imports.mjs +18 -3
  173. package/runtime/scripts/hfs/slot-required.mjs +13 -0
  174. package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
  175. package/runtime/scripts/hfs/slots.mjs +1 -0
  176. package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
  177. package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
  178. package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
  179. package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
  180. package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
  181. package/runtime/scripts/hfs/tree-findings.mjs +22 -0
  182. package/runtime/scripts/lib/command-tokens.mjs +126 -0
  183. package/runtime/scripts/lib/compose-values.mjs +11 -0
  184. package/runtime/scripts/lib/deployment-findings.mjs +7 -0
  185. package/runtime/scripts/lib/json-clone.mjs +61 -0
  186. package/runtime/scripts/lib/list.mjs +4 -0
  187. package/runtime/scripts/lib/roots.mjs +59 -0
  188. package/runtime/scripts/lib/runtime-example.mjs +13 -0
  189. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  190. package/runtime/scripts/lib/sql-ast.mjs +32 -0
  191. package/runtime/scripts/lib/stack-services.mjs +1 -1
  192. package/runtime/scripts/lib/swarm-stack.mjs +126 -0
  193. package/runtime/scripts/lib/trim-end.mjs +8 -0
  194. package/runtime/scripts/lib/ts-analysis.mjs +23 -0
  195. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  196. package/runtime/scripts/lib/workflow-events.mjs +8 -0
  197. package/scaffold/add-app.mjs +77 -59
  198. package/scaffold/add-cli-lite.mjs +24 -17
  199. package/scaffold/add-compose.mjs +79 -0
  200. package/scaffold/add-database-lite.mjs +14 -0
  201. package/scaffold/add-fe-overlay.mjs +122 -0
  202. package/scaffold/add-fe-page.mjs +269 -0
  203. package/scaffold/add-inbox-lite.mjs +87 -124
  204. package/scaffold/add-plan.mjs +183 -0
  205. package/scaffold/add-table.mjs +132 -70
  206. package/scaffold/add.mjs +186 -204
  207. package/scaffold/app.mjs +128 -43
  208. package/scaffold/edition-gate.mjs +3 -3
  209. package/scaffold/image.mjs +2 -2
  210. package/scaffold/lite-exports.mjs +2 -1
  211. package/scaffold/service.mjs +2 -2
  212. package/src/main.mjs +238 -119
  213. package/stack/age-install.mjs +37 -0
  214. package/stack/backup.mjs +100 -0
  215. package/stack/build-args.mjs +42 -0
  216. package/stack/build.mjs +173 -0
  217. package/stack/cli.mjs +130 -0
  218. package/stack/config.mjs +57 -0
  219. package/stack/context.mjs +57 -0
  220. package/stack/converge.mjs +107 -0
  221. package/stack/deploy.mjs +301 -0
  222. package/stack/digest.mjs +16 -0
  223. package/stack/docker-transport.mjs +53 -0
  224. package/stack/edge-alias.mjs +118 -0
  225. package/stack/errors.mjs +21 -0
  226. package/stack/image-scan.mjs +152 -0
  227. package/stack/ingress-create.mjs +114 -0
  228. package/stack/ingress-publish.mjs +273 -0
  229. package/stack/ingress.mjs +706 -0
  230. package/stack/jobs.mjs +51 -0
  231. package/stack/live-restore.mjs +177 -0
  232. package/stack/prepare.mjs +266 -0
  233. package/stack/private-file.mjs +22 -0
  234. package/stack/provision.mjs +90 -0
  235. package/stack/prune.mjs +46 -0
  236. package/stack/release-store.mjs +87 -0
  237. package/stack/remote.mjs +205 -0
  238. package/stack/render.mjs +149 -0
  239. package/stack/retire.mjs +47 -0
  240. package/stack/sealed.mjs +149 -0
  241. package/stack/task.mjs +42 -0
  242. package/stack/verify.mjs +99 -0
  243. package/sync/ci-commands.mjs +13 -0
  244. package/sync/index.mjs +127 -64
  245. package/sync/stack-images.mjs +127 -0
  246. package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
  247. package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
  248. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
  249. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
  250. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
  251. package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
  252. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
  253. package/templates/app/codegen-graphql.mjs.partial +13 -2
  254. package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
  255. package/templates/app/codegen-prelude.mjs.partial +8 -0
  256. package/templates/app/gitignore +4 -0
  257. package/templates/app/gitignore-lite +4 -0
  258. package/templates/app/hooks/husky/pre-commit +7 -12
  259. package/templates/app/package-scripts/package.json +1 -0
  260. package/templates/app/package-scripts/supabase.json +7 -0
  261. package/templates/app/package-scripts-lite/package.json +1 -7
  262. package/templates/app/skeleton/.editorconfig +1 -0
  263. package/templates/app/skeleton/.gitattributes +7 -1
  264. package/templates/app/skeleton/.github/dependabot.yml +17 -0
  265. package/templates/app/skeleton/.nvmrc +1 -1
  266. package/templates/app/skeleton/.sops.yaml +1 -1
  267. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
  268. package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
  269. package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
  270. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
  271. package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
  272. package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
  273. package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
  274. package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
  275. package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  276. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  277. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  278. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
  279. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  280. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
  281. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
  282. package/templates/app/skeleton/README.md +6 -6
  283. package/templates/app/skeleton/scripts/codegen.mjs +1 -2
  284. package/templates/app/skeleton-lite/.editorconfig +16 -0
  285. package/templates/app/skeleton-lite/.gitattributes +8 -0
  286. package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
  287. package/templates/app/skeleton-lite/.nvmrc +1 -0
  288. package/templates/app/skeleton-lite/.sops.yaml +3 -2
  289. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +193 -0
  290. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
  291. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
  292. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
  293. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
  294. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
  295. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
  296. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
  297. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
  298. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
  299. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
  300. package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
  301. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
  302. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
  303. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  304. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  305. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  306. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
  307. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  308. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
  309. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
  310. package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
  311. package/templates/app/skeleton-lite/.starciwork/workspace.yaml +9 -0
  312. package/templates/app/skeleton-lite/README.md +8 -4
  313. package/templates/app/skeleton-lite/supabase/config.toml +8 -1
  314. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
  315. package/templates/app/starciwork.gitignore-lite +6 -0
  316. package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
  317. package/templates/be/image/worker/Dockerfile +1 -1
  318. package/templates/be/inbox/database-client.spec.ts.tpl +22 -28
  319. package/templates/be/inbox/database-client.ts.tpl +14 -28
  320. package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
  321. package/templates/be/inbox/event-index.ts.tpl +1 -0
  322. package/templates/be/inbox/events.json.tpl +10 -0
  323. package/templates/be/inbox/migration.sql.tpl +1 -0
  324. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
  325. package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
  326. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
  327. package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
  328. package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
  329. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
  330. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
  331. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
  332. package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
  333. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
  334. package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
  335. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
  336. package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
  337. package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
  338. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
  339. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
  340. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
  341. package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
  342. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
  343. package/templates/be/inbox/platform/migration.sql.tpl +48 -0
  344. package/templates/be/inbox/service.spec.ts.tpl +49 -20
  345. package/templates/be/inbox/service.ts.tpl +27 -16
  346. package/templates/be/inbox/sql.ts.tpl +2 -2
  347. package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -3
  348. package/templates/be/inbox/webhook-door.ts.tpl +4 -3
  349. package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
  350. package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
  351. package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
  352. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
  353. package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
  354. package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
  355. package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
  356. package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
  357. package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
  358. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
  359. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
  360. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
  361. package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
  362. package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
  363. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
  364. package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
  365. package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
  366. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
  367. package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
  368. package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
  369. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
  370. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
  371. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
  372. package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
  373. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
  374. package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
  375. package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
  376. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
  377. package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
  378. package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
  379. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
  380. package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
  381. package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
  382. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
  383. package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
  384. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
  385. package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
  386. package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
  387. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
  388. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
  389. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
  390. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
  391. package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
  392. package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
  393. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
  394. package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
  395. package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
  396. package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +3 -3
  397. package/templates/be/patterns/webhooks/webhook.door.ts.tpl +3 -3
  398. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
  399. package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
  400. package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
  401. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
  402. package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
  403. package/templates/be/skeleton/apps/__app__/src/__app__.env.ts +21 -0
  404. package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +1 -1
  405. package/templates/be/skeleton/apps/__app__/src/app.module.ts +15 -45
  406. package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
  407. package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
  408. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +44 -0
  409. package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
  410. package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
  411. package/templates/be/skeleton/apps/__app__/src/main.ts +18 -10
  412. package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
  413. package/templates/be/skeleton/apps/cli/src/cli.env.ts +13 -0
  414. package/templates/be/skeleton/apps/cli/src/cli.options.ts +7 -17
  415. package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
  416. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
  417. package/templates/be/skeleton/apps/cli/src/main.ts +3 -2
  418. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
  419. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
  420. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
  421. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
  422. package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
  423. package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
  424. package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
  425. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
  426. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
  427. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
  428. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
  429. package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
  430. package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
  431. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
  432. package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
  433. package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
  434. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
  435. package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
  436. package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
  437. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
  438. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
  439. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
  440. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
  441. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
  442. package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
  443. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
  444. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
  445. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
  446. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
  447. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
  448. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
  449. package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
  450. package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +6 -6
  451. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
  452. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
  453. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
  454. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  455. package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
  456. package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
  457. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
  458. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
  459. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
  460. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
  461. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
  462. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
  463. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
  464. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
  465. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
  466. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
  467. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
  468. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
  469. package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
  470. package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
  471. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
  472. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
  473. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
  474. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
  475. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
  476. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
  477. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
  478. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
  479. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
  480. package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
  481. package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
  482. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
  483. package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
  484. package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
  485. package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
  486. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
  487. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
  488. package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
  489. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +25 -0
  490. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +1 -1
  491. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +18 -58
  492. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
  493. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
  494. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
  495. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +47 -0
  496. package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
  497. package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
  498. package/templates/be/skeleton-lite/apps/api/src/main.ts +5 -13
  499. package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +17 -0
  500. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
  501. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
  502. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
  503. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
  504. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
  505. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
  506. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
  507. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
  508. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
  509. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
  510. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
  511. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
  512. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
  513. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
  514. package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
  515. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
  516. package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
  517. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
  518. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  519. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
  520. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
  521. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
  522. package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
  523. package/templates/be/table/api/feature.module.ts.tpl +2 -1
  524. package/templates/be/table/api/feature.options.ts.tpl +2 -0
  525. package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
  526. package/templates/be/table/api/graphql.module.ts.tpl +3 -2
  527. package/templates/be/table/api/graphql.options.ts.tpl +2 -0
  528. package/templates/be/table/migration.sql.tpl +1 -0
  529. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
  530. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
  531. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
  532. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
  533. package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
  534. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
  535. package/templates/fe/auth/supabase-client.ts.tpl +34 -0
  536. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
  537. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
  538. package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
  539. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
  540. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
  541. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
  542. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
  543. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
  544. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
  545. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
  546. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
  547. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
  548. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
  549. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
  550. package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
  551. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
  552. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
  553. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
  554. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
  555. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
  556. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
  557. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
  558. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  559. package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
  560. package/upgrade/check.mjs +3 -1
  561. package/upgrade/file-state.mjs +68 -0
  562. package/upgrade/index.mjs +58 -94
  563. package/runtime/scripts/api/git/log.mjs +0 -5
  564. package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +0 -55
@@ -1,15 +1,22 @@
1
1
  // database-migrations.mjs - L02 migration name, UTC ordering and base-branch immutability checks. Git reads go
2
2
  // through the runtime API call files, and specs may inject the same small runner contract used by database.mjs.
3
3
  import { lsTree } from '../../api/git/ls-tree.mjs';
4
- import { log } from '../../api/git/log.mjs';
5
4
  import { mergeBase } from '../../api/git/merge-base.mjs';
6
5
  import { show } from '../../api/git/show.mjs';
7
6
  import { withoutGitLocalEnv } from '../../lib/git.mjs';
8
7
  import { eachInOrder, findInOrder } from '../../lib/in-order.mjs';
9
8
  import { sameText } from '../../lib/same-text.mjs';
10
9
  import { found, readText } from './read.mjs';
11
- import { DB_MIGRATION_SHAPE, isActiveMigration, MIGRATIONS_ARCHIVE_DIR, MIGRATIONS_DIR } from './database-constants.mjs';
12
- const MIGRATION_NAME = /^(\d{14})_([a-z0-9]+(?:-[a-z0-9]+)*)\.sql$/;
10
+ import { DB_MIGRATION_DESTRUCTIVE, DB_MIGRATION_SHAPE, isActiveMigration, MIGRATIONS_ARCHIVE_DIR, MIGRATIONS_DIR } from './database-constants.mjs';
11
+ import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { loadTypescript } from '../../lib/package-at.mjs';
14
+
15
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
16
+ const MIGRATION_NAME = /^(\d{14})_([a-z0-9]+(?:-[a-z0-9]+)*|contract_[a-z0-9]+(?:[-_][a-z0-9]+)*|[a-z0-9]+(?:-[a-z0-9]+)*_contract_[a-z0-9]+(?:[-_][a-z0-9]+)*)\.sql$/;
17
+ export const isMigrationName = (name) => MIGRATION_NAME.test(name);
18
+ const RESTAMP_HEADER = /^-- starci:restamp (\d{14}) (\d{14})(?:\r?\n|$)/;
19
+ const withoutRestampHeader = (text) => text?.replace(RESTAMP_HEADER, '') ?? null;
13
20
 
14
21
  /** `<ts14>` -> the UTC instant it names, or null when the digits are no real time (month 13, day 32, ...). */
15
22
  export function migrationStamp(text) {
@@ -27,7 +34,6 @@ export function defaultGit(args, { cwd }) {
27
34
  if (verb === 'merge-base') { const sha = mergeBase(cwd, rest[0], rest[1]); return { ok: sha !== null, stdout: sha ?? '' }; }
28
35
  let call = show;
29
36
  if (verb === 'ls-tree') call = lsTree;
30
- else if (verb === 'log') call = log;
31
37
  const result = call(rest, { cwd, env, maxBuffer: 64 * 1024 * 1024 });
32
38
  return { ok: !result.error && result.status === 0, stdout: result.stdout };
33
39
  }
@@ -45,11 +51,11 @@ async function baseShaOf(git, repoRoot, base) {
45
51
  return found;
46
52
  }
47
53
 
48
- async function baseMigrationNames(git, repoRoot, sha) {
54
+ async function migrationFilesAt(git, repoRoot, sha) {
49
55
  const result = await runGit(git, repoRoot, ['ls-tree', '-r', '--name-only', '-z', sha, '--', MIGRATIONS_DIR]);
50
56
  if (!result.ok) return [];
51
57
  const prefix = `${MIGRATIONS_DIR}/`;
52
- return result.stdout.split('\0').filter(Boolean).filter(isActiveMigration).map((entry) => entry.slice(prefix.length));
58
+ return result.stdout.split('\0').filter((entry) => entry.startsWith(prefix));
53
59
  }
54
60
 
55
61
  async function baseFileText(git, repoRoot, sha, file) {
@@ -58,46 +64,67 @@ async function baseFileText(git, repoRoot, sha, file) {
58
64
  return result.ok ? result.stdout : null;
59
65
  }
60
66
 
61
- async function migrationTimeLimit(git, repoRoot, file, now) {
62
- const result = await runGit(git, repoRoot, ['log', '-1', '--format=%ct', '--', file]);
63
- const seconds = Number(result.stdout.trim());
64
- if (result.ok && Number.isFinite(seconds) && seconds > 0) return { at: seconds * 1000, tracked: true };
65
- return { at: now(), tracked: false };
66
- }
67
-
68
- async function localMigrationFindings(local, repoRoot, git, now) {
67
+ function localMigrationFindings(local) {
69
68
  const findings = [];
70
69
  const stamps = new Map();
71
- await eachInOrder(local, async (entry) => {
70
+ for (const entry of local) {
72
71
  if (!entry.match) {
73
72
  findings.push(found(DB_MIGRATION_SHAPE, entry.file, `${entry.file} is not named <ts14>_<kebab>.sql; a migration is created by \`npm run db:new\` (supabase migration new), never renamed by hand`, { expected: '<ts14>_<kebab>.sql' }));
74
- return;
73
+ continue;
75
74
  }
76
75
  const stamp = migrationStamp(entry.match[1]);
77
76
  if (stamp === null) {
78
- findings.push(found(DB_MIGRATION_SHAPE, entry.file, `${entry.file} carries ${entry.match[1]}, which is no valid UTC time; the stamp is the real creation time`, { stamp: entry.match[1] }));
79
- return;
80
- }
81
- const limit = await migrationTimeLimit(git, repoRoot, entry.file, now);
82
- if (stamp > limit.at) {
83
- const boundary = limit.tracked ? `the file's commit time ${new Date(limit.at).toISOString()}` : `the current time ${new Date(limit.at).toISOString()}`;
84
- findings.push(found(DB_MIGRATION_SHAPE, entry.file, `${entry.file} is stamped ${entry.match[1]}, in the future relative to ${boundary}; a migration stamp is its real creation time, never a hand-picked date`, { stamp: entry.match[1], tracked: limit.tracked }));
77
+ findings.push(found(DB_MIGRATION_SHAPE, entry.file, `${entry.file} carries ${entry.match[1]}, which is no valid UTC time; a migration stamp must name a valid UTC instant`, { stamp: entry.match[1] }));
78
+ continue;
85
79
  }
86
80
  if (stamps.has(stamp)) findings.push(found(DB_MIGRATION_SHAPE, entry.file, `${entry.file} shares stamp ${entry.match[1]} with ${stamps.get(stamp)}; stamps are strictly increasing`, { stamp: entry.match[1], other: stamps.get(stamp) }));
87
81
  stamps.set(stamp, entry.file);
88
- });
82
+ }
89
83
  return findings;
90
84
  }
91
85
 
92
- async function baseMigrationEntryFindings(entry, repoRoot, git, baseSha, onBase, baseMax) {
86
+ /** HEAD owns the append boundary. Archived names retire the old chain, so a replacement baseline starts afresh. */
87
+ function migrationHistory(files, retired, localNames) {
88
+ const prefix = `${MIGRATIONS_DIR}/`;
89
+ const onHead = new Set(files.filter(isActiveMigration).map((file) => file.slice(prefix.length)));
90
+ const retiredNames = new Set([...retired, ...files.filter((file) => file.startsWith(`${MIGRATIONS_ARCHIVE_DIR}/`)).map((file) => file.slice(MIGRATIONS_ARCHIVE_DIR.length + 1))].filter((name) => !localNames.has(name)));
91
+ const stamps = [...onHead].filter((name) => !retiredNames.has(name)).map((name) => MIGRATION_NAME.exec(name)?.[1]).filter(Boolean).map(migrationStamp).filter((stamp) => stamp !== null);
92
+ return { onHead, latest: stamps.length ? Math.max(...stamps) : null };
93
+ }
94
+
95
+ function migrationOrderingFindings(local, history, restamped = new Set()) {
96
+ return local.flatMap((entry) => {
97
+ const stamp = entry.match ? migrationStamp(entry.match[1]) : null;
98
+ if (history.onHead.has(entry.name) || restamped.has(entry.name) || stamp === null || history.latest === null || stamp > history.latest) return [];
99
+ return [found(DB_MIGRATION_SHAPE, entry.file, `${entry.file} is stamped ${entry.match[1]}, not after every migration already in repository history (latest active ${new Date(history.latest).toISOString()}); a new migration sorts after the committed active chain`, { stamp: entry.match[1], latest: new Date(history.latest).toISOString() })];
100
+ });
101
+ }
102
+
103
+ /** A restamp changes only the version, and its header proves the explicit old->new map against repository SQL. */
104
+ async function verifiedRestamps(local, files, repoRoot, git, sha) {
105
+ const prefix = `${MIGRATIONS_DIR}/`;
106
+ const active = files.filter(isActiveMigration).map((file) => file.slice(prefix.length));
107
+ const claims = local.map((entry) => ({ ...entry, header: RESTAMP_HEADER.exec(readText(repoRoot, entry.file) ?? '') })).filter((entry) => entry.header);
108
+ const accepted = [];
109
+ await eachInOrder(claims, async (entry) => {
110
+ const [, oldStamp, newStamp] = entry.header;
111
+ if (!entry.match || entry.match[1] !== newStamp || migrationStamp(oldStamp) === null || oldStamp === newStamp) return;
112
+ if (claims.filter((claim) => claim.header[1] === oldStamp).length !== 1) return;
113
+ const names = active.filter((name) => name.startsWith(`${oldStamp}_`) && name.slice(14) === entry.name.slice(14));
114
+ if (names.length !== 1 || local.some((candidate) => candidate.name === names[0])) return;
115
+ const committed = await baseFileText(git, repoRoot, sha, `${prefix}${names[0]}`);
116
+ if (committed === null || !sameText(withoutRestampHeader(readText(repoRoot, entry.file)), withoutRestampHeader(committed))) return;
117
+ accepted.push({ source: names[0], destination: entry.name });
118
+ });
119
+ return accepted;
120
+ }
121
+
122
+ async function baseMigrationEntryFindings(entry, repoRoot, git, baseSha, onBase) {
93
123
  if (!entry.match) return [];
94
- const stamp = migrationStamp(entry.match[1]);
95
124
  if (onBase.has(entry.name)) {
96
125
  const committed = await baseFileText(git, repoRoot, baseSha.sha, entry.file);
97
126
  const current = readText(repoRoot, entry.file);
98
127
  if (!sameText(current, committed)) return [found(DB_MIGRATION_SHAPE, entry.file, `${entry.file} differs from its content on ${baseSha.ref}; a migration on the base branch is immutable - write a new migration`, { base: baseSha.ref })];
99
- } else if (baseMax !== null && stamp !== null && stamp <= baseMax) {
100
- return [found(DB_MIGRATION_SHAPE, entry.file, `${entry.file} is stamped ${entry.match[1]}, not after every migration on ${baseSha.ref} (latest ${String(new Date(baseMax).toISOString())}); a new migration sorts after the base ones`, { stamp: entry.match[1], base: baseSha.ref })];
101
128
  }
102
129
  return [];
103
130
  }
@@ -112,26 +139,279 @@ function removedMigrationFindings(local, retired, baseNames, baseSha) {
112
139
 
113
140
  /**
114
141
  * L02 findings for names, stamps, ordering and immutability of supabase/migrations/*.sql. `archived` are the files of
115
- * supabase/migrations/_archive/ (app.supabase.migrations.archive): never judged, and a base migration moved there at a baseline
116
- * cutover is retired, not removed.
142
+ * supabase/migrations/_archive/ (app.supabase.migrations.archive): never judged, and a migration moved there at a baseline
143
+ * cutover is retired from the ordering chain, not removed. New files append after HEAD's active migrations; timestamps
144
+ * on archived migrations, remote branches and the wall clock do not constrain a replacement baseline.
117
145
  */
118
- export async function migrationShapeFindings({ repoRoot, migrations, archived = [], git, base, now }) {
146
+ export async function migrationShapeFindings({ repoRoot, migrations, archived = [], git, base }) {
119
147
  const prefix = `${MIGRATIONS_DIR}/`;
120
148
  const local = migrations.map((file) => {
121
149
  const name = file.startsWith(prefix) ? file.slice(prefix.length) : file;
122
150
  return { file, name, match: MIGRATION_NAME.exec(name) };
123
151
  });
124
- const findings = await localMigrationFindings(local, repoRoot, git, now);
152
+ const findings = localMigrationFindings(local);
153
+ const retired = new Set(archived.map((file) => file.slice(MIGRATIONS_ARCHIVE_DIR.length + 1)));
154
+ const headFiles = await migrationFilesAt(git, repoRoot, 'HEAD');
125
155
  const baseSha = await baseShaOf(git, repoRoot, base);
156
+ const baseFiles = baseSha ? await migrationFilesAt(git, repoRoot, baseSha.sha) : [];
157
+ const restamps = await verifiedRestamps(local, baseSha ? baseFiles : headFiles, repoRoot, git, baseSha?.sha ?? 'HEAD');
158
+ for (const restamp of restamps) retired.add(restamp.source);
159
+ const history = migrationHistory(headFiles, retired, new Set(local.map((entry) => entry.name)));
160
+ findings.push(...migrationOrderingFindings(local, history, new Set(restamps.map((entry) => entry.destination))));
126
161
  if (!baseSha) return findings;
127
- const baseNames = await baseMigrationNames(git, repoRoot, baseSha.sha);
162
+ const baseNames = baseFiles.filter(isActiveMigration).map((file) => file.slice(prefix.length));
128
163
  const onBase = new Set(baseNames);
129
- const baseStamps = baseNames.map((name) => MIGRATION_NAME.exec(name)?.[1]).filter(Boolean).map(migrationStamp).filter((stamp) => stamp !== null);
130
- const baseMax = baseStamps.length ? Math.max(...baseStamps) : null;
131
164
  await eachInOrder(local, async (entry) => {
132
- findings.push(...await baseMigrationEntryFindings(entry, repoRoot, git, baseSha, onBase, baseMax));
165
+ findings.push(...await baseMigrationEntryFindings(entry, repoRoot, git, baseSha, onBase));
133
166
  });
134
- const retired = new Set(archived.map((file) => file.slice(MIGRATIONS_ARCHIVE_DIR.length + 1)));
135
167
  findings.push(...removedMigrationFindings(local, retired, baseNames, baseSha));
136
168
  return findings;
137
169
  }
170
+
171
+ const sqlName = (token) => token && ['word', 'ident'].includes(token.t) ? token.v : null;
172
+ const sqlWord = (token, value) => token?.t === 'word' && token.v === value;
173
+
174
+ function commentEnd(text, start) {
175
+ if (text.startsWith('--', start)) {
176
+ const end = text.indexOf('\n', start + 2);
177
+ return end === -1 ? text.length : end;
178
+ }
179
+ if (!text.startsWith('/*', start)) return null;
180
+ let depth = 1;
181
+ let index = start + 2;
182
+ while (index < text.length && depth > 0) {
183
+ if (text.startsWith('/*', index)) { depth += 1; index += 2; }
184
+ else if (text.startsWith('*/', index)) { depth -= 1; index += 2; }
185
+ else index += 1;
186
+ }
187
+ return index;
188
+ }
189
+
190
+ function quotedToken(text, start, quote, escaped = false) {
191
+ let index = start + 1;
192
+ let value = '';
193
+ while (index < text.length) {
194
+ if (text[index] === quote && text[index + 1] === quote) { value += quote; index += 2; }
195
+ else if (text[index] === quote) return { value, end: index + 1 };
196
+ else if (escaped && text[index] === '\\') { value += text[index + 1] ?? ''; index += 2; }
197
+ else { value += text[index]; index += 1; }
198
+ }
199
+ return { value, end: index };
200
+ }
201
+
202
+ function sqlTokenAt(text, index) {
203
+ const char = text[index];
204
+ if (char === "'" || char === '"') {
205
+ const token = quotedToken(text, index, char);
206
+ return { t: char === '"' ? 'ident' : 'string', v: token.value, end: token.end };
207
+ }
208
+ if (/[eE]/.test(char) && text[index + 1] === "'") {
209
+ const token = quotedToken(text, index + 1, "'", true);
210
+ return { t: 'string', v: token.value, end: token.end };
211
+ }
212
+ const tag = char === '$' ? /^\$(?:[A-Za-z_]\w*)?\$/.exec(text.slice(index))?.[0] : null;
213
+ if (tag) {
214
+ const end = text.indexOf(tag, index + tag.length);
215
+ return { t: 'body', v: text.slice(index + tag.length, end === -1 ? text.length : end), end: end === -1 ? text.length : end + tag.length };
216
+ }
217
+ const word = /^[A-Za-z_][A-Za-z0-9_$]*/.exec(text.slice(index))?.[0];
218
+ if (word) return { t: 'word', v: word.toLowerCase(), end: index + word.length };
219
+ const number = /^\d+(?:\.\d+)?/.exec(text.slice(index))?.[0];
220
+ if (number) return { t: 'number', v: number, end: index + number.length };
221
+ return { t: 'punct', v: char, end: index + 1 };
222
+ }
223
+
224
+ /** Synchronous SQL tokens; literal values stay internal and must never be copied to a finding. */
225
+ export function databaseSqlTokens(text) {
226
+ const tokens = [];
227
+ let index = 0;
228
+ let line = 1;
229
+ while (index < text.length) {
230
+ if (/\s/.test(text[index])) { if (text[index] === '\n') line += 1; index += 1; continue; }
231
+ const end = commentEnd(text, index);
232
+ if (end !== null) { line += text.slice(index, end).split('\n').length - 1; index = end; continue; }
233
+ const token = sqlTokenAt(text, index);
234
+ tokens.push({ t: token.t, v: token.v, line, index });
235
+ line += text.slice(index, token.end).split('\n').length - 1;
236
+ index = token.end;
237
+ }
238
+ return tokens;
239
+ }
240
+
241
+ /** Split executable SQL, retaining function bodies as opaque tokens until their language is known. */
242
+ export function databaseSqlStatements(tokens) {
243
+ const statements = [];
244
+ let current = [];
245
+ for (const token of tokens) {
246
+ if (token.t === 'punct' && token.v === ';') { if (current.length) statements.push(current); current = []; }
247
+ else current.push(token);
248
+ }
249
+ if (current.length) statements.push(current);
250
+ return statements;
251
+ }
252
+
253
+ function executableStatementTokens(statement, depth) {
254
+ const isDo = sqlWord(statement[0], 'do');
255
+ const functionAt = statement.findIndex((token) => sqlWord(token, 'function') || sqlWord(token, 'procedure'));
256
+ const isFunction = sqlWord(statement[0], 'create') && functionAt > 0 && functionAt < 4;
257
+ if (!isDo && !isFunction) return statement;
258
+ const languageAt = statement.findIndex((token) => sqlWord(token, 'language'));
259
+ const language = statement[languageAt + 1]?.v ?? 'sql';
260
+ if (languageAt !== -1 && !['sql', 'plpgsql'].includes(language)) return statement;
261
+ return statement.flatMap((token, index) => {
262
+ const body = token.t === 'body' || (token.t === 'string' && (isDo || sqlWord(statement[index - 1], 'as')));
263
+ if (!body) return [token];
264
+ return databaseExecutableTokens(token.v, depth + 1).map((child) => ({ ...child, line: child.line + token.line - 1 }));
265
+ });
266
+ }
267
+
268
+ /** SQL and PL/pgSQL bodies are scanned; comments, data literals and other function languages are opaque. */
269
+ export function databaseExecutableTokens(text, depth = 0) {
270
+ const tokens = databaseSqlTokens(text);
271
+ if (depth > 8) return tokens;
272
+ return databaseSqlStatements(tokens).flatMap((statement) => [...executableStatementTokens(statement, depth), { t: 'punct', v: ';' }]);
273
+ }
274
+
275
+ function nameAt(tokens, index) {
276
+ const first = sqlName(tokens[index]);
277
+ if (first === null) return null;
278
+ const parts = [first];
279
+ let next = index + 1;
280
+ while (tokens[next]?.v === '.' && sqlName(tokens[next + 1]) !== null) { parts.push(sqlName(tokens[next + 1])); next += 2; }
281
+ return { name: parts.join('.'), leaf: parts.at(-1), next };
282
+ }
283
+
284
+ const skipWords = (tokens, index, words) => {
285
+ let next = index;
286
+ while (words.some((word) => sqlWord(tokens[next], word))) next += 1;
287
+ return next;
288
+ };
289
+
290
+ function dropObjects(tokens, start) {
291
+ const kind = tokens[start + 1]?.v;
292
+ if (!['table', 'role', 'user', 'schema', 'type', 'view', 'materialized', 'sequence', 'function', 'procedure', 'index', 'policy', 'trigger'].includes(kind)) return [];
293
+ let index = skipWords(tokens, start + 2, ['view', 'if', 'exists', 'concurrently']);
294
+ const objects = [];
295
+ while (index < tokens.length) {
296
+ const object = nameAt(tokens, index);
297
+ if (!object) break;
298
+ objects.push({ ...object, kind, line: tokens[start].line });
299
+ if (tokens[object.next]?.v !== ',') break;
300
+ index = object.next + 1;
301
+ }
302
+ return objects;
303
+ }
304
+
305
+ function columnAlterObject(tokens, index, table, line) {
306
+ let next = index + 1;
307
+ if (sqlWord(tokens[next], 'column')) next += 1;
308
+ const column = nameAt(tokens, next);
309
+ if (!column) return null;
310
+ const tail = tokens.slice(column.next);
311
+ const type = sqlWord(tail[0], 'type') || (sqlWord(tail[0], 'set') && sqlWord(tail[1], 'data') && sqlWord(tail[2], 'type'));
312
+ const notNull = sqlWord(tail[0], 'set') && sqlWord(tail[1], 'not') && sqlWord(tail[2], 'null');
313
+ const defaultSet = tokens.some((token, at) => sqlWord(token, 'alter') && sqlName(tokens[at + 1 + Number(sqlWord(tokens[at + 1], 'column'))]) === column.leaf
314
+ && sqlWord(tokens[at + 2 + Number(sqlWord(tokens[at + 1], 'column'))], 'set') && sqlWord(tokens[at + 3 + Number(sqlWord(tokens[at + 1], 'column'))], 'default'));
315
+ if (!type && !(notNull && !defaultSet)) return null;
316
+ return { name: `${table.name}.${column.name}`, leaf: column.leaf, table: table.leaf, kind: 'column', line };
317
+ }
318
+
319
+ function alteredColumn(table, column, line) {
320
+ return { name: `${table.name}.${column.name}`, leaf: column.leaf, table: table.leaf, kind: 'column', line };
321
+ }
322
+
323
+ function tableActionObject(body, index, table, line) {
324
+ if (sqlWord(body[index], 'drop')) {
325
+ const at = skipWords(body, index + 1, ['column', 'constraint', 'if', 'exists']);
326
+ if (sqlWord(body[at], 'default') || sqlWord(body[at], 'not')) return null;
327
+ const column = nameAt(body, at);
328
+ return column ? alteredColumn(table, column, line) : null;
329
+ }
330
+ if (sqlWord(body[index], 'rename')) {
331
+ const column = sqlWord(body[index + 1], 'column') ? nameAt(body, index + 2) : null;
332
+ return column ? alteredColumn(table, column, line) : { ...table, kind: 'table', line };
333
+ }
334
+ return sqlWord(body[index], 'alter') ? columnAlterObject(body, index, table, line) : null;
335
+ }
336
+
337
+ function alterObjects(tokens, start) {
338
+ const table = nameAt(tokens, skipWords(tokens, start + 2, ['if', 'exists', 'only']));
339
+ if (!table) return [];
340
+ const body = tokens.slice(table.next);
341
+ return body.map((token, index) => tableActionObject(body, index, table, tokens[start].line)).filter(Boolean);
342
+ }
343
+
344
+ function truncatedObjects(tokens, start) {
345
+ const objects = [];
346
+ let index = skipWords(tokens, start + 1, ['table', 'only']);
347
+ while (index < tokens.length) {
348
+ const table = nameAt(tokens, index);
349
+ if (!table) break;
350
+ objects.push({ ...table, kind: 'table', line: tokens[start].line });
351
+ if (tokens[table.next]?.v !== ',') break;
352
+ index = skipWords(tokens, table.next + 1, ['only']);
353
+ }
354
+ return objects;
355
+ }
356
+
357
+ function destructiveObjects(statement) {
358
+ const objects = [];
359
+ for (let index = 0; index < statement.length; index += 1) {
360
+ if (sqlWord(statement[index], 'drop')) objects.push(...dropObjects(statement, index));
361
+ else if (sqlWord(statement[index], 'truncate')) objects.push(...truncatedObjects(statement, index));
362
+ else if (sqlWord(statement[index], 'alter') && sqlWord(statement[index + 1], 'table')) objects.push(...alterObjects(statement, index));
363
+ else if (sqlWord(statement[index], 'alter') && ['type', 'schema', 'view', 'sequence', 'function', 'procedure', 'index'].includes(statement[index + 1]?.v)) {
364
+ const object = nameAt(statement, index + 2);
365
+ if (object && statement.slice(object.next).some((token) => sqlWord(token, 'rename'))) objects.push({ ...object, kind: statement[index + 1].v, line: statement[index].line });
366
+ }
367
+ }
368
+ return objects;
369
+ }
370
+
371
+ function sourceNames(ts, text, file) {
372
+ const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
373
+ const names = new Set();
374
+ const addSql = (sql) => { for (const token of databaseSqlTokens(sql)) if (sqlName(token) !== null) names.add(token.v); };
375
+ const visit = (node) => {
376
+ if (ts.isStringLiteralLike(node)) addSql(node.text);
377
+ if (ts.isTemplateExpression(node)) { addSql(node.head.text); for (const span of node.templateSpans) addSql(span.literal.text); }
378
+ if (/\.entity\.[cm]?tsx?$/.test(file) && ts.isIdentifier(node)) names.add(node.text);
379
+ ts.forEachChild(node, visit);
380
+ };
381
+ visit(source);
382
+ return names;
383
+ }
384
+
385
+ function destructiveObjectFinding(file, object, contract, references) {
386
+ const reference = references.find(({ names }) => names.has(object.leaf));
387
+ if (contract && !reference) return null;
388
+ const reason = reference && contract ? `${reference.file} still references this object; remove the reference before the contract migration`
389
+ : 'use a *_contract_* migration after the application stops using this object';
390
+ return found(DB_MIGRATION_DESTRUCTIVE, file, `${file}:${object.line} destructively changes ${object.name}; ${reason}`,
391
+ { line: object.line, object: object.name, ...(reference ? { reference: reference.file } : {}) });
392
+ }
393
+
394
+ function destructiveFileFindings(file, text, references) {
395
+ const contract = /_contract_[a-z0-9]+(?:[-_][a-z0-9]+)*\.sql$/.test(file);
396
+ const seen = new Set();
397
+ const objects = databaseSqlStatements(databaseExecutableTokens(text)).flatMap(destructiveObjects);
398
+ return objects.flatMap((object) => {
399
+ const key = `${object.line}:${object.name}`;
400
+ if (seen.has(key)) return [];
401
+ seen.add(key);
402
+ const finding = destructiveObjectFinding(file, object, contract, references);
403
+ return finding ? [finding] : [];
404
+ });
405
+ }
406
+
407
+ /** Destructive changes require a contract migration and no remaining SQL/entity reference in be/src. */
408
+ export function destructiveMigrationFindings({ repoRoot, migrations, files }) {
409
+ const sources = files.filter((file) => /^be\/src\/.+\.(?:sql|entity)\.[cm]?tsx?$/.test(file));
410
+ const ts = sources.length ? loadTypescript(repoRoot, HERE) : null;
411
+ if (sources.length && !ts) return [found(DB_MIGRATION_DESTRUCTIVE, 'hfs.json', 'destructive migration checks require the TypeScript parser to read be/src references')];
412
+ const references = sources.map((file) => ({ file, names: sourceNames(ts, readText(repoRoot, file) ?? '', file) }));
413
+ return migrations.flatMap((file) => {
414
+ const text = readText(repoRoot, file);
415
+ return text === null ? [] : destructiveFileFindings(file, text, references);
416
+ });
417
+ }
@@ -5,6 +5,7 @@ import { eachInOrder } from '../../lib/in-order.mjs';
5
5
  import { parsePlpgsqlBody, parseSql } from '../sql/pg-parse.mjs';
6
6
  import { DB_DEFINER_SAFE, DB_DYNAMIC_DDL } from './database-constants.mjs';
7
7
 
8
+ const DB_DEFINER_REVOKE = 'DB_DEFINER_REVOKE';
8
9
  const sval = (node) => node?.String?.sval;
9
10
  const nameOf = (list, defaultSchema) => {
10
11
  const parts = (list ?? []).map(sval).filter((part) => part !== undefined);
@@ -183,17 +184,66 @@ async function definerDynamicFindings(file, fn, queries, baseLine) {
183
184
  return findings;
184
185
  }
185
186
 
187
+ const OUTPUT_PARAMETER_MODES = new Set(['OUT', 'TABLE'].map((mode) => ['FUNC', 'PARAM', mode].join('_')));
188
+ const RESET_ALL_KIND = ['VAR', 'RESET', 'ALL'].join('_');
189
+ const inputParameters = (node) => (node.parameters ?? []).map((item) => item.FunctionParameter)
190
+ .filter((parameter) => !OUTPUT_PARAMETER_MODES.has(parameter?.mode));
191
+
192
+ function typeIdentity(type) {
193
+ const node = type?.TypeName ?? type;
194
+ const parts = (node?.names ?? []).map(sval).filter(Boolean);
195
+ if (parts[0] === 'pg_catalog') parts.shift();
196
+ return `${parts.join('.')}${node?.arrayBounds?.length ? '[]' : ''}`;
197
+ }
198
+
199
+ const functionArguments = (fn) => fn.node.func?.objargs ?? inputParameters(fn.node).map((parameter) => parameter.argType);
200
+ const argumentIdentity = (args) => args.map(typeIdentity).join(',');
201
+
202
+ function matchesFunction(fn, object, defaultSchema) {
203
+ if (nameOf(object?.objname, defaultSchema) !== fn.name) return false;
204
+ return object?.args_unspecified === true || argumentIdentity(object?.objargs ?? []) === argumentIdentity(functionArguments(fn));
205
+ }
206
+
207
+ const matchesAlter = (fn, alter) => matchesFunction(fn, alter.node.func, alter.schema);
208
+
209
+ function grantMatchesFunction(fn, grant) {
210
+ if (grant.objtype !== 'OBJECT_FUNCTION') return false;
211
+ if (grant.privileges.length && !grant.privileges.includes('execute') && !grant.privileges.includes('all')) return false;
212
+ if (grant.targtype === 'ACL_TARGET_ALL_IN_SCHEMA') return grant.objects.some((object) => sval(object) === fn.schema);
213
+ return grant.objects.some((object) => matchesFunction(fn, object?.ObjectWithArgs, grant.defaultSchema));
214
+ }
215
+
186
216
  function revokedRolesFor(fn, grants) {
187
217
  const roles = new Set();
188
218
  for (const grant of grants) {
189
- if (grant.grant || grant.objtype !== 'OBJECT_FUNCTION') continue;
190
- if (!grant.privileges.includes('execute') && !grant.privileges.includes('all')) continue;
191
- const matches = grant.objects.some((object) => nameOf(object?.ObjectWithArgs?.objname, grant.defaultSchema) === fn.name);
192
- if (matches) for (const role of grant.grantees) roles.add(role);
219
+ if (grant.index <= (fn.originIndex ?? fn.index)) continue;
220
+ if (!grantMatchesFunction(fn, grant)) continue;
221
+ for (const role of grant.grantees) {
222
+ if (grant.grant) roles.delete(role);
223
+ else roles.add(role);
224
+ }
193
225
  }
194
226
  return roles;
195
227
  }
196
228
 
229
+ function pathSetting(options, pinned, facts) {
230
+ for (const option of options) {
231
+ const setting = option?.DefElem?.arg?.VariableSetStmt;
232
+ if (setting?.name === 'search_path' || setting?.kind === RESET_ALL_KIND) pinned = facts.isSearchPathSet(option.DefElem);
233
+ }
234
+ return pinned;
235
+ }
236
+
237
+ function hasPinnedSearchPath(fn, facts) {
238
+ let pinned = pathSetting(fn.node.options ?? [], false, facts);
239
+ const originIndex = fn.originIndex ?? fn.index;
240
+ for (const alter of facts.alterFunctions) {
241
+ if (alter.index <= originIndex || !matchesAlter(fn, alter)) continue;
242
+ pinned = pathSetting(alter.node.actions ?? [], pinned, facts);
243
+ }
244
+ return pinned;
245
+ }
246
+
197
247
  async function doBlockFindings(file, block) {
198
248
  if (block.body === undefined || block.language !== 'plpgsql') {
199
249
  return [found(DB_DYNAMIC_DDL, file, `${file}:${block.line} DO block in ${block.language} cannot be inspected; a DO block is plpgsql so a static pass sees its statements`, { line: block.line })];
@@ -206,14 +256,12 @@ async function doBlockFindings(file, block) {
206
256
  function definerDeclarationFindings(file, fn, facts, exposed, definer) {
207
257
  if (!definer) return [];
208
258
  const findings = [];
209
- const pathSet = functionOption(fn.node, 'set').some((option) => facts.isSearchPathSet(option))
210
- || facts.alterFunctions.some((alter) => alter.name === fn.name && alter.setsPath && alter.index > fn.index);
211
- if (!pathSet) findings.push(found(DB_DEFINER_SAFE, file, `${file}:${fn.line} security definer function ${fn.name} sets no search_path; add \`set search_path = ''\` (or a pinned schema list) on the create, or an alter function in the same migration`, { line: fn.line, function: fn.name }));
259
+ if (!hasPinnedSearchPath(fn, facts)) findings.push(found(DB_DEFINER_SAFE, file, `${file}:${fn.line} security definer function ${fn.name} sets no search_path; add \`set search_path = ''\` (or a pinned schema list) on the create, or an alter function in the same migration`, { line: fn.line, function: fn.name }));
212
260
  if (exposed.has(fn.schema)) {
213
261
  const revoked = revokedRolesFor(fn, facts.grants);
214
262
  if (!revoked.has('public') || !revoked.has('anon')) findings.push(found(DB_DEFINER_SAFE, file, `${file}:${fn.line} security definer function ${fn.name} lives in the exposed schema ${fn.schema}; keep it in a non-exposed schema or revoke execute from public and anon in the same migration`, { line: fn.line, function: fn.name }));
215
263
  }
216
- if (!fn.node.is_procedure && !fn.node.returnType) findings.push(found(DB_DEFINER_SAFE, file, `${file}:${fn.line} security definer function ${fn.name} declares no return type`, { line: fn.line, function: fn.name }));
264
+ if (!fn.altered && !fn.node.is_procedure && !fn.node.returnType) findings.push(found(DB_DEFINER_SAFE, file, `${file}:${fn.line} security definer function ${fn.name} declares no return type`, { line: fn.line, function: fn.name }));
217
265
  return findings;
218
266
  }
219
267
 
@@ -235,14 +283,76 @@ async function functionBodyFindings(file, fn, definer) {
235
283
  async function functionFindings(file, fn, facts, exposed) {
236
284
  const definer = securityDefiner(fn.node);
237
285
  const findings = definerDeclarationFindings(file, fn, facts, exposed, definer);
238
- findings.push(...await functionBodyFindings(file, fn, definer));
286
+ const becomesDefiner = facts.alterFunctions.some((alter) => alter.securityDefiner && alter.index > fn.index && matchesAlter(fn, alter));
287
+ findings.push(...await functionBodyFindings(file, fn, definer || becomesDefiner));
239
288
  return findings;
240
289
  }
241
290
 
291
+ function alteredDefinerFindings(file, alter, facts, exposed) {
292
+ if (!alter.securityDefiner) return [];
293
+ const created = facts.functions.filter((fn) => fn.index < alter.index && matchesAlter(fn, alter)).at(-1);
294
+ const node = created?.node ?? { func: alter.node.func };
295
+ const fn = { ...alter, node, altered: true, originIndex: created?.index ?? -1 };
296
+ return definerDeclarationFindings(file, fn, facts, exposed, true);
297
+ }
298
+
242
299
  /** All PL/pgSQL-backed L04/L06 findings for one migration's extracted facts. */
243
300
  export async function plpgsqlFindings(file, facts, exposed) {
244
301
  const findings = [];
245
302
  await eachInOrder(facts.doBlocks, async (block) => { findings.push(...await doBlockFindings(file, block)); });
246
303
  await eachInOrder(facts.functions, async (fn) => { findings.push(...await functionFindings(file, fn, facts, exposed)); });
304
+ for (const alter of facts.alterFunctions) findings.push(...alteredDefinerFindings(file, alter, facts, exposed));
247
305
  return findings;
248
306
  }
307
+
308
+ const functionKey = (fn) => `${fn.name}/${argumentIdentity(functionArguments(fn))}`;
309
+
310
+ function applyFunctionAlter(functions, alter) {
311
+ if (!functionOption({ options: alter.node.actions }, 'security').length) return;
312
+ const matching = [...functions.values()].filter((fn) => matchesAlter(fn, alter));
313
+ if (!matching.length) matching.push({ ...alter, node: { func: alter.node.func }, originIndex: -1 });
314
+ for (const fn of matching) {
315
+ functions.set(functionKey(fn), { ...fn, file: alter.file, line: alter.line, definer: alter.securityDefiner });
316
+ }
317
+ }
318
+
319
+ function applyFunctionDrop(functions, statement) {
320
+ if (statement.node.removeType !== 'OBJECT_FUNCTION') return;
321
+ for (const fn of functions.values()) {
322
+ if (statement.node.objects?.some((object) => matchesFunction(fn, object.ObjectWithArgs, statement.defaultSchema))) {
323
+ functions.delete(functionKey(fn));
324
+ }
325
+ }
326
+ }
327
+
328
+ function finalFunctionAccess(analyses) {
329
+ const functions = new Map();
330
+ const grants = [];
331
+ let offset = 0;
332
+ for (const { file, facts } of analyses) {
333
+ const ordered = (items) => items.map((item) => ({ ...item, file, index: offset + item.index }));
334
+ const created = new Map(ordered(facts.functions).map((fn) => [fn.index, fn]));
335
+ const altered = new Map(ordered(facts.alterFunctions).map((fn) => [fn.index, fn]));
336
+ grants.push(...ordered(facts.grants));
337
+ for (const statement of ordered(facts.statements)) {
338
+ const fn = created.get(statement.index);
339
+ if (fn) {
340
+ const key = functionKey(fn);
341
+ const originIndex = functions.get(key)?.originIndex ?? fn.index;
342
+ functions.set(key, { ...fn, originIndex, definer: securityDefiner(fn.node) });
343
+ }
344
+ const alter = altered.get(statement.index);
345
+ if (alter) applyFunctionAlter(functions, alter);
346
+ if (statement.type === 'DropStmt') applyFunctionDrop(functions, statement);
347
+ }
348
+ offset += facts.statements.length;
349
+ }
350
+ return { functions: [...functions.values()], grants };
351
+ }
352
+
353
+ /** Authenticated execution is judged after all active migrations, including later revokes and grants. */
354
+ export function migrationSetDefinerFindings(analyses, exposed) {
355
+ const { functions, grants } = finalFunctionAccess(analyses);
356
+ return functions.filter((fn) => fn.definer && exposed.has(fn.schema) && !revokedRolesFor(fn, grants).has('authenticated'))
357
+ .map((fn) => found(DB_DEFINER_REVOKE, fn.file, `${fn.file}:${fn.line} security definer function ${fn.name} lives in the exposed schema ${fn.schema}; the migration set ends without revoking execute from authenticated; revoke execute from public, anon and authenticated`, { line: fn.line, function: fn.name }));
358
+ }