Phials plugin documentation
User guide
AI Disclosure: This page was generated by an LLM and may contain inaccuracies. Hand-crafted documentation will be implemented over time on the road to 1.0

validateManifest

Since Plugin API: 1.0.0

Validate a plugin manifest

Signature

export function validateManifest(manifest: unknown): ValidationResult {
    const errors: string[] = [];
    if (!manifest || typeof manifest !== "object") {
        return { valid: false, errors: ["Manifest must be an object"] };
    }
    const m = manifest as Record<string, unknown>;
    const unknownFields = Object.keys(m).filter((field) => !MANIFEST_FIELDS.has(field));
    if (unknownFields.length > 0) {
        errors.push(`Unknown manifest field(s): ${unknownFields.join(", ")}`);
    }
    if (typeof m.id !== "string" || !m.id.trim()) {
        errors.push('Missing or invalid "id" field');
    }
    else if (!validatePluginId(m.id)) {
        errors.push('Invalid or reserved "id". Use lowercase vendor.plugin-name and do not use the "phials." namespace');
    }
    if (typeof m.name !== "string" || !m.name.trim()) {
        errors.push('Missing or invalid "name" field');
    }
    if (typeof m.version !== "string" || !m.version.trim()) {
        errors.push('Missing or invalid "version" field');
    }
    else if (!validateSemver(m.version)) {
        errors.push('Invalid "version" format. Must be semver (e.g., "1.0.0")');
    }
    if (typeof m.minAppVersion !== "string" || !m.minAppVersion.trim()) {
        errors.push('Missing or invalid "minAppVersion" field');
    }
    else if (!validateSemver(m.minAppVersion)) {
        errors.push('Invalid "minAppVersion" format. Must be semver (e.g., "0.1.0")');
    }
    if (typeof m.pluginApiVersion !== "string" || !m.pluginApiVersion.trim()) {
        errors.push('Missing or invalid "pluginApiVersion" field');
    }
    else if (m.pluginApiVersion !== SUPPORTED_PLUGIN_API_VERSION) {
        errors.push(`Invalid "pluginApiVersion": expected exactly "${SUPPORTED_PLUGIN_API_VERSION}"`);
    }
    if (typeof m.author !== "string" || !m.author.trim()) {
        errors.push('Missing or invalid "author" field');
    }
    if (typeof m.description !== "string" || !m.description.trim()) {
        errors.push('Missing or invalid "description" field');
    }
    for (const field of ["authorUrl", "repository"] as const) {
        const value = m[field];
        if (value === undefined)
            continue;
        if (typeof value !== "string" || !value.trim()) {
            errors.push(`Invalid "${field}" field - must be a non-empty HTTPS URL`);
            continue;
        }
        try {
            const url = new URL(value);
            if (url.protocol !== "https:" || url.username || url.password) {
                errors.push(`Invalid "${field}" field - must be a public HTTPS URL`);
            }
        }
        catch {
            errors.push(`Invalid "${field}" field - must be a public HTTPS URL`);
        }
    }
    if (m.icons !== undefined) {
        if (!Array.isArray(m.icons)) {
            errors.push('Invalid "icons" field - must be an array');
        }
        else {
            const seen = new Set<string>();
            for (const icon of m.icons) {
                if (typeof icon !== "string" ||
                    !icon.trim() ||
                    !ICONIFY_ICON_PATTERN.test(icon)) {
                    errors.push(`Invalid icon: "${String(icon)}"`);
                }
                else if (seen.has(icon)) {
                    errors.push(`Duplicate icon: "${icon}"`);
                }
                else {
                    seen.add(icon);
                }
            }
        }
    }
    if (m.permissions !== undefined) {
        if (!Array.isArray(m.permissions)) {
            errors.push('Invalid "permissions" field - must be an array');
        }
        else {
            const seen = new Set<PluginPermission>();
            for (const p of m.permissions) {
                if (!VALID_PERMISSIONS.has(p as PluginPermission)) {
                    errors.push(`Invalid permission: "${p}"`);
                }
                else if (seen.has(p as PluginPermission)) {
                    errors.push(`Duplicate permission: "${p}"`);
                }
                else {
                    seen.add(p as PluginPermission);
                }
            }
            for (const [write, read] of IMPLIED_PERMISSION_PAIRS) {
                if (seen.has(write) && seen.has(read)) {
                    errors.push(`Redundant permissions: "${write}" already implies "${read}"`);
                }
            }
        }
    }
    return { valid: errors.length === 0, errors };
}