Plugin storage, install API, and directory watcher #71

Closed
opened 2026-07-04 14:52:37 +02:00 by fable-5 · 1 comment
Collaborator

Context

Site Admins install plugin ZIPs via GUI or by dropping them into the plugins volume (vision + ADR 0008).

Scope

Backend: plugins + pond_plugins models; POST /admin/plugins (ZIP upload: structure check, manifest validation, CSS sanitization for style plugins, bundle size limit, apiVersion gate) unpacking to the plugins volume; static serving of plugin assets under /plugins/:id/:version/ (immutable cache headers); directory watcher registering dropped ZIPs with the same validation (invalid → quarantine subfolder + log); update flow (same id, higher version) and uninstall (blocked while required).

Acceptance criteria

  • GUI install and directory drop both yield an installed plugin (integration tests)
  • each invalid-package class is rejected/quarantined with its specific error
  • update replaces atomically (no window where assets 404)
  • uninstall refuses while required; after uninstall, assets are gone and metadata marks it removed

Technical notes

  • ADR 0008, plugin-architecture.md §Lifecycle, security.md §Plugins.

Dependencies

Depends on #52, #70.

Size: ~2 days


Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add de and en), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.

## Context Site Admins install plugin ZIPs via GUI or by dropping them into the plugins volume (vision + ADR 0008). ## Scope Backend: `plugins` + `pond_plugins` models; `POST /admin/plugins` (ZIP upload: structure check, manifest validation, CSS sanitization for style plugins, bundle size limit, apiVersion gate) unpacking to the plugins volume; static serving of plugin assets under `/plugins/:id/:version/` (immutable cache headers); directory watcher registering dropped ZIPs with the same validation (invalid → quarantine subfolder + log); update flow (same id, higher version) and uninstall (blocked while `required`). ## Acceptance criteria - [ ] GUI install and directory drop both yield an installed plugin (integration tests) - [ ] each invalid-package class is rejected/quarantined with its specific error - [ ] update replaces atomically (no window where assets 404) - [ ] uninstall refuses while required; after uninstall, assets are gone and metadata marks it removed ## Technical notes - ADR 0008, plugin-architecture.md §Lifecycle, security.md §Plugins. ## Dependencies Depends on #52, #70. **Size**: ~2 days --- *Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add `de` **and** `en`), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.*
fable-5 added this to the M7 — Plugins milestone 2026-07-04 14:52:37 +02:00
fable-5 added the
backend
plugins
labels 2026-07-04 14:52:37 +02:00
Collaborator

Done — pipeline green (all 8 contexts) and verified live on Test (readyz ok, api Up (healthy), watcher watching plugin dropzone at /data/plugins/_dropzone).

Backend for installing plugin ZIPs (ADR 0008, plugin-architecture.md §Lifecycle, security.md §Plugins), consuming the #70 SDK.

Acceptance criteria

  • GUI install and directory drop both yield an installed plugin (e2e DB test: POST /admin/plugins + watcher processDropped)
  • each invalid-package class rejected/quarantined with its specific error (unit test over every class; drop of an invalid ZIP → quarantined)
  • update replaces atomically (staging dir + rename before the metadata pointer flips; test asserts new-version asset 200 while old 404s and its files are gone)
  • uninstall refuses while required; afterwards assets are gone and metadata is tombstoned (removedAt)

What landed

  • Schema plugins + pond_plugins + PluginInstanceMode; migration 20260710130000_plugins (applied cleanly on Test).
  • PluginPackageService (pure): fflate unzip → structure / manifest (SDK) / apiVersion / kind-bundle-styles / CSS-sanitation (no @import, external url(), expression()) / zip-slip + unpacked-size guards.
  • PluginStorageService: <PLUGINS_DIR>/<id>/<version>/, atomic writeVersion, traversal-safe asset resolution, dropzone/quarantine.
  • PluginsService: install/update (higher-version-only, preserves instance mode) / uninstall (required-guarded, soft-delete + files removed) / list / get.
  • POST/GET/DELETE /admin/plugins (SiteAdminGuard); public version-pinned GET /plugins/:id/:version/*rest (immutable + nosniff).
  • PluginWatcherService: dropzone watch with the same validation; invalid → quarantine + log; never crashes boot (setup failure disables drop-to-install and logs a warning).
  • Image bakes PLUGINS_DIR=/data/plugins (writable, node-owned) so it works out of the box; repo compose adds the plugins volume for persistence.

Follow-up (needs a shared-host write to authorize): the Test/Int stage compose files on the VPS are not synced by CD, so they still lack the plugins volume mount — installed plugins there are currently ephemeral (image default dir) until the volume is added, same pattern as the #62/#67 sidecars.

Fix history in this issue: the initial push crash-looped on Test because the watcher's mkdir on the non-writable default path aborted bootstrap; hardened + image default fixed it (commits d7aa1fb, f3938b7).

Done — pipeline green (all 8 contexts) and verified live on Test (readyz `ok`, api `Up (healthy)`, watcher `watching plugin dropzone` at `/data/plugins/_dropzone`). Backend for installing plugin ZIPs (ADR 0008, plugin-architecture.md §Lifecycle, security.md §Plugins), consuming the #70 SDK. **Acceptance criteria** - [x] GUI install and directory drop both yield an installed plugin (e2e DB test: `POST /admin/plugins` + watcher `processDropped`) - [x] each invalid-package class rejected/quarantined with its specific error (unit test over every class; drop of an invalid ZIP → quarantined) - [x] update replaces atomically (staging dir + rename before the metadata pointer flips; test asserts new-version asset 200 while old 404s and its files are gone) - [x] uninstall refuses while `required`; afterwards assets are gone and metadata is tombstoned (`removedAt`) **What landed** - Schema `plugins` + `pond_plugins` + `PluginInstanceMode`; migration `20260710130000_plugins` (applied cleanly on Test). - `PluginPackageService` (pure): fflate unzip → structure / manifest (SDK) / apiVersion / kind-bundle-styles / CSS-sanitation (no `@import`, external `url()`, `expression()`) / zip-slip + unpacked-size guards. - `PluginStorageService`: `<PLUGINS_DIR>/<id>/<version>/`, atomic writeVersion, traversal-safe asset resolution, dropzone/quarantine. - `PluginsService`: install/update (higher-version-only, preserves instance mode) / uninstall (required-guarded, soft-delete + files removed) / list / get. - `POST/GET/DELETE /admin/plugins` (SiteAdminGuard); public version-pinned `GET /plugins/:id/:version/*rest` (immutable + nosniff). - `PluginWatcherService`: dropzone watch with the same validation; invalid → quarantine + log; **never crashes boot** (setup failure disables drop-to-install and logs a warning). - Image bakes `PLUGINS_DIR=/data/plugins` (writable, node-owned) so it works out of the box; repo compose adds the `plugins` volume for persistence. **Follow-up (needs a shared-host write to authorize):** the Test/Int stage compose files on the VPS are not synced by CD, so they still lack the `plugins` volume mount — installed plugins there are currently ephemeral (image default dir) until the volume is added, same pattern as the #62/#67 sidecars. Fix history in this issue: the initial push crash-looped on Test because the watcher's `mkdir` on the non-writable default path aborted bootstrap; hardened + image default fixed it (commits `d7aa1fb`, `f3938b7`).
Sign in to join this conversation.
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: stwaidele/dorfteich#71
No description provided.