@holo-js/media 0.2.6 → 0.3.0
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/chunk-VIEJQKMM.mjs +17 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +119 -35
- package/dist/projectConfig.d.ts +12 -0
- package/dist/projectConfig.mjs +8 -0
- package/package.json +13 -5
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// src/projectConfig.ts
|
|
2
|
+
import { registerConfigNormalizer } from "@holo-js/config/registry";
|
|
3
|
+
function normalizeMediaConfig(config = {}) {
|
|
4
|
+
return Object.freeze({ ...config });
|
|
5
|
+
}
|
|
6
|
+
function defineMediaConfig(config) {
|
|
7
|
+
return Object.freeze({ ...config });
|
|
8
|
+
}
|
|
9
|
+
registerConfigNormalizer({
|
|
10
|
+
name: "media",
|
|
11
|
+
normalize: normalizeMediaConfig
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
normalizeMediaConfig,
|
|
16
|
+
defineMediaConfig
|
|
17
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as _holo_js_db from '@holo-js/db';
|
|
2
2
|
import { Entity, TableDefinition, ModelRecord, RelationMap, DynamicRelationResolver, ModelQueryBuilder, ModelRelationPath, RelationConstraintDefinition, RelatedColumnNameForRelationPath, ModelCollection, EntityWithLoaded, PaginatedResult, SimplePaginatedResult, CursorPaginatedResult, ResolveEagerLoads } from '@holo-js/db';
|
|
3
3
|
import { StorageContent } from '@holo-js/storage/runtime';
|
|
4
|
+
export { HoloMediaConfig, defineMediaConfig, normalizeMediaConfig } from './projectConfig.js';
|
|
4
5
|
import { QueueJobDefinition, QueueJsonValue } from '@holo-js/queue';
|
|
5
6
|
|
|
6
7
|
interface MediaCollectionDefinition<TName extends string = string> {
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineMediaConfig,
|
|
3
|
+
normalizeMediaConfig
|
|
4
|
+
} from "./chunk-VIEJQKMM.mjs";
|
|
5
|
+
|
|
1
6
|
// src/model/entity.ts
|
|
2
7
|
import { Entity as Entity2 } from "@holo-js/db";
|
|
3
8
|
|
|
@@ -370,11 +375,15 @@ var Media = defineModel(mediaTable, {
|
|
|
370
375
|
});
|
|
371
376
|
|
|
372
377
|
// src/model/adder.ts
|
|
373
|
-
import {
|
|
378
|
+
import { randomUUID } from "crypto";
|
|
379
|
+
import { lookup } from "dns/promises";
|
|
380
|
+
import { readFile, realpath } from "fs/promises";
|
|
381
|
+
import { request as requestHttp } from "http";
|
|
382
|
+
import { request as requestHttps } from "https";
|
|
383
|
+
import { BlockList, isIP } from "net";
|
|
374
384
|
import { tmpdir } from "os";
|
|
375
385
|
import { basename as basename2, resolve, sep } from "path";
|
|
376
|
-
import {
|
|
377
|
-
import { randomUUID } from "crypto";
|
|
386
|
+
import { Readable } from "stream";
|
|
378
387
|
import { Storage as Storage3 } from "@holo-js/storage/runtime";
|
|
379
388
|
import { connectionAsyncContext as connectionAsyncContext4 } from "@holo-js/db";
|
|
380
389
|
|
|
@@ -895,10 +904,23 @@ var MediaItem = class {
|
|
|
895
904
|
|
|
896
905
|
// src/model/adder.ts
|
|
897
906
|
var DEFAULT_REMOTE_MEDIA_MAX_SIZE = 10 * 1024 * 1024;
|
|
907
|
+
var BLOCKED_REMOTE_MEDIA_ADDRESSES = createBlockedRemoteMediaAddresses();
|
|
898
908
|
var LOCAL_MEDIA_SOURCE_ROOTS = Object.freeze([
|
|
899
909
|
tmpdir(),
|
|
900
910
|
resolve(process.cwd(), "storage")
|
|
901
911
|
]);
|
|
912
|
+
var defaultRemoteMediaAddressResolver = async (hostname) => await lookup(hostname, {
|
|
913
|
+
all: true,
|
|
914
|
+
verbatim: true
|
|
915
|
+
});
|
|
916
|
+
function createRemoteMediaDownloader(resolver, requester) {
|
|
917
|
+
return async (url) => {
|
|
918
|
+
const address = await resolveRemoteMediaAddress(url, resolver);
|
|
919
|
+
return await requester(url, address);
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
var defaultRemoteMediaDownloader = createRemoteMediaDownloader(defaultRemoteMediaAddressResolver, requestRemoteMedia);
|
|
923
|
+
var remoteMediaDownloader = defaultRemoteMediaDownloader;
|
|
902
924
|
var MediaAddValidationException = class extends Error {
|
|
903
925
|
error;
|
|
904
926
|
constructor(details) {
|
|
@@ -923,17 +945,10 @@ function resolveImplicitDiskName() {
|
|
|
923
945
|
return defaultDisk.name;
|
|
924
946
|
}
|
|
925
947
|
function parseRemoteFileName(url) {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
return basename2(parsedUrl.pathname) || "media.bin";
|
|
929
|
-
} catch {
|
|
930
|
-
return "media.bin";
|
|
931
|
-
}
|
|
948
|
+
const parsedUrl = new URL(url);
|
|
949
|
+
return basename2(parsedUrl.pathname) || "media.bin";
|
|
932
950
|
}
|
|
933
951
|
function formatBytes(bytes) {
|
|
934
|
-
if (!Number.isFinite(bytes) || bytes < 0) {
|
|
935
|
-
return "0 bytes";
|
|
936
|
-
}
|
|
937
952
|
const units = ["bytes", "KB", "MB", "GB"];
|
|
938
953
|
let value = bytes;
|
|
939
954
|
let unitIndex = 0;
|
|
@@ -974,7 +989,7 @@ async function readRemoteMediaContents(response, fileName, maxSize, collectionNa
|
|
|
974
989
|
if (!response.body) {
|
|
975
990
|
const contents2 = new Uint8Array(await response.arrayBuffer());
|
|
976
991
|
if (contents2.byteLength > resolvedMaxSize) {
|
|
977
|
-
throw createMaxSizeError(fileName, collectionName
|
|
992
|
+
throw createMaxSizeError(fileName, collectionName, resolvedMaxSize, contents2.byteLength);
|
|
978
993
|
}
|
|
979
994
|
return contents2;
|
|
980
995
|
}
|
|
@@ -993,7 +1008,7 @@ async function readRemoteMediaContents(response, fileName, maxSize, collectionNa
|
|
|
993
1008
|
totalSize += value.byteLength;
|
|
994
1009
|
if (totalSize > resolvedMaxSize) {
|
|
995
1010
|
await reader.cancel().catch(() => void 0);
|
|
996
|
-
throw createMaxSizeError(fileName, collectionName
|
|
1011
|
+
throw createMaxSizeError(fileName, collectionName, resolvedMaxSize, totalSize);
|
|
997
1012
|
}
|
|
998
1013
|
chunks.push(value);
|
|
999
1014
|
}
|
|
@@ -1014,8 +1029,8 @@ async function resolveMediaSource(input, overrideFileName, overrideName, options
|
|
|
1014
1029
|
{ url: input },
|
|
1015
1030
|
overrideFileName,
|
|
1016
1031
|
overrideName,
|
|
1017
|
-
options
|
|
1018
|
-
options
|
|
1032
|
+
options.maxSize,
|
|
1033
|
+
options.collectionName
|
|
1019
1034
|
);
|
|
1020
1035
|
}
|
|
1021
1036
|
if (typeof input !== "string" && isUrlInput(input)) {
|
|
@@ -1023,8 +1038,8 @@ async function resolveMediaSource(input, overrideFileName, overrideName, options
|
|
|
1023
1038
|
input,
|
|
1024
1039
|
overrideFileName,
|
|
1025
1040
|
overrideName,
|
|
1026
|
-
options
|
|
1027
|
-
options
|
|
1041
|
+
options.maxSize,
|
|
1042
|
+
options.collectionName
|
|
1028
1043
|
);
|
|
1029
1044
|
}
|
|
1030
1045
|
if (typeof input === "string" || isPathInput(input)) {
|
|
@@ -1079,26 +1094,24 @@ function isUrlInput(input) {
|
|
|
1079
1094
|
}
|
|
1080
1095
|
async function resolveRemoteMediaSource(input, overrideFileName, overrideName, maxSize, collectionName) {
|
|
1081
1096
|
const remoteUrl = resolveRemoteMediaUrl(input.url);
|
|
1082
|
-
const response = await
|
|
1083
|
-
redirect: "manual"
|
|
1084
|
-
});
|
|
1097
|
+
const response = await remoteMediaDownloader(remoteUrl);
|
|
1085
1098
|
if (response.status >= 300 && response.status < 400) {
|
|
1086
1099
|
throw new Error("[@holo-js/media] Remote media downloads do not follow redirects.");
|
|
1087
1100
|
}
|
|
1088
1101
|
if (!response.ok) {
|
|
1089
1102
|
throw new Error(
|
|
1090
|
-
`[Holo Media] Failed to download media from "${remoteUrl}" (${response.status} ${response.statusText}).`
|
|
1103
|
+
`[Holo Media] Failed to download media from "${remoteUrl.toString()}" (${response.status} ${response.statusText}).`
|
|
1091
1104
|
);
|
|
1092
1105
|
}
|
|
1093
1106
|
const fileName = sanitizeFileName(
|
|
1094
|
-
overrideFileName ?? input.fileName ?? parseRemoteFileName(remoteUrl)
|
|
1107
|
+
overrideFileName ?? input.fileName ?? parseRemoteFileName(remoteUrl.toString())
|
|
1095
1108
|
);
|
|
1096
1109
|
const responseMimeType = response.headers.get("content-type")?.split(";")[0];
|
|
1097
1110
|
const contentLengthHeader = response.headers.get("content-length");
|
|
1098
1111
|
const contentLength = contentLengthHeader?.trim() ? Number(contentLengthHeader) : Number.NaN;
|
|
1099
1112
|
const resolvedMaxSize = typeof maxSize === "number" ? maxSize : DEFAULT_REMOTE_MEDIA_MAX_SIZE;
|
|
1100
1113
|
if (Number.isFinite(contentLength) && contentLength > resolvedMaxSize) {
|
|
1101
|
-
throw createMaxSizeError(fileName, collectionName
|
|
1114
|
+
throw createMaxSizeError(fileName, collectionName, resolvedMaxSize, contentLength);
|
|
1102
1115
|
}
|
|
1103
1116
|
const contents = await readRemoteMediaContents(
|
|
1104
1117
|
response,
|
|
@@ -1142,22 +1155,91 @@ function resolveRemoteMediaUrl(value) {
|
|
|
1142
1155
|
if (isBlockedRemoteMediaHost(url.hostname)) {
|
|
1143
1156
|
throw new Error("[@holo-js/media] Remote media URLs must not target local or private hosts.");
|
|
1144
1157
|
}
|
|
1145
|
-
return url
|
|
1158
|
+
return url;
|
|
1159
|
+
}
|
|
1160
|
+
function createBlockedRemoteMediaAddresses() {
|
|
1161
|
+
const blockList = new BlockList();
|
|
1162
|
+
for (const [network, prefix] of [
|
|
1163
|
+
["0.0.0.0", 8],
|
|
1164
|
+
["10.0.0.0", 8],
|
|
1165
|
+
["100.64.0.0", 10],
|
|
1166
|
+
["127.0.0.0", 8],
|
|
1167
|
+
["169.254.0.0", 16],
|
|
1168
|
+
["172.16.0.0", 12],
|
|
1169
|
+
["192.0.0.0", 24],
|
|
1170
|
+
["192.0.2.0", 24],
|
|
1171
|
+
["192.168.0.0", 16],
|
|
1172
|
+
["198.18.0.0", 15],
|
|
1173
|
+
["198.51.100.0", 24],
|
|
1174
|
+
["203.0.113.0", 24],
|
|
1175
|
+
["224.0.0.0", 4]
|
|
1176
|
+
]) {
|
|
1177
|
+
blockList.addSubnet(network, prefix, "ipv4");
|
|
1178
|
+
}
|
|
1179
|
+
for (const [network, prefix] of [
|
|
1180
|
+
["::", 128],
|
|
1181
|
+
["::1", 128],
|
|
1182
|
+
["fc00::", 7],
|
|
1183
|
+
["fe80::", 10],
|
|
1184
|
+
["ff00::", 8]
|
|
1185
|
+
]) {
|
|
1186
|
+
blockList.addSubnet(network, prefix, "ipv6");
|
|
1187
|
+
}
|
|
1188
|
+
return blockList;
|
|
1189
|
+
}
|
|
1190
|
+
function isBlockedRemoteMediaAddress(address) {
|
|
1191
|
+
return BLOCKED_REMOTE_MEDIA_ADDRESSES.check(address.address, address.family === 4 ? "ipv4" : "ipv6");
|
|
1192
|
+
}
|
|
1193
|
+
async function resolveRemoteMediaAddress(url, resolver) {
|
|
1194
|
+
const addresses = await resolver(url.hostname);
|
|
1195
|
+
if (addresses.length === 0 || addresses.some(isBlockedRemoteMediaAddress)) {
|
|
1196
|
+
throw new Error("[@holo-js/media] Remote media URLs must not resolve to local or private hosts.");
|
|
1197
|
+
}
|
|
1198
|
+
return addresses[0];
|
|
1199
|
+
}
|
|
1200
|
+
function requestRemoteMedia(url, address) {
|
|
1201
|
+
return new Promise((resolveResponse, reject) => {
|
|
1202
|
+
const request = resolveRemoteMediaRequest(url.protocol)(url, {
|
|
1203
|
+
headers: {
|
|
1204
|
+
host: url.host
|
|
1205
|
+
},
|
|
1206
|
+
lookup: createPinnedRemoteMediaLookup(address)
|
|
1207
|
+
}, (response) => {
|
|
1208
|
+
const headers = new Headers();
|
|
1209
|
+
appendRemoteMediaHeaders(headers, response.headers);
|
|
1210
|
+
resolveResponse(new Response(Readable.toWeb(response), {
|
|
1211
|
+
status: response.statusCode,
|
|
1212
|
+
statusText: response.statusMessage,
|
|
1213
|
+
headers
|
|
1214
|
+
}));
|
|
1215
|
+
});
|
|
1216
|
+
request.on("error", reject);
|
|
1217
|
+
request.end();
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
function createPinnedRemoteMediaLookup(address) {
|
|
1221
|
+
return (_hostname, options, callback) => options.all ? callback(null, [address]) : callback(null, address.address, address.family);
|
|
1222
|
+
}
|
|
1223
|
+
function appendRemoteMediaHeaders(headers, source) {
|
|
1224
|
+
for (const [name, value] of Object.entries(source)) {
|
|
1225
|
+
if (Array.isArray(value)) {
|
|
1226
|
+
for (const entry of value) headers.append(name, entry);
|
|
1227
|
+
} else if (typeof value === "string") {
|
|
1228
|
+
headers.set(name, value);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
function resolveRemoteMediaRequest(protocol) {
|
|
1233
|
+
return protocol === "https:" ? requestHttps : requestHttp;
|
|
1146
1234
|
}
|
|
1147
1235
|
function isBlockedRemoteMediaHost(hostname) {
|
|
1148
|
-
const
|
|
1236
|
+
const lowerCaseHostname = hostname.toLowerCase();
|
|
1237
|
+
const normalized = lowerCaseHostname.startsWith("[") && lowerCaseHostname.endsWith("]") ? lowerCaseHostname.slice(1, -1) : lowerCaseHostname;
|
|
1149
1238
|
if (normalized === "localhost" || normalized.endsWith(".localhost")) {
|
|
1150
1239
|
return true;
|
|
1151
1240
|
}
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
const [first = 0, second = 0] = normalized.split(".").map(Number);
|
|
1155
|
-
return first === 0 || first === 10 || first === 127 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first >= 224;
|
|
1156
|
-
}
|
|
1157
|
-
if (ipVersion === 6) {
|
|
1158
|
-
return normalized === "::1" || normalized === "::" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe80:");
|
|
1159
|
-
}
|
|
1160
|
-
return false;
|
|
1241
|
+
const family = isIP(normalized);
|
|
1242
|
+
return family !== 0 && isBlockedRemoteMediaAddress({ address: normalized, family });
|
|
1161
1243
|
}
|
|
1162
1244
|
function validateSource(collection2, source) {
|
|
1163
1245
|
if (typeof collection2.maxSize === "number" && source.size > collection2.maxSize) {
|
|
@@ -1754,6 +1836,7 @@ export {
|
|
|
1754
1836
|
conversion,
|
|
1755
1837
|
createDefaultMediaConversionExecutor,
|
|
1756
1838
|
defaultMediaConversionExecutor,
|
|
1839
|
+
defineMediaConfig,
|
|
1757
1840
|
defineMediaModel,
|
|
1758
1841
|
dispatchQueuedMediaConversions,
|
|
1759
1842
|
ensureMediaQueueJobRegistered,
|
|
@@ -1763,6 +1846,7 @@ export {
|
|
|
1763
1846
|
getMediaPathGenerator,
|
|
1764
1847
|
normalizeCollectionDefinitions,
|
|
1765
1848
|
normalizeConversionDefinitions,
|
|
1849
|
+
normalizeMediaConfig,
|
|
1766
1850
|
normalizeMediaDefinition,
|
|
1767
1851
|
registerMediaDefinition,
|
|
1768
1852
|
requireMediaDefinition,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface HoloMediaConfig {
|
|
2
|
+
readonly [key: string]: unknown;
|
|
3
|
+
}
|
|
4
|
+
declare module '@holo-js/config' {
|
|
5
|
+
interface HoloConfigRegistry {
|
|
6
|
+
media: Readonly<HoloMediaConfig>;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
declare function normalizeMediaConfig(config?: HoloMediaConfig): Readonly<HoloMediaConfig>;
|
|
10
|
+
declare function defineMediaConfig<TConfig extends HoloMediaConfig>(config: TConfig): Readonly<TConfig>;
|
|
11
|
+
|
|
12
|
+
export { type HoloMediaConfig, defineMediaConfig, normalizeMediaConfig };
|
package/package.json
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holo-js/media",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Holo-JS Framework - media collections and conversions for Holo-JS models",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
10
|
-
"import": "./dist/index.mjs"
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"default": "./dist/index.mjs"
|
|
12
|
+
},
|
|
13
|
+
"./config": {
|
|
14
|
+
"types": "./dist/projectConfig.d.ts",
|
|
15
|
+
"import": "./dist/projectConfig.mjs",
|
|
16
|
+
"default": "./dist/projectConfig.mjs"
|
|
11
17
|
}
|
|
12
18
|
},
|
|
13
19
|
"main": "./dist/index.mjs",
|
|
@@ -22,12 +28,14 @@
|
|
|
22
28
|
"test": "vitest --run"
|
|
23
29
|
},
|
|
24
30
|
"dependencies": {
|
|
25
|
-
"@holo-js/
|
|
26
|
-
"@holo-js/
|
|
27
|
-
"@holo-js/
|
|
31
|
+
"@holo-js/config": "^0.3.0",
|
|
32
|
+
"@holo-js/db": "^0.3.0",
|
|
33
|
+
"@holo-js/queue": "^0.3.0",
|
|
34
|
+
"@holo-js/storage": "^0.3.0",
|
|
28
35
|
"sharp": "^0.34.4"
|
|
29
36
|
},
|
|
30
37
|
"devDependencies": {
|
|
38
|
+
"@holo-js/db-sqlite": "^0.3.0",
|
|
31
39
|
"tsup": "^8.3.5",
|
|
32
40
|
"typescript": "^5.7.2",
|
|
33
41
|
"vitest": "^4.1.5"
|