dorfteich/docs/architecture/plugin-architecture.md
Claude Fable 5 97f94f247b
All checks were successful
CD / Build and push images (push) Successful in 3m54s
CD / Deploy to Test (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 4m9s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m13s
CD / Promote to Int (push) Successful in 13s
CI / Auth e2e pack (push) Successful in 5m36s
CI / Import/export fidelity gate (push) Successful in 47s
draw.io reference plugin: fullscreen editing, inline SVG rendering
A new block plugin bundling the OFFICIAL draw.io editor — nothing ever
loads from diagrams.net; the sandbox CSP pins every request to the
plugin's own version-pinned asset path (zero-external-network verified
live via a request-capture run).

Plugin (packages/plugins/drawio):
- block data { xml, svg }: xml is the draw.io source (document of
  record), svg the rendered snapshot as raw markup — render mode,
  office/PDF exports (the existing fallback renderer already inlines
  data.svg) and the public view all show the diagram without running
  diagram code
- edit mode: snapshot + "edit in fullscreen" (an empty block opens the
  editor immediately); the bundled editor runs in a child iframe of the
  plugin's own assets and speaks draw.io's JSON embed protocol —
  Save & Exit exports xmlsvg, persists { xml, svg } via blockData, and
  drops back to the inline size
- build.mjs fetches the pinned release (v30.3.6) into a gitignored
  vendor/ cache (fonts-build pattern; skipped in CI — plugin.js still
  bundles, the installable ZIP needs a dev machine) and packs a trimmed
  webapp subset: no dev sources, no embed.diagrams.net integrations
  bundle, no standalone viewers, no MathJax/templates/PWA — 27 MiB ZIP,
  85 MiB unpacked, de+en editor languages

Host/SDK extensions (generic, not drawio-specific):
- new ui.enterFullscreen()/exitFullscreen(): the surface's frame becomes
  a viewport-covering overlay — same sandboxed iframe, only geometry
  changes; destroy removes the frame, so a vanished plugin can never
  leave the app covered
- sandbox CSP: connect-src/frame-src now allow the plugin's OWN asset
  path (was 'none') — bundled apps lazy-load their resources and run in
  a child frame, but the api and external hosts stay unreachable; HTML
  assets are served with the same CSP so a packaged page cannot widen
  the rules, and child frames inherit the sandbox attribute
- plugin size limits raised (ZIP 5→64 MiB, unpacked 20→256 MiB) for
  bundled-app plugins; content types for xml/txt/ico assets

Verified end to end against a local stack (9/9): install via dropzone
(85 MiB validation), block insert, fullscreen entry, bundled editor
boots inside the double sandbox (German UI), shape drawn, Save & Exit
persists, snapshot renders inline, survives reload, zero off-origin
requests throughout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-12 14:15:30 +02:00

8.7 KiB

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

{
  "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.

These live in packages/plugins/ in the monorepo, are built by CI, and double as the plugin-SDK integration tests.