Add simplified NIP-46 bunker page with click-to-copy QR codes (v0.41.0)
Some checks failed
Go / build-and-release (push) Has been cancelled

- Add BunkerView with two QR codes: client (bunker://) and signer (nostr+connect://)
- Add click-to-copy functionality on QR codes with visual "Copied!" feedback
- Add CAT requirement warning (only shows when ACL mode is active)
- Remove WireGuard dependencies from bunker page
- Add /api/bunker/info public endpoint for relay URL, ACL mode, CAT status
- Add Cashu token verification for WebSocket connections
- Add kind permission checking for Cashu token scopes
- Add cashuToken field to Listener for connection-level token tracking

Files modified:
- app/handle-bunker.go: New bunker info endpoint (without WireGuard)
- app/handle-event.go: Add Cashu token kind permission check
- app/handle-websocket.go: Extract and verify Cashu token on WS upgrade
- app/listener.go: Add cashuToken field
- app/server.go: Register bunker info endpoint
- app/web/src/BunkerView.svelte: Complete rewrite with QR codes
- app/web/src/api.js: Add getBunkerInfo() function
- pkg/version/version: Bump to v0.41.0

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-28 18:36:04 +02:00
parent ea4a54c5e7
commit 1b17acb50c
12 changed files with 516 additions and 526 deletions

View File

@@ -12,6 +12,7 @@ import (
"lol.mleku.dev/log"
"git.mleku.dev/mleku/nostr/encoders/envelopes/authenvelope"
"git.mleku.dev/mleku/nostr/encoders/hex"
"next.orly.dev/pkg/cashu/token"
"next.orly.dev/pkg/protocol/publish"
"git.mleku.dev/mleku/nostr/utils/units"
)
@@ -55,6 +56,12 @@ func (s *Server) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
return
}
whitelist:
// Extract and verify Cashu access token if verifier is configured
var cashuToken *token.Token
if s.CashuVerifier != nil {
cashuToken = s.extractWebSocketToken(r, remote)
}
// Create an independent context for this connection
// This context will be cancelled when the connection closes or server shuts down
ctx, cancel := context.WithCancel(s.Ctx)
@@ -99,6 +106,7 @@ whitelist:
conn: conn,
remote: remote,
req: r,
cashuToken: cashuToken, // Verified Cashu access token (nil if none provided)
startTime: time.Now(),
writeChan: make(chan publish.WriteRequest, 100), // Buffered channel for writes
writeDone: make(chan struct{}),
@@ -291,3 +299,54 @@ func (s *Server) Pinger(
}
}
}
// extractWebSocketToken extracts and verifies a Cashu access token from a WebSocket upgrade request.
// Checks query param first (for browser WebSocket clients), then headers.
// Returns nil if no token is provided or if token verification fails.
func (s *Server) extractWebSocketToken(r *http.Request, remote string) *token.Token {
// Try query param first (WebSocket clients often can't set custom headers)
tokenStr := r.URL.Query().Get("token")
// Try X-Cashu-Token header
if tokenStr == "" {
tokenStr = r.Header.Get("X-Cashu-Token")
}
// Try Authorization: Cashu scheme
if tokenStr == "" {
auth := r.Header.Get("Authorization")
if strings.HasPrefix(auth, "Cashu ") {
tokenStr = strings.TrimPrefix(auth, "Cashu ")
}
}
// No token provided - this is fine, connection proceeds without token
if tokenStr == "" {
return nil
}
// Parse the token
tok, err := token.Parse(tokenStr)
if err != nil {
log.W.F("ws %s: invalid Cashu token format: %v", remote, err)
return nil
}
// Verify token - accept both "relay" and "nip46" scopes for WebSocket connections
// NIP-46 connections are also WebSocket-based
ctx := context.Background()
if err := s.CashuVerifier.Verify(ctx, tok, remote); err != nil {
log.W.F("ws %s: Cashu token verification failed: %v", remote, err)
return nil
}
// Check scope - allow "relay" or "nip46"
if tok.Scope != token.ScopeRelay && tok.Scope != token.ScopeNIP46 {
log.W.F("ws %s: Cashu token has invalid scope %q for WebSocket", remote, tok.Scope)
return nil
}
log.D.F("ws %s: verified Cashu token with scope %q, expires %v",
remote, tok.Scope, tok.ExpiresAt())
return tok
}