@holo-js/media 0.1.5 → 0.1.7
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/dist/index.mjs +75 -12
- 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 {
|
|
369
|
-
import {
|
|
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
|
-
|
|
962
|
-
|
|
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 >
|
|
989
|
+
if (totalSize > resolvedMaxSize) {
|
|
978
990
|
await reader.cancel().catch(() => void 0);
|
|
979
|
-
throw createMaxSizeError(fileName, collectionName ?? "default",
|
|
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
|
|
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 "${
|
|
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(
|
|
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
|
-
|
|
1077
|
-
|
|
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.
|
|
3
|
+
"version": "0.1.7",
|
|
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.
|
|
26
|
-
"@holo-js/queue": "^0.1.
|
|
27
|
-
"@holo-js/storage": "^0.1.
|
|
25
|
+
"@holo-js/db": "^0.1.7",
|
|
26
|
+
"@holo-js/queue": "^0.1.7",
|
|
27
|
+
"@holo-js/storage": "^0.1.7",
|
|
28
28
|
"sharp": "^0.34.4"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|