Les plugins en sandbox stockent la configuration propre au site via ctx.settings. Une page d’administration Block Kit charge les valeurs actuelles, accepte les modifications, les valide, puis les écrit dans le même stockage limité au plugin. Les champs déclarés avec type: "secret" sont chiffrés avant qu’EmDash ne les écrive dans la base de données.
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 ont un espace de noms 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 les écritures conditionnelles pour rejeter les mises à jour fondées sur une révision obsolète. Les mêmes méthodes fonctionnent dans les plugins natifs comme dans les plugins en sandbox.
Utilisez ctx.kv séparément pour l’état interne et les valeurs mises en cache :
| API | Usage | 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 d’espace de noms interne du plugin d’EmDash.
Les plugins existants peuvent continuer à lire ctx.kv.get("settings:<key>"). L’alias KV complet settings: reste pris en charge pendant toute la série 1.x. Toute suppression dans une version majeure ultérieure s’accompagnera d’une période de dépréciation et d’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 doivent être chiffrées. Un secret_input dans Block Kit ne fait que masquer la saisie dans le navigateur ; il ne marque pas à lui seul une valeur stockée comme secrète.
ctx.settings ne nécessite aucune capability, car l’hôte fixe son espace de noms au plugin courant. Ajouter un champ de paramètres n’étend pas le declaredAccess du plugin et ne déclenche pas de nouveau consentement aux 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 également 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 les 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 tant que l’utilisateur ne l’a pas modifié, et peuvent contenir une chaîne vide si l’utilisateur place le focus dans le champ puis l’efface. saveSettings n’écrit une nouvelle clé d’API que si la chaîne soumise n’est pas vide. La page utilise has_value pour indiquer qu’une valeur enregistrée existe sans renvoyer cette valeur au navigateur.
Block Kit est la référence canonique pour les interactions, les blocs, les éléments de formulaire, les builders et les champs conditionnels.
Valeurs secrètes
EmDash chiffre les champs secret du schéma avec AES-GCM. Les données authentifiées lient chaque valeur à son ID de plugin et à sa clé de paramètre : copier une enveloppe vers un autre plugin ou une autre clé fait donc échouer le déchiffrement. La première clé de EMDASH_ENCRYPTION_KEY chiffre les nouvelles écritures ; à la lecture, EmDash sélectionne les clés plus anciennes d’après leur empreinte. Les clés manquantes, erronées ou altérées échouent en mode fermé (fail closed). 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 la valeur immédiatement précédente de cette clé dans les messages de ctx.log et dans les données structurées.
Le plugin reçoit toujours le texte en clair et peut le transformer ou l’envoyer via l’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 de nouveau la valeur pour la remplacer par une enveloppe chiffrée. Si un secret ne doit pas être écrit dans la base de données d’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 en sandbox ne peuvent pas lire l’environnement du processus hôte ni les bindings de la plateforme.
Prévoyez une action distincte et délibérée si les utilisateurs doivent effacer un secret. Traiter un champ masqué vide comme une suppression peut effacer un identifiant fonctionnel lorsqu’un utilisateur enregistre un paramètre sans rapport.
Valeurs par défaut et mises à niveau
Appliquez les valeurs par défaut lors de la lecture d’une clé afin 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 les 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 version ultérieure ajoute un paramètre, les sites existants ne l’exécutent pas de nouveau. Conservez la valeur de 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 des soumissions ou des logs | Une collection ctx.storage déclarée |
| Contenu modifié 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’offre ni requêtes sur les champs ni index. Stockage 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. Consultez Votre premier plugin natif pour ce format.