<feed xmlns='http://www.w3.org/2005/Atom'>
<title>coinos-server.git/index.ts, branch master</title>
<subtitle>coinos server — legacy coinos.io backend (Lightning, on-chain, Liquid)</subtitle>
<id>https://git.coinos.io/coinos-server.git/atom?h=master</id>
<link rel='self' href='https://git.coinos.io/coinos-server.git/atom?h=master'/>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/'/>
<updated>2026-10-13T23:23:37Z</updated>
<entry>
<title>users: let a password unlock the encrypted nostr key for any session</title>
<updated>2026-10-13T23:23:37Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-10-13T23:23:37Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=12349761c03e769e1d54e878746a10d0a464b38d'/>
<id>urn:sha1:12349761c03e769e1d54e878746a10d0a464b38d</id>
<content type='text'>
/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 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01UjHzoBmW1iPpEe5qC2LDYB
</content>
</entry>
<entry>
<title>ecash: drop anonymous token storage in favour of a stateless status lookup</title>
<updated>2026-09-27T21:48:24Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-27T21:48:24Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=b1e175c1faa46ed67cf0575f8d8808f1ae10892c'/>
<id>urn:sha1:b1e175c1faa46ed67cf0575f8d8808f1ae10892c</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>lightning: quote the routing fee up front and add a max-send quote endpoint</title>
<updated>2026-09-22T02:22:03Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-22T02:21:29Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=5929817e0436979821980abf2f5f4b8a187e8654'/>
<id>urn:sha1:5929817e0436979821980abf2f5f4b8a187e8654</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>ecash: redeem tokens by melting to the user's own invoice</title>
<updated>2026-09-16T11:53:22Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-16T11:53:22Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=c72fa2a5ccf1cc04073f9ff12606491971dadd36'/>
<id>urn:sha1:c72fa2a5ccf1cc04073f9ff12606491971dadd36</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>security: drop the admin endpoint surface and land pending hardening</title>
<updated>2026-09-14T03:44:48Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-09-14T03:44:48Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=d8a361bdcfaa8cc5b39d9675e74b0e1d2e42be2c'/>
<id>urn:sha1:d8a361bdcfaa8cc5b39d9675e74b0e1d2e42be2c</id>
<content type='text'>
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) &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_0112TGi9GGNWqji1XQ5jZRzc
</content>
</entry>
<entry>
<title>fund: authenticated recovery of a rotated fund's old link</title>
<updated>2026-08-28T18:11:32Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-08-28T18:11:32Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=a7fc7959eeb58fa7230c7e0869012f03ad2b23b7'/>
<id>urn:sha1:a7fc7959eeb58fa7230c7e0869012f03ad2b23b7</id>
<content type='text'>
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:&lt;oldid&gt;` -&gt; 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-&gt;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 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01DKkev76U4YRnRAqM6jqbae
</content>
</entry>
<entry>
<title>harden: remove debug /echo endpoint; widen LN health probe timeout</title>
<updated>2026-08-27T16:01:45Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-08-27T16:01:45Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=01636561aa58b031ac01bdc9a5779ace35040389'/>
<id>urn:sha1:01636561aa58b031ac01bdc9a5779ace35040389</id>
<content type='text'>
Remove the unauthenticated POST /echo route — a request-body reflector with
no place in production; it served only to echo attacker probe payloads.

Raise the health-check listener-backlog probe timeout from 5s to 15s. The
waitanyinvoice probe normally returns in ~1s but can intermittently take ~8s
while lightningd is briefly busy; the 5s cap turned those transient spikes
into health-check failures (and, at 3 consecutive, an unnecessary process
exit / container restart). 15s absorbs the busy spikes while a genuinely
wedged RPC still fails the check.

Co-Authored-By: Claude Opus 4.8 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01DKkev76U4YRnRAqM6jqbae
</content>
</entry>
<entry>
<title>ln: bound receive-listener wait, phase-aware watchdog, re-enable health check</title>
<updated>2026-08-22T15:08:13Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-08-22T15:08:13Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=658724acea184ba4f15ddc70fe9f1cf8946ff51d'/>
<id>urn:sha1:658724acea184ba4f15ddc70fe9f1cf8946ff51d</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01XiwgmQKwKnRxUmVDd4xSf7
</content>
</entry>
<entry>
<title>upload: require auth on /upload/:type; bump sharp past libvips CVEs (S7)</title>
<updated>2026-08-08T06:29:25Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-08-08T05:59:45Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=582eab0d3e78d3c92b3fbf2d41d1845ca5761387'/>
<id>urn:sha1:582eab0d3e78d3c92b3fbf2d41d1845ca5761387</id>
<content type='text'>
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 -&gt; 0.35.3 (out of the
vulnerable &lt; 0.35.0 libvips range, GHSA-f88m-g3jw-g9cj et al.).

Co-Authored-By: Claude Opus 4.8 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>bolt12 zaps: pay offers, NWC pay/receive, per-user offers, NIP-44</title>
<updated>2026-07-30T15:51:36Z</updated>
<author>
<name>Adam Soltys</name>
<email>adam@coinos.io</email>
</author>
<published>2026-07-30T15:51:36Z</published>
<link rel='alternate' type='text/html' href='https://git.coinos.io/coinos-server.git/commit/?id=add3ad93ff37f1daaedab5788f00663be299555c'/>
<id>urn:sha1:add3ad93ff37f1daaedab5788f00663be299555c</id>
<content type='text'>
- 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 &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
