@soulbatical/tetra-dev-toolkit 1.10.2 → 1.12.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.
|
@@ -24,14 +24,23 @@ 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
|
|
28
|
-
const
|
|
29
|
-
|
|
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`)
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
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) {
|
|
33
42
|
results.skipped = true
|
|
34
|
-
results.skipReason = 'Project does not use systemDB/userDB pattern'
|
|
43
|
+
results.skipReason = 'Project does not use systemDB/userDB pattern (no local systemDb.ts and no @soulbatical/tetra-core)'
|
|
35
44
|
return results
|
|
36
45
|
}
|
|
37
46
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mixed Database Usage Detection — HARD BLOCK
|
|
3
|
+
*
|
|
4
|
+
* Controllers MUST use only ONE type of database helper.
|
|
5
|
+
* The DB helper must match the controller naming convention:
|
|
6
|
+
*
|
|
7
|
+
* AdminXxxController → adminDB(req) only
|
|
8
|
+
* UserXxxController → userDB(req) only
|
|
9
|
+
* PublicXxxController → publicDB() only
|
|
10
|
+
* SystemXxxController → systemDB(context) only
|
|
11
|
+
* SuperadminXxxController → superadminDB(req) only
|
|
12
|
+
* WebhookXxxController → systemDB(context) only (webhooks have no user session)
|
|
13
|
+
*
|
|
14
|
+
* Violations:
|
|
15
|
+
* - CRITICAL: Controller uses systemDB when user context is available (should be adminDB/userDB)
|
|
16
|
+
* - HIGH: Controller mixes multiple DB helper types
|
|
17
|
+
* - MEDIUM: Controller DB helper doesn't match naming convention
|
|
18
|
+
*
|
|
19
|
+
* Reference: stella_howto_get slug="tetra-architecture-guide"
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readFileSync, existsSync } from 'fs'
|
|
23
|
+
import { join, basename, dirname } from 'path'
|
|
24
|
+
import { glob } from 'glob'
|
|
25
|
+
|
|
26
|
+
const DB_PATTERNS = {
|
|
27
|
+
systemDB: { level: 'SYSTEM', pattern: /systemDB\s*\(/g, desc: 'System-level (cron, webhooks)' },
|
|
28
|
+
adminDB: { level: 'ADMIN', pattern: /adminDB\s*\(/g, desc: 'Admin operations (org-scoped)' },
|
|
29
|
+
userDB: { level: 'USER', pattern: /userDB\s*\(/g, desc: 'User-specific operations' },
|
|
30
|
+
publicDB: { level: 'PUBLIC', pattern: /publicDB\s*\(/g, desc: 'Public/unauthenticated' },
|
|
31
|
+
superadminDB: { level: 'SUPERADMIN', pattern: /superadminDB\s*\(/g, desc: 'Cross-org superadmin' }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Determine expected DB level from controller filename
|
|
36
|
+
*/
|
|
37
|
+
function getExpectedLevel(fileName) {
|
|
38
|
+
const lower = fileName.toLowerCase()
|
|
39
|
+
// Order matters — check compound names first
|
|
40
|
+
if (lower.includes('system')) return 'SYSTEM'
|
|
41
|
+
if (lower.includes('superadmin')) return 'SUPERADMIN'
|
|
42
|
+
if (lower.includes('webhook')) return 'SYSTEM' // webhooks have no user session
|
|
43
|
+
if (lower.includes('cron')) return 'SYSTEM'
|
|
44
|
+
if (lower.includes('admin')) return 'ADMIN'
|
|
45
|
+
if (lower.includes('user')) return 'USER'
|
|
46
|
+
if (lower.includes('public')) return 'PUBLIC'
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if file has user authentication context available
|
|
52
|
+
*/
|
|
53
|
+
function hasUserContext(content) {
|
|
54
|
+
return content.includes('AuthenticatedRequest') ||
|
|
55
|
+
content.includes('req.userToken') ||
|
|
56
|
+
content.includes('req.user') ||
|
|
57
|
+
content.includes('authenticateToken')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function check(projectRoot, config = {}) {
|
|
61
|
+
const results = {
|
|
62
|
+
name: 'Mixed DB Usage',
|
|
63
|
+
slug: 'mixed-db-usage',
|
|
64
|
+
passed: true,
|
|
65
|
+
skipped: false,
|
|
66
|
+
findings: [],
|
|
67
|
+
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
|
|
68
|
+
details: { controllersChecked: 0, violations: 0 }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const files = await glob('**/*[Cc]ontroller*.ts', {
|
|
72
|
+
cwd: projectRoot,
|
|
73
|
+
ignore: [
|
|
74
|
+
'**/node_modules/**',
|
|
75
|
+
'**/.next/**',
|
|
76
|
+
'**/dist/**',
|
|
77
|
+
'**/build/**',
|
|
78
|
+
...(config.ignore || []),
|
|
79
|
+
'**/*.test.ts',
|
|
80
|
+
'**/*.spec.ts',
|
|
81
|
+
'**/*.d.ts'
|
|
82
|
+
]
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if (files.length === 0) {
|
|
86
|
+
results.skipped = true
|
|
87
|
+
results.skipReason = 'No controller files found'
|
|
88
|
+
return results
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
const fullPath = join(projectRoot, file)
|
|
93
|
+
let content
|
|
94
|
+
try { content = readFileSync(fullPath, 'utf-8') } catch { continue }
|
|
95
|
+
|
|
96
|
+
results.details.controllersChecked++
|
|
97
|
+
const fileName = basename(file)
|
|
98
|
+
|
|
99
|
+
// Detect all DB usages in this file
|
|
100
|
+
const dbUsages = new Map() // level -> [{type, line, code}]
|
|
101
|
+
const lines = content.split('\n')
|
|
102
|
+
|
|
103
|
+
for (const [dbType, cfg] of Object.entries(DB_PATTERNS)) {
|
|
104
|
+
lines.forEach((line, idx) => {
|
|
105
|
+
// Reset regex lastIndex
|
|
106
|
+
cfg.pattern.lastIndex = 0
|
|
107
|
+
if (cfg.pattern.test(line)) {
|
|
108
|
+
const level = cfg.level
|
|
109
|
+
if (!dbUsages.has(level)) dbUsages.set(level, [])
|
|
110
|
+
dbUsages.get(level).push({
|
|
111
|
+
type: dbType,
|
|
112
|
+
line: idx + 1,
|
|
113
|
+
code: line.trim().substring(0, 120)
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (dbUsages.size === 0) continue
|
|
120
|
+
|
|
121
|
+
const expectedLevel = getExpectedLevel(fileName)
|
|
122
|
+
const usedLevels = [...dbUsages.keys()]
|
|
123
|
+
const userContextAvailable = hasUserContext(content)
|
|
124
|
+
|
|
125
|
+
// CRITICAL: systemDB used when user context is available and it's not a system controller
|
|
126
|
+
if (dbUsages.has('SYSTEM') && userContextAvailable && expectedLevel !== 'SYSTEM') {
|
|
127
|
+
results.passed = false
|
|
128
|
+
const locs = dbUsages.get('SYSTEM')
|
|
129
|
+
for (const loc of locs) {
|
|
130
|
+
results.findings.push({
|
|
131
|
+
file,
|
|
132
|
+
line: loc.line,
|
|
133
|
+
type: 'systemDB with user context',
|
|
134
|
+
severity: 'critical',
|
|
135
|
+
message: `${fileName} uses systemDB() but has user authentication context → MUST use ${expectedLevel === 'ADMIN' ? 'adminDB(req)' : expectedLevel === 'USER' ? 'userDB(req)' : 'adminDB(req) or userDB(req)'}`,
|
|
136
|
+
snippet: loc.code,
|
|
137
|
+
fix: `Replace systemDB('context') with ${expectedLevel === 'ADMIN' ? 'adminDB(req)' : 'userDB(req)'}. systemDB bypasses RLS — user operations MUST go through RLS. See: stella_howto_get slug="tetra-architecture-guide"`
|
|
138
|
+
})
|
|
139
|
+
results.summary.critical++
|
|
140
|
+
results.summary.total++
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// HIGH: Multiple DB helper types in one controller
|
|
145
|
+
if (usedLevels.length > 1) {
|
|
146
|
+
results.passed = false
|
|
147
|
+
results.findings.push({
|
|
148
|
+
file,
|
|
149
|
+
line: 1,
|
|
150
|
+
type: 'mixed db usage',
|
|
151
|
+
severity: 'high',
|
|
152
|
+
message: `${fileName} mixes ${usedLevels.join(' + ')} database helpers. Controllers MUST use exactly ONE DB helper type.`,
|
|
153
|
+
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.`
|
|
154
|
+
})
|
|
155
|
+
results.summary.high++
|
|
156
|
+
results.summary.total++
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// MEDIUM: DB helper doesn't match naming convention
|
|
160
|
+
if (expectedLevel && usedLevels.length === 1 && !usedLevels.includes(expectedLevel)) {
|
|
161
|
+
const actualLevel = usedLevels[0]
|
|
162
|
+
// Don't double-report if already caught as critical
|
|
163
|
+
if (!(actualLevel === 'SYSTEM' && userContextAvailable)) {
|
|
164
|
+
results.findings.push({
|
|
165
|
+
file,
|
|
166
|
+
line: 1,
|
|
167
|
+
type: 'naming mismatch',
|
|
168
|
+
severity: 'medium',
|
|
169
|
+
message: `${fileName} implies ${expectedLevel} but uses ${actualLevel}. Either rename the controller or change the DB helper.`,
|
|
170
|
+
fix: `Rename to match DB level, or change DB helper to match controller naming convention.`
|
|
171
|
+
})
|
|
172
|
+
results.summary.medium++
|
|
173
|
+
results.summary.total++
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
results.details.violations = results.findings.length
|
|
179
|
+
return results
|
|
180
|
+
}
|
|
@@ -106,39 +106,55 @@ export async function run(config, projectRoot) {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
if (
|
|
109
|
+
// Also check if project has tetra-core as npm package (no local systemDb.ts)
|
|
110
|
+
let hasTetraCore = false
|
|
111
|
+
for (const pkgPath of [`${projectRoot}/package.json`, `${projectRoot}/backend/package.json`]) {
|
|
112
|
+
if (existsSync(pkgPath)) {
|
|
113
|
+
try {
|
|
114
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
115
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
116
|
+
if (deps['@soulbatical/tetra-core']) { hasTetraCore = true; break }
|
|
117
|
+
} catch { /* skip */ }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!systemDbPath && !hasTetraCore) {
|
|
110
122
|
results.skipped = true
|
|
111
|
-
results.skipReason = 'No systemDb.ts found'
|
|
123
|
+
results.skipReason = 'No systemDb.ts found and no @soulbatical/tetra-core dependency'
|
|
112
124
|
return results
|
|
113
125
|
}
|
|
114
126
|
|
|
115
|
-
const systemDbContent = readFileSync(systemDbPath, 'utf-8')
|
|
116
|
-
const whitelistMatch = systemDbContent.match(/new Set\s*(?:<[^>]+>)?\s*\(\s*\[([\s\S]*?)\]\s*\)/m)
|
|
117
|
-
|
|
118
127
|
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
|
|
125
|
-
})
|
|
126
|
-
.filter(Boolean)
|
|
127
|
-
whitelist = new Set(entries)
|
|
128
|
-
}
|
|
129
128
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
129
|
+
// Only check whitelist size if there's a local systemDb.ts
|
|
130
|
+
if (systemDbPath) {
|
|
131
|
+
const systemDbContent = readFileSync(systemDbPath, 'utf-8')
|
|
132
|
+
const whitelistMatch = systemDbContent.match(/new Set\s*(?:<[^>]+>)?\s*\(\s*\[([\s\S]*?)\]\s*\)/m)
|
|
133
|
+
|
|
134
|
+
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)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (whitelist.size > MAX_WHITELIST_SIZE) {
|
|
146
|
+
results.passed = false
|
|
147
|
+
results.findings.push({
|
|
148
|
+
file: systemDbPath.replace(projectRoot + '/', ''),
|
|
149
|
+
line: 1,
|
|
150
|
+
type: 'whitelist-too-large',
|
|
151
|
+
severity: 'critical',
|
|
152
|
+
message: `systemDB whitelist has ${whitelist.size} entries (max: ${MAX_WHITELIST_SIZE}). This indicates systemDB is being used where adminDB/userDB should be used instead.`,
|
|
153
|
+
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.`
|
|
154
|
+
})
|
|
155
|
+
results.summary.critical++
|
|
156
|
+
results.summary.total++
|
|
157
|
+
}
|
|
142
158
|
}
|
|
143
159
|
|
|
144
160
|
// ─── Check 2: systemDB in forbidden locations ───────────────
|
|
@@ -146,6 +162,10 @@ export async function run(config, projectRoot) {
|
|
|
146
162
|
const files = await glob('**/*.ts', {
|
|
147
163
|
cwd: projectRoot,
|
|
148
164
|
ignore: [
|
|
165
|
+
'**/node_modules/**',
|
|
166
|
+
'**/.next/**',
|
|
167
|
+
'**/dist/**',
|
|
168
|
+
'**/build/**',
|
|
149
169
|
...config.ignore,
|
|
150
170
|
'**/systemDb.ts',
|
|
151
171
|
'**/superadminDb.ts',
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tetra Core Compliance — HARD BLOCK
|
|
3
|
+
*
|
|
4
|
+
* If a project has @soulbatical/tetra-core as a dependency, it MUST use:
|
|
5
|
+
* 1. configureAuth() — sets up authentication middleware
|
|
6
|
+
* 2. authenticateToken — middleware on protected routes
|
|
7
|
+
* 3. adminDB/userDB/systemDB — db helpers (NOT raw createClient)
|
|
8
|
+
*
|
|
9
|
+
* If ANY of these are missing, the project is NOT secure.
|
|
10
|
+
* There is no "skip" — if you have tetra-core, you use it fully or not at all.
|
|
11
|
+
*
|
|
12
|
+
* This check also detects projects that SHOULD have tetra-core but don't:
|
|
13
|
+
* - Has a backend/ dir with Express + Supabase → MUST have tetra-core
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync, existsSync } from 'fs'
|
|
17
|
+
import { join } from 'path'
|
|
18
|
+
import { glob } from 'glob'
|
|
19
|
+
|
|
20
|
+
export const meta = {
|
|
21
|
+
id: 'tetra-core-compliance',
|
|
22
|
+
name: 'Tetra Core Compliance',
|
|
23
|
+
category: 'security',
|
|
24
|
+
severity: 'critical',
|
|
25
|
+
description: 'Ensures projects using @soulbatical/tetra-core implement ALL required security patterns — auth, middleware, db helpers'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function run(config, projectRoot) {
|
|
29
|
+
const results = {
|
|
30
|
+
passed: true,
|
|
31
|
+
findings: [],
|
|
32
|
+
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Step 1: Does this project have tetra-core? ────────────────
|
|
36
|
+
|
|
37
|
+
const packageJsonPaths = [
|
|
38
|
+
join(projectRoot, 'package.json'),
|
|
39
|
+
join(projectRoot, 'backend', 'package.json')
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
let hasTetraCore = false
|
|
43
|
+
let tetraCoreLocation = null
|
|
44
|
+
|
|
45
|
+
for (const pkgPath of packageJsonPaths) {
|
|
46
|
+
if (!existsSync(pkgPath)) continue
|
|
47
|
+
try {
|
|
48
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
49
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
50
|
+
if (deps['@soulbatical/tetra-core']) {
|
|
51
|
+
hasTetraCore = true
|
|
52
|
+
tetraCoreLocation = pkgPath.replace(projectRoot + '/', '')
|
|
53
|
+
break
|
|
54
|
+
}
|
|
55
|
+
} catch { /* skip */ }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Step 2: Check if project SHOULD have tetra-core ───────────
|
|
59
|
+
|
|
60
|
+
if (!hasTetraCore) {
|
|
61
|
+
// Check if this is an Express + Supabase backend that should have it
|
|
62
|
+
const backendDirs = ['backend/src', 'src']
|
|
63
|
+
let hasExpress = false
|
|
64
|
+
let hasSupabase = false
|
|
65
|
+
|
|
66
|
+
for (const dir of backendDirs) {
|
|
67
|
+
const pkgPath = dir === 'src' ? join(projectRoot, 'package.json') : join(projectRoot, 'backend', 'package.json')
|
|
68
|
+
if (!existsSync(pkgPath)) continue
|
|
69
|
+
try {
|
|
70
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
71
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
72
|
+
if (deps['express']) hasExpress = true
|
|
73
|
+
if (deps['@supabase/supabase-js']) hasSupabase = true
|
|
74
|
+
} catch { /* skip */ }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (hasExpress && hasSupabase) {
|
|
78
|
+
results.passed = false
|
|
79
|
+
results.findings.push({
|
|
80
|
+
type: 'missing-tetra-core',
|
|
81
|
+
severity: 'critical',
|
|
82
|
+
message: 'Project has Express + Supabase but does NOT have @soulbatical/tetra-core. This means NO standard auth middleware, NO db helpers, NO RLS enforcement. Install tetra-core and follow the architecture guide.',
|
|
83
|
+
fix: 'npm install @soulbatical/tetra-core && see stella_howto_get slug="tetra-architecture-guide"'
|
|
84
|
+
})
|
|
85
|
+
results.summary.critical++
|
|
86
|
+
results.summary.total++
|
|
87
|
+
} else {
|
|
88
|
+
// Not a backend project, skip
|
|
89
|
+
results.skipped = true
|
|
90
|
+
results.skipReason = 'Not a Tetra backend project (no Express + Supabase)'
|
|
91
|
+
return results
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return results
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Step 3: Has tetra-core → check FULL compliance ────────────
|
|
98
|
+
|
|
99
|
+
const backendSrc = existsSync(join(projectRoot, 'backend', 'src')) ? 'backend/src' : 'src'
|
|
100
|
+
|
|
101
|
+
// Check 3a: configureAuth() is called somewhere
|
|
102
|
+
const tsFiles = await glob(`${backendSrc}/**/*.ts`, {
|
|
103
|
+
cwd: projectRoot,
|
|
104
|
+
ignore: ['**/node_modules/**', '**/*.test.ts', '**/*.spec.ts', '**/*.d.ts']
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
let hasConfigureAuth = false
|
|
108
|
+
let hasAuthenticateToken = false
|
|
109
|
+
let hasDbHelpers = false
|
|
110
|
+
let dbHelperImports = { adminDB: false, userDB: false, systemDB: false, publicDB: false, superadminDB: false }
|
|
111
|
+
let hasRawCreateClient = false
|
|
112
|
+
let rawCreateClientFiles = []
|
|
113
|
+
|
|
114
|
+
for (const file of tsFiles) {
|
|
115
|
+
try {
|
|
116
|
+
const content = readFileSync(join(projectRoot, file), 'utf-8')
|
|
117
|
+
|
|
118
|
+
if (content.includes('configureAuth')) hasConfigureAuth = true
|
|
119
|
+
if (content.includes('authenticateToken')) hasAuthenticateToken = true
|
|
120
|
+
|
|
121
|
+
// Check for db helper usage
|
|
122
|
+
if (/(?:import|from).*(?:adminDB|userDB|systemDB|publicDB|superadminDB)/.test(content)) {
|
|
123
|
+
hasDbHelpers = true
|
|
124
|
+
if (content.includes('adminDB')) dbHelperImports.adminDB = true
|
|
125
|
+
if (content.includes('userDB')) dbHelperImports.userDB = true
|
|
126
|
+
if (content.includes('systemDB')) dbHelperImports.systemDB = true
|
|
127
|
+
if (content.includes('publicDB')) dbHelperImports.publicDB = true
|
|
128
|
+
if (content.includes('superadminDB')) dbHelperImports.superadminDB = true
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check for raw createClient usage (should be caught by direct-supabase-client check too)
|
|
132
|
+
if (/import\s*\{[^}]*createClient[^}]*\}\s*from\s*['"]@supabase\/supabase-js['"]/.test(content)) {
|
|
133
|
+
if (!/import\s+type/.test(content)) {
|
|
134
|
+
// Not in core wrapper files
|
|
135
|
+
if (!/core\//.test(file) && !/SupabaseUserClient/.test(file) && !/scripts\//.test(file)) {
|
|
136
|
+
hasRawCreateClient = true
|
|
137
|
+
rawCreateClientFiles.push(file)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch { /* skip */ }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ─── Report findings ──────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
if (!hasConfigureAuth) {
|
|
147
|
+
results.passed = false
|
|
148
|
+
results.findings.push({
|
|
149
|
+
type: 'missing-configureAuth',
|
|
150
|
+
severity: 'critical',
|
|
151
|
+
message: 'Project has @soulbatical/tetra-core but NEVER calls configureAuth(). This means the auth middleware is not initialized — all routes are potentially unprotected.',
|
|
152
|
+
fix: 'Create an auth-config.ts that calls configureAuth({ loadUserContext, roleHierarchy }). See sparkbuddy-live/backend/src/auth-config.ts for reference.'
|
|
153
|
+
})
|
|
154
|
+
results.summary.critical++
|
|
155
|
+
results.summary.total++
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!hasAuthenticateToken) {
|
|
159
|
+
results.passed = false
|
|
160
|
+
results.findings.push({
|
|
161
|
+
type: 'missing-authenticateToken',
|
|
162
|
+
severity: 'critical',
|
|
163
|
+
message: 'Project has @soulbatical/tetra-core but NO route uses authenticateToken middleware. All API endpoints are unprotected.',
|
|
164
|
+
fix: 'Add authenticateToken middleware to all protected routes: router.use(authenticateToken, requireOrganizationAdmin)'
|
|
165
|
+
})
|
|
166
|
+
results.summary.critical++
|
|
167
|
+
results.summary.total++
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!hasDbHelpers) {
|
|
171
|
+
results.passed = false
|
|
172
|
+
results.findings.push({
|
|
173
|
+
type: 'missing-db-helpers',
|
|
174
|
+
severity: 'critical',
|
|
175
|
+
message: 'Project has @soulbatical/tetra-core but does NOT use any db helpers (adminDB, userDB, systemDB, publicDB). Database access has no RLS enforcement.',
|
|
176
|
+
fix: 'Use adminDB(req) for admin routes, userDB(req) for user routes, systemDB(context) for system operations. See stella_howto_get slug="tetra-architecture-guide"'
|
|
177
|
+
})
|
|
178
|
+
results.summary.critical++
|
|
179
|
+
results.summary.total++
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Info: which helpers are used
|
|
183
|
+
const usedHelpers = Object.entries(dbHelperImports).filter(([_, used]) => used).map(([name]) => name)
|
|
184
|
+
const unusedHelpers = Object.entries(dbHelperImports).filter(([_, used]) => !used).map(([name]) => name)
|
|
185
|
+
|
|
186
|
+
results.info = {
|
|
187
|
+
tetraCoreLocation,
|
|
188
|
+
configureAuth: hasConfigureAuth,
|
|
189
|
+
authenticateToken: hasAuthenticateToken,
|
|
190
|
+
dbHelpers: {
|
|
191
|
+
used: usedHelpers,
|
|
192
|
+
unused: unusedHelpers
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return results
|
|
197
|
+
}
|
package/lib/runner.js
CHANGED
|
@@ -12,6 +12,8 @@ import * as serviceKeyExposure from './checks/security/service-key-exposure.js'
|
|
|
12
12
|
import * as deprecatedSupabaseAdmin from './checks/security/deprecated-supabase-admin.js'
|
|
13
13
|
import * as directSupabaseClient from './checks/security/direct-supabase-client.js'
|
|
14
14
|
import * as frontendSupabaseQueries from './checks/security/frontend-supabase-queries.js'
|
|
15
|
+
import * as tetraCoreCompliance from './checks/security/tetra-core-compliance.js'
|
|
16
|
+
import * as mixedDbUsage from './checks/security/mixed-db-usage.js'
|
|
15
17
|
import * as systemdbWhitelist from './checks/security/systemdb-whitelist.js'
|
|
16
18
|
import * as huskyHooks from './checks/stability/husky-hooks.js'
|
|
17
19
|
import * as ciPipeline from './checks/stability/ci-pipeline.js'
|
|
@@ -35,6 +37,8 @@ const ALL_CHECKS = {
|
|
|
35
37
|
deprecatedSupabaseAdmin,
|
|
36
38
|
directSupabaseClient,
|
|
37
39
|
frontendSupabaseQueries,
|
|
40
|
+
tetraCoreCompliance,
|
|
41
|
+
mixedDbUsage,
|
|
38
42
|
systemdbWhitelist,
|
|
39
43
|
gitignoreValidation
|
|
40
44
|
],
|