| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
| |
/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
|
| |
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
| |
/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]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |
|
|
| |
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
| |
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|