@homenshum/convex-mcp-nodebench 0.9.5 → 0.9.7

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.5",
82
+ version: "0.9.7",
83
83
  }, {
84
84
  capabilities: {
85
85
  tools: {},
@@ -41,6 +41,27 @@ function auditAuthorization(convexDir) {
41
41
  const content = readFileSync(filePath, "utf-8");
42
42
  const relativePath = filePath.replace(convexDir, "").replace(/^[\\/]/, "");
43
43
  const lines = content.split("\n");
44
+ // Pre-scan: detect local helper functions that wrap getAuthUserId/getUserIdentity
45
+ // Pattern: function getSafeUserId(ctx) { ... getAuthUserId(ctx) ... }
46
+ const authHelperNames = [];
47
+ const helperFuncPattern = /(?:async\s+)?function\s+(\w+)\s*\(/g;
48
+ let hm;
49
+ while ((hm = helperFuncPattern.exec(content)) !== null) {
50
+ const hStart = content.slice(0, hm.index).split("\n").length - 1;
51
+ const hBody = lines.slice(hStart, Math.min(hStart + 30, lines.length)).join("\n");
52
+ if (/getAuthUserId|getUserIdentity|getAuthSessionId/.test(hBody)) {
53
+ authHelperNames.push(hm[1]);
54
+ }
55
+ }
56
+ // Also check arrow function helpers: const getUserId = async (ctx) => { ... }
57
+ const arrowHelperPattern = /(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*(?::\s*[^=]+)?\s*=>/g;
58
+ while ((hm = arrowHelperPattern.exec(content)) !== null) {
59
+ const hStart = content.slice(0, hm.index).split("\n").length - 1;
60
+ const hBody = lines.slice(hStart, Math.min(hStart + 20, lines.length)).join("\n");
61
+ if (/getAuthUserId|getUserIdentity|getAuthSessionId/.test(hBody)) {
62
+ authHelperNames.push(hm[1]);
63
+ }
64
+ }
44
65
  for (let i = 0; i < lines.length; i++) {
45
66
  const line = lines[i];
46
67
  for (const ft of funcTypes) {
@@ -69,10 +90,14 @@ function auditAuthorization(convexDir) {
69
90
  }
70
91
  }
71
92
  const body = lines.slice(i, endLine).join("\n");
72
- const hasAuthCheck = /ctx\.auth\.getUserIdentity\s*\(\s*\)/.test(body) ||
93
+ // Check for direct auth calls OR calls to local auth helper wrappers
94
+ const hasDirectAuth = /ctx\.auth\.getUserIdentity\s*\(\s*\)/.test(body) ||
73
95
  /getUserIdentity/.test(body) ||
74
96
  /getAuthUserId/.test(body) ||
75
97
  /getAuthSessionId/.test(body);
98
+ const callsAuthHelper = authHelperNames.length > 0 &&
99
+ authHelperNames.some(h => new RegExp(`\\b${h}\\s*\\(`).test(body));
100
+ const hasAuthCheck = hasDirectAuth || callsAuthHelper;
76
101
  const hasDbWrite = dbWriteOps.test(body);
77
102
  const isSensitiveName = writeSensitive.test(funcName);
78
103
  if (hasAuthCheck) {
@@ -81,12 +106,13 @@ function auditAuthorization(convexDir) {
81
106
  const identityAssign = body.match(/(?:const|let)\s+(\w+)\s*=\s*await\s+ctx\.auth\.getUserIdentity\s*\(\s*\)/);
82
107
  if (identityAssign) {
83
108
  const varName = identityAssign[1];
84
- // Recognize: if (!var), if (var === null), if (var), var &&, var ?, throw on !var
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`).test(body);
109
+ // Recognize: if (!var), if (var === null), if (var), var &&, var ?, throw on !var, comparisons
110
+ 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);
86
111
  if (!hasNullCheck) {
87
112
  uncheckedIdentity++;
113
+ // Queries can intentionally return different data for auth/unauth — warning not critical
88
114
  issues.push({
89
- severity: "critical",
115
+ severity: ft === "query" ? "warning" : "critical",
90
116
  location: `${relativePath}:${i + 1}`,
91
117
  functionName: funcName,
92
118
  message: `${ft} "${funcName}" calls getUserIdentity() but doesn't check for null. Unauthenticated users will get undefined identity.`,
@@ -98,12 +124,13 @@ function auditAuthorization(convexDir) {
98
124
  const authUserAssign = body.match(/(?:const|let)\s+(\w+)\s*=\s*await\s+getAuthUserId\s*\(/);
99
125
  if (authUserAssign) {
100
126
  const varName = authUserAssign[1];
101
- // Recognize: if (!var), if (var === null), if (var), var &&, var ?, throw on !var
102
- 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`).test(body);
127
+ // Recognize: if (!var), if (var === null), if (var), var &&, var ?, throw on !var, comparisons
128
+ 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);
103
129
  if (!hasNullCheck) {
104
130
  uncheckedIdentity++;
131
+ // Queries can intentionally return different data for auth/unauth — warning not critical
105
132
  issues.push({
106
- severity: "critical",
133
+ severity: ft === "query" ? "warning" : "critical",
107
134
  location: `${relativePath}:${i + 1}`,
108
135
  functionName: funcName,
109
136
  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: "critical",
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, "function_audit", JSON.stringify(issues), issues.length);
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
@@ -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.5",
78
+ version: "0.9.7",
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, "storage_usage", JSON.stringify(issues), issues.length);
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.5",
3
+ "version": "0.9.7",
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": {