@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 +1 -0
- package/dist/index.js +433 -168
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
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
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
735
|
-
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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: "
|
|
792
|
-
category: "
|
|
827
|
+
ruleId: "api-mutation-route-review-auth",
|
|
828
|
+
category: "api",
|
|
793
829
|
severity: "warning",
|
|
794
|
-
confidence:
|
|
795
|
-
title: "
|
|
796
|
-
message:
|
|
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: "
|
|
799
|
-
fixPrompt: createApiAuthFixPrompt(analysis.relativeFile),
|
|
800
|
-
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
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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 (
|
|
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:
|
|
954
|
+
evidence.push({ label: `auth/session check detected in ${method} handler` });
|
|
899
955
|
} else {
|
|
900
|
-
evidence.push({ label:
|
|
956
|
+
evidence.push({ label: `no auth/session check detected in ${method} handler` });
|
|
901
957
|
}
|
|
902
958
|
if (hasSecretProtection) {
|
|
903
|
-
evidence.push({ label:
|
|
959
|
+
evidence.push({ label: `secret token check detected in ${method} handler` });
|
|
904
960
|
}
|
|
905
|
-
if (
|
|
906
|
-
evidence.push({
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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 (
|
|
916
|
-
|
|
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
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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
|
-
|
|
930
|
-
relativeFile,
|
|
931
|
-
methods,
|
|
992
|
+
method,
|
|
932
993
|
intent,
|
|
933
|
-
authExpected:
|
|
934
|
-
confidence:
|
|
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
|
|
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 (
|
|
1059
|
+
if (isMutationMethod(method)) {
|
|
951
1060
|
return "review";
|
|
952
1061
|
}
|
|
953
1062
|
return false;
|
|
954
1063
|
}
|
|
955
|
-
function
|
|
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" &&
|
|
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
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
-
|
|
1100
|
+
handlers.push(extractRouteHandlerBody(content, match.index ?? 0, match[1], "function"));
|
|
979
1101
|
}
|
|
980
1102
|
for (const match of content.matchAll(constExportPattern)) {
|
|
981
|
-
|
|
1103
|
+
handlers.push(extractRouteHandlerBody(content, match.index ?? 0, match[1], "const"));
|
|
982
1104
|
}
|
|
983
|
-
return
|
|
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
|
|
998
|
-
|
|
999
|
-
|
|
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")
|
|
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
|
|
1454
|
+
Verify that the ${method} handler is safe to remain public.
|
|
1206
1455
|
|
|
1207
1456
|
Instructions:
|
|
1208
|
-
-
|
|
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
|
|
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
|
|
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
|
-
-
|
|
1228
|
-
-
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|