Plugins sandboxed armazenam configuração específica do site por meio de ctx.settings. Uma página de administração Block Kit carrega os valores atuais, aceita alterações, valida-as e as grava pelo mesmo armazenamento com escopo de plugin. Campos declarados com type: "secret" são criptografados antes de o EmDash os gravar no banco de dados.
Ler e gravar configurações
Todo hook e rota recebe esta interface de configurações em 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 }>>;
}
As configurações são namespaced por plugin. Dois plugins podem usar a mesma chave sem ler ou sobrescrever os valores um do outro.
Quando solicitações concorrentes podem alterar a mesma chave, use escritas condicionais para rejeitar atualizações baseadas em uma revisão obsoleta. Os mesmos métodos funcionam em plugins nativos e sandboxed.
Use ctx.kv separadamente para estado interno e valores em cache:
| API | Propósito | Exemplo |
|---|---|---|
ctx.settings | Valores configuráveis pelo usuário | apiKey |
ctx.kv com state: | Estado interno persistente | state:lastSync |
ctx.kv com cache: | Dados calculados ou remotos reutilizáveis | cache:feed |
As seguintes chamadas cobrem as operações 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 retorna null quando a chave não existe. list retorna chaves sem o prefixo interno de namespace de plugin do EmDash.
Plugins existentes podem continuar a ler ctx.kv.get("settings:<key>"). O alias KV completo settings: permanece suportado pelo restante da linha de releases 0.x. O EmDash não o removerá antes de 1.0, e qualquer remoção posterior incluirá um período de depreciação e orientação de migração. Código novo deve usar ctx.settings.
Adicionar uma página de configurações
Declare a página em emdash-plugin.jsonc para que apareça na navegação de administração do 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 }
}
}
O schema informa ao EmDash quais valores exigem criptografia. Um secret_input no Block Kit apenas mascara a entrada do navegador; não marca por si só um valor armazenado como secret.
ctx.settings não precisa de capability porque o host fixa seu namespace no plugin atual. Adicionar um campo de configuração não amplia o declaredAccess do plugin nem dispara um novo consentimento de capabilities. Um administrador concede ao plugin acesso a uma credencial ao inseri-la no formulário de configurações desse plugin.
O plugin também deve fornecer uma rota privada chamada admin. O EmDash envia page_load quando a página abre e form_submit quando o usuário envia o formulário.
Adicione @emdash-cms/blocks e zod para usar o tipo de resposta e validar interações:
pnpm add @emdash-cms/blocks zod
A seguinte rota carrega três valores e grava apenas campos de formulário 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);
}
Os valores enviados omitem o secret até o usuário editá-lo e podem conter uma string vazia se o usuário focar e limpar o campo. saveSettings grava uma nova chave de API somente quando a string enviada não está vazia. A página usa has_value para mostrar que existe um valor salvo sem devolver o valor ao navegador.
Block Kit é a referência canônica para interações, blocos, elementos de formulário, builders e campos condicionais.
Valores secret
O EmDash criptografa campos de schema secret com AES-GCM. Os dados autenticados vinculam cada valor ao ID do plugin e à chave de configuração, de modo que copiar um envelope para outro plugin ou chave falha na descriptografia. A primeira chave em EMDASH_ENCRYPTION_KEY criptografa novas gravações; o EmDash seleciona chaves mais antigas pela impressão digital na leitura. Chaves ausentes, erradas ou adulteradas falham de forma fechada. Respostas de administração e erros do host não contêm o texto em claro. Depois que um plugin lê ou grava um secret, o logger do host redige o valor exato atual e imediatamente anterior dessa chave das mensagens ctx.log e dos dados estruturados.
O plugin ainda recebe o texto em claro e pode transformá-lo ou enviá-lo por acesso de rede ou e-mail declarado. Revise essas capabilities antes de inserir uma credencial e nunca registre material secret derivado ou codificado.
Valores em texto claro existentes permanecem legíveis. Salve o valor novamente para substituí-lo por um envelope criptografado. Se um secret não deve ser escrito no banco EmDash mesmo em forma criptografada, use um plugin nativo respaldado por um secret de implantação ou um serviço externo de credenciais. Plugins sandboxed não podem ler o ambiente do processo host nem bindings de plataforma.
Forneça uma ação separada e deliberada se os usuários precisarem limpar um secret. Tratar um campo mascarado vazio como exclusão pode apagar uma credencial funcional quando o usuário salva uma configuração não relacionada.
Valores padrão e upgrades
Aplique padrões ao ler uma chave para que instalações existentes recebam uma nova configuração sem migração:
const enabled = (await ctx.settings.get<boolean>("enabled")) ?? true;
const maxItems = (await ctx.settings.get<number>("maxItems")) ?? 100;
Você pode persistir valores iniciais durante a instalação:
hooks: {
"plugin:install": async (_event, ctx) => {
await ctx.settings.set("enabled", true);
await ctx.settings.set("maxItems", 100);
},
},
plugin:install é executado apenas em uma instalação nova. Quando um release posterior adiciona uma configuração, sites existentes não o executam de novo. Mantenha o fallback na leitura ou inicialize a chave ausente de forma idempotente durante plugin:activate.
Escolher KV ou storage
| Dados | Usar |
|---|---|
| Valores pequenos configuráveis pelo usuário | ctx.settings |
| Estado interno pequeno ou cursores | ctx.kv com prefixo state: |
| Registros consultáveis como envios ou logs | Uma collection ctx.storage declarada |
| Conteúdo editado pelo editor EmDash regular | Uma collection de conteúdo do site |
KV oferece acesso direto por chave e listagem por prefixo, mas não tem consultas por campo nem índices. Storage fornece collections de documentos com filtragem indexada, ordenação, contagem e paginação.
Plugins nativos podem em vez disso declarar admin.settingsSchema dentro de definePlugin() e deixar o EmDash gerar o formulário. Veja Your first native plugin para esse formato.