ipx 0.9.4 → 0.9.8
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 +76 -45
- package/dist/chunks/middleware.mjs +78 -46
- 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,53 +83,64 @@ 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
|
+
const isWindows = process.platform === "win32";
|
|
110
|
+
function isValidPath(fp) {
|
|
111
|
+
if (isWindows) {
|
|
112
|
+
fp = fp.slice(pathe.parse(fp).root.length);
|
|
113
|
+
}
|
|
114
|
+
if (/[<>:"|?*]/.test(fp)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
111
119
|
|
|
112
120
|
const createHTTPSource = (options) => {
|
|
113
121
|
const httpsAgent = new https__default.Agent({ keepAlive: true });
|
|
114
122
|
const httpAgent = new http__default.Agent({ keepAlive: true });
|
|
115
|
-
let
|
|
116
|
-
if (typeof
|
|
117
|
-
|
|
123
|
+
let _domains = options.domains || [];
|
|
124
|
+
if (typeof _domains === "string") {
|
|
125
|
+
_domains = _domains.split(",").map((s) => s.trim());
|
|
118
126
|
}
|
|
119
|
-
const
|
|
127
|
+
const domains = _domains.map((d) => new URL(d).hostname || new URL("http://" + d).hostname).filter(Boolean);
|
|
120
128
|
return async (id, reqOptions) => {
|
|
121
|
-
const
|
|
122
|
-
if (!
|
|
123
|
-
throw createError("Hostname is missing
|
|
129
|
+
const hostname = new URL(id).hostname;
|
|
130
|
+
if (!hostname) {
|
|
131
|
+
throw createError("Hostname is missing", 403, id);
|
|
124
132
|
}
|
|
125
|
-
if (!reqOptions?.bypassDomain && !
|
|
126
|
-
throw createError("Forbidden host
|
|
133
|
+
if (!reqOptions?.bypassDomain && !domains.find((domain) => hostname === domain)) {
|
|
134
|
+
throw createError("Forbidden host", 403, hostname);
|
|
127
135
|
}
|
|
128
136
|
const response = await ohmyfetch.fetch(id, {
|
|
129
|
-
agent: id.startsWith("https") ? httpsAgent : httpAgent
|
|
137
|
+
agent: id.startsWith("https") ? httpsAgent : httpAgent,
|
|
138
|
+
...options.fetchOptions
|
|
130
139
|
});
|
|
131
140
|
if (!response.ok) {
|
|
132
|
-
throw createError(
|
|
141
|
+
throw createError("Fetch error", response.status || 500, response.statusText);
|
|
133
142
|
}
|
|
134
|
-
let maxAge = options.maxAge
|
|
143
|
+
let maxAge = options.maxAge;
|
|
135
144
|
const _cacheControl = response.headers.get("cache-control");
|
|
136
145
|
if (_cacheControl) {
|
|
137
146
|
const m = _cacheControl.match(/max-age=(\d+)/);
|
|
@@ -147,7 +156,7 @@ const createHTTPSource = (options) => {
|
|
|
147
156
|
return {
|
|
148
157
|
mtime,
|
|
149
158
|
maxAge,
|
|
150
|
-
getData: cachedPromise(() => response.
|
|
159
|
+
getData: cachedPromise(() => response.arrayBuffer().then((ab) => Buffer.from(ab)))
|
|
151
160
|
};
|
|
152
161
|
};
|
|
153
162
|
};
|
|
@@ -384,12 +393,14 @@ const h = height;
|
|
|
384
393
|
const s = resize;
|
|
385
394
|
const pos = position;
|
|
386
395
|
|
|
387
|
-
const SUPPORTED_FORMATS = ["jpeg", "png", "webp", "avif", "tiff"];
|
|
396
|
+
const SUPPORTED_FORMATS = ["jpeg", "png", "webp", "avif", "tiff", "gif"];
|
|
388
397
|
function createIPX(userOptions) {
|
|
389
398
|
const defaults = {
|
|
390
399
|
dir: getEnv("IPX_DIR", "."),
|
|
391
400
|
domains: getEnv("IPX_DOMAINS", []),
|
|
392
401
|
alias: getEnv("IPX_ALIAS", {}),
|
|
402
|
+
fetchOptions: getEnv("IPX_FETCH_OPTIONS", {}),
|
|
403
|
+
maxAge: getEnv("IPX_MAX_AGE", 300),
|
|
393
404
|
sharp: {}
|
|
394
405
|
};
|
|
395
406
|
const options = defu__default(userOptions, defaults);
|
|
@@ -399,12 +410,15 @@ function createIPX(userOptions) {
|
|
|
399
410
|
};
|
|
400
411
|
if (options.dir) {
|
|
401
412
|
ctx.sources.filesystem = createFilesystemSource({
|
|
402
|
-
dir: options.dir
|
|
413
|
+
dir: options.dir,
|
|
414
|
+
maxAge: options.maxAge
|
|
403
415
|
});
|
|
404
416
|
}
|
|
405
417
|
if (options.domains) {
|
|
406
418
|
ctx.sources.http = createHTTPSource({
|
|
407
|
-
domains: options.domains
|
|
419
|
+
domains: options.domains,
|
|
420
|
+
fetchOptions: options.fetchOptions,
|
|
421
|
+
maxAge: options.maxAge
|
|
408
422
|
});
|
|
409
423
|
}
|
|
410
424
|
return function ipx(id, modifiers = {}, reqOptions = {}) {
|
|
@@ -420,7 +434,7 @@ function createIPX(userOptions) {
|
|
|
420
434
|
const getSrc = cachedPromise(() => {
|
|
421
435
|
const source = ufo.hasProtocol(id) ? "http" : "filesystem";
|
|
422
436
|
if (!ctx.sources[source]) {
|
|
423
|
-
throw createError("Unknown source
|
|
437
|
+
throw createError("Unknown source", 400, source);
|
|
424
438
|
}
|
|
425
439
|
return ctx.sources[source](id, reqOptions);
|
|
426
440
|
});
|
|
@@ -440,10 +454,7 @@ function createIPX(userOptions) {
|
|
|
440
454
|
meta
|
|
441
455
|
};
|
|
442
456
|
}
|
|
443
|
-
const animated = modifiers.animated !== void 0 || modifiers.a !== void 0;
|
|
444
|
-
if (animated) {
|
|
445
|
-
format = "webp";
|
|
446
|
-
}
|
|
457
|
+
const animated = modifiers.animated !== void 0 || modifiers.a !== void 0 || format === "gif";
|
|
447
458
|
const Sharp = await import('sharp').then((r) => r.default || r);
|
|
448
459
|
let sharp = Sharp(data, { animated });
|
|
449
460
|
Object.assign(sharp.options, options.sharp);
|
|
@@ -476,6 +487,8 @@ function createIPX(userOptions) {
|
|
|
476
487
|
};
|
|
477
488
|
}
|
|
478
489
|
|
|
490
|
+
const MODIFIER_SEP = /[,&]/g;
|
|
491
|
+
const MODIFIER_VAL_SEP = /[_=:]/g;
|
|
479
492
|
async function _handleRequest(req, ipx) {
|
|
480
493
|
const res = {
|
|
481
494
|
statusCode: 200,
|
|
@@ -483,19 +496,19 @@ async function _handleRequest(req, ipx) {
|
|
|
483
496
|
headers: {},
|
|
484
497
|
body: ""
|
|
485
498
|
};
|
|
486
|
-
const [modifiersStr = "", ...idSegments] = req.url.
|
|
487
|
-
const id = ufo.decode(idSegments.join("/"));
|
|
499
|
+
const [modifiersStr = "", ...idSegments] = req.url.substring(1).split("/");
|
|
500
|
+
const id = safeString(ufo.decode(idSegments.join("/")));
|
|
488
501
|
if (!modifiersStr) {
|
|
489
|
-
throw createError("Modifiers
|
|
502
|
+
throw createError("Modifiers are missing", 400, req.url);
|
|
490
503
|
}
|
|
491
504
|
if (!id || id === "/") {
|
|
492
|
-
throw createError("Resource id is missing
|
|
505
|
+
throw createError("Resource id is missing", 400, req.url);
|
|
493
506
|
}
|
|
494
507
|
const modifiers = /* @__PURE__ */ Object.create(null);
|
|
495
508
|
if (modifiersStr !== "_") {
|
|
496
|
-
for (const p of modifiersStr.split(
|
|
497
|
-
const [key, value = ""] = p.split(
|
|
498
|
-
modifiers[key] = ufo.decode(value);
|
|
509
|
+
for (const p of modifiersStr.split(MODIFIER_SEP)) {
|
|
510
|
+
const [key, value = ""] = p.split(MODIFIER_VAL_SEP);
|
|
511
|
+
modifiers[safeString(key)] = safeString(ufo.decode(value));
|
|
499
512
|
}
|
|
500
513
|
}
|
|
501
514
|
const img = ipx(id, modifiers, req.options);
|
|
@@ -509,7 +522,7 @@ async function _handleRequest(req, ipx) {
|
|
|
509
522
|
}
|
|
510
523
|
res.headers["Last-Modified"] = +src.mtime + "";
|
|
511
524
|
}
|
|
512
|
-
if (src.maxAge
|
|
525
|
+
if (typeof src.maxAge === "number") {
|
|
513
526
|
res.headers["Cache-Control"] = `max-age=${+src.maxAge}, public, s-maxage=${+src.maxAge}`;
|
|
514
527
|
}
|
|
515
528
|
const { data, format } = await img.data();
|
|
@@ -523,21 +536,21 @@ async function _handleRequest(req, ipx) {
|
|
|
523
536
|
res.headers["Content-Type"] = `image/${format}`;
|
|
524
537
|
}
|
|
525
538
|
res.body = data;
|
|
526
|
-
return res;
|
|
539
|
+
return sanetizeReponse(res);
|
|
527
540
|
}
|
|
528
541
|
function handleRequest(req, ipx) {
|
|
529
542
|
return _handleRequest(req, ipx).catch((err) => {
|
|
530
543
|
const statusCode = parseInt(err.statusCode) || 500;
|
|
531
|
-
const statusMessage = err.statusMessage ?
|
|
544
|
+
const statusMessage = err.statusMessage ? err.statusMessage : `IPX Error (${statusCode})`;
|
|
532
545
|
if (process.env.NODE_ENV !== "production" && statusCode === 500) {
|
|
533
546
|
console.error(err);
|
|
534
547
|
}
|
|
535
|
-
return {
|
|
548
|
+
return sanetizeReponse({
|
|
536
549
|
statusCode,
|
|
537
550
|
statusMessage,
|
|
538
|
-
body:
|
|
551
|
+
body: "IPX Error: " + err,
|
|
539
552
|
headers: {}
|
|
540
|
-
};
|
|
553
|
+
});
|
|
541
554
|
});
|
|
542
555
|
}
|
|
543
556
|
function createIPXMiddleware(ipx) {
|
|
@@ -552,6 +565,24 @@ function createIPXMiddleware(ipx) {
|
|
|
552
565
|
});
|
|
553
566
|
};
|
|
554
567
|
}
|
|
568
|
+
function sanetizeReponse(res) {
|
|
569
|
+
return {
|
|
570
|
+
statusCode: res.statusCode || 200,
|
|
571
|
+
statusMessage: res.statusMessage ? safeString(res.statusMessage) : "OK",
|
|
572
|
+
headers: safeStringObject(res.headers || {}),
|
|
573
|
+
body: typeof res.body === "string" ? xss__default(safeString(res.body)) : res.body || ""
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function safeString(input) {
|
|
577
|
+
return JSON.stringify(input).replace(/^"|"$/g, "");
|
|
578
|
+
}
|
|
579
|
+
function safeStringObject(input) {
|
|
580
|
+
const dst = {};
|
|
581
|
+
for (const key in input) {
|
|
582
|
+
dst[key] = safeString(input[key]);
|
|
583
|
+
}
|
|
584
|
+
return dst;
|
|
585
|
+
}
|
|
555
586
|
|
|
556
587
|
exports.createIPX = createIPX;
|
|
557
588
|
exports.createIPXMiddleware = createIPXMiddleware;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import defu from 'defu';
|
|
2
2
|
import { imageMeta } from 'image-meta';
|
|
3
|
-
import {
|
|
3
|
+
import { withLeadingSlash, hasProtocol, joinURL, decode } from 'ufo';
|
|
4
4
|
import { promises } from 'fs';
|
|
5
|
-
import { resolve, join } from 'pathe';
|
|
6
|
-
import isValidPath from 'is-valid-path';
|
|
5
|
+
import { resolve, join, parse } from 'pathe';
|
|
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,53 +72,64 @@ 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
|
+
const isWindows = process.platform === "win32";
|
|
99
|
+
function isValidPath(fp) {
|
|
100
|
+
if (isWindows) {
|
|
101
|
+
fp = fp.slice(parse(fp).root.length);
|
|
102
|
+
}
|
|
103
|
+
if (/[<>:"|?*]/.test(fp)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
99
108
|
|
|
100
109
|
const createHTTPSource = (options) => {
|
|
101
110
|
const httpsAgent = new https.Agent({ keepAlive: true });
|
|
102
111
|
const httpAgent = new http.Agent({ keepAlive: true });
|
|
103
|
-
let
|
|
104
|
-
if (typeof
|
|
105
|
-
|
|
112
|
+
let _domains = options.domains || [];
|
|
113
|
+
if (typeof _domains === "string") {
|
|
114
|
+
_domains = _domains.split(",").map((s) => s.trim());
|
|
106
115
|
}
|
|
107
|
-
const
|
|
116
|
+
const domains = _domains.map((d) => new URL(d).hostname || new URL("http://" + d).hostname).filter(Boolean);
|
|
108
117
|
return async (id, reqOptions) => {
|
|
109
|
-
const
|
|
110
|
-
if (!
|
|
111
|
-
throw createError("Hostname is missing
|
|
118
|
+
const hostname = new URL(id).hostname;
|
|
119
|
+
if (!hostname) {
|
|
120
|
+
throw createError("Hostname is missing", 403, id);
|
|
112
121
|
}
|
|
113
|
-
if (!reqOptions?.bypassDomain && !
|
|
114
|
-
throw createError("Forbidden host
|
|
122
|
+
if (!reqOptions?.bypassDomain && !domains.find((domain) => hostname === domain)) {
|
|
123
|
+
throw createError("Forbidden host", 403, hostname);
|
|
115
124
|
}
|
|
116
125
|
const response = await fetch(id, {
|
|
117
|
-
agent: id.startsWith("https") ? httpsAgent : httpAgent
|
|
126
|
+
agent: id.startsWith("https") ? httpsAgent : httpAgent,
|
|
127
|
+
...options.fetchOptions
|
|
118
128
|
});
|
|
119
129
|
if (!response.ok) {
|
|
120
|
-
throw createError(
|
|
130
|
+
throw createError("Fetch error", response.status || 500, response.statusText);
|
|
121
131
|
}
|
|
122
|
-
let maxAge = options.maxAge
|
|
132
|
+
let maxAge = options.maxAge;
|
|
123
133
|
const _cacheControl = response.headers.get("cache-control");
|
|
124
134
|
if (_cacheControl) {
|
|
125
135
|
const m = _cacheControl.match(/max-age=(\d+)/);
|
|
@@ -135,7 +145,7 @@ const createHTTPSource = (options) => {
|
|
|
135
145
|
return {
|
|
136
146
|
mtime,
|
|
137
147
|
maxAge,
|
|
138
|
-
getData: cachedPromise(() => response.
|
|
148
|
+
getData: cachedPromise(() => response.arrayBuffer().then((ab) => Buffer.from(ab)))
|
|
139
149
|
};
|
|
140
150
|
};
|
|
141
151
|
};
|
|
@@ -372,12 +382,14 @@ const h = height;
|
|
|
372
382
|
const s = resize;
|
|
373
383
|
const pos = position;
|
|
374
384
|
|
|
375
|
-
const SUPPORTED_FORMATS = ["jpeg", "png", "webp", "avif", "tiff"];
|
|
385
|
+
const SUPPORTED_FORMATS = ["jpeg", "png", "webp", "avif", "tiff", "gif"];
|
|
376
386
|
function createIPX(userOptions) {
|
|
377
387
|
const defaults = {
|
|
378
388
|
dir: getEnv("IPX_DIR", "."),
|
|
379
389
|
domains: getEnv("IPX_DOMAINS", []),
|
|
380
390
|
alias: getEnv("IPX_ALIAS", {}),
|
|
391
|
+
fetchOptions: getEnv("IPX_FETCH_OPTIONS", {}),
|
|
392
|
+
maxAge: getEnv("IPX_MAX_AGE", 300),
|
|
381
393
|
sharp: {}
|
|
382
394
|
};
|
|
383
395
|
const options = defu(userOptions, defaults);
|
|
@@ -387,12 +399,15 @@ function createIPX(userOptions) {
|
|
|
387
399
|
};
|
|
388
400
|
if (options.dir) {
|
|
389
401
|
ctx.sources.filesystem = createFilesystemSource({
|
|
390
|
-
dir: options.dir
|
|
402
|
+
dir: options.dir,
|
|
403
|
+
maxAge: options.maxAge
|
|
391
404
|
});
|
|
392
405
|
}
|
|
393
406
|
if (options.domains) {
|
|
394
407
|
ctx.sources.http = createHTTPSource({
|
|
395
|
-
domains: options.domains
|
|
408
|
+
domains: options.domains,
|
|
409
|
+
fetchOptions: options.fetchOptions,
|
|
410
|
+
maxAge: options.maxAge
|
|
396
411
|
});
|
|
397
412
|
}
|
|
398
413
|
return function ipx(id, modifiers = {}, reqOptions = {}) {
|
|
@@ -408,7 +423,7 @@ function createIPX(userOptions) {
|
|
|
408
423
|
const getSrc = cachedPromise(() => {
|
|
409
424
|
const source = hasProtocol(id) ? "http" : "filesystem";
|
|
410
425
|
if (!ctx.sources[source]) {
|
|
411
|
-
throw createError("Unknown source
|
|
426
|
+
throw createError("Unknown source", 400, source);
|
|
412
427
|
}
|
|
413
428
|
return ctx.sources[source](id, reqOptions);
|
|
414
429
|
});
|
|
@@ -428,10 +443,7 @@ function createIPX(userOptions) {
|
|
|
428
443
|
meta
|
|
429
444
|
};
|
|
430
445
|
}
|
|
431
|
-
const animated = modifiers.animated !== void 0 || modifiers.a !== void 0;
|
|
432
|
-
if (animated) {
|
|
433
|
-
format = "webp";
|
|
434
|
-
}
|
|
446
|
+
const animated = modifiers.animated !== void 0 || modifiers.a !== void 0 || format === "gif";
|
|
435
447
|
const Sharp = await import('sharp').then((r) => r.default || r);
|
|
436
448
|
let sharp = Sharp(data, { animated });
|
|
437
449
|
Object.assign(sharp.options, options.sharp);
|
|
@@ -464,6 +476,8 @@ function createIPX(userOptions) {
|
|
|
464
476
|
};
|
|
465
477
|
}
|
|
466
478
|
|
|
479
|
+
const MODIFIER_SEP = /[,&]/g;
|
|
480
|
+
const MODIFIER_VAL_SEP = /[_=:]/g;
|
|
467
481
|
async function _handleRequest(req, ipx) {
|
|
468
482
|
const res = {
|
|
469
483
|
statusCode: 200,
|
|
@@ -471,19 +485,19 @@ async function _handleRequest(req, ipx) {
|
|
|
471
485
|
headers: {},
|
|
472
486
|
body: ""
|
|
473
487
|
};
|
|
474
|
-
const [modifiersStr = "", ...idSegments] = req.url.
|
|
475
|
-
const id = decode(idSegments.join("/"));
|
|
488
|
+
const [modifiersStr = "", ...idSegments] = req.url.substring(1).split("/");
|
|
489
|
+
const id = safeString(decode(idSegments.join("/")));
|
|
476
490
|
if (!modifiersStr) {
|
|
477
|
-
throw createError("Modifiers
|
|
491
|
+
throw createError("Modifiers are missing", 400, req.url);
|
|
478
492
|
}
|
|
479
493
|
if (!id || id === "/") {
|
|
480
|
-
throw createError("Resource id is missing
|
|
494
|
+
throw createError("Resource id is missing", 400, req.url);
|
|
481
495
|
}
|
|
482
496
|
const modifiers = /* @__PURE__ */ Object.create(null);
|
|
483
497
|
if (modifiersStr !== "_") {
|
|
484
|
-
for (const p of modifiersStr.split(
|
|
485
|
-
const [key, value = ""] = p.split(
|
|
486
|
-
modifiers[key] = decode(value);
|
|
498
|
+
for (const p of modifiersStr.split(MODIFIER_SEP)) {
|
|
499
|
+
const [key, value = ""] = p.split(MODIFIER_VAL_SEP);
|
|
500
|
+
modifiers[safeString(key)] = safeString(decode(value));
|
|
487
501
|
}
|
|
488
502
|
}
|
|
489
503
|
const img = ipx(id, modifiers, req.options);
|
|
@@ -497,7 +511,7 @@ async function _handleRequest(req, ipx) {
|
|
|
497
511
|
}
|
|
498
512
|
res.headers["Last-Modified"] = +src.mtime + "";
|
|
499
513
|
}
|
|
500
|
-
if (src.maxAge
|
|
514
|
+
if (typeof src.maxAge === "number") {
|
|
501
515
|
res.headers["Cache-Control"] = `max-age=${+src.maxAge}, public, s-maxage=${+src.maxAge}`;
|
|
502
516
|
}
|
|
503
517
|
const { data, format } = await img.data();
|
|
@@ -511,21 +525,21 @@ async function _handleRequest(req, ipx) {
|
|
|
511
525
|
res.headers["Content-Type"] = `image/${format}`;
|
|
512
526
|
}
|
|
513
527
|
res.body = data;
|
|
514
|
-
return res;
|
|
528
|
+
return sanetizeReponse(res);
|
|
515
529
|
}
|
|
516
530
|
function handleRequest(req, ipx) {
|
|
517
531
|
return _handleRequest(req, ipx).catch((err) => {
|
|
518
532
|
const statusCode = parseInt(err.statusCode) || 500;
|
|
519
|
-
const statusMessage = err.statusMessage ?
|
|
533
|
+
const statusMessage = err.statusMessage ? err.statusMessage : `IPX Error (${statusCode})`;
|
|
520
534
|
if (process.env.NODE_ENV !== "production" && statusCode === 500) {
|
|
521
535
|
console.error(err);
|
|
522
536
|
}
|
|
523
|
-
return {
|
|
537
|
+
return sanetizeReponse({
|
|
524
538
|
statusCode,
|
|
525
539
|
statusMessage,
|
|
526
|
-
body:
|
|
540
|
+
body: "IPX Error: " + err,
|
|
527
541
|
headers: {}
|
|
528
|
-
};
|
|
542
|
+
});
|
|
529
543
|
});
|
|
530
544
|
}
|
|
531
545
|
function createIPXMiddleware(ipx) {
|
|
@@ -540,5 +554,23 @@ function createIPXMiddleware(ipx) {
|
|
|
540
554
|
});
|
|
541
555
|
};
|
|
542
556
|
}
|
|
557
|
+
function sanetizeReponse(res) {
|
|
558
|
+
return {
|
|
559
|
+
statusCode: res.statusCode || 200,
|
|
560
|
+
statusMessage: res.statusMessage ? safeString(res.statusMessage) : "OK",
|
|
561
|
+
headers: safeStringObject(res.headers || {}),
|
|
562
|
+
body: typeof res.body === "string" ? xss(safeString(res.body)) : res.body || ""
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
function safeString(input) {
|
|
566
|
+
return JSON.stringify(input).replace(/^"|"$/g, "");
|
|
567
|
+
}
|
|
568
|
+
function safeStringObject(input) {
|
|
569
|
+
const dst = {};
|
|
570
|
+
for (const key in input) {
|
|
571
|
+
dst[key] = safeString(input[key]);
|
|
572
|
+
}
|
|
573
|
+
return dst;
|
|
574
|
+
}
|
|
543
575
|
|
|
544
576
|
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.8",
|
|
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.7",
|
|
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
|
+
}
|