Files
next.orly.dev/app/handle-event.go
mleku aef9e24e40
Some checks failed
Go / build-and-release (push) Has been cancelled
Require CAT for NIP-46 bunker connections (v0.42.0)
- Enforce Cashu access token for kind 24133 events when Cashu is enabled and ACL is active
- Reject NIP-46 events without valid token with "restricted: NIP-46 requires Cashu access token"
- Verify token scope is NIP-46 or RELAY

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 19:31:39 +02:00

327 lines
9.6 KiB
Go

package app
import (
"context"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"next.orly.dev/pkg/acl"
"next.orly.dev/pkg/cashu/token"
"next.orly.dev/pkg/event/routing"
"git.mleku.dev/mleku/nostr/encoders/envelopes/authenvelope"
"git.mleku.dev/mleku/nostr/encoders/envelopes/eventenvelope"
"git.mleku.dev/mleku/nostr/encoders/envelopes/noticeenvelope"
"git.mleku.dev/mleku/nostr/encoders/envelopes/okenvelope"
"git.mleku.dev/mleku/nostr/encoders/hex"
"git.mleku.dev/mleku/nostr/encoders/kind"
"git.mleku.dev/mleku/nostr/encoders/reason"
"next.orly.dev/pkg/protocol/nip43"
)
func (l *Listener) HandleEvent(msg []byte) (err error) {
log.D.F("HandleEvent: START handling event: %s", msg)
// 1. Raw JSON validation (before unmarshal) - use validation service
if result := l.eventValidator.ValidateRawJSON(msg); !result.Valid {
log.W.F("HandleEvent: rejecting event with validation error: %s", result.Msg)
// Send NOTICE to alert client developers about the issue
if noticeErr := noticeenvelope.NewFrom(result.Msg).Write(l); noticeErr != nil {
log.E.F("failed to send NOTICE for validation error: %v", noticeErr)
}
// Send OK false with the error message
if err = l.sendRawValidationError(result); chk.E(err) {
return
}
return nil
}
// decode the envelope
env := eventenvelope.NewSubmission()
log.I.F("HandleEvent: received event message length: %d", len(msg))
if msg, err = env.Unmarshal(msg); chk.E(err) {
log.E.F("HandleEvent: failed to unmarshal event: %v", err)
return
}
log.I.F(
"HandleEvent: successfully unmarshaled event, kind: %d, pubkey: %s, id: %0x",
env.E.Kind, hex.Enc(env.E.Pubkey), env.E.ID,
)
defer func() {
if env != nil && env.E != nil {
env.E.Free()
}
}()
if len(msg) > 0 {
log.I.F("extra '%s'", msg)
}
// Check if sprocket is enabled and process event through it
if l.sprocketManager != nil && l.sprocketManager.IsEnabled() {
if l.sprocketManager.IsDisabled() {
// Sprocket is disabled due to failure - reject all events
log.W.F("sprocket is disabled, rejecting event %0x", env.E.ID)
if err = Ok.Error(
l, env,
"sprocket disabled - events rejected until sprocket is restored",
); chk.E(err) {
return
}
return
}
if !l.sprocketManager.IsRunning() {
// Sprocket is enabled but not running - reject all events
log.W.F(
"sprocket is enabled but not running, rejecting event %0x",
env.E.ID,
)
if err = Ok.Error(
l, env,
"sprocket not running - events rejected until sprocket starts",
); chk.E(err) {
return
}
return
}
// Process event through sprocket
response, sprocketErr := l.sprocketManager.ProcessEvent(env.E)
if chk.E(sprocketErr) {
log.E.F("sprocket processing failed: %v", sprocketErr)
if err = Ok.Error(
l, env, "sprocket processing failed",
); chk.E(err) {
return
}
return
}
// Handle sprocket response
switch response.Action {
case "accept":
// Continue with normal processing
log.D.F("sprocket accepted event %0x", env.E.ID)
case "reject":
// Return OK false with message
if err = okenvelope.NewFrom(
env.Id(), false,
reason.Error.F(response.Msg),
).Write(l); chk.E(err) {
return
}
return
case "shadowReject":
// Return OK true but abort processing
if err = Ok.Ok(l, env, ""); chk.E(err) {
return
}
log.D.F("sprocket shadow rejected event %0x", env.E.ID)
return
default:
log.W.F("unknown sprocket action: %s", response.Action)
// Default to accept for unknown actions
}
}
// Event validation (ID, timestamp, signature) - use validation service
if result := l.eventValidator.ValidateEvent(env.E); !result.Valid {
if err = l.sendValidationError(env, result); chk.E(err) {
return
}
return
}
// Check Cashu token kind permissions if a token was provided
if l.cashuToken != nil && !l.cashuToken.IsKindPermitted(int(env.E.Kind)) {
log.W.F("HandleEvent: rejecting event kind %d - not permitted by Cashu token", env.E.Kind)
if err = Ok.Error(l, env, "event kind not permitted by access token"); chk.E(err) {
return
}
return
}
// 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" {
if l.cashuToken == nil {
log.W.F("HandleEvent: rejecting NIP-46 event - Cashu access token required")
if err = Ok.Error(l, env, "restricted: NIP-46 requires Cashu access token"); chk.E(err) {
return
}
return
}
// Also verify the token has NIP-46 scope
if l.cashuToken.Scope != token.ScopeNIP46 && l.cashuToken.Scope != token.ScopeRelay {
log.W.F("HandleEvent: rejecting NIP-46 event - token scope %q not valid for NIP-46", l.cashuToken.Scope)
if err = Ok.Error(l, env, "restricted: access token scope not valid for NIP-46"); chk.E(err) {
return
}
return
}
}
// Handle NIP-43 special events before ACL checks
switch env.E.Kind {
case nip43.KindJoinRequest:
// Process join request and return early
if err = l.HandleNIP43JoinRequest(env.E); chk.E(err) {
log.E.F("failed to process NIP-43 join request: %v", err)
}
return
case nip43.KindLeaveRequest:
// Process leave request and return early
if err = l.HandleNIP43LeaveRequest(env.E); chk.E(err) {
log.E.F("failed to process NIP-43 leave request: %v", err)
}
return
case kind.PolicyConfig.K:
// Handle policy configuration update events (kind 12345)
// Only policy admins can update policy configuration
if err = l.HandlePolicyConfigUpdate(env.E); chk.E(err) {
log.E.F("failed to process policy config update: %v", err)
if err = Ok.Error(l, env, err.Error()); chk.E(err) {
return
}
return
}
// Send OK response
if err = Ok.Ok(l, env, "policy configuration updated"); chk.E(err) {
return
}
return
case kind.FollowList.K:
// Check if this is a follow list update from a policy admin
// If so, refresh the policy follows cache immediately
if l.IsPolicyAdminFollowListEvent(env.E) {
// Process the follow list update (async, don't block)
go func() {
if updateErr := l.HandlePolicyAdminFollowListUpdate(env.E); updateErr != nil {
log.W.F("failed to update policy follows from admin follow list: %v", updateErr)
}
}()
}
// Continue with normal follow list processing (store the event)
}
// Authorization check (policy + ACL) - use authorization service
decision := l.eventAuthorizer.Authorize(env.E, l.authedPubkey.Load(), l.remote, env.E.Kind)
if !decision.Allowed {
log.D.F("HandleEvent: authorization denied: %s (requireAuth=%v)", decision.DenyReason, decision.RequireAuth)
if decision.RequireAuth {
// Send OK false with reason
if err = okenvelope.NewFrom(
env.Id(), false,
reason.AuthRequired.F(decision.DenyReason),
).Write(l); chk.E(err) {
return
}
// Send AUTH challenge
if err = authenvelope.NewChallengeWith(l.challenge.Load()).Write(l); chk.E(err) {
return
}
} else {
// Send OK false with blocked reason
if err = Ok.Blocked(l, env, decision.DenyReason); chk.E(err) {
return
}
}
return
}
log.I.F("HandleEvent: authorized with access level %s", decision.AccessLevel)
// Route special event kinds (ephemeral, etc.) - use routing service
if routeResult := l.eventRouter.Route(env.E, l.authedPubkey.Load()); routeResult.Action != routing.Continue {
if routeResult.Action == routing.Handled {
// Event fully handled by router, send OK and return
log.D.F("event %0x handled by router", env.E.ID)
if err = Ok.Ok(l, env, routeResult.Message); chk.E(err) {
return
}
return
} else if routeResult.Action == routing.Error {
// Router encountered an error
if err = l.sendRoutingError(env, routeResult); chk.E(err) {
return
}
return
}
}
log.D.F("processing regular event %0x (kind %d)", env.E.ID, env.E.Kind)
// NIP-70 protected tag validation - use validation service
if acl.Registry.Active.Load() != "none" {
if result := l.eventValidator.ValidateProtectedTag(env.E, l.authedPubkey.Load()); !result.Valid {
if err = l.sendValidationError(env, result); chk.E(err) {
return
}
return
}
}
// Handle delete events specially - save first, then process deletions
if env.E.Kind == kind.EventDeletion.K {
log.I.F("processing delete event %0x", env.E.ID)
// Save and deliver using processing service
result := l.eventProcessor.Process(context.Background(), env.E)
if result.Blocked {
if err = Ok.Error(l, env, result.BlockMsg); chk.E(err) {
return
}
return
}
if result.Error != nil {
chk.E(result.Error)
return
}
// Process deletion targets (remove referenced events)
if err = l.HandleDelete(env); err != nil {
log.W.F("HandleDelete failed for event %0x: %v", env.E.ID, err)
}
if err = Ok.Ok(l, env, ""); chk.E(err) {
return
}
log.D.F("processed delete event %0x", env.E.ID)
return
}
// Process event: save, run hooks, and deliver to subscribers
result := l.eventProcessor.Process(context.Background(), env.E)
if result.Blocked {
if err = Ok.Error(l, env, result.BlockMsg); chk.E(err) {
return
}
return
}
if result.Error != nil {
chk.E(result.Error)
return
}
// Send success response
if err = Ok.Ok(l, env, ""); chk.E(err) {
return
}
log.D.F("saved event %0x", env.E.ID)
return
}
// isPeerRelayPubkey checks if the given pubkey belongs to a peer relay
func (l *Listener) isPeerRelayPubkey(pubkey []byte) bool {
if l.syncManager == nil {
return false
}
peerPubkeyHex := hex.Enc(pubkey)
// Check if this pubkey matches any of our configured peer relays' NIP-11 pubkeys
for _, peerURL := range l.syncManager.GetPeers() {
if l.syncManager.IsAuthorizedPeer(peerURL, peerPubkeyHex) {
return true
}
}
return false
}