Ogni plugin in sandbox ha un emdash-plugin.jsonc accanto al suo package.json. Viene modificato manualmente e contiene l’identità del plugin, il suo contratto di fiducia (capabilities, hosts, storage) e i campi del profilo che il registro mostra. emdash-plugin init ne crea uno; la CLI legge ./emdash-plugin.jsonc automaticamente per build, dev, validate, bundle e publish.
Il file è JSONC: i commenti e le virgole finali sono consentiti.
Il seguente esempio 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]" },
// 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": {}
}
Identità
| Campo | Richiesto | Note |
|---|---|---|
slug | Sì | ID sicuro per URL nello spazio dei nomi del publisher. /^[a-z][a-z0-9_-]*$/, max 64 caratteri. |
publisher | Sì | Il DID o handle del tuo account Atmosphere. Vedi Ancoraggio del publisher. |
version | No | Semver 2.0 senza metadati di build. Solitamente omettilo — vedi sotto. |
slug e publisher insieme formano l’identità del pacchetto. EmDash deriva l’identificatore completo del pacchetto automaticamente da essi.
version risiede in package.json
La build riconcilia la version del manifesto con package.json#version:
- Entrambi impostati e uguali → va bene.
- Entrambi impostati e diversi → errore critico.
- Uno impostato → quel valore vince.
- Nessuno impostato → errore critico.
Il pattern raccomandato per un plugin distribuito tramite npm è di omettere version dal manifesto e lasciare che package.json sia l’unica fonte di verità (i tuoi strumenti di release la incrementano già lì). I plugin solo per registro senza package.json devono impostare version nel manifesto — non c’è altro posto per essa.
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 diversi. Impostare entrambi è un errore. |
security / securityContacts | Sì | Uno dei due. Ogni contatto necessita almeno di email o url. securityContacts: [...] (≤ 8) per diversi. Impostare entrambi è un errore. |
name | No | Nome visualizzato. Per impostazione predefinita è lo slug. |
description | No | Mantienilo breve (circa 140 caratteri). I valori lunghi potrebbero essere troncati negli elenchi. |
keywords | No | ≤ 5 voci. |
repo | No | URL https:// del repository sorgente. |
Usa la forma singolare author / security a meno che tu non abbia davvero multipli — è il caso comune e lo scaffold la emette.
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 privilegi aggiuntivi può ometterli completamente.
{
"capabilities": ["network:request", "content:read"],
"allowedHosts": ["api.example.com", "*.cdn.example.com"],
"storage": {
"events": { "indexes": ["timestamp"] },
"submissions": { "indexes": ["email"], "uniqueIndexes": ["token"] }
}
}
Capabilities
I nomi riconosciuti:
| Capability | Concede |
|---|---|
content:read / content:write | Leggere / modificare i contenuti del sito tramite ctx. |
media:read / media:write | Leggere / scrivere media. |
users:read | Leggere i record degli utenti. |
email:send | Inviare email tramite ctx. |
network:request | HTTP in uscita tramite ctx.http, limitato a allowedHosts. |
network:request:unrestricted | HTTP in uscita a 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 inter-campo che la CLI impone (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, usanetwork:request:unrestrictedinvece.network:request:unrestrictedrichiede cheallowedHostssia vuoto — la capability senza restrizioni concede già ogni host, quindi un elenco la contraddirebbe.
I pattern host sono nomi host semplici (nessuno schema, percorso o spazio bianco). Un *. iniziale consente sottodomini: *.cdn.example.com.
Storage
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 anch’essi interrogabili — non elencarli anche in indexes.
"storage": {
"events": { "indexes": ["timestamp", ["collection", "timestamp"]] }
}
Superficie di amministrazione
Opzionale. I plugin in sandbox rendono pagine di amministrazione e widget della dashboard tramite Block Kit; il manifesto dichiara solo dove appaiono. Ometti completamente la chiave admin se il plugin non ha UI di amministrazione.
"admin": {
"pages": [{ "path": "/gallery", "label": "Gallery", "icon": "image" }],
"widgets": [{ "id": "recent-uploads", "title": "Recent uploads", "size": "half" }]
}
Un plugin che dichiara admin.pages o admin.widgets deve anche servire una route admin in src/plugin.ts che rende il contenuto Block Kit — lo schema non può imporre questo (i nomi delle route sono sondati dal codice sorgente, non dal manifesto), ma il runtime lo verifica.
Ancoraggio del publisher
publisher ancora l’identità di pubblicazione così non puoi 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 uno scaffold con emdash-plugin init e l’hai lasciato vuoto, la CLI scrive il DID della sessione attiva di nuovo nel manifesto.
Il seguente esempio 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 ancorato 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 publisher: modifica
publishernel manifesto.
Validare senza pubblicare
emdash-plugin validate # ./emdash-plugin.jsonc
emdash-plugin validate path/ # una directory specifica
Controllo schema offline con diagnostica in stile tsc file:line:column, incluse le regole inter-campo. Adatto per un hook pre-commit o passo CI. Le chiavi duplicate e chiavi sconosciute sono errori (la modalità strict cattura errori di battitura "licens").
I flag 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 completamente il manifesto (e avverte se ne esiste uno al percorso predefinito, così la storia di sicurezza dell’ancoraggio del publisher rimane visibile).