@holo-js/media 0.1.5 → 0.1.6

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.
Files changed (2) hide show
  1. package/dist/index.mjs +75 -12
  2. package/package.json +4 -4
package/dist/index.mjs CHANGED
@@ -365,8 +365,10 @@ var Media = defineModel(mediaTable, {
365
365
  });
366
366
 
367
367
  // src/model/adder.ts
368
- import { basename as basename2 } from "path";
369
- import { readFile } from "fs/promises";
368
+ import { isIP } from "net";
369
+ import { tmpdir } from "os";
370
+ import { basename as basename2, resolve, sep } from "path";
371
+ import { readFile, realpath } from "fs/promises";
370
372
  import { randomUUID } from "crypto";
371
373
  import { Storage as Storage3 } from "@holo-js/storage/runtime";
372
374
  import { connectionAsyncContext as connectionAsyncContext4 } from "@holo-js/db";
@@ -887,6 +889,11 @@ var MediaItem = class {
887
889
  };
888
890
 
889
891
  // src/model/adder.ts
892
+ var DEFAULT_REMOTE_MEDIA_MAX_SIZE = 10 * 1024 * 1024;
893
+ var LOCAL_MEDIA_SOURCE_ROOTS = Object.freeze([
894
+ tmpdir(),
895
+ resolve(process.cwd(), "storage")
896
+ ]);
890
897
  var MediaAddValidationException = class extends Error {
891
898
  error;
892
899
  constructor(details) {
@@ -958,8 +965,13 @@ function createMaxSizeError(fileName, collectionName, maxSize, actualSize) {
958
965
  });
959
966
  }
960
967
  async function readRemoteMediaContents(response, fileName, maxSize, collectionName) {
961
- if (typeof maxSize !== "number" || !response.body) {
962
- return new Uint8Array(await response.arrayBuffer());
968
+ const resolvedMaxSize = typeof maxSize === "number" ? maxSize : DEFAULT_REMOTE_MEDIA_MAX_SIZE;
969
+ if (!response.body) {
970
+ const contents2 = new Uint8Array(await response.arrayBuffer());
971
+ if (contents2.byteLength > resolvedMaxSize) {
972
+ throw createMaxSizeError(fileName, collectionName ?? "default", resolvedMaxSize, contents2.byteLength);
973
+ }
974
+ return contents2;
963
975
  }
964
976
  const reader = response.body.getReader();
965
977
  const chunks = [];
@@ -974,9 +986,9 @@ async function readRemoteMediaContents(response, fileName, maxSize, collectionNa
974
986
  continue;
975
987
  }
976
988
  totalSize += value.byteLength;
977
- if (totalSize > maxSize) {
989
+ if (totalSize > resolvedMaxSize) {
978
990
  await reader.cancel().catch(() => void 0);
979
- throw createMaxSizeError(fileName, collectionName ?? "default", maxSize, totalSize);
991
+ throw createMaxSizeError(fileName, collectionName ?? "default", resolvedMaxSize, totalSize);
980
992
  }
981
993
  chunks.push(value);
982
994
  }
@@ -1011,7 +1023,7 @@ async function resolveMediaSource(input, overrideFileName, overrideName, options
1011
1023
  );
1012
1024
  }
1013
1025
  if (typeof input === "string" || isPathInput(input)) {
1014
- const path = typeof input === "string" ? input : input.path;
1026
+ const path = await resolveLocalMediaSourcePath(typeof input === "string" ? input : input.path);
1015
1027
  const contents2 = await readFile(path);
1016
1028
  const fileName2 = sanitizeFileName(overrideFileName ?? basename2(path));
1017
1029
  return {
@@ -1061,20 +1073,27 @@ function isUrlInput(input) {
1061
1073
  return typeof input === "object" && input !== null && "url" in input;
1062
1074
  }
1063
1075
  async function resolveRemoteMediaSource(input, overrideFileName, overrideName, maxSize, collectionName) {
1064
- const response = await fetch(input.url);
1076
+ const remoteUrl = resolveRemoteMediaUrl(input.url);
1077
+ const response = await fetch(remoteUrl, {
1078
+ redirect: "manual"
1079
+ });
1080
+ if (response.status >= 300 && response.status < 400) {
1081
+ throw new Error("[@holo-js/media] Remote media downloads do not follow redirects.");
1082
+ }
1065
1083
  if (!response.ok) {
1066
1084
  throw new Error(
1067
- `[Holo Media] Failed to download media from "${input.url}" (${response.status} ${response.statusText}).`
1085
+ `[Holo Media] Failed to download media from "${remoteUrl}" (${response.status} ${response.statusText}).`
1068
1086
  );
1069
1087
  }
1070
1088
  const fileName = sanitizeFileName(
1071
- overrideFileName ?? input.fileName ?? parseRemoteFileName(input.url)
1089
+ overrideFileName ?? input.fileName ?? parseRemoteFileName(remoteUrl)
1072
1090
  );
1073
1091
  const responseMimeType = response.headers.get("content-type")?.split(";")[0];
1074
1092
  const contentLengthHeader = response.headers.get("content-length");
1075
1093
  const contentLength = contentLengthHeader?.trim() ? Number(contentLengthHeader) : Number.NaN;
1076
- if (typeof maxSize === "number" && Number.isFinite(contentLength) && contentLength > maxSize) {
1077
- throw createMaxSizeError(fileName, collectionName ?? "default", maxSize, contentLength);
1094
+ const resolvedMaxSize = typeof maxSize === "number" ? maxSize : DEFAULT_REMOTE_MEDIA_MAX_SIZE;
1095
+ if (Number.isFinite(contentLength) && contentLength > resolvedMaxSize) {
1096
+ throw createMaxSizeError(fileName, collectionName ?? "default", resolvedMaxSize, contentLength);
1078
1097
  }
1079
1098
  const contents = await readRemoteMediaContents(
1080
1099
  response,
@@ -1091,6 +1110,50 @@ async function resolveRemoteMediaSource(input, overrideFileName, overrideName, m
1091
1110
  name: getDisplayName(fileName, overrideName ?? input.name)
1092
1111
  };
1093
1112
  }
1113
+ async function resolveLocalMediaSourcePath(path) {
1114
+ const resolvedPath = await realpath(path);
1115
+ const allowedRoots = await Promise.all(LOCAL_MEDIA_SOURCE_ROOTS.map(async (root) => await realpath(root).catch(() => null)));
1116
+ if (allowedRoots.some((root) => root && isPathWithinRoot(resolvedPath, root))) {
1117
+ return resolvedPath;
1118
+ }
1119
+ throw new Error("[@holo-js/media] Media path sources must resolve inside an allowed upload directory.");
1120
+ }
1121
+ function isPathWithinRoot(path, root) {
1122
+ return path === root || path.startsWith(`${root}${sep}`);
1123
+ }
1124
+ function resolveRemoteMediaUrl(value) {
1125
+ let url;
1126
+ try {
1127
+ url = new URL(value);
1128
+ } catch {
1129
+ throw new Error("[@holo-js/media] Remote media URLs must be valid absolute URLs.");
1130
+ }
1131
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
1132
+ throw new Error("[@holo-js/media] Remote media URLs must use http or https.");
1133
+ }
1134
+ if (url.username || url.password) {
1135
+ throw new Error("[@holo-js/media] Remote media URLs must not include credentials.");
1136
+ }
1137
+ if (isBlockedRemoteMediaHost(url.hostname)) {
1138
+ throw new Error("[@holo-js/media] Remote media URLs must not target local or private hosts.");
1139
+ }
1140
+ return url.toString();
1141
+ }
1142
+ function isBlockedRemoteMediaHost(hostname) {
1143
+ const normalized = hostname.toLowerCase();
1144
+ if (normalized === "localhost" || normalized.endsWith(".localhost")) {
1145
+ return true;
1146
+ }
1147
+ const ipVersion = isIP(normalized);
1148
+ if (ipVersion === 4) {
1149
+ const [first = 0, second = 0] = normalized.split(".").map(Number);
1150
+ return first === 0 || first === 10 || first === 127 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first >= 224;
1151
+ }
1152
+ if (ipVersion === 6) {
1153
+ return normalized === "::1" || normalized === "::" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe80:");
1154
+ }
1155
+ return false;
1156
+ }
1094
1157
  function validateSource(collection2, source) {
1095
1158
  if (typeof collection2.maxSize === "number" && source.size > collection2.maxSize) {
1096
1159
  throw createMaxSizeError(source.fileName, collection2.name, collection2.maxSize, source.size);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holo-js/media",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Holo-JS Framework - media collections and conversions for Holo-JS models",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,9 +22,9 @@
22
22
  "test": "vitest --run"
23
23
  },
24
24
  "dependencies": {
25
- "@holo-js/db": "^0.1.5",
26
- "@holo-js/queue": "^0.1.5",
27
- "@holo-js/storage": "^0.1.5",
25
+ "@holo-js/db": "^0.1.6",
26
+ "@holo-js/queue": "^0.1.6",
27
+ "@holo-js/storage": "^0.1.6",
28
28
  "sharp": "^0.34.4"
29
29
  },
30
30
  "devDependencies": {