Plugins können API-Routen für ihre Admin-Oberfläche und externe Integrationen bereitstellen. Routen werden unter /_emdash/api/plugins/<slug>/<route-name> eingehängt (<slug> ist das Feld slug des Plugins aus emdash-plugin.jsonc — zur Laufzeit als ctx.plugin.id verfügbar) und laufen innerhalb der Sandbox-Laufzeit mit demselben PluginContext, den auch Hooks erhalten.
Diese Seite behandelt Sandboxed Plugins. Native Plugins verwenden dieselben Routenoptionen, dieselbe Authentifizierung und dasselbe URL-Layout, aber ihre Handler erhalten ein einziges kombiniertes Kontextobjekt. Diese Signatur finden Sie unter Ihr erstes natives Plugin.
Routen definieren
Deklarieren Sie Routen im Default-Export von src/plugin.ts. Fügen Sie zod als Laufzeitabhängigkeit hinzu, wenn eine Route Eingaben validiert oder als MCP-Tool bereitgestellt wird:
pnpm add zod
Das folgende Beispiel validiert eine Anfrage nach Einsendungen und fragt den Plugin-Speicher ab:
import type { SandboxedPlugin } from "emdash/plugin";
import { z } from "zod";
const submissionsInput = z.object({
formId: z.string().optional(),
limit: z.coerce.number().int().min(1).max(100).default(50),
cursor: z.string().optional(),
});
const plugin: SandboxedPlugin = {
routes: {
status: {
handler: async (_routeCtx, ctx) => {
return { ok: true, plugin: ctx.plugin.id };
},
},
submissions: {
handler: async (routeCtx, ctx) => {
const parsed = submissionsInput.safeParse(routeCtx.input);
if (!parsed.success) {
return { ok: false, error: { code: "VALIDATION_ERROR" } };
}
const { formId, limit, cursor } = parsed.data;
const result = await ctx.storage.submissions.query({
where: formId ? { formId } : undefined,
orderBy: { createdAt: "desc" },
limit,
cursor,
});
return { ok: true, ...result };
},
},
},
};
export default plugin;
Die Annotation SandboxedPlugin leitet die Typen der Route und des Plugin-Kontexts ab, daher benötigen die Parameter keine Annotationen. Sandboxed Route-Handler erhalten zwei Argumente: (routeCtx, ctx).
routeCtxenthält anfragebezogene Daten:{ input, request, requestMeta }. Seininputbleibtunknown, validieren Sie ihn daher vor der Verwendung.ctxist derselbePluginContext, den Sie in Hooks erhalten —ctx.storage,ctx.settings,ctx.kv,ctx.content,ctx.httpundctx.log.
Indizierte Inhaltsfelder filtern
Plugins mit der Capability content:read können benutzerdefinierte Felder filtern, die eine Sammlung als indexed markiert. Filter laufen in der Datenbank und werden mit AND-Semantik kombiniert:
const result = await ctx.content.list("items", {
where: {
fieldFilters: {
priority: { in: ["urgent", "high"] },
score: { gte: 80 },
resolved: false,
},
},
});
Skalare Werte verwenden exakten Abgleich. Verwenden Sie null für den Abgleich mit Null, { in: [...] } für eine Menge exakter Werte oder gt, gte, lt und lte für Bereichsvergleiche. EmDash lehnt Filter für nicht indizierte Felder, Werte, die nicht zum Feldtyp passen, und mehr als 20 Feldfilter pro Abfrage ab. Ein in-Filter akzeptiert höchstens 50 Werte, und alle exakten Werte, Bereichsgrenzen und in-Mitglieder zusammen haben ein Budget von 50 Operanden pro Abfrage. Null-Abgleiche verbrauchen dieses Budget nicht.
Routen-URLs
Routen werden unter /_emdash/api/plugins/<slug>/<route-name> eingehängt. Routennamen können Schrägstriche für verschachtelte Pfade enthalten.
| Plugin-ID | Routenname | URL |
|---|---|---|
forms | status | /_emdash/api/plugins/forms/status |
forms | submissions | /_emdash/api/plugins/forms/submissions |
seo | settings/save | /_emdash/api/plugins/seo/settings/save |
analytics | events/recent | /_emdash/api/plugins/analytics/events/recent |
Authentifizierung und CSRF
Plugin-Routen sind standardmäßig authentifiziert. Der Dispatcher verlangt eine Sitzung (oder ein Token mit dem Scope admin), bevor er Ihren Handler aufruft. Private Routen verwenden aus Gründen der Abwärtskompatibilität standardmäßig die Berechtigung plugins:manage. Setzen Sie permission auf eine engere EmDash-RBAC-Berechtigung, wenn der Vorgang zu einer bestehenden Capability für Inhalte, Medien, Schema oder Einstellungen gehört:
routes: {
create: {
permission: "content:create",
handler: async (routeCtx, ctx) => {
// Validate routeCtx.input, then create content through ctx.
},
},
},
Private Routen verlangen ihre deklarierte Berechtigung für jede HTTP-Methode. Für Cookie-authentifizierte Anfragen verlangen sie außerdem den CSRF-Header X-EmDash-Request: 1, auch für GET und HEAD, weil eine Plugin-Route für jede Methode denselben Handler ausführen kann. Die Admin-Oberfläche sendet den Header automatisch. Token-authentifizierte Anfragen sind von dem Header ausgenommen, benötigen aber weiterhin den Token-Scope admin und die Routenberechtigung.
Um eine Route von der Authentifizierung auszunehmen, markieren Sie sie mit public: true:
routes: {
track: {
public: true,
handler: async (routeCtx, ctx) => {
const parsed = z.object({ event: z.string() }).safeParse(routeCtx.input);
if (!parsed.success) return { ok: false, error: "INVALID_EVENT" };
ctx.log.info("Tracked", { event: parsed.data.event });
return { ok: true };
},
},
},
Die Bereitstellung öffentlicher Routen ist Teil des geprüften Zugriffs eines Plugins. Das Installieren eines Plugins mit öffentlichen Routen erfordert eine Zustimmung. Wird eine öffentliche Route hinzugefügt oder eine private Route auf öffentlich umgestellt, ist bei der Aktualisierung des Plugins erneut eine Zustimmung erforderlich.
Der authentifizierte Aufrufer
Bei privaten Routen ist routeCtx.user der authentifizierte Benutzer, der die Anfrage stellt — von EmDash aufgelöst und autorisiert, bevor Ihr Handler läuft, sodass Sie ihm für benutzerbezogene Logik vertrauen können (API-Schlüssel pro Benutzer, OAuth-Verbindungen, vom Plugin verwaltete Präferenzen):
routes: {
"connect/start": {
handler: async (routeCtx, ctx) => {
// Never read the acting user from the request body — any authenticated
// session could impersonate another user that way. Use routeCtx.user.
const caller = routeCtx.user;
if (!caller) throw new Error("No caller bound");
await ctx.kv.set(`user:${caller.id}:connection`, { startedAt: Date.now() });
return { userId: caller.id };
},
},
},
routeCtx.user ist bei öffentlichen Routen undefined (sie überspringen die Authentifizierung, daher ist kein Aufrufer gebunden — selbst wenn der Besucher zufällig eine Admin-Sitzung hat) und bei Token-authentifizierten Anfragen, deren Token nicht an einen Benutzer gebunden ist (Maschinen-Token). Die Form entspricht dem von ctx.users zurückgegebenen UserInfo: { id, email, name, role, createdAt } — keine sensiblen Felder.
Beachten Sie, dass die Identität des Aufrufers von der Capability users:read getrennt ist: routeCtx.user sagt Ihnen, wer aufruft, und ist bei privaten Routen immer verfügbar, während ctx.users eine Abfrage des Benutzerverzeichnisses ist, die die Capability erfordert.
Eine Route als MCP-Tool bereitstellen
Plugins können ausgewählte private Routen ausdrücklich über den MCP-Server von EmDash bereitstellen. Die MCP-Bereitstellung wird nie aus der Routenliste abgeleitet:
const createEventInput = z.object({
title: z.string().min(1),
startsAt: z.string().datetime(),
});
const plugin: SandboxedPlugin = {
routes: {
"events/create": {
permission: "content:create",
handler: async (routeCtx, ctx) => {
const parsed = createEventInput.safeParse(routeCtx.input);
if (!parsed.success) return { ok: false, error: "INVALID_EVENT" };
const input = parsed.data;
return { id: await createEvent(input, ctx) };
},
},
},
mcp: {
tools: {
createEvent: {
description: "Create a calendar event when the user asks to add one.",
route: "events/create",
input: createEventInput,
output: z.object({ id: z.string() }),
destructive: false,
},
},
},
};
export default plugin;
EmDash stellt dies als <pluginId>__createEvent bereit. Die referenzierte Route muss privat sein und permission deklarieren. Eingabeschemata sind erforderlich; Ausgabeschemata sind optional. Setzen Sie destructive: true für Tools, die löschen, überschreiben, veröffentlichen, abbuchen oder anderweitig eine schwer umkehrbare Aktion ausführen.
Ein Administrator muss die MCP-Tools eines Plugins separat aktivieren, nachdem er deren Namen, Beschreibungen, Routen, Berechtigungen und Destructive-Flags geprüft hat. Der Aufruf des Tools erfordert dann sowohl die Routenberechtigung als auch entweder den Token-Scope mcp:tools oder mcp:tools:<pluginId>.
Ein MCP-Tool kann keine Route mit response: "raw" referenzieren. MCP-Tools verwenden den JSON-Routenvertrag.
Request-Bodies
Routen ohne request-Deklaration behalten das ursprüngliche Eingabeverhalten. EmDash parst JSON-Request-Bodies für POST, PUT und PATCH sowie Query-Parameter für GET, HEAD und DELETE. Der geparste Wert erreicht einen Sandboxed Handler als routeCtx.input: unknown.
Deklarieren Sie request.body, wenn die Route ein anderes Body-Format oder ein bestimmtes Byte-Limit benötigt. Die verfügbaren Modi sind none, json, text, bytes und form-data. Request-Bodies werden gepuffert. Das Standardmaximum beträgt 1 MiB, und eine Route kann maxBytes auf höchstens 8 MiB anheben.
Verwenden Sie pluginRoute(), um den Eingabetyp aus dem deklarierten Body-Modus abzuleiten. Der Helfer gibt sein Argument zur Laufzeit unverändert zurück:
import { pluginRoute, type SandboxedPlugin } from "emdash/plugin";
const plugin: SandboxedPlugin = {
routes: {
import: pluginRoute({
methods: ["POST"],
request: {
body: "bytes",
maxBytes: 4 * 1024 * 1024,
headers: ["content-type", "x-import-signature"],
},
handler: async (routeCtx) => {
const bytes = routeCtx.input; // Uint8Array
const signature = routeCtx.request.headers["x-import-signature"];
return { accepted: bytes.byteLength, signature };
},
}),
},
};
export default plugin;
Bei body: "none" ist routeCtx.input der geparste Query-String-Datensatz. Eine json-Deklaration belässt den Eingabetyp bei unknown, validieren Sie ihn daher vor der Verwendung. Eine text-Deklaration liefert einen String, und bytes liefert ein Uint8Array.
form-data akzeptiert multipart/form-data und application/x-www-form-urlencoded. Es erzeugt ein geordnetes entries-Array. Text-Einträge enthalten { name, kind: "text", value }; Datei-Einträge enthalten { name, kind: "file", filename, contentType, bytes }. EmDash akzeptiert höchstens 100 Teile, 1 MiB pro Teil und Dateinamen bis zu 255 UTF-8-Bytes. Dateinamen dürfen keine Steuerzeichen oder Pfadtrennzeichen enthalten. Die gesamte codierte Anfrage muss außerdem in das Body-Limit der Route passen.
Validieren Sie geparste Werte, bevor Sie Felder lesen oder Seiteneffekte ausführen. Verwenden Sie safeParse, wenn ungültige Eingaben ein erwarteter Fehler des Aufrufers sind. So kann die Route ein stabiles JSON-Ergebnis zurückgeben, statt ungültige Eingaben in eine interne Ausnahme zu verwandeln:
const createInput = z.object({
title: z.string().min(1).max(200),
email: z.string().email(),
priority: z.enum(["low", "medium", "high"]).default("medium"),
tags: z.array(z.string()).optional(),
});
routes: {
create: {
handler: async (routeCtx, ctx) => {
const parsed = createInput.safeParse(routeCtx.input);
if (!parsed.success) {
return { ok: false, error: { code: "VALIDATION_ERROR" } };
}
const { title, email, priority, tags } = parsed.data;
await ctx.storage.items.put(`item_${Date.now()}`, {
title,
email,
priority,
tags: tags ?? [],
createdAt: new Date().toISOString(),
});
return { ok: true };
},
},
},
Query-String-Eingabe (GET/HEAD/DELETE)
Methoden ohne Body haben keinen Request-Body, daher stammt ihre Eingabe aus dem URL-Query-String. Jeder Wert ist ein String. Wiederholte Schlüssel werden zu Arrays, sodass ?tag=a&tag=b zu { tag: ["a", "b"] } wird; ein einzelnes ?tag=a bleibt { tag: "a" }. Verwenden Sie z.coerce für Zahlen und andere Werte, die keine Strings sind:
const listInput = z.object({
status: z.enum(["open", "closed"]).optional(),
limit: z.coerce.number().int().min(1).max(100).default(20),
tag: z.union([z.string(), z.array(z.string())]).optional(),
});
routes: {
list: {
// GET /_emdash/api/plugins/<slug>/list?status=open&limit=20&tag=a&tag=b
handler: async (routeCtx, ctx) => {
const parsed = listInput.safeParse(routeCtx.input);
if (!parsed.success) return { ok: false, error: "INVALID_QUERY" };
const { status, limit, tag } = parsed.data;
// ...
},
},
},
JSON-Rückgabewerte
Routen verwenden den JSON-Antwortvertrag, sofern sie nicht response: "raw" deklarieren. Geben Sie einen beliebigen JSON-serialisierbaren Wert zurück. Der Dispatcher verpackt ihn in den Standard-Umschlag von EmDash ({ success: true, data: <your value> }) und liefert ihn als application/json aus.
return { id: "abc", count: 42 }; // wrapped to { success: true, data: { id, count } }
return [1, 2, 3]; // wrapped to { success: true, data: [1, 2, 3] }
Fehler
Werfen Sie eine Ausnahme, wenn eine Sandboxed Route nicht abgeschlossen werden kann. EmDash protokolliert die Ausnahme und gibt einen ROUTE_ERROR zurück. Die geworfene Meldung kann in dieser Antwort enthalten sein, geben Sie daher in einer Ausnahmemeldung niemals Zugangsdaten, personenbezogene Daten, interne Pfade oder Stacktraces an:
handler: async (_routeCtx, ctx) => {
try {
return await refreshRemoteIndex(ctx);
} catch {
ctx.log.error("Remote index refresh failed");
throw new Error("Remote index refresh failed");
}
},
Sandboxed Plugin-Code kann durch Werfen einer Response keinen beliebigen HTTP-Status wählen; eine Response überquert nicht die Grenze jedes Sandbox-Runners als strukturierter Fehler. EmDash weist Status für Fehler bei Authentifizierung, Autorisierung, CSRF und fehlenden Routen zu, bevor der Handler läuft. Geben Sie für erwartete Validierungs- und Domänenergebnisse ein JSON-Ergebnis zurück und reservieren Sie Ausnahmen für unerwartete Fehler.
Ein als JSON zurückgegebener erwarteter Fehler verwendet weiterhin die erfolgreiche HTTP-Antwort der Route und erscheint innerhalb des äußeren Umschlags { success: true, data: ... } von EmDash. Nehmen Sie einen stabilen Code auf Anwendungsebene auf, damit Clients dieses Ergebnis unterscheiden können.
HTTP-Methoden
Der Routenname wählt genau einen Handler aus. Deklarieren Sie methods, um einzuschränken, welche HTTP-Methoden ihn aufrufen können. EmDash gibt 405 Method Not Allowed mit einem Allow-Header zurück, bevor der Handler aufgerufen wird, wenn die Request-Methode nicht deklariert ist:
routes: {
item: {
methods: ["GET", "DELETE"],
handler: async (routeCtx, ctx) => {
const parsed = z.object({ id: z.string() }).safeParse(routeCtx.input);
if (!parsed.success) return { ok: false, error: "INVALID_ID" };
const { id } = parsed.data;
switch (routeCtx.request.method) {
case "GET":
return await ctx.storage.items.get(id);
case "DELETE":
await ctx.storage.items.delete(id);
return { deleted: true };
}
},
},
},
Routen ohne methods bleiben aus Kompatibilitätsgründen methodenunabhängig. Prüfen Sie in einer älteren Route routeCtx.request.method, bevor Sie eine Änderung vornehmen, oder fügen Sie methods hinzu, damit der Host die Einschränkung durchsetzt.
Raw-Antworten
Deklarieren Sie response: "raw", wenn eine Route unverpackten Text oder Bytes mit einem benutzerdefinierten Status und sicheren Antwort-Headern zurückgeben muss. Geben Sie pluginResponse() aus emdash/plugin zurück; eine WHATWG-Response überquert die Sandbox-Grenze nicht:
import { pluginResponse, pluginRoute, type SandboxedPlugin } from "emdash/plugin";
const plugin: SandboxedPlugin = {
routes: {
download: pluginRoute({
public: true,
methods: ["GET"],
request: { body: "none" },
response: "raw",
cacheControl: "public, max-age=60",
handler: async () =>
pluginResponse({
status: 200,
headers: {
"content-type": "text/csv; charset=utf-8",
"content-disposition": 'attachment; filename="report.csv"',
},
body: { kind: "text", value: "name,count\nPublished,12\n" },
}),
}),
},
};
export default plugin;
Der Antwort-Body ist { kind: "text", value: string } oder { kind: "bytes", value: Uint8Array } und wird bis zu 8 MiB gepuffert. Raw-Antworten dürfen Accept-Ranges, Content-Disposition, Content-Encoding, Content-Language, Content-Range, Content-Type, ETag, Last-Modified, Location und Retry-After setzen; der Host entfernt jeden anderen vom Plugin gelieferten Header. Er fügt X-Content-Type-Options: nosniff, eine Content Security Policy für Sandbox-Dokumente und Referrer-Policy: no-referrer hinzu. Das cacheControl der Route wendet er nur auf erfolgreiche öffentliche GET- und HEAD-Antworten an. Andere Antworten verwenden private, no-store.
Raw-Routen können keine aktiven Inhalte derselben Origin ausliefern. EmDash lehnt die Medientypen HTML, JavaScript und ECMAScript, XHTML, SVG, XML, CSS, WebAssembly, multipart/related und multipart/x-mixed-replace ab. Verwenden Sie ein natives Plugin oder eine separate Origin, wenn die Antwort aktive Browserinhalte ausführen muss.
Auf die Anfrage zugreifen
routeCtx.request ist eine SandboxedRequest: ein portabler Datensatz { url, method, headers }, der sich im selben Prozess und innerhalb eines Isolates identisch verhält. headers ist ein Record<string, string>, dessen Schlüssel kleingeschriebene Header-Namen sind — greifen Sie über den kleingeschriebenen Namen darauf zu oder iterieren Sie mit Object.entries. url ist ein String, daher parst new URL(request.url) die Query-Parameter. routeCtx.requestMeta enthält IP, User-Agent und Geodaten, plattformübergreifend normalisiert, sofern verfügbar.
Bei einer Route mit request-Deklaration erreichen nur die in request.headers aufgeführten Namen den Handler. EmDash lehnt Deklarationen für Zugangsdaten, Cookies, Cloudflare-Access-Header, Proxy-Autorisierung, Set-Cookie und den CSRF-Header X-EmDash-Request ab. Es entfernt diese Header aus jeder Sandboxed Anfrage, auch bei älteren Routen.
handler: async (routeCtx, ctx) => {
const { request, requestMeta } = routeCtx;
const signature = request.headers["x-import-signature"]; // lowercased key, no .get()
const url = new URL(request.url);
const page = url.searchParams.get("page");
ctx.log.info("Request", { meta: requestMeta });
if (request.method !== "POST") return { error: "POST_REQUIRED" };
},
Häufige Muster
Settings und paginierte Daten
Plugin-Einstellungen verwenden private Routen, Block-Kit-Formulare und ctx.settings. Einstellungen enthält das vollständige Muster für Laden, Validierung, Formular und verschlüsselte Geheimnisse.
Routen, die Plugin-Daten auflisten, sollten den Cursor aus ctx.storage.<collection>.query() zurückgeben. Storage-Paginierung zeigt, wie Sie einen Cursor übergeben und mehrere Seiten abarbeiten, ohne das Seitenmaximum von 100 Einträgen zu überschreiten.
Externer API-Proxy
Leiten Sie eine Anfrage über ctx.http an einen externen Dienst weiter (erfordert die Capability network:request und einen Eintrag in allowedHosts):
routes: {
forecast: {
handler: async (routeCtx, ctx) => {
const parsed = z.object({ city: z.string().min(1) }).safeParse(routeCtx.input);
if (!parsed.success) return { ok: false, error: "INVALID_CITY" };
if (!ctx.http) throw new Error("Network capability not granted");
const apiKey = await ctx.settings.get<string>("apiKey");
if (!apiKey) throw new Error("API key not configured");
const response = await ctx.http.fetch(
`https://api.weather.example.com/forecast?city=${encodeURIComponent(parsed.data.city)}`,
{ headers: { "X-API-Key": apiKey } },
);
if (!response.ok) {
throw new Error(`Weather API error: ${response.status}`);
}
return response.json();
},
},
},
ctx.http.fetch() gibt in beiden Sandbox-Runnern eine gepufferte WHATWG-Response zurück. Binäre Methoden wie arrayBuffer() und blob() erhalten die Bytes über Cloudflare Worker Loader und Node/workerd hinweg. Request- und Response-Bodies sind jeweils auf 8 MiB dekodierte Daten begrenzt. Weiterleitungsziele werden vor jedem Hop geprüft, und Credential-Header werden entfernt, wenn eine Weiterleitung die Origin wechselt.
Routen aus Block Kit aufrufen
Sandboxed Plugins liefern keinen React-Code an das Admin aus. Deklarieren Sie eine admin-Route und geben Sie Block-Kit-Antworten zurück. EmDash sendet die Interaktionen page_load, block_action und form_submit mit der richtigen URL und dem CSRF-Header an diese private Route. Block Kit zeigt den Interaktionsvertrag und eine vollständige Route.
Routen aus Queue- und Scheduled-Handlern aufrufen
Handler für Plattformereignisse (ein Cloudflare-Queue-Consumer, ein benutzerdefinierter scheduled()-Handler) haben keine HTTP-Anfrage und daher kein locals.emdash. Verwenden Sie withEmDashRuntime() aus emdash/middleware, um die Laufzeit direkt zu erhalten und eine Plugin-Route ohne Anfrage aufzurufen:
import { withEmDashRuntime } from "emdash/middleware";
export default {
// ... fetch/scheduled from @emdash-cms/cloudflare/worker
async queue(batch: MessageBatch) {
await withEmDashRuntime(async (runtime) => {
for (const message of batch.messages) {
const result = await runtime.handlePluginApiRoute(
"my-plugin",
"POST",
"/finishJob",
new Request("https://internal/", {
method: "POST",
body: JSON.stringify(message.body),
}),
);
if (result.success) message.ack();
else message.retry();
}
});
},
};
Dies löst dieselbe zwischengespeicherte Laufzeit auf, die auch Request-Handler verwenden, sodass sich Plugin-Speicher, Hooks und Medienzugriff exakt so verhalten wie während einer Anfrage. Bei verbindungsbasierten Datenbankadaptern (z. B. Postgres über Hyperdrive) läuft der Callback unter einer ereignisbezogenen Verbindung, die beim Zurückkehren committet und geschlossen wird.
Routen extern aufrufen
Öffentliche Routen können direkt aufgerufen werden:
curl -X POST https://your-site.com/_emdash/api/plugins/forms/track \
-H "Content-Type: application/json" \
-d '{"event": "pageview"}'
Private Routen benötigen Sitzungs-Zugangsdaten plus X-EmDash-Request: 1 oder ein API-Token mit dem Scope admin. Die folgende Server-zu-Server-Anfrage verwendet ein Token:
curl -X POST https://your-site.com/_emdash/api/plugins/forms/create \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"title": "Hello", "email": "[email protected]"}'
Routenkontext-Referenz
Die folgenden Interfaces fassen die portablen Werte zusammen, die einem Sandboxed Route-Handler zur Verfügung stehen:
// What sandboxed route handlers receive as their two arguments
interface SandboxedRequest {
url: string;
method: string;
headers: Record<string, string>; // lowercased keys
}
interface SandboxedRouteContext {
input: unknown; // validate inside the handler before use
request: SandboxedRequest;
requestMeta?: unknown;
user?: UserInfo; // authenticated caller on private routes; undefined on public routes
}
interface UserInfo {
id: string;
email: string;
name: string | null;
role: number;
createdAt: string;
}
interface PluginContext {
plugin: { id: string; version: string };
storage: PluginStorage;
kv: KVAccess;
log: LogAccess;
site: SiteInfo;
url(path: string): string;
cron?: CronAccess;
content?: ContentAccess; // when content:read or content:write declared
schema?: SchemaAccess; // when schema:read declared
taxonomies?: TaxonomyAccess; // when taxonomies:read declared
bylines?: BylineAccess; // when bylines:read declared
redirects?: RedirectAccess; // when redirects:read or redirects:write declared
media?: MediaAccess; // when any media capability is declared
http?: HttpAccess; // when network:request declared
users?: UserAccess; // when users:read declared
email?: EmailAccess; // when email:send declared and provider configured
}
Native Plugins erhalten ein einziges RouteContext-Argument, das beides kombiniert — siehe Ihr erstes natives Plugin, wenn Sie diesen Weg gehen.