Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 5m38s
CI / Build container images (pull_request) Successful in 4m11s
CI / Auth e2e pack (pull_request) Successful in 8m55s
CI / Import/export fidelity gate (pull_request) Successful in 1m9s
CI / Import/export fidelity gate (push) Blocked by required conditions
CD / Build and push images (push) Successful in 20s
CD / Deploy to Test (push) Failing after 51s
CD / Smoke tests against Test (push) Has been skipped
CD / Promote to Int (push) Has been skipped
CI / Lint, typecheck, test (push) Successful in 5m37s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Has been cancelled
plugins.enabled (instance setting, default on — plugins predate the switch; the VS-NfD reference configuration turns it off) makes every plugin surface answer 404 via a shared guard: Site-Admin install/list/mode, pond activation and plugin list, the sandbox frame and asset routes. The dropzone watcher quarantines drops instead of installing. Deliberately NOT guarded: the authenticated fallback-metadata route — it serves no plugin code and existing plugin_block nodes need it to render their declared fallback (an image fallback degrades to the neutral placeholder while off, because its bytes live on the disabled asset surface). The editor offers no plugin blocks because the pond plugin list is one of the 404ing surfaces. Admin settings panel gets the toggle (i18n de+en) with the documented api-restart note (in-process settings cache). Answers "code execution inside the zone?" with one verifiable off-switch instead of per-plugin trust machinery (#232, ADR 0025). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
161 lines
10 KiB
Markdown
161 lines
10 KiB
Markdown
# Plugin architecture
|
|
|
|
Extends ADR 0008 with the concrete contracts implementers need.
|
|
|
|
## Package format
|
|
|
|
A plugin is a ZIP archive:
|
|
|
|
```
|
|
my-plugin.zip
|
|
├── manifest.json (required)
|
|
├── plugin.js (required for kind=code; single ES module bundle)
|
|
├── styles.css (optional; required for kind=section_style)
|
|
├── i18n/de.json (optional UI strings)
|
|
├── i18n/en.json
|
|
└── assets/… (optional images etc.)
|
|
```
|
|
|
|
### `manifest.json`
|
|
|
|
```json
|
|
{
|
|
"id": "toc",
|
|
"name": "Table of Contents",
|
|
"version": "1.2.0",
|
|
"apiVersion": "1",
|
|
"kind": "code",
|
|
"extensionPoints": [
|
|
{
|
|
"type": "pageTool",
|
|
"id": "toc",
|
|
"title": { "de": "Inhaltsverzeichnis", "en": "Table of contents" }
|
|
}
|
|
],
|
|
"permissions": ["readCurrentPage"],
|
|
"fallback": { "type": "text", "value": "[Table of contents]" },
|
|
"license": "MIT",
|
|
"homepage": "https://…"
|
|
}
|
|
```
|
|
|
|
- `apiVersion`: host checks against its supported range at install time.
|
|
- `permissions`: the capabilities the plugin may call (see API below);
|
|
shown to the Site Admin at install time. Requests outside the declared
|
|
set are rejected at runtime.
|
|
- `fallback`: static representation used in Word/PDF exports and when the
|
|
plugin is disabled but its blocks still exist in documents.
|
|
|
|
## Kinds and extension points
|
|
|
|
| Kind | Extension point | What it does | Sandbox |
|
|
| --------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
| `section_style` | `sectionStyle` | declares named styles (name, i18n label, CSS class body) applicable to container blocks — e.g. colored background boxes | none needed: CSS is sanitized (no `@import`, no `url()` to external hosts) and scoped under `.dt-style-<pluginId>-<styleId>` |
|
|
| `code` | `block` | a custom editor block (diagram, embed, …); host registers a ProseMirror node `plugin_block` instance with `pluginId`, `blockType`, `data` attrs | sandboxed iframe per block |
|
|
| `code` | `pageTool` | read-only widget rendered in the page tools panel or embedded as a block (TOC, page index, cross-page block embed) | sandboxed iframe |
|
|
|
|
`styles.css` is served into the host page, so the install gate (#75) enforces
|
|
its scoping instead of rewriting it: **every rule must be written under one of
|
|
the plugin's own `.dt-style-<pluginId>-<styleId>` classes** (each `<styleId>`
|
|
a declared `sectionStyle` extension point; grouping at-rules like `@media`
|
|
are checked inside, only `@font-face`/`@keyframes` are exempt). Positioning
|
|
out of the content flow (`position` other than `static`/`relative`) is
|
|
rejected — a fixed overlay could shadow the whole app. A rule that violates
|
|
the contract fails the install with `plugin_css_unsafe`.
|
|
|
|
## Sandbox runtime
|
|
|
|
- Each code-plugin surface runs in `<iframe sandbox="allow-scripts">`
|
|
**without** `allow-same-origin` → opaque origin: no cookies, storage, or
|
|
parent DOM. The iframe document is generated by the host and loads only
|
|
the plugin bundle + its assets from the plugin's static path.
|
|
- CSP on plugin frames: `default-src 'none'; script-src <plugin path>;
|
|
img-src <plugin path> blob: data:; style-src <plugin path> 'unsafe-inline';
|
|
connect-src <plugin path>; frame-src <plugin path>`.
|
|
Network and child frames are pinned to the plugin's OWN version-pinned
|
|
asset path — bundled sub-apps (the drawio editor) may lazy-load their
|
|
resources and run in a child iframe of the plugin's assets, but nothing
|
|
can reach the api or any external host. HTML assets are served with the
|
|
same CSP, so a packaged page cannot widen the rules; child frames also
|
|
inherit the `sandbox` attribute (opaque origin, no storage).
|
|
- Host ↔ plugin communication: `postMessage` RPC with structured-clone
|
|
payloads. `packages/plugin-sdk` provides both sides:
|
|
- plugin side: `createPlugin({ onRender, onEdit, … })`, typed `host.*`
|
|
calls;
|
|
- host side: frame lifecycle, request routing, permission filtering,
|
|
timeouts (a hung plugin never blocks the app).
|
|
|
|
## Plugin API (v1 capabilities)
|
|
|
|
All calls are mediated by the host and executed against the REST API with
|
|
the **viewing user's** session — a plugin can never read more than the
|
|
person looking at it could.
|
|
|
|
| Capability | Methods |
|
|
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
| `readCurrentPage` | `getOutline()`, `getContent()` (Markdown), `getMeta()` |
|
|
| `readPond` | `listPages()`, `getPageOutline(pageId)`, `getPageContent(pageId)` |
|
|
| `readBlock` | `getBlock(pageId, blockId)` — cross-page block embedding |
|
|
| `blockData` | `getData()` / `setData(data)` for the plugin's own block instance (writes go through the editor as a normal document change — requires the viewer to have write permission) |
|
|
| `ui` | `resize(height)`, `openPage(pageId)` (host navigates), `toast(msgKey)`, `scrollToHeading(headingId)` (host scrolls to an outline entry, #77), `enterFullscreen()`/`exitFullscreen()` (the frame becomes a viewport-covering overlay — drawio-class editors; destroy always restores) |
|
|
|
|
## Lifecycle & administration
|
|
|
|
1. **Install** (Site Admin): upload ZIP in the admin UI **or** drop it into
|
|
the `plugins/` volume directory (a watcher picks it up). The API
|
|
validates: ZIP structure, manifest schema, `apiVersion`, CSS sanitation,
|
|
bundle size limit. Invalid packages are rejected with a precise error.
|
|
2. **Instance mode** (Site Admin): `disabled` | `optional` | `required`
|
|
(vision: Site Admin activates plugins optionally or mandatorily).
|
|
3. **Pond activation** (Pond Admin): toggle `optional` plugins per pond.
|
|
4. **Update**: uploading the same id with a higher version replaces the
|
|
package after the same validation; open clients use the new version on
|
|
next load.
|
|
5. **Uninstall**: blocked while `required`; otherwise the package is
|
|
removed, existing `plugin_block` nodes render the manifest `fallback`
|
|
(documents are never mutated by plugin removal).
|
|
|
|
**Instance kill switch (issue #200, ADR 0025)**: `plugins.enabled`
|
|
(instance setting, default on; the VS-NfD reference configuration turns
|
|
it off) sits above the whole lifecycle. While off, every plugin surface
|
|
answers 404 — admin install/list/mode, pond activation, the sandbox frame
|
|
and asset routes — and the dropzone watcher quarantines instead of
|
|
installing. Only the authenticated fallback-metadata route stays alive:
|
|
it serves no plugin code, and existing `plugin_block` nodes use it to
|
|
render their declared fallback (an image fallback degrades to the neutral
|
|
placeholder, because its bytes live on the disabled asset surface — in
|
|
the reference configuration no plugin is installed, so nothing degrades).
|
|
The editor offers no plugin blocks because the pond plugin list is one of
|
|
the 404ing surfaces. Like every instance setting it is cached in-process:
|
|
flipping it is followed by an api restart to take full effect. This
|
|
single, verifiable off-switch is what answers "code execution inside the
|
|
zone?" at the offer stage — cheaper than per-plugin trust machinery
|
|
(#232) and sufficient because it removes the surface entirely.
|
|
|
|
## Reference plugins (shipped with the product, also serving as examples)
|
|
|
|
- `section-styles-basic` (`section_style`): a set of colored callout/box
|
|
styles — proves the declarative path.
|
|
- `toc` (`pageTool`): table of contents from the page outline.
|
|
- `page-index` (`pageTool`): filtered page list by label.
|
|
- `mermaid` (`block`): diagram block rendering Mermaid source — proves the
|
|
code-block path end to end (editing UI inside the sandbox).
|
|
- `drawio` (`block`): draw.io diagrams — proves the bundled-app path: the
|
|
official draw.io editor ships as plugin assets (pinned release fetched at
|
|
build time into `vendor/`, gitignored) and runs fullscreen in the
|
|
sandbox; blocks store `{ xml, svg }`, render mode and exports use the
|
|
SVG snapshot.
|
|
- `chordpro` (`block`): ChordPro leadsheets — a dependency-free code
|
|
block: its own minimal parser renders chords above lyrics into a static
|
|
SVG; blocks store `{ source, svg }`, render mode and exports use the
|
|
SVG snapshot.
|
|
- `excalidraw` (`block`): hand-drawn sketches — proves the npm-library
|
|
flavor of the bundled-app path: the Excalidraw React editor is bundled
|
|
straight into `plugin.js` (esbuild) with its font/locale assets shipped
|
|
alongside and loaded via `EXCALIDRAW_ASSET_PATH`; blocks store
|
|
`{ scene, svg }`, render mode and exports use the SVG snapshot (with
|
|
subsetted fonts embedded as `data:` URIs).
|
|
|
|
These live in `packages/plugins/` in the monorepo, are built by CI, and
|
|
double as the plugin-SDK integration tests.
|