@zappie/check 0.2.0 → 0.2.2

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"
@@ -67,7 +67,7 @@ var globallyAllowed = (value) => GLOBAL_PACKAGE_ALLOWLIST.some(
67
67
  function hostname(value) {
68
68
  const wildcard = value.startsWith("*.");
69
69
  const host = wildcard ? value.slice(2) : value;
70
- if (!host.includes(".") || /^\d+(?:\.\d+){3}$/.test(host) || host.length > 253 || host.includes("..")) {
70
+ if (!host.includes(".") || /^\d+(?:\.\d+){3}$/.test(host) || host.length > 253 || host.includes("..") || wildcard && /^(?:ac|co|com|edu|gov|net|org)\.[^.]+$/i.test(host)) {
71
71
  return false;
72
72
  }
73
73
  return host.split(".").every(
@@ -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,7 +181,9 @@ var rootKeys = [
181
181
  "prerender",
182
182
  "menu",
183
183
  "packages",
184
+ "audit",
184
185
  "outbound",
186
+ "security",
185
187
  "budgets",
186
188
  "readiness",
187
189
  "analytics"
@@ -189,7 +191,7 @@ var rootKeys = [
189
191
  function validateConfig(input) {
190
192
  const issues = [];
191
193
  const issue = (value) => issues.push(value);
192
- const error = (path14, code, message5) => issue({ path: path14, code, message: message5, severity: "error" });
194
+ const error = (path15, code, message6) => issue({ path: path15, code, message: message6, severity: "error" });
193
195
  if (!record(input)) {
194
196
  return {
195
197
  valid: false,
@@ -235,16 +237,20 @@ function validateConfig(input) {
235
237
  });
236
238
  });
237
239
  nested(input.outbound, "outbound", ["allow"], issue, (value) => {
238
- const allow = stringList(value.allow, "outbound.allow", error);
239
- allow?.forEach((host, index) => {
240
- if (!hostname(host)) {
241
- error(
242
- `outbound.allow[${index}]`,
243
- "invalid_format",
244
- "Expected a hostname or leading *. subdomain pattern without a scheme, path, port or IP. Use a value such as api.example.com."
245
- );
240
+ hostList(value.allow, "outbound.allow", error);
241
+ });
242
+ nested(input.security, "security", ["csp"], issue, (security) => {
243
+ nested(
244
+ security.csp,
245
+ "security.csp",
246
+ ["imgSrc", "connectSrc", "fontSrc", "styleSrc", "frameSrc"],
247
+ issue,
248
+ (csp) => {
249
+ for (const [name, value] of Object.entries(csp)) {
250
+ hostList(value, `security.csp.${name}`, error);
251
+ }
246
252
  }
247
- });
253
+ );
248
254
  });
249
255
  budgets(input.budgets, issue, error);
250
256
  nested(
@@ -284,12 +290,12 @@ function validateConfig(input) {
284
290
  issues
285
291
  };
286
292
  }
287
- function requiredSlug(value, path14, error) {
293
+ function requiredSlug(value, path15, error) {
288
294
  if (!nonblank(value)) {
289
295
  error(
290
- path14,
296
+ path15,
291
297
  value === void 0 ? "missing_required" : "invalid_type",
292
- `Expected a non-empty storefront slug. Set ${path14} to the slug used by createZappie().`
298
+ `Expected a non-empty storefront slug. Set ${path15} to the slug used by createZappie().`
293
299
  );
294
300
  }
295
301
  }
@@ -308,29 +314,29 @@ function theme(value, error) {
308
314
  );
309
315
  }
310
316
  }
311
- function stringList(value, path14, error, options = {}) {
317
+ function stringList(value, path15, error, options = {}) {
312
318
  if (value === void 0) {
313
319
  return void 0;
314
320
  }
315
321
  if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
316
322
  error(
317
- path14,
323
+ path15,
318
324
  "invalid_type",
319
- `Expected an array of strings. Set ${path14} to string values.`
325
+ `Expected an array of strings. Set ${path15} to string values.`
320
326
  );
321
327
  return void 0;
322
328
  }
323
329
  if (options.nonempty && value.length === 0) {
324
330
  error(
325
- path14,
331
+ path15,
326
332
  "invalid_format",
327
- `Expected at least one value. Add a value to ${path14} or remove the field.`
333
+ `Expected at least one value. Add a value to ${path15} or remove the field.`
328
334
  );
329
335
  }
330
336
  value.forEach((entry, index) => {
331
337
  if (options.slug && !nonblank(entry)) {
332
338
  error(
333
- `${path14}[${index}]`,
339
+ `${path15}[${index}]`,
334
340
  "invalid_format",
335
341
  "Expected a non-empty branch slug. Remove the empty value or use a branch slug."
336
342
  );
@@ -338,17 +344,28 @@ function stringList(value, path14, error, options = {}) {
338
344
  });
339
345
  if (options.unique && new Set(value).size !== value.length) {
340
346
  error(
341
- path14,
347
+ path15,
342
348
  "duplicate",
343
- `Expected unique values. Remove duplicates from ${path14}.`
349
+ `Expected unique values. Remove duplicates from ${path15}.`
344
350
  );
345
351
  }
346
352
  return value;
347
353
  }
354
+ function hostList(value, path15, error) {
355
+ stringList(value, path15, error)?.forEach((host, index) => {
356
+ if (!hostname(host)) {
357
+ error(
358
+ `${path15}[${index}]`,
359
+ "invalid_format",
360
+ "Expected a hostname or leading *. subdomain pattern without a scheme, path, port or IP. Use a value such as api.example.com."
361
+ );
362
+ }
363
+ });
364
+ }
348
365
  function paths(value, error) {
349
366
  const values = stringList(value, "prerender", error, { unique: true });
350
- values?.forEach((path14, index) => {
351
- if (!path14.startsWith("/")) {
367
+ values?.forEach((path15, index) => {
368
+ if (!path15.startsWith("/")) {
352
369
  error(
353
370
  `prerender[${index}]`,
354
371
  "invalid_format",
@@ -851,11 +868,11 @@ var record2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
851
868
  var includes = (values, key) => values.includes(key);
852
869
  function validateTheme(input) {
853
870
  const issues = [];
854
- const issue = (path14, code, message5, severity = "error") => issues.push({ path: path14, code, message: message5, severity });
855
- const keys = (obj, allowed, path14) => {
871
+ const issue = (path15, code, message6, severity = "error") => issues.push({ path: path15, code, message: message6, severity });
872
+ const keys = (obj, allowed, path15) => {
856
873
  for (const key of Object.keys(obj)) {
857
874
  if (!allowed.includes(key)) {
858
- issue(path14 ? `${path14}.${key}` : key, "invalid_theme", "Unknown field");
875
+ issue(path15 ? `${path15}.${key}` : key, "invalid_theme", "Unknown field");
859
876
  }
860
877
  }
861
878
  };
@@ -916,9 +933,9 @@ function validateTheme(input) {
916
933
  }
917
934
  }
918
935
  for (const [key, value] of Object.entries(values)) {
919
- const path14 = `${group}.${key}`;
936
+ const path15 = `${group}.${key}`;
920
937
  if (!includes(TOKEN_NAMES, key)) {
921
- issue(path14, "unknown_token", "Unknown semantic token");
938
+ issue(path15, "unknown_token", "Unknown semantic token");
922
939
  continue;
923
940
  }
924
941
  let code, valid = false;
@@ -936,7 +953,7 @@ function validateTheme(input) {
936
953
  valid = typeof value === "string" && safeValue(value) && (includes(DURATION_TOKENS, key) ? isDuration(value) : isEasing(value));
937
954
  }
938
955
  if (!valid) {
939
- issue(path14, code, "Invalid or unsafe token value");
956
+ issue(path15, code, "Invalid or unsafe token value");
940
957
  }
941
958
  }
942
959
  }
@@ -946,30 +963,30 @@ function validateTheme(input) {
946
963
  } else {
947
964
  const roles = /* @__PURE__ */ new Set();
948
965
  input.fonts.forEach((font, i) => {
949
- const path14 = `fonts[${i}]`;
966
+ const path15 = `fonts[${i}]`;
950
967
  if (!record2(font)) {
951
- issue(path14, "invalid_font", "Expected a font object");
968
+ issue(path15, "invalid_font", "Expected a font object");
952
969
  return;
953
970
  }
954
- keys(font, ["family", "source", "url", "weights", "role"], path14);
971
+ keys(font, ["family", "source", "url", "weights", "role"], path15);
955
972
  if (typeof font.family !== "string" || !isFontFamily(font.family) || font.family.includes(",")) {
956
973
  issue(
957
- `${path14}.family`,
974
+ `${path15}.family`,
958
975
  "invalid_font",
959
976
  "Expected one unquoted font family"
960
977
  );
961
978
  }
962
979
  if (typeof font.source !== "string" || !["google", "self", "system"].includes(font.source)) {
963
- issue(`${path14}.source`, "invalid_font", "Unknown font source");
980
+ issue(`${path15}.source`, "invalid_font", "Unknown font source");
964
981
  }
965
982
  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");
983
+ issue(`${path15}.role`, "invalid_font", "Expected a unique font role");
967
984
  }
968
985
  roles.add(String(font.role));
969
986
  if (font.source === "self" || font.url !== void 0) {
970
987
  if (typeof font.url !== "string" || !isAssetUrl(font.url, false) || !new URL(font.url).pathname.endsWith(".woff2")) {
971
988
  issue(
972
- `${path14}.url`,
989
+ `${path15}.url`,
973
990
  "invalid_font",
974
991
  "Expected an HTTPS .woff2 URL"
975
992
  );
@@ -979,7 +996,7 @@ function validateTheme(input) {
979
996
  (w) => typeof w !== "number" || !Number.isInteger(w) || w < 1 || w > 1e3
980
997
  ))) {
981
998
  issue(
982
- `${path14}.weights`,
999
+ `${path15}.weights`,
983
1000
  "invalid_font",
984
1001
  "Expected 1\u20139 integer weights between 1 and 1000"
985
1002
  );
@@ -1089,13 +1106,13 @@ function escape(value) {
1089
1106
  }
1090
1107
 
1091
1108
  // src/result.ts
1092
- function problem(message5, fix, details = {}) {
1093
- return { ...details, message: message5, fix };
1109
+ function problem(message6, fix, details = {}) {
1110
+ return { ...details, message: message6, fix };
1094
1111
  }
1095
1112
  function step(name, result, problems = []) {
1096
1113
  return { name, result, problems };
1097
1114
  }
1098
- var skipped = (name, message5) => step(name, "skipped", [problem(message5, null)]);
1115
+ var skipped = (name, message6) => step(name, "skipped", [problem(message6, null)]);
1099
1116
 
1100
1117
  // src/config.ts
1101
1118
  function supportsTypeScriptImport(version = process.versions.node) {
@@ -1318,22 +1335,500 @@ async function installedManifest(cwd, name) {
1318
1335
  }
1319
1336
  }
1320
1337
 
1338
+ // src/audit.ts
1339
+ import { access as access2, readFile as readFile4 } from "fs/promises";
1340
+ import path4 from "path";
1341
+
1342
+ // src/command.ts
1343
+ import { spawn } from "child_process";
1344
+ function command(executable, args, cwd, signal) {
1345
+ return new Promise((resolve, reject) => {
1346
+ const child = spawn(executable, args, {
1347
+ cwd,
1348
+ env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" },
1349
+ stdio: ["ignore", "pipe", "pipe"],
1350
+ ...signal ? { signal } : {}
1351
+ });
1352
+ let stdout = "";
1353
+ let stderr = "";
1354
+ child.stdout.on("data", (value) => {
1355
+ stdout += String(value);
1356
+ });
1357
+ child.stderr.on("data", (value) => {
1358
+ stderr += String(value);
1359
+ });
1360
+ child.on("error", reject);
1361
+ child.on("close", (code) => {
1362
+ resolve({ code: code ?? 1, stdout, stderr });
1363
+ });
1364
+ });
1365
+ }
1366
+ function execArgs(manager, binary, args) {
1367
+ if (manager === "npm") {
1368
+ return { executable: "npm", args: ["exec", "--no", "--", binary, ...args] };
1369
+ }
1370
+ if (manager === "yarn") {
1371
+ return { executable: "yarn", args: ["exec", binary, ...args] };
1372
+ }
1373
+ if (manager === "bun") {
1374
+ return { executable: "bun", args: ["x", "--no-install", binary, ...args] };
1375
+ }
1376
+ return { executable: "pnpm", args: ["exec", binary, ...args] };
1377
+ }
1378
+ function scriptArgs(manager, name) {
1379
+ if (manager === "bun") {
1380
+ return { executable: "bun", args: ["run", name] };
1381
+ }
1382
+ if (manager === "npm") {
1383
+ return { executable: "npm", args: ["run", name] };
1384
+ }
1385
+ return { executable: manager, args: [name] };
1386
+ }
1387
+
1388
+ // src/audit.ts
1389
+ var AUDIT_TIMEOUT_MS = 6e4;
1390
+ async function checkAudit(options, dependencies = {}) {
1391
+ const mode = options.audit ?? "report";
1392
+ if (mode === "off") {
1393
+ return skipped("audit", "Dependency auditing is off.");
1394
+ }
1395
+ options.signal?.throwIfAborted();
1396
+ let invalidIgnores = [];
1397
+ let ignores = [];
1398
+ try {
1399
+ const parsed = await auditIgnores(options.cwd);
1400
+ ignores = parsed.ignores;
1401
+ invalidIgnores = parsed.problems;
1402
+ } catch (error) {
1403
+ invalidIgnores.push(
1404
+ problem(
1405
+ `Could not use audit.ignore: ${message2(error)}`,
1406
+ "Fix or remove the invalid acknowledgement. The advisory remains unacknowledged.",
1407
+ { rule: "audit/invalid-ignore", file: "zappie.config.ts", line: 1 }
1408
+ )
1409
+ );
1410
+ }
1411
+ if (options.packageManager === "yarn" || options.packageManager === "bun") {
1412
+ return withProblems(
1413
+ unavailable(
1414
+ mode,
1415
+ `Could not audit dependencies: ${options.packageManager} is not supported.`,
1416
+ "Use npm or pnpm for dependency auditing, or run with --audit=off.",
1417
+ "package.json"
1418
+ ),
1419
+ invalidIgnores,
1420
+ mode
1421
+ );
1422
+ }
1423
+ const lockfile2 = await findLockfile(options.cwd, options.packageManager);
1424
+ if (!lockfile2) {
1425
+ return withProblems(
1426
+ unavailable(
1427
+ mode,
1428
+ "Could not audit dependencies: no lockfile was found.",
1429
+ "Commit the package manager lockfile and run zappie check again.",
1430
+ "package.json"
1431
+ ),
1432
+ invalidIgnores,
1433
+ mode
1434
+ );
1435
+ }
1436
+ const invocation = auditCommand(options.packageManager);
1437
+ const timeoutMs = dependencies.timeoutMs ?? AUDIT_TIMEOUT_MS;
1438
+ const timeout = AbortSignal.timeout(timeoutMs);
1439
+ const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
1440
+ let result;
1441
+ try {
1442
+ result = await (dependencies.run ?? command)(
1443
+ invocation.executable,
1444
+ invocation.args,
1445
+ options.cwd,
1446
+ signal
1447
+ );
1448
+ } catch (error) {
1449
+ options.signal?.throwIfAborted();
1450
+ const detail = timeout.aborted ? `the audit timed out after ${Math.ceil(timeoutMs / 1e3)} seconds` : message2(error);
1451
+ return withProblems(
1452
+ unavailable(
1453
+ mode,
1454
+ `Could not audit dependencies: ${detail}.`,
1455
+ "Check registry access and run zappie check again.",
1456
+ relativeLockfile(options.cwd, lockfile2)
1457
+ ),
1458
+ invalidIgnores,
1459
+ mode
1460
+ );
1461
+ }
1462
+ options.signal?.throwIfAborted();
1463
+ let advisories;
1464
+ try {
1465
+ advisories = parseAuditOutput(options.packageManager, result.stdout);
1466
+ if (result.code > 1) {
1467
+ throw new Error(`the audit command exited with status ${result.code}`);
1468
+ }
1469
+ } catch (error) {
1470
+ return withProblems(
1471
+ unavailable(
1472
+ mode,
1473
+ `Could not audit dependencies: ${message2(error)}.`,
1474
+ "Check registry access and run zappie check again.",
1475
+ relativeLockfile(options.cwd, lockfile2)
1476
+ ),
1477
+ invalidIgnores,
1478
+ mode
1479
+ );
1480
+ }
1481
+ return withProblems(
1482
+ evaluateAudit(
1483
+ advisories,
1484
+ mode,
1485
+ ignores,
1486
+ relativeLockfile(options.cwd, lockfile2)
1487
+ ),
1488
+ invalidIgnores,
1489
+ mode
1490
+ );
1491
+ }
1492
+ function evaluateAudit(advisories, mode, ignores = [], file) {
1493
+ const problems = [];
1494
+ const applied = advisories.map((advisory) => {
1495
+ const acknowledged = ignores.find(
1496
+ ({ id }) => normalizedId(id) === normalizedId(advisory.id)
1497
+ );
1498
+ if (acknowledged && malicious(advisory.id)) {
1499
+ problems.push(
1500
+ problem(
1501
+ `${advisory.id} is a malicious-package advisory and cannot be acknowledged.`,
1502
+ `Remove ${advisory.package} and replace it with a trusted package or release.`,
1503
+ {
1504
+ rule: "audit/invalid-ignore",
1505
+ advisory,
1506
+ ...file ? { file, line: 1 } : {}
1507
+ }
1508
+ )
1509
+ );
1510
+ return advisory;
1511
+ }
1512
+ if (acknowledged && (advisory.fixedVersion || advisory.upgradeVersion || advisory.fixCommand)) {
1513
+ const upgradePackage = advisory.upgradePackage ?? advisory.package;
1514
+ const upgradeVersion = advisory.upgradeVersion ?? advisory.fixedVersion;
1515
+ const fix = advisory.fixCommand ? `Run ${advisory.fixCommand}.` : `Upgrade ${upgradePackage} to ${upgradeVersion} or later.`;
1516
+ const reason = advisory.fixCommand ? `${advisory.id} has an available fix and cannot be acknowledged.` : `${advisory.id} has a fixed version and cannot be acknowledged.`;
1517
+ problems.push(
1518
+ problem(reason, fix, {
1519
+ rule: "audit/invalid-ignore",
1520
+ advisory,
1521
+ ...file ? { file, line: 1 } : {}
1522
+ })
1523
+ );
1524
+ return advisory;
1525
+ }
1526
+ return acknowledged ? { ...advisory, ignored: true, ignoredUntil: acknowledged.until } : advisory;
1527
+ });
1528
+ problems.push(...applied.map((advisory) => advisoryProblem(advisory, file)));
1529
+ const critical = applied.some(
1530
+ ({ severity, ignored }) => severity === "critical" && !ignored
1531
+ );
1532
+ const invalidIgnore2 = problems.some(
1533
+ ({ rule }) => rule === "audit/invalid-ignore"
1534
+ );
1535
+ return step(
1536
+ "audit",
1537
+ mode === "enforce" && (critical || invalidIgnore2) ? "failed" : "passed",
1538
+ problems
1539
+ );
1540
+ }
1541
+ function unavailable(mode, message6, fix, file) {
1542
+ return step("audit", mode === "enforce" ? "failed" : "passed", [
1543
+ problem(message6, fix, { rule: "audit/unavailable", file, line: 1 })
1544
+ ]);
1545
+ }
1546
+ function withProblems(checked, extra, mode) {
1547
+ if (extra.length === 0) {
1548
+ return checked;
1549
+ }
1550
+ return step("audit", mode === "enforce" ? "failed" : checked.result, [
1551
+ ...extra,
1552
+ ...checked.problems
1553
+ ]);
1554
+ }
1555
+ async function findLockfile(cwd, manager) {
1556
+ const names = manager === "npm" ? ["package-lock.json", "npm-shrinkwrap.json"] : ["pnpm-lock.yaml"];
1557
+ let directory = path4.resolve(cwd);
1558
+ for (; ; ) {
1559
+ if (directory !== path4.resolve(cwd) && manager === "pnpm" && await workspaceExcludes(directory, path4.resolve(cwd))) {
1560
+ return null;
1561
+ }
1562
+ for (const name of names) {
1563
+ const candidate = path4.join(directory, name);
1564
+ try {
1565
+ await access2(candidate);
1566
+ return candidate;
1567
+ } catch {
1568
+ }
1569
+ }
1570
+ const parent = path4.dirname(directory);
1571
+ if (parent === directory) {
1572
+ return null;
1573
+ }
1574
+ directory = parent;
1575
+ }
1576
+ }
1577
+ async function workspaceExcludes(directory, cwd) {
1578
+ try {
1579
+ const source = await readFile4(
1580
+ path4.join(directory, "pnpm-workspace.yaml"),
1581
+ "utf8"
1582
+ );
1583
+ const block = /^packages:\s*\n((?:[ \t]+.*(?:\n|$))*)/m.exec(source)?.[1];
1584
+ if (!block) {
1585
+ return false;
1586
+ }
1587
+ const relative2 = path4.relative(directory, cwd).replaceAll(path4.sep, "/");
1588
+ const patterns = [
1589
+ ...block.matchAll(/^\s*-\s*["']?([^"'\s]+)["']?\s*$/gm)
1590
+ ].map((match) => match[1] ?? "");
1591
+ return !patterns.some((pattern) => workspacePattern(pattern, relative2));
1592
+ } catch {
1593
+ return false;
1594
+ }
1595
+ }
1596
+ function workspacePattern(pattern, relative2) {
1597
+ if (pattern === ".") {
1598
+ return relative2 === "";
1599
+ }
1600
+ const expression = pattern.split("*").map((part) => part.replace(/[.*+?^$()|[\]\\]/g, "\\$&")).join("[^/]+");
1601
+ return new RegExp(`^${expression}$`).test(relative2);
1602
+ }
1603
+ function relativeLockfile(cwd, lockfile2) {
1604
+ return path4.relative(cwd, lockfile2).replaceAll(path4.sep, "/");
1605
+ }
1606
+ function auditCommand(manager) {
1607
+ return manager === "pnpm" ? { executable: "pnpm", args: ["audit", "--json"] } : { executable: "npm", args: ["audit", "--json"] };
1608
+ }
1609
+ function parseAuditOutput(manager, source) {
1610
+ const value = JSON.parse(source);
1611
+ requireAuditReport(manager, value);
1612
+ const parsed = manager === "npm" ? npmAdvisories(value) : pnpmAdvisories(value);
1613
+ return parsed.filter(({ severity }) => severity === "high" || severity === "critical").sort((left, right) => left.id.localeCompare(right.id));
1614
+ }
1615
+ function requireAuditReport(manager, value) {
1616
+ if (record3(value.error)) {
1617
+ throw new Error(
1618
+ string(value.error.message) ?? string(value.error.code) ?? string(value.message) ?? "the package manager returned an error"
1619
+ );
1620
+ }
1621
+ if (manager === "npm" && !record3(value.vulnerabilities)) {
1622
+ throw new Error("npm returned no vulnerability report");
1623
+ }
1624
+ if (manager === "pnpm" && !record3(value.advisories) && !record3(value.metadata)) {
1625
+ throw new Error("pnpm returned no advisory report");
1626
+ }
1627
+ }
1628
+ function pnpmAdvisories(value) {
1629
+ if (!record3(value.advisories)) {
1630
+ return [];
1631
+ }
1632
+ return Object.entries(value.advisories).flatMap(([key, raw]) => {
1633
+ if (!record3(raw)) {
1634
+ return [];
1635
+ }
1636
+ const findings = Array.isArray(raw.findings) ? raw.findings : [];
1637
+ const versions = findings.flatMap(
1638
+ (finding) => record3(finding) && typeof finding.version === "string" ? [finding.version] : []
1639
+ );
1640
+ const severity = normalizedSeverity(raw.severity);
1641
+ if (!severity) {
1642
+ return [];
1643
+ }
1644
+ return [
1645
+ {
1646
+ id: string(raw.github_advisory_id) ?? string(raw.id) ?? key,
1647
+ package: string(raw.module_name) ?? "unknown package",
1648
+ severity,
1649
+ vulnerableVersion: versions.join(", ") || string(raw.vulnerable_versions) || "unknown",
1650
+ fixedVersion: firstFixed(string(raw.patched_versions)),
1651
+ ignored: false
1652
+ }
1653
+ ];
1654
+ });
1655
+ }
1656
+ function npmAdvisories(value) {
1657
+ if (!record3(value.vulnerabilities)) {
1658
+ return [];
1659
+ }
1660
+ const advisories = /* @__PURE__ */ new Map();
1661
+ for (const [packageName2, raw] of Object.entries(value.vulnerabilities)) {
1662
+ if (!record3(raw) || !Array.isArray(raw.via)) {
1663
+ continue;
1664
+ }
1665
+ for (const via of raw.via) {
1666
+ if (!record3(via)) {
1667
+ continue;
1668
+ }
1669
+ const severity = normalizedSeverity(via.severity);
1670
+ if (!severity) {
1671
+ continue;
1672
+ }
1673
+ const id = ghsaFrom(string(via.url)) ?? string(via.name) ?? String(via.source ?? packageName2);
1674
+ const upgrade = npmUpgrade(raw.fixAvailable);
1675
+ advisories.set(id, {
1676
+ id,
1677
+ package: packageName2,
1678
+ severity,
1679
+ vulnerableVersion: string(via.range) ?? string(raw.range) ?? "unknown",
1680
+ fixedVersion: upgrade.upgradePackage === packageName2 ? upgrade.upgradeVersion ?? null : null,
1681
+ ...upgrade,
1682
+ ignored: false
1683
+ });
1684
+ }
1685
+ }
1686
+ return [...advisories.values()];
1687
+ }
1688
+ function advisoryProblem(advisory, file) {
1689
+ const acknowledged = advisory.ignored ? ` Acknowledged until ${advisory.ignoredUntil}.` : "";
1690
+ const upgradePackage = advisory.upgradePackage ?? advisory.package;
1691
+ const upgradeVersion = advisory.upgradeVersion ?? advisory.fixedVersion;
1692
+ 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.`;
1693
+ return problem(
1694
+ `${advisory.id}: ${advisory.package} ${advisory.vulnerableVersion} has ${advisory.severity} severity.${acknowledged}`,
1695
+ fix,
1696
+ {
1697
+ rule: `audit/${advisory.id}`,
1698
+ advisory,
1699
+ ...file ? { file, line: 1 } : {}
1700
+ }
1701
+ );
1702
+ }
1703
+ async function auditIgnores(cwd) {
1704
+ let loaded;
1705
+ try {
1706
+ loaded = await loadConfig(cwd);
1707
+ } catch (error) {
1708
+ if (missing(error)) {
1709
+ return { ignores: [], problems: [] };
1710
+ }
1711
+ throw error;
1712
+ }
1713
+ if (!record3(loaded.value) || !record3(loaded.value.audit)) {
1714
+ return { ignores: [], problems: [] };
1715
+ }
1716
+ const problems = [];
1717
+ const ignores = parseAuditIgnores(loaded.value.audit.ignore, problems);
1718
+ return { ignores, problems };
1719
+ }
1720
+ function parseAuditIgnores(value, problems = []) {
1721
+ if (value === void 0) {
1722
+ return [];
1723
+ }
1724
+ if (!Array.isArray(value)) {
1725
+ problems.push(invalidIgnore("audit.ignore must be an array."));
1726
+ return [];
1727
+ }
1728
+ const today = Date.parse(
1729
+ `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}T00:00:00.000Z`
1730
+ );
1731
+ const ninetyDays = 90 * 24 * 60 * 60 * 1e3;
1732
+ const ids = /* @__PURE__ */ new Set();
1733
+ const ignores = [];
1734
+ value.forEach((entry, index) => {
1735
+ if (!ignoreEntry(entry) || !entry.id.trim() || !entry.reason.trim()) {
1736
+ problems.push(
1737
+ invalidIgnore(
1738
+ `audit.ignore[${index}] must have a non-blank id, reason and until.`
1739
+ )
1740
+ );
1741
+ return;
1742
+ }
1743
+ const id = normalizedId(entry.id);
1744
+ if (ids.has(id)) {
1745
+ problems.push(
1746
+ invalidIgnore(`audit.ignore contains duplicate id ${entry.id}.`)
1747
+ );
1748
+ return;
1749
+ }
1750
+ const expiry = Date.parse(`${entry.until}T00:00:00.000Z`);
1751
+ 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) {
1752
+ problems.push(
1753
+ invalidIgnore(
1754
+ `audit.ignore[${index}].until must be a real, unexpired date at most 90 days away.`
1755
+ )
1756
+ );
1757
+ return;
1758
+ }
1759
+ ids.add(id);
1760
+ ignores.push(entry);
1761
+ });
1762
+ return ignores;
1763
+ }
1764
+ function invalidIgnore(message6) {
1765
+ return problem(
1766
+ message6,
1767
+ "Fix or remove the invalid acknowledgement. The advisory remains unacknowledged.",
1768
+ { rule: "audit/invalid-ignore", file: "zappie.config.ts", line: 1 }
1769
+ );
1770
+ }
1771
+ function ignoreEntry(value) {
1772
+ return record3(value) && typeof value.id === "string" && typeof value.reason === "string" && typeof value.until === "string";
1773
+ }
1774
+ function npmUpgrade(value) {
1775
+ if (record3(value) && typeof value.name === "string" && typeof value.version === "string") {
1776
+ return { upgradePackage: value.name, upgradeVersion: value.version };
1777
+ }
1778
+ if (value === true) {
1779
+ return { fixCommand: "npm audit fix" };
1780
+ }
1781
+ return {};
1782
+ }
1783
+ function firstFixed(value) {
1784
+ if (!value || value === "<0.0.0") {
1785
+ return null;
1786
+ }
1787
+ return /(?:^|\s)>=?\s*([^\s|]+)/.exec(value)?.[1] ?? null;
1788
+ }
1789
+ function ghsaFrom(value) {
1790
+ const id = value ? /GHSA-[\w-]+/i.exec(value)?.[0] : void 0;
1791
+ return id ? `GHSA-${id.slice(5).toLowerCase()}` : void 0;
1792
+ }
1793
+ function normalizedId(value) {
1794
+ return value.trim().toLowerCase();
1795
+ }
1796
+ function malicious(value) {
1797
+ return /^MAL-/i.test(value.trim());
1798
+ }
1799
+ function normalizedSeverity(value) {
1800
+ const severity = typeof value === "string" ? value.toLowerCase() : "";
1801
+ return severity === "high" || severity === "critical" ? severity : void 0;
1802
+ }
1803
+ function string(value) {
1804
+ return typeof value === "string" ? value : void 0;
1805
+ }
1806
+ function message2(error) {
1807
+ return error instanceof Error ? error.message : String(error);
1808
+ }
1809
+ function missing(error) {
1810
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
1811
+ }
1812
+ function record3(value) {
1813
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1814
+ }
1815
+
1321
1816
  // src/budgets.ts
1322
- import { access as access2, readFile as readFile6 } from "fs/promises";
1323
- import path6 from "path";
1817
+ import { access as access3, readFile as readFile7 } from "fs/promises";
1818
+ import path7 from "path";
1324
1819
 
1325
1820
  // src/budget-files.ts
1326
- import { readFile as readFile4, readdir } from "fs/promises";
1327
- import path4 from "path";
1821
+ import { readFile as readFile5, readdir } from "fs/promises";
1822
+ import path5 from "path";
1328
1823
  import { gzipSync } from "zlib";
1329
1824
  async function cssSizes(cwd) {
1330
- const root = path4.join(cwd, "build/client");
1825
+ const root = path5.join(cwd, "build/client");
1331
1826
  const files = [];
1332
1827
  await collect(root, files);
1333
1828
  return Promise.all(
1334
1829
  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
1830
+ file: path5.relative(root, file).split(path5.sep).join("/"),
1831
+ bytes: gzipSync(await readFile5(file, "utf8")).byteLength
1337
1832
  }))
1338
1833
  );
1339
1834
  }
@@ -1342,7 +1837,7 @@ async function collect(directory, files) {
1342
1837
  if (entry.isSymbolicLink()) {
1343
1838
  continue;
1344
1839
  }
1345
- const file = path4.join(directory, entry.name);
1840
+ const file = path5.join(directory, entry.name);
1346
1841
  if (entry.isDirectory()) {
1347
1842
  await collect(file, files);
1348
1843
  } else if (entry.isFile()) {
@@ -1352,8 +1847,8 @@ async function collect(directory, files) {
1352
1847
  }
1353
1848
 
1354
1849
  // src/budget-manifest.ts
1355
- import { readFile as readFile5 } from "fs/promises";
1356
- import path5 from "path";
1850
+ import { readFile as readFile6 } from "fs/promises";
1851
+ import path6 from "path";
1357
1852
  import { gzipSync as gzipSync2 } from "zlib";
1358
1853
  function routeKeys(manifest) {
1359
1854
  return Object.entries(manifest).filter(([key, entry]) => routeSource(entry.src ?? key)).map(([key]) => key).sort();
@@ -1367,7 +1862,7 @@ async function graphSizes(cwd, manifest, roots) {
1367
1862
  [...keys].map((key) => manifest[key]).filter((entry) => Boolean(entry)).filter(({ file }) => file.endsWith(".js")).map(async ({ file }) => ({
1368
1863
  file,
1369
1864
  bytes: gzipSync2(
1370
- await readFile5(path5.join(cwd, "build/client", file), "utf8")
1865
+ await readFile6(path6.join(cwd, "build/client", file), "utf8")
1371
1866
  ).byteLength
1372
1867
  }))
1373
1868
  );
@@ -1401,16 +1896,16 @@ async function checkBudgets({
1401
1896
  signal
1402
1897
  }) {
1403
1898
  signal?.throwIfAborted();
1404
- const manifestFile = path6.join(cwd, "build/client/.vite/manifest.json");
1899
+ const manifestFile = path7.join(cwd, "build/client/.vite/manifest.json");
1405
1900
  try {
1406
- await access2(manifestFile);
1901
+ await access3(manifestFile);
1407
1902
  } catch {
1408
1903
  return skipped(
1409
1904
  "budgets",
1410
1905
  "No build/client/.vite/manifest.json is available."
1411
1906
  );
1412
1907
  }
1413
- const manifest = JSON.parse(await readFile6(manifestFile, "utf8"));
1908
+ const manifest = JSON.parse(await readFile7(manifestFile, "utf8"));
1414
1909
  const config = (await loadConfig(cwd)).value;
1415
1910
  const limits = { ...DEFAULT_BUDGETS, ...config.budgets };
1416
1911
  const routes = routeKeys(manifest);
@@ -1465,56 +1960,8 @@ function cssProblem(sizes, limitKb, over) {
1465
1960
  var total = (sizes) => sizes.reduce((sum, asset) => sum + asset.bytes, 0);
1466
1961
 
1467
1962
  // 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
1963
+ import { readFile as readFile8 } from "fs/promises";
1964
+ import path8 from "path";
1518
1965
  async function checkBuild({
1519
1966
  cwd,
1520
1967
  packageManager,
@@ -1522,7 +1969,7 @@ async function checkBuild({
1522
1969
  }) {
1523
1970
  signal?.throwIfAborted();
1524
1971
  const manifest = JSON.parse(
1525
- await readFile7(path7.join(cwd, "package.json"), "utf8")
1972
+ await readFile8(path8.join(cwd, "package.json"), "utf8")
1526
1973
  );
1527
1974
  const invocation = manifest.dependencies?.["react-router"] ? execArgs(packageManager, "react-router", ["build"]) : manifest.scripts?.build ? scriptArgs(packageManager, "build") : void 0;
1528
1975
  if (!invocation) {
@@ -1557,8 +2004,8 @@ ${stderr}`.split(/\r?\n/).map((line) => line.trimEnd()).filter(Boolean).slice(-1
1557
2004
  }
1558
2005
 
1559
2006
  // src/css-files.ts
1560
- import { lstat, readFile as readFile8, readdir as readdir2 } from "fs/promises";
1561
- import path8 from "path";
2007
+ import { lstat, readFile as readFile9, readdir as readdir2 } from "fs/promises";
2008
+ import path9 from "path";
1562
2009
  var FILE_SIZE_CAP = 1024 * 1024;
1563
2010
  var BUILD_FILE_SIZE_CAP = 8 * 1024 * 1024;
1564
2011
  var ignoredDirectories = /* @__PURE__ */ new Set([
@@ -1582,7 +2029,7 @@ async function readCssInputs(cwd) {
1582
2029
  return !name.startsWith(".") && !ignoredDirectories.has(name);
1583
2030
  });
1584
2031
  const built = [];
1585
- await collect2(path8.join(cwd, "build/client"), built, () => true);
2032
+ await collect2(path9.join(cwd, "build/client"), built, () => true);
1586
2033
  const read = (files, cap) => texts(cwd, files, cap);
1587
2034
  return {
1588
2035
  sourceCss: await read(
@@ -1614,7 +2061,7 @@ async function texts(cwd, files, cap) {
1614
2061
  }
1615
2062
  result.push({
1616
2063
  file: relative(cwd, file),
1617
- text: await readFile8(file, "utf8")
2064
+ text: await readFile9(file, "utf8")
1618
2065
  });
1619
2066
  }
1620
2067
  return result;
@@ -1633,7 +2080,7 @@ async function collect2(directory, files, enter) {
1633
2080
  if (entry.isSymbolicLink()) {
1634
2081
  continue;
1635
2082
  }
1636
- const file = path8.join(directory, entry.name);
2083
+ const file = path9.join(directory, entry.name);
1637
2084
  if (entry.isDirectory()) {
1638
2085
  if (enter(entry.name)) {
1639
2086
  await collect2(file, files, enter);
@@ -1747,9 +2194,9 @@ function important(css, code) {
1747
2194
  }
1748
2195
  return problems;
1749
2196
  }
1750
- function importantProblem(message5, file, line) {
2197
+ function importantProblem(message6, file, line) {
1751
2198
  return problem(
1752
- message5,
2199
+ message6,
1753
2200
  "Raise the selector's specificity or reorder the rules instead. For a reviewed case, add /* zappie-allow-important: <reason> */ on the same line.",
1754
2201
  { rule: "css/important", file, ...line ? { line } : {} }
1755
2202
  );
@@ -2333,7 +2780,7 @@ async function checkCss({
2333
2780
  // src/lint.ts
2334
2781
  import zappie from "eslint-plugin-zappie";
2335
2782
  import { createRequire } from "module";
2336
- import path9 from "path";
2783
+ import path10 from "path";
2337
2784
  import { pathToFileURL as pathToFileURL2 } from "url";
2338
2785
  async function checkLint({
2339
2786
  cwd,
@@ -2342,18 +2789,23 @@ async function checkLint({
2342
2789
  signal?.throwIfAborted();
2343
2790
  let ESLint;
2344
2791
  try {
2345
- const require2 = createRequire(path9.join(cwd, "package.json"));
2792
+ const require2 = createRequire(path10.join(cwd, "package.json"));
2346
2793
  const loadedEslint = await import(pathToFileURL2(require2.resolve("eslint")).href);
2347
2794
  ESLint = loadedEslint.ESLint;
2348
2795
  } catch (error) {
2349
2796
  return step("lint", "failed", [
2350
- problem(message2(error), "Install ESLint in the project.", {
2797
+ problem(message3(error), "Install ESLint in the project.", {
2351
2798
  rule: "lint/unavailable"
2352
2799
  })
2353
2800
  ]);
2354
2801
  }
2355
2802
  try {
2356
- const eslint = new ESLint({ cwd });
2803
+ const eslint = new ESLint({
2804
+ cwd,
2805
+ overrideConfig: {
2806
+ settings: { zappie: { outboundAllow: await outboundAllow(cwd) } }
2807
+ }
2808
+ });
2357
2809
  const source = await sourceFile(cwd);
2358
2810
  const config = await eslint.calculateConfigForFile(source);
2359
2811
  const problems = recommendedProblems(config?.rules ?? {});
@@ -2378,7 +2830,7 @@ async function checkLint({
2378
2830
  } catch (error) {
2379
2831
  return step("lint", "failed", [
2380
2832
  problem(
2381
- message2(error),
2833
+ message3(error),
2382
2834
  "Fix the project ESLint configuration and run lint again.",
2383
2835
  {
2384
2836
  rule: "lint/config"
@@ -2387,6 +2839,23 @@ async function checkLint({
2387
2839
  ]);
2388
2840
  }
2389
2841
  }
2842
+ async function outboundAllow(cwd) {
2843
+ try {
2844
+ const value = (await loadConfig(cwd)).value;
2845
+ if (!value || typeof value !== "object" || !("outbound" in value)) {
2846
+ return [];
2847
+ }
2848
+ const outbound = value.outbound;
2849
+ if (!outbound || typeof outbound !== "object" || !("allow" in outbound)) {
2850
+ return [];
2851
+ }
2852
+ return Array.isArray(outbound.allow) ? outbound.allow.filter(
2853
+ (entry) => typeof entry === "string"
2854
+ ) : [];
2855
+ } catch {
2856
+ return [];
2857
+ }
2858
+ }
2390
2859
  function recommendedProblems(applied) {
2391
2860
  const expected = recommendedRules();
2392
2861
  return Object.entries(expected).flatMap(([rule, severity]) => {
@@ -2433,7 +2902,7 @@ async function sourceFile(cwd) {
2433
2902
  for (const candidate of candidates) {
2434
2903
  try {
2435
2904
  await import("fs/promises").then(
2436
- ({ access: access3 }) => access3(path9.join(cwd, candidate))
2905
+ ({ access: access4 }) => access4(path10.join(cwd, candidate))
2437
2906
  );
2438
2907
  return candidate;
2439
2908
  } catch {
@@ -2442,13 +2911,13 @@ async function sourceFile(cwd) {
2442
2911
  }
2443
2912
  return "eslint.config.mjs";
2444
2913
  }
2445
- function message2(error) {
2914
+ function message3(error) {
2446
2915
  return error instanceof Error ? error.message : String(error);
2447
2916
  }
2448
2917
 
2449
2918
  // src/readiness.ts
2450
- import { readFile as readFile9 } from "fs/promises";
2451
- import path10 from "path";
2919
+ import { readFile as readFile10 } from "fs/promises";
2920
+ import path11 from "path";
2452
2921
  import { pathToFileURL as pathToFileURL3 } from "url";
2453
2922
  async function checkReadiness({
2454
2923
  cwd,
@@ -2466,9 +2935,9 @@ async function checkReadiness({
2466
2935
  )
2467
2936
  ]);
2468
2937
  }
2469
- const file = path10.join(cwd, "react-router.config.ts");
2938
+ const file = path11.join(cwd, "react-router.config.ts");
2470
2939
  try {
2471
- await readFile9(file, "utf8");
2940
+ await readFile10(file, "utf8");
2472
2941
  const version = Date.now();
2473
2942
  const config = await import(`${pathToFileURL3(file).href}?readiness=${version}`);
2474
2943
  if (config.default?.routeDiscovery?.mode === "initial") {
@@ -2477,7 +2946,7 @@ async function checkReadiness({
2477
2946
  } catch (error) {
2478
2947
  return step("readiness", "failed", [
2479
2948
  problem(
2480
- message3(error),
2949
+ message4(error),
2481
2950
  'Create react-router.config.ts and set routeDiscovery: { mode: "initial" }.',
2482
2951
  {
2483
2952
  rule: "readiness/route-discovery",
@@ -2494,13 +2963,15 @@ async function checkReadiness({
2494
2963
  )
2495
2964
  ]);
2496
2965
  }
2497
- function message3(error) {
2966
+ function message4(error) {
2498
2967
  return error instanceof Error ? error.message : String(error);
2499
2968
  }
2500
2969
 
2501
2970
  // src/secrets.ts
2502
- import { lstat as lstat2, readFile as readFile10, readdir as readdir3 } from "fs/promises";
2503
- import path11 from "path";
2971
+ import { execFile } from "child_process";
2972
+ import { lstat as lstat2, readFile as readFile11, readdir as readdir3 } from "fs/promises";
2973
+ import path12 from "path";
2974
+ import { promisify } from "util";
2504
2975
 
2505
2976
  // src/secret-patterns.ts
2506
2977
  var FIX = "Move the value to a server environment variable and rotate it.";
@@ -2541,6 +3012,35 @@ function secretMatches(source, envFile) {
2541
3012
  fix: FIX,
2542
3013
  severity: "error"
2543
3014
  }));
3015
+ const gitleaksDefaults = [
3016
+ [/\bghp_[A-Za-z0-9]{36}\b/g, "github-pat", "GitHub personal access token"],
3017
+ [
3018
+ /\bgithub_pat_[A-Za-z0-9_]{82}\b/g,
3019
+ "github-fine-grained-pat",
3020
+ "GitHub fine-grained access token"
3021
+ ],
3022
+ [
3023
+ /\bglpat-[A-Za-z0-9_-]{20,}\b/g,
3024
+ "gitlab-pat",
3025
+ "GitLab personal access token"
3026
+ ],
3027
+ [/\bnpm_[A-Za-z0-9]{36}\b/g, "npm-token", "npm access token"],
3028
+ [/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, "slack-token", "Slack token"],
3029
+ [/\bAIza[0-9A-Za-z_-]{35}\b/g, "google-api-key", "Google API key"],
3030
+ [
3031
+ /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g,
3032
+ "sendgrid-api-key",
3033
+ "SendGrid API key"
3034
+ ]
3035
+ ];
3036
+ for (const [pattern, rule, label] of gitleaksDefaults) {
3037
+ add(matches2, source, pattern, () => ({
3038
+ rule: `secrets/${rule}`,
3039
+ message: `Detected a ${label}.`,
3040
+ fix: FIX,
3041
+ severity: "error"
3042
+ }));
3043
+ }
2544
3044
  const occupied = matches2.map(({ index }) => index);
2545
3045
  const assignment = /\b([A-Za-z_$][\w$]*(?:secret|token|password|api[_-]?key)[\w$]*)\s*[:=]\s*(["'])([^"'\r\n]{24,})\2/gi;
2546
3046
  for (const found of source.matchAll(assignment)) {
@@ -2578,21 +3078,31 @@ function entropy(value) {
2578
3078
 
2579
3079
  // src/secrets.ts
2580
3080
  var SECRET_FILE_SIZE_CAP = 1024 * 1024;
2581
- async function scanSecrets(dir) {
3081
+ var execFileAsync = promisify(execFile);
3082
+ var lockfile = /(?:^|\/)(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?)$/;
3083
+ async function scanSecrets(dir, options = {}) {
2582
3084
  const findings = [];
2583
3085
  for (const file of await filesToScan(dir)) {
2584
- const info = await lstat2(file);
3086
+ let info;
3087
+ try {
3088
+ info = await lstat2(file);
3089
+ } catch (error) {
3090
+ if (missing2(error)) {
3091
+ continue;
3092
+ }
3093
+ throw error;
3094
+ }
2585
3095
  if (!info.isFile() || info.size > SECRET_FILE_SIZE_CAP) {
2586
3096
  continue;
2587
3097
  }
2588
- const contents = await readFile10(file);
3098
+ const contents = await readFile11(file);
2589
3099
  if (contents.includes(0)) {
2590
3100
  continue;
2591
3101
  }
2592
3102
  const source = contents.toString("utf8");
2593
3103
  for (const match of secretMatches(
2594
3104
  source,
2595
- path11.basename(file).startsWith(".env")
3105
+ path12.basename(file).startsWith(".env")
2596
3106
  )) {
2597
3107
  findings.push({
2598
3108
  rule: match.rule,
@@ -2604,14 +3114,21 @@ async function scanSecrets(dir) {
2604
3114
  });
2605
3115
  }
2606
3116
  }
3117
+ if (options.history) {
3118
+ findings.push(...await scanHistory(dir));
3119
+ }
2607
3120
  return findings;
2608
3121
  }
2609
3122
  async function checkSecrets({
2610
3123
  cwd,
3124
+ history,
2611
3125
  signal
2612
3126
  }) {
2613
3127
  signal?.throwIfAborted();
2614
- const findings = await scanSecrets(cwd);
3128
+ const findings = await scanSecrets(
3129
+ cwd,
3130
+ history === void 0 ? {} : { history }
3131
+ );
2615
3132
  signal?.throwIfAborted();
2616
3133
  const failed = findings.some(({ severity }) => severity === "error");
2617
3134
  return step(
@@ -2621,25 +3138,93 @@ async function checkSecrets({
2621
3138
  );
2622
3139
  }
2623
3140
  async function filesToScan(cwd) {
3141
+ const files = await trackedFiles(cwd);
3142
+ for (const directory of ["build/client", "build/server", "workers"]) {
3143
+ await collect3(path12.join(cwd, directory), files);
3144
+ }
3145
+ return [...new Set(files)].filter((file) => !lockfile.test(relative(cwd, file))).sort();
3146
+ }
3147
+ async function trackedFiles(cwd) {
3148
+ try {
3149
+ const { stdout } = await execFileAsync("git", ["ls-files", "-z"], {
3150
+ cwd,
3151
+ encoding: "utf8",
3152
+ maxBuffer: 32 * 1024 * 1024
3153
+ });
3154
+ const files = stdout.split("\0").filter(Boolean).map((file) => path12.join(cwd, file));
3155
+ return files.length > 0 ? files : fallbackFiles(cwd);
3156
+ } catch {
3157
+ return fallbackFiles(cwd);
3158
+ }
3159
+ }
3160
+ async function fallbackFiles(cwd) {
2624
3161
  const files = [];
2625
3162
  for (const directory of ["app", "public"]) {
2626
- await collect3(path11.join(cwd, directory), files);
3163
+ await collect3(path12.join(cwd, directory), files);
2627
3164
  }
2628
- await collect3(
2629
- path11.join(cwd, "build/client"),
2630
- files,
2631
- (file) => file.endsWith(".js")
2632
- );
2633
3165
  try {
2634
3166
  for (const entry of await readdir3(cwd, { withFileTypes: true })) {
2635
3167
  if (entry.isFile() && rootConfig(entry.name)) {
2636
- files.push(path11.join(cwd, entry.name));
3168
+ files.push(path12.join(cwd, entry.name));
2637
3169
  }
2638
3170
  }
2639
3171
  } catch {
2640
- return files.sort();
3172
+ return files;
3173
+ }
3174
+ return files;
3175
+ }
3176
+ async function scanHistory(cwd) {
3177
+ try {
3178
+ const { stdout } = await execFileAsync(
3179
+ "git",
3180
+ [
3181
+ "log",
3182
+ "--all",
3183
+ "--format=commit:%H",
3184
+ "-p",
3185
+ "--no-ext-diff",
3186
+ "--",
3187
+ ".",
3188
+ ":(exclude)package-lock.json",
3189
+ ":(exclude)pnpm-lock.yaml",
3190
+ ":(exclude)yarn.lock",
3191
+ ":(exclude)bun.lock",
3192
+ ":(exclude)bun.lockb"
3193
+ ],
3194
+ { cwd, encoding: "utf8", maxBuffer: 128 * 1024 * 1024 }
3195
+ );
3196
+ return secretMatches(stdout, false).map((match) => {
3197
+ const location2 = historyLocation(stdout, match.index);
3198
+ return {
3199
+ rule: match.rule,
3200
+ file: location2 ?? "git history",
3201
+ line: lineAt(stdout, match.index),
3202
+ message: match.message,
3203
+ fix: match.fix,
3204
+ severity: match.severity
3205
+ };
3206
+ });
3207
+ } catch {
3208
+ return [
3209
+ {
3210
+ rule: "secrets/history-unavailable",
3211
+ file: "git history",
3212
+ line: 1,
3213
+ message: "Could not scan Git history for secrets.",
3214
+ fix: "Check Git access and run zappie check --history again.",
3215
+ severity: "warning"
3216
+ }
3217
+ ];
2641
3218
  }
2642
- return [...new Set(files)].sort();
3219
+ }
3220
+ function historyLocation(source, index) {
3221
+ const prefix = source.slice(0, index);
3222
+ const commit = [...prefix.matchAll(/^commit:([0-9a-f]+)$/gm)].at(-1)?.[1];
3223
+ const changedPaths = [
3224
+ ...prefix.matchAll(/^(?:\+\+\+ b\/|--- a\/)(.+)$/gm)
3225
+ ].map((match) => match[1]);
3226
+ const file = changedPaths.at(-1);
3227
+ return commit && file ? `${commit}:${file}` : void 0;
2643
3228
  }
2644
3229
  async function collect3(directory, files, accept = () => true) {
2645
3230
  try {
@@ -2647,7 +3232,7 @@ async function collect3(directory, files, accept = () => true) {
2647
3232
  if (entry.isSymbolicLink()) {
2648
3233
  continue;
2649
3234
  }
2650
- const file = path11.join(directory, entry.name);
3235
+ const file = path12.join(directory, entry.name);
2651
3236
  if (entry.isDirectory()) {
2652
3237
  await collect3(file, files, accept);
2653
3238
  } else if (entry.isFile() && accept(file)) {
@@ -2655,11 +3240,14 @@ async function collect3(directory, files, accept = () => true) {
2655
3240
  }
2656
3241
  }
2657
3242
  } catch (error) {
2658
- if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
3243
+ if (!missing2(error)) {
2659
3244
  throw error;
2660
3245
  }
2661
3246
  }
2662
3247
  }
3248
+ function missing2(error) {
3249
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
3250
+ }
2663
3251
  function rootConfig(name) {
2664
3252
  return name === "package.json" || name === "wrangler.json" || name === "wrangler.jsonc" || /(?:^|\.)config\.(?:[cm]?[jt]s|json)$/.test(name);
2665
3253
  }
@@ -2668,13 +3256,13 @@ function lineAt(source, index) {
2668
3256
  }
2669
3257
 
2670
3258
  // src/test-step.ts
2671
- import path12 from "path";
3259
+ import path13 from "path";
2672
3260
  async function checkTests({
2673
3261
  cwd,
2674
3262
  packageManager,
2675
3263
  signal
2676
3264
  }) {
2677
- const manifest = await json(path12.join(cwd, "package.json"));
3265
+ const manifest = await json(path13.join(cwd, "package.json"));
2678
3266
  if (!manifest.scripts?.test) {
2679
3267
  return skipped("test", "No test script is defined in package.json.");
2680
3268
  }
@@ -2697,11 +3285,11 @@ async function checkTests({
2697
3285
  }
2698
3286
 
2699
3287
  // src/types-step.ts
2700
- import path13 from "path";
3288
+ import path14 from "path";
2701
3289
  async function checkTypes(options) {
2702
3290
  const { cwd, packageManager, signal } = options;
2703
3291
  signal?.throwIfAborted();
2704
- const manifest = await json(path13.join(cwd, "package.json"));
3292
+ const manifest = await json(path14.join(cwd, "package.json"));
2705
3293
  if (manifest.dependencies?.["react-router"]) {
2706
3294
  const generated = execArgs(packageManager, "react-router", ["typegen"]);
2707
3295
  const result2 = await command(
@@ -2755,6 +3343,7 @@ function outputProblems(output) {
2755
3343
  var runners = [
2756
3344
  { name: "config", run: checkConfig },
2757
3345
  { name: "allowlist", run: checkAllowlist },
3346
+ { name: "audit", run: checkAudit },
2758
3347
  { name: "lint", run: checkLint },
2759
3348
  { name: "types", run: checkTypes },
2760
3349
  { name: "test", run: checkTests },
@@ -2775,13 +3364,13 @@ async function runCheck(options) {
2775
3364
  options.signal?.throwIfAborted();
2776
3365
  checked = step(runner.name, "failed", [
2777
3366
  problem(
2778
- message4(error),
3367
+ message5(error),
2779
3368
  "Fix the step setup and run zappie check again."
2780
3369
  )
2781
3370
  ]);
2782
3371
  }
2783
3372
  steps.push(checked);
2784
- if (checked.result === "failed" && !options.all) {
3373
+ if (checked.result === "failed" && !options.all && runner.name !== "audit") {
2785
3374
  break;
2786
3375
  }
2787
3376
  }
@@ -2790,7 +3379,7 @@ async function runCheck(options) {
2790
3379
  steps
2791
3380
  };
2792
3381
  }
2793
- function message4(error) {
3382
+ function message5(error) {
2794
3383
  return error instanceof Error ? error.message : String(error);
2795
3384
  }
2796
3385
  export {
@@ -2799,6 +3388,7 @@ export {
2799
3388
  SECRET_FILE_SIZE_CAP,
2800
3389
  analyseCss,
2801
3390
  checkAllowlist,
3391
+ checkAudit,
2802
3392
  checkBudgets,
2803
3393
  checkBuild,
2804
3394
  checkConfig,
@@ -2808,7 +3398,9 @@ export {
2808
3398
  checkSecrets,
2809
3399
  checkTests,
2810
3400
  checkTypes,
3401
+ evaluateAudit,
2811
3402
  loadConfig,
3403
+ parseAuditOutput,
2812
3404
  runCheck,
2813
3405
  scanSecrets,
2814
3406
  supportsTypeScriptImport