@starci/hfs 4.0.10 → 4.1.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 (271) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/emit/operations.mjs +1 -1
  3. package/emit/type-schema.mjs +2 -1
  4. package/lint/run.mjs +3 -2
  5. package/package.json +1 -1
  6. package/report/order.mjs +2 -0
  7. package/runtime/config.example.yaml +187 -0
  8. package/runtime/engine/by-code-unit.mjs +12 -0
  9. package/runtime/engine/config.mjs +221 -239
  10. package/runtime/engine/invalid-config.mjs +5 -5
  11. package/runtime/engine/model-config.mjs +90 -0
  12. package/runtime/engine/orca-config.mjs +3 -1
  13. package/runtime/engine/removed-vocabulary.mjs +28 -0
  14. package/runtime/engine/resources-config.mjs +52 -0
  15. package/runtime/engine/runtime-root.mjs +17 -0
  16. package/runtime/engine/secrets.mjs +35 -31
  17. package/runtime/engine/sonar-config.mjs +21 -0
  18. package/runtime/engine/temp-root.mjs +32 -0
  19. package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
  20. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  21. package/runtime/knowledge/hfs/rules.yaml +98 -65
  22. package/runtime/knowledge/hfs/slots.yaml +11 -14
  23. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  24. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  25. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  26. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  27. package/runtime/modules/kernel/failure-codes.yaml +1 -41
  28. package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
  29. package/runtime/modules/models/registry.yaml +25 -359
  30. package/runtime/modules/models/runtimes.yaml +92 -256
  31. package/runtime/modules/models/tiers.yaml +65 -0
  32. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  33. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  34. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  35. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  36. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  37. package/runtime/scripts/api/git/lib.mjs +34 -1
  38. package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
  39. package/runtime/scripts/api/process/run-program.mjs +2 -1
  40. package/runtime/scripts/api/sops/lib.mjs +198 -120
  41. package/runtime/scripts/hfs/allows.mjs +3 -3
  42. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  43. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  44. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  45. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  46. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  47. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  48. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  49. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  50. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  51. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  52. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  53. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  54. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  55. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  56. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  57. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  58. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  59. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  60. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  61. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  62. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  63. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  65. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  66. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  67. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  68. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  69. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  70. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  71. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  72. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  73. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  74. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  75. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  76. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  77. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  78. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  79. package/runtime/scripts/hfs/architecture/index.mjs +158 -102
  80. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  81. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  82. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  83. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  84. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  85. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  86. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  87. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  88. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  89. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  90. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  91. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  92. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  93. package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
  94. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  95. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  96. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  97. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  98. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  99. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  100. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  101. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  102. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  103. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  104. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  105. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  106. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  107. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  108. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  109. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  110. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  111. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  112. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  113. package/runtime/scripts/hfs/architecture.mjs +3 -4
  114. package/runtime/scripts/hfs/check.mjs +61 -70
  115. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  116. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  117. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  118. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  119. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  120. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  121. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  122. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  123. package/runtime/scripts/hfs/repo-identity.mjs +5 -2
  124. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  125. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  126. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  127. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  128. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  129. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  130. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  131. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  132. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  133. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  134. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  135. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  136. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  137. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  138. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  139. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  140. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  141. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  142. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  143. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  144. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  145. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  146. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  147. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  148. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  149. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  150. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  151. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  152. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  153. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  154. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  155. package/runtime/scripts/hfs/secret.mjs +59 -48
  156. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  157. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  158. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  159. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  160. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  161. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  162. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  163. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  164. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  165. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  166. package/runtime/scripts/hfs/slots.mjs +72 -693
  167. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  168. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  169. package/runtime/scripts/hfs/tree.mjs +19 -14
  170. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  171. package/runtime/scripts/hfs/view.mjs +1 -1
  172. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  173. package/runtime/scripts/lib/event-contract.mjs +17 -13
  174. package/runtime/scripts/lib/git.mjs +2 -2
  175. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  176. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  177. package/runtime/scripts/lib/i18n.mjs +3 -2
  178. package/runtime/scripts/lib/in-order.mjs +71 -0
  179. package/runtime/scripts/lib/language.mjs +3 -3
  180. package/runtime/scripts/lib/list.mjs +8 -1
  181. package/runtime/scripts/lib/path-key.mjs +38 -5
  182. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  183. package/runtime/scripts/lib/regex.mjs +1 -1
  184. package/runtime/scripts/lib/same-text.mjs +1 -1
  185. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  186. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  187. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  188. package/runtime/scripts/lib/stack-services.mjs +1 -2
  189. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  190. package/runtime/scripts/lib/walk.mjs +6 -1
  191. package/scaffold/add-cli-lite.mjs +2 -1
  192. package/scaffold/add-table.mjs +2 -2
  193. package/scaffold/add.mjs +3 -2
  194. package/scaffold/app.mjs +4 -2
  195. package/scaffold/edition-gate.mjs +25 -25
  196. package/scaffold/lite-exports.mjs +2 -1
  197. package/scaffold/service.mjs +5 -4
  198. package/sync/hygiene.mjs +15 -9
  199. package/sync/index.mjs +7 -3
  200. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  201. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  202. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  203. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  204. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  205. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  206. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  207. package/templates/app/starciwork.gitignore +1 -1
  208. package/templates/be/image/api/Dockerfile +1 -1
  209. package/templates/be/image/cli/Dockerfile +1 -1
  210. package/templates/be/image/worker/Dockerfile +1 -1
  211. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  212. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  213. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  214. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  215. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  216. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  217. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  218. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  219. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  220. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  221. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  222. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  223. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  224. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  225. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  226. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  227. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  228. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  229. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  230. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  231. package/templates/fe/image/next/Dockerfile +2 -1
  232. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  233. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  234. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  235. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  236. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  237. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  238. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  239. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  240. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  241. package/upgrade/index.mjs +9 -2
  242. package/runtime/engine/admission.mjs +0 -308
  243. package/runtime/engine/canonical-json.mjs +0 -10
  244. package/runtime/engine/db/blob.mjs +0 -315
  245. package/runtime/engine/db/ledger-paths.mjs +0 -83
  246. package/runtime/engine/db/ledger.mjs +0 -1118
  247. package/runtime/engine/db/machine-connection.mjs +0 -135
  248. package/runtime/engine/db/machine-schema.mjs +0 -84
  249. package/runtime/engine/db/machine.mjs +0 -1367
  250. package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
  251. package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
  252. package/runtime/engine/db/provider-reservations.mjs +0 -101
  253. package/runtime/engine/digest.mjs +0 -16
  254. package/runtime/engine/refuse.mjs +0 -11
  255. package/runtime/modules/ops/_labels.yaml +0 -53
  256. package/runtime/scripts/api/node/lib.mjs +0 -14
  257. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  258. package/runtime/scripts/api/process/lib.mjs +0 -111
  259. package/runtime/scripts/api/process/owned-process.mjs +0 -78
  260. package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
  261. package/runtime/scripts/connectors/lib.mjs +0 -488
  262. package/runtime/scripts/lib/clip.mjs +0 -19
  263. package/runtime/scripts/lib/display-names.mjs +0 -259
  264. package/runtime/scripts/lib/example-refs.mjs +0 -158
  265. package/runtime/scripts/lib/json-schema.mjs +0 -52
  266. package/runtime/scripts/lib/process-identity.mjs +0 -6
  267. package/runtime/scripts/lib/read-yaml.mjs +0 -18
  268. package/runtime/scripts/lib/redact.mjs +0 -161
  269. package/runtime/scripts/lib/sleep.mjs +0 -7
  270. package/runtime/scripts/lib/source-phrases.mjs +0 -34
  271. package/runtime/scripts/lib/sqlite.mjs +0 -21
@@ -1,8 +1,9 @@
1
1
  import path from 'node:path';
2
2
  import { machineKit } from './machine-ast.mjs';
3
3
  import { isServerActionModule } from './server-action.mjs';
4
- import { callName, chainParts, contextTypesClient, isDatabaseType, isSupabaseCall, isSupabaseValue, moduleNameOf, reportAt, signatureFromSupabase, supabaseClientType, SUPABASE_MODULES, unwrap } from './supabase-ast.mjs';
4
+ import { callName, chainParts, contextTypesClient, isDatabaseType, isSupabaseCall, moduleNameOf, reportAt, signatureFromSupabase, supabaseClientType, SUPABASE_MODULES, unwrap } from './supabase-ast.mjs';
5
5
  import { checkBackendJwt } from './supabase-be.mjs';
6
+ import { checkFrontendQueryResults, FE_DB_SLOTS, sameSymbol, symbolOf } from './supabase-results.mjs';
6
7
 
7
8
  /**
8
9
  * L10-L16, the slot-driven Supabase machine. Supabase is one data transport with one owner on each side:
@@ -22,193 +23,74 @@ export const BACKEND_SUPABASE_RULE_IDS = Object.freeze(['BE_SUPABASE_CLIENT_OWNE
22
23
 
23
24
  const FE_CLIENT_OWNER = 'FE_SUPABASE_CLIENT_OWNER';
24
25
  const BE_CLIENT_OWNER = 'BE_SUPABASE_CLIENT_OWNER';
25
- const FE_RESULT_TYPED = 'FE_DB_RESULT_TYPED';
26
- const FE_ERROR_HANDLED = 'FE_DB_ERROR_HANDLED';
27
26
  const FE_SERVICE_ROLE = 'FE_SERVICE_ROLE_FORBIDDEN';
28
27
  const FE_SESSION_TRUST = 'FE_AUTH_SESSION_TRUST';
29
28
  const FE_WRITE_SHAPE = 'FE_DB_WRITE_SHAPE';
30
29
  const FE_ROUTE_HANDLER = 'FE_ROUTE_HANDLER_FORBIDDEN';
31
- const FE_DB_SLOTS = new Set(['fe.modules.db', 'fe.package.db', 'fe.modules.db.outcome']);
32
30
  const BE_SUPABASE_SLOT = 'be.integrations.supabase';
33
31
  const FE_CONFIG_SLOT = 'fe.modules.config';
34
32
  const SUPABASE_CALLS = new Set(['from', 'rpc']);
35
- const QUERY_TERMINALS = new Set(['single', 'maybeSingle', 'returns']);
36
- const QUERY_WRITES = new Set(['insert', 'update', 'upsert', 'delete']);
37
- const LIST_BOUNDS = new Set(['gt', 'gte', 'lt', 'lte', 'range']);
38
33
  const supabaseSlotEnabled = (input, slotId) => {
39
34
  const slot = input.graph.resolver.slot(slotId);
40
35
  return Boolean(slot && input.graph.resolver.slotEnabled(slot));
41
36
  };
42
37
 
43
- function checkClientOwnership(input, { slots, ruleId }) {
44
- const kit = machineKit(input);
45
- const { ts } = kit;
46
- const violations = [];
47
- let imports = 0;
48
- let clients = 0;
49
- let calls = 0;
50
- for (const file of input.graph.files.values()) {
51
- const checker = kit.checkerOf(file.sourceFile);
52
- const owned = slots.has(file.slot);
53
- kit.walk(file.sourceFile, node => {
54
- const moduleName = moduleNameOf(ts, node);
55
- if (moduleName && SUPABASE_MODULES.has(moduleName)) {
56
- imports += 1;
57
- if (!owned) reportAt(violations, kit, ruleId, file, node, `${moduleName} is imported by ${file.rel}, whose slot is ${file.slot ?? 'unowned'}; Supabase imports belong only to ${[...slots].join(' or ')}.`, { module: moduleName, slot: file.slot ?? null });
58
- }
59
- if (ts.isTypeReferenceNode(node) && supabaseClientType(kit, checker, node)) {
60
- clients += 1;
61
- if (!isDatabaseType(ts, node.typeArguments?.[0])) reportAt(violations, kit, ruleId, file, node, 'SupabaseClient must carry the committed Database type as SupabaseClient<Database>.');
62
- }
63
- if (!ts.isCallExpression(node)) return true;
64
- const binding = kit.importBinding(checker, node.expression);
65
- if (binding && SUPABASE_MODULES.has(binding.module) && /^create(?:Client|ServerClient|BrowserClient)$/u.test(binding.name)) {
66
- clients += 1;
67
- if (!isDatabaseType(ts, node.typeArguments?.[0]) && !contextTypesClient(kit, checker, node)) {
68
- reportAt(violations, kit, ruleId, file, node, `${binding.name} creates an untyped Supabase client; create it as ${binding.name}<Database>(...) (or give it the contextual type SupabaseClient<Database>).`);
69
- }
70
- }
71
- if (!isSupabaseCall(kit, checker, node)) return true;
72
- const properties = chainParts(ts, node);
73
- const method = callName(ts, node);
74
- if (!SUPABASE_CALLS.has(method) && !properties.includes('auth') && !properties.includes('storage')) return true;
75
- calls += 1;
76
- if (!owned) reportAt(violations, kit, ruleId, file, node, `A Supabase ${method ?? 'client'} call is made from ${file.slot ?? 'an unowned path'}; database, auth and storage calls belong only to ${[...slots].join(' or ')}.`, { method, slot: file.slot ?? null });
77
- return true;
78
- });
79
- }
80
- return { violations, coverage: { status: 'checked', imports, clients, calls, ownerSlots: [...slots] } };
81
- }
82
-
83
- const symbolOf = (kit, checker, node) => kit.aliased(checker, kit.symbolAt(checker, node)) ?? kit.symbolAt(checker, node);
38
+ const CLIENT_FACTORY = /^create(?:Client|ServerClient|BrowserClient)$/u;
84
39
 
85
- const sameSymbol = (kit, checker, node, symbol) => Boolean(symbol && symbolOf(kit, checker, node) === symbol);
86
-
87
- const fromAwaitedSupabase = (kit, checker, node, seen = new Set()) => {
88
- const current = unwrap(kit.ts, node);
89
- if (!current || seen.has(current)) return false;
90
- seen.add(current);
91
- if (kit.ts.isCallExpression(current)) return isSupabaseCall(kit, checker, current);
92
- if (!kit.ts.isIdentifier(current)) return false;
93
- for (const declaration of kit.declarationsOf(checker, current)) {
94
- if (kit.ts.isVariableDeclaration(declaration) && declaration.initializer && fromAwaitedSupabase(kit, checker, declaration.initializer, seen)) return true;
95
- }
96
- return false;
97
- };
98
-
99
- const callIsToOutcome = (kit, checker, graph, call) => {
100
- if (!kit.ts.isCallExpression(call)) return false;
101
- const expression = call.expression;
102
- const name = kit.ts.isIdentifier(expression) ? expression.text : (kit.ts.isPropertyAccessExpression(expression) ? expression.name.text : null);
103
- if (name !== 'toOutcome') return false;
104
- return kit.declarationsOf(checker, kit.ts.isPropertyAccessExpression(expression) ? expression.name : expression)
105
- .some(declaration => {
106
- const rel = kit.graphPath(declaration);
107
- return rel !== null && FE_DB_SLOTS.has(graph.files.get(rel)?.slot);
108
- });
109
- };
40
+ function inspectSupabaseImport(scope, node) {
41
+ const { kit, file, slots, ruleId, violations, counters } = scope;
42
+ const moduleName = moduleNameOf(kit.ts, node);
43
+ if (!moduleName || !SUPABASE_MODULES.has(moduleName)) return;
44
+ counters.imports += 1;
45
+ if (!slots.has(file.slot)) reportAt(violations, kit, ruleId, file, node, `${moduleName} is imported by ${file.rel}, whose slot is ${file.slot ?? 'unowned'}; Supabase imports belong only to ${[...slots].join(' or ')}.`, { module: moduleName, slot: file.slot ?? null });
46
+ }
110
47
 
111
- const parentCall = (ts, node) => {
112
- let current = node;
113
- while (current.parent && ts.isParenthesizedExpression(current.parent)) current = current.parent;
114
- return ts.isCallExpression(current.parent) ? current.parent : null;
115
- };
48
+ function inspectClientTypeReference(scope, node) {
49
+ const { kit, checker, file, ruleId, violations, counters } = scope;
50
+ if (!kit.ts.isTypeReferenceNode(node) || !supabaseClientType(kit, checker, node)) return;
51
+ counters.clients += 1;
52
+ if (!isDatabaseType(kit.ts, node.typeArguments?.[0])) reportAt(violations, kit, ruleId, file, node, 'SupabaseClient must carry the committed Database type as SupabaseClient<Database>.');
53
+ }
116
54
 
117
- const handledAwait = (kit, checker, graph, awaitNode) => {
118
- const direct = parentCall(kit.ts, awaitNode);
119
- if (direct && callIsToOutcome(kit, checker, graph, direct) && direct.arguments.some(argument => argument === awaitNode || argument === awaitNode.parent)) return true;
120
- let cursor = awaitNode;
121
- while (cursor.parent && kit.ts.isParenthesizedExpression(cursor.parent)) cursor = cursor.parent;
122
- const declaration = kit.ts.isVariableDeclaration(cursor.parent) ? cursor.parent : null;
123
- if (!declaration) return false;
124
- if (kit.ts.isIdentifier(declaration.name)) {
125
- const result = symbolOf(kit, checker, declaration.name);
126
- let handled = false;
127
- kit.walk(fileOf(awaitNode), node => {
128
- if (kit.ts.isPropertyAccessExpression(node) && node.name.text === 'error' && sameSymbol(kit, checker, node.expression, result)) handled = true;
129
- if (kit.ts.isElementAccessExpression(node) && kit.ts.isStringLiteralLike(node.argumentExpression) && node.argumentExpression.text === 'error' && sameSymbol(kit, checker, node.expression, result)) handled = true;
130
- if (kit.ts.isCallExpression(node) && callIsToOutcome(kit, checker, graph, node) && node.arguments.some(argument => sameSymbol(kit, checker, unwrap(kit.ts, argument), result))) handled = true;
131
- return !handled;
132
- });
133
- return handled;
55
+ function inspectClientFactoryCall(scope, node) {
56
+ const { kit, checker, file, ruleId, violations, counters } = scope;
57
+ const binding = kit.importBinding(checker, node.expression);
58
+ if (!binding || !SUPABASE_MODULES.has(binding.module) || !CLIENT_FACTORY.test(binding.name)) return;
59
+ counters.clients += 1;
60
+ if (!isDatabaseType(kit.ts, node.typeArguments?.[0]) && !contextTypesClient(kit, checker, node)) {
61
+ reportAt(violations, kit, ruleId, file, node, `${binding.name} creates an untyped Supabase client; create it as ${binding.name}<Database>(...) (or give it the contextual type SupabaseClient<Database>).`);
134
62
  }
135
- if (!kit.ts.isObjectBindingPattern(declaration.name)) return false;
136
- const errorBinding = declaration.name.elements.find(element => {
137
- const property = element.propertyName ?? element.name;
138
- return kit.ts.isIdentifier(property) && property.text === 'error';
139
- });
140
- if (!errorBinding || !kit.ts.isIdentifier(errorBinding.name)) return false;
141
- const error = symbolOf(kit, checker, errorBinding.name);
142
- let references = 0;
143
- kit.walk(fileOf(awaitNode), node => {
144
- if (kit.ts.isIdentifier(node) && sameSymbol(kit, checker, node, error)) references += 1;
145
- return true;
146
- });
147
- return references > 1;
148
- };
63
+ }
149
64
 
150
- const fileOf = node => node.getSourceFile();
65
+ function inspectProviderCall(scope, node) {
66
+ const { kit, checker, file, slots, ruleId, violations, counters } = scope;
67
+ const { ts } = kit;
68
+ if (!isSupabaseCall(kit, checker, node)) return;
69
+ const properties = chainParts(ts, node);
70
+ const method = callName(ts, node);
71
+ if (!SUPABASE_CALLS.has(method) && !properties.includes('auth') && !properties.includes('storage')) return;
72
+ counters.calls += 1;
73
+ if (!slots.has(file.slot)) reportAt(violations, kit, ruleId, file, node, `A Supabase ${method ?? 'client'} call is made from ${file.slot ?? 'an unowned path'}; database, auth and storage calls belong only to ${[...slots].join(' or ')}.`, { method, slot: file.slot ?? null });
74
+ }
151
75
 
152
- const dataSymbolFromAwait = (kit, checker, identifier) => {
153
- const symbol = symbolOf(kit, checker, identifier);
154
- for (const declaration of symbol?.declarations ?? []) {
155
- if (!kit.ts.isBindingElement(declaration) || !kit.ts.isObjectBindingPattern(declaration.parent)) continue;
156
- const property = declaration.propertyName ?? declaration.name;
157
- if (!kit.ts.isIdentifier(property) || property.text !== 'data') continue;
158
- const variable = declaration.parent.parent;
159
- if (kit.ts.isVariableDeclaration(variable) && variable.initializer && fromAwaitedSupabase(kit, checker, variable.initializer)) return symbol;
160
- }
161
- return null;
162
- };
76
+ function inspectOwnershipNode(scope, node) {
77
+ inspectSupabaseImport(scope, node);
78
+ inspectClientTypeReference(scope, node);
79
+ if (!scope.kit.ts.isCallExpression(node)) return;
80
+ inspectClientFactoryCall(scope, node);
81
+ inspectProviderCall(scope, node);
82
+ }
163
83
 
164
- function checkFrontendQueryResults(input) {
84
+ function checkClientOwnership(input, { slots, ruleId }) {
165
85
  const kit = machineKit(input);
166
- const { ts } = kit;
167
86
  const violations = [];
168
- let awaited = 0;
169
- let listReads = 0;
87
+ const counters = { imports: 0, clients: 0, calls: 0 };
170
88
  for (const file of input.graph.files.values()) {
171
89
  const checker = kit.checkerOf(file.sourceFile);
172
- const seen = new Set();
173
- const report = (ruleId, node, message) => {
174
- const key = `${ruleId}:${node.pos}:${message}`;
175
- if (seen.has(key)) return;
176
- seen.add(key);
177
- reportAt(violations, kit, ruleId, file, node, message);
178
- };
179
- kit.walk(file.sourceFile, node => {
180
- if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && QUERY_TERMINALS.has(node.expression.name.text)
181
- && node.typeArguments?.length && isSupabaseValue(kit, checker, node.expression.expression)) {
182
- report(FE_RESULT_TYPED, node, `${node.expression.name.text}<T>() supplies a result generic; derive the row type from Database and let the Supabase chain infer it.`);
183
- }
184
- if ((ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node)) && fromAwaitedSupabase(kit, checker, node.expression)) {
185
- report(FE_RESULT_TYPED, node, 'A Supabase query result is cast or non-null asserted; narrow its typed data/error outcome instead.');
186
- }
187
- if (ts.isAwaitExpression(node) && isSupabaseValue(kit, checker, node.expression)) {
188
- const expression = unwrap(ts, node.expression);
189
- if (!ts.isCallExpression(expression)) return true;
190
- awaited += 1;
191
- const methods = chainParts(ts, expression);
192
- if (methods.includes('select') && !methods.some(method => QUERY_WRITES.has(method)) && !methods.includes('single') && !methods.includes('maybeSingle')) {
193
- listReads += 1;
194
- if (!methods.includes('limit') && !methods.some(method => LIST_BOUNDS.has(method))) {
195
- report(FE_RESULT_TYPED, node, 'A Supabase list read has no .limit(), .range(), or keyset bound (.gt/.gte/.lt/.lte); bound every multi-row read.');
196
- }
197
- }
198
- if (!handledAwait(kit, checker, input.graph, node)) {
199
- report(FE_ERROR_HANDLED, node, 'The awaited Supabase result does not read its error and is not passed whole to toOutcome(result).');
200
- }
201
- }
202
- if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken && ts.isArrayLiteralExpression(node.right) && node.right.elements.length === 0) {
203
- const left = unwrap(ts, node.left);
204
- const swallowed = ts.isIdentifier(left) ? Boolean(dataSymbolFromAwait(kit, checker, left))
205
- : ts.isPropertyAccessExpression(left) && left.name.text === 'data' && fromAwaitedSupabase(kit, checker, left.expression);
206
- if (swallowed) report(FE_ERROR_HANDLED, node, 'Supabase data is collapsed with ?? []; refused or unavailable is not empty data. Pass the whole result to toOutcome.');
207
- }
208
- return true;
209
- });
90
+ const scope = { kit, checker, file, slots, ruleId, violations, counters };
91
+ kit.walk(file.sourceFile, node => inspectOwnershipNode(scope, node));
210
92
  }
211
- return { violations, coverage: { status: 'checked', awaited, listReads } };
93
+ return { violations, coverage: { status: 'checked', ...counters, ownerSlots: [...slots] } };
212
94
  }
213
95
 
214
96
  const words = text => String(text).replace(/([a-z0-9])([A-Z])/gu, '$1_$2').toUpperCase();
@@ -226,33 +108,41 @@ const envRead = (ts, node) => {
226
108
  return ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null;
227
109
  };
228
110
 
111
+ /** Reports a SERVICE_ROLE identifier or string literal: the browser and Next process never hold the service role. */
112
+ function inspectServiceRoleText(ts, node, report) {
113
+ if (ts.isIdentifier(node) && serviceRoleText(node.text)) report(node, 'A SERVICE_ROLE identifier appears in the front end; the browser and Next process never hold the Supabase service role.', node.text);
114
+ if (ts.isStringLiteralLike(node) && serviceRoleText(node.text)) report(node, 'A service_role string appears in the front end; the service role belongs behind the back-end guard chain.', node.text);
115
+ }
116
+
117
+ /** Counts an environment read and reports a credential-shaped or product-specific public variable. */
118
+ function inspectEnvironmentRead(state, file, node, name) {
119
+ state.reads += 1;
120
+ if (credentialEnv(name)) state.report(node, `${name} is a credential-shaped environment read in the front end; no *_SECRET, *_PRIVATE, *_TOKEN, *_PASSWORD or service-role value enters fe/.`, name);
121
+ else if (name.startsWith('NEXT_PUBLIC_') && !CORE_PUBLIC_ENV.has(name) && file.slot !== FE_CONFIG_SLOT) {
122
+ state.report(node, `${name} is not a core Supabase/site public variable and is read outside ${FE_CONFIG_SLOT}; product public configuration is declared by the one config owner.`, name);
123
+ }
124
+ }
125
+
229
126
  function checkFrontendSecretNames(input) {
230
127
  const kit = machineKit(input);
231
128
  const { ts } = kit;
232
129
  const violations = [];
233
- let reads = 0;
130
+ const state = { reads: 0, report: null };
234
131
  for (const file of input.graph.files.values()) {
235
132
  const seen = new Set();
236
- const report = (node, message, name) => {
133
+ state.report = (node, message, name) => {
237
134
  const key = `${node.pos}:${name ?? ''}`;
238
135
  if (seen.has(key)) return;
239
136
  seen.add(key);
240
137
  reportAt(violations, kit, FE_SERVICE_ROLE, file, node, message, name ? { name } : {});
241
138
  };
242
139
  kit.walk(file.sourceFile, node => {
243
- if (ts.isIdentifier(node) && serviceRoleText(node.text)) report(node, 'A SERVICE_ROLE identifier appears in the front end; the browser and Next process never hold the Supabase service role.', node.text);
244
- if (ts.isStringLiteralLike(node) && serviceRoleText(node.text)) report(node, 'A service_role string appears in the front end; the service role belongs behind the back-end guard chain.', node.text);
140
+ inspectServiceRoleText(ts, node, state.report);
245
141
  const name = envRead(ts, node);
246
- if (!name) return true;
247
- reads += 1;
248
- if (credentialEnv(name)) report(node, `${name} is a credential-shaped environment read in the front end; no *_SECRET, *_PRIVATE, *_TOKEN, *_PASSWORD or service-role value enters fe/.`, name);
249
- else if (name.startsWith('NEXT_PUBLIC_') && !CORE_PUBLIC_ENV.has(name) && file.slot !== FE_CONFIG_SLOT) {
250
- report(node, `${name} is not a core Supabase/site public variable and is read outside ${FE_CONFIG_SLOT}; product public configuration is declared by the one config owner.`, name);
251
- }
252
- return true;
142
+ if (name) inspectEnvironmentRead(state, file, node, name);
253
143
  });
254
144
  }
255
- return { violations, coverage: { status: 'checked', envReads: reads } };
145
+ return { violations, coverage: { status: 'checked', envReads: state.reads } };
256
146
  }
257
147
 
258
148
  const isClientModule = (ts, sourceFile) => sourceFile.statements.some((statement, index) => index < 4 && ts.isExpressionStatement(statement)
@@ -260,24 +150,25 @@ const isClientModule = (ts, sourceFile) => sourceFile.statements.some((statement
260
150
 
261
151
  const isDirectiveStatement = (ts, statement) => ts.isExpressionStatement(statement) && ts.isStringLiteralLike(statement.expression);
262
152
 
263
- const exportedValues = (ts, sourceFile) => {
264
- const hasExport = node => node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword);
265
- const out = [];
266
- for (const statement of sourceFile.statements) {
267
- if (ts.isFunctionDeclaration(statement) && hasExport(statement) && statement.name && statement.body) {
268
- out.push({ name: statement.name.text, implementation: statement.body, functionNode: statement });
269
- }
270
- if (!ts.isVariableStatement(statement) || !hasExport(statement)) continue;
271
- for (const declaration of statement.declarationList.declarations) {
272
- if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
153
+ const hasExportModifier = (ts, node) => node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword);
154
+
155
+ const exportedValuesOf = (ts, statement) => {
156
+ if (ts.isFunctionDeclaration(statement)) {
157
+ return hasExportModifier(ts, statement) && statement.name && statement.body
158
+ ? [{ name: statement.name.text, implementation: statement.body, functionNode: statement }] : [];
159
+ }
160
+ if (!ts.isVariableStatement(statement) || !hasExportModifier(ts, statement)) return [];
161
+ return statement.declarationList.declarations
162
+ .filter(declaration => ts.isIdentifier(declaration.name) && declaration.initializer)
163
+ .map(declaration => {
273
164
  const functionNode = (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))
274
165
  && ts.isBlock(declaration.initializer.body) ? declaration.initializer : null;
275
- out.push({ name: declaration.name.text, implementation: declaration.initializer, functionNode });
276
- }
277
- }
278
- return out;
166
+ return { name: declaration.name.text, implementation: declaration.initializer, functionNode };
167
+ });
279
168
  };
280
169
 
170
+ const exportedValues = (ts, sourceFile) => sourceFile.statements.flatMap(statement => exportedValuesOf(ts, statement));
171
+
281
172
  const exportedActions = (ts, sourceFile) => exportedValues(ts, sourceFile).map(value => value.functionNode).filter(Boolean);
282
173
  const exportedImplementations = (ts, sourceFile, name) => exportedValues(ts, sourceFile)
283
174
  .filter(value => value.name === name).map(value => value.implementation);
@@ -293,8 +184,8 @@ const contains = (kit, root, predicate) => {
293
184
 
294
185
  const principalCall = (kit, checker, graph, node) => kit.ts.isCallExpression(node) && (() => {
295
186
  const expression = node.expression;
296
- const name = kit.ts.isIdentifier(expression) ? expression : (kit.ts.isPropertyAccessExpression(expression) ? expression.name : null);
297
- if (!name || name.text !== 'getPrincipal') return false;
187
+ const name = kit.ts.isPropertyAccessExpression(expression) ? expression.name : expression;
188
+ if (!kit.ts.isIdentifier(name) || name.text !== 'getPrincipal') return false;
298
189
  return kit.declarationsOf(checker, name).some(declaration => {
299
190
  const rel = kit.graphPath(declaration);
300
191
  return rel !== null && FE_DB_SLOTS.has(graph.files.get(rel)?.slot);
@@ -333,82 +224,119 @@ const parameterBindings = (ts, name) => {
333
224
  return [];
334
225
  };
335
226
 
227
+ function inspectServerSessionCalls(kit, file, checker, violations) {
228
+ const { ts } = kit;
229
+ if (isClientModule(ts, file.sourceFile)) return;
230
+ kit.walk(file.sourceFile, node => {
231
+ if (ts.isCallExpression(node) && callName(ts, node) === 'getSession' && signatureFromSupabase(checker, node)) {
232
+ reportAt(violations, kit, FE_SESSION_TRUST, file, node, 'auth.getSession() trusts cookie-backed session data in server code; call getPrincipal() (getClaims), and getUser() for sensitive mutations.');
233
+ }
234
+ return true;
235
+ });
236
+ }
237
+
238
+ function inspectPrincipalProvider(kit, file, checker, violations) {
239
+ const { ts } = kit;
240
+ if (!FE_DB_SLOTS.has(file.slot)) return;
241
+ for (const implementation of exportedImplementations(ts, file.sourceFile, 'getPrincipal')) {
242
+ const readsClaims = contains(kit, implementation, node => ts.isCallExpression(node) && callName(ts, node) === 'getClaims'
243
+ && signatureFromSupabase(checker, node));
244
+ if (!readsClaims) reportAt(violations, kit, FE_SESSION_TRUST, file, implementation,
245
+ 'getPrincipal() must establish the server principal through Supabase auth.getClaims().');
246
+ }
247
+ }
248
+
249
+ function inspectActionDirective(kit, file, action, violations) {
250
+ const { ts } = kit;
251
+ const first = action.body.statements[0];
252
+ const functionLevel = first?.expression?.text === 'use server' && isDirectiveStatement(ts, first);
253
+ if (!isServerActionModule(ts, file.sourceFile) && !functionLevel) {
254
+ reportAt(violations, kit, FE_WRITE_SHAPE, file, action, 'An exported write function must be a Server Action through a file-level or function-level `use server` directive.');
255
+ }
256
+ }
257
+
258
+ function inspectActionPrincipal(input, kit, file, checker, action, authenticatesAnonymous, violations) {
259
+ const { ts } = kit;
260
+ // Function directives are the prologue, not work: the principal is the first non-directive statement.
261
+ const work = action.body.statements.slice(action.body.statements.findIndex(statement => !isDirectiveStatement(ts, statement)) >>> 0);
262
+ const first = work[0];
263
+ const principal = first ? firstPrincipal(kit, checker, input.graph, first) : { call: null, symbol: null };
264
+ if (!principal.call || !principal.symbol) {
265
+ reportAt(violations, kit, FE_SESSION_TRUST, file, first ?? action, 'A write Server Action must begin with `const principal = await getPrincipal()` from the db owner.');
266
+ } else if (!authenticatesAnonymous) {
267
+ const refuses = work.slice(1).some(statement => ts.isIfStatement(statement)
268
+ && contains(kit, statement.expression, node => ts.isIdentifier(node) && sameSymbol(kit, checker, node, principal.symbol))
269
+ && typedRefusal(kit, statement.thenStatement));
270
+ if (!refuses) reportAt(violations, kit, FE_SESSION_TRUST, file, action, 'A write Server Action must refuse an anonymous principal with a typed `refused` Outcome before doing work.');
271
+ }
272
+ }
273
+
274
+ function inspectActionInputs(kit, file, action, checker, violations) {
275
+ const { ts } = kit;
276
+ for (const parameter of action.parameters.flatMap(item => parameterBindings(ts, item.name))) {
277
+ const symbol = symbolOf(kit, checker, parameter);
278
+ const references = [];
279
+ kit.walk(action.body, node => {
280
+ if (ts.isIdentifier(node) && sameSymbol(kit, checker, node, symbol)) references.push(node);
281
+ return true;
282
+ });
283
+ references.sort((a, b) => a.getStart() - b.getStart());
284
+ const firstUse = references[0];
285
+ if (firstUse && !parseUse(ts, firstUse)) reportAt(violations, kit, FE_WRITE_SHAPE, file, firstUse,
286
+ `Server Action input ${parameter.text} is used before schema.parse(...) or schema.safeParse(...).`, { parameter: parameter.text });
287
+ }
288
+ }
289
+
290
+ function inspectSensitiveAction(kit, file, checker, action, violations) {
291
+ const { ts } = kit;
292
+ const sensitive = /(?:^|\/)(?:billing|payment|payout|refund|charge|subscription|password|credential|role|permission|membership)[/.-]/iu.test(file.rel);
293
+ if (sensitive && !contains(kit, action.body, node => ts.isCallExpression(node) && callName(ts, node) === 'getUser' && signatureFromSupabase(checker, node))) {
294
+ reportAt(violations, kit, FE_SESSION_TRUST, file, action, 'A money, account-security or role-changing Server Action must call auth.getUser() before the mutation.');
295
+ }
296
+ }
297
+
298
+ function inspectWriteAction(input, kit, file, checker, action, authenticatesAnonymous, violations) {
299
+ inspectActionDirective(kit, file, action, violations);
300
+ inspectActionPrincipal(input, kit, file, checker, action, authenticatesAnonymous, violations);
301
+ inspectActionInputs(kit, file, action, checker, violations);
302
+ inspectSensitiveAction(kit, file, checker, action, violations);
303
+ }
304
+
305
+ function inspectWriteModule(input, kit, file, checker, state) {
306
+ const { ts } = kit;
307
+ if (!FE_DB_SLOTS.has(file.slot) || !path.posix.basename(file.rel).startsWith('write-')) return;
308
+ const classified = input.graph.resolver.classifyPath(file.rel);
309
+ const belowSlot = classified.root && file.rel.startsWith(`${classified.root}/`) ? file.rel.slice(classified.root.length + 1) : '';
310
+ const authenticatesAnonymous = (input.graph.resolver.slot(file.slot)?.anonymousActions ?? []).includes(belowSlot);
311
+ const declared = exportedActions(ts, file.sourceFile);
312
+ if (!declared.length) reportAt(state.violations, kit, FE_WRITE_SHAPE, file, file.sourceFile, `${file.rel} is a write module but exports no Server Action function.`);
313
+ for (const action of declared) {
314
+ state.actions += 1;
315
+ inspectWriteAction(input, kit, file, checker, action, authenticatesAnonymous, state.violations);
316
+ }
317
+ }
318
+
319
+ function inspectRouteHandler(input, kit, file, state) {
320
+ if (!file.rel.endsWith('/route.ts')) return;
321
+ state.routes += 1;
322
+ const classified = input.graph.resolver.classifyPath(file.rel);
323
+ const app = classified.bindings?.app;
324
+ const live = app !== undefined && file.rel === `apps/${app}/src/app/health/live/route.ts`;
325
+ if (!live && file.slot !== 'fe.route.callback') reportAt(state.violations, kit, FE_ROUTE_HANDLER, file, file.sourceFile,
326
+ `${file.rel} is a front-end route handler outside health/live and the fe.route.callback slot; webhooks, cron, public APIs and third-party callbacks belong to be features.`);
327
+ }
328
+
336
329
  function checkFrontendAuthAndRoutes(input) {
337
330
  const kit = machineKit(input);
338
- const { ts } = kit;
339
- const violations = [];
340
- let actions = 0;
341
- let routes = 0;
331
+ const state = { violations: [], actions: 0, routes: 0 };
342
332
  for (const file of input.graph.files.values()) {
343
333
  const checker = kit.checkerOf(file.sourceFile);
344
- if (!isClientModule(ts, file.sourceFile)) {
345
- kit.walk(file.sourceFile, node => {
346
- if (ts.isCallExpression(node) && callName(ts, node) === 'getSession' && signatureFromSupabase(checker, node)) {
347
- reportAt(violations, kit, FE_SESSION_TRUST, file, node, 'auth.getSession() trusts cookie-backed session data in server code; call getPrincipal() (getClaims), and getUser() for sensitive mutations.');
348
- }
349
- return true;
350
- });
351
- }
352
- if (FE_DB_SLOTS.has(file.slot)) {
353
- for (const implementation of exportedImplementations(ts, file.sourceFile, 'getPrincipal')) {
354
- const readsClaims = contains(kit, implementation, node => ts.isCallExpression(node) && callName(ts, node) === 'getClaims'
355
- && signatureFromSupabase(checker, node));
356
- if (!readsClaims) {
357
- reportAt(violations, kit, FE_SESSION_TRUST, file, implementation, 'getPrincipal() must establish the server principal through Supabase auth.getClaims().');
358
- }
359
- }
360
- }
361
- if (FE_DB_SLOTS.has(file.slot) && path.posix.basename(file.rel).startsWith('write-')) {
362
- const classified = input.graph.resolver.classifyPath(file.rel);
363
- const belowSlot = classified.root && file.rel.startsWith(`${classified.root}/`) ? file.rel.slice(classified.root.length + 1) : '';
364
- const authenticatesAnonymous = (input.graph.resolver.slot(file.slot)?.anonymousActions ?? []).includes(belowSlot);
365
- const declared = exportedActions(ts, file.sourceFile);
366
- if (!declared.length) reportAt(violations, kit, FE_WRITE_SHAPE, file, file.sourceFile, `${file.rel} is a write module but exports no Server Action function.`);
367
- for (const action of declared) {
368
- actions += 1;
369
- const functionLevel = action.body.statements[0]?.expression?.text === 'use server' && isDirectiveStatement(ts, action.body.statements[0]);
370
- if (!isServerActionModule(ts, file.sourceFile) && !functionLevel) {
371
- reportAt(violations, kit, FE_WRITE_SHAPE, file, action, 'An exported write function must be a Server Action through a file-level or function-level `use server` directive.');
372
- }
373
- // Function directives are the prologue, not work: the principal is the first non-directive statement.
374
- const work = action.body.statements.slice(action.body.statements.findIndex(statement => !isDirectiveStatement(ts, statement)) >>> 0);
375
- const first = work[0];
376
- const principal = first ? firstPrincipal(kit, checker, input.graph, first) : { call: null, symbol: null };
377
- if (!principal.call || !principal.symbol) {
378
- reportAt(violations, kit, FE_SESSION_TRUST, file, first ?? action, 'A write Server Action must begin with `const principal = await getPrincipal()` from the db owner.');
379
- } else if (!authenticatesAnonymous) {
380
- const refuses = work.slice(1).some(statement => ts.isIfStatement(statement)
381
- && contains(kit, statement.expression, node => ts.isIdentifier(node) && sameSymbol(kit, checker, node, principal.symbol))
382
- && typedRefusal(kit, statement.thenStatement));
383
- if (!refuses) reportAt(violations, kit, FE_SESSION_TRUST, file, action, 'A write Server Action must refuse an anonymous principal with a typed `refused` Outcome before doing work.');
384
- }
385
- for (const parameter of action.parameters.flatMap(item => parameterBindings(ts, item.name))) {
386
- const symbol = symbolOf(kit, checker, parameter);
387
- const references = [];
388
- kit.walk(action.body, node => {
389
- if (ts.isIdentifier(node) && sameSymbol(kit, checker, node, symbol)) references.push(node);
390
- return true;
391
- });
392
- const firstUse = references.sort((a, b) => a.getStart() - b.getStart())[0];
393
- if (firstUse && !parseUse(ts, firstUse)) reportAt(violations, kit, FE_WRITE_SHAPE, file, firstUse, `Server Action input ${parameter.text} is used before schema.parse(...) or schema.safeParse(...).`, { parameter: parameter.text });
394
- }
395
- const sensitive = /(?:^|\/)(?:billing|payment|payout|refund|charge|subscription|password|credential|role|permission|membership)(?:\/|-|\.)/iu.test(file.rel);
396
- if (sensitive && !contains(kit, action.body, node => ts.isCallExpression(node) && callName(ts, node) === 'getUser' && signatureFromSupabase(checker, node))) {
397
- reportAt(violations, kit, FE_SESSION_TRUST, file, action, 'A money, account-security or role-changing Server Action must call auth.getUser() before the mutation.');
398
- }
399
- }
400
- }
401
- if (file.rel.endsWith('/route.ts')) {
402
- routes += 1;
403
- const classified = input.graph.resolver.classifyPath(file.rel);
404
- const app = classified.bindings?.app;
405
- const live = app !== undefined && file.rel === `apps/${app}/src/app/health/live/route.ts`;
406
- if (!live && file.slot !== 'fe.route.callback') {
407
- reportAt(violations, kit, FE_ROUTE_HANDLER, file, file.sourceFile, `${file.rel} is a front-end route handler outside health/live and the fe.route.callback slot; webhooks, cron, public APIs and third-party callbacks belong to be features.`);
408
- }
409
- }
334
+ inspectServerSessionCalls(kit, file, checker, state.violations);
335
+ inspectPrincipalProvider(kit, file, checker, state.violations);
336
+ inspectWriteModule(input, kit, file, checker, state);
337
+ inspectRouteHandler(input, kit, file, state);
410
338
  }
411
- return { violations, coverage: { status: 'checked', actions, routes } };
339
+ return { violations: state.violations, coverage: { status: 'checked', actions: state.actions, routes: state.routes } };
412
340
  }
413
341
 
414
342
  export function checkFrontendSupabase(input) {