From ec6ca80c4dce247ac6c84dacff44dcff55931c13 Mon Sep 17 00:00:00 2001 From: "Claude Opus 4.8" Date: Fri, 10 Jul 2026 16:12:00 +0200 Subject: [PATCH] Add plugin SDK: manifest schema, capabilities, and RPC protocol (#70) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SDK is the contract every other M7 story builds on (ADR 0008, plugin-architecture.md). New package `@dorfteich/plugin-sdk`, standalone (only depends on zod) so a plugin author needs nothing else. - Zod manifest schema (`validateManifest`/`parseManifest`) with actionable `{ path, message }` issues and cross-field rules (extension-point/kind match, unique ids, section_style declares no permissions). Fixtures: 3 valid + 14 invalid variants, asserted individually. - `checkApiVersion` compatibility helper against the host's supported range. - Capability names + method→capability map as the single source of truth for the permission gate. - Transport-agnostic postMessage RPC engine (`createRpcEndpoint`) with request/response ids, per-request timeouts, unknown-method and endpoint-disposed handling, plus a `windowTransport` adapter. - Host side (`createHostBridge`): routes plugin capability calls through the manifest permission gate; drives plugin lifecycle (render/edit/destroy). - Plugin side (`createPlugin`): answers lifecycle calls, exposes a typed `host` proxy. RPC roundtrip verified in a jsdom MessageChannel test (roundtrip, args, timeout, unknown method, undeclared capability, dispose). - README documents the protocol with a mermaid sequence diagram. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 --- packages/plugin-sdk/README.md | 150 ++++++++++ .../manifests/invalid/01-missing-id.json | 18 ++ .../fixtures/manifests/invalid/02-bad-id.json | 19 ++ .../manifests/invalid/03-missing-name.json | 18 ++ .../manifests/invalid/04-bad-version.json | 19 ++ .../manifests/invalid/05-bad-api-version.json | 19 ++ .../manifests/invalid/06-bad-kind.json | 19 ++ .../invalid/07-empty-extension-points.json | 10 + .../manifests/invalid/08-kind-mismatch.json | 18 ++ .../09-duplicate-extension-point-id.json | 27 ++ .../invalid/10-title-missing-de.json | 18 ++ .../invalid/11-unknown-permission.json | 19 ++ .../manifests/invalid/12-bad-fallback.json | 23 ++ .../13-section-style-with-permissions.json | 19 ++ .../manifests/invalid/14-unknown-field.json | 20 ++ .../fixtures/manifests/valid/mermaid.json | 23 ++ .../manifests/valid/section-styles-basic.json | 26 ++ .../fixtures/manifests/valid/toc.json | 28 ++ packages/plugin-sdk/package.json | 34 +++ packages/plugin-sdk/src/api-version.test.ts | 41 +++ packages/plugin-sdk/src/api-version.ts | 61 ++++ packages/plugin-sdk/src/capabilities.ts | 59 ++++ packages/plugin-sdk/src/host.ts | 113 ++++++++ packages/plugin-sdk/src/index.ts | 6 + packages/plugin-sdk/src/manifest.test.ts | 119 ++++++++ packages/plugin-sdk/src/manifest.ts | 174 +++++++++++ packages/plugin-sdk/src/plugin.ts | 136 +++++++++ packages/plugin-sdk/src/rpc.test.ts | 163 +++++++++++ packages/plugin-sdk/src/rpc.ts | 269 ++++++++++++++++++ packages/plugin-sdk/tsconfig.json | 10 + packages/plugin-sdk/vitest.config.ts | 9 + pnpm-lock.yaml | 19 ++ 32 files changed, 1706 insertions(+) create mode 100644 packages/plugin-sdk/README.md create mode 100644 packages/plugin-sdk/fixtures/manifests/invalid/01-missing-id.json create mode 100644 packages/plugin-sdk/fixtures/manifests/invalid/02-bad-id.json create mode 100644 packages/plugin-sdk/fixtures/manifests/invalid/03-missing-name.json create mode 100644 packages/plugin-sdk/fixtures/manifests/invalid/04-bad-version.json create mode 100644 packages/plugin-sdk/fixtures/manifests/invalid/05-bad-api-version.json create mode 100644 packages/plugin-sdk/fixtures/manifests/invalid/06-bad-kind.json create mode 100644 packages/plugin-sdk/fixtures/manifests/invalid/07-empty-extension-points.json create mode 100644 packages/plugin-sdk/fixtures/manifests/invalid/08-kind-mismatch.json create mode 100644 packages/plugin-sdk/fixtures/manifests/invalid/09-duplicate-extension-point-id.json create mode 100644 packages/plugin-sdk/fixtures/manifests/invalid/10-title-missing-de.json create mode 100644 packages/plugin-sdk/fixtures/manifests/invalid/11-unknown-permission.json create mode 100644 packages/plugin-sdk/fixtures/manifests/invalid/12-bad-fallback.json create mode 100644 packages/plugin-sdk/fixtures/manifests/invalid/13-section-style-with-permissions.json create mode 100644 packages/plugin-sdk/fixtures/manifests/invalid/14-unknown-field.json create mode 100644 packages/plugin-sdk/fixtures/manifests/valid/mermaid.json create mode 100644 packages/plugin-sdk/fixtures/manifests/valid/section-styles-basic.json create mode 100644 packages/plugin-sdk/fixtures/manifests/valid/toc.json create mode 100644 packages/plugin-sdk/package.json create mode 100644 packages/plugin-sdk/src/api-version.test.ts create mode 100644 packages/plugin-sdk/src/api-version.ts create mode 100644 packages/plugin-sdk/src/capabilities.ts create mode 100644 packages/plugin-sdk/src/host.ts create mode 100644 packages/plugin-sdk/src/index.ts create mode 100644 packages/plugin-sdk/src/manifest.test.ts create mode 100644 packages/plugin-sdk/src/manifest.ts create mode 100644 packages/plugin-sdk/src/plugin.ts create mode 100644 packages/plugin-sdk/src/rpc.test.ts create mode 100644 packages/plugin-sdk/src/rpc.ts create mode 100644 packages/plugin-sdk/tsconfig.json create mode 100644 packages/plugin-sdk/vitest.config.ts diff --git a/packages/plugin-sdk/README.md b/packages/plugin-sdk/README.md new file mode 100644 index 0000000..4cc82ae --- /dev/null +++ b/packages/plugin-sdk/README.md @@ -0,0 +1,150 @@ +# @dorfteich/plugin-sdk + +The contract between the Dorfteich host app and a plugin bundle: the +**manifest schema**, the **capability names**, and the typed **postMessage RPC +protocol**. It is the one package a plugin author needs — it builds standalone +and pulls in only `zod`. + +Read [ADR 0008](../../docs/architecture/adr/0008-plugin-sandbox.md) and +[plugin-architecture.md](../../docs/architecture/plugin-architecture.md) first; +the manifest example there is normative and mirrored by the fixtures under +[`fixtures/manifests/`](./fixtures/manifests). + +## What's in here + +| Module | Purpose | +| ----------------- | ----------------------------------------------------------------------------------- | +| `manifest.ts` | Zod schema for `manifest.json` + `validateManifest` / `parseManifest`. | +| `api-version.ts` | `checkApiVersion` — is a plugin's `apiVersion` within the host's supported range? | +| `capabilities.ts` | Capability names and the method → capability map that gates RPC calls. | +| `rpc.ts` | Transport-agnostic RPC engine (`createRpcEndpoint`) + a `windowTransport` adapter. | +| `host.ts` | `createHostBridge` — host end: routes plugin calls through the permission gate. | +| `plugin.ts` | `createPlugin` — plugin end: answers lifecycle calls, exposes a typed `host` proxy. | + +## Manifest + +```ts +import { validateManifest } from '@dorfteich/plugin-sdk'; + +const result = validateManifest(JSON.parse(raw)); +if (!result.success) { + // result.issues: [{ path: 'extensionPoints.0.type', message: '…' }, …] +} +``` + +`validateManifest` never throws — it returns a flat list of `{ path, message }` +issues so the install path (#71) can show a Site Admin every problem at once. +`parseManifest` is the throwing variant. Cross-field rules enforced beyond the +field shapes: + +- extension point types must match the plugin `kind` + (`section_style` → `sectionStyle` only; `code` → `block`/`pageTool`); +- extension point `id`s are unique within the manifest; +- `section_style` plugins run no JavaScript and must not declare `permissions`. + +## RPC protocol + +Every code-plugin surface runs in a sandboxed `