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