@holo-js/media 0.2.5 → 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.
@@ -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 { isIP } from "net";
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 { readFile, realpath } from "fs/promises";
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
- try {
927
- const parsedUrl = new URL(url);
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 ?? "default", resolvedMaxSize, contents2.byteLength);
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 ?? "default", resolvedMaxSize, totalSize);
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?.maxSize,
1018
- options?.collectionName
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?.maxSize,
1027
- options?.collectionName
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 fetch(remoteUrl, {
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 ?? "default", resolvedMaxSize, contentLength);
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.toString();
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 normalized = hostname.toLowerCase();
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 ipVersion = isIP(normalized);
1153
- if (ipVersion === 4) {
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 };
@@ -0,0 +1,8 @@
1
+ import {
2
+ defineMediaConfig,
3
+ normalizeMediaConfig
4
+ } from "./chunk-VIEJQKMM.mjs";
5
+ export {
6
+ defineMediaConfig,
7
+ normalizeMediaConfig
8
+ };
package/package.json CHANGED
@@ -1,13 +1,19 @@
1
1
  {
2
2
  "name": "@holo-js/media",
3
- "version": "0.2.5",
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/db": "^0.2.5",
26
- "@holo-js/queue": "^0.2.5",
27
- "@holo-js/storage": "^0.2.5",
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"