Einstellungen

Auf dieser Seite

Sandboxed Plugins speichern site-spezifische Konfiguration über ctx.settings. Eine Block-Kit-Admin-Seite lädt die aktuellen Werte, nimmt Änderungen entgegen, validiert sie und schreibt sie über denselben plugin-bezogenen Store. Felder mit type: "secret" werden verschlüsselt, bevor EmDash sie in die Datenbank schreibt.

Einstellungen lesen und schreiben

Jeder Hook und jede Route erhält diese Settings-Schnittstelle auf 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 nach Plugin namenspaced. Zwei Plugins können denselben Schlüssel verwenden, ohne die Werte des anderen zu lesen oder zu überschreiben.

Wenn parallele Anfragen denselben Schlüssel ändern können, nutzen Sie bedingte Schreibvorgänge, um Updates mit veralteter Revision abzulehnen. Dieselben Methoden funktionieren in Native Plugins und Sandboxed Plugins.

Nutzen Sie ctx.kv getrennt für internen Zustand und gecachte Werte:

APIZweckBeispiel
ctx.settingsBenutzerkonfigurierbare WerteapiKey
ctx.kv mit state:Persistenter interner Zustandstate:lastSync
ctx.kv mit cache:Wiederverwendbare berechnete oder Remote-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 EmDashs internes Plugin-Namespace-Präfix zurück.

Bestehende Plugins können weiterhin ctx.kv.get("settings:<key>") lesen. Der vollständige settings:-KV-Alias bleibt für den Rest der 0.x-Release-Linie unterstützt. EmDash entfernt ihn nicht vor 1.0, und eine spätere Entfernung enthält eine Deprecation-Phase und Migrationshinweise. 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üsselung brauchen. Ein secret_input in Block Kit maskiert nur die Browser-Eingabe; es markiert einen gespeicherten Wert allein nicht als Secret.

ctx.settings braucht keine Capability, weil der Host den Namespace auf das aktuelle Plugin fixiert. Das Hinzufügen eines Einstellungsfelds erweitert weder declaredAccess des Plugins noch löst es eine erneute Capability-Zustimmung aus. Ein Administrator gewährt dem Plugin Zugriff auf ein Credential, indem er es im Einstellungsformular dieses Plugins eingibt.

Das Plugin muss außerdem eine private Route namens admin bereitstellen. EmDash sendet page_load, wenn die Seite öffnet, und form_submit, wenn der Benutzer das Formular absendet.

Fügen Sie @emdash-cms/blocks und zod hinzu, um den Antworttyp zu nutzen 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 lassen das Secret weg, 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 nutzt has_value, um zu zeigen, dass ein gespeicherter Wert existiert, ohne den Wert an den Browser zurückzugeben.

Block Kit ist die kanonische 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 den Setting-Schlüssel, sodass das Kopieren eines Envelopes zu einem anderen Plugin oder Schlüssel die Entschlüsselung fehlschlagen lässt. Der erste Schlüssel in EMDASH_ENCRYPTION_KEY verschlüsselt neue Schreibvorgänge; EmDash wählt ältere Schlüssel anhand ihres Fingerprints beim Lesen. Fehlende, falsche oder manipulierte Schlüssel scheitern geschlossen. Admin-Antworten und Host-Fehler enthalten den Klartext nicht. Nachdem ein Plugin ein Secret gelesen oder geschrieben hat, redaktiert der Host-Logger den aktuellen und unmittelbar vorherigen exakten Wert für diesen Schlüssel aus ctx.log-Nachrichten und strukturierten Daten.

Das Plugin erhält weiterhin den Klartext und kann ihn transformieren oder über deklarierten Netzwerk- oder E-Mail-Zugriff senden. Prüfen Sie diese Capabilities, bevor Sie ein Credential eingeben, und loggen Sie niemals abgeleitetes oder kodiertes Secret-Material.

Bestehende Klartextwerte bleiben lesbar. Speichern Sie den Wert erneut, um ihn durch ein verschlüsseltes Envelope zu ersetzen. Wenn ein Secret nicht einmal in verschlüsselter Form in die EmDash-Datenbank geschrieben werden darf, nutzen Sie ein Native Plugin mit Deployment-Secret oder einem externen Credential-Dienst. Sandboxed Plugins können weder die Umgebung des Host-Prozesses noch Platform Bindings lesen.

Stellen Sie eine separate, bewusste Aktion bereit, wenn Benutzer ein Secret löschen müssen. Ein leeres maskiertes Feld als Löschung zu behandeln kann ein funktionierendes Credential löschen, wenn ein Benutzer eine unrelated Einstellung speichert.

Standardwerte und Upgrades

Wenden Sie Defaults 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 neuen Installation. Wenn ein späteres Release eine Einstellung hinzufügt, führen bestehende Sites es nicht erneut aus. Behalten Sie den Read-Time-Fallback bei, oder initialisieren Sie den fehlenden Schlüssel idempotent während plugin:activate.

KV oder Storage wählen

DatenNutzen
Kleine benutzerkonfigurierbare Wertectx.settings
Kleiner interner Zustand oder Cursorctx.kv mit state:-Präfix
Abfragbare Datensätze wie Submissions oder LogsEine deklarierte ctx.storage-Collection
Inhalt, der über den regulären EmDash-Editor bearbeitet wirdEine Site-Content-Collection

KV unterstützt direkten Schlüsselzugriff und Prefix-Listing, hat aber keine Feldabfragen oder Indexes. Storage bietet Dokument-Collections mit indexierter Filterung, Sortierung, Zählung und Paginierung.

Native Plugins können stattdessen admin.settingsSchema in definePlugin() deklarieren und EmDash das Formular generieren lassen. Siehe Your first native plugin für dieses Format.