( event: H3Event, packageName: string, version: string, skillName: string, filePath: string, )
| 105 | } |
| 106 | |
| 107 | async function handleSkillFile( |
| 108 | event: H3Event, |
| 109 | packageName: string, |
| 110 | version: string, |
| 111 | skillName: string, |
| 112 | filePath: string, |
| 113 | ): Promise<string> { |
| 114 | // Validate file path to prevent directory traversal |
| 115 | if (filePath.includes('..') || filePath.startsWith('/')) { |
| 116 | throw createError({ statusCode: 400, message: 'Invalid file path' }) |
| 117 | } |
| 118 | |
| 119 | // Only allow files within skill subdirectories (scripts/, references/, assets/) |
| 120 | const allowedPrefixes = ['scripts/', 'references/', 'assets/', 'refs/'] |
| 121 | if (!allowedPrefixes.some(p => filePath.startsWith(p))) { |
| 122 | throw createError({ |
| 123 | statusCode: 400, |
| 124 | message: 'File must be in scripts/, references/, or assets/ subdirectory', |
| 125 | }) |
| 126 | } |
| 127 | |
| 128 | try { |
| 129 | const content = await fetchSkillFile(packageName, version, `skills/${skillName}/${filePath}`) |
| 130 | |
| 131 | const ext = filePath.split('.').pop()?.toLowerCase() || '' |
| 132 | const contentTypes: Record<string, string> = { |
| 133 | md: 'text/markdown', |
| 134 | txt: 'text/plain', |
| 135 | json: 'application/json', |
| 136 | js: 'text/javascript', |
| 137 | ts: 'text/typescript', |
| 138 | sh: 'text/x-shellscript', |
| 139 | py: 'text/x-python', |
| 140 | } |
| 141 | setHeader(event, 'Content-Type', contentTypes[ext] || 'text/plain') |
| 142 | |
| 143 | return content |
| 144 | } catch (error) { |
| 145 | if (error && typeof error === 'object' && 'statusCode' in error && error.statusCode === 404) { |
| 146 | throw createError({ statusCode: 404, message: ERROR_SKILL_FILE_NOT_FOUND }) |
| 147 | } |
| 148 | throw error |
| 149 | } |
| 150 | } |
no test coverage detected