Cada plugin sandboxed tiene un emdash-plugin.jsonc junto a su package.json. Se edita manualmente y contiene la identidad del plugin, su contrato de confianza (capacidades, hosts, almacenamiento) y los campos de perfil que muestra el registro. emdash-plugin init genera uno; la 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]" },
// Perfil opcional
"name": "Gallery",
"description": "Bloque de galería de imágenes para EmDash.",
"keywords": ["gallery", "images"],
"repo": "https://github.com/example/plugin-gallery",
// Contrato de confianza
"capabilities": ["content:read"],
"allowedHosts": [],
"storage": {}
}
Identidad
| Campo | Requerido | Notas |
|---|---|---|
slug | Sí | ID seguro para URL dentro del espacio de nombres del publicador. /^[a-z][a-z0-9_-]*$/, máx. 64 caracteres. |
publisher | Sí | La DID o handle de tu cuenta Atmosphere. Ver Fijación de publicador. |
version | No | Semver 2.0 sin metadatos de build. Normalmente omítelo — ver abajo. |
slug y publisher juntos son la identidad del paquete. EmDash deriva automáticamente el identificador completo del paquete a partir de ellos.
version va en package.json
El build reconcilia la version del manifiesto con package.json#version:
- Ambos establecidos e iguales → bien.
- 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 (tu tooling de release ya lo incrementa allí). Los plugins solo de registro sin package.json deben establecer version en el manifiesto — no hay otro lugar donde ponerlo.
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 son opcionales.
| 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 | Mantenlo 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 lo genera así.
Contrato de confianza
El contrato de confianza es capabilities, allowedHosts y storage. Los tres son vacíos por defecto, así 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"] }
}
}
Capacidades
Los nombres reconocidos:
| Capacidad | Otorga |
|---|---|
content:read / content:write | Leer / modificar contenido del sitio vía ctx. |
taxonomies:read | Leer definiciones de taxonomías y términos (solo lectura). |
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 la CLI aplica (la verificación JSON-Schema del editor no lo hace — 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 capacidad sin restricciones ya otorga todos los hosts, así que una lista sería contradictoria.
Los patrones de host son nombres de host desnudos (sin esquema, ruta ni espacios en blanco). Un *. inicial permite subdominios: *.cdn.example.com.
Almacenamiento
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 adicionalmente en indexes.
"storage": {
"events": { "indexes": ["timestamp", ["collection", "timestamp"]] }
}
Superficie de administración
Opcional. Los plugins sandboxed renderizan páginas de administración y widgets de dashboard a través de Block Kit; el manifiesto solo declara dónde aparecen. Omite completamente la clave admin si el plugin no tiene UI de administración.
"admin": {
"pages": [{ "path": "/gallery", "label": "Galería", "icon": "image" }],
"widgets": [{ "id": "recent-uploads", "title": "Subidas recientes", "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 Block Kit — el schema no puede forzar eso (los nombres de ruta se prueban desde el código fuente, no desde el manifiesto), pero el runtime lo verifica.
Fijación de publicador
publisher fija 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, se queda como está escrito. Si creaste el scaffold con emdash-plugin init y lo dejaste en blanco, la CLI escribe la DID de la sesión activa de vuelta al manifiesto.
El siguiente ejemplo muestra la línea que la CLI escribe, con el handle resuelto añadido como comentario para legibilidad:
"publisher": "did:plc:abc123def456", // jane.example.com
En cada publicación subsiguiente, la CLI resuelve la sesión activa y el publisher fijado a DIDs y los compara. Una discrepancia falla inmediatamente con MANIFEST_PUBLISHER_MISMATCH — no hay flag de override. Resuélvelo deliberadamente:
- Sesión incorrecta:
emdash-plugin switch <did>, luego publica de nuevo. - Transferencia genuina del plugin a un nuevo publicador: 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 archivo:línea:columna, incluyendo las reglas entre campos. Adecuado para un hook de pre-commit o paso de CI. Las claves duplicadas y las claves desconocidas son errores (el modo estricto detecta errores tipográficos como "licens").
Los flags de CLI siempre ganan
Los flags explícitos (--license, --author-name, …) sobrescriben los valores del manifiesto cuando ambos están establecidos — útil para overrides 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 publisher-pin permanezca visible).