Plugins können API-Routen für ihre Admin-UI und externe Integrationen bereitstellen. Routen werden unter /_emdash/api/plugins/<slug>/<route-name> eingehängt (der <slug> ist das slug-Feld des Plugins aus emdash-plugin.jsonc — zur Laufzeit als ctx.plugin.id verfügbar) und laufen in der Sandbox-Runtime mit demselben PluginContext, den Hooks erhalten.
Diese Seite behandelt Sandbox-Plugins. Native Plugins nutzen dieselben Routenoptionen, Authentifizierung und URL-Struktur, aber ihre Handler erhalten ein kombiniertes Kontextobjekt. Siehe Your first native plugin für diese Signatur.
Routen definieren
Deklarieren Sie Routen im Default-Export von src/plugin.ts. Fügen Sie zod als Runtime-Abhängigkeit hinzu, wenn eine Route Eingaben validiert oder als MCP-Tool bereitgestellt wird:
pnpm add zod
Das folgende Beispiel validiert eine Submissions-Anfrage und fragt Plugin-Storage 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 für Routen- und Plugin-Kontext ab, sodass die Parameter keine Annotationen brauchen. Sandbox-Routen-Handler nehmen zwei Argumente: (routeCtx, ctx).
routeCtxträgt anfrageförmige Daten:{ input, request, requestMeta }. Seininputbleibtunknown, daher vor der Nutzung validieren.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 Collection als
indexed markiert. Filter laufen in der Datenbank und kombinieren mit AND-Semantik:
const result = await ctx.content.list("items", {
where: {
fieldFilters: {
priority: { in: ["urgent", "high"] },
score: { gte: 80 },
resolved: false,
},
},
});
Skalare Werte nutzen exakten Abgleich. Verwenden Sie null für Null-Abgleich, { in: [...] } für eine Menge exakter
Werte, oder gt, gte, lt und lte für Bereichsvergleiche. EmDash lehnt Filter für Felder ab,
die nicht indiziert sind, Werte, die nicht zum Feldtyp passen, und mehr als 20 Feldfilter pro
Abfrage. 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-Treffer 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 | Route name | 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 Session (oder ein Token mit dem Scope admin), bevor er Ihren Handler aufruft. Private Routen defaulten aus Kompatibilitätsgründen auf die Berechtigung plugins:manage. Setzen Sie permission auf eine engere EmDash-RBAC-Berechtigung, wenn die Operation zu einer bestehenden Content-, Media-, Schema- oder Settings-Capability 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. Sie verlangen auch den CSRF-Header X-EmDash-Request: 1 für cookie-authentifizierte Anfragen, einschließlich GET und HEAD, weil eine Plugin-Route denselben Handler für jede Methode ausführen kann. Die Admin-UI sendet den Header automatisch. Token-authentifizierte Anfragen sind vom Header befreit, brauchen 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 Freigabe öffentlicher Routen ist Teil des geprüften Zugriffs des Plugins. Die Installation eines Plugins mit öffentlichen Routen erfordert Zustimmung. Das Hinzufügen einer öffentlichen Route oder das Ändern einer privaten Route zu öffentlich erfordert bei einem Plugin-Update erneut Zustimmung.
Der authentifizierte Aufrufer
Auf 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 ihn für benutzerbezogene Logik vertrauen können (benutzerbezogene API-Schlüssel, OAuth-Verbindungen, pluginverwaltete Einstellungen):
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 undefined auf öffentlichen Routen (sie überspringen Auth, daher ist kein Aufrufer gebunden — selbst wenn der Besucher zufällig eine Admin-Session hat) und bei token-authentifizierten Anfragen, bei denen das Token nicht an einen Benutzer gebunden ist (Maschinentokens). Die Form entspricht dem von ctx.users zurückgegebenen UserInfo: { id, email, name, role, createdAt } — ohne sensible Felder.
Beachten Sie, dass die Aufruferidentität von der Capability users:read getrennt ist: routeCtx.user sagt Ihnen wer aufruft und ist auf privaten Routen immer verfügbar, während ctx.users eine Benutzerverzeichnis-Abfrage ist, die die Capability erfordert.
Eine Route als MCP-Tool bereitstellen
Plugins können ausgewählte private Routen explizit über den MCP-Server von EmDash bereitstellen. 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, belasten oder sonst eine schwer rückgängig zu machende Aktion ausführen.
Ein Administrator muss die MCP-Tools eines Plugins separat aktivieren, nachdem er 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 nutzen 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 Sandbox-Handler als routeCtx.input: unknown.
Deklarieren Sie request.body, wenn die Route ein anderes Body-Format oder ein bestimmtes Byte-Limit braucht. Die
verfügbaren Modi sind none, json, text, bytes und form-data. Request-Bodies werden gepuffert.
Das Standardmaximum ist 1 MiB, und eine Route kann maxBytes auf höchstens 8 MiB erhöhen.
Verwenden Sie pluginRoute(), um den Eingabetyp aus dem deklarierten Body-Modus abzuleiten. Der Helper 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-Record. Eine json-Deklaration hält
den Eingabetyp als unknown, daher vor der Nutzung validieren. Eine text-Deklaration erzeugt einen String, und
bytes erzeugt ein Uint8Array.
form-data akzeptiert multipart/form-data und application/x-www-form-urlencoded. Es erzeugt ein
geordnetes entries-Array. Textobjekte enthalten { name, kind: "text", value }; Dateieinträge enthalten
{ name, kind: "file", filename, contentType, bytes }. EmDash akzeptiert höchstens 100 Teile, 1 MiB pro
Teil und Dateinamen bis 255 UTF-8-Bytes. Dateinamen dürfen keine Steuerzeichen oder Pfad-
trenner enthalten. Die gesamte kodierte Anfrage muss auch in das Body-Limit der Route passen.
Validieren Sie geparste Werte, bevor Sie Felder lesen oder Nebenwirkungen ausführen. Verwenden Sie safeParse, wenn
ungültige Eingabe ein erwarteter Aufruferfehler ist. So kann die Route ein stabiles JSON-Ergebnis zurückgeben, statt
ungültige Eingabe 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 kommt 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 Nicht-String-Werte:
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 nutzen den JSON-Antwortvertrag, sofern sie nicht response: "raw" deklarieren. Geben Sie jeden
JSON-serialisierbaren Wert zurück. Der Dispatcher wickelt ihn in EmDashs Standard-Envelope
({ success: true, data: <your value> }) und liefert ihn als application/json.
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, wenn eine Sandbox-Route nicht abschließen kann. EmDash protokolliert die Ausnahme und gibt einen ROUTE_ERROR zurück. Die geworfene Nachricht kann in dieser Antwort enthalten sein, legen Sie also niemals Anmeldedaten, personenbezogene Daten, interne Pfade oder Stacktraces in eine Ausnahmenachricht:
handler: async (_routeCtx, ctx) => {
try {
return await refreshRemoteIndex(ctx);
} catch {
ctx.log.error("Remote index refresh failed");
throw new Error("Remote index refresh failed");
}
},
Sandbox-Plugin-Code kann keinen beliebigen HTTP-Status wählen, indem er eine Response wirft; eine Response überquert nicht jede Sandbox-Runner-Grenze als strukturierter Fehler. EmDash weist Statuscodes für Authentifizierungs-, Autorisierungs-, CSRF- und fehlende-Routen-Fehler zu, bevor der Handler läuft. Geben Sie für erwartete Validierungs- und Domänenergebnisse ein JSON-Ergebnis zurück und behalten Sie Ausnahmen für unerwartete Fehler.
Ein als JSON zurückgegebener erwarteter Fehler nutzt weiterhin die erfolgreiche HTTP-Antwort der Route und erscheint innerhalb von EmDashs äußerem Envelope { success: true, data: ... }. Fügen Sie einen stabilen anwendungsbezogenen Code hinzu, damit Clients dieses Ergebnis unterscheiden können.
HTTP-Methoden
Der Routenname wählt einen Handler. Deklarieren Sie methods, um einzuschränken, welche HTTP-Methoden ihn aufrufen dürfen.
EmDash gibt 405 Method Not Allowed mit einem Allow-Header zurück, bevor der Handler aufgerufen wird, wenn die
Anfragemethode 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 methodenagnostisch. Prüfen Sie
routeCtx.request.method in einer Legacy-Route, bevor Sie eine Mutation ausführen, oder fügen Sie methods hinzu, um
die Einschränkung vom Host erzwingen zu lassen.
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 Antwortkörper 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 sandboxed Document Content Security Policy und
Referrer-Policy: no-referrer hinzu. Er wendet das cacheControl der Route nur auf erfolgreiche öffentliche
GET- und HEAD-Antworten an. Andere Antworten nutzen private, no-store.
Raw-Routen können keinen aktiven same-origin-Inhalt ausliefern. EmDash lehnt HTML, JavaScript und
ECMAScript, XHTML, SVG, XML, CSS, WebAssembly, multipart/related und
multipart/x-mixed-replace-Medientypen ab. Verwenden Sie ein natives Plugin oder einen separaten Origin, wenn die Antwort
aktiven Browserinhalt ausführen muss.
Auf die Anfrage zugreifen
routeCtx.request ist ein SandboxedRequest: ein portabler { url, method, headers }-Record, der in-process und in einem Isolate identisch verhält. headers ist ein Record<string, string>, keyed nach lowercased Header-Namen — indexieren Sie mit dem lowercased Namen oder iterieren Sie mit Object.entries. url ist ein String, sodass new URL(request.url) Query-Parameter parst. routeCtx.requestMeta trägt IP, User-Agent und Geo-Daten, plattformübergreifend normalisiert, wenn verfügbar.
Bei einer Route mit request-Deklaration erreichen nur Namen in request.headers den Handler. EmDash
lehnt Deklarationen für Credentials, Cookies, Cloudflare-Access-Header, Proxy-Autorisierung,
Set-Cookie und den CSRF-Header X-EmDash-Request ab. Es streicht diese Header aus jeder Sandbox-
Anfrage, einschließlich Legacy-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-Settings nutzen private Routen, Block-Kit-Formulare und ctx.settings. Settings liefert das vollständige Muster für Laden, Validierung, Formular und verschlüsselte Secrets.
Routen, die Plugin-Daten auflisten, sollten den Cursor von ctx.storage.<collection>.query() zurückgeben. Storage pagination zeigt, wie man einen Cursor übergibt und mehrere Seiten abarbeitet, ohne das Maximum von 100 Elementen pro Seite zu überschreiten.
Externer API-Proxy
Proxien Sie eine Anfrage an einen externen Dienst über ctx.http (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ärmethoden wie arrayBuffer() und blob() bewahren Bytes über Cloudflare Worker Loader und Node/workerd. Request- und Response-Bodies sind jeweils auf 8 MiB dekodierter Daten begrenzt. Redirect-Ziele werden vor jedem Hop geprüft, und Credential-Header werden entfernt, wenn ein Redirect Origins überschreitet.
Routen aus Block Kit aufrufen
Sandbox-Plugins liefern keinen React-Code an die Admin. Deklarieren Sie eine admin-Route und geben Sie Block-Kit-Antworten zurück. EmDash sendet Interaktionen page_load, block_action und form_submit an diese private Route mit der korrekten URL und dem CSRF-Header. Block Kit zeigt den Interaktionsvertrag und eine vollständige Route.
Routen aus Queue- und Scheduled-Handlern aufrufen
Platform-Event-Handler (ein Cloudflare-Queue-Consumer, ein eigener scheduled()-Handler) haben keine HTTP-Anfrage und daher kein locals.emdash. Verwenden Sie withEmDashRuntime() aus emdash/middleware, um die Runtime 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 gecachte Runtime auf, die Request-Handler nutzen, sodass Plugin-Storage, Hooks und Media-Zugriff genau wie während einer Anfrage verhalten. Auf verbindungsbasierten Datenbankadaptern (z. B. Postgres über Hyperdrive) läuft der Callback unter einer event-scoped Verbindung, die beim Return committed und geschlossen wird.
Routen extern aufrufen
Öffentliche Routen sind direkt aufrufbar:
curl -X POST https://your-site.com/_emdash/api/plugins/forms/track \
-H "Content-Type: application/json" \
-d '{"event": "pageview"}'
Private Routen brauchen Session-Credentials plus X-EmDash-Request: 1, oder ein API-Token mit dem Scope admin. Die folgende Server-zu-Server-Anfrage nutzt 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": "user@example.com"}'
Routenkontext-Referenz
Die folgenden Interfaces fassen die portablen Werte zusammen, die einem Sandbox-Routen-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
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 einzelnes RouteContext-Argument, das die beiden kombiniert — siehe Creating native plugins, wenn Sie diesen Weg gehen.