@qodfy/core 0.2.7 → 0.2.8

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 +422 -10
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -919,8 +919,21 @@ function analyzeApiHandler({
919
919
  const webhookRouteInfo = getWebhookRouteInfo(relativeFile, handlerContent);
920
920
  const webhookProvider = webhookRouteInfo?.provider ?? getWebhookProvider(`${normalizedFile}
921
921
  ${handlerContent.toLowerCase()}`);
922
- const hasAuth = hasAuthOrSessionCheck(handlerContent);
923
- const hasSecretProtection = hasSecretProtectionSignal(handlerContent);
922
+ const protectionAnalysis = analyzeHandlerProtection({
923
+ handlerBody: handlerContent,
924
+ fullFileContent: content
925
+ });
926
+ const hasWeakAuthSignal = hasWeakAuthRelatedSignal(handlerContent);
927
+ const hasStrongProtection = hasStrongProtectionCallBeforeSensitiveWork(
928
+ handlerContent,
929
+ protectionAnalysis.sensitiveOperation
930
+ );
931
+ const hasAuth = protectionAnalysis.hasAccessControlGuard || hasStrongProtection;
932
+ const hasSecretProtection = hasSecretProtectionGuardBeforeSensitiveWork(
933
+ handlerContent,
934
+ protectionAnalysis.sensitiveOperation
935
+ );
936
+ const hasWeakSecretSignal = hasWeakSecretProtectionSignal(handlerContent);
924
937
  const hasRateLimit = hasRateLimitSignal(handlerContent);
925
938
  const hasValidation = hasValidationSignal(handlerContent);
926
939
  const hasCacheHeaders = hasCacheHeaderSignal(handlerContent);
@@ -950,13 +963,33 @@ ${handlerContent.toLowerCase()}`);
950
963
  intent = "sensitive-mutation";
951
964
  evidence.push({ label: "path contains", detail: sensitivePathMatch });
952
965
  }
953
- if (hasAuth) {
954
- evidence.push({ label: `auth/session check detected in ${method} handler` });
966
+ if (protectionAnalysis.sensitiveOperation) {
967
+ evidence.push({
968
+ label: `sensitive operation ${protectionAnalysis.sensitiveOperation.label} detected`,
969
+ detail: `${method} handler`
970
+ });
971
+ }
972
+ if (protectionAnalysis.hasAccessControlGuard) {
973
+ evidence.push(...protectionAnalysis.evidence);
974
+ } else if (hasStrongProtection) {
975
+ evidence.push({
976
+ label: "strong protection call detected before sensitive work",
977
+ detail: `${method} handler`
978
+ });
979
+ } else if (hasWeakAuthSignal) {
980
+ evidence.push({
981
+ label: "possible auth-related signal detected",
982
+ detail: `${method} handler`
983
+ });
984
+ } else if (protectionAnalysis.sensitiveOperation) {
985
+ evidence.push({ label: `no access-control guard detected before sensitive operation`, detail: `${method} handler` });
955
986
  } else {
956
987
  evidence.push({ label: `no auth/session check detected in ${method} handler` });
957
988
  }
958
989
  if (hasSecretProtection) {
959
- evidence.push({ label: `secret token check detected in ${method} handler` });
990
+ evidence.push({ label: `secret-token guard detected before sensitive work`, detail: `${method} handler` });
991
+ } else if (hasWeakSecretSignal) {
992
+ evidence.push({ label: `possible secret/token signal detected`, detail: `${method} handler` });
960
993
  }
961
994
  if (intent === "public-form") {
962
995
  evidence.push({
@@ -1166,6 +1199,33 @@ function findMatchingParen(content, openParenIndex) {
1166
1199
  }
1167
1200
  return -1;
1168
1201
  }
1202
+ function getIfStatements(content, startIndex = 0, endIndex = content.length) {
1203
+ const statements = [];
1204
+ const ifPattern = /\bif\s*\(/g;
1205
+ ifPattern.lastIndex = startIndex;
1206
+ let match;
1207
+ while ((match = ifPattern.exec(content)) !== null) {
1208
+ const matchIndex = match.index;
1209
+ if (matchIndex > endIndex) {
1210
+ break;
1211
+ }
1212
+ const openParenIndex = content.indexOf("(", matchIndex);
1213
+ if (openParenIndex === -1 || openParenIndex > endIndex) {
1214
+ continue;
1215
+ }
1216
+ const closeParenIndex = findMatchingParen(content, openParenIndex);
1217
+ if (closeParenIndex === -1 || closeParenIndex > endIndex) {
1218
+ continue;
1219
+ }
1220
+ statements.push({
1221
+ index: matchIndex,
1222
+ condition: content.slice(openParenIndex + 1, closeParenIndex),
1223
+ branchStartIndex: closeParenIndex + 1
1224
+ });
1225
+ ifPattern.lastIndex = closeParenIndex + 1;
1226
+ }
1227
+ return statements;
1228
+ }
1169
1229
  function findMatchingBrace(content, openBraceIndex) {
1170
1230
  let depth = 0;
1171
1231
  for (let index = openBraceIndex; index < content.length; index++) {
@@ -1200,14 +1260,365 @@ function getMethodRank(method) {
1200
1260
  function isMutationMethod(method) {
1201
1261
  return method !== "GET";
1202
1262
  }
1203
- function hasAuthOrSessionCheck(content) {
1204
- const normalizedContent = content.toLowerCase();
1205
- 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");
1263
+ function analyzeHandlerProtection({
1264
+ handlerBody,
1265
+ fullFileContent
1266
+ }) {
1267
+ const imports = getImportInfos(fullFileContent);
1268
+ const sensitiveOperation = getFirstSensitiveOperation(handlerBody);
1269
+ const helperAssignments = getHelperAssignments(handlerBody);
1270
+ for (const assignment of helperAssignments) {
1271
+ if (isRawRequestAccessorHelper(assignment.helperName)) {
1272
+ continue;
1273
+ }
1274
+ if (!isGuardHelperAssignmentNearTop(assignment.index, handlerBody, sensitiveOperation)) {
1275
+ continue;
1276
+ }
1277
+ const guard = findAccessControlGuardForVariable({
1278
+ handlerBody,
1279
+ variableName: assignment.variableName,
1280
+ startIndex: assignment.endIndex
1281
+ });
1282
+ if (!guard) {
1283
+ continue;
1284
+ }
1285
+ if (sensitiveOperation && guard.endIndex > sensitiveOperation.index) {
1286
+ continue;
1287
+ }
1288
+ const importInfo = getImportInfoForHelper(imports, assignment.helperName);
1289
+ const evidence = [
1290
+ { label: "access-control guard detected" },
1291
+ { label: "helper call assigned to variable", detail: assignment.variableName },
1292
+ { label: "guard checks variable", detail: assignment.variableName },
1293
+ { label: guard.returnSignal }
1294
+ ];
1295
+ if (sensitiveOperation) {
1296
+ evidence.push({ label: "guard appears before sensitive operation", detail: sensitiveOperation.label });
1297
+ }
1298
+ if (importInfo?.isProtectionSource) {
1299
+ evidence.push({ label: "helper imported from protection module", detail: importInfo.source });
1300
+ }
1301
+ return {
1302
+ hasAccessControlGuard: true,
1303
+ evidence,
1304
+ sensitiveOperation
1305
+ };
1306
+ }
1307
+ return {
1308
+ hasAccessControlGuard: false,
1309
+ evidence: [],
1310
+ sensitiveOperation
1311
+ };
1312
+ }
1313
+ function getImportInfos(content) {
1314
+ const imports = [];
1315
+ const importPattern = /import\s+(?:type\s+)?([\s\S]*?)\s+from\s+["']([^"']+)["'];?/g;
1316
+ for (const match of content.matchAll(importPattern)) {
1317
+ const importClause = match[1]?.trim();
1318
+ const source = match[2];
1319
+ if (!importClause || !source) {
1320
+ continue;
1321
+ }
1322
+ const isProtectionSource = isProtectionImportSource(source);
1323
+ const namedImportMatch = importClause.match(/\{([\s\S]*?)\}/);
1324
+ if (namedImportMatch?.[1]) {
1325
+ const namedImports = namedImportMatch[1].split(",");
1326
+ for (const namedImport of namedImports) {
1327
+ const parts = namedImport.trim().split(/\s+as\s+/i);
1328
+ const importedName = parts[0]?.trim();
1329
+ const localName = parts[1]?.trim() ?? importedName;
1330
+ if (importedName && localName) {
1331
+ imports.push({
1332
+ localName,
1333
+ importedName,
1334
+ source,
1335
+ isProtectionSource
1336
+ });
1337
+ }
1338
+ }
1339
+ }
1340
+ const clauseBeforeNamedImports = importClause.split("{")[0]?.replace(/,\s*$/, "").trim();
1341
+ if (clauseBeforeNamedImports && !clauseBeforeNamedImports.startsWith("*")) {
1342
+ const defaultImport = clauseBeforeNamedImports.split(",")[0]?.trim();
1343
+ if (defaultImport && /^[A-Za-z_$][\w$]*$/.test(defaultImport)) {
1344
+ imports.push({
1345
+ localName: defaultImport,
1346
+ importedName: "default",
1347
+ source,
1348
+ isProtectionSource
1349
+ });
1350
+ }
1351
+ }
1352
+ const namespaceImportMatch = importClause.match(/\*\s+as\s+([A-Za-z_$][\w$]*)/);
1353
+ if (namespaceImportMatch?.[1]) {
1354
+ imports.push({
1355
+ localName: namespaceImportMatch[1],
1356
+ importedName: "*",
1357
+ source,
1358
+ isProtectionSource
1359
+ });
1360
+ }
1361
+ }
1362
+ return imports;
1363
+ }
1364
+ function isProtectionImportSource(source) {
1365
+ const normalizedSource = source.toLowerCase();
1366
+ const protectionSourceTerms = [
1367
+ "auth",
1368
+ "session",
1369
+ "sessions",
1370
+ "staff",
1371
+ "permission",
1372
+ "permissions",
1373
+ "access",
1374
+ "access-control",
1375
+ "security",
1376
+ "user",
1377
+ "users"
1378
+ ];
1379
+ return protectionSourceTerms.some((term) => normalizedSource.includes(term));
1380
+ }
1381
+ function getHelperAssignments(handlerBody) {
1382
+ const assignments = [];
1383
+ const assignmentPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:await\s+)?([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?)\s*\(/g;
1384
+ for (const match of handlerBody.matchAll(assignmentPattern)) {
1385
+ const variableName = match[1];
1386
+ const helperName = match[2];
1387
+ const index = match.index ?? 0;
1388
+ if (!variableName || !helperName) {
1389
+ continue;
1390
+ }
1391
+ assignments.push({
1392
+ variableName,
1393
+ helperName,
1394
+ index,
1395
+ endIndex: index + match[0].length
1396
+ });
1397
+ }
1398
+ return assignments;
1399
+ }
1400
+ function isRawRequestAccessorHelper(helperName) {
1401
+ const normalizedHelperName = helperName.toLowerCase();
1402
+ const rawAccessorHelpers = [
1403
+ "request.headers.get",
1404
+ "req.headers.get",
1405
+ "headers.get",
1406
+ "request.cookies.get",
1407
+ "req.cookies.get",
1408
+ "request.nexturl.searchparams.get",
1409
+ "req.nexturl.searchparams.get",
1410
+ "searchparams.get",
1411
+ "cookies"
1412
+ ];
1413
+ return rawAccessorHelpers.includes(normalizedHelperName);
1414
+ }
1415
+ function isGuardHelperAssignmentNearTop(assignmentIndex, handlerBody, sensitiveOperation) {
1416
+ if (sensitiveOperation) {
1417
+ return assignmentIndex < sensitiveOperation.index;
1418
+ }
1419
+ return assignmentIndex < Math.min(1500, handlerBody.length);
1420
+ }
1421
+ function findAccessControlGuardForVariable({
1422
+ handlerBody,
1423
+ variableName,
1424
+ startIndex
1425
+ }) {
1426
+ const guardSearchEnd = Math.min(handlerBody.length, startIndex + 2500);
1427
+ const ifStatements = getIfStatements(handlerBody, startIndex, guardSearchEnd);
1428
+ for (const ifStatement of ifStatements) {
1429
+ if (!doesConditionBlockWhenVariableMissing(ifStatement.condition, variableName)) {
1430
+ continue;
1431
+ }
1432
+ const branch = getIfFailureBranch(handlerBody, ifStatement.branchStartIndex);
1433
+ const returnSignal = getAccessDeniedReturnSignal(branch.text);
1434
+ if (!returnSignal) {
1435
+ continue;
1436
+ }
1437
+ return {
1438
+ endIndex: branch.endIndex,
1439
+ returnSignal
1440
+ };
1441
+ }
1442
+ return null;
1443
+ }
1444
+ function doesConditionBlockWhenVariableMissing(condition, variableName) {
1445
+ const escapedVariableName = escapeRegExp(variableName);
1446
+ return new RegExp(`!\\s*${escapedVariableName}(?:\\b|\\?\\.|\\.)`).test(condition) || new RegExp(`\\b${escapedVariableName}\\s*(?:={2,3})\\s*(?:null|undefined|false)\\b`).test(condition) || new RegExp(`\\b(?:null|undefined|false)\\s*(?:={2,3})\\s*${escapedVariableName}\\b`).test(condition) || new RegExp(`!\\s*${escapedVariableName}(?:\\?\\.)?\\.[A-Za-z_$][\\w$]*`).test(condition) || new RegExp(`\\b${escapedVariableName}(?:\\?\\.)?\\.[A-Za-z_$][\\w$]*\\s*(?:={2,3})\\s*false\\b`).test(condition) || new RegExp(`\\bfalse\\s*(?:={2,3})\\s*${escapedVariableName}(?:\\?\\.)?\\.[A-Za-z_$][\\w$]*\\b`).test(condition);
1447
+ }
1448
+ function getIfFailureBranch(handlerBody, branchStartIndex) {
1449
+ let index = branchStartIndex;
1450
+ while (/\s/.test(handlerBody[index] ?? "")) {
1451
+ index++;
1452
+ }
1453
+ if (handlerBody[index] === "{") {
1454
+ const closeBraceIndex = findMatchingBrace(handlerBody, index);
1455
+ if (closeBraceIndex !== -1) {
1456
+ return {
1457
+ text: handlerBody.slice(index, closeBraceIndex + 1),
1458
+ endIndex: closeBraceIndex
1459
+ };
1460
+ }
1461
+ }
1462
+ const semicolonIndex = handlerBody.indexOf(";", index);
1463
+ const newlineIndex = handlerBody.indexOf("\n", index);
1464
+ const branchEndIndex = [semicolonIndex, newlineIndex].filter((candidateIndex) => candidateIndex !== -1).sort((leftIndex, rightIndex) => leftIndex - rightIndex)[0] ?? Math.min(handlerBody.length, index + 300);
1465
+ return {
1466
+ text: handlerBody.slice(index, branchEndIndex + 1),
1467
+ endIndex: branchEndIndex
1468
+ };
1469
+ }
1470
+ function getAccessDeniedReturnSignal(branchText) {
1471
+ const normalizedBranch = branchText.toLowerCase();
1472
+ if (!normalizedBranch.includes("return") && !normalizedBranch.includes("redirect(")) {
1473
+ return null;
1474
+ }
1475
+ if (/\bstatus\s*:\s*401\b/.test(normalizedBranch) || /\b401\b/.test(normalizedBranch)) {
1476
+ return "guard returns 401";
1477
+ }
1478
+ if (/\bstatus\s*:\s*403\b/.test(normalizedBranch) || /\b403\b/.test(normalizedBranch)) {
1479
+ return "guard returns 403";
1480
+ }
1481
+ if (normalizedBranch.includes("unauthorized")) {
1482
+ return "guard returns Unauthorized";
1483
+ }
1484
+ if (normalizedBranch.includes("forbidden")) {
1485
+ return "guard returns Forbidden";
1486
+ }
1487
+ if (/redirect\s*\(\s*["']\/(?:login|sign-in|signin|auth)/.test(normalizedBranch)) {
1488
+ return "guard redirects to login";
1489
+ }
1490
+ return null;
1491
+ }
1492
+ function getImportInfoForHelper(imports, helperName) {
1493
+ const helperRootName = helperName.split(".")[0];
1494
+ return imports.find((importInfo) => importInfo.localName === helperRootName);
1495
+ }
1496
+ function getFirstSensitiveOperation(handlerBody) {
1497
+ const sensitiveOperationPatterns = [
1498
+ { label: "request.formData", pattern: /\brequest\.formData\s*\(/i },
1499
+ { label: "request.json", pattern: /\brequest\.json\s*\(/i },
1500
+ { label: "file.arrayBuffer", pattern: /\b[A-Za-z_$][\w$]*\.arrayBuffer\s*\(/i },
1501
+ { label: "Buffer.from", pattern: /\bBuffer\.from\s*\(/ },
1502
+ { label: "uploadToR2", pattern: /\buploadToR2\s*\(/i },
1503
+ { label: "upload", pattern: /\bupload[A-Za-z0-9_$]*\s*\(/i },
1504
+ { label: "putObject", pattern: /\bputObject\s*\(/i },
1505
+ { label: "storage", pattern: /\bstorage\b/i },
1506
+ { label: "write", pattern: /\bwrite[A-Za-z0-9_$]*\s*\(/i },
1507
+ { label: "create", pattern: /\bcreate[A-Za-z0-9_$]*\s*\(/i },
1508
+ { label: "update", pattern: /\bupdate[A-Za-z0-9_$]*\s*\(/i },
1509
+ { label: "delete", pattern: /\bdelete[A-Za-z0-9_$]*\s*\(/i },
1510
+ { label: "insert", pattern: /\binsert[A-Za-z0-9_$]*\s*\(/i },
1511
+ { label: "cleanup", pattern: /\bcleanup[A-Za-z0-9_$]*\s*\(/i },
1512
+ { label: "revalidate", pattern: /\brevalidate[A-Za-z0-9_$]*\s*\(/i },
1513
+ { label: "prisma", pattern: /\bprisma\./i },
1514
+ { label: "db", pattern: /\bdb\./i },
1515
+ { label: "checkout", pattern: /\bcheckout\b/i },
1516
+ { label: "payment", pattern: /\bpayment\b/i },
1517
+ { label: "send", pattern: /\bsend[A-Za-z0-9_$]*\s*\(/i },
1518
+ { label: "mutation", pattern: /\bmutation\b/i }
1519
+ ];
1520
+ const matches = [];
1521
+ for (const sensitiveOperationPattern of sensitiveOperationPatterns) {
1522
+ const match = sensitiveOperationPattern.pattern.exec(handlerBody);
1523
+ if (match?.index !== void 0) {
1524
+ matches.push({
1525
+ label: sensitiveOperationPattern.label,
1526
+ index: match.index
1527
+ });
1528
+ }
1529
+ }
1530
+ return matches.sort((leftMatch, rightMatch) => leftMatch.index - rightMatch.index)[0];
1531
+ }
1532
+ function escapeRegExp(value) {
1533
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1534
+ }
1535
+ function hasWeakAuthRelatedSignal(content) {
1536
+ const normalizedContent = stripStrongProtectionIdentifiers(content).toLowerCase();
1537
+ const weakSignals = [
1538
+ "session",
1539
+ "authorization",
1540
+ "bearer",
1541
+ "cookies()",
1542
+ "request.cookies",
1543
+ "middleware",
1544
+ "getuser",
1545
+ "jwt",
1546
+ "auth(",
1547
+ "getserversession",
1548
+ "currentuser",
1549
+ "clerkclient"
1550
+ ];
1551
+ return weakSignals.some((signal) => normalizedContent.includes(signal));
1552
+ }
1553
+ function stripStrongProtectionIdentifiers(content) {
1554
+ return content.replace(/\b(?:await\s+)?require(?:Auth|User|Admin)\s*\([^)]*\)/gi, "").replace(/\bauth\.protect\s*\([^)]*\)/gi, "").replace(/\b(?:await\s+)?verifySession\s*\([^)]*\)/gi, "");
1555
+ }
1556
+ function hasStrongProtectionCallBeforeSensitiveWork(handlerBody, sensitiveOperation) {
1557
+ const protectionCutoffIndex = sensitiveOperation?.index ?? Math.min(2e3, handlerBody.length);
1558
+ const strongProtectionPatterns = [
1559
+ /\b(?:await\s+)?requireAuth\s*\(/gi,
1560
+ /\b(?:await\s+)?requireUser\s*\(/gi,
1561
+ /\b(?:await\s+)?requireAdmin\s*\(/gi,
1562
+ /\bauth\.protect\s*\(/gi,
1563
+ /(?:^|[;{\n]\s*)(?:await\s+)?verifySession\s*\(/gi
1564
+ ];
1565
+ for (const pattern of strongProtectionPatterns) {
1566
+ pattern.lastIndex = 0;
1567
+ for (const match of handlerBody.matchAll(pattern)) {
1568
+ if ((match.index ?? Number.POSITIVE_INFINITY) < protectionCutoffIndex) {
1569
+ return true;
1570
+ }
1571
+ }
1572
+ }
1573
+ return hasAuthGuardRedirectBeforeIndex(handlerBody, protectionCutoffIndex) || hasThrowBasedAuthGuardBeforeIndex(handlerBody, protectionCutoffIndex);
1206
1574
  }
1207
- function hasSecretProtectionSignal(content) {
1575
+ function hasAuthGuardRedirectBeforeIndex(handlerBody, cutoffIndex) {
1576
+ const redirectPattern = /\bif\s*\(([\s\S]*?)\)\s*(?:return\s+)?redirect\s*\(\s*["']\/(?:login|sign-in|signin|auth)/gi;
1577
+ for (const match of handlerBody.matchAll(redirectPattern)) {
1578
+ const condition = match[1] ?? "";
1579
+ if ((match.index ?? Number.POSITIVE_INFINITY) < cutoffIndex && /\!/.test(condition)) {
1580
+ return true;
1581
+ }
1582
+ }
1583
+ return false;
1584
+ }
1585
+ function hasThrowBasedAuthGuardBeforeIndex(handlerBody, cutoffIndex) {
1586
+ const throwGuardPattern = /\bif\s*\(([\s\S]*?)\)\s*(?:throw\s+new\s+\w+|throw\s+[^;]*(?:Unauthorized|Forbidden|401|403))/gi;
1587
+ for (const match of handlerBody.matchAll(throwGuardPattern)) {
1588
+ const condition = match[1] ?? "";
1589
+ const guardText = match[0] ?? "";
1590
+ if ((match.index ?? Number.POSITIVE_INFINITY) < cutoffIndex && /\!/.test(condition) && /Unauthorized|Forbidden|401|403/i.test(guardText)) {
1591
+ return true;
1592
+ }
1593
+ }
1594
+ return false;
1595
+ }
1596
+ function hasWeakSecretProtectionSignal(content) {
1208
1597
  const normalizedContent = content.toLowerCase();
1209
1598
  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");
1210
1599
  }
1600
+ function hasSecretProtectionGuardBeforeSensitiveWork(handlerBody, sensitiveOperation) {
1601
+ const protectionCutoffIndex = sensitiveOperation?.index ?? Math.min(2e3, handlerBody.length);
1602
+ const ifStatements = getIfStatements(handlerBody, 0, protectionCutoffIndex);
1603
+ for (const ifStatement of ifStatements) {
1604
+ if (!isSecretTokenGuardCondition(ifStatement.condition)) {
1605
+ continue;
1606
+ }
1607
+ const branch = getIfFailureBranch(handlerBody, ifStatement.branchStartIndex);
1608
+ if (getAccessDeniedReturnSignal(branch.text)) {
1609
+ return true;
1610
+ }
1611
+ }
1612
+ return false;
1613
+ }
1614
+ function isSecretTokenGuardCondition(condition) {
1615
+ const normalizedCondition = condition.toLowerCase();
1616
+ const hasSecretInput = /\b(?:token|secret|authorization|bearer)\b/i.test(condition) || /\bheaders\.get\s*\(\s*["'][^"']*(?:authorization|secret|token|key)[^"']*["']\s*\)/i.test(condition) || /\bsearchParams\.get\s*\(\s*["'](?:secret|token|key)["']\s*\)/i.test(condition);
1617
+ const hasExpectedSecret = /\bprocess\.env\.[A-Za-z0-9_]*(?:SECRET|TOKEN|KEY)\b/.test(condition) || /\b(?:expected|valid|required|cron|revalidate)[A-Za-z0-9_]*(?:Secret|Token|Key)\b/.test(condition) || /\b(?:expected|valid|required|cron|revalidate)[a-z0-9_]*(?:secret|token|key)\b/.test(normalizedCondition);
1618
+ const hasSecretValidationCall = /\b(?:isValid|verify|validate|compare)[A-Za-z0-9_]*(?:Secret|Token|Key)?\s*\(/.test(condition) || /\btimingSafeEqual\s*\(/.test(condition);
1619
+ const hasComparison = /!==|!=|===|==/.test(condition) || /\b(?:timingSafeEqual|compare|verify|isValid|validate)[A-Za-z0-9_]*\s*\(/.test(condition);
1620
+ return hasSecretInput && (hasExpectedSecret || hasSecretValidationCall) && hasComparison;
1621
+ }
1211
1622
  function hasRateLimitSignal(content) {
1212
1623
  const normalizedContent = content.toLowerCase();
1213
1624
  return normalizedContent.includes("ratelimit") || normalizedContent.includes("rate limit") || normalizedContent.includes("limiter") || normalizedContent.includes("upstash") || normalizedContent.includes("throttle");
@@ -1392,7 +1803,8 @@ Determine whether the ${method} handler should be protected.
1392
1803
  Instructions:
1393
1804
  - Inspect each exported HTTP handler separately.
1394
1805
  - 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.
1806
+ - If the ${method} handler already has a guard that returns 401/403 before sensitive logic, explain where it happens and do not add duplicate auth.
1807
+ - 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 before sensitive logic.
1396
1808
  - Also verify protections such as input validation, file size limits for uploads, storage path safety, and rate limiting where relevant.
1397
1809
  - Do not introduce a new auth provider.
1398
1810
  - Do not refactor unrelated code.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qodfy/core",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Scanner engine for Qodfy, an open-source launch readiness scanner for AI-built apps.",
5
5
  "keywords": [
6
6
  "qodfy",
@@ -54,6 +54,7 @@
54
54
  },
55
55
  "scripts": {
56
56
  "build": "tsup src/index.ts --format esm --dts --clean --tsconfig tsconfig.json",
57
- "dev": "tsx src/index.ts"
57
+ "dev": "tsx src/index.ts",
58
+ "test": "node --import tsx --test src/protection.test.ts"
58
59
  }
59
60
  }