DOCUMENT HISTORIQUE — PROPOSITION DU 13 OCTOBRE 2026, REMPLACÉE POUR CE LOT
Décision utilisateur du 14 octobre 2026 : PIN pour les applications officielles TV/mobile uniquement. Le Web stream.fuzero.fr et les clients/API Jellyfin tiers restent accessibles selon leur authentification habituelle. Les exigences de passerelle globale ci-dessous ne sont plus le périmètre de dev.2. Voir README-FUZERO.md et docs/PIN-TV-DEV2.md pour l’implémentation actuelle.

FUZERO 0.2.0 — AUDIT D'ARCHITECTURE DU CODE PARENTAL
Date : 13 octobre 2026
Statut : proposition technique à valider, aucune protection serveur déployée.
Périmètre : lecture des forks locaux TV/mobile et documentation/source officielle.
Aucun compte réel utilisé, aucune production modifiée, aucun secret extrait.

1. CONCLUSION ET DÉCISIONS QUI CONDITIONNENT LA LIVRAISON

Le besoin ne se résout pas par un nouvel écran Android. Il exige un point de contrôle serveur obligatoire devant l'ensemble des accès aux médias, avec une identité persistante distincte de l'autorisation obtenue après saisie du PIN.

Architecture recommandée : clients FUZERO -> HTTPS stream.fuzero.fr -> passerelle FUZERO (authentification, PIN, sessions et proxy média) -> Jellyfin sur un réseau privé. Les jetons Jellyfin restent dans la passerelle, qui continue d'utiliser les comptes et les permissions Jellyfin existants. Il ne faut jamais remplacer les droits utilisateurs par une clé API administrateur commune.

Trois décisions sont indispensables avant d'annoncer la sécurité de V0.2.0 :
- L'API et le Web Jellyfin actuellement accessibles, ainsi que toutes les autres adresses d'accès, doivent être couverts par cette passerelle. Si le Web complet reste P1, livrer un accès Web minimal sécurisé en P0 ou suspendre l'ancien Web public lors du basculement. Le laisser ouvert rend la protection TV contournable.
- Le socle sécurisé initial doit suspendre les téléchargements hors ligne, les lecteurs externes, PiP, lecture de fond et Cast non adaptés à la politique. Leur maintien sans étude spécifique contredirait le verrouillage à chaque sortie. C'est une proposition de périmètre, pas une décision déjà prise par le propriétaire.
- Distinguer l'absence totale de délai de tolérance dans l'interface de la propagation réseau. Un serveur ne peut détecter instantanément une sortie pendant une coupure réseau, et ne peut vérifier indépendamment que le navigateur d'un client modifié possède réellement le premier plan. Une promesse de révocation mondiale à zéro milliseconde serait fausse.

2. CONSTAT SUR LE CODE EXISTANT

TV : applications/fuzero-tv, base Jellyfin Android TV v0.19.10.
AuthenticationRepository.kt authentifie avec mot de passe ou Quick Connect puis conserve AuthenticationResult.accessToken dans AuthenticationStoreUser. SessionRepository.kt restaure ce jeton, l'applique à l'ApiClient et conserve également un jeton pour l'envoi des rapports de plantage. Les appels de catalogue, images et lecture héritent de ces sessions normales. Le PIN FUZERO n'est pas un contrôle serveur existant.

Le nouveau lecteur TV VideoPlayerFragment.kt appelle actuellement unpause() dans onResume(). Il faudra empêcher cette reprise automatique après un verrouillage. Les deux familles de lecteurs (historique et réécriture) sont présentes : tester et protéger les deux ou en sélectionner explicitement une.

Mobile : applications/fuzero-mobile, base Jellyfin Android v2.7.3. Le WebView charge l'interface du serveur, et ApiClientController.kt/NativeInterface synchronisent ses identifiants avec le lecteur natif. PiP, Chromecast, téléchargements et lecteurs externes sont présents dans le périmètre hérité. Une simple injection de CSS/JavaScript de verrouillage ne remplace pas le contrôle d'accès.

Les livraisons précédentes déclarent stream.fuzero.fr et Jellyfin 12.1.0 ; le présent audit n'a pas contrôlé le déploiement effectif ni sa configuration privée. Les sources serveur officielles examinées sont la branche master, donc leur inventaire doit être confirmé contre le commit/image exact du serveur de recette. Le besoin PIN6 partagé ne doit pas être confondu avec les restrictions de classification par âge ou les procédures de récupération de mot de passe Jellyfin. L'ancien EasyPassword/PIN a été retiré en 10.9 pour raisons de sécurité [S1].

3. FRONTIÈRE DE SÉCURITÉ SERVEUR

La passerelle doit être le seul chemin réseau utilisable par les appareils des utilisateurs. Vérifier Internet ET LAN : ports Jellyfin 8096/8920, publication Docker, reverse proxies, IPv4/IPv6, anciens domaines, DNS local, UPnP, DLNA, partages de médias, liens externes et éventuel accès VPN. La découverte réseau désactivée dans l'APK ne ferme pas ces accès.

Le NAS héberge Jellyfin et la passerelle sur un réseau interne ; le pare-feu/refus de publication interdit aux réseaux utilisateurs de joindre Jellyfin directement. L'administration se fait par un chemin privé réservé à l'administrateur. Les comptes FUZERO ordinaires n'ont pas de droits d'administration. Si le LAN est partagé, le simple retrait de la redirection de port Internet est insuffisant.

Passerelle en refus par défaut : seules des routes publiques de bootstrap, branding non sensible, connexion et santé minimale sont ouvertes sans session. Une session identifiée mais verrouillée ne peut lire que son état de sécurité minimal et utiliser les opérations PIN permises. Toutes les routes de contenus exigent une autorisation active et le contrôle des permissions de l'utilisateur. Une nouvelle route amont inconnue ne devient jamais publique implicitement [S2].

Les en-têtes d'identité fournis par le client ne sont pas des preuves : normaliser et remplacer Authorization, X-Emby-Authorization, X-MediaBrowser-Token et variantes pertinentes ; filtrer api_key/token dans query strings et réponses. Ne pas faire confiance au nom du client, au numéro de version ni à son DeviceId pour autoriser un accès. Ces valeurs sont falsifiables.

Les jetons Jellyfin acquis par la passerelle sont chiffrés au repos avec une clé séparée de la base et de ses sauvegardes courantes. Les mots de passe de connexion sont transmis en HTTPS, utilisés pour la vérification, puis abandonnés. Ils ne sont ni persistés ni journalisés. La configuration amont est fixe ; aucun paramètre utilisateur ne choisit une URL de proxy.

4. MODÈLE DE SESSION ET PARCOURS

État serveur proposé :
- Compte : clé (instance Jellyfin, identifiant utilisateur immuable), empreinte PIN, paramètres de hachage, version de sécurité, compteur de tentatives et délais de blocage.
- Session d'identité : identifiant opaque aléatoire, appareil/installation, expiration et révocation ; son secret n'autorise jamais seul le catalogue.
- Autorisation de contenu : identifiant opaque distinct, compte, session d'identité, activation/appareil ou onglet, version de sécurité, expiration et état révoqué.
- Lecture active : média, session de lecture Jellyfin, autorisation de contenu, connexions/flux ouverts à interrompre.

Première connexion :
1. Le client envoie identifiant/mot de passe à la passerelle.
2. La passerelle vérifie auprès de Jellyfin et garde le jeton amont.
3. Elle rend seulement la session d'identité et PIN_A_CREER ou VERROUILLE.
4. Si absent : saisie/confirmation de six chiffres, création atomique côté serveur après authentification récente. Deux appareils ne peuvent écraser le code créé par l'autre.
5. Une validation PIN donne une autorisation de contenu limitée à cette activation. Elle seule permet les requêtes de catalogue/lecture.

Réouverture : restaurer la session d'identité, rester opaque/verrouillé, vérifier le PIN en ligne et créer une nouvelle autorisation. Aucun auto-déverrouillage à partir du stockage persistant. Le statut déverrouillé et son secret restent uniquement en mémoire. Un redémarrage, une restauration de processus ou un échec de vérification serveur revient verrouillé.

Android : protéger les secrets de session persistants avec Android Keystore, exclure les sauvegardes et journaux. Web : privilégier une session d'identité en cookie Secure/HttpOnly/SameSite, contrôles CSRF/Origin, CSP et absence de scripts tiers dans les écrans de sécurité [S3]. L'autorisation de contenu doit être propre à l'onglet, pas un cookie global déverrouillant simultanément tous les onglets. Fetch utilise l'autorisation en mémoire ; les images/vidéos nécessitent une stratégie de transport dédiée vérifiable (tickets opaques courts, liés à la session et à l'autorisation, limités aux ressources et révocables). Ne jamais inclure le PIN, le mot de passe ou un jeton Jellyfin dans leurs URL.

Contrat d'API de principe, à formaliser en OpenAPI avant implémentation :
POST /fuzero/v1/session/login ; GET /fuzero/v1/session ; POST /fuzero/v1/session/logout ; POST /fuzero/v1/pin/create ; POST /fuzero/v1/unlocks ; POST /fuzero/v1/unlocks/{id}/revoke ; POST /fuzero/v1/pin/change ; POST /fuzero/v1/pin/reset ; GET /fuzero/v1/capabilities.

Les routes d'identité ne donnent pas de métadonnées média avant déverrouillage. Les réponses portent des codes stables (AUTH_INVALID, SESSION_EXPIRED, PIN_REQUIRED, PIN_INVALID, PIN_RATE_LIMITED, SERVER_UNAVAILABLE, UPGRADE_REQUIRED), traduits en français par les clients. Ne pas rediriger silencieusement une requête vidéo vers une page HTML d'erreur.

5. PIN, TENTATIVES, MODIFICATION ET RÉCUPÉRATION

Valider une chaîne de six chiffres ASCII, y compris les zéros initiaux. Stocker une empreinte Argon2id salée, avec un secret serveur supplémentaire séparé de la base si le dispositif d'exploitation sait le sauvegarder/restaurer. Régler le coût après mesure sur le NAS ; pas de SHA-256 simple, pas de chiffrement réversible du PIN [S4]. Six chiffres ne donnent qu'un million de combinaisons : même un bon hachage ne rend pas ce PIN équivalent à un mot de passe fort.

Limitation principale PAR COMPTE, persistante et atomique, complétée par session/IP/global : elle ne doit pas se réinitialiser en changeant d'IP, d'appareil, de processus ou en redémarrant le service. Proposition à valider : après 5 erreurs, blocage initial 60 secondes puis croissance bornée, et contrôle des rafales globales avant d'engager Argon2. Éviter un blocage permanent exploitable pour déni de service. Ne pas appliquer cette limitation uniquement dans l'écran.

Modification : ancien PIN vérifié avec le même compteur, nouveau PIN confirmé, transaction unique mettant à jour empreinte + version de sécurité et révoquant toutes les autorisations du compte. Nouvelle saisie PIN pour tous les appareils, y compris celui qui vient de modifier le code. Le code existant n'est jamais renvoyé.

Réinitialisation : saisie fraîche du mot de passe du compte, vérifiée côté serveur avec limitation séparée ; la session d'identité seule ne suffit pas. En cas de succès, établir un droit de récupération à usage unique et très courte durée, lié au compte et à la session, puis changer le PIN et révoquer tous les déverrouillages. Ne pas réutiliser directement un ancien « ForgotPassword/Pin » Jellyfin comme code parental. Si l'enfant connaît le mot de passe du parent, il peut utiliser la récupération expressément prévue par le cahier : ce modèle protège un compte dont le mot de passe reste parental.

6. VERROUILLAGE LOCAL IMMÉDIAT ET LIMITES RÉSEAU

Une sortie confirmée doit synchroniquement : masquer intégralement le contenu, pause audio/vidéo, désactiver reprise média automatique et commandes de notification, supprimer l'autorisation mémoire, annuler les requêtes et travaux de préchargement, puis demander la révocation au serveur. La requête de révocation ne conditionne jamais l'affichage du masque.

Android : un coordinateur partagé par toutes les activités doit distinguer les transitions internes contrôlées des sorties réelles, rotation/recréation, clavier, dialogue système, lancement de permissions et changement de fenêtre. Ne pas utiliser uniquement ProcessLifecycleOwner : ON_PAUSE/ON_STOP y sont volontairement retardés [S5]. Ne pas utiliser uniquement onWindowFocusChanged(false), qui peut correspondre au clavier, ni uniquement onStop, trop tardif pour certaines pertes de premier plan. Combiner événements d'activité, focus/top-resumed, écran et une liste étroite des transitions internes autorisées ; masquer de façon conservatrice pendant les transitions ambiguës. Une exception de clavier/permission n'est jamais un passe-droit durable. Les transitions doivent être testées sur les modèles TV réels.

Android : couvrir toutes les fenêtres protégées et empêcher les aperçus sensibles dans Récents avec les mécanismes appropriés, dont FLAG_SECURE où applicable. Les écrans de chargement et restaurations de navigation commencent opaques avant la première image. Les titres, affiches et notifications système sont aussi du contenu protégé.

Web : visibilitychange(hidden) couvre le changement d'onglet ; window blur/focus couvre des changements de fenêtre qui peuvent laisser la page visible [S6]. Gérer également pagehide/pageshow/BFCache et freeze/discard [S7]. Les événements du navigateur ne donnent pas une preuve parfaite du premier plan de l'OS. Les dialogues internes à la page ne doivent pas déverrouiller ou reverrouiller à tort. Au retour, restaurer d'abord le masque et une autorisation serveur, jamais l'ancien catalogue depuis un cache ou un service worker.

Le serveur révoque dès réception d'une sortie, d'une modification du PIN ou d'un logout. Il ferme les flux/WebSockets concernés et invalide les tickets. Une autorisation courte renouvelable pendant une activation active borne la durée résiduelle quand la révocation n'arrive pas. Sa valeur exacte est un paramètre de sécurité/performance à mesurer et à valider, pas un délai de tolérance dans l'interface. Le renouvellement n'est permis qu'avec l'autorisation active, jamais avec la seule identité persistante. Une autorisation expirée impose à nouveau le PIN.

Après perte réseau, rester verrouillé au retour tant que la validation en ligne ne réussit pas. Pas de PIN validé localement ni de mode hors ligne autorisé par défaut. Une réponse de déverrouillage tardive ne doit jamais rouvrir l'écran après une sortie : marquer chaque activation et rejeter les réponses d'une génération ancienne. Côté serveur, une révocation arrivée avant la fin d'un unlock doit empêcher cette activation d'obtenir un nouveau grant.

Limite explicite : un client modifié qui connaît le PIN ou a volé une autorisation active peut tenter de simuler des signaux d'activité. Aucun signal JavaScript ni nom d'APK ne prouve l'absence d'une sortie. Les données déjà livrées à un appareil ne peuvent être rappelées du réseau, pas plus qu'un film déjà copié. Le modèle visé est un accès serveur impossible sans validation PIN, avec relock immédiat dans les clients pris en charge, et une exposition résiduelle documentée pour panne réseau/compromission du client ; ce n'est pas un DRM.

7. SURFACES JELLYFIN À COUVRIR

L'inventaire doit partir de l'OpenAPI et du code EXACTS de la version déployée ; l'OpenAPI seul peut omettre des anciennes routes. Les contrôleurs officiels inspectés montrent plusieurs familles média et des routes HLS historiques exclues de l'explorateur API [S8].

- Authentification : AuthenticateByName, anciennes variantes disponibles, AuthenticateWithQuickConnect, QuickConnect init/poll/authorize, mot de passe/récupération, tokens de périphérique, logout. Quick Connect peut identifier un appareil mais ne doit jamais produire un accès contenu sans PIN. Une désactivation publique initiale est plus facile à vérifier qu'un passage transparent.
- Catalogue, recherche, recommandations, biographies, états de lecture, bibliothèques, paramètres exposant les titres et groupes.
- Images : affiches, fonds, avatars privés, vignettes de chapitre, miniatures, trickplay. Une image doit rester inaccessible avec sa simple URL/son identifiant après verrouillage ; le nom de fichier n'est pas un secret suffisant.
- PlaybackInfo et MediaSources : réécrire ou rejeter les adresses internes/externes, jetons et URL directes renvoyés dans JSON. Pas de fuite des jetons amont à l'intérieur d'un champ ou d'un manifeste.
- HTTP direct/remux : GET/HEAD/Range, réponses 206/416, reprises et connexions longues. Un contrôle au premier octet ne suffit pas à couper un flux déjà ouvert lors d'une révocation ; le proxy doit connaître et annuler ces flux.
- HLS et éventuel DASH : manifests maîtres/variantes, playlists médias, segments init/media, audio alternatif, sous-titres, clés éventuelles. Réécrire toutes les URL et vérifier l'autorisation à chaque requête, y compris segments issus d'une lecture commencée avant le verrouillage. Gérer URLs relatives, absolues, encodages, redirects et query strings.
- Sous-titres externes/embarqués, polices/attachements, downloads, images/books/PDF si activés : pas de bypass par une famille hors vidéo.
- WebSockets : authentifier à l'ouverture, contrôler les messages sensibles, fermer/révoquer en cours de connexion. Aucun retour de métadonnées après verrouillage. Jellyfin dépend aussi des WebSockets [S9].
- Sessions/Remote control/SyncPlay : éviter qu'un appareil verrouillé puisse relancer un autre lecteur, et qu'une commande externe relance celui qui est verrouillé.
- Caches CDN, proxy, navigateur, service workers et caches images locaux : aucune réponse privée partagée entre utilisateurs ; no-store pour réponses sensibles où nécessaire, purge/masquage des ressources déjà présentes, pas de serve-stale après refus d'autorisation.

Les en-têtes, journaux et traces doivent supprimer PIN, mots de passe, jetons, cookies, tickets et query strings sensibles. Jellyfin documente explicitement les secrets api_key dans les URL et l'importance de Known Proxies [S9]. Un journal d'audit minimal conserve résultat/date/type d'action et identifiant interne non affiché au public, sans contenu ni secret.

8. INTÉGRATION DANS LES FORKS ET MIGRATION

TV : refondre AuthenticationRepository/SessionRepository/AuthenticationStore pour séparer identité et grant ; gating de la navigation dès StartupActivity, sessionstate centralisé, UI création/validation/modification/reset ; adapter ApiClient, JellyfinImage, PlaybackHelper/SdkPlaybackHelper, anciens/nouveaux lecteurs, notifications, service audio et télécommande. Le serveur reste configurable à la compilation avec validation HTTPS stricte.

Mobile : partager le contrat avec TV ; synchroniser identité et grant du WebView avec ApiClientController et les lecteurs sans garder un jeton Jellyfin dans localStorage. Adapter MainActivity, WebViewFragment/JellyfinWebViewClient, NativePlayer, PlayerLifecycleObserver/PlayerService, images et notifications. Le client Web servi au téléphone doit avoir les mêmes états de sécurité, pas seulement un écran natif posé devant lui.

Web : fork/extension contrôlée de l'interface construite depuis une version figée, couche réseau, routeur, média et cache réellement intégrés. Une insertion CSS sur le serveur ne suffit pas. Le menu Mon compte -> Sécurité -> Code parental appelle la même API sur tous supports.

Migration : conserver package et signature Android permet l'installation par-dessus 0.1, mais pas de conserver un jeton amont réutilisable sur une entrée oubliée. Prévoir une conversion/reconnexion contrôlée ; demander à nouveau le mot de passe pour l'enrôlement PIN si l'authentification n'est pas récente. Les préférences et la reprise Jellyfin peuvent être conservées. Fermer toutes les anciennes entrées et traiter les anciennes sessions/caches. Ne pas annoncer la protection en gardant simultanément une route ancienne sans PIN pour « compatibilité ».

Un ancien client 0.1 ou Jellyfin standard doit recevoir un refus/mise à jour nécessaire ; falsifier sa version ne doit pas suffire. La compatibilité conservée concerne comptes, bibliothèques, protocole de lecture/transcodage/sous-titres derrière la passerelle, pas la promesse que tous les clients tiers continueront de fonctionner sans modification.

9. JALONS ET RECETTE DE SÉCURITÉ

A. Inventaire de recette : modèle/architecture NAS, deployment Jellyfin (image/commit/plugins), reverse proxy, ports et DNS, stockage secrets et sauvegardes, comptes test non administrateurs, fichier vidéo de test légal avec deux pistes/sous-titres. Aucune ouverture/fermeture de production pendant cette étude.
B. Architecture acceptée : schéma de confiance, machine d'états, OpenAPI PIN/session, politique de blocage/récupération, stratégie médias/Web par onglet, concessions sur mode hors ligne/Cast, durée résiduelle réseau chiffrée et mesurable.
C. Preuve serveur isolée : connexion/PIN + catalogue/image + vidéo directe/Range + transcodage/HLS + sous-titres + WebSocket, en refus par défaut. Captures réseau expurgées et tests de non-fuite de jeton.
D. Clients : une première TV complète contre recette ; retour 0,1 seconde et 1 seconde, clavier, permission, rotation, navigation interne, écran veille/réveil, double activité, deux lecteurs, touches Home/Back/Play ; aucun flash de contenu ni reprise automatique. Ensuite Web minimal pour fermer le contournement, puis mobile complet.
E. Tests adversariaux autorisés sur recette : identité seule/ancien jeton/absence de token refusés pour API, image, HEAD/Range, téléchargement, playlist et segment ancien ; route inconnue/alias/encodages/redirection interdits ; pas d'accès direct aux ports depuis LAN/Internet de test ; pas de privilège via userId d'un autre compte ; invalidation immédiate après modification PIN ; quotas d'essais partagés et résistants aux courses ; grant tardif/invalidation simultanée ; redémarrage de passerelle et panne base fermés par défaut ; arrêt des flux ouverts et WebSockets.
F. Deux appareils et deux onglets : même code, grants séparés, sortie d'un appareil sans déverrouillage implicite ailleurs, rotation PIN bloque tous les grants ; appareil hors réseau masque localement puis doit revérifier en ligne. Tester cache/BFCache, service worker, miniatures Récents/notifications et absence de secrets dans journaux.
G. Déploiement : sauvegardes vérifiées, migration de session, upgrade signé 0.1 -> 0.2 sur TV réelle, basculement contrôlé de l'unique entrée publique, recontrôle complet. Un retour arrière vers accès non protégé ne doit pas être automatique : en incident, afficher indisponibilité au lieu de rouvrir l'ancien accès.

Fin de recette obligatoire avant mention « contrôle parental sécurisé ». Cet audit ne constitue pas cette recette et ne livre pas un serveur PIN fonctionnel.

10. SOURCES PRIMAIRES CONSULTÉES

S1 https://jellyfin.org/posts/jellyfin-release-10.9.0/ — retrait EasyPassword/PIN ; ne pas confondre avec contrôle parental par classement.
S2 https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html — refus par défaut, vérification de chaque requête.
S3 https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html — gestion et protection des sessions.
S4 https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html — Argon2id, sel, secret séparé et coût du hachage.
S5 https://developer.android.com/reference/androidx/lifecycle/ProcessLifecycleOwner — délai documenté de propagation pause/stop.
S5b https://developer.android.com/guide/components/activities/activity-lifecycle — états d'activité, pause, arrêt, rotation et restauration.
S6 https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API — différences visibilitychange et blur/focus.
S7 https://developer.chrome.com/docs/web-platform/page-lifecycle-api — états de cycle de vie navigateur.
S8 https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/HlsSegmentController.cs — surfaces HLS historiques.
S8b https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/ImageController.cs — familles d'images.
S8c https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Controllers/UserController.cs — authentification et récupération.
S8d https://github.com/jellyfin/jellyfin/blob/master/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs — authentification/claims Jetons Jellyfin.
S9 https://jellyfin.org/docs/general/post-install/networking/reverse-proxy/ — secrets dans URLs, Known Proxies et WebSockets.
S10 https://jellyfin.org/posts/jellyfin-release-12.0/ — évolution des API, clients et cache Web ; justifie de figer/auditer la version effective.

Les mécanismes FUZERO proposés dans ce rapport sont une conception déduite des exigences et de ces références, pas une fonctionnalité intégrée déclarée par Jellyfin. Les sources master et la documentation courante ne remplacent pas l'audit du déploiement exact.

11. CONTRAT D'API À VALIDER — DÉTAIL DE L'ITÉRATION SUIVANTE

Les noms qui suivent sont une proposition, non des routes existantes. Toutes les réponses sensibles ont Cache-Control: no-store. Les mutations Web exigent Origin autorisé et protection CSRF. Aucune opération PIN n'utilise GET. Aucun secret n'apparaît dans l'URL des routes de session/PIN.

POST /fuzero/v1/session/login
  Entrée : username, password, installationId (indicatif, pas une preuve), platform, appVersion.
  Succès 200 : session opaque, accountId opaque, state=PIN_SETUP_REQUIRED|LOCKED, pinEpoch, protocolVersion. Web : secret session en cookie HttpOnly. Android : secret dans le corps puis stockage protégé.
  Échec 401 AUTH_INVALID sans divulguer si le nom existe ; 429 AUTH_RATE_LIMITED ; 503 SERVER_UNAVAILABLE. Aucun AccessToken ou SessionInfo upstream transmis.

GET /fuzero/v1/session
  Identité requise. Renvoie existence du PIN, état LOCKED, pinEpoch et possibilités minimales de gestion ; ni catalogue ni grant. Ne réactive jamais un grant de l'ancien processus/onglet.

POST /fuzero/v1/pin/create
  Identité + preuve de mot de passe récente requises. Entrée newPin, confirmation, requestId idempotent.
  Écriture atomique uniquement si aucun PIN présent. 201 code créé, state=LOCKED ; 409 PIN_ALREADY_EXISTS si créé sur un autre appareil ; 403 REAUTH_REQUIRED si preuve trop ancienne. L'interface peut ensuite valider le nouveau PIN via la route unlock. Aucun PIN par défaut.

POST /fuzero/v1/unlocks
  Identité requise. Entrée pin et activationId aléatoire propre à ce passage au premier plan/onglet.
  Succès 201 : unlockToken opaque uniquement en mémoire, unlockId, pinEpoch, expiresAt, renewalPolicy. Le serveur stocke le digest du secret et son association à l'identité/activation.
  Échec 403 PIN_INVALID/PIN_REQUIRED ; 429 PIN_RATE_LIMITED avec Retry-After ; 401 SESSION_EXPIRED ; 503 sécurité indisponible. La création vérifie après le hachage, sous transaction, que pinEpoch et activation restent valides.

POST /fuzero/v1/unlocks/{id}/renew
  Identité + grant non expiré requis, activation correspondante. Renouvelle seulement tant que la version de sécurité et le compte restent valides. Ne peut recréer un grant révoqué. Aucun mécanisme de renewal fondé seulement sur l'identité ou un cookie persistant.

POST /fuzero/v1/activations/{activationId}/close
  Identité requise. Idempotent, pose une fermeture définitive de cette activation et révoque tous ses grants, y compris s'il n'en existe pas encore. Retour 204 sans permettre de rouvrir cette activation. Cette route traite la course où une requête close arrive avant la réponse unlock. Un nouvel accès nécessite une NOUVELLE activation et un nouveau PIN.

POST /fuzero/v1/pin/change
  Identité requise. Entrée oldPin, newPin, confirmation, expectedPinEpoch et requestId.
  Vérification oldPin limitée ; transaction compare-and-swap sur expectedPinEpoch, remplace empreinte et incrémente pinEpoch, révoque tous grants et ferme les flux. 204 + nouvel état LOCKED ; 409 SECURITY_STATE_CHANGED en cas de modification concurrente. L'ancien PIN n'est jamais relu en clair.

POST /fuzero/v1/pin/reset/challenge
  Identité + password fraîchement saisi. Vérifier que Jellyfin authentifie le MÊME identifiant de compte immuable ; ne pas autoriser une récupération du compte A avec le mot de passe B.
  Retour droit de récupération opaque à usage unique, borné dans le temps, lié à accountId/sessionId/pinEpoch. Le jeton temporaire obtenu auprès de Jellyfin pour cette vérification est traité côté serveur et nettoyé selon ses règles.

POST /fuzero/v1/pin/reset
  Droit de récupération + newPin/confirmation, consommation atomique du droit. Modifie empreinte et pinEpoch, révoque tous grants et impose LOCKED ; ne crée pas une session de lecture. Aucun administrateur du site WordPress n'obtient implicitement ce droit.

POST /fuzero/v1/session/logout
  Identité requise, idempotent : révoque identité et grants de cet appareil, révoque la session amont associée, supprime cookie/stockage local même si le nettoyage réseau doit être repris ultérieurement. Un logout-all distinct est possible avec réauthentification et incrément de authEpoch.

GET /fuzero/v1/capabilities
  Public et non sensible : version du protocole, exigences minimales des clients, serveur configuré, disponibilité générale sans identités. Sert à afficher Mise à jour nécessaire. AppVersion est informatif ; la barrière reste l'obtention du grant.

Transport média : le proxy exige une identité et un grant correspondants sur les appels API Android/fetch. Les SDK doivent les ajouter avant tout chargement, y compris images et lecteur natif. Lorsque les lecteurs Web ne permettent pas des en-têtes personnalisés, une route de tickets média n'émet que des tickets courts et opaques associés à identité, unlockId, ressource, usages/méthodes autorisés ; leur validité est toujours vérifiée contre le grant serveur. Ils ne doivent pas survivre à celui-ci. Le ticket d'une playlist ne vaut pas pour une autre ressource arbitraire. Les détails de transport doivent être prouvés avec le lecteur HLS réellement retenu avant de figer le protocole.

12. MATRICE ÉVÉNEMENTS ET VERSIONS DE SÉCURITÉ

Versions serveur :
- pinEpoch (compte) : augmente pour création, modification ou reset du PIN ; tous les grants portent l'epoch courant. Comparaison à chaque admission de requête, plus interruption des flux longs.
- authEpoch (compte) : augmente lors d'une invalidation FUZERO globale des identités, compte désactivé ou incident nécessitant re-login. Un changement de mot de passe directement via administration Jellyfin doit être détecté/intégré : ne pas supposer qu'un événement fiable existe déjà. Déterminer un hook compatible ou une vérification amont et sa latence en recette.
- identitySessionId : différent par installation/login ; sortie simple ne détruit pas la session d'identité.
- activationId et génération locale : différente à chaque retour réel ; valeur et grant en mémoire seulement. close ferme définitivement cette activation, sans verrouiller par défaut les autres appareils. Le changement PIN révoque, lui, tous les appareils.
- serverBootId : au redémarrage de la passerelle, aucune autorisation de contenu de l'ancienne exécution n'est restaurée sans revérification PIN. Les identités peuvent rester persistantes selon leur validité.

Événement                         | Identité           | Grant / accès            | Interface / lecture
Premier login, PIN absent         | créée              | aucun                    | formulaire PIN, opaque
PIN correct, activation active    | conservée          | nouveau grant court      | catalogue, lecture manuelle
Home / autre app / YouTube        | conservée          | supprimé local + close   | masque et pause immédiats
Retour après 100 ms / 1 s         | conservée          | nouveau PIN obligatoire  | masque avant tout rendu
Changement onglet / fenêtre       | conservée          | close du contexte quitté | masque et pause
Clavier dans écran FUZERO         | conservée          | conservé si même contexte| saisie reste possible
Navigation catalogue -> détail   | conservée          | conservé                 | pas de nouveau PIN
Rotation/recréation interne      | conservée          | conservé en mémoire si preuve interne valide | opaque pendant transition, pas d'auto-play après vraie sortie
Verrouillage écran / veille      | conservée          | close                    | masque / pause
Processus tué / navigateur fermé | conservée si valide| absent au redémarrage    | PIN avant catalogue
Réponse unlock après sortie      | conservée          | réponse rejetée + close  | reste verrouillé
Réseau perdu pendant sortie      | conservée          | supprimé local, close best effort, durée serveur bornée | reste verrouillé jusqu'à vérification en ligne
Changement PIN sur autre appareil| conservée          | tous grants invalidés    | masque / pause, nouveau PIN
Session amont invalidée          | invalidée/refusée   | tous grants liés refusés | reconnexion, session expirée
Erreur base/PIN/passerelle        | conservée si possible| aucun droit nouveau, refus | indisponibilité, jamais accès direct de secours
Reconnexion réseau              | conservée si valide| aucun auto-déverrouillage| PIN / réessayer
Télécommande Play / notification| conservée          | aucun si verrouillé      | aucune reprise avant déverrouillage et action manuelle

Une implémentation initiale à une seule instance de passerelle réduit les difficultés de cohérence : transaction atomique pour les epochs et registre central des connexions à fermer. En réplication ultérieure, invalidations, limites d'essais et admission des requêtes doivent rester cohérentes ; un cache d'epoch périmé serait une fenêtre d'accès. Mesurer séparément délai du masque, délai de pause, rejet de la prochaine requête et interruption d'un flux déjà ouvert.

13. ACCÈS ET ENVIRONNEMENT NÉCESSAIRES AU PROCHAIN JALON

L'inspection menée en parallèle par l'agent principal a repéré sur le NAS un partage SSD_APPS_DOCKER/JELLYFIN avec branding/cache/config et un dossier FUZERO_SITE contenant le déploiement WordPress. Ces emplacements ne prouvent ni une instance de recette, ni un backend PIN. Aucun backend parental existant n'a été identifié à ce stade.

Pour le jalon de preuve : une instance de recette Jellyfin isolée et de même version que production, un compte adulte de test non administrateur (PIN factice), deux clients de test et un média de test ; accès de configuration aux conteneurs, réseau/reverse proxy et pare-feu du NAS ; emplacement de secrets hors sources ; stratégie de sauvegarde/restauration testable. Ces droits doivent servir au périmètre de recette, sans ouvrir publiquement le serveur privé ni recopier les mots de passe/jetons réels dans le dépôt.

Il faudra ensuite un créneau de basculement de production et des décisions explicites sur l'ancien Web, les anciennes applications, les modes hors ligne/Cast et la durée résiduelle acceptable en panne réseau. La préparation graphique, la notice, les messages et la vérification d'upgrade de l'APK peuvent avancer indépendamment ; ils ne valident pas FZ-07/08/09 à eux seuls.

