Cada plugin em sandbox tem um emdash-plugin.jsonc ao lado do seu package.json. É editado manualmente e contém a identidade do plugin, seu contrato de confiança (capabilities, hosts, storage) e os campos de perfil que o registro exibe. emdash-plugin init cria um; a CLI lê ./emdash-plugin.jsonc automaticamente para build, dev, validate, bundle e publish.
O arquivo é JSONC: comentários e vírgulas finais são permitidos.
O exemplo a seguir mostra um manifesto completo para um plugin de galeria de imagens:
{
"$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": "security@example.com" },
// 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": {}
}
Identidade
| Campo | Obrigatório | Notas |
|---|---|---|
slug | Sim | ID seguro para URL dentro do namespace do publisher. /^[a-z][a-z0-9_-]*$/, máx. 64 caracteres. |
publisher | Sim | O DID ou handle da sua conta Atmosphere. Veja Fixação de publisher. |
version | Não | Semver 2.0 sem metadados de build. Geralmente omita — veja abaixo. |
slug e publisher juntos formam a identidade do pacote. EmDash deriva o identificador completo do pacote deles automaticamente.
version vive em package.json
O build reconcilia o version do manifesto com package.json#version:
- Ambos definidos e iguais → ok.
- Ambos definidos e diferentes → erro grave.
- Um definido → esse valor vence.
- Nenhum definido → erro grave.
O padrão recomendado para um plugin distribuído via npm é omitir version do manifesto e deixar package.json ser a única fonte da verdade (suas ferramentas de release já a incrementam lá). Plugins somente de registro sem package.json devem definir version no manifesto — não há outro lugar para ela.
Perfil
Estes alimentam a listagem do registro. license, um autor (author ou authors) e um contato de segurança (security ou securityContacts) são obrigatórios; o resto é opcional.
| Campo | Obrigatório | Notas |
|---|---|---|
license | Sim | Expressão SPDX ("MIT", "Apache-2.0", "MIT OR Apache-2.0"). Usado na primeira publicação; o perfil existente vence em publicações posteriores. |
author / authors | Sim | Um dos dois. author: { name, url?, email? } para um único autor; authors: [...] (≤ 32) para vários. Definir ambos é um erro. |
security / securityContacts | Sim | Um dos dois. Cada contato precisa de pelo menos email ou url. securityContacts: [...] (≤ 8) para vários. Definir ambos é um erro. |
name | Não | Nome de exibição. Padrão é o slug. |
description | Não | Mantenha curto (cerca de 140 caracteres). Valores longos podem ser truncados em listas. |
keywords | Não | ≤ 5 entradas. |
repo | Não | URL https:// do repositório fonte. |
Use a forma singular author / security a menos que você genuinamente tenha múltiplos — é o caso comum e o scaffold a emite.
Contrato de confiança
O contrato de confiança é capabilities, allowedHosts e storage. Todos os três são vazios por padrão, então um plugin que não precisa de privilégios extras pode omiti-los completamente.
{
"capabilities": ["network:request", "content:read"],
"allowedHosts": ["api.example.com", "*.cdn.example.com"],
"storage": {
"events": { "indexes": ["timestamp"] },
"submissions": { "indexes": ["email"], "uniqueIndexes": ["token"] }
}
}
Capabilities
Os nomes reconhecidos:
| Capability | Concede |
|---|---|
content:read / content:write | Ler / mutar conteúdo do site via ctx. |
media:read / media:write | Ler / escrever mídia. |
users:read | Ler registros de usuários. |
email:send | Enviar email via ctx. |
network:request | HTTP de saída via ctx.http, restrito a allowedHosts. |
network:request:unrestricted | HTTP de saída para qualquer host. Usado em vez de network:request. |
hooks.email-transport:register | Registrar um hook de transporte de email. |
hooks.email-events:register | Registrar hooks de ciclo de vida de email. |
hooks.page-fragments:register | Registrar um hook page:fragments (apenas nativo). |
Duas regras entre campos que a CLI impõe (a verificação JSON-Schema do editor não — execute emdash-plugin validate):
network:requestrequer umallowedHostsnão vazio. Se o plugin realmente precisa alcançar qualquer host, usenetwork:request:unrestrictedem vez disso.network:request:unrestrictedrequer queallowedHostsesteja vazio — a capability irrestrita já concede todos os hosts, então uma lista a contradiz.
Padrões de host são nomes de host simples (sem esquema, caminho ou espaço em branco). Um *. inicial permite subdomínios: *.cdn.example.com.
Storage
Um mapa de nome de coleção → configuração de índice. Nomes de coleção seguem a mesma regra /^[a-z][a-z0-9_]*$/ (o runtime usa o nome como sufixo de tabela SQL). Índices são nomes de campos ou arrays compostos; uniqueIndexes também são consultáveis — não os liste também em indexes.
"storage": {
"events": { "indexes": ["timestamp", ["collection", "timestamp"]] }
}
Superfície de administração
Opcional. Plugins em sandbox renderizam páginas de administração e widgets de painel através do Block Kit; o manifesto apenas declara onde eles aparecem. Omita a chave admin completamente se o plugin não tiver UI de administração.
"admin": {
"pages": [{ "path": "/gallery", "label": "Gallery", "icon": "image" }],
"widgets": [{ "id": "recent-uploads", "title": "Recent uploads", "size": "half" }]
}
Um plugin que declara admin.pages ou admin.widgets também deve servir uma rota admin em src/plugin.ts que renderiza o conteúdo do Block Kit — o schema não pode impor isso (nomes de rotas são sondados do fonte, não do manifesto), mas o runtime verifica.
Fixação de publisher
publisher fixa a identidade de publicação para que você não possa acidentalmente publicar um plugin sob a conta errada.
Na sua primeira publicação bem-sucedida, se o publisher do manifesto corresponder à sessão ativa, ele permanece como escrito. Se você fez scaffold com emdash-plugin init e o deixou em branco, a CLI escreve o DID da sessão ativa de volta no manifesto.
O exemplo a seguir mostra a linha que a CLI escreve, com o handle resolvido adicionado como comentário para legibilidade:
"publisher": "did:plc:abc123def456", // jane.example.com
Em cada publicação subsequente, a CLI resolve a sessão ativa e o publisher fixado para DIDs e os compara. Uma incompatibilidade falha imediatamente com MANIFEST_PUBLISHER_MISMATCH — não há flag de substituição. Resolva deliberadamente:
- Sessão errada:
emdash-plugin switch <did>, depois publique novamente. - Transferência genuína do plugin para um novo publisher: edite
publisherno manifesto.
Validar sem publicar
emdash-plugin validate # ./emdash-plugin.jsonc
emdash-plugin validate path/ # um diretório específico
Verificação de schema offline com diagnósticos estilo tsc file:line:column, incluindo as regras entre campos. Adequado para um hook de pré-commit ou etapa de CI. Chaves duplicadas e chaves desconhecidas são erros (o modo estrito captura erros de digitação "licens").
Flags da CLI ainda vencem
Flags explícitos (--license, --author-name, …) substituem valores do manifesto quando ambos estão definidos — útil para substituições de CI. --no-manifest pula o manifesto completamente (e avisa se um existe no caminho padrão, para que a história de segurança de fixação do publisher permaneça visível).