@qodfy/core 0.2.0 → 0.2.1

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 CHANGED
@@ -1,5 +1,8 @@
1
1
  type IssueSeverity = "critical" | "warning" | "info";
2
2
  type IssueCategory = "security" | "environment" | "api" | "webhook" | "ai" | "maintainability" | "project";
3
+ declare const validScanChecks: readonly ["project", "api", "environment", "ai", "webhook", "maintainability", "security"];
4
+ type ScanCheck = typeof validScanChecks[number];
5
+ declare const recommendedScanChecks: ScanCheck[];
3
6
  type Issue = {
4
7
  id: string;
5
8
  ruleId: string;
@@ -24,6 +27,10 @@ type ScanReport = {
24
27
  durationMs: number;
25
28
  };
26
29
  };
27
- declare function scanProject(projectPath: string): Promise<ScanReport>;
30
+ type ScanOptions = {
31
+ projectPath: string;
32
+ checks?: ScanCheck[];
33
+ };
34
+ declare function scanProject(input: string | ScanOptions): Promise<ScanReport>;
28
35
 
29
- export { type Issue, type IssueCategory, type IssueSeverity, type ScanReport, scanProject };
36
+ export { type Issue, type IssueCategory, type IssueSeverity, type ScanCheck, type ScanOptions, type ScanReport, recommendedScanChecks, scanProject, validScanChecks };
package/dist/index.js CHANGED
@@ -2,6 +2,23 @@
2
2
  import fs from "fs/promises";
3
3
  import path from "path";
4
4
  import fg from "fast-glob";
5
+ var validScanChecks = [
6
+ "project",
7
+ "api",
8
+ "environment",
9
+ "ai",
10
+ "webhook",
11
+ "maintainability",
12
+ "security"
13
+ ];
14
+ var recommendedScanChecks = [
15
+ "project",
16
+ "api",
17
+ "environment",
18
+ "ai",
19
+ "webhook",
20
+ "maintainability"
21
+ ];
5
22
  var sourceFilePatterns = ["**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}"];
6
23
  var ignoredPaths = [
7
24
  "node_modules/**",
@@ -105,15 +122,28 @@ var issueIdPrefixes = {
105
122
  "project-source-files-unreadable": "project-source-files-unreadable",
106
123
  "webhook-missing-signature-verification": "webhook-signature-verification"
107
124
  };
108
- async function scanProject(projectPath) {
125
+ async function scanProject(input) {
109
126
  const startTime = Date.now();
127
+ const projectPath = typeof input === "string" ? input : input.projectPath;
128
+ const enabledChecks = getEnabledChecks(
129
+ typeof input === "string" ? void 0 : input.checks
130
+ );
110
131
  const resolvedProjectPath = path.resolve(projectPath);
111
132
  const issues = [];
112
133
  const addIssue = createIssueFactory(issues);
134
+ const runProjectChecks = hasCheck(enabledChecks, "project");
135
+ const runEnvironmentChecks = hasCheck(enabledChecks, "environment");
136
+ const runApiChecks = hasCheck(enabledChecks, "api") || hasCheck(enabledChecks, "security");
137
+ const runAiChecks = hasCheck(enabledChecks, "ai");
138
+ const runWebhookChecks = hasCheck(enabledChecks, "webhook") || hasCheck(enabledChecks, "security");
139
+ const runMaintainabilityChecks = hasCheck(enabledChecks, "maintainability");
140
+ const runSecurityChecks = hasCheck(enabledChecks, "security");
141
+ const shouldScanSourceFiles = enabledChecks.size > 0 && !onlyHasCheck(enabledChecks, "project");
142
+ const shouldReadSourceContent = runEnvironmentChecks || runApiChecks || runAiChecks || runWebhookChecks || runSecurityChecks;
113
143
  const packageJsonPath = path.join(resolvedProjectPath, "package.json");
114
144
  const hasPackageJson = await fileExists(packageJsonPath);
115
145
  let isNextProject = false;
116
- if (!hasPackageJson) {
146
+ if (runProjectChecks && !hasPackageJson) {
117
147
  addIssue({
118
148
  ruleId: "project-missing-package-json",
119
149
  category: "project",
@@ -123,7 +153,7 @@ async function scanProject(projectPath) {
123
153
  suggestion: "Run Qodfy from the project root or pass --path to the app folder.",
124
154
  fixPrompt: createProjectRootFixPrompt()
125
155
  });
126
- } else {
156
+ } else if (runProjectChecks) {
127
157
  const packageJsonResult = await safeReadJson(packageJsonPath);
128
158
  if (!packageJsonResult.ok) {
129
159
  addIssue({
@@ -167,9 +197,9 @@ async function scanProject(projectPath) {
167
197
  }
168
198
  }
169
199
  const envExamplePath = path.join(resolvedProjectPath, ".env.example");
170
- const hasEnvExample = await fileExists(envExamplePath);
200
+ const hasEnvExample = runEnvironmentChecks ? await fileExists(envExamplePath) : false;
171
201
  let envExampleVariables = null;
172
- if (!hasEnvExample) {
202
+ if (runEnvironmentChecks && !hasEnvExample) {
173
203
  addIssue({
174
204
  ruleId: "environment-missing-env-example",
175
205
  category: "environment",
@@ -179,7 +209,7 @@ async function scanProject(projectPath) {
179
209
  suggestion: "Document required variable names only, never real secret values.",
180
210
  fixPrompt: createMissingEnvExampleFixPrompt()
181
211
  });
182
- } else {
212
+ } else if (runEnvironmentChecks) {
183
213
  const envExampleResult = await safeReadFile(envExamplePath);
184
214
  if (!envExampleResult.ok) {
185
215
  addIssue({
@@ -196,8 +226,8 @@ async function scanProject(projectPath) {
196
226
  envExampleVariables = getEnvExampleVariables(envExampleResult.content);
197
227
  }
198
228
  }
199
- const hasReadme = await fileExists(path.join(resolvedProjectPath, "README.md"));
200
- if (!hasReadme) {
229
+ const hasReadme = runProjectChecks ? await fileExists(path.join(resolvedProjectPath, "README.md")) : true;
230
+ if (runProjectChecks && !hasReadme) {
201
231
  addIssue({
202
232
  ruleId: "project-missing-readme",
203
233
  category: "project",
@@ -207,7 +237,7 @@ async function scanProject(projectPath) {
207
237
  fixPrompt: createReadmeFixPrompt()
208
238
  });
209
239
  }
210
- const files = await getSourceFiles(resolvedProjectPath, addIssue);
240
+ const files = shouldScanSourceFiles ? await getSourceFiles(resolvedProjectPath, addIssue) : [];
211
241
  const apiRoutes = files.filter((file) => {
212
242
  return isApiRoute(file);
213
243
  });
@@ -222,56 +252,65 @@ async function scanProject(projectPath) {
222
252
  const relativeFile = normalizePath(path.relative(resolvedProjectPath, file));
223
253
  const statResult = await safeStatFile(file);
224
254
  if (!statResult.ok) {
225
- addIssue({
226
- ruleId: "maintainability-file-unreadable",
227
- category: "maintainability",
228
- severity: "info",
229
- title: "File could not be checked",
230
- message: statResult.reason,
231
- file: relativeFile,
232
- suggestion: "Check file permissions if this file should be included in launch-readiness scans."
233
- });
255
+ if (runMaintainabilityChecks) {
256
+ addIssue({
257
+ ruleId: "maintainability-file-unreadable",
258
+ category: "maintainability",
259
+ severity: "info",
260
+ title: "File could not be checked",
261
+ message: statResult.reason,
262
+ file: relativeFile,
263
+ suggestion: "Check file permissions if this file should be included in launch-readiness scans."
264
+ });
265
+ }
234
266
  continue;
235
267
  }
236
268
  if (statResult.size > MAX_FILE_SIZE_BYTES) {
269
+ if (runMaintainabilityChecks) {
270
+ largeFiles++;
271
+ addIssue({
272
+ ruleId: "maintainability-large-file-skipped",
273
+ category: "maintainability",
274
+ severity: "info",
275
+ title: "Large file skipped from deep scan",
276
+ message: "This file is larger than 500KB and was skipped from deep content checks.",
277
+ file: relativeFile,
278
+ suggestion: "Review large generated or bundled files manually.",
279
+ fixPrompt: createLargeFileFixPrompt(relativeFile)
280
+ });
281
+ }
282
+ continue;
283
+ }
284
+ if (runMaintainabilityChecks && statResult.size > LARGE_FILE_WARNING_BYTES) {
237
285
  largeFiles++;
238
- addIssue({
239
- ruleId: "maintainability-large-file-skipped",
240
- category: "maintainability",
241
- severity: "info",
242
- title: "Large file skipped from deep scan",
243
- message: "This file is larger than 500KB and was skipped from deep content checks.",
244
- file: relativeFile,
245
- suggestion: "Review large generated or bundled files manually.",
246
- fixPrompt: createLargeFileFixPrompt(relativeFile)
286
+ largeFileCandidates.push({
287
+ relativeFile,
288
+ size: statResult.size
247
289
  });
290
+ }
291
+ if (!shouldReadSourceContent) {
248
292
  continue;
249
293
  }
250
294
  const fileResult = await safeReadFile(file);
251
295
  if (!fileResult.ok) {
252
- addIssue({
253
- ruleId: "maintainability-file-unreadable",
254
- category: "maintainability",
255
- severity: "info",
256
- title: "File could not be read",
257
- message: fileResult.reason,
258
- file: relativeFile,
259
- suggestion: "Check file permissions if this file should be included in launch-readiness scans."
260
- });
296
+ if (runMaintainabilityChecks) {
297
+ addIssue({
298
+ ruleId: "maintainability-file-unreadable",
299
+ category: "maintainability",
300
+ severity: "info",
301
+ title: "File could not be read",
302
+ message: fileResult.reason,
303
+ file: relativeFile,
304
+ suggestion: "Check file permissions if this file should be included in launch-readiness scans."
305
+ });
306
+ }
261
307
  continue;
262
308
  }
263
309
  const content = fileResult.content;
264
- if (statResult.size > LARGE_FILE_WARNING_BYTES) {
265
- largeFiles++;
266
- largeFileCandidates.push({
267
- relativeFile,
268
- size: statResult.size
269
- });
270
- }
271
310
  const usesAI = aiKeywords.some(
272
311
  (keyword) => content.toLowerCase().includes(keyword.toLowerCase())
273
312
  );
274
- if (usesAI) {
313
+ if (runAiChecks && usesAI) {
275
314
  aiFiles++;
276
315
  const hasRateLimit = content.includes("rateLimit") || content.includes("ratelimit") || content.includes("upstash") || content.includes("limiter");
277
316
  if (apiRouteSet.has(file) && !hasRateLimit) {
@@ -287,7 +326,7 @@ async function scanProject(projectPath) {
287
326
  });
288
327
  }
289
328
  }
290
- const webhookRouteInfo = apiRouteSet.has(file) ? getWebhookRouteInfo(relativeFile, content) : null;
329
+ const webhookRouteInfo = runWebhookChecks && apiRouteSet.has(file) ? getWebhookRouteInfo(relativeFile, content) : null;
291
330
  if (webhookRouteInfo && !hasWebhookSignatureVerification(content, webhookRouteInfo.provider)) {
292
331
  addIssue({
293
332
  ruleId: "webhook-missing-signature-verification",
@@ -300,24 +339,26 @@ async function scanProject(projectPath) {
300
339
  fixPrompt: createWebhookSignatureFixPrompt(relativeFile)
301
340
  });
302
341
  }
303
- for (const secretMatch of getHardcodedSecretMatches(content)) {
304
- const warningKey = `${relativeFile}:${secretMatch.label}`;
305
- if (hardcodedSecretWarningKeys.has(warningKey)) {
306
- continue;
342
+ if (runSecurityChecks) {
343
+ for (const secretMatch of getHardcodedSecretMatches(content)) {
344
+ const warningKey = `${relativeFile}:${secretMatch.label}`;
345
+ if (hardcodedSecretWarningKeys.has(warningKey)) {
346
+ continue;
347
+ }
348
+ hardcodedSecretWarningKeys.add(warningKey);
349
+ addIssue({
350
+ ruleId: "security-hardcoded-secret",
351
+ category: "security",
352
+ severity: "critical",
353
+ title: "Possible hardcoded secret",
354
+ message: `A string literal in ${relativeFile} matches the pattern for ${secretMatch.label}. Qodfy does not print possible secret values.`,
355
+ file: relativeFile,
356
+ suggestion: "Move secrets into environment variables and rotate the value if this is a real secret.",
357
+ fixPrompt: createHardcodedSecretFixPrompt(relativeFile, secretMatch.label)
358
+ });
307
359
  }
308
- hardcodedSecretWarningKeys.add(warningKey);
309
- addIssue({
310
- ruleId: "security-hardcoded-secret",
311
- category: "security",
312
- severity: "critical",
313
- title: "Possible hardcoded secret",
314
- message: `A string literal in ${relativeFile} matches the pattern for ${secretMatch.label}. Qodfy does not print possible secret values.`,
315
- file: relativeFile,
316
- suggestion: "Move secrets into environment variables and rotate the value if this is a real secret.",
317
- fixPrompt: createHardcodedSecretFixPrompt(relativeFile, secretMatch.label)
318
- });
319
360
  }
320
- if (apiRouteSet.has(file)) {
361
+ if (runApiChecks && apiRouteSet.has(file)) {
321
362
  const hasAuth = content.includes("auth(") || content.includes("getServerSession") || content.includes("currentUser") || content.includes("clerkClient") || content.includes("session");
322
363
  if (!hasAuth && !webhookRouteInfo) {
323
364
  addIssue({
@@ -332,8 +373,8 @@ async function scanProject(projectPath) {
332
373
  });
333
374
  }
334
375
  }
335
- const usedEnvVariables = getUsedEnvVariables(content);
336
- if (envExampleVariables) {
376
+ const usedEnvVariables = runEnvironmentChecks || runSecurityChecks ? getUsedEnvVariables(content) : /* @__PURE__ */ new Set();
377
+ if (runEnvironmentChecks && envExampleVariables) {
337
378
  for (const variableName of usedEnvVariables) {
338
379
  if (shouldIgnoreEnvVariable(variableName) || envExampleVariables.has(variableName)) {
339
380
  continue;
@@ -343,7 +384,7 @@ async function scanProject(projectPath) {
343
384
  missingEnvUsages.set(variableName, filesUsingVariable);
344
385
  }
345
386
  }
346
- if (isClientSideFile(relativeFile, content)) {
387
+ if (runSecurityChecks && isClientSideFile(relativeFile, content)) {
347
388
  for (const variableName of usedEnvVariables) {
348
389
  if (!variableName.startsWith("NEXT_PUBLIC_") && !shouldIgnoreEnvVariable(variableName)) {
349
390
  const warningKey = `${relativeFile}:${variableName}`;
@@ -422,6 +463,16 @@ function createIssueFactory(issues) {
422
463
  function getIssueIdPrefix(ruleId, category) {
423
464
  return issueIdPrefixes[ruleId] ?? `${category}-${ruleId}`;
424
465
  }
466
+ function getEnabledChecks(checks) {
467
+ const checksToEnable = checks && checks.length > 0 ? checks : recommendedScanChecks;
468
+ return new Set(checksToEnable);
469
+ }
470
+ function hasCheck(enabledChecks, check) {
471
+ return enabledChecks.has(check);
472
+ }
473
+ function onlyHasCheck(enabledChecks, check) {
474
+ return enabledChecks.size === 1 && enabledChecks.has(check);
475
+ }
425
476
  async function fileExists(filePath) {
426
477
  try {
427
478
  await fs.access(filePath);
@@ -508,13 +559,16 @@ async function safeReadJson(filePath) {
508
559
  }
509
560
  async function getSourceFiles(projectPath, addIssue) {
510
561
  try {
511
- return await fg(sourceFilePatterns, {
562
+ const files = await fg(sourceFilePatterns, {
512
563
  cwd: projectPath,
513
564
  ignore: ignoredPaths,
514
565
  absolute: true,
515
566
  onlyFiles: true,
516
567
  dot: false
517
568
  });
569
+ return files.sort(
570
+ (leftFile, rightFile) => normalizePath(leftFile).localeCompare(normalizePath(rightFile))
571
+ );
518
572
  } catch {
519
573
  addIssue({
520
574
  ruleId: "project-source-files-unreadable",
@@ -619,8 +673,8 @@ function getWebhookRouteInfo(relativeFile, content) {
619
673
  const normalizedContent = content.toLowerCase();
620
674
  const normalizedRouteContext = `${normalizedFile}
621
675
  ${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"));
676
+ const pathLooksLikeWebhook = /(^|[\/._-])(webhook|webhooks|callback)([\/._-]|$)/.test(normalizedFile);
677
+ 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") || /\bresend\b/.test(normalizedContent) && /\bwebhook\b/.test(normalizedContent) || /\bwebhook_secret\b/.test(normalizedContent) || /\bwebhooksecret\b/.test(normalizedContent) || /\bwebhook\b/.test(normalizedContent) && /\b(signature|secret|event|payload)\b/.test(normalizedContent);
624
678
  if (!pathLooksLikeWebhook && !contentStronglySuggestsWebhook) {
625
679
  return null;
626
680
  }
@@ -631,13 +685,13 @@ ${normalizedContent}`;
631
685
  };
632
686
  }
633
687
  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")) {
688
+ if (normalizedRouteContext.includes("stripe-signature") || normalizedRouteContext.includes("stripe_webhook_secret") || normalizedRouteContext.includes("stripe.webhooks") || normalizedRouteContext.includes("constructevent(") || /\bstripe\b/.test(normalizedRouteContext) && /\bwebhook\b/.test(normalizedRouteContext)) {
635
689
  return "stripe";
636
690
  }
637
- if (normalizedRouteContext.includes("resend") && normalizedRouteContext.includes("webhook")) {
691
+ if (/\bresend\b/.test(normalizedRouteContext) && /\bwebhook\b/.test(normalizedRouteContext)) {
638
692
  return "resend";
639
693
  }
640
- if (normalizedRouteContext.includes("clerk_webhook_secret") || normalizedRouteContext.includes("clerk") && normalizedRouteContext.includes("webhook")) {
694
+ if (normalizedRouteContext.includes("clerk_webhook_secret") || /\bclerk\b/.test(normalizedRouteContext) && /\bwebhook\b/.test(normalizedRouteContext)) {
641
695
  return "clerk";
642
696
  }
643
697
  if (normalizedRouteContext.includes("x-github-event") || normalizedRouteContext.includes("x-hub-signature")) {
@@ -918,5 +972,7 @@ function getErrorCode(error) {
918
972
  return void 0;
919
973
  }
920
974
  export {
921
- scanProject
975
+ recommendedScanChecks,
976
+ scanProject,
977
+ validScanChecks
922
978
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qodfy/core",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Scanner engine for Qodfy, an open-source launch readiness scanner for AI-built apps.",
5
5
  "keywords": [
6
6
  "qodfy",