umedia 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/download.js CHANGED
@@ -5,7 +5,7 @@ import { pipeline } from "node:stream/promises";
5
5
  import path from "node:path";
6
6
  import { zipSync } from "fflate";
7
7
  import { UMediaError, Codes } from "./errors.js";
8
- import { UA, cleanName, rid } from "./util.js";
8
+ import { UA, cleanName, rid, validateUrl } from "./util.js";
9
9
  import { pickFormat } from "./quality.js";
10
10
  import * as registry from "./registry.js";
11
11
  import * as ytdlp from "./providers/ytdlp.js";
@@ -38,6 +38,9 @@ export async function download({ url, quality = "best", type = "video", dir = ".
38
38
  const requestId = rid();
39
39
  const t0 = Date.now();
40
40
 
41
+ // SSRF hygiene FIRST — user-supplied URLs must never reach private/internal hosts
42
+ validateUrl(url);
43
+
41
44
  let resolved, engine;
42
45
  if (MEDIA_EXT.test(String(url).split("#")[0])) {
43
46
  const ext = (String(url).match(MEDIA_EXT) || [])[1].toLowerCase();
@@ -79,6 +82,7 @@ export async function download({ url, quality = "best", type = "video", dir = ".
79
82
  throw new UMediaError(Codes.PROVIDER_UNAVAILABLE,
80
83
  "no direct media URL available on this network (bot-gated) — install yt-dlp for hard networks");
81
84
  }
85
+ validateUrl(chosenUrl); // never fetch private/internal targets, even from a provider response
82
86
  if (!selectedQuality) { selectedQuality = pick.selectedQuality; fallback = pick.fallback; }
83
87
 
84
88
  const ext = (chosenUrl.split("?")[0].match(/\.(mp4|webm|m4a|mp3|jpg|jpeg|png|gif|webp)$/) || [])[1]
@@ -132,6 +136,7 @@ export async function download({ url, quality = "best", type = "video", dir = ".
132
136
  /** yt-dlp escape hatch for downloads on hard networks / 1800+ sites. */
133
137
  export async function downloadWithYtdlp({ url, quality = "best", dir = "./umedia-downloads", onProgress } = {}) {
134
138
  const requestId = rid();
139
+ validateUrl(url);
135
140
  if (!ytdlp.binary()) throw new UMediaError(Codes.PROVIDER_UNAVAILABLE, "yt-dlp not installed — `pip install yt-dlp`");
136
141
  await mkdir(dir, { recursive: true });
137
142
  const { spawn } = await import("node:child_process");
package/lib/registry.js CHANGED
@@ -6,6 +6,7 @@ import * as ig from "./providers/instagram.js";
6
6
  import * as ytdlp from "./providers/ytdlp.js";
7
7
  import * as itunes from "./providers/itunes.js";
8
8
  import { Codes, UMediaError } from "./errors.js";
9
+ import { validateUrl } from "./util.js";
9
10
 
10
11
  const NATIVE = [yt, tiktok, reddit, xprov, ig];
11
12
 
@@ -20,6 +21,9 @@ const DIRECT_MEDIA = /\.(mp4|webm|m4a|mp3|aac|wav|jpg|jpeg|png|gif|webp|mov|mkv)
20
21
 
21
22
  /** Resolve a URL: native adapter first, optional yt-dlp fallback. attempts[] is the honest trail. */
22
23
  export async function resolve(url) {
24
+ // SSRF hygiene FIRST — never dispatch or fetch private/internal targets (bots pass user URLs!)
25
+ validateUrl(url);
26
+
23
27
  // direct media URL (an image/video/audio file) -> single-item result, no platform needed
24
28
  const direct = String(url).split("#")[0].match(DIRECT_MEDIA);
25
29
  if (direct) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "umedia",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Standalone media engine — search, resolve and download media from YouTube, TikTok, Instagram, Reddit, X, iTunes (and 1800+ sites via the yt-dlp tier) entirely on your machine. No hosted API, no keys, no server bills. Honest quality labels, complete galleries, typed errors.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -12,7 +12,7 @@
12
12
  }
13
13
  },
14
14
  "bin": {
15
- "umedia": "./bin/umedia.js"
15
+ "umedia": "bin/umedia.js"
16
16
  },
17
17
  "files": [
18
18
  "index.js",