Les plugins sandboxed stockent la configuration spécifique au site via ctx.settings. Une page d’administration Block Kit charge les valeurs actuelles, accepte les modifications, les valide et les écrit via le même magasin limité au plugin. Les champs déclarés avec type: "secret" sont chiffrés avant qu’EmDash ne les écrive en base.
Lire et écrire les paramètres
Chaque hook et chaque route reçoit cette interface de paramètres sur 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 }>>;
}
Les paramètres sont namespacés par plugin. Deux plugins peuvent utiliser la même clé sans lire ni écraser les valeurs de l’autre.
Lorsque des requêtes concurrentes peuvent modifier la même clé, utilisez des écritures conditionnelles pour rejeter les mises à jour basées sur une révision obsolète. Les mêmes méthodes fonctionnent dans les plugins natifs et sandboxed.
Utilisez ctx.kv séparément pour l’état interne et les valeurs mises en cache :
| API | Objectif | Exemple |
|---|---|---|
ctx.settings | Valeurs configurables par l’utilisateur | apiKey |
ctx.kv avec state: | État interne persistant | state:lastSync |
ctx.kv avec cache: | Données calculées ou distantes réutilisables | cache:feed |
Les appels suivants couvrent les opérations 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 renvoie null lorsque la clé n’existe pas. list renvoie les clés sans le préfixe de namespace interne du plugin EmDash.
Les plugins existants peuvent continuer à lire ctx.kv.get("settings:<key>"). L’alias KV complet settings: reste pris en charge pour le reste de la ligne de releases 0.x. EmDash ne le retirera pas avant 1.0, et tout retrait ultérieur inclura une période de dépréciation et un guide de migration. Le nouveau code doit utiliser ctx.settings.
Ajouter une page de paramètres
Déclarez la page dans emdash-plugin.jsonc pour qu’elle apparaisse dans la navigation d’administration du plugin :
"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 }
}
}
Le schéma indique à EmDash quelles valeurs nécessitent un chiffrement. Un secret_input dans Block Kit ne masque que la saisie navigateur ; il ne marque pas à lui seul une valeur stockée comme secret.
ctx.settings n’a besoin d’aucune capability car l’hôte fixe son namespace au plugin courant. Ajouter un champ de paramètres n’élargit pas le declaredAccess du plugin et ne déclenche pas un nouveau consentement de capabilities. Un administrateur accorde au plugin l’accès à un identifiant en le saisissant dans le formulaire de paramètres de ce plugin.
Le plugin doit aussi fournir une route privée nommée admin. EmDash envoie page_load à l’ouverture de la page et form_submit lorsque l’utilisateur soumet le formulaire.
Ajoutez @emdash-cms/blocks et zod pour utiliser le type de réponse et valider les interactions :
pnpm add @emdash-cms/blocks zod
La route suivante charge trois valeurs et n’écrit que des champs de formulaire validés :
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);
}
Les valeurs soumises omettent le secret jusqu’à ce que l’utilisateur le modifie, et peuvent contenir une chaîne vide si l’utilisateur focusse et vide le champ. saveSettings n’écrit une nouvelle clé API que lorsque la chaîne soumise est non vide. La page utilise has_value pour indiquer qu’une valeur enregistrée existe sans renvoyer la valeur au navigateur.
Block Kit est la référence canonique pour les interactions, blocs, éléments de formulaire, builders et champs conditionnels.
Valeurs secrètes
EmDash chiffre les champs de schéma secret avec AES-GCM. Les données authentifiées lient chaque valeur à son ID de plugin et sa clé de paramètre, de sorte que copier un envelope vers un autre plugin ou une autre clé fait échouer le déchiffrement. La première clé dans EMDASH_ENCRYPTION_KEY chiffre les nouvelles écritures ; EmDash sélectionne les clés plus anciennes par leur empreinte à la lecture. Les clés manquantes, incorrectes ou altérées échouent de façon fermée. Les réponses d’administration et les erreurs de l’hôte ne contiennent pas le texte en clair. Après qu’un plugin a lu ou écrit un secret, le logger de l’hôte masque la valeur exacte actuelle et immédiatement précédente pour cette clé dans les messages ctx.log et les données structurées.
Le plugin reçoit toujours le texte en clair et peut le transformer ou l’envoyer via un accès réseau ou e-mail déclaré. Examinez ces capabilities avant de saisir un identifiant, et ne journalisez jamais de matériel secret dérivé ou encodé.
Les valeurs en clair existantes restent lisibles. Enregistrez à nouveau la valeur pour la remplacer par un envelope chiffré. Si un secret ne doit pas être écrit dans la base EmDash même sous forme chiffrée, utilisez un plugin natif adossé à un secret de déploiement ou un service d’identifiants externe. Les plugins sandboxed ne peuvent pas lire l’environnement du processus hôte ni les bindings de plateforme.
Fournissez une action séparée et délibérée si les utilisateurs doivent effacer un secret. Traiter un champ masqué vide comme une suppression peut effacer un identifiant valide lorsqu’un utilisateur enregistre un paramètre sans rapport.
Valeurs par défaut et mises à niveau
Appliquez des valeurs par défaut à la lecture d’une clé pour que les installations existantes reçoivent un nouveau paramètre sans migration :
const enabled = (await ctx.settings.get<boolean>("enabled")) ?? true;
const maxItems = (await ctx.settings.get<number>("maxItems")) ?? 100;
Vous pouvez persister des valeurs initiales pendant l’installation :
hooks: {
"plugin:install": async (_event, ctx) => {
await ctx.settings.set("enabled", true);
await ctx.settings.set("maxItems", 100);
},
},
plugin:install ne s’exécute que pour une nouvelle installation. Lorsqu’une release ultérieure ajoute un paramètre, les sites existants ne le réexécutent pas. Conservez le repli à la lecture, ou initialisez la clé manquante de façon idempotente pendant plugin:activate.
Choisir KV ou storage
| Données | Utiliser |
|---|---|
| Petites valeurs configurables par l’utilisateur | ctx.settings |
| Petit état interne ou curseurs | ctx.kv avec un préfixe state: |
| Enregistrements interrogeables tels que soumissions ou logs | Une collection ctx.storage déclarée |
| Contenu édité via l’éditeur EmDash habituel | Une collection de contenu du site |
KV prend en charge l’accès direct par clé et le listage par préfixe, mais n’a ni requêtes par champ ni index. Storage fournit des collections de documents avec filtrage indexé, tri, comptage et pagination.
Les plugins natifs peuvent à la place déclarer admin.settingsSchema dans definePlugin() et laisser EmDash générer le formulaire. Voir Your first native plugin pour ce format.