@zappie/check 0.2.0 → 0.2.1

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.js CHANGED
@@ -42,7 +42,7 @@ function distance(left, right) {
42
42
  }
43
43
  return row[right.length] ?? Infinity;
44
44
  }
45
- function unknownKeys(value, allowed, path14, issue) {
45
+ function unknownKeys(value, allowed, path15, issue) {
46
46
  for (const key of Object.keys(value)) {
47
47
  if (allowed.includes(key)) {
48
48
  continue;
@@ -50,7 +50,7 @@ function unknownKeys(value, allowed, path14, issue) {
50
50
  const closest = allowed.map((candidate) => ({ candidate, score: distance(key, candidate) })).sort((left, right) => left.score - right.score)[0];
51
51
  const hint = closest && closest.score <= 2 ? closest.candidate : void 0;
52
52
  issue({
53
- path: path14 ? `${path14}.${key}` : key,
53
+ path: path15 ? `${path15}.${key}` : key,
54
54
  code: "unknown_key",
55
55
  message: hint ? `Unknown field "${key}". Rename it to "${hint}".` : `Unknown field "${key}". Remove it from the configuration.`,
56
56
  severity: "error"
@@ -76,20 +76,20 @@ function hostname(value) {
76
76
  }
77
77
 
78
78
  // ../sdk/src/site-config/sections.ts
79
- function nested(value, path14, keys, issue, validate) {
79
+ function nested(value, path15, keys, issue, validate) {
80
80
  if (value === void 0) {
81
81
  return;
82
82
  }
83
83
  if (!record(value)) {
84
84
  issue({
85
- path: path14,
85
+ path: path15,
86
86
  code: "invalid_type",
87
- message: `Expected an object. Set ${path14} to an object or remove it.`,
87
+ message: `Expected an object. Set ${path15} to an object or remove it.`,
88
88
  severity: "error"
89
89
  });
90
90
  return;
91
91
  }
92
- unknownKeys(value, keys, path14, issue);
92
+ unknownKeys(value, keys, path15, issue);
93
93
  validate(value);
94
94
  }
95
95
  function budgets(value, issue, error) {
@@ -126,18 +126,18 @@ function budgets(value, issue, error) {
126
126
  `Expected a positive finite number. Set budgets.${key} to a number greater than zero.`
127
127
  );
128
128
  } else if (amount > DEFAULT_BUDGETS[key]) {
129
- const message5 = `The ${key} budget exceeds the ${DEFAULT_BUDGETS[key]} default.`;
129
+ const message6 = `The ${key} budget exceeds the ${DEFAULT_BUDGETS[key]} default.`;
130
130
  if (budget.override === void 0) {
131
131
  error(
132
132
  `budgets.${key}`,
133
133
  "budget_override",
134
- `${message5} Add budgets.override.reason to explain the increase.`
134
+ `${message6} Add budgets.override.reason to explain the increase.`
135
135
  );
136
136
  } else {
137
137
  issue({
138
138
  path: `budgets.${key}`,
139
139
  code: "budget_override",
140
- message: `${message5} Lower it to the default when the exception is no longer needed.`,
140
+ message: `${message6} Lower it to the default when the exception is no longer needed.`,
141
141
  severity: "warning"
142
142
  });
143
143
  }
@@ -181,6 +181,7 @@ var rootKeys = [
181
181
  "prerender",
182
182
  "menu",
183
183
  "packages",
184
+ "audit",
184
185
  "outbound",
185
186
  "budgets",
186
187
  "readiness",
@@ -189,7 +190,7 @@ var rootKeys = [
189
190
  function validateConfig(input) {
190
191
  const issues = [];
191
192
  const issue = (value) => issues.push(value);
192
- const error = (path14, code, message5) => issue({ path: path14, code, message: message5, severity: "error" });
193
+ const error = (path15, code, message6) => issue({ path: path15, code, message: message6, severity: "error" });
193
194
  if (!record(input)) {
194
195
  return {
195
196
  valid: false,
@@ -284,12 +285,12 @@ function validateConfig(input) {
284
285
  issues
285
286
  };
286
287
  }
287
- function requiredSlug(value, path14, error) {
288
+ function requiredSlug(value, path15, error) {
288
289
  if (!nonblank(value)) {
289
290
  error(
290
- path14,
291
+ path15,
291
292
  value === void 0 ? "missing_required" : "invalid_type",
292
- `Expected a non-empty storefront slug. Set ${path14} to the slug used by createZappie().`
293
+ `Expected a non-empty storefront slug. Set ${path15} to the slug used by createZappie().`
293
294
  );
294
295
  }
295
296
  }
@@ -308,29 +309,29 @@ function theme(value, error) {
308
309
  );
309
310
  }
310
311
  }
311
- function stringList(value, path14, error, options = {}) {
312
+ function stringList(value, path15, error, options = {}) {
312
313
  if (value === void 0) {
313
314
  return void 0;
314
315
  }
315
316
  if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
316
317
  error(
317
- path14,
318
+ path15,
318
319
  "invalid_type",
319
- `Expected an array of strings. Set ${path14} to string values.`
320
+ `Expected an array of strings. Set ${path15} to string values.`
320
321
  );
321
322
  return void 0;
322
323
  }
323
324
  if (options.nonempty && value.length === 0) {
324
325
  error(
325
- path14,
326
+ path15,
326
327
  "invalid_format",
327
- `Expected at least one value. Add a value to ${path14} or remove the field.`
328
+ `Expected at least one value. Add a value to ${path15} or remove the field.`
328
329
  );
329
330
  }
330
331
  value.forEach((entry, index) => {
331
332
  if (options.slug && !nonblank(entry)) {
332
333
  error(
333
- `${path14}[${index}]`,
334
+ `${path15}[${index}]`,
334
335
  "invalid_format",
335
336
  "Expected a non-empty branch slug. Remove the empty value or use a branch slug."
336
337
  );
@@ -338,17 +339,17 @@ function stringList(value, path14, error, options = {}) {
338
339
  });
339
340
  if (options.unique && new Set(value).size !== value.length) {
340
341
  error(
341
- path14,
342
+ path15,
342
343
  "duplicate",
343
- `Expected unique values. Remove duplicates from ${path14}.`
344
+ `Expected unique values. Remove duplicates from ${path15}.`
344
345
  );
345
346
  }
346
347
  return value;
347
348
  }
348
349
  function paths(value, error) {
349
350
  const values = stringList(value, "prerender", error, { unique: true });
350
- values?.forEach((path14, index) => {
351
- if (!path14.startsWith("/")) {
351
+ values?.forEach((path15, index) => {
352
+ if (!path15.startsWith("/")) {
352
353
  error(
353
354
  `prerender[${index}]`,
354
355
  "invalid_format",
@@ -851,11 +852,11 @@ var record2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
851
852
  var includes = (values, key) => values.includes(key);
852
853
  function validateTheme(input) {
853
854
  const issues = [];
854
- const issue = (path14, code, message5, severity = "error") => issues.push({ path: path14, code, message: message5, severity });
855
- const keys = (obj, allowed, path14) => {
855
+ const issue = (path15, code, message6, severity = "error") => issues.push({ path: path15, code, message: message6, severity });
856
+ const keys = (obj, allowed, path15) => {
856
857
  for (const key of Object.keys(obj)) {
857
858
  if (!allowed.includes(key)) {
858
- issue(path14 ? `${path14}.${key}` : key, "invalid_theme", "Unknown field");
859
+ issue(path15 ? `${path15}.${key}` : key, "invalid_theme", "Unknown field");
859
860
  }
860
861
  }
861
862
  };
@@ -916,9 +917,9 @@ function validateTheme(input) {
916
917
  }
917
918
  }
918
919
  for (const [key, value] of Object.entries(values)) {
919
- const path14 = `${group}.${key}`;
920
+ const path15 = `${group}.${key}`;
920
921
  if (!includes(TOKEN_NAMES, key)) {
921
- issue(path14, "unknown_token", "Unknown semantic token");
922
+ issue(path15, "unknown_token", "Unknown semantic token");
922
923
  continue;
923
924
  }
924
925
  let code, valid = false;
@@ -936,7 +937,7 @@ function validateTheme(input) {
936
937
  valid = typeof value === "string" && safeValue(value) && (includes(DURATION_TOKENS, key) ? isDuration(value) : isEasing(value));
937
938
  }
938
939
  if (!valid) {
939
- issue(path14, code, "Invalid or unsafe token value");
940
+ issue(path15, code, "Invalid or unsafe token value");
940
941
  }
941
942
  }
942
943
  }
@@ -946,30 +947,30 @@ function validateTheme(input) {
946
947
  } else {
947
948
  const roles = /* @__PURE__ */ new Set();
948
949
  input.fonts.forEach((font, i) => {
949
- const path14 = `fonts[${i}]`;
950
+ const path15 = `fonts[${i}]`;
950
951
  if (!record2(font)) {
951
- issue(path14, "invalid_font", "Expected a font object");
952
+ issue(path15, "invalid_font", "Expected a font object");
952
953
  return;
953
954
  }
954
- keys(font, ["family", "source", "url", "weights", "role"], path14);
955
+ keys(font, ["family", "source", "url", "weights", "role"], path15);
955
956
  if (typeof font.family !== "string" || !isFontFamily(font.family) || font.family.includes(",")) {
956
957
  issue(
957
- `${path14}.family`,
958
+ `${path15}.family`,
958
959
  "invalid_font",
959
960
  "Expected one unquoted font family"
960
961
  );
961
962
  }
962
963
  if (typeof font.source !== "string" || !["google", "self", "system"].includes(font.source)) {
963
- issue(`${path14}.source`, "invalid_font", "Unknown font source");
964
+ issue(`${path15}.source`, "invalid_font", "Unknown font source");
964
965
  }
965
966
  if (typeof font.role !== "string" || !["display", "body", "mono"].includes(font.role) || roles.has(font.role)) {
966
- issue(`${path14}.role`, "invalid_font", "Expected a unique font role");
967
+ issue(`${path15}.role`, "invalid_font", "Expected a unique font role");
967
968
  }
968
969
  roles.add(String(font.role));
969
970
  if (font.source === "self" || font.url !== void 0) {
970
971
  if (typeof font.url !== "string" || !isAssetUrl(font.url, false) || !new URL(font.url).pathname.endsWith(".woff2")) {
971
972
  issue(
972
- `${path14}.url`,
973
+ `${path15}.url`,
973
974
  "invalid_font",
974
975
  "Expected an HTTPS .woff2 URL"
975
976
  );
@@ -979,7 +980,7 @@ function validateTheme(input) {
979
980
  (w) => typeof w !== "number" || !Number.isInteger(w) || w < 1 || w > 1e3
980
981
  ))) {
981
982
  issue(
982
- `${path14}.weights`,
983
+ `${path15}.weights`,
983
984
  "invalid_font",
984
985
  "Expected 1\u20139 integer weights between 1 and 1000"
985
986
  );
@@ -1089,13 +1090,13 @@ function escape(value) {
1089
1090
  }
1090
1091
 
1091
1092
  // src/result.ts
1092
- function problem(message5, fix, details = {}) {
1093
- return { ...details, message: message5, fix };
1093
+ function problem(message6, fix, details = {}) {
1094
+ return { ...details, message: message6, fix };
1094
1095
  }
1095
1096
  function step(name, result, problems = []) {
1096
1097
  return { name, result, problems };
1097
1098
  }
1098
- var skipped = (name, message5) => step(name, "skipped", [problem(message5, null)]);
1099
+ var skipped = (name, message6) => step(name, "skipped", [problem(message6, null)]);
1099
1100
 
1100
1101
  // src/config.ts
1101
1102
  function supportsTypeScriptImport(version = process.versions.node) {
@@ -1318,22 +1319,500 @@ async function installedManifest(cwd, name) {
1318
1319
  }
1319
1320
  }
1320
1321
 
1322
+ // src/audit.ts
1323
+ import { access as access2, readFile as readFile4 } from "fs/promises";
1324
+ import path4 from "path";
1325
+
1326
+ // src/command.ts
1327
+ import { spawn } from "child_process";
1328
+ function command(executable, args, cwd, signal) {
1329
+ return new Promise((resolve, reject) => {
1330
+ const child = spawn(executable, args, {
1331
+ cwd,
1332
+ env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" },
1333
+ stdio: ["ignore", "pipe", "pipe"],
1334
+ ...signal ? { signal } : {}
1335
+ });
1336
+ let stdout = "";
1337
+ let stderr = "";
1338
+ child.stdout.on("data", (value) => {
1339
+ stdout += String(value);
1340
+ });
1341
+ child.stderr.on("data", (value) => {
1342
+ stderr += String(value);
1343
+ });
1344
+ child.on("error", reject);
1345
+ child.on("close", (code) => {
1346
+ resolve({ code: code ?? 1, stdout, stderr });
1347
+ });
1348
+ });
1349
+ }
1350
+ function execArgs(manager, binary, args) {
1351
+ if (manager === "npm") {
1352
+ return { executable: "npm", args: ["exec", "--no", "--", binary, ...args] };
1353
+ }
1354
+ if (manager === "yarn") {
1355
+ return { executable: "yarn", args: ["exec", binary, ...args] };
1356
+ }
1357
+ if (manager === "bun") {
1358
+ return { executable: "bun", args: ["x", "--no-install", binary, ...args] };
1359
+ }
1360
+ return { executable: "pnpm", args: ["exec", binary, ...args] };
1361
+ }
1362
+ function scriptArgs(manager, name) {
1363
+ if (manager === "bun") {
1364
+ return { executable: "bun", args: ["run", name] };
1365
+ }
1366
+ if (manager === "npm") {
1367
+ return { executable: "npm", args: ["run", name] };
1368
+ }
1369
+ return { executable: manager, args: [name] };
1370
+ }
1371
+
1372
+ // src/audit.ts
1373
+ var AUDIT_TIMEOUT_MS = 6e4;
1374
+ async function checkAudit(options, dependencies = {}) {
1375
+ const mode = options.audit ?? "report";
1376
+ if (mode === "off") {
1377
+ return skipped("audit", "Dependency auditing is off.");
1378
+ }
1379
+ options.signal?.throwIfAborted();
1380
+ let invalidIgnores = [];
1381
+ let ignores = [];
1382
+ try {
1383
+ const parsed = await auditIgnores(options.cwd);
1384
+ ignores = parsed.ignores;
1385
+ invalidIgnores = parsed.problems;
1386
+ } catch (error) {
1387
+ invalidIgnores.push(
1388
+ problem(
1389
+ `Could not use audit.ignore: ${message2(error)}`,
1390
+ "Fix or remove the invalid acknowledgement. The advisory remains unacknowledged.",
1391
+ { rule: "audit/invalid-ignore", file: "zappie.config.ts", line: 1 }
1392
+ )
1393
+ );
1394
+ }
1395
+ if (options.packageManager === "yarn" || options.packageManager === "bun") {
1396
+ return withProblems(
1397
+ unavailable(
1398
+ mode,
1399
+ `Could not audit dependencies: ${options.packageManager} is not supported.`,
1400
+ "Use npm or pnpm for dependency auditing, or run with --audit=off.",
1401
+ "package.json"
1402
+ ),
1403
+ invalidIgnores,
1404
+ mode
1405
+ );
1406
+ }
1407
+ const lockfile2 = await findLockfile(options.cwd, options.packageManager);
1408
+ if (!lockfile2) {
1409
+ return withProblems(
1410
+ unavailable(
1411
+ mode,
1412
+ "Could not audit dependencies: no lockfile was found.",
1413
+ "Commit the package manager lockfile and run zappie check again.",
1414
+ "package.json"
1415
+ ),
1416
+ invalidIgnores,
1417
+ mode
1418
+ );
1419
+ }
1420
+ const invocation = auditCommand(options.packageManager);
1421
+ const timeoutMs = dependencies.timeoutMs ?? AUDIT_TIMEOUT_MS;
1422
+ const timeout = AbortSignal.timeout(timeoutMs);
1423
+ const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
1424
+ let result;
1425
+ try {
1426
+ result = await (dependencies.run ?? command)(
1427
+ invocation.executable,
1428
+ invocation.args,
1429
+ options.cwd,
1430
+ signal
1431
+ );
1432
+ } catch (error) {
1433
+ options.signal?.throwIfAborted();
1434
+ const detail = timeout.aborted ? `the audit timed out after ${Math.ceil(timeoutMs / 1e3)} seconds` : message2(error);
1435
+ return withProblems(
1436
+ unavailable(
1437
+ mode,
1438
+ `Could not audit dependencies: ${detail}.`,
1439
+ "Check registry access and run zappie check again.",
1440
+ relativeLockfile(options.cwd, lockfile2)
1441
+ ),
1442
+ invalidIgnores,
1443
+ mode
1444
+ );
1445
+ }
1446
+ options.signal?.throwIfAborted();
1447
+ let advisories;
1448
+ try {
1449
+ advisories = parseAuditOutput(options.packageManager, result.stdout);
1450
+ if (result.code > 1) {
1451
+ throw new Error(`the audit command exited with status ${result.code}`);
1452
+ }
1453
+ } catch (error) {
1454
+ return withProblems(
1455
+ unavailable(
1456
+ mode,
1457
+ `Could not audit dependencies: ${message2(error)}.`,
1458
+ "Check registry access and run zappie check again.",
1459
+ relativeLockfile(options.cwd, lockfile2)
1460
+ ),
1461
+ invalidIgnores,
1462
+ mode
1463
+ );
1464
+ }
1465
+ return withProblems(
1466
+ evaluateAudit(
1467
+ advisories,
1468
+ mode,
1469
+ ignores,
1470
+ relativeLockfile(options.cwd, lockfile2)
1471
+ ),
1472
+ invalidIgnores,
1473
+ mode
1474
+ );
1475
+ }
1476
+ function evaluateAudit(advisories, mode, ignores = [], file) {
1477
+ const problems = [];
1478
+ const applied = advisories.map((advisory) => {
1479
+ const acknowledged = ignores.find(
1480
+ ({ id }) => normalizedId(id) === normalizedId(advisory.id)
1481
+ );
1482
+ if (acknowledged && malicious(advisory.id)) {
1483
+ problems.push(
1484
+ problem(
1485
+ `${advisory.id} is a malicious-package advisory and cannot be acknowledged.`,
1486
+ `Remove ${advisory.package} and replace it with a trusted package or release.`,
1487
+ {
1488
+ rule: "audit/invalid-ignore",
1489
+ advisory,
1490
+ ...file ? { file, line: 1 } : {}
1491
+ }
1492
+ )
1493
+ );
1494
+ return advisory;
1495
+ }
1496
+ if (acknowledged && (advisory.fixedVersion || advisory.upgradeVersion || advisory.fixCommand)) {
1497
+ const upgradePackage = advisory.upgradePackage ?? advisory.package;
1498
+ const upgradeVersion = advisory.upgradeVersion ?? advisory.fixedVersion;
1499
+ const fix = advisory.fixCommand ? `Run ${advisory.fixCommand}.` : `Upgrade ${upgradePackage} to ${upgradeVersion} or later.`;
1500
+ const reason = advisory.fixCommand ? `${advisory.id} has an available fix and cannot be acknowledged.` : `${advisory.id} has a fixed version and cannot be acknowledged.`;
1501
+ problems.push(
1502
+ problem(reason, fix, {
1503
+ rule: "audit/invalid-ignore",
1504
+ advisory,
1505
+ ...file ? { file, line: 1 } : {}
1506
+ })
1507
+ );
1508
+ return advisory;
1509
+ }
1510
+ return acknowledged ? { ...advisory, ignored: true, ignoredUntil: acknowledged.until } : advisory;
1511
+ });
1512
+ problems.push(...applied.map((advisory) => advisoryProblem(advisory, file)));
1513
+ const critical = applied.some(
1514
+ ({ severity, ignored }) => severity === "critical" && !ignored
1515
+ );
1516
+ const invalidIgnore2 = problems.some(
1517
+ ({ rule }) => rule === "audit/invalid-ignore"
1518
+ );
1519
+ return step(
1520
+ "audit",
1521
+ mode === "enforce" && (critical || invalidIgnore2) ? "failed" : "passed",
1522
+ problems
1523
+ );
1524
+ }
1525
+ function unavailable(mode, message6, fix, file) {
1526
+ return step("audit", mode === "enforce" ? "failed" : "passed", [
1527
+ problem(message6, fix, { rule: "audit/unavailable", file, line: 1 })
1528
+ ]);
1529
+ }
1530
+ function withProblems(checked, extra, mode) {
1531
+ if (extra.length === 0) {
1532
+ return checked;
1533
+ }
1534
+ return step("audit", mode === "enforce" ? "failed" : checked.result, [
1535
+ ...extra,
1536
+ ...checked.problems
1537
+ ]);
1538
+ }
1539
+ async function findLockfile(cwd, manager) {
1540
+ const names = manager === "npm" ? ["package-lock.json", "npm-shrinkwrap.json"] : ["pnpm-lock.yaml"];
1541
+ let directory = path4.resolve(cwd);
1542
+ for (; ; ) {
1543
+ if (directory !== path4.resolve(cwd) && manager === "pnpm" && await workspaceExcludes(directory, path4.resolve(cwd))) {
1544
+ return null;
1545
+ }
1546
+ for (const name of names) {
1547
+ const candidate = path4.join(directory, name);
1548
+ try {
1549
+ await access2(candidate);
1550
+ return candidate;
1551
+ } catch {
1552
+ }
1553
+ }
1554
+ const parent = path4.dirname(directory);
1555
+ if (parent === directory) {
1556
+ return null;
1557
+ }
1558
+ directory = parent;
1559
+ }
1560
+ }
1561
+ async function workspaceExcludes(directory, cwd) {
1562
+ try {
1563
+ const source = await readFile4(
1564
+ path4.join(directory, "pnpm-workspace.yaml"),
1565
+ "utf8"
1566
+ );
1567
+ const block = /^packages:\s*\n((?:[ \t]+.*(?:\n|$))*)/m.exec(source)?.[1];
1568
+ if (!block) {
1569
+ return false;
1570
+ }
1571
+ const relative2 = path4.relative(directory, cwd).replaceAll(path4.sep, "/");
1572
+ const patterns = [
1573
+ ...block.matchAll(/^\s*-\s*["']?([^"'\s]+)["']?\s*$/gm)
1574
+ ].map((match) => match[1] ?? "");
1575
+ return !patterns.some((pattern) => workspacePattern(pattern, relative2));
1576
+ } catch {
1577
+ return false;
1578
+ }
1579
+ }
1580
+ function workspacePattern(pattern, relative2) {
1581
+ if (pattern === ".") {
1582
+ return relative2 === "";
1583
+ }
1584
+ const expression = pattern.split("*").map((part) => part.replace(/[.*+?^$()|[\]\\]/g, "\\$&")).join("[^/]+");
1585
+ return new RegExp(`^${expression}$`).test(relative2);
1586
+ }
1587
+ function relativeLockfile(cwd, lockfile2) {
1588
+ return path4.relative(cwd, lockfile2).replaceAll(path4.sep, "/");
1589
+ }
1590
+ function auditCommand(manager) {
1591
+ return manager === "pnpm" ? { executable: "pnpm", args: ["audit", "--json"] } : { executable: "npm", args: ["audit", "--json"] };
1592
+ }
1593
+ function parseAuditOutput(manager, source) {
1594
+ const value = JSON.parse(source);
1595
+ requireAuditReport(manager, value);
1596
+ const parsed = manager === "npm" ? npmAdvisories(value) : pnpmAdvisories(value);
1597
+ return parsed.filter(({ severity }) => severity === "high" || severity === "critical").sort((left, right) => left.id.localeCompare(right.id));
1598
+ }
1599
+ function requireAuditReport(manager, value) {
1600
+ if (record3(value.error)) {
1601
+ throw new Error(
1602
+ string(value.error.message) ?? string(value.error.code) ?? string(value.message) ?? "the package manager returned an error"
1603
+ );
1604
+ }
1605
+ if (manager === "npm" && !record3(value.vulnerabilities)) {
1606
+ throw new Error("npm returned no vulnerability report");
1607
+ }
1608
+ if (manager === "pnpm" && !record3(value.advisories) && !record3(value.metadata)) {
1609
+ throw new Error("pnpm returned no advisory report");
1610
+ }
1611
+ }
1612
+ function pnpmAdvisories(value) {
1613
+ if (!record3(value.advisories)) {
1614
+ return [];
1615
+ }
1616
+ return Object.entries(value.advisories).flatMap(([key, raw]) => {
1617
+ if (!record3(raw)) {
1618
+ return [];
1619
+ }
1620
+ const findings = Array.isArray(raw.findings) ? raw.findings : [];
1621
+ const versions = findings.flatMap(
1622
+ (finding) => record3(finding) && typeof finding.version === "string" ? [finding.version] : []
1623
+ );
1624
+ const severity = normalizedSeverity(raw.severity);
1625
+ if (!severity) {
1626
+ return [];
1627
+ }
1628
+ return [
1629
+ {
1630
+ id: string(raw.github_advisory_id) ?? string(raw.id) ?? key,
1631
+ package: string(raw.module_name) ?? "unknown package",
1632
+ severity,
1633
+ vulnerableVersion: versions.join(", ") || string(raw.vulnerable_versions) || "unknown",
1634
+ fixedVersion: firstFixed(string(raw.patched_versions)),
1635
+ ignored: false
1636
+ }
1637
+ ];
1638
+ });
1639
+ }
1640
+ function npmAdvisories(value) {
1641
+ if (!record3(value.vulnerabilities)) {
1642
+ return [];
1643
+ }
1644
+ const advisories = /* @__PURE__ */ new Map();
1645
+ for (const [packageName2, raw] of Object.entries(value.vulnerabilities)) {
1646
+ if (!record3(raw) || !Array.isArray(raw.via)) {
1647
+ continue;
1648
+ }
1649
+ for (const via of raw.via) {
1650
+ if (!record3(via)) {
1651
+ continue;
1652
+ }
1653
+ const severity = normalizedSeverity(via.severity);
1654
+ if (!severity) {
1655
+ continue;
1656
+ }
1657
+ const id = ghsaFrom(string(via.url)) ?? string(via.name) ?? String(via.source ?? packageName2);
1658
+ const upgrade = npmUpgrade(raw.fixAvailable);
1659
+ advisories.set(id, {
1660
+ id,
1661
+ package: packageName2,
1662
+ severity,
1663
+ vulnerableVersion: string(via.range) ?? string(raw.range) ?? "unknown",
1664
+ fixedVersion: upgrade.upgradePackage === packageName2 ? upgrade.upgradeVersion ?? null : null,
1665
+ ...upgrade,
1666
+ ignored: false
1667
+ });
1668
+ }
1669
+ }
1670
+ return [...advisories.values()];
1671
+ }
1672
+ function advisoryProblem(advisory, file) {
1673
+ const acknowledged = advisory.ignored ? ` Acknowledged until ${advisory.ignoredUntil}.` : "";
1674
+ const upgradePackage = advisory.upgradePackage ?? advisory.package;
1675
+ const upgradeVersion = advisory.upgradeVersion ?? advisory.fixedVersion;
1676
+ const fix = malicious(advisory.id) ? `Remove ${advisory.package} and replace it with a trusted package or release.` : advisory.fixCommand ? `Run ${advisory.fixCommand}.` : upgradeVersion ? `Upgrade ${upgradePackage} to ${upgradeVersion} or later.` : `Review ${advisory.id}. If no fix exists, add a reason and an expiry of at most 90 days to audit.ignore.`;
1677
+ return problem(
1678
+ `${advisory.id}: ${advisory.package} ${advisory.vulnerableVersion} has ${advisory.severity} severity.${acknowledged}`,
1679
+ fix,
1680
+ {
1681
+ rule: `audit/${advisory.id}`,
1682
+ advisory,
1683
+ ...file ? { file, line: 1 } : {}
1684
+ }
1685
+ );
1686
+ }
1687
+ async function auditIgnores(cwd) {
1688
+ let loaded;
1689
+ try {
1690
+ loaded = await loadConfig(cwd);
1691
+ } catch (error) {
1692
+ if (missing(error)) {
1693
+ return { ignores: [], problems: [] };
1694
+ }
1695
+ throw error;
1696
+ }
1697
+ if (!record3(loaded.value) || !record3(loaded.value.audit)) {
1698
+ return { ignores: [], problems: [] };
1699
+ }
1700
+ const problems = [];
1701
+ const ignores = parseAuditIgnores(loaded.value.audit.ignore, problems);
1702
+ return { ignores, problems };
1703
+ }
1704
+ function parseAuditIgnores(value, problems = []) {
1705
+ if (value === void 0) {
1706
+ return [];
1707
+ }
1708
+ if (!Array.isArray(value)) {
1709
+ problems.push(invalidIgnore("audit.ignore must be an array."));
1710
+ return [];
1711
+ }
1712
+ const today = Date.parse(
1713
+ `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}T00:00:00.000Z`
1714
+ );
1715
+ const ninetyDays = 90 * 24 * 60 * 60 * 1e3;
1716
+ const ids = /* @__PURE__ */ new Set();
1717
+ const ignores = [];
1718
+ value.forEach((entry, index) => {
1719
+ if (!ignoreEntry(entry) || !entry.id.trim() || !entry.reason.trim()) {
1720
+ problems.push(
1721
+ invalidIgnore(
1722
+ `audit.ignore[${index}] must have a non-blank id, reason and until.`
1723
+ )
1724
+ );
1725
+ return;
1726
+ }
1727
+ const id = normalizedId(entry.id);
1728
+ if (ids.has(id)) {
1729
+ problems.push(
1730
+ invalidIgnore(`audit.ignore contains duplicate id ${entry.id}.`)
1731
+ );
1732
+ return;
1733
+ }
1734
+ const expiry = Date.parse(`${entry.until}T00:00:00.000Z`);
1735
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(entry.until) || !Number.isFinite(expiry) || new Date(expiry).toISOString().slice(0, 10) !== entry.until || expiry < today || expiry - today > ninetyDays) {
1736
+ problems.push(
1737
+ invalidIgnore(
1738
+ `audit.ignore[${index}].until must be a real, unexpired date at most 90 days away.`
1739
+ )
1740
+ );
1741
+ return;
1742
+ }
1743
+ ids.add(id);
1744
+ ignores.push(entry);
1745
+ });
1746
+ return ignores;
1747
+ }
1748
+ function invalidIgnore(message6) {
1749
+ return problem(
1750
+ message6,
1751
+ "Fix or remove the invalid acknowledgement. The advisory remains unacknowledged.",
1752
+ { rule: "audit/invalid-ignore", file: "zappie.config.ts", line: 1 }
1753
+ );
1754
+ }
1755
+ function ignoreEntry(value) {
1756
+ return record3(value) && typeof value.id === "string" && typeof value.reason === "string" && typeof value.until === "string";
1757
+ }
1758
+ function npmUpgrade(value) {
1759
+ if (record3(value) && typeof value.name === "string" && typeof value.version === "string") {
1760
+ return { upgradePackage: value.name, upgradeVersion: value.version };
1761
+ }
1762
+ if (value === true) {
1763
+ return { fixCommand: "npm audit fix" };
1764
+ }
1765
+ return {};
1766
+ }
1767
+ function firstFixed(value) {
1768
+ if (!value || value === "<0.0.0") {
1769
+ return null;
1770
+ }
1771
+ return /(?:^|\s)>=?\s*([^\s|]+)/.exec(value)?.[1] ?? null;
1772
+ }
1773
+ function ghsaFrom(value) {
1774
+ const id = value ? /GHSA-[\w-]+/i.exec(value)?.[0] : void 0;
1775
+ return id ? `GHSA-${id.slice(5).toLowerCase()}` : void 0;
1776
+ }
1777
+ function normalizedId(value) {
1778
+ return value.trim().toLowerCase();
1779
+ }
1780
+ function malicious(value) {
1781
+ return /^MAL-/i.test(value.trim());
1782
+ }
1783
+ function normalizedSeverity(value) {
1784
+ const severity = typeof value === "string" ? value.toLowerCase() : "";
1785
+ return severity === "high" || severity === "critical" ? severity : void 0;
1786
+ }
1787
+ function string(value) {
1788
+ return typeof value === "string" ? value : void 0;
1789
+ }
1790
+ function message2(error) {
1791
+ return error instanceof Error ? error.message : String(error);
1792
+ }
1793
+ function missing(error) {
1794
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
1795
+ }
1796
+ function record3(value) {
1797
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1798
+ }
1799
+
1321
1800
  // src/budgets.ts
1322
- import { access as access2, readFile as readFile6 } from "fs/promises";
1323
- import path6 from "path";
1801
+ import { access as access3, readFile as readFile7 } from "fs/promises";
1802
+ import path7 from "path";
1324
1803
 
1325
1804
  // src/budget-files.ts
1326
- import { readFile as readFile4, readdir } from "fs/promises";
1327
- import path4 from "path";
1805
+ import { readFile as readFile5, readdir } from "fs/promises";
1806
+ import path5 from "path";
1328
1807
  import { gzipSync } from "zlib";
1329
1808
  async function cssSizes(cwd) {
1330
- const root = path4.join(cwd, "build/client");
1809
+ const root = path5.join(cwd, "build/client");
1331
1810
  const files = [];
1332
1811
  await collect(root, files);
1333
1812
  return Promise.all(
1334
1813
  files.filter((file) => file.endsWith(".css")).map(async (file) => ({
1335
- file: path4.relative(root, file).split(path4.sep).join("/"),
1336
- bytes: gzipSync(await readFile4(file, "utf8")).byteLength
1814
+ file: path5.relative(root, file).split(path5.sep).join("/"),
1815
+ bytes: gzipSync(await readFile5(file, "utf8")).byteLength
1337
1816
  }))
1338
1817
  );
1339
1818
  }
@@ -1342,7 +1821,7 @@ async function collect(directory, files) {
1342
1821
  if (entry.isSymbolicLink()) {
1343
1822
  continue;
1344
1823
  }
1345
- const file = path4.join(directory, entry.name);
1824
+ const file = path5.join(directory, entry.name);
1346
1825
  if (entry.isDirectory()) {
1347
1826
  await collect(file, files);
1348
1827
  } else if (entry.isFile()) {
@@ -1352,8 +1831,8 @@ async function collect(directory, files) {
1352
1831
  }
1353
1832
 
1354
1833
  // src/budget-manifest.ts
1355
- import { readFile as readFile5 } from "fs/promises";
1356
- import path5 from "path";
1834
+ import { readFile as readFile6 } from "fs/promises";
1835
+ import path6 from "path";
1357
1836
  import { gzipSync as gzipSync2 } from "zlib";
1358
1837
  function routeKeys(manifest) {
1359
1838
  return Object.entries(manifest).filter(([key, entry]) => routeSource(entry.src ?? key)).map(([key]) => key).sort();
@@ -1367,7 +1846,7 @@ async function graphSizes(cwd, manifest, roots) {
1367
1846
  [...keys].map((key) => manifest[key]).filter((entry) => Boolean(entry)).filter(({ file }) => file.endsWith(".js")).map(async ({ file }) => ({
1368
1847
  file,
1369
1848
  bytes: gzipSync2(
1370
- await readFile5(path5.join(cwd, "build/client", file), "utf8")
1849
+ await readFile6(path6.join(cwd, "build/client", file), "utf8")
1371
1850
  ).byteLength
1372
1851
  }))
1373
1852
  );
@@ -1401,16 +1880,16 @@ async function checkBudgets({
1401
1880
  signal
1402
1881
  }) {
1403
1882
  signal?.throwIfAborted();
1404
- const manifestFile = path6.join(cwd, "build/client/.vite/manifest.json");
1883
+ const manifestFile = path7.join(cwd, "build/client/.vite/manifest.json");
1405
1884
  try {
1406
- await access2(manifestFile);
1885
+ await access3(manifestFile);
1407
1886
  } catch {
1408
1887
  return skipped(
1409
1888
  "budgets",
1410
1889
  "No build/client/.vite/manifest.json is available."
1411
1890
  );
1412
1891
  }
1413
- const manifest = JSON.parse(await readFile6(manifestFile, "utf8"));
1892
+ const manifest = JSON.parse(await readFile7(manifestFile, "utf8"));
1414
1893
  const config = (await loadConfig(cwd)).value;
1415
1894
  const limits = { ...DEFAULT_BUDGETS, ...config.budgets };
1416
1895
  const routes = routeKeys(manifest);
@@ -1465,56 +1944,8 @@ function cssProblem(sizes, limitKb, over) {
1465
1944
  var total = (sizes) => sizes.reduce((sum, asset) => sum + asset.bytes, 0);
1466
1945
 
1467
1946
  // src/build.ts
1468
- import { readFile as readFile7 } from "fs/promises";
1469
- import path7 from "path";
1470
-
1471
- // src/command.ts
1472
- import { spawn } from "child_process";
1473
- function command(executable, args, cwd, signal) {
1474
- return new Promise((resolve, reject) => {
1475
- const child = spawn(executable, args, {
1476
- cwd,
1477
- env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" },
1478
- stdio: ["ignore", "pipe", "pipe"],
1479
- ...signal ? { signal } : {}
1480
- });
1481
- let stdout = "";
1482
- let stderr = "";
1483
- child.stdout.on("data", (value) => {
1484
- stdout += String(value);
1485
- });
1486
- child.stderr.on("data", (value) => {
1487
- stderr += String(value);
1488
- });
1489
- child.on("error", reject);
1490
- child.on("close", (code) => {
1491
- resolve({ code: code ?? 1, stdout, stderr });
1492
- });
1493
- });
1494
- }
1495
- function execArgs(manager, binary, args) {
1496
- if (manager === "npm") {
1497
- return { executable: "npm", args: ["exec", "--no", "--", binary, ...args] };
1498
- }
1499
- if (manager === "yarn") {
1500
- return { executable: "yarn", args: ["exec", binary, ...args] };
1501
- }
1502
- if (manager === "bun") {
1503
- return { executable: "bun", args: ["x", "--no-install", binary, ...args] };
1504
- }
1505
- return { executable: "pnpm", args: ["exec", binary, ...args] };
1506
- }
1507
- function scriptArgs(manager, name) {
1508
- if (manager === "bun") {
1509
- return { executable: "bun", args: ["run", name] };
1510
- }
1511
- if (manager === "npm") {
1512
- return { executable: "npm", args: ["run", name] };
1513
- }
1514
- return { executable: manager, args: [name] };
1515
- }
1516
-
1517
- // src/build.ts
1947
+ import { readFile as readFile8 } from "fs/promises";
1948
+ import path8 from "path";
1518
1949
  async function checkBuild({
1519
1950
  cwd,
1520
1951
  packageManager,
@@ -1522,7 +1953,7 @@ async function checkBuild({
1522
1953
  }) {
1523
1954
  signal?.throwIfAborted();
1524
1955
  const manifest = JSON.parse(
1525
- await readFile7(path7.join(cwd, "package.json"), "utf8")
1956
+ await readFile8(path8.join(cwd, "package.json"), "utf8")
1526
1957
  );
1527
1958
  const invocation = manifest.dependencies?.["react-router"] ? execArgs(packageManager, "react-router", ["build"]) : manifest.scripts?.build ? scriptArgs(packageManager, "build") : void 0;
1528
1959
  if (!invocation) {
@@ -1557,8 +1988,8 @@ ${stderr}`.split(/\r?\n/).map((line) => line.trimEnd()).filter(Boolean).slice(-1
1557
1988
  }
1558
1989
 
1559
1990
  // src/css-files.ts
1560
- import { lstat, readFile as readFile8, readdir as readdir2 } from "fs/promises";
1561
- import path8 from "path";
1991
+ import { lstat, readFile as readFile9, readdir as readdir2 } from "fs/promises";
1992
+ import path9 from "path";
1562
1993
  var FILE_SIZE_CAP = 1024 * 1024;
1563
1994
  var BUILD_FILE_SIZE_CAP = 8 * 1024 * 1024;
1564
1995
  var ignoredDirectories = /* @__PURE__ */ new Set([
@@ -1582,7 +2013,7 @@ async function readCssInputs(cwd) {
1582
2013
  return !name.startsWith(".") && !ignoredDirectories.has(name);
1583
2014
  });
1584
2015
  const built = [];
1585
- await collect2(path8.join(cwd, "build/client"), built, () => true);
2016
+ await collect2(path9.join(cwd, "build/client"), built, () => true);
1586
2017
  const read = (files, cap) => texts(cwd, files, cap);
1587
2018
  return {
1588
2019
  sourceCss: await read(
@@ -1614,7 +2045,7 @@ async function texts(cwd, files, cap) {
1614
2045
  }
1615
2046
  result.push({
1616
2047
  file: relative(cwd, file),
1617
- text: await readFile8(file, "utf8")
2048
+ text: await readFile9(file, "utf8")
1618
2049
  });
1619
2050
  }
1620
2051
  return result;
@@ -1633,7 +2064,7 @@ async function collect2(directory, files, enter) {
1633
2064
  if (entry.isSymbolicLink()) {
1634
2065
  continue;
1635
2066
  }
1636
- const file = path8.join(directory, entry.name);
2067
+ const file = path9.join(directory, entry.name);
1637
2068
  if (entry.isDirectory()) {
1638
2069
  if (enter(entry.name)) {
1639
2070
  await collect2(file, files, enter);
@@ -1747,9 +2178,9 @@ function important(css, code) {
1747
2178
  }
1748
2179
  return problems;
1749
2180
  }
1750
- function importantProblem(message5, file, line) {
2181
+ function importantProblem(message6, file, line) {
1751
2182
  return problem(
1752
- message5,
2183
+ message6,
1753
2184
  "Raise the selector's specificity or reorder the rules instead. For a reviewed case, add /* zappie-allow-important: <reason> */ on the same line.",
1754
2185
  { rule: "css/important", file, ...line ? { line } : {} }
1755
2186
  );
@@ -2333,7 +2764,7 @@ async function checkCss({
2333
2764
  // src/lint.ts
2334
2765
  import zappie from "eslint-plugin-zappie";
2335
2766
  import { createRequire } from "module";
2336
- import path9 from "path";
2767
+ import path10 from "path";
2337
2768
  import { pathToFileURL as pathToFileURL2 } from "url";
2338
2769
  async function checkLint({
2339
2770
  cwd,
@@ -2342,12 +2773,12 @@ async function checkLint({
2342
2773
  signal?.throwIfAborted();
2343
2774
  let ESLint;
2344
2775
  try {
2345
- const require2 = createRequire(path9.join(cwd, "package.json"));
2776
+ const require2 = createRequire(path10.join(cwd, "package.json"));
2346
2777
  const loadedEslint = await import(pathToFileURL2(require2.resolve("eslint")).href);
2347
2778
  ESLint = loadedEslint.ESLint;
2348
2779
  } catch (error) {
2349
2780
  return step("lint", "failed", [
2350
- problem(message2(error), "Install ESLint in the project.", {
2781
+ problem(message3(error), "Install ESLint in the project.", {
2351
2782
  rule: "lint/unavailable"
2352
2783
  })
2353
2784
  ]);
@@ -2378,7 +2809,7 @@ async function checkLint({
2378
2809
  } catch (error) {
2379
2810
  return step("lint", "failed", [
2380
2811
  problem(
2381
- message2(error),
2812
+ message3(error),
2382
2813
  "Fix the project ESLint configuration and run lint again.",
2383
2814
  {
2384
2815
  rule: "lint/config"
@@ -2433,7 +2864,7 @@ async function sourceFile(cwd) {
2433
2864
  for (const candidate of candidates) {
2434
2865
  try {
2435
2866
  await import("fs/promises").then(
2436
- ({ access: access3 }) => access3(path9.join(cwd, candidate))
2867
+ ({ access: access4 }) => access4(path10.join(cwd, candidate))
2437
2868
  );
2438
2869
  return candidate;
2439
2870
  } catch {
@@ -2442,13 +2873,13 @@ async function sourceFile(cwd) {
2442
2873
  }
2443
2874
  return "eslint.config.mjs";
2444
2875
  }
2445
- function message2(error) {
2876
+ function message3(error) {
2446
2877
  return error instanceof Error ? error.message : String(error);
2447
2878
  }
2448
2879
 
2449
2880
  // src/readiness.ts
2450
- import { readFile as readFile9 } from "fs/promises";
2451
- import path10 from "path";
2881
+ import { readFile as readFile10 } from "fs/promises";
2882
+ import path11 from "path";
2452
2883
  import { pathToFileURL as pathToFileURL3 } from "url";
2453
2884
  async function checkReadiness({
2454
2885
  cwd,
@@ -2466,9 +2897,9 @@ async function checkReadiness({
2466
2897
  )
2467
2898
  ]);
2468
2899
  }
2469
- const file = path10.join(cwd, "react-router.config.ts");
2900
+ const file = path11.join(cwd, "react-router.config.ts");
2470
2901
  try {
2471
- await readFile9(file, "utf8");
2902
+ await readFile10(file, "utf8");
2472
2903
  const version = Date.now();
2473
2904
  const config = await import(`${pathToFileURL3(file).href}?readiness=${version}`);
2474
2905
  if (config.default?.routeDiscovery?.mode === "initial") {
@@ -2477,7 +2908,7 @@ async function checkReadiness({
2477
2908
  } catch (error) {
2478
2909
  return step("readiness", "failed", [
2479
2910
  problem(
2480
- message3(error),
2911
+ message4(error),
2481
2912
  'Create react-router.config.ts and set routeDiscovery: { mode: "initial" }.',
2482
2913
  {
2483
2914
  rule: "readiness/route-discovery",
@@ -2494,13 +2925,15 @@ async function checkReadiness({
2494
2925
  )
2495
2926
  ]);
2496
2927
  }
2497
- function message3(error) {
2928
+ function message4(error) {
2498
2929
  return error instanceof Error ? error.message : String(error);
2499
2930
  }
2500
2931
 
2501
2932
  // src/secrets.ts
2502
- import { lstat as lstat2, readFile as readFile10, readdir as readdir3 } from "fs/promises";
2503
- import path11 from "path";
2933
+ import { execFile } from "child_process";
2934
+ import { lstat as lstat2, readFile as readFile11, readdir as readdir3 } from "fs/promises";
2935
+ import path12 from "path";
2936
+ import { promisify } from "util";
2504
2937
 
2505
2938
  // src/secret-patterns.ts
2506
2939
  var FIX = "Move the value to a server environment variable and rotate it.";
@@ -2541,6 +2974,35 @@ function secretMatches(source, envFile) {
2541
2974
  fix: FIX,
2542
2975
  severity: "error"
2543
2976
  }));
2977
+ const gitleaksDefaults = [
2978
+ [/\bghp_[A-Za-z0-9]{36}\b/g, "github-pat", "GitHub personal access token"],
2979
+ [
2980
+ /\bgithub_pat_[A-Za-z0-9_]{82}\b/g,
2981
+ "github-fine-grained-pat",
2982
+ "GitHub fine-grained access token"
2983
+ ],
2984
+ [
2985
+ /\bglpat-[A-Za-z0-9_-]{20,}\b/g,
2986
+ "gitlab-pat",
2987
+ "GitLab personal access token"
2988
+ ],
2989
+ [/\bnpm_[A-Za-z0-9]{36}\b/g, "npm-token", "npm access token"],
2990
+ [/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, "slack-token", "Slack token"],
2991
+ [/\bAIza[0-9A-Za-z_-]{35}\b/g, "google-api-key", "Google API key"],
2992
+ [
2993
+ /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g,
2994
+ "sendgrid-api-key",
2995
+ "SendGrid API key"
2996
+ ]
2997
+ ];
2998
+ for (const [pattern, rule, label] of gitleaksDefaults) {
2999
+ add(matches2, source, pattern, () => ({
3000
+ rule: `secrets/${rule}`,
3001
+ message: `Detected a ${label}.`,
3002
+ fix: FIX,
3003
+ severity: "error"
3004
+ }));
3005
+ }
2544
3006
  const occupied = matches2.map(({ index }) => index);
2545
3007
  const assignment = /\b([A-Za-z_$][\w$]*(?:secret|token|password|api[_-]?key)[\w$]*)\s*[:=]\s*(["'])([^"'\r\n]{24,})\2/gi;
2546
3008
  for (const found of source.matchAll(assignment)) {
@@ -2578,21 +3040,31 @@ function entropy(value) {
2578
3040
 
2579
3041
  // src/secrets.ts
2580
3042
  var SECRET_FILE_SIZE_CAP = 1024 * 1024;
2581
- async function scanSecrets(dir) {
3043
+ var execFileAsync = promisify(execFile);
3044
+ var lockfile = /(?:^|\/)(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?)$/;
3045
+ async function scanSecrets(dir, options = {}) {
2582
3046
  const findings = [];
2583
3047
  for (const file of await filesToScan(dir)) {
2584
- const info = await lstat2(file);
3048
+ let info;
3049
+ try {
3050
+ info = await lstat2(file);
3051
+ } catch (error) {
3052
+ if (missing2(error)) {
3053
+ continue;
3054
+ }
3055
+ throw error;
3056
+ }
2585
3057
  if (!info.isFile() || info.size > SECRET_FILE_SIZE_CAP) {
2586
3058
  continue;
2587
3059
  }
2588
- const contents = await readFile10(file);
3060
+ const contents = await readFile11(file);
2589
3061
  if (contents.includes(0)) {
2590
3062
  continue;
2591
3063
  }
2592
3064
  const source = contents.toString("utf8");
2593
3065
  for (const match of secretMatches(
2594
3066
  source,
2595
- path11.basename(file).startsWith(".env")
3067
+ path12.basename(file).startsWith(".env")
2596
3068
  )) {
2597
3069
  findings.push({
2598
3070
  rule: match.rule,
@@ -2604,14 +3076,21 @@ async function scanSecrets(dir) {
2604
3076
  });
2605
3077
  }
2606
3078
  }
3079
+ if (options.history) {
3080
+ findings.push(...await scanHistory(dir));
3081
+ }
2607
3082
  return findings;
2608
3083
  }
2609
3084
  async function checkSecrets({
2610
3085
  cwd,
3086
+ history,
2611
3087
  signal
2612
3088
  }) {
2613
3089
  signal?.throwIfAborted();
2614
- const findings = await scanSecrets(cwd);
3090
+ const findings = await scanSecrets(
3091
+ cwd,
3092
+ history === void 0 ? {} : { history }
3093
+ );
2615
3094
  signal?.throwIfAborted();
2616
3095
  const failed = findings.some(({ severity }) => severity === "error");
2617
3096
  return step(
@@ -2621,25 +3100,93 @@ async function checkSecrets({
2621
3100
  );
2622
3101
  }
2623
3102
  async function filesToScan(cwd) {
3103
+ const files = await trackedFiles(cwd);
3104
+ for (const directory of ["build/client", "build/server", "workers"]) {
3105
+ await collect3(path12.join(cwd, directory), files);
3106
+ }
3107
+ return [...new Set(files)].filter((file) => !lockfile.test(relative(cwd, file))).sort();
3108
+ }
3109
+ async function trackedFiles(cwd) {
3110
+ try {
3111
+ const { stdout } = await execFileAsync("git", ["ls-files", "-z"], {
3112
+ cwd,
3113
+ encoding: "utf8",
3114
+ maxBuffer: 32 * 1024 * 1024
3115
+ });
3116
+ const files = stdout.split("\0").filter(Boolean).map((file) => path12.join(cwd, file));
3117
+ return files.length > 0 ? files : fallbackFiles(cwd);
3118
+ } catch {
3119
+ return fallbackFiles(cwd);
3120
+ }
3121
+ }
3122
+ async function fallbackFiles(cwd) {
2624
3123
  const files = [];
2625
3124
  for (const directory of ["app", "public"]) {
2626
- await collect3(path11.join(cwd, directory), files);
3125
+ await collect3(path12.join(cwd, directory), files);
2627
3126
  }
2628
- await collect3(
2629
- path11.join(cwd, "build/client"),
2630
- files,
2631
- (file) => file.endsWith(".js")
2632
- );
2633
3127
  try {
2634
3128
  for (const entry of await readdir3(cwd, { withFileTypes: true })) {
2635
3129
  if (entry.isFile() && rootConfig(entry.name)) {
2636
- files.push(path11.join(cwd, entry.name));
3130
+ files.push(path12.join(cwd, entry.name));
2637
3131
  }
2638
3132
  }
2639
3133
  } catch {
2640
- return files.sort();
3134
+ return files;
2641
3135
  }
2642
- return [...new Set(files)].sort();
3136
+ return files;
3137
+ }
3138
+ async function scanHistory(cwd) {
3139
+ try {
3140
+ const { stdout } = await execFileAsync(
3141
+ "git",
3142
+ [
3143
+ "log",
3144
+ "--all",
3145
+ "--format=commit:%H",
3146
+ "-p",
3147
+ "--no-ext-diff",
3148
+ "--",
3149
+ ".",
3150
+ ":(exclude)package-lock.json",
3151
+ ":(exclude)pnpm-lock.yaml",
3152
+ ":(exclude)yarn.lock",
3153
+ ":(exclude)bun.lock",
3154
+ ":(exclude)bun.lockb"
3155
+ ],
3156
+ { cwd, encoding: "utf8", maxBuffer: 128 * 1024 * 1024 }
3157
+ );
3158
+ return secretMatches(stdout, false).map((match) => {
3159
+ const location2 = historyLocation(stdout, match.index);
3160
+ return {
3161
+ rule: match.rule,
3162
+ file: location2 ?? "git history",
3163
+ line: lineAt(stdout, match.index),
3164
+ message: match.message,
3165
+ fix: match.fix,
3166
+ severity: match.severity
3167
+ };
3168
+ });
3169
+ } catch {
3170
+ return [
3171
+ {
3172
+ rule: "secrets/history-unavailable",
3173
+ file: "git history",
3174
+ line: 1,
3175
+ message: "Could not scan Git history for secrets.",
3176
+ fix: "Check Git access and run zappie check --history again.",
3177
+ severity: "warning"
3178
+ }
3179
+ ];
3180
+ }
3181
+ }
3182
+ function historyLocation(source, index) {
3183
+ const prefix = source.slice(0, index);
3184
+ const commit = [...prefix.matchAll(/^commit:([0-9a-f]+)$/gm)].at(-1)?.[1];
3185
+ const changedPaths = [
3186
+ ...prefix.matchAll(/^(?:\+\+\+ b\/|--- a\/)(.+)$/gm)
3187
+ ].map((match) => match[1]);
3188
+ const file = changedPaths.at(-1);
3189
+ return commit && file ? `${commit}:${file}` : void 0;
2643
3190
  }
2644
3191
  async function collect3(directory, files, accept = () => true) {
2645
3192
  try {
@@ -2647,7 +3194,7 @@ async function collect3(directory, files, accept = () => true) {
2647
3194
  if (entry.isSymbolicLink()) {
2648
3195
  continue;
2649
3196
  }
2650
- const file = path11.join(directory, entry.name);
3197
+ const file = path12.join(directory, entry.name);
2651
3198
  if (entry.isDirectory()) {
2652
3199
  await collect3(file, files, accept);
2653
3200
  } else if (entry.isFile() && accept(file)) {
@@ -2655,11 +3202,14 @@ async function collect3(directory, files, accept = () => true) {
2655
3202
  }
2656
3203
  }
2657
3204
  } catch (error) {
2658
- if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
3205
+ if (!missing2(error)) {
2659
3206
  throw error;
2660
3207
  }
2661
3208
  }
2662
3209
  }
3210
+ function missing2(error) {
3211
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
3212
+ }
2663
3213
  function rootConfig(name) {
2664
3214
  return name === "package.json" || name === "wrangler.json" || name === "wrangler.jsonc" || /(?:^|\.)config\.(?:[cm]?[jt]s|json)$/.test(name);
2665
3215
  }
@@ -2668,13 +3218,13 @@ function lineAt(source, index) {
2668
3218
  }
2669
3219
 
2670
3220
  // src/test-step.ts
2671
- import path12 from "path";
3221
+ import path13 from "path";
2672
3222
  async function checkTests({
2673
3223
  cwd,
2674
3224
  packageManager,
2675
3225
  signal
2676
3226
  }) {
2677
- const manifest = await json(path12.join(cwd, "package.json"));
3227
+ const manifest = await json(path13.join(cwd, "package.json"));
2678
3228
  if (!manifest.scripts?.test) {
2679
3229
  return skipped("test", "No test script is defined in package.json.");
2680
3230
  }
@@ -2697,11 +3247,11 @@ async function checkTests({
2697
3247
  }
2698
3248
 
2699
3249
  // src/types-step.ts
2700
- import path13 from "path";
3250
+ import path14 from "path";
2701
3251
  async function checkTypes(options) {
2702
3252
  const { cwd, packageManager, signal } = options;
2703
3253
  signal?.throwIfAborted();
2704
- const manifest = await json(path13.join(cwd, "package.json"));
3254
+ const manifest = await json(path14.join(cwd, "package.json"));
2705
3255
  if (manifest.dependencies?.["react-router"]) {
2706
3256
  const generated = execArgs(packageManager, "react-router", ["typegen"]);
2707
3257
  const result2 = await command(
@@ -2755,6 +3305,7 @@ function outputProblems(output) {
2755
3305
  var runners = [
2756
3306
  { name: "config", run: checkConfig },
2757
3307
  { name: "allowlist", run: checkAllowlist },
3308
+ { name: "audit", run: checkAudit },
2758
3309
  { name: "lint", run: checkLint },
2759
3310
  { name: "types", run: checkTypes },
2760
3311
  { name: "test", run: checkTests },
@@ -2775,13 +3326,13 @@ async function runCheck(options) {
2775
3326
  options.signal?.throwIfAborted();
2776
3327
  checked = step(runner.name, "failed", [
2777
3328
  problem(
2778
- message4(error),
3329
+ message5(error),
2779
3330
  "Fix the step setup and run zappie check again."
2780
3331
  )
2781
3332
  ]);
2782
3333
  }
2783
3334
  steps.push(checked);
2784
- if (checked.result === "failed" && !options.all) {
3335
+ if (checked.result === "failed" && !options.all && runner.name !== "audit") {
2785
3336
  break;
2786
3337
  }
2787
3338
  }
@@ -2790,7 +3341,7 @@ async function runCheck(options) {
2790
3341
  steps
2791
3342
  };
2792
3343
  }
2793
- function message4(error) {
3344
+ function message5(error) {
2794
3345
  return error instanceof Error ? error.message : String(error);
2795
3346
  }
2796
3347
  export {
@@ -2799,6 +3350,7 @@ export {
2799
3350
  SECRET_FILE_SIZE_CAP,
2800
3351
  analyseCss,
2801
3352
  checkAllowlist,
3353
+ checkAudit,
2802
3354
  checkBudgets,
2803
3355
  checkBuild,
2804
3356
  checkConfig,
@@ -2808,7 +3360,9 @@ export {
2808
3360
  checkSecrets,
2809
3361
  checkTests,
2810
3362
  checkTypes,
3363
+ evaluateAudit,
2811
3364
  loadConfig,
3365
+ parseAuditOutput,
2812
3366
  runCheck,
2813
3367
  scanSecrets,
2814
3368
  supportsTypeScriptImport