I plugin in sandbox archiviano la configurazione specifica del sito tramite ctx.settings. Una pagina di amministrazione Block Kit carica i valori correnti, accetta le modifiche, le convalida e le scrive attraverso lo stesso archivio limitato al plugin. I campi dichiarati con type: "secret" vengono cifrati prima che EmDash li scriva nel database.
Leggere e scrivere le impostazioni
Ogni hook e ogni route riceve questa interfaccia delle impostazioni su 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 }>>;
}
Le impostazioni hanno un namespace per plugin. Due plugin possono usare la stessa chiave senza leggere né sovrascrivere i valori dell’altro.
Quando richieste concorrenti possono modificare la stessa chiave, usa le scritture condizionali per rifiutare gli aggiornamenti basati su una revisione obsoleta. Gli stessi metodi funzionano sia nei plugin nativi sia nei plugin in sandbox.
Usa ctx.kv separatamente per lo stato interno e per i valori in cache:
| API | Scopo | Esempio |
|---|---|---|
ctx.settings | Valori configurabili dall’utente | apiKey |
ctx.kv con state: | Stato interno persistente | state:lastSync |
ctx.kv con cache: | Dati calcolati o remoti riutilizzabili | cache:feed |
Le chiamate seguenti coprono le operazioni 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 restituisce null quando la chiave non esiste. list restituisce le chiavi senza il prefisso interno del namespace del plugin di EmDash.
I plugin esistenti possono continuare a leggere ctx.kv.get("settings:<key>"). L’alias KV completo settings: resta supportato per tutta la serie 1.x. Qualsiasi rimozione in una versione major successiva includerà un periodo di deprecazione e una guida alla migrazione. Il nuovo codice dovrebbe usare ctx.settings.
Aggiungere una pagina di impostazioni
Dichiara la pagina in emdash-plugin.jsonc in modo che compaia nella navigazione di amministrazione 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 }
}
}
Lo schema indica a EmDash quali valori richiedono la cifratura. Un secret_input in Block Kit maschera soltanto l’input nel browser; da solo non contrassegna come segreto un valore archiviato.
ctx.settings non richiede alcuna capability, perché l’host fissa il suo namespace al plugin corrente. Aggiungere un campo di impostazioni non amplia il declaredAccess del plugin e non attiva una nuova richiesta di consenso per le capability. Un amministratore concede al plugin l’accesso a una credenziale inserendola nel modulo delle impostazioni di quel plugin.
Il plugin deve inoltre fornire una route privata chiamata admin. EmDash invia page_load quando la pagina si apre e form_submit quando l’utente invia il modulo.
Aggiungi @emdash-cms/blocks e zod per usare il tipo di risposta e convalidare le interazioni:
pnpm add @emdash-cms/blocks zod
La route seguente carica tre valori e scrive solo i campi del modulo convalidati:
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);
}
I valori inviati omettono il segreto finché l’utente non lo modifica, e possono contenere una stringa vuota se l’utente porta il focus sul campo e lo svuota. saveSettings scrive una nuova chiave API solo quando la stringa inviata non è vuota. La pagina usa has_value per mostrare che esiste un valore salvato senza restituire quel valore al browser.
Block Kit è il riferimento canonico per interazioni, blocchi, elementi del modulo, builder e campi condizionali.
Valori secret
EmDash cifra i campi secret dello schema con AES-GCM. I dati autenticati associano ogni valore al suo ID plugin e alla sua chiave di impostazione, quindi copiare una busta in un altro plugin o in un’altra chiave fa fallire la decifratura. La prima chiave in EMDASH_ENCRYPTION_KEY cifra le nuove scritture; in lettura, EmDash seleziona le chiavi più vecchie in base alla loro impronta. Le chiavi mancanti, errate o manomesse falliscono in modo chiuso (fail closed). Le risposte di amministrazione e gli errori dell’host non contengono il testo in chiaro. Dopo che un plugin legge o scrive un segreto, il logger dell’host oculta il valore esatto corrente e quello immediatamente precedente di quella chiave nei messaggi di ctx.log e nei dati strutturati.
Il plugin riceve comunque il testo in chiaro e può trasformarlo o inviarlo tramite l’accesso di rete o e-mail dichiarato. Esamina queste capability prima di inserire una credenziale e non registrare mai nei log materiale segreto derivato o codificato.
I valori in chiaro esistenti restano leggibili. Salva di nuovo il valore per sostituirlo con una busta cifrata. Se un segreto non deve essere scritto nel database di EmDash nemmeno in forma cifrata, usa un plugin nativo basato su un segreto di deployment o su un servizio esterno di credenziali. I plugin in sandbox non possono leggere l’ambiente del processo host né i binding della piattaforma.
Fornisci un’azione separata e deliberata se gli utenti devono cancellare un segreto. Trattare un campo mascherato vuoto come una cancellazione può eliminare una credenziale funzionante quando un utente salva un’impostazione non correlata.
Valori predefiniti e aggiornamenti
Applica i valori predefiniti quando leggi una chiave, in modo che le installazioni esistenti ricevano una nuova impostazione senza una migrazione:
const enabled = (await ctx.settings.get<boolean>("enabled")) ?? true;
const maxItems = (await ctx.settings.get<number>("maxItems")) ?? 100;
Puoi rendere persistenti i valori iniziali durante l’installazione:
hooks: {
"plugin:install": async (_event, ctx) => {
await ctx.settings.set("enabled", true);
await ctx.settings.set("maxItems", 100);
},
},
plugin:install viene eseguito solo per una nuova installazione. Quando una versione successiva aggiunge un’impostazione, i siti esistenti non lo eseguono di nuovo. Mantieni il valore di ripiego in lettura, oppure inizializza la chiave mancante in modo idempotente durante plugin:activate.
Scegliere KV o storage
| Dati | Usa |
|---|---|
| Piccoli valori configurabili dall’utente | ctx.settings |
| Piccolo stato interno o cursori | ctx.kv con un prefisso state: |
| Record interrogabili, come invii o log | Una collection ctx.storage dichiarata |
| Contenuti modificati tramite il normale editor di EmDash | Una collection di contenuti del sito |
KV supporta l’accesso diretto per chiave e l’elenco per prefisso, ma non ha query sui campi né indici. Storage offre collection di documenti con filtraggio indicizzato, ordinamento, conteggio e paginazione.
I plugin nativi possono invece dichiarare admin.settingsSchema dentro definePlugin() e lasciare che EmDash generi il modulo. Consulta Il tuo primo plugin nativo per quel formato.