@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,361 @@
1
+ // R258: queue declarations and pg_cron ticks have one migration owner in either edition.
2
+ // Initialize PostgreSQL's WASM parser at module load; the exported finder itself is synchronous.
3
+ // SQL and TypeScript ASTs keep comments, quoted identifiers and inert string literals out of the inventory.
4
+ import pg from 'libpg-query';
5
+ import { sqlStringParts as strings, walkSqlAst as walk, sqlParser, sqlCallArgument as argument, sqlStringLiteral as sqlLiteral } from '../../lib/sql-ast.mjs';
6
+ import { queueSourceFacts, queueSchedulers, queueProblem as problem } from './database-queue-sources.mjs';
7
+ import { readJson, readText } from './read.mjs';
8
+ import { DB_CRON_OWNED, DB_PGMQ_DECLARED, isActiveMigration } from './database-constants.mjs';
9
+
10
+ await pg.loadModule();
11
+
12
+ const PURE_CALLS = new Set(['jsonb_build_object', 'json_build_object', 'jsonb_build_array', 'json_build_array', 'to_jsonb', 'now', 'clock_timestamp']);
13
+ const TABLE_PRIVILEGES = ['select', 'insert', 'update', 'delete', 'truncate', 'references', 'trigger', 'maintain'];
14
+ const sqlName = (nodes) => strings(nodes).join('.');
15
+ const callName = (call) => sqlName(call.funcname);
16
+ const qualified = (name, schema = 'public') => name.includes('.') ? name : `${schema}.${name}`;
17
+
18
+ const sqlTree = sqlParser(pg);
19
+ const walkExecutable = (tree, visit) => walk(tree, visit, true);
20
+
21
+ function callsOf(tree) {
22
+ const calls = [];
23
+ walkExecutable(tree, (node) => { if (node.FuncCall) calls.push(node.FuncCall); });
24
+ return calls;
25
+ }
26
+
27
+ function inspectCalls(state, tree, file, migration) {
28
+ walkExecutable(tree, (node) => {
29
+ if (node.GrantStmt || node.AlterDefaultPrivilegesStmt) recordGrant(state, node, file, migration);
30
+ });
31
+ for (const call of callsOf(tree)) {
32
+ inspectQueueCall(state, call, file, migration);
33
+ inspectCronCall(state, call, file, migration);
34
+ }
35
+ }
36
+
37
+ function inspectQueueCall(state, call, file, migration) {
38
+ const name = callName(call);
39
+ if (name !== 'pgmq.create' && name !== 'pgmq.drop_queue') return;
40
+ const queue = sqlLiteral(argument(call, 0, ['queue_name']));
41
+ if (name === 'pgmq.create') {
42
+ if (!migration || queue === null) problem(state, DB_PGMQ_DECLARED, file, 'pgmq.create must declare a literal queue in an active migration');
43
+ else state.created.set(queue, file);
44
+ } else if (migration && queue !== null) {
45
+ state.created.delete(queue);
46
+ state.queueAcls.delete(`pgmq.q_${queue}`);
47
+ state.queueAcls.delete(`pgmq.a_${queue}`);
48
+ }
49
+ }
50
+
51
+ function recordSchedule(state, call, file) {
52
+ const entry = { call, file, created: new Map(state.created), functions: new Map(state.functions) };
53
+ state.schedules.push(entry);
54
+ const job = sqlLiteral(argument(call, 0, ['job_name']));
55
+ const schedule = sqlLiteral(argument(call, 1, ['schedule']));
56
+ const command = sqlLiteral(argument(call, 2, ['command']));
57
+ if (job !== null && schedule !== null && command !== null) {
58
+ state.activeCron.set(job, { name: job, schedule, command });
59
+ state.liveSchedules.set(job, entry);
60
+ }
61
+ }
62
+
63
+ function inspectCronCall(state, call, file, migration) {
64
+ const name = callName(call);
65
+ if (name === 'cron.schedule' || name === 'cron.schedule_in_database') {
66
+ if (!migration || name !== 'cron.schedule') problem(state, DB_CRON_OWNED, file, 'cron jobs must be declared by cron.schedule in an active migration');
67
+ else recordSchedule(state, call, file);
68
+ return;
69
+ }
70
+ if (name !== 'cron.unschedule' || !migration) return;
71
+ const job = sqlLiteral(argument(call, 0, ['job_name']));
72
+ if (job !== null) {
73
+ state.unschedules.push({ name: job, file });
74
+ state.activeCron.delete(job);
75
+ state.liveSchedules.delete(job);
76
+ }
77
+ else problem(state, DB_CRON_OWNED, file, 'cron.unschedule must identify the migration-owned job by literal name');
78
+ }
79
+
80
+ function plpgsqlTree(body, parameters = []) {
81
+ let tag = '$hfs_queue$';
82
+ while (body.includes(tag)) tag = `${tag.slice(0, -1)}_$`;
83
+ const args = parameters.map((item) => item.FunctionParameter?.name).filter(Boolean).map((name) => `"${name.replaceAll('"', '""')}" text`).join(',');
84
+ try { return pg.parsePlPgSQLSync(`create function hfs_probe(${args}) returns void language plpgsql as ${tag}${body}${tag};`); } catch { return null; }
85
+ }
86
+
87
+ function dynamicOwnership(state, expression, file, migration) {
88
+ const fragments = [];
89
+ walk(expression, (node) => {
90
+ const literal = node.A_Const?.sval?.sval ?? null;
91
+ if (literal !== null) fragments.push(literal);
92
+ });
93
+ for (const sql of [...fragments, fragments.join('')]) {
94
+ const tree = sqlTree(sql.replace(/%[ILs]/g, 'NULL'));
95
+ walkExecutable(tree, (node) => { if (node.GrantStmt || node.AlterDefaultPrivilegesStmt) recordGrant(state, node, file, migration); });
96
+ for (const call of callsOf(tree)) {
97
+ if (callName(call) === 'pgmq.create') problem(state, DB_PGMQ_DECLARED, file, 'dynamic SQL cannot own a queue declaration; write the call as static migration SQL');
98
+ if (callName(call).startsWith('cron.')) problem(state, DB_CRON_OWNED, file, 'dynamic SQL cannot own a cron declaration; write the call as static migration SQL');
99
+ }
100
+ }
101
+ }
102
+
103
+ function inspectDo(state, node, file) {
104
+ const option = (key) => (node.args ?? []).find((item) => item.DefElem?.defname === key)?.DefElem?.arg?.String?.sval;
105
+ const tree = plpgsqlTree(option('as') ?? '');
106
+ walk(tree, (item) => {
107
+ if (item.PLpgSQL_expr && item.PLpgSQL_expr.parseMode === 0) inspectCalls(state, sqlTree(item.PLpgSQL_expr.query), file, true);
108
+ if (item.PLpgSQL_stmt_dynexecute) {
109
+ const expression = sqlTree(`select ${item.PLpgSQL_stmt_dynexecute.query?.PLpgSQL_expr?.query ?? ''}`);
110
+ dynamicOwnership(state, expression, file, true);
111
+ }
112
+ });
113
+ }
114
+
115
+ function grantCheck(state, node, file) {
116
+ const defaults = node.AlterDefaultPrivilegesStmt;
117
+ const grant = node.GrantStmt ?? defaults?.action;
118
+ if (!grant?.is_grant) return;
119
+ const schemaNames = defaults?.options?.flatMap((item) => item.DefElem?.defname === 'schemas' ? strings(item.DefElem.arg?.List?.items) : []) ?? [];
120
+ const objects = (grant.objects ?? []).map((item) => item.RangeVar ? `${item.RangeVar.schemaname}.${item.RangeVar.relname}` : sqlName(item.ObjectWithArgs?.objname ?? item.List?.items ?? [item]));
121
+ if (![...schemaNames, ...objects].some((name) => /^(?:pgmq|pgmq_public)(?:\.|$)/.test(name))) return;
122
+ const roles = (grant.grantees ?? []).map((item) => item.RoleSpec?.roletype === 'ROLESPEC_PUBLIC' ? 'public' : item.RoleSpec?.rolename);
123
+ const ownedTables = !defaults && grant.targtype === 'ACL_TARGET_OBJECT' && grant.objtype === 'OBJECT_TABLE'
124
+ && objects.every((name) => /^pgmq\.[qa]_/.test(name) && state.created.has(name.slice(7)));
125
+ if (!ownedTables || roles.length === 0 || roles.some((role) => role !== state.appRole)) problem(state, DB_PGMQ_DECLARED, file, `pgmq grants name only q_<queue> and a_<queue> tables of declared queues, and only the ${state.appRole} role`);
126
+ }
127
+
128
+ function recordGrant(state, node, file, executed) {
129
+ state.grants.push({ node, file, created: new Map(state.created), executed });
130
+ const grant = node.GrantStmt;
131
+ if (!executed || !grant || grant.objtype !== 'OBJECT_TABLE' || grant.targtype !== 'ACL_TARGET_OBJECT') return;
132
+ if (!(grant.grantees ?? []).some((item) => item.RoleSpec?.rolename === state.appRole)) return;
133
+ const privileges = grant.privileges?.map((item) => item.AccessPriv?.priv_name).filter(Boolean) ?? TABLE_PRIVILEGES;
134
+ for (const object of grant.objects ?? []) {
135
+ const table = object.RangeVar && `${object.RangeVar.schemaname}.${object.RangeVar.relname}`;
136
+ if (!table || !/^pgmq\.[qa]_/.test(table)) continue;
137
+ const acl = state.queueAcls.get(table) ?? new Map();
138
+ for (const privilege of privileges) updateQueuePrivilege(acl, grant, privilege);
139
+ state.queueAcls.set(table, acl);
140
+ }
141
+ }
142
+
143
+ function updateQueuePrivilege(acl, grant, privilege) {
144
+ if (grant.is_grant) acl.set(privilege, Boolean(grant.grant_option || acl.get(privilege)));
145
+ else if (!grant.grant_option) acl.delete(privilege);
146
+ else if (acl.has(privilege)) acl.set(privilege, false);
147
+ }
148
+
149
+ function migrationStatement(state, node, file) {
150
+ if (node.CreateFunctionStmt) {
151
+ const fn = node.CreateFunctionStmt;
152
+ state.functions.set(qualified(sqlName(fn.funcname)), { node: fn, file });
153
+ inspectFunctionDeclarations(state, fn, file);
154
+ }
155
+ if (node.DropStmt?.removeType === 'OBJECT_FUNCTION') {
156
+ for (const item of node.DropStmt.objects ?? []) {
157
+ const name = qualified(sqlName(item.ObjectWithArgs?.objname));
158
+ state.drops.push({ name, file });
159
+ state.functions.delete(name);
160
+ }
161
+ }
162
+ if (node.DoStmt) inspectDo(state, node.DoStmt, file);
163
+ }
164
+
165
+ function migrationFacts(state, files) {
166
+ for (const file of files.filter((item) => item.endsWith('.sql') && !item.includes('/_archive/')).sort((left, right) => left.localeCompare(right))) {
167
+ const text = readText(state.repoRoot, file);
168
+ if (text === null) continue;
169
+ const tree = sqlTree(text);
170
+ const active = isActiveMigration(file);
171
+ const scheduleStart = state.schedules.length;
172
+ for (const entry of tree?.stmts ?? []) {
173
+ inspectCalls(state, { stmts: [entry] }, file, active);
174
+ if (active) migrationStatement(state, entry.stmt, file);
175
+ }
176
+ completeScheduleInventory(state, scheduleStart);
177
+ }
178
+ }
179
+
180
+ function completeScheduleInventory(state, start) {
181
+ for (const schedule of state.schedules.slice(start)) {
182
+ for (const [queue, file] of state.created) if (!schedule.created.has(queue)) schedule.created.set(queue, file);
183
+ for (const [name, fn] of state.functions) if (!schedule.functions.has(name)) schedule.functions.set(name, fn);
184
+ }
185
+ }
186
+
187
+ function singleCall(tree) {
188
+ if (tree?.stmts?.length !== 1) return null;
189
+ if (tree.stmts[0].stmt?.ReturnStmt) return tree.stmts[0].stmt.ReturnStmt.returnval?.FuncCall ?? null;
190
+ const select = tree.stmts[0].stmt?.SelectStmt;
191
+ if (!select || select.targetList?.length !== 1 || !/^SETOP_NONE$/.test(select.op) || Object.keys(select).some((key) => !['targetList', 'op', 'limitOption'].includes(key))) return null;
192
+ return select.targetList[0].ResTarget?.val?.FuncCall ?? null;
193
+ }
194
+
195
+ function enqueue(state, call) {
196
+ if (callName(call) !== 'pgmq.send') return null;
197
+ const queue = sqlLiteral(argument(call, 0, ['queue_name']));
198
+ const safe = pureArguments(call);
199
+ return queue !== null && state.created.has(queue) && safe ? queue : null;
200
+ }
201
+
202
+ function pureArguments(call) {
203
+ let safe = callsOf(call).every((nested) => PURE_CALLS.has(callName(nested).replace(/^pg_catalog\./, '')));
204
+ walk(call.args, (node) => {
205
+ if (['SubLink', 'SelectStmt', 'InsertStmt', 'UpdateStmt', 'DeleteStmt', 'MergeStmt'].some((key) => node[key])) safe = false;
206
+ });
207
+ return safe;
208
+ }
209
+
210
+ function functionBody(fn) {
211
+ const option = (name) => (fn.options ?? []).find((item) => item.DefElem?.defname === name)?.DefElem?.arg;
212
+ return { body: strings(option('as')?.List?.items).join(''), language: option('language')?.String?.sval ?? 'sql' };
213
+ }
214
+
215
+ function inspectFunctionDeclarations(state, fn, file) {
216
+ const { body, language } = functionBody(fn);
217
+ if (language === 'sql') {
218
+ inspectCalls(state, sqlTree(body), file, false);
219
+ inspectCalls(state, fn.sql_body, file, false);
220
+ }
221
+ if (language === 'plpgsql') {
222
+ walk(plpgsqlTree(body, fn.parameters), (node) => {
223
+ if (node.PLpgSQL_expr?.parseMode === 0) inspectCalls(state, sqlTree(node.PLpgSQL_expr.query), file, false);
224
+ if (node.PLpgSQL_stmt_dynexecute) dynamicOwnership(state, sqlTree(`select ${node.PLpgSQL_stmt_dynexecute.query?.PLpgSQL_expr?.query ?? ''}`), file, false);
225
+ });
226
+ }
227
+ }
228
+
229
+ function standardSqlBody(node) {
230
+ if (!node) return [];
231
+ if (node.List) return node.List.items.flatMap(standardSqlBody);
232
+ return [{ stmts: [{ stmt: node }] }];
233
+ }
234
+
235
+ function plpgsqlTickTrees(fn, body) {
236
+ const tree = plpgsqlTree(body, fn.parameters);
237
+ if (!tree) return null;
238
+ const trees = [];
239
+ let safe = true;
240
+ walk(tree, (item) => {
241
+ for (const key of Object.keys(item)) {
242
+ if (key.startsWith('PLpgSQL_stmt_') && !['PLpgSQL_stmt_block', 'PLpgSQL_stmt_perform', 'PLpgSQL_stmt_return'].includes(key)) safe = false;
243
+ if (key === 'PLpgSQL_expr') trees.push(sqlTree(item[key].query));
244
+ }
245
+ });
246
+ return safe ? trees : null;
247
+ }
248
+
249
+ function tickTrees(fn) {
250
+ const { body, language } = functionBody(fn);
251
+ if (language === 'plpgsql') return plpgsqlTickTrees(fn, body);
252
+ if (language !== 'sql') return null;
253
+ if (fn.sql_body) return standardSqlBody(fn.sql_body);
254
+ return sqlTree(body)?.stmts?.map((entry) => ({ stmts: [entry] })) ?? null;
255
+ }
256
+
257
+ function tickQueues(state, fn) {
258
+ const trees = tickTrees(fn.node);
259
+ if (!trees) return null;
260
+ const queues = trees.map((tree) => singleCall(tree)).map((call) => call && enqueue(state, call));
261
+ return queues.length && queues.every(Boolean) ? queues : null;
262
+ }
263
+
264
+ function scheduleTarget(state, command) {
265
+ const target = singleCall(sqlTree(command));
266
+ if (!target) return { tick: null, queues: null };
267
+ if (!callName(target).endsWith('_tick')) return { tick: null, queues: enqueue(state, target) };
268
+ const tick = qualified(callName(target));
269
+ const fn = state.functions.get(tick);
270
+ const queues = fn && pureArguments(target) && tickQueues(state, fn);
271
+ return { tick, queues };
272
+ }
273
+
274
+ function tickRemoval(state, tick, name) {
275
+ if (!tick) return;
276
+ for (const drop of state.drops.filter((item) => item.name === tick)) {
277
+ if (!state.unschedules.some((item) => item.name === name && item.file === drop.file)) problem(state, DB_CRON_OWNED, drop.file, 'removing a scheduled tick requires cron.unschedule of its job name in the same migration');
278
+ }
279
+ }
280
+
281
+ function scheduleCheck(state, schedule, names, schedulers, ticks) {
282
+ const { call, file } = schedule;
283
+ const name = sqlLiteral(argument(call, 0, ['job_name']));
284
+ const command = sqlLiteral(argument(call, 2, ['command']));
285
+ if (name === null || command === null || sqlLiteral(argument(call, 1, ['schedule'])) === null) {
286
+ problem(state, DB_CRON_OWNED, file, 'cron.schedule requires literal job name, cadence and command for migration ownership');
287
+ return;
288
+ }
289
+ if (names.has(name)) problem(state, DB_CRON_OWNED, file, 'cron job names must be unique across active migrations');
290
+ names.add(name);
291
+ const historical = { ...state, created: schedule.created, functions: schedule.functions };
292
+ const { tick, queues } = scheduleTarget(historical, command);
293
+ if (!queues) problem(state, DB_CRON_OWNED, file, 'a cron command is one pgmq.send to an existing queue or one existing *_tick function that only enqueues; business DML, dynamic SQL and network calls are refused');
294
+ if (state.liveSchedules.get(name) === schedule && !scheduleTarget(state, command).queues) problem(state, DB_CRON_OWNED, file, 'the live cron job targets a missing queue or tick, or its current tick no longer only enqueues');
295
+ tickOwner(state, tick, file, ticks);
296
+ const owners = [name, ...(tick ? [tick, tick.split('.').at(-1)] : []), ...[queues ?? []].flat()];
297
+ if (owners.some((owner) => schedulers.has(owner))) problem(state, DB_CRON_OWNED, file, 'the tick is also declared in an application scheduler list; give it one owner');
298
+ tickRemoval(state, tick, name);
299
+ }
300
+
301
+ function tickOwner(state, tick, file, ticks) {
302
+ if (!tick) return;
303
+ if (ticks.has(tick)) problem(state, DB_CRON_OWNED, file, 'a tick function has more than one cron owner');
304
+ ticks.add(tick);
305
+ }
306
+
307
+ function queueState({ repoRoot, files = [], all = files, repo = {} }) {
308
+ const paths = [...new Set(all)];
309
+ const declaration = readJson(repoRoot, 'hfs.json') ?? repo;
310
+ const connected = ['be', 'fe'].some((side) => (declaration.sides?.[side]?.connections ?? []).some((item) => item.provider === 'supabase'));
311
+ if (repo.edition !== 'lite' && declaration.edition !== 'lite' && !declaration.supabase && !connected && !paths.some((file) => file.startsWith('supabase/'))) return null;
312
+ const state = { repoRoot, appRole: declaration.supabase?.appRole ?? 'app_be', findings: [], constants: new Map(), created: new Map(), functions: new Map(), schedules: [], activeCron: new Map(), liveSchedules: new Map(), unschedules: [], drops: [], grants: [], queueAcls: new Map() };
313
+ state.sources = queueSourceFacts(state, paths, inspectCalls);
314
+ migrationFacts(state, paths);
315
+ return state;
316
+ }
317
+
318
+ function validateQueues(state, inScope) {
319
+ for (const [queue, file] of state.constants) if (!state.created.has(queue)) problem(state, DB_PGMQ_DECLARED, file, 'a queue constant has no pgmq.create in an active migration');
320
+ for (const [queue, file] of state.created) if (!state.constants.has(queue)) problem(state, DB_PGMQ_DECLARED, file, 'pgmq.create has no corresponding queue constant under modules/queues');
321
+ for (const { node, file, created, executed } of state.grants) grantCheck({ ...state, created: executed ? created : state.created }, node, file);
322
+ const names = new Set();
323
+ const ticks = new Set();
324
+ const schedulers = queueSchedulers(state.sources);
325
+ for (const schedule of state.schedules) scheduleCheck(state, schedule, names, schedulers, ticks);
326
+ return state.findings.filter((finding) => inScope(finding.path));
327
+ }
328
+
329
+ /** Cross-file R258 findings. A full app participates when it declares or contains Supabase files. */
330
+ export function databaseQueuesFindings(context) {
331
+ // The app-root pass owns the cross-file inventory; a side pass cannot see its migrations.
332
+ if (context.appSide === true) return [];
333
+ const state = queueState(context);
334
+ return state ? validateQueues(state, context.inScope ?? (() => true)) : [];
335
+ }
336
+
337
+ /** Migration-owned operational metadata for a baseline; callers refuse a cutover when findings are present. */
338
+ export function databaseQueueDeclarations(context) {
339
+ const state = queueState(context);
340
+ if (!state) return { queues: [], cron: [], grants: [], findings: [] };
341
+ return {
342
+ queues: [...state.created.keys()].sort((left, right) => left.localeCompare(right)),
343
+ cron: [...state.activeCron.values()].sort((left, right) => left.name.localeCompare(right.name)),
344
+ grants: queueGrantDeclarations(state),
345
+ findings: validateQueues(state, () => true),
346
+ };
347
+ }
348
+
349
+ function queueGrantDeclarations(state) {
350
+ const grants = [];
351
+ for (const [table, acl] of state.queueAcls) {
352
+ if (!state.created.has(table.slice(7))) continue;
353
+ for (const grantOption of [false, true]) {
354
+ let privileges = [...acl].filter(([, option]) => option === grantOption).map(([privilege]) => privilege);
355
+ if (!privileges.length) continue;
356
+ if (TABLE_PRIVILEGES.every((privilege) => privileges.includes(privilege))) privileges = ['all'];
357
+ grants.push({ privileges, tables: [table], roles: [state.appRole], grantOption });
358
+ }
359
+ }
360
+ return grants;
361
+ }
@@ -62,18 +62,69 @@ function searchPathSchema(node, current) {
62
62
  function addTableFacts(statement, node, facts, defaultSchema) {
63
63
  const relation = statement.type === 'CreateStmt' ? node.relation : node.into?.rel;
64
64
  if (!relation || node.partbound || relation.relpersistence === 't') return;
65
+ if (node.objtype === ['OBJECT', 'MATVIEW'].join('_')) {
66
+ facts.views.push({ ...relationFact(statement, relation, defaultSchema), kind: 'materialized', securityInvoker: false, node });
67
+ return;
68
+ }
65
69
  facts.tables.push({ schema: schemaOf(relation, defaultSchema), name: relation.relname, index: statement.index, line: statement.line });
66
70
  }
67
71
 
72
+ const relationFact = (statement, relation, defaultSchema) => ({
73
+ schema: schemaOf(relation, defaultSchema), name: relation?.relname, index: statement.index, line: statement.line,
74
+ });
75
+
76
+ const rlsCommands = new Map([
77
+ ['AT_EnableRowSecurity', { enabled: true }], ['AT_DisableRowSecurity', { enabled: false }],
78
+ ['AT_ForceRowSecurity', { forced: true }], ['AT_NoForceRowSecurity', { forced: false }],
79
+ ]);
80
+
68
81
  function addAlterTableFacts(statement, node, facts, defaultSchema) {
69
82
  for (const command of node.cmds ?? []) {
70
- const subtype = command?.AlterTableCmd?.subtype;
71
- const item = { schema: schemaOf(node.relation, defaultSchema), name: node.relation?.relname, index: statement.index };
83
+ const alteration = command?.AlterTableCmd;
84
+ const subtype = alteration?.subtype;
85
+ const item = relationFact(statement, node.relation, defaultSchema);
72
86
  if (subtype === 'AT_EnableRowSecurity') facts.enables.push(item);
73
87
  else if (subtype === 'AT_ForceRowSecurity') facts.forces.push(item);
88
+ const event = rlsCommands.get(subtype);
89
+ if (event) facts.rlsEvents.push({ ...item, ...event });
90
+ if (subtype === 'AT_ChangeOwner') facts.owners.push({ ...item, role: alteration.newowner?.rolename });
74
91
  }
75
92
  }
76
93
 
94
+ function addViewFacts(statement, node, facts, defaultSchema) {
95
+ const security = (node.options ?? []).find((option) => option?.DefElem?.defname === 'security_invoker')?.DefElem?.arg;
96
+ const securityInvoker = security?.Boolean?.boolval === true || security?.String?.sval === 'true';
97
+ facts.views.push({ ...relationFact(statement, node.view, defaultSchema), kind: 'view', securityInvoker, node });
98
+ }
99
+
100
+ function addSchemaMoveFacts(statement, node, facts, defaultSchema) {
101
+ if (!node.relation) return;
102
+ const item = relationFact(statement, node.relation, defaultSchema);
103
+ facts.schemaMoves.push({ ...item, fromSchema: item.schema, schema: node.newschema, objtype: node.objectType });
104
+ }
105
+
106
+ function addRoleFacts(statement, node, facts) {
107
+ const kind = statement.type === 'CreateRoleStmt' ? 'create' : 'alter';
108
+ const name = typeof node.role === 'string' ? node.role : node.role?.rolename;
109
+ facts.roles.push({ kind, name, options: node.options ?? [], node, index: statement.index, line: statement.line });
110
+ }
111
+
112
+ function addDropRoleFacts(statement, node, facts) {
113
+ for (const role of node.roles ?? []) facts.roles.push({ kind: 'drop', name: roleOf(role), options: [], node, index: statement.index, line: statement.line });
114
+ }
115
+
116
+ function addRoleGrantFacts(statement, node, facts) {
117
+ facts.roleGrants.push({ node, index: statement.index, line: statement.line });
118
+ }
119
+
120
+ function addPublicationFacts(statement, node, facts, defaultSchema) {
121
+ const tables = (node.pubobjects ?? []).flatMap((object) => {
122
+ const relation = object?.PublicationObjSpec?.pubtable?.relation;
123
+ return relation ? [{ schema: schemaOf(relation, defaultSchema), name: relation.relname }] : [];
124
+ });
125
+ facts.publications.push({ name: node.pubname, action: node.action, allTables: node.for_all_tables === true, tables, node, index: statement.index, line: statement.line });
126
+ }
127
+
77
128
  function addCreatePolicyFacts(statement, node, facts, defaultSchema) {
78
129
  const policy = {
79
130
  name: node.policy_name,
@@ -104,6 +155,7 @@ function addAlterPolicyFacts(statement, node, facts, defaultSchema) {
104
155
  ...(node.qual !== undefined ? { qual: node.qual } : {}),
105
156
  ...(node.with_check !== undefined ? { withCheck: node.with_check } : {}),
106
157
  index: statement.index,
158
+ line: statement.line,
107
159
  });
108
160
  }
109
161
 
@@ -123,6 +175,7 @@ function addGrantFacts(statement, node, facts, defaultSchema) {
123
175
  objtype: node.objtype,
124
176
  objects: node.objects ?? [],
125
177
  privileges: (node.privileges ?? []).map((privilege) => String(privilege?.AccessPriv?.priv_name ?? '').toLowerCase()),
178
+ all: (node.privileges ?? []).length === 0,
126
179
  grantees: (node.grantees ?? []).map(roleOf),
127
180
  defaultSchema,
128
181
  index: statement.index,
@@ -130,13 +183,17 @@ function addGrantFacts(statement, node, facts, defaultSchema) {
130
183
  });
131
184
  }
132
185
 
133
- function addDefaultPrivilegeFacts(statement, node, facts) {
186
+ function addDefaultPrivilegeFacts(statement, node, facts, defaultSchema) {
134
187
  const action = node.action ?? {};
188
+ const schemas = (node.options ?? []).find((option) => option?.DefElem?.defname === 'schemas')?.DefElem?.arg?.List?.items;
135
189
  facts.defaultPrivileges.push({
136
190
  grant: action.is_grant === true,
137
191
  objtype: action.objtype,
138
192
  privileges: (action.privileges ?? []).map((privilege) => String(privilege?.AccessPriv?.priv_name ?? '').toLowerCase()),
193
+ all: (action.privileges ?? []).length === 0,
139
194
  grantees: (action.grantees ?? []).map(roleOf),
195
+ schemas: (schemas ?? []).map(sval).filter(Boolean),
196
+ defaultSchema,
140
197
  index: statement.index,
141
198
  line: statement.line,
142
199
  });
@@ -151,7 +208,8 @@ function addCreateFunctionFacts(statement, node, facts, defaultSchema) {
151
208
  function addAlterFunctionFacts(statement, node, facts, defaultSchema) {
152
209
  const rawName = nameOf(node.func?.objname);
153
210
  const name = rawName.includes('.') ? rawName : `${defaultSchema}.${rawName}`;
154
- facts.alterFunctions.push({ name, setsPath: (node.actions ?? []).some((action) => isSearchPathSet(action?.DefElem)), index: statement.index, line: statement.line });
211
+ const securityDefiner = (node.actions ?? []).some((action) => action?.DefElem?.defname === 'security' && action?.DefElem?.arg?.Boolean?.boolval === true);
212
+ facts.alterFunctions.push({ name, schema: name.split('.')[0], node, securityDefiner, setsPath: (node.actions ?? []).some((action) => isSearchPathSet(action?.DefElem)), index: statement.index, line: statement.line });
155
213
  }
156
214
 
157
215
  function addDoBlockFacts(statement, node, facts) {
@@ -173,6 +231,9 @@ function addBucketCallFacts(statement, node, facts) {
173
231
  const statementFactHandlers = new Map([
174
232
  ['VariableSetStmt', (statement, node, facts, defaultSchema) => searchPathSchema(node, defaultSchema)],
175
233
  ['CreateStmt', addTableFacts], ['CreateTableAsStmt', addTableFacts], ['AlterTableStmt', addAlterTableFacts],
234
+ ['ViewStmt', addViewFacts], ['AlterObjectSchemaStmt', addSchemaMoveFacts],
235
+ ['CreateRoleStmt', addRoleFacts], ['AlterRoleStmt', addRoleFacts], ['DropRoleStmt', addDropRoleFacts],
236
+ ['GrantRoleStmt', addRoleGrantFacts], ['CreatePublicationStmt', addPublicationFacts], ['AlterPublicationStmt', addPublicationFacts],
176
237
  ['CreatePolicyStmt', addCreatePolicyFacts], ['AlterPolicyStmt', addAlterPolicyFacts], ['DropStmt', addDropPolicyFacts],
177
238
  ['GrantStmt', addGrantFacts], ['AlterDefaultPrivilegesStmt', addDefaultPrivilegeFacts], ['CreateFunctionStmt', addCreateFunctionFacts],
178
239
  ['AlterFunctionStmt', addAlterFunctionFacts], ['DoStmt', addDoBlockFacts], ['InsertStmt', addBucketInsertFacts], ['SelectStmt', addBucketCallFacts],
@@ -183,9 +244,11 @@ function factsOf(statements) {
183
244
  const facts = {
184
245
  tables: [], enables: [], forces: [], policies: [], policyEvents: [], grants: [], defaultPrivileges: [],
185
246
  functions: [], alterFunctions: [], doBlocks: [], buckets: [], bucketCalls: [], isSearchPathSet,
247
+ statements: [], views: [], rlsEvents: [], owners: [], schemaMoves: [], roles: [], roleGrants: [], publications: [],
186
248
  };
187
249
  let defaultSchema = 'public';
188
250
  for (const statement of statements) {
251
+ facts.statements.push({ ...statement, defaultSchema });
189
252
  const { node } = statement;
190
253
  const handler = statementFactHandlers.get(statement.type);
191
254
  if (handler) defaultSchema = handler(statement, node, facts, defaultSchema) ?? defaultSchema;
@@ -193,18 +256,37 @@ function factsOf(statements) {
193
256
  return facts;
194
257
  }
195
258
 
259
+ function relationHasLaterEvent(events, table) {
260
+ return events.some((event) => event.schema === table.schema && event.name === table.name && event.index > table.index);
261
+ }
262
+
263
+ function rlsWeakeningFindings(file, events, exposed) {
264
+ const findings = [];
265
+ for (const event of events) {
266
+ if (!exposed.has(event.schema) || (event.enabled !== false && event.forced !== false)) continue;
267
+ const qualified = `${event.schema}.${event.name}`;
268
+ const operation = event.enabled === false ? 'disables row level security' : 'removes forced row level security';
269
+ findings.push(found(DB_RLS_REQUIRED, file, `${file}:${event.line} ${operation} on ${qualified} in the exposed schema ${event.schema}`, { line: event.line, table: qualified }));
270
+ }
271
+ return findings;
272
+ }
273
+
196
274
  function rlsFindings(file, facts, exposed, forceRls) {
275
+ const movedTables = facts.schemaMoves.filter((move) => move.objtype === 'OBJECT_TABLE').map((move) => ({ ...move, moved: true }));
276
+ const targets = [...facts.tables, ...movedTables];
197
277
  const findings = [];
198
- for (const table of facts.tables) {
278
+ for (const table of targets) {
199
279
  if (!exposed.has(table.schema)) continue;
200
280
  const qualified = `${table.schema}.${table.name}`;
201
- if (!facts.enables.some((enable) => enable.schema === table.schema && enable.name === table.name && enable.index > table.index)) {
202
- findings.push(found(DB_RLS_REQUIRED, file, `${file}:${table.line} creates ${qualified} in the exposed schema ${table.schema} without a later \`alter table ${qualified} enable row level security\` in the same migration`, { line: table.line, table: qualified }));
281
+ const operation = table.moved ? 'moves' : 'creates';
282
+ if (!relationHasLaterEvent(facts.enables, table)) {
283
+ findings.push(found(DB_RLS_REQUIRED, file, `${file}:${table.line} ${operation} ${qualified} into the exposed schema ${table.schema} without a later \`alter table ${qualified} enable row level security\` in the same migration`, { line: table.line, table: qualified }));
203
284
  }
204
- if (forceRls.has(qualified) && !facts.forces.some((force) => force.schema === table.schema && force.name === table.name && force.index > table.index)) {
205
- findings.push(found(DB_RLS_REQUIRED, file, `${file}:${table.line} creates ${qualified}, which hfs.json supabase.forceRls declares definer-owned, without \`alter table ${qualified} force row level security\` in the same migration`, { line: table.line, table: qualified }));
285
+ if (forceRls.has(qualified) && !relationHasLaterEvent(facts.forces, table)) {
286
+ findings.push(found(DB_RLS_REQUIRED, file, `${file}:${table.line} ${operation} ${qualified}, which hfs.json supabase.forceRls declares definer-owned, without \`alter table ${qualified} force row level security\` in the same migration`, { line: table.line, table: qualified }));
206
287
  }
207
288
  }
289
+ findings.push(...rlsWeakeningFindings(file, facts.rlsEvents, exposed));
208
290
  return findings;
209
291
  }
210
292
 
@@ -4,11 +4,18 @@ import { found, readJson, readText } from './read.mjs';
4
4
  import { configFindings, parseToml, typesFindings } from './database-config.mjs';
5
5
  import { defaultGit, migrationShapeFindings } from './database-migrations.mjs';
6
6
  import { migrationSetPolicyFindings, sqlAnalysis } from './database-sql.mjs';
7
+ import { migrationSetDefinerFindings } from './database-plpgsql.mjs';
8
+ import { databaseAccessFindings } from './database-access.mjs';
7
9
  import { CONFIG_FILE, DB_CONFIG_POLICY, DB_MIGRATION_SHAPE, isActiveMigration, MIGRATIONS_ARCHIVE_DIR, TYPES_FILE } from './database-constants.mjs';
8
10
  import { byCodeUnit } from '../../lib/list.mjs';
9
11
  import { eachInOrder } from '../../lib/in-order.mjs';
10
12
 
11
13
  export { migrationStamp } from './database-migrations.mjs';
14
+ export {
15
+ DB_BE_ONLY_ACCESS, DB_REALTIME_UNLISTED, DB_APP_ROLE, DB_FORCE_RLS_DEFAULT,
16
+ DB_POLICY_TAUTOLOGY, DB_VIEW_UNSAFE, DB_STORAGE_PREFIX,
17
+ } from './database-access.mjs';
18
+ export const DB_DEFINER_REVOKE = 'DB_DEFINER_REVOKE';
12
19
  export {
13
20
  CONFIG_FILE,
14
21
  DB_CONFIG_POLICY,
@@ -76,6 +83,8 @@ export async function checkDatabase({ repoRoot, files, base, git, edition, supab
76
83
  const analyses = await migrationAnalyses({ repoRoot, migrations, exposed, forceRls, findings });
77
84
  findings.push(
78
85
  ...migrationSetPolicyFindings(analyses),
86
+ ...migrationSetDefinerFindings(analyses, exposed),
87
+ ...await databaseAccessFindings({ analyses, exposed, supabase: declared }),
79
88
  ...await typesFindings({ repoRoot, files, emitTypes, declaration: declared }),
80
89
  );
81
90
  return findings;