<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Otp on Ocarina — précis</title>
    <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/tags/otp/</link>
    <description>Recent content in Otp on Ocarina — précis</description>
    <generator>Hugo</generator>
    <language>fr-FR</language>
    <lastBuildDate>Thu, 28 May 2026 21:02:53 +0200</lastBuildDate>
    <atom:link href="https://mojo-molotov.github.io/from-ocarina-to-igor/fr/tags/otp/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>00.01 — Cartographie de l&#39;écosystème</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/00-big-picture/01-ecosystem-map/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/00-big-picture/01-ecosystem-map/</guid>
      <description>&lt;h1 id=&#34;0001cartographie-de-lécosystème&#34;&gt;00.01 — Cartographie de l&amp;rsquo;écosystème&lt;a class=&#34;anchor&#34; href=&#34;#0001cartographie-de-l%c3%a9cosyst%c3%a8me&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;p&gt;L&amp;rsquo;écosystème Ocarina est constitué de &lt;strong&gt;six dépôts publics&lt;/strong&gt; sur le compte&#xA;GitHub &lt;a href=&#34;https://github.com/mojo-molotov&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;mojo-molotov&lt;/code&gt;&lt;/a&gt;. Ils s&amp;rsquo;organisent en&#xA;&lt;strong&gt;cinq rôles distincts&lt;/strong&gt; qui se composent :&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Rôle&lt;/th&gt;&#xA;          &lt;th&gt;Dépôt&lt;/th&gt;&#xA;          &lt;th&gt;Forme&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Framework&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;ocarina&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Bibliothèque Python publiée sur PyPI&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Application sous test (SUT) publique&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;igoristan&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;SPA React + Vike déployée sur GitHub Pages&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Backend de coordination&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;tests-workers&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;API Vercel Edge + Upstash Redis&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Suite d&amp;rsquo;exemples&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;ocarina-example&lt;/code&gt; &lt;em&gt;(canonique)&lt;/em&gt;, &lt;code&gt;ocarina-with-ai-example&lt;/code&gt; &lt;em&gt;(IA)&lt;/em&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Projets Python e2e tournés vers l&amp;rsquo;Igoristan / CURA&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Documentation publique&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;ocarina-holy-book&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Site VitePress FR + EN, PDF, skills IA&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;schéma-densemble-big-picture&#34;&gt;Schéma d&amp;rsquo;ensemble (big picture)&lt;a class=&#34;anchor&#34; href=&#34;#sch%c3%a9ma-densemble-big-picture&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────────────────────┐&#xA;│                              OCARINA (BIG PICTURE)                              │&#xA;└─────────────────────────────────────────────────────────────────────────────────┘&#xA;&#xA;   ┌──────────────────────────────┐         ┌──────────────────────────────────┐&#xA;   │  [PACKAGE]  ocarina          │ ──────► │  [PACKAGE]  ocarina-example      │&#xA;   │  Framework Python 3.14+      │   pip   │  Suite e2e contre l&amp;#39;Igoristan    │&#xA;   │  ROP / DSL / orchestration   │ install │  (Selenium + Firefox + Redis)    │&#xA;   └──────────────────────────────┘         └──────────────────────────────────┘&#xA;            │  │                                       │  ▲   │&#xA;            │  │                                       │  │  utilise&#xA;            │  │ pip install ocarina                   │  │  IGOR_API_KEY&#xA;            │  ▼                                       ▼  │   │&#xA;            │  ┌────────────────────────────────────┐  pilote │&#xA;            │  │ [PACKAGE]  ocarina-with-ai-example │  ┌──────┴──────────────────┐&#xA;            │  │ Suite e2e CURA Healthcare          │  │ [WEBSITE]  igoristan    │&#xA;            │  │ Claude Code, 99% machine           │  │ React 19 + Vike         │&#xA;            │  └────────────────────────────────────┘  │ GitHub Pages            │&#xA;            │              │                           └──────┬──────────────────┘&#xA;            │              │ pilote                           │ fetch OTP&#xA;            │              ▼                                  ▼&#xA;            │   ┌──────────────────────────────┐    ┌──────────────────────────────┐&#xA;            │   │ [WEBSITE]  CURA Healthcare   │    │ [BACKEND]  tests-workers     │&#xA;            │   │ Heroku, PHP open source      │    │ Vercel Edge Functions        │&#xA;            │   └──────────────────────────────┘    │ + Upstash Redis              │&#xA;            │                                       │ /api/otp, /otp-history,      │&#xA;            │                                       │ /api/corsicadex              │&#xA;            │                                       └──────────────────────────────┘&#xA;            │                                                 ▲&#xA;            │                                                 │ x-api-key&#xA;            │                                                 │ (IGOR_API_KEY ≡ API_SECRET)&#xA;            │                                                 │&#xA;            ▼&#xA;   ┌──────────────────────────────┐&#xA;   │  [DOCS]  ocarina-holy-book   │&#xA;   │  VitePress FR + EN + RU      │&#xA;   │  Skills IA, PDF, llms.txt    │&#xA;   │  GitHub Pages                │&#xA;   └──────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;ocarina&lt;/code&gt; → suites d&amp;rsquo;exemples&lt;/strong&gt; : dépendance Python classique (&lt;code&gt;pip install ocarina&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Suites d&amp;rsquo;exemples → SUT (Igoristan / CURA)&lt;/strong&gt; : pilotage navigateur via Selenium.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Suite Igoristan ↔ &lt;code&gt;tests-workers&lt;/code&gt;&lt;/strong&gt; : appels HTTP (OTP, Corsicadex) ; le secret partagé &lt;code&gt;IGOR_API_KEY&lt;/code&gt; (côté client) ≡ &lt;code&gt;API_SECRET&lt;/code&gt; (côté serveur).&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;responsabilités&#34;&gt;Responsabilités&lt;a class=&#34;anchor&#34; href=&#34;#responsabilit%c3%a9s&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Rôle&lt;/th&gt;&#xA;          &lt;th&gt;Contrat&lt;/th&gt;&#xA;          &lt;th&gt;Besoin&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Framework&lt;/td&gt;&#xA;          &lt;td&gt;&lt;em&gt;Donner&lt;/em&gt; le DSL, l&amp;rsquo;orchestration, la pool de drivers, les reporters&lt;/td&gt;&#xA;          &lt;td&gt;Doit être petit, auditable, sans dépendances cachées&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;SUT public&lt;/td&gt;&#xA;          &lt;td&gt;&lt;em&gt;Offrir&lt;/em&gt; un terrain de jeu volontairement chaotique (random errors, OTP, formulaires capricieux)&lt;/td&gt;&#xA;          &lt;td&gt;Hébergé en permanence sur GitHub Pages, sans backend lourd&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Backend de coordination&lt;/td&gt;&#xA;          &lt;td&gt;&lt;em&gt;Coordonner&lt;/em&gt; les workers sur l&amp;rsquo;OTP, fournir des données pour les tests parallèles&lt;/td&gt;&#xA;          &lt;td&gt;Stateless côté code, l&amp;rsquo;état vit dans Redis (Upstash)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Exemples&lt;/td&gt;&#xA;          &lt;td&gt;&lt;em&gt;Démontrer&lt;/em&gt; comment on utilise réellement Ocarina, à la fois sans IA (canonique) et avec IA (CURA)&lt;/td&gt;&#xA;          &lt;td&gt;Doivent être publics, exécutables, complets, suivis en CI&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Documentation&lt;/td&gt;&#xA;          &lt;td&gt;&lt;em&gt;Transmettre&lt;/em&gt; le pourquoi, le comment, et l&amp;rsquo;usage IA&lt;/td&gt;&#xA;          &lt;td&gt;Doit exister en EN + FR + RU (tradition corso-russe), exposer des fichiers adaptés aux LLMs comme &lt;code&gt;llms.txt&lt;/code&gt;, générer des PDF&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;trois-licences&#34;&gt;Trois licences&lt;a class=&#34;anchor&#34; href=&#34;#trois-licences&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Licence&lt;/th&gt;&#xA;          &lt;th&gt;Dépôts concernés&lt;/th&gt;&#xA;          &lt;th&gt;Pourquoi&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;MIT&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;ocarina&lt;/code&gt;, &lt;code&gt;ocarina-example&lt;/code&gt;, &lt;code&gt;ocarina-with-ai-example&lt;/code&gt;, &lt;code&gt;ocarina-holy-book&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Tout ce qui est code Python ou doc, transmissible « &lt;em&gt;tel quel&lt;/em&gt; », souverain — voir &lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/11-independence/&#34;&gt;&lt;code&gt;../11-independence/&lt;/code&gt;&lt;/a&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;ISC&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;tests-workers&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Choix par défaut du &lt;em&gt;scaffold&lt;/em&gt; Vercel Edge, conservé.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;em&gt;(aucune licence)&lt;/em&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;igoristan&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Application démo publique, hébergée par l&amp;rsquo;auteur (à considérer comme WTFPL).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;points-de-friction-volontaires-entre-dépôts&#34;&gt;Points de friction volontaires entre dépôts&lt;a class=&#34;anchor&#34; href=&#34;#points-de-friction-volontaires-entre-d%c3%a9p%c3%b4ts&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Le design est &lt;strong&gt;volontairement asymétrique&lt;/strong&gt; sur certains axes ; ces frictions sont des outils de test :&lt;/p&gt;</description>
    </item>
    <item>
      <title>06.01 — Stack Vercel Edge</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/06-tests-workers/01-stack-edge/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/06-tests-workers/01-stack-edge/</guid>
      <description>&lt;h1 id=&#34;0601stack-vercel-edge&#34;&gt;06.01 — Stack Vercel Edge&lt;a class=&#34;anchor&#34; href=&#34;#0601stack-vercel-edge&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;h2 id=&#34;packagejson&#34;&gt;&lt;code&gt;package.json&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#packagejson&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;tests-workers&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;#34;version&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;1.0.0&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;#34;scripts&amp;#34;&lt;/span&gt;: {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;#34;vercel-build&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;echo &amp;#39;Build skipped&amp;#39;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;#34;packageManager&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;pnpm@11.2.2&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;#34;dependencies&amp;#34;&lt;/span&gt;: {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;#34;@upstash/redis&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;^1.36.2&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;#34;otplib&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;^13.2.1&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;#34;devDependencies&amp;#34;&lt;/span&gt;: {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;#34;@types/node&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;^25.2.0&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;#34;next&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;^16.1.6&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;#34;@vercel/node&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;^5.5.28&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;#34;typescript&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;^5.9.3&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;#34;license&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;ISC&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Lib&lt;/th&gt;&#xA;          &lt;th&gt;Rôle&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;&lt;code&gt;@upstash/redis&lt;/code&gt;&lt;/strong&gt; (runtime)&lt;/td&gt;&#xA;          &lt;td&gt;Client Redis &lt;em&gt;REST&lt;/em&gt; (pas TCP). Compatible Edge.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;&lt;code&gt;otplib&lt;/code&gt;&lt;/strong&gt; (runtime)&lt;/td&gt;&#xA;          &lt;td&gt;Génération TOTP RFC 6238.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;&lt;code&gt;next&lt;/code&gt;&lt;/strong&gt; (dev seulement)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;em&gt;Types&lt;/em&gt; uniquement (&lt;code&gt;NextRequest&lt;/code&gt;). Pas d&amp;rsquo;app Next.js.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;&lt;code&gt;@vercel/node&lt;/code&gt;&lt;/strong&gt; (dev seulement)&lt;/td&gt;&#xA;          &lt;td&gt;Types Vercel.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;strong&gt;&lt;code&gt;@types/node&lt;/code&gt;&lt;/strong&gt; (dev seulement)&lt;/td&gt;&#xA;          &lt;td&gt;Types Node 22+.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;Note : &lt;strong&gt;aucune&lt;/strong&gt; dépendance runtime au-delà de &lt;code&gt;@upstash/redis&lt;/code&gt; + &lt;code&gt;otplib&lt;/code&gt;.&lt;br&gt;&#xA;Le bundle est microscopique → cold start &amp;lt; 100ms.&lt;/p&gt;</description>
    </item>
    <item>
      <title>06.02 — GET /api/otp</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/06-tests-workers/02-otp-endpoint/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/06-tests-workers/02-otp-endpoint/</guid>
      <description>&lt;h1 id=&#34;0602get-apiotp&#34;&gt;06.02 — &lt;code&gt;GET /api/otp&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#0602get-apiotp&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Fichier source : &lt;a href=&#34;https://github.com/mojo-molotov/tests-workers/blob/main/api/otp.ts&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;api/otp.ts&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;code&#34;&gt;Code&lt;a class=&#34;anchor&#34; href=&#34;#code&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;import&lt;/span&gt; { generate } &lt;span style=&#34;color:#ff79c6&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;otplib&amp;#34;&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;type&lt;/span&gt; { NextRequest } &lt;span style=&#34;color:#ff79c6&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;next/server&amp;#34;&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;import&lt;/span&gt; redis &lt;span style=&#34;color:#ff79c6&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;../lib/redis&amp;#34;&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;import&lt;/span&gt; isAuthorized &lt;span style=&#34;color:#ff79c6&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;../lib/isAuthorized&amp;#34;&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; config &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; { runtime&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;edge&amp;#34;&lt;/span&gt; };&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; SECRET_QUERY_PARAM_KEY &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;secret&amp;#34;&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; OTP_SECRET_MIN_LENGTH &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;32&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; OTP_SECRET_MAX_LENGTH &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;64&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; BASE32_ALPHABET &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;ABCDEFGHIJKLMNOPQRSTUVWXYZ234567&amp;#34;&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; OTP_TTL &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;60&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;5&lt;/span&gt;; &lt;span style=&#34;color:#6272a4&#34;&gt;// 5 minutes&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; OTP_HISTORY_TTL &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; OTP_TTL &lt;span style=&#34;color:#ff79c6&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;60&lt;/span&gt;; &lt;span style=&#34;color:#6272a4&#34;&gt;// 6 minutes&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; MAX_FREE_PAYLOAD_SIZE &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;4096&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; corsHeaders &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Access-Control-Allow-Origin&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;*&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Access-Control-Allow-Methods&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;GET, OPTIONS&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Access-Control-Allow-Headers&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;x-api-key, Content-Type&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;} &lt;span style=&#34;color:#ff79c6&#34;&gt;as&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;function&lt;/span&gt; generateRandomBase32Secret(length: &lt;span style=&#34;color:#8be9fd&#34;&gt;number&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;32&lt;/span&gt;)&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#8be9fd&#34;&gt;string&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;let&lt;/span&gt; secret &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; randomValues &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;new&lt;/span&gt; Uint8Array(length);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  crypto.getRandomValues(randomValues);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;for&lt;/span&gt; (&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;let&lt;/span&gt; i &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;0&lt;/span&gt;; i &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;lt;&lt;/span&gt; length; i&lt;span style=&#34;color:#ff79c6&#34;&gt;++&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    secret &lt;span style=&#34;color:#ff79c6&#34;&gt;+=&lt;/span&gt; BASE32_ALPHABET[randomValues[i] &lt;span style=&#34;color:#ff79c6&#34;&gt;%&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;32&lt;/span&gt;];&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; secret;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;function&lt;/span&gt; ensureSecretLength(secret: &lt;span style=&#34;color:#8be9fd&#34;&gt;string&lt;/span&gt;)&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#8be9fd&#34;&gt;string&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; cleaned &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; secret.toUpperCase().replace(&lt;span style=&#34;color:#f1fa8c&#34;&gt;/\s/g&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; (cleaned.length &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;gt;&lt;/span&gt; OTP_SECRET_MAX_LENGTH) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; validLength &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;Math&lt;/span&gt;.floor(OTP_SECRET_MAX_LENGTH &lt;span style=&#34;color:#ff79c6&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;8&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;8&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; cleaned.substring(&lt;span style=&#34;color:#bd93f9&#34;&gt;0&lt;/span&gt;, validLength);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; (cleaned.length &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;gt;=&lt;/span&gt; OTP_SECRET_MIN_LENGTH) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; validLength &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;Math&lt;/span&gt;.floor(cleaned.length &lt;span style=&#34;color:#ff79c6&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;8&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;8&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; cleaned.substring(&lt;span style=&#34;color:#bd93f9&#34;&gt;0&lt;/span&gt;, validLength);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; repetitions &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;Math&lt;/span&gt;.ceil(OTP_SECRET_MIN_LENGTH &lt;span style=&#34;color:#ff79c6&#34;&gt;/&lt;/span&gt; cleaned.length);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; repeated &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; cleaned.repeat(repetitions);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; validLength &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;Math&lt;/span&gt;.floor(repeated.length &lt;span style=&#34;color:#ff79c6&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;8&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;8&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; repeated.substring(&lt;span style=&#34;color:#bd93f9&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;Math&lt;/span&gt;.max(validLength, OTP_SECRET_MIN_LENGTH));&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;default&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;function&lt;/span&gt; handler(request: &lt;span style=&#34;color:#8be9fd&#34;&gt;NextRequest&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; (request.method &lt;span style=&#34;color:#ff79c6&#34;&gt;===&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;OPTIONS&amp;#34;&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;new&lt;/span&gt; Response(&lt;span style=&#34;color:#ff79c6&#34;&gt;null&lt;/span&gt;, { status: &lt;span style=&#34;color:#8be9fd&#34;&gt;200&lt;/span&gt;, headers: &lt;span style=&#34;color:#8be9fd&#34;&gt;corsHeaders&lt;/span&gt; });&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#ff79c6&#34;&gt;!&lt;/span&gt;isAuthorized(request)) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;new&lt;/span&gt; Response(JSON.stringify({ error&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Unauthorized&amp;#34;&lt;/span&gt; }), {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      status: &lt;span style=&#34;color:#8be9fd&#34;&gt;401&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      headers&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; { &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;, ...corsHeaders },&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    });&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; { searchParams } &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;new&lt;/span&gt; URL(request.url);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; freePayload: &lt;span style=&#34;color:#8be9fd&#34;&gt;Record&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#ff79c6&#34;&gt;string&lt;/span&gt;, &lt;span style=&#34;color:#50fa7b&#34;&gt;string&lt;/span&gt;&amp;gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; {};&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;let&lt;/span&gt; payloadSize &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;0&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  searchParams.forEach((value, key) &lt;span style=&#34;color:#ff79c6&#34;&gt;=&amp;gt;&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; (key &lt;span style=&#34;color:#ff79c6&#34;&gt;===&lt;/span&gt; SECRET_QUERY_PARAM_KEY) &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; entrySize &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;new&lt;/span&gt; TextEncoder().encode(key &lt;span style=&#34;color:#ff79c6&#34;&gt;+&lt;/span&gt; value).length;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; (payloadSize &lt;span style=&#34;color:#ff79c6&#34;&gt;+&lt;/span&gt; entrySize &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;gt;&lt;/span&gt; MAX_FREE_PAYLOAD_SIZE) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      payloadSize &lt;span style=&#34;color:#ff79c6&#34;&gt;+=&lt;/span&gt; entrySize;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    freePayload[key] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; value;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    payloadSize &lt;span style=&#34;color:#ff79c6&#34;&gt;+=&lt;/span&gt; entrySize;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  });&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; (payloadSize &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;gt;&lt;/span&gt; MAX_FREE_PAYLOAD_SIZE) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;new&lt;/span&gt; Response(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      JSON.stringify({&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        error&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Bad Request&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        message&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;`Free payload exceeds maximum size of &lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;${&lt;/span&gt;MAX_FREE_PAYLOAD_SIZE&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt; bytes (got &lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;${&lt;/span&gt;payloadSize&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt; bytes)`&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      }),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        status: &lt;span style=&#34;color:#8be9fd&#34;&gt;400&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        headers&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; { &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;, ...corsHeaders },&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      },&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    );&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; rawSecret &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    searchParams.&lt;span style=&#34;color:#ff79c6&#34;&gt;get&lt;/span&gt;(SECRET_QUERY_PARAM_KEY) &lt;span style=&#34;color:#ff79c6&#34;&gt;??&lt;/span&gt; generateRandomBase32Secret();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; secret &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; ensureSecretLength(rawSecret);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; otpCode &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;await&lt;/span&gt; generate({ secret });&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; now &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;Date&lt;/span&gt;.now();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; expiresAtMs &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; now &lt;span style=&#34;color:#ff79c6&#34;&gt;+&lt;/span&gt; OTP_TTL &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;1000&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; createdAtTimestampLackingMsPrecision &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;Date&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    now &lt;span style=&#34;color:#ff79c6&#34;&gt;-&lt;/span&gt; (now &lt;span style=&#34;color:#ff79c6&#34;&gt;%&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;1000&lt;/span&gt;),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ).toISOString();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; event &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ...freePayload,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    otpCode,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    secret,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    createdAtTimestampLackingMsPrecision,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    expiresAt: &lt;span style=&#34;color:#8be9fd&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;Date&lt;/span&gt;(expiresAtMs).toISOString(),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  };&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; key &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;`otp:&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;${&lt;/span&gt;now&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;:&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;${&lt;/span&gt;crypto.randomUUID()&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;`&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;await&lt;/span&gt; redis.&lt;span style=&#34;color:#ff79c6&#34;&gt;set&lt;/span&gt;(key, JSON.stringify(event), { ex: &lt;span style=&#34;color:#8be9fd&#34;&gt;OTP_HISTORY_TTL&lt;/span&gt; });&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;new&lt;/span&gt; Response(JSON.stringify(event), {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    headers&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; { &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;, ...corsHeaders },&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  });&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;flot&#34;&gt;Flot&lt;a class=&#34;anchor&#34; href=&#34;#flot&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;1. Request arrive&#xA;   ↓&#xA;2. Si OPTIONS → 200 + CORS headers (preflight)&#xA;   ↓&#xA;3. isAuthorized(request) ? Non → 401&#xA;   ↓&#xA;4. Parse searchParams ; tout sauf &amp;#34;secret&amp;#34; est mis dans freePayload&#xA;   ↓&#xA;5. payloadSize &amp;gt; 4 KB ? → 400&#xA;   ↓&#xA;6. secret = ?secret=&amp;lt;X&amp;gt; (si fourni) sinon random Base32 32-chars&#xA;   ↓&#xA;7. ensureSecretLength : assure 32..64 chars, multiple de 8&#xA;   ↓&#xA;8. otpCode = generate({ secret })   ← otplib TOTP RFC 6238&#xA;   ↓&#xA;9. createdAt = floor(now / 1000) * 1000  ← amputation des ms&#xA;   ↓&#xA;10. event = { ...freePayload, otpCode, secret, createdAt, expiresAt }&#xA;    ↓&#xA;11. key = `otp:&amp;lt;now&amp;gt;:&amp;lt;uuid&amp;gt;`&#xA;    ↓&#xA;12. redis.set(key, JSON.stringify(event), { ex: 360 })  ← TTL 6 min&#xA;    ↓&#xA;13. Response = JSON.stringify(event), 200&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;cors-headers&#34;&gt;CORS headers&lt;a class=&#34;anchor&#34; href=&#34;#cors-headers&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; corsHeaders &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Access-Control-Allow-Origin&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;*&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Access-Control-Allow-Methods&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;GET, OPTIONS&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Access-Control-Allow-Headers&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;x-api-key, Content-Type&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;} &lt;span style=&#34;color:#ff79c6&#34;&gt;as&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;*&lt;/code&gt; origin : permet à l&amp;rsquo;Igoristan (depuis &lt;code&gt;https://mojo-molotov.github.io&lt;/code&gt;) d&amp;rsquo;appeler le service.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;GET, OPTIONS&lt;/code&gt; : seuls verbes supportés.&lt;/li&gt;&#xA;&lt;li&gt;Custom header &lt;code&gt;x-api-key&lt;/code&gt; autorisé (sinon le &lt;em&gt;browser&lt;/em&gt; bloquerait).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;code&gt;as const&lt;/code&gt; rend l&amp;rsquo;objet &lt;code&gt;readonly&lt;/code&gt; côté TypeScript.&lt;/p&gt;</description>
    </item>
    <item>
      <title>05.03 — Le faux useAuth &#43; MFA OTP</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/05-igoristan/03-use-auth/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/05-igoristan/03-use-auth/</guid>
      <description>&lt;h1 id=&#34;0503le-faux-useauth--mfa-otp&#34;&gt;05.03 — Le faux &lt;code&gt;useAuth&lt;/code&gt; + MFA OTP&lt;a class=&#34;anchor&#34; href=&#34;#0503le-faux-useauth--mfa-otp&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Le hook &lt;code&gt;useAuth&lt;/code&gt; de l&amp;rsquo;Igoristan est volontairement faux. Il introduit deux mécaniques pour exercer le rejeu et la coordination des workers d&amp;rsquo;Ocarina : un échec aléatoire de 10% et un MFA OTP optionnel.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;code&#34;&gt;Code&lt;a class=&#34;anchor&#34; href=&#34;#code&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ts&#34; data-lang=&#34;ts&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;// src/hooks/useAuth.ts&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; VALID_PASSWORD &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;figatellu&amp;#34;&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; NOT_AUTHENTICATED &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#bd93f9&#34;&gt;1&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; AUTHENTICATED_WITHOUT_MFA &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;1&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; AUTHENTICATED_WITH_MFA &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;2&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;// * ... This is FAKED&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; useAuth &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; () &lt;span style=&#34;color:#ff79c6&#34;&gt;=&amp;gt;&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; [_isAuthenticated, setIsAuthenticated] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; useLocalStorage(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;isAuthenticated&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    NOT_AUTHENTICATED,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  );&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; [isLoading, setIsLoading] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; useState(&lt;span style=&#34;color:#ff79c6&#34;&gt;true&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; isAuthenticated &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; useCallback(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    () &lt;span style=&#34;color:#ff79c6&#34;&gt;=&amp;gt;&lt;/span&gt; _isAuthenticated &lt;span style=&#34;color:#ff79c6&#34;&gt;!==&lt;/span&gt; NOT_AUTHENTICATED,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    [_isAuthenticated],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  );&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; isAuthenticatedWithMFA &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; useCallback(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    () &lt;span style=&#34;color:#ff79c6&#34;&gt;=&amp;gt;&lt;/span&gt; _isAuthenticated &lt;span style=&#34;color:#ff79c6&#34;&gt;===&lt;/span&gt; AUTHENTICATED_WITH_MFA,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    [_isAuthenticated],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  );&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; logout &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; useCallback(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    () &lt;span style=&#34;color:#ff79c6&#34;&gt;=&amp;gt;&lt;/span&gt; setIsAuthenticated(NOT_AUTHENTICATED),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    [setIsAuthenticated],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  );&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  useEffect(() &lt;span style=&#34;color:#ff79c6&#34;&gt;=&amp;gt;&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; delay &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;500&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt; randint(&lt;span style=&#34;color:#bd93f9&#34;&gt;1&lt;/span&gt;, &lt;span style=&#34;color:#bd93f9&#34;&gt;5&lt;/span&gt;); &lt;span style=&#34;color:#6272a4&#34;&gt;// ◄── délai aléatoire 0.5-2.5s&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; timer &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; setTimeout(() &lt;span style=&#34;color:#ff79c6&#34;&gt;=&amp;gt;&lt;/span&gt; setIsLoading(&lt;span style=&#34;color:#ff79c6&#34;&gt;false&lt;/span&gt;), delay);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; () &lt;span style=&#34;color:#ff79c6&#34;&gt;=&amp;gt;&lt;/span&gt; clearTimeout(timer);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }, []);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; login: &lt;span style=&#34;color:#8be9fd&#34;&gt;LoginFn&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; ({ pre &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;false&lt;/span&gt;, password, withMFA }) &lt;span style=&#34;color:#ff79c6&#34;&gt;=&amp;gt;&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; (password &lt;span style=&#34;color:#ff79c6&#34;&gt;===&lt;/span&gt; VALID_PASSWORD &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;Math&lt;/span&gt;.random() &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;0.9&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#6272a4&#34;&gt;// ◄── 10% d&amp;#39;échec malgré bon mdp&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#ff79c6&#34;&gt;!&lt;/span&gt;pre) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        setIsAuthenticated(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          withMFA &lt;span style=&#34;color:#ff79c6&#34;&gt;?&lt;/span&gt; AUTHENTICATED_WITH_MFA : &lt;span style=&#34;color:#8be9fd&#34;&gt;AUTHENTICATED_WITHOUT_MFA&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        );&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;true&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;false&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  };&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; { isAuthenticatedWithMFA, isAuthenticated, isLoading, logout, login };&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;mécaniques&#34;&gt;Mécaniques&lt;a class=&#34;anchor&#34; href=&#34;#m%c3%a9caniques&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;h3 id=&#34;1-localstorage&#34;&gt;1. &lt;code&gt;LocalStorage&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#1-localstorage&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ts&#34; data-lang=&#34;ts&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; [_isAuthenticated, setIsAuthenticated] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; useLocalStorage(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;isAuthenticated&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  NOT_AUTHENTICATED,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Trois valeurs : &lt;code&gt;-1&lt;/code&gt; (pas authentifié), &lt;code&gt;1&lt;/code&gt; (sans MFA), &lt;code&gt;2&lt;/code&gt; (avec MFA).&lt;br&gt;&#xA;Persistées dans &lt;code&gt;localStorage&lt;/code&gt;, faciles d&amp;rsquo;accès pour du test/debug manuel.&lt;/p&gt;</description>
    </item>
    <item>
      <title>06.03 — GET /api/otp-history</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/06-tests-workers/03-otp-history/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/06-tests-workers/03-otp-history/</guid>
      <description>&lt;h1 id=&#34;0603get-apiotp-history&#34;&gt;06.03 — &lt;code&gt;GET /api/otp-history&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#0603get-apiotp-history&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Fichier source : &lt;a href=&#34;https://github.com/mojo-molotov/tests-workers/blob/main/api/otp-history.ts&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;api/otp-history.ts&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;code&#34;&gt;Code&lt;a class=&#34;anchor&#34; href=&#34;#code&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;type&lt;/span&gt; { NextRequest } &lt;span style=&#34;color:#ff79c6&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;next/server&amp;#34;&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;import&lt;/span&gt; redis &lt;span style=&#34;color:#ff79c6&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;../lib/redis&amp;#34;&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;import&lt;/span&gt; isAuthorized &lt;span style=&#34;color:#ff79c6&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;../lib/isAuthorized&amp;#34;&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; config &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; { runtime&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;edge&amp;#34;&lt;/span&gt; };&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; corsHeaders &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Access-Control-Allow-Origin&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;*&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Access-Control-Allow-Methods&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;GET, OPTIONS&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Access-Control-Allow-Headers&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;x-api-key, Content-Type&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;} &lt;span style=&#34;color:#ff79c6&#34;&gt;as&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;default&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;function&lt;/span&gt; handler(request: &lt;span style=&#34;color:#8be9fd&#34;&gt;NextRequest&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; (request.method &lt;span style=&#34;color:#ff79c6&#34;&gt;===&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;OPTIONS&amp;#34;&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;new&lt;/span&gt; Response(&lt;span style=&#34;color:#ff79c6&#34;&gt;null&lt;/span&gt;, { status: &lt;span style=&#34;color:#8be9fd&#34;&gt;200&lt;/span&gt;, headers: &lt;span style=&#34;color:#8be9fd&#34;&gt;corsHeaders&lt;/span&gt; });&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#ff79c6&#34;&gt;!&lt;/span&gt;isAuthorized(request)) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;new&lt;/span&gt; Response(JSON.stringify({ error&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Unauthorized&amp;#34;&lt;/span&gt; }), {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      status: &lt;span style=&#34;color:#8be9fd&#34;&gt;401&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      headers&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; { &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;, ...corsHeaders },&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    });&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;let&lt;/span&gt; cursor &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;0&amp;#34;&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; allEvents &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; [];&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; BATCH_SIZE &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;100&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;do&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; [newCursor, keys] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;await&lt;/span&gt; redis.scan(cursor, {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      match&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;otp:*&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      count: &lt;span style=&#34;color:#8be9fd&#34;&gt;BATCH_SIZE&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    });&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    cursor &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; newCursor;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; (keys.length &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;0&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; values &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;await&lt;/span&gt; redis.mget(...keys);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      allEvents.push(...values.filter(&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;Boolean&lt;/span&gt;));&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  } &lt;span style=&#34;color:#ff79c6&#34;&gt;while&lt;/span&gt; (cursor &lt;span style=&#34;color:#ff79c6&#34;&gt;!==&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;0&amp;#34;&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;new&lt;/span&gt; Response(JSON.stringify(allEvents), {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    headers&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; { &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;, ...corsHeaders },&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  });&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;mécanique-de-scan-redis&#34;&gt;Mécanique de &lt;code&gt;SCAN&lt;/code&gt; Redis&lt;a class=&#34;anchor&#34; href=&#34;#m%c3%a9canique-de-scan-redis&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;let&lt;/span&gt; cursor &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;0&amp;#34;&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; allEvents &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; [];&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; BATCH_SIZE &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;100&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;do&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; [newCursor, keys] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;await&lt;/span&gt; redis.scan(cursor, {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    match&lt;span style=&#34;color:#ff79c6&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;otp:*&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    count: &lt;span style=&#34;color:#8be9fd&#34;&gt;BATCH_SIZE&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  });&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  cursor &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; newCursor;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; (keys.length &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;0&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;const&lt;/span&gt; values &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;await&lt;/span&gt; redis.mget(...keys);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    allEvents.push(...values.filter(&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;Boolean&lt;/span&gt;));&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;} &lt;span style=&#34;color:#ff79c6&#34;&gt;while&lt;/span&gt; (cursor &lt;span style=&#34;color:#ff79c6&#34;&gt;!==&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;0&amp;#34;&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol&gt;&#xA;&lt;li&gt;&lt;code&gt;cursor = &amp;quot;0&amp;quot;&lt;/code&gt; (initialisation).&lt;/li&gt;&#xA;&lt;li&gt;Boucle :&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;SCAN &amp;lt;cursor&amp;gt; MATCH otp:* COUNT 100&lt;/code&gt; retourne &lt;code&gt;[newCursor, keys]&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;Si &lt;code&gt;keys&lt;/code&gt; non vide : &lt;code&gt;MGET k1 k2 ...&lt;/code&gt; récupère les valeurs.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;cursor = newCursor&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;On s&amp;rsquo;arrête quand &lt;code&gt;cursor === &amp;quot;0&amp;quot;&lt;/code&gt; (cycle terminé).&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;Pourquoi &lt;code&gt;SCAN&lt;/code&gt; plutôt que &lt;code&gt;KEYS&lt;/code&gt; ?&lt;/p&gt;</description>
    </item>
    <item>
      <title>07.03 — Scénarios Dashboard login</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/03-scenarios-login/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/03-scenarios-login/</guid>
      <description>&lt;h1 id=&#34;0703scénarios-dashboard-login&#34;&gt;07.03 — Scénarios Dashboard login&lt;a class=&#34;anchor&#34; href=&#34;#0703sc%c3%a9narios-dashboard-login&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Trois familles : happy paths, unhappy paths, data-driven multi-login. Tous exercent le &lt;code&gt;useAuth&lt;/code&gt; à 10% de raté et la coordination OTP.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;dashboard_login&#34;&gt;&lt;code&gt;dashboard_login&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#dashboard_login&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;# src/tests/campaigns/dashboard_login.py&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;create_igoristan_login_campaign&lt;/span&gt;(&lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, drivers_pool: SeleniumWebDriversPool) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; TestCampaign:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; TestCampaign(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Dashboard login&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        suites&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;[&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            create_igoristan_login_happy_paths_test_suite(drivers_pool&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;drivers_pool),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            create_igoristan_login_unhappy_paths_test_suite(drivers_pool&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;drivers_pool),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            create_igoristan_login_data_driven_test_suite(drivers_pool&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;drivers_pool),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;happy-paths&#34;&gt;Happy paths&lt;a class=&#34;anchor&#34; href=&#34;#happy-paths&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;# src/tests/suites/dashboard/access/happy_paths.py&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;create_igoristan_login_happy_paths_test_suite&lt;/span&gt;(&lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, drivers_pool) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; TestSuite:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; TestSuite(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Login happy paths&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        tests&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;[&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            test_dashboard_login_without_otp_happy_path,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            test_dashboard_login_with_otp_happy_path,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            test_dashboard_login_page_back_to_igoristan_button,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        drivers_pool&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;drivers_pool,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;test-1-login-sans-otp&#34;&gt;Test 1 : login sans OTP&lt;a class=&#34;anchor&#34; href=&#34;#test-1-login-sans-otp&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;dashboard_login_without_otp_happy_path&lt;/span&gt;(driver, logger):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    dashboard_creds &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; create_env_getters()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;get_credentials(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;dashboard&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    on_dashboard_login_page &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; DashboardLoginPage(driver&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;driver)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    on_dashboard_welcome_page &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; DashboardWelcomePage(driver&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;driver)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    just_log_error &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; create_just_log_error(logger&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;logger)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    log_error_with_current_url &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; create_log_error_with_current_url(logger&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;logger, driver&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;driver)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    just_log_success &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; create_just_log_success(logger&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;logger)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    log_success_with_current_url_and_take_screenshot &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; create_log_success_with_current_url_and_take_screenshot(&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    retries_amount &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;max&lt;/span&gt;(get_max_workers(), &lt;span style=&#34;color:#bd93f9&#34;&gt;10&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; [&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        drive_page(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_login_page, open_dashboard_login_page)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;failure(just_log_error(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Failed to open the dashboard login page...&amp;#34;&lt;/span&gt;))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;success(just_log_success(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Opened the dashboard login page!&amp;#34;&lt;/span&gt;)),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_login_page, verify_dashboard_login_page)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;failure(log_error_with_current_url(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Failed to verify...&amp;#34;&lt;/span&gt;))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;success(log_success_with_current_url_and_take_screenshot(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Verified!&amp;#34;&lt;/span&gt;)),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_login_page,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                login_without_otp_and_with_retries(dashboard_creds, retries_amount, logger&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;logger))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;failure(just_log_error(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Failed to connect to the dashboard without OTP...&amp;#34;&lt;/span&gt;))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;success(just_log_success(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Connected to the dashboard!&amp;#34;&lt;/span&gt;)),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        drive_page(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_welcome_page, verify_dashboard_welcome_page)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;failure(log_error_with_current_url(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Failed to verify...&amp;#34;&lt;/span&gt;))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;success(log_success_with_current_url_and_take_screenshot(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Verified!&amp;#34;&lt;/span&gt;)),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;retries_amount = max(get_max_workers(), 10)&lt;/code&gt;&lt;/strong&gt; : on rejoue le login interne (au POM) au moins 10 fois, à cause des 10% d&amp;rsquo;échec aléatoire.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;login_without_otp_and_with_retries(creds, retries_amount, logger=logger)&lt;/code&gt;&lt;/strong&gt; : closure qui capture creds + nb retries + logger. Le connector retourne &lt;code&gt;Callable[[DashboardLoginPage], DashboardLoginPage]&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;2 &lt;code&gt;drive_page&lt;/code&gt;&lt;/strong&gt; : login + welcome page. Chacun ouvre, vérifie, screenshot.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Log factories partout&lt;/strong&gt; : aucune lambda inline.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;SeleniumTitleMixin&lt;/code&gt; côté POM&lt;/strong&gt; : &lt;code&gt;get_current_title()&lt;/code&gt; est utilisé par le hook &lt;code&gt;on_failure&lt;/code&gt; de &lt;code&gt;act&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h3 id=&#34;test-2-login-avec-otp&#34;&gt;Test 2 : login avec OTP&lt;a class=&#34;anchor&#34; href=&#34;#test-2-login-avec-otp&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;dashboard_login_with_otp_happy_path&lt;/span&gt;(driver, logger):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#6272a4&#34;&gt;# ... setup pareil ...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    cache &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; in_memory_cache_with_30m_ttl&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    fresh_cache_key_for_username &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; reserve_free_cache_key(cache)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    fresh_cache_key_for_otp_send_button_click_date &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; reserve_free_cache_key(cache)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; [&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        drive_page(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_login_page, open_dashboard_login_page)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_login_page, verify_dashboard_login_page)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_login_page,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                start_to_login_with_otp_and_with_retries(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    dashboard_creds, retries_amount,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    cache&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;cache, logger&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;logger,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    username_cache_key&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;fresh_cache_key_for_username,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    otp_send_button_click_date_cache_key&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;fresh_cache_key_for_otp_send_button_click_date,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                ))&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_login_page, verify_otp_screen)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_login_page,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                type_otp_with_retries(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    retries_amount,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    cache&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;cache, logger&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;logger,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    username_cache_key&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;fresh_cache_key_for_username,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    otp_send_button_click_date_cache_key&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;fresh_cache_key_for_otp_send_button_click_date,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                ))&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        drive_page(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_welcome_page, verify_dashboard_welcome_page)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_welcome_page, click_on_go_to_nested_page_btn)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        drive_page(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_protected_page, verify_dashboard_protected_page)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Trois &lt;code&gt;drive_page&lt;/code&gt;. Cinq &lt;code&gt;act&lt;/code&gt; dans le premier. Cache L1 + clés réservées pour partager des valeurs entre acts (cf. &lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/08-caches-locks/&#34;&gt;&lt;code&gt;08-caches-locks.md&lt;/code&gt;&lt;/a&gt;).&lt;/p&gt;</description>
    </item>
    <item>
      <title>00.04 — Relations entre les six dépôts</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/00-big-picture/04-repo-relations/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/00-big-picture/04-repo-relations/</guid>
      <description>&lt;h1 id=&#34;0004relations-entre-les-six-dépôts&#34;&gt;00.04 — Relations entre les six dépôts&lt;a class=&#34;anchor&#34; href=&#34;#0004relations-entre-les-six-d%c3%a9p%c3%b4ts&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;p&gt;Chaque arête du graphe est un &lt;em&gt;contrat&lt;/em&gt; : un secret, une URL, un type de payload, ou une version. On les détaille ici, par paire.&lt;/p&gt;&#xA;&lt;h2 id=&#34;ocarina--ocarina-example&#34;&gt;&lt;code&gt;ocarina&lt;/code&gt; ↔ &lt;code&gt;ocarina-example&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#ocarina--ocarina-example&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Direction&lt;/th&gt;&#xA;          &lt;th&gt;Mécanisme&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;ocarina&lt;/code&gt; → &lt;code&gt;ocarina-example&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;pip install ocarina&lt;/code&gt; depuis PyPI&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;ocarina-example&lt;/code&gt; → &lt;code&gt;ocarina&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Aucune (l&amp;rsquo;exemple ne pousse rien)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;L&amp;rsquo;exemple &lt;strong&gt;écrit ses propres adapters&lt;/strong&gt; au-dessus du framework :&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;lib/ext/ocarina/adapters/agnostic/act.py&lt;/code&gt; → ajoute le hook &lt;code&gt;on_failure&lt;/code&gt; qui transforme une page d&amp;rsquo;erreur HTTP (titre matché par &lt;code&gt;ERROR_PAGE_REGEX&lt;/code&gt;) en &lt;code&gt;HttpErrorPageReachedError&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;lib/ext/ocarina/adapters/agnostic/match_page.py&lt;/code&gt; → &lt;code&gt;create_match_page(raised_exceptions=transient_errors)&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;lib/ext/ocarina/adapters/agnostic/env_getters.py&lt;/code&gt; → typed &lt;code&gt;EnvGetters[_CredsKeys, _ValuesKeys]&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;lib/ext/ocarina/adapters/selenium/test_suite.py&lt;/code&gt; → fige &lt;code&gt;max_retries_per_test=8&lt;/code&gt;, &lt;code&gt;transient_errors=...&lt;/code&gt;, &lt;code&gt;autoscreen_on_fail=True&lt;/code&gt;, propage &lt;code&gt;--only&lt;/code&gt;/&lt;code&gt;--exclude&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;lib/ext/ocarina/adapters/selenium/test_campaign.py&lt;/code&gt; → fige &lt;code&gt;max_workers=get_max_workers()&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;ocarina--ocarina-with-ai-example&#34;&gt;&lt;code&gt;ocarina&lt;/code&gt; ↔ &lt;code&gt;ocarina-with-ai-example&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#ocarina--ocarina-with-ai-example&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;pip install . ruff mypy mypy-extensions typing-extensions pre-commit&lt;/code&gt; car l&amp;rsquo;exemple IA a la dépendance fixée dans son &lt;code&gt;pyproject.toml&lt;/code&gt; : &lt;code&gt;ocarina&amp;gt;=1.0.3&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;L&amp;rsquo;adapter &lt;code&gt;act&lt;/code&gt; est &lt;strong&gt;minimaliste&lt;/strong&gt; ici : pas de &lt;code&gt;on_failure&lt;/code&gt; (CURA n&amp;rsquo;a pas de page d&amp;rsquo;erreur volontairement aléatoire à intercepter). Voir &lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/08-ai-example/&#34;&gt;&lt;code&gt;../08-ai-example/&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;L&amp;rsquo;adapter &lt;code&gt;create_drivers_pool&lt;/code&gt; est &lt;strong&gt;surchargé&lt;/strong&gt; pour bâtir un Chrome &lt;em&gt;clean&lt;/em&gt; (password manager off, leak detection off). Voir &lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/08-ai-example/06-data-gaps/&#34;&gt;&lt;code&gt;../08-ai-example/06-data-gaps.md&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;ocarina-example--igoristan&#34;&gt;&lt;code&gt;ocarina-example&lt;/code&gt; ↔ &lt;code&gt;igoristan&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#ocarina-example--igoristan&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Direction&lt;/th&gt;&#xA;          &lt;th&gt;Contrat&lt;/th&gt;&#xA;          &lt;th&gt;Détail&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;ocarina-example&lt;/code&gt; → &lt;code&gt;igoristan&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;URLs publiques&lt;/td&gt;&#xA;          &lt;td&gt;Tout passe par &lt;code&gt;https://mojo-molotov.github.io/igoristan/&amp;lt;route&amp;gt;&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;igoristan&lt;/code&gt; → &lt;code&gt;ocarina-example&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;aucun (le SUT ne sait pas qu&amp;rsquo;il est testé)&lt;/td&gt;&#xA;          &lt;td&gt; — &lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;&lt;code&gt;src/constants/pages/&lt;/code&gt; :&lt;/p&gt;</description>
    </item>
    <item>
      <title>06.07 — Flux de coordination OTP &#43; l&#39;anti-précision volontaire</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/06-tests-workers/07-otp-coordination-flow/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/06-tests-workers/07-otp-coordination-flow/</guid>
      <description>&lt;h1 id=&#34;0607flux-de-coordination-otp--limprécision-volontaire&#34;&gt;06.07 — Flux de coordination OTP + l&amp;rsquo;imprécision volontaire&lt;a class=&#34;anchor&#34; href=&#34;#0607flux-de-coordination-otp--limpr%c3%a9cision-volontaire&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;La raison d&amp;rsquo;être du backend : permettre à N workers parallèles d&amp;rsquo;Ocarina de &lt;strong&gt;récupérer le bon OTP&lt;/strong&gt; pour leur &lt;em&gt;user&lt;/em&gt;, même quand plusieurs OTP sont générés.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;flux&#34;&gt;Flux&lt;a class=&#34;anchor&#34; href=&#34;#flux&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │               Worker (parmi --workers 3 d&amp;#39;Ocarina)                │&#xA;   └───────────────────────────────────────────────────────────────────┘&#xA;                                     │&#xA;                                     ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ Selenium ouvre la page de connexion au Dashboard                  │&#xA;   └─────────────────────────────────┬─────────────────────────────────┘&#xA;                                     ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ Acquisition du lock distribué Redis (OTP_SEND_LOCK_KEY)           │&#xA;   │   → seul ce worker peut cliquer &amp;#34;Send OTP&amp;#34; pendant ACQ            │&#xA;   └─────────────────────────────────┬─────────────────────────────────┘&#xA;                                     ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ min_utc_date = datetime.now(UTC)                                  │&#xA;   └─────────────────────────────────┬─────────────────────────────────┘&#xA;                                     ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ Cache L1 : enregistre min_utc_date + username dans le cache       │&#xA;   │   (clés réservées par reserve_free_cache_key)                     │&#xA;   └─────────────────────────────────┬─────────────────────────────────┘&#xA;                                     ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ Selenium tape username + password + cocher OTP + click &amp;#34;REQ OTP&amp;#34;  │&#xA;   └─────────────────────────────────┬─────────────────────────────────┘&#xA;                                     ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ IGORISTAN UI : fetch /api/otp?_user=&amp;lt;username&amp;gt;                    │&#xA;   │   (x-api-key tapé par Selenium dans l&amp;#39;UI)                         │&#xA;   └─────────────────────────────────┬─────────────────────────────────┘&#xA;                                     ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ TESTS-WORKERS /api/otp :                                          │&#xA;   │   generate(secret) → otpCode                                      │&#xA;   │   createdAt = floor(now/1000)*1000  ← amputation ms               │&#xA;   │   event = { _user, otpCode, createdAt, expiresAt, ... }           │&#xA;   │   redis.set(&amp;#34;otp:&amp;lt;now&amp;gt;:&amp;lt;uuid&amp;gt;&amp;#34;, JSON, EX 360)                     │&#xA;   │   return event                                                    │&#xA;   └──────────────────────────────────┬────────────────────────────────┘&#xA;                                      ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ IGORISTAN UI : affiche écran OTP                                  │&#xA;   └──────────────────────────────────┬────────────────────────────────┘&#xA;                                      ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ Release du lock Redis OTP_SEND_LOCK_KEY                           │&#xA;   └──────────────────────────────────┬────────────────────────────────┘&#xA;                                      ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ Selenium : retrieve_dashboard_otp_code(min_utc_date, _user)       │&#xA;   │   ↓                                                               │&#xA;   │   GET /api/otp-history  (x-api-key = IGOR_API_KEY)                │&#xA;   │   ↓                                                               │&#xA;   │   TESTS-WORKERS : SCAN otp:* + MGET → all events                  │&#xA;   │   ↓                                                               │&#xA;   │   filtre côté client par _user                                    │&#xA;   │   filtre createdAt &amp;gt;= min_utc_date - 1s                           │&#xA;   │   tri ASC sur createdAt                                           │&#xA;   │   return first.otpCode                                            │&#xA;   └──────────────────────────────────┬────────────────────────────────┘&#xA;                                      ▼&#xA;   ┌───────────────────────────────────────────────────────────────────┐&#xA;   │ Selenium tape l&amp;#39;OTP dans l&amp;#39;UI Igoristan                           │&#xA;   │   → AUTHENTICATED_WITH_MFA                                        │&#xA;   └───────────────────────────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;races-conditions&#34;&gt;Races conditions&lt;a class=&#34;anchor&#34; href=&#34;#races-conditions&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Worker A : &lt;code&gt;min_utc_date_A = 13:27:53.123&lt;/code&gt;, génère OTP_A à 13:27:53.250.&lt;/li&gt;&#xA;&lt;li&gt;Worker B : &lt;code&gt;min_utc_date_B = 13:27:53.130&lt;/code&gt;, génère OTP_B à 13:27:53.470.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;Sans coordination, A pourrait récupérer OTP&lt;em&gt;B (au lieu de OTP_A) parce que les timestamps sont _très proches&lt;/em&gt; (et tronqués à la seconde près).&lt;/p&gt;</description>
    </item>
    <item>
      <title>Chapitre 06 — tests-workers, backend Vercel Edge</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/06-tests-workers/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/06-tests-workers/</guid>
      <description>&lt;h1 id=&#34;chapitre-06tests-workers-backend-vercel-edge&#34;&gt;Chapitre 06 — &lt;code&gt;tests-workers&lt;/code&gt;, backend Vercel Edge&lt;a class=&#34;anchor&#34; href=&#34;#chapitre-06tests-workers-backend-vercel-edge&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Trois endpoints HTTP, deux libs (&lt;code&gt;@upstash/redis&lt;/code&gt;, &lt;code&gt;otplib&lt;/code&gt;), zéro Next.js applicatif. Le strict minimum pour &lt;em&gt;coordonner&lt;/em&gt; des tests parallèles sur l&amp;rsquo;Igoristan.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;plan&#34;&gt;Plan&lt;a class=&#34;anchor&#34; href=&#34;#plan&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;#&lt;/th&gt;&#xA;          &lt;th&gt;Fichier&lt;/th&gt;&#xA;          &lt;th&gt;Sujet&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;01&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/06-tests-workers/01-stack-edge/&#34;&gt;&lt;code&gt;01-stack-edge.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Stack Vercel Edge Functions, &lt;code&gt;runtime: &amp;quot;edge&amp;quot;&lt;/code&gt;, Upstash Redis, &lt;code&gt;otplib&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;02&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/06-tests-workers/02-otp-endpoint/&#34;&gt;&lt;code&gt;02-otp-endpoint.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;GET /api/otp&lt;/code&gt; — génération + Redis SET + free payload.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;03&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/06-tests-workers/03-otp-history/&#34;&gt;&lt;code&gt;03-otp-history.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;GET /api/otp-history&lt;/code&gt; — SCAN Redis + retour de tous les events.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;04&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/06-tests-workers/04-corsicadex/&#34;&gt;&lt;code&gt;04-corsicadex.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;GET /api/corsicadex?id=N&lt;/code&gt; — lookup statique.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;05&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/06-tests-workers/05-is-authorized/&#34;&gt;&lt;code&gt;05-is-authorized.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;isAuthorized.ts&lt;/code&gt; — &lt;code&gt;x-api-key&lt;/code&gt; vs &lt;code&gt;apiKey&lt;/code&gt; query param.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;06&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/06-tests-workers/06-redis-upstash/&#34;&gt;&lt;code&gt;06-redis-upstash.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;lib/redis.ts&lt;/code&gt; — config Upstash.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;07&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/06-tests-workers/07-otp-coordination-flow/&#34;&gt;&lt;code&gt;07-otp-coordination-flow.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Le flux complet OTP : Igoristan ↔ workers ↔ Ocarina ↔ Redis.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;objectifs&#34;&gt;Objectifs&lt;a class=&#34;anchor&#34; href=&#34;#objectifs&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;3 endpoints &lt;code&gt;.ts&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;2 fichiers &lt;code&gt;lib/&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;1 fichier &lt;code&gt;consts/&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;Total : ~7 fichiers TypeScript de code, plus &lt;code&gt;package.json&lt;/code&gt; et &lt;code&gt;vercel.json&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;L&amp;rsquo;objectif : être &lt;em&gt;déployable en 30 secondes&lt;/em&gt; par un humain.&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
