| Commit message (Collapse) | Author | Age | Files | Lines |
| ... | |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
| |
Incident-response scripts and the victim list carry account identifiers and
must never reach the public repo; peerswap/ holds runtime panic logs. Ignore
them so they stay local-only and can't be committed by accident.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XiwgmQKwKnRxUmVDd4xSf7
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
audit-lightning-backlog.ts walks settled invoices from the stored pay_index to
surface receives the listener may have missed crediting (companion to the
listener backlog probe). merge-kvrocks-archives.ts overlays one Kvrocks/Redis
archive onto a base without materializing keys in memory (raw SCAN to survive
cursors beyond JS safe-integer range), with kvrocks-merge-lowmem.conf as the
low-memory server config for the merge. All parameterized via CLI flags; no
embedded hosts or secrets.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XiwgmQKwKnRxUmVDd4xSf7
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Fastify 4.x is EOL with no backported fixes; all six open alerts
(critical http-proxy header stripping, static path traversal + auth
bypass, fastify body-validation bypass / host spoofing / DoS) are only
patched on the v5 line. Bump fastify to 5.12.0 and all @fastify/*
plugins to their v5-compatible majors, and migrate the two options v5
deprecated (disableRequestLogging -> logController, maxParamLength ->
routerOptions).
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
| |
Extend the request-log redaction set beyond password/confirm so NWC connection
secrets and TOTP seeds never reach the req/res logs.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XiwgmQKwKnRxUmVDd4xSf7
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Give waitanyinvoice an explicit 30s server-side timeout so a dead/half-open
socket can no longer hold the listener indefinitely; CLN's 904 timeout during a
quiet period is treated as a clean re-arm without counting as a failure. Split
the listener into waiting vs processing phases so the watchdog never resets a
healthy socket merely because a ledger write is slow, and never starts a second
concurrent processor — a stalled processing phase is surfaced loudly instead.
Process the settlement and confirm the durable payment pointer BEFORE advancing
pay_index, so a store load/restart between the cursor write and credit() cannot
drop a received payment. Expose listener status and add a backlog probe to the
health check, and re-enable startHealthCheck().
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XiwgmQKwKnRxUmVDd4xSf7
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
| |
A NIP-47 request whose pubkey/user no longer exists (deleted app or migrated
user) was silently dropped, leaving the client spinning. Send an UNAUTHORIZED
error reply so the client can surface it. Still not logged (not a server fault).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
/v1/ws proxy
Forward cf-connecting-ip (or first x-forwarded-for) upstream so the mints' per-IP
rate limiter buckets by the real client instead of demux's socket IP. Partner
hosts that funnel many users through one IP can be listed in whitelist.json with
a pool size; their bucket key rotates across pool synthetic keys for a
proportionally larger allowance. Reloaded every 60s. Also proxy /v1/ws
(subscriptions) through to the new mint.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
Register patches/@asoltys+clightning-client+0.1.10.patch via patchedDependencies
so bun install re-applies it. The library's reconnect() reused an already-dead
socket and never recovered after a cl restart (looping connect ENOENT, leaking a
socket+timer); the patch rebuilds a fresh socket + readline each reconnection and
clears its retry guard before reconnecting. Companion to the app-side
dropClient()/listener-recovery fixes.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Replace bare `client = null` with dropClient(): stop the clightning-client
library's internal reconnect loop (clear reconnectTimeout, neuter reconnect,
destroy the socket, drop listeners). A dead client was otherwise orphaned and
spun forever retrying `connect ENOENT`, spamming logs and leaking a socket+timer
on every cl restart. Guard the error handler on `c === client` so a stale
client's late error can't drop a newer, healthy one.
Add lnListen.onDrop so the invoice listener recycles immediately when its
long-poll socket dies (bump epoch, re-arm) instead of waiting up to 5 minutes for
the stall watchdog. Stop counting LightningUnavailableError toward the listener's
process.exit escalation, so a cl restart no longer makes the app self-exit — it
retries until cl returns.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The pending reconciliation set is keyed by bolt11, shared across attempts
at the same invoice. A retry made after a prior attempt's CLN parts
resolved (but before check() reconciled its debit) would pass the
listpays-only guard, and the retry's own reverse()/finalize() then
removed the shared entry — permanently hiding the first attempt's debit
from check(), so a failed send was never auto-refunded.
- sendLightning: refuse to pay while the invoice's bolt11 is still in
the pending set; the caller is told to retry shortly.
- check(): an entry whose payment record no longer exists in db or
archive is dropped from the set instead of skipped, so a dead entry
can't block its invoice forever under the new guard.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Melts routed to the retired mint were failing before their proofs were
even read: wallets blind their fee-return change outputs to the current
active keyset, which the old mint doesn't know and rejects outright.
Strip change outputs on that path (recovery melts forfeit change).
Also map the caller's melt quote id to the old mint's re-quoted id so
melt responses and GET status polls report against the id the wallet
knows, and log the connecting IP on the recovery path.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
16 tests covering S1 (jwt.verify vs forged tokens), S2 (bcrypt cost 12 +
legacy detection), S3 (adminpass fails closed on omitted/empty password),
S5 (session cookie flags), S6 (atomic SET NX dedup), S7 (/upload auth),
fnd-003/004 (memo truncation), fnd-006 (deleteSelf sub-account guard),
and S8/fnd-007 (spend/cash mutex serialization). Black-box HTTP + redis
against the live regtest stack; run with `bun run test:regtest`.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
| |
Regenerated via `bun install` inside the app container (the bind-mounted
node_modules is root-owned, so host-side bun install hits EEXIST). Pins
sharp at the CVE-fixed 0.35.3 in the lockfile so a frozen-lockfile build
resolves the fixed tree.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The house Cashu holdings live as one token under redis `cash`. claim()
and mint() decoded the current proofs, swapped at the mint, then wrote
back — with no lock. Two concurrent claims read the same proofs and the
later write clobbered the earlier, so already-swapped proofs vanished
from the token while both users stayed credited (an insolvency leak,
triggerable via the unauthenticated /ecash/:id callback). Wrap both
read-swap-write sequences in an in-process mutex.
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
- pay_keysend never called checkBudget and never recorded to
${pubkey}:payments, so a budget-limited connection string was a
full-drain credential. Budget-check it, honor max_fee, and record the
debit so subsequent budgeted calls count it (S4).
- Replace the non-atomic zScore-then-(unawaited)-zAdd dedup with an
awaited SET NX claim, so two concurrent deliveries of the same signed
event can't both reach handle() and pay twice; also reject
future-dated created_at beyond a small skew (S6).
- Serialize budget-affecting methods per app with an async mutex so a
concurrent request can't read a stale spent-total and overspend (S8).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
| |
POST /upload/:type fed the raw uploaded image straight into sharp with
no auth preValidation (unlike neighboring routes), reaching a native
libvips decoder in the container that holds the JWT secret and CLN hot
wallet. Require auth, and bump sharp 0.33.5 -> 0.35.3 (out of the
vulnerable < 0.35.0 libvips range, GHSA-f88m-g3jw-g9cj et al.).
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
The stored replaceable 13194 info event was intermittently disappearing
from the relay, leaving NWC clients (Alby etc.) with "no info event"
until the next hourly existence-check happened to republish it. Drop the
check-then-publish dance and just republish both server info events every
2 minutes — a replaceable event overwrites in place, so it's cheap and
self-heals immediately. Silence the per-publish success log since it now
fires on every interval.
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Clamp a non-internal receipt's tip to the settled amount so a tip
mutated larger than the amount actually received can't produce a
negative, reversible payment record on the recipient.
Also reject an autowithdraw whose destination resolves back to the
sender: that self-send re-fires completePayment and, since the balance
nets out, never drops below threshold, looping unbounded. The route's
create() already has the equivalent self-send guard; the internal
pay() helper lacked it.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dpqu6wizYe1c1g4k5Gd8JK
|
| |
|
|
|
|
|
|
|
| |
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
sendKeysend debited, ran ln.keysend, and on a throw decided whether to
refund by calling listpays({payment_hash: hash}) with the CALLER's id (a
Nostr ev.id on the NWC path). keysend randomizes its own payment hash —
the CLN command takes no preimage arg — so that lookup could never match:
it returned [], `failed` was always true, and reverse() ran on EVERY
throw. A keysend that threw but actually settled was re-credited while
the sats had left the node, a repeatable hot-wallet drain. Nothing
corrected it either: unlike sendLightning, keysends are never enrolled in
`pending`, so check() never revisits them.
Recovering the real hash from the error payload doesn't work — verified
on CLN v26.06.2 that keysend errors carry no payment_hash under any key
(the error is {code, message, attempts}), so that approach never reverses
and instead eats the user's money on every ordinary routing failure.
Instead tag the payment with our own unique label and record where the
sendpays index stands first (`wait sendpays created 0` returns the
current index without blocking), so a throw can list only sendpays
created since — no full scan — and match ours by label whatever the error
looks like. CLN writes the sendpay record BEFORE sending the HTLC, so no
record at all proves nothing left the node and the debit is safe to
reverse. Verified both directions on regtest: an unroutable keysend is
refunded, and a settled one is correctly detected and left alone.
Known gap: a keysend still in flight when the RPC throws stays debited,
since keysends are not enrolled in `pending` for check() to reconcile.
Reported-by: Muhammed Shekho (@SYR-ROOT) <[email protected]>
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]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Tightens our implementation of the BIP-321-based NWC methods proposed
in https://github.com/nostr-wallet-connect/nwc/pull/2 — the successor
to pay_invoice/make_invoice that clients are asked to transition to
for bolt12 zaps in https://github.com/nostr-protocol/nips/pull/2421:
- reject cross-network payment instructions before paying
(UNSUPPORTED_NETWORK) instead of failing after the debit
- reject unknown req- BIP-321 params, including req-pop since we
can't open proof-of-payment callbacks
- reject a request amount that conflicts with the instruction amount
- payer_note is delivered or rejected, never dropped: bolt11+note is
rejected, internal settlements hand the note over as the payment memo
- receive mints a per-request bolt12 offer carrying the requested
description/amount instead of reusing the standing amountless offer
- get_info reports the node's actual network instead of "mainnet"
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
| |
The old mint's keysets are redeem/melt-only — wallets must never mint or swap
into them. Force active:false in the merged /v1/keysets so mint.coinos.io
advertises a single active sat keyset (the new one) instead of two. The old mint
is panic-locked (mint/swap disabled) regardless; this fixes the client-facing
view a reviewer flagged.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Front the current mint (fresh keys, normal ops) as primary, and transparently
route pre-shutdown ecash melts to the retired panic-mode mint so wallets can melt
old ecash at mint.coinos.io without changing their URL:
- /v1/keysets is the union of both mints' keysets
- /v1/keys/<id> and melt execute route by the keyset owner
- a melt quote is taken on the primary and the invoice remembered, so an
old-keyset melt is re-quoted and executed on the recovery mint (where panic
mode gates blacklisted funds)
- /v1/checkstate is merged across both; CORS added for web wallets
The recovery mint also stands alone at mintrecovery.coinos.io.
|
| |
|
|
|
|
|
|
|
|
|
| |
The zap dispatch parsed the invoice description and, when it wasn't JSON,
suppressed the error only if the message contained "Unexpected" — V8's parse-
error wording. On Bun the message is "Unable to parse JSON string", so every
paid invoice with a non-JSON memo logged a warning. Parse-then-check instead:
a non-JSON description is simply not a zap and is skipped silently; warn only
when an actual kind-9734 zap request fails to process.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
| |
A pubkey can be in `${uid}:apps` before its `app:<pubkey>` record is written
(NWC-connection creation race) or can outlive its record. Destructuring `notify`
off the null lookup threw, and since the catch wraps the whole loop it aborted
notifications for ALL of the user's apps. Skip null/absent apps and continue.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
| |
- meltguard: out-of-band CLNRest spend limiter (per-melt cap + cumulative
hard-freeze) that sits between the panic-mode mint and the node
- mintdemux: fronts the mint hostname, routing between the retired and current
mint instances after the hostname swap
- gitignore *.bak / *.bak-* so local backups (some hold secrets) never commit
Co-Authored-By: Claude Opus 4.8 <[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]>
|
| |
|
|
|
|
|
|
|
|
|
| |
createproof's default proof omits invreq_payer_note, and passing include
replaces the default disclosure set — so for zap payments request both
invreq_payer_note and invoice_amount, which NIP-177 verification requires.
Verified end to end on v26.06.2: NWC pay over nip44 returns a proof that
decodes valid with note + amount disclosed and preimage matching the
payment hash.
Co-Authored-By: Claude Fable 5 <[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]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
apiv2.nobitex.ir became unreachable from our egress, and it was fetched
inline on every binance tick, so it failed and logged on every tick.
Move the Iranian market-rate fetch onto its own 60s timer with a request
timeout, cache the last-good value (falling back to the fixer rate after
an hour), and throttle failure logging. Switch the source to Wallex
(api.wallex.ir BTCTMN), which is reachable.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|