Jedes Sandbox-Plugin hat eine emdash-plugin.jsonc neben seiner package.json. Sie wird manuell bearbeitet und enthält die Identität des Plugins, seinen Vertrauensvertrag (Fähigkeiten, Hosts, Speicher) und die Profilfelder, die die Registry anzeigt. emdash-plugin init erstellt ein Gerüst; die CLI liest ./emdash-plugin.jsonc automatisch für build, dev, validate, bundle und publish.
Die Datei ist JSONC: Kommentare und abschließende Kommas sind erlaubt.
Das folgende Beispiel zeigt ein vollständiges Manifest für ein Image-Gallery-Plugin:
{
"$schema": "./node_modules/@emdash-cms/plugin-cli/schemas/emdash-plugin.schema.json",
"slug": "gallery",
"publisher": "did:plc:abc123def456",
"license": "MIT",
"author": { "name": "Jane Doe", "url": "https://example.com" },
"security": { "email": "[email protected]" },
// Optionales Profil
"name": "Gallery",
"description": "Image-Gallery-Block für EmDash.",
"keywords": ["gallery", "images"],
"repo": "https://github.com/example/plugin-gallery",
// Vertrauensvertrag
"capabilities": ["content:read"],
"allowedHosts": [],
"storage": {}
}
Identität
| Feld | Erforderlich | Hinweise |
|---|---|---|
slug | Ja | URL-sichere ID innerhalb des Publisher-Namensraums. /^[a-z][a-z0-9_-]*$/, max. 64 Zeichen. |
publisher | Ja | Die DID oder der Handle deines Atmosphere-Kontos. Siehe Publisher-Pinning. |
version | Nein | Semver 2.0 ohne Build-Metadaten. Normalerweise weglassen — siehe unten. |
slug und publisher bilden zusammen die Identität des Pakets. EmDash leitet daraus automatisch den vollständigen Paketbezeichner ab.
version gehört in package.json
Der Build gleicht die version des Manifests mit package.json#version ab:
- Beide gesetzt und gleich → in Ordnung.
- Beide gesetzt und unterschiedlich → harter Fehler.
- Eine gesetzt → dieser Wert gewinnt.
- Keine gesetzt → harter Fehler.
Das empfohlene Muster für ein npm-verteiltes Plugin ist, version im Manifest wegzulassen und package.json als einzige Quelle der Wahrheit zu verwenden (dein Release-Tooling erhöht sie bereits dort). Registry-only-Plugins ohne package.json müssen version im Manifest setzen — es gibt keinen anderen Ort dafür.
Profil
Diese fließen in den Registry-Eintrag ein. license, ein Autor (author oder authors) und ein Sicherheitskontakt (security oder securityContacts) sind erforderlich; der Rest ist optional.
| Feld | Erforderlich | Hinweise |
|---|---|---|
license | Ja | SPDX-Ausdruck ("MIT", "Apache-2.0", "MIT OR Apache-2.0"). Wird bei der ersten Veröffentlichung verwendet; das bestehende Profil gewinnt bei späteren Veröffentlichungen. |
author / authors | Ja | Eines der beiden. author: { name, url?, email? } für einen einzelnen Autor; authors: [...] (≤ 32) für mehrere. Beides zu setzen ist ein Fehler. |
security / securityContacts | Ja | Eines der beiden. Jeder Kontakt benötigt mindestens email oder url. securityContacts: [...] (≤ 8) für mehrere. Beides zu setzen ist ein Fehler. |
name | Nein | Anzeigename. Standardmäßig der Slug. |
description | Nein | Halte es kurz (ca. 140 Zeichen). Lange Werte können in Listen abgeschnitten werden. |
keywords | Nein | ≤ 5 Einträge. |
repo | Nein | https://-URL des Quell-Repos. |
Verwende die Singular-Form author / security, es sei denn, du hast tatsächlich mehrere — das ist der häufige Fall, und das Gerüst gibt sie aus.
Vertrauensvertrag
Der Vertrauensvertrag besteht aus capabilities, allowedHosts und storage. Alle drei sind standardmäßig leer, sodass ein Plugin, das keine zusätzlichen Privilegien benötigt, sie vollständig weglassen kann.
{
"capabilities": ["network:request", "content:read"],
"allowedHosts": ["api.example.com", "*.cdn.example.com"],
"storage": {
"events": { "indexes": ["timestamp"] },
"submissions": { "indexes": ["email"], "uniqueIndexes": ["token"] }
}
}
Fähigkeiten
Die erkannten Namen:
| Fähigkeit | Gewährt |
|---|---|
content:read / content:write | Lesen / Ändern von Site-Inhalten über ctx. |
taxonomies:read | Lesen von Taxonomie-Definitionen und Begriffen (nur lesen). |
media:read / media:write | Lesen / Schreiben von Medien. |
users:read | Lesen von Benutzer-Datensätzen. |
email:send | E-Mail senden über ctx. |
network:request | Ausgehende HTTP-Anfragen über ctx.http, beschränkt auf allowedHosts. |
network:request:unrestricted | Ausgehende HTTP-Anfragen an jeden Host. Wird anstelle von network:request verwendet. |
hooks.email-transport:register | Registrierung eines E-Mail-Transport-Hooks. |
hooks.email-events:register | Registrierung von E-Mail-Lebenszyklus-Hooks. |
hooks.page-fragments:register | Registrierung eines page:fragments-Hooks (nur nativ). |
Zwei Kreuzfeld-Regeln, die die CLI durchsetzt (die JSON-Schema-Prüfung des Editors nicht — führe emdash-plugin validate aus):
network:requesterfordert eine nicht-leereallowedHosts. Wenn das Plugin wirklich jeden Host erreichen muss, verwende stattdessennetwork:request:unrestricted.network:request:unrestrictederfordert, dassallowedHostsleer ist — die uneingeschränkte Fähigkeit gewährt bereits jeden Host, sodass eine Liste widersprüchlich wäre.
Host-Muster sind blanke Hostnamen (kein Schema, Pfad oder Leerzeichen). Ein vorangestelltes *. erlaubt Subdomains: *.cdn.example.com.
Speicher
Eine Zuordnung von Sammlungsname → Indexkonfiguration. Sammlungsnamen folgen der gleichen /^[a-z][a-z0-9_]*$/-Regel (die Laufzeit verwendet den Namen als SQL-Tabellensuffix). Indexe sind Feldnamen oder zusammengesetzte Arrays; uniqueIndexes sind ebenfalls abfragbar — liste sie nicht zusätzlich in indexes.
"storage": {
"events": { "indexes": ["timestamp", ["collection", "timestamp"]] }
}
Admin-Oberfläche
Optional. Sandbox-Plugins rendern Admin-Seiten und Dashboard-Widgets über Block Kit; das Manifest deklariert nur, wo sie erscheinen. Lasse den admin-Schlüssel ganz weg, wenn das Plugin keine Admin-UI hat.
"admin": {
"pages": [{ "path": "/gallery", "label": "Gallery", "icon": "image" }],
"widgets": [{ "id": "recent-uploads", "title": "Letzte Uploads", "size": "half" }]
}
Ein Plugin, das admin.pages oder admin.widgets deklariert, muss auch eine admin-Route in src/plugin.ts bereitstellen, die den Block-Kit-Inhalt rendert — das Schema kann das nicht erzwingen (Routennamen werden aus dem Quellcode abgetastet, nicht aus dem Manifest), aber die Laufzeit prüft es.
Publisher-Pinning
publisher fixiert die Veröffentlichungsidentität, damit du ein Plugin nicht versehentlich unter dem falschen Konto veröffentlichen kannst.
Bei deiner ersten erfolgreichen Veröffentlichung, wenn der publisher des Manifests mit der aktiven Sitzung übereinstimmt, bleibt er wie geschrieben. Wenn du mit emdash-plugin init ein Gerüst erstellt und ihn leer gelassen hast, schreibt die CLI die DID der aktiven Sitzung zurück ins Manifest.
Das folgende Beispiel zeigt die Zeile, die die CLI schreibt, wobei der aufgelöste Handle als Kommentar zur Lesbarkeit hinzugefügt wurde:
"publisher": "did:plc:abc123def456", // jane.example.com
Bei jeder nachfolgenden Veröffentlichung löst die CLI die aktive Sitzung und den fixierten publisher zu DIDs auf und vergleicht sie. Eine Nichtübereinstimmung schlägt sofort mit MANIFEST_PUBLISHER_MISMATCH fehl — es gibt kein Override-Flag. Löse es bewusst:
- Falsche Sitzung:
emdash-plugin switch <did>, dann erneut veröffentlichen. - Tatsächliche Übertragung des Plugins an einen neuen Publisher: Bearbeite
publisherim Manifest.
Validierung ohne Veröffentlichung
emdash-plugin validate # ./emdash-plugin.jsonc
emdash-plugin validate path/ # ein bestimmtes Verzeichnis
Offline-Schema-Prüfung mit tsc-ähnlichen datei:zeile:spalte-Diagnosen, einschließlich der Kreuzfeld-Regeln. Geeignet für einen Pre-Commit-Hook oder CI-Schritt. Doppelte Schlüssel und unbekannte Schlüssel sind Fehler (Striktmodus erkennt "licens"-Tippfehler).
CLI-Flags gewinnen trotzdem
Explizite Flags (--license, --author-name, …) überschreiben Manifest-Werte, wenn beide gesetzt sind — nützlich für CI-Overrides. --no-manifest überspringt das Manifest vollständig (und warnt, wenn eines am Standardpfad existiert, damit die Publisher-Pin-Sicherheitsgeschichte sichtbar bleibt).