dorfteich/docs/architecture/plugin-architecture.md
Claude Fable 5 2fe9374f16
All checks were successful
CI / Import/export fidelity gate (pull_request) Successful in 54s
CI / Lint, typecheck, test (pull_request) Successful in 4m25s
CD / Build and push images (push) Successful in 17s
Release / Build release images and notes (push) Successful in 1m18s
CD / Smoke tests against Test (push) Successful in 1m18s
CI / Build container images (push) Has been skipped
CI / Import/export fidelity gate (push) Successful in 59s
CI / Build container images (pull_request) Successful in 3m49s
CI / Auth e2e pack (pull_request) Successful in 6m47s
CD / Deploy to Test (push) Successful in 11s
CD / Promote to Int (push) Successful in 11s
Release / Release-candidate operations QA (push) Successful in 45s
Prod deploy / Deploy the released images to Prod (push) Successful in 15s
CI / Lint, typecheck, test (push) Successful in 4m36s
CI / Auth e2e pack (push) Successful in 6m54s
Doku: Excalidraw als sechstes Standard-Plugin ergänzt
- Tutorial K19: neue Sektion „Excalidraw — Skizzen wie von Hand" mit
  Nadias Bühnenplan-Beispiel (konzerte-live); Intro fünf→sechs Plugins.
- Tutorial K18: fünf→sechs Standard-Plugins.
- Site-Admin-Guide (en+de): Referenzliste + Build-Namensliste +
  Build-Hinweis (~16-MiB-ZIP aus npm).
- plugin-architecture.md: Referenz-Eintrag excalidraw (npm-Library-
  Spielart des Bundled-App-Pfads, {scene, svg}).
- developer/extending (en+de): Excalidraw als zweite Bundled-App-Variante.

Holt die in #136 zugesagten Doku-Ergänzungen nach. Screenshot für K19
folgt nach dem CSP-Deploy (damit die Handschrift korrekt rendert).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-19 22:27:17 +02:00

140 lines
9.1 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).
## 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.
- `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.