Cada plugin en sandbox tiene un emdash-plugin.jsonc junto a su package.json. Se edita manualmente y contiene la identidad del plugin, su contrato de confianza (capabilities, hosts, storage) y los campos de perfil que muestra el registro. emdash-plugin init crea uno; el CLI lee ./emdash-plugin.jsonc automáticamente para build, dev, validate, bundle y publish.
El archivo es JSONC: se permiten comentarios y comas finales.
El siguiente ejemplo muestra un manifiesto completo para un plugin de galería de imágenes:
{
"$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": {}
}
Identidad
| Campo | Requerido | Notas |
|---|---|---|
slug | Sí | ID segura para URL dentro del namespace del publisher. /^[a-z][a-z0-9_-]*$/, máx. 64 caracteres. |
publisher | Sí | El DID o handle de tu cuenta Atmosphere. Ver Anclaje de publisher. |
version | No | Semver 2.0 sin metadatos de build. Normalmente omítelo — ver abajo. |
slug y publisher juntos forman la identidad del paquete. EmDash deriva el identificador completo del paquete automáticamente de ellos.
version vive en package.json
El build reconcilia la version del manifiesto con package.json#version:
- Ambos establecidos e iguales → correcto.
- Ambos establecidos y diferentes → error grave.
- Uno establecido → ese valor gana.
- Ninguno establecido → error grave.
El patrón recomendado para un plugin distribuido por npm es omitir version del manifiesto y dejar que package.json sea la única fuente de verdad (tus herramientas de release ya la incrementan allí). Los plugins solo de registro sin package.json deben establecer version en el manifiesto — no hay otro lugar para ella.
Perfil
Estos alimentan el listado del registro. license, un autor (author o authors) y un contacto de seguridad (security o securityContacts) son requeridos; el resto es opcional.
| Campo | Requerido | Notas |
|---|---|---|
license | Sí | Expresión SPDX ("MIT", "Apache-2.0", "MIT OR Apache-2.0"). Se usa en la primera publicación; el perfil existente gana en publicaciones posteriores. |
author / authors | Sí | Uno de los dos. author: { name, url?, email? } para un solo autor; authors: [...] (≤ 32) para varios. Establecer ambos es un error. |
security / securityContacts | Sí | Uno de los dos. Cada contacto necesita al menos email o url. securityContacts: [...] (≤ 8) para varios. Establecer ambos es un error. |
name | No | Nombre para mostrar. Por defecto es el slug. |
description | No | Manténlo corto (alrededor de 140 caracteres). Los valores largos pueden truncarse en listas. |
keywords | No | ≤ 5 entradas. |
repo | No | URL https:// del repositorio fuente. |
Usa la forma singular author / security a menos que genuinamente tengas múltiples — es el caso común y el scaffold la emite.
Contrato de confianza
El contrato de confianza es capabilities, allowedHosts y storage. Los tres por defecto están vacíos, por lo que un plugin que no necesita privilegios adicionales puede omitirlos por completo.
{
"capabilities": ["network:request", "content:read"],
"allowedHosts": ["api.example.com", "*.cdn.example.com"],
"storage": {
"events": { "indexes": ["timestamp"] },
"submissions": { "indexes": ["email"], "uniqueIndexes": ["token"] }
}
}
Capabilities
Los nombres reconocidos:
| Capability | Otorga |
|---|---|
content:read / content:write | Leer / mutar contenido del sitio vía ctx. |
media:read / media:write | Leer / escribir medios. |
users:read | Leer registros de usuarios. |
email:send | Enviar email vía ctx. |
network:request | HTTP saliente vía ctx.http, restringido a allowedHosts. |
network:request:unrestricted | HTTP saliente a cualquier host. Se usa en lugar de network:request. |
hooks.email-transport:register | Registrar un hook de transporte de email. |
hooks.email-events:register | Registrar hooks de ciclo de vida de email. |
hooks.page-fragments:register | Registrar un hook page:fragments (solo nativo). |
Dos reglas entre campos que el CLI hace cumplir (la verificación JSON-Schema del editor no — ejecuta emdash-plugin validate):
network:requestrequiere unallowedHostsno vacío. Si el plugin realmente debe alcanzar cualquier host, usanetwork:request:unrestricteden su lugar.network:request:unrestrictedrequiere queallowedHostsesté vacío — la capability sin restricciones ya otorga cada host, por lo que una lista la contradiría.
Los patrones de host son nombres de host simples (sin esquema, ruta o espacios en blanco). Un *. inicial permite subdominios: *.cdn.example.com.
Storage
Un mapa de nombre de colección → configuración de índice. Los nombres de colección siguen la misma regla /^[a-z][a-z0-9_]*$/ (el runtime usa el nombre como sufijo de tabla SQL). Los índices son nombres de campo o arrays compuestos; uniqueIndexes también son consultables — no los listes también en indexes.
"storage": {
"events": { "indexes": ["timestamp", ["collection", "timestamp"]] }
}
Superficie de administración
Opcional. Los plugins en sandbox renderizan páginas de administración y widgets de panel a través de Block Kit; el manifiesto solo declara dónde aparecen. Omite la clave admin por completo si el plugin no tiene UI de administración.
"admin": {
"pages": [{ "path": "/gallery", "label": "Gallery", "icon": "image" }],
"widgets": [{ "id": "recent-uploads", "title": "Recent uploads", "size": "half" }]
}
Un plugin que declara admin.pages o admin.widgets también debe servir una ruta admin en src/plugin.ts que renderice el contenido de Block Kit — el schema no puede hacer cumplir eso (los nombres de ruta se sondean desde el código fuente, no del manifiesto), pero el runtime lo verifica.
Anclaje de publisher
publisher ancla la identidad de publicación para que no puedas publicar accidentalmente un plugin bajo la cuenta incorrecta.
En tu primera publicación exitosa, si el publisher del manifiesto coincide con la sesión activa, permanece como está escrito. Si creaste un scaffold con emdash-plugin init y lo dejaste en blanco, el CLI escribe el DID de la sesión activa de vuelta en el manifiesto.
El siguiente ejemplo muestra la línea que escribe el CLI, con el handle resuelto agregado como comentario para legibilidad:
"publisher": "did:plc:abc123def456", // jane.example.com
En cada publicación subsiguiente, el CLI resuelve la sesión activa y el publisher anclado a DIDs y los compara. Una discrepancia falla inmediatamente con MANIFEST_PUBLISHER_MISMATCH — no hay flag de anulación. Resuélvelo deliberadamente:
- Sesión incorrecta:
emdash-plugin switch <did>, luego publica nuevamente. - Transferencia genuina del plugin a un nuevo publisher: edita
publisheren el manifiesto.
Validar sin publicar
emdash-plugin validate # ./emdash-plugin.jsonc
emdash-plugin validate path/ # un directorio específico
Verificación de schema offline con diagnósticos estilo tsc file:line:column, incluyendo las reglas entre campos. Adecuado para un hook pre-commit o paso de CI. Las claves duplicadas y claves desconocidas son errores (el modo estricto atrapa errores tipográficos de "licens").
Los flags del CLI aún ganan
Los flags explícitos (--license, --author-name, …) anulan los valores del manifiesto cuando ambos están establecidos — útil para anulaciones de CI. --no-manifest omite el manifiesto por completo (y advierte si existe uno en la ruta predeterminada, para que la historia de seguridad del anclaje de publisher permanezca visible).