Files
sencho/backend/src/helpers/notificationChannels.ts
T
Anso 7d4e61625f fix(notifications): harden alert dispatch crash-safety and redact webhook secrets in logs (#1255)
* fix(notifications): harden alert dispatch crash-safety and redact webhook secrets in logs

Make NotificationService.dispatchAlert never reject so the many
fire-and-forget callers (monitors, event streams, policy and image-update
paths) cannot trigger an unhandledRejection on an unhealthy database. The
whole dispatch body now sits inside a guard covering node resolution, the
history insert, channel-table reads, and the WebSocket broadcast; a
failure logs and drops the notification instead of crashing the process.
An inner guard still splits the write-success and write-failure metrics.

Also:
- Redact webhook URLs in diagnostic logs via a new maskWebhookUrl helper;
  Discord/Slack/custom webhook URLs embed their token in the path, so only
  the origin is safe to emit.
- Add error logging to four notification-history route handlers that
  previously swallowed database errors silently before returning 500.
- Snapshot the subscriber set before broadcasting so a close/error handler
  firing mid-send cannot mutate the set during iteration.
- Sanitize the admin-supplied route name in dispatch log lines.

Adds tests for dispatch crash-safety (write failure, post-write routing
failure, broadcast send failure), the success-path write metric, webhook
URL masking, and history-route error logging.

* fix(notifications): sanitize route name and patterns in create logs

Apply sanitizeForLog to the admin-supplied route name and stack patterns
in the route-creation log lines, matching the dispatch-site sanitization
and closing the remaining log-injection path on this feature.

Also extend tests: userinfo-stripping in maskWebhookUrl, subscriber-set
snapshot behavior under an unsubscribe-during-send, and error logging on
the mark-read, delete-one, and clear-all notification-history handlers.
2026-05-29 21:09:57 -04:00

31 lines
1.3 KiB
TypeScript

export const NOTIFICATION_CHANNEL_TYPES = ['discord', 'slack', 'webhook'] as const;
export type NotificationChannelType = typeof NOTIFICATION_CHANNEL_TYPES[number];
export const cleanStackPatterns = (patterns: string[]): string[] =>
[...new Set(patterns.map(p => p.trim()).filter(Boolean))];
export function validateHttpsUrl(value: unknown): string | null {
if (!value || typeof value !== 'string' || !value.startsWith('https://')) return 'must be a valid HTTPS URL';
try { new URL(value); } catch { return 'is not a valid URL'; }
return null;
}
/**
* Mask a channel webhook URL for logging. Discord/Slack/custom webhook URLs
* embed their auth token in the path (and sometimes the query), so only the
* origin is safe to emit. Returns `https://host/<redacted>` or a generic
* placeholder if the value is not a parseable URL.
*/
export function maskWebhookUrl(value: unknown): string {
if (typeof value !== 'string' || value.length === 0) return '<no url>';
try {
const { origin, pathname, search } = new URL(value);
// pathname is normalized to '/' for an origin-only URL, so anything else
// (or any query string) is a token-bearing segment we must not log.
const hasSecret = pathname !== '/' || search !== '';
return hasSecret ? `${origin}/<redacted>` : origin;
} catch {
return '<invalid url>';
}
}