export interface IManifestDiagnostic { readonly severity: 'error' | 'warn'; readonly code: string; readonly message: string; readonly field?: string; readonly componentIndex?: number; } export interface IManifestValidationResult { readonly valid: boolean; readonly errors: string[]; readonly diagnostics: ReadonlyArray; } type JsonRecord = Record; const SUPPORTED_FS_POLICIES = new Set([ 'none', 'matrix-only', 'project-readonly', 'project-readwrite', 'global-readonly', ]); const SUPPORTED_HOST_API_POLICIES = new Set([ 'none', 'same-origin', ]); function asRecord(value: unknown): JsonRecord | null { if (!value || typeof value !== 'object' || Array.isArray(value)) { return null; } return value as JsonRecord; } function optionalTrimmedString(value: unknown): string | undefined { if (typeof value !== 'string') { return undefined; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : undefined; } function validateInstallHook( diagnostics: IManifestDiagnostic[], install: JsonRecord, phase: 'validate' | 'migrate' | 'seed' | 'verify', ): void { const hook = asRecord(install[phase]); if (!hook) { if (install[phase] !== undefined) { pushError(diagnostics, { code: 'MXPKG_INSTALL_HOOK_INVALID', field: `install.${phase}`, message: `install.${phase} must be an object when provided`, }); } return; } if (!optionalTrimmedString(hook.script)) { pushError(diagnostics, { code: 'MXPKG_INSTALL_HOOK_SCRIPT_INVALID', field: `install.${phase}.script`, message: `install.${phase}.script must be a non-empty string`, }); } } function pushError( diagnostics: IManifestDiagnostic[], entry: Omit ): void { diagnostics.push({ ...entry, severity: 'error' }); } export function validateManifestForMxCli(manifest: unknown): IManifestValidationResult { const diagnostics: IManifestDiagnostic[] = []; const manifestRecord = asRecord(manifest); if (!manifestRecord) { pushError(diagnostics, { code: 'MXPKG_MANIFEST_NOT_OBJECT', message: 'Manifest must be a JSON object', }); return { valid: false, diagnostics, errors: diagnostics.map((entry) => entry.message), }; } if (!optionalTrimmedString(manifestRecord.name)) { pushError(diagnostics, { code: 'MXPKG_NAME_INVALID', field: 'name', message: 'name must be a non-empty string', }); } if (!optionalTrimmedString(manifestRecord.version)) { pushError(diagnostics, { code: 'MXPKG_VERSION_INVALID', field: 'version', message: 'version must be a non-empty string', }); } const runtime = asRecord(manifestRecord.runtime); if (!runtime || !optionalTrimmedString(runtime.language) || !optionalTrimmedString(runtime.entry)) { pushError(diagnostics, { code: 'MXPKG_RUNTIME_INVALID', field: 'runtime', message: 'runtime.language and runtime.entry are required', }); } const permissions = asRecord(manifestRecord.permissions); const fsPolicy = permissions ? optionalTrimmedString(permissions.fsPolicy) : undefined; if (!fsPolicy || !SUPPORTED_FS_POLICIES.has(fsPolicy)) { pushError(diagnostics, { code: 'MXPKG_PERMISSIONS_FSPOLICY_INVALID', field: 'permissions.fsPolicy', message: 'permissions.fsPolicy must be one of none|matrix-only|project-readonly|project-readwrite|global-readonly', }); } const hostApi = permissions ? optionalTrimmedString(permissions.hostApi) : undefined; if (permissions?.hostApi !== undefined && (!hostApi || !SUPPORTED_HOST_API_POLICIES.has(hostApi))) { pushError(diagnostics, { code: 'MXPKG_PERMISSIONS_HOSTAPI_INVALID', field: 'permissions.hostApi', message: 'permissions.hostApi must be one of none|same-origin', }); } for (const field of ['network', 'subprocess', 'env']) { if (permissions && permissions[field] !== undefined && typeof permissions[field] !== 'boolean') { pushError(diagnostics, { code: 'MXPKG_PERMISSIONS_BOOLEAN_INVALID', field: `permissions.${field}`, message: `permissions.${field} must be boolean when provided`, }); } } const install = asRecord(manifestRecord.install); if (manifestRecord.install !== undefined && !install) { pushError(diagnostics, { code: 'MXPKG_INSTALL_INVALID', field: 'install', message: 'install must be an object when provided', }); } else if (install) { validateInstallHook(diagnostics, install, 'validate'); validateInstallHook(diagnostics, install, 'migrate'); validateInstallHook(diagnostics, install, 'seed'); validateInstallHook(diagnostics, install, 'verify'); } const componentsValue = manifestRecord.components; if (!Array.isArray(componentsValue)) { pushError(diagnostics, { code: 'MXPKG_COMPONENTS_NOT_ARRAY', field: 'components', message: 'components must be an array', }); } else { componentsValue.forEach((entry, componentIndex) => { const component = asRecord(entry); if (!component) { pushError(diagnostics, { code: 'MXPKG_COMPONENT_NOT_OBJECT', field: 'components', componentIndex, message: `component[${componentIndex}] must be an object`, }); return; } const type = optionalTrimmedString(component.type); if (!type) { pushError(diagnostics, { code: 'MXPKG_COMPONENT_TYPE_INVALID', field: 'type', componentIndex, message: `component[${componentIndex}].type must be a non-empty string`, }); return; } const exportName = optionalTrimmedString(component.export); if (component.export !== undefined && !exportName) { pushError(diagnostics, { code: 'MXPKG_COMPONENT_EXPORT_INVALID', field: 'export', componentIndex, message: `component[${componentIndex}].export must be a non-empty string when provided`, }); return; } const mount = optionalTrimmedString(component.mount); if (component.mount !== undefined && !mount) { pushError(diagnostics, { code: 'MXPKG_COMPONENT_MOUNT_INVALID', field: 'mount', componentIndex, message: `component[${componentIndex}].mount must be a non-empty string when provided`, }); return; } if (component.autoStart !== undefined && typeof component.autoStart !== 'boolean') { pushError(diagnostics, { code: 'MXPKG_COMPONENT_AUTOSTART_INVALID', field: 'autoStart', componentIndex, message: `component[${componentIndex}].autoStart must be boolean when provided`, }); return; } if (component.props !== undefined && !asRecord(component.props)) { pushError(diagnostics, { code: 'MXPKG_COMPONENT_PROPS_INVALID', field: 'props', componentIndex, message: `component[${componentIndex}].props must be an object when provided`, }); return; } const componentFsPolicy = optionalTrimmedString(component.fsPolicy); if ( component.fsPolicy !== undefined && (!componentFsPolicy || !SUPPORTED_FS_POLICIES.has(componentFsPolicy)) ) { pushError(diagnostics, { code: 'MXPKG_COMPONENT_FSPOLICY_INVALID', field: 'fsPolicy', componentIndex, message: `component[${componentIndex}].fsPolicy must be one of matrix-only|project-readonly|project-readwrite|global-readonly|none`, }); } }); } const errors = diagnostics .filter((entry) => entry.severity === 'error') .map((entry) => entry.message); return { valid: errors.length === 0, diagnostics, errors, }; }