@qodfy/core 0.2.3 → 0.2.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.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  type IssueSeverity = "critical" | "warning" | "info";
2
2
  type IssueConfidence = "high" | "medium" | "low";
3
+ type IssueEvidence = {
4
+ label: string;
5
+ detail?: string;
6
+ };
3
7
  type IssueCategory = "security" | "environment" | "api" | "webhook" | "ai" | "maintainability" | "project";
4
8
  declare const validScanChecks: readonly ["project", "api", "environment", "ai", "webhook", "maintainability", "security"];
5
9
  type ScanCheck = typeof validScanChecks[number];
@@ -15,6 +19,7 @@ type Issue = {
15
19
  file?: string;
16
20
  suggestion?: string;
17
21
  fixPrompt?: string;
22
+ evidence?: IssueEvidence[];
18
23
  };
19
24
  type ScanReport = {
20
25
  projectPath: string;
@@ -36,4 +41,4 @@ type ScanOptions = {
36
41
  };
37
42
  declare function scanProject(input: string | ScanOptions): Promise<ScanReport>;
38
43
 
39
- export { type Issue, type IssueCategory, type IssueConfidence, 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
@@ -22,11 +22,17 @@ var recommendedScanChecks = [
22
22
  var sourceFilePatterns = ["**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}"];
23
23
  var ignoredPaths = [
24
24
  "node_modules/**",
25
+ "**/node_modules/**",
25
26
  ".next/**",
27
+ "**/.next/**",
26
28
  "dist/**",
29
+ "**/dist/**",
27
30
  "build/**",
31
+ "**/build/**",
28
32
  ".turbo/**",
33
+ "**/.turbo/**",
29
34
  ".vercel/**",
35
+ "**/.vercel/**",
30
36
  "coverage/**",
31
37
  "**/coverage/**",
32
38
  ".cache/**",
@@ -114,10 +120,11 @@ var issueIdPrefixes = {
114
120
  "environment-variable-missing-from-example": "environment-variable-missing-from-example",
115
121
  "security-client-side-secret": "security-client-side-secret",
116
122
  "security-hardcoded-secret": "security-hardcoded-secret",
117
- "api-route-missing-auth": "security-api-auth",
123
+ "sensitive-api-route-missing-auth": "sensitive-api-route-missing-auth",
118
124
  "api-public-read-route": "api-public-read-route",
119
- "api-public-form-abuse-protection": "api-public-form-protection",
120
- "api-internal-route-protection": "api-internal-route-protection",
125
+ "public-form-missing-abuse-protection": "public-form-missing-abuse-protection",
126
+ "internal-route-missing-protection": "internal-route-missing-protection",
127
+ "api-mutation-route-review-auth": "api-mutation-route-review-auth",
121
128
  "ai-route-missing-rate-limit": "ai-route-rate-limit",
122
129
  "maintainability-large-file": "maintainability-large-file",
123
130
  "maintainability-large-file-skipped": "maintainability-large-file-skipped",
@@ -326,7 +333,7 @@ async function scanProject(input) {
326
333
  );
327
334
  if (runAiChecks && usesAI) {
328
335
  aiFiles++;
329
- const hasRateLimit = content.includes("rateLimit") || content.includes("ratelimit") || content.includes("upstash") || content.includes("limiter");
336
+ const hasRateLimit = hasRateLimitSignal(content);
330
337
  if (apiRouteSet.has(file) && !hasRateLimit) {
331
338
  addIssue({
332
339
  ruleId: "ai-route-missing-rate-limit",
@@ -341,18 +348,23 @@ async function scanProject(input) {
341
348
  });
342
349
  }
343
350
  }
344
- const webhookRouteInfo = (runWebhookChecks || runApiChecks) && apiRouteSet.has(file) ? getWebhookRouteInfo(relativeFile, content) : null;
345
- if (webhookRouteInfo && !hasWebhookSignatureVerification(content, webhookRouteInfo.provider)) {
351
+ const apiRouteAnalysis = (runWebhookChecks || runApiChecks) && apiRouteSet.has(file) ? analyzeApiRoute({
352
+ file,
353
+ relativeFile,
354
+ content
355
+ }) : null;
356
+ if (runWebhookChecks && apiRouteAnalysis?.intent === "webhook" && !apiRouteAnalysis.hasWebhookVerification) {
346
357
  addIssue({
347
358
  ruleId: "webhook-missing-signature-verification",
348
359
  category: "webhook",
349
- severity: webhookRouteInfo.confidence === "high" ? "critical" : "warning",
350
- confidence: webhookRouteInfo.confidence === "high" ? "high" : "medium",
360
+ severity: apiRouteAnalysis.confidence === "high" ? "critical" : "warning",
361
+ confidence: apiRouteAnalysis.confidence,
351
362
  title: "Webhook route may be missing signature verification",
352
363
  message: "This webhook route appears to handle external events, but Qodfy could not find signature verification before the event is handled.",
353
364
  file: relativeFile,
354
- suggestion: getWebhookSignatureSuggestion(webhookRouteInfo.provider),
355
- fixPrompt: createWebhookSignatureFixPrompt(relativeFile)
365
+ suggestion: getWebhookSignatureSuggestion(apiRouteAnalysis.webhookProvider),
366
+ fixPrompt: createWebhookSignatureFixPrompt(relativeFile),
367
+ evidence: apiRouteAnalysis.evidence
356
368
  });
357
369
  }
358
370
  if (runSecurityChecks) {
@@ -375,13 +387,11 @@ async function scanProject(input) {
375
387
  });
376
388
  }
377
389
  }
378
- if (runApiChecks && apiRouteSet.has(file)) {
390
+ if (runApiChecks && apiRouteAnalysis) {
379
391
  addApiRouteProtectionIssues({
380
392
  addIssue,
381
- content,
382
393
  includeLowConfidence,
383
- relativeFile,
384
- webhookRouteInfo
394
+ analysis: apiRouteAnalysis
385
395
  });
386
396
  }
387
397
  const usedEnvVariables = runEnvironmentChecks || runSecurityChecks ? getUsedEnvVariables(content) : /* @__PURE__ */ new Set();
@@ -574,13 +584,17 @@ async function safeReadJson(filePath) {
574
584
  }
575
585
  async function getSourceFiles(projectPath, addIssue) {
576
586
  try {
577
- const files = await fg(sourceFilePatterns, {
587
+ const rawFiles = await fg(sourceFilePatterns, {
578
588
  cwd: projectPath,
579
589
  ignore: ignoredPaths,
580
590
  absolute: true,
581
591
  onlyFiles: true,
582
592
  dot: false
583
593
  });
594
+ const files = rawFiles.filter((file) => {
595
+ const relativeFile = normalizePath(path.relative(projectPath, file));
596
+ return !shouldIgnoreSourceFile(relativeFile);
597
+ });
584
598
  return files.sort(
585
599
  (leftFile, rightFile) => normalizePath(leftFile).localeCompare(normalizePath(rightFile))
586
600
  );
@@ -598,6 +612,31 @@ async function getSourceFiles(projectPath, addIssue) {
598
612
  return [];
599
613
  }
600
614
  }
615
+ function shouldIgnoreSourceFile(relativeFile) {
616
+ const normalizedFile = normalizePath(relativeFile);
617
+ const pathParts = normalizedFile.split("/");
618
+ const ignoredPathParts = /* @__PURE__ */ new Set([
619
+ "node_modules",
620
+ ".next",
621
+ "dist",
622
+ "build",
623
+ ".turbo",
624
+ ".vercel",
625
+ "coverage",
626
+ ".cache",
627
+ ".output",
628
+ ".open-next",
629
+ "storybook-static",
630
+ "playwright-report",
631
+ "test-results",
632
+ "generated",
633
+ "__generated__"
634
+ ]);
635
+ if (normalizedFile.endsWith(".d.ts") || normalizedFile.endsWith(".map")) {
636
+ return true;
637
+ }
638
+ return pathParts.some((pathPart) => ignoredPathParts.has(pathPart));
639
+ }
601
640
  function getEnvExampleVariables(content) {
602
641
  const variables = /* @__PURE__ */ new Set();
603
642
  for (const line of content.split(/\r?\n/)) {
@@ -686,108 +725,123 @@ function isApiRoute(filePath) {
686
725
  }
687
726
  function addApiRouteProtectionIssues({
688
727
  addIssue,
689
- content,
690
728
  includeLowConfidence,
691
- relativeFile,
692
- webhookRouteInfo
729
+ analysis
693
730
  }) {
694
- const intent = classifyApiRouteIntent(relativeFile, content, webhookRouteInfo);
695
- const hasAuth = hasAuthOrSessionCheck(content);
696
- const methods = getHttpMethods(content);
697
- if (intent === "webhook") {
731
+ if (analysis.intent === "webhook") {
698
732
  return;
699
733
  }
700
- if (intent === "public-read") {
734
+ if (analysis.intent === "public-read") {
701
735
  if (includeLowConfidence) {
702
736
  addIssue({
703
737
  ruleId: "api-public-read-route",
704
738
  category: "api",
705
739
  severity: "info",
706
- confidence: "low",
740
+ confidence: analysis.confidence,
707
741
  title: "Public read API route detected",
708
742
  message: "This route appears intentionally public. Authentication may not be required.",
709
- file: relativeFile,
743
+ file: analysis.relativeFile,
710
744
  suggestion: "Verify that it only exposes public or published data and has appropriate validation, caching, and abuse protection.",
711
- fixPrompt: createPublicReadRouteFixPrompt(relativeFile)
745
+ fixPrompt: createPublicReadRouteFixPrompt(analysis.relativeFile),
746
+ evidence: analysis.evidence
712
747
  });
713
748
  }
714
749
  return;
715
750
  }
716
- if (intent === "public-form") {
717
- if (!hasAbuseProtection(content)) {
751
+ if (analysis.intent === "public-form") {
752
+ if (!analysis.hasRateLimit && !analysis.hasValidation) {
718
753
  addIssue({
719
- ruleId: "api-public-form-abuse-protection",
754
+ ruleId: "public-form-missing-abuse-protection",
720
755
  category: "api",
721
756
  severity: "warning",
722
- confidence: "medium",
757
+ confidence: analysis.confidence,
723
758
  title: "Public form route may be missing abuse protection",
724
759
  message: "This route appears to accept public submissions. Consider adding rate limiting, validation, or spam protection.",
725
- file: relativeFile,
760
+ file: analysis.relativeFile,
726
761
  suggestion: "Check for rate limiting, validation, captcha, Turnstile, reCAPTCHA, hCaptcha, or another spam protection pattern.",
727
- fixPrompt: createPublicFormProtectionFixPrompt(relativeFile)
762
+ fixPrompt: createPublicFormProtectionFixPrompt(analysis.relativeFile),
763
+ evidence: analysis.evidence
728
764
  });
729
765
  }
730
766
  return;
731
767
  }
732
- if (intent === "internal") {
733
- if (!hasInternalRouteProtection(content)) {
768
+ if (analysis.intent === "internal") {
769
+ if (!analysis.hasAuth && !analysis.hasSecretProtection) {
734
770
  addIssue({
735
- ruleId: "api-internal-route-protection",
736
- category: "api",
771
+ ruleId: "internal-route-missing-protection",
772
+ category: "security",
737
773
  severity: "warning",
738
- confidence: "high",
774
+ confidence: analysis.confidence,
739
775
  title: "Internal API route may be missing protection",
740
776
  message: "This route appears internal or operational. Confirm it is protected by auth, a secret token, or server-only access.",
741
- file: relativeFile,
777
+ file: analysis.relativeFile,
742
778
  suggestion: "Use the project's existing auth pattern or a secret token check for operational routes such as cron, cleanup, or revalidation.",
743
- fixPrompt: createInternalRouteProtectionFixPrompt(relativeFile)
779
+ fixPrompt: createInternalRouteProtectionFixPrompt(analysis.relativeFile),
780
+ evidence: analysis.evidence
744
781
  });
745
782
  }
746
783
  return;
747
784
  }
748
- if (intent === "sensitive-mutation") {
749
- if (!hasAuth) {
785
+ if (analysis.intent === "sensitive-mutation") {
786
+ if (!analysis.hasAuth) {
750
787
  addIssue({
751
- ruleId: "api-route-missing-auth",
752
- category: "api",
788
+ ruleId: "sensitive-api-route-missing-auth",
789
+ category: "security",
753
790
  severity: "warning",
754
- confidence: "high",
791
+ confidence: analysis.confidence,
755
792
  title: "Sensitive API route may be missing authentication",
756
793
  message: "This route appears to handle user-specific or sensitive operations. Confirm it is protected before launch.",
757
- file: relativeFile,
794
+ file: analysis.relativeFile,
758
795
  suggestion: "Review the existing project auth/session pattern and apply it if this route handles private data, uploads, payments, or account changes.",
759
- fixPrompt: createApiAuthFixPrompt(relativeFile)
796
+ fixPrompt: createApiAuthFixPrompt(analysis.relativeFile),
797
+ evidence: analysis.evidence
760
798
  });
761
799
  }
762
800
  return;
763
801
  }
764
- if (hasMutationMethod(methods) && !hasAuth) {
802
+ if (analysis.authExpected === "review" && !analysis.hasAuth) {
765
803
  addIssue({
766
- ruleId: "api-route-missing-auth",
804
+ ruleId: "api-mutation-route-review-auth",
767
805
  category: "api",
768
806
  severity: "warning",
769
- confidence: "medium",
807
+ confidence: analysis.confidence,
770
808
  title: "API mutation route should be reviewed for authentication",
771
- message: "This route appears to handle a mutation, but Qodfy could not find an auth/session check.",
772
- file: relativeFile,
809
+ message: "This route mutates data or handles requests, but Qodfy could not determine whether authentication is required.",
810
+ file: analysis.relativeFile,
773
811
  suggestion: "Confirm the route is intentionally public, or add the existing project auth/session check before handling private data.",
774
- fixPrompt: createApiAuthFixPrompt(relativeFile)
812
+ fixPrompt: createApiAuthFixPrompt(analysis.relativeFile),
813
+ evidence: analysis.evidence
775
814
  });
776
815
  }
777
816
  }
778
- function classifyApiRouteIntent(relativeFile, content, webhookRouteInfo) {
817
+ function analyzeApiRoute({
818
+ file,
819
+ relativeFile,
820
+ content
821
+ }) {
779
822
  const normalizedFile = relativeFile.toLowerCase();
780
- const methods = getHttpMethods(content);
781
- if (webhookRouteInfo || routePathHasAny(normalizedFile, ["webhook", "webhooks", "callback"])) {
782
- return "webhook";
783
- }
784
- if (routePathHasAny(normalizedFile, ["internal", "admin", "cron", "cleanup", "revalidate", "private"])) {
785
- return "internal";
786
- }
787
- if (routePathHasAny(normalizedFile, ["contact", "subscribe", "newsletter", "lead", "inquiry"])) {
788
- return "public-form";
823
+ const methods = getRouteHttpMethods(content);
824
+ const evidence = [];
825
+ const webhookRouteInfo = getWebhookRouteInfo(relativeFile, content);
826
+ const hasAuth = hasAuthOrSessionCheck(content);
827
+ const hasSecretProtection = hasSecretProtectionSignal(content);
828
+ const hasRateLimit = hasRateLimitSignal(content);
829
+ const hasValidation = hasValidationSignal(content);
830
+ const hasCacheHeaders = hasCacheHeaderSignal(content);
831
+ const hasMethodBlocking = hasMethodBlockingSignal(content);
832
+ const webhookProvider = webhookRouteInfo?.provider ?? "unknown";
833
+ const hasWebhookVerification = hasWebhookSignatureVerification(content, webhookProvider);
834
+ if (methods.length > 0) {
835
+ for (const method of methods) {
836
+ evidence.push({ label: "exports", detail: method });
837
+ }
838
+ } else {
839
+ evidence.push({ label: "no exported HTTP method detected" });
789
840
  }
790
- if (routePathHasAny(normalizedFile, [
841
+ const webhookPathMatch = getRoutePathMatch(normalizedFile, ["webhook", "webhooks", "callback"]);
842
+ const internalPathMatch = getRoutePathMatch(normalizedFile, ["internal", "admin", "cron", "cleanup", "revalidate", "private"]);
843
+ const formPathMatch = getRoutePathMatch(normalizedFile, ["contact", "subscribe", "newsletter", "lead", "inquiry"]);
844
+ const sensitivePathMatch = getRoutePathMatch(normalizedFile, [
791
845
  "upload",
792
846
  "checkout",
793
847
  "order",
@@ -802,10 +856,8 @@ function classifyApiRouteIntent(relativeFile, content, webhookRouteInfo) {
802
856
  "cart",
803
857
  "profile",
804
858
  "settings"
805
- ])) {
806
- return "sensitive-mutation";
807
- }
808
- if (routePathHasAny(normalizedFile, [
859
+ ]);
860
+ const publicContentPathMatch = getRoutePathMatch(normalizedFile, [
809
861
  "blog",
810
862
  "blogs",
811
863
  "post",
@@ -818,56 +870,155 @@ function classifyApiRouteIntent(relativeFile, content, webhookRouteInfo) {
818
870
  "categories",
819
871
  "sitemap",
820
872
  "rss"
821
- ])) {
822
- return "public-read";
873
+ ]);
874
+ let intent = "unknown";
875
+ if (webhookRouteInfo || webhookPathMatch) {
876
+ intent = "webhook";
877
+ evidence.push({
878
+ label: webhookPathMatch ? "webhook path detected" : "webhook content detected",
879
+ detail: webhookPathMatch ?? webhookProvider
880
+ });
881
+ } else if (internalPathMatch) {
882
+ intent = "internal";
883
+ evidence.push({ label: "path contains", detail: internalPathMatch });
884
+ } else if (formPathMatch) {
885
+ intent = "public-form";
886
+ evidence.push({ label: "path contains", detail: formPathMatch });
887
+ } else if (hasMutationMethod(methods) && sensitivePathMatch) {
888
+ intent = "sensitive-mutation";
889
+ evidence.push({ label: "path contains", detail: sensitivePathMatch });
890
+ } else if (methods.includes("GET") && publicContentPathMatch && !sensitivePathMatch && !internalPathMatch) {
891
+ intent = "public-read";
892
+ evidence.push({ label: "public content route detected", detail: publicContentPathMatch });
823
893
  }
824
- if (hasMutationMethod(methods)) {
825
- return "unknown";
894
+ if (hasAuth) {
895
+ evidence.push({ label: "auth/session check detected" });
896
+ } else {
897
+ evidence.push({ label: "no auth/session check detected" });
826
898
  }
827
- if (methods.size === 0 || isReadOnlyRoute(methods)) {
828
- return "unknown";
899
+ if (hasSecretProtection) {
900
+ evidence.push({ label: "secret token check detected" });
829
901
  }
830
- return "unknown";
902
+ if (hasRateLimit) {
903
+ evidence.push({ label: "rate limit detected" });
904
+ } else if (intent === "public-form") {
905
+ evidence.push({ label: "no rate limit detected" });
906
+ }
907
+ if (hasValidation) {
908
+ evidence.push({ label: "validation detected" });
909
+ } else if (intent === "public-form") {
910
+ evidence.push({ label: "no validation detected" });
911
+ }
912
+ if (hasCacheHeaders) {
913
+ evidence.push({ label: "cache/public-read safety signal detected" });
914
+ }
915
+ if (hasMethodBlocking) {
916
+ evidence.push({ label: "method blocking detected" });
917
+ }
918
+ if (intent === "webhook") {
919
+ if (hasWebhookVerification) {
920
+ evidence.push({ label: "webhook signature verification detected" });
921
+ } else {
922
+ evidence.push({ label: "no webhook signature verification detected" });
923
+ }
924
+ }
925
+ return {
926
+ file,
927
+ relativeFile,
928
+ methods,
929
+ intent,
930
+ authExpected: getAuthExpectation(intent, methods),
931
+ confidence: getApiRouteConfidence(intent, methods, webhookRouteInfo),
932
+ evidence,
933
+ hasAuth,
934
+ hasSecretProtection,
935
+ hasRateLimit,
936
+ hasValidation,
937
+ hasCacheHeaders,
938
+ hasMethodBlocking,
939
+ hasWebhookVerification,
940
+ webhookProvider
941
+ };
942
+ }
943
+ function getAuthExpectation(intent, methods) {
944
+ if (intent === "internal" || intent === "sensitive-mutation") {
945
+ return true;
946
+ }
947
+ if (intent === "unknown" && hasMutationMethod(methods)) {
948
+ return "review";
949
+ }
950
+ return false;
951
+ }
952
+ function getApiRouteConfidence(intent, methods, webhookRouteInfo) {
953
+ if (intent === "sensitive-mutation" || intent === "internal") {
954
+ return "high";
955
+ }
956
+ if (intent === "webhook") {
957
+ return webhookRouteInfo?.confidence === "high" ? "high" : "medium";
958
+ }
959
+ if (intent === "public-form" || intent === "unknown" && hasMutationMethod(methods)) {
960
+ return "medium";
961
+ }
962
+ return "low";
831
963
  }
832
- function routePathHasAny(normalizedFile, terms) {
833
- return terms.some((term) => {
964
+ function getRoutePathMatch(normalizedFile, terms) {
965
+ return terms.find((term) => {
834
966
  const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
835
967
  return new RegExp(`(^|[\\/._\\[\\]-])${escapedTerm}([\\/._\\[\\]-]|$)`).test(normalizedFile);
836
968
  });
837
969
  }
838
- function getHttpMethods(content) {
970
+ function getExportedHttpMethods(content) {
839
971
  const methods = /* @__PURE__ */ new Set();
840
- const exportedMethodPattern = /\bexport\s+(?:async\s+)?(?:function|const)\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b/g;
841
- const requestMethodPattern = /\b(?:request|req)\.method\s*(?:={2,3}|!={1,2})\s*["'](GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)["']/g;
842
- const methodCasePattern = /\bcase\s+["'](GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)["']/g;
843
- for (const match of content.matchAll(exportedMethodPattern)) {
972
+ const functionExportPattern = /\bexport\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\b/g;
973
+ const constExportPattern = /\bexport\s+const\s+(GET|POST|PUT|PATCH|DELETE)\b/g;
974
+ for (const match of content.matchAll(functionExportPattern)) {
844
975
  methods.add(match[1]);
845
976
  }
977
+ for (const match of content.matchAll(constExportPattern)) {
978
+ methods.add(match[1]);
979
+ }
980
+ return [...methods];
981
+ }
982
+ function getRouteHttpMethods(content) {
983
+ const methods = new Set(getExportedHttpMethods(content));
984
+ const requestMethodPattern = /\b(?:request|req)\.method\s*(?:={2,3}|!={1,2})\s*["'](GET|POST|PUT|PATCH|DELETE)["']/g;
985
+ const methodCasePattern = /\bcase\s+["'](GET|POST|PUT|PATCH|DELETE)["']/g;
846
986
  for (const match of content.matchAll(requestMethodPattern)) {
847
987
  methods.add(match[1]);
848
988
  }
849
989
  for (const match of content.matchAll(methodCasePattern)) {
850
990
  methods.add(match[1]);
851
991
  }
852
- return methods;
992
+ return [...methods];
853
993
  }
854
994
  function hasMutationMethod(methods) {
855
- return ["POST", "PUT", "PATCH", "DELETE"].some((method) => methods.has(method));
856
- }
857
- function isReadOnlyRoute(methods) {
858
- return [...methods].every((method) => method === "GET" || method === "HEAD" || method === "OPTIONS");
995
+ return ["POST", "PUT", "PATCH", "DELETE"].some(
996
+ (method) => methods.includes(method)
997
+ );
859
998
  }
860
999
  function hasAuthOrSessionCheck(content) {
861
1000
  const normalizedContent = content.toLowerCase();
862
1001
  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");
863
1002
  }
864
- function hasInternalRouteProtection(content) {
1003
+ function hasSecretProtectionSignal(content) {
1004
+ const normalizedContent = content.toLowerCase();
1005
+ 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");
1006
+ }
1007
+ function hasRateLimitSignal(content) {
1008
+ const normalizedContent = content.toLowerCase();
1009
+ return normalizedContent.includes("ratelimit") || normalizedContent.includes("rate limit") || normalizedContent.includes("limiter") || normalizedContent.includes("upstash") || normalizedContent.includes("throttle");
1010
+ }
1011
+ function hasValidationSignal(content) {
1012
+ const normalizedContent = content.toLowerCase();
1013
+ 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");
1014
+ }
1015
+ function hasCacheHeaderSignal(content) {
865
1016
  const normalizedContent = content.toLowerCase();
866
- return hasAuthOrSessionCheck(content) || /\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");
1017
+ return normalizedContent.includes("cache-control") || normalizedContent.includes("s-maxage") || normalizedContent.includes("stale-while-revalidate") || normalizedContent.includes("public") || normalizedContent.includes("published") || normalizedContent.includes("status");
867
1018
  }
868
- function hasAbuseProtection(content) {
1019
+ function hasMethodBlockingSignal(content) {
869
1020
  const normalizedContent = content.toLowerCase();
870
- return normalizedContent.includes("ratelimit") || normalizedContent.includes("rate limit") || normalizedContent.includes("limiter") || normalizedContent.includes("captcha") || normalizedContent.includes("turnstile") || normalizedContent.includes("recaptcha") || normalizedContent.includes("hcaptcha") || normalizedContent.includes("validation") || normalizedContent.includes("validate") || normalizedContent.includes("zod") || normalizedContent.includes("safeparse");
1021
+ return normalizedContent.includes("method not allowed") || normalizedContent.includes("status: 405") || /\b405\b/.test(normalizedContent);
871
1022
  }
872
1023
  function getWebhookRouteInfo(relativeFile, content) {
873
1024
  const normalizedFile = relativeFile.toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qodfy/core",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Scanner engine for Qodfy, an open-source launch readiness scanner for AI-built apps.",
5
5
  "keywords": [
6
6
  "qodfy",