npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@clicjo/contrat-exploitation

v2.4.0

Published

Le contrat de l'API d'exploitation de clicjo (doc 12 § 6, D33) : schemas Zod ET table des routes des canaux d'integration — deploiements, interrupteurs, quota, liste nominative, lien de lancement, lecture, canal sortant, widget, purge. Sous-chemin /admin

Readme

@clicjo/contrat-exploitation

Le contrat de l'API d'exploitation de clicjo : schémas Zod et table des routes des canaux d'intégration. Schémas et types dérivés d'une même source — une divergence d'interprétation devient une erreur de compilation, jamais un incident en production.

Publié pour que la CI d'un intégrateur casse au bump de version, plutôt que de découvrir la dérive en production.

Installation

npm install @clicjo/contrat-exploitation

Dépendance unique : zod.

Ce que ce paquet couvre

| Canal | Ce qu'il porte | | ------------------ | ---------------------------------------------------------------------------- | | Déploiements | création, suspension, réactivation, fermeture, quota, interrupteurs d'achat | | Accès apprenant | liens de lancement, jetons d'embarquement, widget | | Entrée dans le jeu | les noms de paramètres d'URL et urlEntree() | | Lecture | progression, recueils, consentements — paginés, filtrables par apprenant | | Canal sortant | déclaration et révocation de connecteurs, signature | | RGPD | purge de données personnelles | | Catalogue | les jeux servis, leurs variables de configuration et leurs salles |

Le back-office de la plateforme (créer un client, émettre une clé, poser un mandat) vit au sous-chemin @clicjo/contrat-exploitation/admin — l'import dit ainsi qu'on franchit une frontière de privilège.

Exemple

import { ROUTES, construireChemin, urlEntree } from '@clicjo/contrat-exploitation';

// La table des routes est la source : ne composez pas les chemins à la main.
const chemin = construireChemin(ROUTES.lireProgression, { jetonDeploiement });

// L'URL à ouvrir dans le navigateur avec un jeton de lancement.
const url = urlEntree('https://jeu.clicjo.fr', 'lien', jetonLancement);

Lire une cohorte entière — depuis la 2.0.0, en suivant le curseur jusqu'à ce qu'il manque :

const progressions = [];
let curseur: string | undefined;

do {
  const query = new URLSearchParams({ limite: '200', ...(curseur ? { curseur } : {}) });
  const reponse = await fetch(`${base}${chemin}?${query}`, { headers: { 'x-cle-api': cle } });

  // Le schéma publié est le validateur : ne relisez pas la réponse à la main.
  const page = ROUTES.lireProgression.reponse.parse(await reponse.json());

  progressions.push(...page.progressions);
  curseur = page.curseurSuivant; // absent = dernière page
} while (curseur);

Lire un seul apprenant — un appel, plutôt que soixante lignes à jeter :

const query = new URLSearchParams({ refApprenant: 'apprenant-42' });
const reponse = await fetch(`${base}${chemin}?${query}`, { headers: { 'x-cle-api': cle } });

Deux pièges que ce paquet existe pour fermer

Les jetons sont OPAQUES. Un jeton de déploiement s'écrit <partition>_<uuid> — ne le décomposez pas, ne le rangez pas dans une colonne de type uuid, ne le validez pas contre une expression régulière d'UUID. Le contrat le déclare string, et c'est tout ce qu'il en promet.

?lien= et ?jeton= ne font pas la même chose. Le premier rouvre une partie pour une personne, le second en ouvre une par le déploiement. Les confondre donne une URL qui fonctionne à l'essai et perd l'apprenant à la reprise. Utilisez urlEntree() plutôt que de composer l'adresse.

Ce que la 2.0.0 rompt

Une seule chose, et elle ne concerne que les trois lectures du canal PULL (lireProgression, lireRecueils, lireConsentements) : elles rendaient tout, elles rendent une page.

Jusqu'ici une lecture rendait l'intégralité du déploiement à chaque appel. Le coût grandissait avec la cohorte, sans plafond, et suivre une personne obligeait à télécharger les soixante autres pour en jeter cinquante-neuf. Ces trois réponses portent désormais un curseurSuivant, et leur requête accepte limite (défaut 50, plafond 200), curseur et refApprenant.

Le seul geste obligatoire à la migration : suivre curseurSuivant. Un appel qui l'ignore compile, répond 200, et rend les 50 premières lignes en ayant l'air d'avoir tout rendu — c'est la raison pour laquelle ceci est une majeure et non une mineure additive.

| Vous faisiez | Vous faites | | --------------------------------------------------- | --------------------------------------------------------- | | un appel, tout le déploiement | une boucle sur curseurSuivant (cf. l'exemple ci-dessus) | | tout lire puis filtrer chez vous sur refApprenant | ?refApprenant=… — un appel, une personne |

Il n'y a ni total ni nombre de pages, et ce n'est pas un oubli : les deux exigeraient un COUNT sur toute la table à chaque appel, et ils mentiraient dès qu'une partie serait jouée entre deux pages. Le curseur est opaque : renvoyez-le tel quel, ne le décomposez pas, ne le supposez pas ordonnable. Un curseur périmé vaut absence de curseur — la première page, jamais une erreur.

Il n'y a pas de paramètre env sur ces trois routes, contrairement à listerDeploiements : le déploiement est dans l'URL et porte son propre environnement. L'étanchéité recette/prod reste assurée par la clé d'API que vous présentez.

Une exception sur le grain, et il faut la connaître avant de dimensionner vos appels : sur lireRecueils, limite compte des apprenants, pas des recueils — une page rend tous les recueils des apprenants qu’elle porte. Le motif est structurel de notre côté : le puits d’événements ne porte pas de colonne de déploiement, et un ordre global sur les recueils ne serait servi par aucun index, donc un tri complet à chaque page. Le volume reste borné parce que le nombre de briques de recueil d’une partie l’est, fixé par le jeu et non par l’apprenant.

Le reste de la majeure est additif — rien à changer pour en profiter :

  • completions ne bouge pas. completionsDatees s'ajoute à côté, avec la date de première complétion de chaque brique. Ne supposez pas que les deux portent les mêmes briques : une complétion dont l’événement a été purgé (droit à l’effacement) figure dans completions et manque à completionsDatees. C’est délibéré — nous ne comblons pas un trou avec une date plausible, ce qui ferait passer une inconnue pour un fait. completions fait foi sur ce qui est résolu, completionsDatees sur ce qui est datable ;
  • derniereActiviteLe (optionnel) date la dernière action jugée — jamais la dernière requête. Un onglet ouvert, un rechargement, une reprise ne comptent pas : sans quoi un apprenant qui a décroché depuis trois semaines paraîtrait actif. Absent = cette personne n'a encore rien fait ;
  • sur un consentement, phrase rend le texte exactement consenti, bénéficiaire interpolé : un numéro de version seul ne justifie pas un envoi. Optionnel — une version introuvable au référentiel ne fait pas échouer la page entière ;
  • sur un recueil, id est un identifiant stable et unique, opaque. Il sert à dédupliquer : une pagination reprise, un rejeu ou deux lectures qui se chevauchent vous feront recevoir deux fois la même réponse, et comparer les recueils champ à champ échoue justement dans le cas où cela compte — deux réponses identiques du même apprenant à deux moments différents. De notre côté il départage : deux recueils d’un même lot partagent leur horodatage, et une pagination sur un ordre partiel saute ou répète des lignes ;
  • retireLe date un retrait, et ?inclureRetires=true rend les consentements retirés. Ils disparaissaient purement, ce qui rendait un retrait indiscernable d'un accord jamais donné — et sans savoir quelle ligne effacer, on ne peut pas donner effet à un retrait. Un retiré est lisible, jamais exploitable : la valeur par défaut reste false.

Versionnage

Semver. Une version mineure est additive : rien de ce que vous épinglez ne change de forme. Une version majeure signale une rupture, et elle est annoncée avant publication.

La 2.0.0 est la première majeure du paquet, et sa rupture est décrite ci-dessus. Épinglez ^2.0.0 : vous recevrez les ajouts, jamais une rupture non annoncée.

La 2.1.0 (2026-08-19) passe les schémas de réponse en mode strip : un champ que le schéma ne décrit pas est accepté et retiré de l'objet validé — vos schémas épinglés cessent ainsi de casser pendant la fenêtre de bascule d'une évolution additive, tandis que les champs déclarés restent exigés à leur nom et dans leur type. Les schémas de requête, eux, restent stricts : sur ce que vous envoyez, la sévérité protège d'un paramètre mal nommé.

La 2.2.0 (2026-08-19) apporte le multi-jeux : jeu optionnel à la création d'un déploiement (absent avec un seul jeu au catalogue : il est choisi ; plusieurs : 400 jeu-requis avec la liste ; inconnu : 400 jeu-inconnu), jeu et versionMaster sur le déploiement publié — le couple est figé à la création, une partie en cours ne change jamais de version — et un catalogue qui liste les jeux réellement servis. Un jeu au contenu invalide répond 503 jeu-indisponible sur ses seuls déploiements. Note de registre : la 2.1.0 n'a jamais été publiée seule (incident de jeton de publication) — la 2.2.0 est la première à porter le strip.

La 2.2.1 (2026-08-19) est ce paragraphe : la 2.2.0 était partie sans documenter ses propres ajouts — le défaut exact qui avait forcé la 2.0.1, une version plus tôt. Aucun changement de code.

La 2.3.0 (2026-08-19) ferme deux résidus. La création d'un déploiement rend désormais le couple jeu / versionMaster figé — optionnels au schéma parce qu'un rejeu d'idempotence antérieur à la 2.3.0 rend l'ancienne forme verbatim (30 jours au plus, la rétention des clés) ; la lecture unitaire reste là où le couple est exigé. Et la politique de rétention se lit (GET /retentions) : durées et points de départ des compteurs, publiés en données — vous n'avez plus à la connaître par annonce. Un serveur dont la composition ne déclare aucune politique répond 501 retentions-non-declarees : « je ne dis pas » n'est pas « rien n'expire ».

La 2.4.0 (2026-08-19) livre le flux des recueils (GET /lecture/deploiements/:jetonDeploiement/recueils/flux) : la lecture au grain du recueil, dans l'ordre où le serveur les a écrits — les nouveaux arrivent toujours en fin de flux, donc conservez votre dernier curseurSuivant et revenez plus tard : vous ne recevrez que le nouveau. Dédupliquez par id : après un effacement concurrent, la reprise préfère vous redonner un recueil déjà vu plutôt que d'en sauter un. La lecture au grain de l'apprenant est inchangée.

La même 2.4.0 apporte aussi le lien de reprise (D68) — la troisième nature de lien, qui ne se confond avec aucune des deux autres : là où le lien de lancement est signé, à usage unique et expire seul, le lien de reprise est opaque (le serveur n'en garde que l'empreinte SHA-256), multi-usage, sans échéance propre — une session qui existe est reprenable, sauf révocation — et révocable : par vous (POST /deploiements/:jetonDeploiement/liens-reprise/revoquer), par nous, et en cascade au retrait de l'apprenant. Vous frappez le lien sur la session d'un apprenant rattaché (POST /deploiements/:jetonDeploiement/liens-reprise, refApprenant recopié de l'API de lecture), le secret vous est rendu une seule fois (re-frapper révoque le précédent — au plus un secret vivant par session), et votre canal le remet à l'apprenant : c'est vous qui le connaissez. L'apprenant le présente à POST /liens-reprise/consommer (publique) et retrouve sa session — jamais une autre, jamais une création. Périmètre : parcours rattachés seulement ; les visiteurs anonymes gardent leur reprise courte par cookie.

Licence

UNLICENSED — tous droits réservés. Le paquet est publié pour être installé par les intégrateurs de clicjo, non pour être réutilisé ou redistribué.