@soulbatical/tetra-core 0.1.13 → 0.1.15

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 (77) hide show
  1. package/dist/generators/rls-auditor.d.ts +39 -0
  2. package/dist/generators/rls-auditor.d.ts.map +1 -0
  3. package/dist/generators/rls-auditor.js +505 -0
  4. package/dist/generators/rls-auditor.js.map +1 -0
  5. package/dist/generators/rls-checker.d.ts +94 -0
  6. package/dist/generators/rls-checker.d.ts.map +1 -0
  7. package/dist/generators/rls-checker.js +215 -0
  8. package/dist/generators/rls-checker.js.map +1 -0
  9. package/dist/generators/rls-generator.d.ts +77 -0
  10. package/dist/generators/rls-generator.d.ts.map +1 -0
  11. package/dist/generators/rls-generator.js +402 -0
  12. package/dist/generators/rls-generator.js.map +1 -0
  13. package/dist/generators/rpc/detail-rpc-generator.d.ts +58 -0
  14. package/dist/generators/rpc/detail-rpc-generator.d.ts.map +1 -0
  15. package/dist/generators/rpc/detail-rpc-generator.js +163 -0
  16. package/dist/generators/rpc/detail-rpc-generator.js.map +1 -0
  17. package/dist/generators/rpc/index.d.ts +24 -0
  18. package/dist/generators/rpc/index.d.ts.map +1 -0
  19. package/dist/generators/rpc/index.js +20 -0
  20. package/dist/generators/rpc/index.js.map +1 -0
  21. package/dist/generators/rpc/rpc-generator.d.ts +150 -0
  22. package/dist/generators/rpc/rpc-generator.d.ts.map +1 -0
  23. package/dist/generators/rpc/rpc-generator.js +743 -0
  24. package/dist/generators/rpc/rpc-generator.js.map +1 -0
  25. package/dist/generators/rpc/templates/array.d.ts +29 -0
  26. package/dist/generators/rpc/templates/array.d.ts.map +1 -0
  27. package/dist/generators/rpc/templates/array.js +40 -0
  28. package/dist/generators/rpc/templates/array.js.map +1 -0
  29. package/dist/generators/rpc/templates/auth.d.ts +85 -0
  30. package/dist/generators/rpc/templates/auth.d.ts.map +1 -0
  31. package/dist/generators/rpc/templates/auth.js +233 -0
  32. package/dist/generators/rpc/templates/auth.js.map +1 -0
  33. package/dist/generators/rpc/templates/column.d.ts +39 -0
  34. package/dist/generators/rpc/templates/column.d.ts.map +1 -0
  35. package/dist/generators/rpc/templates/column.js +97 -0
  36. package/dist/generators/rpc/templates/column.js.map +1 -0
  37. package/dist/generators/rpc/templates/enum.d.ts +33 -0
  38. package/dist/generators/rpc/templates/enum.d.ts.map +1 -0
  39. package/dist/generators/rpc/templates/enum.js +93 -0
  40. package/dist/generators/rpc/templates/enum.js.map +1 -0
  41. package/dist/generators/rpc/templates/nullable.d.ts +31 -0
  42. package/dist/generators/rpc/templates/nullable.d.ts.map +1 -0
  43. package/dist/generators/rpc/templates/nullable.js +50 -0
  44. package/dist/generators/rpc/templates/nullable.js.map +1 -0
  45. package/dist/generators/rpc/templates/related.d.ts +47 -0
  46. package/dist/generators/rpc/templates/related.d.ts.map +1 -0
  47. package/dist/generators/rpc/templates/related.js +182 -0
  48. package/dist/generators/rpc/templates/related.js.map +1 -0
  49. package/dist/generators/rpc/templates/search.d.ts +42 -0
  50. package/dist/generators/rpc/templates/search.d.ts.map +1 -0
  51. package/dist/generators/rpc/templates/search.js +81 -0
  52. package/dist/generators/rpc/templates/search.js.map +1 -0
  53. package/dist/generators/rpc/templates/time.d.ts +44 -0
  54. package/dist/generators/rpc/templates/time.d.ts.map +1 -0
  55. package/dist/generators/rpc/templates/time.js +143 -0
  56. package/dist/generators/rpc/templates/time.js.map +1 -0
  57. package/dist/generators/rpc/utils.d.ts +58 -0
  58. package/dist/generators/rpc/utils.d.ts.map +1 -0
  59. package/dist/generators/rpc/utils.js +92 -0
  60. package/dist/generators/rpc/utils.js.map +1 -0
  61. package/dist/generators/rpc/validator.d.ts +21 -0
  62. package/dist/generators/rpc/validator.d.ts.map +1 -0
  63. package/dist/generators/rpc/validator.js +398 -0
  64. package/dist/generators/rpc/validator.js.map +1 -0
  65. package/dist/index.d.ts +9 -1
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +6 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/shared/auth/index.d.ts +1 -1
  70. package/dist/shared/auth/index.d.ts.map +1 -1
  71. package/dist/shared/auth/routes.d.ts +4 -1
  72. package/dist/shared/auth/routes.d.ts.map +1 -1
  73. package/dist/shared/auth/routes.js +83 -1
  74. package/dist/shared/auth/routes.js.map +1 -1
  75. package/dist/shared/auth/types.d.ts +24 -0
  76. package/dist/shared/auth/types.d.ts.map +1 -1
  77. package/package.json +1 -1
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Tetra RLS Auditor v2.0
3
+ *
4
+ * Generates SQL queries that audit RLS compliance against Tetra standards.
5
+ * Run these queries on any Supabase project to find violations.
6
+ *
7
+ * Categories:
8
+ * A. Foundation — RLS enabled, FORCE RLS, policies exist
9
+ * B. Policy quality — roles, expressions, completeness
10
+ * C. Auth functions — exist, correct signature, SECURITY DEFINER
11
+ * D. RLS bypass routes — functions, views, grants that skip RLS
12
+ * E. Data exposure — Realtime, storage, schema leaks
13
+ * F. Migration verification — Tetra standards applied correctly
14
+ *
15
+ * Total: 22 checks (was 10 in v1.0)
16
+ */
17
+ export interface AuditCheck {
18
+ /** Short identifier */
19
+ id: string;
20
+ /** Human-readable name */
21
+ name: string;
22
+ /** Category for grouping */
23
+ category: 'foundation' | 'policy' | 'auth' | 'bypass' | 'exposure' | 'migration';
24
+ /** Severity: error = must fix, warn = should fix, info = review */
25
+ severity: 'error' | 'warn' | 'info';
26
+ /** SQL query that returns violations (empty result = pass) */
27
+ sql: string;
28
+ }
29
+ /**
30
+ * Get all audit checks.
31
+ * Run each check's SQL on a Supabase project. Non-empty results are violations.
32
+ */
33
+ export declare function getAuditChecks(): AuditCheck[];
34
+ /**
35
+ * Generate a single SQL query that runs ALL audit checks and returns a summary.
36
+ * Returns one row per violation with check_id, severity, category, table_name, and issue.
37
+ */
38
+ export declare function generateAuditSQL(): string;
39
+ //# sourceMappingURL=rls-auditor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rls-auditor.d.ts","sourceRoot":"","sources":["../../src/generators/rls-auditor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,UAAU;IACzB,uBAAuB;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,QAAQ,EAAE,YAAY,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,GAAG,WAAW,CAAC;IACjF,mEAAmE;IACnE,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;IACpC,8DAA8D;IAC9D,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,UAAU,EAAE,CAic7C;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CA4CzC"}
@@ -0,0 +1,505 @@
1
+ /**
2
+ * Tetra RLS Auditor v2.0
3
+ *
4
+ * Generates SQL queries that audit RLS compliance against Tetra standards.
5
+ * Run these queries on any Supabase project to find violations.
6
+ *
7
+ * Categories:
8
+ * A. Foundation — RLS enabled, FORCE RLS, policies exist
9
+ * B. Policy quality — roles, expressions, completeness
10
+ * C. Auth functions — exist, correct signature, SECURITY DEFINER
11
+ * D. RLS bypass routes — functions, views, grants that skip RLS
12
+ * E. Data exposure — Realtime, storage, schema leaks
13
+ * F. Migration verification — Tetra standards applied correctly
14
+ *
15
+ * Total: 22 checks (was 10 in v1.0)
16
+ */
17
+ /**
18
+ * Get all audit checks.
19
+ * Run each check's SQL on a Supabase project. Non-empty results are violations.
20
+ */
21
+ export function getAuditChecks() {
22
+ return [
23
+ // ═══════════════════════════════════════════════════════════
24
+ // A. FOUNDATION — RLS enabled, force, policies exist
25
+ // ═══════════════════════════════════════════════════════════
26
+ {
27
+ id: 'no-rls',
28
+ name: 'Tables without RLS enabled',
29
+ category: 'foundation',
30
+ severity: 'error',
31
+ sql: `
32
+ SELECT c.relname as table_name, 'RLS not enabled' as issue
33
+ FROM pg_class c
34
+ JOIN pg_namespace n ON n.oid = c.relnamespace
35
+ WHERE n.nspname = 'public'
36
+ AND c.relkind = 'r'
37
+ AND c.relrowsecurity = false
38
+ AND c.relname NOT LIKE 'pg_%'
39
+ AND c.relname NOT LIKE '_supabase%'
40
+ AND c.relname NOT LIKE '_prisma%'
41
+ ORDER BY c.relname;`,
42
+ },
43
+ {
44
+ id: 'no-force-rls',
45
+ name: 'Tables with RLS but without FORCE RLS (table owner bypasses)',
46
+ category: 'foundation',
47
+ severity: 'error',
48
+ sql: `
49
+ SELECT c.relname as table_name, 'FORCE RLS not enabled — table owner bypasses all policies' as issue
50
+ FROM pg_class c
51
+ JOIN pg_namespace n ON n.oid = c.relnamespace
52
+ WHERE n.nspname = 'public'
53
+ AND c.relkind = 'r'
54
+ AND c.relrowsecurity = true
55
+ AND c.relforcerowsecurity = false
56
+ AND c.relname NOT LIKE 'pg_%'
57
+ AND c.relname NOT LIKE '_supabase%'
58
+ ORDER BY c.relname;`,
59
+ },
60
+ {
61
+ id: 'rls-no-policies',
62
+ name: 'Tables with RLS enabled but no policies (blocks ALL access)',
63
+ category: 'foundation',
64
+ severity: 'error',
65
+ sql: `
66
+ SELECT c.relname as table_name, 'RLS enabled but zero policies — all access blocked including service' as issue
67
+ FROM pg_class c
68
+ JOIN pg_namespace n ON n.oid = c.relnamespace
69
+ WHERE n.nspname = 'public'
70
+ AND c.relkind = 'r'
71
+ AND c.relrowsecurity = true
72
+ AND c.relname NOT IN (SELECT DISTINCT tablename FROM pg_policies WHERE schemaname = 'public')
73
+ AND c.relname NOT LIKE 'pg_%'
74
+ AND c.relname NOT LIKE '_supabase%'
75
+ ORDER BY c.relname;`,
76
+ },
77
+ // ═══════════════════════════════════════════════════════════
78
+ // B. POLICY QUALITY — roles, expressions, completeness
79
+ // ═══════════════════════════════════════════════════════════
80
+ {
81
+ id: 'public-role-mutate',
82
+ name: 'Mutation policies allowing role public',
83
+ category: 'policy',
84
+ severity: 'error',
85
+ sql: `
86
+ SELECT tablename as table_name, policyname || ' (' || cmd || ')' as issue
87
+ FROM pg_policies
88
+ WHERE schemaname = 'public'
89
+ AND roles::text = '{public}'
90
+ AND cmd != 'SELECT'
91
+ AND qual != 'false'
92
+ ORDER BY tablename, cmd;`,
93
+ },
94
+ {
95
+ id: 'always-true-mutate',
96
+ name: 'USING(true) or WITH CHECK(true) on mutations',
97
+ category: 'policy',
98
+ severity: 'error',
99
+ sql: `
100
+ SELECT tablename as table_name,
101
+ policyname || ': ' ||
102
+ CASE
103
+ WHEN qual = 'true' AND cmd != 'SELECT' THEN 'USING(true) on ' || cmd
104
+ WHEN with_check = 'true' AND cmd IN ('INSERT', 'UPDATE') THEN 'WITH CHECK(true) on ' || cmd
105
+ END as issue
106
+ FROM pg_policies
107
+ WHERE schemaname = 'public'
108
+ AND (
109
+ (qual = 'true' AND cmd != 'SELECT')
110
+ OR (with_check = 'true' AND cmd IN ('INSERT', 'UPDATE'))
111
+ )
112
+ ORDER BY tablename, cmd;`,
113
+ },
114
+ {
115
+ id: 'insert-no-with-check',
116
+ name: 'INSERT policies without WITH CHECK (allows any data)',
117
+ category: 'policy',
118
+ severity: 'error',
119
+ sql: `
120
+ SELECT tablename as table_name,
121
+ policyname || ': INSERT policy has no WITH CHECK clause' as issue
122
+ FROM pg_policies
123
+ WHERE schemaname = 'public'
124
+ AND cmd = 'INSERT'
125
+ AND (with_check IS NULL OR with_check = '')
126
+ AND qual != 'false'
127
+ ORDER BY tablename;`,
128
+ },
129
+ {
130
+ id: 'update-mismatch',
131
+ name: 'UPDATE policies where USING and WITH CHECK differ (can change org_id)',
132
+ category: 'policy',
133
+ severity: 'error',
134
+ sql: `
135
+ SELECT tablename as table_name,
136
+ policyname || ': USING != WITH CHECK — user could UPDATE row to change organization_id' as issue
137
+ FROM pg_policies
138
+ WHERE schemaname = 'public'
139
+ AND cmd = 'UPDATE'
140
+ AND qual IS NOT NULL
141
+ AND with_check IS NOT NULL
142
+ AND qual != 'false'
143
+ AND with_check != 'false'
144
+ AND qual != with_check
145
+ AND qual NOT LIKE '%auth.uid()%'
146
+ ORDER BY tablename;`,
147
+ },
148
+ {
149
+ id: 'missing-crud-coverage',
150
+ name: 'Tables with partial CRUD coverage (missing operations)',
151
+ category: 'policy',
152
+ severity: 'warn',
153
+ sql: `
154
+ WITH table_ops AS (
155
+ SELECT tablename, array_agg(DISTINCT cmd ORDER BY cmd) as ops
156
+ FROM pg_policies
157
+ WHERE schemaname = 'public'
158
+ GROUP BY tablename
159
+ )
160
+ SELECT tablename as table_name,
161
+ 'Has policies for ' || array_to_string(ops, ', ') ||
162
+ ' but missing ' ||
163
+ array_to_string(
164
+ ARRAY(SELECT x FROM unnest(ARRAY['SELECT','INSERT','UPDATE','DELETE']) x WHERE x != ALL(ops)),
165
+ ', '
166
+ ) as issue
167
+ FROM table_ops
168
+ WHERE NOT (ops @> ARRAY['SELECT','INSERT','UPDATE','DELETE'])
169
+ AND NOT (ops = ARRAY['ALL'])
170
+ AND tablename NOT IN ('organization_members', 'organization_invites')
171
+ ORDER BY tablename;`,
172
+ },
173
+ // ═══════════════════════════════════════════════════════════
174
+ // C. AUTH FUNCTIONS — exist, signature, security
175
+ // ═══════════════════════════════════════════════════════════
176
+ {
177
+ id: 'missing-auth-functions',
178
+ name: 'Missing Tetra auth helper functions',
179
+ category: 'auth',
180
+ severity: 'error',
181
+ sql: `
182
+ SELECT fn as table_name, 'Function not found in public schema' as issue
183
+ FROM unnest(ARRAY[
184
+ 'auth_admin_organizations',
185
+ 'auth_user_organizations',
186
+ 'auth_creator_organizations',
187
+ 'auth_current_user_id',
188
+ 'auth_is_superadmin'
189
+ ]) fn
190
+ WHERE NOT EXISTS (
191
+ SELECT 1 FROM pg_proc p
192
+ JOIN pg_namespace n ON n.oid = p.pronamespace
193
+ WHERE n.nspname = 'public' AND p.proname = fn
194
+ );`,
195
+ },
196
+ {
197
+ id: 'auth-fn-not-security-definer',
198
+ name: 'Auth functions without SECURITY DEFINER (cannot read organization_members)',
199
+ category: 'auth',
200
+ severity: 'error',
201
+ sql: `
202
+ SELECT p.proname as table_name,
203
+ 'Not SECURITY DEFINER — function runs as caller who cannot access organization_members' as issue
204
+ FROM pg_proc p
205
+ JOIN pg_namespace n ON n.oid = p.pronamespace
206
+ WHERE n.nspname = 'public'
207
+ AND p.proname IN ('auth_admin_organizations', 'auth_user_organizations',
208
+ 'auth_creator_organizations', 'auth_current_user_id', 'auth_is_superadmin')
209
+ AND NOT p.prosecdef
210
+ ORDER BY p.proname;`,
211
+ },
212
+ {
213
+ id: 'auth-fn-wrong-search-path',
214
+ name: 'Auth functions with non-empty search_path (search_path injection risk)',
215
+ category: 'auth',
216
+ severity: 'error',
217
+ sql: `
218
+ SELECT p.proname as table_name,
219
+ 'search_path is not empty string — vulnerable to search_path injection. Got: ' ||
220
+ COALESCE(
221
+ (SELECT option_value FROM pg_options_to_table(p.proconfig) WHERE option_name = 'search_path'),
222
+ '(not set)'
223
+ ) as issue
224
+ FROM pg_proc p
225
+ JOIN pg_namespace n ON n.oid = p.pronamespace
226
+ WHERE n.nspname = 'public'
227
+ AND p.proname IN ('auth_admin_organizations', 'auth_user_organizations',
228
+ 'auth_creator_organizations', 'auth_current_user_id', 'auth_is_superadmin')
229
+ AND (
230
+ p.proconfig IS NULL
231
+ OR NOT EXISTS (
232
+ SELECT 1 FROM pg_options_to_table(p.proconfig)
233
+ WHERE option_name = 'search_path' AND option_value IN ('', '""')
234
+ )
235
+ )
236
+ ORDER BY p.proname;`,
237
+ },
238
+ {
239
+ id: 'auth-fn-not-stable',
240
+ name: 'Auth functions not marked STABLE (causes repeated evaluation per row)',
241
+ category: 'auth',
242
+ severity: 'warn',
243
+ sql: `
244
+ SELECT p.proname as table_name,
245
+ 'Volatility is ' || p.provolatile || ', should be s (STABLE) for performance' as issue
246
+ FROM pg_proc p
247
+ JOIN pg_namespace n ON n.oid = p.pronamespace
248
+ WHERE n.nspname = 'public'
249
+ AND p.proname IN ('auth_admin_organizations', 'auth_user_organizations',
250
+ 'auth_creator_organizations', 'auth_current_user_id', 'auth_is_superadmin')
251
+ AND p.provolatile != 's'
252
+ ORDER BY p.proname;`,
253
+ },
254
+ // ═══════════════════════════════════════════════════════════
255
+ // D. RLS BYPASS ROUTES — the real dangers
256
+ // ═══════════════════════════════════════════════════════════
257
+ {
258
+ id: 'security-definer-data-fn',
259
+ name: 'SECURITY DEFINER functions that return data (bypass RLS)',
260
+ category: 'bypass',
261
+ severity: 'error',
262
+ sql: `
263
+ SELECT p.proname as table_name,
264
+ 'SECURITY DEFINER + returns ' || pg_catalog.format_type(p.prorettype, NULL) ||
265
+ ' — runs as function owner, bypasses RLS' as issue
266
+ FROM pg_proc p
267
+ JOIN pg_namespace n ON n.oid = p.pronamespace
268
+ WHERE n.nspname = 'public'
269
+ AND p.prosecdef = true
270
+ AND p.proname NOT IN (
271
+ 'auth_admin_organizations', 'auth_user_organizations',
272
+ 'auth_creator_organizations', 'auth_current_user_id', 'auth_is_superadmin',
273
+ 'update_updated_at_column'
274
+ )
275
+ AND p.proname NOT LIKE 'st_%'
276
+ AND p.proname NOT LIKE 'postgis_%'
277
+ AND pg_catalog.format_type(p.prorettype, NULL) NOT IN ('trigger', 'event_trigger', 'void')
278
+ ORDER BY p.proname;`,
279
+ },
280
+ {
281
+ id: 'anon-callable-functions',
282
+ name: 'Functions callable by anon role (unauthenticated access)',
283
+ category: 'bypass',
284
+ severity: 'error',
285
+ sql: `
286
+ SELECT p.proname as table_name,
287
+ 'EXECUTE granted to anon — unauthenticated users can call this function' as issue
288
+ FROM pg_proc p
289
+ JOIN pg_namespace n ON n.oid = p.pronamespace
290
+ WHERE n.nspname = 'public'
291
+ AND p.proname NOT LIKE 'st_%'
292
+ AND p.proname NOT LIKE 'postgis_%'
293
+ AND p.proname NOT LIKE '_supabase%'
294
+ AND has_function_privilege('anon', p.oid, 'EXECUTE')
295
+ AND p.proname NOT IN ('auth_admin_organizations', 'auth_user_organizations',
296
+ 'auth_creator_organizations', 'auth_current_user_id', 'auth_is_superadmin')
297
+ AND pg_catalog.format_type(p.prorettype, NULL) NOT IN ('trigger', 'event_trigger')
298
+ ORDER BY p.proname;`,
299
+ },
300
+ {
301
+ id: 'views-bypass-rls',
302
+ name: 'Views without security_invoker=true (run as view creator, bypass RLS)',
303
+ category: 'bypass',
304
+ severity: 'error',
305
+ sql: `
306
+ SELECT c.relname as table_name,
307
+ 'View runs as DEFINER (creator) — bypasses RLS on underlying tables. Add security_invoker=true' as issue
308
+ FROM pg_class c
309
+ JOIN pg_namespace n ON n.oid = c.relnamespace
310
+ WHERE n.nspname = 'public'
311
+ AND c.relkind = 'v'
312
+ AND (
313
+ c.reloptions IS NULL
314
+ OR 'security_invoker=true' != ALL(c.reloptions)
315
+ )
316
+ ORDER BY c.relname;`,
317
+ },
318
+ {
319
+ id: 'anon-table-grants',
320
+ name: 'Direct table grants to anon role',
321
+ category: 'bypass',
322
+ severity: 'error',
323
+ sql: `
324
+ SELECT table_name,
325
+ 'anon has ' || privilege_type || ' grant — even with RLS, anon can attempt queries' as issue
326
+ FROM information_schema.table_privileges
327
+ WHERE table_schema = 'public'
328
+ AND grantee = 'anon'
329
+ AND privilege_type IN ('INSERT', 'UPDATE', 'DELETE')
330
+ AND table_name NOT LIKE 'pg_%'
331
+ AND table_name NOT LIKE '_supabase%'
332
+ ORDER BY table_name, privilege_type;`,
333
+ },
334
+ // ═══════════════════════════════════════════════════════════
335
+ // E. DATA EXPOSURE — Realtime, storage, schema
336
+ // ═══════════════════════════════════════════════════════════
337
+ {
338
+ id: 'realtime-on-sensitive',
339
+ name: 'Supabase Realtime enabled on sensitive tables',
340
+ category: 'exposure',
341
+ severity: 'warn',
342
+ sql: `
343
+ SELECT tablename as table_name,
344
+ 'Table is in supabase_realtime publication — live data streams bypass application-level auth' as issue
345
+ FROM pg_publication_tables
346
+ WHERE pubname = 'supabase_realtime'
347
+ AND schemaname = 'public'
348
+ AND tablename NOT IN ('messages', 'notifications')
349
+ ORDER BY tablename;`,
350
+ },
351
+ // ═══════════════════════════════════════════════════════════
352
+ // F. MIGRATION VERIFICATION — Tetra standards applied
353
+ // ═══════════════════════════════════════════════════════════
354
+ {
355
+ id: 'legacy-columns',
356
+ name: 'Legacy column names (not snake_case)',
357
+ category: 'migration',
358
+ severity: 'error',
359
+ sql: `
360
+ SELECT table_name, column_name || ' → should be ' ||
361
+ CASE
362
+ WHEN column_name = 'organizationid' THEN 'organization_id'
363
+ WHEN column_name = 'userid' THEN 'user_id'
364
+ WHEN column_name = 'user_public_id' THEN 'user_id'
365
+ WHEN column_name = 'firstname' THEN 'first_name'
366
+ WHEN column_name = 'lastname' THEN 'last_name'
367
+ WHEN column_name = 'createdat' THEN 'created_at'
368
+ WHEN column_name = 'updatedat' THEN 'updated_at'
369
+ WHEN column_name ~ '^[a-z]+id$' AND column_name != 'id' AND length(column_name) > 3
370
+ THEN regexp_replace(column_name, '([a-z])id$', '\\1_id')
371
+ ELSE 'unknown'
372
+ END as issue
373
+ FROM information_schema.columns
374
+ WHERE table_schema = 'public'
375
+ AND table_name NOT LIKE 'pg_%'
376
+ AND table_name NOT LIKE '_supabase%'
377
+ AND (
378
+ column_name IN ('organizationid', 'userid', 'user_public_id', 'firstname', 'lastname', 'createdat', 'updatedat')
379
+ OR (column_name ~ '^[a-z]+id$' AND column_name != 'id' AND length(column_name) > 3)
380
+ )
381
+ ORDER BY table_name, column_name;`,
382
+ },
383
+ {
384
+ id: 'non-tetra-auth-pattern',
385
+ name: 'Policies not using Tetra auth functions',
386
+ category: 'migration',
387
+ severity: 'warn',
388
+ sql: `
389
+ SELECT tablename as table_name,
390
+ policyname || ' (' || cmd || '): uses non-standard auth — ' || left(qual, 60) as issue
391
+ FROM pg_policies
392
+ WHERE schemaname = 'public'
393
+ AND qual IS NOT NULL
394
+ AND qual != 'true'
395
+ AND qual != 'false'
396
+ AND qual NOT LIKE '%auth_admin_organizations%'
397
+ AND qual NOT LIKE '%auth_user_organizations%'
398
+ AND qual NOT LIKE '%auth_creator_organizations%'
399
+ AND qual NOT LIKE '%auth_is_superadmin%'
400
+ AND qual NOT LIKE '%auth.uid()%'
401
+ AND qual NOT LIKE '%is_active%'
402
+ ORDER BY tablename, cmd;`,
403
+ },
404
+ {
405
+ id: 'missing-org-column',
406
+ name: 'Tables without organization_id (except special tables)',
407
+ category: 'migration',
408
+ severity: 'warn',
409
+ sql: `
410
+ SELECT c.relname as table_name, 'No organization_id column — cannot scope by org' as issue
411
+ FROM pg_class c
412
+ JOIN pg_namespace n ON n.oid = c.relnamespace
413
+ WHERE n.nspname = 'public'
414
+ AND c.relkind = 'r'
415
+ AND c.relname NOT LIKE 'pg_%'
416
+ AND c.relname NOT LIKE '_supabase%'
417
+ AND c.relname NOT LIKE '_prisma%'
418
+ AND c.relname NOT IN ('organizations', 'users_public', 'organization_members', 'organization_invites')
419
+ AND NOT EXISTS (
420
+ SELECT 1 FROM information_schema.columns ic
421
+ WHERE ic.table_schema = 'public'
422
+ AND ic.table_name = c.relname
423
+ AND ic.column_name = 'organization_id'
424
+ )
425
+ ORDER BY c.relname;`,
426
+ },
427
+ {
428
+ id: 'org-members-table',
429
+ name: 'organization_members table missing or wrong structure',
430
+ category: 'migration',
431
+ severity: 'error',
432
+ sql: `
433
+ WITH expected_cols AS (
434
+ SELECT unnest(ARRAY['id','user_id','organization_id','role','status','created_at','updated_at']) as col
435
+ ),
436
+ actual_cols AS (
437
+ SELECT column_name as col
438
+ FROM information_schema.columns
439
+ WHERE table_schema = 'public' AND table_name = 'organization_members'
440
+ ),
441
+ table_exists AS (
442
+ SELECT EXISTS (
443
+ SELECT 1 FROM information_schema.tables
444
+ WHERE table_schema = 'public' AND table_name = 'organization_members'
445
+ ) as exists_flag
446
+ )
447
+ SELECT 'organization_members' as table_name,
448
+ CASE
449
+ WHEN NOT (SELECT exists_flag FROM table_exists) THEN 'Table does not exist'
450
+ ELSE 'Missing column: ' || e.col
451
+ END as issue
452
+ FROM expected_cols e
453
+ WHERE NOT (SELECT exists_flag FROM table_exists)
454
+ OR e.col NOT IN (SELECT col FROM actual_cols)
455
+ LIMIT 1;`,
456
+ },
457
+ ];
458
+ }
459
+ /**
460
+ * Generate a single SQL query that runs ALL audit checks and returns a summary.
461
+ * Returns one row per violation with check_id, severity, category, table_name, and issue.
462
+ */
463
+ export function generateAuditSQL() {
464
+ const checks = getAuditChecks();
465
+ const ctes = checks.map((check, i) => {
466
+ const innerSQL = check.sql.trim().replace(/;$/, '');
467
+ return `check_${i} AS (
468
+ SELECT
469
+ '${check.id}' as check_id,
470
+ '${check.severity}' as severity,
471
+ '${check.category}' as category,
472
+ '${check.name}' as check_name,
473
+ t.table_name,
474
+ t.issue
475
+ FROM (${innerSQL}) t
476
+ )`;
477
+ });
478
+ const unions = checks.map((_, i) => `SELECT * FROM check_${i}`).join('\nUNION ALL\n');
479
+ return `-- ============================================================================
480
+ -- Tetra RLS Audit v2.0
481
+ -- ============================================================================
482
+ -- 22 checks across 6 categories:
483
+ -- A. Foundation — RLS enabled, FORCE RLS, policies exist
484
+ -- B. Policy quality — roles, expressions, completeness
485
+ -- C. Auth functions — exist, signature, SECURITY DEFINER
486
+ -- D. RLS bypass routes — functions, views, grants that skip RLS
487
+ -- E. Data exposure — Realtime, storage, schema leaks
488
+ -- F. Migration verification — Tetra standards applied correctly
489
+ --
490
+ -- Empty result = all checks pass.
491
+ -- Generated by: Tetra RLS Auditor v2.0
492
+ -- ============================================================================
493
+
494
+ WITH
495
+ ${ctes.join(',\n')}
496
+
497
+ ${unions}
498
+ ORDER BY
499
+ CASE severity WHEN 'error' THEN 1 WHEN 'warn' THEN 2 ELSE 3 END,
500
+ CASE category WHEN 'foundation' THEN 1 WHEN 'policy' THEN 2 WHEN 'auth' THEN 3 WHEN 'bypass' THEN 4 WHEN 'exposure' THEN 5 ELSE 6 END,
501
+ check_id,
502
+ table_name;
503
+ `;
504
+ }
505
+ //# sourceMappingURL=rls-auditor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rls-auditor.js","sourceRoot":"","sources":["../../src/generators/rls-auditor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAeH;;;GAGG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO;QAEL,8DAA8D;QAC9D,qDAAqD;QACrD,8DAA8D;QAE9D;YACE,EAAE,EAAE,QAAQ;YACZ,IAAI,EAAE,4BAA4B;YAClC,QAAQ,EAAE,YAAY;YACtB,QAAQ,EAAE,OAAO;YACjB,GAAG,EAAE;;;;;;;;;;oBAUS;SACf;QACD;YACE,EAAE,EAAE,cAAc;YAClB,IAAI,EAAE,8DAA8D;YACpE,QAAQ,EAAE,YAAY;YACtB,QAAQ,EAAE,OAAO;YACjB,GAAG,EAAE;;;;;;;;;;oBAUS;SACf;QACD;YACE,EAAE,EAAE,iBAAiB;YACrB,IAAI,EAAE,6DAA6D;YACnE,QAAQ,EAAE,YAAY;YACtB,QAAQ,EAAE,OAAO;YACjB,GAAG,EAAE;;;;;;;;;;oBAUS;SACf;QAED,8DAA8D;QAC9D,uDAAuD;QACvD,8DAA8D;QAE9D;YACE,EAAE,EAAE,oBAAoB;YACxB,IAAI,EAAE,wCAAwC;YAC9C,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,OAAO;YACjB,GAAG,EAAE;;;;;;;yBAOc;SACpB;QACD;YACE,EAAE,EAAE,oBAAoB;YACxB,IAAI,EAAE,8CAA8C;YACpD,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,OAAO;YACjB,GAAG,EAAE;;;;;;;;;;;;;yBAac;SACpB;QACD;YACE,EAAE,EAAE,sBAAsB;YAC1B,IAAI,EAAE,sDAAsD;YAC5D,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,OAAO;YACjB,GAAG,EAAE;;;;;;;;oBAQS;SACf;QACD;YACE,EAAE,EAAE,iBAAiB;YACrB,IAAI,EAAE,uEAAuE;YAC7E,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,OAAO;YACjB,GAAG,EAAE;;;;;;;;;;;;oBAYS;SACf;QACD;YACE,EAAE,EAAE,uBAAuB;YAC3B,IAAI,EAAE,wDAAwD;YAC9D,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,MAAM;YAChB,GAAG,EAAE;;;;;;;;;;;;;;;;;;oBAkBS;SACf;QAED,8DAA8D;QAC9D,iDAAiD;QACjD,8DAA8D;QAE9D;YACE,EAAE,EAAE,wBAAwB;YAC5B,IAAI,EAAE,qCAAqC;YAC3C,QAAQ,EAAE,MAAM;YAChB,QAAQ,EAAE,OAAO;YACjB,GAAG,EAAE;;;;;;;;;;;;;GAaR;SACE;QACD;YACE,EAAE,EAAE,8BAA8B;YAClC,IAAI,EAAE,4EAA4E;YAClF,QAAQ,EAAE,MAAM;YAChB,QAAQ,EAAE,OAAO;YACjB,GAAG,EAAE;;;;;;;;;oBASS;SACf;QACD;YACE,EAAE,EAAE,2BAA2B;YAC/B,IAAI,EAAE,wEAAwE;YAC9E,QAAQ,EAAE,MAAM;YAChB,QAAQ,EAAE,OAAO;YACjB,GAAG,EAAE;;;;;;;;;;;;;;;;;;;oBAmBS;SACf;QACD;YACE,EAAE,EAAE,oBAAoB;YACxB,IAAI,EAAE,uEAAuE;YAC7E,QAAQ,EAAE,MAAM;YAChB,QAAQ,EAAE,MAAM;YAChB,GAAG,EAAE;;;;;;;;;oBASS;SACf;QAED,8DAA8D;QAC9D,0CAA0C;QAC1C,8DAA8D;QAE9D;YACE,EAAE,EAAE,0BAA0B;YAC9B,IAAI,EAAE,0DAA0D;YAChE,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,OAAO;YACjB,GAAG,EAAE;;;;;;;;;;;;;;;;oBAgBS;SACf;QACD;YACE,EAAE,EAAE,yBAAyB;YAC7B,IAAI,EAAE,0DAA0D;YAChE,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,OAAO;YACjB,GAAG,EAAE;;;;;;;;;;;;;oBAaS;SACf;QACD;YACE,EAAE,EAAE,kBAAkB;YACtB,IAAI,EAAE,uEAAuE;YAC7E,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,OAAO;YACjB,GAAG,EAAE;;;;;;;;;;;oBAWS;SACf;QACD;YACE,EAAE,EAAE,mBAAmB;YACvB,IAAI,EAAE,kCAAkC;YACxC,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,OAAO;YACjB,GAAG,EAAE;;;;;;;;;qCAS0B;SAChC;QAED,8DAA8D;QAC9D,+CAA+C;QAC/C,8DAA8D;QAE9D;YACE,EAAE,EAAE,uBAAuB;YAC3B,IAAI,EAAE,+CAA+C;YACrD,QAAQ,EAAE,UAAU;YACpB,QAAQ,EAAE,MAAM;YAChB,GAAG,EAAE;;;;;;;oBAOS;SACf;QAED,8DAA8D;QAC9D,sDAAsD;QACtD,8DAA8D;QAE9D;YACE,EAAE,EAAE,gBAAgB;YACpB,IAAI,EAAE,sCAAsC;YAC5C,QAAQ,EAAE,WAAW;YACrB,QAAQ,EAAE,OAAO;YACjB,GAAG,EAAE;;;;;;;;;;;;;;;;;;;;;;kCAsBuB;SAC7B;QACD;YACE,EAAE,EAAE,wBAAwB;YAC5B,IAAI,EAAE,yCAAyC;YAC/C,QAAQ,EAAE,WAAW;YACrB,QAAQ,EAAE,MAAM;YAChB,GAAG,EAAE;;;;;;;;;;;;;;yBAcc;SACpB;QACD;YACE,EAAE,EAAE,oBAAoB;YACxB,IAAI,EAAE,wDAAwD;YAC9D,QAAQ,EAAE,WAAW;YACrB,QAAQ,EAAE,MAAM;YAChB,GAAG,EAAE;;;;;;;;;;;;;;;;oBAgBS;SACf;QACD;YACE,EAAE,EAAE,mBAAmB;YACvB,IAAI,EAAE,uDAAuD;YAC7D,QAAQ,EAAE,WAAW;YACrB,QAAQ,EAAE,OAAO;YACjB,GAAG,EAAE;;;;;;;;;;;;;;;;;;;;;;;SAuBF;SACJ;KACF,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB;IAC9B,MAAM,MAAM,GAAG,cAAc,EAAE,CAAC;IAEhC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;QACnC,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACpD,OAAO,SAAS,CAAC;;OAEd,KAAK,CAAC,EAAE;OACR,KAAK,CAAC,QAAQ;OACd,KAAK,CAAC,QAAQ;OACd,KAAK,CAAC,IAAI;;;UAGP,QAAQ;EAChB,CAAC;IACD,CAAC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,uBAAuB,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAEtF,OAAO;;;;;;;;;;;;;;;;EAgBP,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;;EAEhB,MAAM;;;;;;CAMP,CAAC;AACF,CAAC"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Tetra RLS Checker v2.0
3
+ *
4
+ * Runs the Tetra RLS audit checks against a live Supabase project
5
+ * and returns a structured report with pass/fail per check.
6
+ *
7
+ * This is NOT just a config checker — it verifies:
8
+ * - Foundation: RLS enabled, FORCE RLS, policies exist
9
+ * - Policy quality: roles, expressions, CRUD coverage
10
+ * - Auth functions: exist, SECURITY DEFINER, search_path
11
+ * - RLS bypass routes: functions/views/grants that skip RLS entirely
12
+ * - Data exposure: Realtime on sensitive tables
13
+ * - Migration state: Tetra standards actually applied in the DB
14
+ *
15
+ * Usage:
16
+ * import { runRLSCheck } from '@tetra/core';
17
+ * const report = await runRLSCheck(supabaseClient);
18
+ * console.log(report.text); // Full formatted report
19
+ * console.log(report.passed); // true only if zero errors
20
+ * if (!report.passed) throw new Error(report.summary);
21
+ */
22
+ import { type AuditCheck } from './rls-auditor.js';
23
+ export interface RLSViolation {
24
+ table_name: string;
25
+ issue: string;
26
+ [key: string]: unknown;
27
+ }
28
+ export interface RLSCheckResult {
29
+ id: string;
30
+ name: string;
31
+ category: AuditCheck['category'];
32
+ severity: AuditCheck['severity'];
33
+ passed: boolean;
34
+ violationCount: number;
35
+ violations: RLSViolation[];
36
+ }
37
+ export interface RLSCategorySummary {
38
+ category: string;
39
+ label: string;
40
+ totalChecks: number;
41
+ passedChecks: number;
42
+ errorCount: number;
43
+ warnCount: number;
44
+ passed: boolean;
45
+ }
46
+ export interface RLSReport {
47
+ /** true if zero errors (warnings don't fail the report) */
48
+ passed: boolean;
49
+ /** Human-readable summary line */
50
+ summary: string;
51
+ /** Total checks run */
52
+ totalChecks: number;
53
+ passedCount: number;
54
+ errorCount: number;
55
+ warnCount: number;
56
+ infoCount: number;
57
+ /** Per-category breakdown */
58
+ categories: RLSCategorySummary[];
59
+ /** Per-check results */
60
+ checks: RLSCheckResult[];
61
+ /** ISO timestamp */
62
+ timestamp: string;
63
+ /** Formatted text report for logging/display */
64
+ text: string;
65
+ }
66
+ /**
67
+ * Minimal Supabase client interface — needs rpc() for SQL execution.
68
+ * Works with @supabase/supabase-js service_role client.
69
+ */
70
+ interface SupabaseExecutor {
71
+ rpc: (fn: string, params: Record<string, unknown>) => Promise<{
72
+ data: unknown;
73
+ error: unknown;
74
+ }>;
75
+ }
76
+ /**
77
+ * Run all Tetra RLS audit checks against a live Supabase project.
78
+ * Uses the combined CTE approach for a single DB round-trip.
79
+ */
80
+ export declare function runRLSCheck(supabase: SupabaseExecutor, options?: {
81
+ /** Only run checks of these severities */
82
+ severities?: AuditCheck['severity'][];
83
+ /** Only run checks in these categories */
84
+ categories?: AuditCheck['category'][];
85
+ /** Skip these check IDs */
86
+ skip?: string[];
87
+ }): Promise<RLSReport>;
88
+ /**
89
+ * Run checks one-by-one (slower but shows which check fails if SQL errors).
90
+ * Use this for debugging when runRLSCheck fails.
91
+ */
92
+ export declare function runRLSCheckDebug(supabase: SupabaseExecutor): Promise<RLSReport>;
93
+ export {};
94
+ //# sourceMappingURL=rls-checker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rls-checker.d.ts","sourceRoot":"","sources":["../../src/generators/rls-checker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnE,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC;IACjC,QAAQ,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC;IACjC,MAAM,EAAE,OAAO,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,YAAY,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,2DAA2D;IAC3D,MAAM,EAAE,OAAO,CAAC;IAChB,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,6BAA6B;IAC7B,UAAU,EAAE,kBAAkB,EAAE,CAAC;IACjC,wBAAwB;IACxB,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,oBAAoB;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,UAAU,gBAAgB;IACxB,GAAG,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAClG;AAaD;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,CAAC,EAAE;IACR,0CAA0C;IAC1C,UAAU,CAAC,EAAE,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;IACtC,0CAA0C;IAC1C,UAAU,CAAC,EAAE,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;IACtC,2BAA2B;IAC3B,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB,GACA,OAAO,CAAC,SAAS,CAAC,CA+CpB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,gBAAgB,GACzB,OAAO,CAAC,SAAS,CAAC,CAkBpB"}