Einstellungen

Auf dieser Seite

Sandbox-Plugins speichern standortspezifische Konfiguration über ctx.settings. Eine Block-Kit-Admin-Seite lädt die aktuellen Werte, nimmt Änderungen entgegen, validiert sie und schreibt sie über denselben pluginbezogenen Speicher. Felder, die mit type: "secret" deklariert sind, werden verschlüsselt, bevor EmDash sie in die Datenbank schreibt.

Einstellungen lesen und schreiben

Jeder Hook und jede Route erhält dieses Einstellungs-Interface über 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 }>>;
}

Einstellungen sind pro Plugin namespaced. Zwei Plugins können denselben Schlüssel verwenden, ohne die Werte des jeweils anderen zu lesen oder zu überschreiben.

Wenn gleichzeitige Anfragen denselben Schlüssel ändern können, verwenden Sie bedingte Schreibvorgänge, um Aktualisierungen auf Basis einer veralteten Revision abzulehnen. Dieselben Methoden funktionieren in nativen Plugins und in Sandbox-Plugins.

Verwenden Sie ctx.kv getrennt davon für internen Zustand und zwischengespeicherte Werte:

APIZweckBeispiel
ctx.settingsVom Benutzer konfigurierbare WerteapiKey
ctx.kv mit state:Persistenter interner Zustandstate:lastSync
ctx.kv mit cache:Wiederverwendbare berechnete oder entfernte Datencache:feed

Die folgenden Aufrufe decken die KV-Operationen ab:

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 gibt null zurück, wenn der Schlüssel nicht existiert. list gibt Schlüssel ohne das interne Plugin-Namespace-Präfix von EmDash zurück.

Bestehende Plugins können weiterhin ctx.kv.get("settings:<key>") lesen. Der vollständige settings:-KV-Alias bleibt während der gesamten 1.x-Reihe unterstützt. Jede Entfernung in einer späteren Hauptversion wird eine Deprecation-Phase und eine Migrationsanleitung enthalten. Neuer Code sollte ctx.settings verwenden.

Eine Einstellungsseite hinzufügen

Deklarieren Sie die Seite in emdash-plugin.jsonc, damit sie in der Admin-Navigation des Plugins erscheint:

"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 }
	}
}

Das Schema teilt EmDash mit, welche Werte verschlüsselt werden müssen. Ein secret_input in Block Kit maskiert nur die Eingabe im Browser; er markiert einen gespeicherten Wert nicht von selbst als Secret.

ctx.settings benötigt keine Capability, da der Host seinen Namespace auf das aktuelle Plugin festlegt. Das Hinzufügen eines Einstellungsfelds erweitert nicht das declaredAccess des Plugins und löst keine erneute Capability-Zustimmung aus. Ein Administrator gewährt dem Plugin Zugriff auf Zugangsdaten, indem er sie im Einstellungsformular dieses Plugins eingibt.

Das Plugin muss außerdem eine private Route mit dem Namen admin bereitstellen. EmDash sendet page_load, wenn die Seite geöffnet wird, und form_submit, wenn der Benutzer das Formular abschickt.

Fügen Sie @emdash-cms/blocks und zod hinzu, um den Antworttyp zu verwenden und Interaktionen zu validieren:

pnpm add @emdash-cms/blocks zod

Die folgende Route lädt drei Werte und schreibt nur validierte Formularfelder:

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);
}

Die übermittelten Werte enthalten das Secret nicht, bis der Benutzer es bearbeitet, und können einen leeren String enthalten, wenn der Benutzer das Feld fokussiert und leert. saveSettings schreibt einen neuen API-Schlüssel nur, wenn der übermittelte String nicht leer ist. Die Seite verwendet has_value, um anzuzeigen, dass ein gespeicherter Wert existiert, ohne den Wert an den Browser zurückzugeben.

Block Kit ist die maßgebliche Referenz für Interaktionen, Blöcke, Formularelemente, Builder und bedingte Felder.

Secret-Werte

EmDash verschlüsselt secret-Schemafelder mit AES-GCM. Die authentifizierten Daten binden jeden Wert an seine Plugin-ID und seinen Einstellungsschlüssel, sodass das Kopieren eines Umschlags in ein anderes Plugin oder zu einem anderen Schlüssel die Entschlüsselung fehlschlagen lässt. Der erste Schlüssel in EMDASH_ENCRYPTION_KEY verschlüsselt neue Schreibvorgänge; beim Lesen wählt EmDash ältere Schlüssel anhand ihres Fingerabdrucks aus. Fehlende, falsche oder manipulierte Schlüssel schlagen geschlossen fehl (fail closed). Admin-Antworten und Host-Fehler enthalten keinen Klartext. Nachdem ein Plugin ein Secret gelesen oder geschrieben hat, schwärzt der Host-Logger den aktuellen und den unmittelbar vorherigen exakten Wert dieses Schlüssels in ctx.log-Meldungen und strukturierten Daten.

Das Plugin erhält weiterhin den Klartext und kann ihn umwandeln oder über deklarierten Netzwerk- oder E-Mail-Zugriff versenden. Prüfen Sie diese Capabilities, bevor Sie Zugangsdaten eingeben, und protokollieren Sie niemals abgeleitetes oder kodiertes Secret-Material.

Bestehende Klartextwerte bleiben lesbar. Speichern Sie den Wert erneut, um ihn durch einen verschlüsselten Umschlag zu ersetzen. Wenn ein Secret auch in verschlüsselter Form nicht in die EmDash-Datenbank geschrieben werden darf, verwenden Sie ein natives Plugin, das auf einem Deployment-Secret oder einem externen Zugangsdatendienst basiert. Sandbox-Plugins können weder die Umgebung des Host-Prozesses noch Plattform-Bindings lesen.

Stellen Sie eine eigene, bewusste Aktion bereit, wenn Benutzer ein Secret löschen müssen. Ein leeres maskiertes Feld als Löschung zu behandeln, kann funktionierende Zugangsdaten löschen, wenn ein Benutzer eine unabhängige Einstellung speichert.

Standardwerte und Upgrades

Wenden Sie Standardwerte beim Lesen eines Schlüssels an, damit bestehende Installationen eine neue Einstellung ohne Migration erhalten:

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

Sie können Anfangswerte bei der Installation persistieren:

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

plugin:install läuft nur bei einer Neuinstallation. Wenn ein späteres Release eine Einstellung hinzufügt, wird es auf bestehenden Sites nicht erneut ausgeführt. Behalten Sie den Fallback beim Lesen bei, oder initialisieren Sie den fehlenden Schlüssel idempotent während plugin:activate.

KV oder Storage wählen

DatenVerwenden
Kleine vom Benutzer konfigurierbare Wertectx.settings
Kleiner interner Zustand oder Cursorctx.kv mit einem state:-Präfix
Abfragbare Datensätze wie Einsendungen oder LogsEine deklarierte ctx.storage-Collection
Inhalte, die im regulären EmDash-Editor bearbeitet werdenEine Content-Collection der Site

KV unterstützt direkten Schlüsselzugriff und Auflistung nach Präfix, bietet aber keine Feldabfragen oder Indizes. Storage stellt Dokument-Collections mit indiziertem Filtern, Sortieren, Zählen und Paginierung bereit.

Native Plugins können stattdessen admin.settingsSchema innerhalb von definePlugin() deklarieren und EmDash das Formular generieren lassen. Dieses Format finden Sie unter Ihr erstes natives Plugin.