Chaque plugin sandboxé a un emdash-plugin.jsonc à côté de son package.json. Il est édité manuellement et contient l’identité du plugin, son contrat de confiance (capacités, hôtes, stockage) et les champs de profil que le registre affiche. emdash-plugin init en génère un ; la CLI lit ./emdash-plugin.jsonc automatiquement pour build, dev, validate, bundle et publish.
Le fichier est en JSONC : les commentaires et les virgules finales sont autorisés.
L’exemple suivant montre un manifeste complet pour un plugin de galerie d’images :
{
"$schema": "./node_modules/@emdash-cms/plugin-cli/schemas/emdash-plugin.schema.json",
"slug": "gallery",
"publisher": "did:plc:abc123def456",
"license": "MIT",
"author": { "name": "Jane Doe", "url": "https://example.com" },
"security": { "email": "[email protected]" },
// Profil optionnel
"name": "Gallery",
"description": "Bloc galerie d'images pour EmDash.",
"keywords": ["gallery", "images"],
"repo": "https://github.com/example/plugin-gallery",
// Contrat de confiance
"capabilities": ["content:read"],
"allowedHosts": [],
"storage": {}
}
Identité
| Champ | Requis | Notes |
|---|---|---|
slug | Oui | ID sûr pour les URL dans l’espace de noms de l’éditeur. /^[a-z][a-z0-9_-]*$/, max 64 caractères. |
publisher | Oui | Le DID ou handle de votre compte Atmosphere. Voir Épinglage d’éditeur. |
version | Non | Semver 2.0 sans métadonnées de build. Habituellement omis — voir ci-dessous. |
slug et publisher ensemble constituent l’identité du package. EmDash en dérive automatiquement l’identifiant complet du package.
version appartient à package.json
Le build réconcilie la version du manifeste avec package.json#version :
- Les deux définis et égaux → ok.
- Les deux définis et différents → erreur fatale.
- Un seul défini → cette valeur l’emporte.
- Aucun défini → erreur fatale.
Le schéma recommandé pour un plugin distribué via npm est d’omettre version du manifeste et de laisser package.json être la source unique de vérité (votre outillage de release l’incrémente déjà là). Les plugins uniquement dans le registre sans package.json doivent définir version dans le manifeste — il n’y a nulle part ailleurs où le mettre.
Profil
Ces éléments alimentent la fiche du registre. license, un auteur (author ou authors) et un contact de sécurité (security ou securityContacts) sont requis ; le reste est optionnel.
| Champ | Requis | Notes |
|---|---|---|
license | Oui | Expression SPDX ("MIT", "Apache-2.0", "MIT OR Apache-2.0"). Utilisé lors de la première publication ; le profil existant l’emporte lors des publications ultérieures. |
author / authors | Oui | L’un des deux. author: { name, url?, email? } pour un seul auteur ; authors: [...] (≤ 32) pour plusieurs. Définir les deux est une erreur. |
security / securityContacts | Oui | L’un des deux. Chaque contact nécessite au moins email ou url. securityContacts: [...] (≤ 8) pour plusieurs. Définir les deux est une erreur. |
name | Non | Nom d’affichage. Par défaut le slug. |
description | Non | Restez bref (environ 140 caractères). Les valeurs longues peuvent être tronquées dans les listes. |
keywords | Non | ≤ 5 entrées. |
repo | Non | URL https:// du dépôt source. |
Utilisez la forme singulière author / security sauf si vous avez réellement plusieurs — c’est le cas courant et le scaffold l’émet ainsi.
Contrat de confiance
Le contrat de confiance est capabilities, allowedHosts et storage. Les trois sont vides par défaut, donc un plugin qui n’a pas besoin de privilèges supplémentaires peut les omettre entièrement.
{
"capabilities": ["network:request", "content:read"],
"allowedHosts": ["api.example.com", "*.cdn.example.com"],
"storage": {
"events": { "indexes": ["timestamp"] },
"submissions": { "indexes": ["email"], "uniqueIndexes": ["token"] }
}
}
Capacités
Les noms reconnus :
| Capacité | Accorde |
|---|---|
content:read / content:write | Lire / modifier le contenu du site via ctx. |
taxonomies:read | Lire les définitions de taxonomies et les termes (lecture seule). |
media:read / media:write | Lire / écrire des médias. |
users:read | Lire les enregistrements utilisateurs. |
email:send | Envoyer des emails via ctx. |
network:request | HTTP sortant via ctx.http, restreint à allowedHosts. |
network:request:unrestricted | HTTP sortant vers n’importe quel hôte. Utilisé à la place de network:request. |
hooks.email-transport:register | Enregistrer un hook de transport email. |
hooks.email-events:register | Enregistrer des hooks de cycle de vie email. |
hooks.page-fragments:register | Enregistrer un hook page:fragments (natif uniquement). |
Deux règles inter-champs que la CLI applique (la vérification JSON-Schema de l’éditeur ne le fait pas — exécutez emdash-plugin validate) :
network:requestnécessite unallowedHostsnon vide. Si le plugin doit vraiment atteindre n’importe quel hôte, utilisez plutôtnetwork:request:unrestricted.network:request:unrestrictednécessite queallowedHostssoit vide — la capacité non restreinte accorde déjà tous les hôtes, donc une liste serait contradictoire.
Les motifs d’hôte sont des noms d’hôte nus (pas de schéma, chemin ou espace). Un *. initial autorise les sous-domaines : *.cdn.example.com.
Stockage
Une correspondance nom de collection → configuration d’index. Les noms de collection suivent la même règle /^[a-z][a-z0-9_]*$/ (le runtime utilise le nom comme suffixe de table SQL). Les index sont des noms de champ ou des tableaux composites ; uniqueIndexes sont aussi interrogeables — ne les listez pas en plus dans indexes.
"storage": {
"events": { "indexes": ["timestamp", ["collection", "timestamp"]] }
}
Surface d’administration
Optionnel. Les plugins sandboxés affichent des pages d’administration et des widgets de tableau de bord via Block Kit ; le manifeste déclare uniquement où ils apparaissent. Omettez entièrement la clé admin si le plugin n’a pas d’interface d’administration.
"admin": {
"pages": [{ "path": "/gallery", "label": "Galerie", "icon": "image" }],
"widgets": [{ "id": "recent-uploads", "title": "Téléchargements récents", "size": "half" }]
}
Un plugin qui déclare admin.pages ou admin.widgets doit aussi servir une route admin dans src/plugin.ts qui rend le contenu Block Kit — le schéma ne peut pas l’imposer (les noms de route sont sondés depuis le code source, pas le manifeste), mais le runtime le vérifie.
Épinglage d’éditeur
publisher épingle l’identité de publication pour que vous ne puissiez pas accidentellement publier un plugin sous le mauvais compte.
Lors de votre première publication réussie, si le publisher du manifeste correspond à la session active, il reste tel qu’écrit. Si vous avez créé le scaffold avec emdash-plugin init et l’avez laissé vide, la CLI écrit le DID de la session active dans le manifeste.
L’exemple suivant montre la ligne que la CLI écrit, avec le handle résolu ajouté en commentaire pour la lisibilité :
"publisher": "did:plc:abc123def456", // jane.example.com
Lors de chaque publication ultérieure, la CLI résout la session active et le publisher épinglé en DIDs et les compare. Une discordance échoue immédiatement avec MANIFEST_PUBLISHER_MISMATCH — il n’y a pas de flag d’override. Résolvez-le délibérément :
- Mauvaise session :
emdash-plugin switch <did>, puis publiez à nouveau. - Transfert réel du plugin à un nouvel éditeur : éditez
publisherdans le manifeste.
Valider sans publier
emdash-plugin validate # ./emdash-plugin.jsonc
emdash-plugin validate path/ # un répertoire spécifique
Vérification de schéma hors ligne avec des diagnostics style tsc fichier:ligne:colonne, incluant les règles inter-champs. Convient pour un hook de pre-commit ou une étape CI. Les clés dupliquées et les clés inconnues sont des erreurs (le mode strict détecte les fautes de frappe comme "licens").
Les flags CLI l’emportent toujours
Les flags explicites (--license, --author-name, …) remplacent les valeurs du manifeste quand les deux sont définis — utile pour les overrides CI. --no-manifest ignore entièrement le manifeste (et avertit s’il en existe un au chemin par défaut, pour que l’histoire de sécurité du publisher-pin reste visible).