feat: nostr identity endpoints#95
Draft
escapedcat wants to merge 6 commits into
Draft
Conversation
Add an `npub` field (bech32, nullable) to the two "who am I" responses so the frontend can show whether the logged-in account has a Nostr identity linked: - REST `GET /v4/users/me` (MeResponse) — also flows into the create-token and update-username responses via the shared struct. - RPC `whoami` (Res) — kept in sync with the REST surface. `user.npub` already exists on the model (migration 98); this is a purely additive projection change, no query or schema change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two Bearer-authenticated sub-resources for an account's Nostr link:
- GET /v4/users/me/nostr -> { npub } (or null) — lets a client poll
just the link state without the full /me payload.
- DELETE /v4/users/me/nostr -> clears the link via set_npub(None).
Idempotent: returns 200 { npub: null } even when nothing was linked,
since clearing your own link needs no NIP-98 proof.
Registered in the v4 users scope. Removes the now-unused #[allow(dead_code)]
on user::queries::set_npub. Linking/replacing a pubkey (PUT) lands
separately since it additionally requires a NIP-98 ownership proof.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds Nostr identity exposure to the authenticated-user surfaces by threading an optional Bech32 npub through the existing “me/whoami” responses and introducing dedicated endpoints to read/clear the linked Nostr pubkey.
Changes:
- Extend RPC
whoamiand RESTGET /v4/users/meresponses withnpub: Option<String>. - Add REST sub-resource endpoints:
GET /v4/users/me/nostrandDELETE /v4/users/me/nostr, plus corresponding tests. - Wire the new endpoints into the v4 users scope and remove the now-unneeded
dead_codeallowance onset_npub.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/rpc/auth/whoami.rs | Adds npub to RPC whoami response and tests passthrough. |
| src/rest/v4/users.rs | Adds npub to /me response; introduces /me/nostr GET/DELETE endpoints and test coverage. |
| src/main.rs | Registers the new /v4/users/me/nostr routes in the server configuration. |
| src/db/main/user/queries.rs | Makes set_npub a referenced query (removes #[allow(dead_code)]). |
| docs/rest/v4/users.md | Documents the new npub field on GET /v4/users/me. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Copilot review on #95 flagged it: get_nostr never touches the pool — the npub is already on auth.user, and the Auth extractor reads the pool from app_data itself, so the Data<MainPool> handler param was dead weight. Removing it makes the signature honest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot review on #95 flagged the public REST docs as incomplete — the new identity sub-resource endpoints weren't listed. Add the Available Endpoints entries plus request/response sections for reading and clearing the linked npub. (PUT link/replace will be documented when that endpoint lands.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Links (or replaces) the Nostr pubkey on an already-authenticated account. This needs TWO credentials at once — a Bearer token to say which account, and a NIP-98 signature to prove control of the pubkey being linked — but Auth and NostrAuth both read the Authorization header. Resolve it by carrying the proof on a dedicated header: - Factor the NIP-98 verification in nostr_auth.rs into a shared `verified_npub(req, header)` helper. - Add a `NostrProof` extractor that reads `X-Nostr-Authorization` (new const `X_NOSTR_AUTHORIZATION`), reusing the same ApiBaseUrl-pinned verification as NostrAuth. NostrAuth keeps reading `Authorization`. Handler: Auth identifies the account, NostrProof proves the pubkey. Conflict is checked at the application level — if the proven npub is already linked to a different account, return 400; re-linking your own npub is an idempotent 200. The empty body keeps the NIP-98 binding to just `u`+`method` (the nip98 module does no body-hash binding). There is deliberately NO DB migration: with no UNIQUE index on user.npub yet, the select-then-set has a documented TOCTOU window that the maintainer-owned partial unique index is meant to close. Flagged in code and reserved for that follow-up rather than pre-empted here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the link/replace endpoint: the dual-credential requirement (Bearer + the NIP-98 proof on the X-Nostr-Authorization header), the exact u/method the proof must sign, and the 200/400/401 responses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Nostr identity endpoints (parity follow-up to #89)
Follow-up to the merged Nostr sign-in endpoint (#89). That PR let a Nostr key
mint a token; this one exposes the linked identity and lets an existing
account manage its Nostr link. All new surface is REST under
/v4/users/me; no new DB migration.What's in here
Expose the linked npub
GET /v4/users/me(MeResponse) gainsnpub: Option<String>(bech32, ornull). Flows into the create-token and update-username responses too viathe shared struct.
whoamiResgets the samenpubfield, so the two "who am I"surfaces stay in sync.
Manage the link (new sub-resource
/v4/users/me/nostr)GET→{ npub }— read the current link (Bearer only).DELETE→ clears it, idempotent200 { npub: null }(Bearer only — clearingyour own link needs no proof).
PUT→ links/replaces the pubkey on the account. Returns200 { npub };400if the npub is already linked to a different account; idempotent re-linkby the same account →
200.user.npubalready exists (migration 98);select_by_npub/set_npubalreadyexisted (the latter was
#[allow(dead_code)], now used). No migration, noschema.sqlchange.Decision that needs your eyes: the PUT dual-credential header
PUT /v4/users/me/nostrneeds two credentials at once — a Bearer token(which account) and a NIP-98 signature (proof you control the pubkey).
Both the
AuthandNostrAuthextractors read theAuthorizationheader, sothey can't share it. I resolved this by carrying the proof on a dedicated
header:
Implementation: factored the NIP-98 verification in
nostr_auth.rsinto ashared
verified_npub(req, header)helper, and added a thinNostrProofextractor that reads
X-Nostr-Authorization(sameApiBaseUrl-pinnedverification as
NostrAuth). The request body stays empty, so there's noSHA-256 body-binding gap. If you'd prefer a different header name or
mechanism, this is the piece to flag — happy to change it.
(Deliberately not the #83 approach of a
nostr_eventbody field —nip98.rswarns body endpoints need body-hash binding, and the client-supplied
urlparam there was the bot-flagged weakness.)
Conflict check is application-level (no unique index yet)
The
PUTconflict check isselect_by_npub→ compare →set_npub, which has adocumented TOCTOU window: two concurrent PUTs linking the same npub to
different accounts could both pass the check. This is intentional for now and
called out in a code comment — the partial unique index on
user.npub(yourin-flight work, closed as #93) is what closes it, after which a
set_npubUNIQUE violation could map to
400. I did not add that index or a migrationhere, to stay off your toes.
Heads-up (not touched): stale comment in the merged sign-in code
src/rest/v4/nostr.rs(merged in #89) has a doc-comment claiming the npubunique index comes "from migration 101" — but on
master, 101 is theaccess_token.import_originsmigration and no such index exists. Therecovery branch it describes is currently dormant. Flagging only; left it
alone since it's coupled to whenever the index actually lands.
Out of scope
create_api_key_with_nostradmin RPC — redundant withPOST /v4/auth/nostr.user.npubunique index / any migration — yours.sub-resource.