aboutsummaryrefslogtreecommitdiffstats
Commit message (Collapse)AuthorAgeFilesLines
* users: let a password unlock the encrypted nostr key for any sessionHEADmasterAdam Soltys28 hours2-0/+34
| | | | | | | | | | /me only reports hasnsec now, so sessions that didn't start with a password login had no way to reveal or sign with their key. POST /nsec returns it after re-checking the password, under the login per-IP failure limit. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UjHzoBmW1iPpEe5qC2LDYB
* login: drop the unused per-username failure counterAdam Soltys37 hours1-7/+0
| | | | | | | | | The counter was incremented on every failed login and read but never acted on; the per-IP limits do the throttling. Its keys never expired and had grown to most of the keyspace. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UjHzoBmW1iPpEe5qC2LDYB
* bolt12: put the requested expiry and single-use on the offer itselfAdam Soltys6 days1-0/+9
| | | | | | | | The expiry was only stored on our invoice record, so CLN kept issuing invoices for the offer after it had expired. Pass it as absolute_expiry, and accept single_use to cap an offer at one payment. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* users: keep the encrypted nostr key and 2FA seed away from bearer tokensAdam Soltys6 days2-4/+15
| | | | | | | | /me and user emits return a hasnsec flag instead of the encrypted key, which is now only sent with a password login or registration. /otpsecret refuses once 2FA is enabled, and disabling 2FA rotates the seed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* analytics-geo: read rotated request log chunksAdam Soltys6 days1-6/+26
| | | | Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Remove response loggerAdam Soltys6 days1-19/+0
| | | | Co-Authored-By: Claude Opus 5.5 <[email protected]>
* me: stop reporting archived balance as locked for frozen accountsAdam Soltys6 days1-16/+1
| | | | Co-Authored-By: Claude Opus 5.5 <[email protected]>
* lightningd: bump to v26.06.9Adam Soltys8 days1-1/+1
| | | | Co-Authored-By: Claude Opus 5.5 <[email protected]>
* email: escape support form HTML and enforce recaptcha v3 scoreAdam Soltys8 days1-37/+56
| | | | | | | | Escape the message body, stop accepting a client-supplied subject, strip control characters from header values, require the expected action and a minimum score from siteverify, and drop the adminpass captcha bypass. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* payments: lowercase payreq before looking up our own invoicesAdam Soltys8 days2-1/+12
| | | | | | | | | An uppercase bolt11 for a coinos invoice missed the getInvoice lookup, so it was paid over lightning to our own node instead of internally. The listener then saw the sender's payment pointer for that bolt11 and skipped crediting the recipient. Normalize in POST /payments and nwc pay_invoice. Co-Authored-By: Claude Opus 5.5 <[email protected]>
* invoices: a supplied account must belong to the invoice's userAdam Soltys8 days1-1/+6
| | | | | | | | | | generate() took any existing account id as the credit target, so an invoice could carry one user's name while paying into someone else's account. A supplied aid now has to be in the invoice user's own account list; otherwise the invoice credits that user's main account. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01KCxLg8s1g8yBwjRwPN4uJR
* Evicted accounts can't receive or log in; limit errors say what's happeningAdam Soltys10 days3-3/+18
| | | | | | | | | | | An evicted account no longer gets lightning-address invoices (stops a scam page's address from minting new ones) or a login token. A send over the server limit now says withdrawals are temporarily limited instead of a generic "Problem sending payment" that read as a frozen account. (Already live since the 2026-10-01 restart.) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
* v3 bridge: a migrated name's legacy receives are paid on to its v3 walletAdam Soltys10 days2-2/+147
| | | | | | | | | | | | | | | | | | A user who moved their name to coinos v3 keeps their legacy account, and old BOLT12 offers, bolt11 invoices and bitcoin/liquid deposit addresses keep paying into it. Each confirmed receive of those kinds is now forwarded as it lands to the name's v3 lightning address (registrar invoice on the ASP node; the v3 forwarder delivers to the Ark mailbox even with the wallet closed). Only new receives move; existing balances stay. Routing cost only: the platform fee is waived (debit exempt), and the direct channels to the ASP node quote 0. Proof required: the registrar holds the name for the legacy account's own key, or for the wallet its session named in a migration grant (/name granted flag). Autowithdraw stands down for forwarded receives; failures retry with backoff (v3fwd:retry, one process at a time) and otherwise stay put. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
* payments: refuse on-chain sends to blocked addresses before debitingAdam Soltys2026-09-291-0/+19
| | | | | | | | | debit() checks blocked_addresses against the payment hash, which for an on-chain send is the txid, so a blocked destination address was never matched. Check each output address of the built transaction in sendOnchain and fail before any debit. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* lnurl: budget unauthenticated invoice generation per recipientAdam Soltys2026-09-291-5/+34
| | | | | | | | | | | | | GET /pay/:user/:amount and the lnurl callback create a CLN invoice and an entry in the recipient's invoice list for anyone holding the link, which a scanner on rotating IPs can do without tripping per-IP limits. Cap fresh invoices at 120 per recipient per hour (reused invoices don't count). Also fail cleanly for unknown usernames and skip invoice ids whose records no longer exist, instead of throwing on the null and breaking the pay page. Throttle the resulting lnurlp warnings per username. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* logging: throttle log lines that clients can trigger at willAdam Soltys2026-09-294-11/+57
| | | | | | | | | | | | Evicted-JWT replays, blocked sends from frozen or limited accounts and failed payment retries each logged one line per request, so a looping client could flood the log, and an evicted token replayed across a proxy pool made atk-autoban.sh block every IP it saw. Add quiet()/suppressed() in lib/logging.ts: the first occurrence per key logs as before, repeats within the window are counted and reported on the next line that gets through. The evicted-auth summary line is worded so the auto-banner ignores it. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* invoices: don't let a supplied id overwrite another user's invoiceAdam Soltys2026-09-292-0/+21
| | | | | | | | | | | | generate() writes invoice:<id> wholesale with the caller's uid, and the old bolt11 pointer keeps resolving to that id, so passing someone else's invoice id re-pointed their invoice at the caller's account. Invoice ids aren't secret (LUD-21 verify URLs, invoice pages, receipts), so refuse an existing id that belongs to a different user, both in generate() and in POST /invoice for anonymous pay-page callers. Both fail with "invoice not found" so ids can't be probed for existence. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* ecash: drop anonymous token storage in favour of a stateless status lookupAdam Soltys2026-09-272-7/+9
| | | | | | | | | POST /cash stored any submitted string under a fresh id with no auth, no validation and no expiry. Replace it with POST /ecash/status, which checks a token the client already holds and returns its total, spent amount and mint. GET /cash/:id/:version stays so previously shared ids keep resolving. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* upload: decode images in a throwaway child process and tighten input checksAdam Soltys2026-09-263-11/+127
| | | | | | | | | | | | | | | Malformed HEIF uploads were corrupting the heap inside libvips/libheif and taking the whole app down with them, since sharp ran in-process. Move the decode into a per-upload child (lib/image-worker.ts, spawned by lib/image.ts with a hard timeout) so a decoder crash fails one request instead of the server, and never load sharp in the main process. Also: only accept jpeg/png/webp/gif as sniffed by file-type instead of any image/* subtype, decode with failOnError and a pixel cap, refuse uploads from blacklisted accounts (a freeze previously only blocked sends), and rate-limit uploads per account. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* app: don't trust client IP headers on onion requestsAdam Soltys2026-09-251-0/+30
| | | | | | | | The tor container connects directly, bypassing Cloudflare, so the client could set cf-connecting-ip, x-forwarded-for and rate-limit-by to anything. Pin those requests to the tor container's address before the rate limiter runs. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* safe-fetch: decode IPv6 hosts before checking for private addressesAdam Soltys2026-09-251-10/+46
| | | | | | | | | URL parsing serializes IPv6 hosts in hex, so an IPv4-mapped address never matched the dotted-quad pattern and passed as public. Expand the address and judge every form that embeds an IPv4 address by that address, and block the remaining non-public IPv6 ranges. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* lightning: don't wedge the listener on an already-credited preimageAdam Soltys2026-09-232-2/+15
| | | | | | | | Skip settlements whose preimage was already credited instead of retrying until the process exits, and reject previously used preimages when they are queued. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* lightningd: upgrade Core Lightning to v26.06.8Adam Soltys2026-09-221-1/+1
| | | | | | | | Point release with security fixes reported through responsible disclosure; upstream withheld the CVE details and some tests, and recommends upgrading as soon as practical. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
* parse: cache node aliases as a small map with a working timestampAdam Soltys2026-09-211-9/+13
| | | | | | | | | | The alias cache stored the full listnodes output as an array and hung its `last` stamp on that array, which JSON.stringify drops, and the refresh condition was inverted besides. Net effect: every lightning send page view refetched all ~16k nodes and rewrote ~10MB to the db. Keep only the id→alias map with a real timestamp and refresh it hourly. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* lightning: log quote outcomesAdam Soltys2026-09-211-0/+2
| | | | | | | | The quote endpoint only logged failures, so a "send everything" that went wrong somewhere after the quote left nothing to correlate against. Log the solved amount and fees for max quotes and the fee for plain ones. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* funds: close deposits, withdrawals and authorizationsAdam Soltys2026-09-211-17/+16
| | | | | | | | | | | | | | | | The fund mechanism is retired. Deposits (POST /payments with `fund`), withdrawals (POST /take) and pre-authorizations (POST /authorize) now refuse unconditionally instead of consulting the `fund:disabled` flag, so the door cannot be reopened by clearing a key. The deposit path never had a kill-switch of its own, so with the flag set users could still pay into funds they were unable to withdraw from. Read paths are left intact: GET /fund/:id and the manager endpoints still serve existing funds so balances stay visible and can be settled out of band. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
* users: reserve service names on registration and renameAdam Soltys2026-09-213-4/+35
| | | | | | | | | | | | A username is also a lightning address, so a name that reads as the service collects payments meant for it. "mint" was registered days after the ecash redesign freed it and received payments addressed to [email protected]; the reserved list held only "ecash", and was duplicated in two places. One shared list now, checked on both paths, covering the service-sounding names an account should never be able to take. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
* mintdemux: melt the new half of a mixed selection instead of refusingAdam Soltys2026-09-211-7/+54
| | | | | | | | | | | | | | | | | A wallet holding leftovers of both eras selects proofs from each, and the two mints cannot co-sign one melt, so such wallets were stuck for good behind the "melt them separately" error. Drop the old inputs and melt the new ones. The old proofs stay unspent at the old mint while the wallet treats them as spent, so each one is recorded with its keyset, amount, secret hash and whether it carried DLEQ; they were only redeemable through the recovery path anyway. Also stop the routing tables from going empty. The hourly refresh is exempt from the mints' per-ip limiter, so it now seeds the last-good keyset lists the client-facing /v1/keysets falls back on, and a start that races the mints coming up retries every 15s rather than waiting out the hour -- an empty old list makes every old-ecash melt look new and routes it to the wrong mint. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
* ecash: identify our own tokens by keyset, not by the mint urlAdam Soltys2026-09-211-20/+28
| | | | | | | | | | | | | | The mint url in a cashu token is a label the sending wallet writes in, and tokens of ours turn up carrying stray ports and paths. Fetching it to compare pubkeys therefore rejected perfectly good tokens, and meant every /cash entry point made an outbound request to a user-supplied address. Keyset ids are derived from the mint's public keys, so a token whose proofs are all on our keysets is ours whatever url it names; a foreign one still fails, and now with a clear message instead of cashu-ts's short-id mapping error. A miss refreshes the cached keyset list once, in case of a rotation. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
* docker: build nutshell on the coinos base imageAdam Soltys2026-09-211-0/+57
| | | | | | | | | | | | Multi-stage build: poetry installs the mint into a python:3.12-slim-trixie stage, then /usr/local and /app are copied onto ghcr.io/coinos/base, the same way the lightningd image is assembled. Source is cloned from the coinos nutshell fork at NUTSHELL_REF, and the image runs `mint` directly under tini instead of `poetry run mint`. Python 3.12 rather than trixie's 3.13 because coincurve ships no cp313 wheel. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* lightning: quote the routing fee up front and add a max-send quote endpointAdam Soltys2026-09-214-8/+482
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Users trying to empty their balance over lightning had to guess a maxfee that covered both an unknown routing fee and the platform fee, and the 0.5% floor we imposed on that guess made an exact "send everything" impossible. xpay routes with askrene's getroutes, so ask askrene the same question before we debit and the fee is known exactly. lib/lnquote.ts mirrors what xpay does in plugins/xpay/xpay.c (v26.06): a throwaway private layer carrying the invoice's route hints (bolt11) or blinded paths via xpay's fake destination node (bolt12), then getroutes over the same layer stack in the same order. The fee is the sum of what leaves our node minus what the destination receives, rounded up to a whole sat since that is what xpay's maxfee reserves. POST /lightning/quote returns the routing fee, the platform fee exactly as debit() charges it (free-tier credit, ln:nofree rate, none on sub-accounts) and the total. With `max` it iterates quote / subtract fees / requote so amount + both fees lands exactly on the balance; an offer gets a probe invoice then the real one for the solved amount, and an LNURL caller can pass `ceiling` and a probe invoice the same way. sendLightning now quotes for every send. With no explicit fee the quote becomes the maxfee; an explicit fee below the quote is rejected before any debit instead of failing inside xpay after one; the old 0.5% floor on explicit fees is gone. If the quote itself fails we fall back to the heuristic and let xpay try. finalize rounds the settled fee up to the whole sat that was quoted and reserved; rounding a 301 msat fee to 0 refunded a sat that the quote had already accounted for and stranded it in the account. The prefer-kappa layer is only passed to xpay when it exists on the node; getroutes rejects unknown layers outright, which broke every regtest send. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* lnurl: expire payRequest callback ids after seven daysAdam Soltys2026-09-162-5/+10
| | | | | | | | | A callback id is read once, minutes after the metadata request, and never again, but the pointer keys were written without an expiry and had grown to roughly 45% of all keys in the main database. Let the set helper take a TTL and apply one to both lnurl pointer keys. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* mintdemux: forward the client's rate-limit key on keyset fetchesAdam Soltys2026-09-161-4/+25
| | | | | | | | | | Keyset fetches counted against the demux's own socket IP at both mints, so the shared per-IP bucket tripped under load and wallets received an empty keyset list. Forward the client's key, serve each mint's last-good list when its upstream fails, and mark the demux's own hourly refresh as loopback so the mints' limiter exempts it. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* ecash: redeem tokens by melting to the user's own invoiceAdam Soltys2026-09-167-291/+170
| | | | | | | | | | | Redeeming ecash is now a plain NUT-05 melt into a Lightning invoice owned by the redeeming user, credited by the regular lightning listener. Drop the house proof pool and its lock, the /mint and /melt routes, and the special-casing of a mint account in NWC and payment routing. Tokens are decoded against the mint's keyset list so short v1 ids resolve, and an empty keyset list is never cached. Co-Authored-By: Claude Fable 5.1 <[email protected]>
* printer: publish receipts at QoS 1 and clean LNURL metadata memosAdam Soltys2026-09-142-1/+19
| | | | | | | | | | | | 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-142-25/+67
| | | | | | | | | | | | | | | 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-1318-100/+280
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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
* ln listener: skip a settlement whose account is gone before creditingAdam Soltys2026-09-051-0/+9
| | | | | | | | | | credit() now returns without writing for a missing user, but the listener's persistence check after credit() then threw and kept the cursor stuck. Check the account up front and skip like an unknown invoice, recording the preimage in `missing`. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMi2Y1oKPZ3TeeFmLej47y
* credit: skip settlements for a deleted account instead of throwingAdam Soltys2026-09-051-0/+10
| | | | | | | | | | | | getUser(inv.uid) returns nothing when the account was deleted but its invoice or bolt12 offer still receives (OCEAN payouts to a deleted user's offer). The destructure on the next line threw inside the lightning listener, which never advanced pay_index and crash-looped the app every 50s, blocking all incoming lightning for two hours. Record the preimage in `missing` and return, as an unknown invoice already does. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMi2Y1oKPZ3TeeFmLej47y
* 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
* sockets: accept read-only tokens on login, deliver payment events onlyAdam Soltys2026-09-041-1/+12
| | | | | | | | | | | | POS terminals log into the websocket with the "<uid>-ro" token that the web flasher pre-fills. track() looked that id up as a user, failed, and closed the socket a second later, dropping the invoice subscription, so devices were listening about one second in six. Strip the suffix the way lib/auth.ts does for HTTP, flag the socket read-only, and forward only "payment" events to it. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_014gVYGi29FjTeLdNTiutkZx
* meltguard: guard /v1/xpay and parse form-encoded melt bodiesAdam Soltys2026-09-041-8/+38
| | | | | | | | | | | | nutshell >= 0.20.3.1 melts via POST /v1/xpay (form field invstring), while older CLNRest wallets post /v1/pay form-encoded under `invoice` or `bolt11`. The guard only parsed JSON `bolt11`, so form bodies never yielded an amount and were rejected, and an xpay backend would have bypassed the caps entirely. Accept both endpoints and all three field names, forward as form on the same endpoint preserving fee params. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_014uxZLvYS4gH9UqUX2Ey8TV
* payments: finalize settled keysends in sendKeysendAdam Soltys2026-09-041-1/+54
| | | | | | | | | | | | | | keysend blocks until it settles, so a successful return already carries the preimage. Previously only the caller was expected to confirm the record, and the NWC pay_keysend path never did, leaving successful keysends stuck pending forever (check() can't rescue them since the label isn't a bolt11). Add finalizeKeysend(): flip pending->confirmed, derive the real fee from amount_sent_msat, refund the unused fee reserve, and refuse to re-create a record a concurrent reverse() already removed. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_014uxZLvYS4gH9UqUX2Ey8TV
* 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-282-5/+30
| | | | | | | | | | | | | | | 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
* scripts: stubborn peer-force LN payer + boltz stuck-swap recoveryAdam Soltys2026-08-272-0/+250
| | | | | | | | | | | | | | | | | | | | | Add scripts/pay-hard.ts: a payment helper for well-connected but inbound-scarce destinations (exchange nodes) that cost-first payers (xpay/renepay) can't reach. When the normal xpay attempt fails, it enumerates the destination's peers, keeps the ones we can reach (preferring peers we already channel with enough outbound), and for each builds a route forced through peer->dest with the invoice's correct final CLTV, probes it with an unclaimable random hash, and pays through the first path that reaches the destination. Probe-before-send, refuses to start with a payment already pending, and stops rather than risk a second in-flight payment. Works against either the main node (cl) or the boltz node (bcln). Add scripts/boltz-recover-stuck.ts: a cron watcher that finds submarine swaps stuck at invoice.pending (lockup confirmed, invoice unexpired, idle past a threshold) and runs pay-hard against bcln to complete them, then lets boltz reconcile (restarting it only if it hasn't picked up the preimage within a grace period). Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01DKkev76U4YRnRAqM6jqbae
* health: tolerate a transient receive-listener lag, fail only if it's stuckAdam Soltys2026-08-271-4/+15
| | | | | | | | | | | | | | | | The listener-backlog probe fired a failure whenever a paid invoice existed beyond the durable cursor — but that is almost always the listener mid- processing that very payment, a normal sub-second lag, not a wedged listener. Those transient catches produced spurious "listener backlog" health failures that then recovered on their own. Grace-check instead: on detecting a paid invoice beyond the cursor, wait briefly and re-read the cursor; if it advanced, the listener is alive and this was transient (pass). Only fail if the cursor did not move at all — the actual "stuck listener" condition the probe exists to catch. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01DKkev76U4YRnRAqM6jqbae
* lnurl: pin SSRF-guard fetches to the validated IP (close DNS-rebinding)Adam Soltys2026-08-271-51/+92
| | | | | | | | | | | | | | | | | | | | | | | | | | The prior guard resolved the target host, checked the IP against the private/loopback/link-local/CGNAT/metadata blocklist, then let got fetch the URL — which re-resolves DNS independently when it connects, leaving a rebinding window where a low-TTL host can pass the check as public and connect as internal. Reimplement safeGot on node http/https with a custom lookup that validates and returns the exact IP the socket then connects to, so the validation and the connection target are one and the same (no resolve/connect gap). Redirects are followed manually and every hop is re-validated the same way; IP-literal hosts (which bypass the lookup hook) are checked up front; the 2MB cap is enforced on the streamed body rather than a spoofable content-length header. Scheme stays restricted to http(s) with an 8s timeout. safeGot's contract is unchanged, so the three lnurl callers are untouched. Verified in-container: every internal target (loopback, docker-internal, 169.254 metadata, CGNAT, ULA, IP-literal, octal-literal) is blocked by the guard, a server-side redirect into the metadata endpoint is blocked at the hop, and legit external resolution (including http->https redirects) still works with correct TLS against the real hostname over the pinned IP. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01DKkev76U4YRnRAqM6jqbae
* harden: remove debug /echo endpoint; widen LN health probe timeoutAdam Soltys2026-08-272-6/+1
| | | | | | | | | | | | | | | Remove the unauthenticated POST /echo route — a request-body reflector with no place in production; it served only to echo attacker probe payloads. Raise the health-check listener-backlog probe timeout from 5s to 15s. The waitanyinvoice probe normally returns in ~1s but can intermittently take ~8s while lightningd is briefly busy; the 5s cap turned those transient spikes into health-check failures (and, at 3 consecutive, an unnecessary process exit / container restart). 15s absorbs the busy spikes while a genuinely wedged RPC still fails the check. 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