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.