Skip to content

feat(account): nostr login#911

Closed
escapedcat wants to merge 45 commits into
mainfrom
feature/nostr-login
Closed

feat(account): nostr login#911
escapedcat wants to merge 45 commits into
mainfrom
feature/nostr-login

Conversation

@escapedcat
Copy link
Copy Markdown
Contributor

No description provided.

escapedcat and others added 30 commits April 11, 2026 11:51
Add password field to Session type so the backup modal can show
credentials later. The password is generated in doSignUp() and
was previously discarded after the API call.

Old sessions are backfilled with an empty string — the password
is lost but the token still works.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the bookmark link in the header with a user icon that opens
a dropdown menu with "My Saved" and "Log out". Uses svelte-outclick
for outside click handling (same as NavDropdownDesktop). Only visible
when the user has a session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add test for password backfill when loading old sessions without
  the password field (mirrors existing savedAreas backfill test)
- Add aria-haspopup="true" to UserMenu trigger button for screen
  readers (matches NavDropdownDesktop pattern)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
UserMenu is now always visible:
- Logged out: outline account icon, dropdown shows "Log in"
- Logged in: filled account icon, dropdown shows "My Saved" + "Log out"

Links to /login which will be built in the next step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Every user gets an auto-generated account on first save. "Log out"
made no sense — clicking Save again would just create another
throwaway. Account switching will be handled by the login flow
which replaces the current session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- POST /api/session/login server route proxying token creation
- /login page with username + password form
- session.login() method to replace current session
- After login, fetches saved places/areas from server to populate store
- Redirects to /saved on success
- Error toast on invalid credentials or server error

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Since every user always has an account (auto-generated on first
save), they need a way to switch to a different account without
a logout step. Links to /login which replaces the current session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hovering the user icon now shows the account username (e.g.
"glossy-wealth-1878") via the title attribute. Shows "Account"
when not logged in.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a user clicks Save for the first time and the throwaway
account is silently created, show a success toast: "Account
created — your saved places are stored on this device."

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Theme toggle is h-10 w-10. User menu trigger was unsized, causing
visual misalignment. Match the dimensions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
session.init() is a synchronous localStorage read but was deferred
to onMount, causing a visible 2-second delay before the user icon
rendered. Move it to the module level with a browser guard so it
runs during script evaluation, before the first render.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- "Switch account" now shows a confirmation dialog warning that
  the current saved places will be replaced
- Remove unused nav.logout i18n key
- Update security comment in session.ts to acknowledge password
  storage in localStorage alongside the token

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- BackupModal shows username + password with copy buttons and
  show/hide toggle. Accessible from "Back up account" in UserMenu.
- Old accounts without a stored password show "Not available"
- Remove switch account confirmation — if the user knows their
  credentials (just logged in), switching is safe
- Remove dead switchAccountConfirm i18n key

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add autoGenerated flag to Session type. Set true in signUp(),
false in login(). UserMenu only shows "Back up account" when the
account was auto-generated — users who logged in with known
credentials don't need to back them up.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When logging in with known credentials, the user already has their
password. No reason to keep it in localStorage where an XSS could
exfiltrate it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Redirecting anonymous users to /map was confusing — they had no
idea why they landed there. Now /saved just shows the empty state
message regardless of session status.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
If the user visits /saved without an account, redirect to /login
where they can log in with existing credentials or learn they
need to save something first.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reject oversized username (>100) or password (>200) before
forwarding to the upstream API.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Users who want a custom username can create an account on
developer.btcmap.org, then log in on the main site. Added
"Don't have an account? Create one here" below the login form.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Close BackupModal on Escape key press
- Remove document.execCommand("copy") fallback — navigator.clipboard
  is the standard API and works on all modern browsers over HTTPS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Users start with no account, so "Switch account" was confusing —
it implies multiple accounts. "Log out" is clearer: it means
"forget this session." If they want a different account, they
log out first, then log in.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Header renders UserMenu twice (desktop + mobile), producing
duplicate DOM IDs. Use a random suffix per instance so each
trigger has a unique ID and the OutClick exclusion works correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Wrap clipboard.writeText in try/catch to prevent unhandled
  rejections (console.error only, no error toast — failure is
  extremely unlikely on HTTPS)
- Replace hardcoded "copied" string with i18n key backup.copied

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The security comment said both token and password are stored, but
manual logins intentionally store an empty password. Clarify that
password persistence only applies to auto-generated sessions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Stop trimming password input — spaces may be valid characters
  in credentials. Username is still trimmed (no valid username
  has leading/trailing spaces).
- Sanitize console.error in login server route and client page
  to only log the HTTP status code, not the full error object
  which may contain request headers with credentials.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevents unintended form submits if the component is ever
rendered inside a form element.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tton

Use role=presentation on the backdrop overlay instead of suppressing
a11y warnings. The backdrop is decorative — keyboard interaction is
handled via svelte:window Escape handler. Add type=button to the
close button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace Math.random() trigger ID with a prop-based ID to avoid
SSR/client hydration mismatches. Header passes distinct IDs for
desktop and mobile instances.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Group authenticated user pages under /user/ to establish a clear
URL pattern for future user features (profile, feed, settings).
Login stays at /login since it's a public entry point.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
escapedcat and others added 5 commits April 12, 2026 13:27
Switch from filled bookmark (ic:baseline-bookmark) to bookmark
with checkmark (ic:baseline-bookmark-added) for the saved state.
Aligns with the Android app's icon pattern — the checkmark is a
more explicit signal than fill alone.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Needed for the upcoming "Sign in with Nostr" flow on the login page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server route that proxies signed NIP-98 events to POST /v4/auth/nostr
and returns a Bearer token + username. Same CORS-avoidance pattern as
the existing /api/session/login and signup routes.

The v4/auth/nostr endpoint is not yet live; this route returns 502 until
the API ships it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shows a "Sign in with Nostr extension" button on /login when window.nostr
is detected (Alby, nos2x, etc.). Signs a NIP-98 kind 27235 event and
posts it to the new /api/session/nostr proxy, exchanging it for a Bearer
token.

nsec paste fallback for mobile comes in a follow-up commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a user doesn't have a NIP-07 extension (common on mobile), they
can paste their nsec key. The key is used once locally to sign a NIP-98
event, then zeroed and never persisted or sent over the network.

The toggle is shown alongside the extension button — on mobile it
becomes the primary path since window.nostr is usually absent.

Follows the Primal mobile-web pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 13, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3ff42af8-4bc8-49e8-9dc5-97625cab4f66

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/nostr-login

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@socket-security
Copy link
Copy Markdown

socket-security Bot commented Apr 13, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednostr-tools@​2.23.39710010091100

View full report

This comment was marked as resolved.

escapedcat and others added 6 commits April 13, 2026 11:18
Replaces duplicate Promise.allSettled hydration blocks in the two /login
code paths (username+password and Nostr) with a single session method.
Next login path (backup code, OAuth, etc.) gets it for free.

Addresses Copilot review comment on PR #911.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously, any error without err.response was treated as a decode error.
Network, CORS, or timeout failures have no .response either, so transport
problems were surfaced as "invalid nsec" — misleading to the user.

Split into two phases:
- Phase 1 (sync decode + sign): any throw → "invalid nsec"
- Phase 2 (async exchange): uses the same 401-vs-generic classifier as the
  extension login path

Addresses Copilot review comment on PR #911.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Return a cleanup function from onMount so the 300ms re-check doesn't
fire after the component is destroyed. Prevents a set-after-unmount if
the user navigates away quickly.

Addresses Copilot review comment on PR #911.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…903

The client signs a NIP-98 event with NOSTR_AUTH_URL in the "u" tag; the
proxy route POSTs to that same URL. Hardcoding it in both places risks
silent signature-verification failures if one drifts (e.g. staging URL).

Single source of truth in $lib/nostr.

Addresses Copilot review comment on PR #911.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reject bodies with Content-Length > 4096 bytes before parsing. A valid
NIP-98 event is ~400 B; 4 KB leaves headroom without accepting obvious
junk. Prevents accidental misuse of the proxy as a parser for large
payloads. Deliberate DoS still needs upstream rate limiting.

Addresses the one Medium finding from the branch security review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
"off" is widely ignored by password managers on type=password inputs.
"new-password" is the documented hint Chrome and Safari respect to
suppress save/fill prompts. Reduces the chance a user's nsec is
silently stored by a password manager.

Addresses a Low finding from the branch security review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Base automatically changed from feature/user-menu-backup-login to main April 16, 2026 05:20
escapedcat and others added 4 commits May 10, 2026 11:34
The signed NIP-98 event's `u` tag must exactly match the URL the API
reconstructs and verifies. Hardcoding api.btcmap.org makes local
dev impossible — running btcmap-api on http://127.0.0.1:8000 and
signing for api.btcmap.org would always fail the URL-match check.

Read VITE_API_BASE_URL with the production endpoint as the default,
strip any trailing slash, then derive NOSTR_AUTH_URL from it. The
SvelteKit proxy that forwards to the API uses the same constant, so
both the signed event and the API hop stay aligned with whatever the
env var points at.

For local dev: set VITE_API_BASE_URL=http://127.0.0.1:8000 in .env.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /v4/auth/nostr API extractor reads the signed event from the
canonical Authorization: Nostr <base64(event)> header, not from the
request body. This was a transport mismatch — the proxy was sending
{signed_event} as JSON body, which the API wouldn't see.

Switch the API hop to base64-encode the signed event JSON and put it
in the Authorization header, with no body. The browser-side contract
of this route is unchanged: the client still POSTs {signed_event} to
/api/session/nostr, the SvelteKit server route does the reshape.

The 4 KB content-length cap on the incoming body still applies, since
the {signed_event} envelope from the browser is what's bounded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…base #903

Adds src/lib/api-base.ts (cherry-picked verbatim from main, where it
already exists in this exact form for the same reason) so every
SvelteKit server-route proxy in src/routes/api/session/* resolves the
btcmap-api base URL the same way:

  VITE_API_BASE_URL > 'https://api.btcmap.org' (default), trailing-/
  stripped.

Without this, /user/saved was failing locally with 401: a token minted
against the local API at http://127.0.0.1:8000 doesn't authenticate
against api.btcmap.org, but the saved-places / saved-areas proxies
were pointing at production regardless of VITE_API_BASE_URL.

Affected proxies, all hardcoded api.btcmap.org before:
- POST /api/session/login
- POST /api/session/signup
- GET/PUT /api/session/saved-places
- GET/PUT /api/session/saved-areas

Also drops the inline VITE_API_BASE_URL read introduced in 63e48c9 from
src/lib/nostr.ts in favour of the shared constant — same end value, just
de-duped.

Out of scope here: the rest of the codebase still has ~25 hardcoded
api.btcmap.org references in map sync, merchant/area page loaders, etc.
Those will surface separately if local-dev coverage is broadened, and
should rebase cleanly once #911 lands on top of current main.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without an explicit navigation, clicking Logout from /user/saved
cleared the session in localStorage but kept the user on the same
page. The page only checks for a session in onMount, so it kept
rendering the post-login UI — and any subsequent saved-* fetch
fired without a token and surfaced as "Failed to load some saved
items".

After session.clear(), goto('/login') so the user lands on a
clean state and can sign back in if they want.
@netlify
Copy link
Copy Markdown

netlify Bot commented May 10, 2026

Deploy Preview for btcmap ready!

Name Link
🔨 Latest commit dbd18e9
🔍 Latest deploy log https://app.netlify.com/projects/btcmap/deploys/6a00555092c2d3000881e0e8
😎 Deploy Preview https://deploy-preview-911--btcmap.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 51 (🔴 down 40 from production)
Accessibility: 97 (no change from production)
Best Practices: 92 (🔴 down 8 from production)
SEO: 96 (no change from production)
PWA: 90 (no change from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@escapedcat escapedcat closed this May 10, 2026
@dadofsambonzuki
Copy link
Copy Markdown
Member

Superseded by #990 ?

@escapedcat escapedcat deleted the feature/nostr-login branch May 10, 2026 18:55
@escapedcat
Copy link
Copy Markdown
Contributor Author

Yes!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants