@starci/hfs 4.9.11 → 4.10.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 (219) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +7 -9
  3. package/db/backup-receipt.mjs +10 -47
  4. package/db/drift.mjs +1 -1
  5. package/db/dump.mjs +11 -10
  6. package/db/entity-list.mjs +1 -1
  7. package/db/migrate.mjs +83 -259
  8. package/db/new.mjs +46 -0
  9. package/db/post-cutover.mjs +13 -17
  10. package/db/postgres.mjs +80 -0
  11. package/emit/contracts.mjs +5 -9
  12. package/package.json +1 -1
  13. package/release/gate.mjs +3 -15
  14. package/runtime/knowledge/hfs/canon-pins.yaml +44 -24
  15. package/runtime/knowledge/hfs/design-truths.yaml +1 -1
  16. package/runtime/knowledge/hfs/fe-stack.yaml +4 -11
  17. package/runtime/knowledge/hfs/rules.yaml +177 -155
  18. package/runtime/knowledge/hfs/slots.yaml +164 -172
  19. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  20. package/runtime/knowledge/patterns/be/idempotency.yaml +3 -3
  21. package/runtime/knowledge/patterns/be/jobs.yaml +4 -4
  22. package/runtime/knowledge/patterns/be/projections.yaml +7 -5
  23. package/runtime/knowledge/patterns/be/queues.yaml +8 -8
  24. package/runtime/knowledge/patterns/be/reactors.yaml +2 -2
  25. package/runtime/knowledge/patterns/be/realtime.yaml +6 -6
  26. package/runtime/knowledge/patterns/be/saga.yaml +3 -3
  27. package/runtime/knowledge/patterns/be/webhooks.yaml +1 -1
  28. package/runtime/modules/kernel/command-policy.yaml +1 -1
  29. package/runtime/modules/kernel/failure-codes.yaml +65 -115
  30. package/runtime/modules/schemas/application-stacks.schema.yaml +5 -5
  31. package/runtime/scripts/hfs/architecture/canon-clones.mjs +1 -1
  32. package/runtime/scripts/hfs/architecture/{supabase-be.mjs → identity.mjs} +45 -15
  33. package/runtime/scripts/hfs/architecture/index.mjs +2 -3
  34. package/runtime/scripts/hfs/architecture/{supabase-ast.mjs → jwt-ast.mjs} +2 -103
  35. package/runtime/scripts/hfs/architecture/register-once.mjs +1 -1
  36. package/runtime/scripts/hfs/architecture/schema-owner.mjs +29 -4
  37. package/runtime/scripts/hfs/architecture/server-action.mjs +3 -4
  38. package/runtime/scripts/hfs/architecture/side-boundary.mjs +1 -1
  39. package/runtime/scripts/hfs/architecture/{supabase-keys.mjs → sql-keys.mjs} +1 -1
  40. package/runtime/scripts/hfs/architecture/sql-owner.mjs +25 -27
  41. package/runtime/scripts/hfs/architecture/transport-owner.mjs +7 -3
  42. package/runtime/scripts/hfs/check.mjs +6 -6
  43. package/runtime/scripts/hfs/coverage-scope.mjs +1 -1
  44. package/runtime/scripts/hfs/db-baseline-verb.mjs +48 -30
  45. package/runtime/scripts/hfs/db-plan-verb.mjs +17 -106
  46. package/runtime/scripts/hfs/db-restamp-verb.mjs +11 -10
  47. package/runtime/scripts/hfs/declaration-shape.mjs +15 -18
  48. package/runtime/scripts/hfs/manifest-shape.mjs +4 -4
  49. package/runtime/scripts/hfs/rule-params-shape.mjs +4 -1
  50. package/runtime/scripts/hfs/rules/a15-findings.mjs +1 -1
  51. package/runtime/scripts/hfs/rules/cli.mjs +1 -1
  52. package/runtime/scripts/hfs/rules/database-access.mjs +8 -74
  53. package/runtime/scripts/hfs/rules/database-baseline.mjs +3 -3
  54. package/runtime/scripts/hfs/rules/database-config.mjs +6 -214
  55. package/runtime/scripts/hfs/rules/database-constants.mjs +8 -10
  56. package/runtime/scripts/hfs/rules/database-migrations.mjs +3 -3
  57. package/runtime/scripts/hfs/rules/database-queues.mjs +4 -5
  58. package/runtime/scripts/hfs/rules/database-sql.mjs +1 -1
  59. package/runtime/scripts/hfs/rules/database.mjs +36 -39
  60. package/runtime/scripts/hfs/rules/edition.mjs +1 -1
  61. package/runtime/scripts/hfs/rules/{supabase-secrets.mjs → lite-secrets.mjs} +6 -6
  62. package/runtime/scripts/hfs/rules/script-hygiene.mjs +1 -10
  63. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +5 -4
  64. package/runtime/scripts/hfs/rules/stacks-release.mjs +3 -2
  65. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +2 -2
  66. package/runtime/scripts/hfs/rules/stacks.mjs +1 -1
  67. package/runtime/scripts/hfs/slot-app-view.mjs +1 -1
  68. package/runtime/scripts/hfs/slots.mjs +8 -2
  69. package/runtime/scripts/hfs/sql/pg-parse.mjs +2 -2
  70. package/runtime/scripts/lib/secret-patterns.mjs +2 -2
  71. package/runtime/scripts/lib/swarm-stack.mjs +7 -4
  72. package/scaffold/add-app.mjs +2 -10
  73. package/scaffold/add-cli-lite.mjs +6 -6
  74. package/scaffold/add-compose.mjs +1 -1
  75. package/scaffold/add-inbox-lite.mjs +4 -8
  76. package/scaffold/add-plan.mjs +1 -1
  77. package/scaffold/add-queue-lite.mjs +16 -24
  78. package/scaffold/add-table.mjs +34 -26
  79. package/scaffold/add.mjs +2 -2
  80. package/scaffold/app.mjs +19 -86
  81. package/scaffold/lite-exports.mjs +1 -1
  82. package/src/main.mjs +19 -20
  83. package/stack/target-binding.mjs +0 -60
  84. package/stack/urgent-floor.mjs +2 -2
  85. package/sync/index.mjs +32 -8
  86. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -17
  87. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +6 -8
  88. package/templates/app/codegen-lite/scripts/codegen.mjs +2 -39
  89. package/templates/app/db-bootstrap/db/bootstrap.sql +139 -0
  90. package/templates/app/gitignore-lite +0 -3
  91. package/templates/app/package-scripts/db.json +9 -0
  92. package/templates/app/package-scripts/package.json +0 -1
  93. package/templates/app/package-scripts-lite/package.json +1 -1
  94. package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
  95. package/templates/app/skeleton/.starcistacks/production/README.md +1 -1
  96. package/templates/app/skeleton/README.md +1 -1
  97. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +84 -19
  98. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +5 -5
  99. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +1 -1
  100. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +1 -1
  101. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +69 -6
  102. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -7
  103. package/templates/app/skeleton-lite/.starcistacks/production/README.md +6 -6
  104. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +1 -1
  105. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +2 -2
  106. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +69 -6
  107. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +17 -9
  108. package/templates/app/skeleton-lite/README.md +11 -11
  109. package/templates/app/skeleton-lite/{supabase → db}/migrations/__timestamp___baseline.sql +21 -1
  110. package/templates/app/tool-config-lite/prettierignore +0 -1
  111. package/templates/be/inbox/database-client.spec.ts.tpl +9 -5
  112. package/templates/be/inbox/database-client.ts.tpl +13 -4
  113. package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +47 -0
  114. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +3 -1
  115. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +2 -2
  116. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +6 -1
  117. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +39 -7
  118. package/templates/be/inbox/platform/migration.sql.tpl +22 -0
  119. package/templates/be/inbox/service.spec.ts.tpl +2 -2
  120. package/templates/be/inbox/service.ts.tpl +1 -2
  121. package/templates/be/patterns/jobs/platform-lite/job-claim.service.spec.ts.tpl +156 -0
  122. package/templates/be/patterns/jobs/platform-lite/job-claim.service.ts.tpl +114 -0
  123. package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +51 -0
  124. package/templates/be/patterns/queues/platform-lite/queue-outbox.service.spec.ts.tpl +62 -0
  125. package/templates/be/patterns/queues/platform-lite/queue-outbox.service.ts.tpl +20 -0
  126. package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +35 -0
  127. package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +4 -1
  128. package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +16 -5
  129. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +8 -8
  130. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +3 -3
  131. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +2 -2
  132. package/templates/be/skeleton-lite/apps/api/Dockerfile +0 -1
  133. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +9 -5
  134. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +7 -4
  135. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +4 -4
  136. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +3 -3
  137. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +9 -13
  138. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +4 -2
  139. package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +2 -2
  140. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +3 -2
  141. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +4 -4
  142. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +3 -3
  143. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +1 -1
  144. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +6 -6
  145. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +3 -3
  146. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/index.ts +4 -0
  147. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +22 -0
  148. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +25 -0
  149. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +12 -0
  150. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +9 -0
  151. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +35 -0
  152. package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +22 -0
  153. package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +12 -0
  154. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +23 -0
  155. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +29 -0
  156. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +22 -0
  157. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +63 -0
  158. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +11 -0
  159. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +32 -0
  160. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +69 -0
  161. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +37 -0
  162. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +36 -0
  163. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +59 -0
  164. package/templates/be/skeleton-lite/src/modules/platform/database/database.functions.ts +24 -0
  165. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +2 -1
  166. package/templates/be/skeleton-lite/src/modules/platform/database/database.options.ts +4 -2
  167. package/templates/be/skeleton-lite/src/modules/platform/database/database.sql.ts +19 -6
  168. package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +6 -0
  169. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  170. package/templates/be/skeleton-lite/src/modules/platform/database/migration-runner.service.ts +1 -1
  171. package/templates/be/skeleton-lite/src/modules/platform/database/primary.config.ts +2 -2
  172. package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +2 -2
  173. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +18 -5
  174. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +1 -1
  175. package/templates/be/table/entities.index.ts.tpl +4 -0
  176. package/templates/be/table/entity.ts.tpl +21 -0
  177. package/templates/be/table/index.ts.tpl +1 -0
  178. package/templates/be/table/module.ts.tpl +1 -1
  179. package/templates/be/table/service.api.ts.tpl +10 -11
  180. package/templates/be/table/service.ts.tpl +5 -6
  181. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +30 -0
  182. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +46 -14
  183. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +31 -0
  184. package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +2 -5
  185. package/db/local-migrate.mjs +0 -194
  186. package/db/reset.mjs +0 -125
  187. package/emit/db-types.mjs +0 -271
  188. package/runtime/scripts/api/supabase/database-dump.mjs +0 -5
  189. package/runtime/scripts/api/supabase/database-push.mjs +0 -5
  190. package/runtime/scripts/api/supabase/database-query.mjs +0 -5
  191. package/runtime/scripts/api/supabase/lib.mjs +0 -55
  192. package/runtime/scripts/api/supabase/migration-repair.mjs +0 -5
  193. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +0 -50
  194. package/runtime/scripts/hfs/architecture/supabase.mjs +0 -226
  195. package/scaffold/supabase-ports.mjs +0 -26
  196. package/templates/app/package-scripts/supabase.json +0 -7
  197. package/templates/app/skeleton-lite/supabase/config.toml +0 -48
  198. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +0 -5
  199. package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +0 -33
  200. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +0 -33
  201. package/templates/be/inbox/sql.ts.tpl +0 -16
  202. package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +0 -33
  203. package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +0 -12
  204. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -16
  205. package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +0 -6
  206. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +0 -27
  207. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.ts +0 -24
  208. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.config.ts +0 -8
  209. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.decorators.ts +0 -17
  210. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts +0 -32
  211. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.options.ts +0 -24
  212. package/templates/be/table/rows.ts.tpl +0 -4
  213. package/templates/be/table/sql.api.ts.tpl +0 -7
  214. package/templates/be/table/sql.ts.tpl +0 -4
  215. package/templates/fe/auth/supabase-client.ts.tpl +0 -38
  216. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +0 -11
  217. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +0 -18
  218. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +0 -19
  219. /package/templates/app/skeleton-lite/{supabase → db}/seed.sql +0 -0
@@ -1,12 +1,17 @@
1
- // supabase-be.mjs - the back-end half of the slot-driven Supabase architecture machine: a single integration owns
2
- // JWT verification, pins the remote JWKS/issuer/audience/algorithms, and never trusts authorization claims from a body.
1
+ // identity.mjs - the back-end identity machine. Under lite the one token verifier is be.integrations.passport: jose's jwtVerify
2
+ // there checks the app's OWN key set (createLocalJWKSet over the keys it signs with, never createRemoteJWKSet), pins issuer,
3
+ // audience and algorithms, and no file decodes a token unverified or reads authorization claims from a request body (R329).
4
+ // In both editions every jwtVerify call holds the JWT policy (R260, jwtPolicyProblems).
3
5
  import path from 'node:path';
4
6
  import { machineKit } from './machine-ast.mjs';
5
- import { importedFrom, reportAt, unwrap } from './supabase-ast.mjs';
7
+ import { importedFrom, jwtPolicyProblems, reportAt, unwrap } from './jwt-ast.mjs';
6
8
  import { accessName } from '../../lib/ts-ast.mjs';
7
9
 
8
- const BE_SUPABASE_SLOT = 'be.integrations.supabase';
9
- const BE_JWT_VERIFIED = 'BE_SUPABASE_JWT_VERIFIED';
10
+ const PASSPORT_SLOT = 'be.integrations.passport';
11
+ const BE_JWT_VERIFIED = 'BE_AUTH_JWT_OWN_JWKS';
12
+
13
+ /** The rule ids the back-end identity machine reports. */
14
+ export const IDENTITY_RULE_IDS = Object.freeze(['BE_AUTH_JWT_OWN_JWKS', 'BE_JWT_ALG_PINNED']);
10
15
  const JWT_MODULES = new Set(['jose', 'jsonwebtoken', 'jwt-decode']);
11
16
  const CLAIM_NAMES = new Set(['role', 'roles', 'membership', 'memberships']);
12
17
 
@@ -33,7 +38,7 @@ const staticStrings = (kit, checker, node) => {
33
38
 
34
39
  const jwksArgument = (kit, checker, node) => {
35
40
  const current = unwrap(kit.ts, node);
36
- if (kit.ts.isCallExpression(current)) return importedFrom(kit, checker, current.expression, new Set(['jose']), new Set(['createRemoteJWKSet']));
41
+ if (kit.ts.isCallExpression(current)) return importedFrom(kit, checker, current.expression, new Set(['jose']), new Set(['createLocalJWKSet']));
37
42
  if (!kit.ts.isIdentifier(current)) return false;
38
43
  return kit.declarationsOf(checker, current).some((declaration) => kit.ts.isVariableDeclaration(declaration) && declaration.initializer
39
44
  && jwksArgument(kit, checker, declaration.initializer));
@@ -69,8 +74,8 @@ const fromRequestBody = (kit, checker, node, seen = new Set()) => {
69
74
 
70
75
  function verifyJwtCall(kit, checker, file, node, counters, violations) {
71
76
  counters.verifyCalls += 1;
72
- if (file.slot !== BE_SUPABASE_SLOT) {
73
- reportAt(violations, kit, BE_JWT_VERIFIED, file, node, `jwtVerify is called outside ${BE_SUPABASE_SLOT}; the integration is the one token verifier.`);
77
+ if (file.slot !== PASSPORT_SLOT) {
78
+ reportAt(violations, kit, BE_JWT_VERIFIED, file, node, `jwtVerify is called outside ${PASSPORT_SLOT}; the integration is the one token verifier.`);
74
79
  return true;
75
80
  }
76
81
  const options = objectLiteral(kit, checker, node.arguments[2]);
@@ -82,10 +87,10 @@ function verifyJwtCall(kit, checker, file, node, counters, violations) {
82
87
  const issuer = property('issuer');
83
88
  const audience = values('audience');
84
89
  const algorithms = values('algorithms');
85
- const good = jwksArgument(kit, checker, node.arguments[1]) && issuer != null && audience?.includes('authenticated')
90
+ const good = jwksArgument(kit, checker, node.arguments[1]) && issuer != null && audience !== null && audience.length > 0
86
91
  && algorithms !== null && algorithms.length > 0;
87
92
  if (!good) {
88
- reportAt(violations, kit, BE_JWT_VERIFIED, file, node, 'Supabase jwtVerify must use createRemoteJWKSet(...) and pin issuer, audience `authenticated`, and a non-empty algorithms list; jwtVerify then enforces signature and exp.');
93
+ reportAt(violations, kit, BE_JWT_VERIFIED, file, node, 'jwtVerify must check the app\'s own key set (createLocalJWKSet(...) over the keys integrations/passport signs with) and pin issuer, audience and a non-empty algorithms list; jwtVerify then enforces signature and exp.');
89
94
  } else counters.verifiedCalls += 1;
90
95
  return false;
91
96
  }
@@ -93,7 +98,7 @@ function verifyJwtCall(kit, checker, file, node, counters, violations) {
93
98
  function inspectJwtCall(kit, checker, file, node, counters, violations) {
94
99
  const binding = importedCall(kit, checker, node);
95
100
  if (binding && JWT_MODULES.has(binding.module) && ['decode', 'decodeJwt', 'jwtDecode'].includes(binding.name)) {
96
- reportAt(violations, kit, BE_JWT_VERIFIED, file, node, `${binding.module}.${binding.name} decodes a token without proving its signature; use the Supabase JWKS verifier.`, { module: binding.module, method: binding.name });
101
+ reportAt(violations, kit, BE_JWT_VERIFIED, file, node, `${binding.module}.${binding.name} decodes a token without proving its signature; verify it through integrations/passport.`, { module: binding.module, method: binding.name });
97
102
  }
98
103
  if (binding?.module === 'jose' && binding.name === 'jwtVerify') return verifyJwtCall(kit, checker, file, node, counters, violations);
99
104
  return false;
@@ -118,10 +123,10 @@ function inspectBackendNode(kit, checker, file, node, counters, violations) {
118
123
  if (!handled) inspectRequestClaims(kit, file, node, checker, violations);
119
124
  }
120
125
 
121
- export function checkBackendJwt(input) {
126
+ function checkOwnJwks(input) {
122
127
  const kit = machineKit(input);
123
128
  const violations = [];
124
- const integrations = [...input.graph.files.values()].filter((file) => file.slot === BE_SUPABASE_SLOT);
129
+ const integrations = [...input.graph.files.values()].filter((file) => file.slot === PASSPORT_SLOT);
125
130
  if (!integrations.length) return { violations, coverage: { status: 'not-applicable', integrationFiles: 0, verifiedCalls: 0 } };
126
131
  const counters = { verifyCalls: 0, verifiedCalls: 0 };
127
132
  for (const file of input.graph.files.values()) {
@@ -129,8 +134,33 @@ export function checkBackendJwt(input) {
129
134
  kit.walk(file.sourceFile, node => inspectBackendNode(kit, checker, file, node, counters, violations));
130
135
  }
131
136
  if (counters.verifyCalls === 0) {
132
- const anchor = integrations.find((file) => path.posix.basename(file.rel).endsWith('.jwks.ts')) ?? integrations[0];
133
- reportAt(violations, kit, BE_JWT_VERIFIED, anchor, anchor.sourceFile, `${BE_SUPABASE_SLOT} has no jose.jwtVerify call backed by createRemoteJWKSet.`);
137
+ const anchor = integrations.find((file) => path.posix.basename(file.rel).endsWith('.tokens.ts')) ?? integrations[0];
138
+ reportAt(violations, kit, BE_JWT_VERIFIED, anchor, anchor.sourceFile, `${PASSPORT_SLOT} has no jose.jwtVerify call backed by createLocalJWKSet over the app's own keys.`);
134
139
  }
135
140
  return { violations, coverage: { status: 'checked', integrationFiles: integrations.length, verifyCalls: counters.verifyCalls, verifiedCalls: counters.verifiedCalls } };
136
141
  }
142
+
143
+ /** R260 BE_JWT_ALG_PINNED: every jose jwtVerify call pins a literal secure algorithm list, a configured issuer and the authenticated role. */
144
+ export function checkJwtPolicy(input) {
145
+ const kit = machineKit(input);
146
+ const violations = [];
147
+ let verifyCalls = 0;
148
+ for (const file of input.graph.files.values()) {
149
+ const checker = kit.checkerOf(file.sourceFile);
150
+ kit.walk(file.sourceFile, node => {
151
+ if (!kit.ts.isCallExpression(node) || !kit.isImportOf(checker, node.expression, 'jwtVerify', 'jose')) return;
152
+ verifyCalls += 1;
153
+ const problems = jwtPolicyProblems(kit.ts, checker, node);
154
+ if (problems.length) reportAt(violations, kit, 'BE_JWT_ALG_PINNED', file, node, problems.join(' '));
155
+ });
156
+ }
157
+ return { violations, coverage: { status: 'checked', verifyCalls } };
158
+ }
159
+
160
+ /** The back-end identity machine: the own-JWKS law where the passport integration exists (lite), the JWT policy everywhere. */
161
+ export function checkBackendIdentity(input) {
162
+ const slot = input.graph.resolver.slot(PASSPORT_SLOT);
163
+ const own = slot && input.graph.resolver.slotEnabled(slot) ? checkOwnJwks(input) : { violations: [], coverage: { status: 'not-applicable', reason: 'no passport integration slot (full edition)' } };
164
+ const policy = checkJwtPolicy(input);
165
+ return { violations: [...own.violations, ...policy.violations], coverage: { status: 'checked', ownJwks: own.coverage, jwtPolicy: policy.coverage } };
166
+ }
@@ -48,7 +48,7 @@ import { checkPackageShape, PACKAGE_SHAPE_RULE_IDS } from './package-shape.mjs';
48
48
  import { checkFeSlotAllows, FE_SLOT_ALLOWS_RULE_IDS } from './fe-slot-allows.mjs';
49
49
  import { checkI18nKeys, I18N_KEYS_RULE_IDS } from './i18n-keys.mjs';
50
50
  import { checkDocLanguage, DOC_LANGUAGE_RULE_IDS } from './doc-language.mjs';
51
- import { checkBackendSupabase, checkFrontendSupabase, BACKEND_SUPABASE_RULE_IDS, FRONTEND_SUPABASE_RULE_IDS } from './supabase.mjs';
51
+ import { checkBackendIdentity, IDENTITY_RULE_IDS } from './identity.mjs';
52
52
  import { checkServerActions, SERVER_ACTION_RULE_IDS } from './server-action.mjs';
53
53
  import { LINT_CODES, onLintSurface } from './surface.mjs';
54
54
  import { byCodeUnit } from '../../lib/list.mjs';
@@ -86,7 +86,7 @@ const BACKEND_MACHINE = {
86
86
  backgroundUnowned: [checkBackgroundUnowned, BACKGROUND_UNOWNED_RULE_IDS],
87
87
  testWorldFiles: [checkTestWorldFiles, TEST_WORLD_FILES_RULE_IDS],
88
88
  contractFixtureGuard: [checkContractFixtureGuard, CONTRACT_FIXTURE_GUARD_RULE_IDS],
89
- supabaseBackend: [checkBackendSupabase, BACKEND_SUPABASE_RULE_IDS],
89
+ identity: [checkBackendIdentity, IDENTITY_RULE_IDS],
90
90
  };
91
91
 
92
92
  // The frontend repository machine (R21, R50, R54, R55, R56, R63, R94, R106): same shape, run for a front-end repository only.
@@ -99,7 +99,6 @@ const FRONTEND_MACHINE = {
99
99
  crossAppDuplicate: [checkCrossAppDuplicate, CROSS_APP_DUPLICATE_RULE_IDS],
100
100
  feSlotAllows: [checkFeSlotAllows, FE_SLOT_ALLOWS_RULE_IDS],
101
101
  i18nKeys: [checkI18nKeys, I18N_KEYS_RULE_IDS],
102
- supabaseFrontend: [checkFrontendSupabase, FRONTEND_SUPABASE_RULE_IDS],
103
102
  serverActions: [checkServerActions, SERVER_ACTION_RULE_IDS],
104
103
  };
105
104
 
@@ -1,11 +1,9 @@
1
- // supabase-ast.mjs - shared TypeScript-symbol and expression helpers for the front-end and back-end Supabase
2
- // architecture checks. Recognition follows imported declarations and inferred types, never local spelling alone.
1
+ // jwt-ast.mjs - shared TypeScript-symbol and expression helpers of the identity checks (token verification and the principal).
2
+ // Recognition follows imported declarations and inferred types, never local spelling alone.
3
3
  import { unwrapExpression } from '../../lib/ts-analysis.mjs';
4
4
  import { accessName as memberName, hasAnyFlag } from '../../lib/ts-ast.mjs';
5
5
  export { accessName as memberName } from '../../lib/ts-ast.mjs';
6
6
 
7
- export const SUPABASE_MODULES = Object.freeze({ has: name => typeof name === 'string' && name.startsWith('@supabase/') });
8
-
9
7
  export const moduleNameOf = (ts, node) => {
10
8
  if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) return node.moduleSpecifier.text;
11
9
  if (!ts.isCallExpression(node)) return null;
@@ -19,39 +17,8 @@ export const importedFrom = (kit, checker, node, modules, names = null) => {
19
17
  return Boolean(binding && modules.has(binding.module) && (names === null || names.has(binding.name)));
20
18
  };
21
19
 
22
- const declarationFromSupabase = (declaration) => /(?:^|\/)node_modules\/@supabase\/[^/]+(?:\/|$)/u
23
- .test(String(declaration?.getSourceFile?.().fileName ?? '').replaceAll('\\', '/'));
24
-
25
- const typeFromSupabase = (checker, node) => {
26
- let type;
27
- try { type = checker.getTypeAtLocation(node); } catch { return false; }
28
- const queue = [type];
29
- const seen = new Set();
30
- while (queue.length) {
31
- const item = queue.shift();
32
- if (!item || seen.has(item)) continue;
33
- seen.add(item);
34
- if ([item.symbol, item.aliasSymbol].some((symbol) => symbol?.declarations?.some(declarationFromSupabase))) return true;
35
- if (item.types) queue.push(...item.types);
36
- if (item.target && item.target !== item) queue.push(item.target);
37
- }
38
- return false;
39
- };
40
-
41
- const signatureFromSupabase = (checker, call) => {
42
- try { return declarationFromSupabase(checker.getResolvedSignature(call)?.declaration); } catch { return false; }
43
- };
44
-
45
20
  export const unwrap = (ts, node) => unwrapExpression(ts, node, { awaitExpressions: true, satisfiesExpressions: false });
46
21
 
47
- const isSupabaseValue = (kit, checker, node) => {
48
- const current = unwrap(kit.ts, node);
49
- if (!current) return false;
50
- if (importedFrom(kit, checker, current, SUPABASE_MODULES)) return true;
51
- if (kit.ts.isCallExpression(current) && signatureFromSupabase(checker, current)) return true;
52
- return typeFromSupabase(checker, current);
53
- };
54
-
55
22
  export const callName = (ts, call) => ts.isCallExpression(call) ? memberName(ts, call.expression) : null;
56
23
 
57
24
  export const chainParts = (ts, expression) => {
@@ -73,74 +40,6 @@ export const chainParts = (ts, expression) => {
73
40
  return names;
74
41
  };
75
42
 
76
- export const isSupabaseCall = (kit, checker, node) => kit.ts.isCallExpression(node)
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
- };
118
-
119
- export const isDatabaseType = (ts, node) => {
120
- if (!node) return false;
121
- if (ts.isTypeReferenceNode(node)) {
122
- const name = ts.isIdentifier(node.typeName) ? node.typeName.text : node.typeName.right.text;
123
- return name === 'Database';
124
- }
125
- return false;
126
- };
127
-
128
- export const supabaseClientType = (kit, checker, node) => {
129
- if (!node || !kit.ts.isTypeReferenceNode(node)) return false;
130
- const name = kit.ts.isIdentifier(node.typeName) ? node.typeName : null;
131
- return Boolean(name && importedFrom(kit, checker, name, SUPABASE_MODULES, new Set(['SupabaseClient'])));
132
- };
133
-
134
- export const contextTypesClient = (kit, checker, call) => {
135
- let parent = call.parent;
136
- while (parent && (kit.ts.isParenthesizedExpression(parent) || kit.ts.isAsExpression(parent))) {
137
- if (kit.ts.isAsExpression(parent) && supabaseClientType(kit, checker, parent.type) && isDatabaseType(kit.ts, parent.type.typeArguments?.[0])) return true;
138
- parent = parent.parent;
139
- }
140
- return kit.ts.isVariableDeclaration(parent) && supabaseClientType(kit, checker, parent.type)
141
- && isDatabaseType(kit.ts, parent.type.typeArguments?.[0]);
142
- };
143
-
144
43
  export const reportAt = (violations, kit, ruleId, file, node, message, extra = {}) => {
145
44
  violations.push({ ruleId, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
146
45
  };
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { isComposeGroup, machineKit } from './machine-ast.mjs';
3
- import { callName } from './supabase-ast.mjs';
3
+ import { callName } from './jwt-ast.mjs';
4
4
  import { isTestWorldSlot } from '../test-world-slot.mjs';
5
5
  import { byCodeUnit } from '../../lib/list.mjs';
6
6
 
@@ -20,6 +20,11 @@ import { byCodeUnit } from '../../lib/list.mjs';
20
20
  * (`<c>` the camelCase capability name), and no other name coming from `persistence/`;
21
21
  * - a migration file is `<epochMs13>-<kebab-name>.ts`, holds a class `<Pascal><epochMs13>` and a `name` property equal to
22
22
  * that class name, and has no unit spec beside it.
23
+ *
24
+ * Lite (persistence.md BE-PERSISTENCE-3, ruling lite-persistence-typeorm-entities) has entities and no be migration: the
25
+ * capability's `persistence/entities/index.ts` declares `<c>Entities`, its `index.ts` exports that one persistence name, and
26
+ * `platform/database/entities.ts` names it in `PRIMARY_ENTITIES`, the one list the app composition registers; there is a
27
+ * single primary connection, so the per-connection checks do not apply.
23
28
  */
24
29
  export const SCHEMA_OWNER_RULE_IDS = ['BE_SCHEMA_OWNER'];
25
30
 
@@ -27,6 +32,8 @@ const RULE = 'BE_SCHEMA_OWNER';
27
32
  const PERSISTENCE_SLOT = 'be.persistence';
28
33
  const SCHEMA_FOLDERS = new Set(['entities', 'migrations']);
29
34
  const MIGRATION_FILE = /^(\d{13})-([a-z0-9]+(?:-[a-z0-9]+)*)\.ts$/u;
35
+ /** The lite entity list every capability's `<c>Entities` is named in (BE-PERSISTENCE-3). */
36
+ const LITE_ENTITY_LIST = `${DATABASE_DIR}/entities.ts`;
30
37
  const camel = name => { const text = pascal(name); return text[0].toLowerCase() + text.slice(1); };
31
38
 
32
39
  function noteSchemaOwner(found, persistence, kind) {
@@ -127,6 +134,22 @@ function reportRegistrationSites(registered, connections, resolver, report) {
127
134
  }
128
135
  }
129
136
 
137
+ /** The identifiers a file of the graph declares or names, for the lite list checks. */
138
+ function identifiersOf(ts, file) {
139
+ const names = new Set();
140
+ const visit = node => { if (ts.isIdentifier(node)) names.add(node.text); ts.forEachChild(node, visit); };
141
+ if (file) visit(file.sourceFile);
142
+ return names;
143
+ }
144
+
145
+ function reportLiteEntityArrays(key, kinds, graph, ts, plain) {
146
+ const wanted = `${camel(kinds.name)}Entities`;
147
+ const listFile = `${key}/persistence/entities/index.ts`;
148
+ const declared = new Set((graph.files.get(listFile)?.sourceFile.statements.filter(ts.isVariableStatement).flatMap(statement => statement.declarationList.declarations) ?? []).map(item => item.name.text));
149
+ if (!declared.has(wanted)) plain(listFile, `${listFile} must export ${wanted}, the entities of the ${kinds.name} capability, the one file that lists them.`, { capability: kinds.name });
150
+ if (!identifiersOf(ts, graph.files.get(LITE_ENTITY_LIST)).has(wanted)) plain(LITE_ENTITY_LIST, `The ${kinds.name} capability has entities but ${LITE_ENTITY_LIST} does not name ${wanted} in PRIMARY_ENTITIES; list it there by name, the one list the app composition registers.`, { capability: kinds.name });
151
+ }
152
+
130
153
  function reportDeclaredPersistenceArrays(key, kinds, registered, graph, ts, plain) {
131
154
  if (!registered.get(key)?.connections.size) plain(`${key}/persistence/connection.ts`, `The ${kinds.name} capability has entities or migrations but no app registers them; add ${camel(kinds.name)}Entities and ${camel(kinds.name)}Migrations to a connection of DatabaseModule.register and of the cli app.`, { capability: kinds.name });
132
155
  const connectionFile = graph.files.get(`${key}/persistence/connection.ts`);
@@ -160,6 +183,7 @@ function reportFoundCapability(input, kit, key, kinds, view) {
160
183
  const { registered, connections, plain, report } = view;
161
184
  const { graph } = input;
162
185
  const { resolver } = kit;
186
+ if (view.lite) { reportLiteEntityArrays(key, kinds, graph, kit.ts, plain); return; }
163
187
  reportDeclaredPersistenceArrays(key, kinds, registered, graph, kit.ts, plain);
164
188
  const only = registered.get(key)?.connections;
165
189
  if (!only || only.size < 1) return;
@@ -170,7 +194,7 @@ function reportFoundCapability(input, kit, key, kinds, view) {
170
194
  }
171
195
 
172
196
  function reportFoundCapabilities(input, kit, found, registered, connections, plain, report) {
173
- const view = { registered, connections, plain, report };
197
+ const view = { registered, connections, plain, report, lite: kit.resolver.repo?.edition === 'lite' };
174
198
  for (const [key, kinds] of found) reportFoundCapability(input, kit, key, kinds, view);
175
199
  }
176
200
 
@@ -178,7 +202,7 @@ function reportPersistenceExport(capabilityRoot, index, statement, element, want
178
202
  const from = statement.moduleSpecifier?.text;
179
203
  const inPersistence = from?.startsWith('.') && path.posix.join(capabilityRoot, from).split('/').includes('persistence');
180
204
  if (inPersistence && !Object.values(wanted).includes(element.name.text)) {
181
- report(index, element, `${element.name.text} is exported from persistence/; the persistence of a capability is exposed only as ${wanted.Entities} and ${wanted.Migrations}.`, { name: element.name.text });
205
+ report(index, element, `${element.name.text} is exported from persistence/; the persistence of a capability is exposed only as ${Object.values(wanted).join(' and ')}.`, { name: element.name.text });
182
206
  }
183
207
  }
184
208
 
@@ -204,10 +228,11 @@ function reportCapabilityIndex(input, kit, capabilityRoot, kinds, report) {
204
228
  const { graph } = input;
205
229
  const index = graph.files.get(`${capabilityRoot}/index.ts`);
206
230
  if (!index) return;
207
- const wanted = { Entities: `${camel(kinds.name)}Entities`, Migrations: `${camel(kinds.name)}Migrations` };
231
+ const lite = kit.resolver.repo?.edition === 'lite';
232
+ const wanted = lite ? { Entities: `${camel(kinds.name)}Entities` } : { Entities: `${camel(kinds.name)}Entities`, Migrations: `${camel(kinds.name)}Migrations` };
208
233
  const exported = collectIndexExports(kit, capabilityRoot, index, wanted, report);
209
234
  for (const [kind, has] of [['Entities', kinds.entities], ['Migrations', kinds.migrations]]) {
210
- if (has && !exported.has(wanted[kind])) report(index, index.sourceFile, `${capabilityRoot}/index.ts must export ${wanted[kind]}, the ${kind.toLowerCase()} of the capability; apps concatenate them per connection into DatabaseModule.register.`, { capability: kinds.name, expected: wanted[kind] });
235
+ if (has && wanted[kind] && !exported.has(wanted[kind])) report(index, index.sourceFile, `${capabilityRoot}/index.ts must export ${wanted[kind]}, the ${kind.toLowerCase()} of the capability; apps concatenate them per connection into DatabaseModule.register.`, { capability: kinds.name, expected: wanted[kind] });
211
236
  }
212
237
  }
213
238
 
@@ -7,7 +7,7 @@ import { sourceLocation } from '../../lib/ts-ast.mjs';
7
7
  * - FE_SERVER_ACTION_FORBIDDEN: a `"use server"` directive anywhere in a front-end source file, in the file's directive prologue
8
8
  * (a Server Action module) or in the prologue of any function body (an inline Server Action), apps and packages alike;
9
9
  * - FE_ROUTE_HANDLER_FORBIDDEN: a route handler (`route.ts`, `route.js` and their module variants) under `apps/<app>/src/app/` other than the
10
- * liveness probe `health/live/route.ts` and the file the slot `fe.route.callback` owns (the Supabase auth code exchange).
10
+ * liveness probe `health/live/route.ts`.
11
11
  *
12
12
  * The check is provider-independent: it runs for every front-end repository.
13
13
  */
@@ -15,7 +15,6 @@ export const SERVER_ACTION_RULE_IDS = Object.freeze(['FE_SERVER_ACTION_FORBIDDEN
15
15
 
16
16
  const SERVER_ACTION = 'FE_SERVER_ACTION_FORBIDDEN';
17
17
  const ROUTE_HANDLER = 'FE_ROUTE_HANDLER_FORBIDDEN';
18
- const CALLBACK_SLOT = 'fe.route.callback';
19
18
  const ROUTE_FILE = /^apps\/([^/]+)\/src\/app\/(?:.+\/)?route\.[cm]?[jt]s$/u;
20
19
 
21
20
  /** The statement nodes of a directive prologue: the leading string-literal expression statements. */
@@ -57,10 +56,10 @@ function serverActionViolations(ts, file) {
57
56
  function routeHandlerViolation(file) {
58
57
  const match = ROUTE_FILE.exec(file.rel);
59
58
  if (!match) return null;
60
- if (file.rel === `apps/${match[1]}/src/app/health/live/route.ts` || file.slot === CALLBACK_SLOT) return null;
59
+ if (file.rel === `apps/${match[1]}/src/app/health/live/route.ts`) return null;
61
60
  return {
62
61
  ruleId: ROUTE_HANDLER, path: file.rel, line: 1, column: 1,
63
- message: `${file.rel} is a front-end route handler outside health/live/route.ts and the ${CALLBACK_SLOT} slot: the front end serves no API. Webhooks, cron, public APIs and third-party callbacks belong to be features; the browser reaches the back end through modules/api.`,
62
+ message: `${file.rel} is a front-end route handler outside health/live/route.ts: the front end serves no API. Webhooks, cron, public APIs and third-party callbacks belong to be features; the browser reaches the back end through modules/api.`,
64
63
  };
65
64
  }
66
65
 
@@ -1,7 +1,7 @@
1
1
  // side-boundary.mjs - what counts as an import that leaves a side of an app (ARCH_INTERNAL_IMPORT_OUTSIDE).
2
2
  // A side (be/ or fe/) imports nothing of the app outside itself: the root holds no source and the other side is another program; a
3
3
  // declared read of the OTHER side (be/contracts/) is codegen input, never an import. A declared read of a tree the APP ROOT owns
4
- // (supabase/types/, sides.<side>.reads of the slot manifest) is the one thing both sides do import: the generated database types.
4
+ // (db/types/, sides.<side>.reads of the slot manifest) is the one thing both sides do import: the generated database types.
5
5
  import path from 'node:path';
6
6
  import { isInside, slash } from './config.mjs';
7
7
 
@@ -248,7 +248,7 @@ function activeMigrations(root) {
248
248
  * Public-table key sets proven by ordered, tracked active migrations. Direct event triggers taint later DDL proof.
249
249
  * Opaque bodies discard current facts; persistent schema machinery hidden inside them is outside static authority.
250
250
  */
251
- export function supabaseUniqueSetsOf(appRoot) {
251
+ export function sqlUniqueSetsOf(appRoot) {
252
252
  const root = path.resolve(appRoot);
253
253
  const files = activeMigrations(root);
254
254
  const state = { tables: new Map(), safe: new Set(), schema: 'public', tainted: false };
@@ -2,8 +2,7 @@ import path from 'node:path';
2
2
  import { analyzeSql, HOLE } from './sql-tokens.mjs';
3
3
  import { machineKit } from './machine-ast.mjs';
4
4
  import { contextModelOf } from './context-map.mjs';
5
- import { supabaseTablesOf } from './supabase-tables.mjs';
6
- import { supabaseUniqueSetsOf } from './supabase-keys.mjs';
5
+ import { sqlUniqueSetsOf } from './sql-keys.mjs';
7
6
 
8
7
  /**
9
8
  * R86 `sql-owner` (BE_SQL_TABLE_OWNER). Every `sql` tagged template (the tag declared in platform/database) of a
@@ -14,7 +13,7 @@ import { supabaseUniqueSetsOf } from './supabase-keys.mjs';
14
13
  * capability's, or those of a same-context capability its owner may import (the tier matrix, through `resolver.importAllowed`
15
14
  * to the table owner's public entry); a table of another context is refused (cross-context data comes only by events);
16
15
  * - every table it names is declared by some entity under TypeORM authority, or by the generated
17
- * `Database['public']['Tables']` contract under Supabase authority;
16
+ * public tables the ordered db/migrations create under the SQL migration authority;
18
17
  * - a SELECT carries LIMIT, or reads a single relation under a complete primary/unique-key equality to scalar values,
19
18
  * or selects only proven scalar aggregates without GROUP BY. Inferred bounds reject multiplying projections and set operations.
20
19
  * Table positions that hold a `${...}` substitution (SqlIdent) are dynamic; they are counted in the coverage and judged by
@@ -130,11 +129,10 @@ export function entitiesOf(kit, graph) {
130
129
 
131
130
  const capabilityTable = root => path.posix.basename(root).replaceAll('-', '_').toLowerCase();
132
131
 
133
- /** Supabase declarations, with an owner when the generated table name matches a capability's persistence folder. */
134
- function supabaseDeclarations(graph, appRoot, ts) {
135
- const generated = supabaseTablesOf(appRoot, ts);
136
- const keys = supabaseUniqueSetsOf(appRoot);
137
- const tables = new Map([...generated].map(table => [table, { table, owner: null, rel: null, uniqueSets: keys.get(table) ?? [] }]));
132
+ /** SQL-authority declarations (the public tables db/migrations create), with an owner when a table name matches a capability's persistence folder. */
133
+ function sqlDeclarations(graph, appRoot) {
134
+ const keys = sqlUniqueSetsOf(appRoot);
135
+ const tables = new Map([...keys.keys()].map(table => [table, { table, owner: null, rel: null, uniqueSets: keys.get(table) ?? [] }]));
138
136
  for (const file of graph.files.values()) {
139
137
  if (file.slot !== 'be.persistence' || !file.owner) continue;
140
138
  const key = capabilityTable(file.owner.root);
@@ -150,16 +148,16 @@ const isOwnTable = (table, ownTable) => {
150
148
  return name === ownTable || name.startsWith(`${ownTable}_`);
151
149
  };
152
150
 
153
- function reportSqlWrite({ tables, supabase, file, ownTable, report }, write) {
151
+ function reportSqlWrite({ tables, sqlAuthority, file, ownTable, report }, write) {
154
152
  const declaration = tables.get(write.table.toLowerCase());
155
153
  if (!declaration) {
156
- report(supabase
157
- ? `SQL writes table ${write.table}, which Database['public']['Tables'] does not declare; regenerate supabase/types/database.types.ts from the migrations.`
154
+ report(sqlAuthority
155
+ ? `SQL writes table ${write.table}, which no db/migrations migration creates; add the table in a migration first.`
158
156
  : `SQL writes table ${write.table}, which no @Entity declares; declare the entity in the owning capability's persistence/entities.`, { table: write.table });
159
- } else if (supabase) {
157
+ } else if (sqlAuthority) {
160
158
  if (isOwnTable(write.table, ownTable) && (declaration.owner === null || declaration.owner === file.owner.root)) return;
161
159
  const ownerNote = declaration.owner ? `, owned by ${declaration.owner}` : '';
162
- report(`SQL in ${file.owner.root} writes table ${write.table}${ownerNote}; under Supabase schema authority a capability writes only its own table (${ownTable}), named for that capability.`, { table: write.table, ...(declaration.owner ? { tableOwner: declaration.owner } : {}) });
160
+ report(`SQL in ${file.owner.root} writes table ${write.table}${ownerNote}; under the SQL migration authority a capability writes only its own table (${ownTable}), named for that capability.`, { table: write.table, ...(declaration.owner ? { tableOwner: declaration.owner } : {}) });
163
161
  } else if (declaration.owner !== file.owner.root) {
164
162
  report(`SQL in ${file.owner.root} writes table ${write.table}, owned by ${declaration.owner}; a capability writes only the tables of its own entities, call the owner's public API instead.`, { table: write.table, tableOwner: declaration.owner });
165
163
  }
@@ -169,11 +167,11 @@ function reportSqlWrites(input) {
169
167
  for (const write of input.result.writes) reportSqlWrite(input, write);
170
168
  }
171
169
 
172
- function reportSqlRead(tables, supabase, file, model, resolver, report, read) {
170
+ function reportSqlRead(tables, sqlAuthority, file, model, resolver, report, read) {
173
171
  const declaration = tables.get(read.table.toLowerCase());
174
172
  if (!declaration) {
175
- report(supabase
176
- ? `SQL reads table ${read.table}, which Database['public']['Tables'] does not declare; regenerate supabase/types/database.types.ts from the migrations.`
173
+ report(sqlAuthority
174
+ ? `SQL reads table ${read.table}, which no db/migrations migration creates; add the table in a migration first.`
177
175
  : `SQL reads table ${read.table}, which no @Entity declares; declare the entity in the owning capability's persistence/entities.`, { table: read.table });
178
176
  return;
179
177
  }
@@ -188,8 +186,8 @@ function reportSqlRead(tables, supabase, file, model, resolver, report, read) {
188
186
  if (!verdict.allowed) report(`SQL in ${file.owner.root} reads table ${read.table}, owned by ${declaration.owner}, which ${file.owner.root} may not import (${verdict.reason}); read it through the owner's public API.`, { table: read.table, tableOwner: declaration.owner, reason: verdict.reason });
189
187
  }
190
188
 
191
- function reportSqlReads({ result, tables, supabase, file, model, resolver, report }) {
192
- for (const read of result.reads) reportSqlRead(tables, supabase, file, model, resolver, report, read);
189
+ function reportSqlReads({ result, tables, sqlAuthority, file, model, resolver, report }) {
190
+ for (const read of result.reads) reportSqlRead(tables, sqlAuthority, file, model, resolver, report, read);
193
191
  }
194
192
 
195
193
  function reportSqlSelects({ result, tables, report }) {
@@ -201,7 +199,7 @@ function reportSqlSelects({ result, tables, report }) {
201
199
  }
202
200
 
203
201
  function inspectSqlTag(input, state, file, checker, node) {
204
- const { kit, ts, tables, supabase, model, resolver } = input;
202
+ const { kit, ts, tables, sqlAuthority, model, resolver } = input;
205
203
  if (!ts.isTaggedTemplateExpression(node)) return true;
206
204
  const home = kit.declarationsOf(checker, node.tag).map(kit.ownerOfDeclaration).find(Boolean);
207
205
  if (home?.tier !== 'platform' || home?.name !== 'database') { state.foreignTags += 1; return true; }
@@ -217,13 +215,13 @@ function inspectSqlTag(input, state, file, checker, node) {
217
215
  state.violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
218
216
  };
219
217
  const result = analyzeSql(text, { uniqueSetsOf: (table, schema) => {
220
- if (supabase && ((schema && schema !== 'public') || table !== table.toLowerCase())) return null;
218
+ if (sqlAuthority && ((schema && schema !== 'public') || table !== table.toLowerCase())) return null;
221
219
  return tables.get(table.toLowerCase())?.uniqueSets ?? null;
222
220
  } });
223
221
  state.dynamic += result.dynamic;
224
222
  const ownTable = capabilityTable(file.owner.root);
225
- reportSqlWrites({ result, tables, supabase, file, ownTable, report });
226
- reportSqlReads({ result, tables, supabase, file, model, resolver, report });
223
+ reportSqlWrites({ result, tables, sqlAuthority, file, ownTable, report });
224
+ reportSqlReads({ result, tables, sqlAuthority, file, model, resolver, report });
227
225
  reportSqlSelects({ result, tables, report });
228
226
  return true;
229
227
  }
@@ -239,13 +237,13 @@ export function checkSqlOwner(input) {
239
237
  const { config, graph } = input;
240
238
  const kit = machineKit(input);
241
239
  const { ts, resolver } = kit;
242
- const supabase = resolver.ruleParams().schemaAuthority === 'supabase';
243
- const entityDeclarations = supabase ? { tables: new Map(), unreadable: 0 } : entitiesOf(kit, graph);
244
- const tables = supabase ? supabaseDeclarations(graph, config.packageRoot, ts) : entityDeclarations.tables;
240
+ const sqlAuthority = resolver.ruleParams().schemaAuthority === 'sql';
241
+ const entityDeclarations = sqlAuthority ? { tables: new Map(), unreadable: 0 } : entitiesOf(kit, graph);
242
+ const tables = sqlAuthority ? sqlDeclarations(graph, config.packageRoot) : entityDeclarations.tables;
245
243
  const model = contextModelOf(kit, graph);
246
244
  const state = { violations: [], templates: 0, dynamic: 0, foreignTags: 0 };
247
- const inspection = { kit, ts, tables, supabase, model, resolver };
245
+ const inspection = { kit, ts, tables, sqlAuthority, model, resolver };
248
246
  for (const file of graph.files.values()) inspectSqlFile(inspection, state, file);
249
247
  return { violations: state.violations, coverage: { status: 'checked', entities: entityDeclarations.tables.size, unreadableEntities: entityDeclarations.unreadable,
250
- ...(supabase ? { supabaseTables: tables.size } : {}), templates: state.templates, dynamicIdentifiers: state.dynamic, foreignTags: state.foreignTags } };
248
+ ...(sqlAuthority ? { sqlTables: tables.size } : {}), templates: state.templates, dynamicIdentifiers: state.dynamic, foreignTags: state.foreignTags } };
251
249
  }
@@ -18,7 +18,8 @@ import { propertyValue } from './function-spec-providers.mjs';
18
18
  * - no file outside the client folder and the stream reader folder (`fe.transport.stream`, `modules/api/sse/clients/`) references the global `fetch`: a call, `globalThis.fetch`, `window.fetch`, or a
19
19
  * reference passed as a value (`useSWR(key, fetch)`, `const send = fetch`), all of which a spelling-based rule misses; with no
20
20
  * client at all every such reference is a finding that says the repository has no client;
21
- * - axios is imported only by the api owner (modules/api, the api package) for its rest calls; every other HTTP library is a
21
+ * - axios is imported only by the api owner (modules/api, the api package) for its rest calls and by the auth door
22
+ * (modules/auth/auth-door.ts) for the app's own auth doors; every other HTTP library is a
22
23
  * second transport wherever it is imported, by static import, `import()` or `require()`;
23
24
  * - an `ApolloClient` is constructed only inside the client folder;
24
25
  * - every GraphQL operation (`graphql/queries/query-*.ts`, `graphql/mutations/mutation-*.ts`, `graphql/subscriptions/subscription-*.ts`) imports the client (or the api
@@ -103,13 +104,16 @@ function isApolloClientConstruction(ts, kit, checker, node) {
103
104
  return Boolean(binding?.name === 'ApolloClient' && binding.module.startsWith('@apollo/client'));
104
105
  }
105
106
 
107
+ /** The one auth door of the session issuer's client (`modules/auth/auth-door.ts`, slot fe.modules.auth): it calls the app's own auth doors with axios. */
108
+ const isAuthDoor = (file) => file.slot === 'fe.modules.auth' && path.posix.basename(file.rel) === 'auth-door.ts';
109
+
106
110
  function inspectLibraryImport({ ts, file, node, transport, report }) {
107
111
  const specifier = moduleSpecifierOf(ts, node);
108
112
  if (!specifier || !ts.isStringLiteralLike(specifier)) return;
109
113
  const library = specifier.text.split('/')[0];
110
- if (!HTTP_LIBRARIES.has(library) || (library === REST_LIBRARY && API_OWNER_SLOTS.has(file.slot))) return;
114
+ if (!HTTP_LIBRARIES.has(library) || (library === REST_LIBRARY && (API_OWNER_SLOTS.has(file.slot) || isAuthDoor(file)))) return;
111
115
  const where = library === REST_LIBRARY
112
- ? 'axios belongs to the api owner only (modules/api/rest, the api package): call its rest functions'
116
+ ? 'axios belongs to the api owner (modules/api/rest, the api package) and the auth door (modules/auth/auth-door.ts) only: call their functions'
113
117
  : `the transport is Apollo GraphQL (${transport}) with axios for the rest calls of modules/api/rest`;
114
118
  report(file, node, `${specifier.text} is a second HTTP transport in ${file.rel}; ${where}.`, { library: specifier.text });
115
119
  }
@@ -26,12 +26,12 @@
26
26
  // FE_NO_TESTS (R97, rules/fe-no-tests.mjs) a front end holds a spec, e2e or test-tool file, a test script or a test dependency; no exception
27
27
  // FE_STACK_FORBIDDEN (rules/fe-stack.mjs) a front-end manifest (the fe side's, or the app root's minus the back end's own libraries)
28
28
  // declares a package knowledge/hfs/fe-stack.yaml forbids
29
- // HFS_LITE_SECRET_CUSTODY (R220, rules/supabase-secrets.mjs) under edition lite: secret material or a forbidden custody path; the R06 scan itself runs for every edition
29
+ // HFS_LITE_SECRET_CUSTODY (R220, rules/lite-secrets.mjs) under edition lite: secret material or a forbidden custody path; the R06 scan itself runs for every edition
30
30
  // HFS_EDITION_FORBIDDEN_PRESENT (R212, rules/edition.mjs) under edition lite: an e2e, contract, test-stack or browser script, or a
31
31
  // dependency of infrastructure lite does not run (test containers, fault proxy, Kafka)
32
32
  // HFS_BACKING_UNDECLARED, HFS_BACKING_UNUSED (R212, rules/edition.mjs) under edition lite: a client library or a pattern of a backing
33
33
  // service hfs.json sides.be.backing does not declare or the edition does not admit, or a declared service no dependency consumes
34
- // DB_MIGRATION_SHAPE ... DB_TYPES_DRIFT (R213-R216, rules/database.mjs, async: passed in as extraFindings) the Supabase migrations, policies, definer functions, buckets, config.toml and generated types
34
+ // DB_MIGRATION_SHAPE ... LITE_DB_MIGRATION (R213-R215, R332, rules/database.mjs, async: passed in as extraFindings) the db/ migrations, policies, definer functions and buckets; generated types
35
35
  // BE_SPEC_PLACEMENT (R102, rules/spec-placement.mjs) a spec or test file outside the four test layers, scripts/ and tools/ included
36
36
  // HFS_REPO_LOCAL_CHECK (R103, rules/repo-local-checks.mjs) a local eslint rule or plugin, a `check-*` script, a relative import in eslint.config
37
37
  // HFS_LINT_SUPPRESSION_FILE (R104, rules/lint-suppression.mjs) an eslint suppressions file, script or option
@@ -83,7 +83,7 @@ import { frontendFindings } from './rules/frontend-tree.mjs';
83
83
  import { lintSuppressionFindings } from './rules/lint-suppression.mjs';
84
84
  import { bypassMarkerFindings } from './rules/bypass-markers.mjs';
85
85
  import { repoLocalCheckFindings } from './rules/repo-local-checks.mjs';
86
- import { supabaseSecretFindings } from './rules/supabase-secrets.mjs';
86
+ import { liteSecretFindings } from './rules/lite-secrets.mjs';
87
87
  import { untrackedSecretFindings } from './rules/secrets.mjs';
88
88
  import { liteStarciworkFinding, pathFindings } from './path-findings.mjs';
89
89
  import { onLintSurface } from './architecture/surface.mjs';
@@ -111,8 +111,8 @@ export const CHECK_CODES = Object.freeze([
111
111
  'HFS_PLAINTEXT_SECRET', 'HFS_STACKS_SHAPE', 'HFS_CI_MISSING_CANON', 'HFS_DEP_VERSION_SKEW', 'HFS_CONTRACT_SNAPSHOT_DRIFT', 'HFS_MCP_TOOLS_SNAPSHOT',
112
112
  'BE_TEST_TOPOLOGY', 'BE_SPEC_PLACEMENT', 'HFS_REPO_LOCAL_CHECK', 'HFS_LINT_SUPPRESSION_FILE', 'HFS_BYPASS_MARKER', 'HFS_PROOF_COMMAND_FILE_MISSING', 'HFS_PEER_INTEGRATION_MISSING', 'BE_INTEGRATION_SPEC_MISSING', 'FE_GRAPHQL_CONTRACT', 'FE_NO_TESTS', 'HFS_MONO_WORKSPACES', 'HFS_MONO_FE_WORKSPACE', 'HFS_MONO_NEST_PROJECTS', 'HFS_MONO_WORKSPACE_DEP', 'BE_CLI_REQUIRED', 'FE_WIRE_GENERATED', 'FE_I18N_PLACEMENT', 'FE_I18N_CATALOG', 'HFS_SERVICE_PLACEMENT', 'HFS_IMAGE_UNPINNED', 'HFS_SERVICE_STACK_DECLARATION', 'HFS_EVENT_CONTRACT', 'BE_ASYNC_SPEC_MISSING', 'BE_SAGA_STEP_COMPENSATION', 'BE_SAGA_STATE_VERSIONED', 'BE_SAGA_EVENT_CONTRACT', 'BE_SAGA_CONSUMER_DEDUPE', 'BE_EVENT_CLASS_CONTRACT', 'BE_PATTERN_SPEC_MISSING', 'BE_CONTRACT_BREAKING', 'BE_KIND_DECLARATION', 'BE_KIND_EMPTY', 'HFS_DOCKER_BUILD_CONTEXT', 'HFS_DOCKER_STAGES', 'HFS_DOCKER_ENTRY', 'HFS_DOCKER_BASE_PIN', 'HFS_DOCKER_SECRETS', 'BE_SAGA_E2E_MISSING',
113
113
  'FE_STACK_FORBIDDEN', 'HFS_EDITION_FORBIDDEN_PRESENT', 'HFS_BACKING_UNDECLARED', 'HFS_BACKING_UNUSED', 'HFS_LITE_SECRET_CUSTODY', 'HFS_STARCIWORK_LITE_FORBIDDEN',
114
- 'DB_MIGRATION_SHAPE', 'DB_RLS_REQUIRED', 'DB_DYNAMIC_DDL', 'DB_POLICY_SHAPE', 'DB_DEFINER_SAFE', 'DB_STORAGE_POLICY', 'DB_CONFIG_POLICY', 'DB_TYPES_DRIFT',
115
- 'HFS_DOCKER_HEALTHCHECK_FAKE', 'DB_BE_ONLY_ACCESS', 'DB_REALTIME_UNLISTED', 'DB_APP_ROLE', 'DB_FORCE_RLS_DEFAULT',
114
+ 'DB_MIGRATION_SHAPE', 'DB_RLS_REQUIRED', 'DB_DYNAMIC_DDL', 'DB_POLICY_SHAPE', 'DB_DEFINER_SAFE', 'DB_STORAGE_POLICY', 'LITE_DB_MIGRATION',
115
+ 'HFS_DOCKER_HEALTHCHECK_FAKE', 'DB_APP_ROLE', 'DB_FORCE_RLS_DEFAULT',
116
116
  'DB_POLICY_TAUTOLOGY', 'DB_VIEW_UNSAFE', 'DB_STORAGE_PREFIX', 'DB_DEFINER_REVOKE',
117
117
  'HFS_GITIGNORE_BLOCK_DRIFT', 'HFS_SONAR_CONFIG', 'HFS_COVERAGE_SCOPE_DRIFT', 'HFS_FORMAT',
118
118
  'HFS_EMPTY_DIR', 'HFS_GHOST_TREE', 'HFS_UNTRACKED_ROOT_ENTRY',
@@ -209,7 +209,7 @@ function minimumInstanceFindings(resolver, required, instances) {
209
209
  function contentRuleFindings({ repoRoot, repo, resolver, files, all, scoped, pins, inScope, isRoot, appSide, editionDeclaration, manifest, phase }) {
210
210
  const findings = [
211
211
  ...pinFindings({ repoRoot, files, profile: repo.profile, pins, only: scoped }),
212
- ...supabaseSecretFindings({ repoRoot, files: files.filter((file) => inScope(file)), resolver, repo }),
212
+ ...liteSecretFindings({ repoRoot, files: files.filter((file) => inScope(file)), resolver, repo }),
213
213
  ...editionFindings({ repoRoot, files: files.filter((file) => inScope(file)), repo, resolver, manifest, withDeclaration: editionDeclaration }),
214
214
  ...feStackFindings({ repoRoot, files: files.filter((file) => inScope(file)), repo, manifest }),
215
215
  ...repoLocalCheckFindings({ repoRoot, files }),