Impostazioni

In questa pagina

I plugin sandboxed memorizzano la configurazione specifica del sito tramite ctx.settings. Una pagina di amministrazione Block Kit carica i valori correnti, accetta le modifiche, le valida e le scrive tramite lo stesso store con ambito plugin. I campi dichiarati con type: "secret" vengono crittografati prima che EmDash li scriva nel database.

Leggere e scrivere le impostazioni

Ogni hook e route riceve questa interfaccia di 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 sono namespaced per plugin. Due plugin possono usare la stessa chiave senza leggere o sovrascrivere i valori l’uno dell’altro.

Quando richieste concorrenti possono modificare la stessa chiave, usa le scritture condizionali per rifiutare aggiornamenti basati su una revisione obsoleta. Gli stessi metodi funzionano nei plugin nativi e sandboxed.

Usa ctx.kv separatamente per lo stato interno e i valori in cache:

APIScopoEsempio
ctx.settingsValori configurabili dall’utenteapiKey
ctx.kv con state:Stato interno persistentestate:lastSync
ctx.kv con cache:Dati calcolati o remoti riutilizzabilicache:feed

Le seguenti chiamate 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 di namespace del plugin di EmDash.

I plugin esistenti possono continuare a leggere ctx.kv.get("settings:<key>"). L’alias KV completo settings: rimane supportato per il resto della linea di release 0.x. EmDash non lo rimuoverà prima di 1.0, e qualsiasi rimozione successiva includerà un periodo di deprecazione e una guida alla migrazione. Il codice nuovo dovrebbe usare ctx.settings.

Aggiungere una pagina di impostazioni

Dichiara la pagina in emdash-plugin.jsonc così appare 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 crittografia. Un secret_input in Block Kit maschera solo l’input del browser; non contrassegna da solo un valore memorizzato come secret.

ctx.settings non necessita di capability perché l’host fissa il suo namespace al plugin corrente. Aggiungere un campo di impostazioni non espande il declaredAccess del plugin né attiva un nuovo consenso alle capability. Un amministratore concede al plugin l’accesso a una credenziale inserendola nel modulo di impostazioni di quel plugin.

Il plugin deve anche fornire una route privata chiamata admin. EmDash invia page_load all’apertura della pagina e form_submit quando l’utente invia il modulo.

Aggiungi @emdash-cms/blocks e zod per usare il tipo di risposta e validare le interazioni:

pnpm add @emdash-cms/blocks zod

La seguente route carica tre valori e scrive solo campi di modulo validati:

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 secret finché l’utente non lo modifica, e possono contenere una stringa vuota se l’utente mette a fuoco e svuota il campo. 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 il valore al browser.

Block Kit è il riferimento canonico per interazioni, blocchi, elementi di modulo, builder e campi condizionali.

Valori secret

EmDash crittografa i campi di schema secret con AES-GCM. I dati autenticati legano ogni valore al suo ID plugin e alla chiave di impostazione, così copiare un envelope su un altro plugin o chiave fa fallire la decrittazione. La prima chiave in EMDASH_ENCRYPTION_KEY crittografa le nuove scritture; EmDash seleziona le chiavi più vecchie tramite fingerprint in lettura. Chiavi mancanti, errate o manomesse falliscono in modo chiuso. Le risposte di amministrazione e gli errori dell’host non contengono il testo in chiaro. Dopo che un plugin legge o scrive un secret, il logger dell’host redige il valore esatto corrente e immediatamente precedente per quella chiave dai messaggi ctx.log e dai dati strutturati.

Il plugin riceve comunque il testo in chiaro e può trasformarlo o inviarlo tramite accesso di rete o email dichiarato. Rivedi quelle capability prima di inserire una credenziale, e non registrare mai materiale secret derivato o codificato.

I valori in chiaro esistenti restano leggibili. Salva di nuovo il valore per sostituirlo con un envelope crittografato. Se un secret non deve essere scritto nel database EmDash nemmeno in forma crittografata, usa un plugin nativo supportato da un secret di deployment o un servizio di credenziali esterno. I plugin sandboxed non possono leggere l’ambiente del processo host né i binding di piattaforma.

Fornisci un’azione separata e deliberata se gli utenti devono cancellare un secret. Trattare un campo mascherato vuoto come eliminazione può cancellare una credenziale funzionante quando l’utente salva un’impostazione non correlata.

Valori predefiniti e aggiornamenti

Applica i default in lettura di una chiave così le installazioni esistenti ricevono una nuova impostazione senza migrazione:

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

Puoi persistere 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 release successiva aggiunge un’impostazione, i siti esistenti non lo rieseguono. Mantieni il fallback in lettura, oppure inizializza la chiave mancante in modo idempotente durante plugin:activate.

Scegliere KV o storage

DatiUsare
Piccoli valori configurabili dall’utentectx.settings
Piccolo stato interno o cursorictx.kv con prefisso state:
Record interrogabili come invii o logUna collection ctx.storage dichiarata
Contenuto modificato tramite l’editor EmDash regolareUna collection di contenuto del sito

KV supporta l’accesso diretto per chiave e l’elenco per prefisso, ma non ha query per campo né indici. Storage fornisce 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. Vedi Your first native plugin per quel formato.