@soulbatical/tetra-dev-toolkit 1.15.1 → 1.16.1

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.
package/README.md CHANGED
@@ -17,8 +17,8 @@ npx tetra-setup
17
17
  ```
18
18
 
19
19
  This creates:
20
- - `.husky/pre-commit` — quick security checks before every commit
21
- - `.husky/pre-push` — hygiene + RLS security gate before every push
20
+ - `.husky/pre-commit` — quick security checks + migration lint before every commit
21
+ - `.husky/pre-push` — full security audit + RLS security gate before every push
22
22
  - `.github/workflows/quality.yml` — full audit on PR/push to main
23
23
  - `.tetra-quality.json` — project config (override defaults)
24
24
 
@@ -30,238 +30,149 @@ npx tetra-setup ci # GitHub Actions only
30
30
  npx tetra-setup config # Config file only
31
31
  ```
32
32
 
33
- Re-running `tetra-setup hooks` on an existing project adds missing hooks without overwriting existing ones.
34
-
35
33
  ---
36
34
 
37
- ## 3-Layer Security Model
38
-
39
- Every Tetra project is protected by three layers. All three must pass before code reaches production. No fallbacks. No soft warnings. Hard errors only.
40
-
41
- ```
42
- LAYER 1: PRE-COMMIT (tetra-audit quick)
43
- Blocks: hardcoded secrets, service key exposure, direct createClient imports
44
-
45
- LAYER 2: PRE-PUSH (tetra-check-rls + tetra-audit hygiene)
46
- Blocks: RLS violations on live DB, repo clutter, missing FORCE RLS
47
-
48
- LAYER 3: BUILD (Railway/deploy — tetra-check-rls --errors-only)
49
- Blocks: anything that bypassed --no-verify. Last line of defense.
50
- ```
51
-
52
- ### Layer 1: Pre-commit
53
-
54
- Installed by `tetra-setup hooks`. Runs `tetra-audit quick` which executes critical security checks:
55
-
56
- | Check | What it catches |
57
- |-------|-----------------|
58
- | Hardcoded Secrets | API keys, tokens, JWTs in source code |
59
- | Service Key Exposure | Supabase service role keys in frontend code |
60
- | Direct Supabase Client | `createClient()` imports outside core db wrappers |
61
-
62
- If any check fails, the commit is **blocked**. No `--force`, no workaround.
63
-
64
- ### Layer 2: Pre-push (RLS Security Gate)
65
-
66
- Installed by `tetra-setup hooks`. Runs before every `git push`. Two checks:
67
-
68
- 1. **Repo hygiene** — `tetra-audit hygiene` blocks clutter
69
- 2. **RLS Security Gate** — `tetra-check-rls --errors-only` against the live Supabase database
70
-
71
- The RLS gate auto-detects your Doppler project from `doppler.yaml` and runs 23 checks across 6 categories:
72
-
73
- | Category | Checks | What it catches |
74
- |----------|--------|-----------------|
75
- | Foundation | 3 | Tables without RLS, missing FORCE RLS, RLS without policies |
76
- | Policy Quality | 5 | Public role mutations, always-true policies, missing WITH CHECK |
77
- | Auth Functions | 4 | Missing auth functions, wrong SECURITY DEFINER, bad search_path |
78
- | Bypass Routes | 5 | SECURITY DEFINER data functions, anon grants, views bypassing RLS |
79
- | Data Exposure | 1 | Realtime enabled on sensitive tables |
80
- | Migration State | 4 | Non-Tetra auth patterns, missing org columns |
81
-
82
- If the RLS gate fails, the push is **blocked**. No `--no-verify` escape — Layer 3 catches that.
83
-
84
- ### Layer 3: Build-time check
85
-
86
- Add to your Railway/deploy build command:
87
-
88
- ```bash
89
- # Railway build command
90
- doppler run -- npx tetra-check-rls --errors-only && npm run build
91
- ```
92
-
93
- This catches developers who bypass git hooks with `--no-verify`. If RLS is broken, the build fails, the deploy never happens.
94
-
95
- ---
96
-
97
- ## Supabase Client Architecture
98
-
99
- Every Tetra project MUST use the 5 db helpers. Direct `createClient` imports are **blocked** by the `direct-supabase-client` check.
100
-
101
- ### The 5 DB Helpers
35
+ ## 8-Layer Security Architecture
102
36
 
103
- | Helper | Key Used | RLS | When to Use |
104
- |--------|----------|-----|-------------|
105
- | `adminDB(req)` | Anon + user JWT | Enforced | Admin panel queries (org-scoped) |
106
- | `userDB(req)` | Anon + user JWT | Enforced | User's own data |
107
- | `publicDB()` | Anon key | Enforced | Public/anonymous access |
108
- | `superadminDB(req)` | Service Role | Bypassed | Cross-org operations (superadmin only, audit logged) |
109
- | `systemDB(context)` | Service Role | Bypassed | Webhooks, crons, system ops (whitelisted, audit logged) |
37
+ Every Tetra project is protected by 8 layers. From the moment a developer writes code to the moment a database query executes — every layer enforces security independently. If one layer is bypassed, the next catches it.
110
38
 
111
- ### Why this matters
39
+ See [SECURITY.md](./SECURITY.md) for the complete architecture reference.
112
40
 
113
41
  ```
114
- createClient(url, SERVICE_ROLE_KEY) → Bypasses ALL RLS. No audit trail. No access control.
115
- adminDB(req) → Uses user's JWT. RLS enforces org boundaries. Audit logged.
42
+ LAYER 1: CONFIG FILES Feature configs define what SHOULD happen
43
+ LAYER 2: PRE-COMMIT Quick checks + migration lint on staged files
44
+ LAYER 3: PRE-PUSH Full security audit (12 checks) + live RLS gate
45
+ LAYER 4: MIGRATION PUSH tetra-db-push blocks unsafe SQL before Supabase
46
+ LAYER 5: CI/CD TypeScript compile + tests + build
47
+ LAYER 6: RUNTIME Express middleware (HTTPS, CORS, auth, rate limit, input sanitize)
48
+ LAYER 7: DATABASE RLS policies enforce org/user isolation on every query
49
+ LAYER 8: DB HELPERS adminDB/userDB/publicDB/systemDB enforce correct access pattern
116
50
  ```
117
51
 
118
- A single `createClient` call with the service role key can leak data across all organizations. The db helpers enforce the security boundary at the application layer.
119
-
120
- ### Allowed exceptions
121
-
122
- These files MAY import `createClient` directly because they ARE the wrappers:
123
-
124
- - `core/systemDb.ts`, `core/publicDb.ts`, `core/adminDb.ts`, `core/userDb.ts`, `core/superadminDb.ts`
125
- - `core/Application.ts` (bootstrap)
126
- - `features/database/services/SupabaseUserClient.ts`
127
- - `auth/controllers/AuthController.ts` (needs `auth.admin` API)
128
- - `backend-mcp/src/supabase-client.ts`, `org-scoped-client.ts`, `api-key-service.ts`, `http-server.ts`
129
- - `scripts/` (not production code)
130
-
131
- Everything else gets a **hard error** at commit time.
132
-
133
52
  ---
134
53
 
135
- ## Usage
136
-
137
- ```bash
138
- npx tetra-audit # Run all checks
139
- npx tetra-audit security # Security checks only
140
- npx tetra-audit stability # Stability checks only
141
- npx tetra-audit codeQuality # Code quality checks only
142
- npx tetra-audit supabase # Supabase checks only
143
- npx tetra-audit hygiene # Repo hygiene checks only
144
- npx tetra-audit quick # Quick critical checks (pre-commit)
145
- npx tetra-audit --ci # CI mode (GitHub Actions annotations)
146
- npx tetra-audit --json # JSON output
147
- npx tetra-audit --verbose # Detailed output with fix suggestions
148
- ```
149
-
150
- Exit codes: `0` = passed, `1` = failed, `2` = error. No middle ground.
151
-
152
- ## RLS Security Gate (standalone)
153
-
154
- ```bash
155
- npx tetra-check-rls # Auto-detect from doppler.yaml
156
- npx tetra-check-rls --url <url> --key <key> # Explicit credentials
157
- npx tetra-check-rls --errors-only # CI/build mode
158
- npx tetra-check-rls --json # JSON output for automation
159
- npx tetra-check-rls --fix # Generate hardening migration SQL
160
- npx tetra-check-rls --check-exec-sql # Verify exec_sql function is secure
161
- ```
162
-
163
- ### Programmatic usage
54
+ ## CLI Tools
164
55
 
165
- ```typescript
166
- import { runRLSCheck } from '@soulbatical/tetra-core';
56
+ | Command | Description |
57
+ |---------|-------------|
58
+ | `tetra-audit` | Run quality/security/hygiene checks |
59
+ | `tetra-audit quick` | Quick critical checks (pre-commit) |
60
+ | `tetra-audit security` | Full security suite (12 checks) |
61
+ | `tetra-audit stability` | Stability suite (3 checks) |
62
+ | `tetra-audit codeQuality` | Code quality suite (4 checks) |
63
+ | `tetra-audit supabase` | Supabase suite (3 checks) |
64
+ | `tetra-audit hygiene` | Repo hygiene suite (2 checks) |
65
+ | `tetra-audit --ci` | CI mode (GitHub Actions annotations) |
66
+ | `tetra-audit --json` | JSON output |
67
+ | `tetra-audit --verbose` | Detailed output with fix suggestions |
68
+ | `tetra-migration-lint` | Offline SQL migration linter (8 rules) |
69
+ | `tetra-migration-lint --staged` | Only git-staged .sql files (pre-commit hook) |
70
+ | `tetra-migration-lint --fix-suggestions` | Show fix SQL per violation |
71
+ | `tetra-db-push` | Safe wrapper: lint + `supabase db push` |
72
+ | `tetra-check-rls` | RLS security gate against live Supabase |
73
+ | `tetra-check-rls --fix` | Generate hardening migration SQL |
74
+ | `tetra-setup` | Install hooks, CI, and config |
75
+ | `tetra-init` | Initialize project config files |
76
+ | `tetra-dev-token` | Generate development tokens |
167
77
 
168
- const report = await runRLSCheck(supabaseServiceClient);
169
- if (!report.passed) {
170
- throw new Error(report.summary); // Hard error. No soft fail.
171
- }
172
- ```
78
+ Exit codes: `0` = passed, `1` = failed (CRITICAL/HIGH), `2` = error. No middle ground.
173
79
 
174
80
  ---
175
81
 
176
82
  ## Check Suites
177
83
 
178
- ### Security (6 checks)
84
+ ### Security (12 checks)
179
85
 
180
86
  | Check | Severity | What it catches |
181
87
  |-------|----------|-----------------|
182
- | Hardcoded Secrets | critical | API keys, tokens, JWTs in source code |
183
- | Service Key Exposure | critical | Supabase service role keys in frontend |
184
- | Deprecated Supabase Admin | high | Legacy `supabaseAdmin` patterns |
185
- | Direct Supabase Client | critical | Direct `createClient` imports outside core wrappers |
186
- | SystemDB Whitelist | critical | Unauthorized service role key usage in authenticated routes |
187
- | Gitignore Validation | high | Missing .gitignore entries, tracked .env files |
88
+ | `hardcoded-secrets` | critical | API keys, tokens, JWTs in source code |
89
+ | `service-key-exposure` | critical | Supabase service role keys in frontend |
90
+ | `deprecated-supabase-admin` | critical | Legacy `supabaseAdmin` patterns |
91
+ | `direct-supabase-client` | critical | Direct `createClient` imports outside core wrappers |
92
+ | `frontend-supabase-queries` | critical | `.from()` / `.rpc()` / `.storage` calls in frontend code |
93
+ | `tetra-core-compliance` | critical | Missing configureAuth, authenticateToken, or db helpers |
94
+ | `mixed-db-usage` | critical | Controller uses wrong DB helper or mixes types |
95
+ | `config-rls-alignment` | critical | Feature config accessLevel does not match RLS policies |
96
+ | `rpc-security-mode` | critical | SECURITY DEFINER on data RPCs (bypasses RLS) |
97
+ | `route-config-alignment` | high | Route middleware does not match config accessLevel |
98
+ | `systemdb-whitelist` | high | systemDB() in unauthorized contexts |
99
+ | `gitignore-validation` | high | Missing .gitignore entries, tracked .env files |
100
+
101
+ ### Migration Lint (8 rules)
102
+
103
+ | Rule | Severity | What it catches |
104
+ |------|----------|-----------------|
105
+ | `DEFINER_DATA_RPC` | critical | SECURITY DEFINER on data RPCs |
106
+ | `CREATE_TABLE_NO_RLS` | critical | New table without ENABLE ROW LEVEL SECURITY |
107
+ | `DISABLE_RLS` | critical | ALTER TABLE ... DISABLE RLS |
108
+ | `USING_TRUE_WRITE` | critical | USING(true) on INSERT/UPDATE/DELETE/ALL policies |
109
+ | `GRANT_PUBLIC_ANON` | critical | GRANT write permissions to public/anon |
110
+ | `RAW_SERVICE_KEY` | critical | Hardcoded JWT in migration file |
111
+ | `DROP_POLICY` | high | DROP POLICY without replacement in same migration |
112
+ | `POLICY_NO_WITH_CHECK` | medium | INSERT/UPDATE policy without WITH CHECK clause |
188
113
 
189
- ### Stability (3 checks)
114
+ ### Supabase (3 checks, auto-detected)
190
115
 
191
116
  | Check | Severity | What it catches |
192
117
  |-------|----------|-----------------|
193
- | Husky Hooks | medium | Missing pre-commit/pre-push hooks |
194
- | CI Pipeline | medium | Missing or incomplete CI config |
195
- | NPM Audit | high | Known vulnerabilities in dependencies |
118
+ | `rls-policy-audit` | critical | Tables without Row Level Security |
119
+ | `rpc-param-mismatch` | critical | TypeScript `.rpc()` calls with wrong parameter names vs SQL |
120
+ | `rpc-generator-origin` | high | RPC functions not generated by Tetra SQL Generator |
196
121
 
197
- ### Code Quality (4 checks)
122
+ ### Stability (3 checks)
198
123
 
199
124
  | Check | Severity | What it catches |
200
125
  |-------|----------|-----------------|
201
- | API Response Format | medium | Non-standard response format |
202
- | File Size | medium | Files exceeding line limits |
203
- | Naming Conventions | medium | Inconsistent file/dir naming |
204
- | Route Separation | high | Business logic in route files |
126
+ | `husky-hooks` | medium | Missing pre-commit/pre-push hooks |
127
+ | `ci-pipeline` | medium | Missing or incomplete CI config |
128
+ | `npm-audit` | high | Known vulnerabilities in dependencies |
205
129
 
206
- ### Supabase (3 checks, auto-detected)
130
+ ### Code Quality (4 checks)
207
131
 
208
132
  | Check | Severity | What it catches |
209
133
  |-------|----------|-----------------|
210
- | RLS Policy Audit | critical | Tables without Row Level Security |
211
- | RPC Param Mismatch | critical | TypeScript `.rpc()` calls with wrong parameter names vs SQL |
212
- | RPC Generator Origin | high | RPC functions not generated by Tetra SQL Generator |
134
+ | `api-response-format` | medium | Non-standard response format |
135
+ | `file-size` | medium | Files exceeding line limits |
136
+ | `naming-conventions` | medium | Inconsistent file/dir naming |
137
+ | `route-separation` | high | Business logic in route files |
213
138
 
214
139
  ### Hygiene (2 checks)
215
140
 
216
141
  | Check | Severity | What it catches |
217
142
  |-------|----------|-----------------|
218
- | File Organization | high | Stray .md, .sh, clutter in code dirs |
219
- | Stella Compliance | medium | Missing Stella integration |
143
+ | `file-organization` | high | Stray .md, .sh, clutter in code dirs |
144
+ | `stella-compliance` | medium | Missing Stella integration |
220
145
 
221
146
  ---
222
147
 
223
- ## Health Checks
224
-
225
- Scored assessment (0-N points) used by the Ralph Manager dashboard:
226
-
227
- | Check | Max | What it measures |
228
- |-------|-----|------------------|
229
- | File Organization | 6pt | Docs in /docs, scripts in /scripts, clean root & code dirs |
230
- | Git | 4pt | Clean working tree, branch hygiene, commit frequency |
231
- | Gitignore | 3pt | Critical entries present |
232
- | CLAUDE.md | 3pt | Project instructions for AI assistants |
233
- | Secrets | 3pt | No exposed secrets |
234
- | Tests | 4pt | Test framework, coverage, test files |
235
- | Naming Conventions | 5pt | File/dir naming consistency |
236
- | Infrastructure YML | 3pt | Railway/Docker config |
237
- | Doppler Compliance | 2pt | Secrets management via Doppler |
238
- | MCP Servers | 2pt | MCP configuration |
239
- | Stella Integration | 2pt | Stella package integration |
240
- | Quality Toolkit | 2pt | Tetra dev-toolkit installed |
241
- | Repo Visibility | 1pt | Private repo |
242
- | RLS Audit | 3pt | Row Level Security policies |
243
- | Plugins | 2pt | Claude Code plugin config |
244
- | VinciFox Widget | 1pt | Widget installation |
148
+ ## Supabase Client Architecture
245
149
 
246
- ---
150
+ Every Tetra project MUST use the 5 db helpers. Direct `createClient` imports are **blocked** by the `direct-supabase-client` check.
247
151
 
248
- ## Auto-fix: Cleanup Script
152
+ ### The 5 DB Helpers
249
153
 
250
- For hygiene issues, an auto-fix script is included:
154
+ | Helper | Key Used | RLS | When to Use |
155
+ |--------|----------|-----|-------------|
156
+ | `adminDB(req)` | Anon + user JWT | Enforced | Admin panel queries (org-scoped) |
157
+ | `userDB(req)` | Anon + user JWT | Enforced | User's own data |
158
+ | `publicDB()` | Anon key | Enforced | Public/anonymous access |
159
+ | `superadminDB(req)` | Service Role | Bypassed | Cross-org operations (superadmin only, audit logged) |
160
+ | `systemDB(context)` | Service Role | Bypassed | Webhooks, crons, system ops (whitelisted, audit logged) |
251
161
 
252
- ```bash
253
- # Dry run (shows what would change)
254
- bash node_modules/@soulbatical/tetra-dev-toolkit/bin/cleanup-repos.sh
162
+ ### Why this matters
255
163
 
256
- # Execute
257
- bash node_modules/@soulbatical/tetra-dev-toolkit/bin/cleanup-repos.sh --execute
258
164
  ```
165
+ createClient(url, SERVICE_ROLE_KEY) → Bypasses ALL RLS. No audit trail. No access control.
166
+ adminDB(req) → Uses user's JWT. RLS enforces org boundaries. Audit logged.
167
+ ```
168
+
169
+ A single `createClient` call with the service role key can leak data across all organizations. The db helpers enforce the security boundary at the application layer.
259
170
 
260
171
  ---
261
172
 
262
173
  ## Configuration
263
174
 
264
- Override defaults in `.tetra-quality.json` or `"tetra-quality"` key in `package.json`:
175
+ Override defaults in `.tetra-quality.json`:
265
176
 
266
177
  ```json
267
178
  {
@@ -274,50 +185,31 @@ Override defaults in `.tetra-quality.json` or `"tetra-quality"` key in `package.
274
185
  },
275
186
  "supabase": {
276
187
  "publicRpcFunctions": ["get_public_data"],
277
- "publicTables": ["public_lookup"]
188
+ "publicTables": ["public_lookup"],
189
+ "backendOnlyTables": ["system_logs", "cron_state"],
190
+ "securityDefinerWhitelist": ["auth_org_id", "auth_uid"]
278
191
  },
279
- "stability": {
280
- "allowedVulnerabilities": {
281
- "critical": 0,
282
- "high": 0,
283
- "moderate": 10
284
- }
285
- }
192
+ "ignore": ["**/legacy/**", "**/scripts/**"]
286
193
  }
287
194
  ```
288
195
 
289
196
  ---
290
197
 
291
- ## CLI Tools
292
-
293
- | Command | Description |
294
- |---------|-------------|
295
- | `tetra-audit` | Run quality/security/hygiene checks |
296
- | `tetra-setup` | Install hooks, CI, and config |
297
- | `tetra-check-rls` | RLS security gate against live Supabase |
298
- | `tetra-init` | Initialize project config files |
299
- | `tetra-dev-token` | Generate development tokens |
300
-
301
- ---
302
-
303
198
  ## New Project Checklist
304
199
 
305
- After `npm install` in a new Tetra project:
306
-
307
200
  ```bash
308
201
  # 1. Install hooks (creates pre-commit + pre-push)
309
202
  npx tetra-setup hooks
310
203
 
311
- # 2. Verify hooks are active
312
- cat .husky/pre-commit # Should contain tetra-audit quick
313
- cat .husky/pre-push # Should contain tetra-check-rls
314
-
315
- # 3. Run full audit
204
+ # 2. Run full audit
316
205
  npx tetra-audit
317
206
 
318
- # 4. Run RLS gate manually once
207
+ # 3. Run RLS gate manually
319
208
  doppler run -- npx tetra-check-rls
320
209
 
210
+ # 4. Verify migration lint works
211
+ npx tetra-migration-lint
212
+
321
213
  # 5. Add to Railway build command
322
214
  # doppler run -- npx tetra-check-rls --errors-only && npm run build
323
215
  ```
@@ -328,44 +220,54 @@ If any step fails, fix it before writing code. No exceptions.
328
220
 
329
221
  ## Changelog
330
222
 
331
- ### 1.9.0
223
+ ### 1.15.0
224
+
225
+ **New: Migration Lint + DB Push Guard**
226
+ - `tetra-migration-lint` — offline SQL migration linter (8 rules)
227
+ - `tetra-db-push` — safe wrapper around `supabase db push` (lint first, push second)
228
+ - Pre-commit hook now lints staged `.sql` files automatically
229
+ - Rules: DEFINER on data RPCs, CREATE TABLE without RLS, DISABLE RLS, USING(true) writes, GRANT public/anon, DROP POLICY without replacement, missing WITH CHECK, hardcoded JWT
230
+
231
+ ### 1.14.0
232
+
233
+ **New: Config-RLS Alignment + Route-Config Alignment + RPC Security Mode**
234
+ - `config-rls-alignment` — verifies feature config accessLevel matches RLS policies on the table
235
+ - `route-config-alignment` — verifies route middleware matches config accessLevel (admin routes need auth middleware)
236
+ - `rpc-security-mode` — scans ALL RPCs for SECURITY DEFINER (must be INVOKER unless whitelisted)
332
237
 
333
- **New: Direct Supabase Client check (CRITICAL)**
334
- - Added `direct-supabase-client` check in security suite
335
- - Blocks any `createClient` import from `@supabase/supabase-js` outside core db wrappers
336
- - Hard error at commit time — no fallback, no override
337
- - Type-only imports (`import type { SupabaseClient }`) are allowed
338
- - Allowed files whitelist: core wrappers, auth controller, MCP server internals, scripts
238
+ ### 1.13.0
339
239
 
340
- **New: 3-Layer Security Model documentation**
341
- - Layer 1: Pre-commit (tetra-audit quick)
342
- - Layer 2: Pre-push (tetra-check-rls + hygiene)
343
- - Layer 3: Build-time (tetra-check-rls --errors-only in Railway)
344
- - Complete Supabase Client Architecture docs with the 5 db helpers
240
+ **New: Mixed DB Usage Detection**
241
+ - `mixed-db-usage` controllers must use exactly ONE DB helper type matching their naming convention
242
+ - AdminController adminDB only, UserController → userDB only, etc.
243
+ - Detects systemDB misuse when authenticated user context is available
345
244
 
346
- **Improved: prepublishOnly hooks**
347
- - tetra-core: `npm run build && npm run typecheck` before publish
348
- - tetra-dev-toolkit: `npm test` before publish
245
+ ### 1.12.0
246
+
247
+ **New: Tetra Core Compliance**
248
+ - `tetra-core-compliance` — if a project has @soulbatical/tetra-core, it MUST use configureAuth, authenticateToken, and db helpers
249
+ - No more "skipped" checks — if tetra-core is a dependency, ALL security checks run
250
+
251
+ ### 1.11.0
252
+
253
+ **New: Frontend Supabase Queries check**
254
+ - `frontend-supabase-queries` — blocks `.from()`, `.rpc()`, `.storage` in frontend code
255
+ - Frontend MUST use API client, never query Supabase directly
256
+
257
+ ### 1.10.0
258
+
259
+ **Improved: RLS Policy Audit**
260
+ - Tables with RLS ON but 0 policies now flagged as CRITICAL (was HIGH)
261
+ - New config: `supabase.backendOnlyTables` for tables that intentionally have no policies
262
+
263
+ ### 1.9.0
349
264
 
350
- ### 1.3.0 (2025-02-21)
265
+ **New: Direct Supabase Client check + 3-Layer Security Model**
351
266
 
352
- **New: Hygiene suite**
353
- - Added `tetra-audit hygiene` — detects stray docs, scripts, clutter in code dirs
354
- - Added `cleanup-repos.sh` auto-fix script in `bin/`
355
- - `tetra-setup hooks` now creates pre-push hook with hygiene gate
356
- - Re-running `tetra-setup hooks` on existing repos adds hygiene check without overwriting
267
+ ### 1.3.0
357
268
 
358
- **New: RPC Param Mismatch check**
359
- - Added `rpc-param-mismatch` check in supabase suite
360
- - Statically compares `.rpc()` calls in TypeScript with SQL function parameter names
361
- - Catches PGRST202 errors before they hit production
269
+ **New: Hygiene suite + RPC Param Mismatch check**
362
270
 
363
271
  ### 1.2.0
364
272
 
365
273
  - Initial public version
366
- - Security suite: hardcoded secrets, service key exposure, deprecated admin, systemdb whitelist, gitignore
367
- - Stability suite: husky hooks, CI pipeline, npm audit
368
- - Code quality suite: API response format
369
- - Supabase suite: RLS policy audit
370
- - Health checks: 16 checks, max 37pt
371
- - CLI: `tetra-audit`, `tetra-setup`, `tetra-dev-token`
@@ -46,11 +46,18 @@ const RULES = [
46
46
  const funcName = match[1]
47
47
  // Auth helpers are OK as DEFINER
48
48
  const authWhitelist = [
49
+ // Auth helpers (need DEFINER to read auth schema)
49
50
  'auth_org_id', 'auth_uid', 'auth_role', 'auth_user_role',
50
51
  'auth_admin_organizations', 'auth_user_id', 'requesting_user_id',
51
52
  'get_auth_org_id', 'get_current_user_id',
52
53
  'auth_user_organizations', 'auth_creator_organizations',
53
- 'auth_organization_id', 'auth_is_admin', 'auth_is_superadmin'
54
+ 'auth_organization_id', 'auth_is_admin', 'auth_is_superadmin',
55
+ // Public RPCs (anon access, DEFINER needed to bypass RLS and return safe columns)
56
+ 'search_public_ad_library',
57
+ // System/billing RPCs (no user context)
58
+ 'get_org_credit_limits',
59
+ // Supabase internal
60
+ 'handle_new_user', 'moddatetime'
54
61
  ]
55
62
  return !authWhitelist.includes(funcName)
56
63
  },
@@ -105,12 +105,46 @@ function parseMigrations(projectRoot) {
105
105
  sqlFiles = findFiles(projectRoot, dir.replace(projectRoot + '/', '') + '/*.sql')
106
106
  } catch { continue }
107
107
 
108
+ // Sort migrations by filename (timestamp order) so later migrations override earlier ones
109
+ sqlFiles.sort()
110
+
108
111
  for (const file of sqlFiles) {
109
112
  let content
110
113
  try { content = readFileSync(file, 'utf-8') } catch { continue }
111
114
 
112
115
  const relFile = file.replace(projectRoot + '/', '')
113
116
 
117
+ // Handle DROP POLICY — removes policy from earlier migration
118
+ const dropPolicyMatches = content.matchAll(/DROP\s+POLICY\s+(?:IF\s+EXISTS\s+)?"?([^";\s]+)"?\s+ON\s+(?:public\.)?(\w+)/gi)
119
+ for (const m of dropPolicyMatches) {
120
+ const policyName = m[1]
121
+ const table = m[2]
122
+ if (tables.has(table)) {
123
+ tables.get(table).policies = tables.get(table).policies.filter(p => p.name !== policyName)
124
+ }
125
+ }
126
+
127
+ // Handle ALTER FUNCTION ... SECURITY INVOKER/DEFINER — overrides earlier CREATE FUNCTION
128
+ const alterFuncMatches = content.matchAll(/ALTER\s+FUNCTION\s+(?:public\.)?(\w+)(?:\s*\([^)]*\))?\s+SECURITY\s+(INVOKER|DEFINER)/gi)
129
+ for (const m of alterFuncMatches) {
130
+ const funcName = m[1]
131
+ const securityMode = m[2].toUpperCase()
132
+ // Update all tables that reference this function
133
+ for (const [, tableInfo] of tables) {
134
+ if (tableInfo.rpcFunctions.has(funcName)) {
135
+ tableInfo.rpcFunctions.get(funcName).securityMode = securityMode
136
+ tableInfo.rpcFunctions.get(funcName).file = relFile
137
+ }
138
+ }
139
+ }
140
+
141
+ // Handle DISABLE RLS — overrides earlier ENABLE
142
+ const disableRlsMatches = content.matchAll(/ALTER\s+TABLE\s+(?:public\.)?(\w+)\s+DISABLE\s+ROW\s+LEVEL\s+SECURITY/gi)
143
+ for (const m of disableRlsMatches) {
144
+ const table = m[1]
145
+ if (tables.has(table)) tables.get(table).rlsEnabled = false
146
+ }
147
+
114
148
  // Find RLS enables
115
149
  const rlsMatches = content.matchAll(/ALTER\s+TABLE\s+(?:public\.)?(\w+)\s+ENABLE\s+ROW\s+LEVEL\s+SECURITY/gi)
116
150
  for (const m of rlsMatches) {
@@ -74,6 +74,11 @@ export async function run(config, projectRoot) {
74
74
  summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 }
75
75
  }
76
76
 
77
+ // Build whitelist from hardcoded + config
78
+ const configWhitelist = config?.supabase?.directSupabaseClientWhitelist || config?.directSupabaseClientWhitelist || []
79
+ const extraPatterns = configWhitelist.map(p => new RegExp(p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
80
+ const allAllowed = [...ALLOWED_FILES, ...extraPatterns]
81
+
77
82
  // Get all TypeScript files
78
83
  const files = await glob('**/*.ts', {
79
84
  cwd: projectRoot,
@@ -107,8 +112,8 @@ export async function run(config, projectRoot) {
107
112
 
108
113
  // Check for createClient import from supabase
109
114
  if (/import\s*\{[^}]*createClient[^}]*\}\s*from\s*['"]@supabase\/supabase-js['"]/.test(line)) {
110
- // Check if this file is in the allowed list
111
- const isAllowed = ALLOWED_FILES.some(pattern => pattern.test(file))
115
+ // Check if this file is in the allowed list (hardcoded + config whitelist)
116
+ const isAllowed = allAllowed.some(pattern => pattern.test(file))
112
117
 
113
118
  if (!isAllowed) {
114
119
  results.passed = false
@@ -129,7 +134,7 @@ export async function run(config, projectRoot) {
129
134
  // Also catch: const supabase = createClient(url, key) without the import
130
135
  // (in case someone imports it via re-export or variable)
131
136
  if (/createClient\s*\(\s*(?:process\.env|supabase|SUPABASE)/.test(line)) {
132
- const isAllowed = ALLOWED_FILES.some(pattern => pattern.test(file))
137
+ const isAllowed = allAllowed.some(pattern => pattern.test(file))
133
138
  const isImportLine = /import/.test(line)
134
139
 
135
140
  if (!isAllowed && !isImportLine) {
@@ -76,6 +76,9 @@ export async function run(config, projectRoot) {
76
76
  details: { controllersChecked: 0, violations: 0 }
77
77
  }
78
78
 
79
+ // Config-based ignore for controllers that legitimately mix DB helpers
80
+ const mixedDbIgnore = config?.supabase?.mixedDbUsageIgnore || []
81
+
79
82
  const files = await glob('**/*[Cc]ontroller*.ts', {
80
83
  cwd: projectRoot,
81
84
  ignore: [
@@ -104,6 +107,9 @@ export async function run(config, projectRoot) {
104
107
  results.details.controllersChecked++
105
108
  const fileName = basename(file)
106
109
 
110
+ // Skip controllers explicitly ignored in config
111
+ if (mixedDbIgnore.some(pattern => file.includes(pattern) || fileName.includes(pattern))) continue
112
+
107
113
  // Detect all DB usages in this file
108
114
  const dbUsages = new Map() // level -> [{type, line, code}]
109
115
  const lines = content.split('\n')
@@ -151,17 +157,25 @@ export async function run(config, projectRoot) {
151
157
 
152
158
  // HIGH: Multiple DB helper types in one controller
153
159
  if (usedLevels.length > 1) {
154
- results.passed = false
155
- results.findings.push({
156
- file,
157
- line: 1,
158
- type: 'mixed db usage',
159
- severity: 'high',
160
- message: `${fileName} mixes ${usedLevels.join(' + ')} database helpers. Controllers MUST use exactly ONE DB helper type.`,
161
- fix: `Split into separate controllers per DB level, or refactor services to accept injected supabase client. Admin + System mix → move system ops to a separate SystemXxxController.`
162
- })
163
- results.summary.high++
164
- results.summary.total++
160
+ // Allow: Superadmin controllers may legitimately use SUPERADMIN + ADMIN
161
+ const isSuperadminMixingAdmin = expectedLevel === 'SUPERADMIN' &&
162
+ usedLevels.length === 2 &&
163
+ usedLevels.includes('SUPERADMIN') &&
164
+ usedLevels.includes('ADMIN')
165
+
166
+ if (!isSuperadminMixingAdmin) {
167
+ results.passed = false
168
+ results.findings.push({
169
+ file,
170
+ line: 1,
171
+ type: 'mixed db usage',
172
+ severity: 'high',
173
+ message: `${fileName} mixes ${usedLevels.join(' + ')} database helpers. Controllers MUST use exactly ONE DB helper type.`,
174
+ fix: `Split into separate controllers per DB level, or refactor services to accept injected supabase client. Admin + System mix → move system ops to a separate SystemXxxController.`
175
+ })
176
+ results.summary.high++
177
+ results.summary.total++
178
+ }
165
179
  }
166
180
 
167
181
  // MEDIUM: DB helper doesn't match naming convention
@@ -123,6 +123,9 @@ export async function run(config, projectRoot) {
123
123
  details: { routesChecked: 0, violations: 0 }
124
124
  }
125
125
 
126
+ // Config-based ignore for specific features/routes
127
+ const routeIgnore = config?.supabase?.routeConfigAlignmentIgnore || []
128
+
126
129
  const featureConfigs = parseFeatureConfigs(projectRoot)
127
130
 
128
131
  if (featureConfigs.length === 0) {
@@ -132,6 +135,8 @@ export async function run(config, projectRoot) {
132
135
  }
133
136
 
134
137
  for (const cfg of featureConfigs) {
138
+ // Skip features explicitly ignored in config
139
+ if (routeIgnore.some(pattern => cfg.tableName === pattern || cfg.configFile.includes(pattern))) continue
135
140
  const routeFiles = findRouteFiles(cfg.featureDir)
136
141
 
137
142
  // --- system: should NOT have any route file ---
@@ -80,11 +80,13 @@ const FORBIDDEN_FILE_PATTERNS = [
80
80
  ]
81
81
 
82
82
  /**
83
- * Maximum allowed whitelist size — anything above indicates architectural problems
83
+ * Default maximum allowed whitelist size — override via .tetra-quality.json:
84
+ * { "supabase": { "maxWhitelistEntries": 50 } }
84
85
  */
85
- const MAX_WHITELIST_SIZE = 35
86
+ const DEFAULT_MAX_WHITELIST_SIZE = 35
86
87
 
87
88
  export async function run(config, projectRoot) {
89
+ const MAX_WHITELIST_SIZE = config?.supabase?.maxWhitelistEntries || DEFAULT_MAX_WHITELIST_SIZE
88
90
  const results = {
89
91
  passed: true,
90
92
  findings: [],
@@ -132,14 +134,52 @@ export async function run(config, projectRoot) {
132
134
  const whitelistMatch = systemDbContent.match(/new Set\s*(?:<[^>]+>)?\s*\(\s*\[([\s\S]*?)\]\s*\)/m)
133
135
 
134
136
  if (whitelistMatch) {
135
- const entries = whitelistMatch[1]
136
- .split('\n')
137
- .map(line => {
138
- const m = line.match(/['"]([^'"]+)['"]/)
139
- return m ? m[1] : null
140
- })
141
- .filter(Boolean)
142
- whitelist = new Set(entries)
137
+ const rawLines = whitelistMatch[1].split('\n')
138
+ let lastComment = null
139
+
140
+ for (let i = 0; i < rawLines.length; i++) {
141
+ const line = rawLines[i].trim()
142
+
143
+ // Track comments — group comments (// ...) or inline comments
144
+ const commentMatch = line.match(/^\s*\/\/\s*(.+)/)
145
+ if (commentMatch) {
146
+ lastComment = commentMatch[1].trim()
147
+ continue
148
+ }
149
+
150
+ const entryMatch = line.match(/['"]([^'"]+)['"]/)
151
+ if (!entryMatch) { continue }
152
+
153
+ const entry = entryMatch[1]
154
+ whitelist.add(entry)
155
+
156
+ // Check for inline comment: 'entry', // reason
157
+ const inlineCommentMatch = line.match(/['"][^'"]+['"]\s*,?\s*\/\/\s*(.+)/)
158
+ const reason = inlineCommentMatch ? inlineCommentMatch[1].trim() : lastComment
159
+
160
+ // Find line number in original file
161
+ const entryLineInFile = systemDbContent.substring(0, systemDbContent.indexOf(entry)).split('\n').length
162
+
163
+ if (!reason) {
164
+ results.findings.push({
165
+ file: systemDbPath.replace(projectRoot + '/', ''),
166
+ line: entryLineInFile,
167
+ type: 'whitelist-no-justification',
168
+ severity: 'high',
169
+ message: `systemDB whitelist entry '${entry}' has NO comment explaining WHY it needs service role key access. Every whitelist entry MUST have a comment above or inline.`,
170
+ fix: `Add a comment explaining why '${entry}' cannot use adminDB/userDB. Example:\n // OAuth callback — browser redirect, no JWT in header\n '${entry}',`
171
+ })
172
+ results.summary.high++
173
+ results.summary.total++
174
+ results.passed = false
175
+ }
176
+
177
+ // Reset lastComment after consuming it for a non-dynamic entry
178
+ // (don't reset for "// Dynamic:" comments which apply to patterns, not specific entries)
179
+ if (lastComment && !lastComment.toLowerCase().startsWith('dynamic')) {
180
+ lastComment = null
181
+ }
182
+ }
143
183
  }
144
184
 
145
185
  if (whitelist.size > MAX_WHITELIST_SIZE) {
@@ -157,6 +197,28 @@ export async function run(config, projectRoot) {
157
197
  }
158
198
  }
159
199
 
200
+ // Also check .tetra-quality.json for documented whitelist with justifications
201
+ const configWhitelist = config?.supabase?.systemDbWhitelist || {}
202
+ if (typeof configWhitelist === 'object' && !Array.isArray(configWhitelist)) {
203
+ // Format: { "context-name": "reason why systemDB is needed" }
204
+ for (const [context, reason] of Object.entries(configWhitelist)) {
205
+ whitelist.add(context)
206
+ if (!reason || typeof reason !== 'string' || reason.trim().length < 5) {
207
+ results.findings.push({
208
+ file: '.tetra-quality.json',
209
+ line: 1,
210
+ type: 'config-whitelist-no-justification',
211
+ severity: 'high',
212
+ message: `systemDbWhitelist entry '${context}' has empty or insufficient justification: "${reason || ''}". Explain WHY this context cannot use adminDB/userDB.`,
213
+ fix: `In .tetra-quality.json, set: "systemDbWhitelist": { "${context}": "Reason: e.g. OAuth callback — no JWT available, browser redirect flow" }`
214
+ })
215
+ results.summary.high++
216
+ results.summary.total++
217
+ results.passed = false
218
+ }
219
+ }
220
+ }
221
+
160
222
  // ─── Check 2: systemDB in forbidden locations ───────────────
161
223
 
162
224
  const files = await glob('**/*.ts', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulbatical/tetra-dev-toolkit",
3
- "version": "1.15.1",
3
+ "version": "1.16.1",
4
4
  "publishConfig": {
5
5
  "access": "restricted"
6
6
  },