All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m21s
CI / Build container images (pull_request) Successful in 3m59s
CI / Auth e2e pack (pull_request) Successful in 8m35s
CI / Import/export fidelity gate (pull_request) Successful in 1m1s
CD / Build and push images (push) Successful in 17s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m17s
CD / Promote to Int (push) Successful in 12s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m27s
CI / Import/export fidelity gate (push) Successful in 58s
CI / Lint, typecheck, test (push) Successful in 6m30s
The install path records the SHA-256 of the delivered bundle ZIP (plugins.bundle_hash; pre-#232 installs show it as unknown until reinstalled). plugins.allowlist in instance_settings names permitted ids with their pinned hashes: empty (default) = not enforced, existing instances unchanged; non-empty = installs of unlisted or deviating bundles are rejected (plugin_not_pinned / plugin_hash_mismatch, 403), and an installed plugin outside the list or with a deviating hash does not load — absent from pond mount lists, frame/assets 404. Every rejection is audited (plugin.rejected, catalogue v1.5). A version bump changes the hash and therefore requires an explicit re-pin — the intended friction (ADR 0025). Admin UI shows observed vs pinned hash per plugin with pin/re-pin/unpin. Scope stated honestly in plugin-architecture.md: the pin answers "is this the reviewed bundle"; post-install disk tampering is platform integrity (ADR 0019), sandbox containment stays the sandbox's job. Hardening guide row + catalog advisory triage; residual risk R-03 resolved. e2e: empty-allowlist compatibility, pinned load, unpinned and tampered installs rejected and audited, pin drift blocks loading while the admin still sees the mismatch, version bump needs re-pin. Full api suite 101 files / 561 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8
133 lines
4.4 KiB
TypeScript
133 lines
4.4 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;
|
|
/** SHA-256 (hex) of the installed bundle ZIP (#232); null = installed
|
|
* before hash recording existed — reinstall to record it. */
|
|
bundleSha256: string | null;
|
|
/** Verdict against `plugins.allowlist` (#232): `not_enforced` while the
|
|
* allowlist is empty; otherwise pinned / unpinned / mismatch. */
|
|
pinning: 'not_enforced' | 'pinned' | 'unpinned' | 'mismatch';
|
|
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',
|
|
'plugin_not_pinned',
|
|
'plugin_hash_mismatch',
|
|
] 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;
|
|
}
|