The plugin manifest
Every sandboxed plugin has an emdash-plugin.jsonc file in its root directory. It sits next to package.json when the plugin is also an npm package. The manifest identifies the plugin, declares the access and storage it needs, and supplies information for its registry listing and releases. The plugin CLI reads it when validating, building, bundling, publishing, and preparing automated releases.
The file uses JSON with Comments (JSONC), so comments and trailing commas are valid. Keep the $schema property created by emdash-plugin init; editors use the generated schema for completion, while emdash-plugin validate performs the complete check.
The following example includes each group of fields:
{ "$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": "security@example.com" }, "name": "Gallery", "description": "Image galleries for EmDash content.", "keywords": ["gallery", "images"], "sections": { "installation": { "file": "./docs/installation.md" }, "faq": { "file": "./docs/faq.md" }, },
"capabilities": ["content:read"], "allowedHosts": [], "storage": { "galleries": { "indexes": ["contentId"] }, },
"admin": { "pages": [{ "path": "/gallery", "label": "Gallery", "icon": "image" }], },
"repo": "https://github.com/example/plugin-gallery", "release": { "requires": { "env:emdash": ">=0.37.0" }, "artifacts": { "icon": { "file": "./images/icon.png" }, "screenshots": [{ "file": "./images/editor.png", "lang": "en" }], }, },}Identity and version
Section titled “Identity and version”| Field | Required | Rules |
|---|---|---|
slug | Yes | Starts with a lowercase letter, then uses lowercase letters, digits, -, or _; maximum 64 characters. |
publisher | Yes | Atmosphere account DID or handle. A DID is recommended because handles can change owners. |
version | Sometimes | Semver 2.0 without build metadata. Omit it when package.json supplies the version. |
The publisher and slug identify the package in the registry. The slug is also used in plugin route URLs, so it is not the npm package name: use gallery for a package such as @example/plugin-gallery.
Keep one version value
Section titled “Keep one version value”The build reconciles version with package.json#version:
- If both files set the version, the values must match.
- If one file sets the version, the build uses it.
- If neither file sets the version, the build fails.
For an npm package, keep the version in package.json and omit it from the manifest. A registry-only plugin without package.json must set version in the manifest.
Package profile
Section titled “Package profile”The package profile supplies the stable information shown across releases.
| Field | Required | Rules |
|---|---|---|
license | Yes | Non-empty SPDX expression, up to 256 characters. |
author or authors | Yes | Use one form. author has name and optional url or email; authors accepts 1–32 entries. |
security or securityContacts | Yes | Use one form. Each contact must contain an email, a url, or both; the list accepts 1–8 entries. |
name | No | Display name, up to 1,024 characters. The slug is used when this is absent. |
description | No | Short description, up to 1,024 characters. Keep it near the 140-grapheme registry convention so lists do not need to truncate it. |
keywords | No | Up to five non-empty strings, each no longer than 128 characters. |
sections | No | Long-form CommonMark sections for description, installation, faq, changelog, and security. |
Each section can be an inline string or a file reference relative to the manifest:
"sections": { "description": "A longer description written in CommonMark.", "installation": { "file": "./docs/installation.md" }, "security": { "file": "./SECURITY.md" },}Each resolved section is limited to 20,000 bytes and 2,000 graphemes. A file reference must remain inside the manifest directory; absolute paths and .. paths that escape it are rejected.
publish creates the package profile on the first release. Later releases do not replace its profile fields. Edit a published profile with emdash-plugin update-package: the command previews the changes by default and writes them only with --yes.
Trust contract
Section titled “Trust contract”The trust contract consists of capabilities, allowedHosts, and storage. These fields tell an operator what the plugin can access and which plugin-owned collections it creates.
The following declaration permits content reads, permits requests to one API and its subdomains, and creates one storage collection:
"capabilities": ["content:read", "network:request"],"allowedHosts": ["api.example.com", "*.cdn.example.com"],"storage": { "events": { "indexes": ["savedAt", ["collection", "savedAt"]], "uniqueIndexes": ["eventId"], },}Capabilities and hosts
Section titled “Capabilities and hosts”All three fields default to empty. The scaffold writes the empty values explicitly so a reviewer can see that the plugin asks for no additional access or collections.
network:request requires at least one bare hostname in allowedHosts. network:request:unrestricted requires an empty list because it deliberately permits any public host. Capabilities and security is the canonical reference for every capability, its implications, host patterns, and runtime enforcement.
Storage
Section titled “Storage”Each key in storage names one plugin-owned collection. Its indexes and uniqueIndexes determine which fields the plugin can filter or order by. Storage explains collection names, index design, queries, and pagination.
Changing any part of the trust contract requires a new plugin version because installed sites consented to the earlier declaration. Use a major version for a broadened contract.
Admin pages and widgets
Section titled “Admin pages and widgets”Sandboxed plugins render admin pages and dashboard widgets with Block Kit. The manifest declares where they appear:
"admin": { "pages": [{ "path": "/settings", "label": "Settings", "icon": "settings" }], "widgets": [{ "id": "recent-events", "title": "Recent events", "size": "half" }],}Page paths begin with / and contain letters, digits, /, _, or -. Widget IDs use the same lowercase identifier characters as plugin slugs. Widget sizes are full, half, or third.
A plugin that declares a page or widget must define a route named admin in src/plugin.ts. The bundle check fails if that route is missing because EmDash would have nothing to call for the Block Kit response.
Release fields
Section titled “Release fields”release describes a particular version. The top-level repo URL is written into release records and is also used by automated-release setup to bind the package profile to its GitHub repository.
| Field | Purpose |
|---|---|
repo | HTTPS URL of the source repository. Automated release setup requires a canonical public GitHub repository URL. |
release.requires | Host requirements keyed by env:<name> or a package DID, with semver ranges as values. |
release.artifacts.icon | Image shown as the release icon. |
release.artifacts.banner | Banner image for the release listing. |
release.artifacts.screenshots | Ordered screenshot gallery with up to eight entries. |
Use env:emdash and env:astro to set the EmDash and Astro version ranges that can install the release:
"release": { "requires": { "env:emdash": ">=0.37.0", "env:astro": ">=5.0.0 <7.0.0", },}EmDash checks these requirements before installation. An invalid range fails manifest validation; an incompatible release is not installed.
Artifact file paths are relative to the manifest and must remain inside its directory. PNG, JPEG, and WebP are supported. Each file is limited to 1 MiB and 8,192 pixels in either dimension. An optional lang property identifies a localized image with a BCP 47 language tag. Publishing uploads these files separately from the plugin bundle and records their measured type, dimensions, checksum, and personal data server (PDS) blob reference.
Publisher pinning
Section titled “Publisher pinning”publisher prevents an accidental publish from the wrong Atmosphere account. Prefer the account’s DID:
"publisher": "did:plc:abc123def456", // jane.example.comBefore publishing or preparing automated releases, the CLI compares the manifest publisher with the active session. A handle is resolved to its current DID for the comparison. A mismatch fails with MANIFEST_PUBLISHER_MISMATCH; there is no override flag.
If the wrong session is active, run emdash-plugin whoami, then switch to the pinned account:
emdash-plugin switch did:plc:abc123def456Change the manifest publisher only when transferring the package to another account.
Validate the manifest
Section titled “Validate the manifest”Validate the manifest before building or publishing:
emdash-plugin validatePass a file or directory to validate a different manifest:
emdash-plugin validate ./packages/plugin-galleryValidation is offline. It reports duplicate properties, unknown fields, invalid values, mutually exclusive forms, and cross-field errors such as network:request without a non-empty allowedHosts list.
Continue with the plugin CLI for build commands or Bundling and publishing for the release flow.