@homenshum/convex-mcp-nodebench 0.4.0 → 0.7.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.
- package/dist/index.js +52 -2
- package/dist/tools/actionAuditTools.d.ts +2 -0
- package/dist/tools/actionAuditTools.js +180 -0
- package/dist/tools/authorizationTools.d.ts +2 -0
- package/dist/tools/authorizationTools.js +201 -0
- package/dist/tools/critterTools.js +76 -5
- package/dist/tools/dataModelingTools.d.ts +2 -0
- package/dist/tools/dataModelingTools.js +168 -0
- package/dist/tools/deploymentTools.js +42 -2
- package/dist/tools/devSetupTools.d.ts +2 -0
- package/dist/tools/devSetupTools.js +170 -0
- package/dist/tools/embeddingProvider.d.ts +6 -0
- package/dist/tools/embeddingProvider.js +3 -0
- package/dist/tools/functionTools.js +24 -1
- package/dist/tools/httpTools.js +128 -48
- package/dist/tools/integrationBridgeTools.js +4 -0
- package/dist/tools/methodologyTools.js +8 -1
- package/dist/tools/migrationTools.d.ts +2 -0
- package/dist/tools/migrationTools.js +133 -0
- package/dist/tools/paginationTools.d.ts +2 -0
- package/dist/tools/paginationTools.js +125 -0
- package/dist/tools/queryEfficiencyTools.d.ts +2 -0
- package/dist/tools/queryEfficiencyTools.js +191 -0
- package/dist/tools/schemaTools.js +95 -1
- package/dist/tools/storageAuditTools.d.ts +2 -0
- package/dist/tools/storageAuditTools.js +148 -0
- package/dist/tools/toolRegistry.d.ts +9 -2
- package/dist/tools/toolRegistry.js +205 -16
- package/dist/tools/transactionSafetyTools.d.ts +2 -0
- package/dist/tools/transactionSafetyTools.js +166 -0
- package/dist/tools/typeSafetyTools.d.ts +2 -0
- package/dist/tools/typeSafetyTools.js +146 -0
- package/dist/types.d.ts +6 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -26,6 +26,16 @@ import { cronTools } from "./tools/cronTools.js";
|
|
|
26
26
|
import { componentTools } from "./tools/componentTools.js";
|
|
27
27
|
import { httpTools } from "./tools/httpTools.js";
|
|
28
28
|
import { critterTools } from "./tools/critterTools.js";
|
|
29
|
+
import { authorizationTools } from "./tools/authorizationTools.js";
|
|
30
|
+
import { queryEfficiencyTools } from "./tools/queryEfficiencyTools.js";
|
|
31
|
+
import { actionAuditTools } from "./tools/actionAuditTools.js";
|
|
32
|
+
import { typeSafetyTools } from "./tools/typeSafetyTools.js";
|
|
33
|
+
import { transactionSafetyTools } from "./tools/transactionSafetyTools.js";
|
|
34
|
+
import { storageAuditTools } from "./tools/storageAuditTools.js";
|
|
35
|
+
import { paginationTools } from "./tools/paginationTools.js";
|
|
36
|
+
import { dataModelingTools } from "./tools/dataModelingTools.js";
|
|
37
|
+
import { devSetupTools } from "./tools/devSetupTools.js";
|
|
38
|
+
import { migrationTools } from "./tools/migrationTools.js";
|
|
29
39
|
import { CONVEX_GOTCHAS } from "./gotchaSeed.js";
|
|
30
40
|
import { REGISTRY } from "./tools/toolRegistry.js";
|
|
31
41
|
import { initEmbeddingIndex } from "./tools/embeddingProvider.js";
|
|
@@ -41,6 +51,16 @@ const ALL_TOOLS = [
|
|
|
41
51
|
...componentTools,
|
|
42
52
|
...httpTools,
|
|
43
53
|
...critterTools,
|
|
54
|
+
...authorizationTools,
|
|
55
|
+
...queryEfficiencyTools,
|
|
56
|
+
...actionAuditTools,
|
|
57
|
+
...typeSafetyTools,
|
|
58
|
+
...transactionSafetyTools,
|
|
59
|
+
...storageAuditTools,
|
|
60
|
+
...paginationTools,
|
|
61
|
+
...dataModelingTools,
|
|
62
|
+
...devSetupTools,
|
|
63
|
+
...migrationTools,
|
|
44
64
|
];
|
|
45
65
|
const toolMap = new Map();
|
|
46
66
|
for (const tool of ALL_TOOLS) {
|
|
@@ -59,10 +79,40 @@ const server = new Server({
|
|
|
59
79
|
getDb();
|
|
60
80
|
seedGotchasIfEmpty(CONVEX_GOTCHAS);
|
|
61
81
|
// ── Background: initialize embedding index for semantic search ───────
|
|
62
|
-
|
|
82
|
+
// Uses Agent-as-a-Graph bipartite corpus: tool nodes + domain nodes for graph-aware retrieval
|
|
83
|
+
const descMap = new Map(ALL_TOOLS.map((t) => [t.name, t.description]));
|
|
84
|
+
// Tool nodes: individual tools with full metadata text
|
|
85
|
+
const toolCorpus = REGISTRY.map((entry) => ({
|
|
63
86
|
name: entry.name,
|
|
64
|
-
text: `${entry.name} ${entry.tags.join(" ")} ${entry.category} ${entry.phase}`,
|
|
87
|
+
text: `${entry.name} ${entry.tags.join(" ")} ${entry.category} ${entry.phase} ${descMap.get(entry.name) ?? ""}`,
|
|
88
|
+
nodeType: "tool",
|
|
65
89
|
}));
|
|
90
|
+
// Domain nodes: aggregate category descriptions for upward traversal
|
|
91
|
+
// When a domain matches, all tools in that domain get a sibling boost
|
|
92
|
+
const categoryTools = new Map();
|
|
93
|
+
for (const entry of REGISTRY) {
|
|
94
|
+
const list = categoryTools.get(entry.category) ?? [];
|
|
95
|
+
list.push(entry.name);
|
|
96
|
+
categoryTools.set(entry.category, list);
|
|
97
|
+
}
|
|
98
|
+
const domainCorpus = [...categoryTools.entries()].map(([category, toolNames]) => {
|
|
99
|
+
const allTags = new Set();
|
|
100
|
+
const descs = [];
|
|
101
|
+
for (const tn of toolNames) {
|
|
102
|
+
const e = REGISTRY.find((r) => r.name === tn);
|
|
103
|
+
if (e)
|
|
104
|
+
e.tags.forEach((t) => allTags.add(t));
|
|
105
|
+
const d = descMap.get(tn);
|
|
106
|
+
if (d)
|
|
107
|
+
descs.push(d);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
name: `domain:${category}`,
|
|
111
|
+
text: `${category} domain: ${toolNames.join(" ")} ${[...allTags].join(" ")} ${descs.map(d => d.slice(0, 80)).join(" ")}`,
|
|
112
|
+
nodeType: "domain",
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
const embeddingCorpus = [...toolCorpus, ...domainCorpus];
|
|
66
116
|
initEmbeddingIndex(embeddingCorpus).catch(() => {
|
|
67
117
|
/* Embedding init failed — semantic search stays disabled */
|
|
68
118
|
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { getDb, genId } from "../db.js";
|
|
4
|
+
import { getQuickRef } from "./toolRegistry.js";
|
|
5
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
6
|
+
function findConvexDir(projectDir) {
|
|
7
|
+
const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex")];
|
|
8
|
+
for (const c of candidates) {
|
|
9
|
+
if (existsSync(c))
|
|
10
|
+
return c;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
function collectTsFiles(dir) {
|
|
15
|
+
const results = [];
|
|
16
|
+
if (!existsSync(dir))
|
|
17
|
+
return results;
|
|
18
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const full = join(dir, entry.name);
|
|
21
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "_generated") {
|
|
22
|
+
results.push(...collectTsFiles(full));
|
|
23
|
+
}
|
|
24
|
+
else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
25
|
+
results.push(full);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
30
|
+
function auditActions(convexDir) {
|
|
31
|
+
const files = collectTsFiles(convexDir);
|
|
32
|
+
const issues = [];
|
|
33
|
+
let totalActions = 0;
|
|
34
|
+
let actionsWithDbAccess = 0;
|
|
35
|
+
let actionsWithoutNodeDirective = 0;
|
|
36
|
+
let actionsWithoutErrorHandling = 0;
|
|
37
|
+
let actionCallingAction = 0;
|
|
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/;
|
|
40
|
+
for (const filePath of files) {
|
|
41
|
+
const content = readFileSync(filePath, "utf-8");
|
|
42
|
+
const relativePath = filePath.replace(convexDir, "").replace(/^[\\/]/, "");
|
|
43
|
+
const lines = content.split("\n");
|
|
44
|
+
const hasUseNode = /["']use node["']/.test(content);
|
|
45
|
+
const actionPattern = /export\s+(?:const\s+(\w+)\s*=|default)\s+(action|internalAction)\s*\(/g;
|
|
46
|
+
let m;
|
|
47
|
+
while ((m = actionPattern.exec(content)) !== null) {
|
|
48
|
+
const funcName = m[1] || "default";
|
|
49
|
+
const funcType = m[2];
|
|
50
|
+
totalActions++;
|
|
51
|
+
const startLine = content.slice(0, m.index).split("\n").length - 1;
|
|
52
|
+
// Extract body using brace tracking
|
|
53
|
+
let depth = 0;
|
|
54
|
+
let foundOpen = false;
|
|
55
|
+
let endLine = Math.min(startLine + 100, lines.length);
|
|
56
|
+
for (let j = startLine; j < lines.length; j++) {
|
|
57
|
+
for (const ch of lines[j]) {
|
|
58
|
+
if (ch === "{") {
|
|
59
|
+
depth++;
|
|
60
|
+
foundOpen = true;
|
|
61
|
+
}
|
|
62
|
+
if (ch === "}")
|
|
63
|
+
depth--;
|
|
64
|
+
}
|
|
65
|
+
if (foundOpen && depth <= 0) {
|
|
66
|
+
endLine = j + 1;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const body = lines.slice(startLine, endLine).join("\n");
|
|
71
|
+
// Check 1: ctx.db access in action (FATAL — not allowed)
|
|
72
|
+
if (/ctx\.db\.(get|query|insert|patch|replace|delete)\s*\(/.test(body)) {
|
|
73
|
+
actionsWithDbAccess++;
|
|
74
|
+
issues.push({
|
|
75
|
+
severity: "critical",
|
|
76
|
+
location: `${relativePath}:${startLine + 1}`,
|
|
77
|
+
functionName: funcName,
|
|
78
|
+
message: `${funcType} "${funcName}" accesses ctx.db directly. Actions cannot access the database — use ctx.runQuery/ctx.runMutation instead.`,
|
|
79
|
+
fix: "Move DB operations into a query or mutation, then call via ctx.runQuery(internal.file.func, args) or ctx.runMutation(...)",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// Check 2: Node API usage without "use node"
|
|
83
|
+
if (!hasUseNode && nodeApis.test(body)) {
|
|
84
|
+
actionsWithoutNodeDirective++;
|
|
85
|
+
issues.push({
|
|
86
|
+
severity: "critical",
|
|
87
|
+
location: `${relativePath}:${startLine + 1}`,
|
|
88
|
+
functionName: funcName,
|
|
89
|
+
message: `${funcType} "${funcName}" uses Node.js APIs but file lacks "use node" directive. Will fail in Convex runtime.`,
|
|
90
|
+
fix: `Add "use node"; at the top of ${relativePath}`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// Check 3: External API calls without try/catch
|
|
94
|
+
const hasFetch = /\bfetch\s*\(/.test(body);
|
|
95
|
+
const hasAxios = /\baxios\b/.test(body);
|
|
96
|
+
const hasExternalCall = hasFetch || hasAxios;
|
|
97
|
+
const hasTryCatch = /try\s*\{/.test(body);
|
|
98
|
+
if (hasExternalCall && !hasTryCatch) {
|
|
99
|
+
actionsWithoutErrorHandling++;
|
|
100
|
+
issues.push({
|
|
101
|
+
severity: "warning",
|
|
102
|
+
location: `${relativePath}:${startLine + 1}`,
|
|
103
|
+
functionName: funcName,
|
|
104
|
+
message: `${funcType} "${funcName}" makes external API calls without try/catch. Network failures will crash the action.`,
|
|
105
|
+
fix: "Wrap fetch/axios calls in try/catch and handle errors gracefully",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// Check 4: Action calling another action
|
|
109
|
+
if (/ctx\.runAction\s*\(/.test(body)) {
|
|
110
|
+
actionCallingAction++;
|
|
111
|
+
issues.push({
|
|
112
|
+
severity: "warning",
|
|
113
|
+
location: `${relativePath}:${startLine + 1}`,
|
|
114
|
+
functionName: funcName,
|
|
115
|
+
message: `${funcType} "${funcName}" calls ctx.runAction(). Only call action from action when crossing runtimes (V8 ↔ Node). Otherwise extract shared logic into a helper function.`,
|
|
116
|
+
fix: "Extract shared logic into an async helper, or use ctx.runMutation/ctx.runQuery as intermediary",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// Check 5: Very long action body (likely doing too much)
|
|
120
|
+
const bodyLines = endLine - startLine;
|
|
121
|
+
if (bodyLines > 80) {
|
|
122
|
+
issues.push({
|
|
123
|
+
severity: "info",
|
|
124
|
+
location: `${relativePath}:${startLine + 1}`,
|
|
125
|
+
functionName: funcName,
|
|
126
|
+
message: `${funcType} "${funcName}" is ${bodyLines} lines long. Consider splitting into smaller actions or extracting helpers.`,
|
|
127
|
+
fix: "Break large actions into smaller, focused functions",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
issues,
|
|
134
|
+
stats: {
|
|
135
|
+
totalActions,
|
|
136
|
+
actionsWithDbAccess,
|
|
137
|
+
actionsWithoutNodeDirective,
|
|
138
|
+
actionsWithoutErrorHandling,
|
|
139
|
+
actionCallingAction,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// ── Tool Definition ─────────────────────────────────────────────────
|
|
144
|
+
export const actionAuditTools = [
|
|
145
|
+
{
|
|
146
|
+
name: "convex_audit_actions",
|
|
147
|
+
description: 'Audit Convex actions for: ctx.db access (fatal — actions cannot access DB directly), missing "use node" directive for Node APIs, external API calls without error handling, and action-calling-action anti-patterns.',
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: "object",
|
|
150
|
+
properties: {
|
|
151
|
+
projectDir: {
|
|
152
|
+
type: "string",
|
|
153
|
+
description: "Absolute path to the project root containing a convex/ directory",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
required: ["projectDir"],
|
|
157
|
+
},
|
|
158
|
+
handler: async (args) => {
|
|
159
|
+
const projectDir = resolve(args.projectDir);
|
|
160
|
+
const convexDir = findConvexDir(projectDir);
|
|
161
|
+
if (!convexDir) {
|
|
162
|
+
return { error: "No convex/ directory found" };
|
|
163
|
+
}
|
|
164
|
+
const { issues, stats } = auditActions(convexDir);
|
|
165
|
+
const db = getDb();
|
|
166
|
+
db.prepare("INSERT INTO audit_results (id, project_dir, audit_type, issues_json, issue_count) VALUES (?, ?, ?, ?, ?)").run(genId("audit"), projectDir, "action_audit", JSON.stringify(issues), issues.length);
|
|
167
|
+
return {
|
|
168
|
+
summary: {
|
|
169
|
+
...stats,
|
|
170
|
+
totalIssues: issues.length,
|
|
171
|
+
critical: issues.filter((i) => i.severity === "critical").length,
|
|
172
|
+
warnings: issues.filter((i) => i.severity === "warning").length,
|
|
173
|
+
},
|
|
174
|
+
issues: issues.slice(0, 30),
|
|
175
|
+
quickRef: getQuickRef("convex_audit_actions"),
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
//# sourceMappingURL=actionAuditTools.js.map
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { getDb, genId } from "../db.js";
|
|
4
|
+
import { getQuickRef } from "./toolRegistry.js";
|
|
5
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
6
|
+
function findConvexDir(projectDir) {
|
|
7
|
+
const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex")];
|
|
8
|
+
for (const c of candidates) {
|
|
9
|
+
if (existsSync(c))
|
|
10
|
+
return c;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
function collectTsFiles(dir) {
|
|
15
|
+
const results = [];
|
|
16
|
+
if (!existsSync(dir))
|
|
17
|
+
return results;
|
|
18
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const full = join(dir, entry.name);
|
|
21
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "_generated") {
|
|
22
|
+
results.push(...collectTsFiles(full));
|
|
23
|
+
}
|
|
24
|
+
else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
25
|
+
results.push(full);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
30
|
+
function auditAuthorization(convexDir) {
|
|
31
|
+
const files = collectTsFiles(convexDir);
|
|
32
|
+
const issues = [];
|
|
33
|
+
let totalPublic = 0;
|
|
34
|
+
let withAuth = 0;
|
|
35
|
+
let withoutAuth = 0;
|
|
36
|
+
let uncheckedIdentity = 0;
|
|
37
|
+
const funcTypes = ["query", "mutation", "action"];
|
|
38
|
+
const writeSensitive = /admin|delete|purge|remove|destroy|drop|reset|wipe|erase|revoke/i;
|
|
39
|
+
const dbWriteOps = /ctx\.db\.(insert|patch|replace|delete)\s*\(/;
|
|
40
|
+
for (const filePath of files) {
|
|
41
|
+
const content = readFileSync(filePath, "utf-8");
|
|
42
|
+
const relativePath = filePath.replace(convexDir, "").replace(/^[\\/]/, "");
|
|
43
|
+
const lines = content.split("\n");
|
|
44
|
+
for (let i = 0; i < lines.length; i++) {
|
|
45
|
+
const line = lines[i];
|
|
46
|
+
for (const ft of funcTypes) {
|
|
47
|
+
const exportPattern = new RegExp(`export\\s+(?:const\\s+(\\w+)\\s*=|default)\\s+${ft}\\s*\\(`);
|
|
48
|
+
const match = line.match(exportPattern);
|
|
49
|
+
if (!match)
|
|
50
|
+
continue;
|
|
51
|
+
const funcName = match[1] || "default";
|
|
52
|
+
totalPublic++;
|
|
53
|
+
// Extract function body using brace tracking
|
|
54
|
+
let depth = 0;
|
|
55
|
+
let foundOpen = false;
|
|
56
|
+
let endLine = Math.min(i + 100, lines.length);
|
|
57
|
+
for (let j = i; j < lines.length; j++) {
|
|
58
|
+
for (const ch of lines[j]) {
|
|
59
|
+
if (ch === "{") {
|
|
60
|
+
depth++;
|
|
61
|
+
foundOpen = true;
|
|
62
|
+
}
|
|
63
|
+
if (ch === "}")
|
|
64
|
+
depth--;
|
|
65
|
+
}
|
|
66
|
+
if (foundOpen && depth <= 0) {
|
|
67
|
+
endLine = j + 1;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const body = lines.slice(i, endLine).join("\n");
|
|
72
|
+
const hasAuthCheck = /ctx\.auth\.getUserIdentity\s*\(\s*\)/.test(body) ||
|
|
73
|
+
/getUserIdentity/.test(body);
|
|
74
|
+
const hasDbWrite = dbWriteOps.test(body);
|
|
75
|
+
const isSensitiveName = writeSensitive.test(funcName);
|
|
76
|
+
if (hasAuthCheck) {
|
|
77
|
+
withAuth++;
|
|
78
|
+
// Check: getUserIdentity called but return not null-checked
|
|
79
|
+
const identityAssign = body.match(/(?:const|let)\s+(\w+)\s*=\s*await\s+ctx\.auth\.getUserIdentity\s*\(\s*\)/);
|
|
80
|
+
if (identityAssign) {
|
|
81
|
+
const varName = identityAssign[1];
|
|
82
|
+
// Look for null check: if (!var), if (var === null), if (var == null), if (!var)
|
|
83
|
+
const hasNullCheck = new RegExp(`if\\s*\\(\\s*!${varName}\\b|if\\s*\\(\\s*${varName}\\s*===?\\s*null|if\\s*\\(\\s*!${varName}\\s*\\)`).test(body);
|
|
84
|
+
if (!hasNullCheck) {
|
|
85
|
+
uncheckedIdentity++;
|
|
86
|
+
issues.push({
|
|
87
|
+
severity: "critical",
|
|
88
|
+
location: `${relativePath}:${i + 1}`,
|
|
89
|
+
functionName: funcName,
|
|
90
|
+
message: `${ft} "${funcName}" calls getUserIdentity() but doesn't check for null. Unauthenticated users will get undefined identity.`,
|
|
91
|
+
fix: `Add: if (!${varName}) { throw new Error("Not authenticated"); }`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
withoutAuth++;
|
|
98
|
+
// Critical: public mutation/action with DB writes but no auth
|
|
99
|
+
if ((ft === "mutation" || ft === "action") && hasDbWrite) {
|
|
100
|
+
issues.push({
|
|
101
|
+
severity: "critical",
|
|
102
|
+
location: `${relativePath}:${i + 1}`,
|
|
103
|
+
functionName: funcName,
|
|
104
|
+
message: `Public ${ft} "${funcName}" writes to DB without auth check. Any client can call this.`,
|
|
105
|
+
fix: `Add: const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated");`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// Critical: sensitive-named function without auth
|
|
109
|
+
if (isSensitiveName) {
|
|
110
|
+
issues.push({
|
|
111
|
+
severity: "critical",
|
|
112
|
+
location: `${relativePath}:${i + 1}`,
|
|
113
|
+
functionName: funcName,
|
|
114
|
+
message: `Public ${ft} "${funcName}" has a sensitive name but no auth check. Consider making it internal or adding auth.`,
|
|
115
|
+
fix: `Either change to internal${ft.charAt(0).toUpperCase() + ft.slice(1)} or add auth check`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
// Warning: public query reading user-specific data without auth
|
|
119
|
+
if (ft === "query" && /ctx\.db\.get|ctx\.db\.query/.test(body) && /user|profile|account|email/i.test(body)) {
|
|
120
|
+
issues.push({
|
|
121
|
+
severity: "warning",
|
|
122
|
+
location: `${relativePath}:${i + 1}`,
|
|
123
|
+
functionName: funcName,
|
|
124
|
+
message: `Public query "${funcName}" accesses user-related data without auth. Consider if unauthenticated access is intended.`,
|
|
125
|
+
fix: "Add auth check if this data should be protected",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
issues,
|
|
134
|
+
stats: {
|
|
135
|
+
totalPublicFunctions: totalPublic,
|
|
136
|
+
publicWithAuth: withAuth,
|
|
137
|
+
publicWithoutAuth: withoutAuth,
|
|
138
|
+
uncheckedIdentity,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// ── Tool Definition ─────────────────────────────────────────────────
|
|
143
|
+
export const authorizationTools = [
|
|
144
|
+
{
|
|
145
|
+
name: "convex_audit_authorization",
|
|
146
|
+
description: "Audit Convex functions for authorization issues: public mutations/actions writing to DB without auth checks, getUserIdentity() without null checks, sensitive-named functions without auth, and unprotected user data queries.",
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: "object",
|
|
149
|
+
properties: {
|
|
150
|
+
projectDir: {
|
|
151
|
+
type: "string",
|
|
152
|
+
description: "Absolute path to the project root containing a convex/ directory",
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
required: ["projectDir"],
|
|
156
|
+
},
|
|
157
|
+
handler: async (args) => {
|
|
158
|
+
const projectDir = resolve(args.projectDir);
|
|
159
|
+
const convexDir = findConvexDir(projectDir);
|
|
160
|
+
if (!convexDir) {
|
|
161
|
+
return { error: "No convex/ directory found" };
|
|
162
|
+
}
|
|
163
|
+
const { issues, stats } = auditAuthorization(convexDir);
|
|
164
|
+
// Store audit result
|
|
165
|
+
const db = getDb();
|
|
166
|
+
db.prepare("INSERT INTO audit_results (id, project_dir, audit_type, issues_json, issue_count) VALUES (?, ?, ?, ?, ?)").run(genId("audit"), projectDir, "authorization", JSON.stringify(issues), issues.length);
|
|
167
|
+
const critical = issues.filter((i) => i.severity === "critical");
|
|
168
|
+
const warnings = issues.filter((i) => i.severity === "warning");
|
|
169
|
+
// Group by issue type
|
|
170
|
+
const byType = {};
|
|
171
|
+
for (const issue of issues) {
|
|
172
|
+
const type = issue.message.includes("without auth check") ? "no_auth_on_write" :
|
|
173
|
+
issue.message.includes("null") ? "unchecked_identity" :
|
|
174
|
+
issue.message.includes("sensitive name") ? "sensitive_no_auth" :
|
|
175
|
+
issue.message.includes("user-related") ? "unprotected_user_data" :
|
|
176
|
+
"other";
|
|
177
|
+
if (!byType[type])
|
|
178
|
+
byType[type] = { count: 0, examples: [] };
|
|
179
|
+
byType[type].count++;
|
|
180
|
+
if (byType[type].examples.length < 3)
|
|
181
|
+
byType[type].examples.push(issue);
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
summary: {
|
|
185
|
+
...stats,
|
|
186
|
+
totalIssues: issues.length,
|
|
187
|
+
critical: critical.length,
|
|
188
|
+
warnings: warnings.length,
|
|
189
|
+
authCoverage: stats.totalPublicFunctions > 0
|
|
190
|
+
? `${stats.publicWithAuth}/${stats.totalPublicFunctions} (${Math.round(100 * stats.publicWithAuth / stats.totalPublicFunctions)}%)`
|
|
191
|
+
: "0/0",
|
|
192
|
+
},
|
|
193
|
+
issuesByType: Object.entries(byType)
|
|
194
|
+
.sort(([, a], [, b]) => b.count - a.count)
|
|
195
|
+
.map(([type, data]) => ({ type, count: data.count, examples: data.examples })),
|
|
196
|
+
quickRef: getQuickRef("convex_audit_authorization"),
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
];
|
|
201
|
+
//# sourceMappingURL=authorizationTools.js.map
|
|
@@ -32,22 +32,26 @@ function scoreCritterCheck(input) {
|
|
|
32
32
|
const taskLower = input.task.toLowerCase().trim();
|
|
33
33
|
const whyLower = input.why.toLowerCase().trim();
|
|
34
34
|
const whoLower = input.who.toLowerCase().trim();
|
|
35
|
-
// Check 1: Circular reasoning
|
|
35
|
+
// Check 1: Circular reasoning (threshold 0.5)
|
|
36
36
|
const taskWords = new Set(taskLower.split(/\s+/).filter((w) => w.length > 3));
|
|
37
37
|
const whyWords = whyLower.split(/\s+/).filter((w) => w.length > 3);
|
|
38
38
|
const overlap = whyWords.filter((w) => taskWords.has(w));
|
|
39
|
-
if (whyWords.length > 0 && overlap.length / whyWords.length > 0.
|
|
39
|
+
if (whyWords.length > 0 && overlap.length / whyWords.length > 0.5) {
|
|
40
40
|
score -= 30;
|
|
41
41
|
feedback.push("Circular: your 'why' mostly restates the task. What user outcome does this enable?");
|
|
42
42
|
}
|
|
43
43
|
// Check 2: Vague audience
|
|
44
|
-
const vagueAudiences = ["users", "everyone", "people", "the team", "stakeholders", "clients"];
|
|
44
|
+
const vagueAudiences = ["users", "everyone", "people", "the team", "stakeholders", "clients", "customers", "developers"];
|
|
45
45
|
if (vagueAudiences.includes(whoLower)) {
|
|
46
46
|
score -= 20;
|
|
47
47
|
feedback.push(`"${input.who}" is too broad. Which user role or API consumer specifically?`);
|
|
48
48
|
}
|
|
49
|
-
// Check 3:
|
|
50
|
-
if (whyLower.length
|
|
49
|
+
// Check 3: Empty or too short
|
|
50
|
+
if (whyLower.length === 0) {
|
|
51
|
+
score -= 40;
|
|
52
|
+
feedback.push("Empty 'why': you haven't stated any purpose at all.");
|
|
53
|
+
}
|
|
54
|
+
else if (whyLower.length < 10) {
|
|
51
55
|
score -= 25;
|
|
52
56
|
feedback.push("The 'why' is too short. What problem does this solve?");
|
|
53
57
|
}
|
|
@@ -61,6 +65,73 @@ function scoreCritterCheck(input) {
|
|
|
61
65
|
score -= 15;
|
|
62
66
|
feedback.push("Citing authority instead of understanding purpose. Why does this matter to the product?");
|
|
63
67
|
}
|
|
68
|
+
// Check 5: Non-answer patterns (count matches, -20 each, cap -40)
|
|
69
|
+
const nonAnswerPatterns = [
|
|
70
|
+
"just because", "don't know", "not sure", "why not", "might need it",
|
|
71
|
+
"no reason", "no idea", "whatever", "idk", "tbd",
|
|
72
|
+
];
|
|
73
|
+
const nonAnswerHits = nonAnswerPatterns.filter((p) => whyLower.includes(p)).length;
|
|
74
|
+
if (nonAnswerHits > 0) {
|
|
75
|
+
const nonAnswerPenalty = Math.min(nonAnswerHits * 20, 40);
|
|
76
|
+
score -= nonAnswerPenalty;
|
|
77
|
+
feedback.push("Non-answer: your 'why' signals unclear purpose. What specific problem does this solve?");
|
|
78
|
+
}
|
|
79
|
+
// Check 6: Repetitive padding (why + who)
|
|
80
|
+
const whyAllWords = whyLower.split(/\s+/).filter((w) => w.length > 2);
|
|
81
|
+
if (whyAllWords.length >= 5) {
|
|
82
|
+
const whyUniqueWords = new Set(whyAllWords);
|
|
83
|
+
if (whyUniqueWords.size / whyAllWords.length < 0.4) {
|
|
84
|
+
score -= 25;
|
|
85
|
+
feedback.push("Repetitive: your 'why' repeats the same words. Articulate distinct reasoning.");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const whoAllWords = whoLower.split(/\s+/).filter((w) => w.length > 2);
|
|
89
|
+
if (whoAllWords.length >= 5) {
|
|
90
|
+
const whoUniqueWords = new Set(whoAllWords);
|
|
91
|
+
if (whoUniqueWords.size / whoAllWords.length < 0.4) {
|
|
92
|
+
score -= 25;
|
|
93
|
+
feedback.push("Repetitive: your 'who' repeats the same words. Name a real audience.");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Check 7: Buzzword-heavy corporate-speak (scans why + who)
|
|
97
|
+
const buzzwords = [
|
|
98
|
+
"leverage", "synergies", "synergy", "paradigm", "holistic", "alignment",
|
|
99
|
+
"transformation", "innovative", "disruptive", "best practices",
|
|
100
|
+
"streamline", "ecosystem", "actionable", "circle back",
|
|
101
|
+
];
|
|
102
|
+
const allText = `${whyLower} ${whoLower}`;
|
|
103
|
+
const buzzCount = buzzwords.filter((b) => allText.includes(b)).length;
|
|
104
|
+
if (buzzCount >= 4) {
|
|
105
|
+
score -= 35;
|
|
106
|
+
feedback.push("Buzzword-heavy: corporate-speak without concrete meaning. What specific problem does this solve?");
|
|
107
|
+
}
|
|
108
|
+
else if (buzzCount >= 3) {
|
|
109
|
+
score -= 30;
|
|
110
|
+
feedback.push("Buzzword-heavy: corporate-speak without concrete meaning. What specific problem does this solve?");
|
|
111
|
+
}
|
|
112
|
+
else if (buzzCount >= 2) {
|
|
113
|
+
score -= 20;
|
|
114
|
+
feedback.push("Buzzword-heavy: corporate-speak without concrete meaning. What specific problem does this solve?");
|
|
115
|
+
}
|
|
116
|
+
// Check 8: Hedging language
|
|
117
|
+
const hedgeWords = ["could", "potentially", "maybe", "possibly", "might", "perhaps", "hopefully"];
|
|
118
|
+
const hedgeCount = hedgeWords.filter((h) => {
|
|
119
|
+
const regex = new RegExp(`\\b${h}\\b`, "i");
|
|
120
|
+
return regex.test(whyLower);
|
|
121
|
+
}).length;
|
|
122
|
+
if (hedgeCount >= 2) {
|
|
123
|
+
score -= 15;
|
|
124
|
+
feedback.push("Hedging: too many 'could/maybe/potentially' signals uncertain value. Be definitive.");
|
|
125
|
+
}
|
|
126
|
+
// Check 9: Task-word echo — same word from task repeated 3+ times in why
|
|
127
|
+
for (const tw of taskWords) {
|
|
128
|
+
const twCount = whyWords.filter((w) => w === tw).length;
|
|
129
|
+
if (twCount >= 3) {
|
|
130
|
+
score -= 20;
|
|
131
|
+
feedback.push(`Echo: "${tw}" appears ${twCount} times in your 'why' — this is filler, not reasoning.`);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
64
135
|
// Bonus for specificity
|
|
65
136
|
if (input.success_looks_like && input.success_looks_like.length > 20) {
|
|
66
137
|
score += 10;
|