Ogni plugin sandboxed ha un emdash-plugin.jsonc accanto al suo package.json. Viene modificato manualmente e contiene l’identità del plugin, il suo contratto di fiducia (capacità, host, archiviazione) e i campi del profilo che il registro mostra. emdash-plugin init ne genera uno; la CLI legge ./emdash-plugin.jsonc automaticamente per build, dev, validate, bundle e publish.
Il file è in JSONC: commenti e virgole finali sono consentiti.
L’esempio seguente mostra un manifesto completo per un plugin galleria immagini:
{
"$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]" },
// Profilo opzionale
"name": "Gallery",
"description": "Blocco galleria immagini per EmDash.",
"keywords": ["gallery", "images"],
"repo": "https://github.com/example/plugin-gallery",
// Contratto di fiducia
"capabilities": ["content:read"],
"allowedHosts": [],
"storage": {}
}
Identità
| Campo | Richiesto | Note |
|---|---|---|
slug | Sì | ID sicuro per URL nello spazio dei nomi dell’editore. /^[a-z][a-z0-9_-]*$/, max 64 caratteri. |
publisher | Sì | Il DID o l’handle del tuo account Atmosphere. Vedi Blocco dell’editore. |
version | No | Semver 2.0 senza metadati di build. Di solito ometti — vedi sotto. |
slug e publisher insieme sono l’identità del pacchetto. EmDash ne ricava automaticamente l’identificatore completo.
version va in package.json
Il build riconcilia la version del manifesto con package.json#version:
- Entrambi impostati e uguali → ok.
- Entrambi impostati e diversi → errore grave.
- Uno impostato → quel valore vince.
- Nessuno impostato → errore grave.
Il pattern raccomandato per un plugin distribuito via npm è omettere version dal manifesto e lasciare che package.json sia l’unica fonte di verità (il tuo tooling di rilascio lo incrementa già lì). I plugin solo nel registro senza package.json devono impostare version nel manifesto — non c’è altro posto dove metterlo.
Profilo
Questi alimentano l’elenco del registro. license, un autore (author o authors) e un contatto di sicurezza (security o securityContacts) sono richiesti; il resto è opzionale.
| Campo | Richiesto | Note |
|---|---|---|
license | Sì | Espressione SPDX ("MIT", "Apache-2.0", "MIT OR Apache-2.0"). Usata alla prima pubblicazione; il profilo esistente vince nelle pubblicazioni successive. |
author / authors | Sì | Uno dei due. author: { name, url?, email? } per un singolo autore; authors: [...] (≤ 32) per più autori. Impostarli entrambi è un errore. |
security / securityContacts | Sì | Uno dei due. Ogni contatto necessita almeno di email o url. securityContacts: [...] (≤ 8) per più contatti. Impostarli entrambi è un errore. |
name | No | Nome visualizzato. Per impostazione predefinita lo slug. |
description | No | Mantienilo breve (circa 140 caratteri). I valori lunghi possono essere troncati nelle liste. |
keywords | No | ≤ 5 voci. |
repo | No | URL https:// del repository sorgente. |
Usa la forma singolare author / security a meno che tu non abbia realmente più di uno — è il caso comune e lo scaffold lo emette così.
Contratto di fiducia
Il contratto di fiducia è capabilities, allowedHosts e storage. Tutti e tre sono vuoti per impostazione predefinita, quindi un plugin che non necessita di privilegi aggiuntivi può ometterli interamente.
{
"capabilities": ["network:request", "content:read"],
"allowedHosts": ["api.example.com", "*.cdn.example.com"],
"storage": {
"events": { "indexes": ["timestamp"] },
"submissions": { "indexes": ["email"], "uniqueIndexes": ["token"] }
}
}
Capacità
I nomi riconosciuti:
| Capacità | Concede |
|---|---|
content:read / content:write | Leggere / modificare contenuti del sito tramite ctx. |
taxonomies:read | Leggere definizioni di tassonomie e termini (sola lettura). |
media:read / media:write | Leggere / scrivere media. |
users:read | Leggere record utenti. |
email:send | Inviare email tramite ctx. |
network:request | HTTP in uscita tramite ctx.http, limitato a allowedHosts. |
network:request:unrestricted | HTTP in uscita verso qualsiasi host. Usato al posto di network:request. |
hooks.email-transport:register | Registrare un hook di trasporto email. |
hooks.email-events:register | Registrare hook del ciclo di vita email. |
hooks.page-fragments:register | Registrare un hook page:fragments (solo nativo). |
Due regole tra campi che la CLI applica (il controllo JSON-Schema dell’editor non lo fa — esegui emdash-plugin validate):
network:requestrichiede unallowedHostsnon vuoto. Se il plugin deve davvero raggiungere qualsiasi host, usa invecenetwork:request:unrestricted.network:request:unrestrictedrichiede cheallowedHostssia vuoto — la capacità senza restrizioni concede già tutti gli host, quindi una lista sarebbe contraddittoria.
I pattern degli host sono nomi di host nudi (nessuno schema, percorso o spazio bianco). Un *. iniziale consente i sottodomini: *.cdn.example.com.
Archiviazione
Una mappa di nome collezione → configurazione indice. I nomi delle collezioni seguono la stessa regola /^[a-z][a-z0-9_]*$/ (il runtime usa il nome come suffisso della tabella SQL). Gli indici sono nomi di campo o array compositi; uniqueIndexes sono anche interrogabili — non elencarli anche in indexes.
"storage": {
"events": { "indexes": ["timestamp", ["collection", "timestamp"]] }
}
Superficie di amministrazione
Opzionale. I plugin sandboxed renderizzano pagine di amministrazione e widget del dashboard tramite Block Kit; il manifesto dichiara solo dove appaiono. Ometti interamente la chiave admin se il plugin non ha un’interfaccia di amministrazione.
"admin": {
"pages": [{ "path": "/gallery", "label": "Galleria", "icon": "image" }],
"widgets": [{ "id": "recent-uploads", "title": "Caricamenti recenti", "size": "half" }]
}
Un plugin che dichiara admin.pages o admin.widgets deve anche servire una route admin in src/plugin.ts che renderizza il contenuto Block Kit — lo schema non può forzarlo (i nomi delle route vengono sondati dal codice sorgente, non dal manifesto), ma il runtime lo verifica.
Blocco dell’editore
publisher blocca l’identità di pubblicazione in modo che non si possa accidentalmente pubblicare un plugin sotto l’account sbagliato.
Alla tua prima pubblicazione riuscita, se il publisher del manifesto corrisponde alla sessione attiva, rimane come scritto. Se hai creato lo scaffold con emdash-plugin init e lo hai lasciato vuoto, la CLI scrive il DID della sessione attiva nel manifesto.
L’esempio seguente mostra la riga che la CLI scrive, con l’handle risolto aggiunto come commento per leggibilità:
"publisher": "did:plc:abc123def456", // jane.example.com
Ad ogni pubblicazione successiva, la CLI risolve la sessione attiva e il publisher bloccato in DID e li confronta. Una discrepanza fallisce immediatamente con MANIFEST_PUBLISHER_MISMATCH — non c’è flag di override. Risolvilo deliberatamente:
- Sessione sbagliata:
emdash-plugin switch <did>, poi pubblica di nuovo. - Trasferimento genuino del plugin a un nuovo editore: modifica
publishernel manifesto.
Validare senza pubblicare
emdash-plugin validate # ./emdash-plugin.jsonc
emdash-plugin validate path/ # una directory specifica
Controllo schema offline con diagnostici stile tsc file:riga:colonna, incluse le regole tra campi. Adatto per un hook pre-commit o step CI. Le chiavi duplicate e le chiavi sconosciute sono errori (la modalità strict cattura errori di battitura come "licens").
I flag della CLI vincono comunque
I flag espliciti (--license, --author-name, …) sovrascrivono i valori del manifesto quando entrambi sono impostati — utile per override CI. --no-manifest salta interamente il manifesto (e avvisa se ne esiste uno al percorso predefinito, in modo che la storia di sicurezza del publisher-pin rimanga visibile).