@qodfy/core 0.2.2 → 0.2.4

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,4 +1,9 @@
1
1
  type IssueSeverity = "critical" | "warning" | "info";
2
+ type IssueConfidence = "high" | "medium" | "low";
3
+ type IssueEvidence = {
4
+ label: string;
5
+ detail?: string;
6
+ };
2
7
  type IssueCategory = "security" | "environment" | "api" | "webhook" | "ai" | "maintainability" | "project";
3
8
  declare const validScanChecks: readonly ["project", "api", "environment", "ai", "webhook", "maintainability", "security"];
4
9
  type ScanCheck = typeof validScanChecks[number];
@@ -8,11 +13,13 @@ type Issue = {
8
13
  ruleId: string;
9
14
  category: IssueCategory;
10
15
  severity: IssueSeverity;
16
+ confidence: IssueConfidence;
11
17
  title: string;
12
18
  message: string;
13
19
  file?: string;
14
20
  suggestion?: string;
15
21
  fixPrompt?: string;
22
+ evidence?: IssueEvidence[];
16
23
  };
17
24
  type ScanReport = {
18
25
  projectPath: string;
@@ -30,7 +37,8 @@ type ScanReport = {
30
37
  type ScanOptions = {
31
38
  projectPath: string;
32
39
  checks?: ScanCheck[];
40
+ includeLowConfidence?: boolean;
33
41
  };
34
42
  declare function scanProject(input: string | ScanOptions): Promise<ScanReport>;
35
43
 
36
- export { type Issue, type IssueCategory, type IssueSeverity, type ScanCheck, type ScanOptions, type ScanReport, recommendedScanChecks, scanProject, validScanChecks };
44
+ export { type Issue, type IssueCategory, type IssueConfidence, type IssueEvidence, type IssueSeverity, type ScanCheck, type ScanOptions, type ScanReport, recommendedScanChecks, scanProject, validScanChecks };
package/dist/index.js CHANGED
@@ -114,7 +114,11 @@ var issueIdPrefixes = {
114
114
  "environment-variable-missing-from-example": "environment-variable-missing-from-example",
115
115
  "security-client-side-secret": "security-client-side-secret",
116
116
  "security-hardcoded-secret": "security-hardcoded-secret",
117
- "api-route-missing-auth": "security-api-auth",
117
+ "sensitive-api-route-missing-auth": "sensitive-api-route-missing-auth",
118
+ "api-public-read-route": "api-public-read-route",
119
+ "public-form-missing-abuse-protection": "public-form-missing-abuse-protection",
120
+ "internal-route-missing-protection": "internal-route-missing-protection",
121
+ "api-mutation-route-review-auth": "api-mutation-route-review-auth",
118
122
  "ai-route-missing-rate-limit": "ai-route-rate-limit",
119
123
  "maintainability-large-file": "maintainability-large-file",
120
124
  "maintainability-large-file-skipped": "maintainability-large-file-skipped",
@@ -125,6 +129,7 @@ var issueIdPrefixes = {
125
129
  async function scanProject(input) {
126
130
  const startTime = Date.now();
127
131
  const projectPath = typeof input === "string" ? input : input.projectPath;
132
+ const includeLowConfidence = typeof input === "string" ? false : Boolean(input.includeLowConfidence);
128
133
  const enabledChecks = getEnabledChecks(
129
134
  typeof input === "string" ? void 0 : input.checks
130
135
  );
@@ -148,6 +153,7 @@ async function scanProject(input) {
148
153
  ruleId: "project-missing-package-json",
149
154
  category: "project",
150
155
  severity: "critical",
156
+ confidence: "high",
151
157
  title: "Missing package.json",
152
158
  message: "Qodfy could not find a package.json file in this project.",
153
159
  suggestion: "Run Qodfy from the project root or pass --path to the app folder.",
@@ -160,6 +166,7 @@ async function scanProject(input) {
160
166
  ruleId: "project-invalid-package-json",
161
167
  category: "project",
162
168
  severity: "critical",
169
+ confidence: "high",
163
170
  title: "Could not read package.json",
164
171
  message: packageJsonResult.reason,
165
172
  file: "package.json",
@@ -171,6 +178,7 @@ async function scanProject(input) {
171
178
  ruleId: "project-invalid-package-json",
172
179
  category: "project",
173
180
  severity: "critical",
181
+ confidence: "high",
174
182
  title: "Invalid package.json",
175
183
  message: "package.json must contain a JSON object at the top level.",
176
184
  file: "package.json",
@@ -188,6 +196,7 @@ async function scanProject(input) {
188
196
  ruleId: "project-next-not-detected",
189
197
  category: "project",
190
198
  severity: "warning",
199
+ confidence: "low",
191
200
  title: "Next.js not detected",
192
201
  message: "This first version of Qodfy is optimized for Next.js projects.",
193
202
  suggestion: "If this is a monorepo, scan the Next.js app folder directly.",
@@ -204,6 +213,7 @@ async function scanProject(input) {
204
213
  ruleId: "environment-missing-env-example",
205
214
  category: "environment",
206
215
  severity: "warning",
216
+ confidence: "medium",
207
217
  title: "Missing .env.example",
208
218
  message: "Add a .env.example file so future developers know which environment variables are required.",
209
219
  suggestion: "Document required variable names only, never real secret values.",
@@ -216,6 +226,7 @@ async function scanProject(input) {
216
226
  ruleId: "environment-missing-env-example",
217
227
  category: "environment",
218
228
  severity: "warning",
229
+ confidence: "medium",
219
230
  title: "Could not read .env.example",
220
231
  message: envExampleResult.reason,
221
232
  file: ".env.example",
@@ -232,6 +243,7 @@ async function scanProject(input) {
232
243
  ruleId: "project-missing-readme",
233
244
  category: "project",
234
245
  severity: "info",
246
+ confidence: "low",
235
247
  title: "Missing README.md",
236
248
  message: "A README helps other developers understand how to run and maintain the project.",
237
249
  fixPrompt: createReadmeFixPrompt()
@@ -257,6 +269,7 @@ async function scanProject(input) {
257
269
  ruleId: "maintainability-file-unreadable",
258
270
  category: "maintainability",
259
271
  severity: "info",
272
+ confidence: "low",
260
273
  title: "File could not be checked",
261
274
  message: statResult.reason,
262
275
  file: relativeFile,
@@ -272,6 +285,7 @@ async function scanProject(input) {
272
285
  ruleId: "maintainability-large-file-skipped",
273
286
  category: "maintainability",
274
287
  severity: "info",
288
+ confidence: "low",
275
289
  title: "Large file skipped from deep scan",
276
290
  message: "This file is larger than 500KB and was skipped from deep content checks.",
277
291
  file: relativeFile,
@@ -298,6 +312,7 @@ async function scanProject(input) {
298
312
  ruleId: "maintainability-file-unreadable",
299
313
  category: "maintainability",
300
314
  severity: "info",
315
+ confidence: "low",
301
316
  title: "File could not be read",
302
317
  message: fileResult.reason,
303
318
  file: relativeFile,
@@ -312,12 +327,13 @@ async function scanProject(input) {
312
327
  );
313
328
  if (runAiChecks && usesAI) {
314
329
  aiFiles++;
315
- const hasRateLimit = content.includes("rateLimit") || content.includes("ratelimit") || content.includes("upstash") || content.includes("limiter");
330
+ const hasRateLimit = hasRateLimitSignal(content);
316
331
  if (apiRouteSet.has(file) && !hasRateLimit) {
317
332
  addIssue({
318
333
  ruleId: "ai-route-missing-rate-limit",
319
334
  category: "ai",
320
335
  severity: "critical",
336
+ confidence: "high",
321
337
  title: "AI route may be missing rate limiting",
322
338
  message: "AI routes can create real API costs. Add rate limiting or usage limits before launch.",
323
339
  file: relativeFile,
@@ -326,17 +342,23 @@ async function scanProject(input) {
326
342
  });
327
343
  }
328
344
  }
329
- const webhookRouteInfo = runWebhookChecks && apiRouteSet.has(file) ? getWebhookRouteInfo(relativeFile, content) : null;
330
- if (webhookRouteInfo && !hasWebhookSignatureVerification(content, webhookRouteInfo.provider)) {
345
+ const apiRouteAnalysis = (runWebhookChecks || runApiChecks) && apiRouteSet.has(file) ? analyzeApiRoute({
346
+ file,
347
+ relativeFile,
348
+ content
349
+ }) : null;
350
+ if (runWebhookChecks && apiRouteAnalysis?.intent === "webhook" && !apiRouteAnalysis.hasWebhookVerification) {
331
351
  addIssue({
332
352
  ruleId: "webhook-missing-signature-verification",
333
353
  category: "webhook",
334
- severity: webhookRouteInfo.confidence === "high" ? "critical" : "warning",
354
+ severity: apiRouteAnalysis.confidence === "high" ? "critical" : "warning",
355
+ confidence: apiRouteAnalysis.confidence,
335
356
  title: "Webhook route may be missing signature verification",
336
357
  message: "This webhook route appears to handle external events, but Qodfy could not find signature verification before the event is handled.",
337
358
  file: relativeFile,
338
- suggestion: getWebhookSignatureSuggestion(webhookRouteInfo.provider),
339
- fixPrompt: createWebhookSignatureFixPrompt(relativeFile)
359
+ suggestion: getWebhookSignatureSuggestion(apiRouteAnalysis.webhookProvider),
360
+ fixPrompt: createWebhookSignatureFixPrompt(relativeFile),
361
+ evidence: apiRouteAnalysis.evidence
340
362
  });
341
363
  }
342
364
  if (runSecurityChecks) {
@@ -350,6 +372,7 @@ async function scanProject(input) {
350
372
  ruleId: "security-hardcoded-secret",
351
373
  category: "security",
352
374
  severity: "critical",
375
+ confidence: "high",
353
376
  title: "Possible hardcoded secret",
354
377
  message: `A string literal in ${relativeFile} matches the pattern for ${secretMatch.label}. Qodfy does not print possible secret values.`,
355
378
  file: relativeFile,
@@ -358,20 +381,12 @@ async function scanProject(input) {
358
381
  });
359
382
  }
360
383
  }
361
- if (runApiChecks && apiRouteSet.has(file)) {
362
- const hasAuth = content.includes("auth(") || content.includes("getServerSession") || content.includes("currentUser") || content.includes("clerkClient") || content.includes("session");
363
- if (!hasAuth && !webhookRouteInfo) {
364
- addIssue({
365
- ruleId: "api-route-missing-auth",
366
- category: "api",
367
- severity: "warning",
368
- title: "API route may be missing authentication",
369
- message: "This API route does not appear to contain an auth/session check.",
370
- file: relativeFile,
371
- suggestion: "Confirm the route is public, or add an auth/session check before handling user data.",
372
- fixPrompt: createApiAuthFixPrompt(relativeFile)
373
- });
374
- }
384
+ if (runApiChecks && apiRouteAnalysis) {
385
+ addApiRouteProtectionIssues({
386
+ addIssue,
387
+ includeLowConfidence,
388
+ analysis: apiRouteAnalysis
389
+ });
375
390
  }
376
391
  const usedEnvVariables = runEnvironmentChecks || runSecurityChecks ? getUsedEnvVariables(content) : /* @__PURE__ */ new Set();
377
392
  if (runEnvironmentChecks && envExampleVariables) {
@@ -396,6 +411,7 @@ async function scanProject(input) {
396
411
  ruleId: "security-client-side-secret",
397
412
  category: "security",
398
413
  severity: "warning",
414
+ confidence: "medium",
399
415
  title: "Possible server secret used in client-side code",
400
416
  message: `${variableName} appears in a client-side file. Server secrets should not be exposed to the browser.`,
401
417
  file: relativeFile,
@@ -411,6 +427,7 @@ async function scanProject(input) {
411
427
  ruleId: "maintainability-large-file",
412
428
  category: "maintainability",
413
429
  severity: "info",
430
+ confidence: "low",
414
431
  title: "Large file detected",
415
432
  message: "This file is larger than the recommended maintainability threshold. Large files can be harder to review, test, and safely modify.",
416
433
  file: largeFile.relativeFile,
@@ -424,6 +441,7 @@ async function scanProject(input) {
424
441
  ruleId: "environment-variable-missing-from-example",
425
442
  category: "environment",
426
443
  severity: "warning",
444
+ confidence: "medium",
427
445
  title: "Environment variable missing from .env.example",
428
446
  message: getMissingEnvMessage(variableName, files2),
429
447
  file: files2.length === 1 ? files2[0] : void 0,
@@ -455,8 +473,9 @@ function createIssueFactory(issues) {
455
473
  const currentCount = (issueCounts.get(issue.ruleId) ?? 0) + 1;
456
474
  issueCounts.set(issue.ruleId, currentCount);
457
475
  issues.push({
476
+ ...issue,
458
477
  id: `${getIssueIdPrefix(issue.ruleId, issue.category)}-${currentCount}`,
459
- ...issue
478
+ confidence: issue.confidence ?? "medium"
460
479
  });
461
480
  };
462
481
  }
@@ -574,6 +593,7 @@ async function getSourceFiles(projectPath, addIssue) {
574
593
  ruleId: "project-source-files-unreadable",
575
594
  category: "project",
576
595
  severity: "critical",
596
+ confidence: "high",
577
597
  title: "Could not scan source files",
578
598
  message: "Qodfy could not list source files in this project.",
579
599
  suggestion: "Check that the project path exists and is readable.",
@@ -668,6 +688,303 @@ function isApiRoute(filePath) {
668
688
  const sourceFileExtension = "(?:ts|tsx|js|jsx|mts|cts|mjs|cjs)";
669
689
  return new RegExp(`/app/api(?:/.+)?/route\\.${sourceFileExtension}$`).test(normalizedFile) || new RegExp(`/pages/api/.+\\.${sourceFileExtension}$`).test(normalizedFile);
670
690
  }
691
+ function addApiRouteProtectionIssues({
692
+ addIssue,
693
+ includeLowConfidence,
694
+ analysis
695
+ }) {
696
+ if (analysis.intent === "webhook") {
697
+ return;
698
+ }
699
+ if (analysis.intent === "public-read") {
700
+ if (includeLowConfidence) {
701
+ addIssue({
702
+ ruleId: "api-public-read-route",
703
+ category: "api",
704
+ severity: "info",
705
+ confidence: analysis.confidence,
706
+ title: "Public read API route detected",
707
+ message: "This route appears intentionally public. Authentication may not be required.",
708
+ file: analysis.relativeFile,
709
+ suggestion: "Verify that it only exposes public or published data and has appropriate validation, caching, and abuse protection.",
710
+ fixPrompt: createPublicReadRouteFixPrompt(analysis.relativeFile),
711
+ evidence: analysis.evidence
712
+ });
713
+ }
714
+ return;
715
+ }
716
+ if (analysis.intent === "public-form") {
717
+ if (!analysis.hasRateLimit && !analysis.hasValidation) {
718
+ addIssue({
719
+ ruleId: "public-form-missing-abuse-protection",
720
+ category: "api",
721
+ severity: "warning",
722
+ confidence: analysis.confidence,
723
+ title: "Public form route may be missing abuse protection",
724
+ message: "This route appears to accept public submissions. Consider adding rate limiting, validation, or spam protection.",
725
+ file: analysis.relativeFile,
726
+ suggestion: "Check for rate limiting, validation, captcha, Turnstile, reCAPTCHA, hCaptcha, or another spam protection pattern.",
727
+ fixPrompt: createPublicFormProtectionFixPrompt(analysis.relativeFile),
728
+ evidence: analysis.evidence
729
+ });
730
+ }
731
+ return;
732
+ }
733
+ if (analysis.intent === "internal") {
734
+ if (!analysis.hasAuth && !analysis.hasSecretProtection) {
735
+ addIssue({
736
+ ruleId: "internal-route-missing-protection",
737
+ category: "security",
738
+ severity: "warning",
739
+ confidence: analysis.confidence,
740
+ title: "Internal API route may be missing protection",
741
+ message: "This route appears internal or operational. Confirm it is protected by auth, a secret token, or server-only access.",
742
+ file: analysis.relativeFile,
743
+ suggestion: "Use the project's existing auth pattern or a secret token check for operational routes such as cron, cleanup, or revalidation.",
744
+ fixPrompt: createInternalRouteProtectionFixPrompt(analysis.relativeFile),
745
+ evidence: analysis.evidence
746
+ });
747
+ }
748
+ return;
749
+ }
750
+ if (analysis.intent === "sensitive-mutation") {
751
+ if (!analysis.hasAuth) {
752
+ addIssue({
753
+ ruleId: "sensitive-api-route-missing-auth",
754
+ category: "security",
755
+ severity: "warning",
756
+ confidence: analysis.confidence,
757
+ title: "Sensitive API route may be missing authentication",
758
+ message: "This route appears to handle user-specific or sensitive operations. Confirm it is protected before launch.",
759
+ file: analysis.relativeFile,
760
+ suggestion: "Review the existing project auth/session pattern and apply it if this route handles private data, uploads, payments, or account changes.",
761
+ fixPrompt: createApiAuthFixPrompt(analysis.relativeFile),
762
+ evidence: analysis.evidence
763
+ });
764
+ }
765
+ return;
766
+ }
767
+ if (analysis.authExpected === "review" && !analysis.hasAuth) {
768
+ addIssue({
769
+ ruleId: "api-mutation-route-review-auth",
770
+ category: "api",
771
+ severity: "warning",
772
+ confidence: analysis.confidence,
773
+ title: "API mutation route should be reviewed for authentication",
774
+ message: "This route mutates data or handles requests, but Qodfy could not determine whether authentication is required.",
775
+ file: analysis.relativeFile,
776
+ suggestion: "Confirm the route is intentionally public, or add the existing project auth/session check before handling private data.",
777
+ fixPrompt: createApiAuthFixPrompt(analysis.relativeFile),
778
+ evidence: analysis.evidence
779
+ });
780
+ }
781
+ }
782
+ function analyzeApiRoute({
783
+ file,
784
+ relativeFile,
785
+ content
786
+ }) {
787
+ const normalizedFile = relativeFile.toLowerCase();
788
+ const methods = getRouteHttpMethods(content);
789
+ const evidence = [];
790
+ const webhookRouteInfo = getWebhookRouteInfo(relativeFile, content);
791
+ const hasAuth = hasAuthOrSessionCheck(content);
792
+ const hasSecretProtection = hasSecretProtectionSignal(content);
793
+ const hasRateLimit = hasRateLimitSignal(content);
794
+ const hasValidation = hasValidationSignal(content);
795
+ const hasCacheHeaders = hasCacheHeaderSignal(content);
796
+ const hasMethodBlocking = hasMethodBlockingSignal(content);
797
+ const webhookProvider = webhookRouteInfo?.provider ?? "unknown";
798
+ const hasWebhookVerification = hasWebhookSignatureVerification(content, webhookProvider);
799
+ if (methods.length > 0) {
800
+ for (const method of methods) {
801
+ evidence.push({ label: "exports", detail: method });
802
+ }
803
+ } else {
804
+ evidence.push({ label: "no exported HTTP method detected" });
805
+ }
806
+ const webhookPathMatch = getRoutePathMatch(normalizedFile, ["webhook", "webhooks", "callback"]);
807
+ const internalPathMatch = getRoutePathMatch(normalizedFile, ["internal", "admin", "cron", "cleanup", "revalidate", "private"]);
808
+ const formPathMatch = getRoutePathMatch(normalizedFile, ["contact", "subscribe", "newsletter", "lead", "inquiry"]);
809
+ const sensitivePathMatch = getRoutePathMatch(normalizedFile, [
810
+ "upload",
811
+ "checkout",
812
+ "order",
813
+ "orders",
814
+ "invoice",
815
+ "invoices",
816
+ "account",
817
+ "user",
818
+ "users",
819
+ "payment",
820
+ "billing",
821
+ "cart",
822
+ "profile",
823
+ "settings"
824
+ ]);
825
+ const publicContentPathMatch = getRoutePathMatch(normalizedFile, [
826
+ "blog",
827
+ "blogs",
828
+ "post",
829
+ "posts",
830
+ "product",
831
+ "products",
832
+ "search",
833
+ "i18n",
834
+ "category",
835
+ "categories",
836
+ "sitemap",
837
+ "rss"
838
+ ]);
839
+ let intent = "unknown";
840
+ if (webhookRouteInfo || webhookPathMatch) {
841
+ intent = "webhook";
842
+ evidence.push({
843
+ label: webhookPathMatch ? "webhook path detected" : "webhook content detected",
844
+ detail: webhookPathMatch ?? webhookProvider
845
+ });
846
+ } else if (internalPathMatch) {
847
+ intent = "internal";
848
+ evidence.push({ label: "path contains", detail: internalPathMatch });
849
+ } else if (formPathMatch) {
850
+ intent = "public-form";
851
+ evidence.push({ label: "path contains", detail: formPathMatch });
852
+ } else if (hasMutationMethod(methods) && sensitivePathMatch) {
853
+ intent = "sensitive-mutation";
854
+ evidence.push({ label: "path contains", detail: sensitivePathMatch });
855
+ } else if (methods.includes("GET") && publicContentPathMatch && !sensitivePathMatch && !internalPathMatch) {
856
+ intent = "public-read";
857
+ evidence.push({ label: "public content route detected", detail: publicContentPathMatch });
858
+ }
859
+ if (hasAuth) {
860
+ evidence.push({ label: "auth/session check detected" });
861
+ } else {
862
+ evidence.push({ label: "no auth/session check detected" });
863
+ }
864
+ if (hasSecretProtection) {
865
+ evidence.push({ label: "secret token check detected" });
866
+ }
867
+ if (hasRateLimit) {
868
+ evidence.push({ label: "rate limit detected" });
869
+ } else if (intent === "public-form") {
870
+ evidence.push({ label: "no rate limit detected" });
871
+ }
872
+ if (hasValidation) {
873
+ evidence.push({ label: "validation detected" });
874
+ } else if (intent === "public-form") {
875
+ evidence.push({ label: "no validation detected" });
876
+ }
877
+ if (hasCacheHeaders) {
878
+ evidence.push({ label: "cache/public-read safety signal detected" });
879
+ }
880
+ if (hasMethodBlocking) {
881
+ evidence.push({ label: "method blocking detected" });
882
+ }
883
+ if (intent === "webhook") {
884
+ if (hasWebhookVerification) {
885
+ evidence.push({ label: "webhook signature verification detected" });
886
+ } else {
887
+ evidence.push({ label: "no webhook signature verification detected" });
888
+ }
889
+ }
890
+ return {
891
+ file,
892
+ relativeFile,
893
+ methods,
894
+ intent,
895
+ authExpected: getAuthExpectation(intent, methods),
896
+ confidence: getApiRouteConfidence(intent, methods, webhookRouteInfo),
897
+ evidence,
898
+ hasAuth,
899
+ hasSecretProtection,
900
+ hasRateLimit,
901
+ hasValidation,
902
+ hasCacheHeaders,
903
+ hasMethodBlocking,
904
+ hasWebhookVerification,
905
+ webhookProvider
906
+ };
907
+ }
908
+ function getAuthExpectation(intent, methods) {
909
+ if (intent === "internal" || intent === "sensitive-mutation") {
910
+ return true;
911
+ }
912
+ if (intent === "unknown" && hasMutationMethod(methods)) {
913
+ return "review";
914
+ }
915
+ return false;
916
+ }
917
+ function getApiRouteConfidence(intent, methods, webhookRouteInfo) {
918
+ if (intent === "sensitive-mutation" || intent === "internal") {
919
+ return "high";
920
+ }
921
+ if (intent === "webhook") {
922
+ return webhookRouteInfo?.confidence === "high" ? "high" : "medium";
923
+ }
924
+ if (intent === "public-form" || intent === "unknown" && hasMutationMethod(methods)) {
925
+ return "medium";
926
+ }
927
+ return "low";
928
+ }
929
+ function getRoutePathMatch(normalizedFile, terms) {
930
+ return terms.find((term) => {
931
+ const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
932
+ return new RegExp(`(^|[\\/._\\[\\]-])${escapedTerm}([\\/._\\[\\]-]|$)`).test(normalizedFile);
933
+ });
934
+ }
935
+ function getExportedHttpMethods(content) {
936
+ const methods = /* @__PURE__ */ new Set();
937
+ const functionExportPattern = /\bexport\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\b/g;
938
+ const constExportPattern = /\bexport\s+const\s+(GET|POST|PUT|PATCH|DELETE)\b/g;
939
+ for (const match of content.matchAll(functionExportPattern)) {
940
+ methods.add(match[1]);
941
+ }
942
+ for (const match of content.matchAll(constExportPattern)) {
943
+ methods.add(match[1]);
944
+ }
945
+ return [...methods];
946
+ }
947
+ function getRouteHttpMethods(content) {
948
+ const methods = new Set(getExportedHttpMethods(content));
949
+ const requestMethodPattern = /\b(?:request|req)\.method\s*(?:={2,3}|!={1,2})\s*["'](GET|POST|PUT|PATCH|DELETE)["']/g;
950
+ const methodCasePattern = /\bcase\s+["'](GET|POST|PUT|PATCH|DELETE)["']/g;
951
+ for (const match of content.matchAll(requestMethodPattern)) {
952
+ methods.add(match[1]);
953
+ }
954
+ for (const match of content.matchAll(methodCasePattern)) {
955
+ methods.add(match[1]);
956
+ }
957
+ return [...methods];
958
+ }
959
+ function hasMutationMethod(methods) {
960
+ return ["POST", "PUT", "PATCH", "DELETE"].some(
961
+ (method) => methods.includes(method)
962
+ );
963
+ }
964
+ function hasAuthOrSessionCheck(content) {
965
+ const normalizedContent = content.toLowerCase();
966
+ return normalizedContent.includes("auth(") || normalizedContent.includes("getserversession") || normalizedContent.includes("currentuser") || normalizedContent.includes("clerkclient") || normalizedContent.includes("session") || normalizedContent.includes("requireauth") || normalizedContent.includes("requireuser") || normalizedContent.includes("requireadmin") || normalizedContent.includes("verifysession") || normalizedContent.includes("getuser") || normalizedContent.includes("jwt") || normalizedContent.includes("authorization") || normalizedContent.includes("bearer") || normalizedContent.includes("cookies()") || normalizedContent.includes("request.cookies") || normalizedContent.includes("middleware");
967
+ }
968
+ function hasSecretProtectionSignal(content) {
969
+ const normalizedContent = content.toLowerCase();
970
+ return /\bprocess\.env\.[A-Za-z0-9_]*SECRET\b/.test(content) || /\bprocess\.env\[['"`][A-Za-z0-9_]*SECRET['"`]\]/.test(content) || normalizedContent.includes("cron_secret") || normalizedContent.includes("revalidate_secret") || normalizedContent.includes("authorization") || normalizedContent.includes("bearer") || normalizedContent.includes("token");
971
+ }
972
+ function hasRateLimitSignal(content) {
973
+ const normalizedContent = content.toLowerCase();
974
+ return normalizedContent.includes("ratelimit") || normalizedContent.includes("rate limit") || normalizedContent.includes("limiter") || normalizedContent.includes("upstash") || normalizedContent.includes("throttle");
975
+ }
976
+ function hasValidationSignal(content) {
977
+ const normalizedContent = content.toLowerCase();
978
+ return normalizedContent.includes("zod") || normalizedContent.includes("schema") || normalizedContent.includes("validate") || normalizedContent.includes("validation") || normalizedContent.includes("sanitize") || normalizedContent.includes("safeparse") || normalizedContent.includes("parse(") || normalizedContent.includes("slugregex") || normalizedContent.includes("isvalid") || normalizedContent.includes("captcha") || normalizedContent.includes("turnstile") || normalizedContent.includes("recaptcha") || normalizedContent.includes("hcaptcha");
979
+ }
980
+ function hasCacheHeaderSignal(content) {
981
+ const normalizedContent = content.toLowerCase();
982
+ return normalizedContent.includes("cache-control") || normalizedContent.includes("s-maxage") || normalizedContent.includes("stale-while-revalidate") || normalizedContent.includes("public") || normalizedContent.includes("published") || normalizedContent.includes("status");
983
+ }
984
+ function hasMethodBlockingSignal(content) {
985
+ const normalizedContent = content.toLowerCase();
986
+ return normalizedContent.includes("method not allowed") || normalizedContent.includes("status: 405") || /\b405\b/.test(normalizedContent);
987
+ }
671
988
  function getWebhookRouteInfo(relativeFile, content) {
672
989
  const normalizedFile = relativeFile.toLowerCase();
673
990
  const normalizedContent = content.toLowerCase();
@@ -761,6 +1078,62 @@ Return:
761
1078
  - The updated code.
762
1079
  - Any edge cases I should test.`;
763
1080
  }
1081
+ function createPublicReadRouteFixPrompt(file) {
1082
+ return `Review the public read API route at ${file}.
1083
+
1084
+ Goal:
1085
+ Verify that this route is safe to remain public.
1086
+
1087
+ Instructions:
1088
+ - Confirm it only exposes published, public, or non-sensitive data.
1089
+ - Check that route params and query values are validated or sanitized.
1090
+ - Check for appropriate cache headers where useful.
1091
+ - Check for abuse protection if the route can be called heavily.
1092
+ - Do not add user authentication unless the route should be private.
1093
+ - Do not refactor unrelated code.
1094
+
1095
+ Return:
1096
+ - Whether this route appears intentionally public.
1097
+ - Any low-risk safety improvements.
1098
+ - Any edge cases I should test.`;
1099
+ }
1100
+ function createPublicFormProtectionFixPrompt(file) {
1101
+ return `Review the public form API route at ${file}.
1102
+
1103
+ Goal:
1104
+ Verify validation, rate limiting, and spam protection.
1105
+
1106
+ Instructions:
1107
+ - Confirm submitted input is validated before it is used.
1108
+ - Check for rate limiting or another abuse protection pattern.
1109
+ - Check whether captcha, Turnstile, reCAPTCHA, or hCaptcha is appropriate.
1110
+ - Do not add user authentication unless the form should be private.
1111
+ - Do not introduce a new service unless necessary.
1112
+ - Keep existing behavior unchanged.
1113
+
1114
+ Return:
1115
+ - Whether abuse protection already exists.
1116
+ - The safest minimal change if protection is missing.
1117
+ - Any edge cases I should test.`;
1118
+ }
1119
+ function createInternalRouteProtectionFixPrompt(file) {
1120
+ return `Review the internal API route at ${file}.
1121
+
1122
+ Goal:
1123
+ Confirm this operational route is protected before launch.
1124
+
1125
+ Instructions:
1126
+ - Check whether the route is protected by existing auth, a secret token, or server-only access.
1127
+ - For cron, cleanup, revalidation, or admin routes, prefer the existing project protection pattern.
1128
+ - Do not introduce a new auth provider.
1129
+ - Do not change route behavior unless protection is missing.
1130
+ - If the route is intentionally reachable, add a short comment explaining the protection boundary.
1131
+
1132
+ Return:
1133
+ - Whether protection already exists.
1134
+ - The safest minimal change if protection is missing.
1135
+ - Any edge cases I should test.`;
1136
+ }
764
1137
  function createMissingEnvVariableFixPrompt(variableName, files) {
765
1138
  return `Update the environment documentation for this project.
766
1139
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qodfy/core",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Scanner engine for Qodfy, an open-source launch readiness scanner for AI-built apps.",
5
5
  "keywords": [
6
6
  "qodfy",