設定

このページ

サンドボックスプラグインは、サイト固有の設定を 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
state: 付きの ctx.kv永続的な内部状態state:lastSync
cache: 付きの ctx.kv再利用できる計算結果やリモートデータ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 エイリアス全体は、1.x の間はサポートされ続けます。後のメジャーバージョンで削除する場合は、非推奨期間と移行ガイダンスが提供されます。新しいコードでは 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 がフィンガープリントで古いキーを選択します。キーが存在しない、誤っている、または改ざんされている場合は、フェイルクローズ(fail closed)で失敗します。管理画面のレスポンスやホストのエラーに平文は含まれません。プラグインがシークレットを読み書きした後は、ホストのロガーが、そのキーの現在の値と直前の値に完全一致するものを 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 は、キーによる直接アクセスとプレフィックスによる一覧取得をサポートしますが、フィールドクエリやインデックスはありません。ストレージは、インデックス付きのフィルタリング、並べ替え、カウント、ページネーションを備えたドキュメントコレクションを提供します。

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