I plugin sandboxed sono isolati per impostazione predefinita. Per fare qualsiasi cosa oltre a leggere e scrivere il proprio KV e storage, un plugin deve dichiarare una capability nel suo manifest. Il bridge della sandbox regola ogni API fornita dall’host in base a quelle dichiarazioni: un plugin che non ha dichiarato content:read non ottiene un ctx.content, e uno che non ha dichiarato network:request non ottiene un ctx.http.
Questa pagina spiega cosa concede ogni capability, come la sandbox le applica e cosa non è applicabile.
Dichiarare le capability
Le capability vivono in emdash-plugin.jsonc, insieme a slug e al resto del contratto di fiducia:
{
"slug": "plugin-hello",
// ...identity + profile...
"capabilities": ["content:read", "network:request"],
"allowedHosts": ["api.example.com"]
}
Dichiara solo ciò di cui il plugin ha effettivamente bisogno. Il registry mostra queste capability agli operatori del sito prima dell’installazione, quindi ogni dichiarazione extra chiede loro di approvare un accesso che il plugin non usa.
Riferimento delle capability
| Capability | Concede l’accesso a |
|---|---|
content:read | ctx.content.get(), ctx.content.list(), ctx.content.getTranslations(), ctx.content.getPublicUrl() |
content:revisions:read | ctx.content.listRevisions(), ctx.content.getRevision() (implica content:read) |
content:write | ctx.content.create(), ctx.content.update(), ctx.content.delete() (implica content:read) |
content:publish | Operazioni versionate di publish, unpublish, schedule e unschedule (implica content:read) |
content:restore | Leggere e ripristinare contenuti nel cestino |
comments:read | ctx.comments.get(), ctx.comments.list(), ctx.comments.count() e dati personali dei commenti |
comments:moderate | ctx.comments.setStatus() con controllo di concorrenza sullo stato atteso (implica comments:read) |
schema:read | ctx.schema.listCollections(), ctx.schema.getCollection() |
hooks.content-policy:register | Hook di policy content:beforePublish, content:beforeSchedule e content:beforeUnpublish |
taxonomies:read | ctx.taxonomies.getAll(), ctx.taxonomies.getTerms(), ctx.taxonomies.getEntryTerms() |
taxonomies:write | ctx.taxonomies.createTerm(), ctx.taxonomies.addEntryTerms(), ctx.taxonomies.removeEntryTerms() (implica taxonomies:read) |
redirects:read | ctx.redirects.list(), ctx.redirects.get() |
redirects:write | ctx.redirects.create(), ctx.redirects.update(), ctx.redirects.delete() (implica redirects:read) |
media:read | ctx.media.get(), ctx.media.list() |
media:bytes:read | ctx.media.readBytes() per media pronti, con una risposta bufferizzata limitata |
media:metadata:write | ctx.media.updateMetadata() per testo alternativo, didascalie e punti focali |
media:write | ctx.media.getUploadUrl(), ctx.media.upload(), ctx.media.delete() (implica media:read) |
network:request | ctx.http.fetch() — limitato a allowedHosts |
network:request:unrestricted | ctx.http.fetch() senza restrizione di host (solo per URL configurati dall’utente) |
users:read | ctx.users.get(), ctx.users.getByEmail(), ctx.users.list() |
email:send | ctx.email.send() (richiede un plugin provider di email configurato) |
hooks.email-transport:register | Consente di registrare l’hook esclusivo email:deliver (provider di trasporto) |
hooks.email-events:register | Consente di registrare gli hook email:beforeSend / email:afterSend |
hooks.page-fragments:register | Consente di registrare l’hook page:fragments (solo plugin nativi) |
Le seguenti regole influenzano quali capability servono a un plugin:
- Implicazioni.
content:write,content:revisions:readecontent:publishimplicano automaticamentecontent:read;comments:moderateimplicacomments:read;taxonomies:writeimplicataxonomies:read;media:writeimplicamedia:read;redirects:writeimplicaredirects:read;network:request:unrestrictedimplicanetwork:request. Non serve elencare entrambe. - Le autorità media sono separate.
media:read,media:bytes:reademedia:metadata:writenon si implicano a vicenda. Dichiara ogni operazione usata dal plugin. La capability esistentemedia:writecontinua a implicaremedia:readper compatibilità. - Le tassonomie sono separate dal contenuto. Le capability di tassonomia non concedono
content:readnécontent:write. Dichiara la capability di contenuto corrispondente se il plugin legge o modifica anche i campi delle voci. - La policy di pubblicazione è separata dall’accesso al contenuto.
hooks.content-policy:registerconsente a un plugin di ispezionare e rifiutare cambiamenti di stato di pubblicazione tramite eventi di hook di policy. Non forniscectx.contentné concede azioni di modifica o pubblicazione del contenuto. network:request:unrestrictedesiste per URL configurati dall’utente. Un plugin webhook in cui l’operatore digita l’URL di destinazione deve raggiungere host assenti dal manifest. I plugin che chiamano sempre API note devono usarenetwork:request+allowedHosts.email:sendè regolato dalla configurazione, non solo dalla capability. Un plugin può dichiarareemail:send, mactx.emailsarà popolato solo se qualche altro plugin ha registrato un trasportoemail:deliver.
content:read restituisce un’identità di voce sicura, incluso l’ID autore, il gruppo di traduzione, i puntatori di revisione e la versione di riga. Usa getTranslations() per scoprire i sibling di locale e getPublicUrl() per risolvere una route pubblicata con le regole di locale e trailing slash del sito. getPublicUrl() restituisce null per bozze, collection non instradabili, slug mancanti e locale che il sito non serve. Non restituisce mai un URL di anteprima.
Gli snapshot di revisione possono contenere valori di campo che un amministratore ha poi rimosso. Dichiara content:revisions:read solo quando il plugin ha bisogno della cronologia conservata. I risultati di revisione omettono l’identità dell’autore della revisione.
schema:read espone definizioni di collection e campi senza ID di database, timestamp, metadati di migrazione o tipi di colonna SQL. Le collection nascoste restano visibili perché hidden controlla la navigazione di amministrazione piuttosto che l’accesso ai dati.
Creare e tradurre contenuti
ctx.content.create() accetta un terzo argomento opzionale per la locale della nuova voce:
const post = await ctx.content.create(
"posts",
{ title: "繁體中文" },
{ locale: "zh-tw" },
);
La corrispondenza della locale non distingue maiuscole/minuscole e memorizza la capitalizzazione dalla configurazione della locale del sito, così zh-tw diventa zh-TW quando quella è la forma configurata. Una locale esplicita malformata genera sempre un errore; quando i18n è configurato, una locale esplicita fuori dall’elenco configurato genera anch’essa un errore. Quando l’opzione è omessa, EmDash usa la locale predefinita configurata del sito; i siti senza configurazione i18n mantengono il predefinito en.
Per aggiungere una locale a una voce esistente, passa il suo ID di database come translationOf:
const translatedPost = await ctx.content.create(
"posts",
{ title: "Bienvenue", sku: "ignored-for-shared-fields" },
{ locale: "fr", translationOf: sourcePost.id },
);
La fonte deve essere una voce attiva nella stessa collection. La nuova voce si unisce al suo gruppo di traduzione, eredita i crediti byline e le assegnazioni di tassonomia e inizia con i valori sorgente per i campi contrassegnati come non traducibili. Un valore fornito per un campo non traducibile non sostituisce il valore sorgente durante la creazione della traduzione. La validazione del contenuto e gli hook di salvataggio passano dallo stesso percorso runtime delle altre create di contenuto. EmDash non rientra nel proprio hook content:afterSave del plugin che crea, e il contenuto creato dall’interno di un hook di salvataggio non esegue di nuovo gli hook di salvataggio.
Ogni gruppo di traduzione può contenere una voce attiva per locale. Creare una seconda voce per lo stesso gruppo e locale genera un errore CONFLICT. Una fonte mancante genera NOT_FOUND, una locale non valida o non configurata genera VALIDATION_ERROR, e un hook di salvataggio può interrompere la create con SAVE_REJECTED.
Cambiare lo stato di pubblicazione
Dichiara content:publish per pubblicare, ritirare, pianificare o annullare la pianificazione di una voce. Ogni azione richiede l’_rev opaco restituito da getVersioned() o dall’azione precedente. EmDash instrada questi metodi attraverso gli stessi hook di policy, promozione delle revisioni, sincronizzazione delle locale, redirect, aggiornamenti dell’uso dei media, invalidazione della cache e after-hook delle azioni REST e MCP.
La seguente route pubblica la bozza corrente solo quando la voce non è cambiata da quando è stata letta:
const current = await ctx.content!.getVersioned!("posts", postId);
if (!current) return { ok: false, error: "NOT_FOUND" };
try {
const published = await ctx.content!.publish!("posts", postId, {
_rev: current._rev,
});
return { ok: true, content: published.item, _rev: published._rev };
} catch (error) {
return { ok: false, error: "PUBLISH_FAILED" };
}
schedule() accetta { scheduledAt, _rev }; gli altri metodi di pubblicazione accettano { _rev }. Questi metodi non accettano un override di publishedAt.
Dichiara content:restore separatamente per leggere e ripristinare le voci nel cestino. getTrashedVersioned() restituisce null per una voce live o mancante. Passa il suo _rev a restore() così un cambiamento concorrente restituisce un conflitto invece di ripristinare uno stato obsoleto.
Creare e assegnare termini di tassonomia
taxonomies:write consente a un plugin di creare termini e applicare delta di assegnazione. Passa ID di riga di termine o ID di gruppo di traduzione. Gli slug di termine non sono accettati perché sono con ambito per tassonomia e locale.
L’esempio seguente crea una categoria figlia e la assegna senza sostituire le altre categorie della voce:
const releaseNotes = await ctx.taxonomies!.createTerm!("category", {
label: "Release notes",
parentId: productUpdatesId,
locale: "en",
});
await ctx.taxonomies!.addEntryTerms!("posts", postId, "category", [releaseNotes.id]);
addEntryTerms() e removeEntryTerms() sono delta di insieme idempotenti. Le aggiunte concorrenti preservano ogni assegnazione. EmDash verifica che la tassonomia sia collegata alla collection, che la voce esista e che ogni termine appartenga alla tassonomia nominata. createTerm() rifiuta parentId quando la tassonomia non è gerarchica invece di ignorarlo. Creare un termine tradotto con translationOf unisce il gruppo di traduzione del termine sorgente; la fonte deve appartenere alla stessa tassonomia e il gruppo può contenere un solo termine per locale.
La creazione di definizioni di tassonomia, il collegamento alle collection, la sostituzione, gli aggiornamenti dei termini e l’eliminazione dei termini non sono disponibili tramite taxonomies:write.
Leggere metadati e byte dei media
media:read restituisce record di media pronti con dimensioni, testo alternativo, didascalia, punto focale, blurhash, colore dominante, ID cartella e un URL di asset autenticato basato sull’ID. I chiamanti autenticati con il permesso media:read possono seguire l’URL; le richieste disconnesse vengono rifiutate prima che la route legga il record media. I metadati non restituiscono la chiave di storage, l’identità dell’autore, l’hash del contenuto né i byte del file. L’hash del contenuto è disponibile solo da readBytes() perché può rivelare se il sito memorizza un file noto.
Dentro un hook o un gestore di route, la seguente chiamata legge al massimo 2 MiB da un elemento media pronto:
const file = await ctx.media!.readBytes!(mediaId, {
maxBytes: 2 * 1024 * 1024,
});
const digest = file.contentHash;
const bytes = file.bytes;
readBytes() bufferizza il risultato. Usa per impostazione predefinita 10 MiB quando maxBytes è omesso e rifiuta valori sopra il massimo host di 16 MiB. EmDash conta i byte mentre consuma lo stream di storage, così una dimensione memorizzata errata non può aggirare il limite richiesto. Media mancanti, in sospeso e non riusciti vengono rifiutati senza rivelare la loro posizione di storage.
Il seguente aggiornamento modifica il testo di accessibilità e il punto focale senza concedere autorità di upload, sostituzione o eliminazione:
const updated = await ctx.media!.updateMetadata!(mediaId, {
alt: "Two people reviewing a printed proof",
focalX: 0.42,
focalY: 0.36,
});
Fornisci entrambe le coordinate focali come numeri da 0 a 1, oppure impostale entrambe a null. Le patch concorrenti su campi di metadati diversi non si sostituiscono a vicenda.
Caricare media
ctx.media.upload() accetta contenuti immagine, video, audio e PDF e genera un errore per qualsiasi altro tipo di contenuto. In un plugin trusted, upload() e getUploadUrl() applicano l’allowlist predefinita di upload media: immagini PNG, JPEG, GIF, WebP e AVIF, qualsiasi tipo video/* o audio/*, e application/pdf. Altri tipi generano un PluginRouteError con stato 415, e un tipo di contenuto malformato ne genera uno con stato 400; un gestore di route può lasciare che entrambi si propaghino come risposta. In un plugin trusted, un file memorizzato da upload() o riservato da getUploadUrl() prende anche un’estensione che corrisponde al tipo di contenuto, qualunque sia l’estensione del nome file; quando il tipo di contenuto non ha un’estensione nota, l’estensione del nome file viene mantenuta solo se appartiene a un tipo di media consentito. Un plugin sandboxed mantiene l’estensione del nome file quando ha da 1 a 10 lettere o cifre.
Gestire i redirect in modo sicuro
redirects:read fornisce l’elenco delle regole paginato per cursore e letture versionate di una singola regola. Aggiungi redirects:write quando il plugin crea, aggiorna o elimina regole. L’accesso in scrittura può cambiare dove vengono inviati i visitatori.
Passa l’_rev restituito da get(), create() o update() invariato quando aggiorni o elimini una regola. EmDash rifiuta una revisione obsoleta così il plugin può rileggere la regola e ricalcolare la modifica invece di sovrascrivere lavoro concorrente.
La revisione traccia la configurazione del redirect. Il conteggio delle visite non rende obsoleta una revisione.
L’esempio seguente aggiorna un redirect solo se non è cambiato dalla lettura:
const current = await ctx.redirects!.get(redirectId);
if (current) {
await ctx.redirects!.update!(redirectId, {
destination: "/guides/current",
_rev: current._rev,
});
}
Le operazioni di create convalidano i pattern di percorso, le regole terminali 410 e 451, le fonti duplicate, i loop su se stessi e i loop multi-hop con le stesse regole dell’API redirect di EmDash. Gli update applicano la convalida dei loop quando cambiano fonte o destinazione. Un update solo di abilitazione può riattivare un loop preesistente, che la pagina Redirects segnala. Il marker auto appartiene ai redirect creati da cambiamenti di contenuto dell’host; l’input del plugin non può impostarlo.
Leggere e moderare i commenti
comments:read concede l’accesso ai commenti non nel cestino. I risultati includono nome e indirizzo email dell’autore, corpo del commento, hash IP pseudonimo, user agent, metadati di moderazione, stato, ID del contenuto di destinazione e timestamp. Escludono l’ID dell’account utente EmDash collegato. Dichiara users:read separatamente quando un plugin deve anche cercare account utente.
list() restituisce prima i commenti più recenti. Accetta filtri status, collection e contentId, un cursore e un limite da 1 a 100. Il limite predefinito è 50. count() accetta gli stessi filtri senza paginazione.
La seguente route approva un commento solo se è ancora in sospeso:
const comment = await ctx.comments!.setStatus!(commentId, "approved", {
expectedStatus: "pending",
});
Se un altro moderatore ha cambiato lo stato dopo che il plugin l’ha letto, setStatus() rifiuta con COMMENT_STATUS_CONFLICT. Rileggi il commento e ricalcola la decisione prima di riprovare. Una richiesta che si sovrappone a una transizione precedente prima che il suo stato sia visibile rifiuta con COMMENT_MODERATION_IN_PROGRESS; attendi che quella transizione finisca, poi leggi il commento corrente prima di riprovare. Una transizione riuscita esegue comment:afterModerate una volta con origin: { source: "plugin", pluginId }. L’approvazione invia la stessa notifica all’autore del core di un’approvazione dell’amministratore. Impostare un commento al suo stato corrente è un no-op e non esegue l’hook né invia un’altra notifica.
Allowlist degli host di rete
I plugin con network:request possono recuperare solo gli host elencati in allowedHosts. Un *. iniziale corrisponde sia al dominio nominato sia ai suoi sottodomini:
"capabilities": ["network:request"],
"allowedHosts": [
"api.example.com", // exact host
"*.cdn.example.com" // cdn.example.com and any subdomain
]
Il bridge controlla l’host dell’URL della richiesta rispetto all’allowlist prima di inoltrare la richiesta. Una richiesta a un host non dichiarato genera un errore nel plugin senza mai lasciare la sandbox.
network:request:unrestricted salta l’allowlist degli host del manifest. Il bridge della sandbox accetta comunque solo HTTP e HTTPS, blocca host interni noti e indirizzi letterali privati, ricontrolla ogni redirect e rimuove le intestazioni di credenziali quando un redirect attraversa le origini. Usa l’accesso non limitato solo quando un operatore fornisce la destinazione a runtime. Per destinazioni fisse, dichiara network:request con host espliciti così la finestra di consenso li nomina.
ctx.http.fetch() bufferizza i body di richiesta e risposta e limita ogni body decodificato a 8 MiB. La Response WHATWG restituita preserva byte binari, testo di stato, intestazioni, URL finale, stato di redirect e comportamento di clone() in entrambi i runner sandbox. Leggi i dati binari con arrayBuffer() o blob().
Cosa applica la sandbox
Quando un sandbox runner è attivo, il runtime applica:
-
Regolazione per capability. La factory PluginContext popola
ctx.content,ctx.comments,ctx.schema,ctx.taxonomies,ctx.redirects,ctx.media,ctx.http,ctx.users,ctx.emailsolo quando è dichiarata la capability corrispondente. Chiamare un metodo su una capability non dichiarata non è possibile: non c’è alcun oggetto lì. -
Ambito di storage e KV. Ogni operazione di storage e KV è limitata all’ID plugin del runtime. Un plugin non può leggere il KV o le collection di storage di un altro plugin e può accedere solo alle collection dichiarate nel suo manifest.
-
Isolamento di rete. Il
fetch()diretto e altri primitivi di rete sono bloccati dal runner. L’unico modo per raggiungere la rete èctx.http.fetch(), che passa dalla convalida dell’host del bridge. -
Nessun binding dell’host. I plugin sandboxed non vedono variabili d’ambiente, filesystem o binding di piattaforma, anche se il worker host li ha. Il runtime del plugin è un isolate pulito solo con il bridge e le capability dichiarate.
-
Limiti di risorse. Il runner Cloudflare usa per impostazione predefinita 50 ms di CPU, 10 subrequest e 30 secondi di wall time per invocazione. Worker Loader applica CPU e subrequest; il runner applica il wall time. Worker Loader ha un tetto di memoria di piattaforma, ma la sua opzione
memoryMbper plugin non è attualmente applicabile. Il runner workerd di Node.js applica solo il predefinito di 30 secondi di wall time; avvisa quando un sito configura limiti di CPU, memoria o subrequest che lo workerd standalone non può applicare. Untimeoutper hook si applica solo quando il plugin in formato sandboxed gira in processo.
Cosa la sandbox non applica
Alcune cose che il sistema di capability non copre e non può coprire:
- Comportamento all’interno di una capability concessa. Un plugin con
content:writepuò modificare qualsiasi contenuto, non solo il proprio. Le capability sono grossolane: dicono «questo plugin può scrivere contenuti», non «questo plugin può scrivere solo i contenuti che ha creato». Un operatore deve valutare il codice e il publisher del plugin prima di concedere quell’accesso. - Lock di modifica delle voci.
ctx.content.update()ectx.content.delete()sono scritture programmatiche. Un editor che detiene il lock di modifica consultivo della voce non le blocca. Coordina le scritture del plugin con gli editor quando entrambi possono aggiornare la stessa voce. - Fiducia dell’operatore su Node.js. Quando il sandbox runner configurato segnala non disponibile (nessun Cloudflare Worker Loader, nessun runner lato Node installato, ecc.), i plugin
sandboxed: []vengono saltati all’avvio. Puoi spostarli inplugins: []per eseguirli in processo, ma allora non c’è isolate V8, non ci sono limiti di risorse e il plugin può chiamarefetch()direttamente o leggere variabili d’ambiente. Trattalo come fiducia a livello nativo. - Canali laterali. Timing, output dei log e dati memorizzati sono visibili a chiunque abbia l’accesso appropriato all’ambiente host. Non usare la sandbox come confine di riservatezza contro l’operatore che la esegue.
Consenso alle capability
Quando un operatore installa un plugin sandboxed dal registry, EmDash mostra una finestra di consenso che elenca le capability dichiarate. Gli aggiornamenti che aggiungono capability — ad esempio, un plugin che prima leggeva solo contenuti e ora vuole fare richieste di rete — appaiono come un diff di capability e richiedono una nuova approvazione prima che la nuova versione abbia effetto.
Dichiarare capability per un possibile uso futuro fa sì che ogni installazione o aggiornamento chieda accesso non necessario. Elenca ciò che usa la versione corrente, poi aggiungi una capability nella versione che inizia a usarla.
Validazione al momento del bundle
emdash-plugin bundle e emdash-plugin publish eseguono controlli aggiuntivi:
- Ogni capability dichiarata deve essere nell’insieme riconosciuto (i refusi fanno fallire la build).
network:requestrichiede unallowedHostsnon vuoto;network:request:unrestrictedrichiede che sia vuoto. Vedi Capabilities and hosts.- Il
backend.jsin bundle non può importare built-in di Node.js (fs,path,child_process, ecc.) — i runtime sandbox non li forniscono.
Vedi the manifest reference per i campi di authoring e Bundling and publishing per i controlli del bundle.