@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
@@ -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,
@@ -51,7 +58,7 @@ async function migrationAnalyses({ repoRoot, migrations, exposed, forceRls, find
51
58
  * starts neither Docker nor the Supabase CLI. A full app is judged when it declares a Supabase connection, while
52
59
  * lite apps are judged even before their initial Supabase files have all been scaffolded.
53
60
  */
54
- export async function checkDatabase({ repoRoot, files, base, git, edition, supabase, emitTypes, now = () => Date.now() } = {}) {
61
+ export async function checkDatabase({ repoRoot, files, base, git, edition, supabase, emitTypes } = {}) {
55
62
  const findings = [];
56
63
  const migrations = files.filter(isActiveMigration).sort(byCodeUnit);
57
64
  const archived = files.filter((file) => file.startsWith(`${MIGRATIONS_ARCHIVE_DIR}/`));
@@ -71,11 +78,13 @@ export async function checkDatabase({ repoRoot, files, base, git, edition, supab
71
78
  const apiSchemas = (config?.api?.schemas ?? []).filter((schema) => typeof schema === 'string');
72
79
  const exposed = new Set(['public', ...apiSchemas]);
73
80
  const forceRls = new Set((declared?.forceRls ?? []).map((name) => (name.includes('.') ? name : `public.${name}`)));
74
- findings.push(...await migrationShapeFindings({ repoRoot, migrations, archived, git: git ?? defaultGit, base, now }));
81
+ findings.push(...await migrationShapeFindings({ repoRoot, migrations, archived, git: git ?? defaultGit, base }));
75
82
 
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;
@@ -0,0 +1,192 @@
1
+ // R250: host scheduling and application logic do not belong in deployment sources; compose mutations stay in dev.
2
+ import { parseWorkflow } from '../runtime-rules/workflow-source.mjs';
3
+ import { words } from '../../lib/dockerfile.mjs';
4
+ import { found, readText } from './read.mjs';
5
+
6
+ const WORKFLOW = /(?:^|\/)\.github\/workflows\/[^/]+\.ya?ml$/;
7
+ const DEPLOY = /(?:^|\/)deploy\//;
8
+ const SCRIPT = /(?:^|\/)scripts\//;
9
+ const COMPOSE = /(?:^|\/)(?:docker-)?compose(?:[.-][^/]*)?\.ya?ml$/;
10
+ const DOCKERFILE = /(?:^|\/)Dockerfile(?:\.[^/]*)?$/;
11
+ const CREDENTIAL_FILE = /(?:^|\/)(?:secrets?\.env(?:\.[^/]*)?|\.env(?:\.[^/]*)?|secrets\/)|\.enc$/i;
12
+ const HOST_CRON = /\b(?:crontab|anacron|crond|supercronic)\b|\/etc\/cron(?:\b|[./])|\bcron\.d\b|\/var\/spool\/cron\b|@(?:reboot|yearly|annually|monthly|weekly|daily|midnight|hourly)\b|\b[\w.@-]+\.timer\b|\bOn(?:Calendar|BootSec|UnitActiveSec|ActiveSec)\s*=|(?:^|[\s;|&"'])at\s+(?:(?:-[a-z]+\s+)*)((?:-[ft]\b)|now\b|tomorrow\b|\d{1,2}:\d{2}\b|midnight\b|noon\b)/im;
13
+ const HOST_CRON_PATH = /(?:^|\/)(?:crontab(?:$|\/)|cron\.d(?:\/|$)|[^/]+\.(?:timer|crontab)$)/;
14
+ const CRON_NUMERIC_FIELD = /^[\d*,/-]+$/;
15
+ const CRON_NAMED_FIELD = /^[\w*,/-]+$/;
16
+ const CRON_CONTAINER = /\bimage\s*:\s*["']?(?:[^\s"']*\/)?(?:cron|crond|supercronic|ofelia)(?=[:@\s"']|$)|\b(?:command|entrypoint)\s*:[^\n]*\b(?:cron|crond|supercronic)\b/i;
17
+ const SLEEP_LOOP = /\b(?:while\s+(?:true|:|\[\s*1\s*\])|for\s*\(\(\s*;\s*;\s*\)\))\s*[;\n][\s\S]{0,4000}?\bsleep\s+/i;
18
+ const INLINE_NODE = /\bnode\b[^\n;&|]*?\s(?:-(?:e|p|pe)\b|--(?:eval|print)(?:\s|=))/;
19
+ const INLINE_SQL_ACTION = /\b(?:insert\s+into|update\s+[\w."']+\s+set|delete\s+from|create\s+(?:table|function|view)|alter\s+table|truncate\s+(?:table\s+)?[\w."'])\b/i;
20
+ const REMOTE_SHELL = /\b(?:curl|wget)\b[^|\n]*\|\s*(?:sudo\s+)?(?:ba|da|z)?sh\b/i;
21
+ const COMPOSE_COMMAND = /\bdocker(?:\s+(?:--(?:context|host)(?:\s+|=)\S+|-H\s+\S+))*\s+compose\b[^\n;&|]*|\bdocker-compose\b[^\n;&|]*/g;
22
+ const COMPOSE_OPTIONS = new Set(['-f', '--file', '-p', '--project-name', '--project-directory', '--env-file', '--profile', '--parallel', '--ansi', '--progress']);
23
+ const DEV = /(?:^|[\s/._="'-])(?:dev|development|local)(?=$|[\s/._"'-])/i;
24
+ const NON_DEV = /(?:^|[\s/._="'-])(?:prod|production|staging|stage|vps|preview)(?=$|[\s/._"'-])/i;
25
+ const WORKFLOW_ENV_NAMES = ['DEPLOY_ENV', 'NODE_ENV', 'ENVIRONMENT', 'STARCI_ENV'];
26
+
27
+ /** Comments carry no executable scheduling; preserve strings so embedded SSH commands remain visible. */
28
+ function sourceText(text) {
29
+ return text.split('\n').map((line) => {
30
+ const trimmed = line.trimStart();
31
+ return trimmed.startsWith('#') || trimmed.startsWith('//') ? '' : line;
32
+ }).join('\n').replaceAll(/\\\r?\n\s*/g, ' ');
33
+ }
34
+
35
+ function cronTable(text) {
36
+ return text.split('\n').some((line) => {
37
+ const fields = line.trimStart().replace(/^["']/, '').trimEnd().split(/\s+/);
38
+ return fields.length >= 6
39
+ && fields.slice(0, 3).every((field) => CRON_NUMERIC_FIELD.test(field))
40
+ && fields.slice(3, 5).every((field) => CRON_NAMED_FIELD.test(field));
41
+ });
42
+ }
43
+
44
+ function firstWordOffsets(text) {
45
+ const offsets = new Set();
46
+ let offset = 0;
47
+ for (const line of text.split(/[\r\n\u2028\u2029]/)) {
48
+ offsets.add(offset + line.length - line.trimStart().length);
49
+ offset += line.length + 1;
50
+ }
51
+ return offsets;
52
+ }
53
+
54
+ function inlineSql(text) {
55
+ if (INLINE_SQL_ACTION.test(text)) return true;
56
+ const offsets = firstWordOffsets(text);
57
+ for (const match of text.matchAll(/select/gi)) {
58
+ const preceding = text[match.index - 1];
59
+ const startsSql = offsets.has(match.index) || preceding === '"' || preceding === "'";
60
+ if (startsSql && /^\s+[^\n;]/.test(text.slice(match.index + match[0].length))) return true;
61
+ }
62
+ return false;
63
+ }
64
+
65
+ function deploymentSource(file) {
66
+ if (CREDENTIAL_FILE.test(file) || /\.(?:md|png|jpe?g|svg|pdf|zip|gz)$/i.test(file)) return false;
67
+ return DEPLOY.test(file) || SCRIPT.test(file) || WORKFLOW.test(file) || COMPOSE.test(file) || DOCKERFILE.test(file);
68
+ }
69
+
70
+ function stackEnvironments(text) {
71
+ return [...text.matchAll(/\.starcistacks\/([^/\s"']+)\//g)].map((match) => match[1]);
72
+ }
73
+
74
+ function composeTargets(command) {
75
+ const parts = words(command);
76
+ const targets = [];
77
+ for (let i = 0; i < parts.length; i += 1) {
78
+ if (['-f', '--file', '--env-file', '--project-directory'].includes(parts[i])) {
79
+ i += 1;
80
+ targets.push(parts[i] ?? '');
81
+ }
82
+ else if (/^--(?:file|env-file|project-directory)=/.test(parts[i])) targets.push(parts[i].slice(parts[i].indexOf('=') + 1));
83
+ else if (/^-f.+/.test(parts[i])) targets.push(parts[i].slice(2));
84
+ }
85
+ return targets;
86
+ }
87
+
88
+ function targetsNonDev(command, { environment, directory, remote }) {
89
+ if (remote || /--context(?:=|\s+)(?:prod(?:uction)?|staging|vps)\b|--host(?:=|\s+)ssh:\/\//i.test(command)) return true;
90
+ const targets = composeTargets(command);
91
+ if (targets.length) return targets.some((target) => {
92
+ const environments = stackEnvironments(target);
93
+ return environments.length ? environments.some((name) => !/^(?:dev|development|local)$/i.test(name)) : !DEV.test(target) || NON_DEV.test(target);
94
+ });
95
+ if (NON_DEV.test(environment) || NON_DEV.test(directory)) return true;
96
+ if (DEV.test(environment) || DEV.test(directory)) return false;
97
+ return true;
98
+ }
99
+
100
+ function composeMutation(command) {
101
+ const parts = words(command);
102
+ const composeAt = parts.findIndex((part) => part === 'compose' || part === 'docker-compose');
103
+ const arguments_ = parts.slice(composeAt + 1);
104
+ for (let i = 0; i < arguments_.length; i += 1) {
105
+ const argument = arguments_[i];
106
+ if (COMPOSE_OPTIONS.has(argument)) { i += 1; continue; }
107
+ if (argument.startsWith('-')) continue;
108
+ return ['up', 'down', 'pull'].includes(argument);
109
+ }
110
+ return false;
111
+ }
112
+
113
+ /** Job-local context prevents a production job from changing how a separate dev compose step is judged. */
114
+ function workflowCommands(doc) {
115
+ const entries = [];
116
+ for (const job of Object.values(doc?.jobs ?? {})) {
117
+ const environment = typeof job?.environment === 'string' ? job.environment : job?.environment?.name ?? '';
118
+ const environmentNames = WORKFLOW_ENV_NAMES.map((key) => job?.env?.[key] ?? doc?.env?.[key] ?? '').join(' ');
119
+ const context = { environment: `${environment} ${environmentNames}` };
120
+ for (const step of Array.isArray(job?.steps) ? job.steps : []) {
121
+ const directory = step?.['working-directory'] ?? job?.defaults?.run?.['working-directory'] ?? doc?.defaults?.run?.['working-directory'] ?? '';
122
+ const stepEnvironments = WORKFLOW_ENV_NAMES.map((key) => step?.env?.[key] ?? '').join(' ');
123
+ for (const source of [step?.run, step?.with?.script]) {
124
+ if (typeof source === 'string') entries.push({ source: sourceText(source), context: { ...context, environment: `${context.environment} ${stepEnvironments}`, directory, remote: Boolean(step?.with?.host && !/^(?:localhost|127\.0\.0\.1)$/.test(step.with.host)) } });
125
+ }
126
+ }
127
+ }
128
+ return entries;
129
+ }
130
+
131
+ function composeOnVps(file, text, doc) {
132
+ let entries = doc ? workflowCommands(doc) : [{ source: text, context: { environment: '', directory: '' } }];
133
+ if (file.endsWith('/package.json')) {
134
+ try { entries = Object.values(JSON.parse(text)?.scripts ?? {}).filter((command) => typeof command === 'string').map((source) => ({ source, context: { environment: '', directory: '' } })); } catch { /* Invalid JSON is judged by its own rule. */ }
135
+ }
136
+ return entries.some(({ source, context }) => [...source.matchAll(COMPOSE_COMMAND)].some((match) => {
137
+ const before = source.slice(0, match.index);
138
+ const linePrefix = before.slice(before.lastIndexOf('\n') + 1);
139
+ const directory = workingDirectory(before) ?? context.directory;
140
+ const remote = context.remote || (/\bssh\b/.test(linePrefix) && !/\b(?:localhost|127\.0\.0\.1)\b/.test(linePrefix));
141
+ return composeMutation(match[0]) && targetsNonDev(match[0], { ...context, directory, remote });
142
+ }));
143
+ }
144
+
145
+ function workingDirectory(source) {
146
+ let directory;
147
+ for (const command of source.split(/[\n;&]/)) {
148
+ const match = /^cd\s+([\w./-]+)/.exec(command.trimStart());
149
+ if (match) directory = match[1];
150
+ }
151
+ return directory;
152
+ }
153
+
154
+ function workflowSchedule(doc, text) {
155
+ return Boolean(doc?.on && typeof doc.on === 'object' && Object.hasOwn(doc.on, 'schedule'))
156
+ || (!doc && text.split('\n').some((line) => /^schedule\s*:/.test(line.trimStart())));
157
+ }
158
+
159
+ function deploymentLogic(file, text) {
160
+ return DEPLOY.test(file) && /\.(?:sh|bash)$/.test(file)
161
+ && (INLINE_NODE.test(text) || /\bpsql\b/.test(text) || inlineSql(text) || REMOTE_SHELL.test(text));
162
+ }
163
+
164
+ function sourceFindings(file, text, doc) {
165
+ const findings = [];
166
+ const cronContainer = CRON_CONTAINER.test(text);
167
+ const host = HOST_CRON.test(text) || HOST_CRON_PATH.test(file) || cronTable(text) || cronContainer || /\b(?:service\s+cron|cron\s+-[a-z]|cron\s*$)/im.test(text);
168
+ if (host) findings.push(found('HFS_HOST_CRON', file, 'Host cron, systemd timers and at jobs are forbidden; use an app-owned scheduler or a declared replicated-job'));
169
+ if ((WORKFLOW.test(file) && workflowSchedule(doc, text)) || (DEPLOY.test(file) && host) || cronContainer || SLEEP_LOOP.test(text)) {
170
+ findings.push(found('HFS_DEPLOY_SCHEDULER', file, 'Deployment sources must not own recurring schedules; move scheduled work into the app and declare its job'));
171
+ }
172
+ if (deploymentLogic(file, text)) {
173
+ findings.push(found('HFS_DEPLOY_NO_LOGIC', file, 'Deployment shell scripts must delegate to docker, starci or the built cli app; move inline application or database logic into the cli app'));
174
+ }
175
+ if (composeOnVps(file, text, doc)) findings.push(found('HFS_DEPLOY_COMPOSE_ON_VPS', file, 'Compose up, down and pull are dev-only; deploy a non-dev environment through its declared Swarm or Kubernetes runtime'));
176
+ return findings;
177
+ }
178
+
179
+ /** Findings are names and paths only: deployment source snippets and credential values are never reported. */
180
+ export function deployScanFindings({ repoRoot, files, all, repo, isRoot, inScope = () => true }) {
181
+ if (repo?.kind !== 'app' || !['lite', 'full'].includes(repo.edition) || isRoot === false) return [];
182
+ const findings = [];
183
+ for (const file of new Set(all ?? files)) {
184
+ if (!inScope(file) || !deploymentSource(file)) continue;
185
+ const raw = readText(repoRoot, file);
186
+ if (raw === null) continue;
187
+ const text = sourceText(raw);
188
+ const doc = WORKFLOW.test(file) ? parseWorkflow(raw) : null;
189
+ findings.push(...sourceFindings(file, text, doc));
190
+ }
191
+ return findings;
192
+ }
@@ -0,0 +1,219 @@
1
+ // R251: product workflow custody, immutable dependencies and release-only triggers, in both editions.
2
+ import { parseWorkflow, workflowFindings } from '../runtime-rules/workflow-source.mjs';
3
+ import { found, readText } from './read.mjs';
4
+ import { isPlainObject as record } from '../../../engine/plain-object.mjs';
5
+ import { workflowEventOptions } from '../../lib/workflow-events.mjs';
6
+
7
+ const DEPLOY = 'HFS_DEPLOY_WORKFLOW';
8
+ const PIN = 'HFS_WORKFLOW_PIN';
9
+ const TOOL = 'HFS_CI_TOOL_INSTALL';
10
+ const TRIGGER = 'HFS_APP_WORKFLOW_TRIGGER';
11
+ // Only the age identity and SSH transport credentials belong in the CI secret store.
12
+ const CI_SECRET_ENV_NAMES = new Set(['SOPS_AGE_KEY', 'SSH_KEY', 'SSH_PRIVATE_KEY', 'SSH_KNOWN_HOSTS', 'VPS_SSH_KEY', 'VPS_SSH_PRIVATE_KEY', 'VPS_KNOWN_HOSTS', 'VPS_SSH_KNOWN_HOSTS', 'DEPLOY_SSH_KEY', 'DEPLOY_SSH_PRIVATE_KEY', 'DEPLOY_KNOWN_HOSTS', 'DEPLOY_SSH_KNOWN_HOSTS']);
13
+ const nonempty = (value) => typeof value === 'string' && value.trim().length > 0;
14
+ function executable(script) {
15
+ let result = '';
16
+ let quote = '';
17
+ for (let index = 0; index < script.length; index++) {
18
+ const character = script[index];
19
+ if (character === '\\' && quote !== "'") { result += character + (script[++index] ?? ''); continue; }
20
+ if (!quote && character === '#' && (index === 0 || /[\s;&|()]/.test(script[index - 1]))) {
21
+ while (index < script.length && script[index] !== '\n') index++;
22
+ result += '\n';
23
+ continue;
24
+ }
25
+ if (character === quote) quote = '';
26
+ else if (!quote && (character === "'" || character === '"')) quote = character;
27
+ result += character;
28
+ }
29
+ return result.replace(/\\\r?\n/g, ' ');
30
+ }
31
+
32
+ function commands(script) {
33
+ const result = [];
34
+ let quote = '';
35
+ let start = 0;
36
+ for (let index = 0; index < script.length; index++) {
37
+ const character = script[index];
38
+ if (character === '\\' && quote !== "'") { index++; continue; }
39
+ if (character === quote) quote = '';
40
+ else if (!quote && (character === "'" || character === '"')) quote = character;
41
+ else if (!quote && /[\n;&|]/.test(character)) { result.push({ text: script.slice(start, index).trim(), start, end: index, separator: character }); start = index + 1; }
42
+ }
43
+ result.push({ text: script.slice(start).trim(), start });
44
+ return result.filter((command) => command.text);
45
+ }
46
+
47
+ const commandBody = (command) => command.replace(/^(?:(?:sudo|env)\s+|[A-Za-z_]\w*=[^\s]+\s+)*/, '');
48
+
49
+ function entries(value) {
50
+ if (!value || typeof value !== 'object') return [];
51
+ return Object.entries(value).flatMap(([key, member]) => [{ key, value: member }, ...entries(member)]);
52
+ }
53
+
54
+ const strings = (value) => entries(value).filter((entry) => typeof entry.value === 'string').map((entry) => entry.value);
55
+ const activeSteps = (job) => (Array.isArray(job.steps) ? job.steps : []).filter((step) => record(step) && step.if !== false && step.if !== 'false' && !/^\$\{\{\s*false\s*\}\}$/.test(String(step.if ?? '')));
56
+ const scripts = (job) => activeSteps(job).flatMap((step) => [step.run, step.with?.script].filter(nonempty).map(executable));
57
+ const validEnvironment = (value) => nonempty(value) || (record(value) && nonempty(value.name));
58
+ const validPermissions = (value) => record(value) || value === 'read-all' || value === 'write-all';
59
+ const validConcurrency = (value) => nonempty(value) || (record(value) && nonempty(value.group));
60
+ const deployName = (value) => !/authorize[\s_.-]+deploy[\s_.-]+key/i.test(value) && /(?:^|[\s/_.-])deploy(?:[\s/_.-]|$)/i.test(value);
61
+ const materializeCommand = (script) => commands(script).some(({ text }) => /^starci\s+app\s+secret\s+materialize\b/.test(commandBody(text)));
62
+ const deploymentCommand = (script) => commands(script).some(({ text }) => /^(?:starci\s+(?:app\s+)?(?:deploy|secret\s+materialize)\b|docker\s+stack\s+deploy\b|(?:ba?sh\s+)?(?:\.\/)?deploy\/|(?:ba?sh\s+)?(?:\.\/)?(?:scripts\/)?[\w.-]*deploy[\w.-]*\.sh\b|npm\s+run\s+deploy(?=[:\s]|$))/.test(commandBody(text)));
63
+
64
+ function hasAgeIdentity(job, doc) {
65
+ const steps = activeSteps(job);
66
+ if ([doc.env, job.env, ...steps.map((step) => step.env)].some((env) => record(env) && nonempty(env.SOPS_AGE_KEY))) return true;
67
+ const reference = /\$\{\{\s*secrets\s*(?:\.\s*SOPS_AGE_KEY|\[\s*(['"])SOPS_AGE_KEY\1\s*\])/;
68
+ return steps.some((step) => strings(step.with).some((value) => reference.test(value)) || (typeof step.run === 'string' && reference.test(executable(step.run))));
69
+ }
70
+
71
+ function triggerFindings(doc, refuse) {
72
+ const on = workflowEventOptions(doc.on);
73
+ if (!record(on) || !Object.keys(on).length) return [refuse(TRIGGER, 'declare release tags [v*] and/or workflow_dispatch')];
74
+ const findings = [];
75
+ for (const [event, options] of Object.entries(on)) {
76
+ if (event === 'workflow_dispatch') continue;
77
+ if (event !== 'push') {
78
+ findings.push(refuse(TRIGGER, `trigger ${event} is forbidden; app CI starts only on release tags [v*] or workflow_dispatch`));
79
+ continue;
80
+ }
81
+ if (!record(options) || !Array.isArray(options.tags) || options.tags.length !== 1 || options.tags[0] !== 'v*' || Object.keys(options).some((key) => key !== 'tags')) {
82
+ findings.push(refuse(TRIGGER, 'push must contain only tags: [v*]; branch and path filters are forbidden'));
83
+ }
84
+ }
85
+ return findings;
86
+ }
87
+
88
+ function actionFindings(doc, refuse) {
89
+ const findings = [];
90
+ for (const { key, value } of entries(doc)) {
91
+ if (key !== 'uses') continue;
92
+ const local = typeof value === 'string' && /^\.\/[\w./-]+$/.test(value);
93
+ const remote = typeof value === 'string' && /^[\w.-]+\/[\w.-]+(?:\/[\w./-]+)?@[a-f\d]{40}$/i.test(value);
94
+ const container = typeof value === 'string' && /^docker:\/\/[^\s@]+@sha256:[a-f\d]{64}$/i.test(value);
95
+ if (!local && !remote && !container) findings.push(refuse(PIN, 'every remote uses entry needs a full 40-hex commit SHA (a container action needs a sha256 digest)'));
96
+ }
97
+ return findings;
98
+ }
99
+
100
+ function secretProblems(doc) {
101
+ const problems = [];
102
+ if (entries(doc).some(({ key, value }) => key === 'secrets' && value === 'inherit')) problems.push('inherited secrets are forbidden');
103
+ for (const value of strings(doc)) {
104
+ for (const expression of value.matchAll(/\$\{\{([\s\S]*?)\}\}/g)) {
105
+ const text = expression[1];
106
+ const refs = [...text.matchAll(/\bsecrets\s*(?:\.\s*([A-Za-z_]\w*)|\[\s*(['"])([^'"]+)\2\s*\])/g)];
107
+ for (const ref of refs) if (!CI_SECRET_ENV_NAMES.has(ref[1] ?? ref[3])) problems.push('a secrets reference is outside the age-key/SSH transport allowlist');
108
+ if (/\bsecrets\b/.test(text.replace(/\bsecrets\s*(?:\.\s*[A-Za-z_]\w*|\[\s*(['"])[^'"]+\1\s*\])/g, ''))) problems.push('dynamic or bulk secrets access is forbidden');
109
+ }
110
+ }
111
+ return [...new Set(problems)];
112
+ }
113
+
114
+ function dotenvTarget(script, start) {
115
+ let index = start;
116
+ while (index < script.length && /\s/.test(script[index])) index++;
117
+ if (script[index] === '"' || script[index] === "'") index++;
118
+ const from = index;
119
+ while (index < script.length && !/[\s"';|<>]/.test(script[index])) index++;
120
+ if (index < script.length && !/[\s"';|]/.test(script[index])) return false;
121
+ const target = script.slice(from, index);
122
+ const name = target.slice(target.lastIndexOf('/') + 1);
123
+ if (name.startsWith('.env')) return /^[.\w-]*$/.test(name.slice(4));
124
+ return name.endsWith('.env') && /^[\w.-]+$/.test(name.slice(0, -4));
125
+ }
126
+
127
+ function scriptWritesDotenv(value) {
128
+ const script = executable(value);
129
+ for (const match of script.matchAll(/>{1,2}|\btee\s+(?:-a\s+)?/g)) {
130
+ if (dotenvTarget(script, match.index + match[0].length)) return true;
131
+ }
132
+ return false;
133
+ }
134
+
135
+ function writesDotenv(doc) {
136
+ return entries(doc).some(({ key, value }) => key === 'env_file' || key === 'env-file' || (['run', 'script'].includes(key) && typeof value === 'string' && scriptWritesDotenv(value)));
137
+ }
138
+
139
+ function deploymentJobs(doc, file) {
140
+ const jobs = record(doc.jobs) ? Object.entries(doc.jobs).filter(([, job]) => record(job)) : [];
141
+ const identified = jobs.filter(([name, job]) => deployName(name) || deployName(String(job.name ?? '')) || scripts(job).some(deploymentCommand) || (typeof job.uses === 'string' && deployName(job.uses)));
142
+ if (identified.length) return identified;
143
+ if (deployName(file) || deployName(String(doc.name ?? ''))) return jobs;
144
+ return [];
145
+ }
146
+
147
+ function deploymentJobFindings(name, job, doc, refuse) {
148
+ const findings = [];
149
+ if (!validEnvironment(job.environment)) findings.push(refuse(DEPLOY, `job ${name} must declare its protected environment`));
150
+ if (!validPermissions(job.permissions ?? doc.permissions)) findings.push(refuse(DEPLOY, `job ${name} must declare permissions at job or workflow scope`));
151
+ if (!validConcurrency(job.concurrency ?? doc.concurrency)) findings.push(refuse(DEPLOY, `job ${name} must declare concurrency at job or workflow scope`));
152
+ if (!hasAgeIdentity(job, doc) && !scripts(job).some(materializeCommand)) findings.push(refuse(DEPLOY, `job ${name} must inject SOPS_AGE_KEY or run starci app secret materialize`));
153
+ return findings;
154
+ }
155
+
156
+ function deploymentFindings(doc, file, refuse) {
157
+ const deployment = deploymentJobs(doc, file);
158
+ const named = deployName(file) || deployName(String(doc.name ?? ''));
159
+ if (!deployment.length && !named) return [];
160
+ const findings = [];
161
+ if (!deployment.length) findings.push(refuse(DEPLOY, 'the deploy workflow must declare a deployment job'));
162
+ for (const [name, job] of deployment) findings.push(...deploymentJobFindings(name, job, doc, refuse));
163
+ for (const problem of secretProblems(doc)) findings.push(refuse(DEPLOY, problem));
164
+ if (writesDotenv(doc)) findings.push(refuse(DEPLOY, 'deploy workflows must never write dotenv files or use env_file blobs; materialize sealed custody'));
165
+ return findings;
166
+ }
167
+
168
+ // A version label alone is not an integrity pin. The immutable reference or verified artifact must belong to this install.
169
+ function verifiedArtifact(artifact, prior) {
170
+ if (!/^\.\/[\w./-]+\.(?:tgz|tar\.gz|whl)$/.test(artifact)) return false;
171
+ return prior.split(/\r?\n/).some((line) => {
172
+ const checksum = line.trim().match(/^echo\s+(['"])([a-f\d]{64})\s+\*?(\S+)\1\s*\|\s*sha256sum\s+(?:--check|-c)\s+-\s*(?:$|&&)/i);
173
+ return checksum && checksum[3].replace(/^\.\//, '') === artifact.slice(2) && !line.includes('||');
174
+ });
175
+ }
176
+
177
+ function pinnedInstall(command, prior) {
178
+ if (/^(?:python(?:3)?\s+-m\s+)?pip(?:3)?\s+install\b/.test(command)) {
179
+ // pip's hash-checking mode refuses every unhashed direct or transitive requirement itself.
180
+ if (/(?:^|\s)--require-hashes(?:\s|$)/.test(command)) return true;
181
+ const targets = command.replace(/^(?:python(?:3)?\s+-m\s+)?pip(?:3)?\s+install\s*/, '').split(/\s+/).filter((token) => !token.startsWith('-'));
182
+ return targets.length > 0 && targets.every((target) => /^git\+https:\/\/\S+@[a-f\d]{40}$/i.test(target) || verifiedArtifact(target, prior));
183
+ }
184
+ if (/^go\s+install\b/.test(command)) {
185
+ const targets = command.replace(/^go\s+install\s*/, '').split(/\s+/).filter((token) => !token.startsWith('-'));
186
+ return targets.length > 0 && targets.every((target) => /^[\w./-]+@[a-f\d]{40}$/i.test(target));
187
+ }
188
+ const targets = command.replace(/^npm\s+(?:(?:-g|--global|--location=global)\s+)*(?:i|install)\s*/, '').split(/\s+/).filter((token) => !token.startsWith('-')).map((token) => token.replace(/^["']|["']$/g, ''));
189
+ return targets.length > 0 && targets.every((target) => /^git\+https:\/\/\S+#[a-f\d]{40}$/i.test(target) || verifiedArtifact(target, prior));
190
+ }
191
+
192
+ function toolFindings(doc, refuse) {
193
+ const findings = [];
194
+ for (const { key, value } of entries(doc)) {
195
+ if (!['run', 'script'].includes(key) || typeof value !== 'string') continue;
196
+ const script = executable(value);
197
+ const statements = commands(script);
198
+ if (statements.some((command, index) => /^\b(?:curl|wget)\b/.test(commandBody(command.text)) && command.separator === '|' && /^(?:ba)?sh\b/.test(commandBody(statements[index + 1]?.text ?? '')) || /^(?:ba)?sh\s+<\(\s*(?:curl|wget)\b/.test(commandBody(command.text)))) findings.push(refuse(TOOL, 'a downloaded installer must be checksum-verified before execution; curl/wget piped to a shell is forbidden'));
199
+ for (const command of statements) {
200
+ const body = commandBody(command.text);
201
+ const install = /^(?:go\s+install|(?:python(?:3)?\s+-m\s+)?pip(?:3)?\s+install)\b/.test(body) || /^npm\s+(?:(?:-g|--global|--location=global)\s+)*(?:i|install)\b/.test(body) && /(?:^|\s)(?:-g|--global|--location=global)(?:\s|$)/.test(body);
202
+ if (install && !pinnedInstall(body, script.slice(0, command.start))) findings.push(refuse(TOOL, 'tool installs need an immutable hash or a verified artifact; use a SHA-pinned setup action instead of a version-only install'));
203
+ }
204
+ }
205
+ return findings;
206
+ }
207
+
208
+ function perWorkflow({ path: file, text }) {
209
+ const refuse = (code, message) => found(code, file, `${code} ${file}: ${message}`);
210
+ const doc = parseWorkflow(text);
211
+ if (!record(doc)) return [refuse(TRIGGER, 'the workflow must be valid YAML with a release-only on block'), ...(deployName(file) ? [refuse(DEPLOY, 'the deploy workflow must be valid YAML')] : [])];
212
+ return [...triggerFindings(doc, refuse), ...actionFindings(doc, refuse), ...deploymentFindings(doc, file, refuse), ...toolFindings(doc, refuse)];
213
+ }
214
+
215
+ /** Synchronous R251 findings over the tracked workflows of a lite/full app. */
216
+ export function deployWorkflowFindings({ repoRoot, files, all = files, repo, isRoot, inScope = () => true }) {
217
+ if (isRoot === false || repo?.kind !== 'app' || !['lite', 'full'].includes(repo.edition)) return [];
218
+ return workflowFindings({ files: all.filter((file) => inScope(file)), read: (file) => readText(repoRoot, file) }, perWorkflow);
219
+ }