Chaque plugin en sandbox possède un emdash-plugin.jsonc à côté de son package.json. Il est édité manuellement et contient l’identité du plugin, son contrat de confiance (capabilities, hosts, storage) et les champs de profil que le registre affiche. emdash-plugin init en crée un ; le 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]" },
// Optional profile
"name": "Gallery",
"description": "Image gallery block for EmDash.",
"keywords": ["gallery", "images"],
"repo": "https://github.com/example/plugin-gallery",
// Trust contract
"capabilities": ["content:read"],
"allowedHosts": [],
"storage": {}
}
Identité
| Champ | Requis | Notes |
|---|---|---|
slug | Oui | ID sécurisé pour URL dans l’espace de noms du publisher. /^[a-z][a-z0-9_-]*$/, max 64 caractères. |
publisher | Oui | Le DID ou handle de votre compte Atmosphere. Voir Épinglage de publisher. |
version | Non | Semver 2.0 sans métadonnées de build. Habituellement à omettre — voir ci-dessous. |
slug et publisher ensemble forment l’identité du package. EmDash dérive l’identifiant complet du package automatiquement à partir d’eux.
version se trouve dans package.json
Le build réconcilie la version du manifeste avec package.json#version :
- Les deux définis et égaux → correct.
- Les deux définis et différents → erreur critique.
- Un seul défini → cette valeur l’emporte.
- Aucun défini → erreur critique.
Le modèle recommandé pour un plugin distribué via npm est d’omettre version du manifeste et de laisser package.json être la seule source de vérité (vos outils de release l’incrémentent déjà là-bas). Les plugins uniquement pour le registre sans package.json doivent définir version dans le manifeste — il n’y a pas d’autre endroit pour elle.
Profil
Ceux-ci alimentent la liste 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é à 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 | Gardez-le court (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 véritablement plusieurs — c’est le cas courant et le scaffold l’émet.
Contrat de confiance
Le contrat de confiance est capabilities, allowedHosts et storage. Tous trois sont vides par défaut, donc un plugin qui ne nécessite pas 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"] }
}
}
Capabilities
Les noms reconnus :
| Capability | Accorde |
|---|---|
content:read / content:write | Lire / muter le contenu du site via ctx. |
media:read / media:write | Lire / écrire les médias. |
users:read | Lire les enregistrements utilisateurs. |
email:send | Envoyer un email 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 d’email. |
hooks.email-events:register | Enregistrer des hooks de cycle de vie d’email. |
hooks.page-fragments:register | Enregistrer un hook page:fragments (natif uniquement). |
Deux règles inter-champs que le 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, utiliseznetwork:request:unrestrictedà la place.network:request:unrestrictednécessite queallowedHostssoit vide — la capability sans restriction accorde déjà tous les hôtes, donc une liste la contredirait.
Les modèles d’hôtes sont des noms d’hôtes simples (pas de schéma, chemin ou espace blanc). Un *. initial autorise les sous-domaines : *.cdn.example.com.
Storage
Une carte de 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 champs ou des tableaux composites ; uniqueIndexes sont également interrogeables — ne les listez pas aussi dans indexes.
"storage": {
"events": { "indexes": ["timestamp", ["collection", "timestamp"]] }
}
Surface d’administration
Optionnel. Les plugins en sandbox rendent les pages d’administration et les widgets de tableau de bord via Block Kit ; le manifeste déclare seulement où ils apparaissent. Omettez entièrement la clé admin si le plugin n’a pas d’UI d’administration.
"admin": {
"pages": [{ "path": "/gallery", "label": "Gallery", "icon": "image" }],
"widgets": [{ "id": "recent-uploads", "title": "Recent uploads", "size": "half" }]
}
Un plugin qui déclare admin.pages ou admin.widgets doit également servir une route admin dans src/plugin.ts qui rend le contenu Block Kit — le schema ne peut pas l’imposer (les noms de routes sont sondés depuis la source, pas depuis le manifeste), mais le runtime le vérifie.
Épinglage de publisher
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éé un scaffold avec emdash-plugin init et l’avez laissé vide, le CLI réécrit le DID de la session active dans le manifeste.
L’exemple suivant montre la ligne que le CLI écrit, avec le handle résolu ajouté en commentaire pour la lisibilité :
"publisher": "did:plc:abc123def456", // jane.example.com
À chaque publication ultérieure, le CLI résout la session active et le publisher épinglé en DIDs et les compare. Une non-correspondance échoue immédiatement avec MANIFEST_PUBLISHER_MISMATCH — il n’y a pas de flag de remplacement. Résolvez-le délibérément :
- Mauvaise session :
emdash-plugin switch <did>, puis publiez à nouveau. - Transfert véritable du plugin à un nouveau publisher : modifiez
publisherdans le manifeste.
Valider sans publier
emdash-plugin validate # ./emdash-plugin.jsonc
emdash-plugin validate path/ # un répertoire spécifique
Vérification de schema hors ligne avec des diagnostics de style tsc file:line:column, incluant les règles inter-champs. Adapté à un hook pre-commit ou une étape CI. Les clés dupliquées et clés inconnues sont des erreurs (le mode strict attrape les fautes de frappe "licens").
Les flags CLI l’emportent toujours
Les flags explicites (--license, --author-name, …) remplacent les valeurs du manifeste lorsque les deux sont définis — utile pour les remplacements CI. --no-manifest ignore entièrement le manifeste (et avertit si un existe au chemin par défaut, de sorte que l’histoire de sécurité de l’épinglage du publisher reste visible).