@websolutespa/bom-mixer-image 3.0.0 → 3.1.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/CHANGELOG.md CHANGED
@@ -1,110 +1,133 @@
1
- # @websolutespa/bom-mixer-image
2
-
3
- ## 3.0.0
4
-
5
- ### Major Changes
6
-
7
- - Release: Bom 3.0
8
-
9
- ## 1.8.4
10
-
11
- ### Patch Changes
12
-
13
- - Fixing: handling latin1 src
14
-
15
- ## 1.8.3
16
-
17
- ### Patch Changes
18
-
19
- - Added: ImageStrategy.Token
20
- - Updated dependencies
21
- - @websolutespa/bom-core@1.8.4
22
-
23
- ## 1.8.2
24
-
25
- ### Patch Changes
26
-
27
- - Modified: imageHandler
28
-
29
- ## 1.8.1
30
-
31
- ### Patch Changes
32
-
33
- - Modified: imageHandler
34
-
35
- ## 1.8.0
36
-
37
- ### Patch Changes
38
-
39
- - Updated dependencies [f8a1d97]
40
- - @websolutespa/bom-core@1.8.0
41
-
42
- ## 1.0.9
43
-
44
- ### Patch Changes
45
-
46
- - Fixing: downgrading eslint to v8.56.0
47
- - Updated dependencies
48
- - @websolutespa/bom-core@1.7.8
49
-
50
- ## 1.0.8
51
-
52
- ### Patch Changes
53
-
54
- - Added: Cache-Control
55
-
56
- ## 1.0.7
57
-
58
- ### Patch Changes
59
-
60
- - Fixing: imageHandler Vercel support
61
- - Updated dependencies
62
- - @websolutespa/bom-core@1.7.7
63
-
64
- ## 1.0.6
65
-
66
- ### Patch Changes
67
-
68
- - Modified: imageHandler, re-route not found to origin
69
-
70
- ## 1.0.5
71
-
72
- ### Patch Changes
73
-
74
- - Modified: imageHandler
75
- - Updated dependencies
76
- - @websolutespa/bom-core@1.7.6
77
-
78
- ## 1.0.4
79
-
80
- ### Patch Changes
81
-
82
- - Modified: imageHandler getOriginalImage
83
- - Updated dependencies
84
- - @websolutespa/bom-core@1.7.5
85
-
86
- ## 1.0.3
87
-
88
- ### Patch Changes
89
-
90
- - 43957d4: Fixing: production local asset public path
91
- - Updated dependencies
92
- - @websolutespa/bom-core@1.7.4
93
-
94
- ## 1.0.2
95
-
96
- ### Patch Changes
97
-
98
- - 75b1311: Fixing: relative paths
99
-
100
- ## 1.0.1
101
-
102
- ### Patch Changes
103
-
104
- - Updating: dependencies
105
-
106
- ## 1.0.0
107
-
108
- ### Patch Changes
109
-
110
- - Added: first release
1
+ # @websolutespa/bom-mixer-image
2
+
3
+ ## 3.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3f7c21b: - Payload 3.91.0 and Next.js 16.4.2 (React 19.2.9): peer dependencies are now ranges, `payload` / `@payloadcms/*` `^3.91.0`, `next` `^16.3.3`, `react` / `react-dom` `^19.2.1`
8
+ - payload-plugin-bowl: the mixer context is built only for reads by an authenticated user (payload 3.90+ resolves the jwt user with the request query, populating tenants before `req.user` is set); email options typed with payload `SendEmailOptions`, no direct nodemailer dependency
9
+ - payload-plugin-storage: `AZURE_STORAGE_CONTAINER_ACCESS` (`private` default since payload 3.90, `blob`, `container`) for containers created by the plugin
10
+ - mixer web: `images.qualities` includes `IMAGE_QUALITY` (next 16 defaults to `[75]`)
11
+
12
+ ### Patch Changes
13
+
14
+ - 12b6e2f: - test: replace the jest presets with shared vitest configurations, `@websolutespa/test/vitest` (`defineNodeConfig`, `defineDomConfig`, `definePayloadConfig`); drop jest dependencies
15
+ - declare every imported package and script binary in the package that uses it (no more reliance on hoisting), drop unused dependencies
16
+ - bom-mixer-checkout: `useCart.add` no longer mutates the previous cart state
17
+
18
+ ## 3.0.1
19
+
20
+ ### Patch Changes
21
+
22
+ - Updating: dependencies
23
+ - Updated dependencies
24
+ - @websolutespa/bom-core@3.0.1
25
+
26
+ ## 3.0.0
27
+
28
+ ### Major Changes
29
+
30
+ - Release: Bom 3.0
31
+
32
+ ## 1.8.4
33
+
34
+ ### Patch Changes
35
+
36
+ - Fixing: handling latin1 src
37
+
38
+ ## 1.8.3
39
+
40
+ ### Patch Changes
41
+
42
+ - Added: ImageStrategy.Token
43
+ - Updated dependencies
44
+ - @websolutespa/bom-core@1.8.4
45
+
46
+ ## 1.8.2
47
+
48
+ ### Patch Changes
49
+
50
+ - Modified: imageHandler
51
+
52
+ ## 1.8.1
53
+
54
+ ### Patch Changes
55
+
56
+ - Modified: imageHandler
57
+
58
+ ## 1.8.0
59
+
60
+ ### Patch Changes
61
+
62
+ - Updated dependencies [f8a1d97]
63
+ - @websolutespa/bom-core@1.8.0
64
+
65
+ ## 1.0.9
66
+
67
+ ### Patch Changes
68
+
69
+ - Fixing: downgrading eslint to v8.56.0
70
+ - Updated dependencies
71
+ - @websolutespa/bom-core@1.7.8
72
+
73
+ ## 1.0.8
74
+
75
+ ### Patch Changes
76
+
77
+ - Added: Cache-Control
78
+
79
+ ## 1.0.7
80
+
81
+ ### Patch Changes
82
+
83
+ - Fixing: imageHandler Vercel support
84
+ - Updated dependencies
85
+ - @websolutespa/bom-core@1.7.7
86
+
87
+ ## 1.0.6
88
+
89
+ ### Patch Changes
90
+
91
+ - Modified: imageHandler, re-route not found to origin
92
+
93
+ ## 1.0.5
94
+
95
+ ### Patch Changes
96
+
97
+ - Modified: imageHandler
98
+ - Updated dependencies
99
+ - @websolutespa/bom-core@1.7.6
100
+
101
+ ## 1.0.4
102
+
103
+ ### Patch Changes
104
+
105
+ - Modified: imageHandler getOriginalImage
106
+ - Updated dependencies
107
+ - @websolutespa/bom-core@1.7.5
108
+
109
+ ## 1.0.3
110
+
111
+ ### Patch Changes
112
+
113
+ - 43957d4: Fixing: production local asset public path
114
+ - Updated dependencies
115
+ - @websolutespa/bom-core@1.7.4
116
+
117
+ ## 1.0.2
118
+
119
+ ### Patch Changes
120
+
121
+ - 75b1311: Fixing: relative paths
122
+
123
+ ## 1.0.1
124
+
125
+ ### Patch Changes
126
+
127
+ - Updating: dependencies
128
+
129
+ ## 1.0.0
130
+
131
+ ### Patch Changes
132
+
133
+ - Added: first release
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { NextRequest } from "next/server";
1
2
  import { IImageResize } from "@websolutespa/bom-core";
2
3
 
3
4
  //#region src/image/image.handler.d.ts
@@ -11,13 +12,23 @@ type IImageOptions = IImageResize & {
11
12
  search: string;
12
13
  url: string;
13
14
  };
15
+ type IImageHandlerNextContext = {
16
+ params: Promise<{
17
+ slugs?: string[];
18
+ }> | {
19
+ slugs?: string[];
20
+ };
21
+ };
14
22
  declare function imageHandler({
15
23
  domains,
16
24
  sizes,
17
25
  quality
18
26
  }: IImageHandlerOptions): any;
19
- //# sourceMappingURL=image.handler.d.ts.map
20
-
27
+ declare function imageGetHandler({
28
+ domains,
29
+ sizes,
30
+ quality
31
+ }: IImageHandlerOptions): (request: NextRequest, context: IImageHandlerNextContext) => Promise<Response>;
21
32
  //#endregion
22
- export { IImageHandlerOptions, IImageOptions, imageHandler };
33
+ export { IImageHandlerOptions, IImageOptions, imageGetHandler, imageHandler };
23
34
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/image/image.handler.ts"],"sourcesContent":[],"mappings":";;;KAsBY,oBAAA;;EAAA,KAAA,EAAA,MAAA,EAAA;EAMA,OAAA,EAAA,MAAA;AAOZ,CAAA;AAA4B,KAPhB,aAAA,GAAgB,YAOA,GAAA;EAAA,MAC1B,EAAA,MAAA;EAAY,MACZ,EAAA,MAAA;EAAU,GACV,EAAA,MAAA;CAAuB;AACF,iBAJP,YAAA,CAIO;EAAA,OAAA;EAAA,KAAA;EAAA;AAAA,CAAA,EAApB,oBAAoB,CAAA,EAAA,GAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/image/image.handler.ts"],"mappings":";;;;KAsBY,oBAAA;EACV,OAAA;EACA,KAAA;EACA,OAAA;AAAA;AAAA,KAGU,aAAA,GAAgB,YAAA;EAC1B,MAAA;EACA,MAAA;EACA,GAAA;AAAA;AAAA,KAGG,wBAAA;EACH,MAAA,EAAQ,OAAA;IAAU,KAAA;EAAA;IAAwB,KAAA;EAAA;AAAA;AAAA,iBAG5B,YAAA,CAAA;EACd,OAAA;EACA,KAAA;EACA;AAAA,GACC,oBAAA;AAAA,iBAsFa,eAAA,CAAA;EACd,OAAA;EACA,KAAA;EACA;AAAA,GACC,oBAAA,IACa,OAAA,EAAS,WAAA,EAAa,OAAA,EAAS,wBAAA,KAA2B,OAAA,CAAQ,QAAA"}
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as fs from "fs";
2
+ import { NextResponse } from "next/server";
2
3
  import * as path from "path";
3
4
  import sharp from "sharp";
4
5
  import { ImageStrategy, decryptImageResize } from "@websolutespa/bom-core";
5
-
6
6
  //#region src/api/api.handler.ts
7
7
  function apiHandler(handler) {
8
8
  return async (request, response) => {
@@ -17,15 +17,13 @@ function apiHandler(handler) {
17
17
  }
18
18
  function errorHandler(error, response) {
19
19
  if (typeof error === "string") {
20
- const is404 = error.toLowerCase().endsWith("not found");
21
- const statusCode = is404 ? 404 : 400;
20
+ const statusCode = error.toLowerCase().endsWith("not found") ? 404 : 400;
22
21
  return response.status(statusCode).json({ message: error });
23
22
  }
24
23
  if (error.name === "UnauthorizedError") return response.status(401).json({ message: "Invalid Token" });
25
24
  console.error(error);
26
25
  return response.status(500).json({ message: error.message });
27
26
  }
28
-
29
27
  //#endregion
30
28
  //#region src/image/image.handler.ts
31
29
  const IMAGE_STRATEGY = process.env.IMAGE_STRATEGY ? process.env.IMAGE_STRATEGY : ImageStrategy.Mixer;
@@ -60,11 +58,9 @@ function imageHandler({ domains = [], sizes = [], quality = IMAGE_QUALITY }) {
60
58
  }
61
59
  if (isInvalidReferer(request)) return response.status(401).send("you are not authorized to perform this action");
62
60
  if (imageOptions.url.indexOf("http") === -1 && isVercel()) response.redirect(307, imageOptions.url);
63
- const originalImageBuffer = await getOriginalImage(imageOptions.url, domains, request.headers);
61
+ const originalImageBuffer = await getOriginalImage(imageOptions.url, domains);
64
62
  if (originalImageBuffer) {
65
- const originalImage = sharp(originalImageBuffer, { animated: isGIF(imageOptions.url) });
66
- const resizedImage = await resizeImage(originalImage, imageOptions);
67
- const compressedImage = resizedImage.webp({ quality: imageOptions.q });
63
+ const compressedImage = (await resizeImage(sharp(originalImageBuffer, { animated: isGIF(imageOptions.url) }), imageOptions)).webp({ quality: imageOptions.q });
68
64
  compressedImage.toFile(cachedName).then(() => console.log("imageHandler.sharp.cached", cachedName)).catch((error) => console.error("imageHandler.sharp.error", cachedName, error));
69
65
  const data = await compressedImage.toBuffer();
70
66
  response.setHeader("Content-Type", "image/webp");
@@ -78,18 +74,65 @@ function imageHandler({ domains = [], sizes = [], quality = IMAGE_QUALITY }) {
78
74
  }
79
75
  } });
80
76
  }
77
+ function imageGetHandler({ domains = [], sizes = [], quality = IMAGE_QUALITY }) {
78
+ return async (request, context) => {
79
+ try {
80
+ let imageOptions;
81
+ try {
82
+ imageOptions = IMAGE_STRATEGY === ImageStrategy.Token ? await getEncryptedImageOptionsNext(request, context, {
83
+ domains,
84
+ sizes,
85
+ quality
86
+ }) : await getImageOptionsNext(request, context, {
87
+ domains,
88
+ sizes,
89
+ quality
90
+ });
91
+ } catch (error) {
92
+ return getJsonErrorResponse(400, error);
93
+ }
94
+ const cachedName = urlToCachedName(imageOptions);
95
+ if (fs.existsSync(cachedName)) return getImageResponse(await getCachedImage(cachedName));
96
+ if (isInvalidRefererValue(request.headers.get("referer"))) return new Response("you are not authorized to perform this action", { status: 401 });
97
+ if (imageOptions.url.indexOf("http") === -1 && isVercel()) return NextResponse.redirect(new URL(imageOptions.url, request.url), 307);
98
+ const originalImageBuffer = await getOriginalImage(imageOptions.url, domains);
99
+ if (originalImageBuffer) {
100
+ const compressedImage = (await resizeImage(sharp(originalImageBuffer, { animated: isGIF(imageOptions.url) }), imageOptions)).webp({ quality: imageOptions.q });
101
+ compressedImage.toFile(cachedName).then(() => console.log("imageHandler.sharp.cached", cachedName)).catch((error) => console.error("imageHandler.sharp.error", cachedName, error));
102
+ return getImageResponse(await compressedImage.toBuffer());
103
+ }
104
+ return getJsonErrorResponse(404, "image not found");
105
+ } catch (error) {
106
+ console.error("imageHandler.error", error);
107
+ return getJsonErrorResponse(error.status || 500, error.message || error.name || error.statusText || "An error occurred");
108
+ }
109
+ };
110
+ }
81
111
  function isGIF(src) {
82
112
  return src.indexOf(".gif") !== -1;
83
113
  }
84
114
  function isInvalidReferer(request) {
85
115
  const { headers } = request;
86
116
  const referer = headers.referer;
117
+ return isInvalidRefererValue(referer);
118
+ }
119
+ function isInvalidRefererValue(referer) {
87
120
  const publicUrl = process.env.NEXT_PUBLIC_URL || "";
88
121
  if (IMAGE_STRATEGY !== ImageStrategy.Token && referer && publicUrl !== "https://" && referer.indexOf(publicUrl) !== 0) return true;
89
122
  return false;
90
123
  }
124
+ async function getImageOptionsNext(request, context, options) {
125
+ const params = await context.params;
126
+ return getImageOptionsFromQuery({
127
+ ...Object.fromEntries(request.nextUrl.searchParams.entries()),
128
+ slugs: params.slugs
129
+ }, options);
130
+ }
91
131
  function getImageOptions(request, options) {
92
- const { slugs, origin = "", search = "", w, h, x, y, q } = request.query;
132
+ return getImageOptionsFromQuery(request.query, options);
133
+ }
134
+ function getImageOptionsFromQuery(query, options) {
135
+ const { slugs, origin = "", search = "", w, h, x, y, q } = query;
93
136
  if (!Array.isArray(slugs) || slugs.length === 0) throw "missing slugs parameter";
94
137
  const width = typeof w === "string" ? parseInt(w) : void 0;
95
138
  if (!width) throw "missing w parameter";
@@ -119,8 +162,18 @@ function getImageOptions(request, options) {
119
162
  search: String(search)
120
163
  };
121
164
  }
165
+ async function getEncryptedImageOptionsNext(request, context, options) {
166
+ const params = await context.params;
167
+ return getEncryptedImageOptionsFromQuery({
168
+ ...Object.fromEntries(request.nextUrl.searchParams.entries()),
169
+ slugs: params.slugs
170
+ }, options);
171
+ }
122
172
  function getEncryptedImageOptions(request, options) {
123
- const { slugs, origin = "", search = "", t } = request.query;
173
+ return getEncryptedImageOptionsFromQuery(request.query, options);
174
+ }
175
+ function getEncryptedImageOptionsFromQuery(query, options) {
176
+ const { slugs, origin = "", search = "", t } = query;
124
177
  if (!Array.isArray(slugs) || slugs.length === 0) throw "missing slugs parameter";
125
178
  if (typeof t !== "string") throw "missing t parameter";
126
179
  const imageResize = decryptImageResize(t);
@@ -148,11 +201,8 @@ async function resizeImage(originalImage, imageOptions) {
148
201
  if (resizeHeight && (focalX !== IMAGE_FOCAL_X || focalY !== IMAGE_FOCAL_Y)) {
149
202
  const { width: originalWidth, height: originalHeight } = await originalImage.metadata();
150
203
  if (originalWidth && originalHeight) {
151
- const resizeAspectRatio = resizeWidth / resizeHeight;
152
- const originalAspectRatio = originalWidth / originalHeight;
153
- const prioritizeHeight = resizeAspectRatio < originalAspectRatio;
154
- const imageToResize = originalImage.clone();
155
- const scaledImage = imageToResize.resize(prioritizeHeight ? {
204
+ const prioritizeHeight = resizeWidth / resizeHeight < originalWidth / originalHeight;
205
+ const scaledImage = originalImage.clone().resize(prioritizeHeight ? {
156
206
  height: resizeHeight,
157
207
  fit: sharp.fit.cover
158
208
  } : {
@@ -177,7 +227,7 @@ async function resizeImage(originalImage, imageOptions) {
177
227
  } else return defaultResizeOptions();
178
228
  } else return defaultResizeOptions();
179
229
  }
180
- function getOriginalImage(url, domains, headers) {
230
+ function getOriginalImage(url, domains) {
181
231
  return new Promise((resolve, reject) => {
182
232
  if (url.indexOf("http") === 0) {
183
233
  if (isInvalidDomain(url, domains)) return reject({
@@ -210,6 +260,16 @@ function getOriginalImage(url, domains, headers) {
210
260
  }
211
261
  });
212
262
  }
263
+ function getImageResponse(data) {
264
+ return new Response(data, { headers: {
265
+ "Cache-Control": `public, max-age=${IMAGE_MAX_AGE}`,
266
+ "Content-Type": "image/webp",
267
+ Expires: new Date(Date.now() + IMAGE_MAX_AGE * 1e3).toUTCString()
268
+ } });
269
+ }
270
+ function getJsonErrorResponse(status, message) {
271
+ return Response.json({ errors: [{ message }] }, { status });
272
+ }
213
273
  function getCachedImage(pathname) {
214
274
  return new Promise((resolve, reject) => {
215
275
  fs.readFile(pathname, (error, data) => {
@@ -220,9 +280,9 @@ function getCachedImage(pathname) {
220
280
  }
221
281
  function urlToFileName(url) {
222
282
  const segments = url.split("?");
223
- const path$1 = segments[0];
283
+ const path = segments[0];
224
284
  const query = segments[1];
225
- let uniqueName = path$1.replace("://", "_").replace(/\.|\/|\?|=/g, "_");
285
+ let uniqueName = path.replace("://", "_").replace(/\.|\/|\?|=/g, "_");
226
286
  uniqueName = uniqueName.substring(0, uniqueName.lastIndexOf("_")) + (query ? "_" + query.replace(/\.|\/|\?|=/g, "_") : "") + "." + uniqueName.substring(uniqueName.lastIndexOf("_") + 1, uniqueName.length);
227
287
  return uniqueName;
228
288
  }
@@ -234,15 +294,14 @@ function urlToCachedName(options) {
234
294
  const parsedName = path.parse(fileName);
235
295
  const cacheDir = isVercel() ? path.join("/tmp", ".cache") : path.join(process.cwd(), ".cache");
236
296
  if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir);
237
- const cachedName = options.h ? path.join(cacheDir, `${parsedName.name}_w_${options.w}_h_${options.h || 0}_x_${options.x || IMAGE_FOCAL_X}_y_${options.y || IMAGE_FOCAL_Y}_q_${options.q}${parsedName.ext}`) : path.join(cacheDir, `${parsedName.name}_w_${options.w}_q_${options.q}${parsedName.ext}`);
238
- return cachedName;
297
+ return options.h ? path.join(cacheDir, `${parsedName.name}_w_${options.w}_h_${options.h || 0}_x_${options.x || IMAGE_FOCAL_X}_y_${options.y || IMAGE_FOCAL_Y}_q_${options.q}${parsedName.ext}`) : path.join(cacheDir, `${parsedName.name}_w_${options.w}_q_${options.q}${parsedName.ext}`);
239
298
  }
240
299
  function isInvalidDomain(src, domains) {
241
300
  if (IMAGE_STRATEGY === ImageStrategy.Token) return false;
242
301
  const url = new URL(src);
243
302
  return !domains.includes(url.hostname);
244
303
  }
245
-
246
304
  //#endregion
247
- export { imageHandler };
305
+ export { imageGetHandler, imageHandler };
306
+
248
307
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["handler: IApiHandler","request: NextApiRequest","response: NextApiResponse","method: ICrudMethod","error: any","IMAGE_STRATEGY: ImageStrategy","request: NextApiRequest","response: NextApiResponse","imageOptions: IImageOptions","error: any","src: string","options: IImageHandlerOptions","originalImage: sharp.Sharp","url: string","domains: string[]","headers: IncomingHttpHeaders","pathname: string","path","options: IImageOptions"],"sources":["../src/api/api.handler.ts","../src/image/image.handler.ts"],"sourcesContent":["import { NextApiRequest, NextApiResponse } from 'next';\r\n\r\nexport type ICrudMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | string;\r\n\r\nexport type IHandler = (request: NextApiRequest, response: NextApiResponse) => Promise<any>;\r\n\r\nexport type IApiHandler = Record<ICrudMethod, IHandler>;\r\n\r\nexport function apiHandler(handler: IApiHandler): any {\r\n return async (request: NextApiRequest, response: NextApiResponse): Promise<any> => {\r\n const method: ICrudMethod = (request.method || 'GET').toLowerCase() as ICrudMethod;\r\n // check handler supports HTTP method\r\n if (!handler[method]) {\r\n return response.status(405).end(`Method ${method} Not Allowed`);\r\n }\r\n try {\r\n // global middleware\r\n // await jwtMiddleware(request, response);\r\n // route handler\r\n await handler[method](request, response);\r\n } catch (error) {\r\n // console.log('error', error);\r\n // global error handler\r\n return errorHandler(error, response);\r\n }\r\n };\r\n}\r\n\r\nexport function errorHandler(error: any, response: NextApiResponse): Promise<any> | void {\r\n if (typeof (error) === 'string') {\r\n // custom application error\r\n const is404 = error.toLowerCase().endsWith('not found');\r\n const statusCode = is404 ? 404 : 400;\r\n return response.status(statusCode).json({ message: error });\r\n }\r\n if (error.name === 'UnauthorizedError') {\r\n // jwt authentication error\r\n return response.status(401).json({ message: 'Invalid Token' });\r\n }\r\n // default to 500 server error\r\n console.error(error);\r\n return response.status(500).json({ message: error.message });\r\n}\r\n","import { IncomingHttpHeaders } from 'http';\r\n// Importing the 'fs' module to work with the file system\r\nimport * as fs from 'fs';\r\n// Importing the 'NextApiRequest' and 'NextApiResponse' from 'next' for handling API requests and responses\r\nimport { NextApiRequest, NextApiResponse } from 'next';\r\n// Importing the 'path' module to work with file and directory paths\r\nimport * as path from 'path';\r\n// Importing 'sharp' for image processing\r\nimport sharp from 'sharp';\r\n// Importing 'apiHandler' from '../api/api.handler'\r\nimport { decryptImageResize, IImageResize, ImageStrategy } from '@websolutespa/bom-core';\r\nimport { apiHandler } from '../api/api.handler';\r\n\r\nconst IMAGE_STRATEGY: ImageStrategy = process.env.IMAGE_STRATEGY ?\r\n process.env.IMAGE_STRATEGY as ImageStrategy :\r\n ImageStrategy.Mixer;\r\n\r\nconst IMAGE_MAX_AGE = 604800;\r\nconst IMAGE_FOCAL_X = 50;\r\nconst IMAGE_FOCAL_Y = 50;\r\nconst IMAGE_QUALITY = 90;\r\n\r\nexport type IImageHandlerOptions = {\r\n domains: string[];\r\n sizes: number[];\r\n quality: number;\r\n};\r\n\r\nexport type IImageOptions = IImageResize & {\r\n origin: string;\r\n search: string;\r\n url: string;\r\n};\r\n\r\n// Exporting the 'imageHandler' function\r\nexport function imageHandler({\r\n domains = [],\r\n sizes = [],\r\n quality = IMAGE_QUALITY,\r\n}: IImageHandlerOptions) {\r\n // Returning the 'apiHandler' function with a configuration object\r\n return apiHandler({\r\n // Defining the 'get' method for handling GET requests\r\n get: async (request: NextApiRequest, response: NextApiResponse) => {\r\n try {\r\n let imageOptions: IImageOptions;\r\n try {\r\n imageOptions = (IMAGE_STRATEGY === ImageStrategy.Token ?\r\n getEncryptedImageOptions(request, { domains, sizes, quality }) :\r\n getImageOptions(request, { domains, sizes, quality }));\r\n } catch (error) {\r\n return response.status(400).json({\r\n errors: [{\r\n message: error,\r\n }],\r\n });\r\n }\r\n // Generating a cached name for the image\r\n const cachedName = urlToCachedName(imageOptions);\r\n // Checking if the cached image exists\r\n if (fs.existsSync(cachedName)) {\r\n // console.log('imageHandler.fromCache', cachedName);\r\n // Reading the cached image\r\n const cachedImage = await getCachedImage(cachedName);\r\n // Setting the 'Content-Type' header to 'image/webp'\r\n response.setHeader('Content-Type', 'image/webp');\r\n // Setting the 'Expires' header to the current time plus the maximum age of the image (in seconds)\r\n response.setHeader('Expires', new Date(Date.now() + IMAGE_MAX_AGE * 1000).toUTCString());\r\n // Setting the 'Cache-Control' header to 'public' and setting 'max-age' to the maximum age of the image\r\n response.setHeader('Cache-Control', `public, max-age=${IMAGE_MAX_AGE}`);\r\n // Ending the response and sending the cached image\r\n return response.send(cachedImage);\r\n }\r\n // Check referrer\r\n if (isInvalidReferer(request)) {\r\n return response.status(401).send('you are not authorized to perform this action');\r\n }\r\n // Temporarily fix Vercel local image issue\r\n if (imageOptions.url.indexOf('http') === -1 && isVercel()) {\r\n response.redirect(307, imageOptions.url);\r\n }\r\n // Reading the original image\r\n const originalImageBuffer = await getOriginalImage(imageOptions.url, domains, request.headers);\r\n // Checking if the original image exists\r\n if (originalImageBuffer) {\r\n // Original image\r\n const originalImage = sharp(originalImageBuffer, {\r\n animated: isGIF(imageOptions.url),\r\n });\r\n // Resizing the original image and converting it to WebP format\r\n const resizedImage = await resizeImage(originalImage, imageOptions);\r\n // Converting resized image to WebP format\r\n const compressedImage = resizedImage\r\n .webp({\r\n quality: imageOptions.q,\r\n // nearLossless: true,\r\n });\r\n // Saving the resized image to the cache\r\n compressedImage.toFile(cachedName)\r\n .then(() => console.log('imageHandler.sharp.cached', cachedName))\r\n .catch((error: any) => console.error('imageHandler.sharp.error', cachedName, error));\r\n // Converting the resized image to a buffer\r\n const data = await compressedImage.toBuffer();\r\n // Setting the 'Content-Type' header to 'image/webp'\r\n response.setHeader('Content-Type', 'image/webp');\r\n // Setting the 'Expires' header to the current time plus the maximum age of the image (in seconds)\r\n response.setHeader('Expires', new Date(Date.now() + IMAGE_MAX_AGE * 1000).toUTCString());\r\n // Setting the 'Cache-Control' header to 'public' and setting 'max-age' to the maximum age of the image\r\n response.setHeader('Cache-Control', `public, max-age=${IMAGE_MAX_AGE}`);\r\n // Sending data in the response\r\n return response.end(data);\r\n }\r\n } catch (error: any) {\r\n // Logging an error message and throwing the error\r\n console.error('imageHandler.error', error);\r\n return response.status(error.status || 500).json({\r\n errors: [{\r\n message: error.message || error.name || error.statusText || 'An error occurred',\r\n }],\r\n });\r\n }\r\n },\r\n });\r\n}\r\n\r\nfunction isGIF(src: string): boolean {\r\n return src.indexOf('.gif') !== -1;\r\n}\r\n\r\nfunction isInvalidReferer(request: NextApiRequest): boolean {\r\n const { headers } = request;\r\n const referer = headers.referer;\r\n const publicUrl = process.env.NEXT_PUBLIC_URL || '';\r\n // console.log('referef', referer, 'publicUrl', publicUrl, 'IMAGE_STRATEGY', IMAGE_STRATEGY, ImageStrategy.Token);\r\n if (\r\n IMAGE_STRATEGY !== ImageStrategy.Token &&\r\n referer &&\r\n publicUrl !== 'https://' &&\r\n referer.indexOf(publicUrl) !== 0\r\n ) {\r\n return true;\r\n }\r\n return false;\r\n}\r\n\r\nfunction getImageOptions(request: NextApiRequest, options: IImageHandlerOptions): IImageOptions {\r\n // Destructuring 'slugs', 'origin', 'search', 'w', 'h', 'x', 'y', 'q' from the request query\r\n const { slugs, origin = '', search = '', w, h, x, y, q } = request.query;\r\n // Checking slugs\r\n if (!Array.isArray(slugs) || slugs.length === 0) {\r\n // console.error('imageHandler', 'missing slugs parameter');\r\n throw 'missing slugs parameter';\r\n }\r\n // Parsing 'w' to an integer and assigning it to 'width'\r\n const width = typeof w === 'string' ? parseInt(w) : undefined;\r\n // Checking if 'width' is not defined\r\n if (!width) {\r\n // Logging an error message and returning a 400 status code with a message\r\n // console.error('imageHandler', 'missing w parameter');\r\n throw 'missing w parameter';\r\n }\r\n // if (width > 2048) {\r\n if (!options.sizes.includes(width)) {\r\n throw 'invalid w parameter';\r\n }\r\n // Parsing 'h' to an integer and assigning it to 'height'\r\n const height = typeof h === 'string' ? parseInt(h) : undefined;\r\n // Parsing 'x' to an integer and assigning it to 'focalX'\r\n const focalX = typeof x === 'string' ? parseInt(x) : IMAGE_FOCAL_X;\r\n // Parsing 'y' to an integer and assigning it to 'focalY'\r\n const focalY = typeof y === 'string' ? parseInt(y) : IMAGE_FOCAL_Y;\r\n // Parsing 'q' to an integer, ensuring it's between 30 and 100, and assigning it to 'imageQuality'\r\n const imageQuality = Math.max(30, Math.min(100, (typeof q === 'string' ? parseInt(q) : options.quality)));\r\n // if (width > 2048) {\r\n if (imageQuality !== options.quality) {\r\n throw 'invalid q parameter';\r\n }\r\n // Generating url\r\n const url = origin + '/' + slugs.join('/') + search;\r\n //\r\n return height ? {\r\n w: width,\r\n h: height,\r\n x: focalX,\r\n y: focalY,\r\n q: imageQuality,\r\n src: url,\r\n url: url,\r\n origin: String(origin),\r\n search: String(search),\r\n } : {\r\n w: width,\r\n q: imageQuality,\r\n src: url,\r\n url: url,\r\n origin: String(origin),\r\n search: String(search),\r\n };\r\n}\r\n\r\nfunction getEncryptedImageOptions(request: NextApiRequest, options: IImageHandlerOptions): IImageOptions {\r\n // Destructuring 'slugs', 'origin', 'search', 't' from the request query\r\n const { slugs, origin = '', search = '', t } = request.query;\r\n // Checking slugs\r\n if (!Array.isArray(slugs) || slugs.length === 0) {\r\n // console.error('imageHandler', 'missing slugs parameter');\r\n throw 'missing slugs parameter';\r\n }\r\n if (typeof t !== 'string') {\r\n throw 'missing t parameter';\r\n }\r\n const imageResize = decryptImageResize(t);\r\n /*\r\n // Checking if 'w' is not defined\r\n if (!imageResize.w) {\r\n throw 'missing w parameter';\r\n }\r\n // if (width > 2048) {\r\n if (!options.sizes.includes(imageResize.w)) {\r\n throw 'invalid w parameter';\r\n }\r\n // Ensuring 'q' is between 30 and 100, and assigning it to 'imageQuality'\r\n const imageQuality = Math.max(30, Math.min(100, imageResize.q || options.quality));\r\n if (imageQuality !== options.quality) {\r\n throw 'invalid q parameter';\r\n }\r\n */\r\n imageResize.src = decodeURIComponent(imageResize.src);\r\n // Generating url\r\n const url = decodeURIComponent(origin + '/' + slugs.join('/') + search);\r\n //\r\n if (imageResize.src !== url) {\r\n throw `invalid src parameter ${imageResize.src} ${url}`;\r\n }\r\n //\r\n return {\r\n ...imageResize,\r\n url: url,\r\n origin: String(origin),\r\n search: String(search),\r\n };\r\n}\r\n\r\nasync function resizeImage(originalImage: sharp.Sharp, imageOptions: IImageOptions): Promise<sharp.Sharp> {\r\n const { w: resizeWidth, h: resizeHeight, x: focalX = IMAGE_FOCAL_X, y: focalY = IMAGE_FOCAL_Y } = imageOptions;\r\n const defaultResizeOptions = () => {\r\n return originalImage.resize({\r\n width: resizeWidth,\r\n height: resizeHeight ? resizeHeight : undefined,\r\n fit: sharp.fit.cover,\r\n position: sharp.strategy.attention,\r\n withoutEnlargement: true,\r\n });\r\n };\r\n if (resizeHeight && (focalX !== IMAGE_FOCAL_X || focalY !== IMAGE_FOCAL_Y)) {\r\n const { width: originalWidth, height: originalHeight } = await originalImage.metadata();\r\n if (originalWidth && originalHeight) {\r\n const resizeAspectRatio = resizeWidth / resizeHeight;\r\n const originalAspectRatio = originalWidth / originalHeight;\r\n const prioritizeHeight = resizeAspectRatio < originalAspectRatio;\r\n const imageToResize = originalImage.clone();\r\n // Scale the image up or down to fit the resize dimensions\r\n const scaledImage = imageToResize.resize(prioritizeHeight ? {\r\n height: resizeHeight,\r\n fit: sharp.fit.cover,\r\n } : {\r\n width: resizeWidth,\r\n fit: sharp.fit.cover,\r\n });\r\n const { info: scaledImageInfo } = await scaledImage.toBuffer({ resolveWithObject: true });\r\n // extractWidth\r\n const extractWidth = resizeWidth ?? scaledImageInfo.width;\r\n const maxOffsetX = scaledImageInfo.width - extractWidth;\r\n const leftFocalEdge = Math.round(\r\n scaledImageInfo.width * (focalX / 100) - extractWidth / 2\r\n );\r\n // extractX\r\n const extractX = Math.min(Math.max(0, leftFocalEdge), maxOffsetX);\r\n // extractHeight\r\n const extractHeight = resizeHeight ?? scaledImageInfo.height;\r\n const maxOffsetY = scaledImageInfo.height - extractHeight;\r\n const topFocalEdge = Math.round(\r\n scaledImageInfo.height * (focalY / 100) - extractHeight / 2\r\n );\r\n // extractY\r\n const extractY = Math.min(Math.max(0, topFocalEdge), maxOffsetY);\r\n // Extract the focal area from the scaled image\r\n return scaledImage.extract({\r\n left: extractX,\r\n top: extractY,\r\n width: extractWidth,\r\n height: extractHeight,\r\n });\r\n } else {\r\n return defaultResizeOptions();\r\n }\r\n } else {\r\n return defaultResizeOptions();\r\n }\r\n}\r\n\r\n// Defining the 'getOriginalImage' function\r\nfunction getOriginalImage(url: string, domains: string[], headers: IncomingHttpHeaders): Promise<Buffer> {\r\n // Returning a new promise\r\n return new Promise((resolve, reject) => {\r\n // Checking if 'url' starts with 'http'\r\n if (url.indexOf('http') === 0) {\r\n if (isInvalidDomain(url, domains)) {\r\n return reject({ status: 401, message: 'invalid origin' });\r\n }\r\n // Fetching the image from the URL\r\n fetch(url, {\r\n method: 'GET',\r\n mode: 'cors',\r\n headers: {\r\n 'Access-Control-Allow-Origin': '*',\r\n },\r\n })\r\n .then(response => {\r\n if (!response.ok) {\r\n return reject({ status: response.status, message: response.statusText });\r\n }\r\n response.arrayBuffer().then(buffer => {\r\n return resolve(Buffer.from(buffer));\r\n }).catch(error => {\r\n return reject(error);\r\n });\r\n })\r\n .catch(error => {\r\n // Rejecting the promise with the error\r\n return reject(error);\r\n });\r\n } else {\r\n // Reading the image from the file system\r\n const filePath = path.join(process.cwd(), 'public', url);\r\n // console.log('imageHandler.getOriginalImage.filePath', filePath);\r\n fs.readFile(filePath, (error, data) => {\r\n if (error) {\r\n return reject(error);\r\n }\r\n // Resolving the promise with the image data\r\n return resolve(data);\r\n });\r\n }\r\n });\r\n}\r\n\r\n// Defining the 'getCachedImage' function\r\nfunction getCachedImage(pathname: string): Promise<Buffer> {\r\n // Returning a new promise\r\n return new Promise((resolve, reject) => {\r\n // Reading the image from the file system\r\n fs.readFile(pathname, (error, data) => {\r\n if (error) {\r\n // Rejecting the promise with the error\r\n return reject(error);\r\n }\r\n // Resolving the promise with the image data\r\n return resolve(data);\r\n });\r\n });\r\n}\r\n\r\n// Defining the 'urlToFileName' function\r\nfunction urlToFileName(url: string): string {\r\n // Replacing useless characters with UNDERSCORE\r\n const segments = url.split('?');\r\n const path = segments[0]!;\r\n const query = segments[1];\r\n let uniqueName = path.replace('://', '_').replace(/\\.|\\/|\\?|=/g, '_');\r\n // Replacing last UNDERSCORE with a DOT\r\n uniqueName = uniqueName.substring(0, uniqueName.lastIndexOf('_'))\r\n + (query ? '_' + query.replace(/\\.|\\/|\\?|=/g, '_') : '')\r\n + '.' + uniqueName.substring(uniqueName.lastIndexOf('_') + 1, uniqueName.length);\r\n // Returning the unique name\r\n return uniqueName;\r\n}\r\n\r\nfunction isVercel(): boolean {\r\n return process && process.env && String(process.env.VERCEL) === '1';\r\n}\r\n\r\n// Defining the 'urlToCachedName' function\r\nfunction urlToCachedName(options: IImageOptions): string {\r\n // Converting the URL to a file name\r\n const fileName = urlToFileName(options.url);\r\n // Parsing the file name\r\n const parsedName = path.parse(fileName);\r\n // Defining the cache directory\r\n const cacheDir = isVercel() ?\r\n path.join('/tmp', '.cache') :\r\n path.join(process.cwd(), '.cache');\r\n // Checking if the cache directory exists\r\n if (!fs.existsSync(cacheDir)) {\r\n // Creating the cache directory\r\n fs.mkdirSync(cacheDir);\r\n }\r\n // Generating the cached name\r\n const cachedName = options.h ?\r\n path.join(cacheDir, `${parsedName.name}_w_${options.w}_h_${options.h || 0}_x_${options.x || IMAGE_FOCAL_X}_y_${options.y || IMAGE_FOCAL_Y}_q_${options.q}${parsedName.ext}`) :\r\n path.join(cacheDir, `${parsedName.name}_w_${options.w}_q_${options.q}${parsedName.ext}`);\r\n // Returning the cached name\r\n return cachedName;\r\n}\r\n\r\nfunction isInvalidDomain(src: string, domains: string[]): boolean {\r\n if (IMAGE_STRATEGY === ImageStrategy.Token) {\r\n return false;\r\n }\r\n const url = new URL(src);\r\n return !domains.includes(url.hostname);\r\n}\r\n"],"mappings":";;;;;;AAQA,SAAgB,WAAWA,SAA2B;AACpD,QAAO,OAAOC,SAAyBC,aAA4C;EACjF,MAAMC,UAAuB,QAAQ,UAAU,OAAO,aAAa;AAEnE,MAAI,CAAC,QAAQ,QACX,QAAO,SAAS,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,YAAY,CAAC,CAAC;AAEjE,MAAI;GAIF,MAAM,QAAQ,QAAQ,SAAS,SAAS;EACzC,SAAQ,OAAO;AAGd,UAAO,aAAa,OAAO,SAAS;EACrC;CACF;AACF;AAED,SAAgB,aAAaC,OAAYF,UAAgD;AACvF,KAAI,OAAQ,UAAW,UAAU;EAE/B,MAAM,QAAQ,MAAM,aAAa,CAAC,SAAS,YAAY;EACvD,MAAM,aAAa,QAAQ,MAAM;AACjC,SAAO,SAAS,OAAO,WAAW,CAAC,KAAK,EAAE,SAAS,MAAO,EAAC;CAC5D;AACD,KAAI,MAAM,SAAS,oBAEjB,QAAO,SAAS,OAAO,IAAI,CAAC,KAAK,EAAE,SAAS,gBAAiB,EAAC;CAGhE,QAAQ,MAAM,MAAM;AACpB,QAAO,SAAS,OAAO,IAAI,CAAC,KAAK,EAAE,SAAS,MAAM,QAAS,EAAC;AAC7D;;;;AC7BD,MAAMG,iBAAgC,QAAQ,IAAI,iBAChD,QAAQ,IAAI,iBACZ,cAAc;AAEhB,MAAM,gBAAgB;AACtB,MAAM,gBAAgB;AACtB,MAAM,gBAAgB;AACtB,MAAM,gBAAgB;AAetB,SAAgB,aAAa,EAC3B,UAAU,CAAE,GACZ,QAAQ,CAAE,GACV,UAAU,eACW,EAAE;AAEvB,QAAO,WAAW,EAEhB,KAAK,OAAOC,SAAyBC,aAA8B;AACjE,MAAI;GACF,IAAIC;AACJ,OAAI;IACF,eAAgB,mBAAmB,cAAc,QAC/C,yBAAyB,SAAS;KAAE;KAAS;KAAO;IAAS,EAAC,GAC9D,gBAAgB,SAAS;KAAE;KAAS;KAAO;IAAS,EAAC;GACxD,SAAQ,OAAO;AACd,WAAO,SAAS,OAAO,IAAI,CAAC,KAAK,EAC/B,QAAQ,CAAC,EACP,SAAS,MACV,CAAC,EACH,EAAC;GACH;GAED,MAAM,aAAa,gBAAgB,aAAa;AAEhD,OAAI,GAAG,WAAW,WAAW,EAAE;IAG7B,MAAM,cAAc,MAAM,eAAe,WAAW;IAEpD,SAAS,UAAU,gBAAgB,aAAa;IAEhD,SAAS,UAAU,WAAW,IAAI,KAAK,KAAK,KAAK,GAAG,gBAAgB,KAAM,aAAa,CAAC;IAExF,SAAS,UAAU,iBAAiB,CAAC,gBAAgB,EAAE,eAAe,CAAC;AAEvE,WAAO,SAAS,KAAK,YAAY;GAClC;AAED,OAAI,iBAAiB,QAAQ,CAC3B,QAAO,SAAS,OAAO,IAAI,CAAC,KAAK,gDAAgD;AAGnF,OAAI,aAAa,IAAI,QAAQ,OAAO,KAAK,MAAM,UAAU,EACvD,SAAS,SAAS,KAAK,aAAa,IAAI;GAG1C,MAAM,sBAAsB,MAAM,iBAAiB,aAAa,KAAK,SAAS,QAAQ,QAAQ;AAE9F,OAAI,qBAAqB;IAEvB,MAAM,gBAAgB,MAAM,qBAAqB,EAC/C,UAAU,MAAM,aAAa,IAAI,CAClC,EAAC;IAEF,MAAM,eAAe,MAAM,YAAY,eAAe,aAAa;IAEnE,MAAM,kBAAkB,aACrB,KAAK,EACJ,SAAS,aAAa,EAEvB,EAAC;IAEJ,gBAAgB,OAAO,WAAW,CAC/B,KAAK,MAAM,QAAQ,IAAI,6BAA6B,WAAW,CAAC,CAChE,MAAM,CAACC,UAAe,QAAQ,MAAM,4BAA4B,YAAY,MAAM,CAAC;IAEtF,MAAM,OAAO,MAAM,gBAAgB,UAAU;IAE7C,SAAS,UAAU,gBAAgB,aAAa;IAEhD,SAAS,UAAU,WAAW,IAAI,KAAK,KAAK,KAAK,GAAG,gBAAgB,KAAM,aAAa,CAAC;IAExF,SAAS,UAAU,iBAAiB,CAAC,gBAAgB,EAAE,eAAe,CAAC;AAEvE,WAAO,SAAS,IAAI,KAAK;GAC1B;EACF,SAAQA,OAAY;GAEnB,QAAQ,MAAM,sBAAsB,MAAM;AAC1C,UAAO,SAAS,OAAO,MAAM,UAAU,IAAI,CAAC,KAAK,EAC/C,QAAQ,CAAC,EACP,SAAS,MAAM,WAAW,MAAM,QAAQ,MAAM,cAAc,oBAC7D,CAAC,EACH,EAAC;EACH;CACF,EACF,EAAC;AACH;AAED,SAAS,MAAMC,KAAsB;AACnC,QAAO,IAAI,QAAQ,OAAO,KAAK;AAChC;AAED,SAAS,iBAAiBJ,SAAkC;CAC1D,MAAM,EAAE,SAAS,GAAG;CACpB,MAAM,UAAU,QAAQ;CACxB,MAAM,YAAY,QAAQ,IAAI,mBAAmB;AAEjD,KACE,mBAAmB,cAAc,SACjC,WACA,cAAc,cACd,QAAQ,QAAQ,UAAU,KAAK,EAE/B,QAAO;AAET,QAAO;AACR;AAED,SAAS,gBAAgBA,SAAyBK,SAA8C;CAE9F,MAAM,EAAE,OAAO,SAAS,IAAI,SAAS,IAAI,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,QAAQ;AAEnE,KAAI,CAAC,MAAM,QAAQ,MAAM,IAAI,MAAM,WAAW,EAE5C,OAAM;CAGR,MAAM,QAAQ,OAAO,MAAM,WAAW,SAAS,EAAE,GAAG;AAEpD,KAAI,CAAC,MAGH,OAAM;AAGR,KAAI,CAAC,QAAQ,MAAM,SAAS,MAAM,CAChC,OAAM;CAGR,MAAM,SAAS,OAAO,MAAM,WAAW,SAAS,EAAE,GAAG;CAErD,MAAM,SAAS,OAAO,MAAM,WAAW,SAAS,EAAE,GAAG;CAErD,MAAM,SAAS,OAAO,MAAM,WAAW,SAAS,EAAE,GAAG;CAErD,MAAM,eAAe,KAAK,IAAI,IAAI,KAAK,IAAI,KAAM,OAAO,MAAM,WAAW,SAAS,EAAE,GAAG,QAAQ,QAAS,CAAC;AAEzG,KAAI,iBAAiB,QAAQ,QAC3B,OAAM;CAGR,MAAM,MAAM,SAAS,MAAM,MAAM,KAAK,IAAI,GAAG;AAE7C,QAAO,SAAS;EACd,GAAG;EACH,GAAG;EACH,GAAG;EACH,GAAG;EACH,GAAG;EACH,KAAK;EACA;EACL,QAAQ,OAAO,OAAO;EACtB,QAAQ,OAAO,OAAO;CACvB,IAAG;EACF,GAAG;EACH,GAAG;EACH,KAAK;EACA;EACL,QAAQ,OAAO,OAAO;EACtB,QAAQ,OAAO,OAAO;CACvB;AACF;AAED,SAAS,yBAAyBL,SAAyBK,SAA8C;CAEvG,MAAM,EAAE,OAAO,SAAS,IAAI,SAAS,IAAI,GAAG,GAAG,QAAQ;AAEvD,KAAI,CAAC,MAAM,QAAQ,MAAM,IAAI,MAAM,WAAW,EAE5C,OAAM;AAER,KAAI,OAAO,MAAM,SACf,OAAM;CAER,MAAM,cAAc,mBAAmB,EAAE;CAgBzC,YAAY,MAAM,mBAAmB,YAAY,IAAI;CAErD,MAAM,MAAM,mBAAmB,SAAS,MAAM,MAAM,KAAK,IAAI,GAAG,OAAO;AAEvE,KAAI,YAAY,QAAQ,IACtB,OAAM,CAAC,sBAAsB,EAAE,YAAY,IAAI,CAAC,EAAE,KAAK;AAGzD,QAAO;EACL,GAAG;EACE;EACL,QAAQ,OAAO,OAAO;EACtB,QAAQ,OAAO,OAAO;CACvB;AACF;AAED,eAAe,YAAYC,eAA4BJ,cAAmD;CACxG,MAAM,EAAE,GAAG,aAAa,GAAG,cAAc,GAAG,SAAS,eAAe,GAAG,SAAS,eAAe,GAAG;CAClG,MAAM,uBAAuB,MAAM;AACjC,SAAO,cAAc,OAAO;GAC1B,OAAO;GACP,QAAQ,eAAe,eAAe;GACtC,KAAK,MAAM,IAAI;GACf,UAAU,MAAM,SAAS;GACzB,oBAAoB;EACrB,EAAC;CACH;AACD,KAAI,iBAAiB,WAAW,iBAAiB,WAAW,gBAAgB;EAC1E,MAAM,EAAE,OAAO,eAAe,QAAQ,gBAAgB,GAAG,MAAM,cAAc,UAAU;AACvF,MAAI,iBAAiB,gBAAgB;GACnC,MAAM,oBAAoB,cAAc;GACxC,MAAM,sBAAsB,gBAAgB;GAC5C,MAAM,mBAAmB,oBAAoB;GAC7C,MAAM,gBAAgB,cAAc,OAAO;GAE3C,MAAM,cAAc,cAAc,OAAO,mBAAmB;IAC1D,QAAQ;IACR,KAAK,MAAM,IAAI;GAChB,IAAG;IACF,OAAO;IACP,KAAK,MAAM,IAAI;GAChB,EAAC;GACF,MAAM,EAAE,MAAM,iBAAiB,GAAG,MAAM,YAAY,SAAS,EAAE,mBAAmB,KAAM,EAAC;GAEzF,MAAM,eAAe,eAAe,gBAAgB;GACpD,MAAM,aAAa,gBAAgB,QAAQ;GAC3C,MAAM,gBAAgB,KAAK,MACzB,gBAAgB,SAAS,SAAS,OAAO,eAAe,EACzD;GAED,MAAM,WAAW,KAAK,IAAI,KAAK,IAAI,GAAG,cAAc,EAAE,WAAW;GAEjE,MAAM,gBAAgB,gBAAgB,gBAAgB;GACtD,MAAM,aAAa,gBAAgB,SAAS;GAC5C,MAAM,eAAe,KAAK,MACxB,gBAAgB,UAAU,SAAS,OAAO,gBAAgB,EAC3D;GAED,MAAM,WAAW,KAAK,IAAI,KAAK,IAAI,GAAG,aAAa,EAAE,WAAW;AAEhE,UAAO,YAAY,QAAQ;IACzB,MAAM;IACN,KAAK;IACL,OAAO;IACP,QAAQ;GACT,EAAC;EACH,MACC,QAAO,sBAAsB;CAEhC,MACC,QAAO,sBAAsB;AAEhC;AAGD,SAAS,iBAAiBK,KAAaC,SAAmBC,SAA+C;AAEvG,QAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AAEtC,MAAI,IAAI,QAAQ,OAAO,KAAK,GAAG;AAC7B,OAAI,gBAAgB,KAAK,QAAQ,CAC/B,QAAO,OAAO;IAAE,QAAQ;IAAK,SAAS;GAAkB,EAAC;GAG3D,MAAM,KAAK;IACT,QAAQ;IACR,MAAM;IACN,SAAS,EACP,+BAA+B,IAChC;GACF,EAAC,CACC,KAAK,cAAY;AAChB,QAAI,CAAC,SAAS,GACZ,QAAO,OAAO;KAAE,QAAQ,SAAS;KAAQ,SAAS,SAAS;IAAY,EAAC;IAE1E,SAAS,aAAa,CAAC,KAAK,YAAU;AACpC,YAAO,QAAQ,OAAO,KAAK,OAAO,CAAC;IACpC,EAAC,CAAC,MAAM,WAAS;AAChB,YAAO,OAAO,MAAM;IACrB,EAAC;GACH,EAAC,CACD,MAAM,WAAS;AAEd,WAAO,OAAO,MAAM;GACrB,EAAC;EACL,OAAM;GAEL,MAAM,WAAW,KAAK,KAAK,QAAQ,KAAK,EAAE,UAAU,IAAI;GAExD,GAAG,SAAS,UAAU,CAAC,OAAO,SAAS;AACrC,QAAI,MACF,QAAO,OAAO,MAAM;AAGtB,WAAO,QAAQ,KAAK;GACrB,EAAC;EACH;CACF;AACF;AAGD,SAAS,eAAeC,UAAmC;AAEzD,QAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;EAEtC,GAAG,SAAS,UAAU,CAAC,OAAO,SAAS;AACrC,OAAI,MAEF,QAAO,OAAO,MAAM;AAGtB,UAAO,QAAQ,KAAK;EACrB,EAAC;CACH;AACF;AAGD,SAAS,cAAcH,KAAqB;CAE1C,MAAM,WAAW,IAAI,MAAM,IAAI;CAC/B,MAAMI,SAAO,SAAS;CACtB,MAAM,QAAQ,SAAS;CACvB,IAAI,aAAaA,OAAK,QAAQ,OAAO,IAAI,CAAC,QAAQ,eAAe,IAAI;CAErE,aAAa,WAAW,UAAU,GAAG,WAAW,YAAY,IAAI,CAAC,IAC5D,QAAQ,MAAM,MAAM,QAAQ,eAAe,IAAI,GAAG,MACnD,MAAM,WAAW,UAAU,WAAW,YAAY,IAAI,GAAG,GAAG,WAAW,OAAO;AAElF,QAAO;AACR;AAED,SAAS,WAAoB;AAC3B,QAAO,WAAW,QAAQ,OAAO,OAAO,QAAQ,IAAI,OAAO,KAAK;AACjE;AAGD,SAAS,gBAAgBC,SAAgC;CAEvD,MAAM,WAAW,cAAc,QAAQ,IAAI;CAE3C,MAAM,aAAa,KAAK,MAAM,SAAS;CAEvC,MAAM,WAAW,UAAU,GACzB,KAAK,KAAK,QAAQ,SAAS,GAC3B,KAAK,KAAK,QAAQ,KAAK,EAAE,SAAS;AAEpC,KAAI,CAAC,GAAG,WAAW,SAAS,EAE1B,GAAG,UAAU,SAAS;CAGxB,MAAM,aAAa,QAAQ,IACzB,KAAK,KAAK,UAAU,GAAG,WAAW,KAAK,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,KAAK,EAAE,GAAG,EAAE,QAAQ,KAAK,cAAc,GAAG,EAAE,QAAQ,KAAK,cAAc,GAAG,EAAE,QAAQ,IAAI,WAAW,KAAK,CAAC,GAC5K,KAAK,KAAK,UAAU,GAAG,WAAW,KAAK,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,IAAI,WAAW,KAAK,CAAC;AAE1F,QAAO;AACR;AAED,SAAS,gBAAgBR,KAAaI,SAA4B;AAChE,KAAI,mBAAmB,cAAc,MACnC,QAAO;CAET,MAAM,MAAM,IAAI,IAAI;AACpB,QAAO,CAAC,QAAQ,SAAS,IAAI,SAAS;AACvC"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/api/api.handler.ts","../src/image/image.handler.ts"],"sourcesContent":["import { NextApiRequest, NextApiResponse } from 'next';\r\n\r\nexport type ICrudMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | string;\r\n\r\nexport type IHandler = (request: NextApiRequest, response: NextApiResponse) => Promise<any>;\r\n\r\nexport type IApiHandler = Record<ICrudMethod, IHandler>;\r\n\r\nexport function apiHandler(handler: IApiHandler): any {\r\n return async (request: NextApiRequest, response: NextApiResponse): Promise<any> => {\r\n const method: ICrudMethod = (request.method || 'GET').toLowerCase() as ICrudMethod;\r\n // check handler supports HTTP method\r\n if (!handler[method]) {\r\n return response.status(405).end(`Method ${method} Not Allowed`);\r\n }\r\n try {\r\n // global middleware\r\n // await jwtMiddleware(request, response);\r\n // route handler\r\n await handler[method](request, response);\r\n } catch (error) {\r\n // console.log('error', error);\r\n // global error handler\r\n return errorHandler(error, response);\r\n }\r\n };\r\n}\r\n\r\nexport function errorHandler(error: any, response: NextApiResponse): Promise<any> | void {\r\n if (typeof (error) === 'string') {\r\n // custom application error\r\n const is404 = error.toLowerCase().endsWith('not found');\r\n const statusCode = is404 ? 404 : 400;\r\n return response.status(statusCode).json({ message: error });\r\n }\r\n if (error.name === 'UnauthorizedError') {\r\n // jwt authentication error\r\n return response.status(401).json({ message: 'Invalid Token' });\r\n }\r\n // default to 500 server error\r\n console.error(error);\r\n return response.status(500).json({ message: error.message });\r\n}\r\n","// Importing the 'fs' module to work with the file system\r\nimport * as fs from 'fs';\r\n// Importing the 'NextApiRequest' and 'NextApiResponse' from 'next' for handling API requests and responses\r\nimport { NextApiRequest, NextApiResponse } from 'next';\r\nimport { NextRequest, NextResponse } from 'next/server';\r\n// Importing the 'path' module to work with file and directory paths\r\nimport * as path from 'path';\r\n// Importing 'sharp' for image processing\r\nimport sharp from 'sharp';\r\n// Importing 'apiHandler' from '../api/api.handler'\r\nimport { decryptImageResize, IImageResize, ImageStrategy } from '@websolutespa/bom-core';\r\nimport { apiHandler } from '../api/api.handler';\r\n\r\nconst IMAGE_STRATEGY: ImageStrategy = process.env.IMAGE_STRATEGY ?\r\n process.env.IMAGE_STRATEGY as ImageStrategy :\r\n ImageStrategy.Mixer;\r\n\r\nconst IMAGE_MAX_AGE = 604800;\r\nconst IMAGE_FOCAL_X = 50;\r\nconst IMAGE_FOCAL_Y = 50;\r\nconst IMAGE_QUALITY = 90;\r\n\r\nexport type IImageHandlerOptions = {\r\n domains: string[];\r\n sizes: number[];\r\n quality: number;\r\n};\r\n\r\nexport type IImageOptions = IImageResize & {\r\n origin: string;\r\n search: string;\r\n url: string;\r\n};\r\n\r\ntype IImageHandlerNextContext = {\r\n params: Promise<{ slugs?: string[] }> | { slugs?: string[] };\r\n};\r\n\r\nexport function imageHandler({\r\n domains = [],\r\n sizes = [],\r\n quality = IMAGE_QUALITY,\r\n}: IImageHandlerOptions) {\r\n // Returning the 'apiHandler' function with a configuration object\r\n return apiHandler({\r\n // Defining the 'get' method for handling GET requests\r\n get: async (request: NextApiRequest, response: NextApiResponse) => {\r\n try {\r\n let imageOptions: IImageOptions;\r\n try {\r\n imageOptions = (IMAGE_STRATEGY === ImageStrategy.Token ?\r\n getEncryptedImageOptions(request, { domains, sizes, quality }) :\r\n getImageOptions(request, { domains, sizes, quality }));\r\n } catch (error) {\r\n return response.status(400).json({\r\n errors: [{\r\n message: error,\r\n }],\r\n });\r\n }\r\n // Generating a cached name for the image\r\n const cachedName = urlToCachedName(imageOptions);\r\n // Checking if the cached image exists\r\n if (fs.existsSync(cachedName)) {\r\n // console.log('imageHandler.fromCache', cachedName);\r\n // Reading the cached image\r\n const cachedImage = await getCachedImage(cachedName);\r\n // Setting the 'Content-Type' header to 'image/webp'\r\n response.setHeader('Content-Type', 'image/webp');\r\n // Setting the 'Expires' header to the current time plus the maximum age of the image (in seconds)\r\n response.setHeader('Expires', new Date(Date.now() + IMAGE_MAX_AGE * 1000).toUTCString());\r\n // Setting the 'Cache-Control' header to 'public' and setting 'max-age' to the maximum age of the image\r\n response.setHeader('Cache-Control', `public, max-age=${IMAGE_MAX_AGE}`);\r\n // Ending the response and sending the cached image\r\n return response.send(cachedImage);\r\n }\r\n // Check referrer\r\n if (isInvalidReferer(request)) {\r\n return response.status(401).send('you are not authorized to perform this action');\r\n }\r\n // Temporarily fix Vercel local image issue\r\n if (imageOptions.url.indexOf('http') === -1 && isVercel()) {\r\n response.redirect(307, imageOptions.url);\r\n }\r\n // Reading the original image\r\n const originalImageBuffer = await getOriginalImage(imageOptions.url, domains);\r\n // Checking if the original image exists\r\n if (originalImageBuffer) {\r\n // Original image\r\n const originalImage = sharp(originalImageBuffer, {\r\n animated: isGIF(imageOptions.url),\r\n });\r\n // Resizing the original image and converting it to WebP format\r\n const resizedImage = await resizeImage(originalImage, imageOptions);\r\n // Converting resized image to WebP format\r\n const compressedImage = resizedImage\r\n .webp({\r\n quality: imageOptions.q,\r\n // nearLossless: true,\r\n });\r\n // Saving the resized image to the cache\r\n compressedImage.toFile(cachedName)\r\n .then(() => console.log('imageHandler.sharp.cached', cachedName))\r\n .catch((error: any) => console.error('imageHandler.sharp.error', cachedName, error));\r\n // Converting the resized image to a buffer\r\n const data = await compressedImage.toBuffer();\r\n // Setting the 'Content-Type' header to 'image/webp'\r\n response.setHeader('Content-Type', 'image/webp');\r\n // Setting the 'Expires' header to the current time plus the maximum age of the image (in seconds)\r\n response.setHeader('Expires', new Date(Date.now() + IMAGE_MAX_AGE * 1000).toUTCString());\r\n // Setting the 'Cache-Control' header to 'public' and setting 'max-age' to the maximum age of the image\r\n response.setHeader('Cache-Control', `public, max-age=${IMAGE_MAX_AGE}`);\r\n // Sending data in the response\r\n return response.end(data);\r\n }\r\n } catch (error: any) {\r\n // Logging an error message and throwing the error\r\n console.error('imageHandler.error', error);\r\n return response.status(error.status || 500).json({\r\n errors: [{\r\n message: error.message || error.name || error.statusText || 'An error occurred',\r\n }],\r\n });\r\n }\r\n },\r\n });\r\n}\r\n\r\nexport function imageGetHandler({\r\n domains = [],\r\n sizes = [],\r\n quality = IMAGE_QUALITY,\r\n}: IImageHandlerOptions) {\r\n return async (request: NextRequest, context: IImageHandlerNextContext): Promise<Response> => {\r\n try {\r\n let imageOptions: IImageOptions;\r\n try {\r\n imageOptions = (IMAGE_STRATEGY === ImageStrategy.Token ?\r\n await getEncryptedImageOptionsNext(request, context, { domains, sizes, quality }) :\r\n await getImageOptionsNext(request, context, { domains, sizes, quality }));\r\n } catch (error) {\r\n return getJsonErrorResponse(400, error);\r\n }\r\n const cachedName = urlToCachedName(imageOptions);\r\n if (fs.existsSync(cachedName)) {\r\n const cachedImage = await getCachedImage(cachedName);\r\n return getImageResponse(cachedImage);\r\n }\r\n if (isInvalidRefererValue(request.headers.get('referer'))) {\r\n return new Response('you are not authorized to perform this action', {\r\n status: 401,\r\n });\r\n }\r\n if (imageOptions.url.indexOf('http') === -1 && isVercel()) {\r\n return NextResponse.redirect(new URL(imageOptions.url, request.url), 307);\r\n }\r\n const originalImageBuffer = await getOriginalImage(imageOptions.url, domains);\r\n if (originalImageBuffer) {\r\n const originalImage = sharp(originalImageBuffer, {\r\n animated: isGIF(imageOptions.url),\r\n });\r\n const resizedImage = await resizeImage(originalImage, imageOptions);\r\n const compressedImage = resizedImage\r\n .webp({\r\n quality: imageOptions.q,\r\n });\r\n compressedImage.toFile(cachedName)\r\n .then(() => console.log('imageHandler.sharp.cached', cachedName))\r\n .catch((error: any) => console.error('imageHandler.sharp.error', cachedName, error));\r\n const data = await compressedImage.toBuffer();\r\n return getImageResponse(data);\r\n }\r\n return getJsonErrorResponse(404, 'image not found');\r\n } catch (error: any) {\r\n console.error('imageHandler.error', error);\r\n return getJsonErrorResponse(\r\n error.status || 500,\r\n error.message || error.name || error.statusText || 'An error occurred'\r\n );\r\n }\r\n };\r\n}\r\n\r\nfunction isGIF(src: string): boolean {\r\n return src.indexOf('.gif') !== -1;\r\n}\r\n\r\nfunction isInvalidReferer(request: NextApiRequest): boolean {\r\n const { headers } = request;\r\n const referer = headers.referer;\r\n return isInvalidRefererValue(referer);\r\n}\r\n\r\nfunction isInvalidRefererValue(referer?: string | null): boolean {\r\n const publicUrl = process.env.NEXT_PUBLIC_URL || '';\r\n // console.log('referef', referer, 'publicUrl', publicUrl, 'IMAGE_STRATEGY', IMAGE_STRATEGY, ImageStrategy.Token);\r\n if (\r\n IMAGE_STRATEGY !== ImageStrategy.Token &&\r\n referer &&\r\n publicUrl !== 'https://' &&\r\n referer.indexOf(publicUrl) !== 0\r\n ) {\r\n return true;\r\n }\r\n return false;\r\n}\r\n\r\nasync function getImageOptionsNext(\r\n request: NextRequest,\r\n context: IImageHandlerNextContext,\r\n options: IImageHandlerOptions\r\n): Promise<IImageOptions> {\r\n const params = await context.params;\r\n const query = Object.fromEntries(request.nextUrl.searchParams.entries());\r\n return getImageOptionsFromQuery({\r\n ...query,\r\n slugs: params.slugs,\r\n }, options);\r\n}\r\n\r\nfunction getImageOptions(request: NextApiRequest, options: IImageHandlerOptions): IImageOptions {\r\n return getImageOptionsFromQuery(request.query, options);\r\n}\r\n\r\nfunction getImageOptionsFromQuery(\r\n query: Partial<Record<'origin' | 'search' | 'w' | 'h' | 'x' | 'y' | 'q', string | string[] | undefined>> & {\r\n slugs?: string[];\r\n },\r\n options: IImageHandlerOptions\r\n): IImageOptions {\r\n // Destructuring 'slugs', 'origin', 'search', 'w', 'h', 'x', 'y', 'q' from the request query\r\n const { slugs, origin = '', search = '', w, h, x, y, q } = query;\r\n // Checking slugs\r\n if (!Array.isArray(slugs) || slugs.length === 0) {\r\n // console.error('imageHandler', 'missing slugs parameter');\r\n throw 'missing slugs parameter';\r\n }\r\n // Parsing 'w' to an integer and assigning it to 'width'\r\n const width = typeof w === 'string' ? parseInt(w) : undefined;\r\n // Checking if 'width' is not defined\r\n if (!width) {\r\n // Logging an error message and returning a 400 status code with a message\r\n // console.error('imageHandler', 'missing w parameter');\r\n throw 'missing w parameter';\r\n }\r\n // if (width > 2048) {\r\n if (!options.sizes.includes(width)) {\r\n throw 'invalid w parameter';\r\n }\r\n // Parsing 'h' to an integer and assigning it to 'height'\r\n const height = typeof h === 'string' ? parseInt(h) : undefined;\r\n // Parsing 'x' to an integer and assigning it to 'focalX'\r\n const focalX = typeof x === 'string' ? parseInt(x) : IMAGE_FOCAL_X;\r\n // Parsing 'y' to an integer and assigning it to 'focalY'\r\n const focalY = typeof y === 'string' ? parseInt(y) : IMAGE_FOCAL_Y;\r\n // Parsing 'q' to an integer, ensuring it's between 30 and 100, and assigning it to 'imageQuality'\r\n const imageQuality = Math.max(30, Math.min(100, (typeof q === 'string' ? parseInt(q) : options.quality)));\r\n // if (width > 2048) {\r\n if (imageQuality !== options.quality) {\r\n throw 'invalid q parameter';\r\n }\r\n // Generating url\r\n const url = origin + '/' + slugs.join('/') + search;\r\n //\r\n return height ? {\r\n w: width,\r\n h: height,\r\n x: focalX,\r\n y: focalY,\r\n q: imageQuality,\r\n src: url,\r\n url: url,\r\n origin: String(origin),\r\n search: String(search),\r\n } : {\r\n w: width,\r\n q: imageQuality,\r\n src: url,\r\n url: url,\r\n origin: String(origin),\r\n search: String(search),\r\n };\r\n}\r\n\r\nasync function getEncryptedImageOptionsNext(\r\n request: NextRequest,\r\n context: IImageHandlerNextContext,\r\n options: IImageHandlerOptions\r\n): Promise<IImageOptions> {\r\n const params = await context.params;\r\n const query = Object.fromEntries(request.nextUrl.searchParams.entries());\r\n return getEncryptedImageOptionsFromQuery({\r\n ...query,\r\n slugs: params.slugs,\r\n }, options);\r\n}\r\n\r\nfunction getEncryptedImageOptions(request: NextApiRequest, options: IImageHandlerOptions): IImageOptions {\r\n return getEncryptedImageOptionsFromQuery(request.query, options);\r\n}\r\n\r\nfunction getEncryptedImageOptionsFromQuery(\r\n query: Partial<Record<'origin' | 'search' | 't', string | string[] | undefined>> & {\r\n slugs?: string[];\r\n },\r\n options: IImageHandlerOptions\r\n): IImageOptions {\r\n // Destructuring 'slugs', 'origin', 'search', 't' from the request query\r\n const { slugs, origin = '', search = '', t } = query;\r\n // Checking slugs\r\n if (!Array.isArray(slugs) || slugs.length === 0) {\r\n // console.error('imageHandler', 'missing slugs parameter');\r\n throw 'missing slugs parameter';\r\n }\r\n if (typeof t !== 'string') {\r\n throw 'missing t parameter';\r\n }\r\n const imageResize = decryptImageResize(t);\r\n /*\r\n // Checking if 'w' is not defined\r\n if (!imageResize.w) {\r\n throw 'missing w parameter';\r\n }\r\n // if (width > 2048) {\r\n if (!options.sizes.includes(imageResize.w)) {\r\n throw 'invalid w parameter';\r\n }\r\n // Ensuring 'q' is between 30 and 100, and assigning it to 'imageQuality'\r\n const imageQuality = Math.max(30, Math.min(100, imageResize.q || options.quality));\r\n if (imageQuality !== options.quality) {\r\n throw 'invalid q parameter';\r\n }\r\n */\r\n imageResize.src = decodeURIComponent(imageResize.src);\r\n // Generating url\r\n const url = decodeURIComponent(origin + '/' + slugs.join('/') + search);\r\n //\r\n if (imageResize.src !== url) {\r\n throw `invalid src parameter ${imageResize.src} ${url}`;\r\n }\r\n //\r\n return {\r\n ...imageResize,\r\n url: url,\r\n origin: String(origin),\r\n search: String(search),\r\n };\r\n}\r\n\r\nasync function resizeImage(originalImage: sharp.Sharp, imageOptions: IImageOptions): Promise<sharp.Sharp> {\r\n const { w: resizeWidth, h: resizeHeight, x: focalX = IMAGE_FOCAL_X, y: focalY = IMAGE_FOCAL_Y } = imageOptions;\r\n const defaultResizeOptions = () => {\r\n return originalImage.resize({\r\n width: resizeWidth,\r\n height: resizeHeight ? resizeHeight : undefined,\r\n fit: sharp.fit.cover,\r\n position: sharp.strategy.attention,\r\n withoutEnlargement: true,\r\n });\r\n };\r\n if (resizeHeight && (focalX !== IMAGE_FOCAL_X || focalY !== IMAGE_FOCAL_Y)) {\r\n const { width: originalWidth, height: originalHeight } = await originalImage.metadata();\r\n if (originalWidth && originalHeight) {\r\n const resizeAspectRatio = resizeWidth / resizeHeight;\r\n const originalAspectRatio = originalWidth / originalHeight;\r\n const prioritizeHeight = resizeAspectRatio < originalAspectRatio;\r\n const imageToResize = originalImage.clone();\r\n // Scale the image up or down to fit the resize dimensions\r\n const scaledImage = imageToResize.resize(prioritizeHeight ? {\r\n height: resizeHeight,\r\n fit: sharp.fit.cover,\r\n } : {\r\n width: resizeWidth,\r\n fit: sharp.fit.cover,\r\n });\r\n const { info: scaledImageInfo } = await scaledImage.toBuffer({ resolveWithObject: true });\r\n // extractWidth\r\n const extractWidth = resizeWidth ?? scaledImageInfo.width;\r\n const maxOffsetX = scaledImageInfo.width - extractWidth;\r\n const leftFocalEdge = Math.round(\r\n scaledImageInfo.width * (focalX / 100) - extractWidth / 2\r\n );\r\n // extractX\r\n const extractX = Math.min(Math.max(0, leftFocalEdge), maxOffsetX);\r\n // extractHeight\r\n const extractHeight = resizeHeight ?? scaledImageInfo.height;\r\n const maxOffsetY = scaledImageInfo.height - extractHeight;\r\n const topFocalEdge = Math.round(\r\n scaledImageInfo.height * (focalY / 100) - extractHeight / 2\r\n );\r\n // extractY\r\n const extractY = Math.min(Math.max(0, topFocalEdge), maxOffsetY);\r\n // Extract the focal area from the scaled image\r\n return scaledImage.extract({\r\n left: extractX,\r\n top: extractY,\r\n width: extractWidth,\r\n height: extractHeight,\r\n });\r\n } else {\r\n return defaultResizeOptions();\r\n }\r\n } else {\r\n return defaultResizeOptions();\r\n }\r\n}\r\n\r\n// Defining the 'getOriginalImage' function\r\nfunction getOriginalImage(url: string, domains: string[]): Promise<Buffer> {\r\n // Returning a new promise\r\n return new Promise((resolve, reject) => {\r\n // Checking if 'url' starts with 'http'\r\n if (url.indexOf('http') === 0) {\r\n if (isInvalidDomain(url, domains)) {\r\n return reject({ status: 401, message: 'invalid origin' });\r\n }\r\n // Fetching the image from the URL\r\n fetch(url, {\r\n method: 'GET',\r\n mode: 'cors',\r\n headers: {\r\n 'Access-Control-Allow-Origin': '*',\r\n },\r\n })\r\n .then(response => {\r\n if (!response.ok) {\r\n return reject({ status: response.status, message: response.statusText });\r\n }\r\n response.arrayBuffer().then(buffer => {\r\n return resolve(Buffer.from(buffer));\r\n }).catch(error => {\r\n return reject(error);\r\n });\r\n })\r\n .catch(error => {\r\n // Rejecting the promise with the error\r\n return reject(error);\r\n });\r\n } else {\r\n // Reading the image from the file system\r\n const filePath = path.join(process.cwd(), 'public', url);\r\n // console.log('imageHandler.getOriginalImage.filePath', filePath);\r\n fs.readFile(filePath, (error, data) => {\r\n if (error) {\r\n return reject(error);\r\n }\r\n // Resolving the promise with the image data\r\n return resolve(data);\r\n });\r\n }\r\n });\r\n}\r\n\r\nfunction getImageResponse(data: Buffer<ArrayBufferLike>): Response {\r\n return new Response(data as BodyInit, {\r\n headers: {\r\n 'Cache-Control': `public, max-age=${IMAGE_MAX_AGE}`,\r\n 'Content-Type': 'image/webp',\r\n Expires: new Date(Date.now() + IMAGE_MAX_AGE * 1000).toUTCString(),\r\n },\r\n });\r\n}\r\n\r\nfunction getJsonErrorResponse(status: number, message: unknown): Response {\r\n return Response.json({\r\n errors: [{\r\n message,\r\n }],\r\n }, {\r\n status,\r\n });\r\n}\r\n\r\n// Defining the 'getCachedImage' function\r\nfunction getCachedImage(pathname: string): Promise<Buffer> {\r\n // Returning a new promise\r\n return new Promise((resolve, reject) => {\r\n // Reading the image from the file system\r\n fs.readFile(pathname, (error, data) => {\r\n if (error) {\r\n // Rejecting the promise with the error\r\n return reject(error);\r\n }\r\n // Resolving the promise with the image data\r\n return resolve(data);\r\n });\r\n });\r\n}\r\n\r\n// Defining the 'urlToFileName' function\r\nfunction urlToFileName(url: string): string {\r\n // Replacing useless characters with UNDERSCORE\r\n const segments = url.split('?');\r\n const path = segments[0]!;\r\n const query = segments[1];\r\n let uniqueName = path.replace('://', '_').replace(/\\.|\\/|\\?|=/g, '_');\r\n // Replacing last UNDERSCORE with a DOT\r\n uniqueName = uniqueName.substring(0, uniqueName.lastIndexOf('_'))\r\n + (query ? '_' + query.replace(/\\.|\\/|\\?|=/g, '_') : '')\r\n + '.' + uniqueName.substring(uniqueName.lastIndexOf('_') + 1, uniqueName.length);\r\n // Returning the unique name\r\n return uniqueName;\r\n}\r\n\r\nfunction isVercel(): boolean {\r\n return process && process.env && String(process.env.VERCEL) === '1';\r\n}\r\n\r\n// Defining the 'urlToCachedName' function\r\nfunction urlToCachedName(options: IImageOptions): string {\r\n // Converting the URL to a file name\r\n const fileName = urlToFileName(options.url);\r\n // Parsing the file name\r\n const parsedName = path.parse(fileName);\r\n // Defining the cache directory\r\n const cacheDir = isVercel() ?\r\n path.join('/tmp', '.cache') :\r\n path.join(process.cwd(), '.cache');\r\n // Checking if the cache directory exists\r\n if (!fs.existsSync(cacheDir)) {\r\n // Creating the cache directory\r\n fs.mkdirSync(cacheDir);\r\n }\r\n // Generating the cached name\r\n const cachedName = options.h ?\r\n path.join(cacheDir, `${parsedName.name}_w_${options.w}_h_${options.h || 0}_x_${options.x || IMAGE_FOCAL_X}_y_${options.y || IMAGE_FOCAL_Y}_q_${options.q}${parsedName.ext}`) :\r\n path.join(cacheDir, `${parsedName.name}_w_${options.w}_q_${options.q}${parsedName.ext}`);\r\n // Returning the cached name\r\n return cachedName;\r\n}\r\n\r\nfunction isInvalidDomain(src: string, domains: string[]): boolean {\r\n if (IMAGE_STRATEGY === ImageStrategy.Token) {\r\n return false;\r\n }\r\n const url = new URL(src);\r\n return !domains.includes(url.hostname);\r\n}\r\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@websolutespa/bom-mixer-image",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Mixer Image module of the BOM Repository",
5
5
  "keywords": [
6
6
  "bom",
@@ -13,7 +13,7 @@
13
13
  "url": "git://github.com/websolutespa/bom.git"
14
14
  },
15
15
  "scripts": {
16
- "lint": "eslint *.ts*",
16
+ "lint": "eslint .",
17
17
  "test": "vitest run",
18
18
  "test-watch": "vitest",
19
19
  "coverage:integration": "npm run test -- --coverage.enabled --coverage.provider=istanbul --coverage.all",
@@ -25,20 +25,23 @@
25
25
  },
26
26
  "dependencies": {},
27
27
  "peerDependencies": {
28
- "@websolutespa/bom-core": "*",
29
- "next": "16.1.6",
28
+ "@websolutespa/bom-core": "^3.0.1",
29
+ "next": "^16.3.3",
30
30
  "sharp": "0.34.2"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/sharp": "^0.30.4",
34
- "@websolutespa/bom-cli": "*",
35
- "@websolutespa/test": "*",
36
- "@websolutespa/tsconfig": "*",
37
- "eslint": "9.31.0",
38
- "@websolutespa/eslint-config": "*",
34
+ "@vitest/coverage-istanbul": "5.0.5",
35
+ "@websolutespa/bom-cli": "^3.1.0",
36
+ "@websolutespa/eslint-config": "^3.1.0",
37
+ "@websolutespa/test": "^3.1.0",
38
+ "@websolutespa/tsconfig": "^3.1.0",
39
+ "eslint": "10.13.0",
39
40
  "npm-run-all": "4.1.5",
40
41
  "sharp": "0.34.2",
41
- "typescript": "5.8.3"
42
+ "tsdown": "0.21.0",
43
+ "typescript": "6.0.3",
44
+ "vitest": "5.0.5"
42
45
  },
43
46
  "publishConfig": {
44
47
  "access": "public"
@@ -1,8 +1,8 @@
1
- import { IncomingHttpHeaders } from 'http';
2
1
  // Importing the 'fs' module to work with the file system
3
2
  import * as fs from 'fs';
4
3
  // Importing the 'NextApiRequest' and 'NextApiResponse' from 'next' for handling API requests and responses
5
4
  import { NextApiRequest, NextApiResponse } from 'next';
5
+ import { NextRequest, NextResponse } from 'next/server';
6
6
  // Importing the 'path' module to work with file and directory paths
7
7
  import * as path from 'path';
8
8
  // Importing 'sharp' for image processing
@@ -32,7 +32,10 @@ export type IImageOptions = IImageResize & {
32
32
  url: string;
33
33
  };
34
34
 
35
- // Exporting the 'imageHandler' function
35
+ type IImageHandlerNextContext = {
36
+ params: Promise<{ slugs?: string[] }> | { slugs?: string[] };
37
+ };
38
+
36
39
  export function imageHandler({
37
40
  domains = [],
38
41
  sizes = [],
@@ -80,7 +83,7 @@ export function imageHandler({
80
83
  response.redirect(307, imageOptions.url);
81
84
  }
82
85
  // Reading the original image
83
- const originalImageBuffer = await getOriginalImage(imageOptions.url, domains, request.headers);
86
+ const originalImageBuffer = await getOriginalImage(imageOptions.url, domains);
84
87
  // Checking if the original image exists
85
88
  if (originalImageBuffer) {
86
89
  // Original image
@@ -123,6 +126,61 @@ export function imageHandler({
123
126
  });
124
127
  }
125
128
 
129
+ export function imageGetHandler({
130
+ domains = [],
131
+ sizes = [],
132
+ quality = IMAGE_QUALITY,
133
+ }: IImageHandlerOptions) {
134
+ return async (request: NextRequest, context: IImageHandlerNextContext): Promise<Response> => {
135
+ try {
136
+ let imageOptions: IImageOptions;
137
+ try {
138
+ imageOptions = (IMAGE_STRATEGY === ImageStrategy.Token ?
139
+ await getEncryptedImageOptionsNext(request, context, { domains, sizes, quality }) :
140
+ await getImageOptionsNext(request, context, { domains, sizes, quality }));
141
+ } catch (error) {
142
+ return getJsonErrorResponse(400, error);
143
+ }
144
+ const cachedName = urlToCachedName(imageOptions);
145
+ if (fs.existsSync(cachedName)) {
146
+ const cachedImage = await getCachedImage(cachedName);
147
+ return getImageResponse(cachedImage);
148
+ }
149
+ if (isInvalidRefererValue(request.headers.get('referer'))) {
150
+ return new Response('you are not authorized to perform this action', {
151
+ status: 401,
152
+ });
153
+ }
154
+ if (imageOptions.url.indexOf('http') === -1 && isVercel()) {
155
+ return NextResponse.redirect(new URL(imageOptions.url, request.url), 307);
156
+ }
157
+ const originalImageBuffer = await getOriginalImage(imageOptions.url, domains);
158
+ if (originalImageBuffer) {
159
+ const originalImage = sharp(originalImageBuffer, {
160
+ animated: isGIF(imageOptions.url),
161
+ });
162
+ const resizedImage = await resizeImage(originalImage, imageOptions);
163
+ const compressedImage = resizedImage
164
+ .webp({
165
+ quality: imageOptions.q,
166
+ });
167
+ compressedImage.toFile(cachedName)
168
+ .then(() => console.log('imageHandler.sharp.cached', cachedName))
169
+ .catch((error: any) => console.error('imageHandler.sharp.error', cachedName, error));
170
+ const data = await compressedImage.toBuffer();
171
+ return getImageResponse(data);
172
+ }
173
+ return getJsonErrorResponse(404, 'image not found');
174
+ } catch (error: any) {
175
+ console.error('imageHandler.error', error);
176
+ return getJsonErrorResponse(
177
+ error.status || 500,
178
+ error.message || error.name || error.statusText || 'An error occurred'
179
+ );
180
+ }
181
+ };
182
+ }
183
+
126
184
  function isGIF(src: string): boolean {
127
185
  return src.indexOf('.gif') !== -1;
128
186
  }
@@ -130,6 +188,10 @@ function isGIF(src: string): boolean {
130
188
  function isInvalidReferer(request: NextApiRequest): boolean {
131
189
  const { headers } = request;
132
190
  const referer = headers.referer;
191
+ return isInvalidRefererValue(referer);
192
+ }
193
+
194
+ function isInvalidRefererValue(referer?: string | null): boolean {
133
195
  const publicUrl = process.env.NEXT_PUBLIC_URL || '';
134
196
  // console.log('referef', referer, 'publicUrl', publicUrl, 'IMAGE_STRATEGY', IMAGE_STRATEGY, ImageStrategy.Token);
135
197
  if (
@@ -143,9 +205,31 @@ function isInvalidReferer(request: NextApiRequest): boolean {
143
205
  return false;
144
206
  }
145
207
 
208
+ async function getImageOptionsNext(
209
+ request: NextRequest,
210
+ context: IImageHandlerNextContext,
211
+ options: IImageHandlerOptions
212
+ ): Promise<IImageOptions> {
213
+ const params = await context.params;
214
+ const query = Object.fromEntries(request.nextUrl.searchParams.entries());
215
+ return getImageOptionsFromQuery({
216
+ ...query,
217
+ slugs: params.slugs,
218
+ }, options);
219
+ }
220
+
146
221
  function getImageOptions(request: NextApiRequest, options: IImageHandlerOptions): IImageOptions {
222
+ return getImageOptionsFromQuery(request.query, options);
223
+ }
224
+
225
+ function getImageOptionsFromQuery(
226
+ query: Partial<Record<'origin' | 'search' | 'w' | 'h' | 'x' | 'y' | 'q', string | string[] | undefined>> & {
227
+ slugs?: string[];
228
+ },
229
+ options: IImageHandlerOptions
230
+ ): IImageOptions {
147
231
  // Destructuring 'slugs', 'origin', 'search', 'w', 'h', 'x', 'y', 'q' from the request query
148
- const { slugs, origin = '', search = '', w, h, x, y, q } = request.query;
232
+ const { slugs, origin = '', search = '', w, h, x, y, q } = query;
149
233
  // Checking slugs
150
234
  if (!Array.isArray(slugs) || slugs.length === 0) {
151
235
  // console.error('imageHandler', 'missing slugs parameter');
@@ -198,9 +282,31 @@ function getImageOptions(request: NextApiRequest, options: IImageHandlerOptions)
198
282
  };
199
283
  }
200
284
 
285
+ async function getEncryptedImageOptionsNext(
286
+ request: NextRequest,
287
+ context: IImageHandlerNextContext,
288
+ options: IImageHandlerOptions
289
+ ): Promise<IImageOptions> {
290
+ const params = await context.params;
291
+ const query = Object.fromEntries(request.nextUrl.searchParams.entries());
292
+ return getEncryptedImageOptionsFromQuery({
293
+ ...query,
294
+ slugs: params.slugs,
295
+ }, options);
296
+ }
297
+
201
298
  function getEncryptedImageOptions(request: NextApiRequest, options: IImageHandlerOptions): IImageOptions {
299
+ return getEncryptedImageOptionsFromQuery(request.query, options);
300
+ }
301
+
302
+ function getEncryptedImageOptionsFromQuery(
303
+ query: Partial<Record<'origin' | 'search' | 't', string | string[] | undefined>> & {
304
+ slugs?: string[];
305
+ },
306
+ options: IImageHandlerOptions
307
+ ): IImageOptions {
202
308
  // Destructuring 'slugs', 'origin', 'search', 't' from the request query
203
- const { slugs, origin = '', search = '', t } = request.query;
309
+ const { slugs, origin = '', search = '', t } = query;
204
310
  // Checking slugs
205
311
  if (!Array.isArray(slugs) || slugs.length === 0) {
206
312
  // console.error('imageHandler', 'missing slugs parameter');
@@ -300,7 +406,7 @@ async function resizeImage(originalImage: sharp.Sharp, imageOptions: IImageOptio
300
406
  }
301
407
 
302
408
  // Defining the 'getOriginalImage' function
303
- function getOriginalImage(url: string, domains: string[], headers: IncomingHttpHeaders): Promise<Buffer> {
409
+ function getOriginalImage(url: string, domains: string[]): Promise<Buffer> {
304
410
  // Returning a new promise
305
411
  return new Promise((resolve, reject) => {
306
412
  // Checking if 'url' starts with 'http'
@@ -345,6 +451,26 @@ function getOriginalImage(url: string, domains: string[], headers: IncomingHttpH
345
451
  });
346
452
  }
347
453
 
454
+ function getImageResponse(data: Buffer<ArrayBufferLike>): Response {
455
+ return new Response(data as BodyInit, {
456
+ headers: {
457
+ 'Cache-Control': `public, max-age=${IMAGE_MAX_AGE}`,
458
+ 'Content-Type': 'image/webp',
459
+ Expires: new Date(Date.now() + IMAGE_MAX_AGE * 1000).toUTCString(),
460
+ },
461
+ });
462
+ }
463
+
464
+ function getJsonErrorResponse(status: number, message: unknown): Response {
465
+ return Response.json({
466
+ errors: [{
467
+ message,
468
+ }],
469
+ }, {
470
+ status,
471
+ });
472
+ }
473
+
348
474
  // Defining the 'getCachedImage' function
349
475
  function getCachedImage(pathname: string): Promise<Buffer> {
350
476
  // Returning a new promise