@qodfy/core 0.1.5 → 0.2.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.d.ts +6 -1
- package/dist/index.js +418 -67
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
type IssueSeverity = "critical" | "warning" | "info";
|
|
2
|
+
type IssueCategory = "security" | "environment" | "api" | "webhook" | "ai" | "maintainability" | "project";
|
|
2
3
|
type Issue = {
|
|
4
|
+
id: string;
|
|
5
|
+
ruleId: string;
|
|
6
|
+
category: IssueCategory;
|
|
3
7
|
severity: IssueSeverity;
|
|
4
8
|
title: string;
|
|
5
9
|
message: string;
|
|
6
10
|
file?: string;
|
|
7
11
|
suggestion?: string;
|
|
12
|
+
fixPrompt?: string;
|
|
8
13
|
};
|
|
9
14
|
type ScanReport = {
|
|
10
15
|
projectPath: string;
|
|
@@ -21,4 +26,4 @@ type ScanReport = {
|
|
|
21
26
|
};
|
|
22
27
|
declare function scanProject(projectPath: string): Promise<ScanReport>;
|
|
23
28
|
|
|
24
|
-
export { type Issue, type IssueSeverity, type ScanReport, scanProject };
|
|
29
|
+
export { type Issue, type IssueCategory, type IssueSeverity, type ScanReport, scanProject };
|
package/dist/index.js
CHANGED
|
@@ -88,37 +88,64 @@ var hardcodedSecretPatterns = [
|
|
|
88
88
|
var LARGE_FILE_WARNING_BYTES = 40 * 1024;
|
|
89
89
|
var LARGE_FILE_REPORT_LIMIT = 10;
|
|
90
90
|
var MAX_FILE_SIZE_BYTES = 500 * 1024;
|
|
91
|
+
var issueIdPrefixes = {
|
|
92
|
+
"project-missing-package-json": "project-missing-package-json",
|
|
93
|
+
"project-invalid-package-json": "project-invalid-package-json",
|
|
94
|
+
"project-next-not-detected": "project-next-not-detected",
|
|
95
|
+
"project-missing-readme": "project-missing-readme",
|
|
96
|
+
"environment-missing-env-example": "environment-missing-env-example",
|
|
97
|
+
"environment-variable-missing-from-example": "environment-variable-missing-from-example",
|
|
98
|
+
"security-client-side-secret": "security-client-side-secret",
|
|
99
|
+
"security-hardcoded-secret": "security-hardcoded-secret",
|
|
100
|
+
"api-route-missing-auth": "security-api-auth",
|
|
101
|
+
"ai-route-missing-rate-limit": "ai-route-rate-limit",
|
|
102
|
+
"maintainability-large-file": "maintainability-large-file",
|
|
103
|
+
"maintainability-large-file-skipped": "maintainability-large-file-skipped",
|
|
104
|
+
"maintainability-file-unreadable": "maintainability-file-unreadable",
|
|
105
|
+
"project-source-files-unreadable": "project-source-files-unreadable",
|
|
106
|
+
"webhook-missing-signature-verification": "webhook-signature-verification"
|
|
107
|
+
};
|
|
91
108
|
async function scanProject(projectPath) {
|
|
92
109
|
const startTime = Date.now();
|
|
93
110
|
const resolvedProjectPath = path.resolve(projectPath);
|
|
94
111
|
const issues = [];
|
|
112
|
+
const addIssue = createIssueFactory(issues);
|
|
95
113
|
const packageJsonPath = path.join(resolvedProjectPath, "package.json");
|
|
96
114
|
const hasPackageJson = await fileExists(packageJsonPath);
|
|
97
115
|
let isNextProject = false;
|
|
98
116
|
if (!hasPackageJson) {
|
|
99
|
-
|
|
117
|
+
addIssue({
|
|
118
|
+
ruleId: "project-missing-package-json",
|
|
119
|
+
category: "project",
|
|
100
120
|
severity: "critical",
|
|
101
121
|
title: "Missing package.json",
|
|
102
122
|
message: "Qodfy could not find a package.json file in this project.",
|
|
103
|
-
suggestion: "Run Qodfy from the project root or pass --path to the app folder."
|
|
123
|
+
suggestion: "Run Qodfy from the project root or pass --path to the app folder.",
|
|
124
|
+
fixPrompt: createProjectRootFixPrompt()
|
|
104
125
|
});
|
|
105
126
|
} else {
|
|
106
127
|
const packageJsonResult = await safeReadJson(packageJsonPath);
|
|
107
128
|
if (!packageJsonResult.ok) {
|
|
108
|
-
|
|
129
|
+
addIssue({
|
|
130
|
+
ruleId: "project-invalid-package-json",
|
|
131
|
+
category: "project",
|
|
109
132
|
severity: "critical",
|
|
110
133
|
title: "Could not read package.json",
|
|
111
134
|
message: packageJsonResult.reason,
|
|
112
135
|
file: "package.json",
|
|
113
|
-
suggestion: "Fix package.json so Qodfy can detect the framework and dependencies."
|
|
136
|
+
suggestion: "Fix package.json so Qodfy can detect the framework and dependencies.",
|
|
137
|
+
fixPrompt: createPackageJsonFixPrompt()
|
|
114
138
|
});
|
|
115
139
|
} else if (!isPackageJsonObject(packageJsonResult.data)) {
|
|
116
|
-
|
|
140
|
+
addIssue({
|
|
141
|
+
ruleId: "project-invalid-package-json",
|
|
142
|
+
category: "project",
|
|
117
143
|
severity: "critical",
|
|
118
144
|
title: "Invalid package.json",
|
|
119
145
|
message: "package.json must contain a JSON object at the top level.",
|
|
120
146
|
file: "package.json",
|
|
121
|
-
suggestion: "Fix package.json so Qodfy can detect the framework and dependencies."
|
|
147
|
+
suggestion: "Fix package.json so Qodfy can detect the framework and dependencies.",
|
|
148
|
+
fixPrompt: createPackageJsonFixPrompt()
|
|
122
149
|
});
|
|
123
150
|
} else {
|
|
124
151
|
const deps = {
|
|
@@ -127,11 +154,14 @@ async function scanProject(projectPath) {
|
|
|
127
154
|
};
|
|
128
155
|
isNextProject = Boolean(deps.next);
|
|
129
156
|
if (!isNextProject) {
|
|
130
|
-
|
|
157
|
+
addIssue({
|
|
158
|
+
ruleId: "project-next-not-detected",
|
|
159
|
+
category: "project",
|
|
131
160
|
severity: "warning",
|
|
132
161
|
title: "Next.js not detected",
|
|
133
162
|
message: "This first version of Qodfy is optimized for Next.js projects.",
|
|
134
|
-
suggestion: "If this is a monorepo, scan the Next.js app folder directly."
|
|
163
|
+
suggestion: "If this is a monorepo, scan the Next.js app folder directly.",
|
|
164
|
+
fixPrompt: createNextNotDetectedFixPrompt()
|
|
135
165
|
});
|
|
136
166
|
}
|
|
137
167
|
}
|
|
@@ -140,21 +170,27 @@ async function scanProject(projectPath) {
|
|
|
140
170
|
const hasEnvExample = await fileExists(envExamplePath);
|
|
141
171
|
let envExampleVariables = null;
|
|
142
172
|
if (!hasEnvExample) {
|
|
143
|
-
|
|
173
|
+
addIssue({
|
|
174
|
+
ruleId: "environment-missing-env-example",
|
|
175
|
+
category: "environment",
|
|
144
176
|
severity: "warning",
|
|
145
177
|
title: "Missing .env.example",
|
|
146
178
|
message: "Add a .env.example file so future developers know which environment variables are required.",
|
|
147
|
-
suggestion: "Document required variable names only, never real secret values."
|
|
179
|
+
suggestion: "Document required variable names only, never real secret values.",
|
|
180
|
+
fixPrompt: createMissingEnvExampleFixPrompt()
|
|
148
181
|
});
|
|
149
182
|
} else {
|
|
150
183
|
const envExampleResult = await safeReadFile(envExamplePath);
|
|
151
184
|
if (!envExampleResult.ok) {
|
|
152
|
-
|
|
185
|
+
addIssue({
|
|
186
|
+
ruleId: "environment-missing-env-example",
|
|
187
|
+
category: "environment",
|
|
153
188
|
severity: "warning",
|
|
154
189
|
title: "Could not read .env.example",
|
|
155
190
|
message: envExampleResult.reason,
|
|
156
191
|
file: ".env.example",
|
|
157
|
-
suggestion: "Make sure .env.example is readable and contains variable names without real secret values."
|
|
192
|
+
suggestion: "Make sure .env.example is readable and contains variable names without real secret values.",
|
|
193
|
+
fixPrompt: createMissingEnvExampleFixPrompt()
|
|
158
194
|
});
|
|
159
195
|
} else {
|
|
160
196
|
envExampleVariables = getEnvExampleVariables(envExampleResult.content);
|
|
@@ -162,13 +198,16 @@ async function scanProject(projectPath) {
|
|
|
162
198
|
}
|
|
163
199
|
const hasReadme = await fileExists(path.join(resolvedProjectPath, "README.md"));
|
|
164
200
|
if (!hasReadme) {
|
|
165
|
-
|
|
201
|
+
addIssue({
|
|
202
|
+
ruleId: "project-missing-readme",
|
|
203
|
+
category: "project",
|
|
166
204
|
severity: "info",
|
|
167
205
|
title: "Missing README.md",
|
|
168
|
-
message: "A README helps other developers understand how to run and maintain the project."
|
|
206
|
+
message: "A README helps other developers understand how to run and maintain the project.",
|
|
207
|
+
fixPrompt: createReadmeFixPrompt()
|
|
169
208
|
});
|
|
170
209
|
}
|
|
171
|
-
const files = await getSourceFiles(resolvedProjectPath,
|
|
210
|
+
const files = await getSourceFiles(resolvedProjectPath, addIssue);
|
|
172
211
|
const apiRoutes = files.filter((file) => {
|
|
173
212
|
return isApiRoute(file);
|
|
174
213
|
});
|
|
@@ -183,32 +222,41 @@ async function scanProject(projectPath) {
|
|
|
183
222
|
const relativeFile = normalizePath(path.relative(resolvedProjectPath, file));
|
|
184
223
|
const statResult = await safeStatFile(file);
|
|
185
224
|
if (!statResult.ok) {
|
|
186
|
-
|
|
225
|
+
addIssue({
|
|
226
|
+
ruleId: "maintainability-file-unreadable",
|
|
227
|
+
category: "maintainability",
|
|
187
228
|
severity: "info",
|
|
188
229
|
title: "File could not be checked",
|
|
189
230
|
message: statResult.reason,
|
|
190
|
-
file: relativeFile
|
|
231
|
+
file: relativeFile,
|
|
232
|
+
suggestion: "Check file permissions if this file should be included in launch-readiness scans."
|
|
191
233
|
});
|
|
192
234
|
continue;
|
|
193
235
|
}
|
|
194
236
|
if (statResult.size > MAX_FILE_SIZE_BYTES) {
|
|
195
237
|
largeFiles++;
|
|
196
|
-
|
|
238
|
+
addIssue({
|
|
239
|
+
ruleId: "maintainability-large-file-skipped",
|
|
240
|
+
category: "maintainability",
|
|
197
241
|
severity: "info",
|
|
198
242
|
title: "Large file skipped from deep scan",
|
|
199
243
|
message: "This file is larger than 500KB and was skipped from deep content checks.",
|
|
200
244
|
file: relativeFile,
|
|
201
|
-
suggestion: "Review large generated or bundled files manually."
|
|
245
|
+
suggestion: "Review large generated or bundled files manually.",
|
|
246
|
+
fixPrompt: createLargeFileFixPrompt(relativeFile)
|
|
202
247
|
});
|
|
203
248
|
continue;
|
|
204
249
|
}
|
|
205
250
|
const fileResult = await safeReadFile(file);
|
|
206
251
|
if (!fileResult.ok) {
|
|
207
|
-
|
|
252
|
+
addIssue({
|
|
253
|
+
ruleId: "maintainability-file-unreadable",
|
|
254
|
+
category: "maintainability",
|
|
208
255
|
severity: "info",
|
|
209
256
|
title: "File could not be read",
|
|
210
257
|
message: fileResult.reason,
|
|
211
|
-
file: relativeFile
|
|
258
|
+
file: relativeFile,
|
|
259
|
+
suggestion: "Check file permissions if this file should be included in launch-readiness scans."
|
|
212
260
|
});
|
|
213
261
|
continue;
|
|
214
262
|
}
|
|
@@ -227,35 +275,29 @@ async function scanProject(projectPath) {
|
|
|
227
275
|
aiFiles++;
|
|
228
276
|
const hasRateLimit = content.includes("rateLimit") || content.includes("ratelimit") || content.includes("upstash") || content.includes("limiter");
|
|
229
277
|
if (apiRouteSet.has(file) && !hasRateLimit) {
|
|
230
|
-
|
|
278
|
+
addIssue({
|
|
279
|
+
ruleId: "ai-route-missing-rate-limit",
|
|
280
|
+
category: "ai",
|
|
231
281
|
severity: "critical",
|
|
232
282
|
title: "AI route may be missing rate limiting",
|
|
233
283
|
message: "AI routes can create real API costs. Add rate limiting or usage limits before launch.",
|
|
234
284
|
file: relativeFile,
|
|
235
|
-
suggestion: "Add rate limiting, usage limits, or per-user quotas before launch."
|
|
285
|
+
suggestion: "Add rate limiting, usage limits, or per-user quotas before launch.",
|
|
286
|
+
fixPrompt: createAiRateLimitFixPrompt(relativeFile)
|
|
236
287
|
});
|
|
237
288
|
}
|
|
238
289
|
}
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
message: "This Stripe webhook route does not appear to verify the Stripe signature before handling the event.",
|
|
248
|
-
file: relativeFile,
|
|
249
|
-
suggestion: "Use stripe.webhooks.constructEvent with the raw request body, Stripe-Signature header, and STRIPE_WEBHOOK_SECRET."
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
if (isClerkWebhook && !hasClerkSignatureVerification) {
|
|
253
|
-
issues.push({
|
|
254
|
-
severity: "critical",
|
|
255
|
-
title: "Clerk webhook may be missing signature verification",
|
|
256
|
-
message: "This Clerk webhook route does not appear to verify the webhook signature before handling the event.",
|
|
290
|
+
const webhookRouteInfo = apiRouteSet.has(file) ? getWebhookRouteInfo(relativeFile, content) : null;
|
|
291
|
+
if (webhookRouteInfo && !hasWebhookSignatureVerification(content, webhookRouteInfo.provider)) {
|
|
292
|
+
addIssue({
|
|
293
|
+
ruleId: "webhook-missing-signature-verification",
|
|
294
|
+
category: "webhook",
|
|
295
|
+
severity: webhookRouteInfo.confidence === "high" ? "critical" : "warning",
|
|
296
|
+
title: "Webhook route may be missing signature verification",
|
|
297
|
+
message: "This webhook route appears to handle external events, but Qodfy could not find signature verification before the event is handled.",
|
|
257
298
|
file: relativeFile,
|
|
258
|
-
suggestion:
|
|
299
|
+
suggestion: getWebhookSignatureSuggestion(webhookRouteInfo.provider),
|
|
300
|
+
fixPrompt: createWebhookSignatureFixPrompt(relativeFile)
|
|
259
301
|
});
|
|
260
302
|
}
|
|
261
303
|
for (const secretMatch of getHardcodedSecretMatches(content)) {
|
|
@@ -264,23 +306,29 @@ async function scanProject(projectPath) {
|
|
|
264
306
|
continue;
|
|
265
307
|
}
|
|
266
308
|
hardcodedSecretWarningKeys.add(warningKey);
|
|
267
|
-
|
|
309
|
+
addIssue({
|
|
310
|
+
ruleId: "security-hardcoded-secret",
|
|
311
|
+
category: "security",
|
|
268
312
|
severity: "critical",
|
|
269
313
|
title: "Possible hardcoded secret",
|
|
270
314
|
message: `A string literal in ${relativeFile} matches the pattern for ${secretMatch.label}. Qodfy does not print possible secret values.`,
|
|
271
315
|
file: relativeFile,
|
|
272
|
-
suggestion: "Move secrets into environment variables and rotate the value if this is a real secret."
|
|
316
|
+
suggestion: "Move secrets into environment variables and rotate the value if this is a real secret.",
|
|
317
|
+
fixPrompt: createHardcodedSecretFixPrompt(relativeFile, secretMatch.label)
|
|
273
318
|
});
|
|
274
319
|
}
|
|
275
320
|
if (apiRouteSet.has(file)) {
|
|
276
321
|
const hasAuth = content.includes("auth(") || content.includes("getServerSession") || content.includes("currentUser") || content.includes("clerkClient") || content.includes("session");
|
|
277
|
-
if (!hasAuth && !
|
|
278
|
-
|
|
322
|
+
if (!hasAuth && !webhookRouteInfo) {
|
|
323
|
+
addIssue({
|
|
324
|
+
ruleId: "api-route-missing-auth",
|
|
325
|
+
category: "api",
|
|
279
326
|
severity: "warning",
|
|
280
327
|
title: "API route may be missing authentication",
|
|
281
328
|
message: "This API route does not appear to contain an auth/session check.",
|
|
282
329
|
file: relativeFile,
|
|
283
|
-
suggestion: "Confirm the route is public, or add an auth/session check before handling user data."
|
|
330
|
+
suggestion: "Confirm the route is public, or add an auth/session check before handling user data.",
|
|
331
|
+
fixPrompt: createApiAuthFixPrompt(relativeFile)
|
|
284
332
|
});
|
|
285
333
|
}
|
|
286
334
|
}
|
|
@@ -303,34 +351,43 @@ async function scanProject(projectPath) {
|
|
|
303
351
|
continue;
|
|
304
352
|
}
|
|
305
353
|
clientSecretWarningKeys.add(warningKey);
|
|
306
|
-
|
|
354
|
+
addIssue({
|
|
355
|
+
ruleId: "security-client-side-secret",
|
|
356
|
+
category: "security",
|
|
307
357
|
severity: "warning",
|
|
308
358
|
title: "Possible server secret used in client-side code",
|
|
309
359
|
message: `${variableName} appears in a client-side file. Server secrets should not be exposed to the browser.`,
|
|
310
360
|
file: relativeFile,
|
|
311
|
-
suggestion: "Move server-only environment variable access to a server component, API route, or server action."
|
|
361
|
+
suggestion: "Move server-only environment variable access to a server component, API route, or server action.",
|
|
362
|
+
fixPrompt: createClientSideSecretFixPrompt(relativeFile, variableName)
|
|
312
363
|
});
|
|
313
364
|
}
|
|
314
365
|
}
|
|
315
366
|
}
|
|
316
367
|
}
|
|
317
368
|
for (const largeFile of getReportedLargeFiles(largeFileCandidates)) {
|
|
318
|
-
|
|
369
|
+
addIssue({
|
|
370
|
+
ruleId: "maintainability-large-file",
|
|
371
|
+
category: "maintainability",
|
|
319
372
|
severity: "info",
|
|
320
373
|
title: "Large file detected",
|
|
321
|
-
message: "Large files
|
|
374
|
+
message: "This file is larger than the recommended maintainability threshold. Large files can be harder to review, test, and safely modify.",
|
|
322
375
|
file: largeFile.relativeFile,
|
|
323
|
-
suggestion: "
|
|
376
|
+
suggestion: "Review whether this file mixes UI, state, data fetching, validation, or business logic. If so, split it into smaller components, hooks, or utilities.",
|
|
377
|
+
fixPrompt: createLargeFileFixPrompt(largeFile.relativeFile)
|
|
324
378
|
});
|
|
325
379
|
}
|
|
326
380
|
for (const [variableName, filesUsingVariable] of getSortedMissingEnvUsages(missingEnvUsages)) {
|
|
327
381
|
const files2 = [...filesUsingVariable].sort();
|
|
328
|
-
|
|
382
|
+
addIssue({
|
|
383
|
+
ruleId: "environment-variable-missing-from-example",
|
|
384
|
+
category: "environment",
|
|
329
385
|
severity: "warning",
|
|
330
386
|
title: "Environment variable missing from .env.example",
|
|
331
387
|
message: getMissingEnvMessage(variableName, files2),
|
|
332
388
|
file: files2.length === 1 ? files2[0] : void 0,
|
|
333
|
-
suggestion: `Add ${variableName}= to .env.example without including a real value
|
|
389
|
+
suggestion: `Add ${variableName}= to .env.example without including a real value.`,
|
|
390
|
+
fixPrompt: createMissingEnvVariableFixPrompt(variableName, files2)
|
|
334
391
|
});
|
|
335
392
|
}
|
|
336
393
|
const criticalCount = issues.filter((issue) => issue.severity === "critical").length;
|
|
@@ -351,6 +408,20 @@ async function scanProject(projectPath) {
|
|
|
351
408
|
}
|
|
352
409
|
};
|
|
353
410
|
}
|
|
411
|
+
function createIssueFactory(issues) {
|
|
412
|
+
const issueCounts = /* @__PURE__ */ new Map();
|
|
413
|
+
return (issue) => {
|
|
414
|
+
const currentCount = (issueCounts.get(issue.ruleId) ?? 0) + 1;
|
|
415
|
+
issueCounts.set(issue.ruleId, currentCount);
|
|
416
|
+
issues.push({
|
|
417
|
+
id: `${getIssueIdPrefix(issue.ruleId, issue.category)}-${currentCount}`,
|
|
418
|
+
...issue
|
|
419
|
+
});
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function getIssueIdPrefix(ruleId, category) {
|
|
423
|
+
return issueIdPrefixes[ruleId] ?? `${category}-${ruleId}`;
|
|
424
|
+
}
|
|
354
425
|
async function fileExists(filePath) {
|
|
355
426
|
try {
|
|
356
427
|
await fs.access(filePath);
|
|
@@ -435,7 +506,7 @@ async function safeReadJson(filePath) {
|
|
|
435
506
|
};
|
|
436
507
|
}
|
|
437
508
|
}
|
|
438
|
-
async function getSourceFiles(projectPath,
|
|
509
|
+
async function getSourceFiles(projectPath, addIssue) {
|
|
439
510
|
try {
|
|
440
511
|
return await fg(sourceFilePatterns, {
|
|
441
512
|
cwd: projectPath,
|
|
@@ -445,11 +516,14 @@ async function getSourceFiles(projectPath, issues) {
|
|
|
445
516
|
dot: false
|
|
446
517
|
});
|
|
447
518
|
} catch {
|
|
448
|
-
|
|
519
|
+
addIssue({
|
|
520
|
+
ruleId: "project-source-files-unreadable",
|
|
521
|
+
category: "project",
|
|
449
522
|
severity: "critical",
|
|
450
523
|
title: "Could not scan source files",
|
|
451
524
|
message: "Qodfy could not list source files in this project.",
|
|
452
|
-
suggestion: "Check that the project path exists and is readable."
|
|
525
|
+
suggestion: "Check that the project path exists and is readable.",
|
|
526
|
+
fixPrompt: createProjectRootFixPrompt()
|
|
453
527
|
});
|
|
454
528
|
return [];
|
|
455
529
|
}
|
|
@@ -536,23 +610,300 @@ function isClientSideFile(relativeFile, content) {
|
|
|
536
610
|
return fileName.includes(".client.") || /(^|\n)\s*["']use client["'];?/.test(content);
|
|
537
611
|
}
|
|
538
612
|
function isApiRoute(filePath) {
|
|
539
|
-
|
|
613
|
+
const normalizedFile = normalizePath(filePath);
|
|
614
|
+
const sourceFileExtension = "(?:ts|tsx|js|jsx|mts|cts|mjs|cjs)";
|
|
615
|
+
return new RegExp(`/app/api(?:/.+)?/route\\.${sourceFileExtension}$`).test(normalizedFile) || new RegExp(`/pages/api/.+\\.${sourceFileExtension}$`).test(normalizedFile);
|
|
540
616
|
}
|
|
541
|
-
function
|
|
617
|
+
function getWebhookRouteInfo(relativeFile, content) {
|
|
542
618
|
const normalizedFile = relativeFile.toLowerCase();
|
|
543
619
|
const normalizedContent = content.toLowerCase();
|
|
544
|
-
|
|
620
|
+
const normalizedRouteContext = `${normalizedFile}
|
|
621
|
+
${normalizedContent}`;
|
|
622
|
+
const pathLooksLikeWebhook = normalizedFile.includes("webhook") || normalizedFile.includes("callback");
|
|
623
|
+
const contentStronglySuggestsWebhook = normalizedContent.includes("stripe.webhooks") || normalizedContent.includes("constructevent(") || normalizedContent.includes("stripe-signature") || normalizedContent.includes("stripe_webhook_secret") || normalizedContent.includes("clerk_webhook_secret") || normalizedContent.includes("svix") || normalizedContent.includes("x-github-event") || normalizedContent.includes("x-hub-signature") || normalizedContent.includes("x-shopify-hmac-sha256") || normalizedContent.includes("resend") && normalizedContent.includes("webhook") || normalizedContent.includes("webhook_secret") || normalizedContent.includes("webhooksecret") || normalizedContent.includes("webhook") && (normalizedContent.includes("signature") || normalizedContent.includes("secret") || normalizedContent.includes("event"));
|
|
624
|
+
if (!pathLooksLikeWebhook && !contentStronglySuggestsWebhook) {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
const provider = getWebhookProvider(normalizedRouteContext);
|
|
628
|
+
return {
|
|
629
|
+
provider,
|
|
630
|
+
confidence: provider === "unknown" ? "likely" : "high"
|
|
631
|
+
};
|
|
545
632
|
}
|
|
546
|
-
function
|
|
547
|
-
|
|
633
|
+
function getWebhookProvider(normalizedRouteContext) {
|
|
634
|
+
if (normalizedRouteContext.includes("stripe-signature") || normalizedRouteContext.includes("stripe_webhook_secret") || normalizedRouteContext.includes("stripe.webhooks") || normalizedRouteContext.includes("constructevent(") || normalizedRouteContext.includes("stripe") && normalizedRouteContext.includes("webhook")) {
|
|
635
|
+
return "stripe";
|
|
636
|
+
}
|
|
637
|
+
if (normalizedRouteContext.includes("resend") && normalizedRouteContext.includes("webhook")) {
|
|
638
|
+
return "resend";
|
|
639
|
+
}
|
|
640
|
+
if (normalizedRouteContext.includes("clerk_webhook_secret") || normalizedRouteContext.includes("clerk") && normalizedRouteContext.includes("webhook")) {
|
|
641
|
+
return "clerk";
|
|
642
|
+
}
|
|
643
|
+
if (normalizedRouteContext.includes("x-github-event") || normalizedRouteContext.includes("x-hub-signature")) {
|
|
644
|
+
return "github";
|
|
645
|
+
}
|
|
646
|
+
if (normalizedRouteContext.includes("x-shopify-hmac-sha256")) {
|
|
647
|
+
return "shopify";
|
|
648
|
+
}
|
|
649
|
+
return "unknown";
|
|
548
650
|
}
|
|
549
|
-
function
|
|
550
|
-
const normalizedFile = relativeFile.toLowerCase();
|
|
651
|
+
function hasWebhookSignatureVerification(content, provider) {
|
|
551
652
|
const normalizedContent = content.toLowerCase();
|
|
552
|
-
|
|
653
|
+
if (provider === "stripe") {
|
|
654
|
+
return normalizedContent.includes("stripe.webhooks.constructevent") || normalizedContent.includes("webhooks.constructevent") || normalizedContent.includes("constructevent(");
|
|
655
|
+
}
|
|
656
|
+
if (provider === "clerk") {
|
|
657
|
+
return (normalizedContent.includes("new webhook(") || normalizedContent.includes("webhook(") || normalizedContent.includes("svix")) && (normalizedContent.includes(".verify(") || normalizedContent.includes("verify(") || normalizedContent.includes("verifywebhook"));
|
|
658
|
+
}
|
|
659
|
+
if (provider === "github") {
|
|
660
|
+
return (normalizedContent.includes("x-hub-signature") || normalizedContent.includes("x-hub-signature-256")) && hasHmacOrVerifyCall(normalizedContent);
|
|
661
|
+
}
|
|
662
|
+
if (provider === "shopify") {
|
|
663
|
+
return normalizedContent.includes("x-shopify-hmac-sha256") && hasHmacOrVerifyCall(normalizedContent);
|
|
664
|
+
}
|
|
665
|
+
if (provider === "resend") {
|
|
666
|
+
return normalizedContent.includes("verifywebhook") || (normalizedContent.includes("new webhook(") || normalizedContent.includes("webhook(") || normalizedContent.includes("svix")) && hasHmacOrVerifyCall(normalizedContent);
|
|
667
|
+
}
|
|
668
|
+
return normalizedContent.includes("constructevent(") || normalizedContent.includes("verifywebhook") || normalizedContent.includes("signature") && hasHmacOrVerifyCall(normalizedContent) || normalizedContent.includes("webhook") && normalizedContent.includes("verify(");
|
|
669
|
+
}
|
|
670
|
+
function hasHmacOrVerifyCall(normalizedContent) {
|
|
671
|
+
return normalizedContent.includes("verify(") || normalizedContent.includes(".verify(") || normalizedContent.includes("verifywebhook") || normalizedContent.includes("createhmac") || normalizedContent.includes("timingsafeequal") || normalizedContent.includes("subtle.verify");
|
|
672
|
+
}
|
|
673
|
+
function getWebhookSignatureSuggestion(provider) {
|
|
674
|
+
if (provider === "stripe") {
|
|
675
|
+
return "Use stripe.webhooks.constructEvent(...) with the Stripe signature header before handling the event.";
|
|
676
|
+
}
|
|
677
|
+
if (provider === "clerk") {
|
|
678
|
+
return "Verify the event with Svix before handling it.";
|
|
679
|
+
}
|
|
680
|
+
if (provider === "github") {
|
|
681
|
+
return "Verify the GitHub signature using the raw request body, X-Hub-Signature-256 header, and webhook secret.";
|
|
682
|
+
}
|
|
683
|
+
if (provider === "shopify") {
|
|
684
|
+
return "Verify the Shopify HMAC using the raw request body, X-Shopify-Hmac-Sha256 header, and webhook secret.";
|
|
685
|
+
}
|
|
686
|
+
if (provider === "resend") {
|
|
687
|
+
return "Verify the Resend webhook signature before handling the event.";
|
|
688
|
+
}
|
|
689
|
+
return "Verify the provider signature using the raw request body and signature header before trusting the event.";
|
|
690
|
+
}
|
|
691
|
+
function createApiAuthFixPrompt(file) {
|
|
692
|
+
return `Review the API route at ${file}.
|
|
693
|
+
|
|
694
|
+
Goal:
|
|
695
|
+
Determine whether this route should be public or protected.
|
|
696
|
+
|
|
697
|
+
Instructions:
|
|
698
|
+
- Inspect the existing authentication/session pattern used in this project.
|
|
699
|
+
- If this route handles private data, user-specific data, uploads, writes, or admin actions, add the existing auth/session check.
|
|
700
|
+
- Do not introduce a new auth provider.
|
|
701
|
+
- Do not refactor unrelated code.
|
|
702
|
+
- Keep the current behavior unchanged.
|
|
703
|
+
- If this route is intentionally public, add a short comment explaining why.
|
|
704
|
+
|
|
705
|
+
Return:
|
|
706
|
+
- A short explanation of what you changed.
|
|
707
|
+
- The updated code.
|
|
708
|
+
- Any edge cases I should test.`;
|
|
553
709
|
}
|
|
554
|
-
function
|
|
555
|
-
return
|
|
710
|
+
function createMissingEnvVariableFixPrompt(variableName, files) {
|
|
711
|
+
return `Update the environment documentation for this project.
|
|
712
|
+
|
|
713
|
+
The variable ${variableName} is used in ${formatPromptFileList(files)} but is missing from .env.example.
|
|
714
|
+
|
|
715
|
+
Instructions:
|
|
716
|
+
- Add ${variableName}= to .env.example.
|
|
717
|
+
- Do not add a real secret value.
|
|
718
|
+
- Check if related environment variables used in the same file should also be documented.
|
|
719
|
+
- Keep comments clear and safe for public repos.
|
|
720
|
+
|
|
721
|
+
Return:
|
|
722
|
+
- The updated .env.example lines.
|
|
723
|
+
- A short explanation.`;
|
|
724
|
+
}
|
|
725
|
+
function createLargeFileFixPrompt(file) {
|
|
726
|
+
return `Review ${file}.
|
|
727
|
+
|
|
728
|
+
Qodfy detected this as a large file.
|
|
729
|
+
|
|
730
|
+
Goal:
|
|
731
|
+
Suggest a safe refactor plan without changing behavior.
|
|
732
|
+
|
|
733
|
+
Instructions:
|
|
734
|
+
- Identify the main responsibilities inside the file.
|
|
735
|
+
- Suggest smaller components, hooks, or utility files that can be extracted.
|
|
736
|
+
- Do not rewrite the whole file at once.
|
|
737
|
+
- Do not change UI behavior.
|
|
738
|
+
- Do not change business logic.
|
|
739
|
+
- Prioritize low-risk extractions first.
|
|
740
|
+
|
|
741
|
+
Return:
|
|
742
|
+
- A short responsibility breakdown.
|
|
743
|
+
- A step-by-step refactor plan.
|
|
744
|
+
- The safest first extraction.`;
|
|
745
|
+
}
|
|
746
|
+
function createAiRateLimitFixPrompt(file) {
|
|
747
|
+
return `Review the AI-related API route at ${file}.
|
|
748
|
+
|
|
749
|
+
Goal:
|
|
750
|
+
Add cost and abuse protection safely.
|
|
751
|
+
|
|
752
|
+
Instructions:
|
|
753
|
+
- Check the existing project patterns for auth, usage limits, or rate limiting.
|
|
754
|
+
- If this route can be called by users, add rate limiting or per-user usage protection.
|
|
755
|
+
- Do not introduce a new service unless necessary.
|
|
756
|
+
- Do not change the AI provider or model behavior.
|
|
757
|
+
- Keep the current response format unchanged.
|
|
758
|
+
- If the route is intentionally public, explain why and recommend a safe limit.
|
|
759
|
+
|
|
760
|
+
Return:
|
|
761
|
+
- The safest protection approach.
|
|
762
|
+
- The updated code.
|
|
763
|
+
- Any environment variables required.`;
|
|
764
|
+
}
|
|
765
|
+
function createWebhookSignatureFixPrompt(file) {
|
|
766
|
+
return `Review the webhook API route at ${file}.
|
|
767
|
+
|
|
768
|
+
Goal:
|
|
769
|
+
Verify that webhook signature validation happens before the event is handled.
|
|
770
|
+
|
|
771
|
+
Instructions:
|
|
772
|
+
- Detect which provider this webhook belongs to based on imports, headers, and environment variables.
|
|
773
|
+
- Use the provider's existing verification pattern if already present.
|
|
774
|
+
- Do not process the webhook event before verification unless required by the provider.
|
|
775
|
+
- Do not introduce unrelated changes.
|
|
776
|
+
- If verification already exists, explain where it happens.
|
|
777
|
+
|
|
778
|
+
Return:
|
|
779
|
+
- Whether signature verification exists.
|
|
780
|
+
- If missing, the safest code change.
|
|
781
|
+
- Any test cases to run.`;
|
|
782
|
+
}
|
|
783
|
+
function createClientSideSecretFixPrompt(file, variableName) {
|
|
784
|
+
return `Review the client-side file at ${file}.
|
|
785
|
+
|
|
786
|
+
Qodfy found ${variableName}, which does not start with NEXT_PUBLIC_.
|
|
787
|
+
|
|
788
|
+
Goal:
|
|
789
|
+
Confirm whether this environment variable may be exposed to the browser.
|
|
790
|
+
|
|
791
|
+
Instructions:
|
|
792
|
+
- Check whether this file is a client component or browser-executed code.
|
|
793
|
+
- If ${variableName} is server-only, move access to a server component, API route, or server action.
|
|
794
|
+
- Do not rename environment variables unless necessary.
|
|
795
|
+
- Do not add real secret values.
|
|
796
|
+
- Keep existing behavior unchanged.
|
|
797
|
+
|
|
798
|
+
Return:
|
|
799
|
+
- Whether the variable is safe in this file.
|
|
800
|
+
- The safest code change if it is not safe.
|
|
801
|
+
- Any edge cases I should test.`;
|
|
802
|
+
}
|
|
803
|
+
function createHardcodedSecretFixPrompt(file, secretLabel) {
|
|
804
|
+
return `Review ${file} for a possible hardcoded ${secretLabel}.
|
|
805
|
+
|
|
806
|
+
Goal:
|
|
807
|
+
Remove any real secret from source code without changing behavior.
|
|
808
|
+
|
|
809
|
+
Instructions:
|
|
810
|
+
- Do not print or copy the secret value in your response.
|
|
811
|
+
- Move the value to an environment variable if it is a real secret.
|
|
812
|
+
- Add only the variable name to .env.example.
|
|
813
|
+
- Recommend rotating the secret if it may have been committed.
|
|
814
|
+
- Do not refactor unrelated code.
|
|
815
|
+
|
|
816
|
+
Return:
|
|
817
|
+
- Whether this looks like a real secret.
|
|
818
|
+
- The safest code change.
|
|
819
|
+
- Any follow-up security steps.`;
|
|
820
|
+
}
|
|
821
|
+
function createMissingEnvExampleFixPrompt() {
|
|
822
|
+
return `Create or update .env.example for this project.
|
|
823
|
+
|
|
824
|
+
Goal:
|
|
825
|
+
Document the environment variables required to run and deploy the app.
|
|
826
|
+
|
|
827
|
+
Instructions:
|
|
828
|
+
- Inspect process.env usage in the project.
|
|
829
|
+
- Add variable names only.
|
|
830
|
+
- Do not add real secret values.
|
|
831
|
+
- Use empty placeholders like VARIABLE_NAME=.
|
|
832
|
+
- Add short comments only where they help future maintainers.
|
|
833
|
+
|
|
834
|
+
Return:
|
|
835
|
+
- The proposed .env.example content.
|
|
836
|
+
- A short explanation of any variables that need manual confirmation.`;
|
|
837
|
+
}
|
|
838
|
+
function createProjectRootFixPrompt() {
|
|
839
|
+
return `Review how Qodfy is being run for this project.
|
|
840
|
+
|
|
841
|
+
Goal:
|
|
842
|
+
Make sure the scanner is pointed at the correct app root.
|
|
843
|
+
|
|
844
|
+
Instructions:
|
|
845
|
+
- Find the folder that contains the app package.json.
|
|
846
|
+
- If this is a monorepo, identify the Next.js app folder.
|
|
847
|
+
- Do not move files or refactor the project.
|
|
848
|
+
- Recommend the correct qodfy scan --path command.
|
|
849
|
+
|
|
850
|
+
Return:
|
|
851
|
+
- The correct folder to scan.
|
|
852
|
+
- The exact command to run.`;
|
|
853
|
+
}
|
|
854
|
+
function createPackageJsonFixPrompt() {
|
|
855
|
+
return `Review package.json.
|
|
856
|
+
|
|
857
|
+
Goal:
|
|
858
|
+
Make package.json readable so tooling can detect the project correctly.
|
|
859
|
+
|
|
860
|
+
Instructions:
|
|
861
|
+
- Check for invalid JSON syntax.
|
|
862
|
+
- Keep existing dependencies and scripts unchanged unless they are malformed.
|
|
863
|
+
- Do not upgrade dependencies.
|
|
864
|
+
- Do not refactor unrelated files.
|
|
865
|
+
|
|
866
|
+
Return:
|
|
867
|
+
- The corrected package.json change.
|
|
868
|
+
- A short explanation.`;
|
|
869
|
+
}
|
|
870
|
+
function createNextNotDetectedFixPrompt() {
|
|
871
|
+
return `Review this project structure.
|
|
872
|
+
|
|
873
|
+
Goal:
|
|
874
|
+
Determine whether Qodfy is scanning the correct Next.js app folder.
|
|
875
|
+
|
|
876
|
+
Instructions:
|
|
877
|
+
- Check whether this is a monorepo.
|
|
878
|
+
- Find the package.json that includes next as a dependency.
|
|
879
|
+
- Do not install or remove packages.
|
|
880
|
+
- Recommend the correct qodfy scan --path command if needed.
|
|
881
|
+
|
|
882
|
+
Return:
|
|
883
|
+
- Whether this is a Next.js app.
|
|
884
|
+
- The exact folder Qodfy should scan.`;
|
|
885
|
+
}
|
|
886
|
+
function createReadmeFixPrompt() {
|
|
887
|
+
return `Create a practical README for this project.
|
|
888
|
+
|
|
889
|
+
Goal:
|
|
890
|
+
Help developers run, configure, and maintain the app.
|
|
891
|
+
|
|
892
|
+
Instructions:
|
|
893
|
+
- Include setup commands, environment variable documentation, local development, build, and deployment notes.
|
|
894
|
+
- Do not include real secret values.
|
|
895
|
+
- Keep the README concise and accurate.
|
|
896
|
+
- Do not invent features that are not in the project.
|
|
897
|
+
|
|
898
|
+
Return:
|
|
899
|
+
- The README content.
|
|
900
|
+
- Any assumptions that need confirmation.`;
|
|
901
|
+
}
|
|
902
|
+
function formatPromptFileList(files) {
|
|
903
|
+
if (files.length === 1) {
|
|
904
|
+
return files[0];
|
|
905
|
+
}
|
|
906
|
+
return `${files.length} files: ${formatFileList(files)}`;
|
|
556
907
|
}
|
|
557
908
|
function isPackageJsonObject(data) {
|
|
558
909
|
return typeof data === "object" && data !== null && !Array.isArray(data);
|