3 Commits

Author SHA1 Message Date
Claude d1bc98d0f8 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.
2026-06-20 15:06:17 +00:00
Jon Staab fefc85d500 Add unbanpubkey and unallowpubkey 2026-05-05 11:36:42 -07:00
Jon Staab fbc805a5a6 Switch supported_nips to strings 2026-05-05 11:36:42 -07:00
8 changed files with 138 additions and 30 deletions
+2 -2
View File
@@ -167,14 +167,14 @@ func (rl *Relay) StartExpirationManager(
}
go rl.expirationManager.start(rl.ctx)
rl.Info.AddSupportedNIP(40)
rl.Info.AddSupportedNIP("40")
}
func (rl *Relay) DisableExpirationManager() {
rl.expirationManager.stop()
rl.expirationManager = nil
idx := slices.Index(rl.Info.SupportedNIPs, 40)
idx := slices.Index(rl.Info.SupportedNIPs, "40")
if idx != -1 {
rl.Info.SupportedNIPs[idx] = rl.Info.SupportedNIPs[len(rl.Info.SupportedNIPs)-1]
rl.Info.SupportedNIPs = rl.Info.SupportedNIPs[0 : len(rl.Info.SupportedNIPs)-1]
+1 -1
View File
@@ -31,7 +31,7 @@ func New(rl *khatru.Relay, repositoryDir string) *GraspServer {
},
}
rl.Info.AddSupportedNIP(34)
rl.Info.AddSupportedNIP("34")
rl.Info.SupportedGrasps = append(rl.Info.SupportedGrasps, "GRASP-01")
base := rl.Router()
+3 -3
View File
@@ -12,13 +12,13 @@ func (rl *Relay) HandleNIP11(w http.ResponseWriter, r *http.Request) {
info := *rl.Info
if nil != rl.DeleteEvent {
info.AddSupportedNIP(9)
info.AddSupportedNIP("9")
}
if nil != rl.Count {
info.AddSupportedNIP(45)
info.AddSupportedNIP("45")
}
if rl.Negentropy {
info.AddSupportedNIP(77)
info.AddSupportedNIP("77")
}
// resolve relative icon and banner URLs against base URL
+27
View File
@@ -21,8 +21,10 @@ type RelayManagementAPI struct {
BanPubKey func(ctx context.Context, pubkey nostr.PubKey, reason string) error
ListBannedPubKeys func(ctx context.Context) ([]nip86.PubKeyReason, error)
UnbanPubKey func(ctx context.Context, pubkey nostr.PubKey, reason string) error
AllowPubKey func(ctx context.Context, pubkey nostr.PubKey, reason string) error
ListAllowedPubKeys func(ctx context.Context) ([]nip86.PubKeyReason, error)
UnallowPubKey func(ctx context.Context, pubkey nostr.PubKey, reason string) error
ListEventsNeedingModeration func(ctx context.Context) ([]nip86.IDReason, error)
AllowEvent func(ctx context.Context, id nostr.ID, reason string) error
BanEvent func(ctx context.Context, id nostr.ID, reason string) error
@@ -41,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)
}
@@ -168,6 +171,14 @@ func (rl *Relay) HandleNIP86(w http.ResponseWriter, r *http.Request) {
} else {
resp.Result = result
}
case nip86.UnbanPubKey:
if rl.ManagementAPI.UnbanPubKey == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
} else if err := rl.ManagementAPI.UnbanPubKey(ctx, thing.PubKey, thing.Reason); err != nil {
resp.Error = err.Error()
} else {
resp.Result = true
}
case nip86.AllowPubKey:
if rl.ManagementAPI.AllowPubKey == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
@@ -184,6 +195,14 @@ func (rl *Relay) HandleNIP86(w http.ResponseWriter, r *http.Request) {
} else {
resp.Result = result
}
case nip86.UnallowPubKey:
if rl.ManagementAPI.UnallowPubKey == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
} else if err := rl.ManagementAPI.UnallowPubKey(ctx, thing.PubKey, thing.Reason); err != nil {
resp.Error = err.Error()
} else {
resp.Result = true
}
case nip86.BanEvent:
if rl.ManagementAPI.BanEvent == nil {
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
@@ -328,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())
+1 -1
View File
@@ -33,7 +33,7 @@ func NewRelay() *Relay {
Info: &nip11.RelayInformationDocument{
Software: "https://pkg.go.dev/fiatjaf.com/nostr/khatru",
Version: "n/a",
SupportedNIPs: []any{1, 11, 42, 70, 86},
SupportedNIPs: []string{"1", "11", "42", "70", "86"},
},
upgrader: websocket.Upgrader{
+18 -18
View File
@@ -9,30 +9,30 @@ import (
func TestAddSupportedNIP(t *testing.T) {
info := RelayInformationDocument{}
info.AddSupportedNIP(12)
info.AddSupportedNIP(12)
info.AddSupportedNIP(13)
info.AddSupportedNIP(1)
info.AddSupportedNIP(12)
info.AddSupportedNIP(44)
info.AddSupportedNIP(2)
info.AddSupportedNIP(13)
info.AddSupportedNIP(2)
info.AddSupportedNIP(13)
info.AddSupportedNIP(0)
info.AddSupportedNIP(17)
info.AddSupportedNIP(19)
info.AddSupportedNIP(1)
info.AddSupportedNIP(18)
info.AddSupportedNIP("12")
info.AddSupportedNIP("12")
info.AddSupportedNIP("13")
info.AddSupportedNIP("1")
info.AddSupportedNIP("12")
info.AddSupportedNIP("44")
info.AddSupportedNIP("2")
info.AddSupportedNIP("13")
info.AddSupportedNIP("2")
info.AddSupportedNIP("13")
info.AddSupportedNIP("0")
info.AddSupportedNIP("17")
info.AddSupportedNIP("19")
info.AddSupportedNIP("1")
info.AddSupportedNIP("18")
assert.Contains(t, info.SupportedNIPs, 0, 1, 2, 12, 13, 17, 18, 19, 44)
assert.Contains(t, info.SupportedNIPs, "0", "1", "2", "12", "13", "17", "18", "19", "44")
}
func TestAddSupportedNIPs(t *testing.T) {
info := RelayInformationDocument{}
info.AddSupportedNIPs([]int{0, 1, 2, 12, 13, 17, 18, 19, 44})
info.AddSupportedNIPs([]int{"0", "1", "2", "12", "13", "17", "18", "19", "44"})
assert.Contains(t, info.SupportedNIPs, 0, 1, 2, 12, 13, 17, 18, 19, 44)
assert.Contains(t, info.SupportedNIPs, "0", "1", "2", "12", "13", "17", "18", "19", "44")
}
func TestFetch(t *testing.T) {
+5 -5
View File
@@ -14,7 +14,7 @@ type RelayInformationDocument struct {
PubKey *nostr.PubKey `json:"pubkey,omitempty"`
Self *nostr.PubKey `json:"self,omitempty"`
Contact string `json:"contact,omitempty"`
SupportedNIPs []any `json:"supported_nips,omitempty"`
SupportedNIPs []string `json:"supported_nips,omitempty"`
Software string `json:"software,omitempty"`
Version string `json:"version,omitempty"`
@@ -33,16 +33,16 @@ type RelayInformationDocument struct {
SupportedGrasps []string `json:"supported_grasps,omitempty"`
}
func (info *RelayInformationDocument) AddSupportedNIP(number int) {
idx := slices.IndexFunc(info.SupportedNIPs, func(n any) bool { return n == number })
func (info *RelayInformationDocument) AddSupportedNIP(nip string) {
idx := slices.IndexFunc(info.SupportedNIPs, func(n string) bool { return n == nip })
if idx != -1 {
return
}
info.SupportedNIPs = append(info.SupportedNIPs, number)
info.SupportedNIPs = append(info.SupportedNIPs, nip)
}
func (info *RelayInformationDocument) AddSupportedNIPs(numbers []int) {
func (info *RelayInformationDocument) AddSupportedNIPs(numbers []string) {
for _, n := range numbers {
info.AddSupportedNIP(n)
}
+81
View File
@@ -1,6 +1,7 @@
package nip86
import (
"encoding/json"
"fmt"
"math"
"net"
@@ -32,6 +33,24 @@ func DecodeRequest(req Request) (MethodParams, error) {
return BanPubKey{pk, reason}, nil
case "listbannedpubkeys":
return ListBannedPubKeys{}, nil
case "unbanpubkey":
if len(req.Params) == 0 {
return nil, fmt.Errorf("invalid number of params for '%s'", req.Method)
}
pkh, ok := req.Params[0].(string)
if !ok {
return nil, fmt.Errorf("missing pubkey param for '%s'", req.Method)
}
pk, err := nostr.PubKeyFromHex(pkh)
if err != nil {
return nil, fmt.Errorf("invalid pubkey param for '%s'", req.Method)
}
var reason string
if len(req.Params) >= 2 {
reason, _ = req.Params[1].(string)
}
return UnbanPubKey{pk, reason}, nil
case "allowpubkey":
if len(req.Params) == 0 {
return nil, fmt.Errorf("invalid number of params for '%s'", req.Method)
@@ -52,6 +71,24 @@ func DecodeRequest(req Request) (MethodParams, error) {
return AllowPubKey{pk, reason}, nil
case "listallowedpubkeys":
return ListAllowedPubKeys{}, nil
case "unallowpubkey":
if len(req.Params) == 0 {
return nil, fmt.Errorf("invalid number of params for '%s'", req.Method)
}
pkh, ok := req.Params[0].(string)
if !ok {
return nil, fmt.Errorf("missing pubkey param for '%s'", req.Method)
}
pk, err := nostr.PubKeyFromHex(pkh)
if err != nil {
return nil, fmt.Errorf("invalid pubkey param for '%s'", req.Method)
}
var reason string
if len(req.Params) >= 2 {
reason, _ = req.Params[1].(string)
}
return UnallowPubKey{pk, reason}, nil
case "listeventsneedingmoderation":
return ListEventsNeedingModeration{}, nil
case "allowevent":
@@ -206,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)
}
@@ -219,8 +275,10 @@ var (
_ MethodParams = (*SupportedMethods)(nil)
_ MethodParams = (*BanPubKey)(nil)
_ MethodParams = (*ListBannedPubKeys)(nil)
_ MethodParams = (*UnbanPubKey)(nil)
_ MethodParams = (*AllowPubKey)(nil)
_ MethodParams = (*ListAllowedPubKeys)(nil)
_ MethodParams = (*UnallowPubKey)(nil)
_ MethodParams = (*ListEventsNeedingModeration)(nil)
_ MethodParams = (*AllowEvent)(nil)
_ MethodParams = (*BanEvent)(nil)
@@ -239,6 +297,7 @@ var (
_ MethodParams = (*GrantAdmin)(nil)
_ MethodParams = (*RevokeAdmin)(nil)
_ MethodParams = (*Stats)(nil)
_ MethodParams = (*SignEvent)(nil)
)
type SupportedMethods struct{}
@@ -256,6 +315,13 @@ type ListBannedPubKeys struct{}
func (ListBannedPubKeys) MethodName() string { return "listbannedpubkeys" }
type UnbanPubKey struct {
PubKey nostr.PubKey
Reason string
}
func (UnbanPubKey) MethodName() string { return "unbanpubkey" }
type AllowPubKey struct {
PubKey nostr.PubKey
Reason string
@@ -267,6 +333,13 @@ type ListAllowedPubKeys struct{}
func (ListAllowedPubKeys) MethodName() string { return "listallowedpubkeys" }
type UnallowPubKey struct {
PubKey nostr.PubKey
Reason string
}
func (UnallowPubKey) MethodName() string { return "unallowpubkey" }
type ListEventsNeedingModeration struct{}
func (ListEventsNeedingModeration) MethodName() string { return "listeventsneedingmoderation" }
@@ -366,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" }