Sprout speaks Nostr. Nothing else.#475
Merged
tlongwell-block merged 6 commits intomainfrom May 5, 2026
Merged
Conversation
f6df0bf to
8f1641e
Compare
baxen
approved these changes
May 4, 2026
Collaborator
baxen
left a comment
There was a problem hiding this comment.
LGTM - reviewed the strategy i think we will want to evolve this forward if there are any specific issues
8f1641e to
5c1c97c
Compare
… COUNT BREAKING CHANGE: All REST API endpoints removed except media, git, health, NIP-05, and NIP-11. Clients must use WebSocket (REQ/EVENT/COUNT) or the new HTTP bridge (POST /events, /query, /count with NIP-98 auth). ## What changed ### Auth (sprout-auth) - Delete Okta OIDC runtime (JWKS cache, JWT validation) - Delete API token system (mint, verify, revoke, hash) - Auth is now pure cryptography: NIP-42 (WS) + NIP-98 (HTTP) - Remove 4 dependencies: jsonwebtoken, reqwest, chrono, subtle ### Relay (sprout-relay) - Delete 17 REST handler files (~5000 lines) - Add HTTP bridge: POST /events, /query, /count with NIP-98 - Add NIP-45 COUNT handler (WebSocket + HTTP bridge) - Add command executor framework (transactional, idempotent) - Add NIP-98 replay prevention cache (moka, 120s TTL) - Add channel access enforcement on bridge read paths - Relocate imeta helpers from deleted messages.rs to handlers/imeta.rs - Clean state.rs (remove MintRateLimiter, token caches) - Simplify auth handler (pure NIP-42, no API token path) ### Foundation (sprout-core, sprout-db) - Add 13 kind constants (10100, 24242, 27235, 30620, 40900-40902, 41010-41012, 46020, 46030-46031) - Add is_command_kind() and is_relay_only_kind() helpers - Add DB pushdown: multi-author IN, multi-id IN, e-tag containment - Add Db::begin_transaction() for atomic command execution - Add Db::count_events() for NIP-45 ### Clients - sprout-mcp: All REST -> WS REQ/EVENT (query() convenience method) - sprout-acp: REST -> NIP-98 HTTP bridge, owner via NIP-OA/config - sprout-cli: All commands -> HTTP bridge with NIP-98 per-request signing - sprout-admin: Rewrite from token minting to relay membership management ## Net effect - 67 files changed, +3821/-11218 (net -7397 lines) - 776 tests pass, 0 failures - 0 clippy warnings ## Migration guide - WS clients: No token needed. Connect -> NIP-42 AUTH -> full access. - HTTP clients: Sign kind:27235 event, base64 encode, send as Authorization: Nostr <base64>. Use POST /events (writes), POST /query (reads), POST /count (aggregates). - Agents: SPROUT_AUTH_TAG for NIP-OA delegation. No API token env var. - Dev mode: X-Pubkey header still works on bridge endpoints.
5c1c97c to
4f87d05
Compare
Resolve conflicts by keeping our pure-Nostr implementation: - relay_members.rs: deleted (functionality moved to api/mod.rs with NIP-OA support) - tokens.rs: deleted (token system removed) - auth.rs, api/mod.rs, relay_client.rs: keep our rewritten versions - Adopted main's enforce_relay_membership 3-arg signature (auth_tag for NIP-OA)
7c859a8 to
cc8fc88
Compare
Replace sort_by(|a, b| field.cmp()) with sort_by_key() to satisfy clippy::unnecessary_sort_by added in Rust 1.95 (CI toolchain). Desktop CI failure is transient (Hermit bootstrap 502 — not our code).
Support deleting parameterized-replaceable events (e.g. kind:30620 workflows) via NIP-09 kind:5 with an a tag instead of e tag. Changes: - ingest.rs: accept either e-tag OR a-tag (not both) for kind:5 validation - side_effects.rs: validate_standard_deletion_event handles a-tag author check - side_effects.rs: handle_a_tag_deletion dispatches on target kind (30620 → DB delete) - sprout-db: add find_workflow_by_owner_and_name for name-based lookup
1. Transaction atomicity (command_executor.rs):
- persist_command_event() returns open transaction via PersistResult enum
- Handlers commit tx AFTER mutations succeed; on failure tx drops → rollback
- Event never stored without its side effects; retries work correctly
2. Multi-author/ids/e-tags SQL pushdown (req.rs):
- filter_to_query_params now wires multi-author, ids, and e_tags into EventQuery
- Fixes batch profile fetches and reaction/thread lookups hitting LIMIT
3. COUNT correctness (count.rs, bridge.rs, event.rs):
- Added channel_ids field to EventQuery for SQL-level channel access
- SECURITY: empty channel_ids → global-only (fails closed)
- Added filter_fully_pushable() guard: fast SQL COUNT when safe,
fallback to query+post-filter (limit=10000) for non-pushable tags
- Both channel-scoped and non-channel paths use dual strategy
4. NIP-98 replay atomicity (bridge.rs):
- Uses moka 0.12 entry().or_insert(()).is_fresh() for atomic check
7e48645 to
c627338
Compare
wesbillman
added a commit
that referenced
this pull request
May 5, 2026
- resolve DM participant display names client-side via batch kind:0 fetch - dedupe kind:39000 metadata by `d` tag, keeping the latest revision - match desktop's case-insensitive sort within each channel section - defer the "Could not load channels" view 2s to absorb transient AsyncError frames from relay reconnect cancellations Co-Authored-By: Claude Opus 4.7 (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.
Why
Sprout has been speaking two languages: a bespoke REST API (~45 endpoints, Bearer tokens, Okta JWT) and Nostr (NIP-01 WebSocket). Every feature ships twice — once as REST JSON, once as a signed event. Every client maintains two code paths. Every new engineer learns two protocols.
This PR deletes one of them.
What
Auth is now pure cryptography. No tokens. No Okta at runtime. No JWKS cache. Your keypair is your identity — prove you own it via Schnorr signature (NIP-42 on WebSocket, NIP-98 on HTTP) and you are in. Corporate identity binding moves upstream to a one-time enrollment step that never touches the relay.
Every read is a NIP-01 REQ. Channel messages, profiles, threads, search, presence — all via
["REQ", sub_id, {filter}]over WebSocket orPOST /queryover HTTP.Every write is a signed EVENT. Channel creation, messages, reactions, DM management, workflow triggers — all via
["EVENT", signed_event]over WebSocket orPOST /eventsover HTTP.NIP-45 COUNT for aggregates. No more N+1 queries for "how many messages in this channel."
NIP-09 a-tag deletion for addressable events. Workflows (kind:30620) can be deleted via
atag reference.14 HTTP endpoints remain (down from ~45): 3 media (Blossom), 3 git (smart HTTP protocol), 3 bridge (POST /events, /query, /count), 4 infra (health, NIP-11, NIP-05), 1 webhook.
Numbers
Commits
4f87d05— feat: pure Nostr protocol (67 files, +4821/-11220)okta.rs,token.rs)POST /events,/query,/countwith NIP-98)bd7d4fd— feat: NIP-09 a-tag deletion (3 files, +121/-4)atagkind:5witha=30620:<pubkey>:<d-tag>)c627338— fix: correctness improvements (6 files, +412/-128)filter_to_query_paramsnow wires all three into EventQuery (fixes batch profile fetches hitting LIMIT)channel_idsfield for SQL-level access enforcement;filter_fully_pushable()guard routes to fast SQL COUNT or safe fallback; empty channel_ids fails closed to global-onlyentry().or_insert(()).is_fresh()for atomic insert-if-absentSecurity
channel_idsfails closed (global-only, not unrestricted)E2E Verification (27/27 pass)
Migration guide
Authorization: Bearer sprout_*Authorization: Nostr <b64>SPROUT_ACP_API_TOKENenv varSPROUT_AUTH_TAG(NIP-OA) or just connect with keypairX-Pubkey: <hex>headerWhat's next (not in this PR)
Super PR
#478 was merged on top of the original work in this PR to get CI green