Ajustes

En esta página

Los plugins sandboxed almacenan configuración específica del sitio mediante ctx.settings. Una página de administración Block Kit carga los valores actuales, acepta cambios, los valida y los escribe a través del mismo almacén con ámbito de plugin. Los campos declarados con type: "secret" se cifran antes de que EmDash los escriba en la base de datos.

Leer y escribir ajustes

Cada hook y ruta recibe esta interfaz de ajustes en ctx:

interface SettingsAccess {
	get<T>(key: string): Promise<T | null>;
	getVersioned<T>(key: string): Promise<{ value: T; revision: string } | null>;
	compareAndSet(key: string, expectedRevision: string | null, value: unknown):
		Promise<{ applied: true; revision: string } | { applied: false }>;
	compareAndDelete(key: string, expectedRevision: string): Promise<{ applied: boolean }>;
	set(key: string, value: unknown): Promise<void>;
	delete(key: string): Promise<boolean>;
	list(prefix?: string): Promise<Array<{ key: string; value: unknown }>>;
}

Los ajustes están namespaced por plugin. Dos plugins pueden usar la misma clave sin leer ni sobrescribir los valores del otro.

Cuando solicitudes concurrentes pueden cambiar la misma clave, usa escrituras condicionales para rechazar actualizaciones basadas en una revisión obsoleta. Los mismos métodos funcionan en plugins nativos y sandboxed.

Usa ctx.kv por separado para estado interno y valores en caché:

APIPropósitoEjemplo
ctx.settingsValores configurables por el usuarioapiKey
ctx.kv con state:Estado interno persistentestate:lastSync
ctx.kv con cache:Datos calculados o remotos reutilizablescache:feed

Las siguientes llamadas cubren las operaciones KV:

const enabled = await ctx.settings.get<boolean>("enabled");
await ctx.kv.set("state:lastSync", new Date().toISOString());
const deleted = await ctx.kv.delete("cache:feed");
const allSettings = await ctx.settings.list();

get devuelve null cuando la clave no existe. list devuelve claves sin el prefijo interno de namespace de plugin de EmDash.

Los plugins existentes pueden seguir leyendo ctx.kv.get("settings:<key>"). El alias KV completo settings: permanece soportado durante el resto de la línea de releases 0.x. EmDash no lo eliminará antes de 1.0, y cualquier eliminación posterior incluirá un periodo de deprecación y guía de migración. El código nuevo debe usar ctx.settings.

Añadir una página de ajustes

Declara la página en emdash-plugin.jsonc para que aparezca en la navegación de administración del plugin:

"admin": {
	"pages": [{ "path": "/settings", "label": "Settings", "icon": "settings" }],
	"settingsSchema": {
		"apiKey": { "type": "secret", "label": "API key" },
		"enabled": { "type": "boolean", "label": "Enabled", "default": true },
		"maxItems": { "type": "number", "label": "Max items", "default": 100 }
	}
}

El esquema indica a EmDash qué valores requieren cifrado. Un secret_input en Block Kit solo enmascara la entrada del navegador; no marca por sí solo un valor almacenado como secreto.

ctx.settings no necesita capability porque el host fija su namespace al plugin actual. Añadir un campo de ajustes no amplía el declaredAccess del plugin ni dispara un re-consentimiento de capabilities. Un administrador concede al plugin acceso a una credencial al introducirla en el formulario de ajustes de ese plugin.

El plugin también debe proporcionar una ruta privada llamada admin. EmDash envía page_load cuando se abre la página y form_submit cuando el usuario envía el formulario.

Añade @emdash-cms/blocks y zod para usar el tipo de respuesta y validar interacciones:

pnpm add @emdash-cms/blocks zod

La siguiente ruta carga tres valores y escribe solo campos de formulario validados:

import type { BlockResponse } from "@emdash-cms/blocks";
import type { PluginContext, SandboxedPlugin } from "emdash/plugin";
import { z } from "zod";

const interactionSchema = z.discriminatedUnion("type", [
	z.object({ type: z.literal("page_load"), page: z.string() }),
	z.object({
		type: z.literal("form_submit"),
		action_id: z.string(),
		block_id: z.string().optional(),
		values: z.object({
			apiKey: z.string().optional(),
			enabled: z.boolean(),
			maxItems: z.number().int().min(1).max(1000),
		}),
	}),
	z.object({
		type: z.literal("block_action"),
		action_id: z.string(),
		block_id: z.string().optional(),
		value: z.unknown().optional(),
	}),
]);

const plugin: SandboxedPlugin = {
	routes: {
		admin: {
			handler: async (routeCtx, ctx) => {
				const parsed = interactionSchema.safeParse(routeCtx.input);
				if (!parsed.success) return { blocks: [] };
				const interaction = parsed.data;

				if (interaction.type === "page_load" && interaction.page === "/settings") {
					return renderSettings(ctx);
				}

				if (interaction.type === "form_submit" && interaction.action_id === "save") {
					await saveSettings(ctx, interaction.values);
					return {
						...(await renderSettings(ctx)),
						toast: { message: "Settings saved", type: "success" },
					};
				}

				return { blocks: [] };
			},
		},
	},
};

export default plugin;

async function renderSettings(ctx: PluginContext): Promise<BlockResponse> {
	const apiKeyConfigured = (await ctx.settings.get<string>("apiKey")) !== null;
	const enabled = (await ctx.settings.get<boolean>("enabled")) ?? true;
	const maxItems = (await ctx.settings.get<number>("maxItems")) ?? 100;

	return {
		blocks: [
			{ type: "header", text: "Plugin settings" },
			{
				type: "form",
				block_id: "settings",
				fields: [
					{
						type: "secret_input",
						action_id: "apiKey",
						label: "API key",
						has_value: apiKeyConfigured,
					},
					{
						type: "toggle",
						action_id: "enabled",
						label: "Enabled",
						initial_value: enabled,
					},
					{
						type: "number_input",
						action_id: "maxItems",
						label: "Max items",
						min: 1,
						max: 1000,
						initial_value: maxItems,
					},
				],
				submit: { label: "Save", action_id: "save" },
			},
		],
	};
}

async function saveSettings(
	ctx: PluginContext,
	values: { apiKey?: string; enabled: boolean; maxItems: number },
) {
	if (values.apiKey) await ctx.settings.set("apiKey", values.apiKey);
	await ctx.settings.set("enabled", values.enabled);
	await ctx.settings.set("maxItems", values.maxItems);
}

Los valores enviados omiten el secreto hasta que el usuario lo edita, y pueden contener una cadena vacía si el usuario enfoca y vacía el campo. saveSettings escribe una nueva clave API solo cuando la cadena enviada no está vacía. La página usa has_value para mostrar que existe un valor guardado sin devolver el valor al navegador.

Block Kit es la referencia canónica para interacciones, bloques, elementos de formulario, builders y campos condicionales.

Valores secretos

EmDash cifra los campos de esquema secret con AES-GCM. Los datos autenticados vinculan cada valor a su ID de plugin y clave de ajuste, de modo que copiar un envelope a otro plugin o clave falla al descifrar. La primera clave en EMDASH_ENCRYPTION_KEY cifra las escrituras nuevas; EmDash selecciona claves antiguas por su fingerprint al leer. Claves ausentes, incorrectas o manipuladas fallan de forma cerrada. Las respuestas de administración y los errores del host no contienen el texto en claro. Tras que un plugin lea o escriba un secreto, el logger del host redacta el valor exacto actual e inmediatamente anterior de esa clave de los mensajes ctx.log y los datos estructurados.

El plugin sigue recibiendo el texto en claro y puede transformarlo o enviarlo mediante acceso de red o correo declarado. Revisa esas capabilities antes de introducir una credencial, y nunca registres material secreto derivado o codificado.

Los valores en texto plano existentes siguen siendo legibles. Guarda el valor de nuevo para reemplazarlo con un envelope cifrado. Si un secreto no debe escribirse en la base de datos de EmDash ni siquiera en forma cifrada, usa un plugin nativo respaldado por un secreto de despliegue o un servicio externo de credenciales. Los plugins sandboxed no pueden leer el entorno del proceso host ni los bindings de plataforma.

Proporciona una acción separada y deliberada si los usuarios necesitan borrar un secreto. Tratar un campo enmascarado vacío como eliminación puede borrar una credencial que funciona cuando el usuario guarda un ajuste no relacionado.

Valores por defecto y actualizaciones

Aplica valores por defecto al leer una clave para que las instalaciones existentes reciban un ajuste nuevo sin migración:

const enabled = (await ctx.settings.get<boolean>("enabled")) ?? true;
const maxItems = (await ctx.settings.get<number>("maxItems")) ?? 100;

Puedes persistir valores iniciales durante la instalación:

hooks: {
	"plugin:install": async (_event, ctx) => {
		await ctx.settings.set("enabled", true);
		await ctx.settings.set("maxItems", 100);
	},
},

plugin:install se ejecuta solo para una instalación nueva. Cuando un release posterior añade un ajuste, los sitios existentes no lo vuelven a ejecutar. Mantén el fallback en tiempo de lectura, o inicializa la clave faltante de forma idempotente durante plugin:activate.

Elegir KV o storage

DatosUsar
Valores pequeños configurables por el usuarioctx.settings
Estado interno pequeño o cursoresctx.kv con prefijo state:
Registros consultables como envíos o logsUna colección ctx.storage declarada
Contenido editado mediante el editor habitual de EmDashUna colección de contenido del sitio

KV soporta acceso directo por clave y listado por prefijo, pero no tiene consultas por campo ni índices. Storage proporciona colecciones de documentos con filtrado indexado, ordenación, conteo y paginación.

Los plugins nativos pueden declarar admin.settingsSchema dentro de definePlugin() y dejar que EmDash genere el formulario. Consulta Your first native plugin para ese formato.