Sur cette page

← Toute la documentation

Catalogue de mods et de skins : contrat

Le contrat du serveur : GET /api/catalog, règles de publication, vérification des archives, fichiers servis.

Statut : implémenté et déployé (étape 1 : le catalogue est alimenté par l'auteur du jeu avec prism-admin ; les comptes et les envois par les joueurs viennent plus tard). Les noms de champs ci-dessous sont le contrat : le client du jeu s'y appuie. Le document ne fait que grandir : les champs inconnus doivent être ignorés par les lecteurs ; il n'y aura pas de « v2 ».

Règles de publication

Skin Mod
Fichier .zip (au plus 8 MiB) .pvmod (au plus 2 MiB)
État à la publication approved (public tout de suite) pending (invisible jusqu'à catalog approve)
Limite 2 skins par auteur (author_key = nom normalisé : espaces réduits, minuscules). Une nouvelle version d'un skin ne compte pas comme un troisième ; retirer un skin libère la place aucune
Permissions aucune celles que le mod déclare (stage, skinImport…)

États d'une ligne : pending → approved ⇄ withdrawn. Seuls les éléments approved sont publics (API, catalog.json, SHA256SUMS.txt, fichiers). Les fichiers pending et withdrawn sont rangés dans catalog/.held/<kind>/, que ni Caddy ni le backend ne servent : un mod en attente ne se télécharge pas en devinant son nom. Une ligne par version ; la version publique d'un identifiant est la plus récente approuvée (plus grand seq).

Le contenu de l'archive est vérifié avant publication, avec les règles de l'importeur du jeu : chemins relatifs sûrs (ni absolu, ni .., ni lettre de lecteur, ni \), pas de lien symbolique, pas de chiffrement, types de fichiers autorisés (mods : .ts .ttf .otf .png .json .md .txt ; skins : .ts .png .ttf .otf), au plus 1 024 entrées / 512 fichiers / 16 MiB par fichier / 64 MiB décompressés, doublons de casse refusés, tailles déclarées contrôlées en lisant réellement chaque entrée, et un fichier d'entrée (main.ts ou index.ts pour un mod, skin.ts pour un skin) à la racine ou dans l'unique dossier racine.

GET /api/catalog

Sans authentification, limité par adresse (120 requêtes par minute par défaut ; 429 avec Retry-After). ETag fort + Cache-Control: no-cache : renvoyer If-None-Match donne 304. Le corps ne change que quand le contenu change (generatedAt = dernière modification).

Paramètre : kind=mod|skin (les deux types si absent ; autre valeur : 400).

{
  "schema": 1,
  "generatedAt": "2026-10-11T09:00:00Z",
  "items": [
    {
      "id": "better-hud",
      "kind": "mod",
      "name": "Better HUD",
      "version": "1.2.0",
      "seq": 7,
      "author": "Ada",
      "descriptionEn": "A cleaner HUD.",
      "descriptionFr": "Un HUD plus propre.",
      "permissions": ["stage"],
      "sha256": "43451505e2c8fb5c986c7e1e19a132d333d1b96e6eb03aa576a276903a8738f2",
      "size": 18342,
      "filename": "better-hud-1.2.0.pvmod",
      "url": "/files/catalog/mod/better-hud-1.2.0.pvmod",
      "status": "approved",
      "owner": "acct:42",
      "createdAt": "2026-10-01T10:00:00Z",
      "updatedAt": "2026-10-11T08:59:00Z",
      "screenshots": ["/files/catalog/mod/better-hud-7-1.png"],
      "versions": [
        { "version": "1.2.0", "seq": 7, "publishedAt": "2026-10-11T08:59:00Z", "filename": "better-hud-1.2.0.pvmod", "url": "/files/catalog/mod/better-hud-1.2.0.pvmod", "sha256": "…", "size": 18342 },
        { "version": "1.1.0", "seq": 3, "publishedAt": "2026-10-02T10:00:00Z", "filename": "better-hud-1.1.0.pvmod", "url": "/files/catalog/mod/better-hud-1.1.0.pvmod", "sha256": "…", "size": 17010 }
      ]
    }
  ]
}
Champ Sens
id identifiant stable de l'élément (minuscules, chiffres, -, _, .), unique par type
kind mod ou skin
name nom affiché
version libellé libre de la version la plus récente approuvée : ne jamais l'analyser ni le trier
seq entier strictement croissant par type (ordre de publication) : c'est lui qui ordonne les versions
author auteur crédité (nom affiché)
descriptionEn texte brut anglais (toujours présent)
descriptionFr texte brut français, absent si non fourni
permissions permissions déclarées par le mod ; [] pour un skin
sha256 / size / filename hash hexadécimal minuscule, octets et nom du fichier de cette version
url adresse de téléchargement : relative au site, ou absolue quand le serveur connaît son adresse publique (PUBLIC_BASE_URL)
status toujours approved dans les documents publics
owner identifiant de compte du propriétaire ; absent tant que les comptes n'existent pas
createdAt / updatedAt première publication approuvée de l'élément / dernière modification de la version courante
screenshots petites images d'aperçu (URL), absent s'il n'y en a pas
versions toutes les versions approuvées, de la plus récente à la plus ancienne ; la première est celle des champs ci-dessus

Les éléments sont triés par type (mod puis skin) puis par nom (sans casse), puis par id.

GET /api/catalog/{kind}/{id}

Un élément, même forme que ci-dessus (un objet, pas de items). 404 si le type est inconnu, si l'identifiant n'existe pas ou si aucune version n'est approuvée. Mêmes ETag, 304 et limite de débit.

Fichiers

/files/catalog/<kind>/<filename>, comme les zips du jeu : Content-Disposition: attachment, Cache-Control: public, max-age=31536000, immutable (un nom de fichier n'est jamais réécrit), Range accepté, X-Content-Type-Options: nosniff. Les aperçus (.png, .jpg, .webp) sont servis sans attachment, immuables aussi. Les chemins contenant un segment caché (.held, .incoming) répondent 404.

Documents générés à chaque changement : /files/catalog/catalog.json (les deux types, même forme que GET /api/catalog) et /files/catalog/SHA256SUMS.txt (<sha256> <kind>/<fichier> pour chaque paquet approuvé, vérifiable avec sha256sum -c).

Administration (sur le serveur, prism-admin catalog …)

catalog add --kind mod|skin --file <.pvmod|.zip> --id <slug> --name <nom> --version <libellé> --author <nom>
            --desc-en <texte> [--desc-fr <texte>] [--permissions stage,skinImport] [--owner <compte>] [--screenshot <png|jpg|webp>]…
catalog list [--kind …] [--status pending|approved|withdrawn]
catalog approve <id> [<version>] [--kind …]        # un mod ne devient public que par là
catalog withdraw <id> [<version>] [--kind …] [--reason …]
catalog verify [--kind …]                          # re-hache chaque fichier (où que son état le range)
catalog regenerate                                 # réécrit catalog.json et SHA256SUMS.txt

Refus : doublon id + version ou nom de fichier ; un identifiant déjà pris par un autre auteur ; un 3ᵉ skin d'un même auteur ; fichier trop gros, qui n'est pas un ZIP, ou dont l'archive contient une entrée dangereuse ; permission déclarée sur un skin ; aperçu qui n'est pas une vraie image (reconnue par son contenu, 512 KiB au plus, trois au plus).

Dans le code : CatalogStore::skins_of_author(author_key) donne les skins d'un auteur (l'assistant de décompte) ; la colonne owner (texte, nulle) attend les comptes.

Côté site

Pages /mods/ et /skins/ (/fr/mods/, /fr/skins/) : cartes (nom, auteur crédité, version, badges de permissions expliquées pour les mods, SHA-256, bouton de téléchargement, aperçus), note « comment l'installer », état vide calme. Elles lisent GET /api/catalog?kind=… côté client.

Source dans le dépôt du site : docs/CATALOG.md