設定

このページ

サンドボックスプラグインは、サイト固有の設定を ctx.settings 経由で保存します。Block Kit の管理ページが現在の値を読み込み、変更を受け取り、検証し、同じプラグインスコープのストア経由で書き込みます。type: "secret" で宣言されたフィールドは、EmDash がデータベースに書き込む前に暗号化されます。

設定の読み書き

すべてのフックとルートは、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 }>>;
}

設定はプラグインごとに名前空間化されています。2 つのプラグインは、互いの値を読み取ったり上書きしたりせずに同じキーを使用できます。

同時リクエストが同じキーを変更しうる場合は、条件付き書き込み を使って古いリビジョンに基づく更新を拒否してください。同じメソッドはネイティブプラグインとサンドボックスプラグインの両方で動作します。

内部状態とキャッシュ値には、別に ctx.kv を使います。

API用途例
ctx.settingsユーザー設定可能な値apiKey
ctx.kv の state:永続的な内部状態state:lastSync
ctx.kv の cache:再利用可能な計算結果またはリモートデータcache:feed

次の呼び出しが 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 は null を返します。list は EmDash の内部プラグイン名前空間プレフィックスなしのキーを返します。

既存のプラグインは引き続き ctx.kv.get("settings:<key>") を読めます。完全な settings: KV エイリアスは 0.x リリースラインの残りの期間サポートされたままです。EmDash は 1.0 より前に削除せず、後の削除には非推奨期間と移行ガイダンスが含まれます。新しいコードは ctx.settings を使うべきです。

設定ページを追加する

プラグインの管理ナビゲーションに表示されるよう、emdash-plugin.jsonc でページを宣言します。

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

スキーマは、どの値に暗号化が必要かを EmDash に伝えます。Block Kit の secret_input はブラウザ入力をマスクするだけで、それ自体が保存された値をシークレットとしてマークするわけではありません。

ホストが名前空間を現在のプラグインに固定するため、ctx.settings に capability は不要です。設定フィールドを追加しても、プラグインの declaredAccess は拡張されず、capability の再同意もトリガーされません。管理者は、そのプラグインの設定フォームに入力することで、プラグインに認証情報へのアクセスを付与します。

プラグインは、admin という名前のプライベートルートも提供する必要があります。ページが開くと EmDash は page_load を送信し、ユーザーがフォームを送信すると form_submit を送信します。

レスポンス型を使い、インタラクションを検証するために @emdash-cms/blocks と zod を追加します。

pnpm add @emdash-cms/blocks zod

次のルートは 3 つの値を読み込み、検証済みのフォームフィールドのみを書き込みます。

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

送信値はユーザーが編集するまでシークレットを省略し、ユーザーがフィールドにフォーカスしてクリアすると空文字列を含む場合があります。saveSettings は、送信された文字列が空でない場合にのみ新しい API キーを書き込みます。ページは has_value を使い、値をブラウザに返さずに保存済みの値が存在することを示します。

Block Kit は、インタラクション、ブロック、フォーム要素、ビルダー、条件付きフィールドの正規リファレンスです。

シークレット値

EmDash は secret スキーマフィールドを AES-GCM で暗号化します。認証済みデータは各値をプラグイン ID と設定キーに結びつけるため、エンベロープを別のプラグインやキーにコピーすると復号に失敗します。EMDASH_ENCRYPTION_KEY の最初のキーが新しい書き込みを暗号化し、EmDash は読み取り時にフィンガープリントで古いキーを選択します。欠落、誤り、改ざんされたキーはフェイルクローズします。管理レスポンスとホストエラーに平文は含まれません。プラグインがシークレットを読み書きした後、ホストロガーはそのキーの現在および直前の正確な値を ctx.log メッセージと構造化データからリダクトします。

プラグインは引き続き平文を受け取り、変換したり、宣言されたネットワークまたはメールアクセス経由で送信したりできます。認証情報を入力する前にそれらの capability を確認し、派生またはエンコードされたシークレット素材を決してログに残さないでください。

既存の平文値は引き続き読めます。値を再度保存すると、暗号化エンベロープに置き換わります。シークレットを暗号化されていても EmDash データベースに書き込んではならない場合は、デプロイメントシークレットまたは外部の認証情報サービスに裏付けられたネイティブプラグインを使います。サンドボックスプラグインはホストプロセスの環境やプラットフォームバインディングを読めません。

ユーザーがシークレットをクリアする必要がある場合は、別の意図的なアクションを提供してください。空のマスク済みフィールドを削除として扱うと、ユーザーが無関係な設定を保存したときに動作中の認証情報を消してしまうことがあります。

デフォルト値とアップグレード

キーを読むときにデフォルトを適用し、既存インストールが移行なしで新しい設定を受け取れるようにします。

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

インストール時に初期値を永続化できます。

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

plugin:install は新規インストール時のみ実行されます。後のリリースが設定を追加しても、既存サイトでは再実行されません。読み取り時フォールバックを維持するか、plugin:activate 中に欠落キーを冪等に初期化してください。

KV と storage の選択

データ使用
小さなユーザー設定可能な値ctx.settings
小さな内部状態またはカーソルstate: プレフィックス付きの ctx.kv
送信やログなどのクエリ可能なレコード宣言された ctx.storage コレクション
通常の EmDash エディターで編集されるコンテンツサイトコンテンツコレクション

KV は直接のキーアクセスとプレフィックス一覧をサポートしますが、フィールドクエリやインデックスはありません。Storage は、インデックス付きフィルタリング、並べ替え、カウント、ページネーション付きのドキュメントコレクションを提供します。

ネイティブプラグインは代わりに definePlugin() 内で admin.settingsSchema を宣言し、EmDash にフォームを生成させることができます。その形式については Your first native plugin を参照してください。