@homenshum/convex-mcp-nodebench 0.9.4 → 0.9.6
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.
- package/dist/index.js +1 -1
- package/dist/tools/actionAuditTools.js +4 -1
- package/dist/tools/authorizationTools.js +8 -4
- package/dist/tools/functionTools.js +7 -3
- package/dist/tools/qualityGateTools.js +7 -6
- package/dist/tools/queryEfficiencyTools.js +6 -3
- package/dist/tools/reportingTools.js +1 -1
- package/dist/tools/storageAuditTools.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -72,7 +72,10 @@ function auditActions(convexDir) {
|
|
|
72
72
|
}
|
|
73
73
|
const body = lines.slice(startLine, endLine).join("\n");
|
|
74
74
|
// Check 1: ctx.db access in action (FATAL — not allowed)
|
|
75
|
-
if
|
|
75
|
+
// BUT: skip if ctx.db is inside an inline ctx.runMutation/ctx.runQuery callback
|
|
76
|
+
// e.g. ctx.runMutation(async (ctx) => { ctx.db.patch(...) }) — the inner ctx is a mutation context
|
|
77
|
+
const hasInlineCallback = /ctx\.run(Mutation|Query)\s*\(\s*async\s*\(/.test(body);
|
|
78
|
+
if (/ctx\.db\.(get|query|insert|patch|replace|delete)\s*\(/.test(body) && !hasInlineCallback) {
|
|
76
79
|
actionsWithDbAccess++;
|
|
77
80
|
issues.push({
|
|
78
81
|
severity: "critical",
|
|
@@ -81,11 +81,13 @@ function auditAuthorization(convexDir) {
|
|
|
81
81
|
const identityAssign = body.match(/(?:const|let)\s+(\w+)\s*=\s*await\s+ctx\.auth\.getUserIdentity\s*\(\s*\)/);
|
|
82
82
|
if (identityAssign) {
|
|
83
83
|
const varName = identityAssign[1];
|
|
84
|
-
|
|
84
|
+
// Recognize: if (!var), if (var === null), if (var), var &&, var ?, throw on !var, comparisons
|
|
85
|
+
const hasNullCheck = new RegExp(`if\\s*\\(\\s*!${varName}\\b|if\\s*\\(\\s*${varName}\\s*===?\\s*null|if\\s*\\(\\s*${varName}\\s*\\)|${varName}\\s*&&|${varName}\\s*\\?|throw.*!${varName}|${varName}\\s*!==?\\s*null|===\\s*${varName}|!==\\s*${varName}`).test(body);
|
|
85
86
|
if (!hasNullCheck) {
|
|
86
87
|
uncheckedIdentity++;
|
|
88
|
+
// Queries can intentionally return different data for auth/unauth — warning not critical
|
|
87
89
|
issues.push({
|
|
88
|
-
severity: "critical",
|
|
90
|
+
severity: ft === "query" ? "warning" : "critical",
|
|
89
91
|
location: `${relativePath}:${i + 1}`,
|
|
90
92
|
functionName: funcName,
|
|
91
93
|
message: `${ft} "${funcName}" calls getUserIdentity() but doesn't check for null. Unauthenticated users will get undefined identity.`,
|
|
@@ -97,11 +99,13 @@ function auditAuthorization(convexDir) {
|
|
|
97
99
|
const authUserAssign = body.match(/(?:const|let)\s+(\w+)\s*=\s*await\s+getAuthUserId\s*\(/);
|
|
98
100
|
if (authUserAssign) {
|
|
99
101
|
const varName = authUserAssign[1];
|
|
100
|
-
|
|
102
|
+
// Recognize: if (!var), if (var === null), if (var), var &&, var ?, throw on !var, comparisons
|
|
103
|
+
const hasNullCheck = new RegExp(`if\\s*\\(\\s*!${varName}\\b|if\\s*\\(\\s*${varName}\\s*===?\\s*null|if\\s*\\(\\s*${varName}\\s*\\)|${varName}\\s*&&|${varName}\\s*\\?|throw.*!${varName}|${varName}\\s*!==?\\s*null|===\\s*${varName}|!==\\s*${varName}`).test(body);
|
|
101
104
|
if (!hasNullCheck) {
|
|
102
105
|
uncheckedIdentity++;
|
|
106
|
+
// Queries can intentionally return different data for auth/unauth — warning not critical
|
|
103
107
|
issues.push({
|
|
104
|
-
severity: "critical",
|
|
108
|
+
severity: ft === "query" ? "warning" : "critical",
|
|
105
109
|
location: `${relativePath}:${i + 1}`,
|
|
106
110
|
functionName: funcName,
|
|
107
111
|
message: `${ft} "${funcName}" calls getAuthUserId() but doesn't check for null. Unauthenticated users will get null userId.`,
|
|
@@ -76,8 +76,11 @@ function auditFunctions(convexDir) {
|
|
|
76
76
|
if (fn.type === "httpAction")
|
|
77
77
|
continue; // httpActions don't have args/returns validators
|
|
78
78
|
if (!fn.hasArgs) {
|
|
79
|
+
// Public mutations/actions without args validators are a security concern (unvalidated client input)
|
|
80
|
+
// Queries and internal functions: just a best-practice recommendation
|
|
81
|
+
const isSecurity = !fn.isInternal && (fn.type === "mutation" || fn.type === "action");
|
|
79
82
|
issues.push({
|
|
80
|
-
severity: "critical",
|
|
83
|
+
severity: isSecurity ? "critical" : "warning",
|
|
81
84
|
location: `${fn.relativePath}:${fn.line}`,
|
|
82
85
|
functionName: fn.name,
|
|
83
86
|
message: `${fn.type} "${fn.name}" is missing args validator`,
|
|
@@ -94,8 +97,9 @@ function auditFunctions(convexDir) {
|
|
|
94
97
|
});
|
|
95
98
|
}
|
|
96
99
|
if (!fn.hasHandler) {
|
|
100
|
+
// Old shorthand syntax (query(async (ctx) => {})) works fine — just a style recommendation
|
|
97
101
|
issues.push({
|
|
98
|
-
severity: "
|
|
102
|
+
severity: "warning",
|
|
99
103
|
location: `${fn.relativePath}:${fn.line}`,
|
|
100
104
|
functionName: fn.name,
|
|
101
105
|
message: `${fn.type} "${fn.name}" is missing handler property (may be using old syntax)`,
|
|
@@ -284,7 +288,7 @@ export const functionTools = [
|
|
|
284
288
|
const functions = extractFunctions(convexDir);
|
|
285
289
|
// Store audit result
|
|
286
290
|
const db = getDb();
|
|
287
|
-
db.prepare("INSERT INTO audit_results (id, project_dir, audit_type, issues_json, issue_count) VALUES (?, ?, ?, ?, ?)").run(genId("audit"), projectDir, "
|
|
291
|
+
db.prepare("INSERT INTO audit_results (id, project_dir, audit_type, issues_json, issue_count) VALUES (?, ?, ?, ?, ?)").run(genId("audit"), projectDir, "functions", JSON.stringify(issues), issues.length);
|
|
288
292
|
const critical = issues.filter((i) => i.severity === "critical");
|
|
289
293
|
const warnings = issues.filter((i) => i.severity === "warning");
|
|
290
294
|
// Aggregate issues by category for cleaner output
|
|
@@ -77,17 +77,18 @@ function runQualityGate(projectDir, thresholds) {
|
|
|
77
77
|
}
|
|
78
78
|
catch { /* skip */ }
|
|
79
79
|
}
|
|
80
|
-
// Check 4: Type safety (as any casts)
|
|
80
|
+
// Check 4: Type safety (as any casts) — exclude test/eval files
|
|
81
81
|
const typeSafety = getLatest("type_safety");
|
|
82
82
|
if (typeSafety) {
|
|
83
83
|
try {
|
|
84
84
|
const issues = JSON.parse(typeSafety.issues_json);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
: 0;
|
|
85
|
+
// Exclude test, eval, benchmark, and fixture files from production quality gate
|
|
86
|
+
const testEvalPattern = /test|__tests__|spec|\.test\.|\.spec\.|fixtures|mocks|evaluation|liveEval|liveApiSmoke|benchmark|calibration|harness|quickTest|comprehensiveTest|comprehensiveEval/i;
|
|
88
87
|
// Each as-any issue represents a FILE, count from message for actual number
|
|
89
88
|
const actualCasts = Array.isArray(issues)
|
|
90
89
|
? issues.reduce((sum, i) => {
|
|
90
|
+
if (testEvalPattern.test(i.location ?? ""))
|
|
91
|
+
return sum; // skip test/eval files
|
|
91
92
|
const countMatch = i.message?.match(/(\d+)\s+`as any`/);
|
|
92
93
|
return sum + (countMatch ? parseInt(countMatch[1], 10) : 0);
|
|
93
94
|
}, 0)
|
|
@@ -102,13 +103,13 @@ function runQualityGate(projectDir, thresholds) {
|
|
|
102
103
|
}
|
|
103
104
|
catch { /* skip */ }
|
|
104
105
|
}
|
|
105
|
-
// Check 5: Unbounded collects
|
|
106
|
+
// Check 5: Unbounded collects (only warning-level — indexed collects are downgraded to info)
|
|
106
107
|
const queryEfficiency = getLatest("query_efficiency");
|
|
107
108
|
if (queryEfficiency) {
|
|
108
109
|
try {
|
|
109
110
|
const issues = JSON.parse(queryEfficiency.issues_json);
|
|
110
111
|
const unbounded = Array.isArray(issues)
|
|
111
|
-
? issues.filter((i) => i.message?.includes(".collect()")).length
|
|
112
|
+
? issues.filter((i) => i.severity === "warning" && i.message?.includes(".collect()")).length
|
|
112
113
|
: 0;
|
|
113
114
|
checks.push({
|
|
114
115
|
metric: "unbounded_collects",
|
|
@@ -63,13 +63,16 @@ function auditQueryEfficiency(convexDir) {
|
|
|
63
63
|
const chain = lines.slice(chainStart, i + 1).join("\n");
|
|
64
64
|
if (!/\.take\s*\(/.test(chain) && !/\.paginate\s*\(/.test(chain)) {
|
|
65
65
|
collectWithoutLimit++;
|
|
66
|
-
// Check if it's a bounded table or could be large
|
|
67
66
|
const tableMatch = chain.match(/\.query\s*\(\s*["'](\w+)["']\s*\)/);
|
|
67
|
+
// Indexed queries are bounded by the index range — lower severity
|
|
68
|
+
const hasIndex = /\.withIndex\s*\(/.test(chain);
|
|
68
69
|
issues.push({
|
|
69
|
-
severity: "warning",
|
|
70
|
+
severity: hasIndex ? "info" : "warning",
|
|
70
71
|
location: `${relativePath}:${i + 1}`,
|
|
71
72
|
message: `.collect() without .take() limit${tableMatch ? ` on table "${tableMatch[1]}"` : ""}. Could return entire table.`,
|
|
72
|
-
fix:
|
|
73
|
+
fix: hasIndex
|
|
74
|
+
? "Consider adding .take(N) for predictable result sizes, even with index filtering"
|
|
75
|
+
: "Add .take(N) before .collect(), or use .paginate() for large result sets",
|
|
73
76
|
});
|
|
74
77
|
}
|
|
75
78
|
}
|
|
@@ -75,7 +75,7 @@ function buildSarif(projectDir, auditTypes, limit) {
|
|
|
75
75
|
tool: {
|
|
76
76
|
driver: {
|
|
77
77
|
name: "convex-mcp-nodebench",
|
|
78
|
-
version: "0.9.
|
|
78
|
+
version: "0.9.6",
|
|
79
79
|
informationUri: "https://www.npmjs.com/package/@homenshum/convex-mcp-nodebench",
|
|
80
80
|
rules: [...rulesMap.values()],
|
|
81
81
|
},
|
|
@@ -133,7 +133,7 @@ export const storageAuditTools = [
|
|
|
133
133
|
}
|
|
134
134
|
const { issues, stats } = auditStorageUsage(convexDir);
|
|
135
135
|
const db = getDb();
|
|
136
|
-
db.prepare("INSERT INTO audit_results (id, project_dir, audit_type, issues_json, issue_count) VALUES (?, ?, ?, ?, ?)").run(genId("audit"), projectDir, "
|
|
136
|
+
db.prepare("INSERT INTO audit_results (id, project_dir, audit_type, issues_json, issue_count) VALUES (?, ?, ?, ?, ?)").run(genId("audit"), projectDir, "storage", JSON.stringify(issues), issues.length);
|
|
137
137
|
return {
|
|
138
138
|
summary: {
|
|
139
139
|
...stats,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homenshum/convex-mcp-nodebench",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"description": "Convex-specific MCP server applying NodeBench self-instruct diligence patterns to Convex development. Schema audit, function compliance, deployment gates, persistent gotcha DB, and methodology guidance. Complements Context7 (raw docs) and official Convex MCP (deployment introspection) with structured verification workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|