All checks were successful
CI / Auth e2e pack (push) Successful in 4m37s
CI / Import/export fidelity gate (push) Successful in 43s
CI / Lint, typecheck, test (push) Successful in 2m54s
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Successful in 3m14s
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m8s
CD / Promote to Int (push) Successful in 9s
The read-only widget surface over page/pond data (ADR 0008 extension point `pageTool`): - Host: PageToolsPanel lists the pond's active pageTool surfaces behind disclosures — each sandbox iframe mounts lazily on first open and tears down on close. The same surfaces are insertable as plugin_block embeds (#76's insert picker now offers pageTool points too; the sandbox drives both through the same render lifecycle). - New `ui.scrollToHeading(headingId)` capability: outline ids are derived from the doc and never stamped into the DOM, so the host resolves the id to its heading position via the shared extractOutline and scrolls the matching rendered heading. - `readPond.listPages` now carries label *names* per summary (PagesService.pluginPageSummaries) — the page-index filter chips work on data the viewer could resolve anyway; per-page permission filtering stays in the service as before. - Reference plugins packages/plugins/toc and packages/plugins/page-index: real SDK consumers (createPlugin + windowTransport), bundled with esbuild into the package ZIP; i18n de/en is inlined at build time — the sandbox CSP forbids runtime fetches, the i18n/ files stay the single source. The toc re-fetches its outline on a slow poll, so live heading edits appear once the collab server has re-derived the content cache. - e2e page-tools.spec.ts covers the acceptance criteria: live outline updates after the persistence debounce, heading click scrolls, embedded page-index navigates via ui.openPage, and a label-restricted reader never sees the denied page in the index. - CI: the auth-e2e job now runs the section-styles (missed in #75), plugin-blocks, and page-tools packs, with login-rate-limit resets. - plugins.e2e.db.test clears the plugin registry up front: a local dev DB is shared with the e2e stack, whose installed real `toc` would otherwise collide with the fixture of the same id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
123 lines
7.2 KiB
Markdown
123 lines
7.2 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'`.
|
|
No network access (`connect-src 'none'`) in v1.
|
|
- 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) |
|
|
|
|
## 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).
|
|
|
|
These live in `packages/plugins/` in the monorepo, are built by CI, and
|
|
double as the plugin-SDK integration tests.
|