macwarden 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/LICENSE +21 -0
  3. package/README.md +199 -2
  4. package/SECURITY.md +104 -0
  5. package/dist/bin.js +21 -0
  6. package/dist/bounded-file.js +1 -0
  7. package/dist/cancellation.js +1 -0
  8. package/dist/caveat-address.js +1 -0
  9. package/dist/cli.js +1 -0
  10. package/dist/command-result.js +1 -0
  11. package/dist/commands/attenuate.js +1 -0
  12. package/dist/commands/bake/allocate.js +1 -0
  13. package/dist/commands/bake/index.js +1 -0
  14. package/dist/commands/bake/write-out.js +1 -0
  15. package/dist/commands/handlers.js +1 -0
  16. package/dist/commands/inspect.js +1 -0
  17. package/dist/commands/inventory.js +1 -0
  18. package/dist/commands/recommend.js +1 -0
  19. package/dist/commands/respond.js +1 -0
  20. package/dist/commands/revoke.js +1 -0
  21. package/dist/commands/rotate.js +1 -0
  22. package/dist/commands/scan.js +1 -0
  23. package/dist/commands/verify.js +1 -0
  24. package/dist/commands/watch.js +1 -0
  25. package/dist/credential-recognition.js +1 -0
  26. package/dist/decode/index.js +1 -0
  27. package/dist/decode/recognition.js +1 -0
  28. package/dist/environment.js +1 -0
  29. package/dist/errors.js +1 -0
  30. package/dist/grade/index.js +1 -0
  31. package/dist/grade/lnd-permissions.json +508 -0
  32. package/dist/grouped-runs.js +1 -0
  33. package/dist/input/index.js +1 -0
  34. package/dist/input/lndconnect.js +1 -0
  35. package/dist/lnd/grpc.js +1 -0
  36. package/dist/lnd/index.js +1 -0
  37. package/dist/lnd/middleware-proto.js +1 -0
  38. package/dist/lnd/tls-identity.js +1 -0
  39. package/dist/online.js +1 -0
  40. package/dist/output/index.js +1 -0
  41. package/dist/output/scan-formats.js +1 -0
  42. package/dist/process-error-context.js +1 -0
  43. package/dist/protected-path.js +1 -0
  44. package/dist/registry/index.js +1 -0
  45. package/dist/registry/process-domain.js +1 -0
  46. package/dist/report/access-review.js +1 -0
  47. package/dist/runtime.js +14274 -0
  48. package/dist/scan/agent-configs.js +1 -0
  49. package/dist/scan/git-history.js +1 -0
  50. package/dist/scan/ignore.js +1 -0
  51. package/dist/scan/index.js +1 -0
  52. package/dist/scan/mcp-context.js +1 -0
  53. package/dist/scan/nwc-match.js +1 -0
  54. package/dist/scan/patterns.js +1 -0
  55. package/dist/scan/redact.js +1 -0
  56. package/dist/secret-capability.js +1 -0
  57. package/dist/secret.js +1 -0
  58. package/dist/version.js +1 -0
  59. package/dist/watch/alert-throttle.js +1 -0
  60. package/dist/watch/alert.js +1 -0
  61. package/dist/watch/canary.js +1 -0
  62. package/dist/watch/output-queue.js +1 -0
  63. package/dist/watch/usage.js +1 -0
  64. package/package.json +69 -4
  65. package/rules/gitleaks.toml +30 -0
  66. package/rules/trufflehog.yaml +27 -0
  67. package/schemas/output/attenuate.schema.json +439 -0
  68. package/schemas/output/bake.schema.json +338 -0
  69. package/schemas/output/inspect.schema.json +174 -0
  70. package/schemas/output/inventory.schema.json +929 -0
  71. package/schemas/output/recommend.schema.json +471 -0
  72. package/schemas/output/registry-entry.schema.json +28 -0
  73. package/schemas/output/respond.schema.json +620 -0
  74. package/schemas/output/revoke.schema.json +368 -0
  75. package/schemas/output/rotate.schema.json +381 -0
  76. package/schemas/output/scan.schema.json +272 -0
  77. package/schemas/output/verify.schema.json +552 -0
  78. package/schemas/output/watch-event.schema.json +232 -0
  79. package/schemas/output/watch.schema.json +377 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,107 @@
1
+ # Changelog
2
+
3
+ ## 1.1.0 (2026-10-08)
4
+
5
+ Security release. It fixes every finding of the independent audit of 1.0.1 ([report](docs/reviews/2026-10-08-audit-1.0.1.md)) and of a differential fuzzing review of the parsers against LND's own macaroon library, and adds `verify` for operators responding to credential exposure such as the BTCPay Server 2.4.2 incident.
6
+
7
+ ### Added
8
+
9
+ - New command `verify <file | ->`: asks your node whether one credential file is still accepted, for operators checking macaroon files that may have been copied (for example in the BTCPay Server 2.4.2 incident, where patching did not invalidate stolen files). It decodes the file offline as `inspect` does, calls CheckMacaroonPermissions with its own permissions, and reports `status` (`accepted` or `rejected`), why LND rejected it (`root-key-missing`, `signature-mismatch`, `expired` or `other`), whether its root key ID is still listed, and its registry label. Exit 1 while LND accepts it, and also when a rejection may depend on the caller (an IP lock or a custom caveat such as a canary's); exit 0 once LND conclusively rejects it. Guidance names the next step: `macwarden revoke`, `respond` for a credential that can bake, or, on root key 0, regenerating every root key first and baking fresh per-app macaroons afterwards. A Nostr Wallet Connect URI is refused (exit 3). Output schema `schemas/output/verify.schema.json`.
10
+ - New [incident playbook](docs/incident-playbook.md) for exposed credentials (BTCPay Server < 2.4.2 and similar): what patching fixes and what it does not, checking each old file with `verify`, replacing and revoking with `respond`, `rotate` and `revoke`, root key ID 0, re-pairing apps and checking for unauthorized activity. Linked from the README.
11
+ - Requires Node.js `^22.23.2 || ^24.18.1 || >=26.5.1` (was `>=22.12.0`): the July 2026 Node.js security releases fix HTTP/2 flaws (CVE-2026-56846, CVE-2026-56848) in `node:http2`, which `watch` uses for its gRPC stream to LND. CI tests the new minimum versions.
12
+
13
+ ### Fixed
14
+
15
+ - `rotate` and `respond` decide whether a credential is a canary from its decoded file, not only from the registry's caveat list. A registry entry whose caveats were cleared while its file still carries an `lnd-custom` caveat is refused as tampering (exit 3 at S4 and P5) before any lock or LND mutation. Before, clearing that metadata let a canary be rotated into an ordinary, accepted credential (MW102-01).
16
+ - `respond --finish` resumes a finish that deleted the old root key ID but stopped before saving the registry: the credential is finished through `rotate`'s F6 repair, which proves the replacement accepted, records the revocation and link, and sends no second deletion. Before, it reported exit 0 with a `repair` item and advised `macwarden revoke <label>`, which exits 2 because the old entry and its replacement share the label. Advice in `respond` now names a credential by its root key ID wherever its label is ambiguous (MW102-02).
17
+ - `respond` no longer reports a replacement that an earlier stage allocated but never completed (`verified: false`, no output file) as `staged-earlier` with advice to switch the app. It is reported as unresolved: item `repair`/`failed` with exit 7 at P5, listed under `unproven`, with whether LND lists the ID and the recovery `macwarden revoke <replacement root key ID>` followed by a fresh stage. The uncertain mutation is never retried (MW102-10).
18
+ - `recommend` applies the `recommendation-broader` widening guard to every selector. Selecting a credential by `id:…` (or `--target-file`) never looked up its registry entry, so usage claiming a method the credential never held (for example SendPaymentV2 on an invoice credential) produced a bake command that widened it. The credential ID is now resolved through its recorded root key ID to the single active registry entry; `--target-file` is also checked against the file's own permissions. When the permissions cannot be established (unregistered, root key 0 by ID, ambiguous), warning `recommendation-unverified` withholds the commands. A credential ID selector no longer suggests revoking the whole root key ID. (MW102-03)
19
+ - `recommend` counts only watch coverage after the credential was created toward the 7-day observation rule. It compared the covered total with the time since creation, so eight days watched before a credential existed plus one second afterwards counted as seven days and printed narrowing and revoke commands. `watch` now records the registered spans per node (`coverageSpans` in the usage file, at most 256 per node; `coverage` is unchanged, so 1.0.1 still reads the file), and `recommend` uses a provable lower bound. A file without spans counts coverage that began before creation only as far as it cannot have lain before it; a credential without a registry entry counts from its first recorded use. (MW102-04)
20
+ - `watch` shutdown keeps its alert budget (MW102-05). An alert waiting behind a running `--alert-command` is discarded at shutdown (reported as `alert-failed`; its event line is still printed) instead of starting after watch was asked to stop; the running command gets one deadline and is then cancelled with its process group or, on Windows, its process tree; and the CLI's forced-exit deadline starts only after Windows tree kills have finished. Before, a second alert could start 12 s into shutdown and outlive watch.
21
+ - `watch --until` honours durations beyond about 24.8 days (MW102-09). Node fires a timer longer than 2^31−1 ms after 1 ms, so `--until 25d` stopped almost at once; the wait now runs in bounded chunks against one monotonic deadline, up to the accepted maximum of 999999d.
22
+ - `scan --git-history` bounds the repository-location check. Each include and alternates file is read once, and the check stops with exit 3 ("git history could not be read") when git would follow more than 4,096 include or alternate entries or parse more than 1 MiB of location files in total, or when more than 256 distinct location files are involved; an interrupt is honoured during the check. Before, eight 100-byte configs each including the next eight times were walked 2.4 million times (MW102-06).
23
+ - The usage and canary files next to the registry are read through the same bounded reader as TLS certificates: a path or symlink target with a protected name (`seed.json`, `tls.key`, ...) is refused before it is opened, anything but a regular file is refused, and a file larger than its format allows (about 20 MiB for usage, 4 KiB for the canary configuration) fails before it is parsed. Registry and watch lock records are read only from plain files, never through a symlink. Before, a symlinked sidecar was followed into a protected file and read whole (MW102-07).
24
+ - Online commands reach a literal IPv6 REST endpoint (`--lnd-rest '[::1]:8080'`). The bracketed host was passed to the TLS connection as a DNS name, so a certificate with the matching IP address SAN failed verification (exit 4). On Node 22, whose own TLS identity check refuses every IPv6 literal, REST and gRPC connections to an IPv6 address use an equally strict check that accepts only a matching IP address SAN. Printed URLs and commands keep the brackets; verification stays on (MW102-08).
25
+ - `inventory --report`: the Markdown and CSV reports carry all the evidence of the JSON review. Markdown adds the rotation links (replaces, replaced by), canary uses (count, first and last use), caveats and flag counts. CSV stays one table with one header row and adds caveats, canary uses, the controls naming each root key ID with their observations, and report-level columns repeated on every row (generated time, macwarden version, node pubkey, alias, network and LND version, empty sign-off fields, the limitation statement). The JSON review adds `canary` per credential and `limitations`. Before, Markdown dropped rotation links and CSV dropped the report identity, controls, sign-off and limitation statement (MW102-11).
26
+ - `watch` and `recommend` have published JSON schemas: `schemas/output/recommend.schema.json`, `watch.schema.json` for watch's final document and `watch-event.schema.json` for the event lines `watch --json` prints before it (call, error, canary and each status event). They are generated with the others, shipped in the package and checked against real outputs, refusals included. Before, the package had no schema for either command (MW102-12).
27
+ - `scan` no longer stalls on a crafted `.json` file. MCP-context tagging rebuilt the whole key path for every string value and compared every finding with every tagged string, so one committed file with deep nesting and many strings (any `.json` file with one candidate finding) held a scan for hours: 19.3 s for 176 KB, about 4 to 5 hours for 5 MB. Tagging is now linear: 75 ms for the 176 KB file, 1.2 s for 5 MB, and it gives up beyond 1,000 levels of nesting. (Parser review 2026-10-08, HIGH-1.)
28
+ - A re-framed macaroon is decoded as LND decodes it. go-macaroon reads packet kinds, lengths and end-of-section markers as any varint encoding (non-minimal `0x83 0x00` for 3, `0x80 0x00` for end-of-section), so anyone holding a macaroon can re-frame it without the root key and LND still honours it. `inspect`, `scan`, `revoke` and `rotate` refused such a copy as undecodable, and `watch` attributed its calls to `id:undecodable` instead of the stolen credential. The fingerprint is now defined over the canonical form (what go-macaroon writes), so a re-framed copy has the fingerprint of the original; every macaroon LND or macwarden writes keeps its fingerprint. Output redaction also recognises a re-framed hex copy. (Parser review 2026-10-08, MEDIUM-1.)
29
+ - Identifier shapes LND authorizes are no longer refused and ignored. An extra op with an empty entity or no actions, an unknown protobuf group, a non-minimal protobuf tag, more than 256 ops, more than 512 actions in one op and more than 4,096 caveats are decoded as LND decodes them (bakery requires actions only on the first op). Before, `inspect` refused such a macaroon, `scan` reported it as undecodable and `watch` could not attribute its calls, although LND granted it full authority. Expansion stays bounded (an entity over 16 bytes or an action over 256 bytes is left out, as is everything past 1,024 permissions or 64 KiB of permission text, and caveats past 4,096); a macaroon past a bound is still decoded, carries the warning `identifier-exceeds-bounds` or `caveats-exceed-bounds`, and its grade fails closed to `UNASSESSED` unless what was kept is already `ADMIN`. The stage `identifier-limits` is no longer produced. (Parser review 2026-10-08, MEDIUM-2.)
30
+ - Output redaction no longer misses a hex macaroon written right after a backslash or a percent sign (for example a Windows path such as `C:\keys\0201…`). Recognition looked only at the text after decoding escapes, so `\020` was read as an octal escape and `%02` as a percent escape, which removed the start of the macaroon. Hex is now also recognised in the literal text. (Parser review 2026-10-08, LOW-1.)
31
+ - Output redaction and scan's NWC secret collection use the linear NWC matcher that scan detection already used. They still ran the published `nwc` regex, which backtracks quadratically when the URI prefix repeats without a secret: 1 MB took 4 to 6 s to collect and 10 to 12 s to render, and `inspect` of a 1 MB macaroon with such a caveat took 9.3 s. Now 31 ms, 1.0 s and 1.3 s, with identical matches. (Parser review 2026-10-08, LOW-2.)
32
+ - `scan` finds NWC connection strings in common escaped forms: HTML or XML with `&amp;` before `secret=` (the detector matched it, then the hit was dropped without being counted), JSON with escaped slashes, percent-encoded (deep links, form fields) and with `+` decoded to a space. Each grants spend authority. Every detector match is now reported, and each line is matched once more after unescaping these forms. (Parser review 2026-10-08, LOW-3.)
33
+ - A `time-before` caveat with a one-digit hour (`2027-10-08T1:00:00Z`), a zone offset hour of 24 (`+24:00`) or an offset minute of 60 is parsed as LND parses it (Go's `time.Parse` falls back to a lenient parser). Before, macwarden reported `bad-time-before` and no expiry, so an expired credential LND refuses was shown with no known expiry, and `revoke`/`rotate` treated it as unusable. (Parser review 2026-10-08, LOW-4.)
34
+
35
+ ## 1.0.1 (2026-10-08)
36
+
37
+ Fixes found in the live field check against LND 0.20.0 (Polar, regtest):
38
+
39
+ - `watch` fails with exit 4 and names the gRPC endpoint it tried (and how it was chosen) when no session ever registers. Before, an unreachable endpoint was retried quietly and the run ended with exit 0 and "watched 0 calls". Reconnecting with backoff now applies only after a session has registered once.
40
+ - `inventory` shows macwarden's own credential as itself instead of "unknown", in the text output, the JSON flags and the access-review report (where it still appears under periodic access review).
41
+ - `inventory --verify` reports a canary as "canary: verifiable only while watch runs" instead of "error", without asking LND.
42
+ - `respond` aligns its text table columns.
43
+ - A triggered canary is unmistakable. `watch` records the canary guard's denied uses apart from ordinary usage (`canaryUses` in the usage file); `inventory` starts that row's status with "CANARY TRIGGERED <n> times, last <time>: assume this credential's location is compromised", adds the `canary-triggered` flag to the JSON row (with `canary: {uses, firstUse, lastUse}`), and ends the text output with a line naming every triggered canary. `inventory --report` flags it and opens its control observations with "Incident response". Before, a used canary read "active; last used <time> (1 calls)", the same as normal use.
44
+ - `recommend` says how long usage was observed. `watch` now records its registered time per node (`coverage` in the usage file), and `recommend` prints `observed for <duration>`. Under 7 days, it warns `short-observation` (rarely used methods such as payments may be missing) and withholds the narrowing command unless `--accept-short-observation` is given. Before, a one-minute watch was enough to print a bake command that would have dropped a payment credential's payment methods. Usage files from 1.0.0 have no coverage record and count as short.
45
+ - GitHub Actions: `actions/checkout` and `actions/setup-node` moved to v5 (Node 24 runtime); GitHub has deprecated Node 20 for actions.
46
+
47
+ ## 1.0.0 (2026-10-08)
48
+
49
+ First production release. It adds runtime visibility (`watch`, canaries, `recommend`), incident response (`respond`), access-review evidence and CI-native scanning to the 1.0.0-rc.1 command set, and closes every finding of the 2026-10-08 deep audit ([report](docs/reviews/2026-10-08-deep-audit.md)). Validated with complete Windows and unprivileged Linux suites, every pinned-regtest LND reference, leak checks and independent reviews. The real-node field check is in [docs/field-check.md](docs/field-check.md).
50
+
51
+
52
+ Audit fixes and hardening (2026-10-08):
53
+
54
+ - Output is rendered in bounded chunks and written with stdout backpressure, so a very large scan no longer builds one string that can exceed V8's limit. A closed stdout or stderr pipe (`| head`) stops output quietly and keeps the computed exit status instead of reporting an internal error.
55
+ - A BakeMacaroon or DeleteMacaroonID request that gets no complete response (timeout, reset, cancellation) is treated as an unknown outcome: exit 7 with the pending record kept and an exact recovery command, never "nothing changed". Recovery commands print absolute paths.
56
+ - Registry: a failed lock acquisition no longer leaves a stale lock; saves sync the directory on POSIX; validation is linear in the number of entries; leftover temporary files from killed saves are removed under the lock.
57
+ - Windows `--out`: the credential is written only through an exclusive handle opened after the owner-only ACL is applied, so a handle opened before the restriction cannot read it.
58
+ - `scan --git-history` refuses repository locations off the local file system (gitfile, commondir, alternates and config includes pointing to network or device paths) before git runs; a shallow clone fails with exit 3 unless `--allow-shallow`; history memory is bounded; SIGINT/SIGTERM stop a scan (exit 6) and kill git.
59
+ - Credential recognition finds grouped base64 runs in linear time: a crafted 200 KB caveat took 52 s to `inspect` and now takes 0.15 s.
60
+ - `scan` scales: known NWC secret redaction, ignore-file matching, read buffers and per-line work are linear; a 200,000-file tree scans in about 15 s instead of 384 s.
61
+ - `--help` lists the commands and `<command> --help` prints its synopsis; refusals name the flag and the rule; `--fail-on` accepts any case.
62
+ - `rules/gitleaks.toml` extends gitleaks' default rules (it replaced them before) and adds keyword prefilters; a pre-commit hook definition ships in `.pre-commit-hooks.yaml`.
63
+ - `inventory` shows an EXPIRES column with expired and expiring flags; `--expiring-within <n>(m|h|d)` exits 1 for a cron reminder. `scan` text marks expired credentials.
64
+ - JSON schemas for every command's `--json` output in `schemas/output/`, shipped in the package and checked against real outputs in the tests.
65
+ - The Windows process-domain query passes an explicit temp directory, defaulting to the system Temp folder in service environments without TEMP or TMP.
66
+ - Package metadata and the (disabled) release workflow are ready for npm trusted publishing; prereleases publish with the `next` tag.
67
+
68
+ Features (2026-10-07 and 2026-10-08):
69
+
70
+ - `scan --format sarif|github` for CI: a SARIF 2.1.0 log for GitHub code scanning and GitHub Actions annotations, both without matched text. `--format json` equals `--json`.
71
+ - `scan --agent-configs` scans the configuration locations of AI agents and MCP clients and marks credentials found inside MCP server entries (`context: "mcp-config"`).
72
+ - `rules/trufflehog.yaml`: the four detectors as TruffleHog custom detectors, with the same regexes as SPEC and the gitleaks pack.
73
+ - `scan --git-history` scans every blob committed on any ref, so a credential that was committed and later deleted is still found. Local git only, offline and read-only.
74
+ - `inspect` decodes Nostr Wallet Connect URIs offline: wallet and client public keys, relays, lud16 and fingerprint, counted as SPEND. The secret is never shown; a relay, lud16 or wallet key that embeds it is refused. Legacy `nostrwalletconnect:` schemes are decoded by `inspect` and detected by `scan` and the gitleaks rule.
75
+ - New offline `attenuate` command adds expiry or IP caveats to a copy of a macaroon. The copy shares the original root key ID, and the command says so.
76
+ - New `watch` command: a read-only LND RPC middleware (gRPC) that attributes every authenticated call to a credential ID, root key ID and registry label, records last use for `inventory`, and guards canary macaroons.
77
+ - `bake --canary` bakes a decoy that LND refuses unless `watch` runs; with `watch` running, every use its permissions allow is denied and alerted in the same call.
78
+ - `inventory` text output lines its columns up under the headers.
79
+ - `watch` canary alerting is rate-limited: every use is still denied, but each canary raises at most one alert per 10 seconds plus a counted summary, and at most one `--alert-command` runs at a time. Previously a flood of canary uses spawned one alert process per use and could exhaust the host.
80
+ - Red-team hardening before release, each item reproduced with a probe and covered by a test:
81
+ - `watch`: canary alerts cannot be suppressed by bursts or invented conditions; every bound on the middleware stream (unread feedback, frame size, registration and stall deadlines, method and macaroon field sizes, reconnect backoff) is enforced over real HTTP/2; feedback stays under 100 ms p99 at 100,000 calls/s; output survives a slow or stalled reader on Linux; alert commands and their children are always reaped; the lock survives crashes, container restarts and start races.
82
+ - `recommend` refuses to print a narrowing command when usage may be incomplete or broader than the credential.
83
+ - `respond` leaves canaries alone, lists root key IDs the registry marks revoked but LND still holds, scopes every proof to its own credential, flags credentials that can bake, and refuses tampered replacement links, chains and cycles.
84
+ - `scan` and `inspect` bound macaroon decoding (a crafted 61 KB file took two minutes and 1.3 GB; now milliseconds and refused), report a credential added at many history paths once with a count, enforce a findings limit, and never print a line that GitHub Actions or a terminal could read as a command or reordered text.
85
+ - Printed commands quote safely for POSIX shells and PowerShell, including curly quotes.
86
+ - New `recommend` command: least-privilege permissions and exact methods from observed usage, with the narrowing commands.
87
+ - `scan` reads files concurrently with a fixed bound and unchanged output order.
88
+ - New `respond` command: after a suspected leak, stage replacements for every registered credential in one confirmed batch, then `--finish` revokes the old root key IDs with per-credential proof (old rejected, new accepted). Resumable; unknown IDs and root key ID 0 are reported with guidance, never touched.
89
+ - `inventory --report markdown|csv|json [--out <file>]` exports access-review evidence with control observations, last use from `watch` and a reviewer sign-off.
90
+ - On Windows, owner-only output files are granted to the current user SID, with the account name as fallback.
91
+
92
+ ## 1.0.0-rc.1 (prerelease candidate, superseded by 1.0.0)
93
+
94
+ - Prepare candidate metadata and the operator guide for replacing default macaroons.
95
+ - Final local validation and readiness approval are pending: fresh complete Windows and unprivileged Linux suites, tarball and installed-command checks, all affected live regtest references, leak checks, independent reviews and a checkpoint-specific delegated decision must cover the exact candidate. Historical composite summaries do not establish final proof.
96
+ - Production release remains pending the engineer's successful real-node field report for this version, resolution of any surprises, and publication. Hosted Actions remain disabled; no macOS, hosted CI, publication, npm provenance or real-node validation is claimed.
97
+
98
+ ## 0.2.0 (local candidate; validation and approval pending)
99
+
100
+ - Add the registry and online `inventory`, verified `bake`, individual `revoke`, and two-step `rotate` commands, including dry-run and recovery paths.
101
+ - Document dedicated session credentials and the admin-equivalent `macaroon:generate` permission.
102
+ - Prepare an actual local package for Windows and unprivileged Linux validation and independent review. Readiness approval is pending; hosted Actions remain disabled. No publication, macOS, hosted CI, npm provenance or real-node validation is claimed.
103
+
104
+ ## 0.1.0
105
+
106
+ - Prepare the offline `inspect` and `scan` commands and the gitleaks rule pack for a local release artifact.
107
+ - Validate the package contents, fresh build, installed commands and credential leak checks locally on Windows and unprivileged Linux before a release decision. GitHub Actions remain disabled; this record does not establish macOS, hosted CI, publication or npm provenance.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 macwarden contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,200 @@
1
- # Temporary Holding Version
1
+ # macwarden
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Credential control for LND: see every macaroon, find the leaked ones, revoke and rotate without locking yourself out.**
4
+
5
+ macwarden is a command-line tool for teams that run LND behind BTCPay Server, LNbits, RTL, ThunderHub, their own backend or an AI agent. It keeps a record of every macaroon you issue, gives each one its own revocable root key, finds credentials that leaked into code, CI or agent configs, and walks you through an incident with proof that the old credentials are dead.
6
+
7
+ It is a single Node.js CLI. It talks only to your own LND node, sends no telemetry, and never prints a credential: a credential only ever appears as a short SHA-256 fingerprint.
8
+
9
+ ```text
10
+ $ macwarden inventory --verify
11
+ node 03f2a1c97b5e4d0e8a6b9f31c2d47e5a0b8c6d2e9f1a3b5c7d9e0f2a4b6c8d0e1f (shop-node) mainnet
12
+ ROOT KEY ID LABEL GRADE AGE EXPIRES STATUS
13
+ 0 — — — — default root key: cannot be individually revoked
14
+ 4294967341 btcpay RECEIVE 41d — active; accepted; last used 2026-10-08T09:12:44Z (18324 calls)
15
+ 4294967342 lnbits SPEND 12d 2026-10-26 active; accepted; expires in 17d; rotate with: macwarden rotate lnbits
16
+ 4294967355 ci-reader READ 3d 2026-10-07 active; expired; revoke with: macwarden revoke ci-reader
17
+ 9187201950 — — — — unknown — not in this registry (another tool or operator may own it)
18
+ ```
19
+
20
+
21
+ ## Why it exists
22
+
23
+ In August 2026 an unauthenticated flaw in BTCPay Server let attackers download LND `.macaroon` files, and funds were stolen ([advisory](https://blog.btcpayserver.org/security-advisory-btcpay-server-2-4-2/)). The fix closed the hole, but it could not invalidate the stolen files. Operators had to work out by hand which credentials existed, which apps used them, and how to replace them without breaking payments.
24
+
25
+ That is hard with LND alone:
26
+
27
+ - **LND keeps no record of what it issued.** `listmacaroonids` returns bare numbers. Who holds which macaroon is up to you ([lnd#10594](https://github.com/lightningnetwork/lnd/issues/10594)).
28
+ - **Most macaroons cannot be revoked one at a time.** Anything baked without `--root_key_id`, including `admin.macaroon`, shares root key 0, which LND will not delete.
29
+ - **Rotation is easy to get wrong.** Operators have locked themselves out by deleting `macaroons.db` or renaming files ([btcpayserver-docker#1112](https://github.com/btcpayserver/btcpayserver-docker/issues/1112), [lnd#11049](https://github.com/lightningnetwork/lnd/issues/11049)).
30
+ - **Secret scanners ignore Lightning credentials.** gitleaks, TruffleHog and GitHub secret scanning have no rule for macaroons, `lndconnect://` URIs or NWC connection strings.
31
+
32
+ ## What it does
33
+
34
+ ### Know what you have
35
+
36
+ - `inventory` lists every root key ID on the node, matched against macwarden's registry: owner label, permission grade, age, expiry, last use, and flags for anything unknown, expired or still on root key 0. `--verify` asks LND whether each recorded macaroon is still accepted. `--expiring-within 14d` exits 1 for a cron reminder.
37
+ - `inspect` decodes a macaroon, `lndconnect://` URI or Nostr Wallet Connect string offline and grades it `ADMIN`, `SPEND`, `OPERATE`, `RECEIVE` or `READ`.
38
+ - `watch` registers a read-only LND RPC middleware and attributes every call to the credential that made it, without storing macaroon bytes. `inventory` then shows when each credential was last used.
39
+
40
+ ### Find leaks before someone else does
41
+
42
+ - `scan` finds macaroons, `lndconnect://` URIs and NWC strings in files and directories, and `--fail-on <grade>` fails a build.
43
+ - `--git-history` also scans every blob ever committed on any branch, so a credential that was committed and later deleted is still found.
44
+ - `--agent-configs` checks the files where AI agents and MCP clients keep credentials: Claude, Cursor, VS Code, Windsurf, Continue, Gemini CLI, Codex, Zed, lnget.
45
+ - `--format sarif` feeds GitHub code scanning; `--format github` gives inline annotations on any plan. Rule packs for gitleaks and TruffleHog are included.
46
+
47
+ ### Issue only what each app needs
48
+
49
+ - `bake` creates one macaroon per app, each on its own fresh root key ID so it can be revoked alone, from presets (`read-only`, `invoice-only`, `pay-only`, `btcpay`) or explicit permissions, with an optional expiry. LND verifies it before it is written.
50
+ - `recommend` reads what `watch` observed and prints the smallest permission set a credential actually needs, with the commands to narrow it. It says how long usage was observed; under 7 days it warns that rarely used methods (such as payments) may be missing and withholds the narrowing command unless you pass `--accept-short-observation`.
51
+ - `attenuate` adds an expiry or IP lock to a copy of a macaroon offline.
52
+
53
+ ### Change credentials safely
54
+
55
+ - `rotate` works in two steps. It bakes and verifies a replacement first and changes nothing else; you switch the app; `rotate --finish` revokes the old credential and confirms LND rejects it. Until you finish, the old one keeps working.
56
+ - `revoke` deletes a credential's root key ID and proves LND now rejects it.
57
+ - Every command that changes the node has `--dry-run`. `revoke`, `rotate --finish` and `respond` ask you to type a confirmation. An interrupted run prints the exact command that completes or undoes it.
58
+
59
+ ### Respond to an incident
60
+
61
+ - `verify <file>` asks your node whether one credential file is still accepted, for example each `.macaroon` file that may have been copied. It exits 1 while LND accepts it and 0 once LND rejects it, and says what to do next: revoke its root key ID, or, for `admin.macaroon` and anything else on root key 0, regenerate every root key.
62
+ - `respond --out-dir <dir>` stages a replacement for every registered credential in one confirmed batch. After the apps are switched, `respond --finish` revokes the old root key IDs and reports, for each credential, proof that the old one is rejected and the new one accepted. It explains what to do about root key 0, which LND cannot revoke on its own. Canaries are never rotated into ordinary credentials, IDs the registry calls revoked but LND still holds are flagged, and if a credential that can bake may have leaked it says so plainly: only regenerating all root keys removes what such a credential could have created.
63
+ - `bake --canary` creates a decoy credential. While `watch` runs, every use its permissions allow is denied and raises an alert within the same call. Put it where a thief would look.
64
+ - `inventory --report markdown|csv|json` writes an access-review record (every credential, its owner, permissions, caveats, expiry, rotation links, last use, canary uses and review flags, with control observations and a sign-off section; all three formats carry the same evidence, and the CSV is a single table with one header row) for SOC 2, ISO 27001 or internal reviews. It is evidence for a reviewer, not a compliance claim.
65
+
66
+ ## Quick start
67
+
68
+ macwarden needs Node.js 22.23.2 or later on the 22 line, 24.18.1 or later on the 24 line, or 26.5.1 or later.
69
+
70
+ ```sh
71
+ npm i -g macwarden@1.1.0
72
+ ```
73
+
74
+ On production hosts, pin the exact version as above rather than installing the latest, and run `npm audit signatures` in a project that depends on macwarden to check the registry signatures of what you installed.
75
+
76
+ ### Offline: check a repository
77
+
78
+ No node and no credentials needed:
79
+
80
+ ```sh
81
+ macwarden scan . --git-history --fail-on READ
82
+ ```
83
+
84
+ It exits 1 if it finds a credential at or above the grade you set, 0 if it finds none. To accept a known test fixture, add its fingerprint to `.macwardenignore`.
85
+
86
+ ### In CI
87
+
88
+ ```yaml
89
+ - uses: actions/checkout@v5
90
+ with:
91
+ fetch-depth: 0
92
+ - run: npx --yes macwarden@1.1.0 scan . --git-history --fail-on READ --format github
93
+ ```
94
+
95
+ [docs/ci-integration.md](docs/ci-integration.md) has a SARIF workflow for GitHub code scanning, the pre-commit hook and the gitleaks and TruffleHog rule packs.
96
+
97
+ ### Online: take control of a node
98
+
99
+ macwarden runs with a macaroon of its own. Because it can bake macaroons, that credential is admin-equivalent, so make it short-lived and keep it out of application configs:
100
+
101
+ ```sh
102
+ lncli bakemacaroon --root_key_id 123456789 --timeout 3600 \
103
+ --save_to ./macwarden-session.macaroon \
104
+ macaroon:generate macaroon:read macaroon:write info:read
105
+
106
+ export MACWARDEN_LND_REST=127.0.0.1:8080
107
+ export MACWARDEN_TLS_CERT=~/.lnd/tls.cert
108
+ export MACWARDEN_MACAROON=./macwarden-session.macaroon
109
+
110
+ macwarden inventory --verify
111
+ macwarden bake --label btcpay --preset btcpay --out ./btcpay.macaroon
112
+ macwarden rotate btcpay --out ./btcpay-next.macaroon
113
+ # switch BTCPay to btcpay-next.macaroon, then:
114
+ macwarden rotate btcpay --finish
115
+ ```
116
+
117
+ Pick an unused root key ID (check `lncli listmacaroonids`), and run `lncli deletemacaroonid 123456789` when the session is over. To move existing apps off `admin.macaroon` and root key 0, follow [Replacing the default macaroons](docs/replacing-default-macaroons.md). It covers the maintenance window, the one step only you can do, and how to confirm nothing is left behind.
118
+
119
+ `macwarden --help` lists every command; `macwarden <command> --help` shows its options. Every command accepts `--json` and prints one document whose shape is published as a JSON Schema in `schemas/output/<command>.schema.json`. `watch --json` first prints one event per line while it runs (`schemas/output/watch-event.schema.json`), then that document as its last line.
120
+
121
+ ### After a leak
122
+
123
+ Patching the hole does not invalidate stolen files: LND keeps accepting a copied macaroon until its root key ID is deleted or every root key is regenerated, and it rewrites `admin.macaroon` from the same root key. Check each file that may have been exposed:
124
+
125
+ ```sh
126
+ macwarden verify /path/to/old/btcpay.macaroon
127
+ # exit 1: LND still accepts it (or may, for another caller). The output prints what to run next.
128
+ # exit 0: LND rejects it now.
129
+ ```
130
+
131
+ [docs/incident-playbook.md](docs/incident-playbook.md) walks through the whole response: what patching fixes, how to replace and revoke credentials with `respond`, `rotate` and `revoke`, what to do about root key 0, re-pairing apps and checking for unauthorized activity.
132
+
133
+ ## Safety guarantees
134
+
135
+ These are enforced in code and covered by tests. [SECURITY.md](SECURITY.md) lists them as rules SAFE-01 to SAFE-11:
136
+
137
+ - It never prints, logs or stores credential bytes. The only file that ever holds a credential is one you asked it to write with `--out`, created owner-only and never overwritten.
138
+ - It never calls `changepassword`, never deletes root key ID 0, and never reads or writes `macaroons.db`, wallet passwords, seeds or TLS keys.
139
+ - A rotation leaves the old credential valid until you confirm the switch. A failure at any step before that changes nothing you depend on.
140
+ - When the outcome of a change is uncertain (a timeout, a dropped connection), it says so, exits 7 and prints the command to reconcile. It never reports "nothing changed" without proof.
141
+ - `inspect` and `scan` are offline. The only network peer is your LND node, with TLS pinned to its certificate. There is no telemetry.
142
+
143
+ ## Compatibility
144
+
145
+ | | |
146
+ | --- | --- |
147
+ | LND | 0.21.x. Every online command is tested against a pinned LND v0.21.4-beta regtest node. |
148
+ | Platforms | Linux and Windows, tested on every change. macOS should work but is not tested. |
149
+ | Node.js | 22.23.2+, 24.18.1+ or 26.5.1+ (the July 2026 security releases, which fix HTTP/2 flaws CVE-2026-56846 and CVE-2026-56848 in the module `watch` uses). No runtime dependencies. |
150
+ | Transport | REST over HTTPS; `watch` uses gRPC. For a Tor-only node, tunnel to localhost over SSH. |
151
+
152
+ LND only: Core Lightning runes, LNbits keys, Eclair and LDK are not supported. Nostr Wallet Connect strings are decoded and found by `scan`, but NWC connections are revoked in the wallet service.
153
+
154
+ ## How it compares
155
+
156
+ | | `lncli` | Lightning Labs macaroon-bakery | macwarden |
157
+ | --- | --- | --- | --- |
158
+ | Decode and grade permissions offline | `printmacaroon` | `--inspect` | `inspect`, with a risk grade |
159
+ | Bake least-privilege macaroons | `bakemacaroon` | role presets | presets, each on its own root key |
160
+ | Record who got what | no | no | registry and `inventory` |
161
+ | Revoke one macaroon | if you tracked its root key ID | no | `revoke`, verified |
162
+ | Rotate without lockout | by hand | no | `rotate`, verified at both steps |
163
+ | Know when a credential was last used | no | no | `watch` |
164
+ | Find leaked credentials | no | no | `scan`, git history, agent configs, SARIF |
165
+ | Incident response with proof | no | no | `respond` |
166
+
167
+ Everything macwarden does online can be done by hand with `lncli`. What it adds is the record-keeping, the verification after every change, and an order of operations that cannot leave you locked out. For a single agent that needs one scoped macaroon, Lightning Labs' script is a good fit. Spending limits per app belong in Lightning Terminal accounts. NWC budgets belong in your wallet service, such as Alby Hub.
168
+
169
+ ## Limits
170
+
171
+ - A clean `scan` is not proof that nothing leaked. Encrypted, archived or unusually encoded credentials can be missed, and git history covers only what is present in your local clone.
172
+ - Revoking a credential blocks new requests. Streams opened before the revoke, such as an invoice subscription, stay open until they disconnect; restart LND if a compromised credential may hold one.
173
+ - While `watch` is registered, LND waits for it on every RPC. Run it as a supervised service and never enable `rpcmiddleware.addmandatory`. Its own work is bounded whatever callers do: canary alerts are rate-limited, at most one alert command runs (killed after 12 s), and usage data keeps at most 20,000 records.
174
+ - On nodes run with `--no-macaroons`, stateless init or a remote signer, macwarden sees only part of the picture.
175
+
176
+ ## Documentation
177
+
178
+ - [Incident playbook: credentials exposed](docs/incident-playbook.md): what to do when macaroon files may have been copied (BTCPay Server < 2.4.2 and similar)
179
+ - [Replacing the default macaroons](docs/replacing-default-macaroons.md): the operator procedure for moving every app onto its own macaroon
180
+ - [CI integration](docs/ci-integration.md): GitHub Actions, SARIF, pre-commit, gitleaks and TruffleHog
181
+ - [Field check](docs/field-check.md): a short, safe procedure to confirm a release against your own node
182
+ - [SPEC.md](SPEC.md): every command, flag, grade, exit code and output format
183
+ - [SECURITY.md](SECURITY.md): the SAFE rules and threat model
184
+ - [ARCHITECTURE.md](ARCHITECTURE.md): how it is built
185
+ - [CHANGELOG.md](CHANGELOG.md): release notes
186
+
187
+ ## Development
188
+
189
+ ```sh
190
+ npm ci
191
+ npm run build
192
+ npm test # product tests
193
+ npm run test:harness
194
+ ```
195
+
196
+ Online behaviour is tested against a pinned regtest LND in Docker: `npm run fixture:up`, `npm run test:oracle`, `npm run fixture:down`. `npm run test:full` runs everything on the current platform. Changes go through the checkpoint process in [HELIX.md](HELIX.md), with roles in [AGENTS.md](AGENTS.md), milestones in [PLAN.md](PLAN.md) and operating notes in [RUNBOOK.md](RUNBOOK.md). Checkpoint contracts and validation tooling live in [tools/validation/](tools/validation/).
197
+
198
+ ## Security and licence
199
+
200
+ Source and issues: https://github.com/ZilverZtream/macwarden. Report vulnerabilities through GitHub private vulnerability reporting rather than a public issue; see [SECURITY.md](SECURITY.md). macwarden is released under the MIT licence.
package/SECURITY.md ADDED
@@ -0,0 +1,104 @@
1
+ # Security
2
+
3
+ This file defines the security requirements for macwarden.
4
+
5
+ ## What macwarden handles
6
+
7
+ macwarden works with LND macaroons. A macaroon is a bearer credential: whoever holds the bytes has its permissions, with no further check. An `admin.macaroon` (or any macaroon that can spend or bake) is enough to move a node's funds.
8
+
9
+ Why this matters now:
10
+
11
+ - The [BTCPay Server 2.4.2 advisory](https://blog.btcpayserver.org/security-advisory-btcpay-server-2-4-2/) (2026-08-07) describes an issue that "could allow an unauthenticated remote attacker to obtain `.macaroon` credential files for LND", which "could be used to take control of an LND node and move funds". It states "attackers exploited this vulnerability. Users were affected and funds were stolen", and advises: "If you use LND, treat its credentials as potentially exposed".
12
+ - Rotation itself has locked operators out. In [btcpayserver-docker#1112](https://github.com/btcpayserver/btcpayserver-docker/issues/1112) rotation deleted `macaroons.db` and a later `changepassword` left a wallet permanently locked ("LND changes the wallet passphrase before touching the macaroon root key, so a failure on the second step leaves the passphrase already changed"). In [lnd#11049](https://github.com/lightningnetwork/lnd/issues/11049) renaming `admin.macaroon` and `macaroons.db` left LND stuck in `WAITING_TO_START`.
13
+
14
+ So macwarden must never become a new leak path, and must never take the risky rotation path that caused those lock-outs.
15
+
16
+ ## Safety rules
17
+
18
+ These IDs are referenced from SPEC.md. Do not renumber.
19
+
20
+ | ID | Rule | How it is tested or enforced |
21
+ |---|---|---|
22
+ | SAFE-01 | Never print, log, or store macaroon bytes, hex or base64. Fingerprints only. Applies to errors, warnings, crashes and `--json` output too. (The single exception is the macaroon file that `bake`, `rotate` or the offline `attenuate` writes to `--out`, which is the product. The `attenuate --out` file is created through the same exclusive, owner-only writer.) | All rendering goes through the `output` module; process-level handlers route crashes through it. A leak test plants a sentinel macaroon and asserts its raw bytes (as UTF-8 and latin1), hex (both cases), base64 and base64url (padded and unpadded) appear in no stdout, stderr, log, registry, error output or other written file across every command, including a crash forced mid-request; it excludes exactly the `--out` file, including the `attenuate --out` copy. |
23
+ | SAFE-02 | Never call `changepassword`; never read or write `macaroons.db`, wallet password files, seeds, TLS keys. | The `lnd` adapter exposes no `changepassword` call; a test asserts the adapter's endpoint list. `scan` never opens these names (case-insensitive, also as directory names); a test asserts it with an fs spy. |
24
+ | SAFE-03 | Never delete root key ID 0. | `revoke` and `rotate` refuse ID 0 (as an argument or as a `--target-file`'s ID) before any network call; the adapter refuses it again. Unit and integration tests for both layers. |
25
+ | SAFE-04 | Rotation is two invocations: **stage** (bake → verify new) → operator switches the app → **finish** (confirm → revoke old → verify old rejected). A failure anywhere before the revoke leaves the old credential valid. | Failure-injection cases 1–8 in PLAN.md M5 (faulty adapter in unit tests, real faults against the fixture in integration tests) assert with the `lncli` acceptance probe that the old macaroon is still accepted unless the finish's DeleteMacaroonID had completed. |
26
+ | SAFE-05 | Every mutating command (`bake`, `revoke`, `rotate`) supports `--dry-run`; `revoke` and `rotate --finish` require typed confirmation unless `--yes`. Rotation stage relying on permissions unproven by the old credential requires typed informed approval showing the exact grade and permissions before lock, reservation or issue; stage --yes does not authorize this fallback. | Integration tests: `--dry-run` leaves LND's root key IDs, the registry and the `--out` path unchanged; without `--yes`, a declined, wrong or missing (non-TTY) confirmation changes nothing. |
27
+ | SAFE-06 | Registry and all outputs are free of secrets. The registry is `0600` in a `0700` directory; macaroon files are written owner-only. Stale-lock recovery requires same-host dead-owner and pathname identity proof; competing owners are preserved. Failed owned lock/output cleanup is visible and reports exit 7 after possible mutation while retaining any primary error. On Windows `--out` is restricted to the current user with `icacls`; if that fails the command warns that the file inherits the folder's permissions. Bytes are written only through an exclusive (share mode 0) handle opened after the restriction, so no handle opened before it can read them; if another handle exists the command fails without writing. | Leak test covers the registry file. Tests check modes `0600`/`0700` on Linux/macOS; on Windows (CI) the `icacls` call is asserted and its failure produces warning `acl-best-effort`; a real-Windows test opens a handle before the restriction and proves the command fails with no bytes readable through it. |
28
+ | SAFE-07 | `inspect` and `scan` are offline and read-only. | Tests run them with networking stubbed to throw and on read-only fixture directories; file mtimes and contents are unchanged afterwards. |
29
+ | SAFE-08 | No telemetry; the only network peer is the configured LND node; TLS certificate verification is on by default. | Code review rule: the `lnd` adapter is the only module that opens network connections (it uses `node:https`). Integration test: a wrong TLS cert is rejected. |
30
+ | SAFE-09 | macwarden's own credential should be a dedicated macaroon; the docs must state plainly that the ability to bake is admin-equivalent, so that macaroon must be protected like `admin.macaroon`. | Documented below and in the README; a unit test asserts `inspect` grades any macaroon holding `macaroon:generate` as `ADMIN`; online commands warn `own-root-key-0`. |
31
+ | SAFE-11 | `watch` is read-only toward the caller's data: the read-only middleware always accepts, the canary guard always denies, and nothing replaces an RPC response. Intercepted macaroons are decoded in memory to a credential ID and root key ID and dropped; payloads and gRPC metadata are never read; only credential IDs, root key IDs, method names, counts and times are written (`<registry>.usage.json`, owner-only). The own macaroon needs `macaroon:write`, which is admin-equivalent like `macaroon:generate` (SAFE-09). A stalled watcher delays and then fails node RPCs (SPEC Commands > 8), so event output never blocks the answers (bounded queue written off the event loop); never recommend `rpcmiddleware.addmandatory`. Zeroed after use: each received gRPC frame and the copied raw macaroon bytes. Not zeroed: the `Secret` wrapper's own copy and derived identifier bytes, which are unreferenced at once and left to garbage collection. The canary guard fails closed (unknown intercept types and internal errors deny); canaries carry only READ or RECEIVE permissions. Every resource an attacker can drive is bounded: alerts are rate-limited per registered canary (one per 10-second window plus a counted summary) and invented caveat conditions share one bucket; at most one alert command runs, killed (process group) 12 s after it starts; event output is a bounded queue that treats EAGAIN as backpressure; usage holds at most 20,000 records with least-recently-seen eviction, is written at most at 64 KiB/s on average, and the summary counts at most 10,000 credentials; shutdown persists usage and releases the lock before waiting at most 13 s for an alert command. Attribution runs behind a bounded queue that drops and counts work it cannot keep up with, so LND feedback latency is independent of the call rate; stream fields are capped (macaroon 64 KiB, method name and caveat condition 256 bytes) before they are copied; a session ends on a missing registration confirmation (30 s), a stalled message (60 s) or more than 16 MiB of unread feedback. Processes an alert command leaves behind are killed (POSIX process groups, 1 s after it exits and when watch stops; on Windows the tree is killed on timeout only). Output that cannot be delivered never keeps watch running: queued lines are dropped and counted and the process exits within a bounded time, non-zero when it had to be terminated. | Unit tests decide every intercept kind; leak tests assert caller macaroons never appear in events, alerts or the usage file; the regtest oracle proves attribution, canary denial, and unregistration on exit. |
32
+ | SAFE-10 | Development and CI integration tests run against regtest only. (The released tool works on any network — it moves no funds.) | The test fixture starts its own regtest LND + bitcoind and aborts if `getinfo` reports any other network or a version not starting with `0.21.4-beta`. |
33
+
34
+ ## Threat model
35
+
36
+ macwarden helps against:
37
+
38
+ - **Over-privileged apps.** `inspect` and `inventory` show what each credential can do; `bake` issues least-privilege macaroons instead of handing out `admin.macaroon`.
39
+ - **Unknown credentials.** `inventory` flags root key IDs LND has that the registry does not know; `inventory --verify` flags registered macaroons LND no longer accepts.
40
+ - **Leaked credentials in repos, configs and backups.** `scan` finds macaroon files, embedded hex/base64 macaroons, `lndconnect://` URIs and NWC connection strings, and can fail a CI job.
41
+ - **Unknown usage and silent leaks.** `watch` attributes every authenticated RPC to a credential and records last use; `recommend` narrows a credential to what it actually calls; a canary macaroon (`bake --canary`) is refused by LND unless `watch` runs, and while it runs every use is denied and alerted within the same call.
42
+ - **Botched rotation.** `rotate` follows SAFE-04 and only ever revokes per-app root key IDs, never root key 0.
43
+ - **Leaked credentials after an incident.** `respond` rotates every registered credential in one confirmed batch through `rotate` (SAFE-04 and SAFE-05 apply to each), then proves each old credential rejected and each replacement accepted. It never touches root key IDs it does not own and never calls `changepassword` (SAFE-02); for root key 0 it prints the manual LND procedure.
44
+ - **A leaked credential that can bake, and a tampered registry, during an incident.** A holder of `macaroon:generate` can create credentials on any root key ID, including the replacements `respond` stages, so every `respond` proof is scoped to its own credential, credentials that can bake are listed and finished first, and the guidance says that only regenerating all root keys removes everything such a holder could have baked. `respond` and `rotate` treat a replacement link macwarden could not have written as tampering (exit 3), never rotate a canary into an ordinary credential, and list root key IDs the registry calls revoked but LND still holds. Registry evidence values are validated, and printed commands are quoted for POSIX shells and PowerShell, including PowerShell's curly single quotes.
45
+ - **Missing review evidence.** `inventory --report` writes an access-review record from registry facts and LND root key IDs only, never credential bytes.
46
+ - **Leaks reaching CI logs and code-scanning tools.** `scan --format sarif|github` reports only paths, kinds, grades and fingerprints, never matched text, so uploading the SARIF log or printing annotations exposes no credential.
47
+
48
+ macwarden does not protect against:
49
+
50
+ - **A compromised host.** An attacker on the machine running LND or macwarden can read the macaroons directly.
51
+ - **Theft of the admin macaroon itself**, or of macwarden's own macaroon (see below).
52
+ - **Streams that are already open.** Deleting a root key ID blocks new requests only. Streams opened earlier (e.g. `HtlcInterceptor`, `SubscribeInvoices`, REST WebSockets) keep running until they disconnect. New requests with this macaroon are now rejected. Streams opened before the revoke stay open until they disconnect; if the credential may be compromised, restart LND.
53
+ - **Anything on root key ID 0.** `admin.macaroon`, `readonly.macaroon`, `invoice.macaroon` and anything else baked on ID 0 cannot be revoked individually. They stay valid until the operator rotates the root keys by hand. `changepassword --new_mac_root_key` regenerates **every** root key, not just ID 0: every macaroon ever baked becomes invalid, including macwarden-baked ones and macwarden's own. LND's docs also describe deleting `macaroons.db`. So the order is always: rotate the root keys first (in a maintenance window), then bake fresh per-app macaroons (PLAN.md M6). Moving apps first and rotating afterwards breaks every app.
54
+ - **Nodes where macwarden's view is incomplete.** With `--no-macaroons` LND checks no macaroons, so macwarden's verification means nothing. With `--mac_root_key` (stateless init) macaroons can be baked offline and never appear in `inventory`. With a remote signer, the signer has its own macaroon store and must be inventoried separately. Other tools and operators can store root keys in the same LND store (Lightning Terminal is reported to; ARCHITECTURE.md assumption 12), which is why `revoke` needs `--unregistered` for IDs it did not issue.
55
+ - **IP-restricted macaroons.** `bake` has no `--ip`: LND is reported to see REST clients as its REST proxy's address (ARCHITECTURE.md assumption 5), so an IP lock either breaks the app or accepts everyone.
56
+ - **Other implementations.** CLN runes, Eclair and LDK are out of scope for v1.
57
+ - **Secrets `scan` does not recognise.** Scanning is pattern-based. Text detectors only match macaroons with location `lnd`; line-wrapped, split, encrypted, archived or oddly encoded credentials can be missed or only flagged as `undecodable-candidate`; UTF-16 without a BOM is treated as binary; files over 5 MiB are skipped; git history is scanned only with `scan --git-history`, and only for history that still exists in the local repository (rewritten history, other clones and objects missing from a partial clone are not covered; a missing object fails the scan). A clean scan is not proof of no leak.
58
+
59
+ Two facts to keep in mind:
60
+
61
+ - **`macaroon:read` is a validity oracle.** It includes CheckMacaroonPermissions, so even an inventory-only macaroon can test whether a stolen macaroon is still valid.
62
+ - **Fingerprints are linkable.** A fingerprint reveals nothing usable about the credential (no feasible preimage), but the same credential has the same fingerprint in every report, so reports can be correlated. A fingerprint changes when a caveat is added.
63
+
64
+ ## The credential macwarden runs with
65
+
66
+ - Online commands need a macaroon. Use a **dedicated** one for macwarden, not `admin.macaroon`, so it can be revoked on its own. First-time setup may use `admin.macaroon`; every online command then warns `own-root-key-0`.
67
+ - `bake` needs `macaroon:generate`. **Holding `macaroon:generate` is admin-equivalent**: it can bake a macaroon with any permission, including spending. Protect macwarden's macaroon exactly like `admin.macaroon`.
68
+ - `revoke` and `rotate` also need `macaroon:write`, which can revoke any non-zero root key ID. macwarden refuses to revoke or rotate the root key ID it is itself using.
69
+ - For `inventory` only, `info:read` and `macaroon:read` are enough. Do not give a read-only workflow a baking macaroon.
70
+ - Do not store macwarden's macaroon in the repo or CI variables of the apps it manages. Run it from an operator machine or a locked-down admin host.
71
+
72
+ Recommended: bake, use and revoke a short-lived session macaroon per session, on its own root key ID, with only `macaroon:generate macaroon:read macaroon:write info:read` and a one-hour `time-before`. `lncli bakemacaroon` defaults to root key ID 0, so pass an explicit `--root_key_id`:
73
+
74
+ ```sh
75
+ ID=$(( (RANDOM << 30) | (RANDOM << 15) | RANDOM )) # check it is not in `lncli listmacaroonids`
76
+ lncli bakemacaroon --root_key_id "$ID" --timeout 3600 \
77
+ --save_to ./macwarden-session.macaroon \
78
+ macaroon:generate macaroon:read macaroon:write info:read
79
+ macwarden inventory --macaroon ./macwarden-session.macaroon # … this session's commands
80
+ lncli deletemacaroonid "$ID"
81
+ rm ./macwarden-session.macaroon
82
+ ```
83
+
84
+ Check the flag names with `lncli bakemacaroon --help` on your LND version.
85
+
86
+ ## Installing
87
+
88
+ - Version `1.0.0` is released (Owner decision 2026-10-08) after complete Windows and unprivileged Linux validation, pinned-regtest references, leak checks and independent review. A real-node field check ([docs/field-check.md](docs/field-check.md)) follows release. Hosted Actions remain disabled, and macOS is not tested. When the package is published through npm trusted publishing, it carries npm provenance that `npm audit signatures` can verify.
89
+ - For online commands install a pinned version and check signatures: `npm i -g macwarden@<exact version>`, then `npm audit signatures`.
90
+ - For the offline commands, `npx macwarden@<exact version> inspect …` is fine.
91
+
92
+ ## Development rules
93
+
94
+ - No real credentials in issues, prompts, logs, commits or test snapshots. Never paste a real macaroon, `lndconnect://` URI or NWC string into an AI agent. Fixtures use throwaway regtest credentials generated by the test suite (SAFE-10).
95
+ - Test vectors committed to the repo come from a disposable regtest node and are labelled as such. Read vectors by path and use fingerprints in reports, logs and commit messages. The repo's own `.macwardenignore` lists their fingerprints so `scan` of the repo stays clean.
96
+ - Run gitleaks only with `--redact`; without it gitleaks prints and stores the matched credential.
97
+ - A real credential that is ever exposed is treated as compromised: revoke it on the node and remove it from history; deleting the text is not enough.
98
+ - The development process (checkpoints, gates, reviews) is in HELIX.md.
99
+
100
+ ## Reporting a vulnerability
101
+
102
+ Use GitHub private vulnerability reporting: the repository's **Security** tab → **Report a vulnerability**. Do not open a public issue for a vulnerability.
103
+
104
+ Please include the macwarden version, the command and flags used, and what you observed. Do not send real macaroons; a fingerprint or a regtest reproduction is enough.
package/dist/bin.js ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { main, completionStatus } from './cli.js';
3
+ import { alertCleanupDone } from './watch/alert.js';
4
+ // watch runs unattended (systemd, CI). If its output can no longer be delivered (a pipe
5
+ // that is never read), a write already handed to the operating system cannot be
6
+ // cancelled: on Windows every write to an inherited pipe is synchronous, and a normal exit
7
+ // waits for the blocked writer. So once watch has finished, a short unref'd deadline
8
+ // terminates the process if it is still alive. With a reader that reads, the process exits
9
+ // on its own before the deadline with its real status; when the deadline fires, the
10
+ // non-zero exit (1 on Windows, SIGKILL on POSIX) reports that output was not delivered.
11
+ // Other commands are not affected: their output is waited for in full.
12
+ // The deadline is armed only after every alert process-tree kill (Windows taskkill) has
13
+ // finished, each bounded: ending this process first would end the alert command itself and
14
+ // orphan the children taskkill has yet to find (audit MW102-05).
15
+ const WATCH_EXIT_GRACE_MS = 3_000;
16
+ const status = await main(process.argv.slice(2));
17
+ process.exitCode = completionStatus(status);
18
+ if (process.argv[2] === 'watch') {
19
+ await alertCleanupDone();
20
+ setTimeout(() => process.kill(process.pid, 'SIGKILL'), WATCH_EXIT_GRACE_MS).unref();
21
+ }
@@ -0,0 +1 @@
1
+ export {api0_readBounded as readBounded, api0_readBoundedFile as readBoundedFile, api0_assertPlainFile as assertPlainFile, api0_BoundedFileError as BoundedFileError, api0_DEFAULT_FILE_LIMIT as DEFAULT_FILE_LIMIT, api0_LOCK_FILE_LIMIT as LOCK_FILE_LIMIT} from "./runtime.js";
@@ -0,0 +1 @@
1
+ export {api1_withCancellation as withCancellation, api1_checkCancellation as checkCancellation, api1_cancellable as cancellable, api1_cancellationSignal as cancellationSignal, api1_cleanup as cleanup, api1_cleanupOutcome as cleanupOutcome, api1_Cancelled as Cancelled} from "./runtime.js";
@@ -0,0 +1 @@
1
+ export {api2_canonicalIp as canonicalIp, api2_canonicalCidr as canonicalCidr} from "./runtime.js";
package/dist/cli.js ADDED
@@ -0,0 +1 @@
1
+ export {api3_writeChunks as writeChunks, api3_main as main, api3_completionStatus as completionStatus, api3_WATCH_SUMMARY_WRITE_MS as WATCH_SUMMARY_WRITE_MS} from "./runtime.js";
@@ -0,0 +1 @@
1
+ export {} from "./runtime.js";
@@ -0,0 +1 @@
1
+ export {api5_run as run, api5_attenuate as attenuate} from "../runtime.js";
@@ -0,0 +1 @@
1
+ export {api6_allocateRootKeyId as allocateRootKeyId} from "../../runtime.js";
@@ -0,0 +1 @@
1
+ export {api7_run as run, api7_bake as bake} from "../../runtime.js";
@@ -0,0 +1 @@
1
+ export {api8_reserveOut as reserveOut} from "../../runtime.js";
@@ -0,0 +1 @@
1
+ export {api9_handlers as handlers} from "../runtime.js";
@@ -0,0 +1 @@
1
+ export {api10_inspectResult as inspectResult, api10_run as run, api10_inspect as inspect} from "../runtime.js";
@@ -0,0 +1 @@
1
+ export {api11_run as run, api11_inventory as inventory} from "../runtime.js";
@@ -0,0 +1 @@
1
+ export {api12_narrowLabel as narrowLabel, api12_run as run, api12_recommend as recommend, api12_SHORT_OBSERVATION_SECONDS as SHORT_OBSERVATION_SECONDS} from "../runtime.js";
@@ -0,0 +1 @@
1
+ export {api13_aggregateExit as aggregateExit, api13_run as run, api13_respond as respond, api13_ROOT_KEY_0_GUIDANCE as ROOT_KEY_0_GUIDANCE, api13_OWN_ON_ROOT_KEY_0 as OWN_ON_ROOT_KEY_0, api13_REVOKED_BUT_PRESENT_GUIDANCE as REVOKED_BUT_PRESENT_GUIDANCE, api13_BAKING_RISK_GUIDANCE as BAKING_RISK_GUIDANCE, api13_CUSTOM_CAVEAT_NOTE as CUSTOM_CAVEAT_NOTE, api13_UNKNOWN_GUIDANCE as UNKNOWN_GUIDANCE} from "../runtime.js";
@@ -0,0 +1 @@
1
+ export {api14_run as run, api14_revoke as revoke} from "../runtime.js";
@@ -0,0 +1 @@
1
+ export {api15_run as run, api15_rotate as rotate} from "../runtime.js";
@@ -0,0 +1 @@
1
+ export {api16_run as run} from "../runtime.js";
@@ -0,0 +1 @@
1
+ export {api17_run as run, api17_verify as verify, api17_ROOT_KEY_0_ACCEPTED as ROOT_KEY_0_ACCEPTED, api17_CAN_BAKE_ACCEPTED as CAN_BAKE_ACCEPTED, api17_CAN_BAKE_ROOT_KEY_0 as CAN_BAKE_ROOT_KEY_0, api17_OWN_ROOT_KEY_ACCEPTED as OWN_ROOT_KEY_ACCEPTED, api17_REJECTED_INCONCLUSIVE as REJECTED_INCONCLUSIVE, api17_CUSTOM_CAVEAT_REJECTED as CUSTOM_CAVEAT_REJECTED} from "../runtime.js";
@@ -0,0 +1 @@
1
+ export {api18_scheduleDeadline as scheduleDeadline, api18_lockOwnerGone as lockOwnerGone, api18_acquireLock as acquireLock, api18_run as run, api18_stdoutEvents as stdoutEvents, api18_watch as watch, api18_CANARY_RELOAD_MS as CANARY_RELOAD_MS, api18_MAX_TIMER_MS as MAX_TIMER_MS} from "../runtime.js";
@@ -0,0 +1 @@
1
+ export {api19_redactEncodedCredentials as redactEncodedCredentials} from "./runtime.js";
@@ -0,0 +1 @@
1
+ export {api20_macaroonEosEnd as macaroonEosEnd, api20_macaroonPacketRange as macaroonPacketRange, api20_macaroonSectionRange as macaroonSectionRange, api20_parseTimeBefore as parseTimeBefore, api20_canonicalMacaroonForm as canonicalMacaroonForm, api20_decodeMacaroon as decodeMacaroon, api20_macaroonId as macaroonId, api20_encodeMacaroon as encodeMacaroon, api20_addFirstPartyCaveat as addFirstPartyCaveat, api20_timeBeforeCondition as timeBeforeCondition, api20_validMacaroonIdentifier as validMacaroonIdentifier, api20_isNwcContainer as isNwcContainer, api20_decodeNwc as decodeNwc, api20_DecodeError as DecodeError} from "../runtime.js";
@@ -0,0 +1 @@
1
+ export {api21_macaroonEnds as macaroonEnds} from "../runtime.js";
@@ -0,0 +1 @@
1
+ export {api22_unsafeNodeDiagnostics as unsafeNodeDiagnostics} from "./runtime.js";
package/dist/errors.js ADDED
@@ -0,0 +1 @@
1
+ export {api23_UsageError as UsageError, api23_InputError as InputError, api23_DecodeError as DecodeError, api23_LndError as LndError, api23_LndUncertainError as LndUncertainError} from "./runtime.js";
@@ -0,0 +1 @@
1
+ export {api24_grade as grade, api24_gradeFacts as gradeFacts, api24_describe as describe} from "../runtime.js";