@soulbatical/tetra-dev-toolkit 1.16.3 → 1.17.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.
@@ -424,8 +424,12 @@ export async function run(config, projectRoot) {
424
424
  }
425
425
  }
426
426
 
427
- // CHECK 4: RPC functions must be SECURITY INVOKER
427
+ // CHECK 4: RPC functions must be SECURITY INVOKER (unless whitelisted)
428
+ const definerWhitelist = config?.supabase?.securityDefinerWhitelist || []
428
429
  for (const rpcName of cfg.rpcFunctions) {
430
+ // Skip if explicitly whitelisted in config
431
+ if (definerWhitelist.includes(rpcName)) continue
432
+
429
433
  const rpcInfo = tableRls.rpcFunctions.get(rpcName)
430
434
  if (rpcInfo && rpcInfo.securityMode === 'DEFINER') {
431
435
  results.passed = false
@@ -435,7 +439,7 @@ export async function run(config, projectRoot) {
435
439
  type: 'rpc-security-definer',
436
440
  severity: 'critical',
437
441
  message: `RPC function "${rpcName}" for table "${tableName}" uses SECURITY DEFINER — this BYPASSES RLS completely. Any authenticated user can see ALL data.`,
438
- fix: `Change to SECURITY INVOKER or remove the SECURITY DEFINER clause.`
442
+ fix: `Change to SECURITY INVOKER or add "${rpcName}" to supabase.securityDefinerWhitelist in .tetra-quality.json.`
439
443
  })
440
444
  results.summary.critical++
441
445
  results.summary.total++
@@ -74,8 +74,8 @@ export async function run(config, projectRoot) {
74
74
  summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 }
75
75
  }
76
76
 
77
- // Build whitelist from hardcoded + config
78
- const configWhitelist = config?.supabase?.directSupabaseClientWhitelist || config?.directSupabaseClientWhitelist || []
77
+ // Build whitelist from hardcoded + config (canonical: security.directSupabaseClientWhitelist)
78
+ const configWhitelist = config?.security?.directSupabaseClientWhitelist || config?.directSupabaseClientWhitelist || []
79
79
  const extraPatterns = configWhitelist.map(p => new RegExp(p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
80
80
  const allAllowed = [...ALLOWED_FILES, ...extraPatterns]
81
81
 
@@ -77,7 +77,8 @@ export async function run(config, projectRoot) {
77
77
  }
78
78
 
79
79
  // Config-based ignore for controllers that legitimately mix DB helpers
80
- const mixedDbIgnore = config?.supabase?.mixedDbUsageIgnore || []
80
+ // Canonical: security.mixedDbWhitelist
81
+ const mixedDbIgnore = config?.security?.mixedDbWhitelist || config?.mixedDbWhitelist || []
81
82
 
82
83
  const files = await glob('**/*[Cc]ontroller*.ts', {
83
84
  cwd: projectRoot,
@@ -157,11 +158,12 @@ export async function run(config, projectRoot) {
157
158
 
158
159
  // HIGH: Multiple DB helper types in one controller
159
160
  if (usedLevels.length > 1) {
160
- // Allow: Superadmin controllers may legitimately use SUPERADMIN + ADMIN
161
+ // Allow: Superadmin controllers use superadminDB which internally calls systemDB
162
+ // So tetra sees SUPERADMIN + SYSTEM (or SUPERADMIN + ADMIN) — both are legitimate
161
163
  const isSuperadminMixingAdmin = expectedLevel === 'SUPERADMIN' &&
162
164
  usedLevels.length === 2 &&
163
165
  usedLevels.includes('SUPERADMIN') &&
164
- usedLevels.includes('ADMIN')
166
+ (usedLevels.includes('ADMIN') || usedLevels.includes('SYSTEM'))
165
167
 
166
168
  if (!isSuperadminMixingAdmin) {
167
169
  results.passed = false
@@ -102,6 +102,32 @@ function hasOrgAdminMiddleware(content) {
102
102
  return /requireOrganizationAdmin/.test(content)
103
103
  }
104
104
 
105
+ /**
106
+ * Check if admin routes are protected by a RouteManager group-level middleware.
107
+ * Many projects apply auth middleware at the route group level (e.g., all /api/admin/* routes)
108
+ * rather than in individual route files.
109
+ */
110
+ function hasRouteManagerGroupAuth(projectRoot) {
111
+ const candidates = [
112
+ join(projectRoot, 'backend/src/core/RouteManager.ts'),
113
+ join(projectRoot, 'src/core/RouteManager.ts'),
114
+ join(projectRoot, 'backend/src/routes/index.ts'),
115
+ join(projectRoot, 'src/routes/index.ts')
116
+ ]
117
+
118
+ for (const file of candidates) {
119
+ if (!existsSync(file)) continue
120
+ try {
121
+ const content = readFileSync(file, 'utf-8')
122
+ // Check for group middleware pattern: prefix '/api/admin' with authenticateToken
123
+ if (/\/api\/admin/.test(content) && /authenticateToken/.test(content)) {
124
+ return true
125
+ }
126
+ } catch { /* skip */ }
127
+ }
128
+ return false
129
+ }
130
+
105
131
  /**
106
132
  * Expected route filename for a given accessLevel
107
133
  */
@@ -123,8 +149,11 @@ export async function run(config, projectRoot) {
123
149
  details: { routesChecked: 0, violations: 0 }
124
150
  }
125
151
 
126
- // Config-based ignore for specific features/routes
127
- const routeIgnore = config?.supabase?.routeConfigAlignmentIgnore || []
152
+ // Canonical: security.routeConfigIgnore
153
+ const routeIgnore = config?.security?.routeConfigIgnore || config?.routeConfigAlignmentIgnore || []
154
+
155
+ // Detect if RouteManager applies group-level auth middleware to /api/admin/*
156
+ const routeManagerHasGroupAuth = hasRouteManagerGroupAuth(projectRoot)
128
157
 
129
158
  const featureConfigs = parseFeatureConfigs(projectRoot)
130
159
 
@@ -176,8 +205,8 @@ export async function run(config, projectRoot) {
176
205
  if (cfg.accessLevel === 'admin') {
177
206
  // Route name should be adminRoutes.ts
178
207
  if (routeName === 'adminRoutes.ts') {
179
- // CRITICAL: admin route MUST have authenticateToken
180
- if (!hasAuthMiddleware(content)) {
208
+ // CRITICAL: admin route MUST have authenticateToken (in file OR via RouteManager group)
209
+ if (!hasAuthMiddleware(content) && !routeManagerHasGroupAuth) {
181
210
  results.findings.push({
182
211
  file: relRouteFile,
183
212
  line: 1,
@@ -192,8 +221,8 @@ export async function run(config, projectRoot) {
192
221
  results.details.violations++
193
222
  }
194
223
 
195
- // CRITICAL: admin route MUST have requireOrganizationAdmin
196
- if (!hasOrgAdminMiddleware(content)) {
224
+ // CRITICAL: admin route MUST have requireOrganizationAdmin (in file OR via RouteManager group)
225
+ if (!hasOrgAdminMiddleware(content) && !routeManagerHasGroupAuth) {
197
226
  results.findings.push({
198
227
  file: relRouteFile,
199
228
  line: 1,
@@ -86,7 +86,8 @@ const FORBIDDEN_FILE_PATTERNS = [
86
86
  const DEFAULT_MAX_WHITELIST_SIZE = 35
87
87
 
88
88
  export async function run(config, projectRoot) {
89
- const MAX_WHITELIST_SIZE = config?.supabase?.maxWhitelistEntries || DEFAULT_MAX_WHITELIST_SIZE
89
+ // Canonical: security.systemDbMaxEntries
90
+ const MAX_WHITELIST_SIZE = config?.security?.systemDbMaxEntries || config?.systemDB?.maxWhitelistEntries || DEFAULT_MAX_WHITELIST_SIZE
90
91
  const results = {
91
92
  passed: true,
92
93
  findings: [],
package/lib/config.js CHANGED
@@ -42,7 +42,13 @@ export const DEFAULT_CONFIG = {
42
42
  // Code patterns
43
43
  checkSqlInjection: true,
44
44
  checkEvalUsage: true,
45
- checkCommandInjection: 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
46
52
  },
47
53
 
48
54
  // Stability checks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulbatical/tetra-dev-toolkit",
3
- "version": "1.16.3",
3
+ "version": "1.17.0",
4
4
  "publishConfig": {
5
5
  "access": "restricted"
6
6
  },