@soulbatical/tetra-dev-toolkit 1.11.0 → 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.
@@ -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
+ }
package/lib/runner.js CHANGED
@@ -13,6 +13,7 @@ import * as deprecatedSupabaseAdmin from './checks/security/deprecated-supabase-
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
15
  import * as tetraCoreCompliance from './checks/security/tetra-core-compliance.js'
16
+ import * as mixedDbUsage from './checks/security/mixed-db-usage.js'
16
17
  import * as systemdbWhitelist from './checks/security/systemdb-whitelist.js'
17
18
  import * as huskyHooks from './checks/stability/husky-hooks.js'
18
19
  import * as ciPipeline from './checks/stability/ci-pipeline.js'
@@ -37,6 +38,7 @@ const ALL_CHECKS = {
37
38
  directSupabaseClient,
38
39
  frontendSupabaseQueries,
39
40
  tetraCoreCompliance,
41
+ mixedDbUsage,
40
42
  systemdbWhitelist,
41
43
  gitignoreValidation
42
44
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulbatical/tetra-dev-toolkit",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "publishConfig": {
5
5
  "access": "restricted"
6
6
  },