aboutsummaryrefslogtreecommitdiffstats
path: root/routes/users.ts
Commit message (Collapse)AuthorAgeFilesLines
* users: let a password unlock the encrypted nostr key for any sessionHEADmasterAdam Soltys30 hours1-0/+33
| | | | | | | | | | /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
* login: drop the unused per-username failure counterAdam Soltys39 hours1-7/+0
| | | | | | | | | 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
* users: keep the encrypted nostr key and 2FA seed away from bearer tokensAdam Soltys6 days1-3/+14
| | | | | | | | /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]>
* me: stop reporting archived balance as locked for frozen accountsAdam Soltys6 days1-16/+1
| | | | Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Evicted accounts can't receive or log in; limit errors say what's happeningAdam Soltys10 days1-0/+5
| | | | | | | | | | | 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
* users: reserve service names on registration and renameAdam Soltys2026-09-211-2/+2
| | | | | | | | | | | | 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]>
* security: drop the admin endpoint surface and land pending hardeningAdam Soltys2026-09-131-59/+57
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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
* flash: build each config image in its own temp dirAdam Soltys2026-09-041-6/+16
| | | | | | | | | | 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
* flash: create printer dir and return errors instead of unhandled 500Adam Soltys2026-09-041-7/+13
| | | | | | | | | | 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
* login: fail-open recaptcha verify on infrastructure errorsAdam Soltys2026-08-251-2/+17
| | | | | | | | | | | | | | | | | | 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
* nwc: 404 missing connections + require key rotation to revive retired onesAdam Soltys2026-08-221-2/+31
| | | | | | | | | | | | | | | | | | | | | | | 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]>
* nwc: enforce per-connection spend budgets + flag-gated incident containmentAdam Soltys2026-08-221-2/+30
| | | | | | | | | | | | | | | | | | | | | | | | | 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
* users: sum sub-account balances in the deleteSelf guard (S12)Adam Soltys2026-08-071-4/+13
| | | | | | | | | | 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]>
* logging: redact auth headers; set cookie flags on session token (S5)Adam Soltys2026-08-071-2/+14
| | | | | | | | | | | 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]>
* auth: fail closed on adminpass, scrub sample secrets (S3)Adam Soltys2026-08-071-2/+7
| | | | | | | | | | | | | | | | 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]>
* auth: hash passwords at bcrypt cost 12 with transparent upgrade (S2)Adam Soltys2026-08-071-2/+17
| | | | | | | | | | 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]>
* sockets: verify JWT signatures on the websocket auth path (S1)Adam Soltys2026-08-071-4/+4
| | | | | | | | | | | | | | 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]>
* Let a migrated account hand its address to coinos v3Adam Soltys2026-08-031-0/+1
| | | | | | | | | | | | 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
* users: return generic 404 from /reset for non-adminsAdam Soltys2026-06-271-1/+10
| | | | | | | | | 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]>
* users: add authenticated self-delete endpointAdam Soltys2026-06-131-0/+103
| | | | | | | | | | 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]>
* nwc: keep notifications opt-in (revert default-on)Adam Soltys2026-06-061-6/+5
| | | | | | | | | | 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]>
* nwc: fix pay_invoice reply reliability, rate-limit drops, notificationsAdam Soltys2026-06-061-1/+6
| | | | | | | | | | | | | | | | | | | | | | | | | 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]>
* listener logsAdam Soltys2026-03-181-2/+1
|
* remove logsAdam Soltys2026-03-171-10/+0
|
* pass bytes for app secretAdam Soltys2026-03-141-1/+2
|
* fix pending payment cleanup and login password verificationAdam Soltys2026-03-061-5/+7
| | | | | | | 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]>
* fix login/reset glitchesAdam Soltys2026-03-031-7/+18
|
* check forwarded host headerAdam Soltys2026-02-061-1/+1
|
* health checkAdam Soltys2026-02-061-45/+6
|
* whitelist ua'sAdam Soltys2026-02-041-0/+11
|
* strip email from usernameAdam Soltys2026-02-021-0/+1
|
* check usernameAdam Soltys2026-01-291-11/+1
|
* skip captcha for torAdam Soltys2026-01-271-0/+4
|
* case insensitive usernamesAdam Soltys2026-01-261-1/+4
|
* captcha bypass whitelistAdam Soltys2026-01-261-10/+13
|
* read payments from archiveAdam Soltys2026-01-221-6/+6
|
* fix admin resetsAdam Soltys2026-01-221-7/+3
|
* api key captcha bypassAdam Soltys2026-01-221-3/+11
|
* captcha thresholdsAdam Soltys2026-01-211-11/+18
|
* require auth for accountsAdam Soltys2026-01-211-0/+5
|
* recaptcha on loginAdam Soltys2026-01-211-2/+32
|
* loggingAdam Soltys2026-01-211-1/+3
|
* check usernameAdam Soltys2026-01-191-0/+1
|
* check app ownershipAdam Soltys2025-11-171-2/+5
|
* check usernameAdam Soltys2025-11-171-2/+5
|
* require challenge after login failuresAdam Soltys2025-10-101-7/+9
|
* fail fasterAdam Soltys2025-10-011-1/+1
|
* challenge verifyAdam Soltys2025-10-011-12/+4
|
* login challengeAdam Soltys2025-10-011-0/+11
|
* fix username change logicAdam Soltys2025-09-301-9/+9
|