@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.
Files changed (2) hide show
  1. package/dist/index.js +89 -24
  2. 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 = 15e3;
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 envExampleWarningKeys = /* @__PURE__ */ new Set();
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
- issues.push({
207
- severity: "info",
208
- title: "Large file detected",
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 (!envExampleVariables.has(variableName)) {
271
- const warningKey = `${relativeFile}:${variableName}`;
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 score = Math.max(0, 100 - criticalCount * 20 - warningCount * 8);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qodfy/core",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Scanner engine for Qodfy, an open-source launch readiness scanner for AI-built apps.",
5
5
  "keywords": [
6
6
  "qodfy",