0a2eb203ee
AlertEvent sans secret ; HttpAlertSender (POST webhook n8n, best-effort) + NullAlertSender ; buildDailyDigest / selectExpiringSoon purs. 6 tests. Rattrape l'entrée CHANGELOG 0.15.1 omise. Palier de risque : reversible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
48 lines
1.6 KiB
TypeScript
48 lines
1.6 KiB
TypeScript
import type { Logger } from "pino";
|
|
import type { AlertEvent, AlertSender } from "./types.js";
|
|
|
|
/**
|
|
* Envoie les alertes à un webhook n8n (qui envoie le mail). Fail-soft : une
|
|
* panne d'alerte ne doit jamais casser la boucle agent ni le gatekeeper — on
|
|
* logge et on continue.
|
|
*/
|
|
export class HttpAlertSender implements AlertSender {
|
|
constructor(
|
|
private readonly webhookUrl: string,
|
|
private readonly logger: Logger,
|
|
private readonly timeoutMs = 10_000,
|
|
) {}
|
|
|
|
async send(event: AlertEvent): Promise<void> {
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
try {
|
|
const res = await fetch(this.webhookUrl, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ source: "chlova", ts: Date.now(), ...event }),
|
|
signal: controller.signal,
|
|
});
|
|
if (!res.ok) {
|
|
this.logger.warn({ kind: event.kind, status: res.status }, "alerte: HTTP non-OK");
|
|
}
|
|
} catch (err) {
|
|
// Jamais propagé : l'alerte est best-effort.
|
|
this.logger.warn(
|
|
{ kind: event.kind, err: err instanceof Error ? err.message : String(err) },
|
|
"alerte: envoi échoué (best-effort)",
|
|
);
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Sender inactif : aucun webhook configuré → on logge seulement (fail-safe). */
|
|
export class NullAlertSender implements AlertSender {
|
|
constructor(private readonly logger: Logger) {}
|
|
async send(event: AlertEvent): Promise<void> {
|
|
this.logger.info({ alert: event }, "alerte (non envoyée : pas de webhook)");
|
|
}
|
|
}
|