macwarden 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +20 -6
  3. package/dist/bin.js +7 -1
  4. package/dist/bounded-file.js +1 -0
  5. package/dist/cancellation.js +1 -1
  6. package/dist/caveat-address.js +1 -1
  7. package/dist/cli.js +1 -1
  8. package/dist/commands/attenuate.js +1 -1
  9. package/dist/commands/bake/allocate.js +1 -1
  10. package/dist/commands/bake/index.js +1 -1
  11. package/dist/commands/bake/write-out.js +1 -1
  12. package/dist/commands/handlers.js +1 -1
  13. package/dist/commands/inspect.js +1 -1
  14. package/dist/commands/inventory.js +1 -1
  15. package/dist/commands/recommend.js +1 -1
  16. package/dist/commands/respond.js +1 -1
  17. package/dist/commands/revoke.js +1 -1
  18. package/dist/commands/rotate.js +1 -1
  19. package/dist/commands/scan.js +1 -1
  20. package/dist/commands/verify.js +1 -0
  21. package/dist/commands/watch.js +1 -1
  22. package/dist/credential-recognition.js +1 -1
  23. package/dist/decode/index.js +1 -1
  24. package/dist/decode/recognition.js +1 -1
  25. package/dist/environment.js +1 -1
  26. package/dist/errors.js +1 -1
  27. package/dist/grade/index.js +1 -1
  28. package/dist/grouped-runs.js +1 -1
  29. package/dist/input/index.js +1 -1
  30. package/dist/input/lndconnect.js +1 -1
  31. package/dist/lnd/grpc.js +1 -1
  32. package/dist/lnd/index.js +1 -1
  33. package/dist/lnd/middleware-proto.js +1 -1
  34. package/dist/lnd/tls-identity.js +1 -0
  35. package/dist/online.js +1 -1
  36. package/dist/output/index.js +1 -1
  37. package/dist/output/scan-formats.js +1 -1
  38. package/dist/process-error-context.js +1 -1
  39. package/dist/protected-path.js +1 -1
  40. package/dist/registry/index.js +1 -1
  41. package/dist/registry/process-domain.js +1 -1
  42. package/dist/report/access-review.js +1 -1
  43. package/dist/runtime.js +1784 -617
  44. package/dist/scan/agent-configs.js +1 -1
  45. package/dist/scan/git-history.js +1 -1
  46. package/dist/scan/ignore.js +1 -1
  47. package/dist/scan/index.js +1 -1
  48. package/dist/scan/mcp-context.js +1 -1
  49. package/dist/scan/nwc-match.js +1 -1
  50. package/dist/scan/patterns.js +1 -1
  51. package/dist/scan/redact.js +1 -1
  52. package/dist/secret.js +1 -1
  53. package/dist/version.js +1 -1
  54. package/dist/watch/alert-throttle.js +1 -1
  55. package/dist/watch/alert.js +1 -1
  56. package/dist/watch/canary.js +1 -1
  57. package/dist/watch/output-queue.js +1 -1
  58. package/dist/watch/usage.js +1 -1
  59. package/package.json +2 -2
  60. package/schemas/output/attenuate.schema.json +2 -0
  61. package/schemas/output/inspect.schema.json +2 -0
  62. package/schemas/output/inventory.schema.json +35 -1
  63. package/schemas/output/recommend.schema.json +471 -0
  64. package/schemas/output/scan.schema.json +2 -0
  65. package/schemas/output/verify.schema.json +552 -0
  66. package/schemas/output/watch-event.schema.json +232 -0
  67. package/schemas/output/watch.schema.json +377 -0
package/dist/runtime.js CHANGED
@@ -1,6 +1,6 @@
1
- import * as builtin0 from "node:async_hooks";
2
- import * as builtin1 from "node:fs";
3
- import * as builtin2 from "node:fs/promises";
1
+ import * as builtin0 from "node:fs";
2
+ import * as builtin1 from "node:fs/promises";
3
+ import * as builtin2 from "node:async_hooks";
4
4
  import * as builtin3 from "node:path";
5
5
  import * as builtin4 from "node:stream";
6
6
  import * as builtin5 from "node:crypto";
@@ -10,8 +10,106 @@ import * as builtin8 from "node:readline";
10
10
  import * as builtin9 from "node:util";
11
11
  import * as builtin10 from "node:http2";
12
12
  import * as builtin11 from "node:https";
13
- const builtins = {"node:async_hooks": builtin0,"node:fs": builtin1,"node:fs/promises": builtin2,"node:path": builtin3,"node:stream": builtin4,"node:crypto": builtin5,"node:child_process": builtin6,"node:os": builtin7,"node:readline": builtin8,"node:util": builtin9,"node:http2": builtin10,"node:https": builtin11};
14
- const factories = {"cancellation.js": (module, exports, require) => {
13
+ const builtins = {"node:fs": builtin0,"node:fs/promises": builtin1,"node:async_hooks": builtin2,"node:path": builtin3,"node:stream": builtin4,"node:crypto": builtin5,"node:child_process": builtin6,"node:os": builtin7,"node:readline": builtin8,"node:util": builtin9,"node:http2": builtin10,"node:https": builtin11};
14
+ const factories = {"bounded-file.js": (module, exports, require) => {
15
+ "use strict";
16
+ exports.__esModule = true;
17
+ exports.LOCK_FILE_LIMIT = exports.DEFAULT_FILE_LIMIT = exports.BoundedFileError = void 0;
18
+ exports.readBounded = readBounded;
19
+ exports.readBoundedFile = readBoundedFile;
20
+ exports.assertPlainFile = assertPlainFile;
21
+ // Bounded regular-file reads for files macwarden reads by path: TLS certificates and the
22
+ // sidecars kept next to the registry. The path and its resolved target are checked against
23
+ // the protected names (SAFE-02) before anything is opened, the opened file must be the
24
+ // regular file that was checked, and at most `limit` bytes are accepted: a larger file fails
25
+ // before its content is parsed, and allocation follows the file's size, never the limit.
26
+ const node_fs_1 = require("node:fs");
27
+ const protected_path_js_1 = require("./protected-path.js");
28
+ /** Why a bounded read was refused before any content was read or accepted. */
29
+ class BoundedFileError extends Error {
30
+ constructor(message) {
31
+ super(message);
32
+ this.name = 'BoundedFileError';
33
+ }
34
+ }
35
+ exports.BoundedFileError = BoundedFileError;
36
+ exports.DEFAULT_FILE_LIMIT = 1_048_576;
37
+ /**
38
+ * Reads at most limit + 1 bytes from an open handle. The buffer starts at the size fstat
39
+ * reported plus one byte (to notice growth) and doubles only while the file keeps yielding
40
+ * data, so a small file never costs a limit-sized allocation.
41
+ */
42
+ async function readBounded(handle, limit, size) {
43
+ const hinted = typeof size === 'bigint' ? (size > BigInt(limit) ? limit : Number(size)) : Math.min(Math.max(size, 0), limit);
44
+ let bytes = Buffer.allocUnsafe(hinted + 1);
45
+ let total = 0;
46
+ for (;;) {
47
+ if (total === bytes.length) {
48
+ if (bytes.length >= limit + 1)
49
+ break;
50
+ const grown = Buffer.allocUnsafe(Math.min(limit + 1, bytes.length * 2));
51
+ bytes.copy(grown, 0, 0, total);
52
+ bytes = grown;
53
+ }
54
+ const { bytesRead } = await handle.read(bytes, total, bytes.length - total, total);
55
+ if (bytesRead === 0)
56
+ break;
57
+ total += bytesRead;
58
+ }
59
+ return bytes.subarray(0, total);
60
+ }
61
+ /**
62
+ * Reads a regular file of at most `limit` bytes. A missing path rejects with the file
63
+ * system's ENOENT error (so callers can treat an optional file as absent); a protected path
64
+ * or target, anything but a regular file, a file replaced between check and open, or one
65
+ * larger than `limit` rejects with BoundedFileError before any content is read.
66
+ */
67
+ async function readBoundedFile(path, io, limit = exports.DEFAULT_FILE_LIMIT) {
68
+ if ((0, protected_path_js_1.isProtectedPath)(path))
69
+ throw new BoundedFileError('protected file');
70
+ const resolved = await io.realpath(path);
71
+ if ((0, protected_path_js_1.isProtectedPath)(resolved))
72
+ throw new BoundedFileError('protected file');
73
+ const before = await io.stat(resolved);
74
+ if (!before.isFile())
75
+ throw new BoundedFileError('not a regular file');
76
+ if (before.size > limit)
77
+ throw new BoundedFileError('file too large');
78
+ const handle = await io.open(resolved, node_fs_1.constants.O_RDONLY | node_fs_1.constants.O_NONBLOCK | (node_fs_1.constants.O_NOFOLLOW ?? 0));
79
+ try {
80
+ const opened = await handle.stat();
81
+ if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino)
82
+ throw new BoundedFileError('file changed');
83
+ if (opened.size > limit)
84
+ throw new BoundedFileError('file too large');
85
+ const bytes = await readBounded(handle, limit, opened.size);
86
+ if (bytes.length > limit)
87
+ throw new BoundedFileError('file too large');
88
+ return bytes;
89
+ }
90
+ finally {
91
+ await handle.close();
92
+ }
93
+ }
94
+ /** Largest lock owner record read: a few fields (pid, host name, start time, process domain). */
95
+ exports.LOCK_FILE_LIMIT = 64 * 1024;
96
+ /**
97
+ * For files read by whole-file reads that must stay in place (lock owner records, whose
98
+ * reads are part of the lock protocol): the path itself must be a regular file, not a
99
+ * symlink, under no protected name, and at most `limit` bytes. Refuses before any open.
100
+ */
101
+ async function assertPlainFile(path, io, limit) {
102
+ if ((0, protected_path_js_1.isProtectedPath)(path))
103
+ throw new BoundedFileError('protected file');
104
+ const meta = await io.lstat(path);
105
+ if (!meta.isFile() || meta.isSymbolicLink())
106
+ throw new BoundedFileError('not a regular file');
107
+ if (meta.size > limit)
108
+ throw new BoundedFileError('file too large');
109
+ }
110
+
111
+ },
112
+ "cancellation.js": (module, exports, require) => {
15
113
  "use strict";
16
114
  exports.__esModule = true;
17
115
  exports.Cancelled = void 0;
@@ -209,6 +307,8 @@ const synopses = {
209
307
  recommend: [`recommend (<label> | <root-key-id> | id:<16 hex> | --target-file <file>) [--accept-short-observation] [--registry <path>] [--json]${online}`],
210
308
  respond: [`respond --out-dir <directory> [--allow-admin] [--yes] [--registry <path>] [--break-lock] [--dry-run] [--json]${online}`,
211
309
  `respond --finish [--yes] [--registry <path>] [--break-lock] [--dry-run] [--json]${online}`],
310
+ verify: [`verify <file | -> [--registry <path>] [--json]${online}`,
311
+ `verify <hex | base64 | lndconnect://...> --unsafe-arg [--registry <path>] [--json]${online}`],
212
312
  };
213
313
  const summaries = [
214
314
  ['inspect', 'decode a macaroon or NWC URI offline and grade what it can do'],
@@ -221,6 +321,7 @@ const summaries = [
221
321
  ['watch', 'observe which credential makes which call (read-only LND middleware); guard canaries'],
222
322
  ['recommend', 'suggest the least permissions a credential needs from observed usage'],
223
323
  ['respond', 'after a suspected leak: stage replacements for every credential, then --finish with proof'],
324
+ ['verify', 'ask LND whether one credential file is still accepted (after a leak)'],
224
325
  ];
225
326
  const usage = ['Usage: macwarden <command> [options]', ' macwarden <command> --help', ' macwarden --help | --version',
226
327
  '', 'Commands:', ...summaries.map(([name, summary]) => ` ${name.padEnd(10)} ${summary}`)].join('\n');
@@ -520,7 +621,7 @@ function byteSort(values) {
520
621
  return [...new Set(values)].sort((left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right)));
521
622
  }
522
623
  function result(parent, child, added, out, dryRun, now) {
523
- const assessed = (0, index_js_2.grade)(child.permissions);
624
+ const assessed = (0, index_js_2.gradeFacts)(child);
524
625
  return {
525
626
  dryRun,
526
627
  out,
@@ -1331,7 +1432,8 @@ const attenuate_js_1 = require("./attenuate.js");
1331
1432
  const watch_js_1 = require("./watch.js");
1332
1433
  const recommend_js_1 = require("./recommend.js");
1333
1434
  const respond_js_1 = require("./respond.js");
1334
- exports.handlers = { inspect: inspect_js_1.inspect, scan: scan_js_1.run, inventory: inventory_js_1.inventory, bake: index_js_1.bake, revoke: revoke_js_1.revoke, rotate: rotate_js_1.rotate, attenuate: attenuate_js_1.attenuate, watch: watch_js_1.watch, recommend: recommend_js_1.recommend, respond: respond_js_1.respond };
1435
+ const verify_js_1 = require("./verify.js");
1436
+ exports.handlers = { inspect: inspect_js_1.inspect, scan: scan_js_1.run, inventory: inventory_js_1.inventory, bake: index_js_1.bake, revoke: revoke_js_1.revoke, rotate: rotate_js_1.rotate, attenuate: attenuate_js_1.attenuate, watch: watch_js_1.watch, recommend: recommend_js_1.recommend, respond: respond_js_1.respond, verify: verify_js_1.verify };
1335
1437
 
1336
1438
  },
1337
1439
  "commands/inspect.js": (module, exports, require) => {
@@ -1370,6 +1472,7 @@ var __importStar = (this && this.__importStar) || (function () {
1370
1472
  };
1371
1473
  })();
1372
1474
  exports.__esModule = true;
1475
+ exports.inspectResult = inspectResult;
1373
1476
  exports.run = run;
1374
1477
  exports.inspect = inspect;
1375
1478
  const fs = __importStar(require("node:fs/promises"));
@@ -1394,6 +1497,22 @@ function datedCaveat(caveat, now) {
1394
1497
  const expired = expiration(caveat.value, now);
1395
1498
  return expired === undefined ? caveat : { ...caveat, expired };
1396
1499
  }
1500
+ /** The offline facts inspect reports for one macaroon; verify reports the same. */
1501
+ function inspectResult(facts, now) {
1502
+ const assessed = (0, index_js_2.gradeFacts)(facts);
1503
+ return {
1504
+ fingerprint: facts.fingerprint,
1505
+ location: facts.location,
1506
+ rootKeyId: facts.rootKeyId.toString(),
1507
+ permissions: facts.permissions,
1508
+ caveats: facts.caveats.map(caveat => datedCaveat(caveat, now)),
1509
+ grade: assessed.grade,
1510
+ reasons: assessed.reasons,
1511
+ warnings: byteSort([...assessed.warnings, ...(0, index_js_2.describe)(facts)]),
1512
+ unassessed: assessed.unassessed,
1513
+ snapshot: '0.21.4-beta',
1514
+ };
1515
+ }
1397
1516
  async function run(args, deps) {
1398
1517
  let unsafeArg = false;
1399
1518
  let endOfFlags = false;
@@ -1435,21 +1554,7 @@ async function run(args, deps) {
1435
1554
  };
1436
1555
  return { command: 'inspect', ok: true, warnings, result, error: null };
1437
1556
  }
1438
- const facts = (0, index_js_1.decodeMacaroon)(secret);
1439
- const assessed = (0, index_js_2.grade)(facts.permissions);
1440
- const now = deps.clock();
1441
- const result = {
1442
- fingerprint: facts.fingerprint,
1443
- location: facts.location,
1444
- rootKeyId: facts.rootKeyId.toString(),
1445
- permissions: facts.permissions,
1446
- caveats: facts.caveats.map(caveat => datedCaveat(caveat, now)),
1447
- grade: assessed.grade,
1448
- reasons: assessed.reasons,
1449
- warnings: byteSort([...assessed.warnings, ...(0, index_js_2.describe)(facts)]),
1450
- unassessed: assessed.unassessed,
1451
- snapshot: '0.21.4-beta',
1452
- };
1557
+ const result = inspectResult((0, index_js_1.decodeMacaroon)(secret), deps.clock());
1453
1558
  return { command: 'inspect', ok: true, warnings, result, error: null };
1454
1559
  }
1455
1560
  catch (error) {
@@ -2030,24 +2135,37 @@ async function runSteps(args, deps) {
2030
2135
  return fail(failure.code, 'R5', failure.message, warnings);
2031
2136
  }
2032
2137
  const ownEntries = registry.entries.filter(entry => entry.nodePubkey.toLowerCase() === pubkey);
2138
+ // The one active registry entry for a non-zero root key ID, or none when there is none or
2139
+ // more than one (the permissions to check against would be ambiguous).
2140
+ const activeFor = (rootKeyId) => {
2141
+ if (rootKeyId === '0')
2142
+ return undefined;
2143
+ const found = ownEntries.filter(item => item.rootKeyId === rootKeyId && item.status === 'active');
2144
+ return found.length === 1 ? found[0] : undefined;
2145
+ };
2033
2146
  let entry;
2034
2147
  let select;
2035
2148
  let label = selector;
2149
+ // Permissions decoded from --target-file: the selected credential's own, so the widening
2150
+ // guard also applies to a credential with no registry entry (a default macaroon).
2151
+ let filePermissions = null;
2152
+ let fileRootKeyId = null;
2036
2153
  if (targetFile !== undefined) {
2037
- // The credential ID comes from the identifier only; the file's bytes are not kept.
2154
+ // The credential ID and permissions come from the identifier only; the file's bytes are not kept.
2038
2155
  let identity;
2039
2156
  try {
2040
2157
  if ((0, protected_path_js_1.isProtectedPath)(targetFile))
2041
2158
  return fail(3, 'R6', 'protected target file', warnings);
2042
- identity = (0, index_js_1.macaroonId)((await (0, online_js_1.readOnlineCredential)(targetFile, { stdin: process.stdin, fs: deps.fs })).secret);
2159
+ const secret = (await (0, online_js_1.readOnlineCredential)(targetFile, { stdin: process.stdin, fs: deps.fs })).secret;
2160
+ identity = (0, index_js_1.macaroonId)(secret);
2161
+ filePermissions = (0, index_js_1.decodeMacaroon)(secret).permissions;
2043
2162
  }
2044
2163
  catch {
2045
2164
  return fail(3, 'R6', 'unreadable or undecodable target file', warnings);
2046
2165
  }
2047
2166
  select = { credential: identity.id };
2048
2167
  label = identity.id;
2049
- if (identity.rootKeyId !== 0n)
2050
- entry = ownEntries.find(item => item.rootKeyId === identity.rootKeyId.toString() && item.status === 'active');
2168
+ fileRootKeyId = identity.rootKeyId.toString();
2051
2169
  }
2052
2170
  else if (byCredential) {
2053
2171
  select = { credential: selector };
@@ -2056,7 +2174,7 @@ async function runSteps(args, deps) {
2056
2174
  // Root key 0 is shared by every default macaroon: only a fingerprint can tell them apart.
2057
2175
  if (selector === '0')
2058
2176
  return fail(2, 'R6', 'root key ID 0 is shared by the default macaroons; select one with --target-file or its credential ID (id:…)', warnings);
2059
- entry = ownEntries.find(item => item.rootKeyId === selector && item.status === 'active');
2177
+ entry = activeFor(selector);
2060
2178
  select = { rootKeyId: selector };
2061
2179
  }
2062
2180
  else {
@@ -2068,6 +2186,19 @@ async function runSteps(args, deps) {
2068
2186
  select = { rootKeyId: entry.rootKeyId };
2069
2187
  }
2070
2188
  const records = usage.forCredential(pubkey, select);
2189
+ // A credential selector resolves to a registry entry through its usage identity: the root
2190
+ // key ID recorded with it (and the file's, for --target-file) must be one non-zero value.
2191
+ // One credential ID under several root key IDs is inconsistent evidence and resolves to none.
2192
+ const identities = new Set(records.map(item => item.rootKeyId));
2193
+ if (fileRootKeyId !== null)
2194
+ identities.add(fileRootKeyId);
2195
+ const identity = identities.size === 1 ? [...identities][0] : null;
2196
+ if (select.credential !== undefined) {
2197
+ if (identity !== null)
2198
+ entry = activeFor(identity);
2199
+ if (identities.size > 1)
2200
+ filePermissions = null;
2201
+ }
2071
2202
  const byMethod = new Map();
2072
2203
  for (const record of records) {
2073
2204
  const current = byMethod.get(record.method);
@@ -2091,18 +2222,35 @@ async function runSteps(args, deps) {
2091
2222
  warnings.push({ code: 'usage-incomplete', message: 'the usage data may be missing calls (records were evicted, or watch '
2092
2223
  + 'was overloaded and did not attribute some calls), and any credential could be affected; no narrowing command is printed. '
2093
2224
  + 'For a complete observation, stop watch, move the usage file aside and run watch again' });
2094
- const beyond = current === null ? [] : methods.filter(method => !unmapped.includes(method)
2095
- && !current.includes(`uri:${method}`) && !(methodPermissions[method] ?? []).every(permission => current.includes(permission)));
2225
+ // Every permission set the credential is known to hold must already cover each observed
2226
+ // method: the registry entry's and, for --target-file, the file's own.
2227
+ const held = [current, filePermissions].filter((item) => item !== null);
2228
+ const covers = (permissions, method) => permissions.includes(`uri:${method}`)
2229
+ || (methodPermissions[method] ?? []).every(permission => permissions.includes(permission));
2230
+ const beyond = methods.filter(method => !unmapped.includes(method) && held.some(permissions => !covers(permissions, method)));
2231
+ // Without any known permission set, narrowing cannot be told from widening.
2232
+ const unverified = observed.length > 0 && held.length === 0;
2233
+ if (unverified)
2234
+ warnings.push({ code: 'recommendation-unverified', message: 'the observed usage could not be matched to the permissions '
2235
+ + 'of exactly one active registry entry (or of --target-file), so a narrowing command could widen the credential; none is printed' });
2096
2236
  if (beyond.length)
2097
2237
  warnings.push({ code: 'recommendation-broader', message: `observed methods this credential's recorded permissions do not cover: `
2098
2238
  + `${beyond.join(', ')}; the usage data or the registry may be wrong, so no narrowing command is printed` });
2099
- // How long watch observed this node, counted from the credential's creation when it is
2100
- // registered later than watch first ran. Files written before 1.0.1 record no coverage.
2239
+ // How long watch observed this node after the credential was created: a provable lower
2240
+ // bound, never time watch covered before it existed. Its creation is the registry entry's;
2241
+ // without one, the credential existed no later than its first recorded use. Two bounds,
2242
+ // the larger is taken: the recorded spans after creation (from 1.1.0), and the covered
2243
+ // total minus the most that can lie before creation (all time from the first registration
2244
+ // to it). Files written before 1.0.1 record no coverage.
2101
2245
  const coverage = usage.coverageFor(pubkey);
2102
2246
  let seconds = null;
2103
2247
  if (coverage) {
2104
- const since = Math.max(Date.parse(coverage.firstStart), entry ? Date.parse(entry.createdAt) : 0);
2105
- seconds = Math.max(0, Math.min(coverage.coveredSeconds, Math.floor((Date.parse(coverage.lastEnd) - since) / 1_000)));
2248
+ const firstUse = records.length ? records.reduce((first, item) => Math.min(first, Date.parse(item.firstSeen)), Infinity) : null;
2249
+ const created = entry ? Date.parse(entry.createdAt) : firstUse;
2250
+ const since = Math.max(Date.parse(coverage.firstStart), created ?? -Infinity);
2251
+ const before = Math.ceil((since - Date.parse(coverage.firstStart)) / 1_000);
2252
+ const lower = Math.max(coverage.coveredSeconds - before, usage.coveredSince(pubkey, since) ?? 0);
2253
+ seconds = Math.max(0, Math.min(lower, Math.floor((Date.parse(coverage.lastEnd) - since) / 1_000)));
2106
2254
  }
2107
2255
  const short = seconds === null || seconds < exports.SHORT_OBSERVATION_SECONDS;
2108
2256
  const observation = { seconds, since: coverage?.firstStart ?? null, until: coverage?.lastEnd ?? null,
@@ -2114,16 +2262,18 @@ async function runSteps(args, deps) {
2114
2262
  + (acceptShort ? 'The narrowing command is printed because --accept-short-observation was given'
2115
2263
  : 'Keep watch running longer, or pass --accept-short-observation to print the narrowing command anyway') });
2116
2264
  const commands = [];
2117
- if (observed.length && !unmapped.length && !incomplete && !beyond.length && (!short || acceptShort)) {
2265
+ if (observed.length && !unmapped.length && !incomplete && !unverified && !beyond.length && (!short || acceptShort)) {
2118
2266
  const label = entry?.label ? narrowLabel(entry.label) : 'narrowed';
2119
2267
  const connection = `--lnd-rest ${quote(host)} --tls-cert ${quote((0, online_js_1.recoveryPath)(certPath))} --macaroon ${quote((0, online_js_1.recoveryPath)(ownPath))}` +
2120
2268
  (registryPath ? ` --registry ${quote((0, online_js_1.recoveryPath)(registryPath))}` : '');
2121
2269
  commands.push(`macwarden bake --label ${label} --uri ${methods.join(',')} --out <file> ${connection}`);
2122
- if (entry?.label)
2270
+ // Revoking deletes the whole root key ID: only suggested when the selection covered every
2271
+ // credential observed on it (a label or root key ID), never from one credential's usage.
2272
+ if (entry?.label && select.credential === undefined)
2123
2273
  commands.push(`after switching the app to the new file: macwarden revoke ${entry.label} ${connection}`);
2124
2274
  }
2125
2275
  const result = { node: pubkey, selector: label, label: entry?.label ?? null,
2126
- rootKeyId: entry?.rootKeyId ?? (byId ? selector : null), credentials: sortUnique(records.map(record => record.credential)),
2276
+ rootKeyId: entry?.rootKeyId ?? (byId ? selector : identity), credentials: sortUnique(records.map(record => record.credential)),
2127
2277
  observed, unmapped, recommended, recommendedUris, current, unneeded, observation, commands };
2128
2278
  return { schema: 1, command: 'recommend', ok: true, warnings, result, error: null };
2129
2279
  }
@@ -2376,10 +2526,24 @@ async function runSteps(args, deps) {
2376
2526
  const tampered = (0, index_js_3.replacementLinkProblems)(mine)[0];
2377
2527
  if (tampered)
2378
2528
  return fail(3, 'P5', `registry tampering: root key ID ${tampered.rootKeyId} claims to replace ${tampered.replaces}, but ${tampered.problem}; restore the registry from a trusted copy`, warnings);
2529
+ // Whether a credential is a canary is decided by its file, not by the editable registry caveat list:
2530
+ // a custom caveat the registry does not record would let the batch rotate a canary into an
2531
+ // ordinary credential. Refused before any mutation, like a forged replacement link.
2532
+ for (const entry of originals(mine)) {
2533
+ if ((0, index_js_3.hasCustomCaveat)(entry))
2534
+ continue;
2535
+ const file = await readCredential(entry.outPath, deps);
2536
+ if (file && file.facts.fingerprint === entry.fingerprint && file.facts.caveats.some(caveat => caveat.type === 'lnd-custom'))
2537
+ return fail(3, 'P5', `registry tampering: root key ID ${entry.rootKeyId}: its credential file carries a custom caveat ` +
2538
+ '(a canary, or another lnd-custom caveat) that the registry does not record; restore the registry from a trusted copy', warnings);
2539
+ }
2379
2540
  // Only active entries cover an ID LND holds; a revoked entry for a present ID needs action.
2380
2541
  const active = new Set(mine.filter(entry => entry.status === 'active').map(entry => entry.rootKeyId));
2381
2542
  const revokedIds = new Set(mine.filter(entry => entry.status === 'revoked').map(entry => entry.rootKeyId));
2382
2543
  const canBake = (entry) => entry.grade === 'ADMIN' || entry.permissions.includes('macaroon:generate');
2544
+ // revoke resolves a label among this node's active entries; where that is ambiguous, advice names the ID.
2545
+ const target = (entry) => entry.label !== null
2546
+ && mine.filter(other => other.status === 'active' && other.label === entry.label).length === 1 ? entry.label : entry.rootKeyId;
2383
2547
  const items = [];
2384
2548
  for (const entry of originals(mine).sort((a, b) => byId(a.rootKeyId, b.rootKeyId))) {
2385
2549
  const staged = stagedReplacement(mine, entry);
@@ -2391,12 +2555,28 @@ async function runSteps(args, deps) {
2391
2555
  ? 'macwarden\'s own credential: bake a new own macaroon, switch MACWARDEN_MACAROON to it, then revoke this ID'
2392
2556
  : entry.rootKeyId === '0' ? 'root key ID 0: see the root key ID 0 guidance' : 'no label: revoke it by root key ID, then bake a replacement' });
2393
2557
  else if ((0, index_js_3.hasCustomCaveat)(entry))
2394
- items.push({ ...base, action: 'manual', status: 'manual', note: exports.CUSTOM_CAVEAT_NOTE.replace('<label>', entry.label) });
2395
- else if (!inLnd.has(entry.rootKeyId))
2396
- items.push({ ...base, action: 'repair', status: 'manual',
2397
- note: `absent from LND while the registry says active: run macwarden revoke ${entry.label} to record it` });
2558
+ items.push({ ...base, action: 'manual', status: 'manual', note: exports.CUSTOM_CAVEAT_NOTE.replace('<label>', target(entry)) });
2559
+ // A replacement an earlier stage allocated but never verified or wrote (an uncertain bake, a failed
2560
+ // verification or write, a failed registry update) is unresolved work, not a usable stage. It is
2561
+ // never retried: LND may hold, or still create, its root key ID.
2562
+ else if (staged && (!staged.verified || !staged.outPath || !staged.fingerprint)) {
2563
+ const listed = inLnd.has(staged.rootKeyId);
2564
+ items.push({ ...base, outPath: null, action: 'repair', status: 'failed',
2565
+ note: `replacement root key ID ${staged.rootKeyId} is incomplete: abandon it with macwarden revoke ${staged.rootKeyId}, then rerun macwarden respond --out-dir <directory>`,
2566
+ error: { code: 7, step: 'P5', message: `replacement root key ID ${staged.rootKeyId} for root key ID ${entry.rootKeyId} was allocated ` +
2567
+ 'by an earlier stage that did not complete (no verified replacement file); ' +
2568
+ (listed ? 'LND lists it' : 'LND does not list it, but may still create it') + '; it is not retried automatically. ' +
2569
+ `Inspect with ${(0, online_js_1.inventoryRecoveryCommand)(request)}, abandon it with ${(0, online_js_1.recoveryCommand)(request, staged.rootKeyId)}, ` +
2570
+ 'then rerun macwarden respond --out-dir <directory> to stage a new replacement; the old credential is unchanged' } });
2571
+ }
2572
+ // A staged replacement comes first even when the old ID is already absent: an earlier finish
2573
+ // deleted it and was stopped before saving the registry, and rotate F6 repairs exactly that,
2574
+ // after proving the replacement accepted.
2398
2575
  else if (staged)
2399
2576
  items.push({ ...base, action: 'finish', status: 'planned', note: null });
2577
+ else if (!inLnd.has(entry.rootKeyId))
2578
+ items.push({ ...base, action: 'repair', status: 'manual',
2579
+ note: `absent from LND while the registry says active: run macwarden revoke ${target(entry)} to record it` });
2400
2580
  else
2401
2581
  items.push({ ...base, action: 'stage', status: 'planned', note: null });
2402
2582
  }
@@ -2421,13 +2601,18 @@ async function runSteps(args, deps) {
2421
2601
  bakingRisk: { rootKeyIds: baking, guidance: baking.length ? exports.BAKING_RISK_GUIDANCE : [] },
2422
2602
  outDir: request.outDir ? (0, node_path_1.resolve)(request.outDir) : null, items, unproven: [],
2423
2603
  };
2604
+ const incomplete = items.filter(item => item.status === 'failed');
2605
+ for (const item of incomplete)
2606
+ result.unproven.push({ label: item.label, rootKeyId: item.rootKeyId,
2607
+ reason: `replacement root key ID ${item.replacementRootKeyId} incomplete: revoke it by ID, then stage again` });
2424
2608
  const bakingFirst = (item) => baking.includes(item.rootKeyId) ? 0 : 1;
2425
2609
  const selected = items.filter(item => item.action === (request.finish ? 'finish' : 'stage'))
2426
2610
  .sort((a, b) => bakingFirst(a) - bakingFirst(b) || byId(a.rootKeyId, b.rootKeyId));
2427
2611
  for (const item of items) {
2428
2612
  if (!request.finish && item.action === 'finish') {
2429
2613
  item.status = 'staged-earlier';
2430
- item.note = 'replacement staged earlier: switch the app, then run macwarden respond --finish';
2614
+ item.note = inLnd.has(item.rootKeyId) ? 'replacement staged earlier: switch the app, then run macwarden respond --finish'
2615
+ : `replacement staged earlier; old root key ID ${item.rootKeyId} is already absent from LND (an earlier finish was interrupted): run macwarden respond --finish to prove the replacement and record the revocation`;
2431
2616
  }
2432
2617
  if (request.finish && item.action === 'stage') {
2433
2618
  // A credential that already replaced a now-revoked one (an earlier finish, or a past
@@ -2448,8 +2633,10 @@ async function runSteps(args, deps) {
2448
2633
  }
2449
2634
  const done = (exitCode) => ({ schema: 1, command: 'respond', ok: true, warnings, result,
2450
2635
  error: null, ...(exitCode ? { exitCode } : {}) });
2451
- if (request.dryRun || !selected.length)
2636
+ if (request.dryRun)
2452
2637
  return done(0);
2638
+ if (!selected.length)
2639
+ return done(incomplete.length ? 7 : 0);
2453
2640
  // P6: the output directory exists and is a directory; created owner-only when missing.
2454
2641
  if (!request.finish) {
2455
2642
  const outDir = (0, node_path_1.resolve)(request.outDir);
@@ -2499,6 +2686,8 @@ async function runSteps(args, deps) {
2499
2686
  }
2500
2687
  const rotated = outcome.result;
2501
2688
  item.status = request.finish ? 'finished' : 'staged';
2689
+ if (rotated.repaired)
2690
+ item.note = `old root key ID ${item.rootKeyId} was already absent from LND: registry repaired, no second deletion sent`;
2502
2691
  item.replacementRootKeyId = rotated.new.rootKeyId;
2503
2692
  item.replacementFingerprint = rotated.new.fingerprint;
2504
2693
  item.outPath = rotated.new.outPath;
@@ -2544,8 +2733,8 @@ async function runSteps(args, deps) {
2544
2733
  // that was not confirmed: exit 7, like any mutation whose outcome is unknown.
2545
2734
  const unprovenFinish = selected.some(item => item.status === 'finished' && item.proof !== null && item.proof.proven === false);
2546
2735
  // An interrupt after any staged or finished credential means LND changed (7); before any, nothing changed (6).
2547
- const code = interrupted ? (codes.some(value => value === 0 || value === 7) ? 7 : 6)
2548
- : aggregateExit([...codes, ...(unprovenFinish ? [7] : [])]);
2736
+ const code = interrupted ? (codes.some(value => value === 0 || value === 7) || incomplete.length ? 7 : 6)
2737
+ : aggregateExit([...codes, ...(unprovenFinish || incomplete.length ? [7] : [])]);
2549
2738
  return done(code);
2550
2739
  }
2551
2740
  async function run(args, deps) {
@@ -2963,7 +3152,7 @@ async function runSteps(args, deps, warnings, state) {
2963
3152
  revokedEntry = { nodePubkey: pubkey, rootKeyId: id.toString(), label: null,
2964
3153
  permissions: facts ? [...facts.permissions] : [],
2965
3154
  caveats: facts?.caveats.map(({ type, value }) => ({ type, value })) ?? [],
2966
- grade: facts ? (0, index_js_4.grade)(facts.permissions).grade : 'UNASSESSED',
3155
+ grade: facts ? (0, index_js_4.gradeFacts)(facts).grade : 'UNASSESSED',
2967
3156
  fingerprint: facts?.fingerprint ?? null, outPath: facts && request.targetFile ? (0, node_path_1.resolve)(request.targetFile) : null,
2968
3157
  createdAt: revokedAt, verified: facts !== undefined && proof === 'rejected', status: 'revoked', revokedAt,
2969
3158
  revokeEvidence: proof, replaces: null, replacedBy: null };
@@ -3479,6 +3668,11 @@ async function runSteps(args, deps, state) {
3479
3668
  return fail(3, 'S4', 'old credential file root key ID differs from the registry entry');
3480
3669
  if (oldFacts && oldFacts.fingerprint !== old.fingerprint)
3481
3670
  return fail(3, 'S4', 'old credential fingerprint differs from the registry entry');
3671
+ // The registry's caveat list is editable metadata: the credential itself decides whether it is a
3672
+ // canary. A custom caveat the registry does not record is tampering, refused before any mutation.
3673
+ if (oldFacts?.caveats.some(caveat => caveat.type === 'lnd-custom'))
3674
+ return fail(3, 'S4', 'the credential file carries a custom caveat (a canary, or another lnd-custom caveat) that the ' +
3675
+ 'registry entry does not record; a rotation would replace it with an ordinary credential. Restore the registry from a trusted copy');
3482
3676
  if ((0, index_js_3.grade)(old.permissions).grade !== old.grade)
3483
3677
  return fail(3, 'S4', 'registry grade differs from recorded permissions');
3484
3678
  if (oldFacts?.fingerprint === old.fingerprint) {
@@ -3980,6 +4174,329 @@ async function run(args, deps = { fs }) {
3980
4174
  }
3981
4175
  }
3982
4176
 
4177
+ },
4178
+ "commands/verify.js": (module, exports, require) => {
4179
+ "use strict";
4180
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4181
+ if (k2 === undefined) k2 = k;
4182
+ var desc = Object.getOwnPropertyDescriptor(m, k);
4183
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
4184
+ desc = { enumerable: true, get: function() { return m[k]; } };
4185
+ }
4186
+ Object.defineProperty(o, k2, desc);
4187
+ }) : (function(o, m, k, k2) {
4188
+ if (k2 === undefined) k2 = k;
4189
+ o[k2] = m[k];
4190
+ }));
4191
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
4192
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
4193
+ }) : function(o, v) {
4194
+ o["default"] = v;
4195
+ });
4196
+ var __importStar = (this && this.__importStar) || (function () {
4197
+ var ownKeys = function(o) {
4198
+ ownKeys = Object.getOwnPropertyNames || function (o) {
4199
+ var ar = [];
4200
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
4201
+ return ar;
4202
+ };
4203
+ return ownKeys(o);
4204
+ };
4205
+ return function (mod) {
4206
+ if (mod && mod.__esModule) return mod;
4207
+ var result = {};
4208
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
4209
+ __setModuleDefault(result, mod);
4210
+ return result;
4211
+ };
4212
+ })();
4213
+ exports.__esModule = true;
4214
+ exports.CUSTOM_CAVEAT_REJECTED = exports.REJECTED_INCONCLUSIVE = exports.OWN_ROOT_KEY_ACCEPTED = exports.CAN_BAKE_ROOT_KEY_0 = exports.CAN_BAKE_ACCEPTED = exports.ROOT_KEY_0_ACCEPTED = void 0;
4215
+ exports.run = run;
4216
+ exports.verify = verify;
4217
+ const nodeFs = __importStar(require("node:fs/promises"));
4218
+ const cancellation_js_1 = require("../cancellation.js");
4219
+ const online_js_1 = require("../online.js");
4220
+ const environment_js_1 = require("../environment.js");
4221
+ const protected_path_js_1 = require("../protected-path.js");
4222
+ const index_js_1 = require("../decode/index.js");
4223
+ const index_js_2 = require("../input/index.js");
4224
+ const index_js_3 = require("../lnd/index.js");
4225
+ const index_js_4 = require("../registry/index.js");
4226
+ const inspect_js_1 = require("./inspect.js");
4227
+ const valueFlags = ['--registry', '--lnd-rest', '--tls-cert', '--macaroon'];
4228
+ // GetInfo (node identity for the registry match), ListPermissions (snapshot drift, as on every
4229
+ // online command) and CheckMacaroonPermissions are required; ListMacaroonIDs is optional.
4230
+ const required = [
4231
+ ['/lnrpc.Lightning/GetInfo', 'info:read'],
4232
+ ['/lnrpc.Lightning/ListPermissions', 'info:read'],
4233
+ ['/lnrpc.Lightning/CheckMacaroonPermissions', 'macaroon:read'],
4234
+ ];
4235
+ const listIdsMethod = ['/lnrpc.Lightning/ListMacaroonIDs', 'macaroon:read'];
4236
+ // Static operator guidance. macwarden never calls changepassword or touches macaroons.db (SAFE-02).
4237
+ exports.ROOT_KEY_0_ACCEPTED = [
4238
+ 'This credential is on root key ID 0, shared by admin.macaroon, readonly.macaroon, invoice.macaroon and every macaroon baked without --root_key_id. LND refuses to delete root key ID 0, so it cannot be revoked on its own.',
4239
+ 'Only regenerating every root key invalidates it: lncli changepassword --new_mac_root_key on the node (it needs the wallet password). macwarden never runs it (SAFE-02).',
4240
+ "That also invalidates every other macaroon on the node at once: every app's credential, macwarden's own and any replacement staged with macwarden respond. So regenerate first, in a maintenance window, then bake a fresh per-app macaroon for each app (macwarden bake) and re-pair it. Run macwarden inventory beforehand to list the apps that will need one; staging replacements before the regeneration would only see them invalidated too.",
4241
+ 'Procedure: docs/incident-playbook.md and docs/replacing-default-macaroons.md.',
4242
+ ];
4243
+ exports.CAN_BAKE_ACCEPTED = 'This credential can bake macaroons (macaroon:generate). If it leaked, its holder may have created credentials on other root key IDs, and revoking this one does not remove them. Replace and revoke every registered credential with macwarden respond, list unknown root key IDs with macwarden inventory, and if this credential may have been used, regenerate all root keys (see docs/incident-playbook.md).';
4244
+ exports.CAN_BAKE_ROOT_KEY_0 = 'It can also bake macaroons (macaroon:generate): its holder may have created credentials on other root key IDs. The regeneration invalidates those too; afterwards, revoke any root key ID macwarden inventory still shows as unknown that no app of yours uses.';
4245
+ exports.OWN_ROOT_KEY_ACCEPTED = "This credential shares the root key ID of the credential macwarden is running with, and macwarden revoke refuses its own root key ID. Revoke it with another own credential, or delete that root key ID with lncli deletemacaroonid after switching macwarden to a new session macaroon.";
4246
+ exports.REJECTED_INCONCLUSIVE = 'LND rejected this check, but not because the root key ID is gone, the root key was regenerated or the credential expired. A caveat that depends on the caller (an IP lock, or a custom caveat such as a canary\'s) can make LND accept it from another address or while a middleware runs. Treat it as live until its root key ID is revoked.';
4247
+ exports.CUSTOM_CAVEAT_REJECTED = 'It carries a custom caveat (a canary, or another lnd-custom caveat): LND accepts it only while a middleware for that caveat, such as macwarden watch, is registered.';
4248
+ function fail(code, step, message, warnings = []) {
4249
+ return { schema: 1, command: 'verify', ok: false, warnings, result: null, error: { code, step, message } };
4250
+ }
4251
+ function lndFailure(error, step, warnings) {
4252
+ const failure = (0, online_js_1.failureDetails)(error);
4253
+ return fail(failure.code, step, failure.message, warnings);
4254
+ }
4255
+ // Returns the request, or a message naming the flag and the rule (never the value).
4256
+ function parse(args, env) {
4257
+ const supplied = new Map();
4258
+ const inputs = [];
4259
+ let unsafeArg = false;
4260
+ let endOfFlags = false;
4261
+ for (let index = 0; index < args.length; index++) {
4262
+ const arg = args[index];
4263
+ if (endOfFlags) {
4264
+ inputs.push(arg);
4265
+ continue;
4266
+ }
4267
+ if (arg === '--') {
4268
+ endOfFlags = true;
4269
+ continue;
4270
+ }
4271
+ if (arg === '--json')
4272
+ continue;
4273
+ if (arg === '--unsafe-arg') {
4274
+ if (unsafeArg)
4275
+ return (0, online_js_1.flagProblem)(arg, 'repeated');
4276
+ unsafeArg = true;
4277
+ continue;
4278
+ }
4279
+ if (valueFlags.includes(arg)) {
4280
+ const value = args[++index];
4281
+ if (supplied.has(arg))
4282
+ return (0, online_js_1.flagProblem)(arg, 'repeated');
4283
+ if (value === undefined || value.startsWith('--'))
4284
+ return (0, online_js_1.flagProblem)(arg, 'missing');
4285
+ supplied.set(arg, value);
4286
+ continue;
4287
+ }
4288
+ if (arg.startsWith('-') && arg !== '-')
4289
+ return `unknown flag; verify takes: <file | -> --unsafe-arg ${valueFlags.join(' ')} --json`;
4290
+ inputs.push(arg);
4291
+ }
4292
+ if (inputs.length !== 1)
4293
+ return 'expected one input: a credential file, or - for stdin';
4294
+ const host = supplied.get('--lnd-rest') ?? env.MACWARDEN_LND_REST;
4295
+ const cert = supplied.get('--tls-cert') ?? env.MACWARDEN_TLS_CERT;
4296
+ const own = supplied.get('--macaroon') ?? env.MACWARDEN_MACAROON;
4297
+ const connection = (0, online_js_1.connectionProblem)(host, cert, own);
4298
+ if (connection)
4299
+ return connection;
4300
+ return { input: inputs[0], unsafeArg, registry: supplied.get('--registry'),
4301
+ host: (0, online_js_1.endpoint)(host), cert: cert, own: own };
4302
+ }
4303
+ function holds(held, [method, permission]) {
4304
+ return held.has(permission) || held.has(`uri:${method}`);
4305
+ }
4306
+ /** The registry entry for this credential on this node: the same bytes first, else the same root key ID. */
4307
+ function registryMatch(entries, pubkey, facts) {
4308
+ const local = entries.filter(entry => entry.nodePubkey.toLowerCase() === pubkey);
4309
+ const rootKeyId = facts.rootKeyId.toString();
4310
+ const byFingerprint = local.filter(entry => entry.fingerprint === facts.fingerprint);
4311
+ const sameId = (entry) => entry.rootKeyId === rootKeyId;
4312
+ // An active entry outranks a revoked record of the same root key ID.
4313
+ const pick = (rows) => rows.find(entry => entry.status === 'active') ?? rows[0];
4314
+ const exact = pick(byFingerprint.filter(sameId));
4315
+ if (exact)
4316
+ return { label: exact.label, status: exact.status, match: 'fingerprint' };
4317
+ const related = pick(local.filter(sameId));
4318
+ return related ? { label: related.label, status: related.status, match: 'root-key-id' } : null;
4319
+ }
4320
+ function guidance(result) {
4321
+ const id = result.rootKeyId;
4322
+ if (result.status === 'accepted') {
4323
+ const lines = ['LND accepts this credential now: whoever holds a copy can use it against this node.'];
4324
+ if (id === '0')
4325
+ return [...lines, ...exports.ROOT_KEY_0_ACCEPTED, ...(result.canBake ? [exports.CAN_BAKE_ROOT_KEY_0] : [])];
4326
+ if (result.ownCredential)
4327
+ lines.push(exports.OWN_ROOT_KEY_ACCEPTED);
4328
+ else {
4329
+ const label = result.registry?.status === 'active' ? result.registry.label : null;
4330
+ // By root key ID: a label is ambiguous while a rotation is staged (two active entries).
4331
+ lines.push(result.registry === null
4332
+ ? `Revoke it: macwarden revoke ${id} --unregistered. It is not in this registry, so first make sure no app you rely on uses root key ID ${id}.`
4333
+ : `Revoke it: macwarden revoke ${id}${label ? ` (registered as ${label}; if an app still needs access, rotate it instead: macwarden rotate ${label} --out <file>, switch the app, then macwarden rotate ${label} --finish)` : ''}.`);
4334
+ lines.push(`Revoking root key ID ${id} also invalidates every other macaroon baked on it.`);
4335
+ }
4336
+ if (result.canBake)
4337
+ lines.push(exports.CAN_BAKE_ACCEPTED);
4338
+ return lines;
4339
+ }
4340
+ if (!result.conclusive) {
4341
+ const lines = [exports.REJECTED_INCONCLUSIVE];
4342
+ if (result.caveats.some(caveat => caveat.type === 'lnd-custom'))
4343
+ lines.push(exports.CUSTOM_CAVEAT_REJECTED);
4344
+ if (id !== '0')
4345
+ lines.push(`To make sure: macwarden revoke ${id}${result.registry === null ? ' --unregistered' : ''}.`);
4346
+ return lines;
4347
+ }
4348
+ const why = result.rejection === 'root-key-missing' ? 'its root key ID no longer exists'
4349
+ : result.rejection === 'signature-mismatch' ? 'its root key was regenerated'
4350
+ : result.rejection === 'expired' ? 'a time-before caveat has passed' : 'its root key ID is no longer listed';
4351
+ const lines = [`LND rejects this credential now: ${why}. It can no longer be used against this node.`];
4352
+ if (result.rejection === 'expired' && result.rootKeyListed !== false && id !== '0')
4353
+ lines.push(`Its root key ID ${id} still exists: other macaroons baked on it, including copies without this expiry, keep working until it is revoked.`);
4354
+ return lines;
4355
+ }
4356
+ async function runSteps(args, deps, warnings) {
4357
+ // C1 Parse.
4358
+ const request = parse(args, deps.env);
4359
+ if (typeof request === 'string')
4360
+ return fail(2, null, request);
4361
+ // C2 ENV.
4362
+ if ((0, environment_js_1.unsafeNodeDiagnostics)(deps.env))
4363
+ return fail(2, 'C2', 'unsafe Node diagnostic settings');
4364
+ // C3 Read and decode the credential under test, offline, as inspect does.
4365
+ let target;
4366
+ let facts;
4367
+ try {
4368
+ const read = await (0, index_js_2.readCredential)(request.input, { stdin: deps.stdin ?? process.stdin, fs: deps.fs }, { unsafeArg: request.unsafeArg, nwc: true });
4369
+ if (read.literal)
4370
+ warnings.push('unsafe-arg');
4371
+ if ((0, index_js_1.isNwcContainer)(read.secret))
4372
+ return fail(3, 'input-form', 'a Nostr Wallet Connect URI is not an LND credential; revoke NWC connections in the wallet service', warnings);
4373
+ target = read.secret;
4374
+ facts = (0, index_js_1.decodeMacaroon)(target);
4375
+ }
4376
+ catch (error) {
4377
+ if (error instanceof index_js_2.UsageError)
4378
+ return fail(2, null, error.message, warnings);
4379
+ if (error instanceof index_js_2.InputError || error instanceof index_js_1.DecodeError)
4380
+ return fail(3, error.stage, error.message, warnings);
4381
+ throw error;
4382
+ }
4383
+ // An empty permission list is never sent: LND would report any macaroon valid for it.
4384
+ if (facts.permissions.length === 0)
4385
+ return fail(3, 'C3', 'the credential holds no permissions, so LND cannot be asked about it (an empty permission list is never sent)', warnings);
4386
+ const offline = (0, inspect_js_1.inspectResult)(facts, deps.clock());
4387
+ // C4 OWN.
4388
+ let cert;
4389
+ try {
4390
+ if ((0, protected_path_js_1.isProtectedPath)(request.cert) || (0, protected_path_js_1.isProtectedPath)(await deps.fs.realpath(request.cert)))
4391
+ return fail(4, 'C4', 'unreadable TLS certificate', warnings);
4392
+ cert = await (0, online_js_1.readBoundedFile)(request.cert, deps.fs);
4393
+ }
4394
+ catch {
4395
+ return fail(4, 'C4', 'unreadable TLS certificate', warnings);
4396
+ }
4397
+ let own;
4398
+ try {
4399
+ own = (await (0, online_js_1.readOnlineCredential)(request.own, { stdin: deps.stdin ?? process.stdin, fs: deps.fs })).secret;
4400
+ }
4401
+ catch {
4402
+ return fail(4, 'C4', 'unreadable own macaroon', warnings);
4403
+ }
4404
+ let ownFacts;
4405
+ try {
4406
+ ownFacts = (0, index_js_1.decodeMacaroon)(own);
4407
+ }
4408
+ catch {
4409
+ return fail(4, 'C4', 'undecodable own macaroon', warnings);
4410
+ }
4411
+ const held = new Set(ownFacts.permissions);
4412
+ for (const method of required)
4413
+ if (!holds(held, method))
4414
+ return fail(4, 'C4', `own macaroon missing permission: ${method[0]} requires ${method[1]}`, warnings);
4415
+ const canList = holds(held, listIdsMethod);
4416
+ if (ownFacts.rootKeyId === 0n)
4417
+ warnings.push('own-root-key-0');
4418
+ // C5 Registry, read-only (no lock).
4419
+ let entries;
4420
+ try {
4421
+ if (request.registry && (0, protected_path_js_1.isProtectedPath)(request.registry))
4422
+ return fail(3, 'C5', 'protected registry path', warnings);
4423
+ entries = (await (0, index_js_4.openRegistry)(request.registry, { fs: deps.fs, env: deps.env })).entries;
4424
+ }
4425
+ catch {
4426
+ return fail(3, 'C5', 'unreadable or invalid registry', warnings);
4427
+ }
4428
+ // C6 Node identity, snapshot drift, root key IDs.
4429
+ const adapter = deps.lnd ?? deps.createAdapter?.({ restUrl: request.host, tlsCert: cert, macaroon: own });
4430
+ if (!adapter)
4431
+ return fail(8, 'internal', 'unexpected failure', warnings);
4432
+ const lnd = (0, online_js_1.onlineAdapter)(adapter);
4433
+ let info;
4434
+ try {
4435
+ info = (0, online_js_1.nodeInfo)(await lnd.getInfo());
4436
+ }
4437
+ catch (error) {
4438
+ return lndFailure(error, 'C6', warnings);
4439
+ }
4440
+ try {
4441
+ if ((0, online_js_1.drift)(await lnd.listPermissions()))
4442
+ warnings.push('snapshot-drift');
4443
+ }
4444
+ catch (error) {
4445
+ return lndFailure(error, 'C6', warnings);
4446
+ }
4447
+ let rootKeyListed = null;
4448
+ if (canList) {
4449
+ try {
4450
+ rootKeyListed = (await lnd.listMacaroonIds()).includes(facts.rootKeyId);
4451
+ }
4452
+ catch (error) {
4453
+ return lndFailure(error, 'C6', warnings);
4454
+ }
4455
+ }
4456
+ // C7 Ask LND: CheckMacaroonPermissions with the credential's own decoded permissions.
4457
+ let checked;
4458
+ try {
4459
+ checked = await lnd.checkMacaroon(target, facts.permissions);
4460
+ }
4461
+ catch (error) {
4462
+ return lndFailure(error, 'C7', warnings);
4463
+ }
4464
+ const expired = offline.caveats.some(caveat => caveat.type === 'time-before' && caveat.expired === true);
4465
+ const rejection = checked.valid ? null
4466
+ : checked.reason === 'other' && expired ? 'expired' : checked.reason;
4467
+ // A rejection holds everywhere when the root key is gone or regenerated, or the credential
4468
+ // expired; any other rejection may depend on the caller (IP lock, custom caveat).
4469
+ const conclusive = checked.valid || rejection !== 'other' || rootKeyListed === false;
4470
+ const partial = {
4471
+ ...offline,
4472
+ node: { pubkey: info.pubkey, alias: info.alias, network: info.network },
4473
+ status: checked.valid ? 'accepted' : 'rejected',
4474
+ rejection, conclusive, rootKeyListed,
4475
+ ownCredential: facts.rootKeyId === ownFacts.rootKeyId,
4476
+ canBake: offline.grade === 'ADMIN' || facts.permissions.includes('macaroon:generate'),
4477
+ registry: registryMatch(entries, info.pubkey, facts),
4478
+ };
4479
+ const result = { ...partial, guidance: guidance(partial) };
4480
+ // Exit 1 is a finding, as with scan --fail-on: the credential is live, or may be.
4481
+ return { schema: 1, command: 'verify', ok: true, warnings, result, error: null,
4482
+ ...(result.status === 'accepted' || !result.conclusive ? { exitCode: 1 } : {}) };
4483
+ }
4484
+ async function run(args, deps) {
4485
+ const warnings = [];
4486
+ try {
4487
+ return (0, online_js_1.onlineWarnings)(await runSteps(args, deps, warnings));
4488
+ }
4489
+ catch (error) {
4490
+ // verify changes nothing, so an interrupt is always before any mutation (exit 6).
4491
+ return (0, online_js_1.onlineWarnings)(error instanceof cancellation_js_1.Cancelled ? fail(6, 'cancelled', 'interrupted', warnings)
4492
+ : fail(8, 'internal', 'unexpected failure', warnings));
4493
+ }
4494
+ }
4495
+ async function verify(args) {
4496
+ return run(args, { createAdapter: index_js_3.createLndAdapter, fs: nodeFs, env: { ...process.env },
4497
+ clock: () => new Date(), stdin: process.stdin });
4498
+ }
4499
+
3983
4500
  },
3984
4501
  "commands/watch.js": (module, exports, require) => {
3985
4502
  "use strict";
@@ -4017,7 +4534,8 @@ var __importStar = (this && this.__importStar) || (function () {
4017
4534
  };
4018
4535
  })();
4019
4536
  exports.__esModule = true;
4020
- exports.CANARY_RELOAD_MS = void 0;
4537
+ exports.MAX_TIMER_MS = exports.CANARY_RELOAD_MS = void 0;
4538
+ exports.scheduleDeadline = scheduleDeadline;
4021
4539
  exports.lockOwnerGone = lockOwnerGone;
4022
4540
  exports.acquireLock = acquireLock;
4023
4541
  exports.run = run;
@@ -4036,6 +4554,7 @@ const environment_js_1 = require("../environment.js");
4036
4554
  const index_js_2 = require("../lnd/index.js");
4037
4555
  const online_js_1 = require("../online.js");
4038
4556
  const protected_path_js_1 = require("../protected-path.js");
4557
+ const bounded_file_js_1 = require("../bounded-file.js");
4039
4558
  const index_js_3 = require("../registry/index.js");
4040
4559
  const process_domain_js_1 = require("../registry/process-domain.js");
4041
4560
  const index_js_4 = require("../output/index.js");
@@ -4146,6 +4665,30 @@ function parse(args, env) {
4146
4665
  return { host: (0, online_js_1.endpoint)(host), grpcHost: grpc.host, grpcPort: grpc.port, grpcSource, cert, own, registry: get('--registry'),
4147
4666
  name, untilMs, alertCommand, alertsOnly: flags.get('--alerts-only') === true, json: flags.get('--json') === true };
4148
4667
  }
4668
+ /** The longest delay setTimeout honours (signed 32-bit milliseconds); longer ones fire after 1 ms. */
4669
+ exports.MAX_TIMER_MS = 2 ** 31 - 1;
4670
+ /**
4671
+ * Calls onExpire once `ms` have passed on a monotonic clock. `--until` accepts up to 999999
4672
+ * days, far beyond one timer (audit MW102-09: `--until 25d` ended at once), so the wait is
4673
+ * split into chunks of at most MAX_TIMER_MS, each re-measured against the one deadline.
4674
+ */
4675
+ function scheduleDeadline(ms, onExpire, now = () => performance.now()) {
4676
+ const deadline = now() + ms;
4677
+ let timer;
4678
+ let cleared = false;
4679
+ const arm = () => {
4680
+ if (cleared)
4681
+ return;
4682
+ const left = deadline - now();
4683
+ if (left <= 0) {
4684
+ onExpire();
4685
+ return;
4686
+ }
4687
+ timer = setTimeout(arm, Math.min(Math.ceil(left), exports.MAX_TIMER_MS));
4688
+ };
4689
+ arm();
4690
+ return { clear: () => { cleared = true; clearTimeout(timer); } };
4691
+ }
4149
4692
  function defaultSleep(ms, signal) {
4150
4693
  return new Promise(resolve => {
4151
4694
  const timer = setTimeout(done, ms);
@@ -4266,6 +4809,8 @@ async function acquireLock(path, deps) {
4266
4809
  let owner = {};
4267
4810
  let stale;
4268
4811
  try {
4812
+ // The owner record is read only as a plain file: never through a symlink (SAFE-02).
4813
+ await (0, bounded_file_js_1.assertPlainFile)(path, deps.fs, bounded_file_js_1.LOCK_FILE_LIMIT);
4269
4814
  stale = await deps.fs.readFile(path, 'utf8');
4270
4815
  owner = JSON.parse(stale);
4271
4816
  }
@@ -4444,7 +4989,7 @@ async function runSteps(args, deps) {
4444
4989
  signal?.addEventListener('abort', onAbort, { once: true });
4445
4990
  if (signal?.aborted)
4446
4991
  stop.abort();
4447
- const untilTimer = request.untilMs === null ? undefined : setTimeout(() => stop.abort(), request.untilMs);
4992
+ const untilTimer = request.untilMs === null ? undefined : scheduleDeadline(request.untilMs, () => stop.abort());
4448
4993
  const counters = { calls: 0, errors: 0, alerts: 0, canaryUses: 0, alertFailures: 0, reconnects: 0, credentials: new Set(),
4449
4994
  attributionDropped: 0 };
4450
4995
  const work = [];
@@ -4529,7 +5074,8 @@ async function runSteps(args, deps) {
4529
5074
  // Whoever holds a canary controls how often it is used, so alerting is bounded:
4530
5075
  // one alert per canary per window plus a counted summary, and at most one
4531
5076
  // --alert-command process at a time (src/watch/alert-throttle.ts).
4532
- const runner = (0, alert_throttle_js_1.createAlertRunner)(event => (deps.alert ?? alert_js_1.runAlertCommand)(request.alertCommand, event), event => {
5077
+ const runner = (0, alert_throttle_js_1.createAlertRunner)((event, cancel) => deps.alert ? deps.alert(request.alertCommand, event, cancel)
5078
+ : (0, alert_js_1.runAlertCommand)(request.alertCommand, event, undefined, undefined, cancel), event => {
4533
5079
  // A failed alert must be visible: the operator relies on it for paging.
4534
5080
  counters.alertFailures += 1;
4535
5081
  if (!warnings.some(item => (typeof item === 'string' ? item : item.code) === 'alert-failed'))
@@ -4788,7 +5334,7 @@ async function runSteps(args, deps) {
4788
5334
  finally {
4789
5335
  clearInterval(flusher);
4790
5336
  clearInterval(reloader);
4791
- clearTimeout(untilTimer);
5337
+ untilTimer?.clear();
4792
5338
  signal?.removeEventListener('abort', onAbort);
4793
5339
  processCanaries();
4794
5340
  runWork(Number.MAX_SAFE_INTEGER);
@@ -4796,10 +5342,15 @@ async function runSteps(args, deps) {
4796
5342
  usage.endCoverage(pubkey, deps.clock());
4797
5343
  await flush();
4798
5344
  await release();
4799
- // Each alert command is killed by ALERT_KILL_MS, so this wait is bounded too.
4800
- await Promise.race([runner.idle(), new Promise(resolve => setTimeout(resolve, alert_js_1.ALERT_KILL_MS + 1_000).unref())]);
5345
+ // One alert deadline for the whole shutdown (audit MW102-05): no alert command starts from
5346
+ // here on and an alert still waiting is discarded (reported as alert-failed); the running
5347
+ // one may finish within ALERT_KILL_MS, then it is cancelled (group or tree killed), and
5348
+ // the process-tree kills get what is left of ALERT_CLEANUP_MS.
5349
+ const alertDeadline = Date.now() + alert_js_1.ALERT_KILL_MS + alert_js_1.ALERT_CLEANUP_MS;
5350
+ await runner.stop(alert_js_1.ALERT_KILL_MS, alert_js_1.ALERT_CLEANUP_MS);
4801
5351
  // Anything an alert command left running is killed now, not left behind (POSIX groups).
4802
5352
  (0, alert_js_1.reapAlertGroups)();
5353
+ await (0, alert_js_1.alertCleanupDone)(Math.max(0, alertDeadline - Date.now()));
4803
5354
  reportDrops();
4804
5355
  // Queued events go out before the summary; a reader that never reads cannot hold us forever.
4805
5356
  // A reader that never reads must not keep this process alive: after the bounded
@@ -4847,7 +5398,7 @@ exports.__esModule = true;
4847
5398
  exports.redactEncodedCredentials = redactEncodedCredentials;
4848
5399
  const grouped_runs_js_1 = require("./grouped-runs.js");
4849
5400
  const recognition_js_1 = require("./decode/recognition.js");
4850
- function normalized(text, compact = true, source) {
5401
+ function normalized(text, compact = true, source, decode = true, backslashPairs = true) {
4851
5402
  let value = '';
4852
5403
  const starts = [];
4853
5404
  const ends = [];
@@ -4859,9 +5410,10 @@ function normalized(text, compact = true, source) {
4859
5410
  continue;
4860
5411
  }
4861
5412
  const at = i;
4862
- const percentByte = text[i] === '%' && /^[0-9a-f]{2}$/i.test(text.slice(i + 1, i + 3));
4863
- const encodedByte = /^(?:%u([0-9a-f]{4})|&#(?:x([0-9a-f]{1,4})|([0-9]{1,5}));)/i.exec(text.slice(i, i + 12));
4864
- const escape = text[i] === '\\' ? /^(?:u([0-9a-f]{4})|x([0-9a-f]{2})|([0-7]{1,3})|([\\"/bfnrt]))/i.exec(text.slice(i + 1)) : null;
5413
+ const percentByte = decode && text[i] === '%' && /^[0-9a-f]{2}$/i.test(text.slice(i + 1, i + 3));
5414
+ const encodedByte = !decode ? null : /^(?:%u([0-9a-f]{4})|&#(?:x([0-9a-f]{1,4})|([0-9]{1,5}));)/i.exec(text.slice(i, i + 12));
5415
+ const escape = decode && text[i] === '\\' ? (backslashPairs ? /^(?:u([0-9a-f]{4})|x([0-9a-f]{2})|([0-7]{1,3})|([\\"/bfnrt]))/i
5416
+ : /^(?:u([0-9a-f]{4})|x([0-9a-f]{2})|([0-7]{1,3})|(["/bfnrt]))/i).exec(text.slice(i + 1)) : null;
4865
5417
  let character;
4866
5418
  if (encodedByte && parseInt(encodedByte[1] ?? encodedByte[2] ?? encodedByte[3], encodedByte[3] ? 10 : 16) <= 255) {
4867
5419
  character = String.fromCharCode(parseInt(encodedByte[1] ?? encodedByte[2] ?? encodedByte[3], encodedByte[3] ? 10 : 16));
@@ -4889,6 +5441,9 @@ function normalized(text, compact = true, source) {
4889
5441
  }
4890
5442
  return { value, starts, ends, escaped };
4891
5443
  }
5444
+ // A macaroon starts with version 2 and a location (1) or identifier (2) packet kind. LND
5445
+ // also accepts the kind as a non-minimal varint (0x81 0x00 ...), so a re-framed copy starts 0281/0282.
5446
+ const MACAROON_HEX_STARTS = new Set(['0201', '0202', '0281', '0282']);
4892
5447
  function isHexDigit(character) {
4893
5448
  return character !== undefined && /^[0-9a-f]$/i.test(character);
4894
5449
  }
@@ -4900,39 +5455,8 @@ function regularGroups(run) {
4900
5455
  function isByteSeparator(character) {
4901
5456
  return character !== undefined && character !== '\\' && /^[^0-9A-Za-z]$/u.test(character);
4902
5457
  }
4903
- function spans(text, source) {
4904
- const { value, starts, ends } = normalized(text, true, source);
4905
- const found = [];
4906
- const raw = normalized(text, false, source);
4907
- // Decode each decimal byte array once, then redact only confirmed macaroon
4908
- // spans. Ordinary arrays and any surrounding values retain their source text.
4909
- const decimalArrays = /(?:0x[0-9a-f]{2}(?:[^0-9A-Za-z\\]{1,3}0x[0-9a-f]{2})+|\d{1,3}(?:[^0-9A-Za-z\\]{1,3}\d{1,3})+)/gi;
4910
- for (let run = decimalArrays.exec(raw.value); run; run = decimalArrays.exec(raw.value)) {
4911
- const tokens = [...run[0].matchAll(/0x[0-9a-f]{2}|\d+/gi)];
4912
- const values = tokens.map(token => Number(token[0]));
4913
- for (let segmentStart = 0; segmentStart < values.length;) {
4914
- let segmentEnd = segmentStart;
4915
- while (segmentEnd < values.length && values[segmentEnd] <= 255)
4916
- segmentEnd++;
4917
- if (segmentEnd > segmentStart) {
4918
- const bytes = Buffer.from(values.slice(segmentStart, segmentEnd));
4919
- const lengthAt = (0, recognition_js_1.macaroonEnds)(bytes);
4920
- for (let pos = 0; pos < bytes.length; pos++) {
4921
- if (bytes[pos] !== 2)
4922
- continue;
4923
- const length = lengthAt(pos);
4924
- if (length === null)
4925
- continue;
4926
- const begin = run.index + tokens[segmentStart + pos].index;
4927
- const last = tokens[segmentStart + pos + length - 1];
4928
- const end = run.index + last.index + last[0].length;
4929
- found.push([raw.starts[begin], raw.ends[end - 1]]);
4930
- pos += length - 1;
4931
- }
4932
- }
4933
- segmentStart = segmentEnd + 1;
4934
- }
4935
- }
5458
+ // Hex encodings, contiguous or with byte separators, in normalized text.
5459
+ function hexSpans(value, starts, ends, found) {
4936
5460
  // A maximal run is decoded once. Repeated 02 prefixes in an ordinary hex
4937
5461
  // string do not cause a decode of every remaining suffix.
4938
5462
  const hexRuns = /(?:0x)?[0-9a-f]{8,}/gi;
@@ -4945,8 +5469,7 @@ function spans(text, source) {
4945
5469
  return (0, recognition_js_1.macaroonEnds)(bytes);
4946
5470
  });
4947
5471
  for (let offset = 0; offset + 8 <= digits.length; offset++) {
4948
- if (digits.slice(offset, offset + 4).toLowerCase() !== '0201' &&
4949
- digits.slice(offset, offset + 4).toLowerCase() !== '0202')
5472
+ if (!MACAROON_HEX_STARTS.has(digits.slice(offset, offset + 4).toLowerCase()))
4950
5473
  continue;
4951
5474
  const length = phases[offset % 2](Math.floor(offset / 2));
4952
5475
  if (length === null)
@@ -4984,8 +5507,7 @@ function spans(text, source) {
4984
5507
  const digits = digitPositions.map(position => value.charAt(position)).join('');
4985
5508
  const phases = [0, 1].map(phase => (0, recognition_js_1.macaroonEnds)(Buffer.from(digits.slice(phase), 'hex')));
4986
5509
  for (let offset = 0; offset + 8 <= digits.length; offset++) {
4987
- if (digits.slice(offset, offset + 4).toLowerCase() !== '0201' &&
4988
- digits.slice(offset, offset + 4).toLowerCase() !== '0202')
5510
+ if (!MACAROON_HEX_STARTS.has(digits.slice(offset, offset + 4).toLowerCase()))
4989
5511
  continue;
4990
5512
  const length = phases[offset % 2](Math.floor(offset / 2));
4991
5513
  if (length === null)
@@ -4996,6 +5518,52 @@ function spans(text, source) {
4996
5518
  offset += length * 2 - 1;
4997
5519
  }
4998
5520
  }
5521
+ }
5522
+ // A backslash, "%" and "&" start the escapes the decoding layers interpret. An escape-like prefix
5523
+ // ("\0201...", "%0201...") must not hide a hex credential by being decoded together with its
5524
+ // first digits, so hex is also recognised in the literal text (whitespace compacted only).
5525
+ function literalHexSpans(text) {
5526
+ if (!/[\\%&]/.test(text))
5527
+ return [];
5528
+ const { value, starts, ends } = normalized(text, true, undefined, false);
5529
+ const found = [];
5530
+ hexSpans(value, starts, ends, found);
5531
+ return found;
5532
+ }
5533
+ function spans(text, source, backslashPairs = true) {
5534
+ const { value, starts, ends } = normalized(text, true, source, true, backslashPairs);
5535
+ const found = [];
5536
+ const raw = normalized(text, false, source, true, backslashPairs);
5537
+ // Decode each decimal byte array once, then redact only confirmed macaroon
5538
+ // spans. Ordinary arrays and any surrounding values retain their source text.
5539
+ const decimalArrays = /(?:0x[0-9a-f]{2}(?:[^0-9A-Za-z\\]{1,3}0x[0-9a-f]{2})+|\d{1,3}(?:[^0-9A-Za-z\\]{1,3}\d{1,3})+)/gi;
5540
+ for (let run = decimalArrays.exec(raw.value); run; run = decimalArrays.exec(raw.value)) {
5541
+ const tokens = [...run[0].matchAll(/0x[0-9a-f]{2}|\d+/gi)];
5542
+ const values = tokens.map(token => Number(token[0]));
5543
+ for (let segmentStart = 0; segmentStart < values.length;) {
5544
+ let segmentEnd = segmentStart;
5545
+ while (segmentEnd < values.length && values[segmentEnd] <= 255)
5546
+ segmentEnd++;
5547
+ if (segmentEnd > segmentStart) {
5548
+ const bytes = Buffer.from(values.slice(segmentStart, segmentEnd));
5549
+ const lengthAt = (0, recognition_js_1.macaroonEnds)(bytes);
5550
+ for (let pos = 0; pos < bytes.length; pos++) {
5551
+ if (bytes[pos] !== 2)
5552
+ continue;
5553
+ const length = lengthAt(pos);
5554
+ if (length === null)
5555
+ continue;
5556
+ const begin = run.index + tokens[segmentStart + pos].index;
5557
+ const last = tokens[segmentStart + pos + length - 1];
5558
+ const end = run.index + last.index + last[0].length;
5559
+ found.push([raw.starts[begin], raw.ends[end - 1]]);
5560
+ pos += length - 1;
5561
+ }
5562
+ }
5563
+ segmentStart = segmentEnd + 1;
5564
+ }
5565
+ }
5566
+ hexSpans(value, starts, ends, found);
4999
5567
  if (raw.value.includes(String.fromCharCode(2))) {
5000
5568
  // Raw latin1 and recovered escaped bytes use the same structured decoder.
5001
5569
  // Preserve offsets through JSON escapes and UTF-16 zero-byte padding.
@@ -5122,7 +5690,12 @@ function spans(text, source) {
5122
5690
  return found.sort((a, b) => a[0] - b[0]);
5123
5691
  }
5124
5692
  function redactEncodedCredentials(text) {
5125
- const matches = [];
5693
+ const matches = literalHexSpans(text);
5694
+ // A stray backslash before an escaped credential (a backslash, then \x02\x01...) pairs with
5695
+ // the first escape's backslash and hides its byte: text holding two adjacent backslashes is
5696
+ // read once more with every backslash starting an escape on its own.
5697
+ if (text.includes('\\\\'))
5698
+ matches.push(...spans(text, undefined, false));
5126
5699
  let source;
5127
5700
  // A fixed number of escape layers keeps work bounded. Compose source offsets
5128
5701
  // on every pass and inspect each representation before further normalization.
@@ -5154,10 +5727,11 @@ function redactEncodedCredentials(text) {
5154
5727
  "use strict";
5155
5728
  exports.__esModule = true;
5156
5729
  exports.DecodeError = void 0;
5730
+ exports.macaroonEosEnd = macaroonEosEnd;
5157
5731
  exports.macaroonPacketRange = macaroonPacketRange;
5158
5732
  exports.macaroonSectionRange = macaroonSectionRange;
5159
5733
  exports.parseTimeBefore = parseTimeBefore;
5160
- exports.canonicalMacaroonLength = canonicalMacaroonLength;
5734
+ exports.canonicalMacaroonForm = canonicalMacaroonForm;
5161
5735
  exports.decodeMacaroon = decodeMacaroon;
5162
5736
  exports.macaroonId = macaroonId;
5163
5737
  exports.encodeMacaroon = encodeMacaroon;
@@ -5189,29 +5763,57 @@ function varint(data, start, end, stage) {
5189
5763
  if (i === 9 && byte > 1)
5190
5764
  return fail(stage);
5191
5765
  value |= BigInt(byte & 0x7f) << BigInt(i * 7);
5192
- if ((byte & 0x80) === 0) {
5193
- if (stage === 'packet' && i > 0 && byte === 0)
5194
- return fail(stage);
5766
+ if ((byte & 0x80) === 0)
5195
5767
  return [value, offset + 1];
5768
+ }
5769
+ return fail(stage);
5770
+ }
5771
+ // go-macaroon v2.0.0 (LND v0.21.4) reads packet kinds and lengths with
5772
+ // binary.Uvarint: up to 10 bytes, non-minimal encodings accepted, values above
5773
+ // 0x7fffffff refused. Anyone holding a macaroon can re-frame it this way without
5774
+ // the root key (the signature covers the identifier and caveat bytes, not the
5775
+ // framing), so the decoder accepts exactly what LND accepts.
5776
+ const MAX_PACKET_VALUE = 0x7fffffff;
5777
+ function packetVarint(data, start) {
5778
+ let value = 0;
5779
+ for (let i = 0; i < 10; i++) {
5780
+ const offset = start + i;
5781
+ if (offset >= data.length)
5782
+ return null;
5783
+ const byte = data[offset];
5784
+ if (i === 9 && byte > 1)
5785
+ return null;
5786
+ const bits = byte & 0x7f;
5787
+ if (bits !== 0) {
5788
+ if (i >= 5)
5789
+ return null;
5790
+ value += bits * 2 ** (7 * i);
5791
+ if (value > MAX_PACKET_VALUE)
5792
+ return null;
5196
5793
  }
5794
+ if ((byte & 0x80) === 0)
5795
+ return [value, offset + 1];
5197
5796
  }
5198
- return fail(stage);
5797
+ return null;
5798
+ }
5799
+ /** End offset of an end-of-section marker at `start` (any varint encoding of 0), else null. */
5800
+ function macaroonEosEnd(data, start) {
5801
+ const item = packetVarint(data, start);
5802
+ return item !== null && item[0] === 0 ? item[1] : null;
5199
5803
  }
5200
5804
  // Shared structural parser: decoder and bounded recognition use identical
5201
- // packet ordering, canonical lengths, and caveat-location rules.
5805
+ // packet ordering, varint, end-of-section and caveat-location rules.
5202
5806
  function macaroonPacketRange(data, start) {
5203
- const kind = data[start];
5204
- if (![1, 2, 4, 6].includes(kind))
5807
+ const kindItem = packetVarint(data, start);
5808
+ if (kindItem === null || ![1, 2, 4, 6].includes(kindItem[0]))
5205
5809
  return null;
5206
- try {
5207
- const [length, afterLength] = varint(data, start + 1, data.length, 'packet');
5208
- if (length > BigInt(data.length - afterLength))
5209
- return null;
5210
- return { kind, start: afterLength, end: afterLength + Number(length) };
5211
- }
5212
- catch {
5810
+ const lengthItem = packetVarint(data, kindItem[1]);
5811
+ if (lengthItem === null)
5213
5812
  return null;
5214
- }
5813
+ const [length, afterLength] = lengthItem;
5814
+ if (length > data.length - afterLength)
5815
+ return null;
5816
+ return { kind: kindItem[0], start: afterLength, end: afterLength + length };
5215
5817
  }
5216
5818
  function macaroonSectionRange(data, start, header) {
5217
5819
  let pos = start;
@@ -5219,7 +5821,8 @@ function macaroonSectionRange(data, start, header) {
5219
5821
  let location = null;
5220
5822
  let identifier = null;
5221
5823
  let vid = null;
5222
- while (pos < data.length && data[pos] !== 0) {
5824
+ let end = null;
5825
+ while (pos < data.length && (end = macaroonEosEnd(data, pos)) === null) {
5223
5826
  const item = macaroonPacketRange(data, pos);
5224
5827
  if (!item || item.kind <= lastKind || ![1, 2, 4].includes(item.kind) || header && item.kind === 4)
5225
5828
  return null;
@@ -5232,9 +5835,9 @@ function macaroonSectionRange(data, start, header) {
5232
5835
  lastKind = item.kind;
5233
5836
  pos = item.end;
5234
5837
  }
5235
- if (pos >= data.length || identifier === null || !header && location && location.end > location.start && vid === null)
5838
+ if (end === null || identifier === null || !header && location && location.end > location.start && vid === null)
5236
5839
  return null;
5237
- return { location, identifier, vid, end: pos + 1 };
5840
+ return { location, identifier, vid, end };
5238
5841
  }
5239
5842
  function packet(data, start) {
5240
5843
  const item = macaroonPacketRange(data, start);
@@ -5250,6 +5853,54 @@ function section(data, start, header) {
5250
5853
  identifier: data.subarray(item.identifier.start, item.identifier.end),
5251
5854
  vid: item.vid ? data.subarray(item.vid.start, item.vid.end) : null }, item.end];
5252
5855
  }
5856
+ // Group nesting LND's protobuf decoder accepts (protowire's DefaultRecursionLimit of 10,000
5857
+ // lets a group hold 10,000 nested groups). Deeper nesting is refused there and here.
5858
+ const MAX_GROUP_DEPTH = 10_001;
5859
+ const MAX_GROUP_FIELD = 0x7fffffffn;
5860
+ function skipValue(data, pos, wire) {
5861
+ switch (wire) {
5862
+ case 0: return varint(data, pos, data.length, 'identifier-proto')[1];
5863
+ case 1: return data.length - pos < 8 ? fail('identifier-proto') : pos + 8;
5864
+ case 2: {
5865
+ const [length, afterLength] = varint(data, pos, data.length, 'identifier-proto');
5866
+ if (length > BigInt(data.length - afterLength))
5867
+ return fail('identifier-proto');
5868
+ return afterLength + Number(length);
5869
+ }
5870
+ case 5: return data.length - pos < 4 ? fail('identifier-proto') : pos + 4;
5871
+ default: return fail('identifier-proto');
5872
+ }
5873
+ }
5874
+ // An unknown group (wire type 3) is skipped as protowire.ConsumeFieldValue skips it: every
5875
+ // member is consumed, nested groups must close with their own field number, and members may
5876
+ // use field numbers up to 2^31-1. Iterative, with only the open field numbers kept.
5877
+ function skipGroup(data, pos, field) {
5878
+ const open = [field];
5879
+ while (open.length > 0) {
5880
+ const [tag, afterTag] = varint(data, pos, data.length, 'identifier-proto');
5881
+ const member = tag >> 3n;
5882
+ const wire = Number(tag & 7n);
5883
+ if (member === 0n || member > MAX_GROUP_FIELD)
5884
+ return fail('identifier-proto');
5885
+ pos = afterTag;
5886
+ if (wire === 3) {
5887
+ if (open.length >= MAX_GROUP_DEPTH)
5888
+ return fail('identifier-proto');
5889
+ open.push(member);
5890
+ }
5891
+ else if (wire === 4) {
5892
+ if (open.pop() !== member)
5893
+ return fail('identifier-proto');
5894
+ }
5895
+ else {
5896
+ pos = skipValue(data, pos, wire);
5897
+ }
5898
+ }
5899
+ return pos;
5900
+ }
5901
+ // The protobuf wire format as LND's decoder (golang/protobuf v1.5.4 over protobuf-go) reads it:
5902
+ // varints up to 10 bytes (non-minimal accepted), field numbers 1 to 2^29-1, wire types 0, 1,
5903
+ // 2, 5 and groups (3 ... 4); a lone end-group or wire types 6 and 7 are refused.
5253
5904
  function protobuf(data, visit) {
5254
5905
  let pos = 0;
5255
5906
  while (pos < data.length) {
@@ -5258,55 +5909,40 @@ function protobuf(data, visit) {
5258
5909
  const wire = Number(tag & 7n);
5259
5910
  if (field === 0n || field > MAX_PROTO_FIELD)
5260
5911
  return fail('identifier-proto');
5261
- pos = afterTag;
5262
5912
  let value = null;
5263
- switch (wire) {
5264
- case 0:
5265
- [, pos] = varint(data, pos, data.length, 'identifier-proto');
5266
- break;
5267
- case 1:
5268
- if (data.length - pos < 8)
5269
- return fail('identifier-proto');
5270
- pos += 8;
5271
- break;
5272
- case 2: {
5273
- const [length, afterLength] = varint(data, pos, data.length, 'identifier-proto');
5274
- if (length > BigInt(data.length - afterLength))
5275
- return fail('identifier-proto');
5276
- pos = afterLength + Number(length);
5277
- value = data.subarray(afterLength, pos);
5278
- break;
5279
- }
5280
- case 5:
5281
- if (data.length - pos < 4)
5282
- return fail('identifier-proto');
5283
- pos += 4;
5284
- break;
5285
- default:
5913
+ if (wire === 2) {
5914
+ const [length, afterLength] = varint(data, afterTag, data.length, 'identifier-proto');
5915
+ if (length > BigInt(data.length - afterLength))
5286
5916
  return fail('identifier-proto');
5917
+ pos = afterLength + Number(length);
5918
+ value = data.subarray(afterLength, pos);
5919
+ }
5920
+ else {
5921
+ pos = wire === 3 ? skipGroup(data, afterTag, field) : skipValue(data, afterTag, wire);
5287
5922
  }
5288
5923
  visit(Number(field), wire, value);
5289
5924
  }
5290
5925
  }
5291
- function protobufString(value) {
5926
+ /** A proto3 string field: LND refuses the whole identifier when one is not valid UTF-8. */
5927
+ function checkedString(value) {
5292
5928
  try {
5293
- return utf8.decode(value);
5929
+ utf8.decode(value);
5294
5930
  }
5295
5931
  catch {
5296
- return fail('identifier-proto');
5297
- }
5298
- }
5299
- // Limits on what one identifier may expand to. Permissions are entity x action
5300
- // products, so a small identifier could otherwise decode to hundreds of megabytes.
5301
- // LND v0.21.4 has 164 RPC methods (longest URI 49 bytes) across 19 entities whose
5302
- // longest name is 8 bytes, so a real macaroon carries at most about 200 permissions.
5303
- // The entity is repeated in every permission it applies to; capping it at 16 bytes
5304
- // bounds the expansion to about 6 bytes of permission text per identifier byte.
5305
- const IDENTIFIER_LIMITS = {
5306
- operations: 256, actionsPerOperation: 512, permissions: 1024,
5307
- entityBytes: 16, actionBytes: 256, permissionBytes: 65_536,
5308
- };
5309
- /** At most this many caveats per macaroon; LND adds a handful. */
5932
+ fail('identifier-proto');
5933
+ }
5934
+ }
5935
+ // Bounds on what one identifier may expand to. Permissions are entity x action products, so a
5936
+ // small identifier could otherwise decode to hundreds of megabytes. LND v0.21.4 has 164 RPC
5937
+ // methods (longest URI 49 bytes) across 19 entities whose longest name is 8 bytes, so a real
5938
+ // macaroon carries at most about 200 permissions. The entity is repeated in every permission it
5939
+ // applies to; leaving out entities over 16 bytes (and actions over 256 bytes) bounds the
5940
+ // expansion to about 6 bytes of permission text per identifier byte, and the totals bound it
5941
+ // absolutely. Exceeding a bound never refuses the macaroon (LND honours it): what fits is
5942
+ // decoded, the rest is left out, and the warning identifier-exceeds-bounds makes the grade fail
5943
+ // closed.
5944
+ const IDENTIFIER_BOUNDS = { permissions: 1024, permissionBytes: 65_536, entityBytes: 16, actionBytes: 256 };
5945
+ /** Caveats kept per macaroon; LND adds a handful. Further caveats are parsed and counted, not kept. */
5310
5946
  const MAX_CAVEATS = 4096;
5311
5947
  function identifier(data) {
5312
5948
  if (data[0] !== 3)
@@ -5315,6 +5951,7 @@ function identifier(data) {
5315
5951
  const permissions = [];
5316
5952
  let operations = 0;
5317
5953
  let permissionBytes = 0;
5954
+ let exceedsBounds = false;
5318
5955
  protobuf(data.subarray(1), (field, wire, value) => {
5319
5956
  if (wire !== 2 || value === null)
5320
5957
  return;
@@ -5322,35 +5959,42 @@ function identifier(data) {
5322
5959
  storageId = value;
5323
5960
  if (field !== 3)
5324
5961
  return;
5325
- if (++operations > IDENTIFIER_LIMITS.operations)
5326
- return fail('identifier-limits');
5327
- let entity = '';
5328
- const actions = [];
5962
+ // Bakery expands every op's actions in order (an empty entity included) and requires only
5963
+ // that the first op has an action. The entity is a proto3 string: the last one wins.
5964
+ const first = operations++ === 0;
5965
+ let entity = Buffer.alloc(0);
5966
+ let actions = 0;
5329
5967
  protobuf(value, (opField, opWire, opValue) => {
5330
- if (opWire !== 2 || opValue === null)
5968
+ if (opWire !== 2 || opValue === null || (opField !== 1 && opField !== 2))
5331
5969
  return;
5332
- if (opField === 1) {
5333
- if (opValue.length > IDENTIFIER_LIMITS.entityBytes)
5334
- return fail('identifier-limits');
5335
- entity = protobufString(opValue);
5336
- }
5337
- if (opField === 2) {
5338
- if (opValue.length > IDENTIFIER_LIMITS.actionBytes || actions.length >= IDENTIFIER_LIMITS.actionsPerOperation)
5339
- return fail('identifier-limits');
5340
- actions.push(protobufString(opValue));
5341
- }
5970
+ checkedString(opValue);
5971
+ if (opField === 1)
5972
+ entity = opValue;
5973
+ else
5974
+ actions++;
5342
5975
  });
5343
- if (!entity || actions.length === 0)
5976
+ if (first && actions === 0)
5344
5977
  return fail('identifier-proto');
5345
- // Checked before building any string, so the expansion itself is bounded.
5346
- if (permissions.length + actions.length > IDENTIFIER_LIMITS.permissions)
5347
- return fail('identifier-limits');
5348
- for (const action of actions) {
5349
- permissionBytes += entity.length + 1 + action.length;
5350
- if (permissionBytes > IDENTIFIER_LIMITS.permissionBytes)
5351
- return fail('identifier-limits');
5352
- permissions.push(`${entity}:${action}`);
5978
+ if (actions === 0 || exceedsBounds)
5979
+ return;
5980
+ if (entity.length > IDENTIFIER_BOUNDS.entityBytes) {
5981
+ exceedsBounds = true;
5982
+ return;
5353
5983
  }
5984
+ const entityText = entity.toString('utf8');
5985
+ protobuf(value, (opField, opWire, action) => {
5986
+ if (opField !== 2 || opWire !== 2 || action === null || exceedsBounds)
5987
+ return;
5988
+ const size = entity.length + 1 + action.length;
5989
+ // Checked before building any string, so the expansion itself is bounded.
5990
+ if (action.length > IDENTIFIER_BOUNDS.actionBytes || permissions.length >= IDENTIFIER_BOUNDS.permissions
5991
+ || permissionBytes + size > IDENTIFIER_BOUNDS.permissionBytes) {
5992
+ exceedsBounds = true;
5993
+ return;
5994
+ }
5995
+ permissionBytes += size;
5996
+ permissions.push(`${entityText}:${action.toString('utf8')}`);
5997
+ });
5354
5998
  });
5355
5999
  if (storageId === null)
5356
6000
  return fail('identifier-root-key-id');
@@ -5364,7 +6008,7 @@ function identifier(data) {
5364
6008
  const rootKeyId = BigInt(decimal);
5365
6009
  if (rootKeyId > MAX_UINT64)
5366
6010
  return fail('identifier-root-key-id');
5367
- return { rootKeyId, permissions };
6011
+ return { rootKeyId, permissions, exceedsBounds };
5368
6012
  }
5369
6013
  function caveat(sectionValue, warnings) {
5370
6014
  if (sectionValue.vid !== null && sectionValue.vid.length > 0) {
@@ -5381,26 +6025,52 @@ function caveat(sectionValue, warnings) {
5381
6025
  }
5382
6026
  return { type: 'unknown', value: condition };
5383
6027
  }
6028
+ /** Days from 1970-01-01 to a proleptic Gregorian date (Howard Hinnant's days_from_civil). */
6029
+ function daysFromCivil(year, month, day) {
6030
+ const y = month <= 2 ? year - 1 : year;
6031
+ const era = Math.floor(y / 400);
6032
+ const yearOfEra = y - era * 400;
6033
+ const dayOfYear = Math.floor((153 * (month + (month > 2 ? -3 : 9)) + 2) / 5) + day - 1;
6034
+ return era * 146_097 + yearOfEra * 365 + Math.floor(yearOfEra / 4) - Math.floor(yearOfEra / 100) + dayOfYear - 719_468;
6035
+ }
6036
+ /**
6037
+ * The instant (Unix nanoseconds) LND enforces for a time-before value. Bakery checks it with Go's
6038
+ * time.Parse(time.RFC3339Nano), whose general parser (the fallback after the strict RFC 3339
6039
+ * path) accepts a one- or two-digit hour, a comma or a period before any number of fraction
6040
+ * digits (nanoseconds kept), and a zone offset of up to 24 hours and 60 minutes.
6041
+ */
5384
6042
  function parseTimeBefore(value) {
5385
- const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:[.,](\d+))?(Z|([+-])(\d{2}):(\d{2}))$/.exec(value);
6043
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{1,2}):(\d{2}):(\d{2})(?:[.,](\d+))?(Z|([+-])(\d{2}):(\d{2}))$/.exec(value);
5386
6044
  if (!match)
5387
6045
  return undefined;
5388
- const [, yearText, monthText, dayText, hourText, minuteText, secondText, fraction, zone, , offsetHour, offsetMinute] = match;
6046
+ const [, yearText, monthText, dayText, hourText, minuteText, secondText, fraction, zone, sign, offsetHourText, offsetMinuteText] = match;
5389
6047
  const year = Number(yearText), month = Number(monthText), day = Number(dayText);
5390
6048
  const hour = Number(hourText), minute = Number(minuteText), second = Number(secondText);
6049
+ const offsetHour = zone === 'Z' ? 0 : Number(offsetHourText), offsetMinute = zone === 'Z' ? 0 : Number(offsetMinuteText);
5391
6050
  const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
5392
6051
  const daysInMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
5393
6052
  if (month < 1 || month > 12 || day < 1 || day > daysInMonth[month - 1]
5394
- || hour > 23 || minute > 59 || second > 59
5395
- || (zone !== 'Z' && (Number(offsetHour) > 23 || Number(offsetMinute) > 59)))
5396
- return undefined;
5397
- // Go's time.Parse(RFC3339Nano) accepts a comma or a period before the fraction.
5398
- const seconds = Date.parse(value.replace(/[.,]\d+(?=Z|[+-]\d{2}:\d{2}$)/, ''));
5399
- if (!Number.isFinite(seconds))
6053
+ || hour > 23 || minute > 59 || second > 59 || offsetHour > 24 || offsetMinute > 60)
5400
6054
  return undefined;
5401
- return BigInt(seconds) * 1000000n + BigInt((fraction ?? '').slice(0, 9).padEnd(9, '0'));
6055
+ const offset = (sign === '-' ? -1 : 1) * (offsetHour * 3600 + offsetMinute * 60);
6056
+ const seconds = daysFromCivil(year, month, day) * 86_400 + hour * 3600 + minute * 60 + second - offset;
6057
+ return BigInt(seconds) * 1000000000n + BigInt((fraction ?? '').slice(0, 9).padEnd(9, '0'));
6058
+ }
6059
+ function varintSize(value) {
6060
+ let size = 1;
6061
+ while (value >= 128) {
6062
+ value = Math.floor(value / 128);
6063
+ size++;
6064
+ }
6065
+ return size;
6066
+ }
6067
+ const packetSize = (kind, value) => varintSize(kind) + varintSize(value.length) + value.length;
6068
+ /** Size of a section as go-macaroon's MarshalBinary writes it: empty location and verification ID packets are omitted. */
6069
+ function canonicalSectionSize(value) {
6070
+ return (value.location?.length ? packetSize(1, value.location) : 0) + packetSize(2, value.identifier)
6071
+ + (value.vid?.length ? packetSize(4, value.vid) : 0) + 1;
5402
6072
  }
5403
- function parsedMacaroon(data) {
6073
+ function parsedMacaroon(data, onCaveat) {
5404
6074
  if (data[0] !== 2) {
5405
6075
  if (data.subarray(0, 3).toString('ascii') === '001') {
5406
6076
  throw new errors_js_1.DecodeError('v2-header', 'macaroon v1 format is not supported');
@@ -5408,19 +6078,25 @@ function parsedMacaroon(data) {
5408
6078
  return fail('v2-header');
5409
6079
  }
5410
6080
  const [header, afterHeader] = section(data, 1, true);
6081
+ let canonicalLength = 1 + canonicalSectionSize(header);
5411
6082
  const caveats = [];
6083
+ let omittedCaveats = 0;
5412
6084
  let pos = afterHeader;
5413
6085
  for (;;) {
5414
6086
  if (pos >= data.length)
5415
6087
  return fail('packet');
5416
- if (data[pos] === 0) {
5417
- pos++;
6088
+ const eos = macaroonEosEnd(data, pos);
6089
+ if (eos !== null) {
6090
+ pos = eos;
5418
6091
  break;
5419
6092
  }
5420
- if (caveats.length >= MAX_CAVEATS)
5421
- return fail('packet');
5422
6093
  const [next, after] = section(data, pos, false);
5423
- caveats.push(next);
6094
+ if (caveats.length < MAX_CAVEATS)
6095
+ caveats.push(next);
6096
+ else
6097
+ omittedCaveats++;
6098
+ onCaveat?.(next);
6099
+ canonicalLength += canonicalSectionSize(next);
5424
6100
  pos = after;
5425
6101
  }
5426
6102
  if (pos >= data.length)
@@ -5428,15 +6104,32 @@ function parsedMacaroon(data) {
5428
6104
  const [kind, signature, end] = packet(data, pos);
5429
6105
  if (kind !== 6 || signature.length !== 32)
5430
6106
  return fail('packet');
5431
- return { parsed: { header, caveats, signature }, end };
6107
+ canonicalLength += 1 + packetSize(6, signature);
6108
+ return { parsed: { header, caveats, signature }, end, canonicalLength, omittedCaveats };
5432
6109
  }
5433
- // Generic Secret containers retain malformed data. Only a fully valid offline
5434
- // macaroon receives the canonical public identity ending at its signature.
5435
- function canonicalMacaroonLength(data) {
6110
+ /**
6111
+ * The canonical public identity of a fully valid offline macaroon: the bytes go-macaroon's
6112
+ * MarshalBinary writes for it (minimal varints, one-byte end-of-section markers, no empty
6113
+ * location or verification ID packets). Every macaroon LND or macwarden writes is already in
6114
+ * this form: `length` is then the length of that prefix and `reencoded` is null. A re-framed
6115
+ * copy of the same credential (which LND accepts) is re-encoded, so both share one fingerprint.
6116
+ * Trailing bytes are never part of it. Generic Secret containers holding anything else get null
6117
+ * and keep hashing their own bytes.
6118
+ */
6119
+ function canonicalMacaroonForm(data) {
5436
6120
  try {
5437
- const { parsed, end } = parsedMacaroon(data);
6121
+ const { parsed, end, canonicalLength } = parsedMacaroon(data);
5438
6122
  identifier(parsed.header.identifier);
5439
- return end;
6123
+ if (canonicalLength === end)
6124
+ return { length: end, reencoded: null };
6125
+ // Any non-canonical framing only adds bytes, so equal lengths mean equal bytes.
6126
+ const marshalled = (value) => encodeSection({ identifier: value.identifier,
6127
+ location: value.location?.length ? value.location : null, vid: value.vid?.length ? value.vid : null });
6128
+ const pieces = [Buffer.from([2]), ...marshalled(parsed.header)];
6129
+ parsedMacaroon(data, value => pieces.push(...marshalled(value)));
6130
+ pieces.push(Buffer.from([0]), encodePacket(6, parsed.signature));
6131
+ const reencoded = Buffer.concat(pieces);
6132
+ return { length: reencoded.length, reencoded };
5440
6133
  }
5441
6134
  catch {
5442
6135
  return null;
@@ -5444,14 +6137,21 @@ function canonicalMacaroonLength(data) {
5444
6137
  }
5445
6138
  function decodeMacaroon(mac) {
5446
6139
  const data = (0, secret_capability_js_1.revealSecret)(mac);
5447
- const { parsed: { header, caveats }, end } = parsedMacaroon(data);
5448
- const { rootKeyId, permissions } = identifier(header.identifier);
6140
+ const { parsed: { header, caveats }, end, omittedCaveats } = parsedMacaroon(data);
6141
+ const { rootKeyId, permissions, exceedsBounds } = identifier(header.identifier);
5449
6142
  const warnings = [];
6143
+ // Fail closed: what could not be kept is named, and the grade treats it as unassessed.
6144
+ if (exceedsBounds)
6145
+ warnings.push('identifier-exceeds-bounds');
6146
+ if (omittedCaveats > 0)
6147
+ warnings.push('caveats-exceed-bounds');
5450
6148
  const decodedCaveats = caveats.map(value => caveat(value, warnings));
5451
6149
  if (end < data.length)
5452
6150
  warnings.push('trailing-bytes');
5453
- // One fingerprint definition: the canonical container hashes its own bytes.
5454
- const canonical = new secret_js_1.Secret(data.subarray(0, end));
6151
+ // One fingerprint definition: the canonical container (go-macaroon's MarshalBinary form,
6152
+ // which is the input up to its signature for every macaroon LND writes) hashes its own bytes.
6153
+ const form = canonicalMacaroonForm(data);
6154
+ const canonical = new secret_js_1.Secret(form?.reencoded ?? data.subarray(0, end));
5455
6155
  const fingerprint = canonical.fingerprint;
5456
6156
  const facts = {
5457
6157
  location: header.location?.toString('utf8') ?? null,
@@ -5715,8 +6415,10 @@ function macaroonEnds(bytes) {
5715
6415
  return valid;
5716
6416
  }
5717
6417
  for (let pos = bytes.length - 1; pos >= 0; pos--) {
5718
- if (bytes[pos] === 0) {
5719
- const signature = packet(pos + 1);
6418
+ // Any varint encoding of 0 ends a section, as in go-macaroon.
6419
+ const eos = (0, index_js_1.macaroonEosEnd)(bytes, pos);
6420
+ if (eos !== null) {
6421
+ const signature = packet(eos);
5720
6422
  if (signature?.[0] === 6n && signature[1] === 32n)
5721
6423
  tails[pos] = signature[3];
5722
6424
  continue;
@@ -5821,6 +6523,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5821
6523
  };
5822
6524
  exports.__esModule = true;
5823
6525
  exports.grade = grade;
6526
+ exports.gradeFacts = gradeFacts;
5824
6527
  exports.describe = describe;
5825
6528
  const lnd_permissions_json_1 = __importDefault(require("./lnd-permissions.json"));
5826
6529
  const classes = ['READ', 'RECEIVE', 'OPERATE', 'SPEND', 'ADMIN'];
@@ -5867,12 +6570,19 @@ function classOfUri(permission) {
5867
6570
  }
5868
6571
  return highest;
5869
6572
  }
5870
- function grade(permissions) {
6573
+ /**
6574
+ * `incomplete` is true when the decoder could not keep every permission of the identifier
6575
+ * (warning identifier-exceeds-bounds): the permissions left out count as unassessed, so the
6576
+ * grade fails closed to UNASSESSED unless the kept ones already give ADMIN.
6577
+ */
6578
+ function grade(permissions, incomplete = false) {
5871
6579
  const assessed = new Map();
5872
6580
  const unassessed = new Set();
5873
6581
  const warnings = new Set();
5874
6582
  const unique = new Set(permissions);
5875
- if (unique.size === 0)
6583
+ if (incomplete)
6584
+ warnings.add('identifier-exceeds-bounds');
6585
+ else if (unique.size === 0)
5876
6586
  warnings.add('no-permissions');
5877
6587
  for (const permission of unique) {
5878
6588
  const isUri = uriShape.test(permission);
@@ -5890,12 +6600,16 @@ function grade(permissions) {
5890
6600
  if (!highest || classes.indexOf(permissionClass) > classes.indexOf(highest))
5891
6601
  highest = permissionClass;
5892
6602
  }
5893
- const resultGrade = unassessed.size > 0 && highest !== 'ADMIN'
6603
+ const resultGrade = (unassessed.size > 0 || incomplete) && highest !== 'ADMIN'
5894
6604
  ? 'UNASSESSED' : highest ?? 'UNASSESSED';
5895
6605
  const reasons = resultGrade === 'UNASSESSED' ? [] : byteSort([...assessed].filter(([, permissionClass]) => permissionClass === resultGrade)
5896
6606
  .map(([permission]) => permission));
5897
6607
  return { grade: resultGrade, reasons, warnings: byteSort(warnings), unassessed: byteSort(unassessed) };
5898
6608
  }
6609
+ /** The grade of decoded facts, failing closed when the identifier exceeded the decoder's bounds. */
6610
+ function gradeFacts(facts) {
6611
+ return grade(facts.permissions, facts.warnings.includes('identifier-exceeds-bounds'));
6612
+ }
5899
6613
  function describe(facts) {
5900
6614
  const warnings = new Set(facts.warnings);
5901
6615
  if (facts.rootKeyId === 0n)
@@ -6222,6 +6936,7 @@ exports.openGrpcStream = openGrpcStream;
6222
6936
  // applies here. Framing: 1-byte compression flag (must be 0), 4-byte length, payload.
6223
6937
  const node_http2_1 = require("node:http2");
6224
6938
  const errors_js_1 = require("../errors.js");
6939
+ const tls_identity_js_1 = require("./tls-identity.js");
6225
6940
  exports.MAX_GRPC_MESSAGE = 200 * 1024 * 1024;
6226
6941
  /** A message that stops arriving midway (a stuck or hostile peer) ends the call after this. */
6227
6942
  exports.STALL_TIMEOUT_MS = 60_000;
@@ -6312,7 +7027,7 @@ function openGrpcStream(target, onMessage) {
6312
7027
  const servername = /^[0-9.]+$|:/.test(target.host) ? undefined : target.host;
6313
7028
  try {
6314
7029
  session = (0, node_http2_1.connect)(`https://${target.host.includes(':') ? `[${target.host}]` : target.host}:${target.port}`, {
6315
- ca: target.ca, rejectUnauthorized: true, servername,
7030
+ ca: target.ca, rejectUnauthorized: true, servername, ...(0, tls_identity_js_1.identityCheck)(target.host),
6316
7031
  });
6317
7032
  }
6318
7033
  catch {
@@ -6529,6 +7244,7 @@ const node_https_1 = require("node:https");
6529
7244
  const errors_js_1 = require("../errors.js");
6530
7245
  const secret_js_1 = require("../secret.js");
6531
7246
  const grpc_js_1 = require("./grpc.js");
7247
+ const tls_identity_js_1 = require("./tls-identity.js");
6532
7248
  const middleware_proto_js_1 = require("./middleware-proto.js");
6533
7249
  const MAX_RESPONSE = 1_048_576;
6534
7250
  const TIMEOUT_MS = 10_000;
@@ -6616,6 +7332,10 @@ function ids(body) {
6616
7332
  }
6617
7333
  function createLndAdapter(config) {
6618
7334
  const peer = endpoint(config);
7335
+ // WHATWG URL keeps an IPv6 literal in brackets ("[::1]"); the transport and its TLS
7336
+ // identity check need the bare address, or Node treats it as a DNS name and the
7337
+ // certificate's IP SAN never matches. Printed URLs and commands keep the brackets.
7338
+ const transportHost = peer.hostname.replace(/^\[(.*)\]$/, '$1');
6619
7339
  const ca = Buffer.from(config.tlsCert);
6620
7340
  const own = config.macaroon;
6621
7341
  function call(stage, method, path, payload) {
@@ -6646,8 +7366,8 @@ function createLndAdapter(config) {
6646
7366
  return;
6647
7367
  }
6648
7368
  try {
6649
- req = (0, node_https_1.request)({ protocol: 'https:', hostname: peer.hostname, port: peer.port || 443,
6650
- method, path, ca, rejectUnauthorized: true, agent: false,
7369
+ req = (0, node_https_1.request)({ protocol: 'https:', hostname: transportHost, port: peer.port || 443,
7370
+ method, path, ca, rejectUnauthorized: true, ...(0, tls_identity_js_1.identityCheck)(transportHost), agent: false,
6651
7371
  headers: { 'Grpc-Metadata-macaroon': (0, secret_capability_js_1.revealSecret)(own).toString('hex'),
6652
7372
  ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) } }, response => {
6653
7373
  const chunks = [];
@@ -7137,6 +7857,47 @@ function encodeFeedback(refMsgId, error = '') {
7137
7857
  return out.subarray(0, position);
7138
7858
  }
7139
7859
 
7860
+ },
7861
+ "lnd/tls-identity.js": (module, exports, require) => {
7862
+ "use strict";
7863
+ // Server identity check for an LND endpoint given as an IPv6 literal. Host names and IPv4
7864
+ // addresses keep Node's default check (no option is passed). Some supported Node releases
7865
+ // (22.x) pass the host through domainToASCII before matching, which turns "::1" into "" and
7866
+ // refuses every IPv6 certificate, even one whose IP address SAN matches. This replacement
7867
+ // is as strict as Node's own IP rule: only IP address SANs count (never the common name),
7868
+ // compared in canonical form, and quoted SAN lists (which need an escaping-aware parser)
7869
+ // are refused rather than parsed.
7870
+ exports.__esModule = true;
7871
+ exports.identityCheck = identityCheck;
7872
+ /** Canonical (WHATWG: compressed, lower case) form of an IPv6 address, or null. */
7873
+ function canonicalIPv6(address) {
7874
+ const bare = address.replace(/^\[(.*)\]$/, '$1');
7875
+ if (!bare.includes(':') || /[[\]/?#@]/.test(bare))
7876
+ return null;
7877
+ try {
7878
+ return new URL(`https://[${bare}]/`).hostname;
7879
+ }
7880
+ catch {
7881
+ return null;
7882
+ }
7883
+ }
7884
+ /** TLS options for `host`: a strict IP SAN check for an IPv6 literal, nothing otherwise. */
7885
+ function identityCheck(host) {
7886
+ const wanted = canonicalIPv6(host);
7887
+ if (wanted === null)
7888
+ return {};
7889
+ return {
7890
+ checkServerIdentity(_name, cert) {
7891
+ const altnames = cert.subjectaltname ?? '';
7892
+ const names = altnames.includes('"') ? [] : altnames.split(', ');
7893
+ if (names.some(name => name.startsWith('IP Address:') && canonicalIPv6(name.slice(11)) === wanted))
7894
+ return undefined;
7895
+ const reason = `IP: ${host} is not in the cert's list`;
7896
+ return Object.assign(new Error(`Hostname/IP does not match certificate's altnames: ${reason}`), { code: 'ERR_TLS_CERT_ALTNAME_INVALID', reason, host });
7897
+ },
7898
+ };
7899
+ }
7900
+
7140
7901
  },
7141
7902
  "online.js": (module, exports, require) => {
7142
7903
  "use strict";
@@ -7144,7 +7905,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
7144
7905
  return (mod && mod.__esModule) ? mod : { "default": mod };
7145
7906
  };
7146
7907
  exports.__esModule = true;
7147
- exports.LABEL_RULE = void 0;
7908
+ exports.readBoundedFile = exports.LABEL_RULE = void 0;
7148
7909
  exports.endpoint = endpoint;
7149
7910
  exports.credentialText = credentialText;
7150
7911
  exports.connectionProblem = connectionProblem;
@@ -7155,7 +7916,6 @@ exports.drift = drift;
7155
7916
  exports.samePermissions = samePermissions;
7156
7917
  exports.listIds = listIds;
7157
7918
  exports.failureDetails = failureDetails;
7158
- exports.readBoundedFile = readBoundedFile;
7159
7919
  exports.onlineAdapter = onlineAdapter;
7160
7920
  exports.shellQuote = shellQuote;
7161
7921
  exports.recoveryPath = recoveryPath;
@@ -7168,7 +7928,6 @@ const cancellation_js_1 = require("./cancellation.js");
7168
7928
  const node_fs_1 = require("node:fs");
7169
7929
  const node_path_1 = require("node:path");
7170
7930
  const errors_js_1 = require("./errors.js");
7171
- const protected_path_js_1 = require("./protected-path.js");
7172
7931
  const lnd_permissions_json_1 = __importDefault(require("./grade/lnd-permissions.json"));
7173
7932
  function endpoint(host) {
7174
7933
  if (!/:[0-9]{1,5}$/.test(host))
@@ -7231,36 +7990,8 @@ function failureDetails(error) {
7231
7990
  return { code: error.code, message: error.code === 4
7232
7991
  ? 'LND transport, TLS or authentication failure' : 'LND request failed' };
7233
7992
  }
7234
- async function readBoundedFile(path, io) {
7235
- if ((0, protected_path_js_1.isProtectedPath)(path))
7236
- throw new Error('protected file');
7237
- const resolved = await io.realpath(path);
7238
- if ((0, protected_path_js_1.isProtectedPath)(resolved))
7239
- throw new Error('protected file');
7240
- const before = await io.stat(resolved);
7241
- if (!before.isFile())
7242
- throw new Error('not a regular file');
7243
- const handle = await io.open(resolved, node_fs_1.constants.O_RDONLY | node_fs_1.constants.O_NONBLOCK | (node_fs_1.constants.O_NOFOLLOW ?? 0));
7244
- try {
7245
- const opened = await handle.stat();
7246
- if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino)
7247
- throw new Error('file changed');
7248
- const bytes = Buffer.allocUnsafe(1_048_577);
7249
- let total = 0;
7250
- while (total < bytes.length) {
7251
- const { bytesRead } = await handle.read(bytes, total, bytes.length - total, null);
7252
- if (!bytesRead)
7253
- break;
7254
- total += bytesRead;
7255
- }
7256
- if (total > 1_048_576)
7257
- throw new Error('file too large');
7258
- return bytes.subarray(0, total);
7259
- }
7260
- finally {
7261
- await handle.close();
7262
- }
7263
- }
7993
+ var bounded_file_js_1 = require("./bounded-file.js");
7994
+ module.exports = exports = { __proto__: null, ...exports, get readBoundedFile() { return bounded_file_js_1.readBoundedFile; } };
7264
7995
  function onlineAdapter(adapter) {
7265
7996
  return {
7266
7997
  getInfo: () => (0, cancellation_js_1.cancellable)(() => adapter.getInfo()),
@@ -7312,6 +8043,7 @@ function readOnlineCredential(path, io) {
7312
8043
  function onlineWarnings(result) {
7313
8044
  const messages = {
7314
8045
  'acl-best-effort': "the file inherits the folder's permissions",
8046
+ 'unsafe-arg': 'literal credentials can appear in shell history and process listings',
7315
8047
  'own-root-key-0': 'own credential uses root key ID 0',
7316
8048
  'snapshot-drift': 'LND permissions differ from the bundled snapshot',
7317
8049
  'canary-requires-watch': 'LND accepts this canary only while macwarden watch runs; a running watch picks it up within 60 seconds (or at its next reconnect), then denies and alerts on every use its permissions allow',
@@ -7345,6 +8077,7 @@ const lndconnect_js_1 = require("../input/lndconnect.js");
7345
8077
  const recognition_js_1 = require("../decode/recognition.js");
7346
8078
  const node_util_1 = require("node:util");
7347
8079
  const patterns_js_1 = require("../scan/patterns.js");
8080
+ const nwc_match_js_1 = require("../scan/nwc-match.js");
7348
8081
  const credential_recognition_js_1 = require("../credential-recognition.js");
7349
8082
  const redact_js_1 = require("../scan/redact.js");
7350
8083
  function rotatePlanResult(plan, next, warnings, repaired) {
@@ -7358,10 +8091,7 @@ function revokePlanResult(plan, warnings, alreadyAbsent) {
7358
8091
  result.display = { alreadyAbsent };
7359
8092
  return result;
7360
8093
  }
7361
- const redactors = [patterns_js_1.lndconnect, patterns_js_1.nwc]
7362
- .map(pattern => ({ regex: new RegExp(pattern.source, `${pattern.flags}g`),
7363
- isLndconnect: pattern === patterns_js_1.lndconnect,
7364
- consumesDelimiter: pattern === patterns_js_1.nwc }));
8094
+ const lndconnectAll = new RegExp(patterns_js_1.lndconnect.source, `${patterns_js_1.lndconnect.flags}g`);
7365
8095
  function hasCredentialInLndconnect(value) {
7366
8096
  const bytes = (0, lndconnect_js_1.parseLndconnect)(value);
7367
8097
  return bytes !== null && (0, recognition_js_1.macaroonEnds)(bytes)(0) !== null;
@@ -7393,12 +8123,11 @@ function redact(text) {
7393
8123
  return safe;
7394
8124
  }
7395
8125
  function redactUncached(text) {
7396
- const withUrisRedacted = redactors.reduce((value, { regex, isLndconnect, consumesDelimiter }) => value.replace(regex, match => {
7397
- if (isLndconnect && !hasCredentialInLndconnect(match))
7398
- return match;
7399
- const delimiter = consumesDelimiter && /[^0-9a-f]$/i.test(match) ? match.slice(-1) : '';
7400
- return `[redacted]${delimiter}`;
7401
- }), text);
8126
+ lndconnectAll.lastIndex = 0;
8127
+ const withLndconnectRedacted = text.replace(lndconnectAll, match => hasCredentialInLndconnect(match) ? '[redacted]' : match);
8128
+ // NWC URIs through the linear matcher: the published regex backtracks quadratically on
8129
+ // repeated prefixes. The match consumes one delimiter after the secret, which is kept.
8130
+ const withUrisRedacted = (0, nwc_match_js_1.replaceNwc)(withLndconnectRedacted, match => `[redacted]${/[^0-9a-f]$/i.test(match) ? match.slice(-1) : ''}`);
7402
8131
  return (0, credential_recognition_js_1.redactEncodedCredentials)(withUrisRedacted);
7403
8132
  }
7404
8133
  function assertPlain(value, seen = new WeakSet()) {
@@ -7556,6 +8285,23 @@ parent ${safeText(result.parentFingerprint)}
7556
8285
  result.added.map(caveat => `added ${safeText(caveat)}
7557
8286
  `).join('') + inspectText(result);
7558
8287
  }
8288
+ function verifyText(result) {
8289
+ const why = result.rejection === null ? '' : ` (${safeText(result.rejection)})`;
8290
+ const status = result.status === 'accepted' ? 'ACCEPTED by LND: this credential is live'
8291
+ : result.conclusive ? `rejected by LND${why}`
8292
+ : `REJECTED BY THIS CHECK ONLY${why}: it may still be accepted from another caller`;
8293
+ const registry = result.registry === null ? 'not in this registry'
8294
+ : `${safeText(result.registry.label ?? 'unlabelled')} (${safeText(result.registry.status)}${result.registry.match === 'fingerprint' ? ', same credential' : ', same root key ID, different credential'})`;
8295
+ const lines = [
8296
+ `node ${safeText(result.node.pubkey)} (${safeText(result.node.alias)}) ${safeText(result.node.network)}`,
8297
+ `status ${status}`,
8298
+ `root key ${result.rootKeyListed === null ? 'not checked (own credential cannot call ListMacaroonIDs)'
8299
+ : result.rootKeyListed ? 'listed in LND' : 'not listed in LND'}${result.ownCredential ? "; macwarden's own root key ID" : ''}`,
8300
+ `registry ${registry}`,
8301
+ ...result.guidance.map(line => `next ${safeText(line)}`),
8302
+ ];
8303
+ return inspectText(result) + `${lines.join('\n')}\n`;
8304
+ }
7559
8305
  function respondText(result) {
7560
8306
  const lines = [`respond ${result.mode}${result.dryRun ? ' (dry run: nothing changed)' : ''} node ${safeText(result.node.pubkey)} (${safeText(result.node.alias)}) ${safeText(result.node.network)}`];
7561
8307
  if (result.outDir)
@@ -8035,6 +8781,9 @@ function* renderChunks(result, mode) {
8035
8781
  case 'respond':
8036
8782
  rendered = respondText(result.result);
8037
8783
  break;
8784
+ case 'verify':
8785
+ rendered = isInspectResult(result.result) ? verifyText(result.result) : genericText(result);
8786
+ break;
8038
8787
  default:
8039
8788
  rendered = genericText(result);
8040
8789
  break;
@@ -8351,6 +9100,7 @@ const node_os_1 = require("node:os");
8351
9100
  const node_path_1 = require("node:path");
8352
9101
  const node_crypto_1 = require("node:crypto");
8353
9102
  const protected_path_js_1 = require("../protected-path.js");
9103
+ const bounded_file_js_1 = require("../bounded-file.js");
8354
9104
  const maxRootKeyId = 18446744073709551615n;
8355
9105
  const fields = ['nodePubkey', 'label', 'rootKeyId', 'permissions', 'caveats', 'grade',
8356
9106
  'fingerprint', 'outPath', 'createdAt', 'verified', 'status', 'revokedAt',
@@ -8715,6 +9465,8 @@ class Registry {
8715
9465
  }
8716
9466
  let lock;
8717
9467
  try {
9468
+ // The lock is ours to read only as a plain file: never through a symlink (SAFE-02).
9469
+ await (0, bounded_file_js_1.assertPlainFile)(lockPath, this.fs, bounded_file_js_1.LOCK_FILE_LIMIT);
8718
9470
  const text = await this.fs.readFile(lockPath, 'utf8');
8719
9471
  lock = JSON.parse(text);
8720
9472
  }
@@ -9072,7 +9824,7 @@ function ownProcessDomain() {
9072
9824
  "report/access-review.js": (module, exports, require) => {
9073
9825
  "use strict";
9074
9826
  exports.__esModule = true;
9075
- exports.REPORT_FORMATS = void 0;
9827
+ exports.LIMITATIONS = exports.REPORT_FORMATS = void 0;
9076
9828
  exports.buildAccessReview = buildAccessReview;
9077
9829
  exports.csvField = csvField;
9078
9830
  exports.formatAccessReview = formatAccessReview;
@@ -9082,6 +9834,8 @@ exports.formatAccessReview = formatAccessReview;
9082
9834
  const credential_recognition_js_1 = require("../credential-recognition.js");
9083
9835
  const index_js_1 = require("../decode/index.js");
9084
9836
  exports.REPORT_FORMATS = ['markdown', 'csv', 'json'];
9837
+ /** The scope statement every format carries. */
9838
+ exports.LIMITATIONS = 'This report is evidence for a reviewer. It does not by itself establish compliance with any framework.';
9085
9839
  const FLAGS = ['default-root-key', 'own-credential', 'unknown', 'missing-in-lnd', 'revoked-but-present',
9086
9840
  'admin-grade', 'no-expiry', 'expired', 'unlabelled', 'canary-triggered'];
9087
9841
  const byId = (left, right) => { const a = BigInt(left), b = BigInt(right); return a < b ? -1 : a > b ? 1 : 0; };
@@ -9131,12 +9885,14 @@ function buildAccessReview(input) {
9131
9885
  flags.push('expired');
9132
9886
  if (entry.label === null)
9133
9887
  flags.push('unlabelled');
9134
- if (input.canary?.(entry.rootKeyId))
9888
+ const triggered = input.canary?.(entry.rootKeyId) ?? null;
9889
+ if (triggered)
9135
9890
  flags.push('canary-triggered');
9136
9891
  credentials.push({ rootKeyId: entry.rootKeyId, inLnd: present, label: entry.label, status: entry.status,
9137
9892
  grade: entry.grade, permissions: [...entry.permissions].sort(), caveats: entry.caveats.map(caveat => `${caveat.type} ${caveat.value}`),
9138
9893
  expiresAt: at, createdAt: entry.createdAt, revokedAt: entry.revokedAt, revokeEvidence: entry.revokeEvidence,
9139
- replaces: entry.replaces, replacedBy: entry.replacedBy, ...observed(entry.rootKeyId), flags });
9894
+ replaces: entry.replaces, replacedBy: entry.replacedBy, ...observed(entry.rootKeyId),
9895
+ canary: triggered ? { uses: triggered.uses, firstUse: triggered.firstUse, lastUse: triggered.lastUse } : null, flags });
9140
9896
  }
9141
9897
  for (const id of [...inLnd].filter(id => !seen.has(id)).sort(byId)) {
9142
9898
  const flags = id === '0' ? ['default-root-key'] : id === input.ownRootKeyId ? [] : ['unknown'];
@@ -9144,7 +9900,7 @@ function buildAccessReview(input) {
9144
9900
  flags.push('own-credential');
9145
9901
  credentials.push({ rootKeyId: id, inLnd: true, label: null, status: 'not-in-registry', grade: null, permissions: [],
9146
9902
  caveats: [], expiresAt: null, createdAt: null, revokedAt: null, revokeEvidence: null, replaces: null,
9147
- replacedBy: null, ...observed(id), flags });
9903
+ replacedBy: null, ...observed(id), canary: null, flags });
9148
9904
  }
9149
9905
  credentials.sort((a, b) => byId(a.rootKeyId, b.rootKeyId));
9150
9906
  const flagged = Object.fromEntries(FLAGS.map(flag => [flag, credentials.filter(item => item.flags.includes(flag)).length]));
@@ -9168,6 +9924,7 @@ function buildAccessReview(input) {
9168
9924
  observation: 'Expired credentials still active and revoked entries whose root key ID is still in LND should be removed with macwarden revoke.' },
9169
9925
  ],
9170
9926
  signOff: { reviewer: null, date: null, decision: null, notes: null },
9927
+ limitations: exports.LIMITATIONS,
9171
9928
  };
9172
9929
  }
9173
9930
  /** Credential recognition plus removal of control characters; reports are plain text. */
@@ -9188,7 +9945,25 @@ const COLUMNS = [
9188
9945
  ['revoke_evidence', item => item.revokeEvidence ?? ''], ['replaces', item => item.replaces ?? ''], ['replaced_by', item => item.replacedBy ?? ''],
9189
9946
  ['last_used', item => item.lastUsed ?? ''], ['calls', item => item.calls === null ? '' : String(item.calls)],
9190
9947
  ['flags', item => item.flags.join(' ')],
9948
+ ['caveats', item => item.caveats.join('; ')], ['canary_uses', item => item.canary ? String(item.canary.uses) : ''],
9949
+ ['canary_first_use', item => item.canary?.firstUse ?? ''], ['canary_last_use', item => item.canary?.lastUse ?? ''],
9950
+ ];
9951
+ /**
9952
+ * Columns after the credential columns: the controls that name this row, then report-level
9953
+ * fields repeated on every row, so the CSV stays one table with one header row.
9954
+ */
9955
+ const REPORT_COLUMNS = [
9956
+ ['controls', (review, item) => naming(review, item).map(control => control.control).join('; ')],
9957
+ ['control_observations', (review, item) => naming(review, item).map(control => `${control.control}: ${control.observation}`).join(' | ')],
9958
+ ['generated_at', review => review.generatedAt], ['macwarden_version', review => review.macwardenVersion],
9959
+ ['node_pubkey', review => review.node.pubkey], ['node_alias', review => review.node.alias],
9960
+ ['node_network', review => review.node.network], ['node_version', review => review.node.version],
9961
+ ['reviewer', () => ''], ['review_date', () => ''], ['review_decision', () => ''], ['review_notes', () => ''],
9962
+ ['limitations', review => review.limitations],
9191
9963
  ];
9964
+ function naming(review, item) {
9965
+ return review.controls.filter(control => control.rootKeyIds.includes(item.rootKeyId));
9966
+ }
9192
9967
  /** RFC 4180 field; a leading formula character is neutralised so spreadsheets never evaluate it. */
9193
9968
  function csvField(value) {
9194
9969
  const safe = /^[=+\-@\t\r]/.test(value) ? `'${value}` : value;
@@ -9205,8 +9980,9 @@ function formatAccessReview(review, format) {
9205
9980
  if (format === 'json')
9206
9981
  return `${JSON.stringify(safe, null, 2)}\n`;
9207
9982
  if (format === 'csv')
9208
- return [COLUMNS.map(([name]) => name).join(','),
9209
- ...safe.credentials.map(item => COLUMNS.map(([, value]) => csvField(value(item))).join(','))].join('\r\n') + '\r\n';
9983
+ return [[...COLUMNS, ...REPORT_COLUMNS].map(([name]) => name).join(','),
9984
+ ...safe.credentials.map(item => [...COLUMNS.map(([, value]) => value(item)), ...REPORT_COLUMNS.map(([, value]) => value(safe, item))]
9985
+ .map(csvField).join(','))].join('\r\n') + '\r\n';
9210
9986
  const node = safe.node;
9211
9987
  const lines = [
9212
9988
  `# Lightning credential access review`, '',
@@ -9214,15 +9990,20 @@ function formatAccessReview(review, format) {
9214
9990
  `- Node: ${markdownCell(node.pubkey)} (${markdownCell(node.alias)}), ${markdownCell(node.network)}, LND ${markdownCell(node.version)}`,
9215
9991
  `- Tool: macwarden ${markdownCell(safe.macwardenVersion)}`,
9216
9992
  `- Root key IDs in LND: ${safe.totals.inLnd}; active registry entries: ${safe.totals.active}; revoked: ${safe.totals.revoked}; unknown: ${safe.totals.unknown}`,
9217
- '', 'This report is evidence for a reviewer. It does not by itself establish compliance with any framework.', '',
9993
+ `- Flag counts: ${Object.entries(safe.totals.flagged).map(([flag, count]) => `${markdownCell(flag)} ${count}`).join(', ')}`,
9994
+ '', safe.limitations, '',
9218
9995
  '## Credentials', '',
9219
- '| Root key ID | In LND | Label | Status | Grade | Expires | Created | Revoked | Evidence | Last used | Calls | Flags |',
9220
- '|---|---|---|---|---|---|---|---|---|---|---|---|',
9996
+ '| Root key ID | In LND | Label | Status | Grade | Expires | Created | Revoked | Evidence | Replaces | Replaced by | Last used | Calls | Canary uses | Flags |',
9997
+ '|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|',
9221
9998
  ...safe.credentials.map(item => `| ${[item.rootKeyId, item.inLnd ? 'yes' : 'no', item.label ?? '', item.status, item.grade ?? '',
9222
- item.expiresAt ?? '', item.createdAt ?? '', item.revokedAt ?? '', item.revokeEvidence ?? '', item.lastUsed ?? 'not observed',
9223
- item.calls === null ? '' : String(item.calls), item.flags.join(', ')].map(markdownCell).join(' | ')} |`),
9999
+ item.expiresAt ?? '', item.createdAt ?? '', item.revokedAt ?? '', item.revokeEvidence ?? '', item.replaces ?? '', item.replacedBy ?? '',
10000
+ item.lastUsed ?? 'not observed', item.calls === null ? '' : String(item.calls),
10001
+ item.canary ? `${item.canary.uses} denied (first ${item.canary.firstUse}, last ${item.canary.lastUse})` : '',
10002
+ item.flags.join(', ')].map(markdownCell).join(' | ')} |`),
9224
10003
  '', '## Permissions', '',
9225
10004
  ...safe.credentials.filter(item => item.permissions.length).map(item => `- ${markdownCell(item.rootKeyId)} ${markdownCell(item.label ?? '')}: ${item.permissions.map(markdownCell).join(', ')}`),
10005
+ '', '## Caveats', '',
10006
+ ...safe.credentials.filter(item => item.caveats.length).map(item => `- ${markdownCell(item.rootKeyId)} ${markdownCell(item.label ?? '')}: ${item.caveats.map(markdownCell).join('; ')}`),
9226
10007
  '', '## Control observations', '',
9227
10008
  ...safe.controls.flatMap(control => [`### ${control.control}`, '', control.observation, '',
9228
10009
  control.rootKeyIds.length ? `Root key IDs: ${control.rootKeyIds.map(markdownCell).join(', ')}` : 'Root key IDs: none', '']),
@@ -9343,7 +10124,7 @@ var __importStar = (this && this.__importStar) || (function () {
9343
10124
  };
9344
10125
  })();
9345
10126
  exports.__esModule = true;
9346
- exports.PINNED_CONFIG = exports.MAX_HISTORY_ENTRIES = exports.GIT_IDLE_TIMEOUT_MS = exports.MODE_OTHER = exports.MODE_SYMLINK = exports.MODE_REGULAR = exports.GitHistoryError = void 0;
10127
+ exports.PREFLIGHT_LIMITS = exports.PINNED_CONFIG = exports.MAX_HISTORY_ENTRIES = exports.GIT_IDLE_TIMEOUT_MS = exports.MODE_OTHER = exports.MODE_SYMLINK = exports.MODE_REGULAR = exports.GitHistoryError = void 0;
9347
10128
  exports.nonLocalPath = nonLocalPath;
9348
10129
  exports.locateRepository = locateRepository;
9349
10130
  exports.gitEnvironment = gitEnvironment;
@@ -9443,14 +10224,87 @@ async function local(base, value) {
9443
10224
  throw new GitHistoryError(NOT_LOCAL);
9444
10225
  return real ?? path;
9445
10226
  }
9446
- async function small(path) {
9447
- const stat = await fs.stat(path).catch(() => null);
9448
- if (!stat?.isFile())
9449
- return null;
9450
- if (stat.size > MAX_LOCATION_FILE)
9451
- throw new GitHistoryError(UNREADABLE);
9452
- return fs.readFile(path, 'utf8');
10227
+ exports.PREFLIGHT_LIMITS = { files: 256, bytes: 1024 * 1024, edges: 4096 };
10228
+ /** One preflight: budgets, counters, memoized results and the chain currently being walked. */
10229
+ class Preflight {
10230
+ stats;
10231
+ limits;
10232
+ /** Canonical location → its expansion, once validated. Each location is read at most once. */
10233
+ done = new Map();
10234
+ walking = new Set();
10235
+ constructor(limits = {}, stats) {
10236
+ this.limits = { ...exports.PREFLIGHT_LIMITS, ...limits };
10237
+ this.stats = Object.assign(stats ?? {}, { files: 0, bytes: 0, edges: 0, expandedEdges: 0, expandedBytes: 0 });
10238
+ }
10239
+ key(path) { return process.platform === 'win32' ? (0, node_path_1.resolve)(path).toLowerCase() : (0, node_path_1.resolve)(path); }
10240
+ /** Checks cancellation and the cumulative budgets; exceeding one fails like unreadable input. */
10241
+ step() {
10242
+ (0, cancellation_js_1.checkCancellation)();
10243
+ const { stats, limits } = this;
10244
+ if (stats.files > limits.files || stats.bytes > limits.bytes || stats.edges > limits.edges
10245
+ || stats.expandedEdges > limits.edges || stats.expandedBytes > limits.bytes)
10246
+ throw new GitHistoryError(UNREADABLE);
10247
+ }
10248
+ edge() { this.stats.edges++; this.step(); }
10249
+ async small(path) {
10250
+ this.stats.files++;
10251
+ this.step();
10252
+ const stat = await fs.stat(path).catch(() => null);
10253
+ if (!stat?.isFile())
10254
+ return null;
10255
+ if (stat.size > MAX_LOCATION_FILE)
10256
+ throw new GitHistoryError(UNREADABLE);
10257
+ const bytes = await fs.readFile(path);
10258
+ // The file may have grown between stat and read: count and bound what was actually read.
10259
+ if (bytes.length > MAX_LOCATION_FILE)
10260
+ throw new GitHistoryError(UNREADABLE);
10261
+ this.stats.bytes += bytes.length;
10262
+ this.step();
10263
+ return bytes.toString('utf8');
10264
+ }
10265
+ /** Adds a followed edge and the child's expansion to `into`, failing once it exceeds a budget. */
10266
+ follow(into, child) {
10267
+ into.height = Math.max(into.height, 1 + child.height);
10268
+ into.edges += 1 + child.edges;
10269
+ into.bytes += child.bytes;
10270
+ if (into.edges > this.limits.edges || into.bytes > this.limits.bytes)
10271
+ throw new GitHistoryError(UNREADABLE);
10272
+ }
10273
+ /** Adds a root location's expansion to the totals for the whole repository. */
10274
+ root(expansion) {
10275
+ this.stats.expandedEdges += expansion.edges;
10276
+ this.stats.expandedBytes += expansion.bytes;
10277
+ this.step();
10278
+ }
10279
+ /**
10280
+ * Validates `location` at `depth` at most once. A location validated earlier is accepted
10281
+ * when its deepest chain still fits under `max` from this depth; one on the chain being
10282
+ * walked is a cycle, which git would follow until its own depth limit, so it is refused.
10283
+ */
10284
+ async visit(location, depth, max, walk) {
10285
+ const key = this.key(location);
10286
+ const known = this.done.get(key);
10287
+ if (known !== undefined) {
10288
+ if (depth + known.height > max)
10289
+ throw new GitHistoryError(UNREADABLE);
10290
+ return known;
10291
+ }
10292
+ if (this.walking.has(key))
10293
+ throw new GitHistoryError(UNREADABLE);
10294
+ this.walking.add(key);
10295
+ try {
10296
+ const expansion = await walk();
10297
+ if (depth + expansion.height > max)
10298
+ throw new GitHistoryError(UNREADABLE);
10299
+ this.done.set(key, expansion);
10300
+ return expansion;
10301
+ }
10302
+ finally {
10303
+ this.walking.delete(key);
10304
+ }
10305
+ }
9453
10306
  }
10307
+ const NOTHING = { height: 0, edges: 0, bytes: 0 };
9454
10308
  /** Unquotes a C-style quoted alternates line as git writes them. */
9455
10309
  function unquoteAlternate(line) {
9456
10310
  if (!line.startsWith('"') || !line.endsWith('"') || line.length < 2)
@@ -9462,18 +10316,25 @@ function unquoteAlternate(line) {
9462
10316
  return named[escape] ?? escape;
9463
10317
  });
9464
10318
  }
9465
- async function checkAlternates(objects, depth) {
9466
- const text = await small((0, node_path_1.join)(objects, 'info', 'alternates'));
9467
- if (text === null)
9468
- return;
9469
- if (depth >= MAX_ALTERNATE_DEPTH)
9470
- throw new GitHistoryError(UNREADABLE);
9471
- for (const raw of text.split(/\r?\n/)) {
9472
- const line = raw.trim();
9473
- if (!line || line.startsWith('#'))
9474
- continue;
9475
- await checkAlternates(await local(objects, unquoteAlternate(line)), depth + 1);
9476
- }
10319
+ /** Alternates chains of at most MAX_ALTERNATE_DEPTH levels; each objects directory is read once. */
10320
+ function checkAlternates(run, objects, depth) {
10321
+ const file = (0, node_path_1.join)(objects, 'info', 'alternates');
10322
+ return run.visit(file, depth, MAX_ALTERNATE_DEPTH, async () => {
10323
+ const text = await run.small(file);
10324
+ if (text === null)
10325
+ return NOTHING;
10326
+ if (depth >= MAX_ALTERNATE_DEPTH)
10327
+ throw new GitHistoryError(UNREADABLE);
10328
+ const own = { height: 0, edges: 0, bytes: Buffer.byteLength(text) };
10329
+ for (const raw of text.split(/\r?\n/)) {
10330
+ const line = raw.trim();
10331
+ if (!line || line.startsWith('#'))
10332
+ continue;
10333
+ run.edge();
10334
+ run.follow(own, await checkAlternates(run, await local(objects, unquoteAlternate(line)), depth + 1));
10335
+ }
10336
+ return own;
10337
+ });
9477
10338
  }
9478
10339
  /** Include paths named by [include] and [includeIf "..."] sections of one config file. */
9479
10340
  function includePaths(text) {
@@ -9502,19 +10363,27 @@ function includePaths(text) {
9502
10363
  }
9503
10364
  return paths;
9504
10365
  }
9505
- async function checkIncludes(file, depth) {
9506
- const text = await small(file);
9507
- if (text === null)
9508
- return;
9509
- for (const value of includePaths(text)) {
9510
- if (depth >= MAX_INCLUDE_DEPTH)
9511
- throw new GitHistoryError(UNREADABLE);
9512
- const named = value.startsWith('~/') || value.startsWith('~\\') ? (0, node_path_1.join)((0, node_os_1.homedir)(), value.slice(2)) : value;
9513
- // %(prefix)/ names the git installation, which is not repository input.
9514
- if (named.startsWith('%(prefix)/'))
9515
- continue;
9516
- await checkIncludes(await local((0, node_path_1.dirname)(file), named), depth + 1);
9517
- }
10366
+ /** Include chains of at most MAX_INCLUDE_DEPTH levels; each config file is read once. */
10367
+ function checkIncludes(run, file, depth) {
10368
+ return run.visit(file, depth, MAX_INCLUDE_DEPTH, async () => {
10369
+ const text = await run.small(file);
10370
+ if (text === null)
10371
+ return NOTHING;
10372
+ const own = { height: 0, edges: 0, bytes: Buffer.byteLength(text) };
10373
+ for (const value of includePaths(text)) {
10374
+ if (depth >= MAX_INCLUDE_DEPTH)
10375
+ throw new GitHistoryError(UNREADABLE);
10376
+ run.edge();
10377
+ const named = value.startsWith('~/') || value.startsWith('~\\') ? (0, node_path_1.join)((0, node_os_1.homedir)(), value.slice(2)) : value;
10378
+ // %(prefix)/ names the git installation, which is not repository input.
10379
+ if (named.startsWith('%(prefix)/')) {
10380
+ run.follow(own, NOTHING);
10381
+ continue;
10382
+ }
10383
+ run.follow(own, await checkIncludes(run, await local((0, node_path_1.dirname)(file), named), depth + 1));
10384
+ }
10385
+ return own;
10386
+ });
9518
10387
  }
9519
10388
  /** GIT_CEILING_DIRECTORIES as git reads it: discovery never moves up into one of these. */
9520
10389
  function ceilings() {
@@ -9526,7 +10395,8 @@ function ceilings() {
9526
10395
  * Finds the repository containing `directory` the way git discovery would (a .git
9527
10396
  * directory or gitfile in it or a parent) and validates every location git would read.
9528
10397
  */
9529
- async function locateRepository(directory) {
10398
+ async function locateRepository(directory, options = {}) {
10399
+ const run = new Preflight(options.limits, options.stats);
9530
10400
  if (nonLocalPath((0, node_path_1.resolve)(directory)))
9531
10401
  throw new GitHistoryError(NOT_LOCAL);
9532
10402
  let current = await fs.realpath(directory).catch(() => { throw new GitHistoryError(NOT_WORK_TREE); });
@@ -9540,7 +10410,7 @@ async function locateRepository(directory) {
9540
10410
  if (stat) {
9541
10411
  let gitDir;
9542
10412
  if (stat.isFile()) {
9543
- const text = await small(candidate);
10413
+ const text = await run.small(candidate);
9544
10414
  const target = text === null ? null : /^gitdir:\s*(.+?)\s*$/m.exec(text)?.[1];
9545
10415
  if (!target)
9546
10416
  throw new GitHistoryError(NOT_WORK_TREE);
@@ -9551,11 +10421,11 @@ async function locateRepository(directory) {
9551
10421
  const gitStat = await fs.stat(gitDir).catch(() => null);
9552
10422
  if (!gitStat?.isDirectory())
9553
10423
  throw new GitHistoryError(NOT_WORK_TREE);
9554
- const common = await small((0, node_path_1.join)(gitDir, 'commondir'));
10424
+ const common = await run.small((0, node_path_1.join)(gitDir, 'commondir'));
9555
10425
  const commonDir = common === null ? gitDir : await local(gitDir, common.trim());
9556
- await checkAlternates((0, node_path_1.join)(commonDir, 'objects'), 0);
9557
- await checkIncludes((0, node_path_1.join)(commonDir, 'config'), 0);
9558
- await checkIncludes((0, node_path_1.join)(gitDir, 'config.worktree'), 0);
10426
+ run.root(await checkAlternates(run, (0, node_path_1.join)(commonDir, 'objects'), 0));
10427
+ run.root(await checkIncludes(run, (0, node_path_1.join)(commonDir, 'config'), 0));
10428
+ run.root(await checkIncludes(run, (0, node_path_1.join)(gitDir, 'config.worktree'), 0));
9559
10429
  return { gitDir, workTree: current };
9560
10430
  }
9561
10431
  const parent = (0, node_path_1.dirname)(current);
@@ -10033,8 +10903,7 @@ var __importStar = (this && this.__importStar) || (function () {
10033
10903
  };
10034
10904
  })();
10035
10905
  exports.__esModule = true;
10036
- exports.SCAN_CONCURRENCY = exports.FindingsLimitError = exports.HISTORY_DEDUP_WORK_LIMIT = exports.ALSO_AT_EXAMPLES = exports.FINDINGS_LIMIT = exports.GitHistoryError = exports.IgnoreFileError = void 0;
10037
- exports.readBounded = readBounded;
10906
+ exports.readBounded = exports.SCAN_CONCURRENCY = exports.FindingsLimitError = exports.HISTORY_DEDUP_WORK_LIMIT = exports.ALSO_AT_EXAMPLES = exports.FINDINGS_LIMIT = exports.GitHistoryError = exports.IgnoreFileError = void 0;
10038
10907
  exports.scan = scan;
10039
10908
  const mcp_context_js_1 = require("./mcp-context.js");
10040
10909
  const node_crypto_1 = require("node:crypto");
@@ -10053,6 +10922,8 @@ const nwc_match_js_1 = require("./nwc-match.js");
10053
10922
  const ignore_js_1 = require("./ignore.js");
10054
10923
  const git_history_js_1 = require("./git-history.js");
10055
10924
  const cancellation_js_1 = require("../cancellation.js");
10925
+ const bounded_file_js_1 = require("../bounded-file.js");
10926
+ module.exports = exports = { __proto__: null, ...exports, get readBounded() { return bounded_file_js_1.readBounded; } };
10056
10927
  var ignore_js_2 = require("./ignore.js");
10057
10928
  module.exports = exports = { __proto__: null, ...exports, get IgnoreFileError() { return ignore_js_2.IgnoreFileError; } };
10058
10929
  var git_history_js_2 = require("./git-history.js");
@@ -10085,7 +10956,8 @@ const detectors = [
10085
10956
  const detectorRegexes = detectors.map(({ kind, pattern }) => ({ kind, regex: new RegExp(pattern.source, `${pattern.flags.replace('g', '')}g`) }));
10086
10957
  // Every detector match contains one of these (case-insensitive superset), so a line
10087
10958
  // without any of them cannot produce a hit and is not split into code points.
10088
- const MAY_MATCH = /lndconnect:\/\/|walletconnect:|0201036c6e6402|AgEDbG5kA/i;
10959
+ // "walletconnect%3a" covers percent-encoded NWC URIs, which are matched on a normalised copy.
10960
+ const MAY_MATCH = /lndconnect:\/\/|walletconnect:|walletconnect%3a|0201036c6e6402|AgEDbG5kA/i;
10089
10961
  const WINDOW = 65_536;
10090
10962
  const STEP = 32_768;
10091
10963
  function fingerprint(data) {
@@ -10106,7 +10978,7 @@ function withFacts(path, line, column, kind, facts, now, ctx) {
10106
10978
  (0, redact_js_1.collectNwcSecrets)(facts.location, ctx.secrets);
10107
10979
  for (const value of [...facts.permissions, ...facts.caveats.map(caveat => caveat.value)])
10108
10980
  (0, redact_js_1.collectNwcSecrets)(value, ctx.secrets);
10109
- const assessed = (0, index_js_2.grade)(facts.permissions);
10981
+ const assessed = (0, index_js_2.gradeFacts)(facts);
10110
10982
  shared = {
10111
10983
  fingerprint: facts.fingerprint, grade: assessed.grade,
10112
10984
  rootKeyId: facts.rootKeyId.toString(), permissions: facts.permissions.map(redact_js_1.redactDecoded),
@@ -10154,11 +11026,13 @@ function credentialBytes(hit) {
10154
11026
  return null;
10155
11027
  return (0, lndconnect_js_1.parseLndconnect)(hit.text);
10156
11028
  }
10157
- function collectHits(points) {
11029
+ function collectHits(points, only) {
10158
11030
  const hits = new Map();
10159
11031
  for (let offset = 0; offset < points.length; offset += STEP) {
10160
11032
  const window = points.slice(offset, offset + WINDOW).join('');
10161
11033
  for (const { kind, regex } of detectorRegexes) {
11034
+ if (only !== undefined && kind !== only)
11035
+ continue;
10162
11036
  regex.lastIndex = 0;
10163
11037
  let codeUnit = 0;
10164
11038
  let codePoint = 0;
@@ -10205,15 +11079,31 @@ function scanText(path, text, now, ctx) {
10205
11079
  if (!MAY_MATCH.test(lines[i]))
10206
11080
  continue;
10207
11081
  const points = Array.from(lines[i]);
10208
- for (const hit of collectHits(points)) {
11082
+ const hits = collectHits(points);
11083
+ // NWC URIs in common escaped forms (JSON-escaped slashes, percent-encoding, "+" as a
11084
+ // space) are matched on one normalised copy of the line; a URI the line already matched
11085
+ // at the same column is not reported twice.
11086
+ const unescaped = (0, nwc_match_js_1.nwcUnescaped)(points, lines[i]);
11087
+ if (unescaped) {
11088
+ const seen = new Set(hits.filter(hit => hit.kind === 'nwc').map(hit => hit.start));
11089
+ for (const hit of collectHits(unescaped.points, 'nwc')) {
11090
+ const start = unescaped.origin[hit.start];
11091
+ if (!seen.has(start)) {
11092
+ seen.add(start);
11093
+ hits.push({ ...hit, start });
11094
+ }
11095
+ }
11096
+ hits.sort((a, b) => a.start - b.start);
11097
+ }
11098
+ for (const hit of hits) {
10209
11099
  const column = hit.start + 1;
10210
11100
  const line = i + 1;
10211
11101
  if (hit.kind === 'nwc') {
10212
- const secret = /(?:[?&])secret=([0-9a-f]{64})(?=[^0-9a-f]|$)/i.exec(hit.text);
10213
- if (secret) {
10214
- ctx.secrets.add(secret[1].toLowerCase());
10215
- findings.push(nullFinding(path, line, column, 'nwc', fingerprint(Buffer.from(secret[1], 'hex'))));
10216
- }
11102
+ // The matcher ends every match with secret=<64 hex> and at most one delimiter. Any
11103
+ // match is reported (fail closed): "&amp;secret=" and other separators included.
11104
+ const secret = /secret=([0-9a-f]{64})[^0-9a-f]?$/i.exec(hit.text)[1].toLowerCase();
11105
+ ctx.secrets.add(secret);
11106
+ findings.push(nullFinding(path, line, column, 'nwc', fingerprint(Buffer.from(secret, 'hex'))));
10217
11107
  continue;
10218
11108
  }
10219
11109
  const bytes = credentialBytes(hit);
@@ -10357,30 +11247,6 @@ function tagMcpContext(path, text, findings) {
10357
11247
  finding.context = 'mcp-config';
10358
11248
  }
10359
11249
  }
10360
- /**
10361
- * Reads at most limit + 1 bytes. The buffer starts at the size fstat reported plus one
10362
- * byte (to notice growth) and doubles only while the file keeps yielding data, so a small
10363
- * file never costs a limit-sized allocation.
10364
- */
10365
- async function readBounded(handle, limit, size) {
10366
- const hinted = typeof size === 'bigint' ? (size > BigInt(limit) ? limit : Number(size)) : Math.min(Math.max(size, 0), limit);
10367
- let bytes = Buffer.allocUnsafe(hinted + 1);
10368
- let total = 0;
10369
- for (;;) {
10370
- if (total === bytes.length) {
10371
- if (bytes.length >= limit + 1)
10372
- break;
10373
- const grown = Buffer.allocUnsafe(Math.min(limit + 1, bytes.length * 2));
10374
- bytes.copy(grown, 0, 0, total);
10375
- bytes = grown;
10376
- }
10377
- const { bytesRead } = await handle.read(bytes, total, bytes.length - total, total);
10378
- if (bytesRead === 0)
10379
- break;
10380
- total += bytesRead;
10381
- }
10382
- return bytes.subarray(0, total);
10383
- }
10384
11250
  async function scan(paths, opts = {}) {
10385
11251
  const io = opts.fs ?? fs;
10386
11252
  const now = opts.clock?.() ?? new Date();
@@ -10415,7 +11281,7 @@ async function scan(paths, opts = {}) {
10415
11281
  !openedPathStat.isFile() || openedPathStat.isSymbolicLink() ||
10416
11282
  !sameFileIdentity(openedPathStat, openedStat))
10417
11283
  throw new index_js_3.InputError('input-read', 'cannot read ignore file');
10418
- const bytes = await readBounded(handle, TEXT_LIMIT, openedStat.size);
11284
+ const bytes = await (0, bounded_file_js_1.readBounded)(handle, TEXT_LIMIT, openedStat.size);
10419
11285
  if (bytes.length > TEXT_LIMIT)
10420
11286
  throw new index_js_3.InputError('input-read', 'cannot read ignore file');
10421
11287
  content = bytes.toString('utf8');
@@ -10466,7 +11332,7 @@ async function scan(paths, opts = {}) {
10466
11332
  identity = fileIdentity(openedStat, target);
10467
11333
  if (scannedFiles.has(identity))
10468
11334
  return { type: 'dup' };
10469
- bytes = await readBounded(handle, macaroonFile ? MACAROON_LIMIT : TEXT_LIMIT, openedStat.size);
11335
+ bytes = await (0, bounded_file_js_1.readBounded)(handle, macaroonFile ? MACAROON_LIMIT : TEXT_LIMIT, openedStat.size);
10470
11336
  }
10471
11337
  finally {
10472
11338
  if (handle)
@@ -10980,22 +11846,22 @@ async function scan(paths, opts = {}) {
10980
11846
  // as `context: "mcp-config"`. Positions use the scanner's convention: 1-based lines split
10981
11847
  // on CRLF, LF or CR, and 1-based columns in Unicode code points.
10982
11848
  exports.__esModule = true;
11849
+ exports.MAX_DEPTH = void 0;
10983
11850
  exports.mcpServerStringSpans = mcpServerStringSpans;
10984
11851
  exports.withinSpans = withinSpans;
10985
11852
  const CONTAINERS = new Set(['mcpServers', 'mcp_servers', 'context_servers']);
10986
- function insideServer(path, mcpFile) {
10987
- for (let i = 0; i < path.length - 1; i++) {
10988
- if (CONTAINERS.has(path[i]))
10989
- return true;
10990
- if (path[i] === 'servers' && (mcpFile || (i > 0 && path[i - 1] === 'mcp')))
10991
- return true;
10992
- }
10993
- return false;
11853
+ /** Tagging gives up (returns []) beyond this nesting depth; real configuration files stay far below. */
11854
+ exports.MAX_DEPTH = 1000;
11855
+ const EMPTY_PATH = { inside: false, last: null, lastQualifies: false };
11856
+ function append(path, element, mcpFile) {
11857
+ return { inside: path.inside || path.lastQualifies, last: element,
11858
+ lastQualifies: CONTAINERS.has(element) || (element === 'servers' && (mcpFile || path.last === 'mcp')) };
10994
11859
  }
10995
11860
  /**
10996
11861
  * Spans of every string value inside a server entry. Returns [] for text that is not
10997
- * well-formed JSON with optional comments and trailing commas: tagging is best effort
10998
- * and never changes what is detected.
11862
+ * well-formed JSON with optional comments and trailing commas, or that nests deeper than
11863
+ * MAX_DEPTH: tagging is best effort and never changes what is detected. Work is linear in
11864
+ * the text length.
10999
11865
  */
11000
11866
  function mcpServerStringSpans(text, mcpFile) {
11001
11867
  const chars = Array.from(text.startsWith('') ? text.slice(1) : text);
@@ -11017,8 +11883,13 @@ function mcpServerStringSpans(text, mcpFile) {
11017
11883
  column++;
11018
11884
  };
11019
11885
  const stack = [];
11020
- const keyPath = () => stack.flatMap(frame => frame.kind === 'object'
11021
- ? (frame.key === null ? [] : [frame.key]) : ['[]']);
11886
+ const pathOf = (frame) => {
11887
+ if (!frame)
11888
+ return EMPTY_PATH;
11889
+ if (frame.kind === 'array')
11890
+ return append(frame.parents, '[]', mcpFile);
11891
+ return frame.key === null ? frame.parents : append(frame.parents, frame.key, mcpFile);
11892
+ };
11022
11893
  const readString = () => {
11023
11894
  const start = { line, column };
11024
11895
  advance();
@@ -11076,14 +11947,12 @@ function mcpServerStringSpans(text, mcpFile) {
11076
11947
  continue;
11077
11948
  }
11078
11949
  const top = stack.at(-1);
11079
- if (c === '{') {
11080
- advance();
11081
- stack.push({ kind: 'object', key: null, expectKey: true });
11082
- continue;
11083
- }
11084
- if (c === '[') {
11950
+ if (c === '{' || c === '[') {
11951
+ if (stack.length >= exports.MAX_DEPTH)
11952
+ return [];
11953
+ const parents = pathOf(top);
11085
11954
  advance();
11086
- stack.push({ kind: 'array' });
11955
+ stack.push(c === '{' ? { kind: 'object', key: null, expectKey: true, parents } : { kind: 'array', parents });
11087
11956
  continue;
11088
11957
  }
11089
11958
  if (c === '}' || c === ']') {
@@ -11117,7 +11986,7 @@ function mcpServerStringSpans(text, mcpFile) {
11117
11986
  top.key = token.value;
11118
11987
  continue;
11119
11988
  }
11120
- if (insideServer(keyPath(), mcpFile))
11989
+ if (pathOf(top).inside)
11121
11990
  spans.push({ start: token.start, end: token.end });
11122
11991
  afterValue();
11123
11992
  continue;
@@ -11138,9 +12007,23 @@ function mcpServerStringSpans(text, mcpFile) {
11138
12007
  return stack.length === 0 ? spans : [];
11139
12008
  }
11140
12009
  const before = (a, b) => a.line < b.line || (a.line === b.line && a.column <= b.column);
11141
- /** True when the position falls within one of the spans (inclusive of the quotes). */
12010
+ /**
12011
+ * True when the position falls within one of the spans (inclusive of the quotes). Spans come
12012
+ * in document order and never overlap (string values cannot nest), so a binary search finds
12013
+ * the only candidate: each finding costs O(log spans), not O(spans).
12014
+ */
11142
12015
  function withinSpans(position, spans) {
11143
- return spans.some(span => before(span.start, position) && before(position, span.end));
12016
+ let low = 0, high = spans.length - 1, candidate = -1;
12017
+ while (low <= high) {
12018
+ const middle = (low + high) >>> 1;
12019
+ if (before(spans[middle].start, position)) {
12020
+ candidate = middle;
12021
+ low = middle + 1;
12022
+ }
12023
+ else
12024
+ high = middle - 1;
12025
+ }
12026
+ return candidate >= 0 && before(position, spans[candidate].end);
11144
12027
  }
11145
12028
 
11146
12029
  },
@@ -11148,6 +12031,8 @@ function withinSpans(position, spans) {
11148
12031
  "use strict";
11149
12032
  exports.__esModule = true;
11150
12033
  exports.nwcMatches = nwcMatches;
12034
+ exports.replaceNwc = replaceNwc;
12035
+ exports.nwcUnescaped = nwcUnescaped;
11151
12036
  // Linear-time equivalent of the published `nwc` detector regex (src/scan/patterns.ts),
11152
12037
  // used for scanning. The regex's lazy middle `[^\s"'<>]*?` makes a backtracking engine
11153
12038
  // rescan the rest of a segment from every candidate start, which is quadratic when an
@@ -11199,6 +12084,58 @@ function nwcMatches(text) {
11199
12084
  }
11200
12085
  return matches;
11201
12086
  }
12087
+ /**
12088
+ * Replaces every match of the published `nwc` regex exactly as `text.replace(/nwc/gi, replacer)`
12089
+ * would, in linear time. Used by output redaction and scan's secret collection.
12090
+ */
12091
+ function replaceNwc(text, replacer) {
12092
+ if (!/walletconnect:/i.test(text))
12093
+ return text;
12094
+ let output = '';
12095
+ let cursor = 0;
12096
+ for (const match of nwcMatches(text)) {
12097
+ output += text.slice(cursor, match.index) + replacer(match.text);
12098
+ cursor = match.index + match.text.length;
12099
+ }
12100
+ return cursor === 0 ? text : output + text.slice(cursor);
12101
+ }
12102
+ const NWC_ESCAPE_HINT = /\\\/|%[0-9a-f]{2}|nostr walletconnect/i;
12103
+ /**
12104
+ * One normalised copy of a line, for NWC URIs written in common escaped forms: JSON-escaped
12105
+ * slashes ("\/" becomes "/"), one level of percent-encoding (ASCII bytes only) and a "+"
12106
+ * decoded to a space in the scheme ("nostr walletconnect"). `origin[i]` is the index in the
12107
+ * original code points of normalised code point i. Null when the line has none of these.
12108
+ * (An "&amp;" before secret= needs no unescaping: the matcher accepts it as it is.)
12109
+ */
12110
+ function nwcUnescaped(points, line) {
12111
+ if (!/walletconnect/i.test(line) || !NWC_ESCAPE_HINT.test(line))
12112
+ return null;
12113
+ const out = [];
12114
+ const origin = [];
12115
+ for (let i = 0; i < points.length;) {
12116
+ if (points[i] === '\\' && points[i + 1] === '/') {
12117
+ out.push('/');
12118
+ origin.push(i);
12119
+ i += 2;
12120
+ continue;
12121
+ }
12122
+ if (points[i] === '%' && /^[0-7][0-9a-f]$/i.test(`${points[i + 1] ?? ''}${points[i + 2] ?? ''}`)) {
12123
+ out.push(String.fromCharCode(parseInt(points[i + 1] + points[i + 2], 16)));
12124
+ origin.push(i);
12125
+ i += 3;
12126
+ continue;
12127
+ }
12128
+ out.push(points[i]);
12129
+ origin.push(i);
12130
+ i++;
12131
+ }
12132
+ for (let i = 5; i + 13 < out.length; i++) {
12133
+ if (out[i] === ' ' && out.slice(i - 5, i).join('').toLowerCase() === 'nostr'
12134
+ && out.slice(i + 1, i + 14).join('').toLowerCase() === 'walletconnect')
12135
+ out[i] = '+';
12136
+ }
12137
+ return { points: out, origin };
12138
+ }
11202
12139
 
11203
12140
  },
11204
12141
  "scan/patterns.js": (module, exports, require) => {
@@ -11222,14 +12159,14 @@ exports.collectNwcSecrets = collectNwcSecrets;
11222
12159
  exports.knownSecretRedactor = knownSecretRedactor;
11223
12160
  exports.redactKnownSecrets = redactKnownSecrets;
11224
12161
  const credential_recognition_js_1 = require("../credential-recognition.js");
12162
+ const nwc_match_js_1 = require("./nwc-match.js");
11225
12163
  const patterns_js_1 = require("./patterns.js");
11226
12164
  const lndconnectAll = new RegExp(patterns_js_1.lndconnect.source, `${patterns_js_1.lndconnect.flags}g`);
11227
- const nwcAll = new RegExp(patterns_js_1.nwc.source, `${patterns_js_1.nwc.flags.replace('g', '')}g`);
12165
+ // The published nwc regex backtracks quadratically on repeated prefixes; the linear matcher
12166
+ // returns exactly its matches (src/scan/nwc-match.ts).
11228
12167
  function redactStructured(text) {
11229
12168
  lndconnectAll.lastIndex = 0;
11230
- nwcAll.lastIndex = 0;
11231
- return text.replace(lndconnectAll, '[redacted]')
11232
- .replace(nwcAll, match => `[redacted]${/[^0-9a-f]$/i.test(match) ? match.slice(-1) : ''}`);
12169
+ return (0, nwc_match_js_1.replaceNwc)(text.replace(lndconnectAll, '[redacted]'), match => `[redacted]${/[^0-9a-f]$/i.test(match) ? match.slice(-1) : ''}`);
11233
12170
  }
11234
12171
  // Same rule as the output renderer: a complete macaroon is at least 36 bytes and every
11235
12172
  // encoding the recognizer reads needs at least one printable ASCII character per byte, so a
@@ -11261,9 +12198,10 @@ function redactPath(text) {
11261
12198
  // those known identities for redaction; ordinary encoded-looking facts remain
11262
12199
  // unchanged.
11263
12200
  function collectNwcSecrets(text, secrets) {
11264
- nwcAll.lastIndex = 0;
11265
- for (const match of text.matchAll(nwcAll)) {
11266
- const parameter = /(?:[?&])secret=([0-9a-f]{64})(?=[^0-9a-f]|$)/i.exec(match[0]);
12201
+ if (!/walletconnect:/i.test(text))
12202
+ return;
12203
+ for (const match of (0, nwc_match_js_1.nwcMatches)(text)) {
12204
+ const parameter = /(?:[?&])secret=([0-9a-f]{64})(?=[^0-9a-f]|$)/i.exec(match.text);
11267
12205
  if (parameter)
11268
12206
  secrets.add(parameter[1].toLowerCase());
11269
12207
  }
@@ -11438,9 +12376,10 @@ class Secret {
11438
12376
  constructor(bytes) {
11439
12377
  const stored = bufferFrom(bytes);
11440
12378
  this.#bytes = stored;
11441
- const canonicalLength = (0, index_js_1.canonicalMacaroonLength)(stored);
12379
+ // A valid macaroon hashes its canonical form (see canonicalMacaroonForm), anything else its bytes.
12380
+ const canonical = (0, index_js_1.canonicalMacaroonForm)(stored);
11442
12381
  const hash = newHash('sha256');
11443
- hashUpdate(hash, subarray(stored, 0, canonicalLength ?? stored.length));
12382
+ hashUpdate(hash, canonical?.reencoded ?? subarray(stored, 0, canonical?.length ?? stored.length));
11444
12383
  this.fingerprint = `fp:${hashDigest(hash, 'hex').slice(0, 16)}`;
11445
12384
  }
11446
12385
  toString() {
@@ -11545,23 +12484,50 @@ function createAlertThrottle(options) {
11545
12484
  suppressed: () => suppressedTotal,
11546
12485
  };
11547
12486
  }
11548
- /** Runs alert commands one at a time; while one runs, only the newest event waits. */
12487
+ /**
12488
+ * Runs alert commands one at a time; while one runs, only the newest event waits.
12489
+ * Each run gets an AbortSignal: aborting it cancels the command (its process tree is killed).
12490
+ * stop() ends alerting for good (watch shutdown, audit MW102-05): nothing new starts, the
12491
+ * waiting event is discarded, and the running command gets one deadline after which it is
12492
+ * cancelled. Discarded and refused events are reported through onFailure: no command ran.
12493
+ */
11549
12494
  function createAlertRunner(run, onFailure) {
11550
12495
  let running = null;
12496
+ let control = null;
11551
12497
  let pending = null;
11552
12498
  let replaced = 0;
12499
+ let stopped = false;
11553
12500
  const start = (event) => {
11554
- running = run(event).catch(() => onFailure(event)).finally(() => {
12501
+ const own = new AbortController();
12502
+ control = own;
12503
+ let attempt;
12504
+ try {
12505
+ attempt = run(event, own.signal);
12506
+ }
12507
+ catch (error) {
12508
+ attempt = Promise.reject(error);
12509
+ }
12510
+ running = attempt.catch(() => onFailure(event)).finally(() => {
11555
12511
  running = null;
11556
- if (pending) {
12512
+ control = null;
12513
+ if (pending && !stopped) {
11557
12514
  const next = pending;
11558
12515
  pending = null;
11559
12516
  start(next);
11560
12517
  }
11561
12518
  });
11562
12519
  };
12520
+ const wait = (promise, ms) => {
12521
+ let timer;
12522
+ return Promise.race([promise.then(() => true), new Promise(resolve => { timer = setTimeout(() => resolve(false), ms); })])
12523
+ .finally(() => clearTimeout(timer));
12524
+ };
11563
12525
  return {
11564
12526
  submit(event) {
12527
+ if (stopped) {
12528
+ onFailure(event);
12529
+ return;
12530
+ }
11565
12531
  if (running) {
11566
12532
  if (pending)
11567
12533
  replaced += 1;
@@ -11573,6 +12539,23 @@ function createAlertRunner(run, onFailure) {
11573
12539
  /** Resolves once no alert command is running or waiting. */
11574
12540
  async idle() { while (running)
11575
12541
  await running; },
12542
+ /**
12543
+ * Stops alerting: refuses new events, discards the waiting one, lets the running command
12544
+ * finish for at most graceMs, then cancels it and waits at most settleMs for it to settle.
12545
+ */
12546
+ async stop(graceMs, settleMs) {
12547
+ stopped = true;
12548
+ if (pending) {
12549
+ const dropped = pending;
12550
+ pending = null;
12551
+ onFailure(dropped);
12552
+ }
12553
+ const active = running;
12554
+ if (!active || await wait(active, graceMs))
12555
+ return;
12556
+ control?.abort();
12557
+ await wait(active, settleMs);
12558
+ },
11576
12559
  /** Alert command runs skipped because a newer event replaced a waiting one. */
11577
12560
  replaced: () => replaced,
11578
12561
  };
@@ -11582,8 +12565,9 @@ function createAlertRunner(run, onFailure) {
11582
12565
  "watch/alert.js": (module, exports, require) => {
11583
12566
  "use strict";
11584
12567
  exports.__esModule = true;
11585
- exports.ALERT_GRACE_MS = exports.ALERT_KILL_MS = exports.ALERT_TIMEOUT_MS = void 0;
12568
+ exports.ALERT_CLEANUP_MS = exports.ALERT_GRACE_MS = exports.ALERT_KILL_MS = exports.ALERT_TIMEOUT_MS = void 0;
11586
12569
  exports.reapAlertGroups = reapAlertGroups;
12570
+ exports.alertCleanupDone = alertCleanupDone;
11587
12571
  exports.runAlertCommand = runAlertCommand;
11588
12572
  // Runs the operator's --alert-command for a canary event: a fixed absolute executable,
11589
12573
  // no shell, no arguments, a minimal environment, the event JSON on stdin and a timeout.
@@ -11602,12 +12586,23 @@ exports.runAlertCommand = runAlertCommand;
11602
12586
  // On Windows there are no process groups: a timed-out command's process tree is killed with
11603
12587
  // System32 taskkill /T /F, but children of a command that has already exited cannot be found
11604
12588
  // by tree and are not killed. Alert commands on Windows must not leave background processes.
12589
+ //
12590
+ // Shutdown (audit MW102-05): watch stops the alert runner, which cancels a command still
12591
+ // running at its deadline through the AbortSignal: the group gets SIGKILL (POSIX) or the tree
12592
+ // is killed with taskkill (Windows), and the run settles once that is done. Every taskkill is
12593
+ // tracked until it exits (each bounded by TASKKILL_MS): watch waits for them before it returns
12594
+ // and the CLI arms its forced-exit deadline only after they finished (alertCleanupDone), since
12595
+ // ending this process ends the command itself and orphans the children taskkill must find.
11605
12596
  // spawn is used rather than execFile: execFile does not pass `detached` through, so it
11606
12597
  // cannot create the process group.
11607
12598
  const node_child_process_1 = require("node:child_process");
11608
12599
  exports.ALERT_TIMEOUT_MS = 10_000;
11609
12600
  exports.ALERT_KILL_MS = 12_000;
11610
12601
  exports.ALERT_GRACE_MS = 1_000;
12602
+ /** How long watch's shutdown waits for process-tree kills to finish before it returns. */
12603
+ exports.ALERT_CLEANUP_MS = 1_000;
12604
+ /** A taskkill that has not exited by then is abandoned (and killed). */
12605
+ const TASKKILL_MS = 10_000;
11611
12606
  // Process groups of alert commands that may still have members (POSIX).
11612
12607
  const liveGroups = new Set();
11613
12608
  const killGroup = (pid) => {
@@ -11622,18 +12617,51 @@ function reapAlertGroups() {
11622
12617
  for (const pid of [...liveGroups])
11623
12618
  killGroup(pid);
11624
12619
  }
12620
+ // Windows process-tree kills (taskkill) that have not finished yet.
12621
+ const treeKills = new Set();
11625
12622
  /** Kills a Windows process tree with System32 taskkill: absolute path, no shell, bounded. */
11626
12623
  function killTree(pid, spawn) {
11627
12624
  const root = process.env.SystemRoot;
11628
12625
  if (!root || !/^[A-Za-z]:[\\/]/.test(root))
11629
- return;
12626
+ return Promise.resolve();
12627
+ let killer;
12628
+ try {
12629
+ killer = spawn(`${root}\\System32\\taskkill.exe`, ['/T', '/F', '/PID', String(pid)], { shell: false, windowsHide: true, stdio: 'ignore' });
12630
+ }
12631
+ catch {
12632
+ return Promise.resolve(); /* best effort */
12633
+ }
12634
+ const done = new Promise(resolve => {
12635
+ const finish = () => { clearTimeout(timer); treeKills.delete(done); resolve(); };
12636
+ const timer = setTimeout(() => { try {
12637
+ killer.kill();
12638
+ }
12639
+ catch { /* gone */ } finish(); }, TASKKILL_MS);
12640
+ timer.unref?.();
12641
+ killer.once('exit', finish);
12642
+ killer.once('error', finish);
12643
+ });
12644
+ treeKills.add(done);
12645
+ return done;
12646
+ }
12647
+ /** Resolves once every process-tree kill started so far has finished, or after `ms`. */
12648
+ async function alertCleanupDone(ms = TASKKILL_MS) {
12649
+ let timer;
12650
+ let expired = false;
12651
+ const deadline = new Promise(resolve => { timer = setTimeout(() => { expired = true; resolve(); }, ms); });
11630
12652
  try {
11631
- const killer = spawn(`${root}\\System32\\taskkill.exe`, ['/T', '/F', '/PID', String(pid)], { shell: false, windowsHide: true, stdio: 'ignore' });
11632
- killer.once('error', () => undefined);
12653
+ while (treeKills.size && !expired)
12654
+ await Promise.race([Promise.all([...treeKills]), deadline]);
12655
+ }
12656
+ finally {
12657
+ clearTimeout(timer);
11633
12658
  }
11634
- catch { /* best effort */ }
11635
12659
  }
11636
- function runAlertCommand(command, event, spawn = node_child_process_1.spawn, platform = process.platform) {
12660
+ /**
12661
+ * Runs the alert command once. Aborting `signal` cancels it: the process group (POSIX) or
12662
+ * tree (Windows) is killed at once, and the run settles with an error after that finished.
12663
+ */
12664
+ function runAlertCommand(command, event, spawn = node_child_process_1.spawn, platform = process.platform, signal) {
11637
12665
  return new Promise((resolve, reject) => {
11638
12666
  const env = {};
11639
12667
  for (const name of ['PATH', 'Path', 'SystemRoot', 'SYSTEMROOT', 'HOME', 'USERPROFILE', 'TEMP', 'TMP', 'TMPDIR'])
@@ -11643,17 +12671,25 @@ function runAlertCommand(command, event, spawn = node_child_process_1.spawn, pla
11643
12671
  let settled = false;
11644
12672
  // Filled in after spawn; settle may run before (a synchronous spawn failure).
11645
12673
  const timers = {};
12674
+ let cancel = () => settle(new Error('alert command cancelled'));
12675
+ const onAbort = () => cancel();
11646
12676
  const settle = (error) => {
11647
12677
  if (settled)
11648
12678
  return;
11649
12679
  settled = true;
11650
12680
  clearTimeout(timers.term);
11651
12681
  clearTimeout(timers.kill);
12682
+ signal?.removeEventListener('abort', onAbort);
11652
12683
  if (error)
11653
12684
  reject(error);
11654
12685
  else
11655
12686
  resolve();
11656
12687
  };
12688
+ if (signal?.aborted) {
12689
+ settle(new Error('alert command cancelled'));
12690
+ return;
12691
+ }
12692
+ signal?.addEventListener('abort', onAbort, { once: true });
11657
12693
  let child;
11658
12694
  try {
11659
12695
  child = spawn(command, [], { shell: false, windowsHide: true, env, detached: group, stdio: ['pipe', 'ignore', 'ignore'] });
@@ -11673,7 +12709,8 @@ function runAlertCommand(command, event, spawn = node_child_process_1.spawn, pla
11673
12709
  }
11674
12710
  settle(code === 0 ? undefined : new Error('alert command failed'));
11675
12711
  });
11676
- const signal = (name) => {
12712
+ /** Signals the command's group (POSIX) or kills its tree (Windows); resolves when that is done. */
12713
+ const kill = (name) => {
11677
12714
  // A negative PID addresses the process group created by detached: true.
11678
12715
  try {
11679
12716
  if (group && child.pid)
@@ -11681,7 +12718,7 @@ function runAlertCommand(command, event, spawn = node_child_process_1.spawn, pla
11681
12718
  // Windows has no catchable SIGTERM: terminating the command first would orphan its
11682
12719
  // children before the tree could be found. Kill the whole tree while the command lives.
11683
12720
  else if (platform === 'win32' && child.pid)
11684
- killTree(child.pid, spawn);
12721
+ return killTree(child.pid, spawn);
11685
12722
  else
11686
12723
  child.kill(name);
11687
12724
  }
@@ -11691,9 +12728,12 @@ function runAlertCommand(command, event, spawn = node_child_process_1.spawn, pla
11691
12728
  }
11692
12729
  catch { /* already gone */ }
11693
12730
  }
12731
+ return Promise.resolve();
11694
12732
  };
11695
- timers.term = setTimeout(() => signal('SIGTERM'), exports.ALERT_TIMEOUT_MS);
11696
- timers.kill = setTimeout(() => { signal('SIGKILL'); settle(new Error('alert command timed out')); }, exports.ALERT_KILL_MS);
12733
+ // Cancelled (watch is stopping): kill now; the run settles once the tree kill is done.
12734
+ cancel = () => { void kill('SIGKILL').then(() => settle(new Error('alert command cancelled'))); };
12735
+ timers.term = setTimeout(() => { void kill('SIGTERM'); }, exports.ALERT_TIMEOUT_MS);
12736
+ timers.kill = setTimeout(() => { void kill('SIGKILL'); settle(new Error('alert command timed out')); }, exports.ALERT_KILL_MS);
11697
12737
  child.stdin?.on('error', () => undefined);
11698
12738
  child.stdin?.end(`${JSON.stringify({ schema: 1, ...event })}\n`);
11699
12739
  });
@@ -11736,6 +12776,7 @@ var __importStar = (this && this.__importStar) || (function () {
11736
12776
  };
11737
12777
  })();
11738
12778
  exports.__esModule = true;
12779
+ exports.CANARY_FILE_LIMIT = void 0;
11739
12780
  exports.canaryConfigPath = canaryConfigPath;
11740
12781
  exports.newCaveatName = newCaveatName;
11741
12782
  exports.newCondition = newCondition;
@@ -11751,9 +12792,12 @@ exports.isCanary = isCanary;
11751
12792
  // innocuous because `lncli printmacaroon` shows it; each canary has its own condition.
11752
12793
  const nodeFs = __importStar(require("node:fs/promises"));
11753
12794
  const node_crypto_1 = require("node:crypto");
12795
+ const bounded_file_js_1 = require("../bounded-file.js");
11754
12796
  const words = ['tenant', 'scope', 'profile', 'policy', 'region', 'cluster'];
11755
12797
  const namePattern = /^(?:tenant|scope|profile|policy|region|cluster)-[0-9a-f]{8}$/;
11756
12798
  const conditionPattern = /^[0-9a-f]{16}$/;
12799
+ /** Largest canary configuration read: the format holds one short name (about 50 bytes). */
12800
+ exports.CANARY_FILE_LIMIT = 4096;
11757
12801
  function canaryConfigPath(registryPath) { return `${registryPath}.canary.json`; }
11758
12802
  function newCaveatName(random = node_crypto_1.randomBytes) {
11759
12803
  const bytes = random(5);
@@ -11781,10 +12825,15 @@ function parse(text) {
11781
12825
  throw new Error('invalid canary configuration');
11782
12826
  return { caveatName: record.caveatName };
11783
12827
  }
12828
+ /**
12829
+ * Reads the configuration through the bounded regular-file reader: a protected path or
12830
+ * target (SAFE-02), anything but a regular file, or a file over CANARY_FILE_LIMIT is refused
12831
+ * before its content is read. A missing file is null.
12832
+ */
11784
12833
  async function readCanaryConfig(path, fs = nodeFs) {
11785
12834
  let text;
11786
12835
  try {
11787
- text = await fs.readFile(path, 'utf8');
12836
+ text = (await (0, bounded_file_js_1.readBoundedFile)(path, fs, exports.CANARY_FILE_LIMIT)).toString('utf8');
11788
12837
  }
11789
12838
  catch (error) {
11790
12839
  if (error.code === 'ENOENT')
@@ -12020,7 +13069,7 @@ var __importStar = (this && this.__importStar) || (function () {
12020
13069
  };
12021
13070
  })();
12022
13071
  exports.__esModule = true;
12023
- exports.UsageStore = exports.UsageFileError = exports.COVERAGE_LIMIT = exports.CANARY_LIMIT = exports.USAGE_LIMIT = void 0;
13072
+ exports.UsageStore = exports.UsageFileError = exports.USAGE_FILE_LIMIT = exports.SPAN_LIMIT = exports.COVERAGE_LIMIT = exports.CANARY_LIMIT = exports.USAGE_LIMIT = void 0;
12024
13073
  exports.usagePath = usagePath;
12025
13074
  exports.readUsage = readUsage;
12026
13075
  exports.writeUsage = writeUsage;
@@ -12031,6 +13080,7 @@ exports.writeUsage = writeUsage;
12031
13080
  const nodeFs = __importStar(require("node:fs/promises"));
12032
13081
  const node_crypto_1 = require("node:crypto");
12033
13082
  const node_path_1 = require("node:path");
13083
+ const bounded_file_js_1 = require("../bounded-file.js");
12034
13084
  // Callers choose how many distinct (credential, method) pairs exist, so the store is
12035
13085
  // bounded. At capacity the least recently seen record is evicted (O(1): the Map keeps
12036
13086
  // records in recency order), and the eviction is remembered so `recommend` can tell
@@ -12041,6 +13091,22 @@ exports.USAGE_LIMIT = 20_000;
12041
13091
  // one registry serves. At capacity the least recently updated record is dropped.
12042
13092
  exports.CANARY_LIMIT = 1_000;
12043
13093
  exports.COVERAGE_LIMIT = 64;
13094
+ /**
13095
+ * Per node, the most recent registered spans (start, end), oldest first and disjoint. They
13096
+ * place the covered time on the clock, so recommend can count only the coverage after a
13097
+ * credential was created. At most this many per node: when more, the oldest is dropped,
13098
+ * which only ever undercounts. Written as the top-level `coverageSpans` (from 1.1.0), apart
13099
+ * from `coverage`, whose records 1.0.1 reads with exactly their original keys.
13100
+ */
13101
+ exports.SPAN_LIMIT = 256;
13102
+ /**
13103
+ * Largest usage file read, derived from the record caps: 1 KiB per usage record (watch
13104
+ * writes at most about 600 bytes: a method name over 256 bytes is recorded as
13105
+ * /unknown/Unknown), 512 bytes per canary use or coverage record (at most about 200), 64 bytes per
13106
+ * coverage span (55 written) and 4 KiB for the counters. A larger file is refused before it is parsed.
13107
+ */
13108
+ exports.USAGE_FILE_LIMIT = 4096 + exports.USAGE_LIMIT * 1024 + (exports.CANARY_LIMIT + exports.COVERAGE_LIMIT) * 512
13109
+ + exports.COVERAGE_LIMIT * exports.SPAN_LIMIT * 64;
12044
13110
  class UsageFileError extends Error {
12045
13111
  code = 3;
12046
13112
  constructor(message) { super(message); this.name = 'UsageFileError'; }
@@ -12070,6 +13136,25 @@ function validCoverage(value) {
12070
13136
  && typeof value.lastEnd === 'string' && time.test(value.lastEnd)
12071
13137
  && count(value.coveredSeconds) && count(value.sessions);
12072
13138
  }
13139
+ function validSpans(value) {
13140
+ return object(value) && exactKeys(value, ['nodePubkey', 'spans'])
13141
+ && typeof value.nodePubkey === 'string' && NODE.test(value.nodePubkey)
13142
+ && Array.isArray(value.spans) && value.spans.length <= exports.SPAN_LIMIT
13143
+ && value.spans.every(span => Array.isArray(span) && span.length === 2
13144
+ && span.every(at => typeof at === 'string' && time.test(at)) && Date.parse(span[0]) <= Date.parse(span[1]));
13145
+ }
13146
+ /** The union of spans: overlapping or touching ones merged, so no time is counted twice. */
13147
+ function union(spans) {
13148
+ const merged = [];
13149
+ for (const [from, to] of [...spans].sort((a, b) => a[0] - b[0])) {
13150
+ const last = merged.at(-1);
13151
+ if (last && from <= last[1])
13152
+ last[1] = Math.max(last[1], to);
13153
+ else
13154
+ merged.push([from, to]);
13155
+ }
13156
+ return merged;
13157
+ }
12073
13158
  function validRecord(value) {
12074
13159
  if (!value || typeof value !== 'object' || Array.isArray(value))
12075
13160
  return false;
@@ -12108,6 +13193,8 @@ class UsageStore {
12108
13193
  coverages = new Map();
12109
13194
  /** Per node: the instant up to which the current registration was already counted. */
12110
13195
  covering = new Map();
13196
+ /** Per node with a coverage record: its registered spans in ms (see SPAN_LIMIT). */
13197
+ spans = new Map();
12111
13198
  constructor(limit = exports.USAGE_LIMIT) {
12112
13199
  this.limit = limit;
12113
13200
  }
@@ -12138,11 +13225,25 @@ class UsageStore {
12138
13225
  const now = at.toISOString();
12139
13226
  const existing = this.coverages.get(nodePubkey);
12140
13227
  this.coverages.delete(nodePubkey);
12141
- if (!existing && this.coverages.size >= exports.COVERAGE_LIMIT)
12142
- this.coverages.delete(this.coverages.keys().next().value);
13228
+ if (!existing && this.coverages.size >= exports.COVERAGE_LIMIT) {
13229
+ const oldest = this.coverages.keys().next().value;
13230
+ this.coverages.delete(oldest);
13231
+ this.spans.delete(oldest);
13232
+ }
12143
13233
  this.coverages.set(nodePubkey, existing ? { ...existing, sessions: existing.sessions + 1, lastEnd: now }
12144
13234
  : { nodePubkey, firstStart: now, lastEnd: now, coveredSeconds: 0, sessions: 1 });
12145
13235
  this.covering.set(nodePubkey, at.getTime());
13236
+ // A new span, starting no earlier than the last one ended (a clock stepping back never
13237
+ // makes spans overlap); one starting exactly where the last ended continues it.
13238
+ const spans = existing ? this.spans.get(nodePubkey) ?? [] : [];
13239
+ const last = spans.at(-1);
13240
+ const from = Math.max(at.getTime(), last?.[1] ?? -Infinity);
13241
+ if (!last || from > last[1]) {
13242
+ spans.push([from, from]);
13243
+ if (spans.length > exports.SPAN_LIMIT)
13244
+ spans.shift();
13245
+ }
13246
+ this.spans.set(nodePubkey, spans);
12146
13247
  this.touch();
12147
13248
  }
12148
13249
  /** Counts the time since the last call as covered, while a registration is current. */
@@ -12160,6 +13261,9 @@ class UsageStore {
12160
13261
  record.coveredSeconds += seconds;
12161
13262
  this.covering.set(nodePubkey, from + seconds * 1_000);
12162
13263
  }
13264
+ const span = this.spans.get(nodePubkey)?.at(-1);
13265
+ if (span && until > span[1])
13266
+ span[1] = until;
12163
13267
  record.lastEnd = at.toISOString();
12164
13268
  this.touch();
12165
13269
  }
@@ -12172,6 +13276,21 @@ class UsageStore {
12172
13276
  const record = this.coverages.get(nodePubkey.toLowerCase());
12173
13277
  return record ? { ...record } : null;
12174
13278
  }
13279
+ /**
13280
+ * Whole seconds of recorded spans after the instant `since` (ms): a lower bound of the time
13281
+ * watch was registered with this node since then. Null when no spans were recorded (a file
13282
+ * written before 1.1.0, or rewritten by 1.0.1).
13283
+ */
13284
+ coveredSince(nodePubkey, since) {
13285
+ const node = nodePubkey.toLowerCase();
13286
+ const spans = this.coverages.has(node) ? this.spans.get(node) : undefined;
13287
+ if (!spans?.length)
13288
+ return null;
13289
+ let ms = 0;
13290
+ for (const [from, to] of spans)
13291
+ ms += Math.max(0, to - Math.max(from, since));
13292
+ return Math.floor(ms / 1_000);
13293
+ }
12175
13294
  key(nodePubkey, credential, rootKeyId, method) {
12176
13295
  return `${nodePubkey}\0${credential}\0${rootKeyId}\0${method}`;
12177
13296
  }
@@ -12250,7 +13369,9 @@ class UsageStore {
12250
13369
  lastEviction: this.lastEviction, droppedAttributions: this.droppedAttributions,
12251
13370
  lastAttributionDrop: this.lastAttributionDropMs === null ? this.lastAttributionDrop
12252
13371
  : new Date(this.lastAttributionDropMs).toISOString(), canaryUses: [...this.canaryUses.values()],
12253
- coverage: [...this.coverages.values()], entries: [...this.records.values()] })}\n`;
13372
+ coverage: [...this.coverages.values()], coverageSpans: [...this.spans].map(([nodePubkey, spans]) => ({ nodePubkey,
13373
+ spans: spans.map(([from, to]) => [new Date(from).toISOString(), new Date(to).toISOString()]) })),
13374
+ entries: [...this.records.values()] })}\n`;
12254
13375
  }
12255
13376
  static parse(text, limit = exports.USAGE_LIMIT) {
12256
13377
  let data;
@@ -12273,7 +13394,8 @@ class UsageStore {
12273
13394
  || !count(value.droppedAttributions) || !instant(value.lastAttributionDrop)
12274
13395
  || !Array.isArray(value.entries) || !value.entries.every(validRecord)
12275
13396
  || (value.canaryUses !== undefined && (!Array.isArray(value.canaryUses) || !value.canaryUses.every(validCanaryUse)))
12276
- || (value.coverage !== undefined && (!Array.isArray(value.coverage) || !value.coverage.every(validCoverage))))
13397
+ || (value.coverage !== undefined && (!Array.isArray(value.coverage) || !value.coverage.every(validCoverage)))
13398
+ || (value.coverageSpans !== undefined && (!Array.isArray(value.coverageSpans) || !value.coverageSpans.every(validSpans))))
12277
13399
  throw new UsageFileError('invalid usage file');
12278
13400
  const store = new UsageStore(limit);
12279
13401
  store.dropped = value.dropped;
@@ -12299,6 +13421,14 @@ class UsageStore {
12299
13421
  store.coverages.delete(store.coverages.keys().next().value);
12300
13422
  store.coverages.set(item.nodePubkey, { ...item });
12301
13423
  }
13424
+ // Spans are kept only for nodes that kept a coverage record, so they share its bound.
13425
+ for (const item of value.coverageSpans ?? []) {
13426
+ if (store.spans.has(item.nodePubkey))
13427
+ throw new UsageFileError('invalid usage file');
13428
+ if (!store.coverages.has(item.nodePubkey))
13429
+ continue;
13430
+ store.spans.set(item.nodePubkey, union(item.spans.map(([from, to]) => [Date.parse(from), Date.parse(to)])));
13431
+ }
12302
13432
  // Oldest first, so the most recently seen records are kept when a file holds more
12303
13433
  // than this store's limit (for example one written before the limit was lowered).
12304
13434
  const ordered = [...value.entries].sort((a, b) => a.lastSeen < b.lastSeen ? -1 : a.lastSeen > b.lastSeen ? 1 : 0);
@@ -12323,14 +13453,21 @@ function sortRecords(records) {
12323
13453
  || order(a.credential, b.credential) || order(a.method, b.method));
12324
13454
  }
12325
13455
  function usagePath(registryPath) { return `${registryPath}.usage.json`; }
13456
+ /**
13457
+ * Reads the usage file through the bounded regular-file reader: a path or target with a
13458
+ * protected name (SAFE-02), anything but a regular file, or a file over USAGE_FILE_LIMIT is
13459
+ * refused before its content is read or parsed. A missing file is an empty store.
13460
+ */
12326
13461
  async function readUsage(path, fs = nodeFs) {
12327
13462
  let text;
12328
13463
  try {
12329
- text = await fs.readFile(path, 'utf8');
13464
+ text = (await (0, bounded_file_js_1.readBoundedFile)(path, fs, exports.USAGE_FILE_LIMIT)).toString('utf8');
12330
13465
  }
12331
13466
  catch (error) {
12332
13467
  if (error.code === 'ENOENT')
12333
13468
  return new UsageStore();
13469
+ if (error instanceof bounded_file_js_1.BoundedFileError && error.message === 'file too large')
13470
+ throw new UsageFileError('usage file too large');
12334
13471
  throw new UsageFileError('cannot read usage file');
12335
13472
  }
12336
13473
  return UsageStore.parse(text);
@@ -12879,229 +14016,259 @@ function load(id) { if (id === 'grade/lnd-permissions.json') return snapshot; if
12879
14016
  const parts = id.split('/'); parts.pop(); for (const part of name.split('/')) {
12880
14017
  if (part === '..') parts.pop(); else if (part !== '.') parts.push(part);
12881
14018
  } return load(parts.join('/')); }); return module.exports; }
12882
- export const api0_withCancellation = load("cancellation.js").withCancellation;
12883
- export const api0_checkCancellation = load("cancellation.js").checkCancellation;
12884
- export const api0_cancellable = load("cancellation.js").cancellable;
12885
- export const api0_cancellationSignal = load("cancellation.js").cancellationSignal;
12886
- export const api0_cleanup = load("cancellation.js").cleanup;
12887
- export const api0_cleanupOutcome = load("cancellation.js").cleanupOutcome;
12888
- export const api0_Cancelled = load("cancellation.js").Cancelled;
12889
- export const api1_canonicalIp = load("caveat-address.js").canonicalIp;
12890
- export const api1_canonicalCidr = load("caveat-address.js").canonicalCidr;
12891
- export const api2_writeChunks = load("cli.js").writeChunks;
12892
- export const api2_main = load("cli.js").main;
12893
- export const api2_completionStatus = load("cli.js").completionStatus;
12894
- export const api2_WATCH_SUMMARY_WRITE_MS = load("cli.js").WATCH_SUMMARY_WRITE_MS;
12895
- export const api4_run = load("commands/attenuate.js").run;
12896
- export const api4_attenuate = load("commands/attenuate.js").attenuate;
12897
- export const api5_allocateRootKeyId = load("commands/bake/allocate.js").allocateRootKeyId;
12898
- export const api6_run = load("commands/bake/index.js").run;
12899
- export const api6_bake = load("commands/bake/index.js").bake;
12900
- export const api7_reserveOut = load("commands/bake/write-out.js").reserveOut;
12901
- export const api8_handlers = load("commands/handlers.js").handlers;
12902
- export const api9_run = load("commands/inspect.js").run;
12903
- export const api9_inspect = load("commands/inspect.js").inspect;
12904
- export const api10_run = load("commands/inventory.js").run;
12905
- export const api10_inventory = load("commands/inventory.js").inventory;
12906
- export const api11_narrowLabel = load("commands/recommend.js").narrowLabel;
12907
- export const api11_run = load("commands/recommend.js").run;
12908
- export const api11_recommend = load("commands/recommend.js").recommend;
12909
- export const api11_SHORT_OBSERVATION_SECONDS = load("commands/recommend.js").SHORT_OBSERVATION_SECONDS;
12910
- export const api12_aggregateExit = load("commands/respond.js").aggregateExit;
12911
- export const api12_run = load("commands/respond.js").run;
12912
- export const api12_respond = load("commands/respond.js").respond;
12913
- export const api12_ROOT_KEY_0_GUIDANCE = load("commands/respond.js").ROOT_KEY_0_GUIDANCE;
12914
- export const api12_OWN_ON_ROOT_KEY_0 = load("commands/respond.js").OWN_ON_ROOT_KEY_0;
12915
- export const api12_REVOKED_BUT_PRESENT_GUIDANCE = load("commands/respond.js").REVOKED_BUT_PRESENT_GUIDANCE;
12916
- export const api12_BAKING_RISK_GUIDANCE = load("commands/respond.js").BAKING_RISK_GUIDANCE;
12917
- export const api12_CUSTOM_CAVEAT_NOTE = load("commands/respond.js").CUSTOM_CAVEAT_NOTE;
12918
- export const api12_UNKNOWN_GUIDANCE = load("commands/respond.js").UNKNOWN_GUIDANCE;
12919
- export const api13_run = load("commands/revoke.js").run;
12920
- export const api13_revoke = load("commands/revoke.js").revoke;
12921
- export const api14_run = load("commands/rotate.js").run;
12922
- export const api14_rotate = load("commands/rotate.js").rotate;
12923
- export const api15_run = load("commands/scan.js").run;
12924
- export const api16_lockOwnerGone = load("commands/watch.js").lockOwnerGone;
12925
- export const api16_acquireLock = load("commands/watch.js").acquireLock;
12926
- export const api16_run = load("commands/watch.js").run;
12927
- export const api16_stdoutEvents = load("commands/watch.js").stdoutEvents;
12928
- export const api16_watch = load("commands/watch.js").watch;
12929
- export const api16_CANARY_RELOAD_MS = load("commands/watch.js").CANARY_RELOAD_MS;
12930
- export const api17_redactEncodedCredentials = load("credential-recognition.js").redactEncodedCredentials;
12931
- export const api18_macaroonPacketRange = load("decode/index.js").macaroonPacketRange;
12932
- export const api18_macaroonSectionRange = load("decode/index.js").macaroonSectionRange;
12933
- export const api18_parseTimeBefore = load("decode/index.js").parseTimeBefore;
12934
- export const api18_canonicalMacaroonLength = load("decode/index.js").canonicalMacaroonLength;
12935
- export const api18_decodeMacaroon = load("decode/index.js").decodeMacaroon;
12936
- export const api18_macaroonId = load("decode/index.js").macaroonId;
12937
- export const api18_encodeMacaroon = load("decode/index.js").encodeMacaroon;
12938
- export const api18_addFirstPartyCaveat = load("decode/index.js").addFirstPartyCaveat;
12939
- export const api18_timeBeforeCondition = load("decode/index.js").timeBeforeCondition;
12940
- export const api18_validMacaroonIdentifier = load("decode/index.js").validMacaroonIdentifier;
12941
- export const api18_isNwcContainer = load("decode/index.js").isNwcContainer;
12942
- export const api18_decodeNwc = load("decode/index.js").decodeNwc;
12943
- export const api18_DecodeError = load("decode/index.js").DecodeError;
12944
- export const api19_macaroonEnds = load("decode/recognition.js").macaroonEnds;
12945
- export const api20_unsafeNodeDiagnostics = load("environment.js").unsafeNodeDiagnostics;
12946
- export const api21_UsageError = load("errors.js").UsageError;
12947
- export const api21_InputError = load("errors.js").InputError;
12948
- export const api21_DecodeError = load("errors.js").DecodeError;
12949
- export const api21_LndError = load("errors.js").LndError;
12950
- export const api21_LndUncertainError = load("errors.js").LndUncertainError;
12951
- export const api22_grade = load("grade/index.js").grade;
12952
- export const api22_describe = load("grade/index.js").describe;
12953
- export const api23_groupedRuns = load("grouped-runs.js").groupedRuns;
12954
- export const api23_groupedBase64 = load("grouped-runs.js").groupedBase64;
12955
- export const api24_readCredential = load("input/index.js").readCredential;
12956
- export const api24_InputError = load("input/index.js").InputError;
12957
- export const api24_UsageError = load("input/index.js").UsageError;
12958
- export const api25_parseLndconnect = load("input/lndconnect.js").parseLndconnect;
12959
- export const api26_openGrpcStream = load("lnd/grpc.js").openGrpcStream;
12960
- export const api26_MAX_GRPC_MESSAGE = load("lnd/grpc.js").MAX_GRPC_MESSAGE;
12961
- export const api26_STALL_TIMEOUT_MS = load("lnd/grpc.js").STALL_TIMEOUT_MS;
12962
- export const api26_MAX_PENDING_WRITE_BYTES = load("lnd/grpc.js").MAX_PENDING_WRITE_BYTES;
12963
- export const api26_WRITE_OVERHEAD_BYTES = load("lnd/grpc.js").WRITE_OVERHEAD_BYTES;
12964
- export const api26_PING_INTERVAL_MS = load("lnd/grpc.js").PING_INTERVAL_MS;
12965
- export const api26_PING_TIMEOUT_MS = load("lnd/grpc.js").PING_TIMEOUT_MS;
12966
- export const api27_createLndAdapter = load("lnd/index.js").createLndAdapter;
12967
- export const api27_registerMiddleware = load("lnd/index.js").registerMiddleware;
12968
- export const api28_decodeMiddlewareRequest = load("lnd/middleware-proto.js").decodeMiddlewareRequest;
12969
- export const api28_encodeRegistration = load("lnd/middleware-proto.js").encodeRegistration;
12970
- export const api28_encodeFeedback = load("lnd/middleware-proto.js").encodeFeedback;
12971
- export const api28_MAX_MACAROON_BYTES = load("lnd/middleware-proto.js").MAX_MACAROON_BYTES;
12972
- export const api28_MAX_METHOD_BYTES = load("lnd/middleware-proto.js").MAX_METHOD_BYTES;
12973
- export const api28_MAX_CONDITION_BYTES = load("lnd/middleware-proto.js").MAX_CONDITION_BYTES;
12974
- export const api28_UNKNOWN_METHOD = load("lnd/middleware-proto.js").UNKNOWN_METHOD;
12975
- export const api29_endpoint = load("online.js").endpoint;
12976
- export const api29_credentialText = load("online.js").credentialText;
12977
- export const api29_connectionProblem = load("online.js").connectionProblem;
12978
- export const api29_flagProblem = load("online.js").flagProblem;
12979
- export const api29_nodeInfo = load("online.js").nodeInfo;
12980
- export const api29_nodePubkey = load("online.js").nodePubkey;
12981
- export const api29_drift = load("online.js").drift;
12982
- export const api29_samePermissions = load("online.js").samePermissions;
12983
- export const api29_listIds = load("online.js").listIds;
12984
- export const api29_failureDetails = load("online.js").failureDetails;
12985
- export const api29_readBoundedFile = load("online.js").readBoundedFile;
12986
- export const api29_onlineAdapter = load("online.js").onlineAdapter;
12987
- export const api29_shellQuote = load("online.js").shellQuote;
12988
- export const api29_recoveryPath = load("online.js").recoveryPath;
12989
- export const api29_recoveryCommand = load("online.js").recoveryCommand;
12990
- export const api29_inventoryRecoveryCommand = load("online.js").inventoryRecoveryCommand;
12991
- export const api29_readOnlineCredential = load("online.js").readOnlineCredential;
12992
- export const api29_onlineWarnings = load("online.js").onlineWarnings;
12993
- export const api29_LABEL_RULE = load("online.js").LABEL_RULE;
12994
- export const api30_rotatePlanResult = load("output/index.js").rotatePlanResult;
12995
- export const api30_revokePlanResult = load("output/index.js").revokePlanResult;
12996
- export const api30_error = load("output/index.js").error;
12997
- export const api30_warning = load("output/index.js").warning;
12998
- export const api30_renderWatchEvent = load("output/index.js").renderWatchEvent;
12999
- export const api30_formatDuration = load("output/index.js").formatDuration;
13000
- export const api30_revokeConfirmation = load("output/index.js").revokeConfirmation;
13001
- export const api30_writeRevokePrompt = load("output/index.js").writeRevokePrompt;
13002
- export const api30_writeRotateFinishPlan = load("output/index.js").writeRotateFinishPlan;
13003
- export const api30_rotateConfirmation = load("output/index.js").rotateConfirmation;
13004
- export const api30_writeRotatePrompt = load("output/index.js").writeRotatePrompt;
13005
- export const api30_render = load("output/index.js").render;
13006
- export const api30_renderChunks = load("output/index.js").renderChunks;
13007
- export const api30_inventoryResult = load("output/index.js").inventoryResult;
13008
- export const api30_writeRotateStagePlan = load("output/index.js").writeRotateStagePlan;
13009
- export const api31_ruleIdFor = load("output/scan-formats.js").ruleIdFor;
13010
- export const api31_findingSeverity = load("output/scan-formats.js").findingSeverity;
13011
- export const api31_locationOf = load("output/scan-formats.js").locationOf;
13012
- export const api31_fileUri = load("output/scan-formats.js").fileUri;
13013
- export const api31_sarifPieces = load("output/scan-formats.js").sarifPieces;
13014
- export const api31_escapeData = load("output/scan-formats.js").escapeData;
13015
- export const api31_escapeProperty = load("output/scan-formats.js").escapeProperty;
13016
- export const api31_githubLines = load("output/scan-formats.js").githubLines;
13017
- export const api31_SARIF_MAX_RESULTS = load("output/scan-formats.js").SARIF_MAX_RESULTS;
13018
- export const api32_withProcessErrorContext = load("process-error-context.js").withProcessErrorContext;
13019
- export const api32_markMutationSent = load("process-error-context.js").markMutationSent;
13020
- export const api32_internalErrorMessage = load("process-error-context.js").internalErrorMessage;
13021
- export const api33_isProtectedPath = load("protected-path.js").isProtectedPath;
13022
- export const api33_resolveUnprotectedPath = load("protected-path.js").resolveUnprotectedPath;
13023
- export const api34_openRegistry = load("registry/index.js").openRegistry;
13024
- export const api34_replacementProblem = load("registry/index.js").replacementProblem;
13025
- export const api34_replacementLinkProblems = load("registry/index.js").replacementLinkProblems;
13026
- export const api34_hasCustomCaveat = load("registry/index.js").hasCustomCaveat;
13027
- export const api34_RegistryInputError = load("registry/index.js").RegistryInputError;
13028
- export const api34_RegistryUsageError = load("registry/index.js").RegistryUsageError;
13029
- export const api34_Registry = load("registry/index.js").Registry;
13030
- export const api35_validDomain = load("registry/process-domain.js").validDomain;
13031
- export const api35_ownProcessDomain = load("registry/process-domain.js").ownProcessDomain;
13032
- export const api36_buildAccessReview = load("report/access-review.js").buildAccessReview;
13033
- export const api36_csvField = load("report/access-review.js").csvField;
13034
- export const api36_formatAccessReview = load("report/access-review.js").formatAccessReview;
13035
- export const api36_REPORT_FORMATS = load("report/access-review.js").REPORT_FORMATS;
13036
- export const api37_agentConfigCandidates = load("scan/agent-configs.js").agentConfigCandidates;
13037
- export const api37_discoverAgentConfigs = load("scan/agent-configs.js").discoverAgentConfigs;
13038
- export const api37_defaultAgentConfigEnvironment = load("scan/agent-configs.js").defaultAgentConfigEnvironment;
13039
- export const api38_nonLocalPath = load("scan/git-history.js").nonLocalPath;
13040
- export const api38_locateRepository = load("scan/git-history.js").locateRepository;
13041
- export const api38_gitEnvironment = load("scan/git-history.js").gitEnvironment;
13042
- export const api38_assertWorkTree = load("scan/git-history.js").assertWorkTree;
13043
- export const api38_isShallowRepository = load("scan/git-history.js").isShallowRepository;
13044
- export const api38_historyEntries = load("scan/git-history.js").historyEntries;
13045
- export const api38_blobSizes = load("scan/git-history.js").blobSizes;
13046
- export const api38_readBlobs = load("scan/git-history.js").readBlobs;
13047
- export const api38_GitHistoryError = load("scan/git-history.js").GitHistoryError;
13048
- export const api38_MODE_REGULAR = load("scan/git-history.js").MODE_REGULAR;
13049
- export const api38_MODE_SYMLINK = load("scan/git-history.js").MODE_SYMLINK;
13050
- export const api38_MODE_OTHER = load("scan/git-history.js").MODE_OTHER;
13051
- export const api38_GIT_IDLE_TIMEOUT_MS = load("scan/git-history.js").GIT_IDLE_TIMEOUT_MS;
13052
- export const api38_MAX_HISTORY_ENTRIES = load("scan/git-history.js").MAX_HISTORY_ENTRIES;
13053
- export const api38_PINNED_CONFIG = load("scan/git-history.js").PINNED_CONFIG;
13054
- export const api39_parseIgnoreFile = load("scan/ignore.js").parseIgnoreFile;
13055
- export const api39_IgnoreFileError = load("scan/ignore.js").IgnoreFileError;
13056
- export const api40_readBounded = load("scan/index.js").readBounded;
13057
- export const api40_scan = load("scan/index.js").scan;
13058
- export const api40_IgnoreFileError = load("scan/index.js").IgnoreFileError;
13059
- export const api40_GitHistoryError = load("scan/index.js").GitHistoryError;
13060
- export const api40_FINDINGS_LIMIT = load("scan/index.js").FINDINGS_LIMIT;
13061
- export const api40_ALSO_AT_EXAMPLES = load("scan/index.js").ALSO_AT_EXAMPLES;
13062
- export const api40_HISTORY_DEDUP_WORK_LIMIT = load("scan/index.js").HISTORY_DEDUP_WORK_LIMIT;
13063
- export const api40_FindingsLimitError = load("scan/index.js").FindingsLimitError;
13064
- export const api40_SCAN_CONCURRENCY = load("scan/index.js").SCAN_CONCURRENCY;
13065
- export const api41_mcpServerStringSpans = load("scan/mcp-context.js").mcpServerStringSpans;
13066
- export const api41_withinSpans = load("scan/mcp-context.js").withinSpans;
13067
- export const api42_nwcMatches = load("scan/nwc-match.js").nwcMatches;
13068
- export const api43_macaroonHex = load("scan/patterns.js").macaroonHex;
13069
- export const api43_macaroonBase64 = load("scan/patterns.js").macaroonBase64;
13070
- export const api43_lndconnect = load("scan/patterns.js").lndconnect;
13071
- export const api43_nwc = load("scan/patterns.js").nwc;
13072
- export const api44_redactDecoded = load("scan/redact.js").redactDecoded;
13073
- export const api44_redactPath = load("scan/redact.js").redactPath;
13074
- export const api44_collectNwcSecrets = load("scan/redact.js").collectNwcSecrets;
13075
- export const api44_knownSecretRedactor = load("scan/redact.js").knownSecretRedactor;
13076
- export const api44_redactKnownSecrets = load("scan/redact.js").redactKnownSecrets;
13077
- export const api45_Secret = load("secret.js").Secret;
13078
- export const api46_macwardenVersion = load("version.js").macwardenVersion;
13079
- export const api47_createAlertThrottle = load("watch/alert-throttle.js").createAlertThrottle;
13080
- export const api47_createAlertRunner = load("watch/alert-throttle.js").createAlertRunner;
13081
- export const api47_ALERT_WINDOW_MS = load("watch/alert-throttle.js").ALERT_WINDOW_MS;
13082
- export const api47_MAX_TRACKED_CANARIES = load("watch/alert-throttle.js").MAX_TRACKED_CANARIES;
13083
- export const api48_reapAlertGroups = load("watch/alert.js").reapAlertGroups;
13084
- export const api48_runAlertCommand = load("watch/alert.js").runAlertCommand;
13085
- export const api48_ALERT_TIMEOUT_MS = load("watch/alert.js").ALERT_TIMEOUT_MS;
13086
- export const api48_ALERT_KILL_MS = load("watch/alert.js").ALERT_KILL_MS;
13087
- export const api48_ALERT_GRACE_MS = load("watch/alert.js").ALERT_GRACE_MS;
13088
- export const api49_canaryConfigPath = load("watch/canary.js").canaryConfigPath;
13089
- export const api49_newCaveatName = load("watch/canary.js").newCaveatName;
13090
- export const api49_newCondition = load("watch/canary.js").newCondition;
13091
- export const api49_canaryCondition = load("watch/canary.js").canaryCondition;
13092
- export const api49_readCanaryConfig = load("watch/canary.js").readCanaryConfig;
13093
- export const api49_ensureCanaryConfig = load("watch/canary.js").ensureCanaryConfig;
13094
- export const api49_canaryEntries = load("watch/canary.js").canaryEntries;
13095
- export const api49_isCanary = load("watch/canary.js").isCanary;
13096
- export const api50_retryable = load("watch/output-queue.js").retryable;
13097
- export const api50_fdWriter = load("watch/output-queue.js").fdWriter;
13098
- export const api50_createOutputQueue = load("watch/output-queue.js").createOutputQueue;
13099
- export const api50_OUTPUT_LIMIT_BYTES = load("watch/output-queue.js").OUTPUT_LIMIT_BYTES;
13100
- export const api51_usagePath = load("watch/usage.js").usagePath;
13101
- export const api51_readUsage = load("watch/usage.js").readUsage;
13102
- export const api51_writeUsage = load("watch/usage.js").writeUsage;
13103
- export const api51_USAGE_LIMIT = load("watch/usage.js").USAGE_LIMIT;
13104
- export const api51_CANARY_LIMIT = load("watch/usage.js").CANARY_LIMIT;
13105
- export const api51_COVERAGE_LIMIT = load("watch/usage.js").COVERAGE_LIMIT;
13106
- export const api51_UsageFileError = load("watch/usage.js").UsageFileError;
13107
- export const api51_UsageStore = load("watch/usage.js").UsageStore;
14019
+ export const api0_readBounded = load("bounded-file.js").readBounded;
14020
+ export const api0_readBoundedFile = load("bounded-file.js").readBoundedFile;
14021
+ export const api0_assertPlainFile = load("bounded-file.js").assertPlainFile;
14022
+ export const api0_BoundedFileError = load("bounded-file.js").BoundedFileError;
14023
+ export const api0_DEFAULT_FILE_LIMIT = load("bounded-file.js").DEFAULT_FILE_LIMIT;
14024
+ export const api0_LOCK_FILE_LIMIT = load("bounded-file.js").LOCK_FILE_LIMIT;
14025
+ export const api1_withCancellation = load("cancellation.js").withCancellation;
14026
+ export const api1_checkCancellation = load("cancellation.js").checkCancellation;
14027
+ export const api1_cancellable = load("cancellation.js").cancellable;
14028
+ export const api1_cancellationSignal = load("cancellation.js").cancellationSignal;
14029
+ export const api1_cleanup = load("cancellation.js").cleanup;
14030
+ export const api1_cleanupOutcome = load("cancellation.js").cleanupOutcome;
14031
+ export const api1_Cancelled = load("cancellation.js").Cancelled;
14032
+ export const api2_canonicalIp = load("caveat-address.js").canonicalIp;
14033
+ export const api2_canonicalCidr = load("caveat-address.js").canonicalCidr;
14034
+ export const api3_writeChunks = load("cli.js").writeChunks;
14035
+ export const api3_main = load("cli.js").main;
14036
+ export const api3_completionStatus = load("cli.js").completionStatus;
14037
+ export const api3_WATCH_SUMMARY_WRITE_MS = load("cli.js").WATCH_SUMMARY_WRITE_MS;
14038
+ export const api5_run = load("commands/attenuate.js").run;
14039
+ export const api5_attenuate = load("commands/attenuate.js").attenuate;
14040
+ export const api6_allocateRootKeyId = load("commands/bake/allocate.js").allocateRootKeyId;
14041
+ export const api7_run = load("commands/bake/index.js").run;
14042
+ export const api7_bake = load("commands/bake/index.js").bake;
14043
+ export const api8_reserveOut = load("commands/bake/write-out.js").reserveOut;
14044
+ export const api9_handlers = load("commands/handlers.js").handlers;
14045
+ export const api10_inspectResult = load("commands/inspect.js").inspectResult;
14046
+ export const api10_run = load("commands/inspect.js").run;
14047
+ export const api10_inspect = load("commands/inspect.js").inspect;
14048
+ export const api11_run = load("commands/inventory.js").run;
14049
+ export const api11_inventory = load("commands/inventory.js").inventory;
14050
+ export const api12_narrowLabel = load("commands/recommend.js").narrowLabel;
14051
+ export const api12_run = load("commands/recommend.js").run;
14052
+ export const api12_recommend = load("commands/recommend.js").recommend;
14053
+ export const api12_SHORT_OBSERVATION_SECONDS = load("commands/recommend.js").SHORT_OBSERVATION_SECONDS;
14054
+ export const api13_aggregateExit = load("commands/respond.js").aggregateExit;
14055
+ export const api13_run = load("commands/respond.js").run;
14056
+ export const api13_respond = load("commands/respond.js").respond;
14057
+ export const api13_ROOT_KEY_0_GUIDANCE = load("commands/respond.js").ROOT_KEY_0_GUIDANCE;
14058
+ export const api13_OWN_ON_ROOT_KEY_0 = load("commands/respond.js").OWN_ON_ROOT_KEY_0;
14059
+ export const api13_REVOKED_BUT_PRESENT_GUIDANCE = load("commands/respond.js").REVOKED_BUT_PRESENT_GUIDANCE;
14060
+ export const api13_BAKING_RISK_GUIDANCE = load("commands/respond.js").BAKING_RISK_GUIDANCE;
14061
+ export const api13_CUSTOM_CAVEAT_NOTE = load("commands/respond.js").CUSTOM_CAVEAT_NOTE;
14062
+ export const api13_UNKNOWN_GUIDANCE = load("commands/respond.js").UNKNOWN_GUIDANCE;
14063
+ export const api14_run = load("commands/revoke.js").run;
14064
+ export const api14_revoke = load("commands/revoke.js").revoke;
14065
+ export const api15_run = load("commands/rotate.js").run;
14066
+ export const api15_rotate = load("commands/rotate.js").rotate;
14067
+ export const api16_run = load("commands/scan.js").run;
14068
+ export const api17_run = load("commands/verify.js").run;
14069
+ export const api17_verify = load("commands/verify.js").verify;
14070
+ export const api17_ROOT_KEY_0_ACCEPTED = load("commands/verify.js").ROOT_KEY_0_ACCEPTED;
14071
+ export const api17_CAN_BAKE_ACCEPTED = load("commands/verify.js").CAN_BAKE_ACCEPTED;
14072
+ export const api17_CAN_BAKE_ROOT_KEY_0 = load("commands/verify.js").CAN_BAKE_ROOT_KEY_0;
14073
+ export const api17_OWN_ROOT_KEY_ACCEPTED = load("commands/verify.js").OWN_ROOT_KEY_ACCEPTED;
14074
+ export const api17_REJECTED_INCONCLUSIVE = load("commands/verify.js").REJECTED_INCONCLUSIVE;
14075
+ export const api17_CUSTOM_CAVEAT_REJECTED = load("commands/verify.js").CUSTOM_CAVEAT_REJECTED;
14076
+ export const api18_scheduleDeadline = load("commands/watch.js").scheduleDeadline;
14077
+ export const api18_lockOwnerGone = load("commands/watch.js").lockOwnerGone;
14078
+ export const api18_acquireLock = load("commands/watch.js").acquireLock;
14079
+ export const api18_run = load("commands/watch.js").run;
14080
+ export const api18_stdoutEvents = load("commands/watch.js").stdoutEvents;
14081
+ export const api18_watch = load("commands/watch.js").watch;
14082
+ export const api18_CANARY_RELOAD_MS = load("commands/watch.js").CANARY_RELOAD_MS;
14083
+ export const api18_MAX_TIMER_MS = load("commands/watch.js").MAX_TIMER_MS;
14084
+ export const api19_redactEncodedCredentials = load("credential-recognition.js").redactEncodedCredentials;
14085
+ export const api20_macaroonEosEnd = load("decode/index.js").macaroonEosEnd;
14086
+ export const api20_macaroonPacketRange = load("decode/index.js").macaroonPacketRange;
14087
+ export const api20_macaroonSectionRange = load("decode/index.js").macaroonSectionRange;
14088
+ export const api20_parseTimeBefore = load("decode/index.js").parseTimeBefore;
14089
+ export const api20_canonicalMacaroonForm = load("decode/index.js").canonicalMacaroonForm;
14090
+ export const api20_decodeMacaroon = load("decode/index.js").decodeMacaroon;
14091
+ export const api20_macaroonId = load("decode/index.js").macaroonId;
14092
+ export const api20_encodeMacaroon = load("decode/index.js").encodeMacaroon;
14093
+ export const api20_addFirstPartyCaveat = load("decode/index.js").addFirstPartyCaveat;
14094
+ export const api20_timeBeforeCondition = load("decode/index.js").timeBeforeCondition;
14095
+ export const api20_validMacaroonIdentifier = load("decode/index.js").validMacaroonIdentifier;
14096
+ export const api20_isNwcContainer = load("decode/index.js").isNwcContainer;
14097
+ export const api20_decodeNwc = load("decode/index.js").decodeNwc;
14098
+ export const api20_DecodeError = load("decode/index.js").DecodeError;
14099
+ export const api21_macaroonEnds = load("decode/recognition.js").macaroonEnds;
14100
+ export const api22_unsafeNodeDiagnostics = load("environment.js").unsafeNodeDiagnostics;
14101
+ export const api23_UsageError = load("errors.js").UsageError;
14102
+ export const api23_InputError = load("errors.js").InputError;
14103
+ export const api23_DecodeError = load("errors.js").DecodeError;
14104
+ export const api23_LndError = load("errors.js").LndError;
14105
+ export const api23_LndUncertainError = load("errors.js").LndUncertainError;
14106
+ export const api24_grade = load("grade/index.js").grade;
14107
+ export const api24_gradeFacts = load("grade/index.js").gradeFacts;
14108
+ export const api24_describe = load("grade/index.js").describe;
14109
+ export const api25_groupedRuns = load("grouped-runs.js").groupedRuns;
14110
+ export const api25_groupedBase64 = load("grouped-runs.js").groupedBase64;
14111
+ export const api26_readCredential = load("input/index.js").readCredential;
14112
+ export const api26_InputError = load("input/index.js").InputError;
14113
+ export const api26_UsageError = load("input/index.js").UsageError;
14114
+ export const api27_parseLndconnect = load("input/lndconnect.js").parseLndconnect;
14115
+ export const api28_openGrpcStream = load("lnd/grpc.js").openGrpcStream;
14116
+ export const api28_MAX_GRPC_MESSAGE = load("lnd/grpc.js").MAX_GRPC_MESSAGE;
14117
+ export const api28_STALL_TIMEOUT_MS = load("lnd/grpc.js").STALL_TIMEOUT_MS;
14118
+ export const api28_MAX_PENDING_WRITE_BYTES = load("lnd/grpc.js").MAX_PENDING_WRITE_BYTES;
14119
+ export const api28_WRITE_OVERHEAD_BYTES = load("lnd/grpc.js").WRITE_OVERHEAD_BYTES;
14120
+ export const api28_PING_INTERVAL_MS = load("lnd/grpc.js").PING_INTERVAL_MS;
14121
+ export const api28_PING_TIMEOUT_MS = load("lnd/grpc.js").PING_TIMEOUT_MS;
14122
+ export const api29_createLndAdapter = load("lnd/index.js").createLndAdapter;
14123
+ export const api29_registerMiddleware = load("lnd/index.js").registerMiddleware;
14124
+ export const api30_decodeMiddlewareRequest = load("lnd/middleware-proto.js").decodeMiddlewareRequest;
14125
+ export const api30_encodeRegistration = load("lnd/middleware-proto.js").encodeRegistration;
14126
+ export const api30_encodeFeedback = load("lnd/middleware-proto.js").encodeFeedback;
14127
+ export const api30_MAX_MACAROON_BYTES = load("lnd/middleware-proto.js").MAX_MACAROON_BYTES;
14128
+ export const api30_MAX_METHOD_BYTES = load("lnd/middleware-proto.js").MAX_METHOD_BYTES;
14129
+ export const api30_MAX_CONDITION_BYTES = load("lnd/middleware-proto.js").MAX_CONDITION_BYTES;
14130
+ export const api30_UNKNOWN_METHOD = load("lnd/middleware-proto.js").UNKNOWN_METHOD;
14131
+ export const api31_identityCheck = load("lnd/tls-identity.js").identityCheck;
14132
+ export const api32_endpoint = load("online.js").endpoint;
14133
+ export const api32_credentialText = load("online.js").credentialText;
14134
+ export const api32_connectionProblem = load("online.js").connectionProblem;
14135
+ export const api32_flagProblem = load("online.js").flagProblem;
14136
+ export const api32_nodeInfo = load("online.js").nodeInfo;
14137
+ export const api32_nodePubkey = load("online.js").nodePubkey;
14138
+ export const api32_drift = load("online.js").drift;
14139
+ export const api32_samePermissions = load("online.js").samePermissions;
14140
+ export const api32_listIds = load("online.js").listIds;
14141
+ export const api32_failureDetails = load("online.js").failureDetails;
14142
+ export const api32_onlineAdapter = load("online.js").onlineAdapter;
14143
+ export const api32_shellQuote = load("online.js").shellQuote;
14144
+ export const api32_recoveryPath = load("online.js").recoveryPath;
14145
+ export const api32_recoveryCommand = load("online.js").recoveryCommand;
14146
+ export const api32_inventoryRecoveryCommand = load("online.js").inventoryRecoveryCommand;
14147
+ export const api32_readOnlineCredential = load("online.js").readOnlineCredential;
14148
+ export const api32_onlineWarnings = load("online.js").onlineWarnings;
14149
+ export const api32_LABEL_RULE = load("online.js").LABEL_RULE;
14150
+ export const api32_readBoundedFile = load("online.js").readBoundedFile;
14151
+ export const api33_rotatePlanResult = load("output/index.js").rotatePlanResult;
14152
+ export const api33_revokePlanResult = load("output/index.js").revokePlanResult;
14153
+ export const api33_error = load("output/index.js").error;
14154
+ export const api33_warning = load("output/index.js").warning;
14155
+ export const api33_renderWatchEvent = load("output/index.js").renderWatchEvent;
14156
+ export const api33_formatDuration = load("output/index.js").formatDuration;
14157
+ export const api33_revokeConfirmation = load("output/index.js").revokeConfirmation;
14158
+ export const api33_writeRevokePrompt = load("output/index.js").writeRevokePrompt;
14159
+ export const api33_writeRotateFinishPlan = load("output/index.js").writeRotateFinishPlan;
14160
+ export const api33_rotateConfirmation = load("output/index.js").rotateConfirmation;
14161
+ export const api33_writeRotatePrompt = load("output/index.js").writeRotatePrompt;
14162
+ export const api33_render = load("output/index.js").render;
14163
+ export const api33_renderChunks = load("output/index.js").renderChunks;
14164
+ export const api33_inventoryResult = load("output/index.js").inventoryResult;
14165
+ export const api33_writeRotateStagePlan = load("output/index.js").writeRotateStagePlan;
14166
+ export const api34_ruleIdFor = load("output/scan-formats.js").ruleIdFor;
14167
+ export const api34_findingSeverity = load("output/scan-formats.js").findingSeverity;
14168
+ export const api34_locationOf = load("output/scan-formats.js").locationOf;
14169
+ export const api34_fileUri = load("output/scan-formats.js").fileUri;
14170
+ export const api34_sarifPieces = load("output/scan-formats.js").sarifPieces;
14171
+ export const api34_escapeData = load("output/scan-formats.js").escapeData;
14172
+ export const api34_escapeProperty = load("output/scan-formats.js").escapeProperty;
14173
+ export const api34_githubLines = load("output/scan-formats.js").githubLines;
14174
+ export const api34_SARIF_MAX_RESULTS = load("output/scan-formats.js").SARIF_MAX_RESULTS;
14175
+ export const api35_withProcessErrorContext = load("process-error-context.js").withProcessErrorContext;
14176
+ export const api35_markMutationSent = load("process-error-context.js").markMutationSent;
14177
+ export const api35_internalErrorMessage = load("process-error-context.js").internalErrorMessage;
14178
+ export const api36_isProtectedPath = load("protected-path.js").isProtectedPath;
14179
+ export const api36_resolveUnprotectedPath = load("protected-path.js").resolveUnprotectedPath;
14180
+ export const api37_openRegistry = load("registry/index.js").openRegistry;
14181
+ export const api37_replacementProblem = load("registry/index.js").replacementProblem;
14182
+ export const api37_replacementLinkProblems = load("registry/index.js").replacementLinkProblems;
14183
+ export const api37_hasCustomCaveat = load("registry/index.js").hasCustomCaveat;
14184
+ export const api37_RegistryInputError = load("registry/index.js").RegistryInputError;
14185
+ export const api37_RegistryUsageError = load("registry/index.js").RegistryUsageError;
14186
+ export const api37_Registry = load("registry/index.js").Registry;
14187
+ export const api38_validDomain = load("registry/process-domain.js").validDomain;
14188
+ export const api38_ownProcessDomain = load("registry/process-domain.js").ownProcessDomain;
14189
+ export const api39_buildAccessReview = load("report/access-review.js").buildAccessReview;
14190
+ export const api39_csvField = load("report/access-review.js").csvField;
14191
+ export const api39_formatAccessReview = load("report/access-review.js").formatAccessReview;
14192
+ export const api39_REPORT_FORMATS = load("report/access-review.js").REPORT_FORMATS;
14193
+ export const api39_LIMITATIONS = load("report/access-review.js").LIMITATIONS;
14194
+ export const api40_agentConfigCandidates = load("scan/agent-configs.js").agentConfigCandidates;
14195
+ export const api40_discoverAgentConfigs = load("scan/agent-configs.js").discoverAgentConfigs;
14196
+ export const api40_defaultAgentConfigEnvironment = load("scan/agent-configs.js").defaultAgentConfigEnvironment;
14197
+ export const api41_nonLocalPath = load("scan/git-history.js").nonLocalPath;
14198
+ export const api41_locateRepository = load("scan/git-history.js").locateRepository;
14199
+ export const api41_gitEnvironment = load("scan/git-history.js").gitEnvironment;
14200
+ export const api41_assertWorkTree = load("scan/git-history.js").assertWorkTree;
14201
+ export const api41_isShallowRepository = load("scan/git-history.js").isShallowRepository;
14202
+ export const api41_historyEntries = load("scan/git-history.js").historyEntries;
14203
+ export const api41_blobSizes = load("scan/git-history.js").blobSizes;
14204
+ export const api41_readBlobs = load("scan/git-history.js").readBlobs;
14205
+ export const api41_GitHistoryError = load("scan/git-history.js").GitHistoryError;
14206
+ export const api41_MODE_REGULAR = load("scan/git-history.js").MODE_REGULAR;
14207
+ export const api41_MODE_SYMLINK = load("scan/git-history.js").MODE_SYMLINK;
14208
+ export const api41_MODE_OTHER = load("scan/git-history.js").MODE_OTHER;
14209
+ export const api41_GIT_IDLE_TIMEOUT_MS = load("scan/git-history.js").GIT_IDLE_TIMEOUT_MS;
14210
+ export const api41_MAX_HISTORY_ENTRIES = load("scan/git-history.js").MAX_HISTORY_ENTRIES;
14211
+ export const api41_PINNED_CONFIG = load("scan/git-history.js").PINNED_CONFIG;
14212
+ export const api41_PREFLIGHT_LIMITS = load("scan/git-history.js").PREFLIGHT_LIMITS;
14213
+ export const api42_parseIgnoreFile = load("scan/ignore.js").parseIgnoreFile;
14214
+ export const api42_IgnoreFileError = load("scan/ignore.js").IgnoreFileError;
14215
+ export const api43_scan = load("scan/index.js").scan;
14216
+ export const api43_IgnoreFileError = load("scan/index.js").IgnoreFileError;
14217
+ export const api43_GitHistoryError = load("scan/index.js").GitHistoryError;
14218
+ export const api43_FINDINGS_LIMIT = load("scan/index.js").FINDINGS_LIMIT;
14219
+ export const api43_ALSO_AT_EXAMPLES = load("scan/index.js").ALSO_AT_EXAMPLES;
14220
+ export const api43_HISTORY_DEDUP_WORK_LIMIT = load("scan/index.js").HISTORY_DEDUP_WORK_LIMIT;
14221
+ export const api43_FindingsLimitError = load("scan/index.js").FindingsLimitError;
14222
+ export const api43_SCAN_CONCURRENCY = load("scan/index.js").SCAN_CONCURRENCY;
14223
+ export const api43_readBounded = load("scan/index.js").readBounded;
14224
+ export const api44_mcpServerStringSpans = load("scan/mcp-context.js").mcpServerStringSpans;
14225
+ export const api44_withinSpans = load("scan/mcp-context.js").withinSpans;
14226
+ export const api44_MAX_DEPTH = load("scan/mcp-context.js").MAX_DEPTH;
14227
+ export const api45_nwcMatches = load("scan/nwc-match.js").nwcMatches;
14228
+ export const api45_replaceNwc = load("scan/nwc-match.js").replaceNwc;
14229
+ export const api45_nwcUnescaped = load("scan/nwc-match.js").nwcUnescaped;
14230
+ export const api46_macaroonHex = load("scan/patterns.js").macaroonHex;
14231
+ export const api46_macaroonBase64 = load("scan/patterns.js").macaroonBase64;
14232
+ export const api46_lndconnect = load("scan/patterns.js").lndconnect;
14233
+ export const api46_nwc = load("scan/patterns.js").nwc;
14234
+ export const api47_redactDecoded = load("scan/redact.js").redactDecoded;
14235
+ export const api47_redactPath = load("scan/redact.js").redactPath;
14236
+ export const api47_collectNwcSecrets = load("scan/redact.js").collectNwcSecrets;
14237
+ export const api47_knownSecretRedactor = load("scan/redact.js").knownSecretRedactor;
14238
+ export const api47_redactKnownSecrets = load("scan/redact.js").redactKnownSecrets;
14239
+ export const api48_Secret = load("secret.js").Secret;
14240
+ export const api49_macwardenVersion = load("version.js").macwardenVersion;
14241
+ export const api50_createAlertThrottle = load("watch/alert-throttle.js").createAlertThrottle;
14242
+ export const api50_createAlertRunner = load("watch/alert-throttle.js").createAlertRunner;
14243
+ export const api50_ALERT_WINDOW_MS = load("watch/alert-throttle.js").ALERT_WINDOW_MS;
14244
+ export const api50_MAX_TRACKED_CANARIES = load("watch/alert-throttle.js").MAX_TRACKED_CANARIES;
14245
+ export const api51_reapAlertGroups = load("watch/alert.js").reapAlertGroups;
14246
+ export const api51_alertCleanupDone = load("watch/alert.js").alertCleanupDone;
14247
+ export const api51_runAlertCommand = load("watch/alert.js").runAlertCommand;
14248
+ export const api51_ALERT_TIMEOUT_MS = load("watch/alert.js").ALERT_TIMEOUT_MS;
14249
+ export const api51_ALERT_KILL_MS = load("watch/alert.js").ALERT_KILL_MS;
14250
+ export const api51_ALERT_GRACE_MS = load("watch/alert.js").ALERT_GRACE_MS;
14251
+ export const api51_ALERT_CLEANUP_MS = load("watch/alert.js").ALERT_CLEANUP_MS;
14252
+ export const api52_canaryConfigPath = load("watch/canary.js").canaryConfigPath;
14253
+ export const api52_newCaveatName = load("watch/canary.js").newCaveatName;
14254
+ export const api52_newCondition = load("watch/canary.js").newCondition;
14255
+ export const api52_canaryCondition = load("watch/canary.js").canaryCondition;
14256
+ export const api52_readCanaryConfig = load("watch/canary.js").readCanaryConfig;
14257
+ export const api52_ensureCanaryConfig = load("watch/canary.js").ensureCanaryConfig;
14258
+ export const api52_canaryEntries = load("watch/canary.js").canaryEntries;
14259
+ export const api52_isCanary = load("watch/canary.js").isCanary;
14260
+ export const api52_CANARY_FILE_LIMIT = load("watch/canary.js").CANARY_FILE_LIMIT;
14261
+ export const api53_retryable = load("watch/output-queue.js").retryable;
14262
+ export const api53_fdWriter = load("watch/output-queue.js").fdWriter;
14263
+ export const api53_createOutputQueue = load("watch/output-queue.js").createOutputQueue;
14264
+ export const api53_OUTPUT_LIMIT_BYTES = load("watch/output-queue.js").OUTPUT_LIMIT_BYTES;
14265
+ export const api54_usagePath = load("watch/usage.js").usagePath;
14266
+ export const api54_readUsage = load("watch/usage.js").readUsage;
14267
+ export const api54_writeUsage = load("watch/usage.js").writeUsage;
14268
+ export const api54_USAGE_LIMIT = load("watch/usage.js").USAGE_LIMIT;
14269
+ export const api54_CANARY_LIMIT = load("watch/usage.js").CANARY_LIMIT;
14270
+ export const api54_COVERAGE_LIMIT = load("watch/usage.js").COVERAGE_LIMIT;
14271
+ export const api54_SPAN_LIMIT = load("watch/usage.js").SPAN_LIMIT;
14272
+ export const api54_USAGE_FILE_LIMIT = load("watch/usage.js").USAGE_FILE_LIMIT;
14273
+ export const api54_UsageFileError = load("watch/usage.js").UsageFileError;
14274
+ export const api54_UsageStore = load("watch/usage.js").UsageStore;