Plugins em sandbox armazenam a configuração específica do site por meio de ctx.settings. Uma página de administração do Block Kit carrega os valores atuais, aceita alterações, valida-as e as grava pelo mesmo armazenamento com escopo do plugin. Os campos declarados com type: "secret" são criptografados antes de o EmDash gravá-los no banco de dados.
Ler e gravar configurações
Todo hook e toda rota recebem 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 têm namespace por plugin. Dois plugins podem usar a mesma chave sem ler nem sobrescrever os valores um do outro.
Quando requisições simultâneas puderem alterar a mesma chave, use as gravações condicionais para rejeitar atualizações baseadas em uma revisão desatualizada. Os mesmos métodos funcionam em plugins nativos e em plugins em sandbox.
Use ctx.kv separadamente para estado interno e valores em cache:
| API | Finalidade | 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 chamadas a seguir cobrem as operações de 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 as chaves sem o prefixo interno de namespace do plugin do EmDash.
Plugins existentes podem continuar lendo ctx.kv.get("settings:<key>"). O alias KV completo settings: continua com suporte durante toda a série 1.x. Qualquer remoção em uma versão major posterior incluirá um período de descontinuação e orientações de migração. 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 ela 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 no navegador; ele não marca, por si só, um valor armazenado como secreto.
ctx.settings não precisa de nenhuma capability, porque o host fixa o namespace dele no plugin atual. Adicionar um campo de configuração não amplia o declaredAccess do plugin nem aciona 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 precisa fornecer uma rota privada chamada admin. O EmDash envia page_load quando a página é aberta 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 as interações:
pnpm add @emdash-cms/blocks zod
A rota a seguir carrega três valores e grava apenas os campos do formulário que foram 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 secreto até que o usuário o edite e podem conter uma string vazia se o usuário focar o campo e apagá-lo. 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 esse valor ao navegador.
O Block Kit é a referência canônica para interações, blocos, elementos de formulário, builders e campos condicionais.
Valores secret
O EmDash criptografa os campos secret do schema com AES-GCM. Os dados autenticados vinculam cada valor ao ID do plugin e à chave da configuração, de modo que copiar um envelope para outro plugin ou outra chave faz a descriptografia falhar. A primeira chave em EMDASH_ENCRYPTION_KEY criptografa as novas gravações; na leitura, o EmDash seleciona as chaves mais antigas pela impressão digital. Chaves ausentes, incorretas ou adulteradas falham de forma fechada (fail closed). As respostas de administração e os erros do host não contêm o texto simples. Depois que um plugin lê ou grava um segredo, o logger do host oculta o valor exato atual e o imediatamente anterior dessa chave nas mensagens de ctx.log e nos dados estruturados.
O plugin ainda recebe o texto simples e pode transformá-lo ou enviá-lo por meio do acesso de rede ou de e-mail declarado. Revise essas capabilities antes de inserir uma credencial e nunca registre em log material secreto derivado ou codificado.
Os valores em texto simples existentes continuam legíveis. Salve o valor novamente para substituí-lo por um envelope criptografado. Se um segredo não puder ser gravado no banco de dados do EmDash nem mesmo de forma criptografada, use um plugin nativo apoiado por um segredo de implantação ou por um serviço externo de credenciais. Plugins em sandbox não conseguem ler o ambiente do processo do host nem os bindings da plataforma.
Forneça uma ação separada e deliberada caso os usuários precisem apagar um segredo. Tratar um campo mascarado vazio como exclusão pode apagar uma credencial que funciona quando o usuário salva uma configuração sem relação.
Valores padrão e upgrades
Aplique os valores padrão ao ler uma chave para que as instalações existentes recebam uma nova configuração sem uma migração:
const enabled = (await ctx.settings.get<boolean>("enabled")) ?? true;
const maxItems = (await ctx.settings.get<number>("maxItems")) ?? 100;
Você pode persistir os 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 uma versão posterior adiciona uma configuração, os sites existentes não o executam novamente. Mantenha o valor de fallback na leitura ou inicialize a chave ausente de forma idempotente durante plugin:activate.
Escolher KV ou storage
| Dados | Use |
|---|---|
| Valores pequenos configuráveis pelo usuário | ctx.settings |
| Pequeno estado interno ou cursores | ctx.kv com o prefixo state: |
| Registros consultáveis, como envios ou logs | Uma coleção ctx.storage declarada |
| Conteúdo editado pelo editor normal do EmDash | Uma coleção de conteúdo do site |
O KV oferece acesso direto por chave e listagem por prefixo, mas não tem consultas por campo nem índices. O Storage fornece coleções de documentos com filtragem indexada, ordenação, contagem e paginação.
Os plugins nativos podem, em vez disso, declarar admin.settingsSchema dentro de definePlugin() e deixar o EmDash gerar o formulário. Consulte Seu primeiro plugin nativo para esse formato.