@soulbatical/tetra-dev-toolkit 1.18.1 → 1.20.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.
@@ -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'} HealthCheckType
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'|'smoke-readiness'} 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', 'backend/src/features/telegram/']
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', 'backend/src/features/telegram/']
53
53
  },
54
54
  {
55
55
  pattern: /function playMacAlert\(\)/,
@@ -29,7 +29,7 @@ export const meta = {
29
29
  name: 'Config ↔ RLS Alignment',
30
30
  category: 'security',
31
31
  severity: 'critical',
32
- description: 'Verifies that feature config accessLevel matches actual RLS policies on each table. The golden 1:1 check.'
32
+ description: 'Verifies that feature config accessLevel matches actual RLS policies on each table. Uses live DB when available, falls back to migration parsing.'
33
33
  }
34
34
 
35
35
  /**
@@ -390,17 +390,23 @@ function validateRlsClause(clause) {
390
390
  return null
391
391
  }
392
392
 
393
- export async function run(config, projectRoot) {
393
+ export async function run(config, projectRoot, options = {}) {
394
394
  const results = {
395
395
  passed: true,
396
396
  skipped: false,
397
397
  findings: [],
398
398
  summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
399
- details: { tablesChecked: 0, configsFound: 0, alignmentErrors: 0 }
399
+ details: { tablesChecked: 0, configsFound: 0, alignmentErrors: 0, source: 'migrations' }
400
400
  }
401
401
 
402
402
  const featureConfigs = parseFeatureConfigs(projectRoot)
403
- const rlsData = parseMigrations(projectRoot)
403
+
404
+ // Use live DB state if available (passed from rls-live-audit via runner),
405
+ // otherwise fall back to migration file parsing
406
+ const rlsData = options.liveState || parseMigrations(projectRoot)
407
+ if (options.liveState) {
408
+ results.details.source = 'live-db'
409
+ }
404
410
 
405
411
  results.details.configsFound = featureConfigs.size
406
412
 
@@ -63,6 +63,15 @@ 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
+
66
75
  // Scripts (not production code)
67
76
  /scripts\//,
68
77
  ]
@@ -34,10 +34,13 @@ 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 (always exclude node_modules, even nested ones)
38
38
  const files = await glob('**/*.{ts,tsx,js,jsx,json}', {
39
39
  cwd: projectRoot,
40
- ignore: config.ignore
40
+ ignore: [
41
+ '**/node_modules/**',
42
+ ...config.ignore
43
+ ]
41
44
  })
42
45
 
43
46
  for (const file of files) {
@@ -1,21 +1,35 @@
1
1
  /**
2
2
  * RLS Live DB Audit — queries pg_policies from the LIVE database
3
3
  *
4
- * The source of truth for RLS policy validation. Migration file parsing can miss:
4
+ * The PRIMARY source of truth for RLS policy validation.
5
+ * When this check runs successfully, migration-based RLS checks
6
+ * (rls-policy-audit, config-rls-alignment) are skipped for current state
7
+ * and only validate unapplied migrations (delta).
8
+ *
9
+ * Migration file parsing can miss:
5
10
  * - Policies applied directly via SQL (not in migration files)
6
11
  * - PL/pgSQL dynamic policies (EXECUTE format)
7
12
  * - Policies overridden by later manual changes
8
13
  *
9
14
  * Requires SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY in environment.
10
- * Connects via @supabase/supabase-js and calls a lightweight RPC.
11
15
  *
12
16
  * Setup (one-time per project):
13
17
  * CREATE OR REPLACE FUNCTION tetra_rls_audit()
14
- * RETURNS TABLE(tablename text, policyname text, using_clause text, with_check_clause text)
18
+ * RETURNS TABLE(tablename text, policyname text, cmd text, using_clause text, with_check_clause text)
15
19
  * LANGUAGE sql SECURITY DEFINER AS $$
16
- * SELECT tablename::text, policyname::text, qual::text, with_check::text
20
+ * SELECT tablename::text, policyname::text, cmd::text, qual::text, with_check::text
17
21
  * FROM pg_policies WHERE schemaname = 'public';
18
22
  * $$;
23
+ *
24
+ * -- Optional: also return RLS enabled status per table
25
+ * CREATE OR REPLACE FUNCTION tetra_rls_tables()
26
+ * RETURNS TABLE(table_name text, rls_enabled boolean, rls_forced boolean)
27
+ * LANGUAGE sql SECURITY DEFINER AS $$
28
+ * SELECT c.relname::text, c.relrowsecurity, c.relforcerowsecurity
29
+ * FROM pg_class c
30
+ * JOIN pg_namespace n ON n.oid = c.relnamespace
31
+ * WHERE n.nspname = 'public' AND c.relkind = 'r';
32
+ * $$;
19
33
  */
20
34
 
21
35
  export const meta = {
@@ -70,13 +84,83 @@ function validateRlsClause(clause) {
70
84
  return null
71
85
  }
72
86
 
87
+ /**
88
+ * Fetch data from a Supabase RPC endpoint.
89
+ * Returns null if the RPC doesn't exist or fails.
90
+ */
91
+ async function callRpc(supabaseUrl, supabaseKey, rpcName) {
92
+ try {
93
+ const response = await fetch(`${supabaseUrl}/rest/v1/rpc/${rpcName}`, {
94
+ method: 'POST',
95
+ headers: {
96
+ 'apikey': supabaseKey,
97
+ 'Authorization': `Bearer ${supabaseKey}`,
98
+ 'Content-Type': 'application/json',
99
+ },
100
+ body: '{}',
101
+ })
102
+ if (!response.ok) return null
103
+ return await response.json()
104
+ } catch {
105
+ return null
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Build a structured live DB state from raw policy + table data.
111
+ * This is the same format that config-rls-alignment uses from migration parsing,
112
+ * so it can transparently use either source.
113
+ *
114
+ * Returns: Map<tableName, { rlsEnabled, policies: [{ name, operation, using, withCheck }] }>
115
+ */
116
+ export function buildLiveState(policies, tableStatus) {
117
+ const tables = new Map()
118
+
119
+ // Initialize from table status (if available)
120
+ if (Array.isArray(tableStatus)) {
121
+ for (const t of tableStatus) {
122
+ if (!t?.table_name) continue
123
+ tables.set(t.table_name, {
124
+ rlsEnabled: t.rls_enabled || false,
125
+ policies: [],
126
+ rpcFunctions: new Map(),
127
+ source: 'live-db'
128
+ })
129
+ }
130
+ }
131
+
132
+ // Add policies
133
+ for (const policy of policies) {
134
+ if (!policy) continue
135
+ const { tablename, policyname, cmd, using_clause, with_check_clause } = policy
136
+ if (!tablename) continue
137
+
138
+ if (!tables.has(tablename)) {
139
+ // Table exists in policies but not in table status — it has RLS (otherwise no policies)
140
+ tables.set(tablename, { rlsEnabled: true, policies: [], rpcFunctions: new Map(), source: 'live-db' })
141
+ }
142
+
143
+ tables.get(tablename).policies.push({
144
+ name: policyname,
145
+ operation: (cmd || 'ALL').toUpperCase(),
146
+ using: using_clause || '',
147
+ withCheck: with_check_clause || '',
148
+ file: 'LIVE DB'
149
+ })
150
+ }
151
+
152
+ return tables
153
+ }
154
+
73
155
  export async function run(config, projectRoot) {
74
156
  const results = {
75
157
  passed: true,
76
158
  skipped: false,
77
159
  findings: [],
78
160
  summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
79
- details: { policiesChecked: 0, tablesChecked: 0, violations: 0 }
161
+ details: { policiesChecked: 0, tablesChecked: 0, violations: 0 },
162
+ // Expose live data so runner can pass it to other checks
163
+ _liveData: null
80
164
  }
81
165
 
82
166
  const supabaseUrl = process.env.SUPABASE_URL
@@ -105,7 +189,7 @@ export async function run(config, projectRoot) {
105
189
  const status = response.status
106
190
  if (status === 404) {
107
191
  results.skipped = true
108
- results.skipReason = 'RPC tetra_rls_audit() not found. Create it once:\n\n CREATE OR REPLACE FUNCTION tetra_rls_audit()\n RETURNS TABLE(tablename text, policyname text, using_clause text, with_check_clause text)\n LANGUAGE sql SECURITY DEFINER AS $$\n SELECT tablename::text, policyname::text, qual::text, with_check::text\n FROM pg_policies WHERE schemaname = \'public\';\n $$;'
192
+ results.skipReason = 'RPC tetra_rls_audit() not found. Create it once:\n\n CREATE OR REPLACE FUNCTION tetra_rls_audit()\n RETURNS TABLE(tablename text, policyname text, cmd text, using_clause text, with_check_clause text)\n LANGUAGE sql SECURITY DEFINER AS $$\n SELECT tablename::text, policyname::text, cmd::text, qual::text, with_check::text\n FROM pg_policies WHERE schemaname = \'public\';\n $$;'
109
193
  } else {
110
194
  results.skipped = true
111
195
  results.skipReason = `RPC tetra_rls_audit() failed with status ${status}`
@@ -126,6 +210,13 @@ export async function run(config, projectRoot) {
126
210
  return results
127
211
  }
128
212
 
213
+ // Optionally fetch table-level RLS status
214
+ const tableStatus = await callRpc(supabaseUrl, supabaseKey, 'tetra_rls_tables')
215
+
216
+ // Build structured live state (shared with config-rls-alignment)
217
+ const liveState = buildLiveState(policies, tableStatus)
218
+ results._liveData = { policies, tableStatus, liveState }
219
+
129
220
  const backendOnlyTables = config.supabase?.backendOnlyTables || []
130
221
  const tablesChecked = new Set()
131
222
 
package/lib/runner.js CHANGED
@@ -47,6 +47,7 @@ import { check as checkBundleSize } from './checks/health/bundle-size.js'
47
47
  import { check as checkSast } from './checks/health/sast.js'
48
48
  import { check as checkLicenseAudit } from './checks/health/license-audit.js'
49
49
  import { check as checkSecurityLayers } from './checks/health/security-layers.js'
50
+ import { check as checkSmokeReadiness } from './checks/health/smoke-readiness.js'
50
51
 
51
52
  /**
52
53
  * Adapt a health check (score-based) to the runner format (meta + run).
@@ -93,12 +94,12 @@ const ALL_CHECKS = {
93
94
  frontendSupabaseQueries,
94
95
  tetraCoreCompliance,
95
96
  mixedDbUsage,
96
- configRlsAlignment,
97
97
  rpcSecurityMode,
98
98
  systemdbWhitelist,
99
99
  gitignoreValidation,
100
100
  routeConfigAlignment,
101
- rlsLiveAudit
101
+ rlsLiveAudit, // Must run BEFORE config-rls-alignment (provides live DB data)
102
+ configRlsAlignment, // Uses live DB data from rls-live-audit when available
102
103
  ],
103
104
  stability: [
104
105
  huskyHooks,
@@ -116,7 +117,8 @@ const ALL_CHECKS = {
116
117
  adaptHealthCheck('bundle-size', 'Bundle Size Monitoring', 'low', checkBundleSize),
117
118
  adaptHealthCheck('sast', 'Static Application Security Testing', 'medium', checkSast),
118
119
  adaptHealthCheck('license-audit', 'License Compliance', 'low', checkLicenseAudit),
119
- adaptHealthCheck('security-layers', 'Security Layer Coverage', 'high', checkSecurityLayers)
120
+ adaptHealthCheck('security-layers', 'Security Layer Coverage', 'high', checkSecurityLayers),
121
+ adaptHealthCheck('smoke-readiness', 'Smoke Test Readiness', 'medium', checkSmokeReadiness)
120
122
  ],
121
123
  codeQuality: [
122
124
  apiResponseFormat,
@@ -162,6 +164,10 @@ export async function runAllChecks(options = {}) {
162
164
  }
163
165
  }
164
166
 
167
+ // Track rls-live-audit result across suites so migration-based RLS checks can be skipped.
168
+ // rls-live-audit runs in 'security' suite, rls-policy-audit runs in 'supabase' suite.
169
+ let rlsLiveData = null
170
+
165
171
  for (const suite of suites) {
166
172
  if (!config.suites[suite]) {
167
173
  continue
@@ -174,11 +180,43 @@ export async function runAllChecks(options = {}) {
174
180
  }
175
181
 
176
182
  for (const check of checks) {
177
- const checkResult = {
178
- id: check.meta.id,
179
- name: check.meta.name,
180
- severity: check.meta.severity,
181
- ...await check.run(config, projectRoot)
183
+ let checkResult
184
+
185
+ // rls-policy-audit is pure migration parsing — skip when live DB is available
186
+ if (rlsLiveData && check.meta.id === 'rls-policy-audit') {
187
+ checkResult = {
188
+ id: check.meta.id,
189
+ name: `${check.meta.name} (skipped — live DB is source of truth)`,
190
+ severity: check.meta.severity,
191
+ passed: true,
192
+ skipped: true,
193
+ skipReason: 'Skipped: rls-live-audit succeeded — live DB is the source of truth for current RLS state.',
194
+ findings: [],
195
+ summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 }
196
+ }
197
+ }
198
+ // config-rls-alignment: always runs, but uses live DB data when available
199
+ else if (rlsLiveData && check.meta.id === 'config-rls-alignment') {
200
+ checkResult = {
201
+ id: check.meta.id,
202
+ name: `${check.meta.name} (live DB)`,
203
+ severity: check.meta.severity,
204
+ ...await check.run(config, projectRoot, { liveState: rlsLiveData.liveState })
205
+ }
206
+ }
207
+ else {
208
+ checkResult = {
209
+ id: check.meta.id,
210
+ name: check.meta.name,
211
+ severity: check.meta.severity,
212
+ ...await check.run(config, projectRoot)
213
+ }
214
+ }
215
+
216
+ // Capture live data from rls-live-audit for downstream checks
217
+ if (check.meta.id === 'rls-live-audit' && !checkResult.skipped && checkResult._liveData) {
218
+ rlsLiveData = checkResult._liveData
219
+ delete checkResult._liveData // Don't leak internal data into output
182
220
  }
183
221
 
184
222
  results.suites[suite].checks.push(checkResult)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulbatical/tetra-dev-toolkit",
3
- "version": "1.18.1",
3
+ "version": "1.20.0",
4
4
  "publishConfig": {
5
5
  "access": "restricted"
6
6
  },
@@ -32,7 +32,9 @@
32
32
  "tetra-check-rls": "./bin/tetra-check-rls.js",
33
33
  "tetra-migration-lint": "./bin/tetra-migration-lint.js",
34
34
  "tetra-db-push": "./bin/tetra-db-push.js",
35
- "tetra-check-peers": "./bin/tetra-check-peers.js"
35
+ "tetra-check-peers": "./bin/tetra-check-peers.js",
36
+ "tetra-security-gate": "./bin/tetra-security-gate.js",
37
+ "tetra-smoke": "./bin/tetra-smoke.js"
36
38
  },
37
39
  "files": [
38
40
  "bin/",