package auth import ( "encoding/base64" "encoding/json" "fmt" "strings" "time" "northwest.io/nostr" ) type ValidationOptions struct { TimestampWindow int64 ValidatePayload bool ExpectedURI string ExpectedMethod string PayloadHash string } // ParseAuthHeader parses "Nostr " format. func ParseAuthHeader(header string) (*nostr.Event, error) { if header == "" { return nil, fmt.Errorf("empty authorization header") } if !strings.HasPrefix(header, "Nostr ") { return nil, fmt.Errorf("invalid authorization header: must start with 'Nostr '") } encoded := strings.TrimPrefix(header, "Nostr ") if encoded == "" { return nil, fmt.Errorf("empty authorization token") } decoded, err := base64.StdEncoding.DecodeString(encoded) if err != nil { return nil, fmt.Errorf("invalid base64 encoding: %w", err) } var event nostr.Event if err := json.Unmarshal(decoded, &event); err != nil { return nil, fmt.Errorf("invalid event JSON: %w", err) } return &event, nil } func ValidateAuthEvent(event *nostr.Event, opts ValidationOptions) error { if event.Kind != 27235 { return fmt.Errorf("invalid event kind: expected 27235, got %d", event.Kind) } if !event.Verify() { return fmt.Errorf("invalid event signature") } now := time.Now().Unix() age := now - event.CreatedAt if age < 0 { return fmt.Errorf("event timestamp is in the future") } if opts.TimestampWindow > 0 && age > opts.TimestampWindow { return fmt.Errorf("event too old: %d seconds (max %d)", age, opts.TimestampWindow) } if opts.ExpectedURI != "" { uTag := event.Tags.Find("u") if uTag == nil { return fmt.Errorf("missing 'u' tag in auth event") } eventURI := uTag.Value() if eventURI != opts.ExpectedURI { return fmt.Errorf("URI mismatch: expected %s, got %s", opts.ExpectedURI, eventURI) } } if opts.ExpectedMethod != "" { methodTag := event.Tags.Find("method") if methodTag == nil { return fmt.Errorf("missing 'method' tag in auth event") } eventMethod := methodTag.Value() if eventMethod != opts.ExpectedMethod { return fmt.Errorf("method mismatch: expected %s, got %s", opts.ExpectedMethod, eventMethod) } } if opts.ValidatePayload && opts.PayloadHash != "" { payloadTag := event.Tags.Find("payload") if payloadTag == nil { return fmt.Errorf("missing 'payload' tag in auth event") } eventHash := payloadTag.Value() if eventHash != opts.PayloadHash { return fmt.Errorf("payload hash mismatch") } } return nil } func ExtractPubkey(event *nostr.Event) string { return event.PubKey }