Skip to content

feat: public profiles, NIP-05 identity, search & presence tools#28

Merged
tlongwell-block merged 5 commits into
mainfrom
feature/public-profiles
Mar 11, 2026
Merged

feat: public profiles, NIP-05 identity, search & presence tools#28
tlongwell-block merged 5 commits into
mainfrom
feature/public-profiles

Conversation

@tlongwell-block
Copy link
Copy Markdown
Collaborator

@tlongwell-block tlongwell-block commented Mar 11, 2026

Summary

Public user profiles with display name resolution, NIP-05 identity verification, and two new MCP tools — crossfire-reviewed across 3 rounds (Opus 9/10, Codex 8/10).

What Changed

REST API (2 new endpoints)

  • GET /api/users/{pubkey}/profile — read any user's profile by pubkey (authenticated)
  • POST /api/users/batch — bulk-resolve display names and NIP-05 handles for up to 200 pubkeys; invalid inputs (wrong-length and non-hex) correctly reported in missing
  • PUT /api/users/me/profile — now supports nip05_handle field with format validation, relay-domain restriction, and lowercase canonicalization

NIP-05 Identity Verification

  • GET /.well-known/nostr.json?name=alice — standard NIP-05 endpoint with CORS support
  • Domain-restricted: only serves handles matching the relay's own domain
  • Shared canonicalize_nip05() validator used by both REST and kind:0 paths — lowercases local+domain before storage

Kind:0 Event Sync

  • Nostr kind:0 (profile metadata) events sync display_name, about, avatar_url, and nip05 to the users table
  • Absolute-state semantics: absent fields are cleared, not left stale
  • NIP-05 handles validated via shared canonicalizer — invalid/off-domain handles silently cleared
  • Duplicate NIP-05 handle (UNIQUE constraint) no longer kills entire profile sync — retries without the contested handle so other fields are still written
  • empty_to_none helper respects UNIQUE constraint on nip05_handle

MCP Tools (2 new + 1 fixed)

  • search — full-text search across messages via Typesense (GET /api/search)
  • get_presence — bulk presence lookup by pubkey (GET /api/presence)
  • set_profile — fixed to support nip05_handle (was silently dropped)

Error Handling

  • Duplicate nip05_handle returns 409 Conflict (was generic 500)
  • NIP-05 format validation: rejects malformed handles and wrong-domain claims with clear 400 errors
  • Empty string clears nip05_handle (REST and kind:0 paths)

Testing (22 new tests)

  • 18 new REST integration tests: profile CRUD, batch resolution, NIP-05 round-trip/clear/duplicate/negative cases, batch non-hex handling
  • 3 new MCP integration tests: self profile, other profile, batch lookup
  • 1 new relay integration test: kind:0 NIP-05 sync regression (valid syncs, off-domain cleared)
  • Tool count: 38 → 40 (assertions updated)
  • All 59 integration tests pass (35 REST + 14 relay + 10 MCP)
  • cargo fmt, cargo clippy, cargo check all clean

TESTING.md Updates

  • Section 6 tool table rewritten: correct all tool names, add 8 missing tools (40 total)
  • Fixed exercises A-5, B-6 for accuracy
  • Added bootstrap channel timing note (harness discovery gap)
  • Fixed expected results for batch profile resolution
  • Updated test counts (35/14/10 = 59)

Files Changed (12 files, +1374/-75)

File Change
crates/sprout-db/src/user.rs update_user_profile nip05 param, get_user_by_nip05, empty_to_none
crates/sprout-db/src/lib.rs Db wrappers for new functions
crates/sprout-relay/src/api/users.rs Profile endpoints, batch, NIP-05 canonicalization, 409 conflict
crates/sprout-relay/src/api/nip05.rs New — NIP-05 handler + shared canonicalize_nip05()
crates/sprout-relay/src/api/mod.rs Re-exports + nip05 module
crates/sprout-relay/src/router.rs 2 new routes + NIP-05 route
crates/sprout-relay/src/handlers/side_effects.rs Kind:0 dispatch, NIP-05 validation, duplicate resilience
crates/sprout-mcp/src/server.rs search, get_presence tools + set_profile nip05 fix
crates/sprout-test-client/tests/e2e_mcp.rs 3 new tests, tool count 40
crates/sprout-test-client/tests/e2e_rest_api.rs 18 new tests
crates/sprout-test-client/tests/e2e_relay.rs 1 new kind:0 NIP-05 regression test
TESTING.md Tool table rewrite, exercise fixes, bootstrap note, test counts

Crossfire Review History

Round Opus Codex Key Fix
R1 8/10 6/10 NIP-05 validation, 409 conflict, round-trip tests, percent_encode
R2 7/10 Kind:0 bypasses NIP-05 validation → fixed
R3 9/10 8/10 Shared canonicalizer, batch non-hex, kind:0 duplicate resilience

Deferred

  • set_presence MCP tool (requires new REST endpoint or WS event dispatch)
  • get_channel_feed / get_user_channels MCP tools
  • Kind:0 created_at timestamp ordering guard (pre-existing gap)
  • ACP harness dynamic channel discovery (tracked separately)

…P-05

Add REST endpoints for reading any user's profile and batch-resolving
display names, MCP tools for agent profile access, kind:0 side-effect
sync from Nostr metadata events, and a working NIP-05 identity endpoint.

REST API:
- GET /api/users/{pubkey}/profile — read any user's profile (auth required)
- POST /api/users/batch — batch resolve display names (max 200, dedup,
  case-normalize; invalid-length inputs returned in `missing`)

MCP tools:
- get_user_profile — read own or any user's profile by pubkey
- get_users_batch — batch resolve display names for message attribution

Kind:0 side effect:
- NIP-01 profile metadata events now sync to the users table
- Absolute-state semantics: absent fields are cleared (set to NULL)
- Fields: display_name (with name fallback), avatar_url (picture/image),
  about, nip05_handle
- Empty strings converted to NULL via empty_to_none() to respect the
  UNIQUE constraint on nip05_handle

NIP-05 (/.well-known/nostr.json):
- Replaces empty stub with working identity verification
- Exact domain match against relay_url (no LIKE wildcards)
- Access-Control-Allow-Origin: * header (NIP-05 spec requirement)
- No authentication required (public discovery endpoint)

DB changes:
- update_user_profile gains nip05_handle parameter
- New get_user_by_nip05 function (exact match, not LIKE)
- No schema migrations required

Tests:
- 12 new REST API e2e tests (profile CRUD, batch, NIP-05, auth, edges)
- 3 new MCP e2e tests (self profile, other profile, batch)
- Tool count assertion updated from 36 to 38
- All 55 e2e tests pass (32 REST + 10 MCP + 13 relay)
- Update MCP tool count from 36 to 38 across all references
- Add get_user_profile and get_users_batch to Profile tools table
- Update integration test counts: 32 REST, 10 MCP, 55 total
  (plus 7 workflow tests noted separately)
- Add B-8 exercise: Bob tests profile resolution via MCP tools
- Add C-8 exercise: Charlie verifies NIP-05 identity endpoint
- Add C-9 exercise: Charlie tests profile lookup edge cases
- Update A-5 to set Alice's NIP-05 handle (alice@localhost)
- Update Expected Results table with profile and NIP-05 rows
- Update 'as of' date to 2026-03-11
…, update TESTING.md

- Add 'search' MCP tool wrapping GET /api/search (Typesense full-text)
- Add 'get_presence' MCP tool wrapping GET /api/presence (bulk lookup)
- Fix set_profile to support nip05_handle field (was silently dropped)
- Thread nip05_handle through UpdateProfileBody → update_user_profile
- Update TESTING.md section 6 tool table: correct all tool names,
  add missing tools (40 total), remove nonexistent tools
- Fix exercises A-5, B-6 for accuracy; add bootstrap channel timing note
- Fix expected results: Charlie in profiles map (not missing list)
- Update e2e test assertions: 38 → 40 tools
…ests

Crossfire R1 + R2 fixes:
- Validate nip05_handle format (user@domain) and restrict domain to
  relay's own domain on both REST and kind:0 ingestion paths
- Return 409 Conflict on duplicate nip05_handle (was generic 500)
- Allow empty string to clear nip05_handle (empty → NULL via DB layer)
- Kind:0 side effect now validates NIP-05 format and domain before
  syncing to users table (invalid handles silently cleared)
- Add percent_encode to get_presence MCP tool for consistency
- Add 3 NIP-05 integration tests: round-trip set+lookup, clear handle,
  duplicate handle conflict
- Update doc comments to include nip05_handle
- Make extract_domain pub(crate) for reuse across modules
…lience

Crossfire R3 fixes:
- Extract shared canonicalize_nip05() in nip05.rs — lowercases local+domain,
  validates format and relay domain match, used by both REST and kind:0 paths
- Batch endpoint: 64-char non-hex inputs now correctly reported in missing
  list instead of silently dropped
- Kind:0 duplicate NIP-05: retry profile sync without contested handle so
  display_name/about/avatar_url are still written (UNIQUE violation no longer
  drops all profile fields)
- Add kind:0 NIP-05 regression test: valid handle syncs, off-domain cleared
- Update batch test to cover non-hex 64-char inputs
@tlongwell-block tlongwell-block force-pushed the feature/public-profiles branch from f0faadf to f5b534f Compare March 11, 2026 17:25
@tlongwell-block tlongwell-block merged commit a3380c3 into main Mar 11, 2026
8 checks passed
@tlongwell-block tlongwell-block deleted the feature/public-profiles branch March 11, 2026 17:48
tlongwell-block added a commit that referenced this pull request Mar 11, 2026
* origin/main:
  Plumb desktop profile identity UI (#32)
  Add unread indicators to desktop channels (#31)
  feat: public profiles, NIP-05 identity, search & presence tools (#28)
  Add desktop settings page (#30)
  Remove OS titlebar and add custom window chrome (#29)

# Conflicts:
#	TESTING.md
#	crates/sprout-mcp/src/server.rs
#	crates/sprout-test-client/tests/e2e_mcp.rs
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.

1 participant