aboutsummaryrefslogtreecommitdiffstats
path: root/routes
Commit message (Collapse)AuthorAgeFilesLines
* printer: publish receipts at QoS 1 and clean LNURL metadata memosAdam Soltys2026-09-141-0/+1
| | | | | | | | | | | | Receipts were published at QoS 0, so a printer that was briefly offline (WiFi blip, keepalive timeout) never received them. QoS 1 lets the broker queue them for printers that hold a persistent session. LNURL-pay invoices carry the raw metadata array as their memo when the payer leaves no comment; unwrap the text/plain entry before printing. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_011cCouc5ASRXHDp5esLciEK
* ecash: make claims work against the v1 keyset mintAdam Soltys2026-09-141-6/+3
| | | | | | | | | | | | | | | cashu-ts 2.9.0 only derives v0 keyset ids, so CashuWallet rejected the mint's current keyset and receive() could never run. Build the wallet from keys preloaded off our own mint instead of verifying, decode tokens with the keysets (2.9.0 shortens v1 ids in the tokens it encodes, and won't expand them otherwise), and treat an unreadable house-pool value as empty rather than throwing — the pool key had been found holding a bare integer, which surfaced as "n.startsWith is not a function" on every redeem. The pool write is awaited so a dropped set can't strand freshly swapped proofs. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_0166Ci2XdCair1B9ych7w8dq
* security: drop the admin endpoint surface and land pending hardeningAdam Soltys2026-09-134-60/+94
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Removes the admin-gated endpoints and the account flags they set, plus a batch of hardening that had accumulated uncommitted in the tree. These were deployed and verified together, so they land as one change. admin surface: - drop POST /hidepay, POST /unlimit and GET /users, their handlers, and the now-unused admin preValidation export - `unlimited` was only ever written, never read; `hidepay` only appeared in output projections. Both flags go, along with their whitelist, public-fields and type entries. request log: - redactBody now recurses. It only masked top-level keys, so a body that nests credentials one level down wrote them to the log in plaintext, and pin fields were not on the list at all. payments: - sendOnchain/build gate `aid` through requireAccount, so a caller can only spend from an account it owns - sendOnchain signs only a tx we composed, matched against a short-lived per-account build receipt - raise xpay retry_for to 60s; route-finding preferred a drained zero-fee hop and burned the whole budget on it. check()'s reversal window keeps its 2x margin. ecash / invoices / webhooks: - resolve a token's mint through the IP-pinning fetcher instead of plain fetch, closing an unauthenticated SSRF path into the internal network - reject non-positive amounts before the balance write - /sign requires the caller to own the address, rather than signing under any address the hot wallet controls - webhook delivery no longer disables TLS verification; the body carries the merchant's shared secret registration: - refuse a username the v3 registrar holds unless the caller's own nostr key is the holder (lib/names.ts) liquid: - honour the deposit kill switch at invoice generation and at credit elements image: - pin 23.3.4 and verify the release tarball checksum Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_0112TGi9GGNWqji1XQ5jZRzc
* flash: build each config image in its own temp dirAdam Soltys2026-09-041-6/+16
| | | | | | | | | | The handler wrote every request's config.txt and littlefs.img to the same paths, so two overlapping requests could hand one user the other's wifi key and API token. Use a per-request mkdtemp dir, read the image into memory, and remove the dir when done. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMi2Y1oKPZ3TeeFmLej47y
* flash: create printer dir and return errors instead of unhandled 500Adam Soltys2026-09-041-7/+13
| | | | | | | | | | The web flasher config endpoint wrote ./printer/config.txt without creating the directory, so on a fresh deploy it threw ENOENT and the JSON error body was flashed to devices as the LittleFS partition. Create the directory, validate inputs, and route errors through bail. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_014gVYGi29FjTeLdNTiutkZx
* fund: authenticated recovery of a rotated fund's old linkAdam Soltys2026-08-281-4/+29
| | | | | | | | | | | | | | | When a fund's secret id has been rotated (invalidating an exfiltrated id list), its old id 404s. Let a legit owner who bookmarked the old link find the fund again — but reveal the new id ONLY to an authenticated manager of the new fund, via a `fund:rotated:<oldid>` -> newid map. An anonymous caller or a non-owner gets the same "fund not found" as before, so an attacker replaying a leaked old id learns nothing (an open old->new redirect would defeat the rotation). The /fund/:id route now runs `optional` auth so req.user is available without requiring it; bearer funds with no managers are intentionally not recoverable this way. rotate-funds.ts records the mapping on any future rotation. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01DKkev76U4YRnRAqM6jqbae
* login: fail-open recaptcha verify on infrastructure errorsAdam Soltys2026-08-251-2/+17
| | | | | | | | | | | | | | | | | | verifyRecaptcha failed closed on ANY error from the Google siteverify call, so a transient coinos<->Google network blip (or a Google slowdown) turned every non-admin login into a "failed captcha" 401 until connectivity recovered — a self-inflicted login outage from an external dependency. Distinguish a real verdict from an infrastructure failure: fail CLOSED only on a genuine success:false from Google; on a thrown error (network, timeout, 5xx) fail OPEN and log it. Reaching Google is not part of authentication — the password and the per-IP login rate limits still gate the request, and captcha is only bot-mitigation, so allowing a login through when the verifier is unreachable is the safer failure mode. Also bound the call with a 6s timeout so a slow verifier can't hang the login. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01XiwgmQKwKnRxUmVDd4xSf7
* take/authorize: kill-switch + validate fund-claim amountsAdam Soltys2026-08-251-4/+37
| | | | | | | | | | | | | | | | | | The /take fund-claim path could pay out unbacked balance: the only ceiling on a take is sats(fiat / rates[currency]) from the authorization, and a crypto-denominated authorization (rate ~1) or a non-finite/zero rate turns that ceiling astronomically large, so a take is no longer bounded by the authorized fiat value. Combined with the fund debit/credit running per-call, an authorizer account could be drained far beyond intent. - Add a `fund:disabled` redis kill-switch to both /authorize and /take that fails the fund/authorize/take mechanism closed while it is audited. - Reject non-finite/zero rates, non-fiat (crypto) authorization currencies, and non-positive/NaN fiat and amounts, so the fiat cap can no longer be defeated. - Require the authorizer account to exist before debiting it. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01XiwgmQKwKnRxUmVDd4xSf7
* nwc: 404 missing connections + require key rotation to revive retired onesAdam Soltys2026-08-221-2/+31
| | | | | | | | | | | | | | | | | | | | | | | Damus's one-click setup derives its NWC secret deterministically from the user's nsec, so after the credential invalidation its re-mint reused the same retired pubkey and could never recover; meanwhile GET /app/:pubkey crashed into a 500 on missing records instead of the 404 the client needs to know to re-create the connection. We store client-supplied connection secrets (verified: all 2744 Damus app records in prod carry one), so a disclosed DB means disclosed secrets and re-authorizing the same pubkey would re-authorize the leaked bearer credential. POST /app therefore rejects any pubkey that is quarantined or predates nwc:credential-cutoff with an explicit 403 telling the client to mint a fresh secret; a fresh secret yields a fresh pubkey and passes. Owner posts refresh the record's created epoch so live connections stay clear of the cutoff, and GET /apps drops stale set members instead of 500ing the listing. Damus needs a versioned derivation salt so one-click can mint a genuinely new secret; PR submitted separately. Co-Authored-By: Claude Fable 5 <[email protected]>
* nwc: enforce per-connection spend budgets + flag-gated incident containmentAdam Soltys2026-08-221-2/+30
| | | | | | | | | | | | | | | | | | | | | | | | | 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
* confirm: never re-credit receive outputs on wallet-funded txsAdam Soltys2026-08-221-0/+20
| | | | | | | | | | | | | | | | | | A transaction our own wallet contributed inputs to carries a "send" detail; every receive output on it is then our own change or an internal move, never an external deposit. Crediting such an output mints phantom balance against funds already in custody — the misclassification an external report flagged in the on-chain send path (wallet ownership is not account ownership). Guard the credit at the settlement layer, which is the correct boundary and covers both bitcoin and liquid uniformly: if the tx has any send detail, skip every receive output (log a tripwire for amounts >= 300 sats so continued probing is visible). Rejecting these at send time instead would break legitimate withdrawals, whose change routinely lands on a reused deposit address that resolves to an invoice. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01XiwgmQKwKnRxUmVDd4xSf7
* nostr: resolve NIP-05 via the v3 registrar first; empty (not 500) on unknown ↵Adam Soltys2026-08-171-1/+15
| | | | | | | | | | | name A claimed name's NIP-05 belongs to its wallet key in the registrar, so query NAMES_URL/.well-known/nostr.json first and fall back to the local lookup for legacy accounts. Return an empty names object for an unknown name per NIP-05 instead of throwing a 500. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* users: sum sub-account balances in the deleteSelf guard (S12)Adam Soltys2026-08-071-4/+13
| | | | | | | | | | The "withdraw your balance before deleting" guard summed only the main account, but the deletion loop wipes every sub-account's balance:/pending: keys unconditionally. A user with dust in the main account and savings in a sub-account passed the guard and silently lost the sub-account funds. Sum balance+pending across all ${id}:accounts before refusing. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* payments: never lose a settled deposit to an oversized memo (S9/S10)Adam Soltys2026-08-071-1/+4
| | | | | | | | | | | | | | credit() ran fail("memo too long") on memos >5000 chars, but it runs AFTER settlement: an incoming lightning/bolt12 deposit has already advanced pay_index, and an internal send has already debited the sender. The throw was only logged, replay() hit the same throw, and the sats sat in the house wallet uncredited. The memo is attacker-controllable and was uncapped via the lnurl comment param and the bolt12 payer_note. Truncate the memo in credit() instead of throwing (a clipped note is harmless), and cap the lnurl comment to the advertised 512 at ingress. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* logging: redact auth headers; set cookie flags on session token (S5)Adam Soltys2026-08-071-2/+14
| | | | | | | | | | | The request logger persisted full req.headers to the "req" log, including the Authorization bearer token and the token cookie — both non-expiring JWTs, so log-read access became account takeover. Redact the authorization and cookie headers. Also set httpOnly/secure/sameSite on the token cookie so it isn't readable by injected JS or sent over plaintext HTTP. (Revocation is already handled by the evicted set.) Co-Authored-By: Claude Opus 4.8 <[email protected]>
* auth: fail closed on adminpass, scrub sample secrets (S3)Adam Soltys2026-08-073-4/+9
| | | | | | | | | | | | | | | | The master-password path (isAdmin = password === config.adminpass) logs in as any user, skipping password, 2FA, and captcha. Worse, when adminpass is unset an omitted password field made undefined === undefined true, authenticating as anyone. Require both the configured value and the supplied password to be non-empty and match exactly, across every adminpass comparison (login, recaptcha bypass, support email, freeze). Also scrub config.ts.sample: the sample shipped a working-looking JWT secret, two server nsec keys, and adminpass "pwa". Replace with fail-closed placeholders and regenerate instructions. Rotate any deployment that ever shipped the sample values. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* auth: hash passwords at bcrypt cost 12 with transparent upgrade (S2)Adam Soltys2026-08-071-2/+17
| | | | | | | | | | Registration, password change, and admin reset all hashed at bcrypt cost 4 (16 rounds, ~1,300 guesses/s/thread) — the single factor gating the custodial balance and the nip49 passphrase for the stored nostr key. Raise all three sites to cost 12, and on successful login re-hash any legacy cost-4 hash now that we hold the verified plaintext. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* sockets: verify JWT signatures on the websocket auth path (S1)Adam Soltys2026-08-071-4/+4
| | | | | | | | | | | | | | The websocket auth gate used jwt.decode(), which parses the payload but never checks the signature — anyone could forge {id: <victim uid>} (alg:none or any key), register their socket under the victim's uid, and receive every emit for that account. Switch both track() and the "login" gate to jwt.verify(token, config.jwt), rejecting forged/altered tokens. Also project the full-record socket emits through pick(user, whitelist) so a "user" event can never carry the bcrypt hash, TOTP secret, or PIN — matching the shape /login and /me already return. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* lnurl: defer to the v3 registrar for names it has claimedAdam Soltys2026-08-041-15/+53
| | | | | | | | | | | | | | Serving local accounts unconditionally sent migrated names (whose legacy user record still exists) back to the old account, and left v3-only custom names 500ing — only npub1* names had a Cloudflare redirect. Resolution order is now: registrar-claimed -> registrar (response handed through, cached 60s, fail-open to local if the registrar is down), else local account, else 404. The migrated flag stays out of it: register.ts stamps it on essentially every account. No loop: we defer only for names the registrar HAS, and it never queries us for those. Co-Authored-By: Claude Fable 5 <[email protected]>
* lnurl: serve local accounts directly instead of deferring to v3 registrarAdam Soltys2026-08-041-19/+9
| | | | | | | | | | | | | | | | | A recent change made this coinos.io lnurlp endpoint defer any migrated-flagged account to the v3 names registrar and 404 when the registrar didn't have the name. But registration stamps that flag on essentially every account and never syncs names to the registrar, so this broke lightning-address resolution for nearly all accounts. The v3 registrar is the front for v3 accounts and falls back to this endpoint, so this endpoint is the fallback target: it must serve its own accounts directly and must not defer back (which also avoids a loop once the registrar's fallback is live). Serve the local account; 404 only when no local account exists. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dpqu6wizYe1c1g4k5Gd8JK
* Let a migrated account hand its address to coinos v3Adam Soltys2026-08-032-4/+5
| | | | | | | | | | | | A `migrated` flag on the user, settable through /user and visible on the public user endpoint. It changes exactly one thing: lnurlp for that username defers to the names registrar, so payments to [email protected] arrive in the person's v3 wallet. The account itself is untouched — same username, same history, still usable here — which is why this is a flag and not a rename or a shutdown. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
* LNURL: fall back to the v3 names registrar for unknown usersAdam Soltys2026-08-021-1/+21
| | | | | | | | | An address at this domain may now belong to a coinos v3 wallet rather than a local account. When the username isn't ours, ask the names registrar and pass its payRequest through unchanged — its callback is absolute, so the payer talks to it directly from there. Co-Authored-By: Claude Fable 5 <[email protected]>
* confirm: guard the first-seen deposit credit atomically (COINOS-2)Adam Soltys2026-07-311-0/+8
| | | | | | | | | | | | | | | | /confirm is unauthenticated, and its first-seen branch credited a deposit after a plain getPayment() read with no lock. Concurrent callers (walletnotify, catchUp, bulk sweeps, or anyone posting the endpoint) all read p == null for one deposit and each ran credit(), inflating both the pending balance and the spendable credit:<net>:<uid> fee accumulator — a fee waiver nobody paid for. Verified on regtest: 10 concurrent /confirm on one 100,000 sat deposit credited 1,000,000 pending and 4,000 fee credit. With the NX guard, exactly 100,000 and 400. Mirrors the lock the confirm stage already had. Reported-by: Muhammed Shekho (@SYR-ROOT) <[email protected]> Co-Authored-By: Claude Fable 5 <[email protected]>
* take: claim single-use authorizations atomically (COINOS-3)Adam Soltys2026-07-311-17/+38
| | | | | | | | | | | | | | | | | | | take() enforced single-use by reading authorization.claimed, checking it, then writing it back — with yielding awaits in between and no lock. N concurrent POST /take for one authorization all passed the gate, so each debited the authorizer and funded the pool. Verified on regtest: 10 concurrent takes redeemed one 10,000 sat single-use authorization 5 times (50,000 debited from the authorizer, 50,000 credited to the caller). With the SET NX claim, exactly one wins. Funding also runs inside a try that releases the claim on failure and only marks the record claimed once the money has landed — otherwise a funding that throws (insufficient balance, server limit) burns the authorization: claim key set, fund never funded, every later take skipping the funding block. Reported-by: Muhammed Shekho (@SYR-ROOT) <[email protected]> Co-Authored-By: Claude Fable 5 <[email protected]>
* mint/lightning: refuse caller-supplied invoices, tiered fees, log hygieneAdam Soltys2026-07-301-3/+11
| | | | | | | | | | | | | | | - invoices: reject caller-supplied bolt11/bolt12; the server always mints its own. Closes the invoice-registration path behind the shared-node credit and payment-hijack vectors. - lightning: skip mint-owned invoices (lbl* label) in the paid-invoice listener. - payments: lightning free-tier fee credit, withheld for flagged high-throughput (ln:nofree) accounts which pay an elevated rate; the failed-send reversal restores credit at the same rate. freezeCheck made crash-resilient. Lightning and bolt12 sends stay pending until the preimage settles. - payments (route): validate payment-request / invoice input types. - logging: collapse repeated xpay failure lines into one. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* bolt12 zaps: pay offers, NWC pay/receive, per-user offers, NIP-44Adam Soltys2026-07-302-35/+74
| | | | | | | | | | | | | | | | - sendLightning fetches an invoice from lno1 offers (payer_note passthrough for NIP-177 zap intents) and pays it through the existing bolt12 path - NWC: new nwc#2 pay/receive methods (BIP-321 URIs, payer proofs stubbed until CLN 26.06 createproof), nip44_v2 alongside nip04 with per-request scheme detection, dual 23196/23197 notifications, 13194 refreshed when its content or encryption tag goes stale - standing per-user bolt12 offer via GET /offer; incoming bolt12 payments record invreq_payer_note as memo - parse/fetchinvoice routes handle offers; zap display parses kind 9736 receipts (amount tag + embedded intent) and drops a call to a sync() helper that never existed Co-Authored-By: Claude Fable 5 <[email protected]>
* preimages: support fiat prices, converted at invoice-creation timeAdam Soltys2026-07-141-8/+25
| | | | | | | | | | | | | | POST /preimages now accepts fiat + optional currency (defaults to the merchant's) as an alternative to a sat price. The fiat amount is stored per entry and converted to sats at the moment a usePreimage invoice is created, so the price tracks the exchange rate instead of freezing at upload time. The invoice also takes on the entry's currency and rate so receipts display the fiat price exactly as the merchant set it. If no rate is available for the entry's currency, the preimage is pushed back onto the queue and creation fails rather than issuing an any-amount invoice. Co-Authored-By: Claude Fable 5 <[email protected]>
* preimages: enforce a merchant-set price per preimage batchAdam Soltys2026-07-131-11/+24
| | | | | | | | | | | | | Invoice creation is unauthenticated, so buyers could name their own price for a queued preimage (create the usePreimage invoice with amount: 1). POST /preimages now takes an optional price (sats) that is stored per entry and overrides the requested amount when the preimage is consumed; omitting it keeps the old any-amount behavior. Also gate the stored-preimage reveal in credit() on the invoice being fully paid, since internal sends can settle partial amounts. Bare-hex entries queued before this change still parse (treated as priceless). Co-Authored-By: Claude Fable 5 <[email protected]>
* Merge remote-tracking branch 'origin/master'Adam Soltys2026-07-111-0/+76
|\ | | | | | | | | # Conflicts: # lib/payments.ts
| * invoices: sell merchant-supplied preimages via a usePreimage queueAdam Soltys2026-07-101-0/+76
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Merchants POST /preimages (auth) to pre-load 32-byte hex preimages into a FIFO queue; creating an invoice with usePreimage: true consumes the next one so the invoice's payment hash is sha256(preimage). Paying the invoice reveals the merchant-chosen secret to the buyer — via their own wallet on external settlements, and via GET /invoice/:id once settled — enabling backend-less digital download stores (encrypted files on static hosting, preimage + public string = decryption key). - GET /preimages lists the queue with payment hashes so the merchant can map files to future invoices; POST /preimages/delete clears it - empty queue fails invoice creation rather than issuing a random preimage the buyer would pay for but couldn't decrypt with - preimages cln rejects as already used are discarded and the next tried - consumed preimages persist at preimage:${payment_hash} (TTL = invoice expiry) and credit() prefers them as the settlement ref, so internal coinos-to-coinos payments reveal the real preimage instead of the sender's uid Co-Authored-By: Claude Fable 5 <[email protected]>
* | payments: use the debited tip when crediting internal transfersAdam Soltys2026-07-102-4/+15
|/ | | | | | | | | | | | | | | | credit() now credits the tip the sender was actually debited (passed in by the internal-transfer callers) instead of independently re-reading the invoice. This closes a time-of-check/time-of-use gap where an invoice's tip could be changed between the sender debit and the receiver credit, letting a payer be credited a tip they were never charged. External receipts (lightning/ecash) keep reading the invoice tip, since there is no paired coinos debit to diverge from. Also harden POST /invoice/:id: validate the tip is a sane non-negative integer and reject tip changes on an already-settled invoice. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* users: return generic 404 from /reset for non-adminsAdam Soltys2026-06-271-1/+10
| | | | | | | | | The admin-only password-reset endpoint returned "disabled" to non-admin callers, which confirmed both that the endpoint exists and that it is admin-gated. Return a generic 404 identical to an unregistered route instead; still log the rejected attempt server-side for monitoring. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* lnurl: SSRF-guard user-supplied URL fetches + validate zap eventsAdam Soltys2026-06-151-5/+9
| | | | | | | | | | | | | | | | The /lnurl/proxy, /decode and /encode handlers fetched an arbitrary user-supplied URL via got() and returned the body — an unauthenticated SSRF reaching internal services. Route those fetches through a new safeGot() that rejects hosts resolving to loopback/private/link-local/ CGNAT/metadata ranges, re-validates on each redirect hop, restricts the scheme to http(s), and caps timeout + response size. Also validate the NIP-57 zap request (kind 9734 + signature) before storing it, instead of trusting the client-supplied event. These endpoints can't be removed — they back lightning-address/LNURL parsing and resolution in the wallet UI. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* users: add authenticated self-delete endpointAdam Soltys2026-06-131-0/+103
| | | | | | | | | | POST /delete (auth) wipes every key tied to the calling user, mirroring scripts/delall-legacy.ts: user records, payments/invoices, sub-accounts, NWC apps, nostr follow graph + nip5. Gated by typing the username and refuses while balance+pending > 10k sats so funds can't be destroyed by accident. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* nwc: keep notifications opt-in (revert default-on)Adam Soltys2026-06-061-6/+5
| | | | | | | | | | Reverting the notify default-on from 8cb18821 — most users don't consume notifications and defaulting on would flood the relay with payment_sent/ payment_received for ~3k apps. Notifications stay opt-in: clients enable by sending notify:"true" when creating the NWC connection. The pay_invoice reply-reliability and rate-limit fixes from 8cb18821 are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* nwc: fix pay_invoice reply reliability, rate-limit drops, notificationsAdam Soltys2026-06-061-1/+6
| | | | | | | | | | | | | | | | | | | | | | | | | Three issues reported by an NWC client integrator (refs #80): (a) pay_invoice sync reply: the handler polled listpays for ~20s then returned a misleading "Payment timed out" error on slow routes even though the payment settled — client never got the preimage. sendLightning already runs xpay to completion and finalize() sets p.ref = preimage, so return that directly; keep a longer listpays loop only as a fallback, and on still-in-flight return an explicit "check with lookup_invoice" rather than implying failure. (b) rate limiting: raised per-pubkey cap 5/min -> 60/min (normal usage — a balance check + payment + a few lookup_invoice reconciles — trivially exceeded 5), and over-limit requests now get a NIP-47 RATE_LIMITED error reply instead of a silent drop that looked exactly like a lost reply. Also: every request path now always sends a 23195 reply (success, error, or handler-returned- nothing) so a client never hangs waiting. (c) notifications: payment_received/payment_sent are advertised in the info event but app.notify defaulted to false unless the client sent notify:"true" (only ~1% of apps had it on), so they never fired. Default notify ON; only disable when explicitly "false". (Existing apps still false — backfill TBD.) Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* mirror payment records to arc + atomic /confirm lock to prevent double-creditsAdam Soltys2026-05-171-3/+27
| | | | | | | | | | | | | | | | | | | | | | | | Follows up the Apr 29 double-credit incident. Two parallel callers of /confirm (walletnotify, catchUp, bulk sweeps) could each read p.confirmed=false and each run the multi() that increments balance — because main-db pointers were sometimes missing while arc still had them, gf()'s arc fallback returned stale state. Three pieces here: - routes/payments.ts: SET NX EX 60 lock around the /confirm credit block keyed by `confirmlock:${txid}:${vout}`. Concurrent callers short-circuit cleanly. The lock is released on success and on every early-return. - lib/payments.ts credit() + routes/payments.ts /confirm: mirror the payment/invoice records and the `payment:<txid>:<vout>` / `payment:<hash>` pointer keys into arc as soon as they're written. gf() will then find the prior credit via arc fallback even if the main-db copy is wiped by a future data-loss event. - routes/payments.ts hide `revertedDuplicate` payments from the user's payment list — matches the same filter added to lib/nwc.ts list_transactions so reverted records never surface in either rendering path. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* remove /gateway endpointAdam Soltys2026-05-171-9/+0
| | | | | | | | | Unauthenticated arbitrary db.set abuse vector. Exploited to inflate user balances and override safety controls (bitcoin:limit, limit, hardfreeze, freeze) in real time, leading to a hot-wallet drain. Route and handler both fully removed. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* lnurl proxyAdam Soltys2026-03-271-0/+12
|
* don't remove missing paymentsAdam Soltys2026-03-241-2/+1
|
* listener logsAdam Soltys2026-03-181-2/+1
|
* remove logsAdam Soltys2026-03-176-33/+3
|
* adjust ln minfeeregtest-dataAdam Soltys2026-03-151-6/+0
|
* pass bytes for app secretAdam Soltys2026-03-141-1/+2
|
* use bytes for nostr toolsAdam Soltys2026-03-131-0/+1
|
* fix pending payment cleanup and login password verificationAdam Soltys2026-03-061-5/+7
| | | | | | | Skip non-lightning entries in pending payments check and handle malformed password hashes gracefully during login. Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix login/reset glitchesAdam Soltys2026-03-031-7/+18
|
* fix lnurl description_hash mismatchAdam Soltys2026-02-251-2/+6
| | | | Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix addmanagersAdam Soltys2026-02-101-5/+4
|
* filter null managersAdam Soltys2026-02-101-2/+2
|