@qodfy/core 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  type IssueSeverity = "critical" | "warning" | "info";
2
+ type IssueConfidence = "high" | "medium" | "low";
2
3
  type IssueCategory = "security" | "environment" | "api" | "webhook" | "ai" | "maintainability" | "project";
3
4
  declare const validScanChecks: readonly ["project", "api", "environment", "ai", "webhook", "maintainability", "security"];
4
5
  type ScanCheck = typeof validScanChecks[number];
@@ -8,6 +9,7 @@ type Issue = {
8
9
  ruleId: string;
9
10
  category: IssueCategory;
10
11
  severity: IssueSeverity;
12
+ confidence: IssueConfidence;
11
13
  title: string;
12
14
  message: string;
13
15
  file?: string;
@@ -30,7 +32,8 @@ type ScanReport = {
30
32
  type ScanOptions = {
31
33
  projectPath: string;
32
34
  checks?: ScanCheck[];
35
+ includeLowConfidence?: boolean;
33
36
  };
34
37
  declare function scanProject(input: string | ScanOptions): Promise<ScanReport>;
35
38
 
36
- export { type Issue, type IssueCategory, type IssueSeverity, type ScanCheck, type ScanOptions, type ScanReport, recommendedScanChecks, scanProject, validScanChecks };
39
+ export { type Issue, type IssueCategory, type IssueConfidence, type IssueSeverity, type ScanCheck, type ScanOptions, type ScanReport, recommendedScanChecks, scanProject, validScanChecks };
package/dist/index.js CHANGED
@@ -115,6 +115,9 @@ var issueIdPrefixes = {
115
115
  "security-client-side-secret": "security-client-side-secret",
116
116
  "security-hardcoded-secret": "security-hardcoded-secret",
117
117
  "api-route-missing-auth": "security-api-auth",
118
+ "api-public-read-route": "api-public-read-route",
119
+ "api-public-form-abuse-protection": "api-public-form-protection",
120
+ "api-internal-route-protection": "api-internal-route-protection",
118
121
  "ai-route-missing-rate-limit": "ai-route-rate-limit",
119
122
  "maintainability-large-file": "maintainability-large-file",
120
123
  "maintainability-large-file-skipped": "maintainability-large-file-skipped",
@@ -125,6 +128,7 @@ var issueIdPrefixes = {
125
128
  async function scanProject(input) {
126
129
  const startTime = Date.now();
127
130
  const projectPath = typeof input === "string" ? input : input.projectPath;
131
+ const includeLowConfidence = typeof input === "string" ? false : Boolean(input.includeLowConfidence);
128
132
  const enabledChecks = getEnabledChecks(
129
133
  typeof input === "string" ? void 0 : input.checks
130
134
  );
@@ -148,6 +152,7 @@ async function scanProject(input) {
148
152
  ruleId: "project-missing-package-json",
149
153
  category: "project",
150
154
  severity: "critical",
155
+ confidence: "high",
151
156
  title: "Missing package.json",
152
157
  message: "Qodfy could not find a package.json file in this project.",
153
158
  suggestion: "Run Qodfy from the project root or pass --path to the app folder.",
@@ -160,6 +165,7 @@ async function scanProject(input) {
160
165
  ruleId: "project-invalid-package-json",
161
166
  category: "project",
162
167
  severity: "critical",
168
+ confidence: "high",
163
169
  title: "Could not read package.json",
164
170
  message: packageJsonResult.reason,
165
171
  file: "package.json",
@@ -171,6 +177,7 @@ async function scanProject(input) {
171
177
  ruleId: "project-invalid-package-json",
172
178
  category: "project",
173
179
  severity: "critical",
180
+ confidence: "high",
174
181
  title: "Invalid package.json",
175
182
  message: "package.json must contain a JSON object at the top level.",
176
183
  file: "package.json",
@@ -188,6 +195,7 @@ async function scanProject(input) {
188
195
  ruleId: "project-next-not-detected",
189
196
  category: "project",
190
197
  severity: "warning",
198
+ confidence: "low",
191
199
  title: "Next.js not detected",
192
200
  message: "This first version of Qodfy is optimized for Next.js projects.",
193
201
  suggestion: "If this is a monorepo, scan the Next.js app folder directly.",
@@ -204,6 +212,7 @@ async function scanProject(input) {
204
212
  ruleId: "environment-missing-env-example",
205
213
  category: "environment",
206
214
  severity: "warning",
215
+ confidence: "medium",
207
216
  title: "Missing .env.example",
208
217
  message: "Add a .env.example file so future developers know which environment variables are required.",
209
218
  suggestion: "Document required variable names only, never real secret values.",
@@ -216,6 +225,7 @@ async function scanProject(input) {
216
225
  ruleId: "environment-missing-env-example",
217
226
  category: "environment",
218
227
  severity: "warning",
228
+ confidence: "medium",
219
229
  title: "Could not read .env.example",
220
230
  message: envExampleResult.reason,
221
231
  file: ".env.example",
@@ -232,6 +242,7 @@ async function scanProject(input) {
232
242
  ruleId: "project-missing-readme",
233
243
  category: "project",
234
244
  severity: "info",
245
+ confidence: "low",
235
246
  title: "Missing README.md",
236
247
  message: "A README helps other developers understand how to run and maintain the project.",
237
248
  fixPrompt: createReadmeFixPrompt()
@@ -257,6 +268,7 @@ async function scanProject(input) {
257
268
  ruleId: "maintainability-file-unreadable",
258
269
  category: "maintainability",
259
270
  severity: "info",
271
+ confidence: "low",
260
272
  title: "File could not be checked",
261
273
  message: statResult.reason,
262
274
  file: relativeFile,
@@ -272,6 +284,7 @@ async function scanProject(input) {
272
284
  ruleId: "maintainability-large-file-skipped",
273
285
  category: "maintainability",
274
286
  severity: "info",
287
+ confidence: "low",
275
288
  title: "Large file skipped from deep scan",
276
289
  message: "This file is larger than 500KB and was skipped from deep content checks.",
277
290
  file: relativeFile,
@@ -298,6 +311,7 @@ async function scanProject(input) {
298
311
  ruleId: "maintainability-file-unreadable",
299
312
  category: "maintainability",
300
313
  severity: "info",
314
+ confidence: "low",
301
315
  title: "File could not be read",
302
316
  message: fileResult.reason,
303
317
  file: relativeFile,
@@ -318,6 +332,7 @@ async function scanProject(input) {
318
332
  ruleId: "ai-route-missing-rate-limit",
319
333
  category: "ai",
320
334
  severity: "critical",
335
+ confidence: "high",
321
336
  title: "AI route may be missing rate limiting",
322
337
  message: "AI routes can create real API costs. Add rate limiting or usage limits before launch.",
323
338
  file: relativeFile,
@@ -326,12 +341,13 @@ async function scanProject(input) {
326
341
  });
327
342
  }
328
343
  }
329
- const webhookRouteInfo = runWebhookChecks && apiRouteSet.has(file) ? getWebhookRouteInfo(relativeFile, content) : null;
344
+ const webhookRouteInfo = (runWebhookChecks || runApiChecks) && apiRouteSet.has(file) ? getWebhookRouteInfo(relativeFile, content) : null;
330
345
  if (webhookRouteInfo && !hasWebhookSignatureVerification(content, webhookRouteInfo.provider)) {
331
346
  addIssue({
332
347
  ruleId: "webhook-missing-signature-verification",
333
348
  category: "webhook",
334
349
  severity: webhookRouteInfo.confidence === "high" ? "critical" : "warning",
350
+ confidence: webhookRouteInfo.confidence === "high" ? "high" : "medium",
335
351
  title: "Webhook route may be missing signature verification",
336
352
  message: "This webhook route appears to handle external events, but Qodfy could not find signature verification before the event is handled.",
337
353
  file: relativeFile,
@@ -350,6 +366,7 @@ async function scanProject(input) {
350
366
  ruleId: "security-hardcoded-secret",
351
367
  category: "security",
352
368
  severity: "critical",
369
+ confidence: "high",
353
370
  title: "Possible hardcoded secret",
354
371
  message: `A string literal in ${relativeFile} matches the pattern for ${secretMatch.label}. Qodfy does not print possible secret values.`,
355
372
  file: relativeFile,
@@ -359,19 +376,13 @@ async function scanProject(input) {
359
376
  }
360
377
  }
361
378
  if (runApiChecks && apiRouteSet.has(file)) {
362
- const hasAuth = content.includes("auth(") || content.includes("getServerSession") || content.includes("currentUser") || content.includes("clerkClient") || content.includes("session");
363
- if (!hasAuth && !webhookRouteInfo) {
364
- addIssue({
365
- ruleId: "api-route-missing-auth",
366
- category: "api",
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
- }
379
+ addApiRouteProtectionIssues({
380
+ addIssue,
381
+ content,
382
+ includeLowConfidence,
383
+ relativeFile,
384
+ webhookRouteInfo
385
+ });
375
386
  }
376
387
  const usedEnvVariables = runEnvironmentChecks || runSecurityChecks ? getUsedEnvVariables(content) : /* @__PURE__ */ new Set();
377
388
  if (runEnvironmentChecks && envExampleVariables) {
@@ -396,6 +407,7 @@ async function scanProject(input) {
396
407
  ruleId: "security-client-side-secret",
397
408
  category: "security",
398
409
  severity: "warning",
410
+ confidence: "medium",
399
411
  title: "Possible server secret used in client-side code",
400
412
  message: `${variableName} appears in a client-side file. Server secrets should not be exposed to the browser.`,
401
413
  file: relativeFile,
@@ -411,6 +423,7 @@ async function scanProject(input) {
411
423
  ruleId: "maintainability-large-file",
412
424
  category: "maintainability",
413
425
  severity: "info",
426
+ confidence: "low",
414
427
  title: "Large file detected",
415
428
  message: "This file is larger than the recommended maintainability threshold. Large files can be harder to review, test, and safely modify.",
416
429
  file: largeFile.relativeFile,
@@ -424,6 +437,7 @@ async function scanProject(input) {
424
437
  ruleId: "environment-variable-missing-from-example",
425
438
  category: "environment",
426
439
  severity: "warning",
440
+ confidence: "medium",
427
441
  title: "Environment variable missing from .env.example",
428
442
  message: getMissingEnvMessage(variableName, files2),
429
443
  file: files2.length === 1 ? files2[0] : void 0,
@@ -455,8 +469,9 @@ function createIssueFactory(issues) {
455
469
  const currentCount = (issueCounts.get(issue.ruleId) ?? 0) + 1;
456
470
  issueCounts.set(issue.ruleId, currentCount);
457
471
  issues.push({
472
+ ...issue,
458
473
  id: `${getIssueIdPrefix(issue.ruleId, issue.category)}-${currentCount}`,
459
- ...issue
474
+ confidence: issue.confidence ?? "medium"
460
475
  });
461
476
  };
462
477
  }
@@ -574,6 +589,7 @@ async function getSourceFiles(projectPath, addIssue) {
574
589
  ruleId: "project-source-files-unreadable",
575
590
  category: "project",
576
591
  severity: "critical",
592
+ confidence: "high",
577
593
  title: "Could not scan source files",
578
594
  message: "Qodfy could not list source files in this project.",
579
595
  suggestion: "Check that the project path exists and is readable.",
@@ -668,6 +684,191 @@ function isApiRoute(filePath) {
668
684
  const sourceFileExtension = "(?:ts|tsx|js|jsx|mts|cts|mjs|cjs)";
669
685
  return new RegExp(`/app/api(?:/.+)?/route\\.${sourceFileExtension}$`).test(normalizedFile) || new RegExp(`/pages/api/.+\\.${sourceFileExtension}$`).test(normalizedFile);
670
686
  }
687
+ function addApiRouteProtectionIssues({
688
+ addIssue,
689
+ content,
690
+ includeLowConfidence,
691
+ relativeFile,
692
+ webhookRouteInfo
693
+ }) {
694
+ const intent = classifyApiRouteIntent(relativeFile, content, webhookRouteInfo);
695
+ const hasAuth = hasAuthOrSessionCheck(content);
696
+ const methods = getHttpMethods(content);
697
+ if (intent === "webhook") {
698
+ return;
699
+ }
700
+ if (intent === "public-read") {
701
+ if (includeLowConfidence) {
702
+ addIssue({
703
+ ruleId: "api-public-read-route",
704
+ category: "api",
705
+ severity: "info",
706
+ confidence: "low",
707
+ title: "Public read API route detected",
708
+ message: "This route appears intentionally public. Authentication may not be required.",
709
+ file: relativeFile,
710
+ suggestion: "Verify that it only exposes public or published data and has appropriate validation, caching, and abuse protection.",
711
+ fixPrompt: createPublicReadRouteFixPrompt(relativeFile)
712
+ });
713
+ }
714
+ return;
715
+ }
716
+ if (intent === "public-form") {
717
+ if (!hasAbuseProtection(content)) {
718
+ addIssue({
719
+ ruleId: "api-public-form-abuse-protection",
720
+ category: "api",
721
+ severity: "warning",
722
+ confidence: "medium",
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: relativeFile,
726
+ suggestion: "Check for rate limiting, validation, captcha, Turnstile, reCAPTCHA, hCaptcha, or another spam protection pattern.",
727
+ fixPrompt: createPublicFormProtectionFixPrompt(relativeFile)
728
+ });
729
+ }
730
+ return;
731
+ }
732
+ if (intent === "internal") {
733
+ if (!hasInternalRouteProtection(content)) {
734
+ addIssue({
735
+ ruleId: "api-internal-route-protection",
736
+ category: "api",
737
+ severity: "warning",
738
+ confidence: "high",
739
+ title: "Internal API route may be missing protection",
740
+ message: "This route appears internal or operational. Confirm it is protected by auth, a secret token, or server-only access.",
741
+ file: relativeFile,
742
+ suggestion: "Use the project's existing auth pattern or a secret token check for operational routes such as cron, cleanup, or revalidation.",
743
+ fixPrompt: createInternalRouteProtectionFixPrompt(relativeFile)
744
+ });
745
+ }
746
+ return;
747
+ }
748
+ if (intent === "sensitive-mutation") {
749
+ if (!hasAuth) {
750
+ addIssue({
751
+ ruleId: "api-route-missing-auth",
752
+ category: "api",
753
+ severity: "warning",
754
+ confidence: "high",
755
+ title: "Sensitive API route may be missing authentication",
756
+ message: "This route appears to handle user-specific or sensitive operations. Confirm it is protected before launch.",
757
+ file: relativeFile,
758
+ 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)
760
+ });
761
+ }
762
+ return;
763
+ }
764
+ if (hasMutationMethod(methods) && !hasAuth) {
765
+ addIssue({
766
+ ruleId: "api-route-missing-auth",
767
+ category: "api",
768
+ severity: "warning",
769
+ confidence: "medium",
770
+ title: "API mutation route should be reviewed for authentication",
771
+ message: "This route appears to handle a mutation, but Qodfy could not find an auth/session check.",
772
+ file: relativeFile,
773
+ suggestion: "Confirm the route is intentionally public, or add the existing project auth/session check before handling private data.",
774
+ fixPrompt: createApiAuthFixPrompt(relativeFile)
775
+ });
776
+ }
777
+ }
778
+ function classifyApiRouteIntent(relativeFile, content, webhookRouteInfo) {
779
+ const normalizedFile = relativeFile.toLowerCase();
780
+ const methods = getHttpMethods(content);
781
+ if (webhookRouteInfo || routePathHasAny(normalizedFile, ["webhook", "webhooks", "callback"])) {
782
+ return "webhook";
783
+ }
784
+ if (routePathHasAny(normalizedFile, ["internal", "admin", "cron", "cleanup", "revalidate", "private"])) {
785
+ return "internal";
786
+ }
787
+ if (routePathHasAny(normalizedFile, ["contact", "subscribe", "newsletter", "lead", "inquiry"])) {
788
+ return "public-form";
789
+ }
790
+ if (routePathHasAny(normalizedFile, [
791
+ "upload",
792
+ "checkout",
793
+ "order",
794
+ "orders",
795
+ "invoice",
796
+ "invoices",
797
+ "account",
798
+ "user",
799
+ "users",
800
+ "payment",
801
+ "billing",
802
+ "cart",
803
+ "profile",
804
+ "settings"
805
+ ])) {
806
+ return "sensitive-mutation";
807
+ }
808
+ if (routePathHasAny(normalizedFile, [
809
+ "blog",
810
+ "blogs",
811
+ "post",
812
+ "posts",
813
+ "product",
814
+ "products",
815
+ "search",
816
+ "i18n",
817
+ "category",
818
+ "categories",
819
+ "sitemap",
820
+ "rss"
821
+ ])) {
822
+ return "public-read";
823
+ }
824
+ if (hasMutationMethod(methods)) {
825
+ return "unknown";
826
+ }
827
+ if (methods.size === 0 || isReadOnlyRoute(methods)) {
828
+ return "unknown";
829
+ }
830
+ return "unknown";
831
+ }
832
+ function routePathHasAny(normalizedFile, terms) {
833
+ return terms.some((term) => {
834
+ const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
835
+ return new RegExp(`(^|[\\/._\\[\\]-])${escapedTerm}([\\/._\\[\\]-]|$)`).test(normalizedFile);
836
+ });
837
+ }
838
+ function getHttpMethods(content) {
839
+ const methods = /* @__PURE__ */ new Set();
840
+ const exportedMethodPattern = /\bexport\s+(?:async\s+)?(?:function|const)\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b/g;
841
+ const requestMethodPattern = /\b(?:request|req)\.method\s*(?:={2,3}|!={1,2})\s*["'](GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)["']/g;
842
+ const methodCasePattern = /\bcase\s+["'](GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)["']/g;
843
+ for (const match of content.matchAll(exportedMethodPattern)) {
844
+ methods.add(match[1]);
845
+ }
846
+ for (const match of content.matchAll(requestMethodPattern)) {
847
+ methods.add(match[1]);
848
+ }
849
+ for (const match of content.matchAll(methodCasePattern)) {
850
+ methods.add(match[1]);
851
+ }
852
+ return methods;
853
+ }
854
+ function hasMutationMethod(methods) {
855
+ return ["POST", "PUT", "PATCH", "DELETE"].some((method) => methods.has(method));
856
+ }
857
+ function isReadOnlyRoute(methods) {
858
+ return [...methods].every((method) => method === "GET" || method === "HEAD" || method === "OPTIONS");
859
+ }
860
+ function hasAuthOrSessionCheck(content) {
861
+ const normalizedContent = content.toLowerCase();
862
+ 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
+ }
864
+ function hasInternalRouteProtection(content) {
865
+ const normalizedContent = content.toLowerCase();
866
+ return hasAuthOrSessionCheck(content) || /\bprocess\.env\.[A-Za-z0-9_]*SECRET\b/.test(content) || /\bprocess\.env\[['"`][A-Za-z0-9_]*SECRET['"`]\]/.test(content) || normalizedContent.includes("cron_secret") || normalizedContent.includes("revalidate_secret") || normalizedContent.includes("authorization") || normalizedContent.includes("bearer") || normalizedContent.includes("token");
867
+ }
868
+ function hasAbuseProtection(content) {
869
+ const normalizedContent = content.toLowerCase();
870
+ return normalizedContent.includes("ratelimit") || normalizedContent.includes("rate limit") || normalizedContent.includes("limiter") || normalizedContent.includes("captcha") || normalizedContent.includes("turnstile") || normalizedContent.includes("recaptcha") || normalizedContent.includes("hcaptcha") || normalizedContent.includes("validation") || normalizedContent.includes("validate") || normalizedContent.includes("zod") || normalizedContent.includes("safeparse");
871
+ }
671
872
  function getWebhookRouteInfo(relativeFile, content) {
672
873
  const normalizedFile = relativeFile.toLowerCase();
673
874
  const normalizedContent = content.toLowerCase();
@@ -761,6 +962,62 @@ Return:
761
962
  - The updated code.
762
963
  - Any edge cases I should test.`;
763
964
  }
965
+ function createPublicReadRouteFixPrompt(file) {
966
+ return `Review the public read API route at ${file}.
967
+
968
+ Goal:
969
+ Verify that this route is safe to remain public.
970
+
971
+ Instructions:
972
+ - Confirm it only exposes published, public, or non-sensitive data.
973
+ - Check that route params and query values are validated or sanitized.
974
+ - Check for appropriate cache headers where useful.
975
+ - Check for abuse protection if the route can be called heavily.
976
+ - Do not add user authentication unless the route should be private.
977
+ - Do not refactor unrelated code.
978
+
979
+ Return:
980
+ - Whether this route appears intentionally public.
981
+ - Any low-risk safety improvements.
982
+ - Any edge cases I should test.`;
983
+ }
984
+ function createPublicFormProtectionFixPrompt(file) {
985
+ return `Review the public form API route at ${file}.
986
+
987
+ Goal:
988
+ Verify validation, rate limiting, and spam protection.
989
+
990
+ Instructions:
991
+ - Confirm submitted input is validated before it is used.
992
+ - Check for rate limiting or another abuse protection pattern.
993
+ - Check whether captcha, Turnstile, reCAPTCHA, or hCaptcha is appropriate.
994
+ - Do not add user authentication unless the form should be private.
995
+ - Do not introduce a new service unless necessary.
996
+ - Keep existing behavior unchanged.
997
+
998
+ Return:
999
+ - Whether abuse protection already exists.
1000
+ - The safest minimal change if protection is missing.
1001
+ - Any edge cases I should test.`;
1002
+ }
1003
+ function createInternalRouteProtectionFixPrompt(file) {
1004
+ return `Review the internal API route at ${file}.
1005
+
1006
+ Goal:
1007
+ Confirm this operational route is protected before launch.
1008
+
1009
+ Instructions:
1010
+ - Check whether the route is protected by existing auth, a secret token, or server-only access.
1011
+ - For cron, cleanup, revalidation, or admin routes, prefer the existing project protection pattern.
1012
+ - Do not introduce a new auth provider.
1013
+ - Do not change route behavior unless protection is missing.
1014
+ - If the route is intentionally reachable, add a short comment explaining the protection boundary.
1015
+
1016
+ Return:
1017
+ - Whether protection already exists.
1018
+ - The safest minimal change if protection is missing.
1019
+ - Any edge cases I should test.`;
1020
+ }
764
1021
  function createMissingEnvVariableFixPrompt(variableName, files) {
765
1022
  return `Update the environment documentation for this project.
766
1023
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qodfy/core",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Scanner engine for Qodfy, an open-source launch readiness scanner for AI-built apps.",
5
5
  "keywords": [
6
6
  "qodfy",