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.
- package/CHANGELOG.md +32 -0
- package/README.md +20 -6
- package/dist/bin.js +7 -1
- package/dist/bounded-file.js +1 -0
- package/dist/cancellation.js +1 -1
- package/dist/caveat-address.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/commands/attenuate.js +1 -1
- package/dist/commands/bake/allocate.js +1 -1
- package/dist/commands/bake/index.js +1 -1
- package/dist/commands/bake/write-out.js +1 -1
- package/dist/commands/handlers.js +1 -1
- package/dist/commands/inspect.js +1 -1
- package/dist/commands/inventory.js +1 -1
- package/dist/commands/recommend.js +1 -1
- package/dist/commands/respond.js +1 -1
- package/dist/commands/revoke.js +1 -1
- package/dist/commands/rotate.js +1 -1
- package/dist/commands/scan.js +1 -1
- package/dist/commands/verify.js +1 -0
- package/dist/commands/watch.js +1 -1
- package/dist/credential-recognition.js +1 -1
- package/dist/decode/index.js +1 -1
- package/dist/decode/recognition.js +1 -1
- package/dist/environment.js +1 -1
- package/dist/errors.js +1 -1
- package/dist/grade/index.js +1 -1
- package/dist/grouped-runs.js +1 -1
- package/dist/input/index.js +1 -1
- package/dist/input/lndconnect.js +1 -1
- package/dist/lnd/grpc.js +1 -1
- package/dist/lnd/index.js +1 -1
- package/dist/lnd/middleware-proto.js +1 -1
- package/dist/lnd/tls-identity.js +1 -0
- package/dist/online.js +1 -1
- package/dist/output/index.js +1 -1
- package/dist/output/scan-formats.js +1 -1
- package/dist/process-error-context.js +1 -1
- package/dist/protected-path.js +1 -1
- package/dist/registry/index.js +1 -1
- package/dist/registry/process-domain.js +1 -1
- package/dist/report/access-review.js +1 -1
- package/dist/runtime.js +1784 -617
- package/dist/scan/agent-configs.js +1 -1
- package/dist/scan/git-history.js +1 -1
- package/dist/scan/ignore.js +1 -1
- package/dist/scan/index.js +1 -1
- package/dist/scan/mcp-context.js +1 -1
- package/dist/scan/nwc-match.js +1 -1
- package/dist/scan/patterns.js +1 -1
- package/dist/scan/redact.js +1 -1
- package/dist/secret.js +1 -1
- package/dist/version.js +1 -1
- package/dist/watch/alert-throttle.js +1 -1
- package/dist/watch/alert.js +1 -1
- package/dist/watch/canary.js +1 -1
- package/dist/watch/output-queue.js +1 -1
- package/dist/watch/usage.js +1 -1
- package/package.json +2 -2
- package/schemas/output/attenuate.schema.json +2 -0
- package/schemas/output/inspect.schema.json +2 -0
- package/schemas/output/inventory.schema.json +35 -1
- package/schemas/output/recommend.schema.json +471 -0
- package/schemas/output/scan.schema.json +2 -0
- package/schemas/output/verify.schema.json +552 -0
- package/schemas/output/watch-event.schema.json +232 -0
- package/schemas/output/watch.schema.json +377 -0
package/dist/runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as builtin0 from "node:
|
|
2
|
-
import * as builtin1 from "node:fs";
|
|
3
|
-
import * as builtin2 from "node:
|
|
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:
|
|
14
|
-
const factories = {"
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
2095
|
-
|
|
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
|
|
2100
|
-
//
|
|
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
|
|
2105
|
-
|
|
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
|
-
|
|
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 :
|
|
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
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
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
|
|
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.
|
|
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 :
|
|
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 =>
|
|
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
|
-
|
|
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
|
-
//
|
|
4800
|
-
|
|
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
|
|
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
|
-
|
|
4904
|
-
|
|
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()
|
|
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()
|
|
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.
|
|
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
|
|
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,
|
|
5805
|
+
// packet ordering, varint, end-of-section and caveat-location rules.
|
|
5202
5806
|
function macaroonPacketRange(data, start) {
|
|
5203
|
-
const
|
|
5204
|
-
if (![1, 2, 4, 6].includes(
|
|
5807
|
+
const kindItem = packetVarint(data, start);
|
|
5808
|
+
if (kindItem === null || ![1, 2, 4, 6].includes(kindItem[0]))
|
|
5205
5809
|
return null;
|
|
5206
|
-
|
|
5207
|
-
|
|
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
|
-
|
|
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 (
|
|
5838
|
+
if (end === null || identifier === null || !header && location && location.end > location.start && vid === null)
|
|
5236
5839
|
return null;
|
|
5237
|
-
return { location, identifier, vid, end
|
|
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
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5929
|
+
utf8.decode(value);
|
|
5294
5930
|
}
|
|
5295
5931
|
catch {
|
|
5296
|
-
|
|
5297
|
-
}
|
|
5298
|
-
}
|
|
5299
|
-
//
|
|
5300
|
-
//
|
|
5301
|
-
//
|
|
5302
|
-
//
|
|
5303
|
-
//
|
|
5304
|
-
//
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
};
|
|
5309
|
-
/**
|
|
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
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
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
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
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 (
|
|
5976
|
+
if (first && actions === 0)
|
|
5344
5977
|
return fail('identifier-proto');
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
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, ,
|
|
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
|
-
|
|
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
|
-
|
|
5417
|
-
|
|
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.
|
|
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
|
-
|
|
6107
|
+
canonicalLength += 1 + packetSize(6, signature);
|
|
6108
|
+
return { parsed: { header, caveats, signature }, end, canonicalLength, omittedCaveats };
|
|
5432
6109
|
}
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
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
|
-
|
|
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
|
|
5454
|
-
|
|
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
|
-
|
|
5719
|
-
|
|
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
|
-
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
7235
|
-
|
|
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
|
|
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
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
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
|
-
|
|
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),
|
|
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]) =>
|
|
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
|
-
|
|
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.
|
|
9223
|
-
item.calls === null ? '' : String(item.calls),
|
|
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
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
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
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
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
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
10213
|
-
|
|
10214
|
-
|
|
10215
|
-
|
|
10216
|
-
|
|
11102
|
+
// The matcher ends every match with secret=<64 hex> and at most one delimiter. Any
|
|
11103
|
+
// match is reported (fail closed): "&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
|
-
|
|
10987
|
-
|
|
10988
|
-
|
|
10989
|
-
|
|
10990
|
-
|
|
10991
|
-
|
|
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
|
|
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
|
|
11021
|
-
|
|
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
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
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 (
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 "&" 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11265
|
-
|
|
11266
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11632
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11696
|
-
|
|
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
|
|
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.
|
|
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()],
|
|
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
|
|
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
|
|
12883
|
-
export const
|
|
12884
|
-
export const
|
|
12885
|
-
export const
|
|
12886
|
-
export const
|
|
12887
|
-
export const
|
|
12888
|
-
export const
|
|
12889
|
-
export const
|
|
12890
|
-
export const
|
|
12891
|
-
export const
|
|
12892
|
-
export const
|
|
12893
|
-
export const
|
|
12894
|
-
export const
|
|
12895
|
-
export const
|
|
12896
|
-
export const
|
|
12897
|
-
export const
|
|
12898
|
-
export const
|
|
12899
|
-
export const
|
|
12900
|
-
export const
|
|
12901
|
-
export const
|
|
12902
|
-
export const
|
|
12903
|
-
export const
|
|
12904
|
-
export const
|
|
12905
|
-
export const
|
|
12906
|
-
export const
|
|
12907
|
-
export const
|
|
12908
|
-
export const
|
|
12909
|
-
export const
|
|
12910
|
-
export const
|
|
12911
|
-
export const
|
|
12912
|
-
export const
|
|
12913
|
-
export const
|
|
12914
|
-
export const
|
|
12915
|
-
export const
|
|
12916
|
-
export const
|
|
12917
|
-
export const
|
|
12918
|
-
export const
|
|
12919
|
-
export const
|
|
12920
|
-
export const
|
|
12921
|
-
export const
|
|
12922
|
-
export const
|
|
12923
|
-
export const
|
|
12924
|
-
export const
|
|
12925
|
-
export const
|
|
12926
|
-
export const
|
|
12927
|
-
export const
|
|
12928
|
-
export const
|
|
12929
|
-
export const
|
|
12930
|
-
export const
|
|
12931
|
-
export const
|
|
12932
|
-
export const
|
|
12933
|
-
export const
|
|
12934
|
-
export const
|
|
12935
|
-
export const
|
|
12936
|
-
export const
|
|
12937
|
-
export const
|
|
12938
|
-
export const
|
|
12939
|
-
export const
|
|
12940
|
-
export const
|
|
12941
|
-
export const
|
|
12942
|
-
export const
|
|
12943
|
-
export const
|
|
12944
|
-
export const
|
|
12945
|
-
export const
|
|
12946
|
-
export const
|
|
12947
|
-
export const
|
|
12948
|
-
export const
|
|
12949
|
-
export const
|
|
12950
|
-
export const
|
|
12951
|
-
export const
|
|
12952
|
-
export const
|
|
12953
|
-
export const
|
|
12954
|
-
export const
|
|
12955
|
-
export const
|
|
12956
|
-
export const
|
|
12957
|
-
export const
|
|
12958
|
-
export const
|
|
12959
|
-
export const
|
|
12960
|
-
export const
|
|
12961
|
-
export const
|
|
12962
|
-
export const
|
|
12963
|
-
export const
|
|
12964
|
-
export const
|
|
12965
|
-
export const
|
|
12966
|
-
export const
|
|
12967
|
-
export const
|
|
12968
|
-
export const
|
|
12969
|
-
export const
|
|
12970
|
-
export const
|
|
12971
|
-
export const
|
|
12972
|
-
export const
|
|
12973
|
-
export const
|
|
12974
|
-
export const
|
|
12975
|
-
export const
|
|
12976
|
-
export const
|
|
12977
|
-
export const
|
|
12978
|
-
export const
|
|
12979
|
-
export const
|
|
12980
|
-
export const
|
|
12981
|
-
export const
|
|
12982
|
-
export const
|
|
12983
|
-
export const
|
|
12984
|
-
export const
|
|
12985
|
-
export const
|
|
12986
|
-
export const
|
|
12987
|
-
export const
|
|
12988
|
-
export const
|
|
12989
|
-
export const
|
|
12990
|
-
export const
|
|
12991
|
-
export const
|
|
12992
|
-
export const
|
|
12993
|
-
export const
|
|
12994
|
-
export const
|
|
12995
|
-
export const
|
|
12996
|
-
export const
|
|
12997
|
-
export const
|
|
12998
|
-
export const
|
|
12999
|
-
export const
|
|
13000
|
-
export const
|
|
13001
|
-
export const
|
|
13002
|
-
export const
|
|
13003
|
-
export const
|
|
13004
|
-
export const
|
|
13005
|
-
export const
|
|
13006
|
-
export const
|
|
13007
|
-
export const
|
|
13008
|
-
export const
|
|
13009
|
-
export const
|
|
13010
|
-
export const
|
|
13011
|
-
export const
|
|
13012
|
-
export const
|
|
13013
|
-
export const
|
|
13014
|
-
export const
|
|
13015
|
-
export const
|
|
13016
|
-
export const
|
|
13017
|
-
export const
|
|
13018
|
-
export const
|
|
13019
|
-
export const
|
|
13020
|
-
export const
|
|
13021
|
-
export const
|
|
13022
|
-
export const
|
|
13023
|
-
export const
|
|
13024
|
-
export const
|
|
13025
|
-
export const
|
|
13026
|
-
export const
|
|
13027
|
-
export const
|
|
13028
|
-
export const
|
|
13029
|
-
export const
|
|
13030
|
-
export const
|
|
13031
|
-
export const
|
|
13032
|
-
export const
|
|
13033
|
-
export const
|
|
13034
|
-
export const
|
|
13035
|
-
export const
|
|
13036
|
-
export const
|
|
13037
|
-
export const
|
|
13038
|
-
export const
|
|
13039
|
-
export const
|
|
13040
|
-
export const
|
|
13041
|
-
export const
|
|
13042
|
-
export const
|
|
13043
|
-
export const
|
|
13044
|
-
export const
|
|
13045
|
-
export const
|
|
13046
|
-
export const
|
|
13047
|
-
export const
|
|
13048
|
-
export const
|
|
13049
|
-
export const
|
|
13050
|
-
export const
|
|
13051
|
-
export const
|
|
13052
|
-
export const
|
|
13053
|
-
export const
|
|
13054
|
-
export const
|
|
13055
|
-
export const
|
|
13056
|
-
export const
|
|
13057
|
-
export const
|
|
13058
|
-
export const
|
|
13059
|
-
export const
|
|
13060
|
-
export const
|
|
13061
|
-
export const
|
|
13062
|
-
export const
|
|
13063
|
-
export const
|
|
13064
|
-
export const
|
|
13065
|
-
export const
|
|
13066
|
-
export const
|
|
13067
|
-
export const
|
|
13068
|
-
export const
|
|
13069
|
-
export const
|
|
13070
|
-
export const
|
|
13071
|
-
export const
|
|
13072
|
-
export const
|
|
13073
|
-
export const
|
|
13074
|
-
export const
|
|
13075
|
-
export const
|
|
13076
|
-
export const
|
|
13077
|
-
export const
|
|
13078
|
-
export const
|
|
13079
|
-
export const
|
|
13080
|
-
export const
|
|
13081
|
-
export const
|
|
13082
|
-
export const
|
|
13083
|
-
export const
|
|
13084
|
-
export const
|
|
13085
|
-
export const
|
|
13086
|
-
export const
|
|
13087
|
-
export const
|
|
13088
|
-
export const
|
|
13089
|
-
export const
|
|
13090
|
-
export const
|
|
13091
|
-
export const
|
|
13092
|
-
export const
|
|
13093
|
-
export const
|
|
13094
|
-
export const
|
|
13095
|
-
export const
|
|
13096
|
-
export const
|
|
13097
|
-
export const
|
|
13098
|
-
export const
|
|
13099
|
-
export const
|
|
13100
|
-
export const
|
|
13101
|
-
export const
|
|
13102
|
-
export const
|
|
13103
|
-
export const
|
|
13104
|
-
export const
|
|
13105
|
-
export const
|
|
13106
|
-
export const
|
|
13107
|
-
export const
|
|
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;
|