48798d4247
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 0875e2a087 |
Add the sandbox host runtime for plugin iframes (#73)
All checks were successful
CI / Build container images (push) Has been skipped
CI / Lint, typecheck, test (push) Successful in 2m57s
CI / Import/export fidelity gate (push) Successful in 46s
CD / Build and push images (push) Successful in 3m16s
CD / Deploy to Test (push) Successful in 8s
CI / Auth e2e pack (push) Successful in 4m8s
CD / Smoke tests against Test (push) Successful in 1m9s
CD / Promote to Int (push) Successful in 10s
Implements the security core of the plugin system: code-plugin surfaces run in opaque-origin iframes (sandbox="allow-scripts", never allow-same-origin) with a capability-filtered RPC bridge. - api: serve a per-plugin sandbox frame document at /plugins/:id/:version/frame with a CSP that pins every load to the plugin's own asset path (built from APP_BASE_URL, not the request Host, so a Host-rewriting proxy cannot break it) and forbids network access (connect-src 'none'). Plugin assets get Access-Control-Allow-Origin: * so the null-origin frame can load its own module bundle. - web: sandbox-host creates the frame, wires the SDK host bridge over a source-filtered postMessage transport, drives render under a 5 s deadline (hung/failed plugin -> placeholder, never a frozen page), and tears down on unmount. PluginFrame/PluginPreviewPage surface it; the built-in ui.resize handler clamps plugin-requested heights. - plugin-sdk: host bridge reports gate violations via onViolation and registers a gated handler for every v1 method, so an undeclared capability is rejected with capability_not_permitted (not unknown_method). - tests: SDK gate unit test; web sandbox unit tests (opaque origin, source filtering, timeout); and the e2e security pack with a permanent malicious fixture plugin proving no escape (DOM/cookies/storage/fetch/ undeclared capability all blocked) plus well-behaved and hung cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 |
|||
| 621aa47244 |
Add plugin storage, install API, and directory watcher (#71)
Some checks failed
CI / Auth e2e pack (push) Waiting to run
CI / Import/export fidelity gate (push) Waiting to run
CI / Build container images (push) Waiting to run
CD / Build and push images (push) Failing after 1m33s
CD / Deploy to Test (push) Has been skipped
CD / Smoke tests against Test (push) Has been skipped
CD / Promote to Int (push) Has been skipped
CI / Lint, typecheck, test (push) Has been cancelled
Backend for installing plugin ZIPs (ADR 0008, plugin-architecture.md §Lifecycle, security.md §Plugins). Consumes the #70 SDK for validation. - Schema: `plugins` (id, name, version, apiVersion, kind, mode, manifest jsonb, removedAt soft-delete) + `pond_plugins` (per-pond activation) + `PluginInstanceMode` enum; migration 20260710130000_plugins. - `PluginPackageService`: pure, stateless ZIP → validated package via fflate — structure check, manifest validation (SDK), apiVersion gate, kind/bundle/styles rules, CSS sanitation (no @import / external url() / expression()), zip-slip and unpacked-size guards. Each failure carries a stable PluginErrorCode; manifest issues travel as ApiError details. - `PluginStorageService`: on-disk layout `<PLUGINS_DIR>/<id>/<version>/`; atomic writeVersion (staging dir + rename, no 404 window mid-update), removeVersion/removePlugin, traversal-safe asset resolution, dropzone + quarantine dirs. - `PluginsService`: install/update (update only to a strictly higher version, preserving the admin's instance mode; files land before the metadata pointer flips) / uninstall (refused while required; soft-delete + files removed + pond activations dropped) / list / get. - `POST/GET/DELETE /admin/plugins` (SiteAdminGuard, multer memory upload), error→HTTP-status mapping. Public version-pinned static serving at `GET /plugins/:id/:version/*rest` with immutable cache + nosniff, only for the installed current version. - `PluginWatcherService`: watches `<PLUGINS_DIR>/_dropzone/`, runs the same validation, installs valid drops and quarantines invalid ones with the error logged; inert under NODE_ENV=test (tests drive processDropped). - SDK: `compareVersions`/`isHigherVersion`. shared: `PluginView`, `PluginInstanceMode`, `PLUGIN_ERROR_CODES`, `PLUGINS_DIR` env, plugin error i18n (de+en). Compose: `plugins` volume + `PLUGINS_DIR`. - Tests: package unit test (valid + each invalid class) and an e2e DB test (GUI install + immutable serving, non-admin 403, invalid-manifest details, dropzone install + quarantine, atomic higher-only update, required-guarded uninstall that removes files and tombstones metadata). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1 |
|||
| ec6ca80c4d |
Add plugin SDK: manifest schema, capabilities, and RPC protocol (#70)
All checks were successful
CD / Build and push images (push) Successful in 3m14s
CI / Lint, typecheck, test (push) Successful in 3m18s
CI / Auth e2e pack (push) Successful in 4m3s
CI / Import/export fidelity gate (push) Successful in 54s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m9s
CD / Promote to Int (push) Has been skipped
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
|