diff options
| author | Adam Soltys <[email protected]> | 2026-08-22 08:08:04 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-22 08:08:04 -0700 |
| commit | 0bb7ab45344068ed8ea3cc77ad9f7c2a038bb45f (patch) | |
| tree | f288d01fdc82b2df7ba9fd3f6ef6f8792ef49880 /test/spend-budget.test.ts | |
| parent | 38bfeeecdafb678d2a495c2865d1ff4dd6255ac9 (diff) | |
| download | coinos-server-0bb7ab45344068ed8ea3cc77ad9f7c2a038bb45f.tar.gz coinos-server-0bb7ab45344068ed8ea3cc77ad9f7c2a038bb45f.zip | |
nwc: enforce per-connection spend budgets + flag-gated incident containment
Thread the remaining allowance through every spend path (pay_invoice, pay,
pay_keysend, internal transfer) as maxTotal, and have debit() assert the spend
stays within it after it reloads the invoice and captures the tip — so a
delegated NWC credential can no longer exceed its configured budget, and a
mutated tip can't slip a small-budget check and drain via the internal path.
checkBudget now returns the remaining allowance alongside any error; a blank
budget is treated as deliberately unlimited and a positive budget requires a
known renewal period.
Add operator-controlled containment for a suspected credential disclosure, all
gated behind Redis flags with no hardcoded scope:
nwc:disabled fail every method closed
nwc:credential-cutoff retire connections created before an epoch
nwc:quarantined block an individual disclosed connection
nwc:guard:dormant-keysend quarantine the first keysend on a long-idle app
Fix an ownership check that read the bare pubkey instead of app:<pubkey>, which
let an authenticated user overwrite another connection's config, and validate
budget/fee/renewal inputs.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XiwgmQKwKnRxUmVDd4xSf7
Diffstat (limited to 'test/spend-budget.test.ts')
| -rw-r--r-- | test/spend-budget.test.ts | 38 |
1 files changed, 38 insertions, 0 deletions
diff --git a/test/spend-budget.test.ts b/test/spend-budget.test.ts new file mode 100644 index 00000000..81c16b2a --- /dev/null +++ b/test/spend-budget.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, test } from "bun:test"; +import { + assertWithinSpendLimit, + debitTotal, +} from "$lib/spend-budget"; + +describe("NWC budget covers the final captured tip and fees", () => { + test("a small invoice with an oversized mutable tip is rejected", () => { + expect( + debitTotal({ amount: 19, tip: 1_701_450, fee: 0, ourfee: 0 }), + ).toBe(1_701_469); + expect(() => + assertWithinSpendLimit({ + amount: 19, + tip: 1_701_450, + maxTotal: 10_000, + }), + ).toThrow("Budget exceeded"); + }); + + test("the exact remaining allowance succeeds", () => { + expect( + assertWithinSpendLimit({ + amount: 9_000, + tip: 500, + fee: 400, + ourfee: 100, + maxTotal: 10_000, + }), + ).toBe(10_000); + }); + + test("an unlimited first-party debit remains unaffected", () => { + expect( + assertWithinSpendLimit({ amount: 19, tip: 1_701_450 }), + ).toBe(1_701_469); + }); +}); |
