diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ff10ac3..fa807bd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -139,6 +139,18 @@ where the setting lives now (see `dune migrate:config`, which reports these too) ### Changed — BREAKING +- **The contract keeps only what core consumes; each plugin declares its own extension points + (#127).** `AdminPageRegistration`, `AdminRouteRegistration`, `ContentEditorPlugin`, + `AdminBarAction` and `AdminBarActionContext` leave `@dune/core/plugins` and + `@dune/plugin-api`; core never read them. The keys `admin.pages`, `admin.routes`, + `editor.content` (now `admin.editor`) and `bar.actions` leave `Capabilities` / + `Contributions` and belong to the plugin that reads them (`@dune/plugin-admin`, + `@dune/plugin-inline-edit`), which declares them by declaration merging. Both interfaces are + now open to merging; the rule is that a key is prefixed with the name of the plugin that + consumes it, and unprefixed names are core's. At runtime nothing changes: the registry was + already keyed by string. `ResponseTransformContext.barActions` and `.adminPrefix` are replaced + by `ResponseTransformContext.capabilities`, the same reader `MountApi.capabilities` offers: a + bar-owning plugin reads `list("bar.actions")` and `get("admin.prefix")` per request. - **Site-wide permissions are checked on `app:site`, not `app:admin` (#130).** `pages.update` and the other admin-tier actions name what they protect (this site's content), not the panel that happens to administer it, so the same check works on a site with no admin, one admin diff --git a/src/cli/response-transforms.ts b/src/cli/response-transforms.ts index 6f506b17..692d73ba 100644 --- a/src/cli/response-transforms.ts +++ b/src/cli/response-transforms.ts @@ -38,7 +38,9 @@ interface AdminAuthMiddleware { authenticate(req: Request): Promise; } import type { DuneConfig } from "../config/types.ts"; -import type { AdminBarAction, AdminBarActionContext, DunePlugin, ResponseTransformContext } from "../hooks/types.ts"; +import type { DunePlugin, ResponseTransformContext } from "../hooks/types.ts"; +import type { CapabilityReader } from "../plugin-api/capabilities.ts"; + import type { DuneAuthSystem } from "../auth/authz.ts"; import type { PageIndex } from "../content/types.ts"; import { highestAdminRole, roleHasPermission, SITE_RESOURCE } from "../auth/authz-schema.ts"; @@ -47,6 +49,13 @@ import { hasSessionCookie } from "./admin-bar-inject.ts"; import { isAdminPath } from "./serve-utils.ts"; import { scrubMarkersFromResponse } from "./marker-scrub.ts"; +/** What a transform sees when the caller supplies no registry: nothing is provided. */ +const EMPTY_CAPABILITIES: CapabilityReader = { + get: () => undefined, + list: () => [], + providers: () => [], +}; + /** * Narrow shape accepted from a route resolver's result — just the fields * this module actually reads. `RouteResolver.resolve()` (`routing/resolver.ts`) @@ -72,11 +81,8 @@ export interface RunResponseTransformsOptions { auth: AdminAuthMiddleware | null; /** Cookies that carry the `editor.session` provider's session (`RequestAuthenticator.sessionCookies`); omitted means any cookie. */ sessionCookies?: readonly string[]; - /** - * Bar buttons contributed to the `bar.actions` point, in plugin registration - * order. Handed to transform plugins as `ResponseTransformContext.barActions`. - */ - barActions?: readonly ((ctx: AdminBarActionContext) => AdminBarAction[])[]; + /** What plugins provide; handed to transform plugins as `ResponseTransformContext.capabilities`. Omitted means nothing is provided. */ + capabilities?: CapabilityReader; /** * Polizy authz system — the sole authority for the `pages.update` check * below (dec-identity-unification Phase 5c/7). Undefined only when authz @@ -134,8 +140,8 @@ export async function runPluginResponseTransforms( config, adminPrefix, actionToRelations, - barActions = [], sessionCookies, + capabilities = EMPTY_CAPABILITIES, } = opts; const transformPlugins = plugins.filter((p) => p.transformResponse); @@ -202,12 +208,7 @@ export async function runPluginResponseTransforms( template: matchedPage.template, } : null, - // "" when no admin is installed: plugins that build admin links only do so for a signed-in admin. - adminPrefix: adminPrefix ?? "", - // A plugin can contribute to `bar.actions` without declaring - // transformResponse itself, so the list comes from the contribution - // point, not from the plugins selected to run a transform. - barActions, + capabilities, }); } diff --git a/src/hooks/types.ts b/src/hooks/types.ts index 089a82ee..4ee1cf69 100644 --- a/src/hooks/types.ts +++ b/src/hooks/types.ts @@ -249,133 +249,6 @@ export interface PublicRouteRegistration { island?: string; } -/** - * Context passed to a `bar.actions` contribution. - * - * Deliberately the same narrowed shape {@link ResponseTransformContext} - * passes as `page`/`adminPrefix` (plus `template`, which actions need to - * decide what to show but `transformResponse` itself has never needed) — - * a plugin contributing bar actions is answering "what belongs on the bar - * for this response," the same question `transformResponse` answers for - * the page body. - * - * @since 1.2.0 - */ -export interface AdminBarActionContext { - /** Content page matching the current URL, or null for non-content routes. */ - page: { sourcePath: string; route: string; title: string | null; template: string } | null; - /** Admin panel URL prefix (e.g. `"/admin"`). */ - adminPrefix: string; -} - -/** - * A button contributed to the shared admin bar UI through the `bar.actions` - * contribution point. Rendered by whichever plugin owns the - * bar (e.g. `@dune/plugin-inline-edit`) — the contributing plugin controls - * only the button's appearance and behavior, not its position or styling. - * - * @since 1.2.0 - */ -export interface AdminBarAction { - /** - * Unique id — namespace by plugin name to avoid collisions across - * plugins, e.g. `"pdf-export:download"`. - */ - id: string; - /** Button label. */ - label: string; - /** Optional emoji/icon prefix. */ - icon?: string; - /** - * Renders as a plain link (``) when set — the common case for - * downloads and navigation. Mutually exclusive with `onClick`; `href` - * takes precedence if both are set. - */ - href?: string; - /** - * Renders as a `