@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
@@ -3,9 +3,11 @@ import { canonical, isInside } from './config.mjs';
3
3
  import { parseContract, installedSWR } from './next-data-contract.mjs';
4
4
  import { relativePath, unwrapExpression } from './typescript.mjs';
5
5
  import { anyDescendant, normalizedSymbol, normalizedSymbolValue, valueSymbol as sharedValueSymbol } from './ast-walks.mjs';
6
- import { isConstVariable, sourceLocation } from '../../lib/ts-ast.mjs';
6
+ import { sourceLocation } from '../../lib/ts-ast.mjs';
7
7
  import { byCodeUnit } from '../../lib/list.mjs';
8
8
  import { createNextDataKeyInspector } from './next-data-key.mjs';
9
+ import { functionNode, constInitializer, inferredSymbol } from './next-data-symbols.mjs';
10
+ import { inferredFetcher, inferredIdentities, inferredKeyBindings } from './next-data-inference.mjs';
9
11
  export const SWR_KEY_RULE_ID = 'FE_SWR_KEY_IDENTITY';
10
12
  export const SWR_MUTATION_RULE_ID = 'FE_SWR_MUTATION_RESOURCE_IDENTITY';
11
13
  export const SWR_DATA_RULE_IDS = [SWR_KEY_RULE_ID, SWR_MUTATION_RULE_ID];
@@ -155,15 +157,6 @@ function discoverCalls(config, context, targets) {
155
157
  return calls;
156
158
  }
157
159
 
158
- function functionNode(ts, declaration) {
159
- if (ts.isFunctionDeclaration(declaration) && declaration.body) return declaration;
160
- if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
161
- const value = unwrapExpression(ts, declaration.initializer);
162
- if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) return value;
163
- }
164
- return null;
165
- }
166
-
167
160
  function exportedHook(config, context, entry, reasons) {
168
161
  const source = context.files.find(candidate => canonical(candidate.fileName) === canonical(path.join(config.root, ...entry.path.split('/'))));
169
162
  if (!source) {
@@ -223,19 +216,15 @@ function resolvedIdentities(ts, checker, fn, entry, reasons) {
223
216
  return identities.length === entry.identities.length ? identities : null;
224
217
  }
225
218
 
226
- function constInitializer(ts, symbol) {
227
- const declarations = symbol?.getDeclarations?.() ?? [];
228
- if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer
229
- || !isConstVariable(ts, declarations[0])) return null;
230
- return declarations[0].initializer;
231
- }
232
-
233
219
  function violation(config, call, ruleId, message, extra = {}) {
234
220
  return { ruleId, path: relativePath(config.root, call.source.fileName), ...sourceLocation(call.source, call.node), message, ...extra };
235
221
  }
236
222
 
237
- const inspectKey = createNextDataKeyInspector({ anyDescendant, normalizedSymbol, unwrapExpression, constInitializer, sourceLocation, violation,
238
- SWR_KEY_RULE_ID, SWR_MUTATION_RULE_ID });
223
+ const keyInspectorDependencies = { anyDescendant, normalizedSymbol, unwrapExpression, constInitializer, sourceLocation, violation,
224
+ SWR_KEY_RULE_ID, SWR_MUTATION_RULE_ID };
225
+ const inspectKey = createNextDataKeyInspector(keyInspectorDependencies);
226
+
227
+ const inspectInferredKey = createNextDataKeyInspector({ ...keyInspectorDependencies, normalizedSymbol: inferredSymbol });
239
228
 
240
229
  function isUnshadowedCommonJsRequire(ts, checker, expression, seen = new Set(), depth = 0) {
241
230
  expression = unwrapExpression(ts, expression);
@@ -372,7 +361,26 @@ function lifecycleCoverage(ruleIds, contract, installed, calls, details) {
372
361
  swr: { name: installed.name, version: installed.version, major: installed.major } };
373
362
  }
374
363
 
375
- /** Check explicitly declared SWR lifecycle keys against the target program and installed SWR identity. */
364
+ function inspectDefaultLifecycle(config, context, installed, lifecycleCalls, violations, reasons) {
365
+ for (const call of lifecycleCalls) {
366
+ const fetcher = inferredFetcher(context.ts, call.checker, call.node.arguments[1]);
367
+ if (!fetcher) {
368
+ reasons.push(`${relativePath(config.root, call.source.fileName)}:${sourceLocation(call.source, call.node).line} fetcher inputs cannot be inferred; dataLifecycle is required to declare identities`);
369
+ continue;
370
+ }
371
+ const entry = { id: `swr:${relativePath(config.root, call.source.fileName)}:${sourceLocation(call.source, call.node).line}`,
372
+ path: relativePath(config.root, call.source.fileName), export: call.owner?.name?.text ?? '<inferred hook>', kind: call.kind };
373
+ const identities = inferredIdentities(context, call, fetcher, reasons);
374
+ const selected = new Set(identities.map(identity => identity.id));
375
+ const findings = [];
376
+ inspectInferredKey(config, context, entry, call, [...identities, ...inferredKeyBindings(context, call, reasons)], findings, reasons);
377
+ violations.push(...findings.filter(finding => finding.identity === undefined || selected.has(finding.identity)));
378
+ }
379
+ const details = [...new Set(reasons)].sort(byCodeUnit);
380
+ return { violations, coverage: lifecycleCoverage([...SWR_DATA_RULE_IDS], { hooks: lifecycleCalls }, installed, lifecycleCalls.length, details) };
381
+ }
382
+
383
+ /** Check declared lifecycles, or infer captured request identities when production SWR has no declaration. */
376
384
  export function checkFrontendDataLifecycle(config, context) {
377
385
  const ruleIds = [...SWR_DATA_RULE_IDS];
378
386
  const references = swrReferences(config, context);
@@ -381,17 +389,17 @@ export function checkFrontendDataLifecycle(config, context) {
381
389
  return unavailableDataLifecycle(ruleIds, [String(error.message ?? error)]);
382
390
  }
383
391
  if (!contract && !references.found) return { violations: [], coverage: { status: 'not-applicable', ruleIds } };
384
- if (!contract) return { violations: [], coverage: { status: 'unavailable', ruleIds,
385
- details: ['package.json#starci.codePatterns.next.dataLifecycle is required when production source selects SWR', ...references.unsupported] } };
386
392
  const reasons = [];
387
393
  const violations = [];
388
394
  reasons.push(...references.unsupported);
395
+ if (!contract && references.unsupported.length) reasons.push('package.json#starci.codePatterns.next.dataLifecycle is required for unsupported SWR bindings');
389
396
  let installed;
390
397
  try { installed = installedSWR(config.packageRoot ?? config.root); } catch (error) {
391
398
  return { violations, coverage: { status: 'unavailable', ruleIds, details: [String(error.message ?? error)] } };
392
399
  }
393
- if (installed.major !== contract.swr.major) reasons.push(`installed swr ${installed.version} does not match declared major ${contract.swr.major}`);
400
+ if (contract && installed.major !== contract.swr.major) reasons.push(`installed swr ${installed.version} does not match declared major ${contract.swr.major}`);
394
401
  const { lifecycleCalls } = lifecycleCallSets(config, context, installed, reasons);
402
+ if (!contract) return inspectDefaultLifecycle(config, context, installed, lifecycleCalls, violations, reasons);
395
403
  const { hookCache, entriesByHook } = hookEntries(config, context, contract, reasons);
396
404
  inspectHookLifecycle(config, context, lifecycleCalls, hookCache, entriesByHook, violations, reasons);
397
405
  addUndeclaredLifecycleReasons(config, lifecycleCalls, hookCache, reasons);
@@ -96,9 +96,23 @@ function checkBackend(graph) {
96
96
  const reached = reachFrom(graph, rootFiles);
97
97
  const violations = [];
98
98
  reportUncomposedBackendOwners(byUnit, reached, roots, counts, violations);
99
+ reportUncomposedTransports(graph, reached, roots, counts, violations);
99
100
  return { violations, coverage: { status: 'checked', ...counts } };
100
101
  }
101
102
 
103
+ /** A reached feature does not compose its other transport modules. Judge each module's runtime path. */
104
+ function reportUncomposedTransports(graph, reached, roots, counts, violations) {
105
+ for (const [rel, node] of graph.files) {
106
+ if (!node.owner || !rel.startsWith('src/features/') || TEST_FILE.test(rel)) continue;
107
+ if (!/\/transport\/[^/]+\/[^/]+\.module\.[cm]?[jt]sx?$/.test(rel) || reached.has(rel)) continue;
108
+ counts.notComposed += 1;
109
+ violations.push({
110
+ ruleId: 'BE_FEATURE_NOT_COMPOSED', path: rel, owner: node.owner.root, slot: node.slot, appRoots: roots,
111
+ message: `Transport module ${rel} is not composed into any app: no runtime import path leads from ${roots.join(', ')} to it (a type-only import composes nothing).`,
112
+ });
113
+ }
114
+ }
115
+
102
116
  /** Route table of one app: arrays of segments, each {kind:'static'|'dyn'|'catch'|'optcatch', text}. */
103
117
  function routeSegmentsOf(parts) {
104
118
  const segments = [];
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { machineKit } from './machine-ast.mjs';
2
+ import { isComposeGroup, machineKit } from './machine-ast.mjs';
3
3
  import { callName } from './supabase-ast.mjs';
4
4
  import { isTestWorldSlot } from '../test-world-slot.mjs';
5
5
  import { byCodeUnit } from '../../lib/list.mjs';
@@ -7,14 +7,15 @@ import { byCodeUnit } from '../../lib/list.mjs';
7
7
  /**
8
8
  * R45 `register-once` (BE_MODULE_SHAPE), the module graph read from the app roots (owner rule of 2026-09-30, the
9
9
  * reference's `CodingModule imports DeviceModule` shape). Each capability has one representative module: the only module
10
- * of the capability an app registers, once, in `apps/<app>/src/app.module.ts` as `X.register({ isGlobal: true, ... })`.
11
- * It may split itself into sub-modules imported as plain `imports: [Sub]`; a sub-module is never in an app and has one
10
+ * of the capability an app registers, once, in `apps/<app>/src/app.module.ts` as `X.register({ isGlobal: true, ... })`, or in a
11
+ * composition group `apps/<app>/src/compose/<area>.ts` the root spreads (an app root here means the root and its groups).
12
+ * It may split itself into sub-modules imported as `imports: [Sub.register({})]`; a sub-module is never in an app and has one
12
13
  * importer, its parent. Over every `@Module` class of the program:
13
14
  *
14
15
  * - a module in an app root's imports (a `X.register(...)` call or a listed class, helpers of the root included) is
15
16
  * listed once there, is imported by no other module, and, when it is a capability module of src/modules, is registered
16
17
  * with the literal `isGlobal: true` (a bare capability class is not a registration);
17
- * - a module imported by a non-app module has exactly one importer and appears in no app root; the one exception is
18
+ * - a module imported by a non-app module (as `Sub.register({})`) has exactly one importer and appears in no app root; the one exception is
18
19
  * the feature case: a feature's application module is imported by each protocol module of the same feature;
19
20
  * - `isGlobal: true` appears only in an app root.
20
21
  */
@@ -57,6 +58,7 @@ function enclosingModule(ts, modules, node) {
57
58
  /** Records the reference an element of an imports list, or a register call, makes to a module. */
58
59
  function noteReference(scan, element, viaImports) {
59
60
  const { ts, kit, modules, checker, file, seen, references, isAppRoot } = scan;
61
+ if (seen.has(element)) return;
60
62
  const call = callName(ts, element) === 'register';
61
63
  if (ts.isCallExpression(element) && !call) return;
62
64
  const target = targetOf(kit, modules, checker, call ? element.expression.expression : element);
@@ -72,14 +74,19 @@ const isImportsList = (scan, node) => scan.ts.isPropertyAssignment(node) && scan
72
74
  const isStrayGlobal = (scan, node) => scan.ts.isPropertyAssignment(node) && scan.kit.propertyNameText(node.name) === 'isGlobal'
73
75
  && node.initializer.kind === scan.ts.SyntaxKind.TrueKeyword && !scan.isAppRoot(scan.file) && !scan.isWorld(scan.file);
74
76
 
77
+ // A composition group's module list is any array of the group: the root spreads it into its imports.
78
+ const isGroupList = (scan, node) => scan.ts.isArrayLiteralExpression(node) && isComposeGroup(scan.file.rel);
79
+
75
80
  function visitReferenceNode(scan, node) {
76
81
  if (isImportsList(scan, node)) {
77
82
  for (const element of node.initializer.elements) noteReference(scan, element, true);
83
+ } else if (isGroupList(scan, node)) {
84
+ for (const element of node.elements) noteReference(scan, element, true);
78
85
  } else if (callName(scan.ts, node) === 'register' && !scan.seen.has(node)) {
79
86
  noteReference(scan, node, false);
80
87
  }
81
88
  if (isStrayGlobal(scan, node)) {
82
- scan.report(scan.file, node, '`isGlobal: true` appears only in the app root (apps/<app>/src/app.module.ts), where the representative module of a capability is registered; a module never decides its own globality.');
89
+ scan.report(scan.file, node, '`isGlobal: true` appears only in the app root (apps/<app>/src/app.module.ts or a group of apps/<app>/src/compose/), where the representative module of a capability is registered; a module never decides its own globality.');
83
90
  }
84
91
  return true;
85
92
  }
@@ -170,11 +177,12 @@ export function checkRegisterOnce(input) {
170
177
  const { ts } = kit;
171
178
  const violations = [];
172
179
  const report = (file, node, message, extra = {}) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
173
- const isAppRoot = file => Boolean(file.slot?.startsWith('be.app.')) && path.posix.basename(file.rel) === 'app.module.ts';
180
+ const isAppRoot = file => Boolean(file.slot?.startsWith('be.app.')) && (path.posix.basename(file.rel) === 'app.module.ts' || isComposeGroup(file.rel));
174
181
  // The test world (slot be.tests.world) is the test composition root: useTestWorld registers capability modules with
175
182
  // `isGlobal: true` the way an app root does. A spec (integration, contract, e2e) is not a composition root.
176
183
  const isWorld = file => isTestWorldSlot(file.slot);
177
- const appOf = file => config.apps.find(app => file.rel === `apps/${app.name}/src/app.module.ts`)?.name ?? null;
184
+ const appOf = file => config.apps.find(app => file.rel === `apps/${app.name}/src/app.module.ts`
185
+ || (isComposeGroup(file.rel) && file.rel.startsWith(`apps/${app.name}/src/compose/`)))?.name ?? null;
178
186
  const modules = moduleDeclarations(graph, kit, ts);
179
187
  const references = moduleReferences(graph, kit, ts, modules, isWorld, isAppRoot, report);
180
188
  const rootReferences = references.filter(item => item.root);
@@ -4,6 +4,7 @@ import { lsFiles } from '../../api/git/ls-files.mjs';
4
4
  import { revParseQuery } from '../../api/git/rev-parse-query.mjs';
5
5
  import { gitOutputOf } from '../../lib/git.mjs';
6
6
  import { trimTrailingSlashes } from '../trailing-slashes.mjs';
7
+ import { requiredFileApplies } from '../slot-required.mjs';
7
8
 
8
9
  /**
9
10
  * HFS check 5: the files and directories the slot manifest requires (knowledge/hfs/slots.yaml `requires`,
@@ -173,7 +174,7 @@ function requirementExists(tree, target) {
173
174
  }
174
175
 
175
176
  function reportMissingRequirement(requirement, tree, report) {
176
- if (requirementExists(tree, requirement.target)) return;
177
+ if (requirementExists(tree, requirement.target) || !requiredFileApplies(requirement.slot, requirement.target, tree.files)) return;
177
178
  const rootExists = !requirement.root || tree.directories.has(requirement.root);
178
179
  if (!rootExists) {
179
180
  report(ruleFor(requirement.slot, requirement.target, false), requirement.root, requirement.slot, requirement.root, { kind: 'directory' },
@@ -1,13 +1,14 @@
1
1
  import path from 'node:path';
2
2
  import { isInside } from './config.mjs';
3
3
  import { relativePath, unwrapExpression } from './typescript.mjs';
4
- import { commonJsRequireReasons, decoratorCallee, moduleExportsOf, nodeDecorators, normalizedSymbol, normalizedSymbolValue, programSourcesOf, returnedExpressions, selectedNode, valueSymbol, violation } from './ast-walks.mjs';
4
+ import { commonJsRequireReasons, constructedDecoratorKind, decoratorCallee, moduleExportsOf, mutableDecoratorKind, nodeDecorators, normalizedSymbol, normalizedSymbolValue, programSourcesOf, returnedExpressions, selectedNode, valueSymbol, violation } from './ast-walks.mjs';
5
5
  import { createBackendSourceShapeChecker } from './source-names-shape.mjs';
6
+ import { byCodeUnit } from '../../lib/list.mjs';
6
7
 
7
8
  const SOURCE_LAYOUT_RULE_ID = 'BE_FEATURE_LAYOUT_INVALID';
8
9
  const SOURCE_NAME_RULE_ID = 'BE_SOURCE_FORM';
9
10
  const FRAMEWORK_EXPORTS = new Map([
10
- ['@nestjs/graphql', new Set(['Args', 'ArgsType', 'InputType', 'Mutation', 'ObjectType', 'Query', 'registerEnumType'])],
11
+ ['@nestjs/graphql', new Set(['Args', 'ArgsType', 'InputType', 'Mutation', 'ObjectType', 'Query', 'Subscription', 'registerEnumType'])],
11
12
  ['typeorm', new Set(['Entity', 'EntitySchema', 'MigrationInterface', 'ViewEntity'])],
12
13
  ]);
13
14
  const APPLICATION_ROLES = new Set(['command', 'contracts', 'handler', 'query']);
@@ -237,6 +238,11 @@ function graphqlField({ config, context, sourceFile, node, decorator, kind, nami
237
238
  }
238
239
  const methodName = node.name && (context.ts.isIdentifier(node.name) || context.ts.isStringLiteralLike(node.name)) ? node.name.text : null;
239
240
  const name = selected.status === 'literal' ? selected.value : methodName;
241
+ if (selected.status === 'literal') {
242
+ if (!methodName) namingReasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot prove the @${kind} method name`);
243
+ else if (selected.value !== methodName) violations.push(violation(config, sourceFile, selected.node, SOURCE_NAME_RULE_ID,
244
+ `GraphQL @${kind} field name ${selected.value} must equal its method name ${methodName}.`));
245
+ }
240
246
  if (!name) namingReasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot prove the @${kind} field name`);
241
247
  else if (!camelCase(name)) violations.push(violation(config, sourceFile, selected.node ?? node.name ?? node, SOURCE_NAME_RULE_ID,
242
248
  `GraphQL field name ${name} must be camelCase.`));
@@ -316,7 +322,46 @@ const checkBackendSourceShapeImpl = createBackendSourceShapeChecker({
316
322
  TRACE_NEW,
317
323
  });
318
324
 
325
+ function inspectSubscriptionSource({ config, context, sourceFile, checker, framework, configuredRoot, namingReasons, violations }) {
326
+ const visit = node => {
327
+ for (const decorator of nodeDecorators(context.ts, node)) {
328
+ const kind = decoratorKind(context.ts, checker, decorator, framework.bySymbol);
329
+ if (kind === 'Subscription') graphqlField({ config, context, sourceFile, node, decorator, kind, namingReasons, violations });
330
+ else if (!kind && !configuredRoot) {
331
+ const dynamic = mutableDecoratorKind(context.ts, checker, decorator, framework.bySymbol)
332
+ ?? constructedDecoratorKind(context.ts, checker, decorator, framework.bySymbol, TRACE_NEW);
333
+ if (['Subscription', 'multiple framework', 'unproven framework'].includes(dynamic)) {
334
+ namingReasons.push(`${relativePath(config.root, sourceFile.fileName)} uses a mutable or constructed ${dynamic} decorator identity`);
335
+ }
336
+ }
337
+ }
338
+ context.ts.forEachChild(node, visit);
339
+ };
340
+ visit(sourceFile);
341
+ }
342
+
319
343
  export function checkBackendSourceShape(config, context) {
320
- return checkBackendSourceShapeImpl(config, context);
344
+ const result = checkBackendSourceShapeImpl(config, context);
345
+ const roots = absoluteRoots(config.root, [...config.backend.features, ...config.backend.modules]);
346
+ const localFiles = new Set(context.files.map(file => canonical(file.fileName)));
347
+ const frameworkByChecker = new Map();
348
+ const namingReasons = [];
349
+ // The shared shape walk dispatches Query and Mutation; subscriptions use the same naming relation here.
350
+ for (const sourceFile of context.files) {
351
+ const fileName = canonical(sourceFile.fileName);
352
+ const configuredRoot = locatedRoot(roots, fileName);
353
+ const owner = config.hfs?.ownerOf?.(relativePath(config.root, fileName));
354
+ const slot = owner ? config.hfs.slot(owner.slot) : null;
355
+ if (!configuredRoot && !(slot?.tier === 'feature' && slot.profiles?.includes('be'))) continue;
356
+ const checker = context.checkerFor(fileName);
357
+ if (!frameworkByChecker.has(checker)) frameworkByChecker.set(checker, frameworkTargets(config, context, checker, localFiles));
358
+ const framework = frameworkByChecker.get(checker);
359
+ inspectSubscriptionSource({ config, context, sourceFile, checker, framework, configuredRoot, namingReasons, violations: result.violations });
360
+ }
361
+ if (namingReasons.length) result.coverage.naming = {
362
+ status: 'unavailable', reason: 'one or more backend source naming relations are not statically proved',
363
+ details: [...new Set([...(result.coverage.naming.details ?? []), ...namingReasons])].sort(byCodeUnit),
364
+ };
365
+ return result;
321
366
  }
322
367
  export { SOURCE_LAYOUT_RULE_ID, SOURCE_NAME_RULE_ID };
@@ -0,0 +1,37 @@
1
+ import { catFile } from '../../api/git/cat-file.mjs';
2
+ import { diff } from '../../api/git/diff.mjs';
3
+ import { gitOutputOf } from '../../lib/git.mjs';
4
+
5
+ /** Eligible additions, copies, edits and rename destinations in the index, read in one blob batch. */
6
+ export function stagedConsumerSources(root, accept) {
7
+ try {
8
+ const options = { cwd: root, maxBuffer: 256 * 1024 * 1024 };
9
+ const files = gitOutputOf(diff(['--cached', '--relative', '--name-only', '--diff-filter=ACMR', '-z', '--', '.'], options))
10
+ .split('\0').filter(file => file && accept(file));
11
+ if (!files.length) return [];
12
+ // :./ keeps paths relative to this side's root even when git's top level is the parent app folder.
13
+ // -z terminates requests with NUL; output headers and blob trailers still use LF.
14
+ const bytes = gitOutputOf(catFile(['--batch', '-z'], {
15
+ ...options, encoding: null, input: files.map(file => `:./${file}\0`).join(''),
16
+ }));
17
+ if (!Buffer.isBuffer(bytes)) return [];
18
+ const sources = [];
19
+ let cursor = 0;
20
+ for (const file of files) {
21
+ const end = bytes.indexOf(10, cursor);
22
+ if (end < 0) return [];
23
+ const header = /^[0-9a-f]+ blob (\d+)$/u.exec(bytes.subarray(cursor, end).toString('utf8'));
24
+ if (!header) return []; // An index changed during the read: discard this supplement, keep disk consumers.
25
+ const size = Number(header[1]);
26
+ const start = end + 1;
27
+ const next = start + size;
28
+ if (!Number.isSafeInteger(size) || next >= bytes.length || bytes[next] !== 10) return [];
29
+ sources.push({ file, text: bytes.subarray(start, next).toString('utf8') });
30
+ cursor = next + 1;
31
+ }
32
+ return sources;
33
+ } catch {
34
+ // A non-git fixture (or an unavailable index) still uses the existing filesystem consumers.
35
+ return [];
36
+ }
37
+ }
@@ -1,6 +1,10 @@
1
1
  // supabase-ast.mjs - shared TypeScript-symbol and expression helpers for the front-end and back-end Supabase
2
2
  // architecture checks. Recognition follows imported declarations and inferred types, never local spelling alone.
3
- export const SUPABASE_MODULES = new Set(['@supabase/ssr', '@supabase/supabase-js']);
3
+ import { unwrapExpression } from '../../lib/ts-analysis.mjs';
4
+ import { accessName as memberName, hasAnyFlag } from '../../lib/ts-ast.mjs';
5
+ export { accessName as memberName } from '../../lib/ts-ast.mjs';
6
+
7
+ export const SUPABASE_MODULES = Object.freeze({ has: name => typeof name === 'string' && name.startsWith('@supabase/') });
4
8
 
5
9
  export const moduleNameOf = (ts, node) => {
6
10
  if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) return node.moduleSpecifier.text;
@@ -15,7 +19,7 @@ export const importedFrom = (kit, checker, node, modules, names = null) => {
15
19
  return Boolean(binding && modules.has(binding.module) && (names === null || names.has(binding.name)));
16
20
  };
17
21
 
18
- const declarationFromSupabase = (declaration) => /(?:^|\/)node_modules\/@supabase\/(?:ssr|supabase-js|auth-js|postgrest-js|storage-js)(?:\/|$)/u
22
+ const declarationFromSupabase = (declaration) => /(?:^|\/)node_modules\/@supabase\/[^/]+(?:\/|$)/u
19
23
  .test(String(declaration?.getSourceFile?.().fileName ?? '').replaceAll('\\', '/'));
20
24
 
21
25
  const typeFromSupabase = (checker, node) => {
@@ -38,12 +42,7 @@ const signatureFromSupabase = (checker, call) => {
38
42
  try { return declarationFromSupabase(checker.getResolvedSignature(call)?.declaration); } catch { return false; }
39
43
  };
40
44
 
41
- export const unwrap = (ts, node) => {
42
- let current = node;
43
- while (current && (ts.isParenthesizedExpression(current) || ts.isAwaitExpression(current) || ts.isAsExpression(current)
44
- || ts.isTypeAssertionExpression(current) || ts.isNonNullExpression(current))) current = current.expression;
45
- return current;
46
- };
45
+ export const unwrap = (ts, node) => unwrapExpression(ts, node, { awaitExpressions: true, satisfiesExpressions: false });
47
46
 
48
47
  const isSupabaseValue = (kit, checker, node) => {
49
48
  const current = unwrap(kit.ts, node);
@@ -53,23 +52,19 @@ const isSupabaseValue = (kit, checker, node) => {
53
52
  return typeFromSupabase(checker, current);
54
53
  };
55
54
 
56
- export const callName = (ts, call) => ts.isCallExpression(call) && ts.isPropertyAccessExpression(call.expression) ? call.expression.name.text : null;
55
+ export const callName = (ts, call) => ts.isCallExpression(call) ? memberName(ts, call.expression) : null;
57
56
 
58
57
  export const chainParts = (ts, expression) => {
59
58
  const names = [];
60
59
  let current = unwrap(ts, expression);
61
60
  while (current) {
62
61
  if (ts.isCallExpression(current)) {
63
- if (ts.isPropertyAccessExpression(current.expression)) {
64
- names.unshift(current.expression.name.text);
65
- current = unwrap(ts, current.expression.expression);
66
- continue;
67
- }
68
62
  current = unwrap(ts, current.expression);
69
63
  continue;
70
64
  }
71
- if (ts.isPropertyAccessExpression(current)) {
72
- names.unshift(current.name.text);
65
+ if (ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)) {
66
+ const name = memberName(ts, current);
67
+ if (name) names.unshift(name);
73
68
  current = unwrap(ts, current.expression);
74
69
  continue;
75
70
  }
@@ -79,7 +74,47 @@ export const chainParts = (ts, expression) => {
79
74
  };
80
75
 
81
76
  export const isSupabaseCall = (kit, checker, node) => kit.ts.isCallExpression(node)
82
- && (signatureFromSupabase(checker, node) || isSupabaseValue(kit, checker, kit.ts.isPropertyAccessExpression(node.expression) ? node.expression.expression : node.expression));
77
+ && (signatureFromSupabase(checker, node) || isSupabaseValue(kit, checker,
78
+ kit.ts.isPropertyAccessExpression(node.expression) || kit.ts.isElementAccessExpression(node.expression) ? node.expression.expression : node.expression));
79
+
80
+ const authStorageValue = (kit, checker, expression, seen = new Set()) => {
81
+ const current = unwrap(kit.ts, expression);
82
+ if (!current || seen.has(current)) return false;
83
+ seen.add(current);
84
+ if (['auth', 'storage'].includes(memberName(kit.ts, current)) && isSupabaseValue(kit, checker, current.expression)) return true;
85
+ if (checker.getTypeAtLocation(current).symbol?.declarations?.some(declaration =>
86
+ /\/node_modules\/@supabase\/(?:storage-js|auth-js)\//u.test(String(declaration.getSourceFile().fileName).replaceAll('\\', '/')))) return true;
87
+ if (!kit.ts.isIdentifier(current)) return false;
88
+ return kit.declarationsOf(checker, current).some(declaration =>
89
+ (kit.ts.isVariableDeclaration(declaration) || kit.ts.isPropertyDeclaration(declaration)) && declaration.initializer
90
+ && authStorageValue(kit, checker, declaration.initializer, seen));
91
+ };
92
+
93
+ const chainAccesses = (ts, node) => {
94
+ const accesses = [];
95
+ let current = unwrap(ts, node);
96
+ while (current) {
97
+ if (ts.isCallExpression(current)) current = unwrap(ts, current.expression);
98
+ else if (ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)) {
99
+ accesses.unshift(current);
100
+ current = unwrap(ts, current.expression);
101
+ } else break;
102
+ }
103
+ return accesses;
104
+ };
105
+
106
+ /** The first forbidden SDK access in a call chain, anchored once even when more methods follow it. */
107
+ export const supabaseDataAccess = (kit, checker, node, members, { storageAllowed = false } = {}) => {
108
+ for (const access of chainAccesses(kit.ts, node)) {
109
+ const name = memberName(kit.ts, access);
110
+ if (storageAllowed && (name === 'storage' || name === 'auth')) return null;
111
+ if (members.has(name) && isSupabaseValue(kit, checker, access.expression)) {
112
+ if (storageAllowed && authStorageValue(kit, checker, access.expression)) return null;
113
+ return { name, node: access };
114
+ }
115
+ }
116
+ return null;
117
+ };
83
118
 
84
119
  export const isDatabaseType = (ts, node) => {
85
120
  if (!node) return false;
@@ -109,3 +144,165 @@ export const contextTypesClient = (kit, checker, call) => {
109
144
  export const reportAt = (violations, kit, ruleId, file, node, message, extra = {}) => {
110
145
  violations.push({ ruleId, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
111
146
  };
147
+
148
+ const declarationsOf = (ts, checker, node) => {
149
+ const target = ts.isPropertyAccessExpression(node) ? node.name : node;
150
+ // A shorthand property (`issuer,`) names the variable it reads, not the property it creates.
151
+ let symbol = ts.isShorthandPropertyAssignment(target.parent ?? {}) && target.parent.name === target
152
+ ? checker.getShorthandAssignmentValueSymbol(target.parent)
153
+ : checker.getSymbolAtLocation(target);
154
+ if (symbol && hasAnyFlag(symbol.flags, ts.SymbolFlags.Alias)) symbol = checker.getAliasedSymbol(symbol);
155
+ return symbol?.declarations ?? [];
156
+ };
157
+
158
+ const configType = (checker, node) => {
159
+ const type = checker.getTypeAtLocation(node);
160
+ return [type.symbol, type.aliasSymbol].some(symbol => symbol?.declarations?.some(declaration =>
161
+ /(?:\.config|\.options|\.env)\.ts$|\/modules\/config\/|\/node_modules\/@nestjs\/config\//u.test(String(declaration.getSourceFile().fileName).replaceAll('\\', '/'))
162
+ || /(?:Config|Options|Env)$/u.test(symbol.name)));
163
+ };
164
+
165
+ /** Trace issuer construction through configuration values and local URL/string transformations. */
166
+ const issuerFromConfig = (ts, checker, expression, seen = new Set()) => {
167
+ const node = unwrap(ts, expression);
168
+ if (!node || seen.has(node)) return false;
169
+ const next = new Set(seen).add(node);
170
+ if (ts.isIdentifier(node)) {
171
+ return declarationsOf(ts, checker, node).some(declaration => {
172
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) return issuerFromConfig(ts, checker, declaration.initializer, next);
173
+ if (ts.isParameter(declaration) || ts.isPropertyDeclaration(declaration)) return configType(checker, node);
174
+ return /(?:\.config|\.env)\.ts$|\/modules\/config\//u.test(String(declaration.getSourceFile().fileName).replaceAll('\\', '/'));
175
+ });
176
+ }
177
+ if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
178
+ return issuerFromConfig(ts, checker, node.expression, next) || declarationsOf(ts, checker, node).some(declaration =>
179
+ (ts.isParameter(declaration) || ts.isPropertyDeclaration(declaration)) && !declaration.initializer && configType(checker, node));
180
+ }
181
+ if (ts.isTemplateExpression(node)) return node.templateSpans.some(span => issuerFromConfig(ts, checker, span.expression, next));
182
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
183
+ return issuerFromConfig(ts, checker, node.left, next) || issuerFromConfig(ts, checker, node.right, next);
184
+ }
185
+ if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
186
+ return issuerFromConfig(ts, checker, node.expression, next) || node.arguments?.some(argument => issuerFromConfig(ts, checker, argument, next));
187
+ }
188
+ return false;
189
+ };
190
+
191
+ const objectValue = (ts, checker, expression) => {
192
+ const node = unwrap(ts, expression);
193
+ if (!node) return null;
194
+ if (ts.isObjectLiteralExpression(node)) return node;
195
+ if (!ts.isIdentifier(node)) return null;
196
+ return declarationsOf(ts, checker, node).find(declaration => ts.isVariableDeclaration(declaration)
197
+ && declaration.initializer && ts.isObjectLiteralExpression(unwrap(ts, declaration.initializer)))?.initializer ?? null;
198
+ };
199
+
200
+ const literalString = (ts, checker, expression) => {
201
+ const node = unwrap(ts, expression);
202
+ if (!node) return null;
203
+ if (ts.isStringLiteralLike(node)) return node.text;
204
+ const type = checker.getTypeAtLocation(node);
205
+ return type.isStringLiteral?.() ? type.value : null;
206
+ };
207
+
208
+ /** A role read must belong to the payload produced by this verification, including destructuring aliases. */
209
+ const verifiedPayload = (ts, checker, expression, call, seen = new Set()) => {
210
+ const node = unwrap(ts, expression);
211
+ if (!node || seen.has(node)) return false;
212
+ const next = new Set(seen).add(node);
213
+ if ((ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) && memberName(ts, node) === 'payload') {
214
+ const value = unwrap(ts, node.expression);
215
+ if (value === call) return true;
216
+ if (ts.isIdentifier(value)) return declarationsOf(ts, checker, value).some(declaration =>
217
+ ts.isVariableDeclaration(declaration) && unwrap(ts, declaration.initializer) === call);
218
+ }
219
+ if (!ts.isIdentifier(node)) return false;
220
+ return declarationsOf(ts, checker, node).some(declaration => {
221
+ if (ts.isVariableDeclaration(declaration)) return declaration.initializer && verifiedPayload(ts, checker, declaration.initializer, call, next);
222
+ if (!ts.isBindingElement(declaration) || (declaration.propertyName ?? declaration.name).getText() !== 'payload') return false;
223
+ return unwrap(ts, declaration.parent.parent.initializer) === call;
224
+ });
225
+ };
226
+
227
+ const rejects = (ts, statement) => {
228
+ if (ts.isBlock(statement)) {
229
+ const exit = statement.statements.find(item => ts.isThrowStatement(item) || ts.isReturnStatement(item));
230
+ return Boolean(exit && rejects(ts, exit));
231
+ }
232
+ if (ts.isThrowStatement(statement)) return true;
233
+ if (!ts.isReturnStatement(statement)) return false;
234
+ const value = statement.expression && unwrap(ts, statement.expression);
235
+ return !value || value.kind === ts.SyntaxKind.NullKeyword || value.kind === ts.SyntaxKind.FalseKeyword
236
+ || (ts.isCallExpression(value) && /^(?:refused|err|fail|failure)$/u.test(value.expression.getText()));
237
+ };
238
+
239
+ const roleCondition = (ts, checker, condition, call, equality = false) => {
240
+ const node = unwrap(ts, condition);
241
+ if (!node || !ts.isBinaryExpression(node)) return false;
242
+ const combined = equality ? ts.SyntaxKind.AmpersandAmpersandToken : ts.SyntaxKind.BarBarToken;
243
+ if (node.operatorToken.kind === combined) return roleCondition(ts, checker, node.left, call, equality) || roleCondition(ts, checker, node.right, call, equality);
244
+ const operators = equality ? [ts.SyntaxKind.EqualsEqualsEqualsToken, ts.SyntaxKind.EqualsEqualsToken]
245
+ : [ts.SyntaxKind.ExclamationEqualsEqualsToken, ts.SyntaxKind.ExclamationEqualsToken];
246
+ if (!operators.includes(node.operatorToken.kind)) return false;
247
+ const role = expression => (ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression))
248
+ && memberName(ts, expression) === 'role' && verifiedPayload(ts, checker, expression.expression, call);
249
+ return (role(node.left) && literalString(ts, checker, node.right) === 'authenticated')
250
+ || (role(node.right) && literalString(ts, checker, node.left) === 'authenticated');
251
+ };
252
+
253
+ const returnsPrincipal = (ts, statement) => {
254
+ let accepted = false;
255
+ const visit = node => {
256
+ if (ts.isFunctionLike(node)) return;
257
+ if (ts.isReturnStatement(node) && !rejects(ts, node)) accepted = true;
258
+ ts.forEachChild(node, visit);
259
+ };
260
+ visit(statement);
261
+ return accepted;
262
+ };
263
+
264
+ const statementsAfterVerify = (ts, call) => {
265
+ let statement = call;
266
+ while (statement.parent && !ts.isBlock(statement.parent)) statement = statement.parent;
267
+ const block = statement.parent;
268
+ if (!block || !ts.isBlock(block)) return [];
269
+ return block.statements.slice(block.statements.indexOf(statement) + 1);
270
+ };
271
+
272
+ const rejectsRoleGuard = (ts, checker, statement, call) => {
273
+ if (!ts.isIfStatement(statement)) return false;
274
+ if (roleCondition(ts, checker, statement.expression, call) && rejects(ts, statement.thenStatement)) return true;
275
+ return Boolean(statement.elseStatement && roleCondition(ts, checker, statement.expression, call, true) && rejects(ts, statement.elseStatement));
276
+ };
277
+
278
+ const rejectsOtherRoles = (ts, checker, call) => {
279
+ for (const item of statementsAfterVerify(ts, call)) {
280
+ if (ts.isReturnStatement(item) || ts.isThrowStatement(item)) return false;
281
+ if (rejectsRoleGuard(ts, checker, item, call)) return true;
282
+ if (returnsPrincipal(ts, item)) return false;
283
+ }
284
+ return false;
285
+ };
286
+
287
+ /** R260 is shared by the architecture machine and the published typed ESLint rule. */
288
+ export const jwtPolicyProblems = (ts, checker, call) => {
289
+ const problems = [];
290
+ const options = objectValue(ts, checker, call.arguments[2]);
291
+ const property = name => options?.properties.findLast(entry =>
292
+ (ts.isPropertyAssignment(entry) || ts.isShorthandPropertyAssignment(entry))
293
+ && (ts.isIdentifier(entry.name) || ts.isStringLiteralLike(entry.name)) && entry.name.text === name);
294
+ const value = name => {
295
+ const entry = property(name);
296
+ return entry && unwrap(ts, ts.isPropertyAssignment(entry) ? entry.initializer : entry.name);
297
+ };
298
+ const algorithms = value('algorithms');
299
+ const secure = new Set(['ES256', 'RS256', 'EdDSA']);
300
+ if (!algorithms || !ts.isArrayLiteralExpression(algorithms) || !algorithms.elements.length
301
+ || algorithms.elements.some(item => !ts.isStringLiteralLike(item) || !secure.has(item.text))
302
+ || options?.properties.some(entry => ts.isSpreadAssignment(entry))) {
303
+ problems.push('BE_JWT_ALG_PINNED: algorithms must be a non-empty literal subset of ES256, RS256 and EdDSA.');
304
+ }
305
+ if (!issuerFromConfig(ts, checker, value('issuer'))) problems.push('BE_JWT_ALG_PINNED: issuer must derive from typed configuration.');
306
+ if (!rejectsOtherRoles(ts, checker, call)) problems.push('BE_JWT_ALG_PINNED: the verified principal must reject every role other than authenticated before returning it.');
307
+ return problems;
308
+ };
@@ -3,6 +3,7 @@
3
3
  import path from 'node:path';
4
4
  import { machineKit } from './machine-ast.mjs';
5
5
  import { importedFrom, reportAt, unwrap } from './supabase-ast.mjs';
6
+ import { accessName } from '../../lib/ts-ast.mjs';
6
7
 
7
8
  const BE_SUPABASE_SLOT = 'be.integrations.supabase';
8
9
  const BE_JWT_VERIFIED = 'BE_SUPABASE_JWT_VERIFIED';
@@ -47,12 +48,6 @@ const importedCall = (kit, checker, call) => {
47
48
  return owner?.name === 'default' ? { module: owner.module, name: call.expression.name.text } : null;
48
49
  };
49
50
 
50
- const accessName = (ts, node) => {
51
- if (ts.isPropertyAccessExpression(node)) return node.name.text;
52
- if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)) return node.argumentExpression.text;
53
- return null;
54
- };
55
-
56
51
  const requestLike = (ts, node) => {
57
52
  const current = unwrap(ts, node);
58
53
  if (ts.isIdentifier(current)) return /^(?:req|request)$/iu.test(current.text);