설정

이 페이지

샌드박스 플러그인은 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 }>>;
}

설정은 플러그인별로 네임스페이스됩니다. 두 플러그인은 서로의 값을 읽거나 덮어쓰지 않고 같은 키를 사용할 수 있습니다.

동시 요청이 같은 키를 변경할 수 있으면 조건부 쓰기를 사용해 오래된 리비전에 기반한 업데이트를 거부하세요. 같은 메서드는 네이티브 플러그인과 샌드박스 플러그인 모두에서 동작합니다.

내부 상태와 캐시 값은 별도로 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 별칭은 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

다음 라우트는 세 값을 로드하고 검증된 폼 필드만 씁니다.

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을 참고하세요.