ipx 4.0.0-alpha.2 → 4.0.0-beta.1
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/README.md +492 -77
- package/dist/THIRD-PARTY-LICENSES.md +54 -0
- package/dist/_chunks/libs/@fastify/accept-negotiator.mjs +1 -1
- package/dist/_chunks/libs/crossws.d.mts +1 -0
- package/dist/_chunks/libs/css-select.mjs +2 -2
- package/dist/_chunks/libs/css-tree.mjs +68 -106
- package/dist/_chunks/libs/csso.mjs +16 -29
- package/dist/_chunks/libs/etag.mjs +1 -1
- package/dist/_chunks/libs/h3.d.mts +3 -0
- package/dist/_chunks/libs/h3.mjs +114 -72
- package/dist/_chunks/libs/image-meta.d.mts +0 -4
- package/dist/_chunks/libs/image-meta.mjs +6 -6
- package/dist/_chunks/libs/sax.mjs +21 -20
- package/dist/_chunks/libs/svgo.mjs +788 -756
- package/dist/_chunks/libs/ufo.mjs +17 -24
- package/dist/_chunks/node-fs.mjs +982 -158
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +184 -16
- package/dist/index.mjs +2 -2
- package/package.json +20 -17
package/dist/_chunks/node-fs.mjs
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { __exportAll, __toESM } from "./rolldown-runtime.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { hasProtocol, joinURL, withLeadingSlash } from "./libs/ufo.mjs";
|
|
3
3
|
import { HTTPError, defineEventHandler } from "./libs/h3.mjs";
|
|
4
4
|
import { imageMeta } from "./libs/image-meta.mjs";
|
|
5
5
|
import { require_etag } from "./libs/etag.mjs";
|
|
6
6
|
import { require_accept_negotiator } from "./libs/@fastify/accept-negotiator.mjs";
|
|
7
|
-
import { join, parse, resolve } from "pathe";
|
|
8
7
|
function VArg(argument) {
|
|
9
8
|
if (argument === "Infinity") return Infinity;
|
|
10
9
|
if (argument === "undefined") return;
|
|
@@ -15,6 +14,78 @@ function VArg(argument) {
|
|
|
15
14
|
} catch {}
|
|
16
15
|
return argument;
|
|
17
16
|
}
|
|
17
|
+
function invalidArg(name, argument, expected) {
|
|
18
|
+
throw new HTTPError({
|
|
19
|
+
statusCode: 400,
|
|
20
|
+
statusText: "IPX_INVALID_MODIFIER_ARG",
|
|
21
|
+
message: `Invalid \`${name}\` modifier argument: \`${argument}\` (expected ${expected})`
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function isOmitted(argument) {
|
|
25
|
+
return argument === void 0 || argument === "" || argument === "undefined";
|
|
26
|
+
}
|
|
27
|
+
function VNumber(name, options = {}) {
|
|
28
|
+
const { min, max, integer } = options;
|
|
29
|
+
const expected = [integer ? "an integer" : "a number", min !== void 0 && max !== void 0 ? `between ${min} and ${max}` : min !== void 0 && `greater than or equal to ${min}` || max !== void 0 && `less than or equal to ${max}` || ""].filter(Boolean).join(" ");
|
|
30
|
+
return (argument) => {
|
|
31
|
+
if (isOmitted(argument)) return;
|
|
32
|
+
const value = VArg(argument);
|
|
33
|
+
if (typeof value === "number" && Number.isFinite(value) && (!integer || Number.isInteger(value)) && (min === void 0 || value >= min) && (max === void 0 || value <= max)) return value;
|
|
34
|
+
invalidArg(name, argument, expected);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function VEnum(name, values) {
|
|
38
|
+
const expected = `one of: ${values.join(", ")}`;
|
|
39
|
+
return (argument) => {
|
|
40
|
+
if (isOmitted(argument)) return;
|
|
41
|
+
if (values.includes(argument)) return argument;
|
|
42
|
+
invalidArg(name, argument, expected);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function VBoolean(name) {
|
|
46
|
+
return (argument) => {
|
|
47
|
+
if (isOmitted(argument)) return;
|
|
48
|
+
if (argument === "true" || argument === "1") return true;
|
|
49
|
+
if (argument === "false" || argument === "0") return false;
|
|
50
|
+
invalidArg(name, argument, "`true` or `false`");
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const HEX_COLOR_RE = /^#?(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i;
|
|
54
|
+
const FN_COLOR_RE = /^(?:rgba?|hsla?|hwb)\([\d\s%,./-]+\)$/i;
|
|
55
|
+
const NAMED_COLOR_RE = /^[a-z]{3,20}$/i;
|
|
56
|
+
function VColor(name) {
|
|
57
|
+
return (argument) => {
|
|
58
|
+
if (isOmitted(argument)) return;
|
|
59
|
+
if (HEX_COLOR_RE.test(argument)) return argument.startsWith("#") ? argument : `#${argument}`;
|
|
60
|
+
if (FN_COLOR_RE.test(argument) || NAMED_COLOR_RE.test(argument)) return argument;
|
|
61
|
+
invalidArg(name, argument, "a hex (`f00`, `ff0000`) or named (`red`) colour");
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const SIZE_RE = /^(\d+)(?:x(\d+))?$/;
|
|
65
|
+
function VSize(name) {
|
|
66
|
+
return (argument) => {
|
|
67
|
+
if (isOmitted(argument)) return;
|
|
68
|
+
const match = SIZE_RE.exec(argument);
|
|
69
|
+
const width = Number(match?.[1]);
|
|
70
|
+
const height = match?.[2] === void 0 ? width : Number(match[2]);
|
|
71
|
+
if (match && width > 0 && height > 0) return {
|
|
72
|
+
width,
|
|
73
|
+
height
|
|
74
|
+
};
|
|
75
|
+
invalidArg(name, argument, "`{width}x{height}` of positive integers");
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function VRequired(name, mapper) {
|
|
79
|
+
return (argument) => {
|
|
80
|
+
const value = mapper(argument);
|
|
81
|
+
if (value === void 0) throw new HTTPError({
|
|
82
|
+
statusCode: 400,
|
|
83
|
+
statusText: "IPX_MISSING_MODIFIER_ARG",
|
|
84
|
+
message: `Missing \`${name}\` modifier argument`
|
|
85
|
+
});
|
|
86
|
+
return value;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
18
89
|
function parseArgs(arguments_, mappers) {
|
|
19
90
|
const vargs = arguments_.split("_");
|
|
20
91
|
return mappers.map((v, index) => v(vargs[index]));
|
|
@@ -24,7 +95,97 @@ function getHandler(key) {
|
|
|
24
95
|
}
|
|
25
96
|
function applyHandler(context, pipe, handler, argumentsString) {
|
|
26
97
|
const arguments_ = handler.args ? parseArgs(argumentsString, handler.args) : [];
|
|
27
|
-
|
|
98
|
+
try {
|
|
99
|
+
return handler.apply(context, pipe, ...arguments_);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw asModifierError(error);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function asModifierError(error) {
|
|
105
|
+
if (HTTPError.isError(error)) return error;
|
|
106
|
+
return new HTTPError({
|
|
107
|
+
statusCode: 400,
|
|
108
|
+
statusText: "IPX_INVALID_MODIFIER",
|
|
109
|
+
message: `Cannot apply modifier: ${error?.message || error}`,
|
|
110
|
+
cause: error
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
114
|
+
function opacityOverlay(opacity, background) {
|
|
115
|
+
return background ? {
|
|
116
|
+
input: Buffer.from(`<svg xmlns="${SVG_NS}" width="1" height="1"><rect width="1" height="1" fill="${background}" fill-opacity="${1 - opacity}"/></svg>`),
|
|
117
|
+
tile: true,
|
|
118
|
+
blend: "over"
|
|
119
|
+
} : {
|
|
120
|
+
input: { create: {
|
|
121
|
+
width: 1,
|
|
122
|
+
height: 1,
|
|
123
|
+
channels: 4,
|
|
124
|
+
background: {
|
|
125
|
+
r: 0,
|
|
126
|
+
g: 0,
|
|
127
|
+
b: 0,
|
|
128
|
+
alpha: opacity
|
|
129
|
+
}
|
|
130
|
+
} },
|
|
131
|
+
tile: true,
|
|
132
|
+
blend: "dest-in"
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function clampToMaxDimension(max, desired, source) {
|
|
136
|
+
const { width, height } = desired;
|
|
137
|
+
if (!max || width === void 0 && height === void 0) return desired;
|
|
138
|
+
const ratio = source?.width && source?.height ? source.width / source.height : void 0;
|
|
139
|
+
const effectiveWidth = width ?? (ratio && height ? height * ratio : void 0);
|
|
140
|
+
const effectiveHeight = height ?? (ratio && width ? width / ratio : void 0);
|
|
141
|
+
const scale = Math.min(effectiveWidth ? max / effectiveWidth : 1, effectiveHeight ? max / effectiveHeight : 1);
|
|
142
|
+
if (scale >= 1) return desired;
|
|
143
|
+
const clamp = (value) => value === void 0 ? void 0 : Math.max(1, Math.min(max, Math.round(value * scale)));
|
|
144
|
+
return {
|
|
145
|
+
width: clamp(width),
|
|
146
|
+
height: clamp(height)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function resizeOutputDimensions(desired, source, options = {}) {
|
|
150
|
+
const { width, height } = desired;
|
|
151
|
+
if (width === void 0 && height === void 0) return;
|
|
152
|
+
const sourceWidth = source?.width;
|
|
153
|
+
const sourceHeight = source?.height;
|
|
154
|
+
if (!sourceWidth || !sourceHeight) return desired;
|
|
155
|
+
const derive = (value) => Math.max(1, Math.round(value));
|
|
156
|
+
let out = {
|
|
157
|
+
width: width ?? derive(height * sourceWidth / sourceHeight),
|
|
158
|
+
height: height ?? derive(width * sourceHeight / sourceWidth)
|
|
159
|
+
};
|
|
160
|
+
if (options.fit === "outside" && width !== void 0 && height !== void 0) {
|
|
161
|
+
const scale = Math.max(width / sourceWidth, height / sourceHeight);
|
|
162
|
+
out = {
|
|
163
|
+
width: derive(sourceWidth * scale),
|
|
164
|
+
height: derive(sourceHeight * scale)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return options.withoutEnlargement ? {
|
|
168
|
+
width: Math.min(out.width, sourceWidth),
|
|
169
|
+
height: Math.min(out.height, sourceHeight)
|
|
170
|
+
} : out;
|
|
171
|
+
}
|
|
172
|
+
function clampExtendEdges(max, edges, source) {
|
|
173
|
+
if (!max) return edges;
|
|
174
|
+
const [top, bottom] = clampEdges(max - (source?.height || 0), edges.top, edges.bottom);
|
|
175
|
+
const [left, right] = clampEdges(max - (source?.width || 0), edges.left, edges.right);
|
|
176
|
+
return {
|
|
177
|
+
top,
|
|
178
|
+
right,
|
|
179
|
+
bottom,
|
|
180
|
+
left
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function clampEdges(available, a, b) {
|
|
184
|
+
const total = (a || 0) + (b || 0);
|
|
185
|
+
if (total === 0 || total <= available) return [a, b];
|
|
186
|
+
const scale = Math.max(0, available) / total;
|
|
187
|
+
const clamp = (value) => value === void 0 ? void 0 : Math.floor(value * scale);
|
|
188
|
+
return [clamp(a), clamp(b)];
|
|
28
189
|
}
|
|
29
190
|
function clampDimensionsPreservingAspectRatio(sourceDimensions, desiredDimensions) {
|
|
30
191
|
const desiredAspectRatio = desiredDimensions.width / desiredDimensions.height;
|
|
@@ -43,11 +204,16 @@ function clampDimensionsPreservingAspectRatio(sourceDimensions, desiredDimension
|
|
|
43
204
|
};
|
|
44
205
|
}
|
|
45
206
|
var handlers_exports = /* @__PURE__ */ __exportAll({
|
|
207
|
+
autoorient: () => autoorient,
|
|
46
208
|
b: () => b,
|
|
47
209
|
background: () => background,
|
|
48
210
|
blur: () => blur,
|
|
211
|
+
brightness: () => brightness,
|
|
212
|
+
clahe: () => clahe,
|
|
49
213
|
crop: () => crop,
|
|
214
|
+
dilate: () => dilate,
|
|
50
215
|
enlarge: () => enlarge,
|
|
216
|
+
erode: () => erode,
|
|
51
217
|
extend: () => extend,
|
|
52
218
|
extract: () => extract,
|
|
53
219
|
fit: () => fit,
|
|
@@ -58,11 +224,15 @@ var handlers_exports = /* @__PURE__ */ __exportAll({
|
|
|
58
224
|
grayscale: () => grayscale,
|
|
59
225
|
h: () => h,
|
|
60
226
|
height: () => height,
|
|
227
|
+
hue: () => hue,
|
|
61
228
|
kernel: () => kernel,
|
|
229
|
+
lightness: () => lightness,
|
|
230
|
+
linear: () => linear,
|
|
62
231
|
median: () => median,
|
|
63
232
|
modulate: () => modulate,
|
|
64
233
|
negate: () => negate,
|
|
65
234
|
normalize: () => normalize,
|
|
235
|
+
opacity: () => opacity,
|
|
66
236
|
pos: () => pos,
|
|
67
237
|
position: () => position,
|
|
68
238
|
q: () => q,
|
|
@@ -70,42 +240,92 @@ var handlers_exports = /* @__PURE__ */ __exportAll({
|
|
|
70
240
|
resize: () => resize,
|
|
71
241
|
rotate: () => rotate,
|
|
72
242
|
s: () => s,
|
|
243
|
+
saturation: () => saturation,
|
|
73
244
|
sharpen: () => sharpen,
|
|
74
245
|
threshold: () => threshold,
|
|
75
246
|
tint: () => tint,
|
|
76
247
|
trim: () => trim,
|
|
248
|
+
unflatten: () => unflatten,
|
|
77
249
|
w: () => w,
|
|
78
250
|
width: () => width
|
|
79
251
|
});
|
|
80
252
|
const quality = {
|
|
81
|
-
args: [
|
|
253
|
+
args: [VNumber("quality", {
|
|
254
|
+
min: 1,
|
|
255
|
+
max: 100,
|
|
256
|
+
integer: true
|
|
257
|
+
})],
|
|
82
258
|
order: -1,
|
|
83
259
|
apply: (context, _pipe, quality) => {
|
|
84
260
|
context.quality = quality;
|
|
85
261
|
}
|
|
86
262
|
};
|
|
87
263
|
const fit = {
|
|
88
|
-
args: [
|
|
264
|
+
args: [VEnum("fit", [
|
|
265
|
+
"contain",
|
|
266
|
+
"cover",
|
|
267
|
+
"fill",
|
|
268
|
+
"inside",
|
|
269
|
+
"outside"
|
|
270
|
+
])],
|
|
89
271
|
order: -1,
|
|
90
272
|
apply: (context, _pipe, fit) => {
|
|
91
273
|
context.fit = fit;
|
|
92
274
|
}
|
|
93
275
|
};
|
|
276
|
+
const POSITIONS = [
|
|
277
|
+
"top",
|
|
278
|
+
"right top",
|
|
279
|
+
"right",
|
|
280
|
+
"right bottom",
|
|
281
|
+
"bottom",
|
|
282
|
+
"left bottom",
|
|
283
|
+
"left",
|
|
284
|
+
"left top",
|
|
285
|
+
"north",
|
|
286
|
+
"northeast",
|
|
287
|
+
"east",
|
|
288
|
+
"southeast",
|
|
289
|
+
"south",
|
|
290
|
+
"southwest",
|
|
291
|
+
"west",
|
|
292
|
+
"northwest",
|
|
293
|
+
"center",
|
|
294
|
+
"centre",
|
|
295
|
+
"entropy",
|
|
296
|
+
"attention"
|
|
297
|
+
];
|
|
298
|
+
const GRAVITIES = /* @__PURE__ */ new Set([
|
|
299
|
+
0,
|
|
300
|
+
1,
|
|
301
|
+
2,
|
|
302
|
+
3,
|
|
303
|
+
4,
|
|
304
|
+
5,
|
|
305
|
+
6,
|
|
306
|
+
7,
|
|
307
|
+
8,
|
|
308
|
+
16,
|
|
309
|
+
17
|
|
310
|
+
]);
|
|
311
|
+
const VPosition = (name) => {
|
|
312
|
+
const vEnum = VEnum(name, POSITIONS);
|
|
313
|
+
return (argument) => {
|
|
314
|
+
if (/^\d+$/.test(argument) && GRAVITIES.has(Number(argument))) return Number(argument);
|
|
315
|
+
return vEnum(argument?.replace("-", " "));
|
|
316
|
+
};
|
|
317
|
+
};
|
|
94
318
|
const position = {
|
|
95
|
-
args: [
|
|
319
|
+
args: [VPosition("position")],
|
|
96
320
|
order: -1,
|
|
97
321
|
apply: (context, _pipe, position) => {
|
|
98
322
|
context.position = position;
|
|
99
323
|
}
|
|
100
324
|
};
|
|
101
|
-
const HEX_RE = /^([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i;
|
|
102
|
-
const SHORTHEX_RE = /^([\da-f])([\da-f])([\da-f])$/i;
|
|
103
325
|
const background = {
|
|
104
|
-
args: [
|
|
326
|
+
args: [VColor("background")],
|
|
105
327
|
order: -1,
|
|
106
328
|
apply: (context, _pipe, background) => {
|
|
107
|
-
background = String(background);
|
|
108
|
-
if (!background.startsWith("#") && (HEX_RE.test(background) || SHORTHEX_RE.test(background))) background = "#" + background;
|
|
109
329
|
context.background = background;
|
|
110
330
|
}
|
|
111
331
|
};
|
|
@@ -116,33 +336,49 @@ const enlarge = {
|
|
|
116
336
|
}
|
|
117
337
|
};
|
|
118
338
|
const kernel = {
|
|
119
|
-
args: [
|
|
339
|
+
args: [VEnum("kernel", [
|
|
340
|
+
"nearest",
|
|
341
|
+
"linear",
|
|
342
|
+
"cubic",
|
|
343
|
+
"mitchell",
|
|
344
|
+
"lanczos2",
|
|
345
|
+
"lanczos3",
|
|
346
|
+
"mks2013",
|
|
347
|
+
"mks2021"
|
|
348
|
+
])],
|
|
120
349
|
apply: (context, _pipe, kernel) => {
|
|
121
350
|
context.kernel = kernel;
|
|
122
351
|
}
|
|
123
352
|
};
|
|
124
353
|
const width = {
|
|
125
|
-
args: [
|
|
354
|
+
args: [VNumber("width", {
|
|
355
|
+
min: 1,
|
|
356
|
+
integer: true
|
|
357
|
+
})],
|
|
126
358
|
apply: (context, pipe, width) => {
|
|
127
|
-
|
|
359
|
+
const clamped = clampToMaxDimension(context.maxOutputDimension, { width }, context.meta);
|
|
360
|
+
const withoutEnlargement = !context.enlarge;
|
|
361
|
+
context.outputDimensions = resizeOutputDimensions(clamped, context.meta, { withoutEnlargement });
|
|
362
|
+
return pipe.resize(clamped.width, void 0, { withoutEnlargement });
|
|
128
363
|
}
|
|
129
364
|
};
|
|
130
365
|
const height = {
|
|
131
|
-
args: [
|
|
366
|
+
args: [VNumber("height", {
|
|
367
|
+
min: 1,
|
|
368
|
+
integer: true
|
|
369
|
+
})],
|
|
132
370
|
apply: (context, pipe, height) => {
|
|
133
|
-
|
|
371
|
+
const clamped = clampToMaxDimension(context.maxOutputDimension, { height }, context.meta);
|
|
372
|
+
const withoutEnlargement = !context.enlarge;
|
|
373
|
+
context.outputDimensions = resizeOutputDimensions(clamped, context.meta, { withoutEnlargement });
|
|
374
|
+
return pipe.resize(void 0, clamped.height, { withoutEnlargement });
|
|
134
375
|
}
|
|
135
376
|
};
|
|
136
377
|
const resize = {
|
|
137
|
-
args: [
|
|
138
|
-
VArg,
|
|
139
|
-
VArg,
|
|
140
|
-
VArg
|
|
141
|
-
],
|
|
378
|
+
args: [VSize("resize")],
|
|
142
379
|
apply: (context, pipe, size) => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (!height) height = width;
|
|
380
|
+
if (!size) return;
|
|
381
|
+
let { width, height } = size;
|
|
146
382
|
if (!context.enlarge) {
|
|
147
383
|
const clamped = clampDimensionsPreservingAspectRatio(context.meta, {
|
|
148
384
|
width,
|
|
@@ -151,7 +387,12 @@ const resize = {
|
|
|
151
387
|
width = clamped.width;
|
|
152
388
|
height = clamped.height;
|
|
153
389
|
}
|
|
154
|
-
|
|
390
|
+
const capped = clampToMaxDimension(context.maxOutputDimension, {
|
|
391
|
+
width,
|
|
392
|
+
height
|
|
393
|
+
});
|
|
394
|
+
context.outputDimensions = resizeOutputDimensions(capped, context.meta, { fit: context.fit });
|
|
395
|
+
return pipe.resize(capped.width, capped.height, {
|
|
155
396
|
fit: context.fit,
|
|
156
397
|
position: context.position,
|
|
157
398
|
background: context.background,
|
|
@@ -160,34 +401,71 @@ const resize = {
|
|
|
160
401
|
}
|
|
161
402
|
};
|
|
162
403
|
const trim = {
|
|
163
|
-
args: [
|
|
404
|
+
args: [VNumber("trim", { min: 0 })],
|
|
164
405
|
apply: (_context, pipe, threshold) => {
|
|
165
|
-
return pipe.trim(threshold);
|
|
406
|
+
return pipe.trim({ threshold });
|
|
166
407
|
}
|
|
167
408
|
};
|
|
409
|
+
const EXTEND_WITH = [
|
|
410
|
+
"background",
|
|
411
|
+
"copy",
|
|
412
|
+
"repeat",
|
|
413
|
+
"mirror"
|
|
414
|
+
];
|
|
415
|
+
const VExtendEdge = (name) => VNumber(name, {
|
|
416
|
+
min: 0,
|
|
417
|
+
max: 1e4,
|
|
418
|
+
integer: true
|
|
419
|
+
});
|
|
168
420
|
const extend = {
|
|
169
421
|
args: [
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
422
|
+
VExtendEdge("extend.top"),
|
|
423
|
+
VExtendEdge("extend.right"),
|
|
424
|
+
VExtendEdge("extend.bottom"),
|
|
425
|
+
VExtendEdge("extend.left"),
|
|
426
|
+
VEnum("extend.extendWith", EXTEND_WITH)
|
|
174
427
|
],
|
|
175
|
-
|
|
176
|
-
|
|
428
|
+
order: 1,
|
|
429
|
+
apply: (context, pipe, top, right, bottom, left, extendWith) => {
|
|
430
|
+
const edges = clampExtendEdges(context.maxOutputDimension, {
|
|
177
431
|
top,
|
|
178
|
-
left,
|
|
179
|
-
bottom,
|
|
180
432
|
right,
|
|
181
|
-
|
|
433
|
+
bottom,
|
|
434
|
+
left
|
|
435
|
+
}, context.outputDimensions ?? context.meta);
|
|
436
|
+
return pipe.extend({
|
|
437
|
+
top: edges.top,
|
|
438
|
+
left: edges.left,
|
|
439
|
+
bottom: edges.bottom,
|
|
440
|
+
right: edges.right,
|
|
441
|
+
background: context.background,
|
|
442
|
+
extendWith
|
|
182
443
|
});
|
|
183
444
|
}
|
|
184
445
|
};
|
|
446
|
+
const MAX_EXTRACT = 1e8;
|
|
185
447
|
const extract = {
|
|
186
448
|
args: [
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
449
|
+
VRequired("extract.left", VNumber("extract.left", {
|
|
450
|
+
min: 0,
|
|
451
|
+
max: MAX_EXTRACT,
|
|
452
|
+
integer: true
|
|
453
|
+
})),
|
|
454
|
+
VRequired("extract.top", VNumber("extract.top", {
|
|
455
|
+
min: 0,
|
|
456
|
+
max: MAX_EXTRACT,
|
|
457
|
+
integer: true
|
|
458
|
+
})),
|
|
459
|
+
VRequired("extract.width", VNumber("extract.width", {
|
|
460
|
+
min: 1,
|
|
461
|
+
max: MAX_EXTRACT,
|
|
462
|
+
integer: true
|
|
463
|
+
})),
|
|
464
|
+
VRequired("extract.height", VNumber("extract.height", {
|
|
465
|
+
min: 1,
|
|
466
|
+
max: MAX_EXTRACT,
|
|
467
|
+
integer: true
|
|
468
|
+
}))
|
|
191
469
|
],
|
|
192
470
|
apply: (_context, pipe, left, top, width, height) => {
|
|
193
471
|
return pipe.extract({
|
|
@@ -199,9 +477,18 @@ const extract = {
|
|
|
199
477
|
}
|
|
200
478
|
};
|
|
201
479
|
const rotate = {
|
|
202
|
-
args: [
|
|
203
|
-
|
|
204
|
-
|
|
480
|
+
args: [VNumber("rotate", {
|
|
481
|
+
min: -3600,
|
|
482
|
+
max: 3600
|
|
483
|
+
})],
|
|
484
|
+
apply: (context, pipe, angle) => {
|
|
485
|
+
return pipe.rotate(angle, { background: context.background });
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
const autoorient = {
|
|
489
|
+
args: [],
|
|
490
|
+
apply: (_context, pipe) => {
|
|
491
|
+
return pipe.autoOrient();
|
|
205
492
|
}
|
|
206
493
|
};
|
|
207
494
|
const flip = {
|
|
@@ -218,102 +505,243 @@ const flop = {
|
|
|
218
505
|
};
|
|
219
506
|
const sharpen = {
|
|
220
507
|
args: [
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
508
|
+
VNumber("sharpen.sigma", {
|
|
509
|
+
min: 1e-6,
|
|
510
|
+
max: 10
|
|
511
|
+
}),
|
|
512
|
+
VNumber("sharpen.flat", {
|
|
513
|
+
min: 0,
|
|
514
|
+
max: 1e6
|
|
515
|
+
}),
|
|
516
|
+
VNumber("sharpen.jagged", {
|
|
517
|
+
min: 0,
|
|
518
|
+
max: 1e6
|
|
519
|
+
}),
|
|
520
|
+
VNumber("sharpen.x1", {
|
|
521
|
+
min: 0,
|
|
522
|
+
max: 1e6
|
|
523
|
+
}),
|
|
524
|
+
VNumber("sharpen.y2", {
|
|
525
|
+
min: 0,
|
|
526
|
+
max: 1e6
|
|
527
|
+
}),
|
|
528
|
+
VNumber("sharpen.y3", {
|
|
529
|
+
min: 0,
|
|
530
|
+
max: 1e6
|
|
531
|
+
})
|
|
224
532
|
],
|
|
225
|
-
apply: (_context, pipe, sigma, flat, jagged) => {
|
|
226
|
-
return pipe.sharpen({
|
|
533
|
+
apply: (_context, pipe, sigma, flat, jagged, x1, y2, y3) => {
|
|
534
|
+
return sigma === void 0 ? pipe.sharpen() : pipe.sharpen({
|
|
227
535
|
sigma,
|
|
228
536
|
m1: flat,
|
|
229
|
-
m2: jagged
|
|
537
|
+
m2: jagged,
|
|
538
|
+
x1,
|
|
539
|
+
y2,
|
|
540
|
+
y3
|
|
230
541
|
});
|
|
231
542
|
}
|
|
232
543
|
};
|
|
233
544
|
const median = {
|
|
234
|
-
args: [
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
],
|
|
545
|
+
args: [VNumber("median", {
|
|
546
|
+
min: 1,
|
|
547
|
+
max: 1e3,
|
|
548
|
+
integer: true
|
|
549
|
+
})],
|
|
239
550
|
apply: (_context, pipe, size) => {
|
|
240
551
|
return pipe.median(size);
|
|
241
552
|
}
|
|
242
553
|
};
|
|
243
554
|
const blur = {
|
|
244
555
|
args: [
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
556
|
+
VNumber("blur.sigma", {
|
|
557
|
+
min: .3,
|
|
558
|
+
max: 1e3
|
|
559
|
+
}),
|
|
560
|
+
VEnum("blur.precision", [
|
|
561
|
+
"integer",
|
|
562
|
+
"float",
|
|
563
|
+
"approximate"
|
|
564
|
+
]),
|
|
565
|
+
VNumber("blur.minAmplitude", {
|
|
566
|
+
min: .001,
|
|
567
|
+
max: 1
|
|
568
|
+
})
|
|
248
569
|
],
|
|
249
|
-
apply: (_context, pipe, sigma) => {
|
|
250
|
-
return pipe.blur(
|
|
570
|
+
apply: (_context, pipe, sigma, precision, minAmplitude) => {
|
|
571
|
+
return sigma === void 0 ? pipe.blur() : pipe.blur({
|
|
572
|
+
sigma,
|
|
573
|
+
...precision === void 0 ? {} : { precision },
|
|
574
|
+
...minAmplitude === void 0 ? {} : { minAmplitude }
|
|
575
|
+
});
|
|
251
576
|
}
|
|
252
577
|
};
|
|
253
|
-
const
|
|
578
|
+
const MAX_MORPHOLOGY = 100;
|
|
579
|
+
const dilate = {
|
|
580
|
+
args: [VNumber("dilate", {
|
|
581
|
+
min: 1,
|
|
582
|
+
max: MAX_MORPHOLOGY,
|
|
583
|
+
integer: true
|
|
584
|
+
})],
|
|
585
|
+
apply: (_context, pipe, width) => {
|
|
586
|
+
return pipe.dilate(width);
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
const erode = {
|
|
590
|
+
args: [VNumber("erode", {
|
|
591
|
+
min: 1,
|
|
592
|
+
max: MAX_MORPHOLOGY,
|
|
593
|
+
integer: true
|
|
594
|
+
})],
|
|
595
|
+
apply: (_context, pipe, width) => {
|
|
596
|
+
return pipe.erode(width);
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
const MAX_CLAHE = 100;
|
|
600
|
+
const clahe = {
|
|
254
601
|
args: [
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
602
|
+
VRequired("clahe.width", VNumber("clahe.width", {
|
|
603
|
+
min: 1,
|
|
604
|
+
max: MAX_CLAHE,
|
|
605
|
+
integer: true
|
|
606
|
+
})),
|
|
607
|
+
VNumber("clahe.height", {
|
|
608
|
+
min: 1,
|
|
609
|
+
max: MAX_CLAHE,
|
|
610
|
+
integer: true
|
|
611
|
+
}),
|
|
612
|
+
VNumber("clahe.maxSlope", {
|
|
613
|
+
min: 0,
|
|
614
|
+
max: 100,
|
|
615
|
+
integer: true
|
|
616
|
+
})
|
|
258
617
|
],
|
|
618
|
+
apply: (context, pipe, width, height, maxSlope) => {
|
|
619
|
+
return pipe.clahe({
|
|
620
|
+
width: Math.min(width, context.meta.width || width),
|
|
621
|
+
height: Math.min(height ?? width, (context.meta.height || height) ?? width),
|
|
622
|
+
...maxSlope === void 0 ? {} : { maxSlope }
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
const flatten = {
|
|
627
|
+
args: [],
|
|
259
628
|
apply: (context, pipe) => {
|
|
260
629
|
return pipe.flatten({ background: context.background });
|
|
261
630
|
}
|
|
262
631
|
};
|
|
632
|
+
const unflatten = {
|
|
633
|
+
args: [],
|
|
634
|
+
apply: (_context, pipe) => {
|
|
635
|
+
return pipe.unflatten();
|
|
636
|
+
}
|
|
637
|
+
};
|
|
263
638
|
const gamma = {
|
|
264
|
-
args: [
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
639
|
+
args: [VNumber("gamma", {
|
|
640
|
+
min: 1,
|
|
641
|
+
max: 3
|
|
642
|
+
}), VNumber("gamma.gammaOut", {
|
|
643
|
+
min: 1,
|
|
644
|
+
max: 3
|
|
645
|
+
})],
|
|
269
646
|
apply: (_context, pipe, gamma, gammaOut) => {
|
|
270
647
|
return pipe.gamma(gamma, gammaOut);
|
|
271
648
|
}
|
|
272
649
|
};
|
|
273
650
|
const negate = {
|
|
274
|
-
args: [
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
VArg
|
|
278
|
-
],
|
|
279
|
-
apply: (_context, pipe) => {
|
|
280
|
-
return pipe.negate();
|
|
651
|
+
args: [VBoolean("negate.alpha")],
|
|
652
|
+
apply: (_context, pipe, alpha) => {
|
|
653
|
+
return alpha === void 0 ? pipe.negate() : pipe.negate({ alpha });
|
|
281
654
|
}
|
|
282
655
|
};
|
|
283
656
|
const normalize = {
|
|
284
|
-
args: [
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
657
|
+
args: [VNumber("normalize.lower", {
|
|
658
|
+
min: 0,
|
|
659
|
+
max: 99
|
|
660
|
+
}), VNumber("normalize.upper", {
|
|
661
|
+
min: 1,
|
|
662
|
+
max: 100
|
|
663
|
+
})],
|
|
664
|
+
apply: (_context, pipe, lower, upper) => {
|
|
665
|
+
return pipe.normalize({
|
|
666
|
+
lower,
|
|
667
|
+
upper
|
|
668
|
+
});
|
|
291
669
|
}
|
|
292
670
|
};
|
|
293
671
|
const threshold = {
|
|
294
|
-
args: [
|
|
295
|
-
|
|
296
|
-
|
|
672
|
+
args: [VNumber("threshold", {
|
|
673
|
+
min: 0,
|
|
674
|
+
max: 255,
|
|
675
|
+
integer: true
|
|
676
|
+
}), VBoolean("threshold.grayscale")],
|
|
677
|
+
apply: (_context, pipe, threshold, grayscale) => {
|
|
678
|
+
return grayscale === void 0 ? pipe.threshold(threshold) : pipe.threshold(threshold, { grayscale });
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
const linear = {
|
|
682
|
+
args: [VNumber("linear.a"), VNumber("linear.b")],
|
|
683
|
+
apply: (_context, pipe, a, b) => {
|
|
684
|
+
return pipe.linear(a, b);
|
|
297
685
|
}
|
|
298
686
|
};
|
|
299
687
|
const modulate = {
|
|
300
|
-
args: [
|
|
301
|
-
|
|
688
|
+
args: [
|
|
689
|
+
VNumber("modulate.brightness", { min: 0 }),
|
|
690
|
+
VNumber("modulate.saturation", { min: 0 }),
|
|
691
|
+
VNumber("modulate.hue", { integer: true }),
|
|
692
|
+
VNumber("modulate.lightness")
|
|
693
|
+
],
|
|
694
|
+
apply: (_context, pipe, brightness, saturation, hue, lightness) => {
|
|
302
695
|
return pipe.modulate({
|
|
303
|
-
brightness,
|
|
304
|
-
saturation,
|
|
305
|
-
hue
|
|
696
|
+
...brightness === void 0 ? {} : { brightness },
|
|
697
|
+
...saturation === void 0 ? {} : { saturation },
|
|
698
|
+
...hue === void 0 ? {} : { hue },
|
|
699
|
+
...lightness === void 0 ? {} : { lightness }
|
|
306
700
|
});
|
|
307
701
|
}
|
|
308
702
|
};
|
|
703
|
+
const brightness = {
|
|
704
|
+
args: [VRequired("brightness", VNumber("brightness", { min: 0 }))],
|
|
705
|
+
apply: (_context, pipe, brightness) => {
|
|
706
|
+
return pipe.modulate({ brightness });
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
const saturation = {
|
|
710
|
+
args: [VRequired("saturation", VNumber("saturation", { min: 0 }))],
|
|
711
|
+
apply: (_context, pipe, saturation) => {
|
|
712
|
+
return pipe.modulate({ saturation });
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
const hue = {
|
|
716
|
+
args: [VRequired("hue", VNumber("hue", { integer: true }))],
|
|
717
|
+
apply: (_context, pipe, hue) => {
|
|
718
|
+
return pipe.modulate({ hue });
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
const lightness = {
|
|
722
|
+
args: [VRequired("lightness", VNumber("lightness"))],
|
|
723
|
+
apply: (_context, pipe, lightness) => {
|
|
724
|
+
return pipe.modulate({ lightness });
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
const opacity = {
|
|
728
|
+
args: [VRequired("opacity", VNumber("opacity", {
|
|
729
|
+
min: 0,
|
|
730
|
+
max: 1
|
|
731
|
+
}))],
|
|
732
|
+
apply: (context, pipe, opacity) => {
|
|
733
|
+
const overlay = opacityOverlay(opacity, context.background);
|
|
734
|
+
return (context.background ? pipe : pipe.ensureAlpha()).composite([overlay]);
|
|
735
|
+
}
|
|
736
|
+
};
|
|
309
737
|
const tint = {
|
|
310
|
-
args: [
|
|
738
|
+
args: [VColor("tint")],
|
|
311
739
|
apply: (_context, pipe, rgb) => {
|
|
312
740
|
return pipe.tint(rgb);
|
|
313
741
|
}
|
|
314
742
|
};
|
|
315
743
|
const grayscale = {
|
|
316
|
-
args: [
|
|
744
|
+
args: [],
|
|
317
745
|
apply: (_context, pipe) => {
|
|
318
746
|
return pipe.grayscale();
|
|
319
747
|
}
|
|
@@ -325,9 +753,94 @@ const w = width;
|
|
|
325
753
|
const h = height;
|
|
326
754
|
const s = resize;
|
|
327
755
|
const pos = position;
|
|
756
|
+
const UNSAFE_ELEMENTS = /* @__PURE__ */ new Set([
|
|
757
|
+
"script",
|
|
758
|
+
"foreignobject",
|
|
759
|
+
"iframe",
|
|
760
|
+
"embed",
|
|
761
|
+
"object",
|
|
762
|
+
"handler",
|
|
763
|
+
"listener",
|
|
764
|
+
"base",
|
|
765
|
+
"link",
|
|
766
|
+
"meta"
|
|
767
|
+
]);
|
|
768
|
+
const ANIMATION_ELEMENTS = /* @__PURE__ */ new Set([
|
|
769
|
+
"animate",
|
|
770
|
+
"animatemotion",
|
|
771
|
+
"animatetransform",
|
|
772
|
+
"set"
|
|
773
|
+
]);
|
|
774
|
+
const ANIMATION_VALUE_ATTRS = [
|
|
775
|
+
"from",
|
|
776
|
+
"to",
|
|
777
|
+
"by",
|
|
778
|
+
"values"
|
|
779
|
+
];
|
|
780
|
+
const URI_ATTRS = /* @__PURE__ */ new Set(["href", "src"]);
|
|
781
|
+
const SAFE_URI_SCHEMES = /* @__PURE__ */ new Set([
|
|
782
|
+
"http",
|
|
783
|
+
"https",
|
|
784
|
+
"mailto",
|
|
785
|
+
"tel",
|
|
786
|
+
"ftp"
|
|
787
|
+
]);
|
|
788
|
+
const SAFE_DATA_URI_RE = /^data:image\/(?!svg\+xml)[\w.+-]+[,;]/;
|
|
789
|
+
const URI_SCHEME_RE = /^([a-z][\d+.a-z-]*):/;
|
|
790
|
+
const URI_IGNORED_CHARS_RE = /[\u0000-\u0020]/g;
|
|
791
|
+
function localName(name) {
|
|
792
|
+
const colonIndex = name.indexOf(":");
|
|
793
|
+
return (colonIndex === -1 ? name : name.slice(colonIndex + 1)).toLowerCase();
|
|
794
|
+
}
|
|
795
|
+
function isUnsafeURI(value) {
|
|
796
|
+
const uri = value.replace(URI_IGNORED_CHARS_RE, "").toLowerCase();
|
|
797
|
+
const scheme = URI_SCHEME_RE.exec(uri)?.[1];
|
|
798
|
+
if (!scheme) return false;
|
|
799
|
+
if (scheme === "data") return !SAFE_DATA_URI_RE.test(uri);
|
|
800
|
+
return !SAFE_URI_SCHEMES.has(scheme);
|
|
801
|
+
}
|
|
802
|
+
function isUnsafeAnimation(attributes) {
|
|
803
|
+
const attrs = {};
|
|
804
|
+
for (const [name, value] of Object.entries(attributes)) attrs[localName(name)] = value;
|
|
805
|
+
if (localName(attrs.attributename?.trim() || "").startsWith("on")) return true;
|
|
806
|
+
return ANIMATION_VALUE_ATTRS.some((attr) => (attrs[attr] || "").split(";").some((value) => isUnsafeURI(value)));
|
|
807
|
+
}
|
|
808
|
+
function detach(node, parent) {
|
|
809
|
+
parent.children = parent.children.filter((child) => child !== node);
|
|
810
|
+
}
|
|
811
|
+
const sanitizeSVGPlugin = {
|
|
812
|
+
name: "ipx-sanitize",
|
|
813
|
+
fn: () => ({
|
|
814
|
+
doctype: { enter: (node, parentNode) => detach(node, parentNode) },
|
|
815
|
+
instruction: { enter: (node, parentNode) => detach(node, parentNode) },
|
|
816
|
+
element: { enter: (node, parentNode) => {
|
|
817
|
+
const name = localName(node.name);
|
|
818
|
+
if (UNSAFE_ELEMENTS.has(name)) {
|
|
819
|
+
detach(node, parentNode);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (ANIMATION_ELEMENTS.has(name) && isUnsafeAnimation(node.attributes)) {
|
|
823
|
+
detach(node, parentNode);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
for (const [attr, value] of Object.entries(node.attributes)) {
|
|
827
|
+
const attrName = localName(attr);
|
|
828
|
+
if (attrName.startsWith("on") || URI_ATTRS.has(attrName) && isUnsafeURI(value)) delete node.attributes[attr];
|
|
829
|
+
}
|
|
830
|
+
} }
|
|
831
|
+
})
|
|
832
|
+
};
|
|
328
833
|
function getEnv(name) {
|
|
329
834
|
const value = globalThis.process?.env?.[name];
|
|
330
|
-
if (value
|
|
835
|
+
if (value === void 0) return;
|
|
836
|
+
try {
|
|
837
|
+
return JSON.parse(value);
|
|
838
|
+
} catch {
|
|
839
|
+
return value;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
function getBuiltinModule(id) {
|
|
843
|
+
return globalThis.process?.getBuiltinModule?.(id);
|
|
331
844
|
}
|
|
332
845
|
function requireModule(id) {
|
|
333
846
|
const { createRequire } = globalThis.process.getBuiltinModule("node:module");
|
|
@@ -341,6 +854,7 @@ function cachedPromise(function_) {
|
|
|
341
854
|
return p;
|
|
342
855
|
};
|
|
343
856
|
}
|
|
857
|
+
const DEFAULT_MAX_OUTPUT_DIMENSION = 8192;
|
|
344
858
|
const SUPPORTED_FORMATS = /* @__PURE__ */ new Set([
|
|
345
859
|
"jpeg",
|
|
346
860
|
"png",
|
|
@@ -356,6 +870,7 @@ function createIPX(userOptions) {
|
|
|
356
870
|
...userOptions,
|
|
357
871
|
alias: userOptions.alias || getEnv("IPX_ALIAS") || {},
|
|
358
872
|
maxAge: userOptions.maxAge ?? getEnv("IPX_MAX_AGE") ?? 60,
|
|
873
|
+
maxOutputDimension: userOptions.maxOutputDimension ?? getEnv("IPX_MAX_OUTPUT_DIMENSION") ?? DEFAULT_MAX_OUTPUT_DIMENSION,
|
|
359
874
|
sharpOptions: {
|
|
360
875
|
jpegProgressive: true,
|
|
361
876
|
...userOptions.sharpOptions
|
|
@@ -421,25 +936,43 @@ function createIPX(userOptions) {
|
|
|
421
936
|
}
|
|
422
937
|
let mFormat = modifiers.f || modifiers.format;
|
|
423
938
|
if (mFormat === "jpg") mFormat = "jpeg";
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
939
|
+
if (imageMeta$1.type === "svg" && (!mFormat || mFormat === "svg")) {
|
|
940
|
+
const sanitize = !options.svg?.unsafeSkipSanitize;
|
|
941
|
+
const shouldOptimize = options.svg?.optimize !== false;
|
|
942
|
+
const svgoConfig = options.svg?.optimize === false ? void 0 : options.svg?.optimize;
|
|
943
|
+
if (!sanitize && !shouldOptimize) return {
|
|
944
|
+
data: sourceData,
|
|
945
|
+
format: "svg+xml",
|
|
946
|
+
meta: imageMeta$1
|
|
947
|
+
};
|
|
948
|
+
const plugins = sanitize ? ["removeScripts", sanitizeSVGPlugin] : [];
|
|
949
|
+
if (svgoConfig?.plugins) plugins.push(...svgoConfig.plugins);
|
|
950
|
+
else if (shouldOptimize) plugins.push("preset-default");
|
|
431
951
|
const { optimize } = await getSVGO();
|
|
952
|
+
let svg;
|
|
953
|
+
try {
|
|
954
|
+
svg = optimize(sourceData.toString("utf8"), {
|
|
955
|
+
...svgoConfig,
|
|
956
|
+
plugins
|
|
957
|
+
}).data;
|
|
958
|
+
} catch (error) {
|
|
959
|
+
throw new HTTPError({
|
|
960
|
+
statusCode: 400,
|
|
961
|
+
statusText: `IPX_INVALID_SVG`,
|
|
962
|
+
message: `Cannot parse SVG: ${id}`,
|
|
963
|
+
cause: error
|
|
964
|
+
});
|
|
965
|
+
}
|
|
432
966
|
return {
|
|
433
|
-
data:
|
|
434
|
-
...options.svgo,
|
|
435
|
-
plugins: ["removeScripts", ...options.svgo?.plugins || []]
|
|
436
|
-
}).data,
|
|
967
|
+
data: svg,
|
|
437
968
|
format: "svg+xml",
|
|
438
969
|
meta: imageMeta$1
|
|
439
970
|
};
|
|
440
971
|
}
|
|
972
|
+
const format = mFormat && SUPPORTED_FORMATS.has(mFormat) ? mFormat : SUPPORTED_FORMATS.has(imageMeta$1.type || "") ? imageMeta$1.type : "jpeg";
|
|
441
973
|
const animated = modifiers.animated !== void 0 || modifiers.a !== void 0 || format === "gif";
|
|
442
|
-
|
|
974
|
+
const Sharp = await getSharp();
|
|
975
|
+
let sharp = Sharp(sourceData, {
|
|
443
976
|
animated,
|
|
444
977
|
...options.sharpOptions
|
|
445
978
|
});
|
|
@@ -449,15 +982,34 @@ function createIPX(userOptions) {
|
|
|
449
982
|
name,
|
|
450
983
|
args: arguments_
|
|
451
984
|
})).filter((h) => h.handler).sort((a, b) => {
|
|
452
|
-
const
|
|
453
|
-
const
|
|
454
|
-
return
|
|
985
|
+
const aOrder = a.handler.order || 0;
|
|
986
|
+
const bOrder = b.handler.order || 0;
|
|
987
|
+
return aOrder === bOrder ? (a.name || "").localeCompare(b.name || "") : aOrder - bOrder;
|
|
455
988
|
});
|
|
456
|
-
const handlerContext = {
|
|
989
|
+
const handlerContext = {
|
|
990
|
+
meta: imageMeta$1,
|
|
991
|
+
maxOutputDimension: options.maxOutputDimension
|
|
992
|
+
};
|
|
457
993
|
for (const h of handlers) sharp = applyHandler(handlerContext, sharp, h.handler, h.args.toString()) || sharp;
|
|
994
|
+
if (handlerContext.background) try {
|
|
995
|
+
Sharp({ create: {
|
|
996
|
+
width: 1,
|
|
997
|
+
height: 1,
|
|
998
|
+
channels: 4,
|
|
999
|
+
background: handlerContext.background
|
|
1000
|
+
} });
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
throw asModifierError(error);
|
|
1003
|
+
}
|
|
458
1004
|
if (SUPPORTED_FORMATS.has(format || "")) sharp = sharp.toFormat(format, { quality: handlerContext.quality });
|
|
1005
|
+
let processedImage;
|
|
1006
|
+
try {
|
|
1007
|
+
processedImage = await sharp.toBuffer();
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
throw asModifierError(error);
|
|
1010
|
+
}
|
|
459
1011
|
return {
|
|
460
|
-
data:
|
|
1012
|
+
data: processedImage,
|
|
461
1013
|
format,
|
|
462
1014
|
meta: imageMeta$1
|
|
463
1015
|
};
|
|
@@ -467,42 +1019,54 @@ function createIPX(userOptions) {
|
|
|
467
1019
|
}
|
|
468
1020
|
var import_etag = /* @__PURE__ */ __toESM(require_etag(), 1);
|
|
469
1021
|
var import_accept_negotiator = require_accept_negotiator();
|
|
470
|
-
function createIPXFetchHandler(ipx) {
|
|
471
|
-
return createIPXHandler(ipx).fetch;
|
|
1022
|
+
function createIPXFetchHandler(ipx, opts) {
|
|
1023
|
+
return createIPXHandler(ipx, opts).fetch;
|
|
472
1024
|
}
|
|
473
|
-
function createIPXNodeHandler(ipx) {
|
|
1025
|
+
function createIPXNodeHandler(ipx, opts) {
|
|
474
1026
|
const { toNodeHandler } = requireModule("srvx/node");
|
|
475
|
-
return toNodeHandler(createIPXFetchHandler(ipx));
|
|
1027
|
+
return toNodeHandler(createIPXFetchHandler(ipx, opts));
|
|
476
1028
|
}
|
|
477
1029
|
function serveIPX(ipx, opts) {
|
|
478
1030
|
const { serve } = requireModule("srvx");
|
|
479
|
-
const
|
|
1031
|
+
const { parseURL, ...serverOptions } = opts || {};
|
|
1032
|
+
const fetch = createIPXFetchHandler(ipx, { parseURL });
|
|
480
1033
|
return serve({
|
|
481
|
-
...
|
|
1034
|
+
...serverOptions,
|
|
482
1035
|
fetch
|
|
483
1036
|
});
|
|
484
1037
|
}
|
|
485
1038
|
const MODIFIER_SEP = /[&,]/g;
|
|
486
1039
|
const MODIFIER_VAL_SEP = /[:=_]/;
|
|
487
|
-
function
|
|
1040
|
+
function parseIPXURL(url) {
|
|
1041
|
+
const [modifiersString = "", ...idSegments] = new URL(url).pathname.slice(1).split("/");
|
|
1042
|
+
const id = decode$1(idSegments.join("/"));
|
|
1043
|
+
if (!modifiersString) throw new HTTPError({
|
|
1044
|
+
statusCode: 400,
|
|
1045
|
+
statusText: "IPX_MISSING_MODIFIERS",
|
|
1046
|
+
message: `Modifiers are missing: ${safeString(id)}`
|
|
1047
|
+
});
|
|
1048
|
+
const modifiers = Object.create(null);
|
|
1049
|
+
if (modifiersString !== "_") for (const p of modifiersString.split(MODIFIER_SEP)) {
|
|
1050
|
+
const [key = "", ...values] = p.split(MODIFIER_VAL_SEP);
|
|
1051
|
+
modifiers[key] = values.map((v) => decode$1(v)).join("_");
|
|
1052
|
+
}
|
|
1053
|
+
return {
|
|
1054
|
+
id,
|
|
1055
|
+
modifiers
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
function createIPXHandler(ipx, opts = {}) {
|
|
1059
|
+
const parseURL = opts.parseURL || parseIPXURL;
|
|
488
1060
|
return defineEventHandler(async (event) => {
|
|
489
|
-
const
|
|
490
|
-
const id = safeString(
|
|
491
|
-
if (!modifiersString) throw new HTTPError({
|
|
492
|
-
statusCode: 400,
|
|
493
|
-
statusText: "IPX_MISSING_MODIFIERS",
|
|
494
|
-
message: `Modifiers are missing: ${id}`
|
|
495
|
-
});
|
|
1061
|
+
const parsed = await parseURL(event.req.url) || {};
|
|
1062
|
+
const id = safeString(parsed.id);
|
|
496
1063
|
if (!id || id === "/") throw new HTTPError({
|
|
497
1064
|
statusCode: 400,
|
|
498
1065
|
statusText: "IPX_MISSING_ID",
|
|
499
1066
|
message: `Resource id is missing: ${event.path}`
|
|
500
1067
|
});
|
|
501
1068
|
const modifiers = Object.create(null);
|
|
502
|
-
|
|
503
|
-
const [key, ...values] = p.split(MODIFIER_VAL_SEP);
|
|
504
|
-
modifiers[safeString(key)] = values.map((v) => safeString(decode(v))).join("_");
|
|
505
|
-
}
|
|
1069
|
+
for (const [key, value] of Object.entries(parsed.modifiers || {})) modifiers[safeString(key)] = safeString(value);
|
|
506
1070
|
if ((modifiers.f || modifiers.format) === "auto") {
|
|
507
1071
|
const acceptHeader = event.req.headers.get("accept") || "";
|
|
508
1072
|
const animated = modifiers.animated ?? modifiers.a;
|
|
@@ -517,6 +1081,9 @@ function createIPXHandler(ipx) {
|
|
|
517
1081
|
const img = ipx(id, modifiers);
|
|
518
1082
|
const sourceMeta = await img.getSourceMeta();
|
|
519
1083
|
sendResponseHeaderIfNotSet(event, "content-security-policy", "default-src 'none'");
|
|
1084
|
+
sendResponseHeaderIfNotSet(event, "x-content-type-options", "nosniff");
|
|
1085
|
+
if (typeof sourceMeta.maxAge === "number") sendResponseHeaderIfNotSet(event, "cache-control", `max-age=${+sourceMeta.maxAge}, public, s-maxage=${+sourceMeta.maxAge}`);
|
|
1086
|
+
const ifNoneMatch = event.req.headers.get("if-none-match");
|
|
520
1087
|
if (sourceMeta.mtime) {
|
|
521
1088
|
sendResponseHeaderIfNotSet(event, "last-modified", sourceMeta.mtime.toUTCString());
|
|
522
1089
|
const _ifModifiedSince = event.req.headers.get("if-modified-since");
|
|
@@ -525,13 +1092,22 @@ function createIPXHandler(ipx) {
|
|
|
525
1092
|
return;
|
|
526
1093
|
}
|
|
527
1094
|
}
|
|
1095
|
+
const weakEtag = sourceMeta.mtime ? getWeakEtag(id, modifiers, sourceMeta.mtime) : void 0;
|
|
1096
|
+
if (weakEtag) {
|
|
1097
|
+
sendResponseHeaderIfNotSet(event, "etag", weakEtag);
|
|
1098
|
+
if (etagMatches(ifNoneMatch, weakEtag)) {
|
|
1099
|
+
event.res.status = 304;
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
528
1103
|
const { data, format } = await img.process();
|
|
529
|
-
if (
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
1104
|
+
if (!weakEtag) {
|
|
1105
|
+
const etag = (0, import_etag.default)(data);
|
|
1106
|
+
sendResponseHeaderIfNotSet(event, "etag", etag);
|
|
1107
|
+
if (etagMatches(ifNoneMatch, etag)) {
|
|
1108
|
+
event.res.status = 304;
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
535
1111
|
}
|
|
536
1112
|
if (format) sendResponseHeaderIfNotSet(event, "content-type", `image/${format}`);
|
|
537
1113
|
return data;
|
|
@@ -540,6 +1116,25 @@ function createIPXHandler(ipx) {
|
|
|
540
1116
|
function sendResponseHeaderIfNotSet(event, name, value) {
|
|
541
1117
|
if (!event.res.headers.has(name)) event.res.headers.set(name, value);
|
|
542
1118
|
}
|
|
1119
|
+
function getWeakEtag(id, modifiers, mtime) {
|
|
1120
|
+
const crypto = getBuiltinModule("node:crypto");
|
|
1121
|
+
if (!crypto?.createHash) return;
|
|
1122
|
+
const key = JSON.stringify([
|
|
1123
|
+
mtime.getTime(),
|
|
1124
|
+
id,
|
|
1125
|
+
Object.keys(modifiers).sort().map((key) => [key, modifiers[key]])
|
|
1126
|
+
]);
|
|
1127
|
+
return `W/"${crypto.createHash("sha256").update(key).digest("base64url").slice(0, 27)}"`;
|
|
1128
|
+
}
|
|
1129
|
+
function etagMatches(ifNoneMatch, etag) {
|
|
1130
|
+
if (!ifNoneMatch || !etag) return false;
|
|
1131
|
+
if (ifNoneMatch === "*") return true;
|
|
1132
|
+
const opaque = opaqueTag(etag);
|
|
1133
|
+
return ifNoneMatch.split(",").some((tag) => opaqueTag(tag.trim()) === opaque);
|
|
1134
|
+
}
|
|
1135
|
+
function opaqueTag(tag) {
|
|
1136
|
+
return tag.startsWith("W/") ? tag.slice(2) : tag;
|
|
1137
|
+
}
|
|
543
1138
|
function autoDetectFormat(acceptHeader, animated) {
|
|
544
1139
|
if (animated) return (0, import_accept_negotiator.negotiate)(acceptHeader, ["image/webp", "image/gif"])?.split("/")[1] || "gif";
|
|
545
1140
|
return (0, import_accept_negotiator.negotiate)(acceptHeader, [
|
|
@@ -552,33 +1147,225 @@ function autoDetectFormat(acceptHeader, animated) {
|
|
|
552
1147
|
"image/gif"
|
|
553
1148
|
])?.split("/")[1] || "jpeg";
|
|
554
1149
|
}
|
|
1150
|
+
function decode$1(input) {
|
|
1151
|
+
try {
|
|
1152
|
+
return decodeURIComponent(input);
|
|
1153
|
+
} catch {
|
|
1154
|
+
return input;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
555
1157
|
function safeString(input) {
|
|
556
|
-
return JSON.stringify(input).replace(/^"|"$/g, "").replace(/\\+/g, "\\").replace(/\\"/g, "\"");
|
|
1158
|
+
return JSON.stringify(input ?? "").replace(/^"|"$/g, "").replace(/\\+/g, "\\").replace(/\\"/g, "\"");
|
|
557
1159
|
}
|
|
558
1160
|
const HTTP_RE = /^https?:\/\//;
|
|
1161
|
+
const REDIRECT_STATUS = /* @__PURE__ */ new Set([
|
|
1162
|
+
301,
|
|
1163
|
+
302,
|
|
1164
|
+
303,
|
|
1165
|
+
307,
|
|
1166
|
+
308
|
|
1167
|
+
]);
|
|
1168
|
+
const MAX_REDIRECTS = 3;
|
|
1169
|
+
function parseIPv4(input) {
|
|
1170
|
+
const parts = input.split(".");
|
|
1171
|
+
if (parts.length !== 4) return;
|
|
1172
|
+
const octets = [];
|
|
1173
|
+
for (const part of parts) {
|
|
1174
|
+
if (!/^\d{1,3}$/.test(part)) return;
|
|
1175
|
+
const octet = Number(part);
|
|
1176
|
+
if (octet > 255) return;
|
|
1177
|
+
octets.push(octet);
|
|
1178
|
+
}
|
|
1179
|
+
return octets;
|
|
1180
|
+
}
|
|
1181
|
+
function parseIPv6(input) {
|
|
1182
|
+
const zoneIndex = input.indexOf("%");
|
|
1183
|
+
const halves = (zoneIndex === -1 ? input : input.slice(0, zoneIndex)).split("::");
|
|
1184
|
+
if (halves.length > 2) return;
|
|
1185
|
+
const expand = (part) => {
|
|
1186
|
+
if (part === "") return [];
|
|
1187
|
+
const bytes = [];
|
|
1188
|
+
const groups = part.split(":");
|
|
1189
|
+
for (const [index, group] of groups.entries()) {
|
|
1190
|
+
if (group.includes(".")) {
|
|
1191
|
+
if (index !== groups.length - 1) return;
|
|
1192
|
+
const octets = parseIPv4(group);
|
|
1193
|
+
if (!octets) return;
|
|
1194
|
+
bytes.push(...octets);
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
if (!/^[\da-f]{1,4}$/i.test(group)) return;
|
|
1198
|
+
const value = Number.parseInt(group, 16);
|
|
1199
|
+
bytes.push(value >> 8, value & 255);
|
|
1200
|
+
}
|
|
1201
|
+
return bytes;
|
|
1202
|
+
};
|
|
1203
|
+
const head = expand(halves[0]);
|
|
1204
|
+
const tail = halves.length === 2 ? expand(halves[1]) : [];
|
|
1205
|
+
if (!head || !tail) return;
|
|
1206
|
+
if (halves.length === 1) return head.length === 16 ? head : void 0;
|
|
1207
|
+
const fill = 16 - head.length - tail.length;
|
|
1208
|
+
if (fill < 0) return;
|
|
1209
|
+
return [
|
|
1210
|
+
...head,
|
|
1211
|
+
...Array.from({ length: fill }, () => 0),
|
|
1212
|
+
...tail
|
|
1213
|
+
];
|
|
1214
|
+
}
|
|
1215
|
+
function embeddedIPv4(bytes) {
|
|
1216
|
+
if (bytes.slice(0, 10).every((byte) => byte === 0) && (bytes[10] === 255 && bytes[11] === 255 || bytes[10] === 0 && bytes[11] === 0)) return bytes.slice(12);
|
|
1217
|
+
if (bytes.slice(0, 8).every((byte) => byte === 0) && bytes[8] === 255 && bytes[9] === 255 && bytes[10] === 0 && bytes[11] === 0) return bytes.slice(12);
|
|
1218
|
+
if (bytes[0] === 0 && bytes[1] === 100 && bytes[2] === 255 && bytes[3] === 155 && bytes.slice(4, 12).every((byte) => byte === 0)) return bytes.slice(12);
|
|
1219
|
+
if (bytes[0] === 32 && bytes[1] === 2) return bytes.slice(2, 6);
|
|
1220
|
+
}
|
|
1221
|
+
function isPublicIPv4(octets) {
|
|
1222
|
+
const [a, b, c] = octets;
|
|
1223
|
+
return !(a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 0 && c === 0 || a === 192 && b === 0 && c === 2 || a === 192 && b === 88 && c === 99 || a === 192 && b === 168 || a === 198 && (b === 18 || b === 19) || a === 198 && b === 51 && c === 100 || a === 203 && b === 0 && c === 113 || a >= 224);
|
|
1224
|
+
}
|
|
1225
|
+
function isPublicIPv6(bytes) {
|
|
1226
|
+
const embedded = embeddedIPv4(bytes);
|
|
1227
|
+
if (embedded) return isPublicIPv4(embedded);
|
|
1228
|
+
const [a, b] = bytes;
|
|
1229
|
+
return !(a === 255 || a === 254 && (b & 192) === 128 || a === 254 && (b & 192) === 192 || (a & 254) === 252 || a === 0 && b === 100 && bytes[2] === 255 && bytes[3] === 155 || a === 1 && b === 0 && bytes.slice(2, 8).every((x) => x === 0) || a === 32 && b === 1 && bytes[2] === 13 && bytes[3] === 184 || a === 32 && b === 1 && bytes[2] === 0 && bytes[3] === 0);
|
|
1230
|
+
}
|
|
1231
|
+
function isPublicIP(address, family) {
|
|
1232
|
+
if (family === 4) {
|
|
1233
|
+
const octets = parseIPv4(address);
|
|
1234
|
+
return octets ? isPublicIPv4(octets) : false;
|
|
1235
|
+
}
|
|
1236
|
+
if (family === 6) {
|
|
1237
|
+
const bytes = parseIPv6(address);
|
|
1238
|
+
return bytes ? isPublicIPv6(bytes) : false;
|
|
1239
|
+
}
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
function decode(input) {
|
|
1243
|
+
try {
|
|
1244
|
+
return decodeURIComponent(input);
|
|
1245
|
+
} catch {
|
|
1246
|
+
return input;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
559
1249
|
function ipxHttpStorage(_options = {}) {
|
|
560
1250
|
const allowAllDomains = _options.allowAllDomains ?? getEnv("IPX_HTTP_ALLOW_ALL_DOMAINS") ?? false;
|
|
561
1251
|
let _domains = _options.domains || getEnv("IPX_HTTP_DOMAINS") || [];
|
|
562
1252
|
const defaultMaxAge = _options.maxAge || getEnv("IPX_HTTP_MAX_AGE") || 300;
|
|
563
1253
|
const fetchOptions = _options.fetchOptions || getEnv("IPX_HTTP_FETCH_OPTIONS") || {};
|
|
1254
|
+
const blockPrivateIPs = _options.blockPrivateIPs ?? getEnv("IPX_HTTP_BLOCK_PRIVATE_IPS") ?? false;
|
|
564
1255
|
if (typeof _domains === "string") _domains = _domains.split(",").map((s) => s.trim());
|
|
565
1256
|
const domains = new Set(_domains.map((d) => {
|
|
566
1257
|
if (!HTTP_RE.test(d)) d = "http://" + d;
|
|
567
1258
|
return new URL(d).hostname;
|
|
568
1259
|
}).filter(Boolean));
|
|
569
|
-
function validateId(id) {
|
|
570
|
-
|
|
1260
|
+
async function validateId(id) {
|
|
1261
|
+
let url;
|
|
1262
|
+
try {
|
|
1263
|
+
url = new URL(decode(id));
|
|
1264
|
+
} catch {
|
|
1265
|
+
throw new HTTPError({
|
|
1266
|
+
statusCode: 400,
|
|
1267
|
+
statusText: `IPX_INVALID_URL`,
|
|
1268
|
+
message: `Invalid URL: ${id}`
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
await validateURL(url, id);
|
|
1272
|
+
return url.toString();
|
|
1273
|
+
}
|
|
1274
|
+
async function validateURL(url, id = url.toString()) {
|
|
571
1275
|
if (!url.hostname) throw new HTTPError({
|
|
572
1276
|
statusCode: 403,
|
|
573
1277
|
statusText: `IPX_MISSING_HOSTNAME`,
|
|
574
1278
|
message: `Hostname is missing: ${id}`
|
|
575
1279
|
});
|
|
1280
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new HTTPError({
|
|
1281
|
+
statusCode: 403,
|
|
1282
|
+
statusText: `IPX_FORBIDDEN_PROTOCOL`,
|
|
1283
|
+
message: `Forbidden protocol: ${url.protocol}`
|
|
1284
|
+
});
|
|
576
1285
|
if (!allowAllDomains && !domains.has(url.hostname)) throw new HTTPError({
|
|
577
1286
|
statusCode: 403,
|
|
578
1287
|
statusText: `IPX_FORBIDDEN_HOST`,
|
|
579
1288
|
message: `Forbidden host: ${url.hostname}`
|
|
580
1289
|
});
|
|
581
|
-
|
|
1290
|
+
if (blockPrivateIPs) await validatePublicIP(url, id);
|
|
1291
|
+
}
|
|
1292
|
+
async function validatePublicIP(url, id) {
|
|
1293
|
+
const hostname = url.hostname.replace(/^\[|]$/g, "");
|
|
1294
|
+
const net = getBuiltinModule("node:net");
|
|
1295
|
+
const dns = getBuiltinModule("node:dns");
|
|
1296
|
+
if (!net?.isIP || !dns?.promises?.lookup) throw new HTTPError({
|
|
1297
|
+
statusCode: 500,
|
|
1298
|
+
statusText: `IPX_IP_CHECK_UNAVAILABLE`,
|
|
1299
|
+
message: `Cannot verify the address of ${hostname}: \`blockPrivateIPs\` requires \`node:net\` and \`node:dns\`.`
|
|
1300
|
+
});
|
|
1301
|
+
const assertPublic = (address, family) => {
|
|
1302
|
+
if (!isPublicIP(address, family)) throw new HTTPError({
|
|
1303
|
+
statusCode: 403,
|
|
1304
|
+
statusText: `IPX_FORBIDDEN_IP`,
|
|
1305
|
+
message: `Forbidden IP address: ${address} (${id})`
|
|
1306
|
+
});
|
|
1307
|
+
};
|
|
1308
|
+
const family = net.isIP(hostname);
|
|
1309
|
+
if (family) {
|
|
1310
|
+
assertPublic(hostname, family);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
let addresses;
|
|
1314
|
+
try {
|
|
1315
|
+
addresses = await dns.promises.lookup(hostname, {
|
|
1316
|
+
all: true,
|
|
1317
|
+
verbatim: true
|
|
1318
|
+
});
|
|
1319
|
+
} catch {
|
|
1320
|
+
throw new HTTPError({
|
|
1321
|
+
statusCode: 502,
|
|
1322
|
+
statusText: `IPX_DNS_LOOKUP_FAILED`,
|
|
1323
|
+
message: `Cannot resolve host: ${hostname}`
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
if (addresses.length === 0) throw new HTTPError({
|
|
1327
|
+
statusCode: 502,
|
|
1328
|
+
statusText: `IPX_DNS_LOOKUP_FAILED`,
|
|
1329
|
+
message: `Cannot resolve host: ${hostname}`
|
|
1330
|
+
});
|
|
1331
|
+
for (const { address, family } of addresses) assertPublic(address, family || net.isIP(address));
|
|
1332
|
+
}
|
|
1333
|
+
async function fetchURL(url, init) {
|
|
1334
|
+
const _init = {
|
|
1335
|
+
...fetchOptions,
|
|
1336
|
+
...init
|
|
1337
|
+
};
|
|
1338
|
+
if (allowAllDomains && !blockPrivateIPs || _init.redirect) return fetch(url, _init);
|
|
1339
|
+
let currentURL = url;
|
|
1340
|
+
let method = _init.method || "GET";
|
|
1341
|
+
for (let i = 0; i <= MAX_REDIRECTS; i++) {
|
|
1342
|
+
const response = await fetch(currentURL, {
|
|
1343
|
+
..._init,
|
|
1344
|
+
method,
|
|
1345
|
+
redirect: "manual"
|
|
1346
|
+
});
|
|
1347
|
+
if (!REDIRECT_STATUS.has(response.status)) return response;
|
|
1348
|
+
const location = response.headers.get("location");
|
|
1349
|
+
if (!location) return response;
|
|
1350
|
+
let nextURL;
|
|
1351
|
+
try {
|
|
1352
|
+
nextURL = new URL(location, currentURL);
|
|
1353
|
+
} catch {
|
|
1354
|
+
throw new HTTPError({
|
|
1355
|
+
statusCode: 502,
|
|
1356
|
+
statusText: `IPX_INVALID_REDIRECT`,
|
|
1357
|
+
message: `Invalid redirect location: ${location}`
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
await validateURL(nextURL, location);
|
|
1361
|
+
if (response.status === 303 && method !== "HEAD") method = "GET";
|
|
1362
|
+
currentURL = nextURL.toString();
|
|
1363
|
+
}
|
|
1364
|
+
throw new HTTPError({
|
|
1365
|
+
statusCode: 502,
|
|
1366
|
+
statusText: `IPX_TOO_MANY_REDIRECTS`,
|
|
1367
|
+
message: `Too many redirects (max ${MAX_REDIRECTS}): ${url}`
|
|
1368
|
+
});
|
|
582
1369
|
}
|
|
583
1370
|
function parseResponse(response) {
|
|
584
1371
|
let maxAge = defaultMaxAge;
|
|
@@ -600,12 +1387,9 @@ function ipxHttpStorage(_options = {}) {
|
|
|
600
1387
|
return {
|
|
601
1388
|
name: "ipx:http",
|
|
602
1389
|
async getMeta(id) {
|
|
603
|
-
const url = validateId(id);
|
|
1390
|
+
const url = await validateId(id);
|
|
604
1391
|
try {
|
|
605
|
-
const response = await
|
|
606
|
-
...fetchOptions,
|
|
607
|
-
method: "HEAD"
|
|
608
|
-
});
|
|
1392
|
+
const response = await fetchURL(url, { method: "HEAD" });
|
|
609
1393
|
if (!response.ok) return {};
|
|
610
1394
|
const { maxAge, mtime } = parseResponse(response);
|
|
611
1395
|
return {
|
|
@@ -617,8 +1401,7 @@ function ipxHttpStorage(_options = {}) {
|
|
|
617
1401
|
}
|
|
618
1402
|
},
|
|
619
1403
|
async getData(id) {
|
|
620
|
-
const
|
|
621
|
-
const response = await fetch(url, fetchOptions);
|
|
1404
|
+
const response = await fetchURL(await validateId(id));
|
|
622
1405
|
if (!response.ok) throw new HTTPError({
|
|
623
1406
|
statusCode: response.status,
|
|
624
1407
|
statusText: response.statusText,
|
|
@@ -628,33 +1411,66 @@ function ipxHttpStorage(_options = {}) {
|
|
|
628
1411
|
}
|
|
629
1412
|
};
|
|
630
1413
|
}
|
|
1414
|
+
const { join, parse, resolve, relative, isAbsolute, sep } = /* #__PURE__ */ getBuiltinModule("node:path") || {};
|
|
1415
|
+
const NOT_IN_THIS_DIR = /* @__PURE__ */ new Set([
|
|
1416
|
+
"ENOENT",
|
|
1417
|
+
"ENOTDIR",
|
|
1418
|
+
"ELOOP",
|
|
1419
|
+
"ENAMETOOLONG"
|
|
1420
|
+
]);
|
|
631
1421
|
function ipxFSStorage(_options = {}) {
|
|
1422
|
+
const fs = getBuiltinModule("node:fs/promises");
|
|
1423
|
+
if (!fs) throw new Error("ipxFSStorage requires the `node:fs/promises` builtin, which this runtime does not provide");
|
|
632
1424
|
const dirs = resolveDirs(_options.dir);
|
|
633
|
-
const maxAge = _options.maxAge
|
|
634
|
-
const
|
|
1425
|
+
const maxAge = _options.maxAge ?? getEnv("IPX_FS_MAX_AGE");
|
|
1426
|
+
const allowSymlinksOutsideDir = _options.allowSymlinksOutsideDir ?? getEnv("IPX_FS_ALLOW_SYMLINKS_OUTSIDE_DIR") ?? false;
|
|
1427
|
+
const realDirCache = [];
|
|
1428
|
+
const getRealDir = (index, dir) => {
|
|
1429
|
+
let realDir = realDirCache[index];
|
|
1430
|
+
if (!realDir) {
|
|
1431
|
+
realDir = fs.realpath(dir).catch(() => dir);
|
|
1432
|
+
realDirCache[index] = realDir;
|
|
1433
|
+
}
|
|
1434
|
+
return realDir;
|
|
1435
|
+
};
|
|
635
1436
|
const resolveFile = async (id) => {
|
|
636
|
-
for (const dir of dirs) {
|
|
1437
|
+
for (const [index, dir] of dirs.entries()) {
|
|
637
1438
|
const filePath = join(dir, id);
|
|
638
|
-
if (!isValidPath(filePath) || !filePath
|
|
1439
|
+
if (!isValidPath(filePath) || !isInsideDir(filePath, dir)) throw new HTTPError({
|
|
639
1440
|
statusCode: 403,
|
|
640
1441
|
statusText: `IPX_FORBIDDEN_PATH`,
|
|
641
1442
|
message: `Forbidden path: ${id}`
|
|
642
1443
|
});
|
|
1444
|
+
let stats;
|
|
1445
|
+
let realPath;
|
|
643
1446
|
try {
|
|
644
|
-
|
|
645
|
-
if (
|
|
646
|
-
return {
|
|
647
|
-
stats,
|
|
648
|
-
read: () => fs.readFile(filePath)
|
|
649
|
-
};
|
|
1447
|
+
stats = await fs.stat(filePath);
|
|
1448
|
+
if (stats.isFile() && !allowSymlinksOutsideDir) realPath = await fs.realpath(filePath);
|
|
650
1449
|
} catch (error) {
|
|
651
|
-
if (error.code
|
|
1450
|
+
if (NOT_IN_THIS_DIR.has(error.code)) continue;
|
|
652
1451
|
throw new HTTPError({
|
|
653
1452
|
statusCode: 403,
|
|
654
1453
|
statusText: `IPX_FORBIDDEN_FILE`,
|
|
655
1454
|
message: `Cannot access file: ${id}`
|
|
656
1455
|
});
|
|
657
1456
|
}
|
|
1457
|
+
if (!stats.isFile()) continue;
|
|
1458
|
+
if (realPath !== void 0) {
|
|
1459
|
+
let realDir = await getRealDir(index, dir);
|
|
1460
|
+
if (!isInsideDir(realPath, realDir)) {
|
|
1461
|
+
realDirCache[index] = void 0;
|
|
1462
|
+
realDir = await getRealDir(index, dir);
|
|
1463
|
+
}
|
|
1464
|
+
if (!isInsideDir(realPath, realDir)) throw new HTTPError({
|
|
1465
|
+
statusCode: 403,
|
|
1466
|
+
statusText: `IPX_FORBIDDEN_SYMLINK`,
|
|
1467
|
+
message: `Forbidden symlink: ${id}`
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
return {
|
|
1471
|
+
stats,
|
|
1472
|
+
read: () => fs.readFile(realPath ?? filePath)
|
|
1473
|
+
};
|
|
658
1474
|
}
|
|
659
1475
|
throw new HTTPError({
|
|
660
1476
|
statusCode: 404,
|
|
@@ -677,14 +1493,22 @@ function ipxFSStorage(_options = {}) {
|
|
|
677
1493
|
}
|
|
678
1494
|
};
|
|
679
1495
|
}
|
|
680
|
-
const isWindows = process
|
|
1496
|
+
const isWindows = globalThis.process?.platform === "win32";
|
|
681
1497
|
function isValidPath(fp) {
|
|
682
1498
|
if (isWindows) fp = fp.slice(parse(fp).root.length);
|
|
683
1499
|
if (/["*:<>?|]/.test(fp)) return false;
|
|
684
1500
|
return true;
|
|
685
1501
|
}
|
|
1502
|
+
function isInsideDir(filePath, dir) {
|
|
1503
|
+
if (isWindows) {
|
|
1504
|
+
filePath = filePath.toLowerCase();
|
|
1505
|
+
dir = dir.toLowerCase();
|
|
1506
|
+
}
|
|
1507
|
+
const rel = relative(dir, filePath);
|
|
1508
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
1509
|
+
}
|
|
686
1510
|
function resolveDirs(dirs) {
|
|
687
1511
|
if (!dirs || !Array.isArray(dirs)) return [resolve(dirs || getEnv("IPX_FS_DIR") || ".")];
|
|
688
1512
|
return dirs.map((dirs) => resolve(dirs));
|
|
689
1513
|
}
|
|
690
|
-
export { createIPX, createIPXFetchHandler, createIPXNodeHandler, ipxFSStorage, ipxHttpStorage, serveIPX };
|
|
1514
|
+
export { createIPX, createIPXFetchHandler, createIPXNodeHandler, ipxFSStorage, ipxHttpStorage, parseIPXURL, serveIPX };
|