@soulbatical/tetra-dev-toolkit 1.8.4 → 1.8.6

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.
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Tetra Dev Toolkit - RLS Gate
5
+ *
6
+ * Pre-deploy gate that verifies RLS security on a live Supabase project.
7
+ * Exit code 0 = all checks pass, exit code 1 = violations found.
8
+ *
9
+ * Usage:
10
+ * tetra-check-rls # Auto-detect Supabase config from env/doppler
11
+ * tetra-check-rls --url <url> --key <key> # Explicit credentials
12
+ * tetra-check-rls --errors-only # Only show errors, skip warnings
13
+ * tetra-check-rls --json # JSON output for CI
14
+ * tetra-check-rls --fix # Generate hardening migration SQL
15
+ *
16
+ * Environment variables:
17
+ * SUPABASE_URL - Supabase project URL
18
+ * SUPABASE_SERVICE_KEY - Service role key (NOT anon key!)
19
+ *
20
+ * CI Usage (GitHub Actions):
21
+ * - name: RLS Security Gate
22
+ * run: npx tetra-check-rls --errors-only
23
+ * env:
24
+ * SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
25
+ * SUPABASE_SERVICE_KEY: ${{ secrets.SUPABASE_SERVICE_KEY }}
26
+ */
27
+
28
+ import { program } from 'commander'
29
+ import { createClient } from '@supabase/supabase-js'
30
+ import { readFileSync, existsSync } from 'fs'
31
+ import { join, resolve, dirname } from 'path'
32
+ import { config as dotenvConfig } from 'dotenv'
33
+ import chalk from 'chalk'
34
+
35
+ // Load tetra-core dynamically (it's a peer/workspace dep)
36
+ async function loadTetraCore() {
37
+ try {
38
+ return await import('@soulbatical/tetra-core')
39
+ } catch {
40
+ // Fallback: try relative path in monorepo
41
+ try {
42
+ return await import('../../core/dist/index.js')
43
+ } catch {
44
+ console.error(chalk.red('ERROR: @soulbatical/tetra-core not found. Install it first.'))
45
+ process.exit(1)
46
+ }
47
+ }
48
+ }
49
+
50
+ function findSupabaseConfig() {
51
+ // Try .env, .env.local, backend/.env, backend/.env.local
52
+ const cwd = process.cwd()
53
+ const envPaths = [
54
+ join(cwd, '.env'),
55
+ join(cwd, '.env.local'),
56
+ join(cwd, 'backend', '.env'),
57
+ join(cwd, 'backend', '.env.local'),
58
+ ]
59
+
60
+ for (const p of envPaths) {
61
+ if (existsSync(p)) dotenvConfig({ path: p })
62
+ }
63
+
64
+ const url = process.env.SUPABASE_URL || process.env.VITE_SUPABASE_URL
65
+ const key = process.env.SUPABASE_SERVICE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY
66
+
67
+ return { url, key }
68
+ }
69
+
70
+ program
71
+ .name('tetra-check-rls')
72
+ .description('Pre-deploy RLS security gate — checks live Supabase project')
73
+ .version('1.0.0')
74
+ .option('--url <url>', 'Supabase project URL')
75
+ .option('--key <key>', 'Supabase service role key')
76
+ .option('--errors-only', 'Only fail on errors, ignore warnings')
77
+ .option('--json', 'Output as JSON')
78
+ .option('--fix', 'Generate hardening migration SQL to stdout')
79
+ .option('--check-exec-sql', 'Only check if exec_sql is installed and secure')
80
+ .action(async (options) => {
81
+ try {
82
+ const tetra = await loadTetraCore()
83
+ const config = findSupabaseConfig()
84
+
85
+ const url = options.url || config.url
86
+ const key = options.key || config.key
87
+
88
+ if (!url || !key) {
89
+ console.error(chalk.red('ERROR: Missing Supabase credentials.'))
90
+ console.error(chalk.gray('Set SUPABASE_URL + SUPABASE_SERVICE_KEY, or use --url and --key'))
91
+ process.exit(1)
92
+ }
93
+
94
+ if (!key.startsWith('eyJ')) {
95
+ console.error(chalk.red('ERROR: Key does not look like a service_role key (should start with eyJ).'))
96
+ console.error(chalk.gray('Make sure you are using the SERVICE_ROLE key, not the anon key.'))
97
+ process.exit(1)
98
+ }
99
+
100
+ const supabase = createClient(url, key)
101
+
102
+ // --- Check exec_sql exists and is secure ---
103
+ if (tetra.checkExecSQLExists) {
104
+ const execCheck = await tetra.checkExecSQLExists(supabase)
105
+
106
+ if (!execCheck.exists) {
107
+ console.error(chalk.red.bold('\n BLOCKED: exec_sql function not found\n'))
108
+ console.error(chalk.yellow(' The RLS checker requires the exec_sql function to be deployed.'))
109
+ console.error(chalk.yellow(' This function is SAFE — it uses SECURITY INVOKER and only service_role can call it.\n'))
110
+ console.error(chalk.white(' To deploy it, run this migration:'))
111
+ console.error(chalk.cyan(' npx tetra-check-rls --fix | head -50\n'))
112
+ console.error(chalk.white(' Or get the full SQL:'))
113
+ console.error(chalk.cyan(' import { generateExecSQL } from "@soulbatical/tetra-core"'))
114
+ console.error(chalk.cyan(' console.log(generateExecSQL())\n'))
115
+ process.exit(1)
116
+ }
117
+
118
+ if (!execCheck.secure) {
119
+ console.error(chalk.red.bold('\n CRITICAL: exec_sql exists but is NOT SECURE\n'))
120
+ for (const issue of execCheck.issues) {
121
+ console.error(chalk.red(` - ${issue}`))
122
+ }
123
+ console.error(chalk.yellow('\n Redeploy the Tetra version to fix:'))
124
+ console.error(chalk.cyan(' npx tetra-check-rls --fix\n'))
125
+ process.exit(1)
126
+ }
127
+
128
+ if (options.checkExecSql) {
129
+ console.log(chalk.green(' exec_sql is installed and secure.'))
130
+ process.exit(0)
131
+ }
132
+ }
133
+
134
+ // --- Generate fix migration ---
135
+ if (options.fix) {
136
+ if (tetra.generateExecSQL) {
137
+ console.log(tetra.generateExecSQL())
138
+ }
139
+ // Also generate the audit SQL for reference
140
+ if (tetra.generateAuditSQL) {
141
+ console.log('\n-- To audit manually, run this SQL in the Supabase SQL editor:')
142
+ console.log(tetra.generateAuditSQL())
143
+ }
144
+ process.exit(0)
145
+ }
146
+
147
+ // --- Run full RLS audit ---
148
+ console.log(chalk.blue.bold('\n Tetra RLS Security Gate\n'))
149
+
150
+ const report = await tetra.runRLSCheck(supabase, {
151
+ severities: options.errorsOnly ? ['error'] : undefined,
152
+ })
153
+
154
+ if (options.json) {
155
+ console.log(JSON.stringify(report, null, 2))
156
+ } else {
157
+ console.log(report.text)
158
+ }
159
+
160
+ if (!report.passed) {
161
+ console.error(chalk.red.bold(`\n DEPLOY BLOCKED: ${report.errorCount} error(s) found\n`))
162
+ console.error(chalk.yellow(' Fix all errors before deploying. Warnings are advisory.\n'))
163
+ process.exit(1)
164
+ }
165
+
166
+ if (report.warnCount > 0) {
167
+ console.log(chalk.yellow(`\n PASS with ${report.warnCount} warning(s) — review recommended\n`))
168
+ } else {
169
+ console.log(chalk.green.bold('\n ALL CLEAR — RLS security verified\n'))
170
+ }
171
+
172
+ process.exit(0)
173
+ } catch (err) {
174
+ console.error(chalk.red(`\n ERROR: ${err.message}\n`))
175
+ if (err.stack && process.env.DEBUG) console.error(err.stack)
176
+ process.exit(1)
177
+ }
178
+ })
179
+
180
+ program.parse()
@@ -153,7 +153,7 @@ echo "✅ Pre-commit checks passed"
153
153
  console.log(' ⏭️ .husky/pre-commit already exists (use --force to overwrite)')
154
154
  }
155
155
 
156
- // Create or extend pre-push hook with hygiene check
156
+ // Create or extend pre-push hook with hygiene check + RLS security gate
157
157
  const prePushPath = join(huskyDir, 'pre-push')
158
158
  const hygieneBlock = `
159
159
  # Tetra hygiene check — blocks push if repo contains clutter
@@ -167,23 +167,72 @@ if [ $? -ne 0 ]; then
167
167
  exit 1
168
168
  fi
169
169
  echo "✅ Repo hygiene passed"
170
+ `
171
+
172
+ // RLS security gate — auto-detects Doppler project from doppler.yaml
173
+ const rlsBlock = `
174
+ # Tetra RLS Security Gate — blocks push if DB has security violations
175
+ # Auto-detects Doppler project from doppler.yaml
176
+ DOPPLER_PROJECT=""
177
+ for dopfile in doppler.yaml backend/doppler.yaml; do
178
+ if [ -f "$dopfile" ]; then
179
+ DOPPLER_PROJECT=$(grep 'project:' "$dopfile" | head -1 | sed 's/.*project: *//')
180
+ break
181
+ fi
182
+ done
183
+
184
+ if [ -n "$DOPPLER_PROJECT" ]; then
185
+ echo ""
186
+ echo "🔐 Running RLS security gate..."
187
+ doppler run --project "$DOPPLER_PROJECT" --config dev_backend -- npx tetra-check-rls --errors-only 2>/dev/null
188
+ RLS_EXIT=$?
189
+ if [ $RLS_EXIT -ne 0 ]; then
190
+ echo ""
191
+ echo "════════════════════════════════════════════════════════════"
192
+ echo "❌ PUSH BLOCKED — RLS SECURITY VIOLATION"
193
+ echo "════════════════════════════════════════════════════════════"
194
+ echo ""
195
+ echo "Fix all errors before pushing."
196
+ echo "Run for details: doppler run --project $DOPPLER_PROJECT --config dev_backend -- npx tetra-check-rls"
197
+ echo ""
198
+ exit 1
199
+ fi
200
+ echo "✅ RLS security gate passed"
201
+ else
202
+ echo "⚠️ No doppler.yaml found — skipping RLS check (add doppler.yaml to enable)"
203
+ fi
170
204
  `
171
205
 
172
206
  if (!existsSync(prePushPath)) {
173
- // No pre-push hook yet — create one
174
- const prePushContent = `#!/bin/sh\n${hygieneBlock}\n`
207
+ // No pre-push hook yet — create one with both checks
208
+ const prePushContent = `#!/bin/sh\n${hygieneBlock}\n${rlsBlock}\n`
175
209
  writeFileSync(prePushPath, prePushContent)
176
210
  execSync(`chmod +x ${prePushPath}`)
177
- console.log(' ✅ Created .husky/pre-push with hygiene check')
211
+ console.log(' ✅ Created .husky/pre-push with hygiene + RLS security gate')
178
212
  } else {
179
- // Pre-push hook exists — add hygiene check if not already there
180
- const existing = readFileSync(prePushPath, 'utf-8')
213
+ // Pre-push hook exists — add missing checks
214
+ let existing = readFileSync(prePushPath, 'utf-8')
215
+ let changed = false
216
+
181
217
  if (!existing.includes('tetra-audit hygiene')) {
182
- writeFileSync(prePushPath, existing.trimEnd() + '\n' + hygieneBlock)
218
+ existing = existing.trimEnd() + '\n' + hygieneBlock
219
+ changed = true
183
220
  console.log(' ✅ Added hygiene check to existing .husky/pre-push')
184
221
  } else {
185
222
  console.log(' ⏭️ .husky/pre-push already has hygiene check')
186
223
  }
224
+
225
+ if (!existing.includes('tetra-check-rls')) {
226
+ existing = existing.trimEnd() + '\n' + rlsBlock
227
+ changed = true
228
+ console.log(' ✅ Added RLS security gate to existing .husky/pre-push')
229
+ } else {
230
+ console.log(' ⏭️ .husky/pre-push already has RLS security gate')
231
+ }
232
+
233
+ if (changed) {
234
+ writeFileSync(prePushPath, existing)
235
+ }
187
236
  }
188
237
 
189
238
  // Add prepare script to package.json
@@ -29,7 +29,8 @@ export async function run(config, projectRoot) {
29
29
  }
30
30
 
31
31
  const ignoreMigrations = config.codeQuality?.namingConventions?.ignoreMigrations || []
32
- const health = await healthCheck(projectRoot, { ignoreMigrations })
32
+ const ignoreViolations = config.codeQuality?.namingConventions?.ignoreViolations || []
33
+ const health = await healthCheck(projectRoot, { ignoreMigrations, ignoreViolations })
33
34
  result.details = health.details
34
35
 
35
36
  const dbViolations = health.details.database?.violations || []
@@ -28,6 +28,7 @@ function scanDatabaseNaming(projectPath, options = {}) {
28
28
  ]
29
29
 
30
30
  const ignoreMigrations = options.ignoreMigrations || []
31
+ const ignoreViolations = options.ignoreViolations || []
31
32
 
32
33
  const sqlFiles = []
33
34
  for (const dir of migrationDirs) {
@@ -291,10 +292,19 @@ function scanDatabaseNaming(projectPath, options = {}) {
291
292
  }
292
293
  }
293
294
 
295
+ // Apply ignoreViolations — suppress violations matching any pattern
296
+ const finalViolations = ignoreViolations.length > 0
297
+ ? violations.filter(v => {
298
+ const ignored = ignoreViolations.some(pattern => v.includes(pattern))
299
+ if (ignored) compliant++
300
+ return !ignored
301
+ })
302
+ : violations
303
+
294
304
  return {
295
305
  totalFields,
296
306
  compliant,
297
- violations: violations.slice(0, 50),
307
+ violations: finalViolations.slice(0, 50),
298
308
  compliancePercent: totalFields > 0 ? Math.round((compliant / totalFields) * 100) : 100
299
309
  }
300
310
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulbatical/tetra-dev-toolkit",
3
- "version": "1.8.4",
3
+ "version": "1.8.6",
4
4
  "publishConfig": {
5
5
  "access": "restricted"
6
6
  },
@@ -28,7 +28,8 @@
28
28
  "tetra-audit": "./bin/tetra-audit.js",
29
29
  "tetra-init": "./bin/tetra-init.js",
30
30
  "tetra-setup": "./bin/tetra-setup.js",
31
- "tetra-dev-token": "./bin/tetra-dev-token.js"
31
+ "tetra-dev-token": "./bin/tetra-dev-token.js",
32
+ "tetra-check-rls": "./bin/tetra-check-rls.js"
32
33
  },
33
34
  "files": [
34
35
  "bin/",