( srcUrl: string, filename: string, videosDir: string, )
| 231 | */ |
| 232 | // fallow-ignore-next-line complexity |
| 233 | async function downloadVideoBody( |
| 234 | srcUrl: string, |
| 235 | filename: string, |
| 236 | videosDir: string, |
| 237 | ): Promise<string | null> { |
| 238 | if (isPrivateUrl(srcUrl)) return null; // cheap pre-check; safeFetch re-checks every hop |
| 239 | let ext = ""; |
| 240 | try { |
| 241 | ext = extname(new URL(srcUrl).pathname).toLowerCase(); |
| 242 | } catch { |
| 243 | return null; |
| 244 | } |
| 245 | if (!DOWNLOADABLE_VIDEO_EXTS.has(ext)) return null; // streaming manifest / unknown — leave on origin |
| 246 | try { |
| 247 | // safeFetch resolves redirects manually and re-runs isPrivateUrl on each |
| 248 | // Location hop, so a public URL cannot 30x to an internal/metadata host. |
| 249 | const res = await safeFetch(srcUrl, { |
| 250 | signal: AbortSignal.timeout(120000), // up to ~75 MB on a slow link; aborts cleanly → still-frame fallback |
| 251 | headers: { "User-Agent": "HyperFrames/1.0" }, |
| 252 | }); |
| 253 | if (!res || !res.ok || !res.body) return null; |
| 254 | const ct = (res.headers.get("content-type") || "").toLowerCase(); |
| 255 | if (ct && !ct.startsWith("video/") && !ct.includes("octet-stream")) return null; |
| 256 | const declared = Number(res.headers.get("content-length") || 0); |
| 257 | if (declared && declared > MAX_VIDEO_BYTES) return null; // too big — leave on origin |
| 258 | // Stream with a hard cap; a chunked response has no Content-Length to trust. |
| 259 | const chunks: Buffer[] = []; |
| 260 | let total = 0; |
| 261 | for await (const chunk of res.body as unknown as AsyncIterable<Uint8Array>) { |
| 262 | total += chunk.length; |
| 263 | if (total > MAX_VIDEO_BYTES) return null; // abort oversized stream — no partial file written |
| 264 | chunks.push(Buffer.from(chunk)); |
| 265 | } |
| 266 | if (total < 1024) return null; // too small to be a real video (likely an error blob) |
| 267 | const safe = /\.[a-z0-9]+$/i.test(filename) ? filename.replace(/[^\w.-]/g, "_") : `video${ext}`; |
| 268 | writeFileSync(join(videosDir, safe), Buffer.concat(chunks)); |
| 269 | return `assets/videos/${safe}`; |
| 270 | } catch { |
| 271 | return null; |
| 272 | } |
| 273 | } |
| 274 | |
| 275 | /** A <video> descriptor scanned from the DOM (rich: has rect + nearby text). */ |
| 276 | interface VideoDescriptor { |
no test coverage detected