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