@homenshum/convex-mcp-nodebench 0.9.2 → 0.9.4
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 +5 -2
- package/dist/tools/authorizationTools.js +23 -5
- package/dist/tools/dataModelingTools.js +40 -0
- package/dist/tools/paginationTools.js +5 -3
- package/dist/tools/reportingTools.js +1 -1
- package/dist/tools/typeSafetyTools.js +2 -2
- package/dist/tools/vectorSearchTools.js +16 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -36,7 +36,10 @@ function auditActions(convexDir) {
|
|
|
36
36
|
let actionsWithoutErrorHandling = 0;
|
|
37
37
|
let actionCallingAction = 0;
|
|
38
38
|
// Node APIs that require "use node" directive
|
|
39
|
-
|
|
39
|
+
// NOTE: process.env is polyfilled by Convex in both runtimes — NOT a Node-only API
|
|
40
|
+
// NOTE: crypto.subtle / crypto.getRandomValues are Web Crypto API — available without "use node"
|
|
41
|
+
const nodeApis = /\b(require\s*\(|__dirname|__filename|Buffer\.|fs\.|path\.|child_process|net\.|http\.|https\.)\b/;
|
|
42
|
+
const nodeCryptoApis = /\bcrypto\.(create|randomBytes|pbkdf2|scrypt|sign|verify|getCiphers|getDiffieHellman|getHashes|Hash|Hmac|Cipher|Decipher)\b/;
|
|
40
43
|
for (const filePath of files) {
|
|
41
44
|
const content = readFileSync(filePath, "utf-8");
|
|
42
45
|
const relativePath = filePath.replace(convexDir, "").replace(/^[\\/]/, "");
|
|
@@ -80,7 +83,7 @@ function auditActions(convexDir) {
|
|
|
80
83
|
});
|
|
81
84
|
}
|
|
82
85
|
// Check 2: Node API usage without "use node"
|
|
83
|
-
if (!hasUseNode && nodeApis.test(body)) {
|
|
86
|
+
if (!hasUseNode && (nodeApis.test(body) || nodeCryptoApis.test(body))) {
|
|
84
87
|
actionsWithoutNodeDirective++;
|
|
85
88
|
issues.push({
|
|
86
89
|
severity: "critical",
|
|
@@ -70,7 +70,9 @@ function auditAuthorization(convexDir) {
|
|
|
70
70
|
}
|
|
71
71
|
const body = lines.slice(i, endLine).join("\n");
|
|
72
72
|
const hasAuthCheck = /ctx\.auth\.getUserIdentity\s*\(\s*\)/.test(body) ||
|
|
73
|
-
/getUserIdentity/.test(body)
|
|
73
|
+
/getUserIdentity/.test(body) ||
|
|
74
|
+
/getAuthUserId/.test(body) ||
|
|
75
|
+
/getAuthSessionId/.test(body);
|
|
74
76
|
const hasDbWrite = dbWriteOps.test(body);
|
|
75
77
|
const isSensitiveName = writeSensitive.test(funcName);
|
|
76
78
|
if (hasAuthCheck) {
|
|
@@ -79,7 +81,6 @@ function auditAuthorization(convexDir) {
|
|
|
79
81
|
const identityAssign = body.match(/(?:const|let)\s+(\w+)\s*=\s*await\s+ctx\.auth\.getUserIdentity\s*\(\s*\)/);
|
|
80
82
|
if (identityAssign) {
|
|
81
83
|
const varName = identityAssign[1];
|
|
82
|
-
// Look for null check: if (!var), if (var === null), if (var == null), if (!var)
|
|
83
84
|
const hasNullCheck = new RegExp(`if\\s*\\(\\s*!${varName}\\b|if\\s*\\(\\s*${varName}\\s*===?\\s*null|if\\s*\\(\\s*!${varName}\\s*\\)`).test(body);
|
|
84
85
|
if (!hasNullCheck) {
|
|
85
86
|
uncheckedIdentity++;
|
|
@@ -92,21 +93,38 @@ function auditAuthorization(convexDir) {
|
|
|
92
93
|
});
|
|
93
94
|
}
|
|
94
95
|
}
|
|
96
|
+
// Check: getAuthUserId called but return not null-checked
|
|
97
|
+
const authUserAssign = body.match(/(?:const|let)\s+(\w+)\s*=\s*await\s+getAuthUserId\s*\(/);
|
|
98
|
+
if (authUserAssign) {
|
|
99
|
+
const varName = authUserAssign[1];
|
|
100
|
+
const hasNullCheck = new RegExp(`if\\s*\\(\\s*!${varName}\\b|if\\s*\\(\\s*${varName}\\s*===?\\s*null|throw.*!${varName}`).test(body);
|
|
101
|
+
if (!hasNullCheck) {
|
|
102
|
+
uncheckedIdentity++;
|
|
103
|
+
issues.push({
|
|
104
|
+
severity: "critical",
|
|
105
|
+
location: `${relativePath}:${i + 1}`,
|
|
106
|
+
functionName: funcName,
|
|
107
|
+
message: `${ft} "${funcName}" calls getAuthUserId() but doesn't check for null. Unauthenticated users will get null userId.`,
|
|
108
|
+
fix: `Add: if (!${varName}) { throw new Error("Not authenticated"); }`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
95
112
|
}
|
|
96
113
|
else {
|
|
97
114
|
withoutAuth++;
|
|
98
115
|
// Critical: public mutation/action with DB writes but no auth
|
|
99
116
|
if ((ft === "mutation" || ft === "action") && hasDbWrite) {
|
|
117
|
+
const sensitiveHint = isSensitiveName ? ` Name "${funcName}" suggests a destructive operation.` : "";
|
|
100
118
|
issues.push({
|
|
101
119
|
severity: "critical",
|
|
102
120
|
location: `${relativePath}:${i + 1}`,
|
|
103
121
|
functionName: funcName,
|
|
104
|
-
message: `Public ${ft} "${funcName}" writes to DB without auth check. Any client can call this
|
|
122
|
+
message: `Public ${ft} "${funcName}" writes to DB without auth check. Any client can call this.${sensitiveHint}`,
|
|
105
123
|
fix: `Add: const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated");`,
|
|
106
124
|
});
|
|
107
125
|
}
|
|
108
|
-
|
|
109
|
-
|
|
126
|
+
else if (isSensitiveName) {
|
|
127
|
+
// Only flag sensitive name separately if not already caught by DB-write check
|
|
110
128
|
issues.push({
|
|
111
129
|
severity: "critical",
|
|
112
130
|
location: `${relativePath}:${i + 1}`,
|
|
@@ -35,6 +35,46 @@ function auditDataModeling(convexDir) {
|
|
|
35
35
|
tableNames.add(m[1]);
|
|
36
36
|
totalTables++;
|
|
37
37
|
}
|
|
38
|
+
// Detect spread-imported table providers (e.g. ...authTables adds "users", "sessions")
|
|
39
|
+
// Strategy 1: Known spreads from popular Convex packages
|
|
40
|
+
const knownSpreads = {
|
|
41
|
+
authTables: ["users", "authSessions", "authAccounts", "authRefreshTokens", "authVerificationCodes", "authRateLimits", "authVerifiers"],
|
|
42
|
+
};
|
|
43
|
+
// Strategy 2: Parse inline comments next to spreads for table name hints
|
|
44
|
+
// e.g. ...authTables, // `users`, `sessions`
|
|
45
|
+
const spreadCommentPattern = /\.\.\.(\w+)\s*,?\s*\/\/\s*(.+)/g;
|
|
46
|
+
let sm;
|
|
47
|
+
while ((sm = spreadCommentPattern.exec(content)) !== null) {
|
|
48
|
+
const spreadName = sm[1];
|
|
49
|
+
const comment = sm[2];
|
|
50
|
+
// Extract backtick-quoted or quoted table names from comment
|
|
51
|
+
const commentTables = [...comment.matchAll(/[`"'](\w+)[`"']/g)].map(m => m[1]);
|
|
52
|
+
// Merge known + comment-discovered tables
|
|
53
|
+
const tables = new Set([
|
|
54
|
+
...(knownSpreads[spreadName] ?? []),
|
|
55
|
+
...commentTables,
|
|
56
|
+
]);
|
|
57
|
+
for (const t of tables) {
|
|
58
|
+
if (!tableNames.has(t)) {
|
|
59
|
+
tableNames.add(t);
|
|
60
|
+
totalTables++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Strategy 3: If no comment matched, still apply known spreads
|
|
65
|
+
const simpleSpreadPattern = /\.\.\.(\w+)/g;
|
|
66
|
+
let sm2;
|
|
67
|
+
while ((sm2 = simpleSpreadPattern.exec(content)) !== null) {
|
|
68
|
+
const tables = knownSpreads[sm2[1]];
|
|
69
|
+
if (tables) {
|
|
70
|
+
for (const t of tables) {
|
|
71
|
+
if (!tableNames.has(t)) {
|
|
72
|
+
tableNames.add(t);
|
|
73
|
+
totalTables++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
38
78
|
// Per-table analysis
|
|
39
79
|
let currentTable = "";
|
|
40
80
|
let tableStartLine = 0;
|
|
@@ -45,16 +45,18 @@ function auditPagination(convexDir) {
|
|
|
45
45
|
functionsUsingPagination.add(relativePath);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
// Check:
|
|
48
|
+
// Check: public queries using paginate but missing paginationOptsValidator in args
|
|
49
|
+
// NOTE: internalQuery doesn't need paginationOptsValidator — it's not client-callable
|
|
49
50
|
const funcPattern = /export\s+(?:const\s+(\w+)\s*=|default)\s+(query|internalQuery)\s*\(/g;
|
|
50
51
|
let m;
|
|
51
52
|
while ((m = funcPattern.exec(content)) !== null) {
|
|
52
53
|
const funcName = m[1] || "default";
|
|
54
|
+
const funcType = m[2];
|
|
53
55
|
const startLine = content.slice(0, m.index).split("\n").length - 1;
|
|
54
56
|
const chunk = lines.slice(startLine, Math.min(startLine + 30, lines.length)).join("\n");
|
|
55
57
|
if (/\.paginate\s*\(/.test(chunk)) {
|
|
56
|
-
//
|
|
57
|
-
if (!/paginationOptsValidator/.test(chunk) && !/paginationOpts/.test(chunk)) {
|
|
58
|
+
// Only flag public queries — internalQuery can use paginate with hardcoded opts
|
|
59
|
+
if (funcType === "query" && !/paginationOptsValidator/.test(chunk) && !/paginationOpts/.test(chunk)) {
|
|
58
60
|
missingPaginationOptsValidator++;
|
|
59
61
|
issues.push({
|
|
60
62
|
severity: "critical",
|
|
@@ -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.4",
|
|
79
79
|
informationUri: "https://www.npmjs.com/package/@homenshum/convex-mcp-nodebench",
|
|
80
80
|
rules: [...rulesMap.values()],
|
|
81
81
|
},
|
|
@@ -18,10 +18,10 @@ function collectTsFiles(dir) {
|
|
|
18
18
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
19
19
|
for (const entry of entries) {
|
|
20
20
|
const full = join(dir, entry.name);
|
|
21
|
-
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "_generated") {
|
|
21
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "_generated" && !entry.name.startsWith("_type")) {
|
|
22
22
|
results.push(...collectTsFiles(full));
|
|
23
23
|
}
|
|
24
|
-
else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
24
|
+
else if (entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) {
|
|
25
25
|
results.push(full);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -44,7 +44,13 @@ function auditVectorSearch(convexDir) {
|
|
|
44
44
|
if (existsSync(schemaPath)) {
|
|
45
45
|
const schema = readFileSync(schemaPath, "utf-8");
|
|
46
46
|
const lines = schema.split("\n");
|
|
47
|
+
// Track current table context: scan for "tableName: defineTable(" or "tableName = defineTable("
|
|
48
|
+
let currentTable = "";
|
|
47
49
|
for (let i = 0; i < lines.length; i++) {
|
|
50
|
+
const tableMatch = lines[i].match(/(\w+)\s*[:=]\s*defineTable\s*\(/);
|
|
51
|
+
if (tableMatch) {
|
|
52
|
+
currentTable = tableMatch[1];
|
|
53
|
+
}
|
|
48
54
|
// Match .vectorIndex("name", { ... })
|
|
49
55
|
const viMatch = lines[i].match(/\.vectorIndex\s*\(\s*["']([^"']+)["']/);
|
|
50
56
|
if (viMatch) {
|
|
@@ -56,7 +62,7 @@ function auditVectorSearch(convexDir) {
|
|
|
56
62
|
const filters = filterMatch
|
|
57
63
|
? filterMatch[1].match(/["']([^"']+)["']/g)?.map(s => s.replace(/["']/g, "")) ?? []
|
|
58
64
|
: [];
|
|
59
|
-
vectorIndexes.push({
|
|
65
|
+
vectorIndexes.push({ tableName: currentTable || viMatch[1], indexName: viMatch[1], dimensions: dims, filterFields: filters, line: i + 1 });
|
|
60
66
|
// Check: uncommon dimension size
|
|
61
67
|
if (dims > 0 && !KNOWN_DIMENSIONS[dims]) {
|
|
62
68
|
const nearest = Object.keys(KNOWN_DIMENSIONS)
|
|
@@ -99,18 +105,19 @@ function auditVectorSearch(convexDir) {
|
|
|
99
105
|
const relativePath = filePath.replace(convexDir, "").replace(/^[\\/]/, "");
|
|
100
106
|
const lines = content.split("\n");
|
|
101
107
|
for (let i = 0; i < lines.length; i++) {
|
|
108
|
+
// vectorSearch("tableName", "indexName", options) — first arg is TABLE name
|
|
102
109
|
const vsMatch = lines[i].match(/\.vectorSearch\s*\(\s*["']([^"']+)["']/);
|
|
103
110
|
if (vsMatch) {
|
|
104
111
|
vectorSearchCallCount++;
|
|
105
|
-
const
|
|
106
|
-
// Check if the referenced index
|
|
107
|
-
const matchingIdx = vectorIndexes.find(vi => vi.
|
|
112
|
+
const tableName = vsMatch[1];
|
|
113
|
+
// Check if the referenced table has any vector index
|
|
114
|
+
const matchingIdx = vectorIndexes.find(vi => vi.tableName === tableName);
|
|
108
115
|
if (!matchingIdx && vectorIndexes.length > 0) {
|
|
109
116
|
issues.push({
|
|
110
117
|
severity: "critical",
|
|
111
118
|
location: `${relativePath}:${i + 1}`,
|
|
112
|
-
message: `vectorSearch references
|
|
113
|
-
fix: `Add a .vectorIndex("
|
|
119
|
+
message: `vectorSearch references table "${tableName}" which has no vectorIndex defined in schema.ts.`,
|
|
120
|
+
fix: `Add a .vectorIndex("by_embedding", { ... }) to the "${tableName}" table in schema.ts`,
|
|
114
121
|
});
|
|
115
122
|
}
|
|
116
123
|
// Check: no filter parameter when filterFields exist
|
|
@@ -119,7 +126,7 @@ function auditVectorSearch(convexDir) {
|
|
|
119
126
|
issues.push({
|
|
120
127
|
severity: "info",
|
|
121
128
|
location: `${relativePath}:${i + 1}`,
|
|
122
|
-
message: `vectorSearch on "${
|
|
129
|
+
message: `vectorSearch on "${tableName}" doesn't use filter — available filterFields: ${matchingIdx.filterFields.join(", ")}`,
|
|
123
130
|
fix: "Add filter parameter to narrow results and improve performance",
|
|
124
131
|
});
|
|
125
132
|
}
|
|
@@ -131,7 +138,7 @@ function auditVectorSearch(convexDir) {
|
|
|
131
138
|
issues.push({
|
|
132
139
|
severity: "critical",
|
|
133
140
|
location: `${relativePath}:${i + 1}`,
|
|
134
|
-
message: `Vector dimensions mismatch: code uses ${codeDims} but schema defines ${matchingIdx.dimensions} for
|
|
141
|
+
message: `Vector dimensions mismatch: code uses ${codeDims} but schema defines ${matchingIdx.dimensions} for table "${tableName}".`,
|
|
135
142
|
fix: `Ensure embedding model output (${codeDims}) matches schema dimensions (${matchingIdx.dimensions})`,
|
|
136
143
|
});
|
|
137
144
|
}
|
|
@@ -139,7 +146,7 @@ function auditVectorSearch(convexDir) {
|
|
|
139
146
|
}
|
|
140
147
|
}
|
|
141
148
|
}
|
|
142
|
-
const tablesWithVectors = [...new Set(vectorIndexes.map(vi => vi.
|
|
149
|
+
const tablesWithVectors = [...new Set(vectorIndexes.map(vi => vi.tableName))];
|
|
143
150
|
const dimensions = [...new Set(vectorIndexes.map(vi => vi.dimensions).filter(d => d > 0))];
|
|
144
151
|
return {
|
|
145
152
|
issues,
|
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.4",
|
|
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": {
|