O EmDash pode executar plugins em dois modos: trusted e sandboxed. Esta página explica como cada modo funciona, que proteções oferece e as implicações de segurança para diferentes alvos de deployment.
Modos de execução
| Trusted | Sandboxed | |
|---|---|---|
| Executa em | Processo principal | Isolate V8 isolado (Dynamic Worker Loader) |
| Capabilities | Informativas (não aplicadas) | Aplicadas em tempo de execução |
| Limites de recursos | Nenhum | CPU, memória, subpedidos, tempo de relógio |
| Acesso à rede | Ilimitado | Bloqueado; apenas via ctx.http com lista de hosts permitidos |
| Acesso a dados | Acesso completo à base de dados | Limitado às capabilities declaradas via bridge RPC |
| Disponível em | Todas as plataformas | Apenas Cloudflare Workers |
Modo trusted
Plugins trusted executam no mesmo processo que o site Astro. São carregados a partir de pacotes npm ou ficheiros locais e configurados em astro.config.mjs:
import myPlugin from "@emdash-cms/plugin-analytics";
export default defineConfig({
integrations: [
emdash({
plugins: [myPlugin()],
}),
],
});
Em modo trusted:
- As capabilities são documentação, não enforcement. Um plugin que declara
["read:content"]ainda pode aceder a tudo no processo. O campocapabilitiesinforma os administradores sobre o que o plugin pretende usar. - Sem limites de recursos. CPU, memória e rede não são limitados. Um plugin com problemas pode bloquear todo o pedido.
- Acesso total ao processo. Os plugins partilham o runtime Node.js/Workers com o site Astro. Podem importar qualquer módulo, aceder a variáveis de ambiente e ler/escrever o sistema de ficheiros (em Node.js).
Modo sandboxed (Cloudflare Workers)
Plugins sandboxed executam em isolates V8 isolados fornecidos pela API Dynamic Worker Loader da Cloudflare. Cada plugin tem o seu próprio isolate com limites aplicados.
Para ativar o sandboxing, configure o sandbox runner na config Astro:
export default defineConfig({
integrations: [
emdash({
sandboxRunner: "@emdash-cms/cloudflare/sandbox",
sandboxed: [
{
manifest: seoPluginManifest,
code: seoPluginCode,
},
],
}),
],
});
O que o sandbox impõe
-
Aplicação de capabilities
Se um plugin declara
capabilities: ["read:content"], só pode chamarctx.content.get()ectx.content.list(). Tentarctx.content.create()lança erro de permissão. Isto é aplicado pelo bridge RPC — o plugin não contorna porque não tem acesso direto à base de dados. -
Limites de recursos
Cada invocação (hook ou rota) corre com:
Recurso Predefinição Aplicado por Tempo de CPU 50ms Worker Loader (isolate V8) Subpedidos 10 por invocação Worker Loader (isolate V8) Tempo de relógio 30 segundos Runner EmDash ( Promise.race)Memória ~128MB Teto da plataforma V8 (não configurável por plugin) Exceder CPU ou subpedidos faz o Worker Loader abortar o isolate e lançar exceção. Exceder o tempo de relógio faz o EmDash rejeitar a promise de invocação. A memória é limitada pelo teto V8 mas não é configurável por plugin.
Estes são os valores predefinidos integrados. Limites personalizados podem ser configurados com uma
SandboxRunnerFactoryque passe outros valores viaSandboxOptions.limits. Configuração por site na integração EmDash ainda não está implementada. -
Isolamento de rede
Plugins sandboxed têm
globalOutbound: null— chamadas diretas afetch()são bloqueadas ao nível V8. Devem usarctx.http.fetch(), proxificado pelo bridge. O bridge valida o host de destino contra a listaallowedHostsdo plugin. -
Âmbito do armazenamento
Todas as operações de armazenamento (KV, collections) ficam limitadas ao ID do plugin. Um plugin não lê dados de outro. Acesso a conteúdo e media passa pelo bridge, que verifica capabilities em cada chamada.
-
Restrições de funcionalidade
Algumas funcionalidades só existem em modo trusted:
- API routes — Endpoints REST personalizados (
routes) não estão disponíveis. Plugins sandboxed interagem via páginas admin Block Kit e hooks. - Tipos de bloco Portable Text — Blocos PT precisam de componentes Astro para render no site (
componentsEntry), carregados em build a partir de npm. Plugins sandboxed instalam-se em runtime e não podem incluir componentes. - Páginas admin React personalizadas — Plugins sandboxed usam Block Kit para UI admin em vez de componentes React.
O comando
emdash plugin bundleavisa se um plugin declara estas funcionalidades. - API routes — Endpoints REST personalizados (
Arquitetura
Plugins sandboxed comunicam com o EmDash através de um bridge RPC:
┌─────────────────────┐ RPC ┌──────────────────────┐
│ Plugin Isolate │ ◄──────────► │ PluginBridge │
│ (Worker Loader) │ (binding) │ (WorkerEntrypoint) │
│ │ │ │
│ ctx.kv.get(k) │──────────────│► kvGet(k) │
│ ctx.content.list() │──────────────│► contentList() │
│ ctx.http.fetch(u) │──────────────│► httpFetch(u) │
└─────────────────────┘ └──────────────────────┘
│
▼
┌──────────────┐
│ D1 / R2 │
└──────────────┘
O código do plugin corre num isolate V8. Recebe um objeto ctx em que cada método é um proxy para o bridge. O bridge corre no worker principal EmDash e faz as operações reais de base/armazenamento após validar capabilities.
Configuração Wrangler
O sandboxing requer Dynamic Worker Loader. Adicione ao wrangler.jsonc:
{
"worker_loaders": [{ "binding": "LOADER" }],
"r2_buckets": [{ "binding": "MEDIA", "bucket_name": "emdash-media" }],
"d1_databases": [{ "binding": "DB", "database_name": "emdash" }]
}
Deployments Node.js
Ao fazer deploy em Node.js (ou qualquer plataforma que não seja Cloudflare):
- Usa-se
NoopSandboxRunner. DevolveisAvailable() === false. - Tentar carregar plugins sandboxed lança
SandboxNotAvailableError. - Todos os plugins devem ser registados como trusted no array
plugins. - Declarações de capabilities são apenas informativas — não são aplicadas.
O que isto significa para a segurança
| Ameaça | Cloudflare (Sandboxed) | Node.js (só Trusted) |
|---|---|---|
| Plugin lê dados que não devia | Bloqueado por verificações de capabilities no bridge | Não impedido — acesso total à BD |
| Plugin faz chamadas de rede não autorizadas | Bloqueado por globalOutbound: null + lista de hosts | Não impedido — pode chamar fetch() diretamente |
| Plugin esgota CPU | Isolate abortado pelo Worker Loader | Não impedido — bloqueia o event loop |
| Plugin esgota memória | Isolate terminado pelo Worker Loader | Não impedido — pode derrubar o processo |
| Plugin acede a variáveis de ambiente | Sem acesso (contexto V8 isolado) | Não impedido — partilha process.env |
| Plugin acede ao sistema de ficheiros | Sem FS em Workers | Não impedido — acesso total a fs |
Recomendações para deployments Node.js
- Instale plugins apenas de fontes confiáveis. Reveja o código-fonte antes de instalar. Prefira plugins de maintainers conhecidos.
- Use capabilities como checklist de revisão. Mesmo sem enforcement documentam o âmbito pretendido. Um plugin com
["network:fetch"]sem necessidade de rede é suspeito. - Monitorize o uso de recursos. Use monitorização ao nível do processo (ex.:
--max-old-space-size, health checks) para detetar plugins descontrolados. - Considere Cloudflare para plugins não confiáveis. Se precisar de executar plugins de origem desconhecida (ex.: marketplace), faça deploy em Cloudflare Workers onde o sandboxing está disponível.
Mesma API, garantias diferentes
O código do plugin é idêntico em ambos os modos. A API definePlugin(), a forma de ctx, hooks, routes e storage funcionam da mesma forma. O que muda é o enforcement:
// This plugin works in both trusted and sandboxed mode
export default definePlugin({
id: "analytics",
version: "1.0.0",
capabilities: ["read:content", "network:fetch"],
allowedHosts: ["api.analytics.example.com"],
hooks: {
"content:afterSave": async (event, ctx) => {
// In trusted mode: ctx.http is always present (capabilities not enforced)
// In sandboxed mode: ctx.http is present because "network:fetch" is declared
await ctx.http.fetch("https://api.analytics.example.com/track", {
method: "POST",
body: JSON.stringify({ contentId: event.content.id }),
});
},
},
});
O objetivo é permitir que autores desenvolvam localmente em modo trusted (iteração mais rápida, debugging mais simples) e façam deploy em produção em modo sandboxed sem alterar código.