| /** | |
| * PluginManifestValidator — schema validation for plugin manifest files. | |
| * | |
| * Uses the shared Zod schema (`pluginManifestV1Schema`) to validate | |
| * manifest payloads. Provides both a safe `parse()` variant (returns | |
| * a result union) and a throwing `parseOrThrow()` for HTTP error | |
| * propagation at install time. | |
| * | |
| * @see PLUGIN_SPEC.md §10 — Plugin Manifest | |
| * @see packages/shared/src/validators/plugin.ts — Zod schema definition | |
| */ | |
| import { pluginManifestV1Schema } from "@paperclipai/shared"; | |
| import type { PaperclipPluginManifestV1 } from "@paperclipai/shared"; | |
| import { PLUGIN_API_VERSION } from "@paperclipai/shared"; | |
| import { badRequest } from "../errors.js"; | |
| // --------------------------------------------------------------------------- | |
| // Supported manifest API versions | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * The set of plugin API versions this host can accept. | |
| * When a new API version is introduced, add it here. Old versions should be | |
| * retained until the host drops support for them. | |
| */ | |
| const SUPPORTED_VERSIONS = [PLUGIN_API_VERSION] as const; | |
| // --------------------------------------------------------------------------- | |
| // Parse result types | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Successful parse result. | |
| */ | |
| export interface ManifestParseSuccess { | |
| success: true; | |
| manifest: PaperclipPluginManifestV1; | |
| } | |
| /** | |
| * Failed parse result. `errors` is a human-readable description of what went | |
| * wrong; `details` is the raw Zod error list for programmatic inspection. | |
| */ | |
| export interface ManifestParseFailure { | |
| success: false; | |
| errors: string; | |
| details: Array<{ path: (string | number)[]; message: string }>; | |
| } | |
| /** Union of parse outcomes. */ | |
| export type ManifestParseResult = ManifestParseSuccess | ManifestParseFailure; | |
| // --------------------------------------------------------------------------- | |
| // PluginManifestValidator interface | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Service for parsing and validating plugin manifests. | |
| * | |
| * @see PLUGIN_SPEC.md §10 — Plugin Manifest | |
| */ | |
| export interface PluginManifestValidator { | |
| /** | |
| * Try to parse `input` as a plugin manifest. | |
| * | |
| * Returns a {@link ManifestParseSuccess} when the input passes all | |
| * validation rules, or a {@link ManifestParseFailure} with human-readable | |
| * error messages when it does not. | |
| * | |
| * This is the "safe" variant — it never throws. | |
| */ | |
| parse(input: unknown): ManifestParseResult; | |
| /** | |
| * Parse `input` as a plugin manifest, throwing a 400 HttpError on failure. | |
| * | |
| * Use this at install time when an invalid manifest should surface as an | |
| * HTTP error to the caller. | |
| * | |
| * @throws {HttpError} 400 Bad Request if the manifest is invalid. | |
| */ | |
| parseOrThrow(input: unknown): PaperclipPluginManifestV1; | |
| /** | |
| * Return the list of plugin API versions supported by this host. | |
| * | |
| * Callers can use this to present the supported version range to operators | |
| * or to decide whether a candidate plugin can be installed. | |
| */ | |
| getSupportedVersions(): readonly number[]; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Factory | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Create a {@link PluginManifestValidator}. | |
| * | |
| * Usage: | |
| * ```ts | |
| * const validator = pluginManifestValidator(); | |
| * | |
| * // Safe parse — inspect the result | |
| * const result = validator.parse(rawManifest); | |
| * if (!result.success) { | |
| * console.error(result.errors); | |
| * return; | |
| * } | |
| * const manifest = result.manifest; | |
| * | |
| * // Throwing parse — use at install time | |
| * const manifest = validator.parseOrThrow(rawManifest); | |
| * | |
| * // Check supported versions | |
| * const versions = validator.getSupportedVersions(); // [1] | |
| * ``` | |
| */ | |
| export function pluginManifestValidator(): PluginManifestValidator { | |
| return { | |
| parse(input: unknown): ManifestParseResult { | |
| const result = pluginManifestV1Schema.safeParse(input); | |
| if (result.success) { | |
| return { | |
| success: true, | |
| manifest: result.data as PaperclipPluginManifestV1, | |
| }; | |
| } | |
| const details = result.error.errors.map((issue) => ({ | |
| path: issue.path, | |
| message: issue.message, | |
| })); | |
| const errors = details | |
| .map(({ path, message }) => | |
| path.length > 0 ? `${path.join(".")}: ${message}` : message, | |
| ) | |
| .join("; "); | |
| return { | |
| success: false, | |
| errors, | |
| details, | |
| }; | |
| }, | |
| parseOrThrow(input: unknown): PaperclipPluginManifestV1 { | |
| const result = this.parse(input); | |
| if (!result.success) { | |
| throw badRequest(`Invalid plugin manifest: ${result.errors}`, result.details); | |
| } | |
| return result.manifest; | |
| }, | |
| getSupportedVersions(): readonly number[] { | |
| return SUPPORTED_VERSIONS; | |
| }, | |
| }; | |
| } | |