@soulbatical/tetra-dev-toolkit 1.15.1 → 1.16.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.
- package/README.md +144 -242
- package/bin/tetra-migration-lint.js +8 -1
- package/lib/checks/security/config-rls-alignment.js +34 -0
- package/lib/checks/security/direct-supabase-client.js +8 -3
- package/lib/checks/security/mixed-db-usage.js +25 -11
- package/lib/checks/security/route-config-alignment.js +5 -0
- package/lib/checks/security/systemdb-whitelist.js +4 -2
- package/package.json +1 -1
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` —
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
39
|
+
See [SECURITY.md](./SECURITY.md) for the complete architecture reference.
|
|
112
40
|
|
|
113
41
|
```
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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 (
|
|
84
|
+
### Security (12 checks)
|
|
179
85
|
|
|
180
86
|
| Check | Severity | What it catches |
|
|
181
87
|
|-------|----------|-----------------|
|
|
182
|
-
|
|
|
183
|
-
|
|
|
184
|
-
|
|
|
185
|
-
|
|
|
186
|
-
|
|
|
187
|
-
|
|
|
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
|
-
###
|
|
114
|
+
### Supabase (3 checks, auto-detected)
|
|
190
115
|
|
|
191
116
|
| Check | Severity | What it catches |
|
|
192
117
|
|-------|----------|-----------------|
|
|
193
|
-
|
|
|
194
|
-
|
|
|
195
|
-
|
|
|
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
|
-
###
|
|
122
|
+
### Stability (3 checks)
|
|
198
123
|
|
|
199
124
|
| Check | Severity | What it catches |
|
|
200
125
|
|-------|----------|-----------------|
|
|
201
|
-
|
|
|
202
|
-
|
|
|
203
|
-
|
|
|
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
|
-
###
|
|
130
|
+
### Code Quality (4 checks)
|
|
207
131
|
|
|
208
132
|
| Check | Severity | What it catches |
|
|
209
133
|
|-------|----------|-----------------|
|
|
210
|
-
|
|
|
211
|
-
|
|
|
212
|
-
|
|
|
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
|
-
|
|
|
219
|
-
|
|
|
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
|
-
##
|
|
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
|
-
|
|
152
|
+
### The 5 DB Helpers
|
|
249
153
|
|
|
250
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
"
|
|
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.
|
|
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
|
-
#
|
|
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.
|
|
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
|
-
|
|
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:
|
|
341
|
-
-
|
|
342
|
-
-
|
|
343
|
-
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
265
|
+
**New: Direct Supabase Client check + 3-Layer Security Model**
|
|
351
266
|
|
|
352
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
*
|
|
83
|
+
* Default maximum allowed whitelist size — override via .tetra-quality.json:
|
|
84
|
+
* { "supabase": { "maxWhitelistEntries": 50 } }
|
|
84
85
|
*/
|
|
85
|
-
const
|
|
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: [],
|