@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.
@@ -1,342 +0,0 @@
1
- /**
2
- * Route ↔ Config Alignment Check
3
- *
4
- * Verifies that route files match the feature config accessLevel:
5
- *
6
- * - accessLevel 'admin' → route file must be adminRoutes.ts AND must have authenticateToken + requireOrganizationAdmin
7
- * - accessLevel 'user' → route file must be userRoutes.ts AND must have authenticateToken
8
- * - accessLevel 'public' → route can be publicRoutes.ts, NO auth middleware required
9
- * - accessLevel 'system' → should NOT have any route file (backend-only)
10
- * - accessLevel 'creator' → route must have authenticateToken
11
- *
12
- * CRITICAL if an admin endpoint has no auth middleware.
13
- * HIGH if route name doesn't match access level.
14
- */
15
-
16
- import { readFileSync, existsSync } from 'fs'
17
- import { join, basename, dirname } from 'path'
18
- import { globSync } from 'glob'
19
-
20
- export const meta = {
21
- id: 'route-config-alignment',
22
- name: 'Route ↔ Config Alignment',
23
- category: 'security',
24
- severity: 'critical',
25
- description: 'Verifies route middleware matches feature config accessLevel'
26
- }
27
-
28
- /**
29
- * Find files matching a glob pattern
30
- */
31
- function findFiles(projectRoot, pattern) {
32
- try {
33
- return globSync(pattern, { cwd: projectRoot, absolute: true, ignore: ['**/node_modules/**'] })
34
- } catch {
35
- return []
36
- }
37
- }
38
-
39
- /**
40
- * Parse all feature configs to extract tableName → accessLevel + feature directory
41
- */
42
- function parseFeatureConfigs(projectRoot) {
43
- const configs = [] // { tableName, accessLevel, configFile, featureDir }
44
-
45
- const configFiles = [
46
- ...findFiles(projectRoot, 'backend/src/features/**/config/*.config.ts'),
47
- ...findFiles(projectRoot, 'src/features/**/config/*.config.ts')
48
- ]
49
-
50
- for (const file of configFiles) {
51
- let content
52
- try { content = readFileSync(file, 'utf-8') } catch { continue }
53
-
54
- const tableMatch = content.match(/tableName:\s*['"]([^'"]+)['"]/)
55
- if (!tableMatch) continue
56
-
57
- const tableName = tableMatch[1]
58
-
59
- const accessMatch = content.match(/accessLevel:\s*['"]([^'"]+)['"]/)
60
- const accessLevel = accessMatch ? accessMatch[1] : 'admin'
61
-
62
- // Feature directory is two levels up from config file (features/X/config/file.ts → features/X)
63
- const configDir = dirname(file)
64
- const featureDir = dirname(configDir)
65
-
66
- configs.push({
67
- tableName,
68
- accessLevel,
69
- configFile: file.replace(projectRoot + '/', ''),
70
- featureDir
71
- })
72
- }
73
-
74
- return configs
75
- }
76
-
77
- /**
78
- * Find route files in a feature directory
79
- */
80
- function findRouteFiles(featureDir) {
81
- const routesDir = join(featureDir, 'routes')
82
- if (!existsSync(routesDir)) return []
83
-
84
- try {
85
- return globSync('*.ts', { cwd: routesDir, absolute: true, ignore: ['*.d.ts'] })
86
- } catch {
87
- return []
88
- }
89
- }
90
-
91
- /**
92
- * Check if a route file contains authenticateToken middleware
93
- */
94
- function hasAuthMiddleware(content) {
95
- return /authenticateToken/.test(content)
96
- }
97
-
98
- /**
99
- * Check if a route file contains requireOrganizationAdmin middleware
100
- */
101
- function hasOrgAdminMiddleware(content) {
102
- return /requireOrganizationAdmin/.test(content)
103
- }
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
-
131
- /**
132
- * Expected route filename for a given accessLevel
133
- */
134
- function expectedRouteFileName(accessLevel) {
135
- switch (accessLevel) {
136
- case 'admin': return 'adminRoutes.ts'
137
- case 'user': return 'userRoutes.ts'
138
- case 'public': return 'publicRoutes.ts'
139
- default: return null
140
- }
141
- }
142
-
143
- export async function run(config, projectRoot) {
144
- const results = {
145
- passed: true,
146
- skipped: false,
147
- findings: [],
148
- summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
149
- details: { routesChecked: 0, violations: 0 }
150
- }
151
-
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)
157
-
158
- const featureConfigs = parseFeatureConfigs(projectRoot)
159
-
160
- if (featureConfigs.length === 0) {
161
- results.skipped = true
162
- results.skipReason = 'No feature config files found'
163
- return results
164
- }
165
-
166
- for (const cfg of featureConfigs) {
167
- // Skip features explicitly ignored in config
168
- if (routeIgnore.some(pattern => cfg.tableName === pattern || cfg.configFile.includes(pattern))) continue
169
- const routeFiles = findRouteFiles(cfg.featureDir)
170
-
171
- // --- system: should NOT have any route file ---
172
- if (cfg.accessLevel === 'system') {
173
- if (routeFiles.length > 0) {
174
- const routeNames = routeFiles.map(f => basename(f)).join(', ')
175
- results.findings.push({
176
- file: cfg.configFile,
177
- line: 1,
178
- type: 'system-has-routes',
179
- severity: 'high',
180
- message: `Config declares accessLevel "system" for table "${cfg.tableName}" but feature has route files: ${routeNames}. System features should be backend-only with no HTTP routes.`,
181
- fix: `Remove route files or change accessLevel in the config.`
182
- })
183
- results.summary.high++
184
- results.summary.total++
185
- results.passed = false
186
- results.details.violations++
187
- }
188
- continue
189
- }
190
-
191
- // Skip features with no routes (may be intentional for some configs)
192
- if (routeFiles.length === 0) continue
193
-
194
- // Check each route file in this feature
195
- for (const routeFile of routeFiles) {
196
- results.details.routesChecked++
197
-
198
- let content
199
- try { content = readFileSync(routeFile, 'utf-8') } catch { continue }
200
-
201
- const routeName = basename(routeFile)
202
- const relRouteFile = routeFile.replace(projectRoot + '/', '')
203
-
204
- // --- admin checks ---
205
- if (cfg.accessLevel === 'admin') {
206
- // Route name should be adminRoutes.ts
207
- if (routeName === 'adminRoutes.ts') {
208
- // CRITICAL: admin route MUST have authenticateToken (in file OR via RouteManager group)
209
- if (!hasAuthMiddleware(content) && !routeManagerHasGroupAuth) {
210
- results.findings.push({
211
- file: relRouteFile,
212
- line: 1,
213
- type: 'admin-route-no-auth',
214
- severity: 'critical',
215
- message: `Admin route for table "${cfg.tableName}" is missing authenticateToken middleware. Endpoints are accessible without authentication.`,
216
- fix: `Add authenticateToken middleware: router.use(authenticateToken, requireOrganizationAdmin)`
217
- })
218
- results.summary.critical++
219
- results.summary.total++
220
- results.passed = false
221
- results.details.violations++
222
- }
223
-
224
- // CRITICAL: admin route MUST have requireOrganizationAdmin (in file OR via RouteManager group)
225
- if (!hasOrgAdminMiddleware(content) && !routeManagerHasGroupAuth) {
226
- results.findings.push({
227
- file: relRouteFile,
228
- line: 1,
229
- type: 'admin-route-no-org-admin',
230
- severity: 'critical',
231
- message: `Admin route for table "${cfg.tableName}" is missing requireOrganizationAdmin middleware. Any authenticated user can access admin endpoints.`,
232
- fix: `Add requireOrganizationAdmin middleware: router.use(authenticateToken, requireOrganizationAdmin)`
233
- })
234
- results.summary.critical++
235
- results.summary.total++
236
- results.passed = false
237
- results.details.violations++
238
- }
239
- }
240
- }
241
-
242
- // --- user checks ---
243
- if (cfg.accessLevel === 'user') {
244
- if (routeName === 'userRoutes.ts') {
245
- if (!hasAuthMiddleware(content)) {
246
- results.findings.push({
247
- file: relRouteFile,
248
- line: 1,
249
- type: 'user-route-no-auth',
250
- severity: 'critical',
251
- message: `User route for table "${cfg.tableName}" is missing authenticateToken middleware. Endpoints are accessible without authentication.`,
252
- fix: `Add authenticateToken middleware: router.use(authenticateToken)`
253
- })
254
- results.summary.critical++
255
- results.summary.total++
256
- results.passed = false
257
- results.details.violations++
258
- }
259
- }
260
-
261
- // HIGH: user-level feature shouldn't primarily use adminRoutes
262
- if (routeName === 'adminRoutes.ts') {
263
- results.findings.push({
264
- file: relRouteFile,
265
- line: 1,
266
- type: 'user-feature-admin-route',
267
- severity: 'high',
268
- message: `Config declares accessLevel "user" for table "${cfg.tableName}" but has adminRoutes.ts. Route file name does not match access level.`,
269
- fix: `Rename to userRoutes.ts or update config accessLevel to "admin".`
270
- })
271
- results.summary.high++
272
- results.summary.total++
273
- results.passed = false
274
- results.details.violations++
275
- }
276
- }
277
-
278
- // --- creator checks ---
279
- if (cfg.accessLevel === 'creator') {
280
- // Creator routes must have authenticateToken
281
- if (!hasAuthMiddleware(content) && routeName !== 'publicRoutes.ts') {
282
- results.findings.push({
283
- file: relRouteFile,
284
- line: 1,
285
- type: 'creator-route-no-auth',
286
- severity: 'critical',
287
- message: `Creator route "${routeName}" for table "${cfg.tableName}" is missing authenticateToken middleware. Endpoints are accessible without authentication.`,
288
- fix: `Add authenticateToken middleware: router.use(authenticateToken)`
289
- })
290
- results.summary.critical++
291
- results.summary.total++
292
- results.passed = false
293
- results.details.violations++
294
- }
295
- }
296
-
297
- // --- public checks: no auth required, just verify naming ---
298
- // public routes are fine without auth middleware, no action needed
299
-
300
- // --- Cross-access-level route name mismatch ---
301
- // Skip if route file has explicit @tetra-audit-ignore for this check
302
- const hasIgnoreDirective = /@tetra-audit-ignore\s+route-config-alignment\b/.test(content)
303
- if (cfg.accessLevel === 'admin' && routeName === 'publicRoutes.ts' && !hasAuthMiddleware(content) && !hasIgnoreDirective) {
304
- results.findings.push({
305
- file: relRouteFile,
306
- line: 1,
307
- type: 'admin-feature-public-route-no-auth',
308
- severity: 'critical',
309
- message: `Config declares accessLevel "admin" for table "${cfg.tableName}" but has a publicRoutes.ts without auth. Admin data may be exposed publicly.`,
310
- fix: `Either add auth middleware to publicRoutes.ts, or add @tetra-audit-ignore route-config-alignment comment if the public route is intentional.`
311
- })
312
- results.summary.critical++
313
- results.summary.total++
314
- results.passed = false
315
- results.details.violations++
316
- }
317
- }
318
-
319
- // HIGH: Check if expected route file name exists for the access level
320
- const expected = expectedRouteFileName(cfg.accessLevel)
321
- if (expected && routeFiles.length > 0) {
322
- const hasExpected = routeFiles.some(f => basename(f) === expected)
323
- if (!hasExpected) {
324
- const routeNames = routeFiles.map(f => basename(f)).join(', ')
325
- results.findings.push({
326
- file: cfg.configFile,
327
- line: 1,
328
- type: 'route-name-mismatch',
329
- severity: 'high',
330
- message: `Config declares accessLevel "${cfg.accessLevel}" for table "${cfg.tableName}" expecting ${expected} but found: ${routeNames}.`,
331
- fix: `Rename the primary route file to ${expected} or update the config accessLevel.`
332
- })
333
- results.summary.high++
334
- results.summary.total++
335
- results.passed = false
336
- results.details.violations++
337
- }
338
- }
339
- }
340
-
341
- return results
342
- }
@@ -1,175 +0,0 @@
1
- /**
2
- * RPC Security Mode Check — HARD BLOCK
3
- *
4
- * Scans ALL SQL migrations for RPC functions and verifies their security mode:
5
- *
6
- * SECURITY DEFINER = function runs as the DB owner, BYPASSES RLS completely.
7
- * SECURITY INVOKER = function runs as the calling user, RLS is enforced.
8
- *
9
- * Rules:
10
- * - Data query RPCs (get_*, list_*, search_*) → MUST be INVOKER
11
- * - Auth helper functions (auth_org_id, auth_uid) → DEFINER is OK (they need to read auth.users)
12
- * - Count/results RPCs linked to feature configs → MUST be INVOKER
13
- * - Public RPCs (explicitly returning only public columns) → DEFINER is OK if whitelisted
14
- *
15
- * Whitelist: .tetra-quality.json → supabase.securityDefinerWhitelist: ['auth_org_id', ...]
16
- *
17
- * Reference: stella_howto_get slug="tetra-architecture-guide"
18
- */
19
-
20
- import { readFileSync, existsSync } from 'fs'
21
- import { join } from 'path'
22
- import { globSync } from 'glob'
23
-
24
- export const meta = {
25
- id: 'rpc-security-mode',
26
- name: 'RPC Security Mode',
27
- category: 'security',
28
- severity: 'critical',
29
- description: 'Verifies all RPC functions use SECURITY INVOKER (not DEFINER) unless explicitly whitelisted. DEFINER bypasses RLS completely.'
30
- }
31
-
32
- // Functions that legitimately need SECURITY DEFINER
33
- const BUILTIN_DEFINER_WHITELIST = [
34
- // Auth helpers (need to read auth schema / organization_members)
35
- 'auth_org_id',
36
- 'auth_admin_organizations',
37
- 'auth_user_organizations',
38
- 'auth_creator_organizations',
39
- 'get_user_org_role',
40
- 'get_org_id',
41
- 'handle_new_user',
42
- 'moddatetime',
43
- // Public RPCs (called by anon users, need DEFINER to bypass RLS and return only safe columns)
44
- 'search_public_ad_library',
45
- // System/billing RPCs (called by systemDB, no user context)
46
- 'get_org_credit_limits',
47
- // Supabase internal
48
- 'pgsodium_encrypt',
49
- 'pgsodium_decrypt'
50
- ]
51
-
52
- export async function run(config, projectRoot) {
53
- const results = {
54
- passed: true,
55
- skipped: false,
56
- findings: [],
57
- summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
58
- details: { rpcsFound: 0, definerCount: 0, invokerCount: 0, defaultCount: 0, whitelistedCount: 0 }
59
- }
60
-
61
- const migrationDirs = [
62
- join(projectRoot, 'supabase/migrations'),
63
- join(projectRoot, 'backend/supabase/migrations')
64
- ]
65
-
66
- const sqlFiles = []
67
- for (const dir of migrationDirs) {
68
- if (!existsSync(dir)) continue
69
- try {
70
- const files = globSync('*.sql', { cwd: dir, absolute: true })
71
- sqlFiles.push(...files)
72
- } catch { /* skip */ }
73
- }
74
-
75
- if (sqlFiles.length === 0) {
76
- results.skipped = true
77
- results.skipReason = 'No SQL migration files found'
78
- return results
79
- }
80
-
81
- // Build whitelist from config + builtins
82
- const userWhitelist = (config.supabase?.securityDefinerWhitelist || [])
83
- const whitelist = new Set([...BUILTIN_DEFINER_WHITELIST, ...userWhitelist])
84
-
85
- // Track latest definition per function (migrations can override)
86
- const functions = new Map() // funcName → { securityMode, file, line, isDataQuery }
87
-
88
- for (const file of sqlFiles) {
89
- let content
90
- try { content = readFileSync(file, 'utf-8') } catch { continue }
91
-
92
- const relFile = file.replace(projectRoot + '/', '')
93
-
94
- // Find all CREATE [OR REPLACE] FUNCTION statements
95
- const funcRegex = /CREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+(?:public\.)?(\w+)\s*\(/gi
96
- let match
97
-
98
- while ((match = funcRegex.exec(content)) !== null) {
99
- const funcName = match[1]
100
- const startPos = match.index
101
-
102
- // Extract the function body (up to next CREATE FUNCTION or end of file, max 5000 chars)
103
- const bodyEnd = Math.min(startPos + 5000, content.length)
104
- const funcBody = content.substring(startPos, bodyEnd)
105
-
106
- // Determine security mode
107
- let securityMode = 'DEFAULT' // PostgreSQL default is INVOKER
108
- if (/SECURITY\s+DEFINER/i.test(funcBody.substring(0, 2000))) {
109
- securityMode = 'DEFINER'
110
- } else if (/SECURITY\s+INVOKER/i.test(funcBody.substring(0, 2000))) {
111
- securityMode = 'INVOKER'
112
- }
113
-
114
- // Determine if this is a data query function
115
- const isDataQuery = /^(get_|list_|search_|find_|fetch_|count_)/i.test(funcName) ||
116
- /_counts$|_results$|_detail$/i.test(funcName)
117
-
118
- // Calculate line number
119
- const beforeMatch = content.substring(0, startPos)
120
- const line = (beforeMatch.match(/\n/g) || []).length + 1
121
-
122
- // Store (later definitions override earlier ones)
123
- functions.set(funcName, { securityMode, file: relFile, line, isDataQuery, funcBody: funcBody.substring(0, 500) })
124
- }
125
- }
126
-
127
- results.details.rpcsFound = functions.size
128
-
129
- for (const [funcName, info] of functions) {
130
- if (info.securityMode === 'DEFINER') {
131
- results.details.definerCount++
132
-
133
- // Check whitelist
134
- if (whitelist.has(funcName)) {
135
- results.details.whitelistedCount++
136
- continue
137
- }
138
-
139
- // Data query RPCs with DEFINER = CRITICAL
140
- if (info.isDataQuery) {
141
- results.passed = false
142
- results.findings.push({
143
- file: info.file,
144
- line: info.line,
145
- type: 'data-rpc-security-definer',
146
- severity: 'critical',
147
- message: `Data RPC "${funcName}" uses SECURITY DEFINER — bypasses ALL RLS policies. Any authenticated user can see ALL data from ALL organizations.`,
148
- fix: `Change to SECURITY INVOKER or remove the SECURITY DEFINER clause. If this function legitimately needs DEFINER, add "${funcName}" to supabase.securityDefinerWhitelist in .tetra-quality.json.`
149
- })
150
- results.summary.critical++
151
- results.summary.total++
152
- } else {
153
- // Non-data RPCs with DEFINER = HIGH (should still be investigated)
154
- results.findings.push({
155
- file: info.file,
156
- line: info.line,
157
- type: 'non-data-rpc-security-definer',
158
- severity: 'high',
159
- message: `RPC "${funcName}" uses SECURITY DEFINER but is not whitelisted. DEFINER functions bypass RLS — ensure this is intentional.`,
160
- fix: `Change to SECURITY INVOKER, or add "${funcName}" to supabase.securityDefinerWhitelist in .tetra-quality.json if DEFINER is intentional.`
161
- })
162
- results.summary.high++
163
- results.summary.total++
164
- results.passed = false
165
- }
166
- } else if (info.securityMode === 'INVOKER') {
167
- results.details.invokerCount++
168
- } else {
169
- results.details.defaultCount++
170
- // DEFAULT = INVOKER in PostgreSQL, which is correct
171
- }
172
- }
173
-
174
- return results
175
- }