@soulbatical/tetra-dev-toolkit 1.5.0 → 1.6.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.
@@ -110,7 +110,9 @@ export async function check(projectPath) {
110
110
  else if (lower.includes('codeql')) result.details.tool = 'codeql'
111
111
  else if (lower.includes('sonar')) result.details.tool = 'sonarqube'
112
112
  else if (lower.includes('snyk')) result.details.tool = 'snyk'
113
+ // CI-only SAST (e.g. semgrep --config auto) counts as config too
113
114
  result.details.configFound = true
115
+ result.score += 1
114
116
  }
115
117
  break
116
118
  }
@@ -16,6 +16,7 @@ export * as npmAudit from './stability/npm-audit.js'
16
16
  // Supabase checks
17
17
  export * as rlsPolicyAudit from './supabase/rls-policy-audit.js'
18
18
  export * as rpcParamMismatch from './supabase/rpc-param-mismatch.js'
19
+ export * as rpcGeneratorOrigin from './supabase/rpc-generator-origin.js'
19
20
 
20
21
  // Health checks (project ecosystem)
21
22
  export * as health from './health/index.js'
@@ -0,0 +1,217 @@
1
+ /**
2
+ * RPC Generator Origin Check
3
+ *
4
+ * Ensures that filter/count RPC functions (get_*_results, get_*_counts) are
5
+ * ONLY created via the SQL Generator, never hand-written.
6
+ *
7
+ * Origin: February 2026, SparkBuddy. Hand-editing SQL caused 30+ minutes of
8
+ * debugging when security blocks didn't match, search_path broke, and
9
+ * DO blocks/regex replacements on production failed silently. The only
10
+ * reliable path is: fix the config → regenerate via SQL Generator → deploy.
11
+ *
12
+ * Modes:
13
+ * - Pre-commit (default): Only checks git-staged files matching the pattern.
14
+ * This avoids false positives on legacy files.
15
+ * - Full audit (config.rpcGeneratorOrigin.checkAll = true): Checks ALL files.
16
+ * Use for comprehensive audits.
17
+ *
18
+ * This prevents:
19
+ * - Hand-written RPCs with wrong security patterns (accessLevel mismatch)
20
+ * - Missing public. prefix in search_path="" functions
21
+ * - Security blocks that don't match the generator's output
22
+ * - Silent breakage when generator overwrites manual edits on next run
23
+ */
24
+
25
+ import { readFileSync, existsSync, readdirSync } from 'fs'
26
+ import { join, relative } from 'path'
27
+ import { execSync } from 'child_process'
28
+
29
+ export const meta = {
30
+ id: 'rpc-generator-origin',
31
+ name: 'RPC Generator Origin',
32
+ category: 'supabase',
33
+ severity: 'critical',
34
+ description: 'Ensures get_*_results and get_*_counts SQL files are generated by the SQL Generator, not hand-written'
35
+ }
36
+
37
+ /**
38
+ * Pattern for files that MUST come from the SQL Generator
39
+ */
40
+ const GENERATED_FILE_PATTERN = /get_\w+_(results|counts)\.sql$/
41
+
42
+ /**
43
+ * Required header that the SQL Generator always includes
44
+ */
45
+ const GENERATOR_HEADER = '-- Generator: SQL Generator'
46
+
47
+ /**
48
+ * Alternative headers from older generator versions
49
+ */
50
+ const LEGACY_HEADERS = [
51
+ '-- Generated by SQL Generator',
52
+ '-- Auto-generated by RPCGenerator',
53
+ '-- Generator: RPC Generator'
54
+ ]
55
+
56
+ /**
57
+ * Try to get git-staged files matching our pattern.
58
+ * Returns null if git is not available or not in a repo.
59
+ */
60
+ function getStagedFiles(projectRoot) {
61
+ try {
62
+ const output = execSync('git diff --cached --name-only --diff-filter=ACM', {
63
+ cwd: projectRoot,
64
+ encoding: 'utf-8',
65
+ timeout: 5000
66
+ }).trim()
67
+
68
+ if (!output) return []
69
+
70
+ return output.split('\n')
71
+ .filter(f => GENERATED_FILE_PATTERN.test(f))
72
+ .map(f => join(projectRoot, f))
73
+ } catch {
74
+ return null // Not a git repo or git not available
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Get ALL matching files from migration directories
80
+ */
81
+ function getAllFiles(config, projectRoot) {
82
+ const migrationPaths = [
83
+ ...(config.paths?.migrations || ['supabase/migrations', 'migrations']),
84
+ 'backend/supabase/migrations'
85
+ ]
86
+
87
+ const sqlFiles = []
88
+ for (const relPath of migrationPaths) {
89
+ const dir = join(projectRoot, relPath)
90
+ if (!existsSync(dir)) continue
91
+ try {
92
+ const files = readdirSync(dir)
93
+ .filter(f => GENERATED_FILE_PATTERN.test(f))
94
+ .sort()
95
+ for (const f of files) {
96
+ sqlFiles.push(join(dir, f))
97
+ }
98
+ } catch {
99
+ // ignore
100
+ }
101
+ }
102
+
103
+ // De-duplicate: keep only latest per function name
104
+ const latestByFunction = new Map()
105
+ for (const filePath of sqlFiles) {
106
+ const fileName = filePath.split('/').pop()
107
+ const funcMatch = fileName.match(/\d+_(get_\w+(?:_results|_counts))\.sql$/)
108
+ if (!funcMatch) continue
109
+ latestByFunction.set(funcMatch[1], filePath)
110
+ }
111
+
112
+ return [...latestByFunction.values()]
113
+ }
114
+
115
+ /**
116
+ * Check a single file for the generator header
117
+ */
118
+ function checkFile(filePath, projectRoot) {
119
+ const relPath = relative(projectRoot, filePath)
120
+
121
+ let content
122
+ try {
123
+ content = readFileSync(filePath, 'utf-8').substring(0, 500)
124
+ } catch {
125
+ return null // Can't read
126
+ }
127
+
128
+ const hasCurrentHeader = content.includes(GENERATOR_HEADER)
129
+ const hasLegacyHeader = LEGACY_HEADERS.some(h => content.includes(h))
130
+
131
+ if (hasCurrentHeader || hasLegacyHeader) {
132
+ return { ok: true, file: relPath }
133
+ }
134
+
135
+ return {
136
+ ok: false,
137
+ file: relPath,
138
+ finding: {
139
+ file: relPath,
140
+ line: 1,
141
+ type: 'Hand-written RPC function',
142
+ severity: 'critical',
143
+ message: `${relPath} — Missing "-- Generator: SQL Generator" header. ` +
144
+ `This file appears to be hand-written. ` +
145
+ `RPC filter/count functions MUST be generated via: npm run generate:rpc <feature>. ` +
146
+ `Fix the feature config instead, then regenerate.`,
147
+ fix: [
148
+ '1. Find the feature config: backend/src/features/<feature>/config/<feature>.config.ts',
149
+ '2. Fix the config (accessLevel, customWhereClause, etc.)',
150
+ '3. Regenerate: cd backend && npm run generate:rpc <feature>',
151
+ '4. NEVER edit SQL files directly — the generator will overwrite your changes'
152
+ ].join('\n')
153
+ }
154
+ }
155
+ }
156
+
157
+ export async function run(config, projectRoot) {
158
+ const checkAll = config.rpcGeneratorOrigin?.checkAll === true
159
+ const results = {
160
+ passed: true,
161
+ findings: [],
162
+ summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
163
+ details: {
164
+ mode: checkAll ? 'full' : 'staged',
165
+ filesChecked: 0,
166
+ filesWithHeader: 0,
167
+ filesWithoutHeader: 0
168
+ }
169
+ }
170
+
171
+ let filesToCheck
172
+
173
+ if (checkAll) {
174
+ // Full audit mode: check all files
175
+ filesToCheck = getAllFiles(config, projectRoot)
176
+ } else {
177
+ // Pre-commit mode: only staged files
178
+ const stagedFiles = getStagedFiles(projectRoot)
179
+
180
+ if (stagedFiles === null) {
181
+ // Not in a git repo — fall back to all files
182
+ filesToCheck = getAllFiles(config, projectRoot)
183
+ results.details.mode = 'full (no git)'
184
+ } else if (stagedFiles.length === 0) {
185
+ // No staged RPC files — nothing to check
186
+ results.details.mode = 'staged (no matching files)'
187
+ return results
188
+ } else {
189
+ filesToCheck = stagedFiles
190
+ }
191
+ }
192
+
193
+ if (filesToCheck.length === 0) {
194
+ results.skipped = true
195
+ results.skipReason = 'No get_*_results/counts SQL files found'
196
+ return results
197
+ }
198
+
199
+ for (const filePath of filesToCheck) {
200
+ const result = checkFile(filePath, projectRoot)
201
+ if (!result) continue
202
+
203
+ results.details.filesChecked++
204
+
205
+ if (result.ok) {
206
+ results.details.filesWithHeader++
207
+ } else {
208
+ results.details.filesWithoutHeader++
209
+ results.passed = false
210
+ results.findings.push(result.finding)
211
+ results.summary.critical++
212
+ results.summary.total++
213
+ }
214
+ }
215
+
216
+ return results
217
+ }
package/lib/runner.js CHANGED
@@ -18,6 +18,7 @@ import * as apiResponseFormat from './checks/codeQuality/api-response-format.js'
18
18
  import * as gitignoreValidation from './checks/security/gitignore-validation.js'
19
19
  import * as rlsPolicyAudit from './checks/supabase/rls-policy-audit.js'
20
20
  import * as rpcParamMismatch from './checks/supabase/rpc-param-mismatch.js'
21
+ import * as rpcGeneratorOrigin from './checks/supabase/rpc-generator-origin.js'
21
22
  import * as fileOrganization from './checks/hygiene/file-organization.js'
22
23
 
23
24
  // Register all checks
@@ -39,7 +40,8 @@ const ALL_CHECKS = {
39
40
  ],
40
41
  supabase: [
41
42
  rlsPolicyAudit,
42
- rpcParamMismatch
43
+ rpcParamMismatch,
44
+ rpcGeneratorOrigin
43
45
  ],
44
46
  hygiene: [
45
47
  fileOrganization
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulbatical/tetra-dev-toolkit",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "publishConfig": {
5
5
  "access": "restricted"
6
6
  },