@soulbatical/tetra-dev-toolkit 1.20.0 β†’ 2.0.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.
@@ -43,7 +43,7 @@ program
43
43
  console.log('')
44
44
 
45
45
  const components = component === 'all' || !component
46
- ? ['hooks', 'ci', 'config', 'smoke']
46
+ ? ['hooks', 'ci', 'config']
47
47
  : [component]
48
48
 
49
49
  for (const comp of components) {
@@ -81,12 +81,9 @@ program
81
81
  case 'license-audit':
82
82
  await setupLicenseAudit(options)
83
83
  break
84
- case 'smoke':
85
- await setupSmoke(options)
86
- break
87
84
  default:
88
85
  console.log(`Unknown component: ${comp}`)
89
- console.log('Available: hooks, ci, config, smoke, prettier, eslint-security, coverage, lighthouse, commitlint, depcruiser, knip, license-audit')
86
+ console.log('Available: hooks, ci, config, prettier, eslint-security, coverage, lighthouse, commitlint, depcruiser, knip, license-audit')
90
87
  }
91
88
  }
92
89
 
@@ -870,171 +867,4 @@ async function setupLicenseAudit(options) {
870
867
  console.log(' πŸ“¦ Run: npm install --save-dev license-checker')
871
868
  }
872
869
 
873
- // ─── Smoke Tests ─────────────────────────────────────────────
874
-
875
- async function setupSmoke(options) {
876
- console.log('πŸ”₯ Setting up smoke tests...')
877
-
878
- // Step 1: Detect project name from git remote or package.json
879
- let projectName = null
880
- try {
881
- const remoteUrl = execSync('git remote get-url origin 2>/dev/null', { encoding: 'utf8' }).trim()
882
- const match = remoteUrl.match(/\/([^/]+?)(?:\.git)?$/)
883
- if (match) projectName = match[1]
884
- } catch { /* no git remote */ }
885
-
886
- if (!projectName) {
887
- try {
888
- const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf-8'))
889
- projectName = pkg.name?.replace(/^@[^/]+\//, '') || null
890
- } catch { /* no package.json */ }
891
- }
892
-
893
- if (!projectName) {
894
- const { basename } = await import('path')
895
- projectName = basename(projectRoot)
896
- }
897
-
898
- console.log(` Project: ${projectName}`)
899
-
900
- // Step 2: Get deploy config from ralph-manager
901
- let backendUrl = null
902
- let frontendUrl = null
903
-
904
- const ralphUrl = process.env.RALPH_MANAGER_API || 'http://localhost:3005'
905
- try {
906
- const resp = await fetch(`${ralphUrl}/api/internal/projects?name=${encodeURIComponent(projectName)}`, {
907
- signal: AbortSignal.timeout(5000),
908
- })
909
- if (resp.ok) {
910
- const { data } = await resp.json()
911
- if (data?.deploy_config) {
912
- const dc = data.deploy_config
913
- backendUrl = dc.backend?.url || (dc.domains?.api_domain ? `https://${dc.domains.api_domain}` : null)
914
- frontendUrl = dc.frontend?.url || (dc.domains?.frontend_domain ? `https://${dc.domains.frontend_domain}` : null)
915
- }
916
- }
917
- } catch {
918
- console.log(' ⚠️ Could not reach ralph-manager β€” using manual detection')
919
- }
920
-
921
- // Fallback: detect from railway.json
922
- if (!backendUrl) {
923
- const railwayPath = join(projectRoot, 'railway.json')
924
- if (existsSync(railwayPath)) {
925
- // Railway auto-deploy URL convention: {service-name}-production.up.railway.app
926
- backendUrl = `https://${projectName}-production.up.railway.app`
927
- console.log(` ℹ️ Guessed Railway URL: ${backendUrl}`)
928
- }
929
- }
930
-
931
- if (!backendUrl) {
932
- console.log(' ❌ Could not detect production URL.')
933
- console.log(' Add deploy_config in ralph-manager or pass --url manually.')
934
- console.log(' You can also add "smoke.baseUrl" to .tetra-quality.json manually.')
935
- return
936
- }
937
-
938
- console.log(` Backend: ${backendUrl}`)
939
- if (frontendUrl) console.log(` Frontend: ${frontendUrl}`)
940
-
941
- // Step 3: Add smoke config to .tetra-quality.json
942
- const configPath = join(projectRoot, '.tetra-quality.json')
943
- let config = {}
944
-
945
- if (existsSync(configPath)) {
946
- try {
947
- config = JSON.parse(readFileSync(configPath, 'utf-8'))
948
- } catch { /* invalid JSON, start fresh */ }
949
- }
950
-
951
- if (!config.smoke || options.force) {
952
- config.smoke = {
953
- baseUrl: backendUrl,
954
- ...(frontendUrl ? { frontendUrl } : {}),
955
- timeout: 10000,
956
- checks: {
957
- health: true,
958
- healthDeep: true,
959
- authEndpoints: [
960
- { path: '/api/admin/users', expect: { status: 401 }, description: 'Auth wall: admin' },
961
- ],
962
- ...(frontendUrl ? {
963
- frontendPages: [
964
- { path: '/', expect: { status: 200 }, description: 'Homepage' },
965
- ]
966
- } : {}),
967
- },
968
- notify: {
969
- telegram: true,
970
- onSuccess: false,
971
- onFailure: true,
972
- },
973
- }
974
-
975
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n')
976
- console.log(' βœ… Added smoke config to .tetra-quality.json')
977
- } else {
978
- console.log(' ⏭️ Smoke config already exists (use --force to overwrite)')
979
- }
980
-
981
- // Step 4: Create post-deploy GitHub Actions workflow
982
- const workflowDir = join(projectRoot, '.github/workflows')
983
- if (!existsSync(workflowDir)) {
984
- mkdirSync(workflowDir, { recursive: true })
985
- }
986
-
987
- const smokeWorkflowPath = join(workflowDir, 'post-deploy-tests.yml')
988
- if (!existsSync(smokeWorkflowPath) || options.force) {
989
- let workflowContent = `name: Post-Deploy Smoke Tests
990
-
991
- on:
992
- # Triggered after deploy via webhook
993
- repository_dispatch:
994
- types: [deploy-completed]
995
-
996
- # Manual trigger
997
- workflow_dispatch:
998
-
999
- # Scheduled: every 6 hours
1000
- schedule:
1001
- - cron: '0 */6 * * *'
1002
-
1003
- jobs:
1004
- smoke:
1005
- uses: mralbertzwolle/tetra/.github/workflows/smoke-tests.yml@main
1006
- with:
1007
- backend-url: ${backendUrl}
1008
- `
1009
- if (frontendUrl) {
1010
- workflowContent += ` frontend-url: ${frontendUrl}\n`
1011
- }
1012
- workflowContent += ` wait-seconds: 30
1013
- post-deploy: true
1014
- `
1015
-
1016
- writeFileSync(smokeWorkflowPath, workflowContent)
1017
- console.log(' βœ… Created .github/workflows/post-deploy-tests.yml')
1018
- } else {
1019
- console.log(' ⏭️ Post-deploy workflow already exists (use --force to overwrite)')
1020
- }
1021
-
1022
- // Step 5: Verify smoke tests work
1023
- console.log('')
1024
- console.log(' πŸ§ͺ Quick verification...')
1025
- try {
1026
- const resp = await fetch(`${backendUrl}/api/health`, { signal: AbortSignal.timeout(10000) })
1027
- if (resp.ok) {
1028
- console.log(` βœ… ${backendUrl}/api/health β†’ ${resp.status} OK`)
1029
- } else {
1030
- console.log(` ⚠️ ${backendUrl}/api/health β†’ ${resp.status}`)
1031
- }
1032
- } catch (err) {
1033
- console.log(` ❌ ${backendUrl}/api/health β†’ ${err.message}`)
1034
- }
1035
-
1036
- console.log('')
1037
- console.log(' Next: run `tetra-smoke` to test all endpoints')
1038
- }
1039
-
1040
870
  program.parse()
@@ -38,4 +38,3 @@ export { check as checkLicenseAudit } from './license-audit.js'
38
38
  export { check as checkSast } from './sast.js'
39
39
  export { check as checkBundleSize } from './bundle-size.js'
40
40
  export { check as checkSecurityLayers } from './security-layers.js'
41
- export { check as checkSmokeReadiness } from './smoke-readiness.js'
@@ -34,7 +34,6 @@ import { check as checkLicenseAudit } from './license-audit.js'
34
34
  import { check as checkSast } from './sast.js'
35
35
  import { check as checkBundleSize } from './bundle-size.js'
36
36
  import { check as checkSecurityLayers } from './security-layers.js'
37
- import { check as checkSmokeReadiness } from './smoke-readiness.js'
38
37
  import { calculateHealthStatus } from './types.js'
39
38
 
40
39
  /**
@@ -77,8 +76,7 @@ export async function scanProjectHealth(projectPath, projectName, options = {})
77
76
  checkLicenseAudit(projectPath),
78
77
  checkSast(projectPath),
79
78
  checkBundleSize(projectPath),
80
- checkSecurityLayers(projectPath),
81
- checkSmokeReadiness(projectPath)
79
+ checkSecurityLayers(projectPath)
82
80
  ])
83
81
 
84
82
  const totalScore = checks.reduce((sum, c) => sum + c.score, 0)
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  /**
8
- * @typedef {'plugins'|'mcps'|'git'|'tests'|'secrets'|'quality-toolkit'|'naming-conventions'|'rls-audit'|'rpc-param-mismatch'|'typescript-strict'|'prettier'|'coverage-thresholds'|'eslint-security'|'dependency-cruiser'|'conventional-commits'|'knip'|'dependency-automation'|'license-audit'|'sast'|'bundle-size'|'gitignore'|'repo-visibility'|'vincifox-widget'|'stella-integration'|'claude-md'|'doppler-compliance'|'infrastructure-yml'|'file-organization'|'security-layers'|'smoke-readiness'} HealthCheckType
8
+ * @typedef {'plugins'|'mcps'|'git'|'tests'|'secrets'|'quality-toolkit'|'naming-conventions'|'rls-audit'|'rpc-param-mismatch'|'typescript-strict'|'prettier'|'coverage-thresholds'|'eslint-security'|'dependency-cruiser'|'conventional-commits'|'knip'|'dependency-automation'|'license-audit'|'sast'|'bundle-size'|'gitignore'|'repo-visibility'|'vincifox-widget'|'stella-integration'|'claude-md'|'doppler-compliance'|'infrastructure-yml'|'file-organization'|'security-layers'} HealthCheckType
9
9
  *
10
10
  * @typedef {'ok'|'warning'|'error'} HealthStatus
11
11
  *
@@ -29,7 +29,7 @@ const DUPLICATE_PATTERNS = [
29
29
  {
30
30
  pattern: /const QUESTIONS_DIR\s*=\s*join\(tmpdir\(\)/,
31
31
  label: 'Local QUESTIONS_DIR (telegram question store)',
32
- allowedIn: ['stella/src/telegram.ts', 'backend/src/features/telegram/']
32
+ allowedIn: ['stella/src/telegram.ts']
33
33
  },
34
34
  {
35
35
  pattern: /function detectWorkspaceRef\(\)/,
@@ -49,7 +49,7 @@ const DUPLICATE_PATTERNS = [
49
49
  {
50
50
  pattern: /function splitMessage\(text:\s*string/,
51
51
  label: 'Local splitMessage helper (for telegram)',
52
- allowedIn: ['stella/src/telegram.ts', 'tools/helpers.ts', 'backend/src/features/telegram/']
52
+ allowedIn: ['stella/src/telegram.ts', 'tools/helpers.ts']
53
53
  },
54
54
  {
55
55
  pattern: /function playMacAlert\(\)/,
@@ -24,23 +24,14 @@ export async function run(config, projectRoot) {
24
24
  summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 }
25
25
  }
26
26
 
27
- // Check if project uses the systemDB/userDB pattern (local file OR tetra-core package)
28
- const hasLocalSystemDb = existsSync(`${projectRoot}/backend/src/core/systemDb.ts`) ||
29
- existsSync(`${projectRoot}/src/core/systemDb.ts`)
27
+ // Check if project uses the systemDB/userDB pattern
28
+ const hasSystemDb = existsSync(`${projectRoot}/backend/src/core/systemDb.ts`) ||
29
+ existsSync(`${projectRoot}/src/core/systemDb.ts`)
30
30
 
31
- let hasTetraCore = false
32
- for (const pkgPath of [`${projectRoot}/package.json`, `${projectRoot}/backend/package.json`]) {
33
- if (!existsSync(pkgPath)) continue
34
- try {
35
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
36
- const deps = { ...pkg.dependencies, ...pkg.devDependencies }
37
- if (deps['@soulbatical/tetra-core']) { hasTetraCore = true; break }
38
- } catch { /* skip */ }
39
- }
40
-
41
- if (!hasLocalSystemDb && !hasTetraCore) {
31
+ if (!hasSystemDb) {
32
+ // Project doesn't use this pattern, skip check
42
33
  results.skipped = true
43
- results.skipReason = 'Project does not use systemDB/userDB pattern (no local systemDb.ts and no @soulbatical/tetra-core)'
34
+ results.skipReason = 'Project does not use systemDB/userDB pattern'
44
35
  return results
45
36
  }
46
37
 
@@ -63,15 +63,6 @@ const ALLOWED_FILES = [
63
63
  // Domain middleware (sets RLS session vars β€” needs direct client)
64
64
  /middleware\/domainOrganizationMiddleware\.ts$/,
65
65
 
66
- // Auth routes that only use Supabase Auth API (not DB queries)
67
- /routes\/auth\.ts$/,
68
-
69
- // WebSocket auth verification (only uses auth.getUser for token validation)
70
- /services\/terminalWebSocket\.ts$/,
71
-
72
- // Frontend Supabase client (Vite apps β€” client-side auth only, no Tetra backend)
73
- /frontend\/src\/lib\/supabase\.ts$/,
74
-
75
66
  // Scripts (not production code)
76
67
  /scripts\//,
77
68
  ]
@@ -83,19 +74,10 @@ export async function run(config, projectRoot) {
83
74
  summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 }
84
75
  }
85
76
 
86
- // Build whitelist from hardcoded + config (canonical: security.directSupabaseClientWhitelist)
87
- const configWhitelist = config?.security?.directSupabaseClientWhitelist || config?.directSupabaseClientWhitelist || []
88
- const extraPatterns = configWhitelist.map(p => new RegExp(p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
89
- const allAllowed = [...ALLOWED_FILES, ...extraPatterns]
90
-
91
77
  // Get all TypeScript files
92
78
  const files = await glob('**/*.ts', {
93
79
  cwd: projectRoot,
94
80
  ignore: [
95
- '**/node_modules/**',
96
- '**/.next/**',
97
- '**/dist/**',
98
- '**/build/**',
99
81
  ...config.ignore,
100
82
  '**/*.test.ts',
101
83
  '**/*.spec.ts',
@@ -121,8 +103,8 @@ export async function run(config, projectRoot) {
121
103
 
122
104
  // Check for createClient import from supabase
123
105
  if (/import\s*\{[^}]*createClient[^}]*\}\s*from\s*['"]@supabase\/supabase-js['"]/.test(line)) {
124
- // Check if this file is in the allowed list (hardcoded + config whitelist)
125
- const isAllowed = allAllowed.some(pattern => pattern.test(file))
106
+ // Check if this file is in the allowed list
107
+ const isAllowed = ALLOWED_FILES.some(pattern => pattern.test(file))
126
108
 
127
109
  if (!isAllowed) {
128
110
  results.passed = false
@@ -143,7 +125,7 @@ export async function run(config, projectRoot) {
143
125
  // Also catch: const supabase = createClient(url, key) without the import
144
126
  // (in case someone imports it via re-export or variable)
145
127
  if (/createClient\s*\(\s*(?:process\.env|supabase|SUPABASE)/.test(line)) {
146
- const isAllowed = allAllowed.some(pattern => pattern.test(file))
128
+ const isAllowed = ALLOWED_FILES.some(pattern => pattern.test(file))
147
129
  const isImportLine = /import/.test(line)
148
130
 
149
131
  if (!isAllowed && !isImportLine) {
@@ -189,5 +171,5 @@ function getFixSuggestion(file, content) {
189
171
  if (content.includes('AuthenticatedRequest') || content.includes('req.userToken')) {
190
172
  return `User context available β€” use adminDB(req) or userDB(req) instead of createClient()`
191
173
  }
192
- return `Use one of: adminDB(req), userDB(req), publicDB(), superadminDB(req), or systemDB(context). See: stella_howto_get slug="tetra-architecture-guide"`
174
+ return `Use one of: adminDB(req), userDB(req), publicDB(), superadminDB(req), or systemDB(context)`
193
175
  }
@@ -154,7 +154,7 @@ export async function run(config, projectRoot) {
154
154
  severity: 'critical',
155
155
  message: `BLOCKED: Frontend code has direct Supabase ${pattern.desc}. Move to backend API endpoint.`,
156
156
  snippet: line.trim().substring(0, 120),
157
- fix: `Create a backend route+controller+service, use adminDB(req), and call via apiClient.get() from frontend. See: stella_howto_get slug="tetra-architecture-guide" for the complete migration pattern.`
157
+ fix: `Create a backend API endpoint and call it via fetch() or your API client instead.`
158
158
  })
159
159
  results.summary.critical++
160
160
  results.summary.total++
@@ -34,13 +34,10 @@ export async function run(config, projectRoot) {
34
34
  summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 }
35
35
  }
36
36
 
37
- // Get all source files (always exclude node_modules, even nested ones)
37
+ // Get all source files
38
38
  const files = await glob('**/*.{ts,tsx,js,jsx,json}', {
39
39
  cwd: projectRoot,
40
- ignore: [
41
- '**/node_modules/**',
42
- ...config.ignore
43
- ]
40
+ ignore: config.ignore
44
41
  })
45
42
 
46
43
  for (const file of files) {
@@ -63,9 +63,6 @@ const ALLOWED_FILE_PATTERNS = [
63
63
  /BaseCronService/,
64
64
  // Internal service-to-service routes (API key auth, no user JWT)
65
65
  /internalRoutes/,
66
- // Billing routers β€” hybrid files with both authenticated admin routes and unauthenticated webhook handlers
67
- // systemDB is needed for Tetra BillingService config callbacks (getSystemDB, getWebhookDB)
68
- /billingRouter/,
69
66
  ]
70
67
 
71
68
  /**
@@ -83,14 +80,11 @@ const FORBIDDEN_FILE_PATTERNS = [
83
80
  ]
84
81
 
85
82
  /**
86
- * Default maximum allowed whitelist size β€” override via .tetra-quality.json:
87
- * { "supabase": { "maxWhitelistEntries": 50 } }
83
+ * Maximum allowed whitelist size β€” anything above indicates architectural problems
88
84
  */
89
- const DEFAULT_MAX_WHITELIST_SIZE = 35
85
+ const MAX_WHITELIST_SIZE = 35
90
86
 
91
87
  export async function run(config, projectRoot) {
92
- // Canonical: security.systemDbMaxEntries
93
- const MAX_WHITELIST_SIZE = config?.security?.systemDbMaxEntries || config?.systemDB?.maxWhitelistEntries || DEFAULT_MAX_WHITELIST_SIZE
94
88
  const results = {
95
89
  passed: true,
96
90
  findings: [],
@@ -112,118 +106,39 @@ export async function run(config, projectRoot) {
112
106
  }
113
107
  }
114
108
 
115
- // Also check if project has tetra-core as npm package (no local systemDb.ts)
116
- let hasTetraCore = false
117
- for (const pkgPath of [`${projectRoot}/package.json`, `${projectRoot}/backend/package.json`]) {
118
- if (existsSync(pkgPath)) {
119
- try {
120
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
121
- const deps = { ...pkg.dependencies, ...pkg.devDependencies }
122
- if (deps['@soulbatical/tetra-core']) { hasTetraCore = true; break }
123
- } catch { /* skip */ }
124
- }
125
- }
126
-
127
- if (!systemDbPath && !hasTetraCore) {
109
+ if (!systemDbPath) {
128
110
  results.skipped = true
129
- results.skipReason = 'No systemDb.ts found and no @soulbatical/tetra-core dependency'
111
+ results.skipReason = 'No systemDb.ts found'
130
112
  return results
131
113
  }
132
114
 
133
- let whitelist = new Set()
134
-
135
- // Only check whitelist size if there's a local systemDb.ts
136
- if (systemDbPath) {
137
- const systemDbContent = readFileSync(systemDbPath, 'utf-8')
138
- const whitelistMatch = systemDbContent.match(/new Set\s*(?:<[^>]+>)?\s*\(\s*\[([\s\S]*?)\]\s*\)/m)
139
-
140
- if (whitelistMatch) {
141
- const rawLines = whitelistMatch[1].split('\n')
142
- let groupComment = null
143
-
144
- for (let i = 0; i < rawLines.length; i++) {
145
- const line = rawLines[i].trim()
146
-
147
- // Track group comments β€” a comment line that is NOT inline with an entry
148
- // Group comments apply to ALL entries below them until the next group comment
149
- if (/^\s*\/\//.test(line) && !line.match(/['"][^'"]+['"]/)) {
150
- const commentText = line.replace(/^\s*\/\/\s*/, '').trim()
151
- if (commentText.length > 0) {
152
- groupComment = commentText
153
- }
154
- continue
155
- }
156
-
157
- // Skip empty lines (don't reset group comment)
158
- if (!line || line === ',') continue
159
-
160
- const entryMatch = line.match(/['"]([^'"]+)['"]/)
161
- if (!entryMatch) continue
162
-
163
- const entry = entryMatch[1]
164
- whitelist.add(entry)
165
-
166
- // Check for inline comment: 'entry', // reason
167
- const inlineCommentMatch = line.match(/['"][^'"]+['"]\s*,?\s*\/\/\s*(.+)/)
168
- const inlineReason = inlineCommentMatch ? inlineCommentMatch[1].trim() : null
169
-
170
- // Entry is justified if it has an inline comment OR falls under a group comment
171
- const hasJustification = inlineReason || groupComment
115
+ const systemDbContent = readFileSync(systemDbPath, 'utf-8')
116
+ const whitelistMatch = systemDbContent.match(/new Set\s*(?:<[^>]+>)?\s*\(\s*\[([\s\S]*?)\]\s*\)/m)
172
117
 
173
- if (!hasJustification) {
174
- // Find line number in original file
175
- const entryLineInFile = systemDbContent.substring(0, systemDbContent.indexOf(entry)).split('\n').length
176
-
177
- results.findings.push({
178
- file: systemDbPath.replace(projectRoot + '/', ''),
179
- line: entryLineInFile,
180
- type: 'whitelist-no-justification',
181
- severity: 'high',
182
- message: `systemDB whitelist entry '${entry}' has NO comment explaining WHY it needs service role key access. Add a group comment above or an inline comment.`,
183
- fix: `Add a comment explaining why '${entry}' cannot use adminDB/userDB. Example:\n // OAuth callback β€” browser redirect, no JWT in header\n '${entry}',`
184
- })
185
- results.summary.high++
186
- results.summary.total++
187
- results.passed = false
188
- }
189
- }
190
- }
191
-
192
- if (whitelist.size > MAX_WHITELIST_SIZE) {
193
- results.passed = false
194
- results.findings.push({
195
- file: systemDbPath.replace(projectRoot + '/', ''),
196
- line: 1,
197
- type: 'whitelist-too-large',
198
- severity: 'critical',
199
- message: `systemDB whitelist has ${whitelist.size} entries (max: ${MAX_WHITELIST_SIZE}). This indicates systemDB is being used where adminDB/userDB should be used instead.`,
200
- fix: `Refactor: services should receive a supabase client via dependency injection, not create their own via systemDB(). Only cron jobs, webhooks, and OAuth callbacks should use systemDB.`
118
+ let whitelist = new Set()
119
+ if (whitelistMatch) {
120
+ const entries = whitelistMatch[1]
121
+ .split('\n')
122
+ .map(line => {
123
+ const m = line.match(/['"]([^'"]+)['"]/)
124
+ return m ? m[1] : null
201
125
  })
202
- results.summary.critical++
203
- results.summary.total++
204
- }
126
+ .filter(Boolean)
127
+ whitelist = new Set(entries)
205
128
  }
206
129
 
207
- // Also check .tetra-quality.json for documented whitelist with justifications
208
- const configWhitelist = config?.supabase?.systemDbWhitelist || {}
209
- if (typeof configWhitelist === 'object' && !Array.isArray(configWhitelist)) {
210
- // Format: { "context-name": "reason why systemDB is needed" }
211
- for (const [context, reason] of Object.entries(configWhitelist)) {
212
- whitelist.add(context)
213
- if (!reason || typeof reason !== 'string' || reason.trim().length < 5) {
214
- results.findings.push({
215
- file: '.tetra-quality.json',
216
- line: 1,
217
- type: 'config-whitelist-no-justification',
218
- severity: 'high',
219
- message: `systemDbWhitelist entry '${context}' has empty or insufficient justification: "${reason || ''}". Explain WHY this context cannot use adminDB/userDB.`,
220
- fix: `In .tetra-quality.json, set: "systemDbWhitelist": { "${context}": "Reason: e.g. OAuth callback β€” no JWT available, browser redirect flow" }`
221
- })
222
- results.summary.high++
223
- results.summary.total++
224
- results.passed = false
225
- }
226
- }
130
+ if (whitelist.size > MAX_WHITELIST_SIZE) {
131
+ results.passed = false
132
+ results.findings.push({
133
+ file: systemDbPath.replace(projectRoot + '/', ''),
134
+ line: 1,
135
+ type: 'whitelist-too-large',
136
+ severity: 'critical',
137
+ message: `systemDB whitelist has ${whitelist.size} entries (max: ${MAX_WHITELIST_SIZE}). This indicates systemDB is being used where SupabaseUserClient should be used instead. Refactor services to accept a supabase client parameter instead of calling systemDB() directly.`,
138
+ fix: `Refactor: services should receive a supabase client via dependency injection, not create their own via systemDB(). Only cron jobs, webhooks, and OAuth callbacks should use systemDB.`
139
+ })
140
+ results.summary.critical++
141
+ results.summary.total++
227
142
  }
228
143
 
229
144
  // ─── Check 2: systemDB in forbidden locations ───────────────
@@ -231,10 +146,6 @@ export async function run(config, projectRoot) {
231
146
  const files = await glob('**/*.ts', {
232
147
  cwd: projectRoot,
233
148
  ignore: [
234
- '**/node_modules/**',
235
- '**/.next/**',
236
- '**/dist/**',
237
- '**/build/**',
238
149
  ...config.ignore,
239
150
  '**/systemDb.ts',
240
151
  '**/superadminDb.ts',
package/lib/config.js CHANGED
@@ -42,13 +42,7 @@ export const DEFAULT_CONFIG = {
42
42
  // Code patterns
43
43
  checkSqlInjection: true,
44
44
  checkEvalUsage: true,
45
- checkCommandInjection: true,
46
-
47
- // Whitelists β€” project-specific overrides in .tetra-quality.json
48
- directSupabaseClientWhitelist: [], // Files allowed to import createClient directly
49
- mixedDbWhitelist: [], // Controllers allowed to mix DB helper types
50
- routeConfigIgnore: [], // Tables to skip in route↔config alignment check
51
- systemDbMaxEntries: 35 // Max systemDB whitelist entries before warning
45
+ checkCommandInjection: true
52
46
  },
53
47
 
54
48
  // Stability checks