@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.
- package/README.md +235 -238
- package/bin/tetra-setup.js +2 -172
- package/lib/checks/health/index.js +0 -1
- package/lib/checks/health/scanner.js +1 -3
- package/lib/checks/health/types.js +1 -1
- package/lib/checks/hygiene/stella-compliance.js +2 -2
- package/lib/checks/security/deprecated-supabase-admin.js +6 -15
- package/lib/checks/security/direct-supabase-client.js +4 -22
- package/lib/checks/security/frontend-supabase-queries.js +1 -1
- package/lib/checks/security/hardcoded-secrets.js +2 -5
- package/lib/checks/security/systemdb-whitelist.js +27 -116
- package/lib/config.js +1 -7
- package/lib/runner.js +7 -120
- package/package.json +2 -7
- package/bin/tetra-check-peers.js +0 -359
- package/bin/tetra-db-push.js +0 -91
- package/bin/tetra-migration-lint.js +0 -317
- package/bin/tetra-security-gate.js +0 -293
- package/bin/tetra-smoke.js +0 -532
- package/lib/checks/health/smoke-readiness.js +0 -150
- package/lib/checks/security/config-rls-alignment.js +0 -637
- package/lib/checks/security/mixed-db-usage.js +0 -204
- package/lib/checks/security/rls-live-audit.js +0 -255
- package/lib/checks/security/route-config-alignment.js +0 -342
- package/lib/checks/security/rpc-security-mode.js +0 -175
- package/lib/checks/security/tetra-core-compliance.js +0 -197
package/bin/tetra-setup.js
CHANGED
|
@@ -43,7 +43,7 @@ program
|
|
|
43
43
|
console.log('')
|
|
44
44
|
|
|
45
45
|
const components = component === 'all' || !component
|
|
46
|
-
? ['hooks', 'ci', 'config'
|
|
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,
|
|
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'
|
|
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'
|
|
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'
|
|
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
|
|
28
|
-
const
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
|
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
|
|
125
|
-
const isAllowed =
|
|
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 =
|
|
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)
|
|
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
|
|
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
|
|
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
|
-
*
|
|
87
|
-
* { "supabase": { "maxWhitelistEntries": 50 } }
|
|
83
|
+
* Maximum allowed whitelist size β anything above indicates architectural problems
|
|
88
84
|
*/
|
|
89
|
-
const
|
|
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
|
-
|
|
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
|
|
111
|
+
results.skipReason = 'No systemDb.ts found'
|
|
130
112
|
return results
|
|
131
113
|
}
|
|
132
114
|
|
|
133
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
whitelist = new Set(entries)
|
|
205
128
|
}
|
|
206
129
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
whitelist
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|