@qodfy/core 0.1.5 → 0.1.6

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.
Files changed (2) hide show
  1. package/dist/index.js +77 -29
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -236,26 +236,14 @@ async function scanProject(projectPath) {
236
236
  });
237
237
  }
238
238
  }
239
- const isStripeWebhook = isStripeWebhookRoute(relativeFile, content);
240
- const hasStripeSignatureVerification = hasStripeWebhookSignatureVerification(content);
241
- const isClerkWebhook = isClerkWebhookRoute(relativeFile, content);
242
- const hasClerkSignatureVerification = hasClerkWebhookSignatureVerification(content);
243
- if (isStripeWebhook && !hasStripeSignatureVerification) {
239
+ const webhookRouteInfo = apiRouteSet.has(file) ? getWebhookRouteInfo(relativeFile, content) : null;
240
+ if (webhookRouteInfo && !hasWebhookSignatureVerification(content, webhookRouteInfo.provider)) {
244
241
  issues.push({
245
- severity: "critical",
246
- title: "Stripe webhook may be missing signature verification",
247
- message: "This Stripe webhook route does not appear to verify the Stripe signature before handling the event.",
242
+ severity: webhookRouteInfo.confidence === "high" ? "critical" : "warning",
243
+ title: "Webhook route may be missing signature verification",
244
+ message: "This webhook route appears to handle external events, but Qodfy could not find signature verification before the event is handled.",
248
245
  file: relativeFile,
249
- suggestion: "Use stripe.webhooks.constructEvent with the raw request body, Stripe-Signature header, and STRIPE_WEBHOOK_SECRET."
250
- });
251
- }
252
- if (isClerkWebhook && !hasClerkSignatureVerification) {
253
- issues.push({
254
- severity: "critical",
255
- title: "Clerk webhook may be missing signature verification",
256
- message: "This Clerk webhook route does not appear to verify the webhook signature before handling the event.",
257
- file: relativeFile,
258
- suggestion: "Use svix Webhook(...).verify(...) with CLERK_WEBHOOK_SECRET and Clerk webhook headers."
246
+ suggestion: getWebhookSignatureSuggestion(webhookRouteInfo.provider)
259
247
  });
260
248
  }
261
249
  for (const secretMatch of getHardcodedSecretMatches(content)) {
@@ -274,7 +262,7 @@ async function scanProject(projectPath) {
274
262
  }
275
263
  if (apiRouteSet.has(file)) {
276
264
  const hasAuth = content.includes("auth(") || content.includes("getServerSession") || content.includes("currentUser") || content.includes("clerkClient") || content.includes("session");
277
- if (!hasAuth && !isStripeWebhook && !isClerkWebhook) {
265
+ if (!hasAuth && !webhookRouteInfo) {
278
266
  issues.push({
279
267
  severity: "warning",
280
268
  title: "API route may be missing authentication",
@@ -536,23 +524,83 @@ function isClientSideFile(relativeFile, content) {
536
524
  return fileName.includes(".client.") || /(^|\n)\s*["']use client["'];?/.test(content);
537
525
  }
538
526
  function isApiRoute(filePath) {
539
- return filePath.includes(`${path.sep}app${path.sep}api${path.sep}`) || filePath.includes(`${path.sep}pages${path.sep}api${path.sep}`);
527
+ const normalizedFile = normalizePath(filePath);
528
+ const sourceFileExtension = "(?:ts|tsx|js|jsx|mts|cts|mjs|cjs)";
529
+ return new RegExp(`/app/api(?:/.+)?/route\\.${sourceFileExtension}$`).test(normalizedFile) || new RegExp(`/pages/api/.+\\.${sourceFileExtension}$`).test(normalizedFile);
540
530
  }
541
- function isStripeWebhookRoute(relativeFile, content) {
531
+ function getWebhookRouteInfo(relativeFile, content) {
542
532
  const normalizedFile = relativeFile.toLowerCase();
543
533
  const normalizedContent = content.toLowerCase();
544
- return normalizedContent.includes("stripe") && (normalizedFile.includes("webhook") || normalizedContent.includes("webhook") || normalizedContent.includes("stripe.webhooks"));
534
+ const normalizedRouteContext = `${normalizedFile}
535
+ ${normalizedContent}`;
536
+ const pathLooksLikeWebhook = normalizedFile.includes("webhook") || normalizedFile.includes("callback");
537
+ const contentStronglySuggestsWebhook = normalizedContent.includes("stripe.webhooks") || normalizedContent.includes("constructevent(") || normalizedContent.includes("stripe-signature") || normalizedContent.includes("stripe_webhook_secret") || normalizedContent.includes("clerk_webhook_secret") || normalizedContent.includes("svix") || normalizedContent.includes("x-github-event") || normalizedContent.includes("x-hub-signature") || normalizedContent.includes("x-shopify-hmac-sha256") || normalizedContent.includes("resend") && normalizedContent.includes("webhook") || normalizedContent.includes("webhook_secret") || normalizedContent.includes("webhooksecret") || normalizedContent.includes("webhook") && (normalizedContent.includes("signature") || normalizedContent.includes("secret") || normalizedContent.includes("event"));
538
+ if (!pathLooksLikeWebhook && !contentStronglySuggestsWebhook) {
539
+ return null;
540
+ }
541
+ const provider = getWebhookProvider(normalizedRouteContext);
542
+ return {
543
+ provider,
544
+ confidence: provider === "unknown" ? "likely" : "high"
545
+ };
545
546
  }
546
- function hasStripeWebhookSignatureVerification(content) {
547
- return content.includes("stripe.webhooks.constructEvent") || content.includes("webhooks.constructEvent") || content.includes("constructEvent(");
547
+ function getWebhookProvider(normalizedRouteContext) {
548
+ if (normalizedRouteContext.includes("stripe-signature") || normalizedRouteContext.includes("stripe_webhook_secret") || normalizedRouteContext.includes("stripe.webhooks") || normalizedRouteContext.includes("constructevent(") || normalizedRouteContext.includes("stripe") && normalizedRouteContext.includes("webhook")) {
549
+ return "stripe";
550
+ }
551
+ if (normalizedRouteContext.includes("resend") && normalizedRouteContext.includes("webhook")) {
552
+ return "resend";
553
+ }
554
+ if (normalizedRouteContext.includes("clerk_webhook_secret") || normalizedRouteContext.includes("clerk") && normalizedRouteContext.includes("webhook")) {
555
+ return "clerk";
556
+ }
557
+ if (normalizedRouteContext.includes("x-github-event") || normalizedRouteContext.includes("x-hub-signature")) {
558
+ return "github";
559
+ }
560
+ if (normalizedRouteContext.includes("x-shopify-hmac-sha256")) {
561
+ return "shopify";
562
+ }
563
+ return "unknown";
548
564
  }
549
- function isClerkWebhookRoute(relativeFile, content) {
550
- const normalizedFile = relativeFile.toLowerCase();
565
+ function hasWebhookSignatureVerification(content, provider) {
551
566
  const normalizedContent = content.toLowerCase();
552
- return (normalizedContent.includes("clerk") || normalizedContent.includes("clerk_webhook_secret")) && (normalizedFile.includes("webhook") || normalizedContent.includes("webhook"));
567
+ if (provider === "stripe") {
568
+ return normalizedContent.includes("stripe.webhooks.constructevent") || normalizedContent.includes("webhooks.constructevent") || normalizedContent.includes("constructevent(");
569
+ }
570
+ if (provider === "clerk") {
571
+ return (normalizedContent.includes("new webhook(") || normalizedContent.includes("webhook(") || normalizedContent.includes("svix")) && (normalizedContent.includes(".verify(") || normalizedContent.includes("verify(") || normalizedContent.includes("verifywebhook"));
572
+ }
573
+ if (provider === "github") {
574
+ return (normalizedContent.includes("x-hub-signature") || normalizedContent.includes("x-hub-signature-256")) && hasHmacOrVerifyCall(normalizedContent);
575
+ }
576
+ if (provider === "shopify") {
577
+ return normalizedContent.includes("x-shopify-hmac-sha256") && hasHmacOrVerifyCall(normalizedContent);
578
+ }
579
+ if (provider === "resend") {
580
+ return normalizedContent.includes("verifywebhook") || (normalizedContent.includes("new webhook(") || normalizedContent.includes("webhook(") || normalizedContent.includes("svix")) && hasHmacOrVerifyCall(normalizedContent);
581
+ }
582
+ return normalizedContent.includes("constructevent(") || normalizedContent.includes("verifywebhook") || normalizedContent.includes("signature") && hasHmacOrVerifyCall(normalizedContent) || normalizedContent.includes("webhook") && normalizedContent.includes("verify(");
553
583
  }
554
- function hasClerkWebhookSignatureVerification(content) {
555
- return content.includes("new Webhook(") || content.includes(".verify(") || content.includes("svix");
584
+ function hasHmacOrVerifyCall(normalizedContent) {
585
+ return normalizedContent.includes("verify(") || normalizedContent.includes(".verify(") || normalizedContent.includes("verifywebhook") || normalizedContent.includes("createhmac") || normalizedContent.includes("timingsafeequal") || normalizedContent.includes("subtle.verify");
586
+ }
587
+ function getWebhookSignatureSuggestion(provider) {
588
+ if (provider === "stripe") {
589
+ return "Use stripe.webhooks.constructEvent(...) with the Stripe signature header before handling the event.";
590
+ }
591
+ if (provider === "clerk") {
592
+ return "Verify the event with Svix before handling it.";
593
+ }
594
+ if (provider === "github") {
595
+ return "Verify the GitHub signature using the raw request body, X-Hub-Signature-256 header, and webhook secret.";
596
+ }
597
+ if (provider === "shopify") {
598
+ return "Verify the Shopify HMAC using the raw request body, X-Shopify-Hmac-Sha256 header, and webhook secret.";
599
+ }
600
+ if (provider === "resend") {
601
+ return "Verify the Resend webhook signature before handling the event.";
602
+ }
603
+ return "Verify the provider signature using the raw request body and signature header before trusting the event.";
556
604
  }
557
605
  function isPackageJsonObject(data) {
558
606
  return typeof data === "object" && data !== null && !Array.isArray(data);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qodfy/core",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Scanner engine for Qodfy, an open-source launch readiness scanner for AI-built apps.",
5
5
  "keywords": [
6
6
  "qodfy",