From d1bc98d0f85b3299de95ffac70eea2595ba65e68 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 15:06:17 +0000 Subject: [PATCH] Add signevent method to NIP-86 (nip86 + khatru) Implements the signevent capability from nostr-protocol/nips#2389: relay admins can ask the relay to sign an unsigned event template with its own (self) key. - nip86: SignEvent MethodParams type + "signevent" decode case (round-trips the {kind, content, tags, created_at} param into a nostr.Event). - khatru: RelayManagementAPI.SignEvent hook + dispatch returning the signed event as the result. --- khatru/nip86.go | 9 +++++++++ nip86/methods.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/khatru/nip86.go b/khatru/nip86.go index 59108b3..70456c1 100644 --- a/khatru/nip86.go +++ b/khatru/nip86.go @@ -43,6 +43,7 @@ type RelayManagementAPI struct { Stats func(ctx context.Context) (nip86.Response, error) GrantAdmin func(ctx context.Context, pubkey nostr.PubKey, methods []string) error RevokeAdmin func(ctx context.Context, pubkey nostr.PubKey, methods []string) error + SignEvent func(ctx context.Context, event nostr.Event) (nostr.Event, error) Generic func(ctx context.Context, request nip86.Request) (nip86.Response, error) } @@ -346,6 +347,14 @@ func (rl *Relay) HandleNIP86(w http.ResponseWriter, r *http.Request) { } else { resp.Result = result } + case nip86.SignEvent: + if rl.ManagementAPI.SignEvent == nil { + resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName()) + } else if result, err := rl.ManagementAPI.SignEvent(ctx, thing.Event); err != nil { + resp.Error = err.Error() + } else { + resp.Result = result + } default: if rl.ManagementAPI.Generic == nil { resp.Error = fmt.Sprintf("method '%s' not known", mp.MethodName()) diff --git a/nip86/methods.go b/nip86/methods.go index ba06914..a942bb8 100644 --- a/nip86/methods.go +++ b/nip86/methods.go @@ -1,6 +1,7 @@ package nip86 import ( + "encoding/json" "fmt" "math" "net" @@ -242,6 +243,25 @@ func DecodeRequest(req Request) (MethodParams, error) { }, nil case "stats": return Stats{}, nil + case "signevent": + if len(req.Params) == 0 { + return nil, fmt.Errorf("invalid number of params for '%s'", req.Method) + } + + // params[0] is an unsigned event template {kind, content, tags, + // created_at}; it arrives as a decoded JSON value, so round-trip it + // through JSON into an Event. + evtJSON, err := json.Marshal(req.Params[0]) + if err != nil { + return nil, fmt.Errorf("invalid event param for '%s'", req.Method) + } + + var evt nostr.Event + if err := json.Unmarshal(evtJSON, &evt); err != nil { + return nil, fmt.Errorf("invalid event param for '%s'", req.Method) + } + + return SignEvent{Event: evt}, nil default: return nil, fmt.Errorf("unknown method '%s'", req.Method) } @@ -277,6 +297,7 @@ var ( _ MethodParams = (*GrantAdmin)(nil) _ MethodParams = (*RevokeAdmin)(nil) _ MethodParams = (*Stats)(nil) + _ MethodParams = (*SignEvent)(nil) ) type SupportedMethods struct{} @@ -418,3 +439,11 @@ func (RevokeAdmin) MethodName() string { return "revokeadmin" } type Stats struct{} func (Stats) MethodName() string { return "stats" } + +// SignEvent asks the relay to sign an unsigned event template with its own +// (self) key and return the full signed event. See NIP-86 `signevent`. +type SignEvent struct { + Event nostr.Event +} + +func (SignEvent) MethodName() string { return "signevent" }