Second half of #75 on top of the section node (2e96173/784f21d): - Install gate for section_style CSS (plugin-css.ts): every rule must be scoped under one of the plugin's own .dt-style-<pluginId>-<styleId> classes (enforced, not rewritten — grouping at-rules checked inside, @font-face/@keyframes exempt, statement at-rules rejected); positioning out of the content flow (anything but static/relative) is rejected as an overlay vector; "</style" is rejected as a breakout vector for inlined embedding. Hostile fixtures from the acceptance list are pinned in plugin-css.test.ts. - Web: usePondPlugins loads the pond's active plugins once per visit; SectionStyleSheets links each active style plugin's immutable styles.css; SectionStyleMenu (toolbar) wraps/restyles/unwraps with a picker fed from the plugins' i18n titles. Sections show a faint dashed hint while editing so unstyled (plugin-disabled) sections stay findable. - PDF export: PluginsService.sectionStyleCssForPond inlines the pond's active section-style CSS into the Gotenberg HTML, so styled sections survive the network-isolated render; covered in export.service.db.test. - Reference plugin packages/plugins/section-styles-basic (callout, info, warning, colored-box; theme-neutral semi-transparent backgrounds), a workspace package whose tests validate it against the SDK schema and whose real files run through the api install gate. - e2e section-styles.spec.ts: install → wrap → computed background in edit and read mode → unwrap → neutral fallback after disabling the plugin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
7.2 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">withoutallow-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'. No network access (connect-src 'none') in v1. - Host ↔ plugin communication:
postMessageRPC with structured-clone payloads.packages/plugin-sdkprovides both sides:- plugin side:
createPlugin({ onRender, onEdit, … }), typedhost.*calls; - host side: frame lifecycle, request routing, permission filtering, timeouts (a hung plugin never blocks the app).
- plugin side:
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) |
Lifecycle & administration
- 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. - Instance mode (Site Admin):
disabled|optional|required(vision: Site Admin activates plugins optionally or mandatorily). - Pond activation (Pond Admin): toggle
optionalplugins per pond. - 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.
- Uninstall: blocked while
required; otherwise the package is removed, existingplugin_blocknodes render the manifestfallback(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).
These live in packages/plugins/ in the monorepo, are built by CI, and
double as the plugin-SDK integration tests.