@qodfy/core 0.1.4 → 0.1.5
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 +89 -24
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -41,6 +41,16 @@ var aiKeywords = [
|
|
|
41
41
|
"streamText",
|
|
42
42
|
"useChat"
|
|
43
43
|
];
|
|
44
|
+
var ignoredEnvVariables = /* @__PURE__ */ new Set([
|
|
45
|
+
"CI",
|
|
46
|
+
"HOME",
|
|
47
|
+
"NODE_ENV",
|
|
48
|
+
"PORT",
|
|
49
|
+
"PWD",
|
|
50
|
+
"VERCEL",
|
|
51
|
+
"VERCEL_ENV",
|
|
52
|
+
"VERCEL_URL"
|
|
53
|
+
]);
|
|
44
54
|
var hardcodedSecretPatterns = [
|
|
45
55
|
{
|
|
46
56
|
label: "OpenAI API key",
|
|
@@ -75,7 +85,8 @@ var hardcodedSecretPatterns = [
|
|
|
75
85
|
pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g
|
|
76
86
|
}
|
|
77
87
|
];
|
|
78
|
-
var LARGE_FILE_WARNING_BYTES =
|
|
88
|
+
var LARGE_FILE_WARNING_BYTES = 40 * 1024;
|
|
89
|
+
var LARGE_FILE_REPORT_LIMIT = 10;
|
|
79
90
|
var MAX_FILE_SIZE_BYTES = 500 * 1024;
|
|
80
91
|
async function scanProject(projectPath) {
|
|
81
92
|
const startTime = Date.now();
|
|
@@ -162,9 +173,10 @@ async function scanProject(projectPath) {
|
|
|
162
173
|
return isApiRoute(file);
|
|
163
174
|
});
|
|
164
175
|
const apiRouteSet = new Set(apiRoutes);
|
|
165
|
-
const
|
|
176
|
+
const missingEnvUsages = /* @__PURE__ */ new Map();
|
|
166
177
|
const clientSecretWarningKeys = /* @__PURE__ */ new Set();
|
|
167
178
|
const hardcodedSecretWarningKeys = /* @__PURE__ */ new Set();
|
|
179
|
+
const largeFileCandidates = [];
|
|
168
180
|
let aiFiles = 0;
|
|
169
181
|
let largeFiles = 0;
|
|
170
182
|
for (const file of files) {
|
|
@@ -203,12 +215,9 @@ async function scanProject(projectPath) {
|
|
|
203
215
|
const content = fileResult.content;
|
|
204
216
|
if (statResult.size > LARGE_FILE_WARNING_BYTES) {
|
|
205
217
|
largeFiles++;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
message: "Large files are harder to maintain and often appear in AI-generated codebases.",
|
|
210
|
-
file: relativeFile,
|
|
211
|
-
suggestion: "Consider splitting this file into smaller modules if it mixes unrelated responsibilities."
|
|
218
|
+
largeFileCandidates.push({
|
|
219
|
+
relativeFile,
|
|
220
|
+
size: statResult.size
|
|
212
221
|
});
|
|
213
222
|
}
|
|
214
223
|
const usesAI = aiKeywords.some(
|
|
@@ -229,6 +238,8 @@ async function scanProject(projectPath) {
|
|
|
229
238
|
}
|
|
230
239
|
const isStripeWebhook = isStripeWebhookRoute(relativeFile, content);
|
|
231
240
|
const hasStripeSignatureVerification = hasStripeWebhookSignatureVerification(content);
|
|
241
|
+
const isClerkWebhook = isClerkWebhookRoute(relativeFile, content);
|
|
242
|
+
const hasClerkSignatureVerification = hasClerkWebhookSignatureVerification(content);
|
|
232
243
|
if (isStripeWebhook && !hasStripeSignatureVerification) {
|
|
233
244
|
issues.push({
|
|
234
245
|
severity: "critical",
|
|
@@ -238,6 +249,15 @@ async function scanProject(projectPath) {
|
|
|
238
249
|
suggestion: "Use stripe.webhooks.constructEvent with the raw request body, Stripe-Signature header, and STRIPE_WEBHOOK_SECRET."
|
|
239
250
|
});
|
|
240
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.",
|
|
257
|
+
file: relativeFile,
|
|
258
|
+
suggestion: "Use svix Webhook(...).verify(...) with CLERK_WEBHOOK_SECRET and Clerk webhook headers."
|
|
259
|
+
});
|
|
260
|
+
}
|
|
241
261
|
for (const secretMatch of getHardcodedSecretMatches(content)) {
|
|
242
262
|
const warningKey = `${relativeFile}:${secretMatch.label}`;
|
|
243
263
|
if (hardcodedSecretWarningKeys.has(warningKey)) {
|
|
@@ -254,7 +274,7 @@ async function scanProject(projectPath) {
|
|
|
254
274
|
}
|
|
255
275
|
if (apiRouteSet.has(file)) {
|
|
256
276
|
const hasAuth = content.includes("auth(") || content.includes("getServerSession") || content.includes("currentUser") || content.includes("clerkClient") || content.includes("session");
|
|
257
|
-
if (!hasAuth && !isStripeWebhook) {
|
|
277
|
+
if (!hasAuth && !isStripeWebhook && !isClerkWebhook) {
|
|
258
278
|
issues.push({
|
|
259
279
|
severity: "warning",
|
|
260
280
|
title: "API route may be missing authentication",
|
|
@@ -267,25 +287,17 @@ async function scanProject(projectPath) {
|
|
|
267
287
|
const usedEnvVariables = getUsedEnvVariables(content);
|
|
268
288
|
if (envExampleVariables) {
|
|
269
289
|
for (const variableName of usedEnvVariables) {
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
if (envExampleWarningKeys.has(warningKey)) {
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
envExampleWarningKeys.add(warningKey);
|
|
276
|
-
issues.push({
|
|
277
|
-
severity: "warning",
|
|
278
|
-
title: "Environment variable missing from .env.example",
|
|
279
|
-
message: `${variableName} is used in ${relativeFile} but is not documented in .env.example.`,
|
|
280
|
-
file: relativeFile,
|
|
281
|
-
suggestion: `Add ${variableName}= to .env.example without including a real value.`
|
|
282
|
-
});
|
|
290
|
+
if (shouldIgnoreEnvVariable(variableName) || envExampleVariables.has(variableName)) {
|
|
291
|
+
continue;
|
|
283
292
|
}
|
|
293
|
+
const filesUsingVariable = missingEnvUsages.get(variableName) ?? /* @__PURE__ */ new Set();
|
|
294
|
+
filesUsingVariable.add(relativeFile);
|
|
295
|
+
missingEnvUsages.set(variableName, filesUsingVariable);
|
|
284
296
|
}
|
|
285
297
|
}
|
|
286
298
|
if (isClientSideFile(relativeFile, content)) {
|
|
287
299
|
for (const variableName of usedEnvVariables) {
|
|
288
|
-
if (!variableName.startsWith("NEXT_PUBLIC_")) {
|
|
300
|
+
if (!variableName.startsWith("NEXT_PUBLIC_") && !shouldIgnoreEnvVariable(variableName)) {
|
|
289
301
|
const warningKey = `${relativeFile}:${variableName}`;
|
|
290
302
|
if (clientSecretWarningKeys.has(warningKey)) {
|
|
291
303
|
continue;
|
|
@@ -302,9 +314,29 @@ async function scanProject(projectPath) {
|
|
|
302
314
|
}
|
|
303
315
|
}
|
|
304
316
|
}
|
|
317
|
+
for (const largeFile of getReportedLargeFiles(largeFileCandidates)) {
|
|
318
|
+
issues.push({
|
|
319
|
+
severity: "info",
|
|
320
|
+
title: "Large file detected",
|
|
321
|
+
message: "Large files are harder to maintain and often appear in AI-generated codebases.",
|
|
322
|
+
file: largeFile.relativeFile,
|
|
323
|
+
suggestion: "Consider splitting this file into smaller modules if it mixes unrelated responsibilities."
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
for (const [variableName, filesUsingVariable] of getSortedMissingEnvUsages(missingEnvUsages)) {
|
|
327
|
+
const files2 = [...filesUsingVariable].sort();
|
|
328
|
+
issues.push({
|
|
329
|
+
severity: "warning",
|
|
330
|
+
title: "Environment variable missing from .env.example",
|
|
331
|
+
message: getMissingEnvMessage(variableName, files2),
|
|
332
|
+
file: files2.length === 1 ? files2[0] : void 0,
|
|
333
|
+
suggestion: `Add ${variableName}= to .env.example without including a real value.`
|
|
334
|
+
});
|
|
335
|
+
}
|
|
305
336
|
const criticalCount = issues.filter((issue) => issue.severity === "critical").length;
|
|
306
337
|
const warningCount = issues.filter((issue) => issue.severity === "warning").length;
|
|
307
|
-
const
|
|
338
|
+
const warningPenalty = Math.min(warningCount * 5, 50);
|
|
339
|
+
const score = Math.max(0, 100 - criticalCount * 20 - warningPenalty);
|
|
308
340
|
return {
|
|
309
341
|
projectPath: resolvedProjectPath,
|
|
310
342
|
isNextProject,
|
|
@@ -454,6 +486,31 @@ function getUsedEnvVariables(content) {
|
|
|
454
486
|
}
|
|
455
487
|
return variables;
|
|
456
488
|
}
|
|
489
|
+
function shouldIgnoreEnvVariable(variableName) {
|
|
490
|
+
return ignoredEnvVariables.has(variableName);
|
|
491
|
+
}
|
|
492
|
+
function getReportedLargeFiles(largeFileCandidates) {
|
|
493
|
+
return [...largeFileCandidates].sort((a, b) => b.size - a.size).slice(0, LARGE_FILE_REPORT_LIMIT);
|
|
494
|
+
}
|
|
495
|
+
function getSortedMissingEnvUsages(missingEnvUsages) {
|
|
496
|
+
return [...missingEnvUsages.entries()].sort(
|
|
497
|
+
([leftVariable], [rightVariable]) => leftVariable.localeCompare(rightVariable)
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
function getMissingEnvMessage(variableName, files) {
|
|
501
|
+
if (files.length === 1) {
|
|
502
|
+
return `${variableName} is used in ${files[0]} but is not documented in .env.example.`;
|
|
503
|
+
}
|
|
504
|
+
return `${variableName} is used in ${files.length} files but is not documented in .env.example. Files: ${formatFileList(files)}.`;
|
|
505
|
+
}
|
|
506
|
+
function formatFileList(files) {
|
|
507
|
+
const filesToShow = files.slice(0, 5);
|
|
508
|
+
const remainingCount = files.length - filesToShow.length;
|
|
509
|
+
if (remainingCount <= 0) {
|
|
510
|
+
return filesToShow.join(", ");
|
|
511
|
+
}
|
|
512
|
+
return `${filesToShow.join(", ")} and ${remainingCount} more`;
|
|
513
|
+
}
|
|
457
514
|
function parseDestructuredEnvNames(destructuredContent) {
|
|
458
515
|
const variables = [];
|
|
459
516
|
for (const part of destructuredContent.split(",")) {
|
|
@@ -489,6 +546,14 @@ function isStripeWebhookRoute(relativeFile, content) {
|
|
|
489
546
|
function hasStripeWebhookSignatureVerification(content) {
|
|
490
547
|
return content.includes("stripe.webhooks.constructEvent") || content.includes("webhooks.constructEvent") || content.includes("constructEvent(");
|
|
491
548
|
}
|
|
549
|
+
function isClerkWebhookRoute(relativeFile, content) {
|
|
550
|
+
const normalizedFile = relativeFile.toLowerCase();
|
|
551
|
+
const normalizedContent = content.toLowerCase();
|
|
552
|
+
return (normalizedContent.includes("clerk") || normalizedContent.includes("clerk_webhook_secret")) && (normalizedFile.includes("webhook") || normalizedContent.includes("webhook"));
|
|
553
|
+
}
|
|
554
|
+
function hasClerkWebhookSignatureVerification(content) {
|
|
555
|
+
return content.includes("new Webhook(") || content.includes(".verify(") || content.includes("svix");
|
|
556
|
+
}
|
|
492
557
|
function isPackageJsonObject(data) {
|
|
493
558
|
return typeof data === "object" && data !== null && !Array.isArray(data);
|
|
494
559
|
}
|