ipx 3.1.1 → 4.0.0-alpha.2

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.
@@ -0,0 +1,690 @@
1
+ import { __exportAll, __toESM } from "./rolldown-runtime.mjs";
2
+ import { decode, hasProtocol, joinURL, withLeadingSlash } from "./libs/ufo.mjs";
3
+ import { HTTPError, defineEventHandler } from "./libs/h3.mjs";
4
+ import { imageMeta } from "./libs/image-meta.mjs";
5
+ import { require_etag } from "./libs/etag.mjs";
6
+ import { require_accept_negotiator } from "./libs/@fastify/accept-negotiator.mjs";
7
+ import { join, parse, resolve } from "pathe";
8
+ function VArg(argument) {
9
+ if (argument === "Infinity") return Infinity;
10
+ if (argument === "undefined") return;
11
+ try {
12
+ const val = JSON.parse(argument);
13
+ const t = typeof val;
14
+ if (t === "boolean" || t === "number" || t === "string" || val === null) return val;
15
+ } catch {}
16
+ return argument;
17
+ }
18
+ function parseArgs(arguments_, mappers) {
19
+ const vargs = arguments_.split("_");
20
+ return mappers.map((v, index) => v(vargs[index]));
21
+ }
22
+ function getHandler(key) {
23
+ return handlers_exports[key];
24
+ }
25
+ function applyHandler(context, pipe, handler, argumentsString) {
26
+ const arguments_ = handler.args ? parseArgs(argumentsString, handler.args) : [];
27
+ return handler.apply(context, pipe, ...arguments_);
28
+ }
29
+ function clampDimensionsPreservingAspectRatio(sourceDimensions, desiredDimensions) {
30
+ const desiredAspectRatio = desiredDimensions.width / desiredDimensions.height;
31
+ let { width, height } = desiredDimensions;
32
+ if (sourceDimensions.width && width > sourceDimensions.width) {
33
+ width = sourceDimensions.width;
34
+ height = Math.round(sourceDimensions.width / desiredAspectRatio);
35
+ }
36
+ if (sourceDimensions.height && height > sourceDimensions.height) {
37
+ height = sourceDimensions.height;
38
+ width = Math.round(sourceDimensions.height * desiredAspectRatio);
39
+ }
40
+ return {
41
+ width,
42
+ height
43
+ };
44
+ }
45
+ var handlers_exports = /* @__PURE__ */ __exportAll({
46
+ b: () => b,
47
+ background: () => background,
48
+ blur: () => blur,
49
+ crop: () => crop,
50
+ enlarge: () => enlarge,
51
+ extend: () => extend,
52
+ extract: () => extract,
53
+ fit: () => fit,
54
+ flatten: () => flatten,
55
+ flip: () => flip,
56
+ flop: () => flop,
57
+ gamma: () => gamma,
58
+ grayscale: () => grayscale,
59
+ h: () => h,
60
+ height: () => height,
61
+ kernel: () => kernel,
62
+ median: () => median,
63
+ modulate: () => modulate,
64
+ negate: () => negate,
65
+ normalize: () => normalize,
66
+ pos: () => pos,
67
+ position: () => position,
68
+ q: () => q,
69
+ quality: () => quality,
70
+ resize: () => resize,
71
+ rotate: () => rotate,
72
+ s: () => s,
73
+ sharpen: () => sharpen,
74
+ threshold: () => threshold,
75
+ tint: () => tint,
76
+ trim: () => trim,
77
+ w: () => w,
78
+ width: () => width
79
+ });
80
+ const quality = {
81
+ args: [VArg],
82
+ order: -1,
83
+ apply: (context, _pipe, quality) => {
84
+ context.quality = quality;
85
+ }
86
+ };
87
+ const fit = {
88
+ args: [VArg],
89
+ order: -1,
90
+ apply: (context, _pipe, fit) => {
91
+ context.fit = fit;
92
+ }
93
+ };
94
+ const position = {
95
+ args: [VArg],
96
+ order: -1,
97
+ apply: (context, _pipe, position) => {
98
+ context.position = position;
99
+ }
100
+ };
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
+ const background = {
104
+ args: [VArg],
105
+ order: -1,
106
+ apply: (context, _pipe, background) => {
107
+ background = String(background);
108
+ if (!background.startsWith("#") && (HEX_RE.test(background) || SHORTHEX_RE.test(background))) background = "#" + background;
109
+ context.background = background;
110
+ }
111
+ };
112
+ const enlarge = {
113
+ args: [],
114
+ apply: (context) => {
115
+ context.enlarge = true;
116
+ }
117
+ };
118
+ const kernel = {
119
+ args: [VArg],
120
+ apply: (context, _pipe, kernel) => {
121
+ context.kernel = kernel;
122
+ }
123
+ };
124
+ const width = {
125
+ args: [VArg],
126
+ apply: (context, pipe, width) => {
127
+ return pipe.resize(width, void 0, { withoutEnlargement: !context.enlarge });
128
+ }
129
+ };
130
+ const height = {
131
+ args: [VArg],
132
+ apply: (context, pipe, height) => {
133
+ return pipe.resize(void 0, height, { withoutEnlargement: !context.enlarge });
134
+ }
135
+ };
136
+ const resize = {
137
+ args: [
138
+ VArg,
139
+ VArg,
140
+ VArg
141
+ ],
142
+ apply: (context, pipe, size) => {
143
+ let [width, height] = String(size).split("x").map(Number);
144
+ if (!width) return;
145
+ if (!height) height = width;
146
+ if (!context.enlarge) {
147
+ const clamped = clampDimensionsPreservingAspectRatio(context.meta, {
148
+ width,
149
+ height
150
+ });
151
+ width = clamped.width;
152
+ height = clamped.height;
153
+ }
154
+ return pipe.resize(width, height, {
155
+ fit: context.fit,
156
+ position: context.position,
157
+ background: context.background,
158
+ kernel: context.kernel
159
+ });
160
+ }
161
+ };
162
+ const trim = {
163
+ args: [VArg],
164
+ apply: (_context, pipe, threshold) => {
165
+ return pipe.trim(threshold);
166
+ }
167
+ };
168
+ const extend = {
169
+ args: [
170
+ VArg,
171
+ VArg,
172
+ VArg,
173
+ VArg
174
+ ],
175
+ apply: (context, pipe, top, right, bottom, left) => {
176
+ return pipe.extend({
177
+ top,
178
+ left,
179
+ bottom,
180
+ right,
181
+ background: context.background
182
+ });
183
+ }
184
+ };
185
+ const extract = {
186
+ args: [
187
+ VArg,
188
+ VArg,
189
+ VArg,
190
+ VArg
191
+ ],
192
+ apply: (_context, pipe, left, top, width, height) => {
193
+ return pipe.extract({
194
+ left,
195
+ top,
196
+ width,
197
+ height
198
+ });
199
+ }
200
+ };
201
+ const rotate = {
202
+ args: [VArg],
203
+ apply: (context, pipe, angel) => {
204
+ return pipe.rotate(angel, { background: context.background });
205
+ }
206
+ };
207
+ const flip = {
208
+ args: [],
209
+ apply: (_context, pipe) => {
210
+ return pipe.flip();
211
+ }
212
+ };
213
+ const flop = {
214
+ args: [],
215
+ apply: (_context, pipe) => {
216
+ return pipe.flop();
217
+ }
218
+ };
219
+ const sharpen = {
220
+ args: [
221
+ VArg,
222
+ VArg,
223
+ VArg
224
+ ],
225
+ apply: (_context, pipe, sigma, flat, jagged) => {
226
+ return pipe.sharpen({
227
+ sigma,
228
+ m1: flat,
229
+ m2: jagged
230
+ });
231
+ }
232
+ };
233
+ const median = {
234
+ args: [
235
+ VArg,
236
+ VArg,
237
+ VArg
238
+ ],
239
+ apply: (_context, pipe, size) => {
240
+ return pipe.median(size);
241
+ }
242
+ };
243
+ const blur = {
244
+ args: [
245
+ VArg,
246
+ VArg,
247
+ VArg
248
+ ],
249
+ apply: (_context, pipe, sigma) => {
250
+ return pipe.blur(sigma);
251
+ }
252
+ };
253
+ const flatten = {
254
+ args: [
255
+ VArg,
256
+ VArg,
257
+ VArg
258
+ ],
259
+ apply: (context, pipe) => {
260
+ return pipe.flatten({ background: context.background });
261
+ }
262
+ };
263
+ const gamma = {
264
+ args: [
265
+ VArg,
266
+ VArg,
267
+ VArg
268
+ ],
269
+ apply: (_context, pipe, gamma, gammaOut) => {
270
+ return pipe.gamma(gamma, gammaOut);
271
+ }
272
+ };
273
+ const negate = {
274
+ args: [
275
+ VArg,
276
+ VArg,
277
+ VArg
278
+ ],
279
+ apply: (_context, pipe) => {
280
+ return pipe.negate();
281
+ }
282
+ };
283
+ const normalize = {
284
+ args: [
285
+ VArg,
286
+ VArg,
287
+ VArg
288
+ ],
289
+ apply: (_context, pipe) => {
290
+ return pipe.normalize();
291
+ }
292
+ };
293
+ const threshold = {
294
+ args: [VArg],
295
+ apply: (_context, pipe, threshold) => {
296
+ return pipe.threshold(threshold);
297
+ }
298
+ };
299
+ const modulate = {
300
+ args: [VArg],
301
+ apply: (_context, pipe, brightness, saturation, hue) => {
302
+ return pipe.modulate({
303
+ brightness,
304
+ saturation,
305
+ hue
306
+ });
307
+ }
308
+ };
309
+ const tint = {
310
+ args: [VArg],
311
+ apply: (_context, pipe, rgb) => {
312
+ return pipe.tint(rgb);
313
+ }
314
+ };
315
+ const grayscale = {
316
+ args: [VArg],
317
+ apply: (_context, pipe) => {
318
+ return pipe.grayscale();
319
+ }
320
+ };
321
+ const crop = extract;
322
+ const q = quality;
323
+ const b = background;
324
+ const w = width;
325
+ const h = height;
326
+ const s = resize;
327
+ const pos = position;
328
+ function getEnv(name) {
329
+ const value = globalThis.process?.env?.[name];
330
+ if (value !== void 0) return JSON.parse(value);
331
+ }
332
+ function requireModule(id) {
333
+ const { createRequire } = globalThis.process.getBuiltinModule("node:module");
334
+ return createRequire(import.meta.url)(id);
335
+ }
336
+ function cachedPromise(function_) {
337
+ let p;
338
+ return (...arguments_) => {
339
+ if (p) return p;
340
+ p = Promise.resolve(function_(...arguments_));
341
+ return p;
342
+ };
343
+ }
344
+ const SUPPORTED_FORMATS = /* @__PURE__ */ new Set([
345
+ "jpeg",
346
+ "png",
347
+ "webp",
348
+ "avif",
349
+ "tiff",
350
+ "heif",
351
+ "gif",
352
+ "heic"
353
+ ]);
354
+ function createIPX(userOptions) {
355
+ const options = {
356
+ ...userOptions,
357
+ alias: userOptions.alias || getEnv("IPX_ALIAS") || {},
358
+ maxAge: userOptions.maxAge ?? getEnv("IPX_MAX_AGE") ?? 60,
359
+ sharpOptions: {
360
+ jpegProgressive: true,
361
+ ...userOptions.sharpOptions
362
+ }
363
+ };
364
+ options.alias = Object.fromEntries(Object.entries(options.alias || {}).map((e) => [withLeadingSlash(e[0]), e[1]]));
365
+ const getSharp = cachedPromise(async () => {
366
+ return await import("sharp").then((r) => r.default || r);
367
+ });
368
+ const getSVGO = cachedPromise(async () => {
369
+ const { optimize } = await import("./svgo-node.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1));
370
+ return { optimize };
371
+ });
372
+ return function ipx(id, modifiers = {}, opts = {}) {
373
+ if (!id) throw new HTTPError({
374
+ statusCode: 400,
375
+ statusText: `IPX_MISSING_ID`,
376
+ message: `Resource id is missing`
377
+ });
378
+ id = hasProtocol(id) ? id : withLeadingSlash(id);
379
+ for (const base in options.alias) if (id.startsWith(base)) id = joinURL(options.alias[base], id.slice(base.length));
380
+ const storage = hasProtocol(id) ? options.httpStorage || options.storage : options.storage || options.httpStorage;
381
+ if (!storage) throw new HTTPError({
382
+ statusCode: 500,
383
+ statusText: `IPX_NO_STORAGE`,
384
+ message: "No storage configured!"
385
+ });
386
+ const getSourceMeta = cachedPromise(async () => {
387
+ const sourceMeta = await storage.getMeta(id, opts);
388
+ if (!sourceMeta) throw new HTTPError({
389
+ statusCode: 404,
390
+ statusText: `IPX_RESOURCE_NOT_FOUND`,
391
+ message: `Resource not found: ${id}`
392
+ });
393
+ const _maxAge = sourceMeta.maxAge ?? options.maxAge;
394
+ return {
395
+ maxAge: typeof _maxAge === "string" ? Number.parseInt(_maxAge) : _maxAge,
396
+ mtime: sourceMeta.mtime ? new Date(sourceMeta.mtime) : void 0
397
+ };
398
+ });
399
+ const getSourceData = cachedPromise(async () => {
400
+ const sourceData = await storage.getData(id, opts);
401
+ if (!sourceData) throw new HTTPError({
402
+ statusCode: 404,
403
+ statusText: `IPX_RESOURCE_NOT_FOUND`,
404
+ message: `Resource not found: ${id}`
405
+ });
406
+ return Buffer.from(sourceData);
407
+ });
408
+ return {
409
+ getSourceMeta,
410
+ process: cachedPromise(async () => {
411
+ const sourceData = await getSourceData();
412
+ let imageMeta$1;
413
+ try {
414
+ imageMeta$1 = imageMeta(sourceData);
415
+ } catch {
416
+ throw new HTTPError({
417
+ statusCode: 400,
418
+ statusText: `IPX_INVALID_IMAGE`,
419
+ message: `Cannot parse image metadata: ${id}`
420
+ });
421
+ }
422
+ let mFormat = modifiers.f || modifiers.format;
423
+ if (mFormat === "jpg") mFormat = "jpeg";
424
+ const format = mFormat && SUPPORTED_FORMATS.has(mFormat) ? mFormat : SUPPORTED_FORMATS.has(imageMeta$1.type || "") ? imageMeta$1.type : "jpeg";
425
+ if (imageMeta$1.type === "svg" && !mFormat) if (options.svgo === false) return {
426
+ data: sourceData,
427
+ format: "svg+xml",
428
+ meta: imageMeta$1
429
+ };
430
+ else {
431
+ const { optimize } = await getSVGO();
432
+ return {
433
+ data: optimize(sourceData.toString("utf8"), {
434
+ ...options.svgo,
435
+ plugins: ["removeScripts", ...options.svgo?.plugins || []]
436
+ }).data,
437
+ format: "svg+xml",
438
+ meta: imageMeta$1
439
+ };
440
+ }
441
+ const animated = modifiers.animated !== void 0 || modifiers.a !== void 0 || format === "gif";
442
+ let sharp = (await getSharp())(sourceData, {
443
+ animated,
444
+ ...options.sharpOptions
445
+ });
446
+ Object.assign(sharp.options, options.sharpOptions);
447
+ const handlers = Object.entries(modifiers).map(([name, arguments_]) => ({
448
+ handler: getHandler(name),
449
+ name,
450
+ args: arguments_
451
+ })).filter((h) => h.handler).sort((a, b) => {
452
+ const aKey = (a.handler.order || a.name || "").toString();
453
+ const bKey = (b.handler.order || b.name || "").toString();
454
+ return aKey.localeCompare(bKey);
455
+ });
456
+ const handlerContext = { meta: imageMeta$1 };
457
+ for (const h of handlers) sharp = applyHandler(handlerContext, sharp, h.handler, h.args.toString()) || sharp;
458
+ if (SUPPORTED_FORMATS.has(format || "")) sharp = sharp.toFormat(format, { quality: handlerContext.quality });
459
+ return {
460
+ data: await sharp.toBuffer(),
461
+ format,
462
+ meta: imageMeta$1
463
+ };
464
+ })
465
+ };
466
+ };
467
+ }
468
+ var import_etag = /* @__PURE__ */ __toESM(require_etag(), 1);
469
+ var import_accept_negotiator = require_accept_negotiator();
470
+ function createIPXFetchHandler(ipx) {
471
+ return createIPXHandler(ipx).fetch;
472
+ }
473
+ function createIPXNodeHandler(ipx) {
474
+ const { toNodeHandler } = requireModule("srvx/node");
475
+ return toNodeHandler(createIPXFetchHandler(ipx));
476
+ }
477
+ function serveIPX(ipx, opts) {
478
+ const { serve } = requireModule("srvx");
479
+ const fetch = createIPXFetchHandler(ipx);
480
+ return serve({
481
+ ...opts,
482
+ fetch
483
+ });
484
+ }
485
+ const MODIFIER_SEP = /[&,]/g;
486
+ const MODIFIER_VAL_SEP = /[:=_]/;
487
+ function createIPXHandler(ipx) {
488
+ return defineEventHandler(async (event) => {
489
+ const [modifiersString = "", ...idSegments] = event.url.pathname.slice(1).split("/");
490
+ const id = safeString(decode(idSegments.join("/")));
491
+ if (!modifiersString) throw new HTTPError({
492
+ statusCode: 400,
493
+ statusText: "IPX_MISSING_MODIFIERS",
494
+ message: `Modifiers are missing: ${id}`
495
+ });
496
+ if (!id || id === "/") throw new HTTPError({
497
+ statusCode: 400,
498
+ statusText: "IPX_MISSING_ID",
499
+ message: `Resource id is missing: ${event.path}`
500
+ });
501
+ const modifiers = Object.create(null);
502
+ if (modifiersString !== "_") for (const p of modifiersString.split(MODIFIER_SEP)) {
503
+ const [key, ...values] = p.split(MODIFIER_VAL_SEP);
504
+ modifiers[safeString(key)] = values.map((v) => safeString(decode(v))).join("_");
505
+ }
506
+ if ((modifiers.f || modifiers.format) === "auto") {
507
+ const acceptHeader = event.req.headers.get("accept") || "";
508
+ const animated = modifiers.animated ?? modifiers.a;
509
+ const autoFormat = autoDetectFormat(acceptHeader, !!animated || animated === "");
510
+ delete modifiers.f;
511
+ delete modifiers.format;
512
+ if (autoFormat) {
513
+ modifiers.format = autoFormat;
514
+ event.res.headers.append("vary", "Accept");
515
+ }
516
+ }
517
+ const img = ipx(id, modifiers);
518
+ const sourceMeta = await img.getSourceMeta();
519
+ sendResponseHeaderIfNotSet(event, "content-security-policy", "default-src 'none'");
520
+ if (sourceMeta.mtime) {
521
+ sendResponseHeaderIfNotSet(event, "last-modified", sourceMeta.mtime.toUTCString());
522
+ const _ifModifiedSince = event.req.headers.get("if-modified-since");
523
+ if (_ifModifiedSince && new Date(_ifModifiedSince) >= sourceMeta.mtime) {
524
+ event.res.status = 304;
525
+ return;
526
+ }
527
+ }
528
+ const { data, format } = await img.process();
529
+ if (typeof sourceMeta.maxAge === "number") sendResponseHeaderIfNotSet(event, "cache-control", `max-age=${+sourceMeta.maxAge}, public, s-maxage=${+sourceMeta.maxAge}`);
530
+ const etag = (0, import_etag.default)(data);
531
+ sendResponseHeaderIfNotSet(event, "etag", etag);
532
+ if (etag && event.req.headers.get("if-none-match") === etag) {
533
+ event.res.status = 304;
534
+ return;
535
+ }
536
+ if (format) sendResponseHeaderIfNotSet(event, "content-type", `image/${format}`);
537
+ return data;
538
+ });
539
+ }
540
+ function sendResponseHeaderIfNotSet(event, name, value) {
541
+ if (!event.res.headers.has(name)) event.res.headers.set(name, value);
542
+ }
543
+ function autoDetectFormat(acceptHeader, animated) {
544
+ if (animated) return (0, import_accept_negotiator.negotiate)(acceptHeader, ["image/webp", "image/gif"])?.split("/")[1] || "gif";
545
+ return (0, import_accept_negotiator.negotiate)(acceptHeader, [
546
+ "image/avif",
547
+ "image/webp",
548
+ "image/jpeg",
549
+ "image/png",
550
+ "image/tiff",
551
+ "image/heif",
552
+ "image/gif"
553
+ ])?.split("/")[1] || "jpeg";
554
+ }
555
+ function safeString(input) {
556
+ return JSON.stringify(input).replace(/^"|"$/g, "").replace(/\\+/g, "\\").replace(/\\"/g, "\"");
557
+ }
558
+ const HTTP_RE = /^https?:\/\//;
559
+ function ipxHttpStorage(_options = {}) {
560
+ const allowAllDomains = _options.allowAllDomains ?? getEnv("IPX_HTTP_ALLOW_ALL_DOMAINS") ?? false;
561
+ let _domains = _options.domains || getEnv("IPX_HTTP_DOMAINS") || [];
562
+ const defaultMaxAge = _options.maxAge || getEnv("IPX_HTTP_MAX_AGE") || 300;
563
+ const fetchOptions = _options.fetchOptions || getEnv("IPX_HTTP_FETCH_OPTIONS") || {};
564
+ if (typeof _domains === "string") _domains = _domains.split(",").map((s) => s.trim());
565
+ const domains = new Set(_domains.map((d) => {
566
+ if (!HTTP_RE.test(d)) d = "http://" + d;
567
+ return new URL(d).hostname;
568
+ }).filter(Boolean));
569
+ function validateId(id) {
570
+ const url = new URL(decodeURIComponent(id));
571
+ if (!url.hostname) throw new HTTPError({
572
+ statusCode: 403,
573
+ statusText: `IPX_MISSING_HOSTNAME`,
574
+ message: `Hostname is missing: ${id}`
575
+ });
576
+ if (!allowAllDomains && !domains.has(url.hostname)) throw new HTTPError({
577
+ statusCode: 403,
578
+ statusText: `IPX_FORBIDDEN_HOST`,
579
+ message: `Forbidden host: ${url.hostname}`
580
+ });
581
+ return url.toString();
582
+ }
583
+ function parseResponse(response) {
584
+ let maxAge = defaultMaxAge;
585
+ if (_options.ignoreCacheControl !== true) {
586
+ 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
+ }
591
+ }
592
+ let mtime;
593
+ const _lastModified = response.headers.get("last-modified");
594
+ if (_lastModified) mtime = new Date(_lastModified);
595
+ return {
596
+ maxAge,
597
+ mtime
598
+ };
599
+ }
600
+ return {
601
+ name: "ipx:http",
602
+ async getMeta(id) {
603
+ const url = validateId(id);
604
+ try {
605
+ const response = await fetch(url, {
606
+ ...fetchOptions,
607
+ method: "HEAD"
608
+ });
609
+ if (!response.ok) return {};
610
+ const { maxAge, mtime } = parseResponse(response);
611
+ return {
612
+ mtime,
613
+ maxAge
614
+ };
615
+ } catch {
616
+ return {};
617
+ }
618
+ },
619
+ async getData(id) {
620
+ const url = validateId(id);
621
+ const response = await fetch(url, fetchOptions);
622
+ if (!response.ok) throw new HTTPError({
623
+ statusCode: response.status,
624
+ statusText: response.statusText,
625
+ message: `Failed to fetch ${id}: ${response.status} ${response.statusText}`
626
+ });
627
+ return await response.arrayBuffer();
628
+ }
629
+ };
630
+ }
631
+ function ipxFSStorage(_options = {}) {
632
+ const dirs = resolveDirs(_options.dir);
633
+ const maxAge = _options.maxAge || getEnv("IPX_FS_MAX_AGE");
634
+ const fs = globalThis.process.getBuiltinModule("node:fs/promises");
635
+ const resolveFile = async (id) => {
636
+ for (const dir of dirs) {
637
+ const filePath = join(dir, id);
638
+ if (!isValidPath(filePath) || !filePath.startsWith(dir + "/")) throw new HTTPError({
639
+ statusCode: 403,
640
+ statusText: `IPX_FORBIDDEN_PATH`,
641
+ message: `Forbidden path: ${id}`
642
+ });
643
+ try {
644
+ const stats = await fs.stat(filePath);
645
+ if (!stats.isFile()) continue;
646
+ return {
647
+ stats,
648
+ read: () => fs.readFile(filePath)
649
+ };
650
+ } catch (error) {
651
+ if (error.code === "ENOENT") continue;
652
+ throw new HTTPError({
653
+ statusCode: 403,
654
+ statusText: `IPX_FORBIDDEN_FILE`,
655
+ message: `Cannot access file: ${id}`
656
+ });
657
+ }
658
+ }
659
+ throw new HTTPError({
660
+ statusCode: 404,
661
+ statusText: `IPX_FILE_NOT_FOUND`,
662
+ message: `File not found: ${id}`
663
+ });
664
+ };
665
+ return {
666
+ name: "ipx:node-fs",
667
+ async getMeta(id) {
668
+ const { stats } = await resolveFile(id);
669
+ return {
670
+ mtime: stats.mtime,
671
+ maxAge
672
+ };
673
+ },
674
+ async getData(id) {
675
+ const { read } = await resolveFile(id);
676
+ return read();
677
+ }
678
+ };
679
+ }
680
+ const isWindows = process.platform === "win32";
681
+ function isValidPath(fp) {
682
+ if (isWindows) fp = fp.slice(parse(fp).root.length);
683
+ if (/["*:<>?|]/.test(fp)) return false;
684
+ return true;
685
+ }
686
+ function resolveDirs(dirs) {
687
+ if (!dirs || !Array.isArray(dirs)) return [resolve(dirs || getEnv("IPX_FS_DIR") || ".")];
688
+ return dirs.map((dirs) => resolve(dirs));
689
+ }
690
+ export { createIPX, createIPXFetchHandler, createIPXNodeHandler, ipxFSStorage, ipxHttpStorage, serveIPX };