ipx 2.0.1 → 2.1.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/cli.cjs CHANGED
@@ -3,7 +3,7 @@
3
3
  const listhen = require('listhen');
4
4
  const citty = require('citty');
5
5
  const cli = require('listhen/cli');
6
- const nodeFs = require('./shared/ipx.ebaf2d0c.cjs');
6
+ const nodeFs = require('./shared/ipx.7601f01b.cjs');
7
7
  require('defu');
8
8
  require('ufo');
9
9
  require('h3');
@@ -15,7 +15,7 @@ require('ofetch');
15
15
  require('pathe');
16
16
 
17
17
  const name = "ipx";
18
- const version = "2.0.1";
18
+ const version = "2.1.0";
19
19
  const description = "High performance, secure and easy-to-use image optimizer.";
20
20
 
21
21
  const serve = citty.defineCommand({
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { listen } from 'listhen';
2
2
  import { defineCommand, runMain } from 'citty';
3
3
  import { getArgs, parseArgs } from 'listhen/cli';
4
- import { c as createIPX, g as ipxFSStorage, i as ipxHttpStorage, e as createIPXNodeServer } from './shared/ipx.4d79c6c9.mjs';
4
+ import { c as createIPX, g as ipxFSStorage, i as ipxHttpStorage, e as createIPXNodeServer } from './shared/ipx.b027cc1c.mjs';
5
5
  import 'defu';
6
6
  import 'ufo';
7
7
  import 'h3';
@@ -13,7 +13,7 @@ import 'ofetch';
13
13
  import 'pathe';
14
14
 
15
15
  const name = "ipx";
16
- const version = "2.0.1";
16
+ const version = "2.1.0";
17
17
  const description = "High performance, secure and easy-to-use image optimizer.";
18
18
 
19
19
  const serve = defineCommand({
package/dist/index.cjs CHANGED
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const nodeFs = require('./shared/ipx.ebaf2d0c.cjs');
3
+ const nodeFs = require('./shared/ipx.7601f01b.cjs');
4
+ const h3 = require('h3');
4
5
  require('defu');
5
6
  require('ufo');
6
- require('h3');
7
7
  require('image-meta');
8
8
  require('destr');
9
9
  require('@fastify/accept-negotiator');
@@ -11,8 +11,9 @@ require('etag');
11
11
  require('ofetch');
12
12
  require('pathe');
13
13
 
14
- function unstorageToIPXStorage(storage, prefix) {
15
- const resolveKey = (id) => prefix + ":" + id;
14
+ function unstorageToIPXStorage(storage, _options = {}) {
15
+ const options = typeof _options === "string" ? { prefix: _options } : _options;
16
+ const resolveKey = (id) => options.prefix ? `${options.prefix}:${id}` : id;
16
17
  return {
17
18
  name: "ipx:" + (storage.name || "unstorage"),
18
19
  async getMeta(id, opts = {}) {
@@ -28,8 +29,24 @@ function unstorageToIPXStorage(storage, prefix) {
28
29
  return;
29
30
  }
30
31
  const storageKey = resolveKey(id);
31
- const data = await storage.getItemRaw(storageKey, opts);
32
- return data;
32
+ let data = await storage.getItemRaw(storageKey, opts);
33
+ if (!data) {
34
+ return;
35
+ }
36
+ if (data instanceof Blob) {
37
+ data = await data.arrayBuffer();
38
+ }
39
+ try {
40
+ return Buffer.from(data);
41
+ } catch (error) {
42
+ throw h3.createError({
43
+ statusCode: 500,
44
+ statusText: `IPX_STORAGE_ERROR`,
45
+ message: `Failed to parse storage data to Buffer:
46
+ ${error.message}`,
47
+ cause: error
48
+ });
49
+ }
33
50
  }
34
51
  };
35
52
  }
package/dist/index.d.cts CHANGED
@@ -145,11 +145,14 @@ type HTTPStorageOptions = {
145
145
  declare function ipxHttpStorage(_options?: HTTPStorageOptions): IPXStorage;
146
146
 
147
147
  type NodeFSSOptions = {
148
- dir?: string;
148
+ dir?: string | string[];
149
149
  maxAge?: number;
150
150
  };
151
151
  declare function ipxFSStorage(_options?: NodeFSSOptions): IPXStorage;
152
152
 
153
- declare function unstorageToIPXStorage(storage: Storage | Driver, prefix: string): IPXStorage;
153
+ type UnstorageIPXStorageOptions = {
154
+ prefix?: string;
155
+ };
156
+ declare function unstorageToIPXStorage(storage: Storage | Driver, _options?: UnstorageIPXStorageOptions | string): IPXStorage;
154
157
 
155
- export { type HTTPStorageOptions, type Handler, type HandlerContext, type IPX, type IPXOptions, type IPXStorage, type IPXStorageMeta, type IPXStorageOptions, type NodeFSSOptions, createIPX, createIPXH3App, createIPXH3Handler, createIPXNodeServer, createIPXPlainServer, createIPXWebServer, ipxFSStorage, ipxHttpStorage, unstorageToIPXStorage };
158
+ export { type HTTPStorageOptions, type Handler, type HandlerContext, type IPX, type IPXOptions, type IPXStorage, type IPXStorageMeta, type IPXStorageOptions, type NodeFSSOptions, type UnstorageIPXStorageOptions, createIPX, createIPXH3App, createIPXH3Handler, createIPXNodeServer, createIPXPlainServer, createIPXWebServer, ipxFSStorage, ipxHttpStorage, unstorageToIPXStorage };
package/dist/index.d.mts CHANGED
@@ -145,11 +145,14 @@ type HTTPStorageOptions = {
145
145
  declare function ipxHttpStorage(_options?: HTTPStorageOptions): IPXStorage;
146
146
 
147
147
  type NodeFSSOptions = {
148
- dir?: string;
148
+ dir?: string | string[];
149
149
  maxAge?: number;
150
150
  };
151
151
  declare function ipxFSStorage(_options?: NodeFSSOptions): IPXStorage;
152
152
 
153
- declare function unstorageToIPXStorage(storage: Storage | Driver, prefix: string): IPXStorage;
153
+ type UnstorageIPXStorageOptions = {
154
+ prefix?: string;
155
+ };
156
+ declare function unstorageToIPXStorage(storage: Storage | Driver, _options?: UnstorageIPXStorageOptions | string): IPXStorage;
154
157
 
155
- export { type HTTPStorageOptions, type Handler, type HandlerContext, type IPX, type IPXOptions, type IPXStorage, type IPXStorageMeta, type IPXStorageOptions, type NodeFSSOptions, createIPX, createIPXH3App, createIPXH3Handler, createIPXNodeServer, createIPXPlainServer, createIPXWebServer, ipxFSStorage, ipxHttpStorage, unstorageToIPXStorage };
158
+ export { type HTTPStorageOptions, type Handler, type HandlerContext, type IPX, type IPXOptions, type IPXStorage, type IPXStorageMeta, type IPXStorageOptions, type NodeFSSOptions, type UnstorageIPXStorageOptions, createIPX, createIPXH3App, createIPXH3Handler, createIPXNodeServer, createIPXPlainServer, createIPXWebServer, ipxFSStorage, ipxHttpStorage, unstorageToIPXStorage };
package/dist/index.d.ts CHANGED
@@ -145,11 +145,14 @@ type HTTPStorageOptions = {
145
145
  declare function ipxHttpStorage(_options?: HTTPStorageOptions): IPXStorage;
146
146
 
147
147
  type NodeFSSOptions = {
148
- dir?: string;
148
+ dir?: string | string[];
149
149
  maxAge?: number;
150
150
  };
151
151
  declare function ipxFSStorage(_options?: NodeFSSOptions): IPXStorage;
152
152
 
153
- declare function unstorageToIPXStorage(storage: Storage | Driver, prefix: string): IPXStorage;
153
+ type UnstorageIPXStorageOptions = {
154
+ prefix?: string;
155
+ };
156
+ declare function unstorageToIPXStorage(storage: Storage | Driver, _options?: UnstorageIPXStorageOptions | string): IPXStorage;
154
157
 
155
- export { type HTTPStorageOptions, type Handler, type HandlerContext, type IPX, type IPXOptions, type IPXStorage, type IPXStorageMeta, type IPXStorageOptions, type NodeFSSOptions, createIPX, createIPXH3App, createIPXH3Handler, createIPXNodeServer, createIPXPlainServer, createIPXWebServer, ipxFSStorage, ipxHttpStorage, unstorageToIPXStorage };
158
+ export { type HTTPStorageOptions, type Handler, type HandlerContext, type IPX, type IPXOptions, type IPXStorage, type IPXStorageMeta, type IPXStorageOptions, type NodeFSSOptions, type UnstorageIPXStorageOptions, createIPX, createIPXH3App, createIPXH3Handler, createIPXNodeServer, createIPXPlainServer, createIPXWebServer, ipxFSStorage, ipxHttpStorage, unstorageToIPXStorage };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
- export { c as createIPX, b as createIPXH3App, a as createIPXH3Handler, e as createIPXNodeServer, f as createIPXPlainServer, d as createIPXWebServer, g as ipxFSStorage, i as ipxHttpStorage } from './shared/ipx.4d79c6c9.mjs';
1
+ export { c as createIPX, b as createIPXH3App, a as createIPXH3Handler, e as createIPXNodeServer, f as createIPXPlainServer, d as createIPXWebServer, g as ipxFSStorage, i as ipxHttpStorage } from './shared/ipx.b027cc1c.mjs';
2
+ import { createError } from 'h3';
2
3
  import 'defu';
3
4
  import 'ufo';
4
- import 'h3';
5
5
  import 'image-meta';
6
6
  import 'destr';
7
7
  import '@fastify/accept-negotiator';
@@ -9,8 +9,9 @@ import 'etag';
9
9
  import 'ofetch';
10
10
  import 'pathe';
11
11
 
12
- function unstorageToIPXStorage(storage, prefix) {
13
- const resolveKey = (id) => prefix + ":" + id;
12
+ function unstorageToIPXStorage(storage, _options = {}) {
13
+ const options = typeof _options === "string" ? { prefix: _options } : _options;
14
+ const resolveKey = (id) => options.prefix ? `${options.prefix}:${id}` : id;
14
15
  return {
15
16
  name: "ipx:" + (storage.name || "unstorage"),
16
17
  async getMeta(id, opts = {}) {
@@ -26,8 +27,24 @@ function unstorageToIPXStorage(storage, prefix) {
26
27
  return;
27
28
  }
28
29
  const storageKey = resolveKey(id);
29
- const data = await storage.getItemRaw(storageKey, opts);
30
- return data;
30
+ let data = await storage.getItemRaw(storageKey, opts);
31
+ if (!data) {
32
+ return;
33
+ }
34
+ if (data instanceof Blob) {
35
+ data = await data.arrayBuffer();
36
+ }
37
+ try {
38
+ return Buffer.from(data);
39
+ } catch (error) {
40
+ throw createError({
41
+ statusCode: 500,
42
+ statusText: `IPX_STORAGE_ERROR`,
43
+ message: `Failed to parse storage data to Buffer:
44
+ ${error.message}`,
45
+ cause: error
46
+ });
47
+ }
31
48
  }
32
49
  };
33
50
  }
@@ -325,7 +325,9 @@ function createIPX(userOptions) {
325
325
  const options = defu.defu(userOptions, {
326
326
  alias: getEnv("IPX_ALIAS") || {},
327
327
  maxAge: getEnv("IPX_MAX_AGE") ?? 60,
328
- sharpOptions: {}
328
+ sharpOptions: {
329
+ jpegProgressive: true
330
+ }
329
331
  });
330
332
  options.alias = Object.fromEntries(
331
333
  Object.entries(options.alias || {}).map((e) => [
@@ -340,8 +342,7 @@ function createIPX(userOptions) {
340
342
  });
341
343
  const getSVGO = cachedPromise(async () => {
342
344
  const { optimize } = await import('svgo');
343
- const { xss } = await import('../chunks/svgo-xss.cjs');
344
- return { optimize, xss };
345
+ return { optimize };
345
346
  });
346
347
  return function ipx(id, modifiers = {}, opts = {}) {
347
348
  if (!id) {
@@ -416,10 +417,10 @@ function createIPX(userOptions) {
416
417
  meta: imageMeta$1
417
418
  };
418
419
  } else {
419
- const { optimize, xss } = await getSVGO();
420
+ const { optimize } = await getSVGO();
420
421
  const svg = optimize(sourceData.toString("utf8"), {
421
422
  ...options.svgo,
422
- plugins: [xss, ...options.svgo?.plugins || []]
423
+ plugins: ["removeScriptElement", ...options.svgo?.plugins || []]
423
424
  }).data;
424
425
  return {
425
426
  data: svg,
@@ -450,8 +451,7 @@ function createIPX(userOptions) {
450
451
  }
451
452
  if (SUPPORTED_FORMATS.has(format || "")) {
452
453
  sharp = sharp.toFormat(format, {
453
- quality: handlerContext.quality,
454
- progressive: format === "jpeg"
454
+ quality: handlerContext.quality
455
455
  });
456
456
  }
457
457
  const processedImage = await sharp.toBuffer();
@@ -519,13 +519,6 @@ function createIPXH3Handler(ipx) {
519
519
  "content-security-policy",
520
520
  "default-src 'none'"
521
521
  );
522
- if (typeof sourceMeta.maxAge === "number") {
523
- sendResponseHeaderIfNotSet(
524
- event,
525
- "cache-control",
526
- `max-age=${+sourceMeta.maxAge}, public, s-maxage=${+sourceMeta.maxAge}`
527
- );
528
- }
529
522
  if (sourceMeta.mtime) {
530
523
  sendResponseHeaderIfNotSet(
531
524
  event,
@@ -539,6 +532,13 @@ function createIPXH3Handler(ipx) {
539
532
  }
540
533
  }
541
534
  const { data, format } = await img.process();
535
+ if (typeof sourceMeta.maxAge === "number") {
536
+ sendResponseHeaderIfNotSet(
537
+ event,
538
+ "cache-control",
539
+ `max-age=${+sourceMeta.maxAge}, public, s-maxage=${+sourceMeta.maxAge}`
540
+ );
541
+ }
542
542
  const etag = getEtag__default(data);
543
543
  sendResponseHeaderIfNotSet(event, "etag", etag);
544
544
  if (etag && h3.getRequestHeader(event, "if-none-match") === etag) {
@@ -605,9 +605,9 @@ function safeString(input) {
605
605
 
606
606
  const HTTP_RE = /^https?:\/\//;
607
607
  function ipxHttpStorage(_options = {}) {
608
- const allowAllDomains = getEnv("IPX_HTTP_ALLOW_ALL_DOMAINS") ?? false;
608
+ const allowAllDomains = _options.allowAllDomains ?? getEnv("IPX_HTTP_ALLOW_ALL_DOMAINS") ?? false;
609
609
  let _domains = _options.domains || getEnv("IPX_HTTP_DOMAINS") || [];
610
- const defaultMaxAge = _options.maxAge || getEnv("IPX_HTTP_MAX_AGE");
610
+ const defaultMaxAge = _options.maxAge || getEnv("IPX_HTTP_MAX_AGE") || 300;
611
611
  const fetchOptions = _options.fetchOptions || getEnv("IPX_HTTP_FETCH_OPTIONS") || {};
612
612
  if (typeof _domains === "string") {
613
613
  _domains = _domains.split(",").map((s) => s.trim());
@@ -684,56 +684,66 @@ function ipxHttpStorage(_options = {}) {
684
684
  }
685
685
 
686
686
  function ipxFSStorage(_options = {}) {
687
- const rootDir = pathe.resolve(_options.dir || getEnv("IPX_FS_DIR") || ".");
687
+ const dirs = resolveDirs(_options.dir);
688
688
  const maxAge = _options.maxAge || getEnv("IPX_FS_MAX_AGE");
689
- const _resolve = (id) => {
690
- const resolved = pathe.join(rootDir, id);
691
- if (!isValidPath(resolved) || !resolved.startsWith(rootDir)) {
689
+ const _getFS = cachedPromise(
690
+ () => import('node:fs/promises').catch(() => {
692
691
  throw h3.createError({
693
- statusCode: 403,
694
- statusText: `IPX_FORBIDDEN_PATH`,
695
- message: `Forbidden path: ${id}`
692
+ statusCode: 500,
693
+ statusText: `IPX_FILESYSTEM_ERROR`,
694
+ message: `Failed to resolve filesystem module`
696
695
  });
697
- }
698
- return resolved;
699
- };
700
- const _getFS = cachedPromise(() => import('node:fs/promises'));
701
- return {
702
- name: "ipx:node-fs",
703
- async getMeta(id) {
704
- const fsPath = _resolve(id);
705
- let stats;
706
- try {
707
- const fs = await _getFS();
708
- stats = await fs.stat(fsPath);
709
- } catch (error) {
710
- throw error.code === "ENOENT" ? h3.createError({
711
- statusCode: 404,
712
- statusText: `IPX_FILE_NOT_FOUND`,
713
- message: `File not found: ${id}`
714
- }) : h3.createError({
696
+ })
697
+ );
698
+ const resolveFile = async (id) => {
699
+ const fs = await _getFS();
700
+ for (const dir of dirs) {
701
+ const filePath = pathe.join(dir, id);
702
+ if (!isValidPath(filePath) || !filePath.startsWith(dir)) {
703
+ throw h3.createError({
715
704
  statusCode: 403,
716
- statusText: `IPX_FORBIDDEN_FILE`,
717
- message: `File access forbidden: (${error.code}) ${id}`
705
+ statusText: `IPX_FORBIDDEN_PATH`,
706
+ message: `Forbidden path: ${id}`
718
707
  });
719
708
  }
720
- if (!stats.isFile()) {
709
+ try {
710
+ const stats = await fs.stat(filePath);
711
+ if (!stats.isFile()) {
712
+ continue;
713
+ }
714
+ return {
715
+ stats,
716
+ read: () => fs.readFile(filePath)
717
+ };
718
+ } catch (error) {
719
+ if (error.code === "ENOENT") {
720
+ continue;
721
+ }
721
722
  throw h3.createError({
722
- statusCode: 400,
723
- statusText: `IPX_INVALID_FILE`,
724
- message: `Path should be a file: ${id}`
723
+ statusCode: 403,
724
+ statusText: `IPX_FORBIDDEN_FILE`,
725
+ message: `Cannot access file: ${id}`
725
726
  });
726
727
  }
728
+ }
729
+ throw h3.createError({
730
+ statusCode: 404,
731
+ statusText: `IPX_FILE_NOT_FOUND`,
732
+ message: `File not found: ${id}`
733
+ });
734
+ };
735
+ return {
736
+ name: "ipx:node-fs",
737
+ async getMeta(id) {
738
+ const { stats } = await resolveFile(id);
727
739
  return {
728
740
  mtime: stats.mtime,
729
741
  maxAge
730
742
  };
731
743
  },
732
744
  async getData(id) {
733
- const fsPath = _resolve(id);
734
- const fs = await _getFS();
735
- const contents = await fs.readFile(fsPath);
736
- return contents;
745
+ const { read } = await resolveFile(id);
746
+ return read();
737
747
  }
738
748
  };
739
749
  }
@@ -747,6 +757,13 @@ function isValidPath(fp) {
747
757
  }
748
758
  return true;
749
759
  }
760
+ function resolveDirs(dirs) {
761
+ if (!dirs || !Array.isArray(dirs)) {
762
+ const dir = pathe.resolve(dirs || getEnv("IPX_FS_DIR") || ".");
763
+ return [dir];
764
+ }
765
+ return dirs.map((dirs2) => pathe.resolve(dirs2));
766
+ }
750
767
 
751
768
  exports.createIPX = createIPX;
752
769
  exports.createIPXH3App = createIPXH3App;
@@ -318,7 +318,9 @@ function createIPX(userOptions) {
318
318
  const options = defu(userOptions, {
319
319
  alias: getEnv("IPX_ALIAS") || {},
320
320
  maxAge: getEnv("IPX_MAX_AGE") ?? 60,
321
- sharpOptions: {}
321
+ sharpOptions: {
322
+ jpegProgressive: true
323
+ }
322
324
  });
323
325
  options.alias = Object.fromEntries(
324
326
  Object.entries(options.alias || {}).map((e) => [
@@ -333,8 +335,7 @@ function createIPX(userOptions) {
333
335
  });
334
336
  const getSVGO = cachedPromise(async () => {
335
337
  const { optimize } = await import('svgo');
336
- const { xss } = await import('../chunks/svgo-xss.mjs');
337
- return { optimize, xss };
338
+ return { optimize };
338
339
  });
339
340
  return function ipx(id, modifiers = {}, opts = {}) {
340
341
  if (!id) {
@@ -409,10 +410,10 @@ function createIPX(userOptions) {
409
410
  meta: imageMeta$1
410
411
  };
411
412
  } else {
412
- const { optimize, xss } = await getSVGO();
413
+ const { optimize } = await getSVGO();
413
414
  const svg = optimize(sourceData.toString("utf8"), {
414
415
  ...options.svgo,
415
- plugins: [xss, ...options.svgo?.plugins || []]
416
+ plugins: ["removeScriptElement", ...options.svgo?.plugins || []]
416
417
  }).data;
417
418
  return {
418
419
  data: svg,
@@ -443,8 +444,7 @@ function createIPX(userOptions) {
443
444
  }
444
445
  if (SUPPORTED_FORMATS.has(format || "")) {
445
446
  sharp = sharp.toFormat(format, {
446
- quality: handlerContext.quality,
447
- progressive: format === "jpeg"
447
+ quality: handlerContext.quality
448
448
  });
449
449
  }
450
450
  const processedImage = await sharp.toBuffer();
@@ -512,13 +512,6 @@ function createIPXH3Handler(ipx) {
512
512
  "content-security-policy",
513
513
  "default-src 'none'"
514
514
  );
515
- if (typeof sourceMeta.maxAge === "number") {
516
- sendResponseHeaderIfNotSet(
517
- event,
518
- "cache-control",
519
- `max-age=${+sourceMeta.maxAge}, public, s-maxage=${+sourceMeta.maxAge}`
520
- );
521
- }
522
515
  if (sourceMeta.mtime) {
523
516
  sendResponseHeaderIfNotSet(
524
517
  event,
@@ -532,6 +525,13 @@ function createIPXH3Handler(ipx) {
532
525
  }
533
526
  }
534
527
  const { data, format } = await img.process();
528
+ if (typeof sourceMeta.maxAge === "number") {
529
+ sendResponseHeaderIfNotSet(
530
+ event,
531
+ "cache-control",
532
+ `max-age=${+sourceMeta.maxAge}, public, s-maxage=${+sourceMeta.maxAge}`
533
+ );
534
+ }
535
535
  const etag = getEtag(data);
536
536
  sendResponseHeaderIfNotSet(event, "etag", etag);
537
537
  if (etag && getRequestHeader(event, "if-none-match") === etag) {
@@ -598,9 +598,9 @@ function safeString(input) {
598
598
 
599
599
  const HTTP_RE = /^https?:\/\//;
600
600
  function ipxHttpStorage(_options = {}) {
601
- const allowAllDomains = getEnv("IPX_HTTP_ALLOW_ALL_DOMAINS") ?? false;
601
+ const allowAllDomains = _options.allowAllDomains ?? getEnv("IPX_HTTP_ALLOW_ALL_DOMAINS") ?? false;
602
602
  let _domains = _options.domains || getEnv("IPX_HTTP_DOMAINS") || [];
603
- const defaultMaxAge = _options.maxAge || getEnv("IPX_HTTP_MAX_AGE");
603
+ const defaultMaxAge = _options.maxAge || getEnv("IPX_HTTP_MAX_AGE") || 300;
604
604
  const fetchOptions = _options.fetchOptions || getEnv("IPX_HTTP_FETCH_OPTIONS") || {};
605
605
  if (typeof _domains === "string") {
606
606
  _domains = _domains.split(",").map((s) => s.trim());
@@ -677,56 +677,66 @@ function ipxHttpStorage(_options = {}) {
677
677
  }
678
678
 
679
679
  function ipxFSStorage(_options = {}) {
680
- const rootDir = resolve(_options.dir || getEnv("IPX_FS_DIR") || ".");
680
+ const dirs = resolveDirs(_options.dir);
681
681
  const maxAge = _options.maxAge || getEnv("IPX_FS_MAX_AGE");
682
- const _resolve = (id) => {
683
- const resolved = join(rootDir, id);
684
- if (!isValidPath(resolved) || !resolved.startsWith(rootDir)) {
682
+ const _getFS = cachedPromise(
683
+ () => import('node:fs/promises').catch(() => {
685
684
  throw createError({
686
- statusCode: 403,
687
- statusText: `IPX_FORBIDDEN_PATH`,
688
- message: `Forbidden path: ${id}`
685
+ statusCode: 500,
686
+ statusText: `IPX_FILESYSTEM_ERROR`,
687
+ message: `Failed to resolve filesystem module`
689
688
  });
690
- }
691
- return resolved;
692
- };
693
- const _getFS = cachedPromise(() => import('node:fs/promises'));
694
- return {
695
- name: "ipx:node-fs",
696
- async getMeta(id) {
697
- const fsPath = _resolve(id);
698
- let stats;
699
- try {
700
- const fs = await _getFS();
701
- stats = await fs.stat(fsPath);
702
- } catch (error) {
703
- throw error.code === "ENOENT" ? createError({
704
- statusCode: 404,
705
- statusText: `IPX_FILE_NOT_FOUND`,
706
- message: `File not found: ${id}`
707
- }) : createError({
689
+ })
690
+ );
691
+ const resolveFile = async (id) => {
692
+ const fs = await _getFS();
693
+ for (const dir of dirs) {
694
+ const filePath = join(dir, id);
695
+ if (!isValidPath(filePath) || !filePath.startsWith(dir)) {
696
+ throw createError({
708
697
  statusCode: 403,
709
- statusText: `IPX_FORBIDDEN_FILE`,
710
- message: `File access forbidden: (${error.code}) ${id}`
698
+ statusText: `IPX_FORBIDDEN_PATH`,
699
+ message: `Forbidden path: ${id}`
711
700
  });
712
701
  }
713
- if (!stats.isFile()) {
702
+ try {
703
+ const stats = await fs.stat(filePath);
704
+ if (!stats.isFile()) {
705
+ continue;
706
+ }
707
+ return {
708
+ stats,
709
+ read: () => fs.readFile(filePath)
710
+ };
711
+ } catch (error) {
712
+ if (error.code === "ENOENT") {
713
+ continue;
714
+ }
714
715
  throw createError({
715
- statusCode: 400,
716
- statusText: `IPX_INVALID_FILE`,
717
- message: `Path should be a file: ${id}`
716
+ statusCode: 403,
717
+ statusText: `IPX_FORBIDDEN_FILE`,
718
+ message: `Cannot access file: ${id}`
718
719
  });
719
720
  }
721
+ }
722
+ throw createError({
723
+ statusCode: 404,
724
+ statusText: `IPX_FILE_NOT_FOUND`,
725
+ message: `File not found: ${id}`
726
+ });
727
+ };
728
+ return {
729
+ name: "ipx:node-fs",
730
+ async getMeta(id) {
731
+ const { stats } = await resolveFile(id);
720
732
  return {
721
733
  mtime: stats.mtime,
722
734
  maxAge
723
735
  };
724
736
  },
725
737
  async getData(id) {
726
- const fsPath = _resolve(id);
727
- const fs = await _getFS();
728
- const contents = await fs.readFile(fsPath);
729
- return contents;
738
+ const { read } = await resolveFile(id);
739
+ return read();
730
740
  }
731
741
  };
732
742
  }
@@ -740,5 +750,12 @@ function isValidPath(fp) {
740
750
  }
741
751
  return true;
742
752
  }
753
+ function resolveDirs(dirs) {
754
+ if (!dirs || !Array.isArray(dirs)) {
755
+ const dir = resolve(dirs || getEnv("IPX_FS_DIR") || ".");
756
+ return [dir];
757
+ }
758
+ return dirs.map((dirs2) => resolve(dirs2));
759
+ }
743
760
 
744
761
  export { createIPXH3Handler as a, createIPXH3App as b, createIPX as c, createIPXWebServer as d, createIPXNodeServer as e, createIPXPlainServer as f, ipxFSStorage as g, ipxHttpStorage as i };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ipx",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "repository": "unjs/ipx",
5
5
  "description": "High performance, secure and easy-to-use image optimizer.",
6
6
  "license": "MIT",
@@ -38,35 +38,35 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@fastify/accept-negotiator": "^1.1.0",
41
- "citty": "^0.1.4",
41
+ "citty": "^0.1.5",
42
42
  "consola": "^3.2.3",
43
- "defu": "^6.1.3",
43
+ "defu": "^6.1.4",
44
44
  "destr": "^2.0.2",
45
45
  "etag": "^1.8.1",
46
- "h3": "^1.8.2",
46
+ "h3": "^1.10.0",
47
47
  "image-meta": "^0.2.0",
48
- "listhen": "^1.5.5",
48
+ "listhen": "^1.5.6",
49
49
  "ofetch": "^1.3.3",
50
- "pathe": "^1.1.1",
50
+ "pathe": "^1.1.2",
51
51
  "sharp": "^0.32.6",
52
- "svgo": "^3.0.2",
53
- "ufo": "^1.3.1",
54
- "unstorage": "^1.9.0",
52
+ "svgo": "^3.2.0",
53
+ "ufo": "^1.3.2",
54
+ "unstorage": "^1.10.1",
55
55
  "xss": "^1.0.14"
56
56
  },
57
57
  "devDependencies": {
58
- "@types/etag": "^1.8.2",
59
- "@types/is-valid-path": "^0.1.1",
60
- "@vitest/coverage-v8": "^0.34.6",
58
+ "@types/etag": "^1.8.3",
59
+ "@types/is-valid-path": "^0.1.2",
60
+ "@vitest/coverage-v8": "^1.1.3",
61
61
  "changelogen": "^0.5.5",
62
- "eslint": "^8.53.0",
62
+ "eslint": "^8.56.0",
63
63
  "eslint-config-unjs": "^0.2.1",
64
64
  "jiti": "^1.21.0",
65
- "prettier": "^3.0.3",
65
+ "prettier": "^3.1.1",
66
66
  "serve-handler": "^6.1.5",
67
- "typescript": "^5.2.2",
67
+ "typescript": "^5.3.3",
68
68
  "unbuild": "^2.0.0",
69
- "vitest": "^0.34.6"
69
+ "vitest": "^1.1.3"
70
70
  },
71
- "packageManager": "pnpm@8.10.2"
71
+ "packageManager": "pnpm@8.10.5"
72
72
  }
@@ -1,116 +0,0 @@
1
- 'use strict';
2
-
3
- const xss = {
4
- name: "removeXSS",
5
- fn() {
6
- return {
7
- element: {
8
- enter: (node, parentNode) => {
9
- if (node.name === "script") {
10
- parentNode.children = parentNode.children.filter(
11
- (child) => child !== node
12
- );
13
- return;
14
- }
15
- for (const event of ALL_EVENTS) {
16
- if (node.attributes[event] != null) {
17
- delete node.attributes[event];
18
- }
19
- }
20
- },
21
- exit: (node, parentNode) => {
22
- if (node.name !== "a") {
23
- return;
24
- }
25
- for (const attr of Object.keys(node.attributes)) {
26
- if (attr === "href" || attr.endsWith(":href")) {
27
- if (node.attributes[attr] == null || !node.attributes[attr].trimStart().startsWith("javascript:")) {
28
- continue;
29
- }
30
- const index = parentNode.children.indexOf(node);
31
- parentNode.children.splice(index, 1, ...node.children);
32
- for (const child of node.children) {
33
- Object.defineProperty(child, "parentNode", {
34
- writable: true,
35
- value: parentNode
36
- });
37
- }
38
- }
39
- }
40
- }
41
- }
42
- };
43
- }
44
- };
45
- const ALL_EVENTS = [
46
- "onabort",
47
- "onactivate",
48
- "onbegin",
49
- "oncancel",
50
- "oncanplay",
51
- "oncanplaythrough",
52
- "onchange",
53
- "onclick",
54
- "onclose",
55
- "oncopy",
56
- "oncuechange",
57
- "oncut",
58
- "ondblclick",
59
- "ondrag",
60
- "ondragend",
61
- "ondragenter",
62
- "ondragleave",
63
- "ondragover",
64
- "ondragstart",
65
- "ondrop",
66
- "ondurationchange",
67
- "onemptied",
68
- "onend",
69
- "onended",
70
- "onerror",
71
- "onfocus",
72
- "onfocusin",
73
- "onfocusout",
74
- "oninput",
75
- "oninvalid",
76
- "onkeydown",
77
- "onkeypress",
78
- "onkeyup",
79
- "onload",
80
- "onloadeddata",
81
- "onloadedmetadata",
82
- "onloadstart",
83
- "onmousedown",
84
- "onmouseenter",
85
- "onmouseleave",
86
- "onmousemove",
87
- "onmouseout",
88
- "onmouseover",
89
- "onmouseup",
90
- "onmousewheel",
91
- "onpaste",
92
- "onpause",
93
- "onplay",
94
- "onplaying",
95
- "onprogress",
96
- "onratechange",
97
- "onrepeat",
98
- "onreset",
99
- "onresize",
100
- "onscroll",
101
- "onseeked",
102
- "onseeking",
103
- "onselect",
104
- "onshow",
105
- "onstalled",
106
- "onsubmit",
107
- "onsuspend",
108
- "ontimeupdate",
109
- "ontoggle",
110
- "onunload",
111
- "onvolumechange",
112
- "onwaiting",
113
- "onzoom"
114
- ];
115
-
116
- exports.xss = xss;
@@ -1,114 +0,0 @@
1
- const xss = {
2
- name: "removeXSS",
3
- fn() {
4
- return {
5
- element: {
6
- enter: (node, parentNode) => {
7
- if (node.name === "script") {
8
- parentNode.children = parentNode.children.filter(
9
- (child) => child !== node
10
- );
11
- return;
12
- }
13
- for (const event of ALL_EVENTS) {
14
- if (node.attributes[event] != null) {
15
- delete node.attributes[event];
16
- }
17
- }
18
- },
19
- exit: (node, parentNode) => {
20
- if (node.name !== "a") {
21
- return;
22
- }
23
- for (const attr of Object.keys(node.attributes)) {
24
- if (attr === "href" || attr.endsWith(":href")) {
25
- if (node.attributes[attr] == null || !node.attributes[attr].trimStart().startsWith("javascript:")) {
26
- continue;
27
- }
28
- const index = parentNode.children.indexOf(node);
29
- parentNode.children.splice(index, 1, ...node.children);
30
- for (const child of node.children) {
31
- Object.defineProperty(child, "parentNode", {
32
- writable: true,
33
- value: parentNode
34
- });
35
- }
36
- }
37
- }
38
- }
39
- }
40
- };
41
- }
42
- };
43
- const ALL_EVENTS = [
44
- "onabort",
45
- "onactivate",
46
- "onbegin",
47
- "oncancel",
48
- "oncanplay",
49
- "oncanplaythrough",
50
- "onchange",
51
- "onclick",
52
- "onclose",
53
- "oncopy",
54
- "oncuechange",
55
- "oncut",
56
- "ondblclick",
57
- "ondrag",
58
- "ondragend",
59
- "ondragenter",
60
- "ondragleave",
61
- "ondragover",
62
- "ondragstart",
63
- "ondrop",
64
- "ondurationchange",
65
- "onemptied",
66
- "onend",
67
- "onended",
68
- "onerror",
69
- "onfocus",
70
- "onfocusin",
71
- "onfocusout",
72
- "oninput",
73
- "oninvalid",
74
- "onkeydown",
75
- "onkeypress",
76
- "onkeyup",
77
- "onload",
78
- "onloadeddata",
79
- "onloadedmetadata",
80
- "onloadstart",
81
- "onmousedown",
82
- "onmouseenter",
83
- "onmouseleave",
84
- "onmousemove",
85
- "onmouseout",
86
- "onmouseover",
87
- "onmouseup",
88
- "onmousewheel",
89
- "onpaste",
90
- "onpause",
91
- "onplay",
92
- "onplaying",
93
- "onprogress",
94
- "onratechange",
95
- "onrepeat",
96
- "onreset",
97
- "onresize",
98
- "onscroll",
99
- "onseeked",
100
- "onseeking",
101
- "onselect",
102
- "onshow",
103
- "onstalled",
104
- "onsubmit",
105
- "onsuspend",
106
- "ontimeupdate",
107
- "ontoggle",
108
- "onunload",
109
- "onvolumechange",
110
- "onwaiting",
111
- "onzoom"
112
- ];
113
-
114
- export { xss };