@qodfy/core 0.2.6 → 0.2.7

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
@@ -20,6 +20,7 @@ type Issue = {
20
20
  suggestion?: string;
21
21
  fixPrompt?: string;
22
22
  evidence?: IssueEvidence[];
23
+ context?: IssueEvidence[];
23
24
  };
24
25
  type ScanReport = {
25
26
  projectPath: string;
package/dist/index.js CHANGED
@@ -354,18 +354,10 @@ async function scanProject(input) {
354
354
  relativeFile,
355
355
  content
356
356
  }) : null;
357
- if (runWebhookChecks && apiRouteAnalysis?.intent === "webhook" && !apiRouteAnalysis.hasWebhookVerification) {
358
- addIssue({
359
- ruleId: "webhook-missing-signature-verification",
360
- category: "webhook",
361
- severity: apiRouteAnalysis.confidence === "high" ? "critical" : "warning",
362
- confidence: apiRouteAnalysis.confidence,
363
- title: "Webhook route may be missing signature verification",
364
- message: "This webhook route appears to handle external events, but Qodfy could not find signature verification before the event is handled.",
365
- file: relativeFile,
366
- suggestion: getWebhookSignatureSuggestion(apiRouteAnalysis.webhookProvider),
367
- fixPrompt: createWebhookSignatureFixPrompt(relativeFile),
368
- evidence: apiRouteAnalysis.evidence
357
+ if (runWebhookChecks && apiRouteAnalysis) {
358
+ addWebhookSignatureIssues({
359
+ addIssue,
360
+ analysis: apiRouteAnalysis
369
361
  });
370
362
  }
371
363
  if (runSecurityChecks) {
@@ -726,95 +718,125 @@ function isApiRoute(filePath) {
726
718
  const sourceFileExtension = "(?:ts|tsx|js|jsx|mts|cts|mjs|cjs)";
727
719
  return new RegExp(`/app/api(?:/.+)?/route\\.${sourceFileExtension}$`).test(normalizedFile) || new RegExp(`/pages/api/.+\\.${sourceFileExtension}$`).test(normalizedFile);
728
720
  }
721
+ function addWebhookSignatureIssues({
722
+ addIssue,
723
+ analysis
724
+ }) {
725
+ for (const handler of analysis.handlers) {
726
+ if (handler.intent !== "webhook" || handler.hasWebhookVerification) {
727
+ continue;
728
+ }
729
+ addIssue({
730
+ ruleId: "webhook-missing-signature-verification",
731
+ category: "webhook",
732
+ severity: handler.confidence === "high" ? "critical" : "warning",
733
+ confidence: handler.confidence,
734
+ title: `Webhook ${handler.method} handler may be missing signature verification`,
735
+ message: `The ${handler.method} handler in this route appears to handle external events, but Qodfy could not find signature verification before the event is handled.`,
736
+ file: analysis.relativeFile,
737
+ suggestion: getWebhookSignatureSuggestion(analysis.webhookProvider),
738
+ fixPrompt: createWebhookSignatureFixPrompt(analysis.relativeFile, handler.method),
739
+ evidence: handler.evidence,
740
+ context: handler.context
741
+ });
742
+ }
743
+ }
729
744
  function addApiRouteProtectionIssues({
730
745
  addIssue,
731
746
  includeLowConfidence,
732
747
  analysis
733
748
  }) {
734
- if (analysis.intent === "webhook") {
735
- return;
736
- }
737
- if (analysis.intent === "public-read") {
738
- if (includeLowConfidence) {
739
- addIssue({
740
- ruleId: "api-public-read-route",
741
- category: "api",
742
- severity: "info",
743
- confidence: analysis.confidence,
744
- title: "Public read API route detected",
745
- message: "This route appears intentionally public. Authentication may not be required.",
746
- file: analysis.relativeFile,
747
- suggestion: "Verify that it only exposes public or published data and has appropriate validation, caching, and abuse protection.",
748
- fixPrompt: createPublicReadRouteFixPrompt(analysis.relativeFile),
749
- evidence: analysis.evidence
750
- });
749
+ for (const handler of analysis.handlers) {
750
+ if (handler.intent === "webhook") {
751
+ continue;
751
752
  }
752
- return;
753
- }
754
- if (analysis.intent === "public-form") {
755
- if (!analysis.hasRateLimit && !analysis.hasValidation) {
756
- addIssue({
757
- ruleId: "public-form-missing-abuse-protection",
758
- category: "api",
759
- severity: "warning",
760
- confidence: analysis.confidence,
761
- title: "Public form route may be missing abuse protection",
762
- message: "This route appears to accept public submissions. Consider adding rate limiting, validation, or spam protection.",
763
- file: analysis.relativeFile,
764
- suggestion: "Check for rate limiting, validation, captcha, Turnstile, reCAPTCHA, hCaptcha, or another spam protection pattern.",
765
- fixPrompt: createPublicFormProtectionFixPrompt(analysis.relativeFile),
766
- evidence: analysis.evidence
767
- });
753
+ if (handler.intent === "public-read") {
754
+ if (includeLowConfidence) {
755
+ addIssue({
756
+ ruleId: "api-public-read-route",
757
+ category: "api",
758
+ severity: "info",
759
+ confidence: handler.confidence,
760
+ title: `Public ${handler.method} handler detected`,
761
+ message: `The ${handler.method} handler in this route appears intentionally public. Authentication may not be required.`,
762
+ file: analysis.relativeFile,
763
+ suggestion: "Verify that it only exposes public or published data and has appropriate validation, caching, and abuse protection.",
764
+ fixPrompt: createPublicReadRouteFixPrompt(analysis.relativeFile, handler.method),
765
+ evidence: handler.evidence,
766
+ context: handler.context
767
+ });
768
+ }
769
+ continue;
768
770
  }
769
- return;
770
- }
771
- if (analysis.intent === "internal") {
772
- if (!analysis.hasAuth && !analysis.hasSecretProtection) {
773
- addIssue({
774
- ruleId: "internal-route-missing-protection",
775
- category: "security",
776
- severity: "warning",
777
- confidence: analysis.confidence,
778
- title: "Internal API route may be missing protection",
779
- message: "This route appears internal or operational. Confirm it is protected by auth, a secret token, or server-only access.",
780
- file: analysis.relativeFile,
781
- suggestion: "Use the project's existing auth pattern or a secret token check for operational routes such as cron, cleanup, or revalidation.",
782
- fixPrompt: createInternalRouteProtectionFixPrompt(analysis.relativeFile),
783
- evidence: analysis.evidence
784
- });
771
+ if (handler.intent === "public-form") {
772
+ if (!handler.hasRateLimit && !handler.hasValidation) {
773
+ addIssue({
774
+ ruleId: "public-form-missing-abuse-protection",
775
+ category: "api",
776
+ severity: "warning",
777
+ confidence: handler.confidence,
778
+ title: `Public form ${handler.method} handler may be missing abuse protection`,
779
+ message: `The ${handler.method} handler in this route appears to accept public submissions. Consider adding rate limiting, validation, or spam protection.`,
780
+ file: analysis.relativeFile,
781
+ suggestion: "Check for rate limiting, validation, captcha, Turnstile, reCAPTCHA, hCaptcha, or another spam protection pattern.",
782
+ fixPrompt: createPublicFormProtectionFixPrompt(analysis.relativeFile, handler.method),
783
+ evidence: handler.evidence,
784
+ context: handler.context
785
+ });
786
+ }
787
+ continue;
785
788
  }
786
- return;
787
- }
788
- if (analysis.intent === "sensitive-mutation") {
789
- if (!analysis.hasAuth) {
789
+ if (handler.intent === "internal") {
790
+ if (!handler.hasAuth && !handler.hasSecretProtection) {
791
+ addIssue({
792
+ ruleId: "internal-route-missing-protection",
793
+ category: "security",
794
+ severity: "warning",
795
+ confidence: handler.confidence,
796
+ title: `Internal ${handler.method} handler may be missing protection`,
797
+ message: `The ${handler.method} handler in this route appears internal or operational. Confirm it is protected by auth, a secret token, or server-only access.`,
798
+ file: analysis.relativeFile,
799
+ suggestion: "Use the project's existing auth pattern or a secret token check for operational handlers such as cron, cleanup, or revalidation.",
800
+ fixPrompt: createInternalRouteProtectionFixPrompt(analysis.relativeFile, handler.method),
801
+ evidence: handler.evidence,
802
+ context: handler.context
803
+ });
804
+ }
805
+ continue;
806
+ }
807
+ if (handler.intent === "sensitive-mutation") {
808
+ if (!handler.hasAuth) {
809
+ addIssue({
810
+ ruleId: "sensitive-api-route-missing-auth",
811
+ category: "security",
812
+ severity: "warning",
813
+ confidence: handler.confidence,
814
+ title: getSensitiveHandlerTitle(handler),
815
+ message: `The ${handler.method} handler in this route appears to handle uploads or sensitive operations. Confirm it is protected before launch.`,
816
+ file: analysis.relativeFile,
817
+ suggestion: "Review the existing project auth/session pattern and apply it if this handler processes private data, uploads, payments, or account changes.",
818
+ fixPrompt: createApiAuthFixPrompt(analysis.relativeFile, handler.method, handler.intent),
819
+ evidence: handler.evidence,
820
+ context: handler.context
821
+ });
822
+ }
823
+ continue;
824
+ }
825
+ if (handler.authExpected === "review" && !handler.hasAuth) {
790
826
  addIssue({
791
- ruleId: "sensitive-api-route-missing-auth",
792
- category: "security",
827
+ ruleId: "api-mutation-route-review-auth",
828
+ category: "api",
793
829
  severity: "warning",
794
- confidence: analysis.confidence,
795
- title: "Sensitive API route may be missing authentication",
796
- message: "This route appears to handle user-specific or sensitive operations. Confirm it is protected before launch.",
830
+ confidence: handler.confidence,
831
+ title: "API mutation handler should be reviewed for authentication",
832
+ message: `The ${handler.method} handler mutates data or handles requests, but Qodfy could not determine whether authentication is required.`,
797
833
  file: analysis.relativeFile,
798
- suggestion: "Review the existing project auth/session pattern and apply it if this route handles private data, uploads, payments, or account changes.",
799
- fixPrompt: createApiAuthFixPrompt(analysis.relativeFile),
800
- evidence: analysis.evidence
834
+ suggestion: "Confirm the handler is intentionally public, or add the existing project auth/session check before handling private data.",
835
+ fixPrompt: createApiAuthFixPrompt(analysis.relativeFile, handler.method, handler.intent),
836
+ evidence: handler.evidence,
837
+ context: handler.context
801
838
  });
802
839
  }
803
- return;
804
- }
805
- if (analysis.authExpected === "review" && !analysis.hasAuth) {
806
- addIssue({
807
- ruleId: "api-mutation-route-review-auth",
808
- category: "api",
809
- severity: "warning",
810
- confidence: analysis.confidence,
811
- title: "API mutation route should be reviewed for authentication",
812
- message: "This route mutates data or handles requests, but Qodfy could not determine whether authentication is required.",
813
- file: analysis.relativeFile,
814
- suggestion: "Confirm the route is intentionally public, or add the existing project auth/session check before handling private data.",
815
- fixPrompt: createApiAuthFixPrompt(analysis.relativeFile),
816
- evidence: analysis.evidence
817
- });
818
840
  }
819
841
  }
820
842
  function analyzeApiRoute({
@@ -822,25 +844,44 @@ function analyzeApiRoute({
822
844
  relativeFile,
823
845
  content
824
846
  }) {
825
- const normalizedFile = relativeFile.toLowerCase();
826
- const methods = getRouteHttpMethods(content);
827
- const evidence = [];
828
- const webhookRouteInfo = getWebhookRouteInfo(relativeFile, content);
829
- const hasAuth = hasAuthOrSessionCheck(content);
830
- const hasSecretProtection = hasSecretProtectionSignal(content);
831
- const hasRateLimit = hasRateLimitSignal(content);
832
- const hasValidation = hasValidationSignal(content);
833
- const hasCacheHeaders = hasCacheHeaderSignal(content);
834
- const hasMethodBlocking = hasMethodBlockingSignal(content);
835
- const webhookProvider = webhookRouteInfo?.provider ?? "unknown";
836
- const hasWebhookVerification = hasWebhookSignatureVerification(content, webhookProvider);
837
- if (methods.length > 0) {
838
- for (const method of methods) {
839
- evidence.push({ label: "exports", detail: method });
840
- }
841
- } else {
842
- evidence.push({ label: "no exported HTTP method detected" });
847
+ const exportedHandlers = getExportedRouteHandlers(content);
848
+ const handlersToAnalyze = exportedHandlers.length > 0 ? exportedHandlers : getRouteHttpMethods(content).map((method) => ({
849
+ method,
850
+ body: content,
851
+ usedFallbackBody: true
852
+ }));
853
+ const handlers = handlersToAnalyze.map(
854
+ (handler) => analyzeApiHandler({
855
+ relativeFile,
856
+ content,
857
+ method: handler.method,
858
+ body: handler.body,
859
+ usedFallbackBody: handler.usedFallbackBody
860
+ })
861
+ );
862
+ for (const handler of handlers) {
863
+ handler.context = getHandlerContext(handler, handlers);
843
864
  }
865
+ const methods = handlers.map((handler) => handler.method);
866
+ const webhookProvider = getRouteWebhookProvider(handlers, relativeFile, content);
867
+ return {
868
+ file,
869
+ relativeFile,
870
+ methods,
871
+ handlers,
872
+ routeIntent: getRouteIntent(handlers),
873
+ evidence: methods.map((method) => ({ label: "exports", detail: method })),
874
+ webhookProvider
875
+ };
876
+ }
877
+ function analyzeApiHandler({
878
+ relativeFile,
879
+ content,
880
+ method,
881
+ body,
882
+ usedFallbackBody
883
+ }) {
884
+ const normalizedFile = relativeFile.toLowerCase();
844
885
  const webhookPathMatch = getRoutePathMatch(normalizedFile, ["webhook", "webhooks", "callback"]);
845
886
  const internalPathMatch = getRoutePathMatch(normalizedFile, ["internal", "admin", "cron", "cleanup", "revalidate", "private"]);
846
887
  const formPathMatch = getRoutePathMatch(normalizedFile, ["contact", "subscribe", "newsletter", "lead", "inquiry"]);
@@ -874,7 +915,22 @@ function analyzeApiRoute({
874
915
  "sitemap",
875
916
  "rss"
876
917
  ]);
918
+ const handlerContent = body || content;
919
+ const webhookRouteInfo = getWebhookRouteInfo(relativeFile, handlerContent);
920
+ const webhookProvider = webhookRouteInfo?.provider ?? getWebhookProvider(`${normalizedFile}
921
+ ${handlerContent.toLowerCase()}`);
922
+ const hasAuth = hasAuthOrSessionCheck(handlerContent);
923
+ const hasSecretProtection = hasSecretProtectionSignal(handlerContent);
924
+ const hasRateLimit = hasRateLimitSignal(handlerContent);
925
+ const hasValidation = hasValidationSignal(handlerContent);
926
+ const hasCacheHeaders = hasCacheHeaderSignal(handlerContent);
927
+ const hasMethodBlocking = hasMethodBlockingSignal(handlerContent);
928
+ const hasWebhookVerification = hasWebhookSignatureVerification(handlerContent, webhookProvider);
929
+ const evidence = [{ label: "exports", detail: method }];
877
930
  let intent = "unknown";
931
+ if (usedFallbackBody) {
932
+ evidence.push({ label: "handler body extraction fallback", detail: "using full file" });
933
+ }
878
934
  if (webhookRouteInfo || webhookPathMatch) {
879
935
  intent = "webhook";
880
936
  evidence.push({
@@ -884,54 +940,59 @@ function analyzeApiRoute({
884
940
  } else if (internalPathMatch) {
885
941
  intent = "internal";
886
942
  evidence.push({ label: "path contains", detail: internalPathMatch });
887
- } else if (formPathMatch) {
943
+ } else if (method === "POST" && formPathMatch) {
888
944
  intent = "public-form";
889
945
  evidence.push({ label: "path contains", detail: formPathMatch });
890
- } else if (hasMutationMethod(methods) && sensitivePathMatch) {
946
+ } else if (method === "GET" && publicContentPathMatch && !sensitivePathMatch && !internalPathMatch) {
947
+ intent = "public-read";
948
+ evidence.push({ label: "public read path detected", detail: publicContentPathMatch });
949
+ } else if (isMutationMethod(method) && sensitivePathMatch && !hasMethodBlocking) {
891
950
  intent = "sensitive-mutation";
892
951
  evidence.push({ label: "path contains", detail: sensitivePathMatch });
893
- } else if (methods.includes("GET") && publicContentPathMatch && !sensitivePathMatch && !internalPathMatch) {
894
- intent = "public-read";
895
- evidence.push({ label: "public content route detected", detail: publicContentPathMatch });
896
952
  }
897
953
  if (hasAuth) {
898
- evidence.push({ label: "auth/session check detected" });
954
+ evidence.push({ label: `auth/session check detected in ${method} handler` });
899
955
  } else {
900
- evidence.push({ label: "no auth/session check detected" });
956
+ evidence.push({ label: `no auth/session check detected in ${method} handler` });
901
957
  }
902
958
  if (hasSecretProtection) {
903
- evidence.push({ label: "secret token check detected" });
959
+ evidence.push({ label: `secret token check detected in ${method} handler` });
904
960
  }
905
- if (hasRateLimit) {
906
- evidence.push({ label: "rate limit detected" });
907
- } else if (intent === "public-form") {
908
- evidence.push({ label: "no rate limit detected" });
909
- }
910
- if (hasValidation) {
911
- evidence.push({ label: "validation detected" });
912
- } else if (intent === "public-form") {
913
- evidence.push({ label: "no validation detected" });
961
+ if (intent === "public-form") {
962
+ evidence.push({
963
+ label: hasRateLimit ? "rate limit detected" : "no rate limit detected",
964
+ detail: `${method} handler`
965
+ });
966
+ evidence.push({
967
+ label: hasValidation ? "validation detected" : "no validation detected",
968
+ detail: `${method} handler`
969
+ });
914
970
  }
915
- if (hasCacheHeaders) {
916
- evidence.push({ label: "cache/public-read safety signal detected" });
971
+ if (intent === "public-read") {
972
+ if (hasRateLimit) {
973
+ evidence.push({ label: "rate limit detected", detail: `${method} handler` });
974
+ }
975
+ if (hasValidation) {
976
+ evidence.push({ label: "validation detected", detail: `${method} handler` });
977
+ }
978
+ if (hasCacheHeaders) {
979
+ evidence.push({ label: "cache/public-read safety signal detected", detail: `${method} handler` });
980
+ }
917
981
  }
918
982
  if (hasMethodBlocking) {
919
- evidence.push({ label: "method blocking detected" });
983
+ evidence.push({ label: "method blocking detected", detail: `${method} handler` });
920
984
  }
921
985
  if (intent === "webhook") {
922
- if (hasWebhookVerification) {
923
- evidence.push({ label: "webhook signature verification detected" });
924
- } else {
925
- evidence.push({ label: "no webhook signature verification detected" });
926
- }
986
+ evidence.push({
987
+ label: hasWebhookVerification ? "webhook signature verification detected" : "no webhook signature verification detected",
988
+ detail: `${method} handler`
989
+ });
927
990
  }
928
991
  return {
929
- file,
930
- relativeFile,
931
- methods,
992
+ method,
932
993
  intent,
933
- authExpected: getAuthExpectation(intent, methods),
934
- confidence: getApiRouteConfidence(intent, methods, webhookRouteInfo),
994
+ authExpected: getHandlerAuthExpectation(intent, method, hasMethodBlocking),
995
+ confidence: getApiHandlerConfidence(intent, method, webhookRouteInfo, hasMethodBlocking),
935
996
  evidence,
936
997
  hasAuth,
937
998
  hasSecretProtection,
@@ -939,31 +1000,89 @@ function analyzeApiRoute({
939
1000
  hasValidation,
940
1001
  hasCacheHeaders,
941
1002
  hasMethodBlocking,
942
- hasWebhookVerification,
943
- webhookProvider
1003
+ hasWebhookVerification
944
1004
  };
945
1005
  }
946
- function getAuthExpectation(intent, methods) {
1006
+ function getHandlerContext(handler, handlers) {
1007
+ const context = [];
1008
+ for (const otherHandler of handlers) {
1009
+ if (otherHandler.method === handler.method) {
1010
+ continue;
1011
+ }
1012
+ context.push({ label: "route also exports", detail: otherHandler.method });
1013
+ if (otherHandler.intent === "public-read") {
1014
+ context.push({ label: "public read handler detected", detail: otherHandler.method });
1015
+ }
1016
+ if (otherHandler.hasCacheHeaders) {
1017
+ context.push({ label: "cache/public-read safety signal detected", detail: `${otherHandler.method} handler` });
1018
+ }
1019
+ if (otherHandler.hasMethodBlocking) {
1020
+ context.push({ label: "method blocking detected", detail: `${otherHandler.method} handler` });
1021
+ }
1022
+ }
1023
+ return context.length > 0 ? context : void 0;
1024
+ }
1025
+ function getRouteIntent(handlers) {
1026
+ const intentPriority = [
1027
+ "webhook",
1028
+ "internal",
1029
+ "sensitive-mutation",
1030
+ "public-form",
1031
+ "public-read",
1032
+ "unknown"
1033
+ ];
1034
+ return intentPriority.find(
1035
+ (intent) => handlers.some((handler) => handler.intent === intent)
1036
+ ) ?? "unknown";
1037
+ }
1038
+ function getRouteWebhookProvider(handlers, relativeFile, content) {
1039
+ const routeWebhookInfo = getWebhookRouteInfo(relativeFile, content);
1040
+ if (routeWebhookInfo?.provider && routeWebhookInfo.provider !== "unknown") {
1041
+ return routeWebhookInfo.provider;
1042
+ }
1043
+ const webhookHandler = handlers.find((handler) => handler.intent === "webhook");
1044
+ if (!webhookHandler) {
1045
+ return "unknown";
1046
+ }
1047
+ const providerFromEvidence = webhookHandler.evidence.find(
1048
+ (item) => item.label === "webhook content detected" && item.detail && item.detail !== "unknown"
1049
+ );
1050
+ return providerFromEvidence?.detail ?? "unknown";
1051
+ }
1052
+ function getHandlerAuthExpectation(intent, method, hasMethodBlocking) {
1053
+ if (hasMethodBlocking || intent === "public-read" || intent === "public-form" || intent === "webhook") {
1054
+ return false;
1055
+ }
947
1056
  if (intent === "internal" || intent === "sensitive-mutation") {
948
1057
  return true;
949
1058
  }
950
- if (intent === "unknown" && hasMutationMethod(methods)) {
1059
+ if (isMutationMethod(method)) {
951
1060
  return "review";
952
1061
  }
953
1062
  return false;
954
1063
  }
955
- function getApiRouteConfidence(intent, methods, webhookRouteInfo) {
1064
+ function getApiHandlerConfidence(intent, method, webhookRouteInfo, hasMethodBlocking) {
1065
+ if (hasMethodBlocking) {
1066
+ return "low";
1067
+ }
956
1068
  if (intent === "sensitive-mutation" || intent === "internal") {
957
1069
  return "high";
958
1070
  }
959
1071
  if (intent === "webhook") {
960
1072
  return webhookRouteInfo?.confidence === "high" ? "high" : "medium";
961
1073
  }
962
- if (intent === "public-form" || intent === "unknown" && hasMutationMethod(methods)) {
1074
+ if (intent === "public-form" || intent === "unknown" && isMutationMethod(method)) {
963
1075
  return "medium";
964
1076
  }
965
1077
  return "low";
966
1078
  }
1079
+ function getSensitiveHandlerTitle(handler) {
1080
+ const pathSignal = handler.evidence.find((item) => item.label === "path contains")?.detail;
1081
+ if (handler.method === "POST" && pathSignal === "upload") {
1082
+ return "Upload POST handler may be missing authentication";
1083
+ }
1084
+ return `Sensitive ${handler.method} handler may be missing authentication`;
1085
+ }
967
1086
  function getRoutePathMatch(normalizedFile, terms) {
968
1087
  return terms.find((term) => {
969
1088
  const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -971,16 +1090,96 @@ function getRoutePathMatch(normalizedFile, terms) {
971
1090
  });
972
1091
  }
973
1092
  function getExportedHttpMethods(content) {
974
- const methods = /* @__PURE__ */ new Set();
975
- const functionExportPattern = /\bexport\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\b/g;
976
- const constExportPattern = /\bexport\s+const\s+(GET|POST|PUT|PATCH|DELETE)\b/g;
1093
+ return getExportedRouteHandlers(content).map((handler) => handler.method);
1094
+ }
1095
+ function getExportedRouteHandlers(content) {
1096
+ const handlers = [];
1097
+ const functionExportPattern = /\bexport\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\s*\(/g;
1098
+ const constExportPattern = /\bexport\s+const\s+(GET|POST|PUT|PATCH|DELETE)\s*=/g;
977
1099
  for (const match of content.matchAll(functionExportPattern)) {
978
- methods.add(match[1]);
1100
+ handlers.push(extractRouteHandlerBody(content, match.index ?? 0, match[1], "function"));
979
1101
  }
980
1102
  for (const match of content.matchAll(constExportPattern)) {
981
- methods.add(match[1]);
1103
+ handlers.push(extractRouteHandlerBody(content, match.index ?? 0, match[1], "const"));
982
1104
  }
983
- return [...methods];
1105
+ return handlers.sort(
1106
+ (leftHandler, rightHandler) => getMethodRank(leftHandler.method) - getMethodRank(rightHandler.method)
1107
+ );
1108
+ }
1109
+ function extractRouteHandlerBody(content, exportIndex, method, exportKind) {
1110
+ const nextExportIndex = findNextRouteHandlerExport(content, exportIndex + 1);
1111
+ const handlerEnd = nextExportIndex === -1 ? content.length : nextExportIndex;
1112
+ const openBraceIndex = getRouteHandlerBodyOpenBrace(content, exportIndex, handlerEnd, exportKind);
1113
+ if (openBraceIndex !== -1 && openBraceIndex < handlerEnd) {
1114
+ const closeBraceIndex = findMatchingBrace(content, openBraceIndex);
1115
+ if (closeBraceIndex !== -1) {
1116
+ return {
1117
+ method,
1118
+ body: content.slice(openBraceIndex, closeBraceIndex + 1),
1119
+ usedFallbackBody: false
1120
+ };
1121
+ }
1122
+ }
1123
+ return {
1124
+ method,
1125
+ body: content.slice(exportIndex, handlerEnd),
1126
+ usedFallbackBody: true
1127
+ };
1128
+ }
1129
+ function getRouteHandlerBodyOpenBrace(content, exportIndex, handlerEnd, exportKind) {
1130
+ if (exportKind === "function") {
1131
+ const openParenIndex = content.indexOf("(", exportIndex);
1132
+ if (openParenIndex !== -1 && openParenIndex < handlerEnd) {
1133
+ const closeParenIndex = findMatchingParen(content, openParenIndex);
1134
+ if (closeParenIndex !== -1 && closeParenIndex < handlerEnd) {
1135
+ return content.indexOf("{", closeParenIndex);
1136
+ }
1137
+ }
1138
+ }
1139
+ const equalsIndex = content.indexOf("=", exportIndex);
1140
+ if (equalsIndex !== -1 && equalsIndex < handlerEnd) {
1141
+ const arrowIndex = content.indexOf("=>", equalsIndex);
1142
+ if (arrowIndex !== -1 && arrowIndex < handlerEnd) {
1143
+ return content.indexOf("{", arrowIndex);
1144
+ }
1145
+ }
1146
+ return content.indexOf("{", exportIndex);
1147
+ }
1148
+ function findNextRouteHandlerExport(content, startIndex) {
1149
+ const nextExportPattern = /\bexport\s+(?:async\s+)?(?:function|const)\s+(GET|POST|PUT|PATCH|DELETE)\b/g;
1150
+ nextExportPattern.lastIndex = startIndex;
1151
+ const match = nextExportPattern.exec(content);
1152
+ return match?.index ?? -1;
1153
+ }
1154
+ function findMatchingParen(content, openParenIndex) {
1155
+ let depth = 0;
1156
+ for (let index = openParenIndex; index < content.length; index++) {
1157
+ const character = content[index];
1158
+ if (character === "(") {
1159
+ depth++;
1160
+ } else if (character === ")") {
1161
+ depth--;
1162
+ if (depth === 0) {
1163
+ return index;
1164
+ }
1165
+ }
1166
+ }
1167
+ return -1;
1168
+ }
1169
+ function findMatchingBrace(content, openBraceIndex) {
1170
+ let depth = 0;
1171
+ for (let index = openBraceIndex; index < content.length; index++) {
1172
+ const character = content[index];
1173
+ if (character === "{") {
1174
+ depth++;
1175
+ } else if (character === "}") {
1176
+ depth--;
1177
+ if (depth === 0) {
1178
+ return index;
1179
+ }
1180
+ }
1181
+ }
1182
+ return -1;
984
1183
  }
985
1184
  function getRouteHttpMethods(content) {
986
1185
  const methods = new Set(getExportedHttpMethods(content));
@@ -994,10 +1193,12 @@ function getRouteHttpMethods(content) {
994
1193
  }
995
1194
  return [...methods];
996
1195
  }
997
- function hasMutationMethod(methods) {
998
- return ["POST", "PUT", "PATCH", "DELETE"].some(
999
- (method) => methods.includes(method)
1000
- );
1196
+ function getMethodRank(method) {
1197
+ const methodOrder = ["GET", "POST", "PUT", "PATCH", "DELETE"];
1198
+ return methodOrder.indexOf(method);
1199
+ }
1200
+ function isMutationMethod(method) {
1201
+ return method !== "GET";
1001
1202
  }
1002
1203
  function hasAuthOrSessionCheck(content) {
1003
1204
  const normalizedContent = content.toLowerCase();
@@ -1017,7 +1218,7 @@ function hasValidationSignal(content) {
1017
1218
  }
1018
1219
  function hasCacheHeaderSignal(content) {
1019
1220
  const normalizedContent = content.toLowerCase();
1020
- return normalizedContent.includes("cache-control") || normalizedContent.includes("s-maxage") || normalizedContent.includes("stale-while-revalidate") || normalizedContent.includes("public") || normalizedContent.includes("published") || normalizedContent.includes("status");
1221
+ return normalizedContent.includes("cache-control") || normalizedContent.includes("s-maxage") || normalizedContent.includes("stale-while-revalidate") || normalizedContent.includes("public") || normalizedContent.includes("published");
1021
1222
  }
1022
1223
  function hasMethodBlockingSignal(content) {
1023
1224
  const normalizedContent = content.toLowerCase();
@@ -1179,7 +1380,53 @@ function getLargeFileIssueCopy(kind) {
1179
1380
  suggestion: "Review whether this file mixes unrelated responsibilities. If so, split it into smaller modules while preserving behavior."
1180
1381
  };
1181
1382
  }
1182
- function createApiAuthFixPrompt(file) {
1383
+ function createApiAuthFixPrompt(file, method, intent = "unknown") {
1384
+ if (method && intent === "sensitive-mutation") {
1385
+ return `Review the API route at ${file}.
1386
+
1387
+ Qodfy detected a possible issue in the ${method} handler.
1388
+
1389
+ Goal:
1390
+ Determine whether the ${method} handler should be protected.
1391
+
1392
+ Instructions:
1393
+ - Inspect each exported HTTP handler separately.
1394
+ - Do not add authentication to a GET handler if it is intentionally public.
1395
+ - If the ${method} handler handles file uploads, private data, storage writes, payments, account changes, or user-specific actions, add the existing project auth/session check to the ${method} handler.
1396
+ - Also verify protections such as input validation, file size limits for uploads, storage path safety, and rate limiting where relevant.
1397
+ - Do not introduce a new auth provider.
1398
+ - Do not refactor unrelated code.
1399
+ - Keep existing response formats unchanged.
1400
+ - If the ${method} handler is intentionally public, add a short comment explaining why and confirm abuse protection exists.
1401
+
1402
+ Return:
1403
+ - Whether each handler is public or protected.
1404
+ - Whether the ${method} handler should be protected.
1405
+ - The safest code change.
1406
+ - Edge cases to test.`;
1407
+ }
1408
+ if (method && intent === "unknown") {
1409
+ return `Review the API route at ${file}.
1410
+
1411
+ Qodfy detected a mutation handler that should be reviewed: ${method}.
1412
+
1413
+ Goal:
1414
+ Determine whether the ${method} handler should be public or protected.
1415
+
1416
+ Instructions:
1417
+ - Inspect each exported HTTP handler separately.
1418
+ - Check what data the ${method} handler reads, writes, or returns.
1419
+ - If it handles private data, user-specific data, writes, uploads, or admin actions, add the existing project auth/session check.
1420
+ - If it is intentionally public, document why and confirm validation and abuse protection exist.
1421
+ - Do not introduce a new auth provider.
1422
+ - Do not refactor unrelated code.
1423
+ - Keep existing response formats unchanged.
1424
+
1425
+ Return:
1426
+ - Whether the ${method} handler should be protected.
1427
+ - The safest code change, if any.
1428
+ - Edge cases to test.`;
1429
+ }
1183
1430
  return `Review the API route at ${file}.
1184
1431
 
1185
1432
  Goal:
@@ -1198,34 +1445,40 @@ Return:
1198
1445
  - The updated code.
1199
1446
  - Any edge cases I should test.`;
1200
1447
  }
1201
- function createPublicReadRouteFixPrompt(file) {
1448
+ function createPublicReadRouteFixPrompt(file, method = "GET") {
1202
1449
  return `Review the public read API route at ${file}.
1203
1450
 
1451
+ Qodfy detected this as a likely public ${method} handler.
1452
+
1204
1453
  Goal:
1205
- Verify that this route is safe to remain public.
1454
+ Verify that the ${method} handler is safe to remain public.
1206
1455
 
1207
1456
  Instructions:
1208
- - Confirm it only exposes published, public, or non-sensitive data.
1457
+ - Inspect each exported HTTP handler separately.
1458
+ - Confirm the ${method} handler only exposes published, public, or non-sensitive data.
1209
1459
  - Check that route params and query values are validated or sanitized.
1210
1460
  - Check for appropriate cache headers where useful.
1211
1461
  - Check for abuse protection if the route can be called heavily.
1212
- - Do not add user authentication unless the route should be private.
1462
+ - Do not add user authentication to the ${method} handler unless it should be private.
1213
1463
  - Do not refactor unrelated code.
1214
1464
 
1215
1465
  Return:
1216
- - Whether this route appears intentionally public.
1466
+ - Whether the ${method} handler appears intentionally public.
1217
1467
  - Any low-risk safety improvements.
1218
1468
  - Any edge cases I should test.`;
1219
1469
  }
1220
- function createPublicFormProtectionFixPrompt(file) {
1470
+ function createPublicFormProtectionFixPrompt(file, method = "POST") {
1221
1471
  return `Review the public form API route at ${file}.
1222
1472
 
1473
+ Qodfy detected this as a likely public ${method} handler.
1474
+
1223
1475
  Goal:
1224
1476
  Verify validation, rate limiting, and spam protection.
1225
1477
 
1226
1478
  Instructions:
1227
- - Confirm submitted input is validated before it is used.
1228
- - Check for rate limiting or another abuse protection pattern.
1479
+ - Inspect each exported HTTP handler separately.
1480
+ - Confirm submitted input in the ${method} handler is validated before it is used.
1481
+ - Check for rate limiting or another abuse protection pattern on the ${method} handler.
1229
1482
  - Check whether captcha, Turnstile, reCAPTCHA, or hCaptcha is appropriate.
1230
1483
  - Do not add user authentication unless the form should be private.
1231
1484
  - Do not introduce a new service unless necessary.
@@ -1236,18 +1489,24 @@ Return:
1236
1489
  - The safest minimal change if protection is missing.
1237
1490
  - Any edge cases I should test.`;
1238
1491
  }
1239
- function createInternalRouteProtectionFixPrompt(file) {
1492
+ function createInternalRouteProtectionFixPrompt(file, method) {
1493
+ const handlerLine = method ? `
1494
+ Qodfy detected this as a likely internal ${method} handler.
1495
+ ` : "";
1496
+ const handlerReference = method ? `the ${method} handler` : "the route";
1240
1497
  return `Review the internal API route at ${file}.
1498
+ ${handlerLine}
1241
1499
 
1242
1500
  Goal:
1243
1501
  Confirm this operational route is protected before launch.
1244
1502
 
1245
1503
  Instructions:
1246
- - Check whether the route is protected by existing auth, a secret token, or server-only access.
1504
+ - Inspect each exported HTTP handler separately.
1505
+ - Check whether ${handlerReference} is protected by existing auth, a secret token, or server-only access.
1247
1506
  - For cron, cleanup, revalidation, or admin routes, prefer the existing project protection pattern.
1248
1507
  - Do not introduce a new auth provider.
1249
1508
  - Do not change route behavior unless protection is missing.
1250
- - If the route is intentionally reachable, add a short comment explaining the protection boundary.
1509
+ - If ${handlerReference} is intentionally reachable, add a short comment explaining the protection boundary.
1251
1510
 
1252
1511
  Return:
1253
1512
  - Whether protection already exists.
@@ -1301,7 +1560,7 @@ Create a safe refactor plan without changing behavior.
1301
1560
 
1302
1561
  Instructions:
1303
1562
  - Identify the main responsibilities inside this module.
1304
- - Check whether the file mixes unrelated concerns such as data fetching, filtering, mapping, sorting, constants, types, validation, or business rules.
1563
+ - Check whether the module mixes unrelated concerns such as data access, filtering, mapping, sorting, constants, types, validation, or business rules.
1305
1564
  - Suggest smaller TypeScript modules that could be extracted safely.
1306
1565
  - Do not rewrite the whole file at once.
1307
1566
  - Do not change business logic.
@@ -1467,8 +1726,13 @@ Return:
1467
1726
  - The updated code.
1468
1727
  - Any environment variables required.`;
1469
1728
  }
1470
- function createWebhookSignatureFixPrompt(file) {
1729
+ function createWebhookSignatureFixPrompt(file, method) {
1730
+ const handlerLine = method ? `
1731
+ Qodfy detected a possible issue in the ${method} webhook handler.
1732
+ ` : "";
1733
+ const handlerReference = method ? `the ${method} handler` : "the webhook route";
1471
1734
  return `Review the webhook API route at ${file}.
1735
+ ${handlerLine}
1472
1736
 
1473
1737
  Goal:
1474
1738
  Verify that webhook signature validation happens before the event is handled.
@@ -1476,7 +1740,8 @@ Verify that webhook signature validation happens before the event is handled.
1476
1740
  Instructions:
1477
1741
  - Detect which provider this webhook belongs to based on imports, headers, and environment variables.
1478
1742
  - Use the provider's existing verification pattern if already present.
1479
- - Do not process the webhook event before verification unless required by the provider.
1743
+ - Verify that ${handlerReference} validates the provider signature before processing the event unless the provider requires a different order.
1744
+ - Do not add user authentication unless the webhook provider requires it.
1480
1745
  - Do not introduce unrelated changes.
1481
1746
  - If verification already exists, explain where it happens.
1482
1747
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qodfy/core",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Scanner engine for Qodfy, an open-source launch readiness scanner for AI-built apps.",
5
5
  "keywords": [
6
6
  "qodfy",