( filePath: string, )
| 308 | * Validate a marketplace manifest file (marketplace.json) |
| 309 | */ |
| 310 | export async function validateMarketplaceManifest( |
| 311 | filePath: string, |
| 312 | ): Promise<ValidationResult> { |
| 313 | const errors: ValidationError[] = [] |
| 314 | const warnings: ValidationWarning[] = [] |
| 315 | const absolutePath = path.resolve(filePath) |
| 316 | |
| 317 | // Read file content — handle ENOENT / EISDIR / permission errors directly |
| 318 | let content: string |
| 319 | try { |
| 320 | content = await readFile(absolutePath, { encoding: 'utf-8' }) |
| 321 | } catch (error: unknown) { |
| 322 | const code = getErrnoCode(error) |
| 323 | let message: string |
| 324 | if (code === 'ENOENT') { |
| 325 | message = `File not found: ${absolutePath}` |
| 326 | } else if (code === 'EISDIR') { |
| 327 | message = `Path is not a file: ${absolutePath}` |
| 328 | } else { |
| 329 | message = `Failed to read file: ${errorMessage(error)}` |
| 330 | } |
| 331 | return { |
| 332 | success: false, |
| 333 | errors: [{ path: 'file', message, code }], |
| 334 | warnings: [], |
| 335 | filePath: absolutePath, |
| 336 | fileType: 'marketplace', |
| 337 | } |
| 338 | } |
| 339 | |
| 340 | let parsed: unknown |
| 341 | try { |
| 342 | parsed = jsonParse(content) |
| 343 | } catch (error) { |
| 344 | return { |
| 345 | success: false, |
| 346 | errors: [ |
| 347 | { |
| 348 | path: 'json', |
| 349 | message: `Invalid JSON syntax: ${errorMessage(error)}`, |
| 350 | }, |
| 351 | ], |
| 352 | warnings: [], |
| 353 | filePath: absolutePath, |
| 354 | fileType: 'marketplace', |
| 355 | } |
| 356 | } |
| 357 | |
| 358 | // Check for path traversal in plugin sources before schema validation |
| 359 | // This ensures we catch security issues even if schema validation fails |
| 360 | if (parsed && typeof parsed === 'object') { |
| 361 | const obj = parsed as Record<string, unknown> |
| 362 | |
| 363 | if (Array.isArray(obj.plugins)) { |
| 364 | obj.plugins.forEach((plugin: unknown, i: number) => { |
| 365 | if (plugin && typeof plugin === 'object' && 'source' in plugin) { |
| 366 | const source = (plugin as { source: unknown }).source |
| 367 | // Check string sources (relative paths) |
no test coverage detected