Todo plugin sandboxed tem um emdash-plugin.jsonc ao lado do seu package.json. Ele é editado manualmente e contém a identidade do plugin, seu contrato de confiança (capacidades, hosts, armazenamento) e os campos de perfil que o registro exibe. emdash-plugin init gera um scaffold; 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": "[email protected]" },
// Perfil opcional
"name": "Gallery",
"description": "Bloco de galeria de imagens para EmDash.",
"keywords": ["gallery", "images"],
"repo": "https://github.com/example/plugin-gallery",
// Contrato de confiança
"capabilities": ["content:read"],
"allowedHosts": [],
"storage": {}
}
Identidade
| Campo | Obrigatório | Notas |
|---|---|---|
slug | Sim | ID seguro para URL no namespace do publicador. /^[a-z][a-z0-9_-]*$/, máx. 64 caracteres. |
publisher | Sim | O DID ou handle da sua conta Atmosphere. Veja Fixação de publicador. |
version | Não | Semver 2.0 sem metadados de build. Normalmente omita — veja abaixo. |
slug e publisher juntos são a identidade do pacote. EmDash deriva automaticamente o identificador completo do pacote a partir deles.
version fica em package.json
O build reconcilia a version do manifesto com package.json#version:
- Ambos definidos e iguais → ok.
- Ambos definidos e diferentes → erro grave.
- Um definido → esse valor prevalece.
- 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 de verdade (seu tooling de release já faz o bump lá). Plugins apenas no registro sem package.json devem definir version no manifesto — não há outro lugar onde colocá-lo.
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 restante é opcional.
| Campo | Obrigatório | Notas |
|---|---|---|
license | Sim | Expressão SPDX ("MIT", "Apache-2.0", "MIT OR Apache-2.0"). Usada na primeira publicação; o perfil existente prevalece 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 emite assim.
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 inteiramente.
{
"capabilities": ["network:request", "content:read"],
"allowedHosts": ["api.example.com", "*.cdn.example.com"],
"storage": {
"events": { "indexes": ["timestamp"] },
"submissions": { "indexes": ["email"], "uniqueIndexes": ["token"] }
}
}
Capacidades
Os nomes reconhecidos:
| Capacidade | Concede |
|---|---|
content:read / content:write | Ler / modificar conteúdo do site via ctx. |
taxonomies:read | Ler definições de taxonomia e termos (somente leitura). |
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 no lugar 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 (somente nativo). |
Duas regras entre campos que a CLI aplica (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 capacidade irrestrita já concede todos os hosts, então uma lista seria contraditória.
Padrões de host são nomes de host puros (sem esquema, caminho ou espaço em branco). Um *. inicial permite subdomínios: *.cdn.example.com.
Armazenamento
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 campo ou arrays compostos; uniqueIndexes também são consultáveis — não os liste adicionalmente em indexes.
"storage": {
"events": { "indexes": ["timestamp", ["collection", "timestamp"]] }
}
Superfície de administração
Opcional. Plugins sandboxed renderizam páginas de administração e widgets do dashboard através do Block Kit; o manifesto apenas declara onde eles aparecem. Omita completamente a chave admin se o plugin não tem UI de administração.
"admin": {
"pages": [{ "path": "/gallery", "label": "Galeria", "icon": "image" }],
"widgets": [{ "id": "recent-uploads", "title": "Uploads recentes", "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 Block Kit — o schema não pode forçar isso (nomes de rota são sondados do código fonte, não do manifesto), mas o runtime verifica.
Fixação de publicador
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 corresponde à sessão ativa, ele permanece como escrito. Se você criou o scaffold com emdash-plugin init e 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 discrepância falha imediatamente com MANIFEST_PUBLISHER_MISMATCH — não há flag de override. Resolva deliberadamente:
- Sessão errada:
emdash-plugin switch <did>, depois publique novamente. - Transferência genuína do plugin para um novo publicador: 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 arquivo:linha:coluna, incluindo as regras entre campos. Adequado para um hook de pre-commit ou passo de CI. Chaves duplicadas e chaves desconhecidas são erros (modo estrito captura erros de digitação como "licens").
Flags da CLI sempre prevalecem
Flags explícitos (--license, --author-name, …) sobrescrevem valores do manifesto quando ambos estão definidos — útil para overrides de CI. --no-manifest pula o manifesto inteiramente (e avisa se existe um no caminho padrão, para que a história de segurança do publisher-pin permaneça visível).