# Alertes

Une alerte part quand un évènement est confirmé : panne, rétablissement, expiration proche. Elle est envoyée vers chaque canal de l’organisation.

## Évènements

| Évènement         | Quand                                                                    |
| ----------------- | ------------------------------------------------------------------------ |
| `down`            | Une panne est confirmée, ou un heartbeat n’a pas signalé dans les délais |
| `up`              | Le moniteur est revenu en ligne après une panne                          |
| `tls_expiring`    | Le certificat expire dans le délai du rappel                             |
| `domain_expiring` | Le nom de domaine expire dans le délai du rappel                         |
| `test`            | Envoi manuel depuis la liste des canaux                                  |

Les rappels d’expiration partent aux seuils choisis dans le moniteur, puis à 7, 3 et 1 jour, une fois par seuil.

## Types de canaux

| Type                  | Cible                               | Notes                                                              |
| --------------------- | ----------------------------------- | ------------------------------------------------------------------ |
| **E-mail**            | Adresse                             | Envoyé depuis Status. Une adresse hors membres doit être confirmée |
| **Notification ntfy** | Adresse complète du sujet, en HTTPS | Le sujet est obligatoire dans l’adresse                            |
| **Webhook signé**     | Adresse HTTPS                       | Requête `POST` JSON, signée (voir ci-dessous)                      |

Une organisation compte 20 canaux au plus. Une cible doit être joignable depuis Internet ; une adresse privée ou locale est refusée. Une adresse ne contient jamais d’identifiants.

## Priorités ntfy

| Évènement                         | Priorité    | Étiquette              |
| --------------------------------- | ----------- | ---------------------- |
| `down`                            | 5 (urgente) | sirène                 |
| `up`                              | 3           | coche verte            |
| `tls_expiring`, `domain_expiring` | 3           | cadenas, avertissement |
| `test`                            | 3           | cloche                 |

Le titre et le message tiennent sur une ligne.

## Reprises

Un envoi échoue quand le webhook ou le sujet ntfy répond autre chose que 2xx (une redirection compte comme un échec : elle n’est pas suivie), quand il ne répond pas en 15 secondes ou quand le réseau coupe. Il est alors repris après 30 secondes, puis 2 minutes, 10 minutes, 30 minutes et 2 heures. Après la dernière reprise, l’envoi est abandonné. L’historique des envois est gardé 30 jours.

## Webhook signé

Chaque requête porte trois en-têtes, au format Standard Webhooks :

| En-tête             | Contenu                                         |
| ------------------- | ----------------------------------------------- |
| `webhook-id`        | Identifiant unique de l’envoi                   |
| `webhook-timestamp` | Heure d’envoi, en secondes depuis l’époque Unix |
| `webhook-signature` | `v1,` suivi de la signature en base64           |

La signature est un HMAC-SHA256, calculé avec la clé du **Secret de signature** sur la chaîne `<webhook-id>.<webhook-timestamp>.<corps brut>`.

Le secret commence par `whsec_`, suivi de la clé en base64. Le préfixe est retiré avant le calcul.

### Vérifier la signature

```js
import { createHmac, timingSafeEqual } from "node:crypto"

export function verifierWebhook(secret, headers, corps) {
  const cle = Buffer.from(secret.replace(/^whsec_/, ""), "base64")
  const message = `${headers["webhook-id"]}.${headers["webhook-timestamp"]}.${corps}`
  const attendu = createHmac("sha256", cle).update(message).digest("base64")
  const recu = headers["webhook-signature"].split(" ").find((s) => s.startsWith("v1,"))?.slice(3) ?? ""
  return recu.length === attendu.length && timingSafeEqual(Buffer.from(recu), Buffer.from(attendu))
}
```

Vérifie la signature sur le corps brut, avant tout parsing JSON. Refuse aussi les requêtes dont `webhook-timestamp` est trop ancien : une marge de quelques minutes est courante.

Ton point d’arrivée répond `2xx` pour acquitter l’envoi. Toute autre réponse déclenche une reprise.
