/** * EmDash Astro types * * This file re-exports types from the core package and defines * the locals interface that the middleware provides. */ import type { Element } from "@premium-cms/blocks"; import type { Kysely } from "kysely"; import type { ContentFieldFilters } from "../content-list-query.js"; import type { RouteCallerInput, RouteMeta } from "../plugins/routes.js"; import type { SettingField } from "../plugins/types.js"; // Re-export core types export type { ContentItem, MediaItem, ContentListResponse, ContentResponse, MediaListResponse, MediaResponse, Storage, Database, } from "../index.js"; /** * Manifest collection definition */ export interface ManifestCollection { label: string; labelSingular: string; supports: string[]; /** "git": entries are files in the site repo; saving commits. */ storage?: "db" | "git"; hasSeo: boolean; urlPattern?: string; /** Whether published entries require a slug. Defaults to true. */ routable?: boolean; titleField?: string; dateField?: string; /** * Omit the auto-generated sidebar entry in the admin. The collection is * still listed in the manifest so its routes, editor, and API keep working * — only the navigation link is dropped. */ hidden?: boolean; /** Valid custom field slugs to render in the admin content list. */ listColumns?: string[]; fields: Record< string, { kind: string; label?: string; required?: boolean; widget?: string; /** * Field options. Two shapes: * - Legacy enum: `Array<{ value, label }>` for select / multiSelect widgets * - Plugin widgets: `Record` for arbitrary per-field config * (e.g. a checkbox grid receiving its column definitions) */ options?: Array<{ value: string; label: string }> | Record; /** The `_emdash_fields` row ID. Used by the admin to forward to upload/media-list API calls. */ id?: string; /** Validation config for the field (e.g. `allowedMimeTypes` for file/image fields, subFields for repeater). */ validation?: Record; /** Render the field read-only (disabled input) in the editor. From `field.options.readOnly`. */ readOnly?: boolean; /** * Restrict the field to one editor mode. `"create"` shows it only on the * new-entry form; `"edit"` only when editing an existing entry; omit for * both. From `field.options.visibility`. */ visibility?: "create" | "edit"; /** String field rendered as a select fed from this same-origin URL. */ optionsUrl?: string; } >; } /** * Plugin manifest entry in the admin manifest */ export interface ManifestPlugin { version?: string; /** Package name for dynamic import (e.g., "@premium-cms/plugin-audit-log") */ package?: string; /** Whether the plugin is currently enabled */ enabled?: boolean; /** * How this plugin renders its admin UI: * - "react": Trusted plugin with React components (default for trusted plugins) * - "blocks": Declarative Block Kit UI via admin route handler * - "none": No admin UI */ adminMode?: "react" | "blocks" | "none"; adminPages?: Array<{ path: string; label?: string; icon?: string; }>; dashboardWidgets?: Array<{ id: string; title?: string; size?: string; }>; fieldWidgets?: Array<{ name: string; label: string; fieldTypes: string[]; elements?: Element[]; }>; /** Portable Text block types provided by this plugin */ portableTextBlocks?: Array<{ type: string; label: string; icon?: string; description?: string; placeholder?: string; fields?: Element[]; }>; } /** * Auth mode indicator for the admin UI * - "passkey": Built-in passkey authentication (default) * - string: External auth provider type (e.g., "cloudflare-access") */ export type ManifestAuthMode = string; /** * The EmDash manifest provided to the admin UI */ export interface EmDashManifest { version: string; commit?: string; hash: string; /** * Version of Astro the host project is built with. Present when the * integration could resolve it. Surfaced so the admin can evaluate a * registry plugin's `env:astro` requirement against the running host. */ astroVersion?: string; collections: Record; plugins: Record; /** * Auth mode for the admin UI. When "passkey", the security settings * (passkey management, self-signup domains) are shown. When using * external auth (e.g., "cloudflare-access"), these are hidden since * authentication is handled externally. */ authMode: ManifestAuthMode; /** * Whether self-signup is enabled (at least one allowed domain is active). * Used by the login page to conditionally show the "Sign up" link. */ signupEnabled?: boolean; /** * i18n configuration from Astro config. * Only present when i18n is enabled (multiple locales configured). */ i18n?: { defaultLocale: string; locales: string[]; prefixDefaultLocale?: boolean; }; contentLocale?: { defaultLocale: string; implicit: boolean; }; /** * Taxonomy definitions for the admin sidebar. */ taxonomies: Array<{ id: string; name: string; label: string; labelSingular?: string; hierarchical: boolean; collections: string[]; locale: string; translationGroup: string; }>; /** * Whether the plugin marketplace is configured. * When true, the admin UI can show marketplace browse/install features. * * When `registry` is also present, the registry replaces the marketplace * for the admin UI's browse and install flows. Existing marketplace-installed * plugins continue to work; new installs and updates use the registry. */ marketplace?: boolean; /** * Static-frontend build mode. When true, the integration does NOT inject the * admin/API/auth/MCP routes — the public pages are prerendered (output: * "static") from a snapshot SQLite and hosted as a static site (GitHub Pages), * while the backend (those routes) runs separately on Cloudflare. */ staticFrontend?: boolean; /** * Decentralized plugin registry configuration. * * When present, the admin UI uses the registry instead of the * centralized marketplace for browse and install. The aggregator URL * and policy fields are read by the browser; the `acceptLabelers` * header value is forwarded with every aggregator request. * * See the `registry` integration option in `astro.config.mjs`. */ registry?: { aggregatorUrl: string; acceptLabelers?: string; policy?: { /** * Minimum release age in seconds. The admin UI's * latest-release selection filter holds back releases younger * than this when computing the recommended install/update. * * Normalized from the integration option's duration string * (`"48h"`) to seconds at manifest build time so the browser * doesn't need a duration parser. */ minimumReleaseAgeSeconds?: number; /** * Publishers / packages exempt from {@link minimumReleaseAgeSeconds}. * See `RegistryConfig.policy.minimumReleaseAgeExclude`. */ minimumReleaseAgeExclude?: string[]; }; }; /** * Admin branding overrides for white-labeling. * Set via the `admin` config in `astro.config.mjs`. */ admin?: { logo?: string; siteName?: string; favicon?: string; }; /** * Managed static-frontend hosting status, for platform-provisioned instances. * Only present when the hosting parent seeded `frontend:connect_url` + * `credits:project_id` at provision. `enabled` reflects whether the frontend * (GitHub Pages) has been connected; `connectUrl` is the parent OAuth entry * point the Settings → General "Connect GitHub" button links to. */ frontend?: { managed: boolean; enabled: boolean; connectUrl: string | null; pagesUrl: string | null; /** The site repo on GitHub (once connected). */ repoUrl?: string | null; }; /** * Managed custom-domain status (platform-provisioned instances). Present when * the hosting parent seeded `custom_domain:api_url` + the instance's default * URL. Drives the Settings → General Site URL / "Check DNS" flow. */ customDomain?: { managed: boolean; defaultUrl: string; }; } /** * Standard handler response shape used by all EmDashHandlers methods. * * The error shape matches `ApiResult` from the core package — typing it * here lets route files use `result.error?.code` without unsafe casts while * keeping the data side loosely coupled (defaults to `unknown`). */ export interface HandlerResponse { success: boolean; data?: T; liveContentChanged?: boolean; error?: { code: string; message: string; details?: Record; }; } /** * The EmDash API handlers provided via Astro.locals * * Data types default to `unknown` to avoid tight coupling with the core * package. Handlers whose data shape is accessed in route files (e.g. * handleContentGet, handleRevisionGet) use narrower types. */ export interface EmDashHandlers { // Content handlers handleContentList: ( collection: string, params: { cursor?: string; limit?: number; status?: string; orderBy?: string; order?: "asc" | "desc"; locale?: string; q?: string; authorId?: string; dateField?: "createdAt" | "updatedAt" | "publishedAt"; dateFrom?: string; dateTo?: string; bylines?: string[]; bylinesNone?: boolean; includeInferredBylines?: boolean; fieldFilters?: ContentFieldFilters; }, ) => Promise; handleContentAuthors: (collection: string) => Promise; handleContentGet: ( collection: string, id: string, locale?: string, ) => Promise< HandlerResponse<{ item: { id: string; authorId: string | null; [key: string]: unknown; }; _rev?: string; }> >; handleContentCreate: ( collection: string, body: { data: Record; slug?: string | null; status?: string; authorId?: string; bylines?: Array<{ bylineId: string; roleLabel?: string | null }>; locale?: string; translationOf?: string; taxonomies?: Record; createdAt?: string | null; publishedAt?: string | null; }, ) => Promise; handleContentUpdate: ( collection: string, id: string, body: { data?: Record; slug?: string | null; status?: string; authorId?: string | null; bylines?: Array<{ bylineId: string; roleLabel?: string | null }>; locale?: string; seo?: { title?: string | null; description?: string | null; image?: string | null; canonical?: string | null; noIndex?: boolean; }; taxonomies?: Record; publishedAt?: string | null; _rev?: string; }, ) => Promise; handleContentDelete: (collection: string, id: string) => Promise; // Trash handlers handleContentListTrashed: ( collection: string, params?: { cursor?: string; limit?: number }, ) => Promise; handleContentRestore: (collection: string, id: string) => Promise; handleContentPermanentDelete: (collection: string, id: string) => Promise; handleContentCountTrashed: (collection: string) => Promise; handleContentGetIncludingTrashed: (collection: string, id: string) => Promise; handleContentDuplicate: ( collection: string, id: string, authorId?: string, ) => Promise; // Publishing & Scheduling handlers handleContentPublish: ( collection: string, id: string, options?: { publishedAt?: string; requireScheduledDue?: boolean }, ) => Promise; handleContentUnpublish: (collection: string, id: string) => Promise; handleContentSchedule: ( collection: string, id: string, scheduledAt: string, ) => Promise; handleContentUnschedule: (collection: string, id: string) => Promise; handleContentCountScheduled: (collection: string) => Promise; handleContentDiscardDraft: (collection: string, id: string) => Promise; handleContentCompare: (collection: string, id: string) => Promise; handleContentTranslations: (collection: string, id: string) => Promise; // Media handlers handleMediaList: (params: { cursor?: string; limit?: number; mimeType?: string | readonly string[]; }) => Promise; handleMediaGet: (id: string) => Promise; handleMediaCreate: (input: { filename: string; mimeType: string; size?: number; width?: number; height?: number; storageKey: string; contentHash?: string; blurhash?: string; dominantColor?: string; authorId?: string; }) => Promise; handleMediaUpdate: ( id: string, input: { alt?: string; caption?: string; width?: number; height?: number }, ) => Promise; handleMediaDelete: (id: string) => Promise; // Revision handlers handleRevisionList: ( collection: string, entryId: string, params?: { limit?: number }, ) => Promise; handleRevisionGet: (revisionId: string) => Promise< HandlerResponse<{ item: { id: string; collection: string; entryId: string; authorId: string | null; [key: string]: unknown; }; }> >; handleRevisionRestore: (revisionId: string, callerUserId: string) => Promise; // Plugin API route handler. `user` is the authenticated caller for // private routes, exposed to plugin handlers as `ctx.user`. handlePluginApiRoute: ( pluginId: string, method: string, path: string, request: Request, user?: RouteCallerInput | null, ) => Promise; // Public-only plugin API route handler for SSR page components. handlePublicPluginApiRoute: ( pluginId: string, method: string, path: string, request: Request, ) => Promise; // Plugin route metadata (for auth/caching decisions before dispatch) getPluginRouteMeta: (pluginId: string, path: string) => RouteMeta | null; /** Every enabled plugin route with the access it requires (route catalogue). */ listPluginRoutes: () => Array<{ pluginId: string; route: string; public: boolean; permission: string; }>; /** Settings schema of a marketplace/registry-installed plugin (null when unknown). */ getRuntimePluginSettingsSchema: (pluginId: string) => Record | null; getEnabledPluginMcpTools: () => Promise< Array<{ pluginId: string; name: string; description: string; route: string; permission: string; destructive: boolean; inputSchema: import("zod").ZodType; outputSchema?: import("zod").ZodType; }> >; getPluginMcpTools: (pluginId?: string) => Promise< Array<{ pluginId: string; name: string; description: string; route: string; permission: string; destructive: boolean; inputSchema: import("zod").ZodType; outputSchema?: import("zod").ZodType; }> >; serializePluginMcpConsent: ( tools: Awaited>, pluginId: string, ) => string; handlePluginMcpTool: ( pluginId: string, toolName: string, route: string, input: unknown, actorId: string, request: Request, caller?: RouteCallerInput | null, ) => Promise; handlePluginMcpDenied: ( pluginId: string, toolName: string, route: string, actorId: string, request: Request, reason: string, ) => Promise; // Media provider handlers getMediaProvider: (providerId: string) => import("../media/types.js").MediaProvider | undefined; getMediaProviderList: () => Array<{ id: string; name: string; icon?: string; capabilities: import("../media/types.js").MediaProviderCapabilities; }>; // Direct access to storage and database for advanced use cases storage: import("../index.js").Storage | null; db: Kysely; getPublicMediaUrl?: (storageKey: string) => string; // Hook pipeline for plugin integrations hooks: import("../plugins/hooks.js").HookPipeline; // Email pipeline for sending emails through the plugin system email: import("../plugins/email.js").EmailPipeline | null; // Configured plugins (for plugin management) configuredPlugins: import("../plugins/types.js").ResolvedPlugin[]; // Statically-sandboxed plugin entries (registered via `sandboxed: []`), // surfaced through the admin plugin management API alongside configured plugins. sandboxedPluginEntries: import("../emdash-runtime.js").SandboxedPluginEntry[]; // Configuration (for checking database type, auth mode, etc.) config: import("./integration/runtime.js").EmDashConfig; // Build the admin manifest from the live database. Only used by admin // routes; logged-out requests don't need it. Per-request, deduplicated // by `requestCached`. getManifest: () => Promise; // Clear the cached URL patterns used by `resolveEmDashPath`. Call after // any schema mutation that creates/updates/deletes a collection's // `urlPattern` so public routing picks up the change immediately. invalidateUrlPatternCache: () => void; // Sandbox runner (for marketplace plugin install/update) getSandboxRunner: () => import("../plugins/sandbox/types.js").SandboxRunner | null; // Whether sandbox bypass mode (sandbox: false) is active. Marketplace // install/update routes use this to skip the SANDBOX_NOT_AVAILABLE gate. isSandboxBypassed: () => boolean; // Sync marketplace plugin states (after install/update/uninstall) syncMarketplacePlugins: () => Promise; // Sync registry plugin states (after install/update/uninstall) syncRegistryPlugins: () => Promise; // Update plugin enabled/disabled status and rebuild hook pipeline setPluginStatus: (pluginId: string, status: "active" | "inactive") => Promise; // Page contribution methods (for EmDashHead/EmDashBodyStart/EmDashBodyEnd) collectPageMetadata: ( page: import("../plugins/types.js").PublicPageContext, ) => Promise; collectPageFragments: ( page: import("../plugins/types.js").PublicPageContext, ) => Promise; /** * Lazy search index health check. Search routes call this before * querying so a crash-corrupted index gets repaired on first use * rather than stalling cold start. Optional because it's only * meaningful when an FTS5-capable runtime is wired in. */ ensureSearchHealthy?: () => Promise; }