@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 CHANGED
@@ -79,7 +79,7 @@ for (const tool of ALL_TOOLS) {
79
79
  // ── Server setup ────────────────────────────────────────────────────
80
80
  const server = new Server({
81
81
  name: "convex-mcp-nodebench",
82
- version: "0.9.2",
82
+ version: "0.9.4",
83
83
  }, {
84
84
  capabilities: {
85
85
  tools: {},
@@ -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
- const nodeApis = /\b(require|__dirname|__filename|Buffer\.|process\.env|fs\.|path\.|crypto\.|child_process|net\.|http\.|https\.)\b/;
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
- // Critical: sensitive-named function without auth
109
- if (isSensitiveName) {
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: functions using paginate but missing paginationOptsValidator in args
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
- // Check for paginationOptsValidator
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.2",
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({ table: viMatch[1], dimensions: dims, filterFields: filters, line: i + 1 });
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 indexName = vsMatch[1];
106
- // Check if the referenced index exists
107
- const matchingIdx = vectorIndexes.find(vi => vi.table === indexName);
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 index "${indexName}" which is not defined in schema.ts.`,
113
- fix: `Add a .vectorIndex("${indexName}", { ... }) to the appropriate table in schema.ts`,
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 "${indexName}" doesn't use filter — available filterFields: ${matchingIdx.filterFields.join(", ")}`,
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 index "${indexName}".`,
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.table))];
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.2",
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": {