EmDash puede ejecutar plugins en dos modos: trusted y sandboxed. Esta página explica cómo funciona cada uno, qué protecciones ofrecen y las implicaciones de seguridad según el destino de despliegue.
Modos de ejecución
| Trusted | Sandboxed | |
|---|---|---|
| Se ejecuta en | Proceso principal | Aislamiento V8 aislado (Dynamic Worker Loader) |
| Capabilities | Orientativas (no se aplican) | Aplicadas en tiempo de ejecución |
| Límites de recursos | Ninguno | CPU, memoria, subrequests, tiempo real |
| Acceso a red | Sin restricciones | Bloqueado; solo vía ctx.http con lista de hosts permitidos |
| Acceso a datos | Acceso completo a la base de datos | Limitado a capabilities declaradas vía puente RPC |
| Disponible en | Todas las plataformas | Solo Cloudflare Workers |
Modo trusted
Los plugins trusted se ejecutan en el mismo proceso que tu sitio Astro. Se cargan desde paquetes npm o archivos locales y se configuran en astro.config.mjs:
import myPlugin from "@emdash-cms/plugin-analytics";
export default defineConfig({
integrations: [
emdash({
plugins: [myPlugin()],
}),
],
});
En modo trusted:
- Las capabilities son documentación, no aplicación. Un plugin que declara
["read:content"]aún puede acceder a todo en el proceso. El campocapabilitiesindica a los administradores qué el plugin pretende usar. - Sin límites de recursos. El uso de CPU, memoria y red no está acotado. Un plugin defectuoso puede bloquear toda la petición.
- Acceso completo al proceso. Los plugins comparten el runtime Node.js/Workers con tu sitio Astro. Pueden importar cualquier módulo, acceder a variables de entorno y leer/escribir el sistema de archivos (en Node.js).
Modo sandboxed (Cloudflare Workers)
Los plugins sandboxed se ejecutan en aislamientos V8 aislados proporcionados por la API Dynamic Worker Loader de Cloudflare. Cada plugin obtiene su propio aislamiento con límites aplicados.
Para habilitar el sandboxing, configura el sandbox runner en tu config de Astro:
export default defineConfig({
integrations: [
emdash({
sandboxRunner: "@emdash-cms/cloudflare/sandbox",
sandboxed: [
{
manifest: seoPluginManifest,
code: seoPluginCode,
},
],
}),
],
});
Qué impone el sandbox
-
Aplicación de capabilities
Si un plugin declara
capabilities: ["read:content"], solo puede llamar actx.content.get()yctx.content.list(). Intentarctx.content.create()lanza un error de permisos. Esto lo aplica el puente RPC: el plugin no puede eludirlo porque no tiene acceso directo a la base de datos. -
Límites de recursos
Cada invocación (hook o ruta) se ejecuta con:
Recurso Valor por defecto Aplicado por Tiempo de CPU 50ms Worker Loader (aislamiento V8) Subrequests 10 por invocación Worker Loader (aislamiento V8) Tiempo de reloj 30 segundos Runner de EmDash ( Promise.race)Memoria ~128MB Techo de la plataforma V8 (no configurable por plugin) Superar los límites de CPU o subrequests hace que el Worker Loader aborte el aislamiento y lance una excepción. Superar el límite de tiempo hace que EmDash rechace la promesa de invocación. La memoria está acotada por el techo de V8 pero no es configurable por plugin.
Estos son los valores por defecto integrados. Se pueden configurar límites personalizados proporcionando una
SandboxRunnerFactoryque pase otros valores víaSandboxOptions.limits. La configuración por sitio en la integración de EmDash aún no está implementada. -
Aislamiento de red
Los plugins sandboxed tienen
globalOutbound: null: las llamadas directas afetch()están bloqueadas a nivel V8. Deben usarctx.http.fetch(), que se proxifica a través del puente. El puente valida el host de destino frente a la listaallowedHostsdel plugin. -
Ámbito del almacenamiento
Todas las operaciones de almacenamiento (KV, colecciones) están acotadas al ID del plugin. Un plugin no puede leer los datos de otro. El acceso a contenido y medios pasa por el puente, que comprueba las capabilities en cada llamada.
-
Restricciones de funcionalidad
Algunas funciones solo existen en modo trusted:
- API routes — Los endpoints REST personalizados (
routes) no están disponibles. Los plugins sandboxed interactúan mediante páginas admin de Block Kit y hooks. - Tipos de bloque Portable Text — Los bloques PT requieren componentes Astro para el render en el sitio (
componentsEntry), cargados en build desde npm. Los plugins sandboxed se instalan en runtime y no pueden incluir componentes. - Páginas admin React personalizadas — Los plugins sandboxed usan Block Kit para la UI admin en lugar de enviar componentes React.
El comando
emdash plugin bundleadvierte si un plugin declara estas características. - API routes — Los endpoints REST personalizados (
Arquitectura
Los plugins sandboxed se comunican con EmDash a través de un puente 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 │
└──────────────┘
El código del plugin corre en un aislamiento V8. Recibe un objeto ctx donde cada método es un proxy al puente. El puente corre en el worker principal de EmDash y realiza las operaciones reales de base de datos/almacenamiento tras validar capabilities.
Configuración de Wrangler
El sandboxing requiere Dynamic Worker Loader. Añade a tu wrangler.jsonc:
{
"worker_loaders": [{ "binding": "LOADER" }],
"r2_buckets": [{ "binding": "MEDIA", "bucket_name": "emdash-media" }],
"d1_databases": [{ "binding": "DB", "database_name": "emdash" }]
}
Despliegues en Node.js
Al desplegar en Node.js (o cualquier plataforma que no sea Cloudflare):
- Se usa
NoopSandboxRunner. DevuelveisAvailable() === false. - Intentar cargar plugins sandboxed lanza
SandboxNotAvailableError. - Todos los plugins deben registrarse como trusted en el array
plugins. - Las declaraciones de capabilities son solo informativas: no se aplican.
Qué implica para la seguridad
| Amenaza | Cloudflare (Sandboxed) | Node.js (solo Trusted) |
|---|---|---|
| El plugin lee datos que no debería | Bloqueado por comprobaciones de capabilities en el puente | No evitado — acceso completo a la BD |
| El plugin hace llamadas de red no autorizadas | Bloqueado por globalOutbound: null + lista de hosts | No evitado — puede llamar a fetch() directamente |
| El plugin agota la CPU | El Worker Loader aborta el aislamiento | No evitado — bloquea el event loop |
| El plugin agota la memoria | El Worker Loader termina el aislamiento | No evitado — puede tumbar el proceso |
| El plugin accede a variables de entorno | Sin acceso (contexto V8 aislado) | No evitado — comparte process.env |
| El plugin accede al sistema de archivos | Sin sistema de archivos en Workers | No evitado — acceso completo a fs |
Recomendaciones para despliegues en Node.js
- Instala plugins solo de fuentes de confianza. Revisa el código fuente antes de instalar. Prefiere plugins de maintainers conocidos.
- Usa las capabilities como lista de revisión. Aunque no se apliquen, documentan el alcance pretendido. Un plugin con
["network:fetch"]que no necesita red es sospechoso. - Supervisa el uso de recursos. Usa monitorización a nivel de proceso (p. ej.
--max-old-space-size, health checks) para detectar plugins descontrolados. - Valora Cloudflare para plugins no confiables. Si necesitas ejecutar plugins de origen desconocido (p. ej. marketplace), despliega en Cloudflare Workers, donde el sandboxing está disponible.
Misma API, distintas garantías
El código del plugin es idéntico en ambos modos. La API definePlugin(), la forma de ctx, hooks, routes y storage funcionan igual. Lo que cambia es la aplicación:
// 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 }),
});
},
},
});
El objetivo es que los autores desarrollen en local en modo trusted (iteración más rápida, depuración más sencilla) y desplieguen en producción en modo sandboxed sin cambiar código.