/** * GET /_emdash/api/toolbar/extensions * * Buttons plugins add to the visual-editing toolbar. A plugin takes part by * exposing a private route named `toolbar` that returns * `{ label, script, config? }`: the toolbar shows the label and, when it is * clicked, loads `script` — which registers * `window.__emdashToolbarExtensions[pluginId] = { open(config) }` — then * calls `open`. The toolbar is a browser surface for signed-in editors, so * this is session-only: an API token gets nothing here. */ import type { Permission } from "@premium-cms/auth"; import type { APIRoute } from "astro"; import { requirePerm } from "#api/authorize.js"; import { apiError, apiSuccess } from "#api/error.js"; export const prerender = false; /** Same threshold as the toolbar itself (author and above). */ const MIN_ROLE = 30; const HTTPS = /^https:\/\//; export interface ToolbarExtension { pluginId: string; label: string; script: string; config: unknown; } /** One line per plugin route considered, for `?debug=1`: why it did or did not become a button. */ interface Diagnostic { pluginId: string; permission: string; outcome: "ok" | "permission-denied" | "route-failed" | "invalid-descriptor"; detail?: string; } export const GET: APIRoute = async ({ request, locals }) => { const { emdash, user } = locals; if (!emdash) return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500); if (!user) return apiError("UNAUTHORIZED", "Authentication required", 401); if (locals.tokenAuth) { return apiError( "TOKEN_AUTH_FORBIDDEN", "The toolbar is a browser surface; API tokens cannot use it.", 403, ); } if (user.role < MIN_ROLE) return apiSuccess({ extensions: [] }); const debug = new URL(request.url).searchParams.has("debug"); const allRoutes = emdash.listPluginRoutes?.() ?? []; const candidates = allRoutes.filter((r) => r.route === "toolbar" && !r.public); const extensions: ToolbarExtension[] = []; const diagnostics: Diagnostic[] = []; for (const candidate of candidates) { const note = (outcome: Diagnostic["outcome"], detail?: string) => diagnostics.push({ pluginId: candidate.pluginId, permission: candidate.permission, outcome, detail, }); // The plugin's own permission for the route still applies to this caller. if (requirePerm(user, candidate.permission as Permission)) { note("permission-denied"); continue; } try { const result = await emdash.handlePluginApiRoute( candidate.pluginId, "POST", "/toolbar", request, { ...user, tokenAuth: false, }, ); if (!result.success) { note("route-failed", `${result.error?.code ?? "?"}: ${result.error?.message ?? ""}`); continue; } const data = (result.data ?? null) as Record | null; const label = typeof data?.label === "string" ? data.label.trim().slice(0, 40) : ""; const script = typeof data?.script === "string" && HTTPS.test(data.script) ? data.script : ""; if (!label || !script) { note("invalid-descriptor", JSON.stringify(data).slice(0, 300)); continue; } extensions.push({ pluginId: candidate.pluginId, label, script, config: data?.config ?? null, }); note("ok"); } catch (error) { // A broken extension must not take the toolbar down for the others. note("route-failed", error instanceof Error ? error.message : String(error)); } } if (debug) { return apiSuccess({ extensions, debug: { user: { id: user.id, role: user.role }, pluginRoutes: allRoutes.length, candidates: candidates.map((c) => c.pluginId), diagnostics, }, }); } return apiSuccess({ extensions }); };