{
  "openapi": "3.1.0",
  "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
  "info": {
    "title": "Library Manager — API mobile locale et synchronisation",
    "version": "1.0.0",
    "summary": "OAuth Authorization Code + PKCE, espaces, fichiers et lecture personnelle bidirectionnelle.",
    "description": "Contrat backend mobile. La seule lecture du contenu est la bibliothèque locale : copie complète vérifiée avant ouverture. Ce document ne fournit pas un lecteur natif ni une preuve appareil physique. UUID/SHA/tokens d’exemple sont synthétiques et inactifs. Les scopes restent subordonnés aux rôles tenant ; les progressions, confirmations et reçus sont privés au compte. Aucun streaming chapitre/HTML. Les fichiers signés sont temporaires et privés. Requêtes JSON UTF-8 strictes<=64 KiB ; formulaires OAuth<=8 KiB. no-store/no-referrer ; erreurs stables, pas de secrets ou SQL. Bibliothèque mobile : isoler par compte, publier copies atomiquement, journaliser intentions et opérations avant réseau, sérialiser refresh et progression, traiter les409 explicitement. Ressource métier inconnue :404 notFound après authentification ; query inconnue/scalar dupliqué :422 validationFailed. OAuth action inconnue :404 invalid_request ; paramètres de formulaire inconnus/dupliqués :400 invalid_request. Méthode métier non supportée :405 avec Allow après contrôle Bearer ; OAuth méthode non supportée :405 avec Allow, sans émission. Les scopes progress:read restent indépendants du catalogue : champs masqués et oracles de filtres/tri fermés sans consentement de lecture. Budgets personnels en base :20installations/100000copies par espace,4096hashes par famille, rotations espacées5s."
  },
  "servers": [
    {
      "url": "https://library-manager.com",
      "description": "Origine publique de référence ; adapter au serveur configuré."
    }
  ],
  "externalDocs": {
    "description": "Guide mobile complet en français et anglais",
    "url": "https://library-manager.com/docs/api"
  },
  "tags": [
    {
      "name": "OAuth public PKCE",
      "description": "Connexion web existante, consentement, échange et révocation des clients publics enregistrés."
    },
    {
      "name": "Compte et espaces",
      "description": "Identité et adhésions actives."
    },
    {
      "name": "Bibliothèque",
      "description": "Métadonnées et versions prêtes, pagination non atomique."
    },
    {
      "name": "Fichiers et copies locales",
      "description": "Transferts fichier et déclarations personnelles, lecture locale uniquement."
    },
    {
      "name": "Progression de lecture",
      "description": "Position privée avec CAS et reçus idempotents."
    },
    {
      "name": "Signets et notes",
      "description": "Signets et notes de passage privés, liés à une version, avec CAS."
    },
    {
      "name": "Imports depuis le téléphone",
      "description": "Envoi local vers cloud sous rôle propriétaire/éditeur et quota."
    }
  ],
  "paths": {
    "/api/v1/mobile/spaces/{spaceId}/books/{bookId}/versions/{versionId}/cover": {
      "get": {
        "operationId": "mobileGetBookCover",
        "tags": [
          "Bibliothèque"
        ],
        "summary": "Récupérer la couverture WebP d’une version prête",
        "description": "Scope requis : library:read ; files:read et confirmation de copie locale non requis pour cette miniature de catalogue. Bearer mobile obligatoire, aucune authentification par cookie. Les droits readBook, l’adhésion active, la famille logique, la version source physique ready et son SHA sont réévalués avant toute réponse304 ou lecture du cache interne. Seule une couverture publiée cover_v1 est servie : image/webp,1..524288 octets, largeur<=640 et hauteur<=960 pixels. Aucune génération à la demande ; couverture absente ou non publiée =>404 notFound. Les octets lus du stockage sont contrôlés en taille et SHA-256 avant réponse. Ce endpoint ne sert aucun chapitre ni lecteur distant ; la lecture du livre exige toujours une copie locale vérifiée. HTTP private,no-store sans TTL de fraîcheur ; une application peut gérer elle-même une copie privée par compte et son ETag, et la revalider avec If-None-Match. Les références de stockage, URLs signées et paramètres producteur ne sont jamais exposés. Toute méthode autre que GET/HEAD reçoit405 Allow: GET, HEAD après Bearer, scope et validation.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "mobileOAuth": [
              "library:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "spaceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "UUID de l’espace actif ; droits résolus par le serveur."
          },
          {
            "name": "bookId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "UUID du livre logique accessible ; normalisé en minuscules."
          },
          {
            "name": "versionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "UUID de la version physique prête de cette famille ; normalisé en minuscules."
          },
          {
            "name": "profile",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "cover_v1"
              ],
              "default": "cover_v1"
            },
            "description": "Seul profil accepté. Query fermée : paramètres inconnus, profil invalide ou scalaire dupliqué =>422 validationFailed."
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 8192
            },
            "description": "ETag fort, faible W/, liste séparée par virgules ou *. Au-delà de8192 caractères, la condition est ignorée et la réponse suit le chemin normal. Autorisation et métadonnées toujours vérifiées avant304."
          },
          {
            "name": "X-Library-Manager-Expected-User",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."
          }
        ],
        "responses": {
          "200": {
            "description": "Octets de la couverture WebP publiée, sans enveloppe JSON.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              },
              "ETag": {
                "description": "ETag fort calculé sur le SHA-256 des octets WebP publiés.",
                "schema": {
                  "type": "string",
                  "pattern": "^\"sha256-[0-9a-f]{64}\"$"
                },
                "example": "\"sha256-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\""
              },
              "Content-Length": {
                "description": "Taille exacte des octets WebP ; HEAD annonce la taille sans retourner de corps.",
                "schema": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 524288
                }
              }
            },
            "content": {
              "image/webp": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "304": {
            "description": "Couverture inchangée après autorisation actuelle et résolution des métadonnées ; aucun corps ni lecture des octets.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              },
              "ETag": {
                "description": "ETag fort calculé sur le SHA-256 des octets WebP publiés.",
                "schema": {
                  "type": "string",
                  "pattern": "^\"sha256-[0-9a-f]{64}\"$"
                },
                "example": "\"sha256-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\""
              }
            }
          },
          "401": {
            "description": "Bearer absent, invalide, expiré, révoqué ou fence de compte différente ; WWW-Authenticate: Bearer.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              },
              "WWW-Authenticate": {
                "schema": {
                  "type": "string",
                  "const": "Bearer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Scope library:read absent ou droits/adhésion à l’espace refusés.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Livre/version inaccessible, chemin encodé interdit ou couverture absente/non publiée ; notFound.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "405": {
            "description": "Méthode refusée après authentification, scope et validation ; Allow: GET, HEAD. Aucun corps.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              },
              "Allow": {
                "schema": {
                  "type": "string",
                  "const": "GET, HEAD"
                }
              }
            }
          },
          "422": {
            "description": "UUID, query ou fence invalides : validationFailed ; taille/SHA du stockage incorrects : integrityMismatch.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "rateLimited : limite de lectures simultanées du cache de couvertures atteinte ; retry borné.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Erreur inattendue convertie en internalError ; aucun détail interne.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "configurationRequired ou providerUnavailable : stockage/configuration momentanément indisponible.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      },
      "head": {
        "operationId": "mobileHeadBookCover",
        "tags": [
          "Bibliothèque"
        ],
        "summary": "Consulter les métadonnées HTTP d’une couverture sans ses octets",
        "description": "Scope requis : library:read ; files:read et confirmation de copie locale non requis pour cette miniature de catalogue. Bearer mobile obligatoire, aucune authentification par cookie. Les droits readBook, l’adhésion active, la famille logique, la version source physique ready et son SHA sont réévalués avant toute réponse304 ou lecture du cache interne. Seule une couverture publiée cover_v1 est servie : image/webp,1..524288 octets, largeur<=640 et hauteur<=960 pixels. Aucune génération à la demande ; couverture absente ou non publiée =>404 notFound. Les octets lus du stockage sont contrôlés en taille et SHA-256 avant réponse. Ce endpoint ne sert aucun chapitre ni lecteur distant ; la lecture du livre exige toujours une copie locale vérifiée. HTTP private,no-store sans TTL de fraîcheur ; une application peut gérer elle-même une copie privée par compte et son ETag, et la revalider avec If-None-Match. Les références de stockage, URLs signées et paramètres producteur ne sont jamais exposés. Toute méthode autre que GET/HEAD reçoit405 Allow: GET, HEAD après Bearer, scope et validation. HEAD retourne toujours un corps vide, sur200,304 et toutes les erreurs.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "mobileOAuth": [
              "library:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "spaceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "UUID de l’espace actif ; droits résolus par le serveur."
          },
          {
            "name": "bookId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "UUID du livre logique accessible ; normalisé en minuscules."
          },
          {
            "name": "versionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "UUID de la version physique prête de cette famille ; normalisé en minuscules."
          },
          {
            "name": "profile",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "cover_v1"
              ],
              "default": "cover_v1"
            },
            "description": "Seul profil accepté. Query fermée : paramètres inconnus, profil invalide ou scalaire dupliqué =>422 validationFailed."
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 8192
            },
            "description": "ETag fort, faible W/, liste séparée par virgules ou *. Au-delà de8192 caractères, la condition est ignorée et la réponse suit le chemin normal. Autorisation et métadonnées toujours vérifiées avant304."
          },
          {
            "name": "X-Library-Manager-Expected-User",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."
          }
        ],
        "responses": {
          "200": {
            "description": "Métadonnées de l’image autorisée ; aucun corps. Content-Type: image/webp et Content-Length annonce la taille des octets qui seraient retournés par GET.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              },
              "ETag": {
                "description": "ETag fort calculé sur le SHA-256 des octets WebP publiés.",
                "schema": {
                  "type": "string",
                  "pattern": "^\"sha256-[0-9a-f]{64}\"$"
                },
                "example": "\"sha256-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\""
              },
              "Content-Length": {
                "description": "Taille exacte des octets WebP ; HEAD annonce la taille sans retourner de corps.",
                "schema": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 524288
                }
              }
            }
          },
          "304": {
            "description": "Couverture inchangée après autorisation actuelle et résolution des métadonnées ; aucun corps ni lecture des octets.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              },
              "ETag": {
                "description": "ETag fort calculé sur le SHA-256 des octets WebP publiés.",
                "schema": {
                  "type": "string",
                  "pattern": "^\"sha256-[0-9a-f]{64}\"$"
                },
                "example": "\"sha256-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\""
              }
            }
          },
          "401": {
            "description": "Bearer absent, invalide, expiré, révoqué ou fence de compte différente ; WWW-Authenticate: Bearer. HEAD ne retourne aucun corps, même pour les erreurs JSON du GET.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              },
              "WWW-Authenticate": {
                "schema": {
                  "type": "string",
                  "const": "Bearer"
                }
              }
            }
          },
          "403": {
            "description": "Scope library:read absent ou droits/adhésion à l’espace refusés. HEAD ne retourne aucun corps, même pour les erreurs JSON du GET.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              }
            }
          },
          "404": {
            "description": "Livre/version inaccessible, chemin encodé interdit ou couverture absente/non publiée ; notFound. HEAD ne retourne aucun corps, même pour les erreurs JSON du GET.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              }
            }
          },
          "405": {
            "description": "Méthode refusée après authentification, scope et validation ; Allow: GET, HEAD. Aucun corps. HEAD ne retourne aucun corps, même pour les erreurs JSON du GET.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              },
              "Allow": {
                "schema": {
                  "type": "string",
                  "const": "GET, HEAD"
                }
              }
            }
          },
          "422": {
            "description": "UUID, query ou fence invalides : validationFailed ; taille/SHA du stockage incorrects : integrityMismatch. HEAD ne retourne aucun corps, même pour les erreurs JSON du GET.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              }
            }
          },
          "429": {
            "description": "rateLimited : limite de lectures simultanées du cache de couvertures atteinte ; retry borné. HEAD ne retourne aucun corps, même pour les erreurs JSON du GET.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              }
            }
          },
          "500": {
            "description": "Erreur inattendue convertie en internalError ; aucun détail interne. HEAD ne retourne aucun corps, même pour les erreurs JSON du GET.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              }
            }
          },
          "503": {
            "description": "configurationRequired ou providerUnavailable : stockage/configuration momentanément indisponible. HEAD ne retourne aucun corps, même pour les erreurs JSON du GET.",
            "headers": {
              "Cache-Control": {
                "description": "Aucune durée de fraîcheur HTTP : private, no-store, même sur erreurs et réponses conditionnelles.",
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Vary": {
                "description": "La représentation dépend du Bearer, jamais de la session cookie.",
                "schema": {
                  "type": "string",
                  "const": "Authorization"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/mobile/me": {
      "get": {
        "operationId": "mobileGetMe",
        "tags": [
          "Compte et espaces"
        ],
        "summary": "Identifier le compte et le grant",
        "description": "Réponse personnelle minimale. Isoler bibliothèque, tokens et journal par compte ; ni cookie ni Authorization Basic ne servent à authentifier les ressources mobiles.\n\nScope requis : aucun scope particulier ; tout Bearer valide suffit. Droits et appartenance active vérifiés indépendamment des scopes. Tout paramètre query inconnu est refusé ; les endpoints sans query refusent toute query.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "mobileOAuth": []
          }
        ],
        "parameters": [
          {
            "name": "X-Library-Manager-Expected-User",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."
          }
        ],
        "responses": {
          "200": {
            "description": "Succès.",
            "headers": {
              "Cache-Control": {
                "description": "private, no-store sur toutes les réponses.",
                "schema": {
                  "type": "string"
                }
              },
              "Referrer-Policy": {
                "description": "no-referrer pour l’API.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                },
                "examples": {
                  "synthetic": {
                    "summary": "Exemple synthétique, identités/URLs/jetons inactifs",
                    "value": {
                      "user": {
                        "id": "77777777-7777-4777-8777-777777777777",
                        "name": "Compte synthétique",
                        "email": "mobile@example.test",
                        "emailVerified": true
                      },
                      "grantId": "88888888-8888-4888-8888-888888888888",
                      "clientId": "library-manager-mobile",
                      "scopes": [
                        "library:read",
                        "files:read",
                        "files:write",
                        "progress:read",
                        "progress:write"
                      ],
                      "readingMode": "local_copy"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiHTTP401"
          },
          "403": {
            "$ref": "#/components/responses/ApiHTTP403"
          },
          "404": {
            "$ref": "#/components/responses/ApiHTTP404"
          },
          "409": {
            "$ref": "#/components/responses/ApiHTTP409"
          },
          "413": {
            "$ref": "#/components/responses/ApiHTTP413"
          },
          "422": {
            "$ref": "#/components/responses/ApiHTTP422"
          },
          "423": {
            "$ref": "#/components/responses/ApiHTTP423"
          },
          "429": {
            "$ref": "#/components/responses/ApiHTTP429"
          },
          "500": {
            "$ref": "#/components/responses/ApiHTTP500"
          },
          "503": {
            "$ref": "#/components/responses/ApiHTTP503"
          },
          "405": {
            "$ref": "#/components/responses/ApiHTTP405"
          }
        }
      }
    },
    "/api/v1/mobile/spaces": {
      "get": {
        "operationId": "mobileListSpaces",
        "tags": [
          "Compte et espaces"
        ],
        "summary": "Lister les espaces accessibles",
        "description": "Seulement les adhésions actives à des espaces actifs. Relister à la reprise ou après refus ; aucun rôle fourni par le client ne fait autorité.\n\nScope requis : library:read. Droits et appartenance active vérifiés indépendamment des scopes. Tout paramètre query inconnu est refusé ; les endpoints sans query refusent toute query.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "mobileOAuth": [
              "library:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "X-Library-Manager-Expected-User",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."
          }
        ],
        "responses": {
          "200": {
            "description": "Succès.",
            "headers": {
              "Cache-Control": {
                "description": "private, no-store sur toutes les réponses.",
                "schema": {
                  "type": "string"
                }
              },
              "Referrer-Policy": {
                "description": "no-referrer pour l’API.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Space"
                  }
                },
                "examples": {
                  "synthetic": {
                    "summary": "Exemple synthétique, identités/URLs/jetons inactifs",
                    "value": [
                      {
                        "id": "11111111-1111-4111-8111-111111111111",
                        "name": "Bibliothèque synthétique",
                        "slug": "demo-mobile",
                        "role": "editor",
                        "locale": "fr",
                        "plan": "free",
                        "createdAt": "2026-10-14T12:00:00.000Z"
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiHTTP401"
          },
          "403": {
            "$ref": "#/components/responses/ApiHTTP403"
          },
          "404": {
            "$ref": "#/components/responses/ApiHTTP404"
          },
          "409": {
            "$ref": "#/components/responses/ApiHTTP409"
          },
          "413": {
            "$ref": "#/components/responses/ApiHTTP413"
          },
          "422": {
            "$ref": "#/components/responses/ApiHTTP422"
          },
          "423": {
            "$ref": "#/components/responses/ApiHTTP423"
          },
          "429": {
            "$ref": "#/components/responses/ApiHTTP429"
          },
          "500": {
            "$ref": "#/components/responses/ApiHTTP500"
          },
          "503": {
            "$ref": "#/components/responses/ApiHTTP503"
          },
          "405": {
            "$ref": "#/components/responses/ApiHTTP405"
          }
        }
      }
    },
    "/api/v1/mobile/spaces/{spaceId}/books": {
      "get": {
        "operationId": "mobileListBooks",
        "tags": [
          "Bibliothèque"
        ],
        "summary": "Parcourir les livres avec tous les filtres",
        "description": "Pagination du catalogue : items, nextCursor, total. Cette liste ne constitue pas un snapshot atomique ; l’absence d’une page ne prouve pas une suppression. Bornes Min<=Max, From<=To ; dates/heures ISO avec fuseau. Les paramètres scalaires dupliqués sont refusés.\n\nScope requis : library:read. Droits et appartenance active vérifiés indépendamment des scopes. Tout paramètre query inconnu est refusé ; les endpoints sans query refusent toute query. Les filtres entre champs se combinent en AND, les valeurs d’un tableau en OR ; un tableau vide n’ajoute pas de filtre. Sans progress:read, readingPercent est null et les filtres readingPercentMin/readingPercentMax ainsi que sort=readingPercent sont refusés403, afin de prévenir un oracle sur la lecture.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "mobileOAuth": [
              "library:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "spaceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "X-Library-Manager-Expected-User",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 512
            },
            "description": "Recherche plein texte PostgreSQL websearch, accents normalisés ; ne pas fournir une requête SQL. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "authors",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 50,
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 1,
                "maxLength": 256
              }
            },
            "description": "Au moins un auteur exact parmi les valeurs, recouvrement OR. Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "series",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 50,
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 1,
                "maxLength": 512
              }
            },
            "description": "Série égale à l’une des valeurs exactes (OR). Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "seriesIndexMin",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000
            },
            "description": "Indice de série >= borne incluse. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "seriesIndexMax",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000
            },
            "description": "Indice de série <= borne incluse. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "genres",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 100,
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 1,
                "maxLength": 256
              }
            },
            "description": "Au moins un genre exact parmi les valeurs, recouvrement OR. Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "tags",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 100,
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 1,
                "maxLength": 256
              }
            },
            "description": "Au moins un tag exact parmi les valeurs, recouvrement OR. Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "languages",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 50,
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 2,
                "maxLength": 35,
                "pattern": "^[a-zA-Z]{2,8}(?:-[a-zA-Z0-9]{1,8})*$"
              }
            },
            "description": "Langue égale à l’une des étiquettes locales (OR). Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "formats",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 6,
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "epub",
                  "mobi",
                  "azw3",
                  "txt",
                  "html",
                  "fb2"
                ]
              }
            },
            "description": "Au moins un format prêt présent dans la famille physique (OR). Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "editions",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 50,
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 1,
                "maxLength": 512
              }
            },
            "description": "Edition égale à l’une des valeurs de métadonnée (OR). Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "publishers",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 50,
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 1,
                "maxLength": 512
              }
            },
            "description": "Editeur égal à l’une des valeurs de métadonnée (OR). Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "publicationDateFrom",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date",
              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"
            },
            "description": "Date de publication >= date ISO incluse, non date d’import. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "publicationDateTo",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date",
              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"
            },
            "description": "Date de publication <= date ISO incluse. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "statuses",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 6,
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "importing",
                  "ready",
                  "processing",
                  "needs_review",
                  "failed",
                  "merged"
                ]
              }
            },
            "description": "Statut métier égal à l’une des valeurs (OR). Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "metadataStatuses",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 7,
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "incomplete",
                  "queued",
                  "processing",
                  "complete",
                  "needs_review",
                  "waiting_configuration",
                  "failed"
                ]
              }
            },
            "description": "Statut métadonnées égal à l’une des valeurs (OR). Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "completeness",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 5,
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "complete",
                  "volume",
                  "part",
                  "omnibus",
                  "unknown"
                ]
              }
            },
            "description": "Complétude égale à l’une des valeurs (OR). Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "favorite",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Etat favori personnel du compte ; absence d’état vaut false. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "ratingMin",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 5
            },
            "description": "Note personnelle >= borne incluse ; une note inconnue ne satisfait pas le filtre. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "ratingMax",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 5
            },
            "description": "Note personnelle <= borne incluse ; une note inconnue ne satisfait pas le filtre. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "readingPercentMin",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100
            },
            "description": "Progression personnelle affichée >= borne incluse ; dernier rapport des lecteurs/web/kosync, absence vaut0. Cette valeur d’affichage ne remplace pas la révision CAS par version. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "readingPercentMax",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100
            },
            "description": "Progression personnelle affichée <= borne incluse ; absence vaut0. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "sizeBytesMin",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991
            },
            "description": "Somme des octets des versions prêtes de la famille >= borne incluse, en octets ; absence vaut0. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "sizeBytesMax",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991
            },
            "description": "Somme des octets des versions prêtes de la famille <= borne incluse, en octets. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "optimized",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Présence d’au moins une version optimized prête dans la famille physique ; absence vaut false. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "presentOnDeviceIds",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 100,
              "type": "array",
              "items": {
                "type": "string",
                "format": "uuid",
                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
              }
            },
            "description": "Au moins un UUID d’appareil personnel dont l’inventaire actuel correspond à une version prête en SHA/taille/format. Ne représente pas les confirmations de copie de cette API mobile. Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "createdFrom",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
            },
            "description": "Création du livre >= instant ISO avec fuseau inclus. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "createdTo",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
            },
            "description": "Création du livre <= instant ISO avec fuseau inclus. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "updatedFrom",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
            },
            "description": "Dernière modification du livre >= instant ISO avec fuseau inclus. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "updatedTo",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
            },
            "description": "Dernière modification du livre <= instant ISO avec fuseau inclus. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "collection",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Livres d'une collection de l'espace (un livre fusionné compte comme le livre qu'il a rejoint). Paramètre scalaire : doublons refusés."
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "default": "title",
              "type": "string",
              "enum": [
                "title",
                "author",
                "series",
                "seriesIndex",
                "publicationDate",
                "createdAt",
                "updatedAt",
                "totalBytes",
                "rating",
                "readingPercent"
              ]
            },
            "description": "Tri stable par le critère demandé avec identité en départage ; utiliser le curseur renvoyé. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "schema": {
              "default": "asc",
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ]
            },
            "description": "Sens du tri, asc par défaut. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "view",
            "in": "query",
            "required": false,
            "schema": {
              "default": "grid",
              "type": "string",
              "enum": [
                "grid",
                "table"
              ]
            },
            "description": "Convention de présentation du catalogue (grid/table) ; le schéma de réponse mobile reste identique. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 2048,
              "pattern": "^[A-Za-z0-9_-]+$"
            },
            "description": "Curseur opaque lié aux filtres/tri/contexte du compte. Le recopier sans fabrication ; conserver les mêmes filtres/tri pendant la pagination. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "schema": {
              "default": 50,
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            },
            "description": "Nombre maximum d’éléments1..100, défaut50. Paramètre scalaire : doublons refusés."
          }
        ],
        "responses": {
          "200": {
            "description": "Succès.",
            "headers": {
              "Cache-Control": {
                "description": "private, no-store sur toutes les réponses.",
                "schema": {
                  "type": "string"
                }
              },
              "Referrer-Policy": {
                "description": "no-referrer pour l’API.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookPage"
                },
                "examples": {
                  "synthetic": {
                    "summary": "Exemple synthétique, identités/URLs/jetons inactifs",
                    "value": {
                      "items": [
                        {
                          "id": "22222222-2222-4222-8222-222222222222",
                          "tenantId": "11111111-1111-4111-8111-111111111111",
                          "title": "Livre de démonstration",
                          "authors": [
                            "Auteur synthétique"
                          ],
                          "series": null,
                          "seriesIndex": null,
                          "seriesPositionLabel": null,
                          "language": "fr",
                          "genres": [],
                          "tags": [],
                          "completeness": "unknown",
                          "formats": [
                            "epub"
                          ],
                          "status": "ready",
                          "metadataStatus": "incomplete",
                          "revision": 1,
                          "favorite": false,
                          "rating": null,
                          "readingPercent": 12.5,
                          "createdAt": "2026-10-14T12:00:00.000Z",
                          "updatedAt": "2026-10-14T12:00:00.000Z"
                        }
                      ],
                      "nextCursor": null,
                      "total": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiHTTP401"
          },
          "403": {
            "$ref": "#/components/responses/ApiHTTP403"
          },
          "404": {
            "$ref": "#/components/responses/ApiHTTP404"
          },
          "409": {
            "$ref": "#/components/responses/ApiHTTP409"
          },
          "413": {
            "$ref": "#/components/responses/ApiHTTP413"
          },
          "422": {
            "$ref": "#/components/responses/ApiHTTP422"
          },
          "423": {
            "$ref": "#/components/responses/ApiHTTP423"
          },
          "429": {
            "$ref": "#/components/responses/ApiHTTP429"
          },
          "500": {
            "$ref": "#/components/responses/ApiHTTP500"
          },
          "503": {
            "$ref": "#/components/responses/ApiHTTP503"
          },
          "405": {
            "$ref": "#/components/responses/ApiHTTP405"
          }
        }
      }
    },
    "/api/v1/mobile/spaces/{spaceId}/books/search": {
      "get": {
        "operationId": "mobileSearchBooks",
        "tags": [
          "Bibliothèque"
        ],
        "summary": "Rechercher les livres d’un espace par mots-clés",
        "description": "Alias dédié du catalogue : q est obligatoire, présent une seule fois, trim puis1..512 caractères. Le paramètre search est interdit sur cet alias, même avec q ; les autres filtres et leur validation sont identiques au catalogue. Recherche plein texte sur six champs de métadonnées exactement : title, subtitle, authors, series, genres, tags. Ne recherche ni fallback_title, nom de fichier, ISBN, éditeur, description ou contenu du livre. Configuration PostgreSQL simple et unaccent : lexèmes insensibles à la casse et aux accents, sans stemming français, sous-chaîne, préfixe ou fuzzy. Les termes non cités se combinent en AND ; OR explicite ouvre une alternative, les guillemets doubles recherchent une séquence de lexèmes et - exclut un terme ou une phrase. La valeur reste un paramètre SQL, jamais une instruction SQL. Aucun tri par pertinence : title asc par défaut, avec UUID en départage ; les tris et directions du catalogue restent disponibles. pageSize1..100, défaut50. Réponse BookPage avec items, nextCursor et total:null ; pagination sans snapshot atomique. Conserver les mêmes mots-clés, filtres, tri et taille de page avec le curseur opaque lié au compte et à l’espace. Scope library:read et Bearer mobile obligatoires, aucun cookie de remplacement ; droits et adhésion active réévalués dans l’espace demandé. Sans progress:read, readingPercent reste null et readingPercentMin, readingPercentMax ou sort=readingPercent sont refusés403 avant accès au catalogue. Query fermée : champs inconnus, q/search ambigu, scalaire dupliqué, q absent/vide/trop long, UUID invalide ou bornes inversées donnent422 validationFailed. Les filtres entre champs se combinent en AND et les valeurs d’un tableau en OR ; un tableau vide n’ajoute pas de filtre. Réponses privées no-store et no-referrer. GET seulement ; les autres méthodes reçoivent405 Allow: GET après contrôle Bearer, sans recherche. Le catalogue historique /books?search=... demeure disponible.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "mobileOAuth": [
              "library:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "spaceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "X-Library-Manager-Expected-User",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."
          },
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 512
            },
            "description": "Mots-clés obligatoires, un unique scalaire, trim puis1..512 caractères. Paramètre search interdit sur cet alias. Lexèmes des métadonnées title/subtitle/authors/series/genres/tags, insensibles à la casse et aux accents. Termes en AND, OR explicite, phrase entre guillemets doubles, - exclusion. Pas de recherche de sous-chaîne, préfixe, fuzzy, nom de fichier, ISBN ou contenu du livre."
          },
          {
            "name": "authors",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 50,
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 1,
                "maxLength": 256
              }
            },
            "description": "Au moins un auteur exact parmi les valeurs, recouvrement OR. Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "series",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 50,
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 1,
                "maxLength": 512
              }
            },
            "description": "Série égale à l’une des valeurs exactes (OR). Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "seriesIndexMin",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000
            },
            "description": "Indice de série >= borne incluse. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "seriesIndexMax",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000000
            },
            "description": "Indice de série <= borne incluse. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "genres",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 100,
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 1,
                "maxLength": 256
              }
            },
            "description": "Au moins un genre exact parmi les valeurs, recouvrement OR. Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "tags",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 100,
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 1,
                "maxLength": 256
              }
            },
            "description": "Au moins un tag exact parmi les valeurs, recouvrement OR. Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "languages",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 50,
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 2,
                "maxLength": 35,
                "pattern": "^[a-zA-Z]{2,8}(?:-[a-zA-Z0-9]{1,8})*$"
              }
            },
            "description": "Langue égale à l’une des étiquettes locales (OR). Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "formats",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 6,
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "epub",
                  "mobi",
                  "azw3",
                  "txt",
                  "html",
                  "fb2"
                ]
              }
            },
            "description": "Au moins un format prêt présent dans la famille physique (OR). Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "editions",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 50,
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 1,
                "maxLength": 512
              }
            },
            "description": "Edition égale à l’une des valeurs de métadonnée (OR). Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "publishers",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 50,
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 1,
                "maxLength": 512
              }
            },
            "description": "Editeur égal à l’une des valeurs de métadonnée (OR). Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "publicationDateFrom",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date",
              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"
            },
            "description": "Date de publication >= date ISO incluse, non date d’import. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "publicationDateTo",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date",
              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"
            },
            "description": "Date de publication <= date ISO incluse. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "statuses",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 6,
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "importing",
                  "ready",
                  "processing",
                  "needs_review",
                  "failed",
                  "merged"
                ]
              }
            },
            "description": "Statut métier égal à l’une des valeurs (OR). Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "metadataStatuses",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 7,
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "incomplete",
                  "queued",
                  "processing",
                  "complete",
                  "needs_review",
                  "waiting_configuration",
                  "failed"
                ]
              }
            },
            "description": "Statut métadonnées égal à l’une des valeurs (OR). Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "completeness",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 5,
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "complete",
                  "volume",
                  "part",
                  "omnibus",
                  "unknown"
                ]
              }
            },
            "description": "Complétude égale à l’une des valeurs (OR). Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "favorite",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Etat favori personnel du compte ; absence d’état vaut false. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "ratingMin",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 5
            },
            "description": "Note personnelle >= borne incluse ; une note inconnue ne satisfait pas le filtre. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "ratingMax",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 5
            },
            "description": "Note personnelle <= borne incluse ; une note inconnue ne satisfait pas le filtre. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "readingPercentMin",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100
            },
            "description": "Progression personnelle affichée >= borne incluse ; dernier rapport des lecteurs/web/kosync, absence vaut0. Cette valeur d’affichage ne remplace pas la révision CAS par version. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "readingPercentMax",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100
            },
            "description": "Progression personnelle affichée <= borne incluse ; absence vaut0. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "sizeBytesMin",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991
            },
            "description": "Somme des octets des versions prêtes de la famille >= borne incluse, en octets ; absence vaut0. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "sizeBytesMax",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991
            },
            "description": "Somme des octets des versions prêtes de la famille <= borne incluse, en octets. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "optimized",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Présence d’au moins une version optimized prête dans la famille physique ; absence vaut false. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "presentOnDeviceIds",
            "in": "query",
            "required": false,
            "schema": {
              "maxItems": 100,
              "type": "array",
              "items": {
                "type": "string",
                "format": "uuid",
                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
              }
            },
            "description": "Au moins un UUID d’appareil personnel dont l’inventaire actuel correspond à une version prête en SHA/taille/format. Ne représente pas les confirmations de copie de cette API mobile. Répéter le nom ou fournir un tableau JSON encodé unique ; les champs différents se combinent en AND.",
            "style": "form",
            "explode": true
          },
          {
            "name": "createdFrom",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
            },
            "description": "Création du livre >= instant ISO avec fuseau inclus. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "createdTo",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
            },
            "description": "Création du livre <= instant ISO avec fuseau inclus. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "updatedFrom",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
            },
            "description": "Dernière modification du livre >= instant ISO avec fuseau inclus. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "updatedTo",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
            },
            "description": "Dernière modification du livre <= instant ISO avec fuseau inclus. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "collection",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Livres d'une collection de l'espace (un livre fusionné compte comme le livre qu'il a rejoint). Paramètre scalaire : doublons refusés."
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "default": "title",
              "type": "string",
              "enum": [
                "title",
                "author",
                "series",
                "seriesIndex",
                "publicationDate",
                "createdAt",
                "updatedAt",
                "totalBytes",
                "rating",
                "readingPercent"
              ]
            },
            "description": "Tri stable par le critère demandé avec identité en départage ; utiliser le curseur renvoyé. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "schema": {
              "default": "asc",
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ]
            },
            "description": "Sens du tri, asc par défaut. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "view",
            "in": "query",
            "required": false,
            "schema": {
              "default": "grid",
              "type": "string",
              "enum": [
                "grid",
                "table"
              ]
            },
            "description": "Convention de présentation du catalogue (grid/table) ; le schéma de réponse mobile reste identique. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 2048,
              "pattern": "^[A-Za-z0-9_-]+$"
            },
            "description": "Curseur opaque lié aux filtres/tri/contexte du compte. Le recopier sans fabrication ; conserver les mêmes filtres/tri pendant la pagination. Paramètre scalaire : doublons refusés."
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "schema": {
              "default": 50,
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            },
            "description": "Nombre maximum d’éléments1..100, défaut50. Paramètre scalaire : doublons refusés."
          }
        ],
        "responses": {
          "200": {
            "description": "Succès.",
            "headers": {
              "Cache-Control": {
                "description": "private, no-store sur toutes les réponses.",
                "schema": {
                  "type": "string"
                }
              },
              "Referrer-Policy": {
                "description": "no-referrer pour l’API.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookPage"
                },
                "examples": {
                  "synthetic": {
                    "summary": "Exemple synthétique, identités/URLs/jetons inactifs",
                    "value": {
                      "items": [
                        {
                          "id": "22222222-2222-4222-8222-222222222222",
                          "tenantId": "11111111-1111-4111-8111-111111111111",
                          "title": "Livre de démonstration",
                          "authors": [
                            "Auteur synthétique"
                          ],
                          "series": null,
                          "seriesIndex": null,
                          "seriesPositionLabel": null,
                          "language": "fr",
                          "genres": [],
                          "tags": [],
                          "completeness": "unknown",
                          "formats": [
                            "epub"
                          ],
                          "status": "ready",
                          "metadataStatus": "incomplete",
                          "revision": 1,
                          "favorite": false,
                          "rating": null,
                          "readingPercent": 12.5,
                          "createdAt": "2026-10-14T12:00:00.000Z",
                          "updatedAt": "2026-10-14T12:00:00.000Z"
                        }
                      ],
                      "nextCursor": null,
                      "total": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiHTTP401"
          },
          "403": {
            "$ref": "#/components/responses/ApiHTTP403"
          },
          "422": {
            "$ref": "#/components/responses/ApiHTTP422"
          },
          "429": {
            "$ref": "#/components/responses/ApiHTTP429"
          },
          "500": {
            "$ref": "#/components/responses/ApiHTTP500"
          },
          "503": {
            "$ref": "#/components/responses/ApiHTTP503"
          },
          "405": {
            "$ref": "#/components/responses/ApiHTTP405"
          }
        }
      }
    },
    "/api/v1/mobile/spaces/{spaceId}/books/{bookId}": {
      "get": {
        "operationId": "mobileGetBook",
        "tags": [
          "Bibliothèque"
        ],
        "summary": "Lire les détails et versions prêtes",
        "description": "Métadonnées, versions ready et état personnel. Versions immuables : ne jamais réutiliser les ancres exactes d’une édition comme si elles appartenaient aux octets d’une autre.\n\nScope requis : library:read. Droits et appartenance active vérifiés indépendamment des scopes. Tout paramètre query inconnu est refusé ; les endpoints sans query refusent toute query.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "mobileOAuth": [
              "library:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "spaceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "bookId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "X-Library-Manager-Expected-User",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."
          }
        ],
        "responses": {
          "200": {
            "description": "Succès.",
            "headers": {
              "Cache-Control": {
                "description": "private, no-store sur toutes les réponses.",
                "schema": {
                  "type": "string"
                }
              },
              "Referrer-Policy": {
                "description": "no-referrer pour l’API.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Book"
                },
                "examples": {
                  "synthetic": {
                    "summary": "Exemple synthétique, identités/URLs/jetons inactifs",
                    "value": {
                      "id": "22222222-2222-4222-8222-222222222222",
                      "tenantId": "11111111-1111-4111-8111-111111111111",
                      "title": "Livre de démonstration",
                      "authors": [
                        "Auteur synthétique"
                      ],
                      "series": null,
                      "seriesIndex": null,
                      "seriesPositionLabel": null,
                      "language": "fr",
                      "genres": [],
                      "tags": [],
                      "completeness": "unknown",
                      "formats": [
                        "epub"
                      ],
                      "status": "ready",
                      "metadataStatus": "incomplete",
                      "revision": 1,
                      "favorite": false,
                      "rating": null,
                      "readingPercent": 12.5,
                      "createdAt": "2026-10-14T12:00:00.000Z",
                      "updatedAt": "2026-10-14T12:00:00.000Z",
                      "metadata": {
                        "title": "Livre de démonstration",
                        "subtitle": null,
                        "authors": [
                          "Auteur synthétique"
                        ],
                        "authorSort": [
                          "Synthétique, Auteur"
                        ],
                        "series": null,
                        "seriesIndex": null,
                        "seriesPositionLabel": null,
                        "language": "fr",
                        "genres": [],
                        "tags": [],
                        "description": null,
                        "publisher": null,
                        "publicationDate": null,
                        "translators": [],
                        "identifiers": {
                          "isbn10": null,
                          "isbn13": null,
                          "asin": null,
                          "other": {}
                        },
                        "completeness": "unknown",
                        "partNumber": null,
                        "partCount": null,
                        "edition": null
                      },
                      "versions": [
                        {
                          "id": "33333333-3333-4333-8333-333333333333",
                          "bookId": "22222222-2222-4222-8222-222222222222",
                          "parentVersionId": null,
                          "kind": "original",
                          "profile": "original",
                          "format": "epub",
                          "filename": "livre-exemple.epub",
                          "mediaType": "application/epub+zip",
                          "sizeBytes": 123456,
                          "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
                          "status": "ready",
                          "createdAt": "2026-10-14T12:00:00.000Z"
                        }
                      ],
                      "progress": {
                        "bookId": "22222222-2222-4222-8222-222222222222",
                        "versionId": "33333333-3333-4333-8333-333333333333",
                        "revision": 1,
                        "updatedAt": "2026-10-14T12:00:00.000Z",
                        "progress": "/body/DocFragment[1]/body/p[1]/text()[1].0",
                        "percentage": 0.125
                      },
                      "userState": {
                        "bookId": "22222222-2222-4222-8222-222222222222",
                        "favorite": false,
                        "rating": null,
                        "note": null,
                        "revision": 0,
                        "updatedAt": null
                      },
                      "readingMode": "local_copy"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiHTTP401"
          },
          "403": {
            "$ref": "#/components/responses/ApiHTTP403"
          },
          "404": {
            "$ref": "#/components/responses/ApiHTTP404"
          },
          "409": {
            "$ref": "#/components/responses/ApiHTTP409"
          },
          "413": {
            "$ref": "#/components/responses/ApiHTTP413"
          },
          "422": {
            "$ref": "#/components/responses/ApiHTTP422"
          },
          "423": {
            "$ref": "#/components/responses/ApiHTTP423"
          },
          "429": {
            "$ref": "#/components/responses/ApiHTTP429"
          },
          "500": {
            "$ref": "#/components/responses/ApiHTTP500"
          },
          "503": {
            "$ref": "#/components/responses/ApiHTTP503"
          },
          "405": {
            "$ref": "#/components/responses/ApiHTTP405"
          }
        }
      }
    },
    "/api/v1/mobile/spaces/{spaceId}/books/{bookId}/versions/{versionId}/download": {
      "get": {
        "operationId": "mobileGetDownload",
        "tags": [
          "Fichiers et copies locales"
        ],
        "summary": "Obtenir la capacité de copie cloud vers local",
        "description": "Télécharger vers une zone temporaire privée, contrôler taille et SHA-256, publier atomiquement puis confirmer la copie. La signature est liée à un reçu fiable et à sa version stockage. Un téléchargement est un transfert de fichier : aucun chapitre/HTML de lecture distante n’est exposé.\n\nScope requis : files:read. Droits et appartenance active vérifiés indépendamment des scopes. Tout paramètre query inconnu est refusé ; les endpoints sans query refusent toute query.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "mobileOAuth": [
              "files:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "spaceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "bookId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "versionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "X-Library-Manager-Expected-User",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."
          }
        ],
        "responses": {
          "200": {
            "description": "Succès.",
            "headers": {
              "Cache-Control": {
                "description": "private, no-store sur toutes les réponses.",
                "schema": {
                  "type": "string"
                }
              },
              "Referrer-Policy": {
                "description": "no-referrer pour l’API.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Download"
                },
                "examples": {
                  "synthetic": {
                    "summary": "Exemple synthétique, identités/URLs/jetons inactifs",
                    "value": {
                      "bookId": "22222222-2222-4222-8222-222222222222",
                      "versionId": "33333333-3333-4333-8333-333333333333",
                      "format": "epub",
                      "filename": "livre-exemple.epub",
                      "mediaType": "application/epub+zip",
                      "sizeBytes": 123456,
                      "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
                      "download": {
                        "url": "https://storage.example.test/synthetic-download.epub?example=inactive",
                        "expiresAt": "2026-10-14T12:05:00.000Z"
                      },
                      "readingMode": "local_copy"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiHTTP401"
          },
          "403": {
            "$ref": "#/components/responses/ApiHTTP403"
          },
          "404": {
            "$ref": "#/components/responses/ApiHTTP404"
          },
          "409": {
            "$ref": "#/components/responses/ApiHTTP409"
          },
          "413": {
            "$ref": "#/components/responses/ApiHTTP413"
          },
          "422": {
            "$ref": "#/components/responses/ApiHTTP422"
          },
          "423": {
            "$ref": "#/components/responses/ApiHTTP423"
          },
          "429": {
            "$ref": "#/components/responses/ApiHTTP429"
          },
          "500": {
            "$ref": "#/components/responses/ApiHTTP500"
          },
          "503": {
            "$ref": "#/components/responses/ApiHTTP503"
          },
          "405": {
            "$ref": "#/components/responses/ApiHTTP405"
          }
        }
      }
    },
    "/api/v1/mobile/spaces/{spaceId}/books/{bookId}/local-copy": {
      "put": {
        "operationId": "mobileConfirmLocalCopy",
        "tags": [
          "Fichiers et copies locales"
        ],
        "summary": "Confirmer une copie locale vérifiée",
        "description": "PUT idempotent. installationId UUID stable conservé avec la bibliothèque. Le serveur valide version ready, SHA et taille exactes mais ne peut pas inspecter le stockage physique du téléphone. Vérifier les octets avant chaque ouverture locale.\n\nScope requis : files:read. Droits et appartenance active vérifiés indépendamment des scopes. Tout paramètre query inconnu est refusé ; les endpoints sans query refusent toute query. Budget SQL atomique :20 installations et100000 copies par compte/espace. Confirmation existante idempotente même au plafond. Budget dépassé:429 rateLimited sans délai; oublier les copies inutiles avant reprise.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "mobileOAuth": [
              "files:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "spaceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "bookId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "X-Library-Manager-Expected-User",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."
          }
        ],
        "responses": {
          "200": {
            "description": "Succès.",
            "headers": {
              "Cache-Control": {
                "description": "private, no-store sur toutes les réponses.",
                "schema": {
                  "type": "string"
                }
              },
              "Referrer-Policy": {
                "description": "no-referrer pour l’API.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LocalCopy"
                },
                "examples": {
                  "synthetic": {
                    "summary": "Exemple synthétique, identités/URLs/jetons inactifs",
                    "value": {
                      "installationId": "44444444-4444-4444-8444-444444444444",
                      "bookId": "22222222-2222-4222-8222-222222222222",
                      "versionId": "33333333-3333-4333-8333-333333333333",
                      "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
                      "sizeBytes": 123456,
                      "updatedAt": "2026-10-14T12:00:00.000Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiHTTP401"
          },
          "403": {
            "$ref": "#/components/responses/ApiHTTP403"
          },
          "404": {
            "$ref": "#/components/responses/ApiHTTP404"
          },
          "409": {
            "$ref": "#/components/responses/ApiHTTP409"
          },
          "413": {
            "$ref": "#/components/responses/ApiHTTP413"
          },
          "422": {
            "$ref": "#/components/responses/ApiHTTP422"
          },
          "423": {
            "$ref": "#/components/responses/ApiHTTP423"
          },
          "429": {
            "$ref": "#/components/responses/ApiHTTP429"
          },
          "500": {
            "$ref": "#/components/responses/ApiHTTP500"
          },
          "503": {
            "$ref": "#/components/responses/ApiHTTP503"
          },
          "405": {
            "$ref": "#/components/responses/ApiHTTP405"
          }
        },
        "requestBody": {
          "required": true,
          "description": "JSON strict UTF-8 <=65536 octets ; champs inconnus et doublons de paramètres refusés.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MobileLocalCopyInput"
              },
              "example": {
                "installationId": "44444444-4444-4444-8444-444444444444",
                "versionId": "33333333-3333-4333-8333-333333333333",
                "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
                "sizeBytes": 123456
              }
            }
          }
        }
      }
    },
    "/api/v1/mobile/spaces/{spaceId}/local-copies": {
      "get": {
        "operationId": "mobileListLocalCopies",
        "tags": [
          "Fichiers et copies locales"
        ],
        "summary": "Lister les confirmations de cette installation",
        "description": "Inventaire personnel courant paginé par versionId. Ce sont des déclarations client, pas un scan des fichiers. Aucun snapshot de suppression : réconcilier explicitement après un parcours incomplet.\n\nScope requis : files:read. Droits et appartenance active vérifiés indépendamment des scopes. Tout paramètre query inconnu est refusé ; les endpoints sans query refusent toute query.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "mobileOAuth": [
              "files:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "spaceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "X-Library-Manager-Expected-User",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."
          },
          {
            "name": "installationId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "UUID stable de cette installation."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "versionId de nextCursor précédent ; omettre pour la première page."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            },
            "description": "Taille de page, défaut50, maximum100."
          }
        ],
        "responses": {
          "200": {
            "description": "Succès.",
            "headers": {
              "Cache-Control": {
                "description": "private, no-store sur toutes les réponses.",
                "schema": {
                  "type": "string"
                }
              },
              "Referrer-Policy": {
                "description": "no-referrer pour l’API.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LocalCopyPage"
                },
                "examples": {
                  "synthetic": {
                    "summary": "Exemple synthétique, identités/URLs/jetons inactifs",
                    "value": {
                      "items": [
                        {
                          "installationId": "44444444-4444-4444-8444-444444444444",
                          "bookId": "22222222-2222-4222-8222-222222222222",
                          "versionId": "33333333-3333-4333-8333-333333333333",
                          "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
                          "sizeBytes": 123456,
                          "updatedAt": "2026-10-14T12:00:00.000Z"
                        }
                      ],
                      "nextCursor": null,
                      "total": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiHTTP401"
          },
          "403": {
            "$ref": "#/components/responses/ApiHTTP403"
          },
          "404": {
            "$ref": "#/components/responses/ApiHTTP404"
          },
          "409": {
            "$ref": "#/components/responses/ApiHTTP409"
          },
          "413": {
            "$ref": "#/components/responses/ApiHTTP413"
          },
          "422": {
            "$ref": "#/components/responses/ApiHTTP422"
          },
          "423": {
            "$ref": "#/components/responses/ApiHTTP423"
          },
          "429": {
            "$ref": "#/components/responses/ApiHTTP429"
          },
          "500": {
            "$ref": "#/components/responses/ApiHTTP500"
          },
          "503": {
            "$ref": "#/components/responses/ApiHTTP503"
          },
          "405": {
            "$ref": "#/components/responses/ApiHTTP405"
          }
        }
      }
    },
    "/api/v1/mobile/spaces/{spaceId}/local-copies/{versionId}": {
      "delete": {
        "operationId": "mobileForgetLocalCopy",
        "tags": [
          "Fichiers et copies locales"
        ],
        "summary": "Oublier une confirmation locale",
        "description": "Idempotent. À appeler après suppression du fichier local/catalogue. Ne supprime ni livre cloud, ni progression, ni copie d’un autre appareil. installationId obligatoire.\n\nScope requis : files:read. Droits et appartenance active vérifiés indépendamment des scopes. Tout paramètre query inconnu est refusé ; les endpoints sans query refusent toute query.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "mobileOAuth": [
              "files:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "spaceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "versionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "X-Library-Manager-Expected-User",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."
          },
          {
            "name": "installationId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": ""
          }
        ],
        "responses": {
          "204": {
            "description": "Confirmation oubliée ; réponse sans corps.",
            "headers": {
              "Cache-Control": {
                "description": "private, no-store sur toutes les réponses.",
                "schema": {
                  "type": "string"
                }
              },
              "Referrer-Policy": {
                "description": "no-referrer pour l’API.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiHTTP401"
          },
          "403": {
            "$ref": "#/components/responses/ApiHTTP403"
          },
          "404": {
            "$ref": "#/components/responses/ApiHTTP404"
          },
          "409": {
            "$ref": "#/components/responses/ApiHTTP409"
          },
          "413": {
            "$ref": "#/components/responses/ApiHTTP413"
          },
          "422": {
            "$ref": "#/components/responses/ApiHTTP422"
          },
          "423": {
            "$ref": "#/components/responses/ApiHTTP423"
          },
          "429": {
            "$ref": "#/components/responses/ApiHTTP429"
          },
          "500": {
            "$ref": "#/components/responses/ApiHTTP500"
          },
          "503": {
            "$ref": "#/components/responses/ApiHTTP503"
          },
          "405": {
            "$ref": "#/components/responses/ApiHTTP405"
          }
        }
      }
    },
    "/api/v1/mobile/spaces/{spaceId}/books/{bookId}/progress": {
      "get": {
        "operationId": "mobileGetProgress",
        "tags": [
          "Progression de lecture"
        ],
        "summary": "Lire la progression personnelle d’une version",
        "description": "Renvoie ReaderProgress ou null. Les ancres conservées entre versions évitent d’inventer une position. Pour une écriture, expectedRevision doit suivre la révision CAS de la progression du livre logique ; une absence totale de progression commence à0.\n\nScope requis : progress:read. Droits et appartenance active vérifiés indépendamment des scopes. Tout paramètre query inconnu est refusé ; les endpoints sans query refusent toute query.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "mobileOAuth": [
              "progress:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "spaceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "bookId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "X-Library-Manager-Expected-User",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."
          },
          {
            "name": "versionId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Version immuable choisie par le lecteur local."
          }
        ],
        "responses": {
          "200": {
            "description": "Succès.",
            "headers": {
              "Cache-Control": {
                "description": "private, no-store sur toutes les réponses.",
                "schema": {
                  "type": "string"
                }
              },
              "Referrer-Policy": {
                "description": "no-referrer pour l’API.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ReaderProgress"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "examples": {
                  "synthetic": {
                    "summary": "Exemple synthétique, identités/URLs/jetons inactifs",
                    "value": {
                      "bookId": "22222222-2222-4222-8222-222222222222",
                      "versionId": "33333333-3333-4333-8333-333333333333",
                      "revision": 1,
                      "updatedAt": "2026-10-14T12:00:00.000Z",
                      "progress": "/body/DocFragment[1]/body/p[1]/text()[1].0",
                      "percentage": 0.125
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiHTTP401"
          },
          "403": {
            "$ref": "#/components/responses/ApiHTTP403"
          },
          "404": {
            "$ref": "#/components/responses/ApiHTTP404"
          },
          "409": {
            "$ref": "#/components/responses/ApiHTTP409"
          },
          "413": {
            "$ref": "#/components/responses/ApiHTTP413"
          },
          "422": {
            "$ref": "#/components/responses/ApiHTTP422"
          },
          "423": {
            "$ref": "#/components/responses/ApiHTTP423"
          },
          "429": {
            "$ref": "#/components/responses/ApiHTTP429"
          },
          "500": {
            "$ref": "#/components/responses/ApiHTTP500"
          },
          "503": {
            "$ref": "#/components/responses/ApiHTTP503"
          },
          "405": {
            "$ref": "#/components/responses/ApiHTTP405"
          }
        }
      },
      "put": {
        "operationId": "mobileSaveProgress",
        "tags": [
          "Progression de lecture"
        ],
        "summary": "Synchroniser une opération de lecture durable",
        "description": "Exige la confirmation personnelle de la copie sur cette installation. Journaliser avant envoi, sérialiser les opérations, conserver le même corps pour un retry. Reçus conservés30 jours, au plus10000 par compte/espace ; un vieux journal exige réconciliation. CAS atomique sur livre logique, valide aussi après fusion. Un retry accepté renvoie son reçu original même si une opération plus récente a avancé. HTTP409 : relire la progression, montrer positions/versions/dates puis choix explicite ; ne pas appliquer max(percentage), ni l’horloge du téléphone, ni retry forcé. Un retour volontaire en arrière est valable.\n\nScope requis : progress:write. Droits et appartenance active vérifiés indépendamment des scopes. Tout paramètre query inconnu est refusé ; les endpoints sans query refusent toute query.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "mobileOAuth": [
              "progress:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "spaceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "bookId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "X-Library-Manager-Expected-User",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."
          }
        ],
        "responses": {
          "200": {
            "description": "Succès.",
            "headers": {
              "Cache-Control": {
                "description": "private, no-store sur toutes les réponses.",
                "schema": {
                  "type": "string"
                }
              },
              "Referrer-Policy": {
                "description": "no-referrer pour l’API.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReaderProgress"
                },
                "examples": {
                  "synthetic": {
                    "summary": "Exemple synthétique, identités/URLs/jetons inactifs",
                    "value": {
                      "bookId": "22222222-2222-4222-8222-222222222222",
                      "versionId": "33333333-3333-4333-8333-333333333333",
                      "revision": 1,
                      "updatedAt": "2026-10-14T12:00:00.000Z",
                      "progress": "/body/DocFragment[1]/body/p[1]/text()[1].0",
                      "percentage": 0.125
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiHTTP401"
          },
          "403": {
            "$ref": "#/components/responses/ApiHTTP403"
          },
          "404": {
            "$ref": "#/components/responses/ApiHTTP404"
          },
          "409": {
            "$ref": "#/components/responses/ApiHTTP409"
          },
          "413": {
            "$ref": "#/components/responses/ApiHTTP413"
          },
          "422": {
            "$ref": "#/components/responses/ApiHTTP422"
          },
          "423": {
            "$ref": "#/components/responses/ApiHTTP423"
          },
          "429": {
            "$ref": "#/components/responses/ApiHTTP429"
          },
          "500": {
            "$ref": "#/components/responses/ApiHTTP500"
          },
          "503": {
            "$ref": "#/components/responses/ApiHTTP503"
          },
          "405": {
            "$ref": "#/components/responses/ApiHTTP405"
          }
        },
        "requestBody": {
          "required": true,
          "description": "JSON strict UTF-8 <=65536 octets ; champs inconnus et doublons de paramètres refusés.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MobileProgressInput"
              },
              "example": {
                "installationId": "44444444-4444-4444-8444-444444444444",
                "operationId": "66666666-6666-4666-8666-666666666666",
                "expectedRevision": 0,
                "versionId": "33333333-3333-4333-8333-333333333333",
                "progress": "/body/DocFragment[1]/body/p[1]/text()[1].0",
                "percentage": 0.125
              }
            }
          }
        }
      }
    },
    "/api/v1/mobile/spaces/{spaceId}/books/{bookId}/annotations": {
      "get": {"operationId":"mobileListAnnotations","tags":["Signets et notes"],"summary":"Lister ses signets et notes d’un livre","description":"Renvoie les signets et notes de passage du compte sur ce livre logique, toutes versions confondues, du plus ancien au plus récent (1000 au plus). Privés : un autre membre de l’espace ne les voit jamais. Chaque élément porte sa version : l’afficher comme « d’une autre version » quand le fichier ouvert diffère, sans le déplacer.\n\nScope requis : progress:read. Droits et appartenance active vérifiés indépendamment des scopes. Tout paramètre query inconnu est refusé ; les endpoints sans query refusent toute query.","security":[{"bearerAuth":[]},{"mobileOAuth":["progress:read"]}],"parameters":[{"name":"spaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Identité UUID transmise ; droits résolus par le serveur."},{"name":"bookId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Identité UUID transmise ; droits résolus par le serveur."},{"name":"X-Library-Manager-Expected-User","in":"header","required":false,"schema":{"type":"string","format":"uuid"},"description":"Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."}],"responses":{"200":{"description":"Succès.","headers":{"Cache-Control":{"description":"private, no-store sur toutes les réponses.","schema":{"type":"string"}},"Referrer-Policy":{"description":"no-referrer pour l’API.","schema":{"type":"string"}},"X-Request-Id":{"description":"UUID corrélable sans journaliser de jeton ou données personnelles.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnnotationList"},"examples":{"synthetic":{"summary":"Exemple synthétique, identités/URLs/jetons inactifs","value":{"items":[{"id":"77777777-7777-4777-8777-777777777777","bookId":"22222222-2222-4222-8222-222222222222","versionId":"33333333-3333-4333-8333-333333333333","chapterId":"chapter-1","position":"/body/DocFragment[1]/body/p[3]/text().42","quote":"Exemple de passage synthétique.","note":"Note synthétique.","revision":1,"createdAt":"2026-10-15T12:00:00.000Z","updatedAt":"2026-10-15T12:00:00.000Z"}]}}}}}},"401":{"$ref":"#/components/responses/ApiHTTP401"},"403":{"$ref":"#/components/responses/ApiHTTP403"},"404":{"$ref":"#/components/responses/ApiHTTP404"},"405":{"$ref":"#/components/responses/ApiHTTP405"},"409":{"$ref":"#/components/responses/ApiHTTP409"},"413":{"$ref":"#/components/responses/ApiHTTP413"},"422":{"$ref":"#/components/responses/ApiHTTP422"},"423":{"$ref":"#/components/responses/ApiHTTP423"},"429":{"$ref":"#/components/responses/ApiHTTP429"},"500":{"$ref":"#/components/responses/ApiHTTP500"},"503":{"$ref":"#/components/responses/ApiHTTP503"}}},
      "post": {"operationId":"mobileCreateAnnotation","tags":["Signets et notes"],"summary":"Placer un signet ou une note de passage","description":"Crée un signet (note absente) ou une note de passage dans une version prête de ce livre. Position : XPointer texte KOReader, ou ratio:<0..1> dans le chapitre nommé par chapterId. Extrait limité à 600 caractères, note à 4000. Un livre accepte au plus 1000 signets et notes par compte (HTTP413). Un Lecteur peut annoter ce qu’il lit.\n\nScope requis : progress:write. Droits et appartenance active vérifiés indépendamment des scopes. Tout paramètre query inconnu est refusé ; les endpoints sans query refusent toute query.","security":[{"bearerAuth":[]},{"mobileOAuth":["progress:write"]}],"parameters":[{"name":"spaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Identité UUID transmise ; droits résolus par le serveur."},{"name":"bookId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Identité UUID transmise ; droits résolus par le serveur."},{"name":"X-Library-Manager-Expected-User","in":"header","required":false,"schema":{"type":"string","format":"uuid"},"description":"Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."}],"requestBody":{"required":true,"description":"JSON strict UTF-8 <=65536 octets ; champs inconnus et doublons de paramètres refusés.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnnotationCreateInput"},"example":{"versionId":"33333333-3333-4333-8333-333333333333","chapterId":"chapter-1","position":"/body/DocFragment[1]/body/p[3]/text().42","quote":"Exemple de passage synthétique.","note":null}}}},"responses":{"201":{"description":"Créé.","headers":{"Cache-Control":{"description":"private, no-store sur toutes les réponses.","schema":{"type":"string"}},"Referrer-Policy":{"description":"no-referrer pour l’API.","schema":{"type":"string"}},"X-Request-Id":{"description":"UUID corrélable sans journaliser de jeton ou données personnelles.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Annotation"},"examples":{"synthetic":{"summary":"Exemple synthétique, identités/URLs/jetons inactifs","value":{"id":"77777777-7777-4777-8777-777777777777","bookId":"22222222-2222-4222-8222-222222222222","versionId":"33333333-3333-4333-8333-333333333333","chapterId":"chapter-1","position":"/body/DocFragment[1]/body/p[3]/text().42","quote":"Exemple de passage synthétique.","note":null,"revision":1,"createdAt":"2026-10-15T12:00:00.000Z","updatedAt":"2026-10-15T12:00:00.000Z"}}}}}},"401":{"$ref":"#/components/responses/ApiHTTP401"},"403":{"$ref":"#/components/responses/ApiHTTP403"},"404":{"$ref":"#/components/responses/ApiHTTP404"},"405":{"$ref":"#/components/responses/ApiHTTP405"},"409":{"$ref":"#/components/responses/ApiHTTP409"},"413":{"$ref":"#/components/responses/ApiHTTP413"},"422":{"$ref":"#/components/responses/ApiHTTP422"},"423":{"$ref":"#/components/responses/ApiHTTP423"},"429":{"$ref":"#/components/responses/ApiHTTP429"},"500":{"$ref":"#/components/responses/ApiHTTP500"},"503":{"$ref":"#/components/responses/ApiHTTP503"}}}
    },
    "/api/v1/mobile/spaces/{spaceId}/books/{bookId}/annotations/{annotationId}": {
      "patch": {"operationId":"mobileUpdateAnnotation","tags":["Signets et notes"],"summary":"Modifier la note d’un signet","description":"Remplace la note sous CAS : expectedRevision doit être la révision lue. Une note vide la retire (le repère redevient un signet). HTTP409 : relire la liste, ne pas réessayer en force.\n\nScope requis : progress:write. Droits et appartenance active vérifiés indépendamment des scopes. Tout paramètre query inconnu est refusé ; les endpoints sans query refusent toute query.","security":[{"bearerAuth":[]},{"mobileOAuth":["progress:write"]}],"parameters":[{"name":"spaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Identité UUID transmise ; droits résolus par le serveur."},{"name":"bookId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Identité UUID transmise ; droits résolus par le serveur."},{"name":"annotationId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Identité UUID transmise ; droits résolus par le serveur."},{"name":"X-Library-Manager-Expected-User","in":"header","required":false,"schema":{"type":"string","format":"uuid"},"description":"Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."}],"requestBody":{"required":true,"description":"JSON strict UTF-8 <=65536 octets ; champs inconnus refusés.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnnotationPatchInput"},"example":{"expectedRevision":1,"note":"Nouvelle note synthétique."}}}},"responses":{"200":{"description":"Succès.","headers":{"Cache-Control":{"description":"private, no-store sur toutes les réponses.","schema":{"type":"string"}},"Referrer-Policy":{"description":"no-referrer pour l’API.","schema":{"type":"string"}},"X-Request-Id":{"description":"UUID corrélable sans journaliser de jeton ou données personnelles.","schema":{"type":"string","format":"uuid"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Annotation"},"examples":{"synthetic":{"summary":"Exemple synthétique, identités/URLs/jetons inactifs","value":{"id":"77777777-7777-4777-8777-777777777777","bookId":"22222222-2222-4222-8222-222222222222","versionId":"33333333-3333-4333-8333-333333333333","chapterId":"chapter-1","position":"/body/DocFragment[1]/body/p[3]/text().42","quote":"Exemple de passage synthétique.","note":"Nouvelle note synthétique.","revision":2,"createdAt":"2026-10-15T12:00:00.000Z","updatedAt":"2026-10-15T12:00:00.000Z"}}}}}},"401":{"$ref":"#/components/responses/ApiHTTP401"},"403":{"$ref":"#/components/responses/ApiHTTP403"},"404":{"$ref":"#/components/responses/ApiHTTP404"},"405":{"$ref":"#/components/responses/ApiHTTP405"},"409":{"$ref":"#/components/responses/ApiHTTP409"},"413":{"$ref":"#/components/responses/ApiHTTP413"},"422":{"$ref":"#/components/responses/ApiHTTP422"},"423":{"$ref":"#/components/responses/ApiHTTP423"},"429":{"$ref":"#/components/responses/ApiHTTP429"},"500":{"$ref":"#/components/responses/ApiHTTP500"},"503":{"$ref":"#/components/responses/ApiHTTP503"}}},
      "delete": {"operationId":"mobileDeleteAnnotation","tags":["Signets et notes"],"summary":"Supprimer un signet ou une note","description":"Supprime le repère si expectedRevision est encore sa révision ; HTTP409 sinon, HTTP404 s’il n’existe plus.\n\nScope requis : progress:write. Droits et appartenance active vérifiés indépendamment des scopes. Tout paramètre query inconnu est refusé ; les endpoints sans query refusent toute query.","security":[{"bearerAuth":[]},{"mobileOAuth":["progress:write"]}],"parameters":[{"name":"spaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Identité UUID transmise ; droits résolus par le serveur."},{"name":"bookId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Identité UUID transmise ; droits résolus par le serveur."},{"name":"annotationId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Identité UUID transmise ; droits résolus par le serveur."},{"name":"X-Library-Manager-Expected-User","in":"header","required":false,"schema":{"type":"string","format":"uuid"},"description":"Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."},{"name":"expectedRevision","in":"query","required":true,"schema":{"type":"integer","minimum":1,"maximum":9007199254740991},"description":"Révision lue dans la liste."}],"responses":{"204":{"description":"Supprimé ; réponse sans corps.","headers":{"Cache-Control":{"description":"private, no-store sur toutes les réponses.","schema":{"type":"string"}},"Referrer-Policy":{"description":"no-referrer pour l’API.","schema":{"type":"string"}},"X-Request-Id":{"description":"UUID corrélable sans journaliser de jeton ou données personnelles.","schema":{"type":"string","format":"uuid"}}}},"401":{"$ref":"#/components/responses/ApiHTTP401"},"403":{"$ref":"#/components/responses/ApiHTTP403"},"404":{"$ref":"#/components/responses/ApiHTTP404"},"405":{"$ref":"#/components/responses/ApiHTTP405"},"409":{"$ref":"#/components/responses/ApiHTTP409"},"413":{"$ref":"#/components/responses/ApiHTTP413"},"422":{"$ref":"#/components/responses/ApiHTTP422"},"423":{"$ref":"#/components/responses/ApiHTTP423"},"429":{"$ref":"#/components/responses/ApiHTTP429"},"500":{"$ref":"#/components/responses/ApiHTTP500"},"503":{"$ref":"#/components/responses/ApiHTTP503"}}}
    },
    "/api/v1/mobile/spaces/{spaceId}/imports": {
      "post": {
        "operationId": "mobileBeginImport",
        "tags": [
          "Imports depuis le téléphone"
        ],
        "summary": "Préparer une copie local vers cloud",
        "description": "Réutilise les quotas, pipeline d’import vérifié et dédoublonnage tenant. Propriétaire/éditeur uniquement, le scope files:write n’élève pas un reader. Limite absolue512 Mio ; quota peut être plus strict. Générer l’idempotencyKey et le hash avant l’envoi, réutiliser la même clé après réseau ambigu. Puis utiliser upload.method/url/headers sans Bearer au stockage.\n\nScope requis : files:write. Droits et appartenance active vérifiés indépendamment des scopes. Tout paramètre query inconnu est refusé ; les endpoints sans query refusent toute query.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "mobileOAuth": [
              "files:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "spaceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "X-Library-Manager-Expected-User",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."
          }
        ],
        "responses": {
          "202": {
            "description": "Succès.",
            "headers": {
              "Cache-Control": {
                "description": "private, no-store sur toutes les réponses.",
                "schema": {
                  "type": "string"
                }
              },
              "Referrer-Policy": {
                "description": "no-referrer pour l’API.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BeginImportResponse"
                },
                "examples": {
                  "synthetic": {
                    "summary": "Exemple synthétique, identités/URLs/jetons inactifs",
                    "value": {
                      "session": {
                        "id": "55555555-5555-4555-8555-555555555555",
                        "revision": 1,
                        "status": "awaiting_upload",
                        "filename": "livre-exemple.epub",
                        "expectedBytes": 123456,
                        "format": "epub",
                        "expiresAt": "2026-10-14T13:00:00.000Z",
                        "bookId": null,
                        "versionId": null,
                        "jobId": null,
                        "duplicateOfBookId": null,
                        "quotaReserved": true,
                        "error": null
                      },
                      "upload": {
                        "importId": "55555555-5555-4555-8555-555555555555",
                        "url": "https://storage.example.test/synthetic-upload.epub?example=inactive",
                        "method": "PUT",
                        "headers": {
                          "content-type": "application/epub+zip"
                        },
                        "expiresAt": "2026-10-14T12:05:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiHTTP401"
          },
          "403": {
            "$ref": "#/components/responses/ApiHTTP403"
          },
          "404": {
            "$ref": "#/components/responses/ApiHTTP404"
          },
          "409": {
            "$ref": "#/components/responses/ApiHTTP409"
          },
          "413": {
            "$ref": "#/components/responses/ApiHTTP413"
          },
          "422": {
            "$ref": "#/components/responses/ApiHTTP422"
          },
          "423": {
            "$ref": "#/components/responses/ApiHTTP423"
          },
          "429": {
            "$ref": "#/components/responses/ApiHTTP429"
          },
          "500": {
            "$ref": "#/components/responses/ApiHTTP500"
          },
          "503": {
            "$ref": "#/components/responses/ApiHTTP503"
          },
          "405": {
            "$ref": "#/components/responses/ApiHTTP405"
          }
        },
        "requestBody": {
          "required": true,
          "description": "JSON strict UTF-8 <=65536 octets ; champs inconnus et doublons de paramètres refusés.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BeginImportInput"
              },
              "example": {
                "filename": "livre-exemple.epub",
                "format": "epub",
                "sizeBytes": 123456,
                "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
                "idempotencyKey": "88888888-8888-4888-8888-888888888888"
              }
            }
          }
        }
      }
    },
    "/api/v1/mobile/spaces/{spaceId}/imports/{importId}": {
      "get": {
        "operationId": "mobileGetImport",
        "tags": [
          "Imports depuis le téléphone"
        ],
        "summary": "Suivre l’import privé",
        "description": "Rôle propriétaire/éditeur et auteur de l’import. Interroger avec délai croissant borné jusqu’à ready/duplicate ou échec/expiration ; queued/processing et succès du PUT ne signifient pas prêt. Conserver les octets locaux et ne pas relancer une conversion payante automatiquement.\n\nScope requis : files:write. Droits et appartenance active vérifiés indépendamment des scopes. Tout paramètre query inconnu est refusé ; les endpoints sans query refusent toute query.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "mobileOAuth": [
              "files:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "spaceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "importId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "X-Library-Manager-Expected-User",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."
          }
        ],
        "responses": {
          "200": {
            "description": "Succès.",
            "headers": {
              "Cache-Control": {
                "description": "private, no-store sur toutes les réponses.",
                "schema": {
                  "type": "string"
                }
              },
              "Referrer-Policy": {
                "description": "no-referrer pour l’API.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportSession"
                },
                "examples": {
                  "synthetic": {
                    "summary": "Exemple synthétique, identités/URLs/jetons inactifs",
                    "value": {
                      "id": "55555555-5555-4555-8555-555555555555",
                      "revision": 1,
                      "status": "awaiting_upload",
                      "filename": "livre-exemple.epub",
                      "expectedBytes": 123456,
                      "format": "epub",
                      "expiresAt": "2026-10-14T13:00:00.000Z",
                      "bookId": null,
                      "versionId": null,
                      "jobId": null,
                      "duplicateOfBookId": null,
                      "quotaReserved": true,
                      "error": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiHTTP401"
          },
          "403": {
            "$ref": "#/components/responses/ApiHTTP403"
          },
          "404": {
            "$ref": "#/components/responses/ApiHTTP404"
          },
          "409": {
            "$ref": "#/components/responses/ApiHTTP409"
          },
          "413": {
            "$ref": "#/components/responses/ApiHTTP413"
          },
          "422": {
            "$ref": "#/components/responses/ApiHTTP422"
          },
          "423": {
            "$ref": "#/components/responses/ApiHTTP423"
          },
          "429": {
            "$ref": "#/components/responses/ApiHTTP429"
          },
          "500": {
            "$ref": "#/components/responses/ApiHTTP500"
          },
          "503": {
            "$ref": "#/components/responses/ApiHTTP503"
          },
          "405": {
            "$ref": "#/components/responses/ApiHTTP405"
          }
        }
      }
    },
    "/api/v1/mobile/spaces/{spaceId}/imports/{importId}/complete": {
      "post": {
        "operationId": "mobileCompleteImport",
        "tags": [
          "Imports depuis le téléphone"
        ],
        "summary": "Finaliser un envoi vérifiable",
        "description": "Après PUT du fichier, fournir la révision reçue et le SHA complet. Le serveur contrôle les octets stockés, puis met le travail en file. Finalisation idempotente pour le même SHA d’une session déjà acceptée ; conserver l’importId et interroger l’état. Si la version finale diffère des octets locaux, la copier et la vérifier avant de la lire comme cette version.\n\nScope requis : files:write. Droits et appartenance active vérifiés indépendamment des scopes. Tout paramètre query inconnu est refusé ; les endpoints sans query refusent toute query.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "mobileOAuth": [
              "files:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "spaceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "importId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Identité UUID transmise ; droits résolus par le serveur."
          },
          {
            "name": "X-Library-Manager-Expected-User",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Fence facultative de changement de compte : différent du compte Bearer =>401 ; UUID invalide =>422."
          }
        ],
        "responses": {
          "202": {
            "description": "Succès.",
            "headers": {
              "Cache-Control": {
                "description": "private, no-store sur toutes les réponses.",
                "schema": {
                  "type": "string"
                }
              },
              "Referrer-Policy": {
                "description": "no-referrer pour l’API.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportSession"
                },
                "examples": {
                  "synthetic": {
                    "summary": "Exemple synthétique, identités/URLs/jetons inactifs",
                    "value": {
                      "id": "55555555-5555-4555-8555-555555555555",
                      "revision": 2,
                      "status": "queued",
                      "filename": "livre-exemple.epub",
                      "expectedBytes": 123456,
                      "format": "epub",
                      "expiresAt": "2026-10-14T13:00:00.000Z",
                      "bookId": null,
                      "versionId": null,
                      "jobId": null,
                      "duplicateOfBookId": null,
                      "quotaReserved": true,
                      "error": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiHTTP401"
          },
          "403": {
            "$ref": "#/components/responses/ApiHTTP403"
          },
          "404": {
            "$ref": "#/components/responses/ApiHTTP404"
          },
          "409": {
            "$ref": "#/components/responses/ApiHTTP409"
          },
          "413": {
            "$ref": "#/components/responses/ApiHTTP413"
          },
          "422": {
            "$ref": "#/components/responses/ApiHTTP422"
          },
          "423": {
            "$ref": "#/components/responses/ApiHTTP423"
          },
          "429": {
            "$ref": "#/components/responses/ApiHTTP429"
          },
          "500": {
            "$ref": "#/components/responses/ApiHTTP500"
          },
          "503": {
            "$ref": "#/components/responses/ApiHTTP503"
          },
          "405": {
            "$ref": "#/components/responses/ApiHTTP405"
          }
        },
        "requestBody": {
          "required": true,
          "description": "JSON strict UTF-8 <=65536 octets ; champs inconnus et doublons de paramètres refusés.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompleteImportInput"
              },
              "example": {
                "expectedRevision": 1,
                "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
              }
            }
          }
        }
      }
    },
    "/api/mobile/oauth/authorize": {
      "get": {
        "tags": [
          "OAuth public PKCE"
        ],
        "operationId": "mobileOAuthAuthorize",
        "summary": "Ouvrir connexion et consentement web",
        "description": "Navigateur système : adresse/mot de passe du compte sur le site uniquement ; email vérifié et conditions en vigueur. GET valide et redirige303 vers /mobile/authorize, sans émettre de code. Après consentement POST web protégé, retour à redirect_uri avec code à usage unique120 s max, state identique. Client officiel et HTTPS retour exact : library-manager-mobile / https://library-manager.com/mobile/callback. PKCE S256 obligatoire ; password grant/plain/client_secret refusés. Les associations Universal Links/App Links doivent être établies par le binaire natif, hors contrat backend.",
        "security": [],
        "parameters": [
          {
            "name": "response_type",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "const": "code"
            },
            "description": ""
          },
          {
            "name": "client_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$",
              "example": "library-manager-mobile"
            },
            "description": ""
          },
          {
            "name": "redirect_uri",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uri",
              "example": "https://library-manager.com/mobile/callback",
              "description": "Doit correspondre exactement à une URL HTTPS du registre du client. Aucune redirection arbitraire, joker, fragment ou secret public-client n’est accepté."
            },
            "description": ""
          },
          {
            "name": "state",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 16,
              "maxLength": 512
            },
            "description": "Aléatoire cryptographique, conservé temporairement et comparé exactement au retour. Contrôles refusés."
          },
          {
            "name": "code_challenge",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{43}$"
            },
            "description": "base64url sans padding de SHA-256(code_verifier ASCII)."
          },
          {
            "name": "code_challenge_method",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "const": "S256"
            },
            "description": ""
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "library:read files:read files:write progress:read progress:write"
            },
            "description": "Scopes séparés par un espace unique ; doublons/unknown refusés. Omission=les cinq scopes ; chacun doit être permis au client."
          }
        ],
        "responses": {
          "303": {
            "description": "Consentement local ; pas encore de code OAuth.",
            "headers": {
              "Cache-Control": {
                "description": "private, no-store sur toutes les réponses.",
                "schema": {
                  "type": "string"
                }
              },
              "Referrer-Policy": {
                "description": "no-referrer pour l’API.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              },
              "Location": {
                "schema": {
                  "type": "string"
                },
                "description": "/mobile/authorize avec paramètres validés."
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/OAuthHTTP400"
          },
          "401": {
            "$ref": "#/components/responses/OAuthHTTP401"
          },
          "403": {
            "$ref": "#/components/responses/OAuthHTTP403"
          },
          "404": {
            "$ref": "#/components/responses/OAuthHTTP404"
          },
          "413": {
            "$ref": "#/components/responses/OAuthHTTP413"
          },
          "429": {
            "$ref": "#/components/responses/OAuthHTTP429"
          },
          "503": {
            "$ref": "#/components/responses/OAuthHTTP503"
          },
          "405": {
            "$ref": "#/components/responses/OAuthHTTP405"
          }
        }
      }
    },
    "/api/mobile/oauth/metadata": {
      "get": {
        "tags": [
          "OAuth public PKCE"
        ],
        "operationId": "mobileOAuthMetadata",
        "summary": "Lire les métadonnées OAuth",
        "description": "Public, GET uniquement, aucune query acceptée. L’issuer et les endpoints proviennent de l’origine configurée. Cette ressource expose OAuth Authorization Code + PKCE ; aucune assertion de serveur OpenID Connect ni id_token.",
        "security": [],
        "responses": {
          "200": {
            "description": "Métadonnées du fournisseur.",
            "headers": {
              "Cache-Control": {
                "description": "private, no-store sur toutes les réponses.",
                "schema": {
                  "type": "string"
                }
              },
              "Referrer-Policy": {
                "description": "no-referrer pour l’API.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthMetadata"
                },
                "examples": {
                  "synthetic": {
                    "summary": "Exemple synthétique, identités/URLs/jetons inactifs",
                    "value": {
                      "issuer": "https://library-manager.com",
                      "authorization_endpoint": "https://library-manager.com/api/mobile/oauth/authorize",
                      "token_endpoint": "https://library-manager.com/api/mobile/oauth/token",
                      "revocation_endpoint": "https://library-manager.com/api/mobile/oauth/revoke",
                      "response_types_supported": [
                        "code"
                      ],
                      "grant_types_supported": [
                        "authorization_code",
                        "refresh_token"
                      ],
                      "code_challenge_methods_supported": [
                        "S256"
                      ],
                      "token_endpoint_auth_methods_supported": [
                        "none"
                      ],
                      "revocation_endpoint_auth_methods_supported": [
                        "none"
                      ],
                      "scopes_supported": [
                        "library:read",
                        "files:read",
                        "files:write",
                        "progress:read",
                        "progress:write"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/OAuthHTTP400"
          },
          "401": {
            "$ref": "#/components/responses/OAuthHTTP401"
          },
          "403": {
            "$ref": "#/components/responses/OAuthHTTP403"
          },
          "404": {
            "$ref": "#/components/responses/OAuthHTTP404"
          },
          "413": {
            "$ref": "#/components/responses/OAuthHTTP413"
          },
          "429": {
            "$ref": "#/components/responses/OAuthHTTP429"
          },
          "503": {
            "$ref": "#/components/responses/OAuthHTTP503"
          },
          "405": {
            "$ref": "#/components/responses/OAuthHTTP405"
          }
        }
      }
    },
    "/api/mobile/oauth/token": {
      "post": {
        "tags": [
          "OAuth public PKCE"
        ],
        "operationId": "mobileOAuthToken",
        "summary": "Échanger le code ou renouveler avec rotation",
        "description": "Client public sans secret ni Authorization Basic. Un seul échange réussit par code ; PKCE/redirection/client doivent être identiques. Persister atomiquement le couple access/refresh après succès. Sérialiser les refresh : un refresh consommé réutilisé révoque durablement toute la famille, même si un autre appel venait de réussir. Durée accès<=600 s ; durée absolue<=7 jours plafonnée session, sans extension par refresh. Reset mot de passe/suppression de session invalide la famille. Une famille contient au plus4096 refresh hashes (initial+4095 rotations). Première rotation immédiate, suivantes espacées5 secondes; refus429 temporarily_unavailable conserve le dernier token valide. Toute tentative Authorization header est invalid_request400: client public sans secret. Codes consommés et familles terminales nettoyés à la prochaine autorisation;20codes en attente/20grants actifs par compte.",
        "security": [],
        "requestBody": {
          "required": true,
          "description": "Formulaire UTF-8 strict <=8192 octets.",
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/OAuthTokenInput"
              },
              "examples": {
                "authorizationCode": {
                  "summary": "Exemple synthétique inactif : échange initial",
                  "value": {
                    "grant_type": "authorization_code",
                    "client_id": "library-manager-mobile",
                    "redirect_uri": "https://library-manager.com/mobile/callback",
                    "code": "lmc_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
                    "code_verifier": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
                  }
                },
                "refreshToken": {
                  "summary": "Exemple synthétique inactif : rotation sérialisée",
                  "value": {
                    "grant_type": "refresh_token",
                    "client_id": "library-manager-mobile",
                    "refresh_token": "lmr_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Nouveau couple de jetons privés.",
            "headers": {
              "Cache-Control": {
                "description": "private, no-store sur toutes les réponses.",
                "schema": {
                  "type": "string"
                }
              },
              "Referrer-Policy": {
                "description": "no-referrer pour l’API.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                },
                "examples": {
                  "synthetic": {
                    "summary": "Exemple synthétique, identités/URLs/jetons inactifs",
                    "value": {
                      "access_token": "lma_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
                      "token_type": "Bearer",
                      "expires_in": 600,
                      "refresh_token": "lmr_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
                      "scope": "library:read files:read files:write progress:read progress:write"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/OAuthHTTP400"
          },
          "401": {
            "$ref": "#/components/responses/OAuthHTTP401"
          },
          "403": {
            "$ref": "#/components/responses/OAuthHTTP403"
          },
          "404": {
            "$ref": "#/components/responses/OAuthHTTP404"
          },
          "413": {
            "$ref": "#/components/responses/OAuthHTTP413"
          },
          "429": {
            "$ref": "#/components/responses/OAuthHTTP429"
          },
          "503": {
            "$ref": "#/components/responses/OAuthHTTP503"
          },
          "405": {
            "$ref": "#/components/responses/OAuthHTTP405"
          }
        }
      }
    },
    "/api/mobile/oauth/revoke": {
      "post": {
        "tags": [
          "OAuth public PKCE"
        ],
        "operationId": "mobileOAuthRevoke",
        "summary": "Révoquer une famille de jetons",
        "description": "Formulaire strict client_id/token et token_type_hint facultatif. Un token inconnu ou appartenant à un autre client donne aussi200 vide ; aucune information d’existence divulguée. Révoquer puis vider Keychain/Keystore et les opérations du compte selon choix utilisateur ; ce endpoint ne supprime pas les fichiers cloud ou locaux.",
        "security": [],
        "requestBody": {
          "required": true,
          "description": "Formulaire UTF-8 strict <=8192 octets.",
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/RevokeInput"
              },
              "example": {
                "client_id": "library-manager-mobile",
                "token": "lmr_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
                "token_type_hint": "refresh_token"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Révocation terminée ou token inconnu ; corps vide.",
            "headers": {
              "Cache-Control": {
                "description": "private, no-store sur toutes les réponses.",
                "schema": {
                  "type": "string"
                }
              },
              "Referrer-Policy": {
                "description": "no-referrer pour l’API.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/OAuthHTTP400"
          },
          "401": {
            "$ref": "#/components/responses/OAuthHTTP401"
          },
          "403": {
            "$ref": "#/components/responses/OAuthHTTP403"
          },
          "404": {
            "$ref": "#/components/responses/OAuthHTTP404"
          },
          "413": {
            "$ref": "#/components/responses/OAuthHTTP413"
          },
          "429": {
            "$ref": "#/components/responses/OAuthHTTP429"
          },
          "503": {
            "$ref": "#/components/responses/OAuthHTTP503"
          },
          "405": {
            "$ref": "#/components/responses/OAuthHTTP405"
          }
        }
      }
    },
    "/.well-known/oauth-authorization-server": {
      "get": {
        "tags": [
          "OAuth public PKCE"
        ],
        "operationId": "mobileOAuthWellKnownMetadata",
        "summary": "Découvrir le serveur OAuth via le chemin standard",
        "description": "Alias public GET de la même projection OAuthMetadata que /api/mobile/oauth/metadata. Ce wrapper simple ignore les paramètres query ; la route canonique metadata les refuse. OAuth Authorization Code avec PKCE S256, pas de serveur OpenID Connect ni id_token. Le serveur ne renvoie aucune configuration privée ou secret. HEAD suit le GET automatique du framework, sans corps.",
        "security": [],
        "responses": {
          "200": {
            "description": "Métadonnées du fournisseur.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                },
                "description": "Réponse non stockable."
              },
              "Referrer-Policy": {
                "description": "no-referrer pour l’API.",
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthMetadata"
                },
                "examples": {
                  "synthetic": {
                    "summary": "Exemple synthétique, identités/URLs/jetons inactifs",
                    "value": {
                      "issuer": "https://library-manager.com",
                      "authorization_endpoint": "https://library-manager.com/api/mobile/oauth/authorize",
                      "token_endpoint": "https://library-manager.com/api/mobile/oauth/token",
                      "revocation_endpoint": "https://library-manager.com/api/mobile/oauth/revoke",
                      "response_types_supported": [
                        "code"
                      ],
                      "grant_types_supported": [
                        "authorization_code",
                        "refresh_token"
                      ],
                      "code_challenge_methods_supported": [
                        "S256"
                      ],
                      "token_endpoint_auth_methods_supported": [
                        "none"
                      ],
                      "revocation_endpoint_auth_methods_supported": [
                        "none"
                      ],
                      "scopes_supported": [
                        "library:read",
                        "files:read",
                        "files:write",
                        "progress:read",
                        "progress:write"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/mobile/callback": {
      "get": {
        "operationId": "mobileOAuthBrowserCallbackFallback",
        "tags": [
          "OAuth public PKCE"
        ],
        "summary": "Retombée navigateur du retour natif",
        "description": "L’application native doit consommer ce lien via Universal Links/App Links et vérifier state avant l’échange. Si le navigateur atteint ce serveur, celui-ci redirige toujours303 vers /docs/api en ignorant tous les paramètres, sans afficher, réfléchir ni conserver code/state. Ce fallback ne termine pas l’authentification et ne prouve pas l’association du lien au binaire natif. Les paramètres inconnus sont également ignorés. HEAD suit le GET automatique sans corps.",
        "security": [],
        "parameters": [
          {
            "name": "code",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Code OAuth reçu par le lien natif ; jamais réfléchi par le fallback serveur."
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "État à vérifier par le client natif ; jamais réfléchi par le fallback serveur."
          },
          {
            "name": "error",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Éventuel refus OAuth à traiter par le client natif ; ignoré par le fallback."
          }
        ],
        "responses": {
          "303": {
            "description": "Redirection fixe, aucun contenu code/state renvoyé.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "const": "/docs/api"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "private, no-store"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              },
              "X-Robots-Tag": {
                "schema": {
                  "type": "string",
                  "const": "noindex, nofollow"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "Jeton opaque lma_ (43 caractères base64url après le préfixe)",
        "description": "Uniquement /api/v1/mobile. Vérifié côté serveur ; durée<=10 min plafonnée grant/session. Aucun JWT à décoder, aucun cookie de remplacement."
      },
      "mobileOAuth": {
        "type": "oauth2",
        "description": "Client public enregistré, Authorization Code et PKCE S256 obligatoires, pas de secret applicatif ni password grant. Rotation refresh et détection de réutilisation.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://library-manager.com/api/mobile/oauth/authorize",
            "tokenUrl": "https://library-manager.com/api/mobile/oauth/token",
            "refreshUrl": "https://library-manager.com/api/mobile/oauth/token",
            "scopes": {
              "library:read": "Lire les espaces actifs, le catalogue, sa recherche et les métadonnées.",
              "files:read": "Télécharger les versions prêtes et gérer les confirmations personnelles de copies locales.",
              "files:write": "Importer un fichier local dans un espace dont le compte est propriétaire ou éditeur.",
              "progress:read": "Lire la progression personnelle de la version choisie.",
              "progress:write": "Synchroniser une progression personnelle après confirmation de la copie locale."
            }
          }
        }
      }
    },
    "schemas": {
      "BeginImportInput": {
        "type": "object",
        "properties": {
          "filename": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "description": "Nom de 1 à 255 caractères après trim ; /, \\, contrôles, . et .. refusés."
          },
          "sizeBytes": {
            "type": "integer",
            "minimum": 1,
            "maximum": 536870912
          },
          "format": {
            "type": "string",
            "enum": [
              "epub",
              "mobi",
              "azw3",
              "txt",
              "html",
              "fb2"
            ]
          },
          "sha256": {
            "type": "string",
            "pattern": "^[a-f0-9]{64}$"
          },
          "idempotencyKey": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
            "description": "UUID durable créé avant l’appel et réutilisé pour la même intention d’import après une issue réseau ambiguë."
          },
          "deviceId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "filename",
          "sizeBytes",
          "format",
          "idempotencyKey"
        ],
        "additionalProperties": false
      },
      "CompleteImportInput": {
        "type": "object",
        "properties": {
          "expectedRevision": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "sha256": {
            "type": "string",
            "pattern": "^[a-f0-9]{64}$"
          }
        },
        "required": [
          "expectedRevision",
          "sha256"
        ],
        "additionalProperties": false
      },
      "MobileLocalCopyInput": {
        "type": "object",
        "properties": {
          "installationId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "versionId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "sha256": {
            "type": "string",
            "pattern": "^[a-f0-9]{64}$"
          },
          "sizeBytes": {
            "type": "integer",
            "minimum": 1,
            "maximum": 536870912
          }
        },
        "required": [
          "installationId",
          "versionId",
          "sha256",
          "sizeBytes"
        ],
        "additionalProperties": false,
        "description": "Envoyer après copie complète, contrôle SHA-256/taille et publication atomique du fichier dans la bibliothèque locale. Le serveur compare cette déclaration à sa version ready exacte ; la même confirmation est idempotente."
      },
      "MobileProgressInput": {
        "type": "object",
        "properties": {
          "expectedRevision": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "versionId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "installationId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "operationId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "progress": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 2048,
                "pattern": "^/body/DocFragment\\[[1-9][0-9]{0,3}\\]/body(?:/[a-z][a-z0-9]*(?:\\[[1-9][0-9]{0,5}\\])?)*/text\\(\\)(?:\\[[1-9][0-9]{0,5}\\])?\\.[0-9]{1,8}$"
              },
              {
                "type": "null"
              }
            ],
            "description": "Position texte KOReader/CREngine uniquement : XPointer absolu, offset Unicode UTF-32, 2048 caractères maximum ; null si aucun repère précis. Les EPUB CFI, JSON de défilement et champs historiques cfi/chapterId/percent ne sont pas acceptés par les écritures publiques."
          },
          "percentage": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Fraction KOReader de lecture, entre 0 et 1."
          }
        },
        "required": [
          "expectedRevision",
          "versionId",
          "progress",
          "percentage",
          "installationId",
          "operationId"
        ],
        "additionalProperties": false,
        "description": "Tous les champs sont obligatoires. Créer et persister operationId une seule fois avec le corps complet. Même opération/même corps : reçu original ; même opération/autre corps : HTTP409. Une nouvelle position exige un nouvel UUID. expectedRevision=0 seulement sans position antérieure. progress contient exclusivement un XPointer KOReader valide ou null ; percentage est une fraction0–1. Les anciens champs et coordonnées sont refusés."
      },
      "MobileCopyQuery": {
        "type": "object",
        "properties": {
          "installationId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          },
          "cursor": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid",
                "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
              },
              {
                "type": "null"
              }
            ]
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "default": 50
          }
        },
        "required": [
          "installationId"
        ],
        "additionalProperties": false
      },
      "MobileProgressQuery": {
        "type": "object",
        "properties": {
          "versionId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "versionId"
        ],
        "additionalProperties": false
      },
      "MobileLocalCopyDeleteQuery": {
        "type": "object",
        "properties": {
          "installationId": {
            "type": "string",
            "format": "uuid",
            "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
          }
        },
        "required": [
          "installationId"
        ],
        "additionalProperties": false
      },
      "Scope": {
        "type": "string",
        "enum": [
          "library:read",
          "files:read",
          "files:write",
          "progress:read",
          "progress:write"
        ]
      },
      "Me": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "user": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string",
                "format": "email"
              },
              "emailVerified": {
                "type": "boolean",
                "const": true
              }
            },
            "required": [
              "id",
              "name",
              "email",
              "emailVerified"
            ]
          },
          "grantId": {
            "type": "string",
            "format": "uuid"
          },
          "clientId": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Scope"
            }
          },
          "readingMode": {
            "type": "string",
            "const": "local_copy"
          }
        },
        "required": [
          "user",
          "grantId",
          "clientId",
          "scopes",
          "readingMode"
        ]
      },
      "Space": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "editor",
              "reader"
            ]
          },
          "locale": {
            "type": "string"
          },
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "pro",
              "unlimited"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "name",
          "slug",
          "role",
          "locale",
          "plan",
          "createdAt"
        ]
      },
      "BookSummary": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "tenantId": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "authors": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "series": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "seriesIndex": {
            "anyOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "null"
              }
            ]
          },
          "seriesPositionLabel": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "language": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "genres": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "completeness": {
            "type": "string",
            "enum": [
              "complete",
              "volume",
              "part",
              "omnibus",
              "unknown"
            ]
          },
          "formats": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "epub",
                "mobi",
                "azw3",
                "txt",
                "html",
                "fb2"
              ]
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "importing",
              "ready",
              "processing",
              "needs_review",
              "failed",
              "merged"
            ]
          },
          "metadataStatus": {
            "type": "string",
            "enum": [
              "incomplete",
              "queued",
              "processing",
              "complete",
              "needs_review",
              "waiting_configuration",
              "failed"
            ]
          },
          "revision": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          },
          "favorite": {
            "type": "boolean"
          },
          "rating": {
            "anyOf": [
              {
                "type": "number",
                "minimum": 1,
                "maximum": 5
              },
              {
                "type": "null"
              }
            ]
          },
          "readingPercent": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "Progression personnelle ; null sans le scope progress:read."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "tenantId",
          "title",
          "authors",
          "series",
          "seriesIndex",
          "seriesPositionLabel",
          "language",
          "genres",
          "tags",
          "completeness",
          "formats",
          "status",
          "metadataStatus",
          "revision",
          "favorite",
          "rating",
          "readingPercent",
          "createdAt",
          "updatedAt"
        ]
      },
      "Version": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "bookId": {
            "type": "string",
            "format": "uuid"
          },
          "parentVersionId": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ]
          },
          "kind": {
            "type": "string",
            "enum": [
              "original",
              "normalized",
              "optimized",
              "converted"
            ]
          },
          "profile": {
            "type": "string",
            "enum": [
              "original",
              "normalized",
              "lossless",
              "balanced",
              "xteink",
              "text_only",
              "custom"
            ]
          },
          "format": {
            "type": "string",
            "enum": [
              "epub",
              "mobi",
              "azw3",
              "txt",
              "html",
              "fb2"
            ]
          },
          "filename": {
            "type": "string"
          },
          "mediaType": {
            "type": "string"
          },
          "sizeBytes": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "sha256": {
            "type": "string",
            "pattern": "^[a-f0-9]{64}$",
            "minLength": 64,
            "maxLength": 64
          },
          "status": {
            "type": "string",
            "const": "ready"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "bookId",
          "parentVersionId",
          "kind",
          "profile",
          "format",
          "filename",
          "mediaType",
          "sizeBytes",
          "sha256",
          "status",
          "createdAt"
        ]
      },
      "BookMetadata": {
        "type": "object",
        "properties": {
          "title": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 512
              },
              {
                "type": "null"
              }
            ]
          },
          "subtitle": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 512
              },
              {
                "type": "null"
              }
            ]
          },
          "authors": {
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 256
            }
          },
          "authorSort": {
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 256
            }
          },
          "series": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 512
              },
              {
                "type": "null"
              }
            ]
          },
          "seriesIndex": {
            "anyOf": [
              {
                "type": "number",
                "minimum": 0,
                "maximum": 1000000
              },
              {
                "type": "null"
              }
            ]
          },
          "seriesPositionLabel": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 128
              },
              {
                "type": "null"
              }
            ]
          },
          "language": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 2,
                "maxLength": 35,
                "pattern": "^[a-zA-Z]{2,8}(?:-[a-zA-Z0-9]{1,8})*$"
              },
              {
                "type": "null"
              }
            ]
          },
          "genres": {
            "maxItems": 100,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 256
            }
          },
          "tags": {
            "maxItems": 100,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 256
            }
          },
          "description": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 50000
              },
              {
                "type": "null"
              }
            ]
          },
          "publisher": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 512
              },
              {
                "type": "null"
              }
            ]
          },
          "publicationDate": {
            "anyOf": [
              {
                "type": "string",
                "format": "date",
                "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"
              },
              {
                "type": "null"
              }
            ]
          },
          "translators": {
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 256
            }
          },
          "identifiers": {
            "type": "object",
            "properties": {
              "isbn10": {
                "anyOf": [
                  {
                    "type": "string",
                    "pattern": "^\\d{9}[\\dXx]$"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "isbn13": {
                "anyOf": [
                  {
                    "type": "string",
                    "pattern": "^\\d{13}$"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "asin": {
                "anyOf": [
                  {
                    "type": "string",
                    "pattern": "^[A-Z0-9]{10}$"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "other": {
                "type": "object",
                "propertyNames": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 64
                },
                "additionalProperties": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 256
                },
                "maxProperties": 16
              }
            },
            "required": [
              "isbn10",
              "isbn13",
              "asin",
              "other"
            ],
            "additionalProperties": false
          },
          "completeness": {
            "type": "string",
            "enum": [
              "complete",
              "volume",
              "part",
              "omnibus",
              "unknown"
            ]
          },
          "partNumber": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 1,
                "maximum": 100000
              },
              {
                "type": "null"
              }
            ]
          },
          "partCount": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 1,
                "maximum": 100000
              },
              {
                "type": "null"
              }
            ]
          },
          "edition": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 512
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "title",
          "subtitle",
          "authors",
          "authorSort",
          "series",
          "seriesIndex",
          "seriesPositionLabel",
          "language",
          "genres",
          "tags",
          "description",
          "publisher",
          "publicationDate",
          "translators",
          "identifiers",
          "completeness",
          "partNumber",
          "partCount",
          "edition"
        ],
        "additionalProperties": false,
        "description": "Métadonnées bibliographiques du livre ; null/unknown est conservé en absence de preuve. Une métadonnée inconnue ne devient pas un titre inventé. Les identités de version restent distinctes des métadonnées."
      },
      "Annotation": {
        "type": "object",
        "additionalProperties": false,
        "description": "Signet (note null) ou note de passage, privé au compte. bookId est le livre logique ; versionId la version où il a été placé.",
        "properties": {"id":{"type":"string","format":"uuid"},"bookId":{"type":"string","format":"uuid"},"versionId":{"type":"string","format":"uuid"},"chapterId":{"anyOf":[{"type":"string","pattern":"^chapter-[1-9][0-9]{0,3}$"},{"type":"null"}],"description":"Chapitre du lecteur ; null seulement avec un XPointer, qui porte son chapitre."},"position":{"anyOf":[{"type":"string","minLength":1,"maxLength":2048,"pattern":"^(?:ratio:(?:0(?:\\.[0-9]{1,6})?|1(?:\\.0{1,6})?)|/body/DocFragment\\[[1-9][0-9]{0,3}\\]/body(?:/[a-z][a-z0-9]*(?:\\[[1-9][0-9]{0,5}\\])?)*/text\\(\\)(?:\\[[1-9][0-9]{0,5}\\])?\\.[0-9]{1,8})$"},{"type":"null"}],"description":"XPointer texte KOReader (exact), ou ratio:<0..1> (six décimales au plus) : fraction de la hauteur du chapitre nommé par chapterId. Aucun EPUB CFI."},"quote":{"type":"string","maxLength":600,"description":"Passage, ou court extrait de la page pour un signet ; chaîne vide si aucun."},"note":{"anyOf":[{"type":"string","minLength":1,"maxLength":4000},{"type":"null"}]},"revision":{"type":"integer","minimum":1,"maximum":9007199254740991},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},
        "required": ["id","bookId","versionId","chapterId","position","quote","note","revision","createdAt","updatedAt"]
      },
      "AnnotationList": {
        "type": "object",
        "additionalProperties": false,
        "properties": {"items":{"type":"array","maxItems":1000,"items":{"$ref":"#/components/schemas/Annotation"}}},
        "required": ["items"]
      },
      "AnnotationCreateInput": {
        "type": "object",
        "additionalProperties": false,
        "description": "chapterId ou un XPointer est requis ; un ratio exige chapterId.",
        "properties": {"versionId":{"type":"string","format":"uuid"},"chapterId":{"anyOf":[{"type":"string","pattern":"^chapter-[1-9][0-9]{0,3}$"},{"type":"null"}],"description":"Chapitre du lecteur ; null seulement avec un XPointer, qui porte son chapitre."},"position":{"anyOf":[{"type":"string","minLength":1,"maxLength":2048,"pattern":"^(?:ratio:(?:0(?:\\.[0-9]{1,6})?|1(?:\\.0{1,6})?)|/body/DocFragment\\[[1-9][0-9]{0,3}\\]/body(?:/[a-z][a-z0-9]*(?:\\[[1-9][0-9]{0,5}\\])?)*/text\\(\\)(?:\\[[1-9][0-9]{0,5}\\])?\\.[0-9]{1,8})$"},{"type":"null"}],"description":"XPointer texte KOReader (exact), ou ratio:<0..1> (six décimales au plus) : fraction de la hauteur du chapitre nommé par chapterId. Aucun EPUB CFI."},"quote":{"type":"string","maxLength":1200,"description":"Espaces fusionnés puis 600 caractères au plus."},"note":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"description":"Vide ou null : signet sans note."}},
        "required": ["versionId","chapterId","position"]
      },
      "AnnotationPatchInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {"expectedRevision":{"type":"integer","minimum":1,"maximum":9007199254740991},"note":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"description":"Vide ou null : retire la note."}},
        "required": ["expectedRevision","note"]
      },
      "ReaderProgress": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "bookId": {
            "type": "string",
            "format": "uuid"
          },
          "versionId": {
            "type": "string",
            "format": "uuid"
          },
          "revision": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "progress": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 2048,
                "pattern": "^/body/DocFragment\\[[1-9][0-9]{0,3}\\]/body(?:/[a-z][a-z0-9]*(?:\\[[1-9][0-9]{0,5}\\])?)*/text\\(\\)(?:\\[[1-9][0-9]{0,5}\\])?\\.[0-9]{1,8}$"
              },
              {
                "type": "null"
              }
            ],
            "description": "Position texte KOReader/CREngine uniquement : XPointer absolu, offset Unicode UTF-32, 2048 caractères maximum ; null si aucun repère précis. Les EPUB CFI, JSON de défilement et champs historiques cfi/chapterId/percent ne sont pas acceptés par les écritures publiques."
          },
          "percentage": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Fraction KOReader de lecture, entre 0 et 1."
          }
        },
        "required": [
          "bookId",
          "versionId",
          "progress",
          "percentage",
          "revision",
          "updatedAt"
        ],
        "description": "Position personnelle de cette version. Le bookId renvoyé est celui du livre logique ; une fusion peut faire différer ce bookId de celui de la version physique. Null signifie aucune position disponible pour la version. Ne pas fabriquer un CFI depuis le pourcentage."
      },
      "BookUserState": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "bookId": {
            "type": "string",
            "format": "uuid"
          },
          "favorite": {
            "type": "boolean"
          },
          "rating": {
            "anyOf": [
              {
                "type": "number",
                "minimum": 1,
                "maximum": 5
              },
              {
                "type": "null"
              }
            ]
          },
          "note": {
            "description": "Mémo privé du lecteur, visible de lui seul ; null quand il est vide. Au plus 4000 caractères après suppression des espaces aux extrémités.",
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 4000
              },
              {
                "type": "null"
              }
            ]
          },
          "revision": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "updatedAt": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "bookId",
          "favorite",
          "rating",
          "note",
          "revision",
          "updatedAt"
        ]
      },
      "Book": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "tenantId": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "authors": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "series": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "seriesIndex": {
            "anyOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "null"
              }
            ]
          },
          "seriesPositionLabel": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "language": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "genres": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "completeness": {
            "type": "string",
            "enum": [
              "complete",
              "volume",
              "part",
              "omnibus",
              "unknown"
            ]
          },
          "formats": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "epub",
                "mobi",
                "azw3",
                "txt",
                "html",
                "fb2"
              ]
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "importing",
              "ready",
              "processing",
              "needs_review",
              "failed",
              "merged"
            ]
          },
          "metadataStatus": {
            "type": "string",
            "enum": [
              "incomplete",
              "queued",
              "processing",
              "complete",
              "needs_review",
              "waiting_configuration",
              "failed"
            ]
          },
          "revision": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          },
          "favorite": {
            "type": "boolean"
          },
          "rating": {
            "anyOf": [
              {
                "type": "number",
                "minimum": 1,
                "maximum": 5
              },
              {
                "type": "null"
              }
            ]
          },
          "readingPercent": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "Progression personnelle ; null sans le scope progress:read."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "metadata": {
            "$ref": "#/components/schemas/BookMetadata"
          },
          "versions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Version"
            }
          },
          "progress": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ReaderProgress"
              },
              {
                "type": "null"
              }
            ],
            "description": "Progression personnelle ; null sans le scope progress:read ou sans position enregistrée."
          },
          "userState": {
            "$ref": "#/components/schemas/BookUserState"
          },
          "readingMode": {
            "type": "string",
            "const": "local_copy"
          }
        },
        "required": [
          "id",
          "tenantId",
          "title",
          "authors",
          "series",
          "seriesIndex",
          "seriesPositionLabel",
          "language",
          "genres",
          "tags",
          "completeness",
          "formats",
          "status",
          "metadataStatus",
          "revision",
          "favorite",
          "rating",
          "readingPercent",
          "createdAt",
          "updatedAt",
          "metadata",
          "versions",
          "progress",
          "userState",
          "readingMode"
        ],
        "description": "Projection mobile explicite : seules les versions ready sont incluses ; les clés de stockage, paramètres de conversion, producteurs, provenances et propositions de revue ne sont pas exposés."
      },
      "BookPage": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BookSummary"
            }
          },
          "nextCursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "total": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "items",
          "nextCursor",
          "total"
        ]
      },
      "LocalCopy": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "installationId": {
            "type": "string",
            "format": "uuid"
          },
          "bookId": {
            "type": "string",
            "format": "uuid"
          },
          "versionId": {
            "type": "string",
            "format": "uuid"
          },
          "sha256": {
            "type": "string",
            "pattern": "^[a-f0-9]{64}$",
            "minLength": 64,
            "maxLength": 64
          },
          "sizeBytes": {
            "type": "integer",
            "minimum": 1,
            "maximum": 536870912
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "installationId",
          "bookId",
          "versionId",
          "sha256",
          "sizeBytes",
          "updatedAt"
        ],
        "description": "Reçu de déclaration du client, personnel au compte, à l’installation et à l’espace. Il ne prouve pas la présence physique des octets sur le téléphone. bookId peut nommer une famille physique conservée après fusion."
      },
      "LocalCopyPage": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LocalCopy"
            }
          },
          "nextCursor": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ]
          },
          "total": {
            "type": "null",
            "description": "Le comptage exact n’est pas calculé."
          }
        },
        "required": [
          "items",
          "nextCursor",
          "total"
        ]
      },
      "Download": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "bookId": {
            "type": "string",
            "format": "uuid"
          },
          "versionId": {
            "type": "string",
            "format": "uuid"
          },
          "format": {
            "type": "string",
            "enum": [
              "epub",
              "mobi",
              "azw3",
              "txt",
              "html",
              "fb2"
            ]
          },
          "filename": {
            "type": "string"
          },
          "mediaType": {
            "type": "string"
          },
          "sizeBytes": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "sha256": {
            "type": "string",
            "pattern": "^[a-f0-9]{64}$",
            "minLength": 64,
            "maxLength": 64
          },
          "download": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "url": {
                "type": "string",
                "format": "uri"
              },
              "expiresAt": {
                "type": "string",
                "format": "date-time"
              }
            },
            "required": [
              "url",
              "expiresAt"
            ]
          },
          "readingMode": {
            "type": "string",
            "const": "local_copy"
          }
        },
        "required": [
          "bookId",
          "versionId",
          "format",
          "filename",
          "mediaType",
          "sizeBytes",
          "sha256",
          "download",
          "readingMode"
        ],
        "description": "Capacité éphémère de téléchargement liée au reçu vérifié et au VersionId du stockage. Durée de signature demandée : 300 s. Aucun Bearer Library Manager ne doit être transmis au domaine de stockage ; l’URL reste privée et ne doit pas être conservée dans le journal hors ligne."
      },
      "ErrorDetails": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "expectedRevision": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "currentRevision": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "expectedInventoryRevision": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "currentInventoryRevision": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "logicalBooks": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "reservedBooks": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "requestedBytes": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "maxBytes": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "retryAfterSeconds": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "bookId": {
            "type": "string",
            "format": "uuid"
          },
          "versionId": {
            "type": "string",
            "format": "uuid"
          },
          "proposalId": {
            "type": "string",
            "format": "uuid"
          },
          "importId": {
            "type": "string",
            "format": "uuid"
          },
          "deviceId": {
            "type": "string",
            "format": "uuid"
          },
          "actionId": {
            "type": "string",
            "format": "uuid"
          },
          "bookLimit": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              {
                "type": "null"
              }
            ]
          },
          "remainingBooks": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              {
                "type": "null"
              }
            ]
          },
          "providerId": {
            "type": "string",
            "enum": [
              "zai",
              "kimi",
              "minimax",
              "openai",
              "anthropic",
              "mistral"
            ]
          },
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "pro",
              "unlimited"
            ]
          },
          "requiredPlan": {
            "type": "string",
            "enum": [
              "free",
              "pro",
              "unlimited"
            ]
          },
          "format": {
            "type": "string",
            "enum": [
              "epub",
              "mobi",
              "azw3",
              "txt",
              "html",
              "fb2"
            ]
          },
          "conflictCode": {
            "type": "string",
            "enum": [
              "destination_exists",
              "source_changed",
              "insufficient_space",
              "unsupported_format"
            ]
          },
          "fields": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "title",
                "subtitle",
                "authors",
                "authorSort",
                "series",
                "seriesIndex",
                "seriesPositionLabel",
                "language",
                "genres",
                "tags",
                "description",
                "publisher",
                "publicationDate",
                "translators",
                "identifiers",
                "completeness",
                "partNumber",
                "partCount",
                "edition"
              ]
            },
            "maxItems": 19
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "billingOperationUnresolved",
                "subscriptionActive",
                "lastOwnerRequiresTenantScope",
                "storageReviewRequired",
                "tenantHasSeveralOwners",
                "integrityReviewRequired"
              ]
            },
            "minItems": 1,
            "maxItems": 6
          },
          "deletionState": {
            "type": "string",
            "enum": [
              "scheduled",
              "cancelled",
              "executing",
              "blocked",
              "completed"
            ]
          }
        },
        "required": [],
        "description": "Détails facultatifs fermés : uniquement les clés sûres du service. Le code d’erreur reste la source de décision, et aucun message SQL, secret ou URL signée n’est transmis."
      },
      "ApiError": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "unauthenticated",
              "forbidden",
              "notFound",
              "validationFailed",
              "revisionConflict",
              "archivedBook",
              "restoreConflict",
              "quotaExceeded",
              "configurationRequired",
              "providerRateLimited",
              "providerUnavailable",
              "integrityMismatch",
              "unsafeArchive",
              "deviceConflict",
              "unsupportedFormat",
              "contentWithoutText",
              "requestTooLarge",
              "rateLimited",
              "internalError",
              "recipientNotFound",
              "recipientAlreadyMember",
              "invitationUnavailable",
              "sharingLimitReached",
              "termsAcceptanceRequired",
              "exportRateLimited",
              "exportExpired",
              "paymentMethodRequired",
              "passwordRejected",
              "accountDeletionScheduled",
              "accountDeletionBlocked",
              "emailMismatch",
              "deletionRateLimited",
              "enrichmentAlreadyProcessed",
              "enrichmentInProgress",
              "enrichmentBusy"
            ]
          },
          "messageKey": {
            "type": "string",
            "pattern": "^errors\\.[A-Za-z][A-Za-z0-9]*$"
          },
          "requestId": {
            "type": "string",
            "format": "uuid"
          },
          "details": {
            "$ref": "#/components/schemas/ErrorDetails"
          }
        },
        "required": [
          "code",
          "messageKey",
          "requestId"
        ]
      },
      "ApiErrorResponse": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ApiError"
          }
        },
        "required": [
          "error"
        ]
      },
      "ImportSession": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "revision": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          },
          "status": {
            "type": "string",
            "enum": [
              "awaiting_upload",
              "uploaded",
              "queued",
              "processing",
              "ready",
              "duplicate",
              "failed",
              "expired"
            ]
          },
          "filename": {
            "type": "string"
          },
          "expectedBytes": {
            "type": "integer",
            "minimum": 1,
            "maximum": 536870912
          },
          "format": {
            "type": "string",
            "enum": [
              "epub",
              "mobi",
              "azw3",
              "txt",
              "html",
              "fb2"
            ]
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "bookId": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ]
          },
          "versionId": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ]
          },
          "jobId": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ]
          },
          "duplicateOfBookId": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ]
          },
          "quotaReserved": {
            "type": "boolean"
          },
          "error": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ApiError"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "id",
          "revision",
          "status",
          "filename",
          "expectedBytes",
          "format",
          "expiresAt",
          "bookId",
          "versionId",
          "jobId",
          "duplicateOfBookId",
          "quotaReserved",
          "error"
        ],
        "description": "Import privé de ce compte dans cet espace. Le PUT du stockage ne signifie pas que l’import est prêt : attendre ready ou duplicate et les identités finales. Conserver le fichier local en cas d’échec et ne pas lancer une opération payante automatiquement."
      },
      "UploadTarget": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "importId": {
            "type": "string",
            "format": "uuid"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "method": {
            "type": "string",
            "const": "PUT"
          },
          "headers": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "importId",
          "url",
          "method",
          "headers",
          "expiresAt"
        ],
        "description": "Capacité temporaire d’envoi : respecter exactement méthode/headers, n’envoyer aucun Bearer au stockage, conserver uniquement l’idempotencyKey et les identités durables dans la file hors ligne."
      },
      "BeginImportResponse": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "session": {
            "$ref": "#/components/schemas/ImportSession"
          },
          "upload": {
            "$ref": "#/components/schemas/UploadTarget"
          }
        },
        "required": [
          "session",
          "upload"
        ]
      },
      "AuthorizationCodeTokenInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "grant_type": {
            "type": "string",
            "const": "authorization_code"
          },
          "client_id": {
            "type": "string",
            "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$",
            "example": "library-manager-mobile"
          },
          "redirect_uri": {
            "type": "string",
            "format": "uri",
            "example": "https://library-manager.com/mobile/callback",
            "description": "Doit correspondre exactement à une URL HTTPS du registre du client. Aucune redirection arbitraire, joker, fragment ou secret public-client n’est accepté."
          },
          "code": {
            "type": "string",
            "pattern": "^lmc_[A-Za-z0-9_-]{43}$"
          },
          "code_verifier": {
            "type": "string",
            "minLength": 43,
            "maxLength": 128,
            "pattern": "^[A-Za-z0-9._~-]+$"
          }
        },
        "required": [
          "grant_type",
          "client_id",
          "redirect_uri",
          "code",
          "code_verifier"
        ]
      },
      "RefreshTokenInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "grant_type": {
            "type": "string",
            "const": "refresh_token"
          },
          "client_id": {
            "type": "string",
            "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$",
            "example": "library-manager-mobile"
          },
          "refresh_token": {
            "type": "string",
            "pattern": "^lmr_[A-Za-z0-9_-]{43}$"
          }
        },
        "required": [
          "grant_type",
          "client_id",
          "refresh_token"
        ]
      },
      "OAuthTokenInput": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/AuthorizationCodeTokenInput"
          },
          {
            "$ref": "#/components/schemas/RefreshTokenInput"
          }
        ],
        "description": "Formulaire UTF-8 application/x-www-form-urlencoded, au plus8192 octets, sans champs dupliqués, inconnus, client_secret ni Authorization Basic. Le grant password n’est pas disponible."
      },
      "TokenResponse": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "access_token": {
            "type": "string",
            "pattern": "^lma_[A-Za-z0-9_-]{43}$"
          },
          "token_type": {
            "type": "string",
            "const": "Bearer"
          },
          "expires_in": {
            "type": "integer",
            "minimum": 0,
            "maximum": 600
          },
          "refresh_token": {
            "type": "string",
            "pattern": "^lmr_[A-Za-z0-9_-]{43}$"
          },
          "scope": {
            "type": "string"
          }
        },
        "required": [
          "access_token",
          "token_type",
          "expires_in",
          "refresh_token",
          "scope"
        ],
        "description": "Accès au plus600 s ; grant au plus7 jours et jamais au-delà de la session autorisante. Rotation du refresh à chaque succès, sans prolonger la durée absolue ; réutiliser un ancien refresh révoque la famille entière. Sérialiser les refresh par compte ; stocker uniquement dans Keychain/Keystore."
      },
      "RevokeInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "client_id": {
            "type": "string",
            "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$",
            "example": "library-manager-mobile"
          },
          "token": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2048
          },
          "token_type_hint": {
            "type": "string",
            "enum": [
              "access_token",
              "refresh_token"
            ]
          }
        },
        "required": [
          "client_id",
          "token"
        ]
      },
      "OAuthError": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "invalid_request",
              "invalid_client",
              "invalid_grant",
              "invalid_scope",
              "unsupported_grant_type",
              "unsupported_response_type",
              "access_denied",
              "server_error",
              "temporarily_unavailable"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "OAuthMetadata": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "issuer": {
            "type": "string",
            "format": "uri"
          },
          "authorization_endpoint": {
            "type": "string",
            "format": "uri"
          },
          "token_endpoint": {
            "type": "string",
            "format": "uri"
          },
          "revocation_endpoint": {
            "type": "string",
            "format": "uri"
          },
          "response_types_supported": {
            "type": "array",
            "items": {
              "type": "string",
              "const": "code"
            }
          },
          "grant_types_supported": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "authorization_code",
                "refresh_token"
              ]
            }
          },
          "code_challenge_methods_supported": {
            "type": "array",
            "items": {
              "type": "string",
              "const": "S256"
            }
          },
          "token_endpoint_auth_methods_supported": {
            "type": "array",
            "items": {
              "type": "string",
              "const": "none"
            }
          },
          "revocation_endpoint_auth_methods_supported": {
            "type": "array",
            "items": {
              "type": "string",
              "const": "none"
            }
          },
          "scopes_supported": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Scope"
            }
          }
        },
        "required": [
          "issuer",
          "authorization_endpoint",
          "token_endpoint",
          "revocation_endpoint",
          "response_types_supported",
          "grant_types_supported",
          "code_challenge_methods_supported",
          "token_endpoint_auth_methods_supported",
          "revocation_endpoint_auth_methods_supported",
          "scopes_supported"
        ]
      }
    },
    "responses": {
      "ApiHTTP401": {
        "description": "Bearer absent, invalide, expiré ou révoqué. Ne pas remplacer le Bearer par les cookies du site ; WWW-Authenticate: Bearer.",
        "headers": {
          "Cache-Control": {
            "description": "private, no-store sur toutes les réponses.",
            "schema": {
              "type": "string"
            }
          },
          "Referrer-Policy": {
            "description": "no-referrer pour l’API.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          "WWW-Authenticate": {
            "schema": {
              "type": "string",
              "const": "Bearer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiErrorResponse"
            }
          }
        }
      },
      "ApiHTTP403": {
        "description": "Scope absent, rôle ou adhésion insuffisant ; termsAcceptanceRequired indique les conditions à accepter sur le site.",
        "headers": {
          "Cache-Control": {
            "description": "private, no-store sur toutes les réponses.",
            "schema": {
              "type": "string"
            }
          },
          "Referrer-Policy": {
            "description": "no-referrer pour l’API.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiErrorResponse"
            }
          }
        }
      },
      "ApiHTTP404": {
        "description": "Livre/version/import introuvable ou non accessible. Ne pas supprimer automatiquement un fichier local.",
        "headers": {
          "Cache-Control": {
            "description": "private, no-store sur toutes les réponses.",
            "schema": {
              "type": "string"
            }
          },
          "Referrer-Policy": {
            "description": "no-referrer pour l’API.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiErrorResponse"
            }
          }
        }
      },
      "ApiHTTP409": {
        "description": "revisionConflict ou quotaExceeded ; relire la progression et demander un choix explicite avant une nouvelle opération.",
        "headers": {
          "Cache-Control": {
            "description": "private, no-store sur toutes les réponses.",
            "schema": {
              "type": "string"
            }
          },
          "Referrer-Policy": {
            "description": "no-referrer pour l’API.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiErrorResponse"
            }
          }
        }
      },
      "ApiHTTP413": {
        "description": "Corps trop volumineux : réduire/corriger, sans retry identique.",
        "headers": {
          "Cache-Control": {
            "description": "private, no-store sur toutes les réponses.",
            "schema": {
              "type": "string"
            }
          },
          "Referrer-Policy": {
            "description": "no-referrer pour l’API.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiErrorResponse"
            }
          }
        }
      },
      "ApiHTTP422": {
        "description": "validationFailed ou integrityMismatch : corriger les paramètres ou récupérer à nouveau les octets.",
        "headers": {
          "Cache-Control": {
            "description": "private, no-store sur toutes les réponses.",
            "schema": {
              "type": "string"
            }
          },
          "Referrer-Policy": {
            "description": "no-referrer pour l’API.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiErrorResponse"
            }
          }
        }
      },
      "ApiHTTP423": {
        "description": "accountDeletionScheduled : compte gelé, ouvrir la gestion de compte sur le site.",
        "headers": {
          "Cache-Control": {
            "description": "private, no-store sur toutes les réponses.",
            "schema": {
              "type": "string"
            }
          },
          "Referrer-Policy": {
            "description": "no-referrer pour l’API.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiErrorResponse"
            }
          }
        }
      },
      "ApiHTTP429": {
        "description": "Limitation de débit : lire details.retryAfterSeconds, appliquer backoff et jitter.",
        "headers": {
          "Cache-Control": {
            "description": "private, no-store sur toutes les réponses.",
            "schema": {
              "type": "string"
            }
          },
          "Referrer-Policy": {
            "description": "no-referrer pour l’API.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiErrorResponse"
            }
          }
        }
      },
      "ApiHTTP500": {
        "description": "Défaillance interne à code fermé ; conserver le journal local.",
        "headers": {
          "Cache-Control": {
            "description": "private, no-store sur toutes les réponses.",
            "schema": {
              "type": "string"
            }
          },
          "Referrer-Policy": {
            "description": "no-referrer pour l’API.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiErrorResponse"
            }
          }
        }
      },
      "ApiHTTP503": {
        "description": "Configuration ou fournisseur indisponible : retry borné pour les opérations idempotentes seulement.",
        "headers": {
          "Cache-Control": {
            "description": "private, no-store sur toutes les réponses.",
            "schema": {
              "type": "string"
            }
          },
          "Referrer-Policy": {
            "description": "no-referrer pour l’API.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiErrorResponse"
            }
          }
        }
      },
      "ApiHTTP405": {
        "description": "Méthode non supportée ; aucun effet métier. Authentification vérifiée avant la réponse.",
        "headers": {
          "Cache-Control": {
            "description": "private, no-store sur toutes les réponses.",
            "schema": {
              "type": "string"
            }
          },
          "Referrer-Policy": {
            "description": "no-referrer pour l’API.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          "Allow": {
            "description": "Méthodes de cette ressource.",
            "schema": {
              "type": "string"
            }
          }
        }
      },
      "OAuthHTTP400": {
        "description": "invalid_request/client/grant/scope ou grant/réponse non supporté.",
        "headers": {
          "Cache-Control": {
            "description": "private, no-store sur toutes les réponses.",
            "schema": {
              "type": "string"
            }
          },
          "Referrer-Policy": {
            "description": "no-referrer pour l’API.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/OAuthError"
            }
          }
        }
      },
      "OAuthHTTP401": {
        "description": "Authentification Basic/Authorization refusée : client public sans secret.",
        "headers": {
          "Cache-Control": {
            "description": "private, no-store sur toutes les réponses.",
            "schema": {
              "type": "string"
            }
          },
          "Referrer-Policy": {
            "description": "no-referrer pour l’API.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/OAuthError"
            }
          }
        }
      },
      "OAuthHTTP403": {
        "description": "access_denied : compte/session/consentement ne permet pas l’autorisation.",
        "headers": {
          "Cache-Control": {
            "description": "private, no-store sur toutes les réponses.",
            "schema": {
              "type": "string"
            }
          },
          "Referrer-Policy": {
            "description": "no-referrer pour l’API.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/OAuthError"
            }
          }
        }
      },
      "OAuthHTTP404": {
        "description": "Action OAuth inconnue.",
        "headers": {
          "Cache-Control": {
            "description": "private, no-store sur toutes les réponses.",
            "schema": {
              "type": "string"
            }
          },
          "Referrer-Policy": {
            "description": "no-referrer pour l’API.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/OAuthError"
            }
          }
        }
      },
      "OAuthHTTP413": {
        "description": "Formulaire dépasse8192 octets.",
        "headers": {
          "Cache-Control": {
            "description": "private, no-store sur toutes les réponses.",
            "schema": {
              "type": "string"
            }
          },
          "Referrer-Policy": {
            "description": "no-referrer pour l’API.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/OAuthError"
            }
          }
        }
      },
      "OAuthHTTP429": {
        "description": "temporarily_unavailable : attendre Retry-After.",
        "headers": {
          "Cache-Control": {
            "description": "private, no-store sur toutes les réponses.",
            "schema": {
              "type": "string"
            }
          },
          "Referrer-Policy": {
            "description": "no-referrer pour l’API.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          "Retry-After": {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 9007199254740991
            },
            "description": "Délai sûr en secondes avant nouvel essai."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/OAuthError"
            }
          }
        }
      },
      "OAuthHTTP503": {
        "description": "server_error ou temporarily_unavailable ; aucune valeur sensible dans l’erreur.",
        "headers": {
          "Cache-Control": {
            "description": "private, no-store sur toutes les réponses.",
            "schema": {
              "type": "string"
            }
          },
          "Referrer-Policy": {
            "description": "no-referrer pour l’API.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/OAuthError"
            }
          }
        }
      },
      "OAuthHTTP405": {
        "description": "Méthode non permise, aucun code ni token émis.",
        "headers": {
          "Cache-Control": {
            "description": "private, no-store sur toutes les réponses.",
            "schema": {
              "type": "string"
            }
          },
          "Referrer-Policy": {
            "description": "no-referrer pour l’API.",
            "schema": {
              "type": "string"
            }
          },
          "X-Request-Id": {
            "description": "UUID corrélable sans journaliser de jeton ou données personnelles.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          "Allow": {
            "schema": {
              "type": "string"
            }
          }
        }
      }
    }
  }
}
