aboutsummaryrefslogtreecommitdiffstats
path: root/lib/spend-budget.ts
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-22 08:08:04 -0700
committerAdam Soltys <[email protected]>2026-08-22 08:08:04 -0700
commit0bb7ab45344068ed8ea3cc77ad9f7c2a038bb45f (patch)
treef288d01fdc82b2df7ba9fd3f6ef6f8792ef49880 /lib/spend-budget.ts
parent38bfeeecdafb678d2a495c2865d1ff4dd6255ac9 (diff)
downloadcoinos-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 'lib/spend-budget.ts')
-rw-r--r--lib/spend-budget.ts39
1 files changed, 39 insertions, 0 deletions
diff --git a/lib/spend-budget.ts b/lib/spend-budget.ts
new file mode 100644
index 00000000..3f2a70ac
--- /dev/null
+++ b/lib/spend-budget.ts
@@ -0,0 +1,39 @@
+const number = (value: unknown) => {
+ const parsed = Number(value);
+ return Number.isFinite(parsed) ? parsed : 0;
+};
+
+export const debitTotal = ({
+ amount,
+ tip = 0,
+ fee = 0,
+ ourfee = 0,
+}: {
+ amount: unknown;
+ tip?: unknown;
+ fee?: unknown;
+ ourfee?: unknown;
+}) => number(amount) + number(tip) + number(fee) + number(ourfee);
+
+// Enforce a caller-specific spending allowance at debit's final choke point,
+// after mutable invoice metadata (notably an internal invoice tip) and fees have
+// been captured. Checking earlier lets a recipient mutate the tip between the
+// NWC budget check and the ledger debit.
+export const assertWithinSpendLimit = ({
+ amount,
+ tip = 0,
+ fee = 0,
+ ourfee = 0,
+ maxTotal,
+}: {
+ amount: unknown;
+ tip?: unknown;
+ fee?: unknown;
+ ourfee?: unknown;
+ maxTotal?: number;
+}) => {
+ const total = debitTotal({ amount, tip, fee, ourfee });
+ if (maxTotal !== undefined && total > maxTotal)
+ throw new Error(`Budget exceeded: ${total} of ${maxTotal} remaining`);
+ return total;
+};