@qodfy/core 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +9 -1
- package/dist/index.js +395 -22
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
type IssueSeverity = "critical" | "warning" | "info";
|
|
2
|
+
type IssueConfidence = "high" | "medium" | "low";
|
|
3
|
+
type IssueEvidence = {
|
|
4
|
+
label: string;
|
|
5
|
+
detail?: string;
|
|
6
|
+
};
|
|
2
7
|
type IssueCategory = "security" | "environment" | "api" | "webhook" | "ai" | "maintainability" | "project";
|
|
3
8
|
declare const validScanChecks: readonly ["project", "api", "environment", "ai", "webhook", "maintainability", "security"];
|
|
4
9
|
type ScanCheck = typeof validScanChecks[number];
|
|
@@ -8,11 +13,13 @@ type Issue = {
|
|
|
8
13
|
ruleId: string;
|
|
9
14
|
category: IssueCategory;
|
|
10
15
|
severity: IssueSeverity;
|
|
16
|
+
confidence: IssueConfidence;
|
|
11
17
|
title: string;
|
|
12
18
|
message: string;
|
|
13
19
|
file?: string;
|
|
14
20
|
suggestion?: string;
|
|
15
21
|
fixPrompt?: string;
|
|
22
|
+
evidence?: IssueEvidence[];
|
|
16
23
|
};
|
|
17
24
|
type ScanReport = {
|
|
18
25
|
projectPath: string;
|
|
@@ -30,7 +37,8 @@ type ScanReport = {
|
|
|
30
37
|
type ScanOptions = {
|
|
31
38
|
projectPath: string;
|
|
32
39
|
checks?: ScanCheck[];
|
|
40
|
+
includeLowConfidence?: boolean;
|
|
33
41
|
};
|
|
34
42
|
declare function scanProject(input: string | ScanOptions): Promise<ScanReport>;
|
|
35
43
|
|
|
36
|
-
export { type Issue, type IssueCategory, type IssueSeverity, type ScanCheck, type ScanOptions, type ScanReport, recommendedScanChecks, scanProject, validScanChecks };
|
|
44
|
+
export { type Issue, type IssueCategory, type IssueConfidence, type IssueEvidence, type IssueSeverity, type ScanCheck, type ScanOptions, type ScanReport, recommendedScanChecks, scanProject, validScanChecks };
|
package/dist/index.js
CHANGED
|
@@ -114,7 +114,11 @@ var issueIdPrefixes = {
|
|
|
114
114
|
"environment-variable-missing-from-example": "environment-variable-missing-from-example",
|
|
115
115
|
"security-client-side-secret": "security-client-side-secret",
|
|
116
116
|
"security-hardcoded-secret": "security-hardcoded-secret",
|
|
117
|
-
"api-route-missing-auth": "
|
|
117
|
+
"sensitive-api-route-missing-auth": "sensitive-api-route-missing-auth",
|
|
118
|
+
"api-public-read-route": "api-public-read-route",
|
|
119
|
+
"public-form-missing-abuse-protection": "public-form-missing-abuse-protection",
|
|
120
|
+
"internal-route-missing-protection": "internal-route-missing-protection",
|
|
121
|
+
"api-mutation-route-review-auth": "api-mutation-route-review-auth",
|
|
118
122
|
"ai-route-missing-rate-limit": "ai-route-rate-limit",
|
|
119
123
|
"maintainability-large-file": "maintainability-large-file",
|
|
120
124
|
"maintainability-large-file-skipped": "maintainability-large-file-skipped",
|
|
@@ -125,6 +129,7 @@ var issueIdPrefixes = {
|
|
|
125
129
|
async function scanProject(input) {
|
|
126
130
|
const startTime = Date.now();
|
|
127
131
|
const projectPath = typeof input === "string" ? input : input.projectPath;
|
|
132
|
+
const includeLowConfidence = typeof input === "string" ? false : Boolean(input.includeLowConfidence);
|
|
128
133
|
const enabledChecks = getEnabledChecks(
|
|
129
134
|
typeof input === "string" ? void 0 : input.checks
|
|
130
135
|
);
|
|
@@ -148,6 +153,7 @@ async function scanProject(input) {
|
|
|
148
153
|
ruleId: "project-missing-package-json",
|
|
149
154
|
category: "project",
|
|
150
155
|
severity: "critical",
|
|
156
|
+
confidence: "high",
|
|
151
157
|
title: "Missing package.json",
|
|
152
158
|
message: "Qodfy could not find a package.json file in this project.",
|
|
153
159
|
suggestion: "Run Qodfy from the project root or pass --path to the app folder.",
|
|
@@ -160,6 +166,7 @@ async function scanProject(input) {
|
|
|
160
166
|
ruleId: "project-invalid-package-json",
|
|
161
167
|
category: "project",
|
|
162
168
|
severity: "critical",
|
|
169
|
+
confidence: "high",
|
|
163
170
|
title: "Could not read package.json",
|
|
164
171
|
message: packageJsonResult.reason,
|
|
165
172
|
file: "package.json",
|
|
@@ -171,6 +178,7 @@ async function scanProject(input) {
|
|
|
171
178
|
ruleId: "project-invalid-package-json",
|
|
172
179
|
category: "project",
|
|
173
180
|
severity: "critical",
|
|
181
|
+
confidence: "high",
|
|
174
182
|
title: "Invalid package.json",
|
|
175
183
|
message: "package.json must contain a JSON object at the top level.",
|
|
176
184
|
file: "package.json",
|
|
@@ -188,6 +196,7 @@ async function scanProject(input) {
|
|
|
188
196
|
ruleId: "project-next-not-detected",
|
|
189
197
|
category: "project",
|
|
190
198
|
severity: "warning",
|
|
199
|
+
confidence: "low",
|
|
191
200
|
title: "Next.js not detected",
|
|
192
201
|
message: "This first version of Qodfy is optimized for Next.js projects.",
|
|
193
202
|
suggestion: "If this is a monorepo, scan the Next.js app folder directly.",
|
|
@@ -204,6 +213,7 @@ async function scanProject(input) {
|
|
|
204
213
|
ruleId: "environment-missing-env-example",
|
|
205
214
|
category: "environment",
|
|
206
215
|
severity: "warning",
|
|
216
|
+
confidence: "medium",
|
|
207
217
|
title: "Missing .env.example",
|
|
208
218
|
message: "Add a .env.example file so future developers know which environment variables are required.",
|
|
209
219
|
suggestion: "Document required variable names only, never real secret values.",
|
|
@@ -216,6 +226,7 @@ async function scanProject(input) {
|
|
|
216
226
|
ruleId: "environment-missing-env-example",
|
|
217
227
|
category: "environment",
|
|
218
228
|
severity: "warning",
|
|
229
|
+
confidence: "medium",
|
|
219
230
|
title: "Could not read .env.example",
|
|
220
231
|
message: envExampleResult.reason,
|
|
221
232
|
file: ".env.example",
|
|
@@ -232,6 +243,7 @@ async function scanProject(input) {
|
|
|
232
243
|
ruleId: "project-missing-readme",
|
|
233
244
|
category: "project",
|
|
234
245
|
severity: "info",
|
|
246
|
+
confidence: "low",
|
|
235
247
|
title: "Missing README.md",
|
|
236
248
|
message: "A README helps other developers understand how to run and maintain the project.",
|
|
237
249
|
fixPrompt: createReadmeFixPrompt()
|
|
@@ -257,6 +269,7 @@ async function scanProject(input) {
|
|
|
257
269
|
ruleId: "maintainability-file-unreadable",
|
|
258
270
|
category: "maintainability",
|
|
259
271
|
severity: "info",
|
|
272
|
+
confidence: "low",
|
|
260
273
|
title: "File could not be checked",
|
|
261
274
|
message: statResult.reason,
|
|
262
275
|
file: relativeFile,
|
|
@@ -272,6 +285,7 @@ async function scanProject(input) {
|
|
|
272
285
|
ruleId: "maintainability-large-file-skipped",
|
|
273
286
|
category: "maintainability",
|
|
274
287
|
severity: "info",
|
|
288
|
+
confidence: "low",
|
|
275
289
|
title: "Large file skipped from deep scan",
|
|
276
290
|
message: "This file is larger than 500KB and was skipped from deep content checks.",
|
|
277
291
|
file: relativeFile,
|
|
@@ -298,6 +312,7 @@ async function scanProject(input) {
|
|
|
298
312
|
ruleId: "maintainability-file-unreadable",
|
|
299
313
|
category: "maintainability",
|
|
300
314
|
severity: "info",
|
|
315
|
+
confidence: "low",
|
|
301
316
|
title: "File could not be read",
|
|
302
317
|
message: fileResult.reason,
|
|
303
318
|
file: relativeFile,
|
|
@@ -312,12 +327,13 @@ async function scanProject(input) {
|
|
|
312
327
|
);
|
|
313
328
|
if (runAiChecks && usesAI) {
|
|
314
329
|
aiFiles++;
|
|
315
|
-
const hasRateLimit =
|
|
330
|
+
const hasRateLimit = hasRateLimitSignal(content);
|
|
316
331
|
if (apiRouteSet.has(file) && !hasRateLimit) {
|
|
317
332
|
addIssue({
|
|
318
333
|
ruleId: "ai-route-missing-rate-limit",
|
|
319
334
|
category: "ai",
|
|
320
335
|
severity: "critical",
|
|
336
|
+
confidence: "high",
|
|
321
337
|
title: "AI route may be missing rate limiting",
|
|
322
338
|
message: "AI routes can create real API costs. Add rate limiting or usage limits before launch.",
|
|
323
339
|
file: relativeFile,
|
|
@@ -326,17 +342,23 @@ async function scanProject(input) {
|
|
|
326
342
|
});
|
|
327
343
|
}
|
|
328
344
|
}
|
|
329
|
-
const
|
|
330
|
-
|
|
345
|
+
const apiRouteAnalysis = (runWebhookChecks || runApiChecks) && apiRouteSet.has(file) ? analyzeApiRoute({
|
|
346
|
+
file,
|
|
347
|
+
relativeFile,
|
|
348
|
+
content
|
|
349
|
+
}) : null;
|
|
350
|
+
if (runWebhookChecks && apiRouteAnalysis?.intent === "webhook" && !apiRouteAnalysis.hasWebhookVerification) {
|
|
331
351
|
addIssue({
|
|
332
352
|
ruleId: "webhook-missing-signature-verification",
|
|
333
353
|
category: "webhook",
|
|
334
|
-
severity:
|
|
354
|
+
severity: apiRouteAnalysis.confidence === "high" ? "critical" : "warning",
|
|
355
|
+
confidence: apiRouteAnalysis.confidence,
|
|
335
356
|
title: "Webhook route may be missing signature verification",
|
|
336
357
|
message: "This webhook route appears to handle external events, but Qodfy could not find signature verification before the event is handled.",
|
|
337
358
|
file: relativeFile,
|
|
338
|
-
suggestion: getWebhookSignatureSuggestion(
|
|
339
|
-
fixPrompt: createWebhookSignatureFixPrompt(relativeFile)
|
|
359
|
+
suggestion: getWebhookSignatureSuggestion(apiRouteAnalysis.webhookProvider),
|
|
360
|
+
fixPrompt: createWebhookSignatureFixPrompt(relativeFile),
|
|
361
|
+
evidence: apiRouteAnalysis.evidence
|
|
340
362
|
});
|
|
341
363
|
}
|
|
342
364
|
if (runSecurityChecks) {
|
|
@@ -350,6 +372,7 @@ async function scanProject(input) {
|
|
|
350
372
|
ruleId: "security-hardcoded-secret",
|
|
351
373
|
category: "security",
|
|
352
374
|
severity: "critical",
|
|
375
|
+
confidence: "high",
|
|
353
376
|
title: "Possible hardcoded secret",
|
|
354
377
|
message: `A string literal in ${relativeFile} matches the pattern for ${secretMatch.label}. Qodfy does not print possible secret values.`,
|
|
355
378
|
file: relativeFile,
|
|
@@ -358,20 +381,12 @@ async function scanProject(input) {
|
|
|
358
381
|
});
|
|
359
382
|
}
|
|
360
383
|
}
|
|
361
|
-
if (runApiChecks &&
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
severity: "warning",
|
|
368
|
-
title: "API route may be missing authentication",
|
|
369
|
-
message: "This API route does not appear to contain an auth/session check.",
|
|
370
|
-
file: relativeFile,
|
|
371
|
-
suggestion: "Confirm the route is public, or add an auth/session check before handling user data.",
|
|
372
|
-
fixPrompt: createApiAuthFixPrompt(relativeFile)
|
|
373
|
-
});
|
|
374
|
-
}
|
|
384
|
+
if (runApiChecks && apiRouteAnalysis) {
|
|
385
|
+
addApiRouteProtectionIssues({
|
|
386
|
+
addIssue,
|
|
387
|
+
includeLowConfidence,
|
|
388
|
+
analysis: apiRouteAnalysis
|
|
389
|
+
});
|
|
375
390
|
}
|
|
376
391
|
const usedEnvVariables = runEnvironmentChecks || runSecurityChecks ? getUsedEnvVariables(content) : /* @__PURE__ */ new Set();
|
|
377
392
|
if (runEnvironmentChecks && envExampleVariables) {
|
|
@@ -396,6 +411,7 @@ async function scanProject(input) {
|
|
|
396
411
|
ruleId: "security-client-side-secret",
|
|
397
412
|
category: "security",
|
|
398
413
|
severity: "warning",
|
|
414
|
+
confidence: "medium",
|
|
399
415
|
title: "Possible server secret used in client-side code",
|
|
400
416
|
message: `${variableName} appears in a client-side file. Server secrets should not be exposed to the browser.`,
|
|
401
417
|
file: relativeFile,
|
|
@@ -411,6 +427,7 @@ async function scanProject(input) {
|
|
|
411
427
|
ruleId: "maintainability-large-file",
|
|
412
428
|
category: "maintainability",
|
|
413
429
|
severity: "info",
|
|
430
|
+
confidence: "low",
|
|
414
431
|
title: "Large file detected",
|
|
415
432
|
message: "This file is larger than the recommended maintainability threshold. Large files can be harder to review, test, and safely modify.",
|
|
416
433
|
file: largeFile.relativeFile,
|
|
@@ -424,6 +441,7 @@ async function scanProject(input) {
|
|
|
424
441
|
ruleId: "environment-variable-missing-from-example",
|
|
425
442
|
category: "environment",
|
|
426
443
|
severity: "warning",
|
|
444
|
+
confidence: "medium",
|
|
427
445
|
title: "Environment variable missing from .env.example",
|
|
428
446
|
message: getMissingEnvMessage(variableName, files2),
|
|
429
447
|
file: files2.length === 1 ? files2[0] : void 0,
|
|
@@ -455,8 +473,9 @@ function createIssueFactory(issues) {
|
|
|
455
473
|
const currentCount = (issueCounts.get(issue.ruleId) ?? 0) + 1;
|
|
456
474
|
issueCounts.set(issue.ruleId, currentCount);
|
|
457
475
|
issues.push({
|
|
476
|
+
...issue,
|
|
458
477
|
id: `${getIssueIdPrefix(issue.ruleId, issue.category)}-${currentCount}`,
|
|
459
|
-
|
|
478
|
+
confidence: issue.confidence ?? "medium"
|
|
460
479
|
});
|
|
461
480
|
};
|
|
462
481
|
}
|
|
@@ -574,6 +593,7 @@ async function getSourceFiles(projectPath, addIssue) {
|
|
|
574
593
|
ruleId: "project-source-files-unreadable",
|
|
575
594
|
category: "project",
|
|
576
595
|
severity: "critical",
|
|
596
|
+
confidence: "high",
|
|
577
597
|
title: "Could not scan source files",
|
|
578
598
|
message: "Qodfy could not list source files in this project.",
|
|
579
599
|
suggestion: "Check that the project path exists and is readable.",
|
|
@@ -668,6 +688,303 @@ function isApiRoute(filePath) {
|
|
|
668
688
|
const sourceFileExtension = "(?:ts|tsx|js|jsx|mts|cts|mjs|cjs)";
|
|
669
689
|
return new RegExp(`/app/api(?:/.+)?/route\\.${sourceFileExtension}$`).test(normalizedFile) || new RegExp(`/pages/api/.+\\.${sourceFileExtension}$`).test(normalizedFile);
|
|
670
690
|
}
|
|
691
|
+
function addApiRouteProtectionIssues({
|
|
692
|
+
addIssue,
|
|
693
|
+
includeLowConfidence,
|
|
694
|
+
analysis
|
|
695
|
+
}) {
|
|
696
|
+
if (analysis.intent === "webhook") {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (analysis.intent === "public-read") {
|
|
700
|
+
if (includeLowConfidence) {
|
|
701
|
+
addIssue({
|
|
702
|
+
ruleId: "api-public-read-route",
|
|
703
|
+
category: "api",
|
|
704
|
+
severity: "info",
|
|
705
|
+
confidence: analysis.confidence,
|
|
706
|
+
title: "Public read API route detected",
|
|
707
|
+
message: "This route appears intentionally public. Authentication may not be required.",
|
|
708
|
+
file: analysis.relativeFile,
|
|
709
|
+
suggestion: "Verify that it only exposes public or published data and has appropriate validation, caching, and abuse protection.",
|
|
710
|
+
fixPrompt: createPublicReadRouteFixPrompt(analysis.relativeFile),
|
|
711
|
+
evidence: analysis.evidence
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
if (analysis.intent === "public-form") {
|
|
717
|
+
if (!analysis.hasRateLimit && !analysis.hasValidation) {
|
|
718
|
+
addIssue({
|
|
719
|
+
ruleId: "public-form-missing-abuse-protection",
|
|
720
|
+
category: "api",
|
|
721
|
+
severity: "warning",
|
|
722
|
+
confidence: analysis.confidence,
|
|
723
|
+
title: "Public form route may be missing abuse protection",
|
|
724
|
+
message: "This route appears to accept public submissions. Consider adding rate limiting, validation, or spam protection.",
|
|
725
|
+
file: analysis.relativeFile,
|
|
726
|
+
suggestion: "Check for rate limiting, validation, captcha, Turnstile, reCAPTCHA, hCaptcha, or another spam protection pattern.",
|
|
727
|
+
fixPrompt: createPublicFormProtectionFixPrompt(analysis.relativeFile),
|
|
728
|
+
evidence: analysis.evidence
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (analysis.intent === "internal") {
|
|
734
|
+
if (!analysis.hasAuth && !analysis.hasSecretProtection) {
|
|
735
|
+
addIssue({
|
|
736
|
+
ruleId: "internal-route-missing-protection",
|
|
737
|
+
category: "security",
|
|
738
|
+
severity: "warning",
|
|
739
|
+
confidence: analysis.confidence,
|
|
740
|
+
title: "Internal API route may be missing protection",
|
|
741
|
+
message: "This route appears internal or operational. Confirm it is protected by auth, a secret token, or server-only access.",
|
|
742
|
+
file: analysis.relativeFile,
|
|
743
|
+
suggestion: "Use the project's existing auth pattern or a secret token check for operational routes such as cron, cleanup, or revalidation.",
|
|
744
|
+
fixPrompt: createInternalRouteProtectionFixPrompt(analysis.relativeFile),
|
|
745
|
+
evidence: analysis.evidence
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (analysis.intent === "sensitive-mutation") {
|
|
751
|
+
if (!analysis.hasAuth) {
|
|
752
|
+
addIssue({
|
|
753
|
+
ruleId: "sensitive-api-route-missing-auth",
|
|
754
|
+
category: "security",
|
|
755
|
+
severity: "warning",
|
|
756
|
+
confidence: analysis.confidence,
|
|
757
|
+
title: "Sensitive API route may be missing authentication",
|
|
758
|
+
message: "This route appears to handle user-specific or sensitive operations. Confirm it is protected before launch.",
|
|
759
|
+
file: analysis.relativeFile,
|
|
760
|
+
suggestion: "Review the existing project auth/session pattern and apply it if this route handles private data, uploads, payments, or account changes.",
|
|
761
|
+
fixPrompt: createApiAuthFixPrompt(analysis.relativeFile),
|
|
762
|
+
evidence: analysis.evidence
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (analysis.authExpected === "review" && !analysis.hasAuth) {
|
|
768
|
+
addIssue({
|
|
769
|
+
ruleId: "api-mutation-route-review-auth",
|
|
770
|
+
category: "api",
|
|
771
|
+
severity: "warning",
|
|
772
|
+
confidence: analysis.confidence,
|
|
773
|
+
title: "API mutation route should be reviewed for authentication",
|
|
774
|
+
message: "This route mutates data or handles requests, but Qodfy could not determine whether authentication is required.",
|
|
775
|
+
file: analysis.relativeFile,
|
|
776
|
+
suggestion: "Confirm the route is intentionally public, or add the existing project auth/session check before handling private data.",
|
|
777
|
+
fixPrompt: createApiAuthFixPrompt(analysis.relativeFile),
|
|
778
|
+
evidence: analysis.evidence
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
function analyzeApiRoute({
|
|
783
|
+
file,
|
|
784
|
+
relativeFile,
|
|
785
|
+
content
|
|
786
|
+
}) {
|
|
787
|
+
const normalizedFile = relativeFile.toLowerCase();
|
|
788
|
+
const methods = getRouteHttpMethods(content);
|
|
789
|
+
const evidence = [];
|
|
790
|
+
const webhookRouteInfo = getWebhookRouteInfo(relativeFile, content);
|
|
791
|
+
const hasAuth = hasAuthOrSessionCheck(content);
|
|
792
|
+
const hasSecretProtection = hasSecretProtectionSignal(content);
|
|
793
|
+
const hasRateLimit = hasRateLimitSignal(content);
|
|
794
|
+
const hasValidation = hasValidationSignal(content);
|
|
795
|
+
const hasCacheHeaders = hasCacheHeaderSignal(content);
|
|
796
|
+
const hasMethodBlocking = hasMethodBlockingSignal(content);
|
|
797
|
+
const webhookProvider = webhookRouteInfo?.provider ?? "unknown";
|
|
798
|
+
const hasWebhookVerification = hasWebhookSignatureVerification(content, webhookProvider);
|
|
799
|
+
if (methods.length > 0) {
|
|
800
|
+
for (const method of methods) {
|
|
801
|
+
evidence.push({ label: "exports", detail: method });
|
|
802
|
+
}
|
|
803
|
+
} else {
|
|
804
|
+
evidence.push({ label: "no exported HTTP method detected" });
|
|
805
|
+
}
|
|
806
|
+
const webhookPathMatch = getRoutePathMatch(normalizedFile, ["webhook", "webhooks", "callback"]);
|
|
807
|
+
const internalPathMatch = getRoutePathMatch(normalizedFile, ["internal", "admin", "cron", "cleanup", "revalidate", "private"]);
|
|
808
|
+
const formPathMatch = getRoutePathMatch(normalizedFile, ["contact", "subscribe", "newsletter", "lead", "inquiry"]);
|
|
809
|
+
const sensitivePathMatch = getRoutePathMatch(normalizedFile, [
|
|
810
|
+
"upload",
|
|
811
|
+
"checkout",
|
|
812
|
+
"order",
|
|
813
|
+
"orders",
|
|
814
|
+
"invoice",
|
|
815
|
+
"invoices",
|
|
816
|
+
"account",
|
|
817
|
+
"user",
|
|
818
|
+
"users",
|
|
819
|
+
"payment",
|
|
820
|
+
"billing",
|
|
821
|
+
"cart",
|
|
822
|
+
"profile",
|
|
823
|
+
"settings"
|
|
824
|
+
]);
|
|
825
|
+
const publicContentPathMatch = getRoutePathMatch(normalizedFile, [
|
|
826
|
+
"blog",
|
|
827
|
+
"blogs",
|
|
828
|
+
"post",
|
|
829
|
+
"posts",
|
|
830
|
+
"product",
|
|
831
|
+
"products",
|
|
832
|
+
"search",
|
|
833
|
+
"i18n",
|
|
834
|
+
"category",
|
|
835
|
+
"categories",
|
|
836
|
+
"sitemap",
|
|
837
|
+
"rss"
|
|
838
|
+
]);
|
|
839
|
+
let intent = "unknown";
|
|
840
|
+
if (webhookRouteInfo || webhookPathMatch) {
|
|
841
|
+
intent = "webhook";
|
|
842
|
+
evidence.push({
|
|
843
|
+
label: webhookPathMatch ? "webhook path detected" : "webhook content detected",
|
|
844
|
+
detail: webhookPathMatch ?? webhookProvider
|
|
845
|
+
});
|
|
846
|
+
} else if (internalPathMatch) {
|
|
847
|
+
intent = "internal";
|
|
848
|
+
evidence.push({ label: "path contains", detail: internalPathMatch });
|
|
849
|
+
} else if (formPathMatch) {
|
|
850
|
+
intent = "public-form";
|
|
851
|
+
evidence.push({ label: "path contains", detail: formPathMatch });
|
|
852
|
+
} else if (hasMutationMethod(methods) && sensitivePathMatch) {
|
|
853
|
+
intent = "sensitive-mutation";
|
|
854
|
+
evidence.push({ label: "path contains", detail: sensitivePathMatch });
|
|
855
|
+
} else if (methods.includes("GET") && publicContentPathMatch && !sensitivePathMatch && !internalPathMatch) {
|
|
856
|
+
intent = "public-read";
|
|
857
|
+
evidence.push({ label: "public content route detected", detail: publicContentPathMatch });
|
|
858
|
+
}
|
|
859
|
+
if (hasAuth) {
|
|
860
|
+
evidence.push({ label: "auth/session check detected" });
|
|
861
|
+
} else {
|
|
862
|
+
evidence.push({ label: "no auth/session check detected" });
|
|
863
|
+
}
|
|
864
|
+
if (hasSecretProtection) {
|
|
865
|
+
evidence.push({ label: "secret token check detected" });
|
|
866
|
+
}
|
|
867
|
+
if (hasRateLimit) {
|
|
868
|
+
evidence.push({ label: "rate limit detected" });
|
|
869
|
+
} else if (intent === "public-form") {
|
|
870
|
+
evidence.push({ label: "no rate limit detected" });
|
|
871
|
+
}
|
|
872
|
+
if (hasValidation) {
|
|
873
|
+
evidence.push({ label: "validation detected" });
|
|
874
|
+
} else if (intent === "public-form") {
|
|
875
|
+
evidence.push({ label: "no validation detected" });
|
|
876
|
+
}
|
|
877
|
+
if (hasCacheHeaders) {
|
|
878
|
+
evidence.push({ label: "cache/public-read safety signal detected" });
|
|
879
|
+
}
|
|
880
|
+
if (hasMethodBlocking) {
|
|
881
|
+
evidence.push({ label: "method blocking detected" });
|
|
882
|
+
}
|
|
883
|
+
if (intent === "webhook") {
|
|
884
|
+
if (hasWebhookVerification) {
|
|
885
|
+
evidence.push({ label: "webhook signature verification detected" });
|
|
886
|
+
} else {
|
|
887
|
+
evidence.push({ label: "no webhook signature verification detected" });
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return {
|
|
891
|
+
file,
|
|
892
|
+
relativeFile,
|
|
893
|
+
methods,
|
|
894
|
+
intent,
|
|
895
|
+
authExpected: getAuthExpectation(intent, methods),
|
|
896
|
+
confidence: getApiRouteConfidence(intent, methods, webhookRouteInfo),
|
|
897
|
+
evidence,
|
|
898
|
+
hasAuth,
|
|
899
|
+
hasSecretProtection,
|
|
900
|
+
hasRateLimit,
|
|
901
|
+
hasValidation,
|
|
902
|
+
hasCacheHeaders,
|
|
903
|
+
hasMethodBlocking,
|
|
904
|
+
hasWebhookVerification,
|
|
905
|
+
webhookProvider
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
function getAuthExpectation(intent, methods) {
|
|
909
|
+
if (intent === "internal" || intent === "sensitive-mutation") {
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
if (intent === "unknown" && hasMutationMethod(methods)) {
|
|
913
|
+
return "review";
|
|
914
|
+
}
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
function getApiRouteConfidence(intent, methods, webhookRouteInfo) {
|
|
918
|
+
if (intent === "sensitive-mutation" || intent === "internal") {
|
|
919
|
+
return "high";
|
|
920
|
+
}
|
|
921
|
+
if (intent === "webhook") {
|
|
922
|
+
return webhookRouteInfo?.confidence === "high" ? "high" : "medium";
|
|
923
|
+
}
|
|
924
|
+
if (intent === "public-form" || intent === "unknown" && hasMutationMethod(methods)) {
|
|
925
|
+
return "medium";
|
|
926
|
+
}
|
|
927
|
+
return "low";
|
|
928
|
+
}
|
|
929
|
+
function getRoutePathMatch(normalizedFile, terms) {
|
|
930
|
+
return terms.find((term) => {
|
|
931
|
+
const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
932
|
+
return new RegExp(`(^|[\\/._\\[\\]-])${escapedTerm}([\\/._\\[\\]-]|$)`).test(normalizedFile);
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
function getExportedHttpMethods(content) {
|
|
936
|
+
const methods = /* @__PURE__ */ new Set();
|
|
937
|
+
const functionExportPattern = /\bexport\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\b/g;
|
|
938
|
+
const constExportPattern = /\bexport\s+const\s+(GET|POST|PUT|PATCH|DELETE)\b/g;
|
|
939
|
+
for (const match of content.matchAll(functionExportPattern)) {
|
|
940
|
+
methods.add(match[1]);
|
|
941
|
+
}
|
|
942
|
+
for (const match of content.matchAll(constExportPattern)) {
|
|
943
|
+
methods.add(match[1]);
|
|
944
|
+
}
|
|
945
|
+
return [...methods];
|
|
946
|
+
}
|
|
947
|
+
function getRouteHttpMethods(content) {
|
|
948
|
+
const methods = new Set(getExportedHttpMethods(content));
|
|
949
|
+
const requestMethodPattern = /\b(?:request|req)\.method\s*(?:={2,3}|!={1,2})\s*["'](GET|POST|PUT|PATCH|DELETE)["']/g;
|
|
950
|
+
const methodCasePattern = /\bcase\s+["'](GET|POST|PUT|PATCH|DELETE)["']/g;
|
|
951
|
+
for (const match of content.matchAll(requestMethodPattern)) {
|
|
952
|
+
methods.add(match[1]);
|
|
953
|
+
}
|
|
954
|
+
for (const match of content.matchAll(methodCasePattern)) {
|
|
955
|
+
methods.add(match[1]);
|
|
956
|
+
}
|
|
957
|
+
return [...methods];
|
|
958
|
+
}
|
|
959
|
+
function hasMutationMethod(methods) {
|
|
960
|
+
return ["POST", "PUT", "PATCH", "DELETE"].some(
|
|
961
|
+
(method) => methods.includes(method)
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
function hasAuthOrSessionCheck(content) {
|
|
965
|
+
const normalizedContent = content.toLowerCase();
|
|
966
|
+
return normalizedContent.includes("auth(") || normalizedContent.includes("getserversession") || normalizedContent.includes("currentuser") || normalizedContent.includes("clerkclient") || normalizedContent.includes("session") || normalizedContent.includes("requireauth") || normalizedContent.includes("requireuser") || normalizedContent.includes("requireadmin") || normalizedContent.includes("verifysession") || normalizedContent.includes("getuser") || normalizedContent.includes("jwt") || normalizedContent.includes("authorization") || normalizedContent.includes("bearer") || normalizedContent.includes("cookies()") || normalizedContent.includes("request.cookies") || normalizedContent.includes("middleware");
|
|
967
|
+
}
|
|
968
|
+
function hasSecretProtectionSignal(content) {
|
|
969
|
+
const normalizedContent = content.toLowerCase();
|
|
970
|
+
return /\bprocess\.env\.[A-Za-z0-9_]*SECRET\b/.test(content) || /\bprocess\.env\[['"`][A-Za-z0-9_]*SECRET['"`]\]/.test(content) || normalizedContent.includes("cron_secret") || normalizedContent.includes("revalidate_secret") || normalizedContent.includes("authorization") || normalizedContent.includes("bearer") || normalizedContent.includes("token");
|
|
971
|
+
}
|
|
972
|
+
function hasRateLimitSignal(content) {
|
|
973
|
+
const normalizedContent = content.toLowerCase();
|
|
974
|
+
return normalizedContent.includes("ratelimit") || normalizedContent.includes("rate limit") || normalizedContent.includes("limiter") || normalizedContent.includes("upstash") || normalizedContent.includes("throttle");
|
|
975
|
+
}
|
|
976
|
+
function hasValidationSignal(content) {
|
|
977
|
+
const normalizedContent = content.toLowerCase();
|
|
978
|
+
return normalizedContent.includes("zod") || normalizedContent.includes("schema") || normalizedContent.includes("validate") || normalizedContent.includes("validation") || normalizedContent.includes("sanitize") || normalizedContent.includes("safeparse") || normalizedContent.includes("parse(") || normalizedContent.includes("slugregex") || normalizedContent.includes("isvalid") || normalizedContent.includes("captcha") || normalizedContent.includes("turnstile") || normalizedContent.includes("recaptcha") || normalizedContent.includes("hcaptcha");
|
|
979
|
+
}
|
|
980
|
+
function hasCacheHeaderSignal(content) {
|
|
981
|
+
const normalizedContent = content.toLowerCase();
|
|
982
|
+
return normalizedContent.includes("cache-control") || normalizedContent.includes("s-maxage") || normalizedContent.includes("stale-while-revalidate") || normalizedContent.includes("public") || normalizedContent.includes("published") || normalizedContent.includes("status");
|
|
983
|
+
}
|
|
984
|
+
function hasMethodBlockingSignal(content) {
|
|
985
|
+
const normalizedContent = content.toLowerCase();
|
|
986
|
+
return normalizedContent.includes("method not allowed") || normalizedContent.includes("status: 405") || /\b405\b/.test(normalizedContent);
|
|
987
|
+
}
|
|
671
988
|
function getWebhookRouteInfo(relativeFile, content) {
|
|
672
989
|
const normalizedFile = relativeFile.toLowerCase();
|
|
673
990
|
const normalizedContent = content.toLowerCase();
|
|
@@ -761,6 +1078,62 @@ Return:
|
|
|
761
1078
|
- The updated code.
|
|
762
1079
|
- Any edge cases I should test.`;
|
|
763
1080
|
}
|
|
1081
|
+
function createPublicReadRouteFixPrompt(file) {
|
|
1082
|
+
return `Review the public read API route at ${file}.
|
|
1083
|
+
|
|
1084
|
+
Goal:
|
|
1085
|
+
Verify that this route is safe to remain public.
|
|
1086
|
+
|
|
1087
|
+
Instructions:
|
|
1088
|
+
- Confirm it only exposes published, public, or non-sensitive data.
|
|
1089
|
+
- Check that route params and query values are validated or sanitized.
|
|
1090
|
+
- Check for appropriate cache headers where useful.
|
|
1091
|
+
- Check for abuse protection if the route can be called heavily.
|
|
1092
|
+
- Do not add user authentication unless the route should be private.
|
|
1093
|
+
- Do not refactor unrelated code.
|
|
1094
|
+
|
|
1095
|
+
Return:
|
|
1096
|
+
- Whether this route appears intentionally public.
|
|
1097
|
+
- Any low-risk safety improvements.
|
|
1098
|
+
- Any edge cases I should test.`;
|
|
1099
|
+
}
|
|
1100
|
+
function createPublicFormProtectionFixPrompt(file) {
|
|
1101
|
+
return `Review the public form API route at ${file}.
|
|
1102
|
+
|
|
1103
|
+
Goal:
|
|
1104
|
+
Verify validation, rate limiting, and spam protection.
|
|
1105
|
+
|
|
1106
|
+
Instructions:
|
|
1107
|
+
- Confirm submitted input is validated before it is used.
|
|
1108
|
+
- Check for rate limiting or another abuse protection pattern.
|
|
1109
|
+
- Check whether captcha, Turnstile, reCAPTCHA, or hCaptcha is appropriate.
|
|
1110
|
+
- Do not add user authentication unless the form should be private.
|
|
1111
|
+
- Do not introduce a new service unless necessary.
|
|
1112
|
+
- Keep existing behavior unchanged.
|
|
1113
|
+
|
|
1114
|
+
Return:
|
|
1115
|
+
- Whether abuse protection already exists.
|
|
1116
|
+
- The safest minimal change if protection is missing.
|
|
1117
|
+
- Any edge cases I should test.`;
|
|
1118
|
+
}
|
|
1119
|
+
function createInternalRouteProtectionFixPrompt(file) {
|
|
1120
|
+
return `Review the internal API route at ${file}.
|
|
1121
|
+
|
|
1122
|
+
Goal:
|
|
1123
|
+
Confirm this operational route is protected before launch.
|
|
1124
|
+
|
|
1125
|
+
Instructions:
|
|
1126
|
+
- Check whether the route is protected by existing auth, a secret token, or server-only access.
|
|
1127
|
+
- For cron, cleanup, revalidation, or admin routes, prefer the existing project protection pattern.
|
|
1128
|
+
- Do not introduce a new auth provider.
|
|
1129
|
+
- Do not change route behavior unless protection is missing.
|
|
1130
|
+
- If the route is intentionally reachable, add a short comment explaining the protection boundary.
|
|
1131
|
+
|
|
1132
|
+
Return:
|
|
1133
|
+
- Whether protection already exists.
|
|
1134
|
+
- The safest minimal change if protection is missing.
|
|
1135
|
+
- Any edge cases I should test.`;
|
|
1136
|
+
}
|
|
764
1137
|
function createMissingEnvVariableFixPrompt(variableName, files) {
|
|
765
1138
|
return `Update the environment documentation for this project.
|
|
766
1139
|
|