| 126 | * Caller is responsible for target-path validation (see installer.ts). |
| 127 | */ |
| 128 | export async function fetchItemFile( |
| 129 | item: RegistryItem, |
| 130 | file: FileTarget, |
| 131 | destPath: string, |
| 132 | baseUrl: string = DEFAULT_REGISTRY_URL, |
| 133 | ): Promise<void> { |
| 134 | // Reject path-traversal in file.path (mirrors assertSafeTarget for file.target). |
| 135 | if (/(^|[/\\])\.\.([/\\]|$)/.test(file.path)) { |
| 136 | throw new Error(`Unsafe file.path "${file.path}": path segments may not contain "..".`); |
| 137 | } |
| 138 | const url = `${baseUrl}/${ITEM_TYPE_DIRS[item.type]}/${item.name}/${file.path}`; |
| 139 | const res = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); |
| 140 | if (!res.ok) { |
| 141 | throw new Error(`File fetch failed: ${url} — HTTP ${res.status}`); |
| 142 | } |
| 143 | const buf = new Uint8Array(await res.arrayBuffer()); |
| 144 | mkdirSync(dirname(destPath), { recursive: true }); |
| 145 | writeFileSync(destPath, buf); |
| 146 | } |