Compare commits

..

1 Commits

Author SHA1 Message Date
woikos
37d4be5e93 v0.51.0: CAT token improvements
Some checks are pending
Go / build-and-release (push) Waiting to run
- Improved Cashu token handling and validation
- Better error messages for token verification
- Version bump to v0.51.0

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 21:54:03 +01:00
5 changed files with 49 additions and 13 deletions

View File

@@ -23,15 +23,27 @@ type CashuMintRequest struct {
}
// CashuMintResponse is the response body for token issuance.
// Field names match NIP-XX Cashu Access Tokens spec.
type CashuMintResponse struct {
BlindedSignature string `json:"blinded_signature"` // Hex-encoded blinded signature C_
KeysetID string `json:"keyset_id"` // Keyset ID used
Expiry int64 `json:"expiry"` // Token expiration timestamp
MintPubkey string `json:"mint_pubkey"` // Hex-encoded mint public key
MintPubkey string `json:"pubkey"` // Hex-encoded mint public key (spec: "pubkey")
}
// handleCashuMint handles POST /cashu/mint - issues a new token.
func (s *Server) handleCashuMint(w http.ResponseWriter, r *http.Request) {
// CORS headers for browser-based CAT token requests
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization")
// Handle preflight
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
// Check if Cashu is enabled
if s.CashuIssuer == nil {
log.W.F("Cashu mint request but issuer not initialized")
@@ -107,6 +119,17 @@ func (s *Server) handleCashuMint(w http.ResponseWriter, r *http.Request) {
// handleCashuKeysets handles GET /cashu/keysets - returns available keysets.
func (s *Server) handleCashuKeysets(w http.ResponseWriter, r *http.Request) {
// CORS headers for browser-based CAT support
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept")
// Handle preflight
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
if s.CashuIssuer == nil {
http.Error(w, "Cashu tokens not enabled", http.StatusNotImplemented)
return

View File

@@ -146,8 +146,9 @@ func (l *Listener) HandleEvent(msg []byte) (err error) {
// Require Cashu token for NIP-46 events when Cashu is enabled and ACL is active
const kindNIP46 = 24133
if env.E.Kind == kindNIP46 && l.CashuVerifier != nil && l.Config.ACLMode != "none" {
log.D.F("HandleEvent: NIP-46 event from %s, cashuToken=%v, ACLMode=%s", l.remote, l.cashuToken != nil, l.Config.ACLMode)
if l.cashuToken == nil {
log.W.F("HandleEvent: rejecting NIP-46 event - Cashu access token required")
log.W.F("HandleEvent: rejecting NIP-46 event from %s - Cashu access token required (connection has no token)", l.remote)
if err = Ok.Error(l, env, "restricted: NIP-46 requires Cashu access token"); chk.E(err) {
return
}

View File

@@ -309,10 +309,12 @@ func (s *Server) Pinger(
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")
log.D.F("ws %s: CAT extraction - query param token: %v", remote, tokenStr != "")
// Try X-Cashu-Token header
if tokenStr == "" {
tokenStr = r.Header.Get("X-Cashu-Token")
log.D.F("ws %s: CAT extraction - X-Cashu-Token header: %v", remote, tokenStr != "")
}
// Try Authorization: Cashu scheme
@@ -321,12 +323,15 @@ func (s *Server) extractWebSocketToken(r *http.Request, remote string) *token.To
if strings.HasPrefix(auth, "Cashu ") {
tokenStr = strings.TrimPrefix(auth, "Cashu ")
}
log.D.F("ws %s: CAT extraction - Authorization header: %v", remote, tokenStr != "")
}
// No token provided - this is fine, connection proceeds without token
if tokenStr == "" {
log.D.F("ws %s: CAT extraction - no token found", remote)
return nil
}
log.D.F("ws %s: CAT extraction - found token (len=%d)", remote, len(tokenStr))
// Parse the token
tok, err := token.Parse(tokenStr)

View File

@@ -3,6 +3,7 @@ package issuer
import (
"context"
"encoding/hex"
"errors"
"fmt"
"time"
@@ -222,22 +223,28 @@ func (i *Issuer) GetActiveKeysetID() string {
// MintInfo contains public information about the mint.
type MintInfo struct {
Name string `json:"name,omitempty"`
Version string `json:"version"`
TokenTTL int64 `json:"token_ttl"`
MaxKinds int `json:"max_kinds,omitempty"`
MaxKindRanges int `json:"max_kind_ranges,omitempty"`
Name string `json:"name,omitempty"`
Version string `json:"version"`
Pubkey string `json:"pubkey"`
TokenTTL int64 `json:"token_ttl"`
MaxKinds int `json:"max_kinds,omitempty"`
MaxKindRanges int `json:"max_kind_ranges,omitempty"`
SupportedScopes []string `json:"supported_scopes,omitempty"`
}
// GetMintInfo returns public information about the issuer.
func (i *Issuer) GetMintInfo(name string) MintInfo {
var pubkeyHex string
if ks := i.keysets.GetSigningKeyset(); ks != nil {
pubkeyHex = hex.EncodeToString(ks.SerializePublicKey())
}
return MintInfo{
Name: name,
Version: "NIP-XX/1",
TokenTTL: int64(i.config.DefaultTTL.Seconds()),
MaxKinds: i.config.MaxKinds,
MaxKindRanges: i.config.MaxKindRanges,
Name: name,
Version: "NIP-XX/1",
Pubkey: pubkeyHex,
TokenTTL: int64(i.config.DefaultTTL.Seconds()),
MaxKinds: i.config.MaxKinds,
MaxKindRanges: i.config.MaxKindRanges,
SupportedScopes: i.config.AllowedScopes,
}
}

View File

@@ -1 +1 @@
v0.50.1
v0.51.0