You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/src/content/docs/plugins/creating-plugins/block-kit.mdx
+81-31Lines changed: 81 additions & 31 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -12,7 +12,10 @@ EmDash's Block Kit lets sandboxed plugins describe their admin UI as JSON. The h
12
12
</Aside>
13
13
14
14
<Asidetype="tip">
15
-
Block Kit elements are also used for [Portable Text block editing fields](/plugins/creating-native-plugins/portable-text-components/). When a plugin declares `fields` on a block type, the editor renders a Block Kit form for editing block data (URL, title, parameters, etc.).
15
+
Native plugins also use Block Kit elements to describe [Portable Text block editing
The route handler takes two arguments: `routeCtx` (with `input`, `request`, `requestMeta`) and `ctx` (the `PluginContext`). `satisfies SandboxedPlugin` infers both.
112
+
The `admin`route is private by default. EmDash sends the correct CSRF header when the admin calls it. The handler still validates `routeCtx.input` because its TypeScript type is `unknown` and a caller can invoke a private plugin route outside the Block Kit page.
76
113
77
114
## Block types
78
115
@@ -86,11 +123,16 @@ The route handler takes two arguments: `routeCtx` (with `input`, `request`, `req
86
123
|`actions`| Horizontal row of buttons and controls |
87
124
|`stats`| Dashboard metric cards with trend indicators |
88
125
|`form`| Input fields with conditional visibility and submit |
89
-
|`image`| Block-level image with caption |
126
+
|`image`| Block-level image with alt text and an optional title|
90
127
|`context`| Small muted help text |
91
128
|`columns`| 2–3 column layout with nested blocks |
92
-
|`empty`| Empty-state placeholder with icon, title, description, optional command line, and action buttons |
129
+
|`empty`| Empty-state title with an optional description, command, and action buttons|
|`chart`| Line or bar time series, or a chart with custom options |
132
+
|`banner`| Status or alert message with a title or description |
133
+
|`meter`| Numeric value displayed against a minimum and maximum |
134
+
|`code`| Read-only TypeScript, TSX, JSONC, Bash, or CSS code |
135
+
|`tab`| Labeled panels containing nested blocks |
94
136
95
137
## Element types
96
138
@@ -102,16 +144,22 @@ The route handler takes two arguments: `routeCtx` (with `input`, `request`, `req
102
144
|`select`| Dropdown select |
103
145
|`toggle`| On/off switch |
104
146
|`secret_input`| Masked input for API keys and tokens |
147
+
|`checkbox`| Select several values from a fixed list |
148
+
|`combobox`| Searchable single-value selection |
149
+
|`date_input`| Date value |
150
+
|`radio`| Single choice from a visible option list |
151
+
152
+
The Portable Text field editor also supports `repeater` and `media_picker`. They are not form fields for a sandboxed plugin admin page.
105
153
106
154
## Builder helpers
107
155
108
-
The `@emdash-cms/blocks` package exports builder helpers for cleaner code:
156
+
The `@emdash-cms/blocks` package exports the same shapes through `blocks` and `elements`builder objects. Builders reduce property-name mistakes while returning ordinary JSON-compatible objects:
109
157
110
158
```typescript
111
159
import { blocks, elements } from"@emdash-cms/blocks";
@@ -155,6 +203,8 @@ Form fields can be conditionally shown based on other field values:
155
203
156
204
The `api_key` field only appears when `auth_enabled` is toggled on. Conditions are evaluated client-side with no round-trip.
157
205
206
+
`secret_input` uses `has_value: true` to show that a value already exists; it does not accept or return the stored value on page load. The field masks typing in the browser, but it does not encrypt a value written to `ctx.kv`. Follow [Secret settings](/plugins/creating-plugins/settings/#secret-values) before storing credentials.
207
+
158
208
## Try it
159
209
160
210
Use the [Block Playground](https://emdash-blocks.cto.cloudflare.dev/) to interactively build and test block layouts.
Copy file name to clipboardExpand all lines: docs/src/content/docs/plugins/creating-plugins/capabilities.mdx
+12-12Lines changed: 12 additions & 12 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -23,7 +23,7 @@ Capabilities live in `emdash-plugin.jsonc`, alongside `slug` and the rest of the
23
23
}
24
24
```
25
25
26
-
Declare only what the plugin actually needs. Capability declarations are also what the marketplace shows site operators on the consent dialog — extra capabilities are friction at install time and a security flag in audits.
26
+
Declare only what the plugin actually needs. The marketplace shows these capabilities to site operators before installation, so every extra declaration asks them to approve access the plugin does not use.
27
27
28
28
## Capability reference
29
29
@@ -42,7 +42,7 @@ Declare only what the plugin actually needs. Capability declarations are also wh
|`hooks.page-fragments:register`| Allows registering the `page:fragments` hook (native plugins only) |
44
44
45
-
A few things worth knowing:
45
+
The following rules affect which capabilities a plugin needs:
46
46
47
47
-**Implications.**`content:write` automatically implies `content:read`; `media:write` implies `media:read`; `network:request:unrestricted` implies `network:request`. You don't need to list both.
48
48
-**Taxonomies are a separate, read-only surface.**`taxonomies:read` grants access to taxonomy definitions, their terms, and the terms assigned to an entry via `ctx.taxonomies`. It is independent of `content:read` — declare both if the plugin reads content *and* its classification. There is no taxonomy *write* access from plugins.
@@ -65,19 +65,19 @@ Locale matching is case-insensitive and stores the casing from the site's locale
65
65
66
66
## Network host allowlists
67
67
68
-
Plugins with `network:request` can only fetch hosts listed in `allowedHosts`. Wildcards are supported for subdomains:
68
+
Plugins with `network:request` can only fetch hosts listed in `allowedHosts`. A leading `*.` matches both the named domain and its subdomains:
69
69
70
70
```jsonc title="emdash-plugin.jsonc"
71
71
"capabilities": ["network:request"],
72
72
"allowedHosts": [
73
73
"api.example.com", // exact host
74
-
"*.cdn.example.com"//any subdomain of cdn.example.com
74
+
"*.cdn.example.com"// cdn.example.com and any subdomain
75
75
]
76
76
```
77
77
78
78
The bridge checks the request URL's host against the allowlist before forwarding the request. A request to a host that wasn't declared throws inside the plugin without ever leaving the sandbox.
79
79
80
-
`network:request:unrestricted` skips the allowlist check entirely. It's intended for plugins where the operator configures the destination URL at runtime (webhook senders, generic HTTP forwarders). Avoid it for plugins where the destination is part of the plugin's design — declare `network:request` with explicit hosts instead, so the consent dialog tells operators exactly where the plugin is going to call.
80
+
`network:request:unrestricted` skips the manifest host allowlist. The sandbox bridge still accepts only HTTP and HTTPS, blocks known internal hosts and private literal addresses, rechecks every redirect, and removes credential headers when a redirect crosses origins. Use unrestricted access only when an operator supplies the destination at runtime. For fixed destinations, declare `network:request` with explicit hosts so the consent dialog names them.
81
81
82
82
## What the sandbox enforces
83
83
@@ -87,37 +87,37 @@ When a sandbox runner is active, the runtime enforces:
87
87
88
88
1.**Capability gating.** The PluginContext factory only populates `ctx.content`, `ctx.taxonomies`, `ctx.media`, `ctx.http`, `ctx.users`, `ctx.email` when the corresponding capability is declared. Calling a method on an undeclared capability isn't possible — there's no object there.
89
89
90
-
2.**Storage and KV scoping.** Every storage and KV operation is scoped to the plugin's slug. A plugin can't read another plugin's KV or its storage collections, and it can only access storage collections it declared in the manifest.
90
+
2.**Storage and KV scoping.** Every storage and KV operation is scoped to the runtime plugin ID. A plugin can't read another plugin's KV or storage collections, and it can access only collections declared in its manifest.
91
91
92
92
3.**Network isolation.** Direct `fetch()` and other network primitives are blocked by the runner. The only way to reach the network is `ctx.http.fetch()`, which goes through the bridge's host validation.
93
93
94
94
4.**No host bindings.** Sandboxed plugins don't see environment variables, the filesystem, or any platform bindings — even if your host worker has them. The plugin runtime is a clean isolate with only the bridge and the declared capabilities.
95
95
96
-
5.**Resource limits.** The runner can enforce CPU, subrequest, wall-clock, and memory limits per invocation. The exact limits depend on which runner you're using; the Cloudflare runner uses the platform's Worker Loader limits (50ms CPU per invocation, 10 subrequests, 30 second wall-clock, ~128MB memory). The Node.js workerd runner (`@emdash-cms/sandbox-workerd`) enforces wall-clock time via `Promise.race`; CPU and memory limits are Cloudflare platform features and are not enforced by standalone workerd. Hooks that exceed the runner's limits are aborted; the EmDash hook timeout (`timeout`in the hook config) enforces a stricter ceiling on top of that.
96
+
5.**Resource limits.** The Cloudflare runner defaults to 50 ms of CPU, 10 subrequests, and 30 seconds of wall time per invocation. Worker Loader enforces CPU and subrequests; the runner enforces wall time. Worker Loader has a platform memory ceiling, but its per-plugin `memoryMb` option is not currently enforceable. The Node.js workerd runner enforces only the 30-second wall-time default; it warns when a site configures CPU, memory, or subrequest limits that standalone workerd cannot enforce. A per-hook `timeout`applies only when the sandboxed-format plugin runs in process.
97
97
98
98
</Steps>
99
99
100
100
## What the sandbox doesn't enforce
101
101
102
102
A few things the capability system doesn't and can't cover:
103
103
104
-
-**Behaviour within a granted capability.** A plugin with `content:write` can edit any content, not only its own. Capabilities are coarse — they say "this plugin can write content," not "this plugin can write only the content it created." Audit-time review is the only check on what a plugin actually does within its grant.
105
-
-**Entry edit locks.**`ctx.content.update()`writes through an entry's [edit lock](/reference/rest-api/#entry-edit-lock), so an editor who has that entry open in the admin does not stop a plugin write. `ctx.content.delete()` behaves the same way, and releases the lock along with the entry it trashes.
104
+
-**Behaviour within a granted capability.** A plugin with `content:write` can edit any content, not only its own. Capabilities are coarse — they say "this plugin can write content," not "this plugin can write only the content it created." An operator must evaluate the plugin's code and publisher before granting that access.
105
+
-**Entry edit locks.**`ctx.content.update()`and `ctx.content.delete()` are programmatic writes. An editor holding the entry's advisory edit lock does not block them. Coordinate plugin writes with editors when both may update the same entry.
106
106
-**Operator trust on Node.js.** When the configured sandbox runner reports unavailable (no Cloudflare Worker Loader, no Node-side runner installed, etc.), `sandboxed: []` plugins are skipped at startup. You can move them into `plugins: []` to run them in-process — but then there's no V8 isolate, no resource limits, and the plugin can call `fetch()` directly or read environment variables. Treat that as native-level trust.
107
107
-**Side channels.** Timing, log output, and stored data are all visible to anyone with appropriate access to the host environment. Don't use the sandbox as a confidentiality boundary against the operator running it.
108
108
109
109
## Capability consent
110
110
111
111
When an operator installs a sandboxed plugin from the marketplace, EmDash shows a consent dialog listing the declared capabilities. Updates that add capabilities — for example, a plugin that previously only read content now wants to make network requests — surface as a capability diff and require fresh approval before the new version takes effect.
112
112
113
-
This is why declaring extra capabilities matters even if you "might use them later". They show up as friction at every install and update, and security audits flag plugins that ask for more than they obviously need. List exactly what the plugin uses, and add new capabilities in a real version when the plugin actually starts using them.
113
+
Declaring capabilities for possible future use makes every installation or updateask for unnecessary access. List what the current version uses, then add a capability in the version that starts using it.
114
114
115
115
## Bundle-time validation
116
116
117
117
`emdash-plugin bundle` and `emdash-plugin publish` perform additional checks:
118
118
119
119
- Every declared capability must be in the recognised set (typos fail the build).
120
-
-`network:request` requires a non-empty `allowedHosts`; `network:request:unrestricted` requires it to be empty. See [the manifest reference](/plugins/creating-plugins/manifest/#capabilities).
120
+
-`network:request` requires a non-empty `allowedHosts`; `network:request:unrestricted` requires it to be empty. See [Capabilities and hosts](/plugins/creating-plugins/manifest/#capabilities-and-hosts).
121
121
- The bundled `backend.js` can't import Node.js built-ins (`fs`, `path`, `child_process`, etc.) — sandbox runtimes don't provide them.
122
122
123
-
See [Bundling and publishing](/plugins/creating-plugins/publishing/) for the full list of checks.
123
+
See [the manifest reference](/plugins/creating-plugins/manifest/#trust-contract) for the authoring fields and [Bundling and publishing](/plugins/creating-plugins/publishing/#validation) for bundle checks.
0 commit comments