package audit import ( "context" "fmt" "net/http" "strings" "time" ) // NtfyNotifier posts alerts to an ntfy topic URL. Used to surface a // degraded audit state (records buffered locally during a loki outage). type NtfyNotifier struct { topicURL string token string http *http.Client } // NewNtfyNotifier constructs a notifier for the given ntfy topic URL // (e.g. https://ntfy.sh/my-topic). token is an optional bearer for // protected ntfy instances; it is held here and only sent in the // Authorization header, never logged. Returns nil when topicURL is empty. func NewNtfyNotifier(topicURL, token string) *NtfyNotifier { if topicURL == "" { return nil } return &NtfyNotifier{ topicURL: strings.TrimRight(topicURL, "/"), token: token, http: &http.Client{Timeout: 10 * time.Second}, } } // Notify posts a message to the ntfy topic. func (n *NtfyNotifier) Notify(ctx context.Context, msg string) error { req, err := http.NewRequestWithContext(ctx, http.MethodPost, n.topicURL, strings.NewReader(msg)) if err != nil { return err } req.Header.Set("Title", "brain-capture audit degraded") req.Header.Set("Priority", "high") req.Header.Set("Tags", "warning,brain") if n.token != "" { req.Header.Set("Authorization", "Bearer "+n.token) } resp, err := n.http.Do(req) if err != nil { return fmt.Errorf("ntfy notify: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("ntfy notify: status %d", resp.StatusCode) } return nil }