dorfteich/packages/shared/src/plugins.ts
Claude Fable 5 0003063c39
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
Add pageTool plugins with toc and page-index references (#77)
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
2026-07-11 13:27:30 +02:00

125 lines
4.0 KiB
TypeScript

import { z } from 'zod';
/**
* Plugin administration types shared between api and web (ADR 0008, issue #71).
* The manifest itself lives in `@dorfteich/plugin-sdk`; these types describe an
* *installed* plugin as the instance stores and surfaces it.
*/
/**
* Instance-level activation a Site Admin sets per plugin (ADR 0008 lifecycle):
* `disabled` — installed but inert; `optional` — available, Pond Admins toggle
* it per pond; `required` — always on everywhere and cannot be uninstalled.
*/
export const PLUGIN_INSTANCE_MODES = ['disabled', 'optional', 'required'] as const;
export type PluginInstanceMode = (typeof PLUGIN_INSTANCE_MODES)[number];
/** One extension point as surfaced to admins (mirrors the manifest entry). */
export interface PluginExtensionPointView {
type: string;
id: string;
title: Record<string, string>;
}
/** An installed plugin as returned by the admin API. */
export interface PluginView {
id: string;
name: string;
version: string;
apiVersion: string;
kind: string;
mode: PluginInstanceMode;
/** Capabilities the manifest declared, shown to the Site Admin at install. */
permissions: string[];
extensionPoints: PluginExtensionPointView[];
/** Base path the sandbox loads assets from: `/plugins/<id>/<version>/`. */
assetBasePath: string;
license: string;
homepage?: string;
installedAt: string;
updatedAt: string;
}
/**
* Machine-readable rejection codes for an install/uninstall attempt. Each is
* also an `errors.<code>` i18n key. `validateManifest` field issues travel in
* the ApiErrorBody `details`.
*/
export const PLUGIN_ERROR_CODES = [
'plugin_invalid_zip',
'plugin_bad_structure',
'plugin_invalid_manifest',
'plugin_api_incompatible',
'plugin_too_large',
'plugin_missing_bundle',
'plugin_missing_styles',
'plugin_css_unsafe',
'plugin_version_not_higher',
'plugin_required_cannot_uninstall',
'plugin_not_found',
'plugin_not_optional',
] as const;
export type PluginErrorCode = (typeof PLUGIN_ERROR_CODES)[number];
/**
* An optional plugin as shown in a pond's plugin settings (issue #72): the
* installed plugin plus whether this pond has activated it. Only `optional`
* plugins appear here — `required` ones are always on and `disabled` ones are
* never available, so neither is a per-pond choice.
*/
export interface PondPluginSetting {
plugin: PluginView;
enabled: boolean;
}
/** Payload to switch a plugin's instance mode (Site Admin, issue #72). */
export const pluginModeInputSchema = z.object({
mode: z.enum(PLUGIN_INSTANCE_MODES),
});
export type PluginModeInput = z.infer<typeof pluginModeInputSchema>;
/** Payload to toggle an optional plugin for one pond (Pond Admin, issue #72). */
export const pondPluginToggleInputSchema = z.object({
enabled: z.boolean(),
});
export type PondPluginToggleInput = z.infer<typeof pondPluginToggleInputSchema>;
/**
* What a `plugin_block` of an inactive plugin renders instead of its sandbox
* (issue #76): the manifest `fallback`, resolved server-side from the stored
* manifest snapshot — which survives uninstall as a tombstone, so blocks in
* documents always have something to show. An image fallback is resolved to
* its served URL while the files exist and degrades to `null` (neutral
* placeholder) once they are gone.
*/
export interface PluginFallbackView {
pluginId: string;
name: string;
fallback: { type: 'text'; value: string } | { type: 'image'; url: string } | null;
}
/**
* Responses of the viewer-scoped plugin API (issue #74, `/api/v1/plugin/…`).
* Every call runs with the requesting user's session behind the standard
* permission guards, so a plugin never sees more than its viewer could. The
* heading outline reuses the editor's `OutlineEntry` (see editor-schema).
*/
export interface PluginPageSummary {
id: string;
title: string;
slug: string;
/** Label names, for pageTool filtering (issue #77). */
labels: string[];
}
export interface PluginPageMeta {
id: string;
title: string;
pondId: string;
slug: string;
}
export interface PluginPageContent {
markdown: string;
}