| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
| |
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 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() 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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
- 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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
- 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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |\
| |
| |
| |
| | |
# Conflicts:
# lib/payments.ts
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
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]>
|
| |/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
| |
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]>
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| | |
|
| |
|
|
|
|
|
| |
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]>
|
| | |
|
| |
|
|
| |
Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
| | |
|
| | |
|