Commit f019b09b authored by Kourser's avatar Kourser
Browse files

Le contrôle du relais retrouve les noms qu'il désigne

« pnpm verif » s'arrêtait avant la moitié de la suite, sur un module
introuvable. Derrière, quatre défauts du même commit — cc663d8d — et le même
accident : un renommage passé plus large qu'il ne devait, qui a mordu au-delà
des identifiants qu'il visait.

Ce que le renommage a emporté
  L'import. verification/relais.ts appelait apps/relais/src/chargeJwt.ts, qui
  n'a jamais existé : composerFcm et CANAUX_ANDROID vivent dans charge.ts. Un
  renommage de la variable locale « charge » en « chargeJwt » avait suivi la
  chaîne jusque dans le chemin du module.

  Le champ de clé. Le contrat de fcm.ts dit « clePem » ; l'appel construisait
  « clePemFcm ». tsc n'y voyait rien : un import dynamique n'est vérifié que
  contre la forme qu'on lui déclare à la main, et cette forme portait la même
  faute. Le contrôle passait donc la compilation pour échouer à l'exécution,
  sur une clé indéfinie.

  Des valeurs, et non plus des noms. « urgence: "mobAndroid" » et
  « urgence: "infoAndroid" » n'appartiennent pas au type Urgence :
  composerFcm retombait sur la branche qui ne réveille pas, et trois
  garanties de priorité et d'expiration échouaient en désignant un défaut qui
  n'était pas là. Les clés de CANAUX_ANDROID avaient subi le même sort.

  Et les libellés, qui sont la sortie lue par un humain : « une mobAndroid
  part en priorité haute » ne veut rien dire.

Une assertion périmée, d'une autre nature
  EM-85 exigeait un 501 — « Android n'est pas encore servi ». C'est le commit
  qui a ajouté FCM qui a laissé cette phrase : il servait Android et
  continuait d'affirmer le contraire.

  Ce qui reste à contrôler n'est plus qu'il refuse, mais comment il refuse
  quand la clé manque. Le relais répond 503 « remise_indisponible » et nomme
  Firebase, parce qu'un refus qui dirait « indisponible » tout court ferait
  chercher la panne du côté d'Apple. Le contrôle vérifie donc les deux : le
  refus, et qu'il nomme le bon côté.

Contrôlé
  492 garanties passent, aucune n'échoue.

Reste ouvert, et sans rapport : la suite s'arrête plus loin sur
« column "nom" does not exist », dans preparation/dossier.ts. Autre
sous-système, autre cause — un écart entre le code et le schéma, pas un
renommage — et corriger demande de savoir quelle colonne était visée.

Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
parent f72b9327
Loading
Loading
Loading
Loading
Loading
+22 −14
Changes for apps/api/src/verification/relais.ts: 22 added lines, 14 removed lines.
Original line number Diff line number Diff line
@@ -169,9 +169,17 @@ export async function verifierRelais(r: Rapport): Promise<void> {
    illisible.statusCode === 400
      && !JSON.stringify(illisible.json()).includes("panique"));

  // Android est servi depuis que le relais porte FCM. Ce qui reste à contrôler
  // n'est donc plus qu'il refuse, mais COMMENT il refuse quand la clé manque :
  // un relais qui répondrait « indisponible » sans dire de quel côté ferait
  // chercher la panne au mauvais endroit.
  const androide = await frapper("cle-instance-un", { ...mobilisation, plateforme: "android" });
  r.verifie("EM-85", "Android n'est pas encore servi, et le relais le dit",
    androide.statusCode === 501);
  const refusAndroid = JSON.stringify(androide.json());
  r.verifie("EM-85", "sans compte Firebase, Android n'est pas remis",
    androide.statusCode === 503
      && (androide.json() as { code?: string }).code === "remise_indisponible");
  r.verifie("EM-85", "et le refus nomme le côté qui manque, pas « indisponible » tout court",
    refusAndroid.includes("Firebase") && !refusAndroid.includes("Apple"));

  issue = { etat: "jeton_mort", motif: "Unregistered" };
  const mort = await frapper("cle-instance-un", mobilisation);
@@ -230,20 +238,20 @@ export async function verifierRelais(r: Rapport): Promise<void> {
  r.titre("Relais — Android");

  const { signerJwt } = await import(`${dossier}fcm.ts`) as {
    signerJwt: (id: { projet: string; compte: string; clePemFcm: string },
    signerJwt: (id: { projet: string; compte: string; clePem: string },
      maintenantS?: number) => string;
  };
  const { composerFcm, CANAUX_ANDROID } = await import(`${dossier}chargeJwt.ts`) as {
  const { composerFcm, CANAUX_ANDROID } = await import(`${dossier}charge.ts`) as {
    composerFcm: (h: Record<string, unknown>, projet: string) =>
      { chemin: string; corps: string };
    CANAUX_ANDROID: { mobAndroid: string; infoAndroid: string };
    CANAUX_ANDROID: { mobilisation: string; information: string };
  };

  // Une paireFcm jetable : aucune clé de service ne traverse une vérification.
  const paireFcm = generateKeyPairSync("rsa", { modulusLength: 2048 });
  const clePemFcm = paireFcm.privateKey.export({ type: "pkcs8", format: "pem" }).toString();
  const idFcm = { projet: "kastell-verif",
    compte: "relais@kastell-verif.iam.gserviceaccount.com", clePemFcm };
    compte: "relais@kastell-verif.iam.gserviceaccount.com", clePem: clePemFcm };

  const jwtFcm = signerJwt(idFcm, 1_800_000_000);
  const [enteteJwt, chargeJwt, signatureJwt] = jwtFcm.split(".");
@@ -260,32 +268,32 @@ export async function verifierRelais(r: Rapport): Promise<void> {

  const mobAndroid = (JSON.parse(composerFcm({
    jeton: "jeton-appareil", plateforme: "android", reference: "reference-verif",
    urgence: "mobAndroid", reveiller: true, exercice: false,
    urgence: "mobilisation", reveiller: true, exercice: false,
  }, "kastell-verif").corps) as { message: Record<string, never> }).message as
    Record<string, never>;
  const android = mobAndroid.android as unknown as Record<string, unknown>;
  const notifAndroid = android.notification as Record<string, unknown>;

  r.verifie("EM-11", "une mobAndroid part en priorité haute", android.priority === "high");
  r.verifie("EM-11", "une mobilisation part en priorité haute", android.priority === "high");
  r.verifie("EM-11", "sur le canal qui doit rester sonore",
    notifAndroid.channel_id === CANAUX_ANDROID.mobAndroid);
  r.verifie("EM-10", "et expire avec la mobAndroid, pas des heures après",
    notifAndroid.channel_id === CANAUX_ANDROID.mobilisation);
  r.verifie("EM-10", "et expire avec la mobilisation, pas des heures après",
    android.ttl === "7200s");

  const infoAndroid = (JSON.parse(composerFcm({
    jeton: "j", plateforme: "android", reference: "r", urgence: "infoAndroid",
    jeton: "j", plateforme: "android", reference: "r", urgence: "information",
    reveiller: true, exercice: true,
  }, "p").corps) as { message: Record<string, never> }).message as Record<string, never>;
  const androidInfo = infoAndroid.android as unknown as Record<string, unknown>;

  r.verifie("EM-10", "une infoAndroid ne réveille pas", androidInfo.priority === "normal");
  r.verifie("EM-10", "une information ne réveille pas", androidInfo.priority === "normal");
  r.verifie("EM-16", "un exercice se nomme exercice, jusque sur l'écran verrouillé",
    String((infoAndroid.notification as unknown as { title: string }).title)
      .includes("EXERCICE"));

  // EM-09 : la chargeJwt ne transporte rien. C'est ce qui rend acceptable de
  // EM-09 : la charge ne transporte rien. C'est ce qui rend acceptable de
  // passer par Google, comme par Apple.
  r.verifie("EM-09", "la chargeJwt Android ne porte ni organisation, ni crise, ni personne",
  r.verifie("EM-09", "la charge Android ne porte ni organisation, ni crise, ni personne",
    (infoAndroid.notification as unknown as { title: string }).title
      .startsWith("Kastell")
    && Object.values(infoAndroid.data as unknown as Record<string, string>)