@qodfy/core 0.2.3 → 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,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
@@ -114,10 +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
118
  "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",
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",
121
122
  "ai-route-missing-rate-limit": "ai-route-rate-limit",
122
123
  "maintainability-large-file": "maintainability-large-file",
123
124
  "maintainability-large-file-skipped": "maintainability-large-file-skipped",
@@ -326,7 +327,7 @@ async function scanProject(input) {
326
327
  );
327
328
  if (runAiChecks && usesAI) {
328
329
  aiFiles++;
329
- const hasRateLimit = content.includes("rateLimit") || content.includes("ratelimit") || content.includes("upstash") || content.includes("limiter");
330
+ const hasRateLimit = hasRateLimitSignal(content);
330
331
  if (apiRouteSet.has(file) && !hasRateLimit) {
331
332
  addIssue({
332
333
  ruleId: "ai-route-missing-rate-limit",
@@ -341,18 +342,23 @@ async function scanProject(input) {
341
342
  });
342
343
  }
343
344
  }
344
- const webhookRouteInfo = (runWebhookChecks || runApiChecks) && apiRouteSet.has(file) ? getWebhookRouteInfo(relativeFile, content) : null;
345
- 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) {
346
351
  addIssue({
347
352
  ruleId: "webhook-missing-signature-verification",
348
353
  category: "webhook",
349
- severity: webhookRouteInfo.confidence === "high" ? "critical" : "warning",
350
- confidence: webhookRouteInfo.confidence === "high" ? "high" : "medium",
354
+ severity: apiRouteAnalysis.confidence === "high" ? "critical" : "warning",
355
+ confidence: apiRouteAnalysis.confidence,
351
356
  title: "Webhook route may be missing signature verification",
352
357
  message: "This webhook route appears to handle external events, but Qodfy could not find signature verification before the event is handled.",
353
358
  file: relativeFile,
354
- suggestion: getWebhookSignatureSuggestion(webhookRouteInfo.provider),
355
- fixPrompt: createWebhookSignatureFixPrompt(relativeFile)
359
+ suggestion: getWebhookSignatureSuggestion(apiRouteAnalysis.webhookProvider),
360
+ fixPrompt: createWebhookSignatureFixPrompt(relativeFile),
361
+ evidence: apiRouteAnalysis.evidence
356
362
  });
357
363
  }
358
364
  if (runSecurityChecks) {
@@ -375,13 +381,11 @@ async function scanProject(input) {
375
381
  });
376
382
  }
377
383
  }
378
- if (runApiChecks && apiRouteSet.has(file)) {
384
+ if (runApiChecks && apiRouteAnalysis) {
379
385
  addApiRouteProtectionIssues({
380
386
  addIssue,
381
- content,
382
387
  includeLowConfidence,
383
- relativeFile,
384
- webhookRouteInfo
388
+ analysis: apiRouteAnalysis
385
389
  });
386
390
  }
387
391
  const usedEnvVariables = runEnvironmentChecks || runSecurityChecks ? getUsedEnvVariables(content) : /* @__PURE__ */ new Set();
@@ -686,108 +690,123 @@ function isApiRoute(filePath) {
686
690
  }
687
691
  function addApiRouteProtectionIssues({
688
692
  addIssue,
689
- content,
690
693
  includeLowConfidence,
691
- relativeFile,
692
- webhookRouteInfo
694
+ analysis
693
695
  }) {
694
- const intent = classifyApiRouteIntent(relativeFile, content, webhookRouteInfo);
695
- const hasAuth = hasAuthOrSessionCheck(content);
696
- const methods = getHttpMethods(content);
697
- if (intent === "webhook") {
696
+ if (analysis.intent === "webhook") {
698
697
  return;
699
698
  }
700
- if (intent === "public-read") {
699
+ if (analysis.intent === "public-read") {
701
700
  if (includeLowConfidence) {
702
701
  addIssue({
703
702
  ruleId: "api-public-read-route",
704
703
  category: "api",
705
704
  severity: "info",
706
- confidence: "low",
705
+ confidence: analysis.confidence,
707
706
  title: "Public read API route detected",
708
707
  message: "This route appears intentionally public. Authentication may not be required.",
709
- file: relativeFile,
708
+ file: analysis.relativeFile,
710
709
  suggestion: "Verify that it only exposes public or published data and has appropriate validation, caching, and abuse protection.",
711
- fixPrompt: createPublicReadRouteFixPrompt(relativeFile)
710
+ fixPrompt: createPublicReadRouteFixPrompt(analysis.relativeFile),
711
+ evidence: analysis.evidence
712
712
  });
713
713
  }
714
714
  return;
715
715
  }
716
- if (intent === "public-form") {
717
- if (!hasAbuseProtection(content)) {
716
+ if (analysis.intent === "public-form") {
717
+ if (!analysis.hasRateLimit && !analysis.hasValidation) {
718
718
  addIssue({
719
- ruleId: "api-public-form-abuse-protection",
719
+ ruleId: "public-form-missing-abuse-protection",
720
720
  category: "api",
721
721
  severity: "warning",
722
- confidence: "medium",
722
+ confidence: analysis.confidence,
723
723
  title: "Public form route may be missing abuse protection",
724
724
  message: "This route appears to accept public submissions. Consider adding rate limiting, validation, or spam protection.",
725
- file: relativeFile,
725
+ file: analysis.relativeFile,
726
726
  suggestion: "Check for rate limiting, validation, captcha, Turnstile, reCAPTCHA, hCaptcha, or another spam protection pattern.",
727
- fixPrompt: createPublicFormProtectionFixPrompt(relativeFile)
727
+ fixPrompt: createPublicFormProtectionFixPrompt(analysis.relativeFile),
728
+ evidence: analysis.evidence
728
729
  });
729
730
  }
730
731
  return;
731
732
  }
732
- if (intent === "internal") {
733
- if (!hasInternalRouteProtection(content)) {
733
+ if (analysis.intent === "internal") {
734
+ if (!analysis.hasAuth && !analysis.hasSecretProtection) {
734
735
  addIssue({
735
- ruleId: "api-internal-route-protection",
736
- category: "api",
736
+ ruleId: "internal-route-missing-protection",
737
+ category: "security",
737
738
  severity: "warning",
738
- confidence: "high",
739
+ confidence: analysis.confidence,
739
740
  title: "Internal API route may be missing protection",
740
741
  message: "This route appears internal or operational. Confirm it is protected by auth, a secret token, or server-only access.",
741
- file: relativeFile,
742
+ file: analysis.relativeFile,
742
743
  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)
744
+ fixPrompt: createInternalRouteProtectionFixPrompt(analysis.relativeFile),
745
+ evidence: analysis.evidence
744
746
  });
745
747
  }
746
748
  return;
747
749
  }
748
- if (intent === "sensitive-mutation") {
749
- if (!hasAuth) {
750
+ if (analysis.intent === "sensitive-mutation") {
751
+ if (!analysis.hasAuth) {
750
752
  addIssue({
751
- ruleId: "api-route-missing-auth",
752
- category: "api",
753
+ ruleId: "sensitive-api-route-missing-auth",
754
+ category: "security",
753
755
  severity: "warning",
754
- confidence: "high",
756
+ confidence: analysis.confidence,
755
757
  title: "Sensitive API route may be missing authentication",
756
758
  message: "This route appears to handle user-specific or sensitive operations. Confirm it is protected before launch.",
757
- file: relativeFile,
759
+ file: analysis.relativeFile,
758
760
  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)
761
+ fixPrompt: createApiAuthFixPrompt(analysis.relativeFile),
762
+ evidence: analysis.evidence
760
763
  });
761
764
  }
762
765
  return;
763
766
  }
764
- if (hasMutationMethod(methods) && !hasAuth) {
767
+ if (analysis.authExpected === "review" && !analysis.hasAuth) {
765
768
  addIssue({
766
- ruleId: "api-route-missing-auth",
769
+ ruleId: "api-mutation-route-review-auth",
767
770
  category: "api",
768
771
  severity: "warning",
769
- confidence: "medium",
772
+ confidence: analysis.confidence,
770
773
  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,
774
+ message: "This route mutates data or handles requests, but Qodfy could not determine whether authentication is required.",
775
+ file: analysis.relativeFile,
773
776
  suggestion: "Confirm the route is intentionally public, or add the existing project auth/session check before handling private data.",
774
- fixPrompt: createApiAuthFixPrompt(relativeFile)
777
+ fixPrompt: createApiAuthFixPrompt(analysis.relativeFile),
778
+ evidence: analysis.evidence
775
779
  });
776
780
  }
777
781
  }
778
- function classifyApiRouteIntent(relativeFile, content, webhookRouteInfo) {
782
+ function analyzeApiRoute({
783
+ file,
784
+ relativeFile,
785
+ content
786
+ }) {
779
787
  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";
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" });
789
805
  }
790
- if (routePathHasAny(normalizedFile, [
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, [
791
810
  "upload",
792
811
  "checkout",
793
812
  "order",
@@ -802,10 +821,8 @@ function classifyApiRouteIntent(relativeFile, content, webhookRouteInfo) {
802
821
  "cart",
803
822
  "profile",
804
823
  "settings"
805
- ])) {
806
- return "sensitive-mutation";
807
- }
808
- if (routePathHasAny(normalizedFile, [
824
+ ]);
825
+ const publicContentPathMatch = getRoutePathMatch(normalizedFile, [
809
826
  "blog",
810
827
  "blogs",
811
828
  "post",
@@ -818,56 +835,155 @@ function classifyApiRouteIntent(relativeFile, content, webhookRouteInfo) {
818
835
  "categories",
819
836
  "sitemap",
820
837
  "rss"
821
- ])) {
822
- return "public-read";
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 });
823
858
  }
824
- if (hasMutationMethod(methods)) {
825
- return "unknown";
859
+ if (hasAuth) {
860
+ evidence.push({ label: "auth/session check detected" });
861
+ } else {
862
+ evidence.push({ label: "no auth/session check detected" });
826
863
  }
827
- if (methods.size === 0 || isReadOnlyRoute(methods)) {
828
- return "unknown";
864
+ if (hasSecretProtection) {
865
+ evidence.push({ label: "secret token check detected" });
829
866
  }
830
- return "unknown";
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";
831
928
  }
832
- function routePathHasAny(normalizedFile, terms) {
833
- return terms.some((term) => {
929
+ function getRoutePathMatch(normalizedFile, terms) {
930
+ return terms.find((term) => {
834
931
  const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
835
932
  return new RegExp(`(^|[\\/._\\[\\]-])${escapedTerm}([\\/._\\[\\]-]|$)`).test(normalizedFile);
836
933
  });
837
934
  }
838
- function getHttpMethods(content) {
935
+ function getExportedHttpMethods(content) {
839
936
  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)) {
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)) {
844
943
  methods.add(match[1]);
845
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;
846
951
  for (const match of content.matchAll(requestMethodPattern)) {
847
952
  methods.add(match[1]);
848
953
  }
849
954
  for (const match of content.matchAll(methodCasePattern)) {
850
955
  methods.add(match[1]);
851
956
  }
852
- return methods;
957
+ return [...methods];
853
958
  }
854
959
  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");
960
+ return ["POST", "PUT", "PATCH", "DELETE"].some(
961
+ (method) => methods.includes(method)
962
+ );
859
963
  }
860
964
  function hasAuthOrSessionCheck(content) {
861
965
  const normalizedContent = content.toLowerCase();
862
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");
863
967
  }
864
- function hasInternalRouteProtection(content) {
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) {
865
981
  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");
982
+ return normalizedContent.includes("cache-control") || normalizedContent.includes("s-maxage") || normalizedContent.includes("stale-while-revalidate") || normalizedContent.includes("public") || normalizedContent.includes("published") || normalizedContent.includes("status");
867
983
  }
868
- function hasAbuseProtection(content) {
984
+ function hasMethodBlockingSignal(content) {
869
985
  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");
986
+ return normalizedContent.includes("method not allowed") || normalizedContent.includes("status: 405") || /\b405\b/.test(normalizedContent);
871
987
  }
872
988
  function getWebhookRouteInfo(relativeFile, content) {
873
989
  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.4",
4
4
  "description": "Scanner engine for Qodfy, an open-source launch readiness scanner for AI-built apps.",
5
5
  "keywords": [
6
6
  "qodfy",