| 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
|
| |
|
|
|
|
|
|
| |
/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]>
|
| |
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
| |
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]>
|