ipx 0.9.4 → 0.9.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.
- package/LICENSE +0 -0
- package/dist/chunks/middleware.cjs +65 -38
- package/dist/chunks/middleware.mjs +65 -37
- package/dist/cli.cjs +0 -1
- package/dist/cli.mjs +0 -1
- package/dist/index.cjs +0 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.mjs +0 -1
- package/package.json +23 -24
package/LICENSE
CHANGED
|
File without changes
|
|
@@ -5,7 +5,6 @@ const imageMeta = require('image-meta');
|
|
|
5
5
|
const ufo = require('ufo');
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const pathe = require('pathe');
|
|
8
|
-
const isValidPath = require('is-valid-path');
|
|
9
8
|
const http = require('http');
|
|
10
9
|
const https = require('https');
|
|
11
10
|
const ohmyfetch = require('ohmyfetch');
|
|
@@ -16,7 +15,6 @@ const xss = require('xss');
|
|
|
16
15
|
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e["default"] : e; }
|
|
17
16
|
|
|
18
17
|
const defu__default = /*#__PURE__*/_interopDefaultLegacy(defu);
|
|
19
|
-
const isValidPath__default = /*#__PURE__*/_interopDefaultLegacy(isValidPath);
|
|
20
18
|
const http__default = /*#__PURE__*/_interopDefaultLegacy(http);
|
|
21
19
|
const https__default = /*#__PURE__*/_interopDefaultLegacy(https);
|
|
22
20
|
const destr__default = /*#__PURE__*/_interopDefaultLegacy(destr);
|
|
@@ -74,9 +72,9 @@ function cachedPromise(fn) {
|
|
|
74
72
|
}
|
|
75
73
|
class IPXError extends Error {
|
|
76
74
|
}
|
|
77
|
-
function createError(
|
|
78
|
-
const err = new IPXError(
|
|
79
|
-
err.statusMessage = "IPX: " +
|
|
75
|
+
function createError(statusMessage, statusCode, trace) {
|
|
76
|
+
const err = new IPXError(statusMessage + (trace ? ` (${trace})` : ""));
|
|
77
|
+
err.statusMessage = "IPX: " + statusMessage;
|
|
80
78
|
err.statusCode = statusCode;
|
|
81
79
|
return err;
|
|
82
80
|
}
|
|
@@ -85,29 +83,35 @@ const createFilesystemSource = (options) => {
|
|
|
85
83
|
const rootDir = pathe.resolve(options.dir);
|
|
86
84
|
return async (id) => {
|
|
87
85
|
const fsPath = pathe.resolve(pathe.join(rootDir, id));
|
|
88
|
-
if (!
|
|
89
|
-
throw createError("Forbidden path
|
|
86
|
+
if (!isValidPath(fsPath) || !fsPath.startsWith(rootDir)) {
|
|
87
|
+
throw createError("Forbidden path", 403, id);
|
|
90
88
|
}
|
|
91
89
|
let stats;
|
|
92
90
|
try {
|
|
93
91
|
stats = await fs.promises.stat(fsPath);
|
|
94
92
|
} catch (err) {
|
|
95
93
|
if (err.code === "ENOENT") {
|
|
96
|
-
throw createError("File not found
|
|
94
|
+
throw createError("File not found", 404, fsPath);
|
|
97
95
|
} else {
|
|
98
|
-
throw createError("File access error
|
|
96
|
+
throw createError("File access error " + err.code, 403, fsPath);
|
|
99
97
|
}
|
|
100
98
|
}
|
|
101
99
|
if (!stats.isFile()) {
|
|
102
|
-
throw createError("Path should be a file
|
|
100
|
+
throw createError("Path should be a file", 400, fsPath);
|
|
103
101
|
}
|
|
104
102
|
return {
|
|
105
103
|
mtime: stats.mtime,
|
|
106
|
-
maxAge: options.maxAge
|
|
104
|
+
maxAge: options.maxAge,
|
|
107
105
|
getData: cachedPromise(() => fs.promises.readFile(fsPath))
|
|
108
106
|
};
|
|
109
107
|
};
|
|
110
108
|
};
|
|
109
|
+
function isValidPath(fp) {
|
|
110
|
+
if (/[<>:"|?*]/.test(fp)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
111
115
|
|
|
112
116
|
const createHTTPSource = (options) => {
|
|
113
117
|
const httpsAgent = new https__default.Agent({ keepAlive: true });
|
|
@@ -120,18 +124,19 @@ const createHTTPSource = (options) => {
|
|
|
120
124
|
return async (id, reqOptions) => {
|
|
121
125
|
const url = new URL(id);
|
|
122
126
|
if (!url.hostname) {
|
|
123
|
-
throw createError("Hostname is missing
|
|
127
|
+
throw createError("Hostname is missing", 403, id);
|
|
124
128
|
}
|
|
125
129
|
if (!reqOptions?.bypassDomain && !hosts.find((host) => url.hostname === host)) {
|
|
126
|
-
throw createError("Forbidden host
|
|
130
|
+
throw createError("Forbidden host", 403, url.hostname);
|
|
127
131
|
}
|
|
128
132
|
const response = await ohmyfetch.fetch(id, {
|
|
129
|
-
agent: id.startsWith("https") ? httpsAgent : httpAgent
|
|
133
|
+
agent: id.startsWith("https") ? httpsAgent : httpAgent,
|
|
134
|
+
...options.fetchOptions
|
|
130
135
|
});
|
|
131
136
|
if (!response.ok) {
|
|
132
|
-
throw createError(
|
|
137
|
+
throw createError("Fetch error", response.status || 500, response.statusText);
|
|
133
138
|
}
|
|
134
|
-
let maxAge = options.maxAge
|
|
139
|
+
let maxAge = options.maxAge;
|
|
135
140
|
const _cacheControl = response.headers.get("cache-control");
|
|
136
141
|
if (_cacheControl) {
|
|
137
142
|
const m = _cacheControl.match(/max-age=(\d+)/);
|
|
@@ -147,7 +152,7 @@ const createHTTPSource = (options) => {
|
|
|
147
152
|
return {
|
|
148
153
|
mtime,
|
|
149
154
|
maxAge,
|
|
150
|
-
getData: cachedPromise(() => response.
|
|
155
|
+
getData: cachedPromise(() => response.arrayBuffer().then((ab) => Buffer.from(ab)))
|
|
151
156
|
};
|
|
152
157
|
};
|
|
153
158
|
};
|
|
@@ -384,12 +389,14 @@ const h = height;
|
|
|
384
389
|
const s = resize;
|
|
385
390
|
const pos = position;
|
|
386
391
|
|
|
387
|
-
const SUPPORTED_FORMATS = ["jpeg", "png", "webp", "avif", "tiff"];
|
|
392
|
+
const SUPPORTED_FORMATS = ["jpeg", "png", "webp", "avif", "tiff", "gif"];
|
|
388
393
|
function createIPX(userOptions) {
|
|
389
394
|
const defaults = {
|
|
390
395
|
dir: getEnv("IPX_DIR", "."),
|
|
391
396
|
domains: getEnv("IPX_DOMAINS", []),
|
|
392
397
|
alias: getEnv("IPX_ALIAS", {}),
|
|
398
|
+
fetchOptions: getEnv("IPX_FETCH_OPTIONS", {}),
|
|
399
|
+
maxAge: getEnv("IPX_MAX_AGE", 300),
|
|
393
400
|
sharp: {}
|
|
394
401
|
};
|
|
395
402
|
const options = defu__default(userOptions, defaults);
|
|
@@ -399,12 +406,15 @@ function createIPX(userOptions) {
|
|
|
399
406
|
};
|
|
400
407
|
if (options.dir) {
|
|
401
408
|
ctx.sources.filesystem = createFilesystemSource({
|
|
402
|
-
dir: options.dir
|
|
409
|
+
dir: options.dir,
|
|
410
|
+
maxAge: options.maxAge
|
|
403
411
|
});
|
|
404
412
|
}
|
|
405
413
|
if (options.domains) {
|
|
406
414
|
ctx.sources.http = createHTTPSource({
|
|
407
|
-
domains: options.domains
|
|
415
|
+
domains: options.domains,
|
|
416
|
+
fetchOptions: options.fetchOptions,
|
|
417
|
+
maxAge: options.maxAge
|
|
408
418
|
});
|
|
409
419
|
}
|
|
410
420
|
return function ipx(id, modifiers = {}, reqOptions = {}) {
|
|
@@ -420,7 +430,7 @@ function createIPX(userOptions) {
|
|
|
420
430
|
const getSrc = cachedPromise(() => {
|
|
421
431
|
const source = ufo.hasProtocol(id) ? "http" : "filesystem";
|
|
422
432
|
if (!ctx.sources[source]) {
|
|
423
|
-
throw createError("Unknown source
|
|
433
|
+
throw createError("Unknown source", 400, source);
|
|
424
434
|
}
|
|
425
435
|
return ctx.sources[source](id, reqOptions);
|
|
426
436
|
});
|
|
@@ -440,10 +450,7 @@ function createIPX(userOptions) {
|
|
|
440
450
|
meta
|
|
441
451
|
};
|
|
442
452
|
}
|
|
443
|
-
const animated = modifiers.animated !== void 0 || modifiers.a !== void 0;
|
|
444
|
-
if (animated) {
|
|
445
|
-
format = "webp";
|
|
446
|
-
}
|
|
453
|
+
const animated = modifiers.animated !== void 0 || modifiers.a !== void 0 || format === "gif";
|
|
447
454
|
const Sharp = await import('sharp').then((r) => r.default || r);
|
|
448
455
|
let sharp = Sharp(data, { animated });
|
|
449
456
|
Object.assign(sharp.options, options.sharp);
|
|
@@ -476,6 +483,8 @@ function createIPX(userOptions) {
|
|
|
476
483
|
};
|
|
477
484
|
}
|
|
478
485
|
|
|
486
|
+
const MODIFIER_SEP = /[,&]/g;
|
|
487
|
+
const MODIFIER_VAL_SEP = /[_=:]/g;
|
|
479
488
|
async function _handleRequest(req, ipx) {
|
|
480
489
|
const res = {
|
|
481
490
|
statusCode: 200,
|
|
@@ -483,19 +492,19 @@ async function _handleRequest(req, ipx) {
|
|
|
483
492
|
headers: {},
|
|
484
493
|
body: ""
|
|
485
494
|
};
|
|
486
|
-
const [modifiersStr = "", ...idSegments] = req.url.
|
|
487
|
-
const id = ufo.decode(idSegments.join("/"));
|
|
495
|
+
const [modifiersStr = "", ...idSegments] = req.url.substring(1).split("/");
|
|
496
|
+
const id = safeString(ufo.decode(idSegments.join("/")));
|
|
488
497
|
if (!modifiersStr) {
|
|
489
|
-
throw createError("Modifiers
|
|
498
|
+
throw createError("Modifiers are missing", 400, req.url);
|
|
490
499
|
}
|
|
491
500
|
if (!id || id === "/") {
|
|
492
|
-
throw createError("Resource id is missing
|
|
501
|
+
throw createError("Resource id is missing", 400, req.url);
|
|
493
502
|
}
|
|
494
503
|
const modifiers = /* @__PURE__ */ Object.create(null);
|
|
495
504
|
if (modifiersStr !== "_") {
|
|
496
|
-
for (const p of modifiersStr.split(
|
|
497
|
-
const [key, value = ""] = p.split(
|
|
498
|
-
modifiers[key] = ufo.decode(value);
|
|
505
|
+
for (const p of modifiersStr.split(MODIFIER_SEP)) {
|
|
506
|
+
const [key, value = ""] = p.split(MODIFIER_VAL_SEP);
|
|
507
|
+
modifiers[safeString(key)] = safeString(ufo.decode(value));
|
|
499
508
|
}
|
|
500
509
|
}
|
|
501
510
|
const img = ipx(id, modifiers, req.options);
|
|
@@ -509,7 +518,7 @@ async function _handleRequest(req, ipx) {
|
|
|
509
518
|
}
|
|
510
519
|
res.headers["Last-Modified"] = +src.mtime + "";
|
|
511
520
|
}
|
|
512
|
-
if (src.maxAge
|
|
521
|
+
if (typeof src.maxAge === "number") {
|
|
513
522
|
res.headers["Cache-Control"] = `max-age=${+src.maxAge}, public, s-maxage=${+src.maxAge}`;
|
|
514
523
|
}
|
|
515
524
|
const { data, format } = await img.data();
|
|
@@ -523,21 +532,21 @@ async function _handleRequest(req, ipx) {
|
|
|
523
532
|
res.headers["Content-Type"] = `image/${format}`;
|
|
524
533
|
}
|
|
525
534
|
res.body = data;
|
|
526
|
-
return res;
|
|
535
|
+
return sanetizeReponse(res);
|
|
527
536
|
}
|
|
528
537
|
function handleRequest(req, ipx) {
|
|
529
538
|
return _handleRequest(req, ipx).catch((err) => {
|
|
530
539
|
const statusCode = parseInt(err.statusCode) || 500;
|
|
531
|
-
const statusMessage = err.statusMessage ?
|
|
540
|
+
const statusMessage = err.statusMessage ? err.statusMessage : `IPX Error (${statusCode})`;
|
|
532
541
|
if (process.env.NODE_ENV !== "production" && statusCode === 500) {
|
|
533
542
|
console.error(err);
|
|
534
543
|
}
|
|
535
|
-
return {
|
|
544
|
+
return sanetizeReponse({
|
|
536
545
|
statusCode,
|
|
537
546
|
statusMessage,
|
|
538
|
-
body:
|
|
547
|
+
body: "IPX Error: " + err,
|
|
539
548
|
headers: {}
|
|
540
|
-
};
|
|
549
|
+
});
|
|
541
550
|
});
|
|
542
551
|
}
|
|
543
552
|
function createIPXMiddleware(ipx) {
|
|
@@ -552,6 +561,24 @@ function createIPXMiddleware(ipx) {
|
|
|
552
561
|
});
|
|
553
562
|
};
|
|
554
563
|
}
|
|
564
|
+
function sanetizeReponse(res) {
|
|
565
|
+
return {
|
|
566
|
+
statusCode: res.statusCode || 200,
|
|
567
|
+
statusMessage: res.statusMessage ? safeString(res.statusMessage) : "OK",
|
|
568
|
+
headers: safeStringObject(res.headers || {}),
|
|
569
|
+
body: typeof res.body === "string" ? xss__default(safeString(res.body)) : res.body || ""
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function safeString(input) {
|
|
573
|
+
return JSON.stringify(input).replace(/^"|"$/g, "");
|
|
574
|
+
}
|
|
575
|
+
function safeStringObject(input) {
|
|
576
|
+
const dst = {};
|
|
577
|
+
for (const key in input) {
|
|
578
|
+
dst[key] = safeString(input[key]);
|
|
579
|
+
}
|
|
580
|
+
return dst;
|
|
581
|
+
}
|
|
555
582
|
|
|
556
583
|
exports.createIPX = createIPX;
|
|
557
584
|
exports.createIPXMiddleware = createIPXMiddleware;
|
|
@@ -3,7 +3,6 @@ import { imageMeta } from 'image-meta';
|
|
|
3
3
|
import { parseURL, withLeadingSlash, hasProtocol, joinURL, decode } from 'ufo';
|
|
4
4
|
import { promises } from 'fs';
|
|
5
5
|
import { resolve, join } from 'pathe';
|
|
6
|
-
import isValidPath from 'is-valid-path';
|
|
7
6
|
import http from 'http';
|
|
8
7
|
import https from 'https';
|
|
9
8
|
import { fetch } from 'ohmyfetch';
|
|
@@ -62,9 +61,9 @@ function cachedPromise(fn) {
|
|
|
62
61
|
}
|
|
63
62
|
class IPXError extends Error {
|
|
64
63
|
}
|
|
65
|
-
function createError(
|
|
66
|
-
const err = new IPXError(
|
|
67
|
-
err.statusMessage = "IPX: " +
|
|
64
|
+
function createError(statusMessage, statusCode, trace) {
|
|
65
|
+
const err = new IPXError(statusMessage + (trace ? ` (${trace})` : ""));
|
|
66
|
+
err.statusMessage = "IPX: " + statusMessage;
|
|
68
67
|
err.statusCode = statusCode;
|
|
69
68
|
return err;
|
|
70
69
|
}
|
|
@@ -73,29 +72,35 @@ const createFilesystemSource = (options) => {
|
|
|
73
72
|
const rootDir = resolve(options.dir);
|
|
74
73
|
return async (id) => {
|
|
75
74
|
const fsPath = resolve(join(rootDir, id));
|
|
76
|
-
if (!isValidPath(
|
|
77
|
-
throw createError("Forbidden path
|
|
75
|
+
if (!isValidPath(fsPath) || !fsPath.startsWith(rootDir)) {
|
|
76
|
+
throw createError("Forbidden path", 403, id);
|
|
78
77
|
}
|
|
79
78
|
let stats;
|
|
80
79
|
try {
|
|
81
80
|
stats = await promises.stat(fsPath);
|
|
82
81
|
} catch (err) {
|
|
83
82
|
if (err.code === "ENOENT") {
|
|
84
|
-
throw createError("File not found
|
|
83
|
+
throw createError("File not found", 404, fsPath);
|
|
85
84
|
} else {
|
|
86
|
-
throw createError("File access error
|
|
85
|
+
throw createError("File access error " + err.code, 403, fsPath);
|
|
87
86
|
}
|
|
88
87
|
}
|
|
89
88
|
if (!stats.isFile()) {
|
|
90
|
-
throw createError("Path should be a file
|
|
89
|
+
throw createError("Path should be a file", 400, fsPath);
|
|
91
90
|
}
|
|
92
91
|
return {
|
|
93
92
|
mtime: stats.mtime,
|
|
94
|
-
maxAge: options.maxAge
|
|
93
|
+
maxAge: options.maxAge,
|
|
95
94
|
getData: cachedPromise(() => promises.readFile(fsPath))
|
|
96
95
|
};
|
|
97
96
|
};
|
|
98
97
|
};
|
|
98
|
+
function isValidPath(fp) {
|
|
99
|
+
if (/[<>:"|?*]/.test(fp)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
99
104
|
|
|
100
105
|
const createHTTPSource = (options) => {
|
|
101
106
|
const httpsAgent = new https.Agent({ keepAlive: true });
|
|
@@ -108,18 +113,19 @@ const createHTTPSource = (options) => {
|
|
|
108
113
|
return async (id, reqOptions) => {
|
|
109
114
|
const url = new URL(id);
|
|
110
115
|
if (!url.hostname) {
|
|
111
|
-
throw createError("Hostname is missing
|
|
116
|
+
throw createError("Hostname is missing", 403, id);
|
|
112
117
|
}
|
|
113
118
|
if (!reqOptions?.bypassDomain && !hosts.find((host) => url.hostname === host)) {
|
|
114
|
-
throw createError("Forbidden host
|
|
119
|
+
throw createError("Forbidden host", 403, url.hostname);
|
|
115
120
|
}
|
|
116
121
|
const response = await fetch(id, {
|
|
117
|
-
agent: id.startsWith("https") ? httpsAgent : httpAgent
|
|
122
|
+
agent: id.startsWith("https") ? httpsAgent : httpAgent,
|
|
123
|
+
...options.fetchOptions
|
|
118
124
|
});
|
|
119
125
|
if (!response.ok) {
|
|
120
|
-
throw createError(
|
|
126
|
+
throw createError("Fetch error", response.status || 500, response.statusText);
|
|
121
127
|
}
|
|
122
|
-
let maxAge = options.maxAge
|
|
128
|
+
let maxAge = options.maxAge;
|
|
123
129
|
const _cacheControl = response.headers.get("cache-control");
|
|
124
130
|
if (_cacheControl) {
|
|
125
131
|
const m = _cacheControl.match(/max-age=(\d+)/);
|
|
@@ -135,7 +141,7 @@ const createHTTPSource = (options) => {
|
|
|
135
141
|
return {
|
|
136
142
|
mtime,
|
|
137
143
|
maxAge,
|
|
138
|
-
getData: cachedPromise(() => response.
|
|
144
|
+
getData: cachedPromise(() => response.arrayBuffer().then((ab) => Buffer.from(ab)))
|
|
139
145
|
};
|
|
140
146
|
};
|
|
141
147
|
};
|
|
@@ -372,12 +378,14 @@ const h = height;
|
|
|
372
378
|
const s = resize;
|
|
373
379
|
const pos = position;
|
|
374
380
|
|
|
375
|
-
const SUPPORTED_FORMATS = ["jpeg", "png", "webp", "avif", "tiff"];
|
|
381
|
+
const SUPPORTED_FORMATS = ["jpeg", "png", "webp", "avif", "tiff", "gif"];
|
|
376
382
|
function createIPX(userOptions) {
|
|
377
383
|
const defaults = {
|
|
378
384
|
dir: getEnv("IPX_DIR", "."),
|
|
379
385
|
domains: getEnv("IPX_DOMAINS", []),
|
|
380
386
|
alias: getEnv("IPX_ALIAS", {}),
|
|
387
|
+
fetchOptions: getEnv("IPX_FETCH_OPTIONS", {}),
|
|
388
|
+
maxAge: getEnv("IPX_MAX_AGE", 300),
|
|
381
389
|
sharp: {}
|
|
382
390
|
};
|
|
383
391
|
const options = defu(userOptions, defaults);
|
|
@@ -387,12 +395,15 @@ function createIPX(userOptions) {
|
|
|
387
395
|
};
|
|
388
396
|
if (options.dir) {
|
|
389
397
|
ctx.sources.filesystem = createFilesystemSource({
|
|
390
|
-
dir: options.dir
|
|
398
|
+
dir: options.dir,
|
|
399
|
+
maxAge: options.maxAge
|
|
391
400
|
});
|
|
392
401
|
}
|
|
393
402
|
if (options.domains) {
|
|
394
403
|
ctx.sources.http = createHTTPSource({
|
|
395
|
-
domains: options.domains
|
|
404
|
+
domains: options.domains,
|
|
405
|
+
fetchOptions: options.fetchOptions,
|
|
406
|
+
maxAge: options.maxAge
|
|
396
407
|
});
|
|
397
408
|
}
|
|
398
409
|
return function ipx(id, modifiers = {}, reqOptions = {}) {
|
|
@@ -408,7 +419,7 @@ function createIPX(userOptions) {
|
|
|
408
419
|
const getSrc = cachedPromise(() => {
|
|
409
420
|
const source = hasProtocol(id) ? "http" : "filesystem";
|
|
410
421
|
if (!ctx.sources[source]) {
|
|
411
|
-
throw createError("Unknown source
|
|
422
|
+
throw createError("Unknown source", 400, source);
|
|
412
423
|
}
|
|
413
424
|
return ctx.sources[source](id, reqOptions);
|
|
414
425
|
});
|
|
@@ -428,10 +439,7 @@ function createIPX(userOptions) {
|
|
|
428
439
|
meta
|
|
429
440
|
};
|
|
430
441
|
}
|
|
431
|
-
const animated = modifiers.animated !== void 0 || modifiers.a !== void 0;
|
|
432
|
-
if (animated) {
|
|
433
|
-
format = "webp";
|
|
434
|
-
}
|
|
442
|
+
const animated = modifiers.animated !== void 0 || modifiers.a !== void 0 || format === "gif";
|
|
435
443
|
const Sharp = await import('sharp').then((r) => r.default || r);
|
|
436
444
|
let sharp = Sharp(data, { animated });
|
|
437
445
|
Object.assign(sharp.options, options.sharp);
|
|
@@ -464,6 +472,8 @@ function createIPX(userOptions) {
|
|
|
464
472
|
};
|
|
465
473
|
}
|
|
466
474
|
|
|
475
|
+
const MODIFIER_SEP = /[,&]/g;
|
|
476
|
+
const MODIFIER_VAL_SEP = /[_=:]/g;
|
|
467
477
|
async function _handleRequest(req, ipx) {
|
|
468
478
|
const res = {
|
|
469
479
|
statusCode: 200,
|
|
@@ -471,19 +481,19 @@ async function _handleRequest(req, ipx) {
|
|
|
471
481
|
headers: {},
|
|
472
482
|
body: ""
|
|
473
483
|
};
|
|
474
|
-
const [modifiersStr = "", ...idSegments] = req.url.
|
|
475
|
-
const id = decode(idSegments.join("/"));
|
|
484
|
+
const [modifiersStr = "", ...idSegments] = req.url.substring(1).split("/");
|
|
485
|
+
const id = safeString(decode(idSegments.join("/")));
|
|
476
486
|
if (!modifiersStr) {
|
|
477
|
-
throw createError("Modifiers
|
|
487
|
+
throw createError("Modifiers are missing", 400, req.url);
|
|
478
488
|
}
|
|
479
489
|
if (!id || id === "/") {
|
|
480
|
-
throw createError("Resource id is missing
|
|
490
|
+
throw createError("Resource id is missing", 400, req.url);
|
|
481
491
|
}
|
|
482
492
|
const modifiers = /* @__PURE__ */ Object.create(null);
|
|
483
493
|
if (modifiersStr !== "_") {
|
|
484
|
-
for (const p of modifiersStr.split(
|
|
485
|
-
const [key, value = ""] = p.split(
|
|
486
|
-
modifiers[key] = decode(value);
|
|
494
|
+
for (const p of modifiersStr.split(MODIFIER_SEP)) {
|
|
495
|
+
const [key, value = ""] = p.split(MODIFIER_VAL_SEP);
|
|
496
|
+
modifiers[safeString(key)] = safeString(decode(value));
|
|
487
497
|
}
|
|
488
498
|
}
|
|
489
499
|
const img = ipx(id, modifiers, req.options);
|
|
@@ -497,7 +507,7 @@ async function _handleRequest(req, ipx) {
|
|
|
497
507
|
}
|
|
498
508
|
res.headers["Last-Modified"] = +src.mtime + "";
|
|
499
509
|
}
|
|
500
|
-
if (src.maxAge
|
|
510
|
+
if (typeof src.maxAge === "number") {
|
|
501
511
|
res.headers["Cache-Control"] = `max-age=${+src.maxAge}, public, s-maxage=${+src.maxAge}`;
|
|
502
512
|
}
|
|
503
513
|
const { data, format } = await img.data();
|
|
@@ -511,21 +521,21 @@ async function _handleRequest(req, ipx) {
|
|
|
511
521
|
res.headers["Content-Type"] = `image/${format}`;
|
|
512
522
|
}
|
|
513
523
|
res.body = data;
|
|
514
|
-
return res;
|
|
524
|
+
return sanetizeReponse(res);
|
|
515
525
|
}
|
|
516
526
|
function handleRequest(req, ipx) {
|
|
517
527
|
return _handleRequest(req, ipx).catch((err) => {
|
|
518
528
|
const statusCode = parseInt(err.statusCode) || 500;
|
|
519
|
-
const statusMessage = err.statusMessage ?
|
|
529
|
+
const statusMessage = err.statusMessage ? err.statusMessage : `IPX Error (${statusCode})`;
|
|
520
530
|
if (process.env.NODE_ENV !== "production" && statusCode === 500) {
|
|
521
531
|
console.error(err);
|
|
522
532
|
}
|
|
523
|
-
return {
|
|
533
|
+
return sanetizeReponse({
|
|
524
534
|
statusCode,
|
|
525
535
|
statusMessage,
|
|
526
|
-
body:
|
|
536
|
+
body: "IPX Error: " + err,
|
|
527
537
|
headers: {}
|
|
528
|
-
};
|
|
538
|
+
});
|
|
529
539
|
});
|
|
530
540
|
}
|
|
531
541
|
function createIPXMiddleware(ipx) {
|
|
@@ -540,5 +550,23 @@ function createIPXMiddleware(ipx) {
|
|
|
540
550
|
});
|
|
541
551
|
};
|
|
542
552
|
}
|
|
553
|
+
function sanetizeReponse(res) {
|
|
554
|
+
return {
|
|
555
|
+
statusCode: res.statusCode || 200,
|
|
556
|
+
statusMessage: res.statusMessage ? safeString(res.statusMessage) : "OK",
|
|
557
|
+
headers: safeStringObject(res.headers || {}),
|
|
558
|
+
body: typeof res.body === "string" ? xss(safeString(res.body)) : res.body || ""
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
function safeString(input) {
|
|
562
|
+
return JSON.stringify(input).replace(/^"|"$/g, "");
|
|
563
|
+
}
|
|
564
|
+
function safeStringObject(input) {
|
|
565
|
+
const dst = {};
|
|
566
|
+
for (const key in input) {
|
|
567
|
+
dst[key] = safeString(input[key]);
|
|
568
|
+
}
|
|
569
|
+
return dst;
|
|
570
|
+
}
|
|
543
571
|
|
|
544
572
|
export { createIPXMiddleware as a, createIPX as c, handleRequest as h };
|
package/dist/cli.cjs
CHANGED
package/dist/cli.mjs
CHANGED
package/dist/index.cjs
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ interface SourceData {
|
|
|
6
6
|
getData: () => Promise<Buffer>;
|
|
7
7
|
}
|
|
8
8
|
declare type Source = (src: string, reqOptions?: any) => Promise<SourceData>;
|
|
9
|
-
declare type SourceFactory = (options
|
|
9
|
+
declare type SourceFactory<T = Record<string, any>> = (options: T) => Source;
|
|
10
10
|
|
|
11
11
|
interface ImageMeta {
|
|
12
12
|
width: number;
|
|
@@ -27,8 +27,10 @@ declare type IPX = (id: string, modifiers?: Record<string, string>, reqOptions?:
|
|
|
27
27
|
};
|
|
28
28
|
interface IPXOptions {
|
|
29
29
|
dir?: false | string;
|
|
30
|
+
maxAge?: number;
|
|
30
31
|
domains?: false | string[];
|
|
31
32
|
alias: Record<string, string>;
|
|
33
|
+
fetchOptions: RequestInit;
|
|
32
34
|
sharp?: {
|
|
33
35
|
[key: string]: any;
|
|
34
36
|
};
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ipx",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"repository": "unjs/ipx",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"exports": {
|
|
@@ -17,43 +17,42 @@
|
|
|
17
17
|
"dist",
|
|
18
18
|
"bin"
|
|
19
19
|
],
|
|
20
|
-
"scripts": {
|
|
21
|
-
"build": "unbuild",
|
|
22
|
-
"dev": "nodemon",
|
|
23
|
-
"lint": "eslint --ext .ts .",
|
|
24
|
-
"prepack": "yarn build",
|
|
25
|
-
"release": "yarn test && standard-version && git push --follow-tags && npm publish",
|
|
26
|
-
"start": "node bin/ipx.js",
|
|
27
|
-
"test": "yarn lint && jest"
|
|
28
|
-
},
|
|
29
20
|
"dependencies": {
|
|
30
21
|
"consola": "^2.15.3",
|
|
31
|
-
"defu": "^
|
|
32
|
-
"destr": "^1.1.
|
|
22
|
+
"defu": "^6.0.0",
|
|
23
|
+
"destr": "^1.1.1",
|
|
33
24
|
"etag": "^1.8.1",
|
|
34
25
|
"image-meta": "^0.1.1",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"xss": "^1.0.10"
|
|
26
|
+
"listhen": "^0.2.13",
|
|
27
|
+
"ohmyfetch": "^0.4.18",
|
|
28
|
+
"pathe": "^0.3.0",
|
|
29
|
+
"sharp": "^0.30.6",
|
|
30
|
+
"ufo": "^0.8.4",
|
|
31
|
+
"xss": "^1.0.13"
|
|
42
32
|
},
|
|
43
33
|
"devDependencies": {
|
|
44
34
|
"@nuxtjs/eslint-config-typescript": "latest",
|
|
45
35
|
"@types/etag": "latest",
|
|
46
36
|
"@types/is-valid-path": "latest",
|
|
47
|
-
"@types/jest": "latest",
|
|
48
37
|
"@types/node-fetch": "latest",
|
|
49
38
|
"@types/sharp": "latest",
|
|
39
|
+
"c8": "latest",
|
|
50
40
|
"eslint": "latest",
|
|
51
|
-
"jest": "latest",
|
|
52
41
|
"jiti": "latest",
|
|
53
42
|
"nodemon": "latest",
|
|
43
|
+
"serve-handler": "^6.1.3",
|
|
54
44
|
"standard-version": "latest",
|
|
55
|
-
"ts-jest": "latest",
|
|
56
45
|
"typescript": "latest",
|
|
57
|
-
"unbuild": "latest"
|
|
46
|
+
"unbuild": "latest",
|
|
47
|
+
"vitest": "latest"
|
|
48
|
+
},
|
|
49
|
+
"packageManager": "pnpm@7.3.0",
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "unbuild",
|
|
52
|
+
"dev": "nodemon",
|
|
53
|
+
"lint": "eslint --ext .ts .",
|
|
54
|
+
"release": "pnpm test && standard-version && git push --follow-tags && pnpm publish",
|
|
55
|
+
"start": "node bin/ipx.js",
|
|
56
|
+
"test": "pnpm lint && vitest run --coverage"
|
|
58
57
|
}
|
|
59
|
-
}
|
|
58
|
+
}
|