(
fileUrl: string,
options: DownloadFileFromUrlOptions = {}
)
| 173 | * For external URLs, validates DNS/SSRF and uses secure fetch with IP pinning. |
| 174 | */ |
| 175 | export async function downloadFileFromUrl( |
| 176 | fileUrl: string, |
| 177 | options: DownloadFileFromUrlOptions = {} |
| 178 | ): Promise<Buffer> { |
| 179 | const { timeoutMs = getMaxExecutionTimeout(), maxBytes, userId } = options |
| 180 | |
| 181 | if (isInternalFileUrl(fileUrl)) { |
| 182 | if (!userId) { |
| 183 | logger.warn('Internal file download denied: no userId provided', { fileUrl }) |
| 184 | throw new Error('Access denied: internal file URL requires an authenticated user') |
| 185 | } |
| 186 | |
| 187 | const key = extractStorageKey(fileUrl) |
| 188 | if (!key) { |
| 189 | logger.warn('Internal file download denied: could not resolve storage key', { fileUrl }) |
| 190 | throw new Error('Access denied: could not resolve internal file key') |
| 191 | } |
| 192 | |
| 193 | const context = inferContextFromKey(key) |
| 194 | |
| 195 | const hasAccess = await verifyFileAccess(key, userId, undefined, context, false) |
| 196 | if (!hasAccess) { |
| 197 | logger.warn('Internal file download denied: access check failed', { key, context, userId }) |
| 198 | throw new Error('Access denied: file not found or insufficient permissions') |
| 199 | } |
| 200 | |
| 201 | const { downloadFile } = await import('@/lib/uploads/core/storage-service') |
| 202 | return downloadFile({ key, context, maxBytes }) |
| 203 | } |
| 204 | |
| 205 | const urlValidation = await validateUrlWithDNS(fileUrl, 'fileUrl') |
| 206 | if (!urlValidation.isValid) { |
| 207 | throw new Error(`Invalid file URL: ${urlValidation.error}`) |
| 208 | } |
| 209 | |
| 210 | const response = await secureFetchWithPinnedIP(fileUrl, urlValidation.resolvedIP!, { |
| 211 | timeout: timeoutMs, |
| 212 | maxResponseBytes: maxBytes, |
| 213 | }) |
| 214 | |
| 215 | if (!response.ok) { |
| 216 | await consumeOrCancelBody(response) |
| 217 | throw new Error(`Failed to download file: ${response.statusText}`) |
| 218 | } |
| 219 | |
| 220 | return readResponseToBufferWithLimit(response, { |
| 221 | maxBytes: maxBytes ?? Number.MAX_SAFE_INTEGER, |
| 222 | label: 'file download', |
| 223 | }) |
| 224 | } |
| 225 | |
| 226 | export async function resolveInternalFileUrl( |
| 227 | filePath: string, |
no test coverage detected