feat: self-service token minting via NIP-98 HTTP Auth#37
Merged
tlongwell-block merged 1 commit intomainfrom Mar 12, 2026
Merged
feat: self-service token minting via NIP-98 HTTP Auth#37tlongwell-block merged 1 commit intomainfrom
tlongwell-block merged 1 commit intomainfrom
Conversation
5588646 to
a3e35f3
Compare
Allow users to mint their own API auth tokens using their Nostr keypair,
eliminating the need for admin CLI access. Tokens are created via a new
POST /api/tokens endpoint that accepts NIP-98 (kind:27235) HTTP Auth for
bootstrap (no existing token required) or Bearer token for delegation.
## New Endpoints
- POST /api/tokens — mint a new token (NIP-98 or Bearer)
- GET /api/tokens — list own tokens (Bearer required)
- DELETE /api/tokens/{id} — revoke one token
- DELETE /api/tokens — revoke all tokens (panic button)
## Security Model
- **Scope whitelist**: 7 self-mintable scopes; admin scopes (admin:channels,
admin:users, jobs:*, subscriptions:*) remain CLI-only
- **Scope escalation prevention**: Bearer-minted tokens must be a subset of
the caller's own scopes and channel_ids
- **NIP-98 payload hash required**: POST /api/tokens requires the payload tag
to cryptographically bind the request body to the signed event
- **Rate limiting**: 5 mints/hr/pubkey via bounded moka LRU cache (100k cap)
- **Token limit**: 10 active tokens per pubkey (atomic conditional INSERT)
- **Distinct error types**: token_revoked vs token_expired vs invalid_token
## Scope Enforcement
All REST endpoints now enforce scopes via RestAuthContext + require_scope().
Migrated 61 call sites across 15 handler files from the old extract_auth_pubkey()
to the new extract_auth_context() which returns scopes, auth method, token ID,
and channel_ids. Token-level channel_ids restrictions are enforced universally
across all channel-scoped handlers.
## Implementation
- sprout-auth: NIP-98 verifier (319 lines, 14 unit tests), scope helpers
(all_known, all_non_admin, is_self_mintable), new error variants
- sprout-db: 5 new token DB functions (conditional INSERT, list, revoke,
revoke-all, get-including-revoked), migration adding created_by_self_mint
- sprout-relay: token CRUD handlers (755 lines), RestAuthContext struct,
MintRateLimiter, debounced last_used_at tracking, channel access checks
- sprout-test-client: 17 e2e integration tests (824 lines) covering NIP-98
auth, Bearer auth, scope validation, rate limiting, revocation, escalation
prevention, and NIP-98 negative cases
## Test Results
- Unit tests: 504+ passed, 0 failed
- Integration tests: 97/97 (48 REST + 18 WS + 14 MCP + 17 Token E2E)
- Crossfire reviewed: Codex 8/10 APPROVE, Opus 9/10 APPROVE
Spec: PLANS/SPROUT_SELF_MINT_SPEC.md
Research: RESEARCH/SPROUT_SELF_SERVICE_TOKEN_MINTING.md
a3e35f3 to
3c15e6f
Compare
tlongwell-block
added a commit
that referenced
this pull request
Mar 12, 2026
* origin/main: feat: self-service token minting via NIP-98 HTTP Auth (#37)
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.
Summary
Allow users to mint their own API auth tokens using their Nostr keypair, eliminating the need for admin CLI access (
sprout-admin mint-token). Self-sovereign identity means self-sovereign token management.New Endpoints
POST/api/tokensGET/api/tokensDELETE/api/tokens/{id}DELETE/api/tokensSecurity Model
POST /api/tokensrequires thepayloadtag to cryptographically bind the request body to the signed event (prevents body substitution)extract_auth_context()explicitly rejects NIP-98 on all non-token endpoints withnip98_not_supportedadmin:channels,admin:users,jobs:*,subscriptions:*) remain CLI-onlySPROUT_MINT_RATE_LIMIT), bounded moka LRU cache (100k entry cap)token_revokedvstoken_expiredvsinvalid_tokenAgent Ownership
Self-minted tokens automatically set
agent_owner_pubkey = caller_pubkeyin the users table — same ownership semantics assprout-admin mint-token --owner-pubkey.Scope Enforcement
All REST endpoints now enforce scopes via
RestAuthContext+require_scope(). Migrated 61 call sites across 15 handler files from the oldextract_auth_pubkey()to the newextract_auth_context()which returns scopes, auth method, token ID, and channel_ids. Token-levelchannel_idsrestrictions are enforced universally across all channel-scoped handlers.Implementation
sprout-authScope::all_known(),all_non_admin(),is_self_mintable(), new error variantssprout-dbsprout-relayRestAuthContext, configurableMintRateLimiter, debouncedlast_used_at, channel access checks, NIP-98 scoped to mint endpoint onlysprout-test-clientConfiguration
SPROUT_MINT_RATE_LIMIT50SPROUT_REQUIRE_AUTH_TOKENfalsetruefor productionTest Results
cargo fmt,cargo clippy -D warningscleanE2E Test Coverage (20 tests)
SPROUT_MINT_RATE_LIMIT)Migration
20260317000001_self_mint_token.sql— addscreated_by_self_mintboolean column toapi_tokenstable. Non-breaking, backward compatible.