Plugin SDK
Déclarez des parcours, schémas, contributions d’éditeur et traitements via le contrat public.
Sur cette page
Définition du plugin
PluginDefinition et definePlugin sont exportés par @bracten/sdk. Le module ESM possède un export par défaut. plugin:pack assemble les imports statiques du SDK et des dépendances JavaScript avant signature ; le package installé est autonome, y compris hors du checkout.
import { definePlugin } from "@bracten/sdk";
export default definePlugin({
priority: 10,
hooks: {
async beforeContentSave(entry, context) {
if (entry.type === "post" && entry.status === "published" && !entry.excerpt) {
context.failValidation("Ajoutez un extrait avant publication.");
}
},
},
});Contributions disponibles
| Clé | Contrat |
|---|---|
| contentTypes / taxonomies / settings | Schémas de contenu, classements, champs de réglages et valeurs validées. |
| contentStatuses | Statuts éditoriaux additionnels non publics, avec permission facultative, depuis le lot CMS 3f23b75. |
| migrations / activate / deactivate | Évolutions versionnées et lifecycle transactionnel. |
| endpoints | GET, POST ou PUT ; public explicite ou permission privée, schémas JSON d’entrée et de sortie facultatifs. |
| adminPages / publicPages | Sections déclaratives texte, formulaires, tableaux et liens. |
| blocks / fieldTypes | Blocs à copie native de secours et champs structurés versionnés. |
| jobs / commands | Handlers de tâches durables et commandes CLI de l’extension. |
| filters | content.title et render.head ; anciennes signatures conservées. |
| hooks / events | Hooks nommés de contenu, médias, utilisateurs, activation et rendu ; six événements métier durables, dont auth.login. |
| roles | Rôles déclarés sans attribution automatique aux utilisateurs. |
| widgets / adminMenu / settingsSections | Vue d’ensemble, liens vers des pages déclarées et sections de réglages. |
| toolbarActions / integrations | Actions privées d’éditeur ou de médiathèque, liens vers les espaces d’intégration. |
| webhooks | Événements nommés avec payload validé et outbox transactionnelle. |
| storageProviders / cacheProviders / emailProviders | Jusqu’à cinq factories par catégorie, choisies dans la configuration serveur. |
| priority | Ordre croissant, défaut 10, puis identifiant de plugin. |
Les permissions du manifest commencent par l’identifiant du plugin. Elles sont attribuées initialement aux rôles système possédant plugins.manage ; les autres rôles les reçoivent explicitement. L’activation et chaque action privée revalident les droits.
Contexte et services
PluginContext fournit pluginId, publicUrl, settings.get/set, database.query(sql, parameters), failValidation(message) et services. PluginRequestContext ajoute user, query et visibility.clause. Le runtime fournit authorize(permission) pour relire les droits ; une opération système ne devient pas un utilisateur implicite. database.query retourne un tableau de lignes et utilise la transaction en cours.
| Service | Usage |
|---|---|
| email.enqueue | Enregistrer un e-mail dans l’outbox avec la transaction métier. |
| jobs.enqueue / schedule / unschedule | Tâches déclarées du module, reprises et déduplication facultative. |
| storage.put / get / list / delete | Objets propres au plugin, immuables, maximum 10 Mo et purge après sept jours. |
| cache.get / set / delete / invalidate | Cache isolé par plugin, TTL et tags ; les effets cache ne sont pas transactionnels. |
| accounts.register | Créer un compte sans rôle ni permission fournis par le client ; confirmation d’adresse obligatoire depuis le lot b528784. |
| access.setRule / grant / revoke | Définir les groupes d’accès et leurs attributions. |
| webhooks.publish | Inscrire un événement déclaré dans l’outbox de la transaction. |
| observability.metric / report | Mesures bornées et rapports expurgés dans l’espace de l’extension. |
| serviceInfo() | Identifier les providers configurés sans exposer leurs secrets. |
Les providers optionnels doivent être testés avant emploi. Les clés de stockage font au plus 200 caractères, avec segments de 100 caractères maximum composés de lettres, chiffres, points, tirets ou soulignements ; un segment commence par une lettre, un chiffre, un tiret ou un soulignement. Les chemins relatifs ascendants sont refusés.
Les factories utilisent PluginProviderContext : pluginId, settings, options et dataDirectory, sans services CMS pour éviter une dépendance circulaire au démarrage. storageProviders retourne { identity, storage }, cacheProviders son contrat de cache et emailProviders un objet deliver({ to, subject, text }, deliveryId), avec close() facultatif. L’identité du stockage décrit un jeu de données stable sans secret. deliveryId reste identique pendant les reprises, sans garantie exactement une fois pour l’effet externe.
Le guide des fournisseurs SDK détaille la sélection serveur, les capabilities réservées aux factories officielles, la fermeture des ressources et la reprise. Les services métier continuent d’utiliser email.enqueue, storage et cache indépendamment du fournisseur choisi.
Inscription et confirmation d’adresse
Depuis b528784, services.accounts.register crée un compte soumis à confirmation et exige un transport e-mail. Membership demande le nom et l’adresse ; le destinataire choisit son mot de passe après ouverture du lien reçu. Le mot de passe initial encore accepté par le SDK ne permet aucun accès et sera remplacé. Les doublons n’écrasent ni mot de passe, ni profil, ni rôles, et la réponse publique ne révèle pas l’existence du compte.
POST /api/v1/email-verification/request reçoit { email } et retourne HTTP 202 avec { accepted: true }. POST /api/v1/email-verification/complete reçoit { token, newPassword } et retourne { success: true } en fermant le cookie de session. Ces routes publiques acceptent un corps JSON de 8 Kio maximum et exigent l’origine exacte. Ouvrir l’URL ne consomme pas le jeton ; l’interface retire son fragment puis attend le POST explicite. La confirmation ne crée aucune session ni approbation Membership.
Le jeton aléatoire de 32 octets, stocké uniquement par empreinte SHA-256, expire après trente minutes et s’utilise une fois. Il est lié à l’adresse et à la version du compte ; une nouvelle émission remplace l’ancienne. La demande est bornée à cinq essais par adresse et trente par adresse réseau sur quinze minutes, avec au plus une émission par compte et par minute. Le jeton brut n’est conservé ni dans un job ni dans l’audit.
La migration additive services:2 ajoute email_verification_required, email_verified_at et email_verifications. Les comptes historiques ou créés explicitement par un administrateur restent autorisés sans être déclarés vérifiés. Un compte soumis à vérification ne peut pas se connecter, terminer une MFA, utiliser un Bearer ou obtenir un groupe Membership avant confirmation. Le changement administratif de son adresse révoque cette confirmation et ses sessions. Le correctif 5dba3aa préserve aussi la hiérarchie des rôles assignés aux comptes inactifs sous un plafond de permissions d’extension.
Confirmer remplace le mot de passe, révoque sessions, challenges MFA et demandes de récupération, tout en conservant une MFA déjà configurée. Restaurer une sauvegarde annule les liens et leurs jobs en attente ; un retour au binaire précédant la migration n’est pas une restauration prise en charge. Ce parcours nécessite une distribution intégrant b33ff99, tag 0.1.0-development-b33ff99, ou une version ultérieure compatible ; 75de887 et les distributions antérieures ne l’incluent pas. docs/email-verification.md détaille les tests PostgreSQL et le parcours Chromium bureau/mobile sur capture locale. Ces preuves ne démontrent ni une réception SMTP externe ni la version déployée sur une instance.
Déclarer des statuts éditoriaux
Le lot 3f23b75 ajoute PluginDefinition.contentStatuses. Chaque déclaration possède name, label et une permission facultative déjà déclarée dans le manifest. Pour agency.workflow, le nom review devient agency.workflow:review. Le nom local suit ^[a-z][a-z0-9_]{0,59}$ ; noms réservés, doublons et clés inconnues sont refusés. Le libellé accepte 1 à 100 caractères, avec au plus vingt statuts par extension.
contentStatuses: [
{ name: "review", label: "À relire" },
{ name: "approved", label: "Validé en interne", permission: "agency.workflow.approve" },
],Les cinq statuts natifs draft, published, scheduled, private et trash restent distincts. Un statut additionnel ne publie rien, ne programme aucune échéance et ne définit aucun moteur de transitions. Créer ou modifier conserve content.create/content.update ; toute nouvelle affectation exige une contribution active et sa permission éventuelle. Quitter published, scheduled ou private exige aussi content.publish, et choisir trash exige content.delete. Les droits sont relus sous verrou avec le plafond Bearer.
GET /api/v1/content-statuses expose name, label, pluginId, permission, active et assignable, sous content.read, avec session ou Bearer explicitement autorisé. assignable ne remplace pas les contrôles du contenu et de sa version. Les scopes Bearer n’accordent aucune permission d’extension : un statut exigeant une telle permission reste inaccessible à ce jeton. Le SDK exporte ContentStatus, NativeContentStatus, PluginContentStatus, ContentStatusOption et parseContentStatus ; contentStatuses conserve sa liste historique des seuls statuts natifs.
La migration content_statuses:1 crée le registre persistant et remplace la contrainte fermée par une clé étrangère, sans réécrire contenus, versions ou snapshots. Retrait, désactivation ou suppression de l’extension préservent l’identité et le dernier libellé. Une valeur déjà présente peut être conservée, mais une nouvelle affectation est refusée. Le registre accepte 2 000 statuts d’extension historiques inclus ; un dépassement annule l’activation ou la mise à jour. Les lectures restent bornées à 2 005 options avec les cinq valeurs natives.
Brouillons, filtres, hooks et révisions conservent l’identifiant complet. Restaurer une révision crée toujours un nouveau draft. Les événements content.created/content.updated le conservent également ; seul un passage à published produit content.published. Le libellé fourni par un auteur reste sa donnée et n’est pas traduit implicitement. docs/additional-content-statuses.md décrit les permissions, le cycle de vie et les preuves locales. Ce contrat nécessite une distribution intégrant b33ff99, tag 0.1.0-development-b33ff99, ou une version ultérieure compatible ; 75de887 et les distributions antérieures ne le fournissent pas. Vérifiez la révision exécutée par l’instance.
Routes et pages déclaratives
| Contribution | Adresse |
|---|---|
| Page admin | /admin/#extensions/<plugin>/<page> |
| Page publique | /extensions/<plugin>/<page> |
| Endpoint privé | /api/v1/extensions/<plugin>/endpoints/<name> |
| Endpoint public | /api/v1/public-extensions/<plugin>/endpoints/<name> |
| Registre admin | GET /api/v1/extensions |
Les pages ne chargent pas de JavaScript arbitraire du plugin dans le navigateur. Les champs et actions sont validés puis rendus par les composants du CMS. Une action et sa page ont des contrôles indépendants ; masquer un bouton ne protège pas un endpoint.
Les mutations publiques exigent Origin exact, jeton signé expirant obtenu sur /api/v1/public-extensions/<plugin>/token et champ anti-robot vide. Le corps est limité à 64 Ko. Les limites durables sont 40 mutations ou 240 lectures par plugin et adresse distante sur quinze minutes ; ces appels partagent leur compteur.
Un endpoint peut déclarer schema.input et schema.output avec PluginJsonSchema. L’entrée est validée avant le handler et la sortie avant commit ; une sortie invalide annule ses écritures. L’ancien input: FieldDefinition[] reste compatible mais ne peut pas coexister avec schema.input. Le sous-ensemble accepte des schémas inline bornés, sans $ref, téléchargement externe ni coercition implicite. Les schémas des endpoints actifs et autorisés alimentent l’OpenAPI de l’instance ; sans déclaration, seul le transport JSON peut être décrit.
Requêtes publiques et accès réservés
const guard = context.visibility.clause("c", 1);
const sql = "SELECT c.id, c.title, c.slug FROM content c " +
"WHERE c.type = $1 AND c.status = 'published' AND " + guard.sql +
" ORDER BY c.published_at DESC, c.id LIMIT 20";
const rows = await context.database.query(sql, ["post", ...guard.parameters]);offset indique le nombre de paramètres déjà présents. Appliquez la même clause avant total, facettes et relations ; ne filtrez jamais seulement après LIMIT. Une règle portée par un plugin désactivé continue à fermer le contenu. Aucun rôle administrateur ne contourne automatiquement ces règles de diffusion.
Distribution et limites
Les modules de confiance peuvent techniquement sortir des helpers ; leur approbation reste nécessaire. Il n’existe pas de chargement de composants admin tiers, de middleware HTTP arbitraire ou de sous-processus isolé fourni par ce contrat.
Le guide des contributions SDK précise les registres, rôles, schémas, événements et providers, avec leurs limites. Consultez aussi les extensions officielles, les blocs, les champs, les hooks et le packaging.