pixel-serve-server 0.0.7 β†’ 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,212 +1,271 @@
1
- # Image Server Middleware
1
+ # Pixel Serve Server
2
2
 
3
- A powerful and customizable middleware for processing, resizing, and serving images in Node.js applications. Built with **TypeScript** and powered by **Sharp**, this package allows you to handle local and network images with robust error handling, fallback images, and customizable options.
3
+ **A modern, type-safe middleware** for processing, resizing, and serving images in Node.js applications. Built with **TypeScript**, powered by **Sharp**, and designed for secure production use with ESM & CJS bundles.
4
4
 
5
- ---
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+ [![npm version](https://img.shields.io/npm/v/pixel-serve-server)](https://www.npmjs.com/package/pixel-serve-server)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9.3-blue.svg)](https://www.typescriptlang.org/)
8
+ [![Node.js](https://img.shields.io/badge/Node.js-18+-blue.svg)](https://nodejs.org/)
6
9
 
7
10
  ## Features
8
11
 
9
- - πŸ–ΌοΈ **Dynamic Image Resizing and Formatting**
10
-
11
- - Supports various formats: `jpeg`, `png`, `webp`, `gif`, `tiff`, `avif`, and `svg`.
12
- - Adjustable dimensions with constraints for safety.
13
-
14
- - 🌐 **Network and Local File Handling**
15
-
16
- - Fetches images from allowed network domains.
17
- - Processes images stored locally with safe path validation.
18
-
19
- - πŸ”’ **Fallback Images**
20
-
21
- - Provides fallback images for invalid or missing sources.
22
-
23
- - πŸ”§ **Highly Configurable**
24
-
25
- - Flexible option to set base directories, private folders, and user-specific paths.
26
- - Supports user-defined ID handlers and folder logic.
27
-
28
- - πŸš€ **Efficient and Scalable**
29
- - Built on **Sharp** for high-performance image processing.
30
- - Handles concurrent requests with ease.
31
-
32
- ---
12
+ - πŸ–ΌοΈ **Dynamic resizing & formatting**: `jpeg`, `png`, `webp`, `gif`, `tiff`, `avif`, `svg` with configurable width/height bounds and quality limits
13
+ - 🌐 **Secure source resolution**: Strict path validation, domain allowlists, and MIME type checks for network fetches
14
+ - πŸ”’ **Fallbacks & private folders**: Built-in placeholder images plus async `getUserFolder` for private assets
15
+ - ⚑ **Caching ready**: ETag + Cache-Control headers out of the box
16
+ - πŸ§ͺ **Type-safe & tested**: 100% TypeScript with Vitest coverage and exported Zod schemas
17
+ - ♻️ **Dual builds**: Works in both ESM and CommonJS environments
33
18
 
34
19
  ## Installation
35
20
 
36
- Install the package using npm or yarn:
37
-
38
21
  ```bash
39
22
  npm install pixel-serve-server
40
23
  ```
41
24
 
42
- ---
43
-
44
- ## Usage
45
-
46
- ### Basic Setup
25
+ ## Quick Start
47
26
 
48
- Here’s how to integrate the middleware with an Express application:
27
+ ### Basic Setup (Express)
49
28
 
50
29
  ```typescript
51
30
  import express from "express";
52
31
  import { registerServe } from "pixel-serve-server";
53
- import path, { dirname } from "node:path";
54
- import { fileURLToPath } from "node:url";
32
+ import path from "node:path";
55
33
 
56
34
  const app = express();
57
- const __filename = fileURLToPath(import.meta.url);
58
- const __dirname = dirname(__filename);
59
-
60
- const BASE_IMAGE_DIR = path.join(__dirname, "../assets/images/public");
61
- const PRIVATE_IMAGE_DIR = path.join(__dirname, "../assets/images/private");
62
35
 
63
36
  const serveImage = registerServe({
64
- baseDir: BASE_IMAGE_DIR, // Base directory for local images
65
- idHandler: (id: string) => `user-${id}`, // Custom handler for user IDs
66
- getUserFolder: async (id: string) => `/private/users/${id}`, // Logic for user-specific folder paths
67
- websiteURL: "example.com", // Your website's base URL
68
- apiRegex: /^\/api\/v1\//, // Regex for removing API prefixes
69
- allowedNetworkList: ["trusted.com"], // List of allowed network domains
37
+ baseDir: path.join(__dirname, "../assets/images/public"),
70
38
  });
71
39
 
72
40
  app.get("/api/v1/pixel/serve", serveImage);
73
41
 
74
42
  app.listen(3000, () => {
75
- console.log("Server is running on http://localhost:3000");
43
+ console.log("Server running on http://localhost:3000");
76
44
  });
77
45
  ```
78
46
 
79
- ---
47
+ ### Advanced Setup with All Options
48
+
49
+ ```typescript
50
+ import express from "express";
51
+ import { registerServe } from "pixel-serve-server";
52
+ import path from "node:path";
53
+
54
+ const app = express();
80
55
 
81
- ### Options
56
+ const serveImage = registerServe({
57
+ // Required: Base directory for public images
58
+ baseDir: path.join(__dirname, "../assets/images/public"),
82
59
 
83
- The `serveImage` middleware accepts the following options:
60
+ // Custom user ID handler
61
+ idHandler: (id: string) => `user-${id}`,
84
62
 
85
- | Option | Type | Description |
86
- | -------------------- | ---------- | ----------------------------------------------------------- |
87
- | `baseDir` | `string` | Base directory for local image files. |
88
- | `idHandler` | `Function` | Function to handle and format user IDs. |
89
- | `getUserFolder` | `Function` | Async function to resolve a user-specific folder path. |
90
- | `websiteURL` | `string` | Your website's base URL for identifying internal resources. |
91
- | `apiRegex` | `RegExp` | Regex to strip API prefixes from internal paths. |
92
- | `allowedNetworkList` | `string[]` | List of allowed domains for network images. |
63
+ // Async function to resolve private folder paths
64
+ getUserFolder: async (req, userId) => {
65
+ // Your logic to resolve user-specific folder
66
+ return `/private/users/${userId}`;
67
+ },
93
68
 
94
- ---
69
+ // Your website's base URL (for treating internal URLs as local)
70
+ websiteURL: "example.com",
95
71
 
96
- ### Example Requests
72
+ // Regex to strip API prefix from internal URLs
73
+ apiRegex: /^\/api\/v1\//,
97
74
 
98
- #### Fetching a Local Image
75
+ // Allowed remote hosts for fetching network images
76
+ allowedNetworkList: ["cdn.example.com", "images.example.com"],
99
77
 
100
- ```bash
101
- GET http://localhost:3000/images?src=/uploads/image1.jpg&width=300&height=300
78
+ // Custom Cache-Control header
79
+ cacheControl: "public, max-age=86400, stale-while-revalidate=604800",
80
+
81
+ // Enable/disable ETag generation
82
+ etag: true,
83
+
84
+ // Image dimension bounds
85
+ minWidth: 50,
86
+ maxWidth: 4000,
87
+ minHeight: 50,
88
+ maxHeight: 4000,
89
+
90
+ // Default JPEG/WebP/AVIF quality
91
+ defaultQuality: 80,
92
+
93
+ // Network fetch timeout (ms)
94
+ requestTimeoutMs: 5000,
95
+
96
+ // Maximum download size from remote sources (bytes)
97
+ maxDownloadBytes: 5_000_000,
98
+ });
99
+
100
+ app.get("/api/v1/pixel/serve", serveImage);
101
+
102
+ app.listen(3000);
102
103
  ```
103
104
 
104
- #### Fetching a Network Image
105
+ ## Configuration Options
106
+
107
+ | Option | Type | Default | Description |
108
+ | -------------------- | ----------------------------------------- | ---------------- | ----------------------------------------------------------------------- |
109
+ | `baseDir` | `string` | **required** | Base directory for local images |
110
+ | `idHandler` | `(id: string) => string` | `id => id` | Transform user IDs before lookup |
111
+ | `getUserFolder` | `(req, id?) => string \| Promise<string>` | `undefined` | Resolve private folder path when `folder=private` |
112
+ | `websiteURL` | `string` | `undefined` | If set, internal URLs pointing to this host are treated as local assets |
113
+ | `apiRegex` | `RegExp` | `/^\/api\/v1\//` | Prefix stripped from internal URLs before lookup |
114
+ | `allowedNetworkList` | `string[]` | `[]` | Allowed remote hosts. Others immediately fall back |
115
+ | `cacheControl` | `string` | `undefined` | Cache-Control header value |
116
+ | `etag` | `boolean` | `true` | Emit ETag and honor If-None-Match |
117
+ | `minWidth` | `number` | `50` | Minimum accepted width |
118
+ | `maxWidth` | `number` | `4000` | Maximum accepted width |
119
+ | `minHeight` | `number` | `50` | Minimum accepted height |
120
+ | `maxHeight` | `number` | `4000` | Maximum accepted height |
121
+ | `defaultQuality` | `number` | `80` | Default JPEG/WebP/AVIF quality |
122
+ | `requestTimeoutMs` | `number` | `5000` | Network fetch timeout |
123
+ | `maxDownloadBytes` | `number` | `5_000_000` | Maximum remote download size |
124
+
125
+ ## Query Parameters
126
+
127
+ | Parameter | Type | Default | Description |
128
+ | --------- | ----------------------- | ----------- | ------------------------------------------------------------------- |
129
+ | `src` | `string` | _required_ | Path or URL to the image source |
130
+ | `format` | `ImageFormat` | `jpeg` | Output format (`jpeg`, `png`, `webp`, `gif`, `tiff`, `avif`, `svg`) |
131
+ | `width` | `number` | `undefined` | Desired output width (px) |
132
+ | `height` | `number` | `undefined` | Desired output height (px) |
133
+ | `quality` | `number` | `80` | Image quality (1-100) |
134
+ | `folder` | `'public' \| 'private'` | `public` | Image folder type |
135
+ | `userId` | `string` | `undefined` | User ID for private folder access |
136
+ | `type` | `'normal' \| 'avatar'` | `normal` | Image type (affects fallback image) |
137
+
138
+ ## Example Requests
139
+
140
+ ### Local Image with Resize
105
141
 
106
142
  ```bash
107
- GET http://localhost:3000/images?src=https://trusted.com/image2.jpg&format=webp
143
+ GET /api/v1/pixel/serve?src=uploads/photo.jpg&width=800&height=600&format=webp
108
144
  ```
109
145
 
110
- #### Handling Private User Folders
146
+ ### Network Image
111
147
 
112
148
  ```bash
113
- GET http://localhost:3000/images?src=/avatar.jpg&folder=private&userId=12345
149
+ GET /api/v1/pixel/serve?src=https://cdn.example.com/image.jpg&format=avif&quality=90
114
150
  ```
115
151
 
116
- ---
152
+ ### Private User Image
117
153
 
118
- ### User Data Parameters
154
+ ```bash
155
+ GET /api/v1/pixel/serve?src=avatar.jpg&folder=private&userId=12345&type=avatar
156
+ ```
157
+
158
+ ## Integration with Pixel Serve Client
119
159
 
120
- The middleware uses the following `UserData` query parameters:
160
+ This package is designed to work seamlessly with [`pixel-serve-client`](https://www.npmjs.com/package/pixel-serve-client), a React component that automatically generates the correct query parameters.
121
161
 
122
- | Parameter | Type | Description |
123
- | --------- | ----------------------- | ---------------------------------------------------- |
124
- | `src` | `string` | Path or URL to the image source. |
125
- | `format` | `ImageFormat` | Desired output format (e.g., `jpeg`, `png`, `webp`). |
126
- | `width` | `number` | Desired width of the output image. |
127
- | `height` | `number` | Desired height of the output image. |
128
- | `quality` | `number` | Image quality (1-100, default: 80). |
129
- | `folder` | `'public' \| 'private'` | Image folder type (default: `public`). |
130
- | `userId` | `string \| null` | User ID for private folder access. |
131
- | `type` | `'normal' \| 'avatar'` | Image type (default: `normal`). |
162
+ ```tsx
163
+ // Client-side (React)
164
+ import Pixel from "pixel-serve-client";
132
165
 
133
- ---
166
+ <Pixel
167
+ src="/uploads/photo.jpg"
168
+ width={800}
169
+ height={600}
170
+ backendUrl="/api/v1/pixel/serve"
171
+ />;
172
+ ```
134
173
 
135
- ### Image Formats
174
+ ## Security Features
136
175
 
137
- The following image formats are supported:
176
+ ### Path Traversal Protection
138
177
 
139
- - `jpeg`
140
- - `jpg`
141
- - `png`
142
- - `webp`
143
- - `gif`
144
- - `tiff`
145
- - `avif`
146
- - `svg`
178
+ All local paths are validated to prevent directory traversal attacks:
147
179
 
148
- Each format is processed with the specified quality settings.
180
+ - Rejects paths with `..`
181
+ - Rejects absolute paths
182
+ - Validates resolved paths stay within `baseDir`
183
+ - Rejects null bytes and control characters
149
184
 
150
- ---
185
+ ### Network Image Security
151
186
 
152
- ### Advanced Configuration
187
+ - Only fetches from explicitly allowed domains (`allowedNetworkList`)
188
+ - Validates MIME type of responses
189
+ - Configurable timeout and size limits
190
+ - Rejects non-HTTP/HTTPS protocols
153
191
 
154
- #### Custom ID Handler
192
+ ### Private Folder Access
155
193
 
156
- Use the `idHandler` option to customize how user IDs are formatted.
194
+ Use `getUserFolder` to implement your own authentication/authorization logic:
157
195
 
158
196
  ```typescript
159
- const options = {
160
- idHandler: (id) => `user-${id.toUpperCase()}`, // Converts ID to uppercase with "user-" prefix
161
- };
197
+ const serveImage = registerServe({
198
+ baseDir: "/public/images",
199
+ getUserFolder: async (req, userId) => {
200
+ const user = await verifyToken(req.headers.authorization);
201
+ if (!user || user.id !== userId) {
202
+ return null; // Will use baseDir instead
203
+ }
204
+ return `/private/users/${userId}`;
205
+ },
206
+ });
162
207
  ```
163
208
 
164
- #### Resolving User Folders
209
+ ## Fallback Images
165
210
 
166
- The `getUserFolder` function dynamically resolves private folder paths for users.
211
+ The package includes built-in fallback images for:
167
212
 
168
- ```typescript
169
- const options = {
170
- getUserFolder: async (id) => `/private/data/users/${id}`, // Returns a private directory path
171
- };
172
- ```
213
+ - **Normal images**: Displayed when an image cannot be loaded
214
+ - **Avatars**: Displayed when an avatar image cannot be loaded
215
+
216
+ These are automatically served when:
173
217
 
174
- #### Allowed Network Domains
218
+ - The requested image doesn't exist
219
+ - Path validation fails
220
+ - Network fetch fails or returns invalid data
221
+ - Image processing fails
175
222
 
176
- Whitelist trusted domains for fetching network images.
223
+ ## Exports
177
224
 
178
225
  ```typescript
179
- const options = {
180
- allowedNetworkList: ["example.com", "cdn.example.com"], // Only allows images from these domains
181
- };
182
- ```
226
+ // Main middleware factory
227
+ import { registerServe } from "pixel-serve-server";
183
228
 
184
- ---
229
+ // Types
230
+ import type {
231
+ PixelServeOptions,
232
+ UserData,
233
+ ImageFormat,
234
+ ImageType,
235
+ } from "pixel-serve-server";
185
236
 
186
- ### Error Handling
237
+ // Zod schemas for validation
238
+ import { optionsSchema, userDataSchema } from "pixel-serve-server";
187
239
 
188
- The middleware automatically falls back to pre-defined images for errors:
240
+ // Utility function
241
+ import { isValidPath } from "pixel-serve-server";
242
+ ```
189
243
 
190
- | Error Condition | Fallback Behavior |
191
- | ----------------------------- | ------------------------------- |
192
- | Invalid local path | Returns a fallback image. |
193
- | Unsupported network domain | Returns a fallback image. |
194
- | Invalid or missing parameters | Defaults to placeholder values. |
244
+ ## Module Formats
245
+
246
+ ```typescript
247
+ // ESM
248
+ import { registerServe } from "pixel-serve-server";
249
+
250
+ // CommonJS
251
+ const { registerServe } = require("pixel-serve-server");
252
+ ```
195
253
 
196
- ### Dependencies
254
+ ## Requirements
197
255
 
198
- This package uses the following dependencies:
256
+ - Node.js >= 18
257
+ - Express 5.x (included as a dependency)
199
258
 
200
- - **Express**: HTTP server framework.
201
- - **Sharp**: High-performance image processing.
202
- - **Axios**: HTTP client for fetching network images.
259
+ ## Dependencies
203
260
 
204
- ---
261
+ - **Sharp**: High-performance image processing
262
+ - **Axios**: HTTP client for fetching network images
263
+ - **Zod**: Runtime validation for options and query params
205
264
 
206
- ### License
265
+ ## License
207
266
 
208
- This package is licensed under the [MIT License](LICENSE).
267
+ MIT
209
268
 
210
- ### Feedback
269
+ ## Contributing
211
270
 
212
- If you encounter issues or have suggestions, feel free to open an [issue](https://github.com/Hiprax/pixel-serve-server/issues).
271
+ Issues and pull requests are welcome at [GitHub](https://github.com/Hiprax/pixel-serve-server).
package/dist/index.d.mts CHANGED
@@ -1,20 +1,30 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
+ import { z } from 'zod';
2
3
 
3
4
  type ImageType = "avatar" | "normal";
4
5
  type ImageFormat = "jpeg" | "jpg" | "png" | "webp" | "gif" | "tiff" | "avif" | "svg";
5
- type Options = {
6
+ type PixelServeOptions = {
6
7
  baseDir: string;
7
8
  idHandler?: (id: string) => string;
8
- getUserFolder?: (req: Request, id?: string | undefined) => Promise<string>;
9
+ getUserFolder?: (req: Request, id?: string) => Promise<string> | string;
9
10
  websiteURL?: string;
10
11
  apiRegex?: RegExp;
11
12
  allowedNetworkList?: string[];
13
+ cacheControl?: string;
14
+ etag?: boolean;
15
+ minWidth?: number;
16
+ maxWidth?: number;
17
+ minHeight?: number;
18
+ maxHeight?: number;
19
+ defaultQuality?: number;
20
+ requestTimeoutMs?: number;
21
+ maxDownloadBytes?: number;
12
22
  };
13
23
  type UserData = {
14
- quality: number | string;
15
- format: ImageFormat;
16
- src?: string;
17
- folder?: string;
24
+ src: string;
25
+ quality?: number | string;
26
+ format?: ImageFormat;
27
+ folder?: "public" | "private";
18
28
  type?: ImageType;
19
29
  userId?: string;
20
30
  width?: number | string;
@@ -24,10 +34,44 @@ type UserData = {
24
34
  /**
25
35
  * @function registerServe
26
36
  * @description A function to register the serveImage function as middleware for Express.
27
- * @param {Options} options - The options object for image processing.
37
+ * @param {PixelServeOptions} options - The options object for image processing.
28
38
  * @returns {function(Request, Response, NextFunction): Promise<void>} The middleware function.
29
39
  */
30
- declare const registerServe: (options: Options) => (req: Request, res: Response, next: NextFunction) => Promise<void>;
40
+ declare const registerServe: (options: PixelServeOptions) => ((req: Request, res: Response, next: NextFunction) => Promise<void>);
41
+
42
+ declare const userDataSchema: z.ZodObject<{
43
+ src: z.ZodDefault<z.ZodOptional<z.ZodString>>;
44
+ format: z.ZodOptional<z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>>;
45
+ width: z.ZodPipe<z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>, z.ZodTransform<number | undefined, string | number | undefined>>, z.ZodOptional<z.ZodNumber>>;
46
+ height: z.ZodPipe<z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>, z.ZodTransform<number | undefined, string | number | undefined>>, z.ZodOptional<z.ZodNumber>>;
47
+ quality: z.ZodPipe<z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>, z.ZodTransform<number | undefined, string | number | undefined>>, z.ZodDefault<z.ZodNumber>>;
48
+ folder: z.ZodDefault<z.ZodEnum<{
49
+ public: "public";
50
+ private: "private";
51
+ }>>;
52
+ type: z.ZodDefault<z.ZodEnum<{
53
+ avatar: "avatar";
54
+ normal: "normal";
55
+ }>>;
56
+ userId: z.ZodPipe<z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>, z.ZodTransform<string | undefined, string | number | undefined>>, z.ZodOptional<z.ZodString>>;
57
+ }, z.core.$strict>;
58
+ declare const optionsSchema: z.ZodObject<{
59
+ baseDir: z.ZodString;
60
+ idHandler: z.ZodOptional<z.ZodCustom<(id: string) => string, (id: string) => string>>;
61
+ getUserFolder: z.ZodOptional<z.ZodCustom<(req: unknown, id?: string) => Promise<string> | string, (req: unknown, id?: string) => Promise<string> | string>>;
62
+ websiteURL: z.ZodOptional<z.ZodUnion<readonly [z.ZodURL, z.ZodString]>>;
63
+ apiRegex: z.ZodDefault<z.ZodCustom<RegExp, RegExp>>;
64
+ allowedNetworkList: z.ZodDefault<z.ZodArray<z.ZodString>>;
65
+ cacheControl: z.ZodOptional<z.ZodString>;
66
+ etag: z.ZodDefault<z.ZodBoolean>;
67
+ minWidth: z.ZodDefault<z.ZodNumber>;
68
+ maxWidth: z.ZodDefault<z.ZodNumber>;
69
+ minHeight: z.ZodDefault<z.ZodNumber>;
70
+ maxHeight: z.ZodDefault<z.ZodNumber>;
71
+ defaultQuality: z.ZodDefault<z.ZodNumber>;
72
+ requestTimeoutMs: z.ZodDefault<z.ZodNumber>;
73
+ maxDownloadBytes: z.ZodDefault<z.ZodNumber>;
74
+ }, z.core.$strict>;
31
75
 
32
76
  /**
33
77
  * @typedef {("avatar" | "normal")} ImageType
@@ -38,8 +82,8 @@ declare const registerServe: (options: Options) => (req: Request, res: Response,
38
82
  *
39
83
  * @param {string} basePath - The base directory to resolve paths.
40
84
  * @param {string} specifiedPath - The path to check.
41
- * @returns {boolean} True if the path is valid, false otherwise.
85
+ * @returns {Promise<boolean>} True if the path is valid, false otherwise.
42
86
  */
43
87
  declare const isValidPath: (basePath: string, specifiedPath: string) => Promise<boolean>;
44
88
 
45
- export { type ImageFormat, type ImageType, type Options, type UserData, isValidPath, registerServe };
89
+ export { type ImageFormat, type ImageType, type PixelServeOptions, type UserData, isValidPath, optionsSchema, registerServe, userDataSchema };
package/dist/index.d.ts CHANGED
@@ -1,20 +1,30 @@
1
1
  import { Request, Response, NextFunction } from 'express';
2
+ import { z } from 'zod';
2
3
 
3
4
  type ImageType = "avatar" | "normal";
4
5
  type ImageFormat = "jpeg" | "jpg" | "png" | "webp" | "gif" | "tiff" | "avif" | "svg";
5
- type Options = {
6
+ type PixelServeOptions = {
6
7
  baseDir: string;
7
8
  idHandler?: (id: string) => string;
8
- getUserFolder?: (req: Request, id?: string | undefined) => Promise<string>;
9
+ getUserFolder?: (req: Request, id?: string) => Promise<string> | string;
9
10
  websiteURL?: string;
10
11
  apiRegex?: RegExp;
11
12
  allowedNetworkList?: string[];
13
+ cacheControl?: string;
14
+ etag?: boolean;
15
+ minWidth?: number;
16
+ maxWidth?: number;
17
+ minHeight?: number;
18
+ maxHeight?: number;
19
+ defaultQuality?: number;
20
+ requestTimeoutMs?: number;
21
+ maxDownloadBytes?: number;
12
22
  };
13
23
  type UserData = {
14
- quality: number | string;
15
- format: ImageFormat;
16
- src?: string;
17
- folder?: string;
24
+ src: string;
25
+ quality?: number | string;
26
+ format?: ImageFormat;
27
+ folder?: "public" | "private";
18
28
  type?: ImageType;
19
29
  userId?: string;
20
30
  width?: number | string;
@@ -24,10 +34,44 @@ type UserData = {
24
34
  /**
25
35
  * @function registerServe
26
36
  * @description A function to register the serveImage function as middleware for Express.
27
- * @param {Options} options - The options object for image processing.
37
+ * @param {PixelServeOptions} options - The options object for image processing.
28
38
  * @returns {function(Request, Response, NextFunction): Promise<void>} The middleware function.
29
39
  */
30
- declare const registerServe: (options: Options) => (req: Request, res: Response, next: NextFunction) => Promise<void>;
40
+ declare const registerServe: (options: PixelServeOptions) => ((req: Request, res: Response, next: NextFunction) => Promise<void>);
41
+
42
+ declare const userDataSchema: z.ZodObject<{
43
+ src: z.ZodDefault<z.ZodOptional<z.ZodString>>;
44
+ format: z.ZodOptional<z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string | undefined, string | undefined>>>;
45
+ width: z.ZodPipe<z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>, z.ZodTransform<number | undefined, string | number | undefined>>, z.ZodOptional<z.ZodNumber>>;
46
+ height: z.ZodPipe<z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>, z.ZodTransform<number | undefined, string | number | undefined>>, z.ZodOptional<z.ZodNumber>>;
47
+ quality: z.ZodPipe<z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>, z.ZodTransform<number | undefined, string | number | undefined>>, z.ZodDefault<z.ZodNumber>>;
48
+ folder: z.ZodDefault<z.ZodEnum<{
49
+ public: "public";
50
+ private: "private";
51
+ }>>;
52
+ type: z.ZodDefault<z.ZodEnum<{
53
+ avatar: "avatar";
54
+ normal: "normal";
55
+ }>>;
56
+ userId: z.ZodPipe<z.ZodPipe<z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>, z.ZodTransform<string | undefined, string | number | undefined>>, z.ZodOptional<z.ZodString>>;
57
+ }, z.core.$strict>;
58
+ declare const optionsSchema: z.ZodObject<{
59
+ baseDir: z.ZodString;
60
+ idHandler: z.ZodOptional<z.ZodCustom<(id: string) => string, (id: string) => string>>;
61
+ getUserFolder: z.ZodOptional<z.ZodCustom<(req: unknown, id?: string) => Promise<string> | string, (req: unknown, id?: string) => Promise<string> | string>>;
62
+ websiteURL: z.ZodOptional<z.ZodUnion<readonly [z.ZodURL, z.ZodString]>>;
63
+ apiRegex: z.ZodDefault<z.ZodCustom<RegExp, RegExp>>;
64
+ allowedNetworkList: z.ZodDefault<z.ZodArray<z.ZodString>>;
65
+ cacheControl: z.ZodOptional<z.ZodString>;
66
+ etag: z.ZodDefault<z.ZodBoolean>;
67
+ minWidth: z.ZodDefault<z.ZodNumber>;
68
+ maxWidth: z.ZodDefault<z.ZodNumber>;
69
+ minHeight: z.ZodDefault<z.ZodNumber>;
70
+ maxHeight: z.ZodDefault<z.ZodNumber>;
71
+ defaultQuality: z.ZodDefault<z.ZodNumber>;
72
+ requestTimeoutMs: z.ZodDefault<z.ZodNumber>;
73
+ maxDownloadBytes: z.ZodDefault<z.ZodNumber>;
74
+ }, z.core.$strict>;
31
75
 
32
76
  /**
33
77
  * @typedef {("avatar" | "normal")} ImageType
@@ -38,8 +82,8 @@ declare const registerServe: (options: Options) => (req: Request, res: Response,
38
82
  *
39
83
  * @param {string} basePath - The base directory to resolve paths.
40
84
  * @param {string} specifiedPath - The path to check.
41
- * @returns {boolean} True if the path is valid, false otherwise.
85
+ * @returns {Promise<boolean>} True if the path is valid, false otherwise.
42
86
  */
43
87
  declare const isValidPath: (basePath: string, specifiedPath: string) => Promise<boolean>;
44
88
 
45
- export { type ImageFormat, type ImageType, type Options, type UserData, isValidPath, registerServe };
89
+ export { type ImageFormat, type ImageType, type PixelServeOptions, type UserData, isValidPath, optionsSchema, registerServe, userDataSchema };
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
- "use strict";var S=Object.create;var h=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var B=Object.getOwnPropertyNames;var C=Object.getPrototypeOf,z=Object.prototype.hasOwnProperty;var G=(e,r)=>{for(var a in r)h(e,a,{get:r[a],enumerable:!0})},O=(e,r,a,s)=>{if(r&&typeof r=="object"||typeof r=="function")for(let t of B(r))!z.call(e,t)&&t!==a&&h(e,t,{get:()=>r[t],enumerable:!(s=_(r,t))||s.enumerable});return e};var c=(e,r,a)=>(a=e!=null?S(C(e)):{},O(r||!e||!e.__esModule?h(a,"default",{value:e,enumerable:!0}):a,e)),$=e=>O(h({},"__esModule",{value:!0}),e);var Q={};G(Q,{isValidPath:()=>b,registerServe:()=>D});module.exports=$(Q);var k=()=>typeof document>"u"?new URL(`file:${__filename}`).href:document.currentScript&&document.currentScript.src||new URL("main.js",document.baseURI).href,l=k();var v=c(require("path")),U=c(require("sharp"));var x=require("fs/promises"),R=c(require("path")),T=require("url"),H=R.default.dirname((0,T.fileURLToPath)(l)),L=e=>R.default.join(H,"assets",e),V=L("noimage.jpg"),W=L("noavatar.png"),u={normal:async()=>(0,x.readFile)(V),avatar:async()=>(0,x.readFile)(W)},w=/^\/api\/v1\//,j=["jpeg","jpg","png","webp","gif","tiff","avif","svg"],I={jpeg:"image/jpeg",jpg:"image/jpeg",png:"image/png",webp:"image/webp",gif:"image/gif",tiff:"image/tiff",avif:"image/avif",svg:"image/svg+xml"};var o=c(require("path")),g=c(require("fs/promises")),A=c(require("axios"));var b=async(e,r)=>{try{if(!e||!r||r.includes("\0")||o.default.isAbsolute(r)||!/^[^\x00-\x1F]+$/.test(r))return!1;let a=o.default.resolve(e),s=o.default.resolve(a,r),[t,i]=await Promise.all([g.realpath(a),g.realpath(s)]);if(!(await g.stat(t)).isDirectory())return!1;let f=t+o.default.sep,y=(i+o.default.sep).startsWith(f)||i===t,d=o.default.relative(t,i);return!d.startsWith("..")&&!o.default.isAbsolute(d)&&y}catch{return!1}},X=async(e,r="normal")=>{try{let a=await A.default.get(e,{responseType:"arraybuffer",timeout:5e3}),s=a.headers["content-type"]?.toLowerCase();return Object.values(I).includes(s??"")?Buffer.from(a.data):await u[r]()}catch{return await u[r]()}},F=async(e,r,a="normal")=>{if(!await b(r,e))return await u[a]();try{return await g.readFile(o.default.resolve(r,e))}catch{return await u[a]()}},N=(e,r,a,s="normal",t=w,i=[])=>{let n=new URL(e);if([a,`www.${a}`].includes(n.host)){let m=n.pathname.replace(t,"");return F(m,r,s)}else return i.includes(n.host)?X(e,s):u[s]()};var q=e=>({...{baseDir:"",idHandler:a=>a,getUserFolder:async()=>"",websiteURL:"",apiRegex:w,allowedNetworkList:[]},...e}),E=e=>({...{quality:80,format:"jpeg",src:"/placeholder/noimage.jpg",folder:"public",type:"normal",width:void 0,height:void 0,userId:void 0},...e,quality:e.quality?Math.min(Math.max(Number(e.quality)||80,1),100):100,width:e.width?Math.min(Math.max(Number(e.width),50),2e3):void 0,height:e.height?Math.min(Math.max(Number(e.height),50),2e3):void 0});var K=async(e,r,a,s)=>{try{let t=E(e.query),i=q(s),n,f=i.baseDir,m;if(t.userId){let p=typeof t.userId=="object"?String(Object.values(t.userId)[0]):String(t.userId);i.idHandler?m=i.idHandler(p):m=p}if(t.folder==="private"){let p=await i?.getUserFolder?.(e,m);p&&(f=p)}let y=j.includes(t?.format?.toLowerCase())?t?.format?.toLowerCase():"jpeg";t?.src?.startsWith("http")?n=await N(t?.src??"",f,i?.websiteURL??"",t?.type,i?.apiRegex,i?.allowedNetworkList):n=await F(t?.src??"",f,t?.type);let d=(0,U.default)(n);if(t?.width||t?.height){let p={width:t?.width??void 0,height:t?.height??void 0,fit:U.default.fit.cover};d=d.resize(p)}let M=await d.toFormat(y,{quality:t?.quality?Number(t?.quality):80}).toBuffer(),P=`${v.default.basename(t?.src??"",v.default.extname(t?.src??""))}.${y}`;r.type(I[y]),r.setHeader("Content-Disposition",`inline; filename="${P}"`),r.send(M)}catch(t){a(t)}},J=e=>async(r,a,s)=>K(r,a,s,e),D=J;0&&(module.exports={isValidPath,registerServe});
1
+ 'use strict';var p=require('path'),crypto=require('crypto'),N=require('sharp'),d=require('fs/promises'),url=require('url'),$=require('axios'),zod=require('zod');function _interopDefault(e){return e&&e.__esModule?e:{default:e}}function _interopNamespace(e){if(e&&e.__esModule)return e;var n=Object.create(null);if(e){Object.keys(e).forEach(function(k){if(k!=='default'){var d=Object.getOwnPropertyDescriptor(e,k);Object.defineProperty(n,k,d.get?d:{enumerable:true,get:function(){return e[k]}});}})}n.default=e;return Object.freeze(n)}var p__default=/*#__PURE__*/_interopDefault(p);var N__default=/*#__PURE__*/_interopDefault(N);var d__namespace=/*#__PURE__*/_interopNamespace(d);var $__default=/*#__PURE__*/_interopDefault($);var j=()=>typeof document>"u"?new URL(`file:${__filename}`).href:document.currentScript&&document.currentScript.tagName.toUpperCase()==="SCRIPT"?document.currentScript.src:new URL("main.js",document.baseURI).href,f=j();var _=p__default.default.dirname(url.fileURLToPath(f)),D=e=>p__default.default.join(_,"assets",e),z=D("noimage.jpg"),k=D("noavatar.png"),u={normal:async()=>d.readFile(z),avatar:async()=>d.readFile(k)},U=/^\/api\/v1\//,x=["jpeg","jpg","png","webp","gif","tiff","avif","svg"],g={jpeg:"image/jpeg",jpg:"image/jpeg",png:"image/png",webp:"image/webp",gif:"image/gif",tiff:"image/tiff",avif:"image/avif",svg:"image/svg+xml"};var B=async(e,r)=>{try{if(!e||!r||r.includes("\0")||p__default.default.isAbsolute(r)||!/^[^\x00-\x1F]+$/.test(r))return !1;let i=p__default.default.resolve(e),o=p__default.default.resolve(i,r),[s,a]=await Promise.all([d__namespace.realpath(i),d__namespace.realpath(o)]);if(!(await d__namespace.stat(s)).isDirectory())return !1;let m=s+p__default.default.sep,c=(a+p__default.default.sep).startsWith(m)||a===s,h=p__default.default.relative(s,a);return !h.startsWith("..")&&!p__default.default.isAbsolute(h)&&c}catch{return false}},G=async(e,r="normal",{timeoutMs:i,maxBytes:o})=>{try{let s=await $__default.default.get(e,{responseType:"arraybuffer",timeout:i,maxContentLength:o,maxBodyLength:o,validateStatus:m=>m>=200&&m<300}),a=s.headers["content-type"]?.toLowerCase()?.split(";")[0]?.trim();return Object.values(g).includes(a??"")?Buffer.from(s.data):await u[r]()}catch{return await u[r]()}},b=async(e,r,i="normal",o)=>{if(!await B(r,e))return await u[i]();try{let a=p__default.default.resolve(r,e);return o&&(await d__namespace.stat(a)).size>o?await u[i]():await d__namespace.readFile(a)}catch{return await u[i]()}},C=(e,r,i,o="normal",s,a=[],{timeoutMs:n,maxBytes:m})=>{try{let l=new URL(e);if(i!==void 0&&[i,`www.${i}`].includes(l.hostname)){let w=l.pathname.replace(s,"");return b(w,r,o,m)}return a.includes(l.hostname)||a.includes(l.host)?["http:","https:"].includes(l.protocol)?G(e,o,{timeoutMs:n,maxBytes:m}):u[o]():u[o]()}catch{return b(e,r,o,m)}};var Q=zod.z.enum(x),V=zod.z.enum(["avatar","normal"]),T=zod.z.object({src:zod.z.string().min(1,"src is required").optional().default("/placeholder/noimage.jpg"),format:zod.z.string().optional().transform(e=>{let r=e?.toLowerCase();return r&&Q.options.includes(r)?r:void 0}).optional(),width:zod.z.union([zod.z.number(),zod.z.string()]).optional().transform(e=>e==null?void 0:Number(e)).pipe(zod.z.number().int().min(50,"width too small").max(4e3,"width too large").optional()),height:zod.z.union([zod.z.number(),zod.z.string()]).optional().transform(e=>e==null?void 0:Number(e)).pipe(zod.z.number().int().min(50,"height too small").max(4e3,"height too large").optional()),quality:zod.z.union([zod.z.number(),zod.z.string()]).optional().transform(e=>e==null?void 0:Number(e)).pipe(zod.z.number().int().min(1).max(100).default(80)),folder:zod.z.enum(["public","private"]).default("public"),type:V.default("normal"),userId:zod.z.union([zod.z.string(),zod.z.number()]).optional().transform(e=>e==null?void 0:String(e).trim()).pipe(zod.z.string().min(1,"userId cannot be empty").max(128,"userId too long").optional())}).strict(),R=zod.z.object({baseDir:zod.z.string().min(1,"baseDir is required"),idHandler:zod.z.custom(e=>typeof e=="function",{message:"idHandler must be a function"}).optional(),getUserFolder:zod.z.custom(e=>typeof e=="function",{message:"getUserFolder must be a function"}).optional(),websiteURL:zod.z.union([zod.z.url(),zod.z.string().regex(/^(?![-.])([\w]+[-.]?)*[\w]+$/)]).optional(),apiRegex:zod.z.instanceof(RegExp).default(U),allowedNetworkList:zod.z.array(zod.z.string()).default([]),cacheControl:zod.z.string().optional(),etag:zod.z.boolean().default(true),minWidth:zod.z.number().int().positive().default(50),maxWidth:zod.z.number().int().positive().default(4e3),minHeight:zod.z.number().int().positive().default(50),maxHeight:zod.z.number().int().positive().default(4e3),defaultQuality:zod.z.number().int().min(1).max(100).default(80),requestTimeoutMs:zod.z.number().int().positive().default(5e3),maxDownloadBytes:zod.z.number().int().positive().default(5e6)}).strict().refine(e=>e.minWidth<=e.maxWidth,{message:"minWidth must be less than or equal to maxWidth",path:["minWidth"]}).refine(e=>e.minHeight<=e.maxHeight,{message:"minHeight must be less than or equal to maxHeight",path:["minHeight"]});var E=e=>R.parse(e),O=(e,r)=>{let i=T.parse(e),o=(s,a,n)=>{if(s!==void 0)return Math.min(Math.max(s,a),n)};return {...i,width:o(i.width,r.minWidth,r.maxWidth),height:o(i.height,r.minHeight,r.maxHeight),quality:i.quality??r.defaultQuality,format:i.format??"jpeg"}};var X=async(e,r,i,o)=>{let s="normal";try{let a=E(o),n=O(e.query,{minWidth:a.minWidth,maxWidth:a.maxWidth,minHeight:a.minHeight,maxHeight:a.maxHeight,defaultQuality:a.defaultQuality});s=n.type??"normal";let m=a.baseDir,l;if(n.userId&&(l=a.idHandler?a.idHandler(n.userId):n.userId),n.folder==="private"&&a.getUserFolder){let F=Promise.resolve(a.getUserFolder(e,l)),q=new Promise((P,A)=>setTimeout(()=>A(new Error("getUserFolder timed out")),a.requestTimeoutMs));try{let P=await Promise.race([F,q]);P&&(m=P);}catch{}}let c=x.includes((n.format??"").toLowerCase())?n.format:"jpeg",w=await(async()=>n.src?n.src.startsWith("http://")||n.src.startsWith("https://")?C(n.src,m,a.websiteURL,n.type,a.apiRegex,a.allowedNetworkList,{timeoutMs:a.requestTimeoutMs,maxBytes:a.maxDownloadBytes}):b(n.src,m,n.type,a.maxDownloadBytes):u[n.type??"normal"]())(),I=N__default.default(w,{failOn:"truncated"});if(n.width||n.height){let F={width:n.width??void 0,height:n.height??void 0,fit:N__default.default.fit.cover,withoutEnlargement:!0};I=I.resize(F);}let v=await I.rotate().toFormat(c,{quality:n.quality}).toBuffer(),W=`${(n.src?p__default.default.basename(n.src,p__default.default.extname(n.src)):"image").replace(/["\\\x00-\x1F\x7F]/g,"_")}.${c}`,y=a.etag?`"${crypto.createHash("sha1").update(v).digest("hex")}"`:void 0;if(y&&e.headers["if-none-match"]===y){r.status(304).end();return}r.type(g[c]),r.setHeader("Content-Disposition",`inline; filename="${W}"`),r.setHeader("Cache-Control",a.cacheControl??"public, max-age=86400, stale-while-revalidate=604800"),y&&r.setHeader("ETag",y),r.setHeader("Content-Length",v.length.toString()),r.send(v);}catch{try{let n=await u[s==="avatar"?"avatar":"normal"]();r.type(g.jpeg),r.setHeader("Content-Disposition",'inline; filename="fallback.jpeg"'),r.setHeader("Cache-Control","public, max-age=60"),r.send(n);}catch(a){i(a);}}},J=e=>async(r,i,o)=>X(r,i,o,e),Y=J;
2
+ exports.isValidPath=B;exports.optionsSchema=R;exports.registerServe=Y;exports.userDataSchema=T;//# sourceMappingURL=index.js.map
2
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../node_modules/tsup/assets/cjs_shims.js","../src/pixel.ts","../src/variables.ts","../src/functions.ts","../src/renders.ts"],"sourcesContent":["/**\r\n * @module ImageService\r\n * @description A module to serve, process, and manage image delivery for web applications.\r\n */\r\n\r\nexport { default as registerServe } from \"./pixel\";\r\nexport * from \"./types\";\r\nexport { isValidPath } from \"./functions\";\r\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () =>\n typeof document === 'undefined'\n ? new URL(`file:${__filename}`).href\n : (document.currentScript && document.currentScript.src) ||\n new URL('main.js', document.baseURI).href\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","import path from \"node:path\";\r\nimport sharp, { FormatEnum, ResizeOptions } from \"sharp\";\r\nimport type { Request, Response, NextFunction } from \"express\";\r\nimport type { Options, UserData, ImageFormat, ImageType } from \"./types\";\r\nimport { allowedFormats, mimeTypes } from \"./variables\";\r\nimport { fetchImage, readLocalImage } from \"./functions\";\r\nimport { renderOptions, renderUserData } from \"./renders\";\r\n\r\n/**\r\n * @typedef {Object} Options\r\n * @property {string} baseDir - The base directory for public image files.\r\n * @property {function(string): string} idHandler - A function to handle user IDs.\r\n * @property {function(string, Request): Promise<string>} getUserFolder - Asynchronous function to retrieve user-specific folders.\r\n * @property {string} websiteURL - The base URL of the website for internal link resolution.\r\n * @property {RegExp} apiRegex - Regex to parse API endpoints from URLs.\r\n * @property {string[]} allowedNetworkList - List of allowed network domains for external image fetching.\r\n */\r\n\r\n/**\r\n * @function serveImage\r\n * @description Processes and serves an image based on user data and options.\r\n * @param {Request} req - The Express request object.\r\n * @param {Response} res - The Express response object.\r\n * @param {NextFunction} next - The Express next function.\r\n * @param {Options} options - The options object for image processing.\r\n * @returns {Promise<void>}\r\n */\r\nconst serveImage = async (\r\n req: Request,\r\n res: Response,\r\n next: NextFunction,\r\n options: Options\r\n) => {\r\n try {\r\n const userData = renderUserData(req.query as UserData);\r\n const parsedOptions = renderOptions(options);\r\n\r\n let imageBuffer;\r\n let baseDir = parsedOptions.baseDir;\r\n let parsedUserId;\r\n\r\n if (userData.userId) {\r\n const userIdStr =\r\n typeof userData.userId === \"object\"\r\n ? String(Object.values(userData.userId)[0])\r\n : String(userData.userId);\r\n if (parsedOptions.idHandler) {\r\n parsedUserId = parsedOptions.idHandler(userIdStr);\r\n } else {\r\n parsedUserId = userIdStr;\r\n }\r\n }\r\n\r\n if (userData.folder === \"private\") {\r\n const dir = await parsedOptions?.getUserFolder?.(req, parsedUserId);\r\n if (dir) {\r\n baseDir = dir;\r\n }\r\n }\r\n\r\n const outputFormat = allowedFormats.includes(\r\n userData?.format?.toLowerCase() as ImageFormat\r\n )\r\n ? userData?.format?.toLowerCase()\r\n : \"jpeg\";\r\n\r\n if (userData?.src?.startsWith(\"http\")) {\r\n imageBuffer = await fetchImage(\r\n userData?.src ?? \"\",\r\n baseDir,\r\n parsedOptions?.websiteURL ?? \"\",\r\n userData?.type as ImageType,\r\n parsedOptions?.apiRegex,\r\n parsedOptions?.allowedNetworkList\r\n );\r\n } else {\r\n imageBuffer = await readLocalImage(\r\n userData?.src ?? \"\",\r\n baseDir,\r\n userData?.type as ImageType\r\n );\r\n }\r\n\r\n let image = sharp(imageBuffer);\r\n\r\n if (userData?.width || userData?.height) {\r\n const resizeOptions = {\r\n width: userData?.width ?? undefined,\r\n height: userData?.height ?? undefined,\r\n fit: sharp.fit.cover,\r\n };\r\n image = image.resize(resizeOptions as ResizeOptions);\r\n }\r\n\r\n const processedImage = await image\r\n .toFormat(outputFormat as keyof FormatEnum, {\r\n quality: userData?.quality ? Number(userData?.quality) : 80,\r\n })\r\n .toBuffer();\r\n\r\n const processedFileName = `${path.basename(\r\n userData?.src ?? \"\",\r\n path.extname(userData?.src ?? \"\")\r\n )}.${outputFormat}`;\r\n\r\n res.type(mimeTypes[outputFormat]);\r\n res.setHeader(\r\n \"Content-Disposition\",\r\n `inline; filename=\"${processedFileName}\"`\r\n );\r\n res.send(processedImage);\r\n } catch (error) {\r\n next(error);\r\n }\r\n};\r\n\r\n/**\r\n * @function registerServe\r\n * @description A function to register the serveImage function as middleware for Express.\r\n * @param {Options} options - The options object for image processing.\r\n * @returns {function(Request, Response, NextFunction): Promise<void>} The middleware function.\r\n */\r\nconst registerServe = (options: Options) => {\r\n return async (req: Request, res: Response, next: NextFunction) =>\r\n serveImage(req, res, next, options);\r\n};\r\n\r\nexport default registerServe;\r\n","import type { ImageFormat } from \"./types\";\r\nimport { readFile } from \"node:fs/promises\";\r\nimport path from \"node:path\";\r\nimport { fileURLToPath } from \"node:url\";\r\n\r\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\r\n\r\nconst getAssetPath = (filename: string) => {\r\n return path.join(__dirname, \"assets\", filename);\r\n};\r\n\r\nconst NOT_FOUND_IMAGE = getAssetPath(\"noimage.jpg\");\r\nconst NOT_FOUND_AVATAR = getAssetPath(\"noavatar.png\");\r\n\r\nexport const FALLBACKIMAGES = {\r\n normal: async () => readFile(NOT_FOUND_IMAGE),\r\n avatar: async () => readFile(NOT_FOUND_AVATAR),\r\n};\r\n\r\nexport const API_REGEX: RegExp = /^\\/api\\/v1\\//;\r\n\r\nexport const allowedFormats: ImageFormat[] = [\r\n \"jpeg\",\r\n \"jpg\",\r\n \"png\",\r\n \"webp\",\r\n \"gif\",\r\n \"tiff\",\r\n \"avif\",\r\n \"svg\",\r\n];\r\n\r\nexport const mimeTypes: Readonly<Record<string, string>> = {\r\n jpeg: \"image/jpeg\",\r\n jpg: \"image/jpeg\",\r\n png: \"image/png\",\r\n webp: \"image/webp\",\r\n gif: \"image/gif\",\r\n tiff: \"image/tiff\",\r\n avif: \"image/avif\",\r\n svg: \"image/svg+xml\",\r\n};\r\n","import path from \"node:path\";\r\nimport * as fs from \"node:fs/promises\";\r\nimport axios from \"axios\";\r\nimport { mimeTypes, API_REGEX, FALLBACKIMAGES } from \"./variables\";\r\nimport type { ImageType } from \"./types\";\r\n\r\n/**\r\n * @typedef {(\"avatar\" | \"normal\")} ImageType\r\n * @description Defines the type of image being processed.\r\n */\r\n\r\n/**\r\n * Checks if a specified path is valid within a base path.\r\n *\r\n * @param {string} basePath - The base directory to resolve paths.\r\n * @param {string} specifiedPath - The path to check.\r\n * @returns {boolean} True if the path is valid, false otherwise.\r\n */\r\nexport const isValidPath = async (\r\n basePath: string,\r\n specifiedPath: string\r\n): Promise<boolean> => {\r\n try {\r\n if (!basePath || !specifiedPath) return false;\r\n if (specifiedPath.includes(\"\\0\")) return false;\r\n if (path.isAbsolute(specifiedPath)) return false;\r\n if (!/^[^\\x00-\\x1F]+$/.test(specifiedPath)) return false;\r\n\r\n const resolvedBase = path.resolve(basePath);\r\n const resolvedPath = path.resolve(resolvedBase, specifiedPath);\r\n\r\n const [realBase, realPath] = await Promise.all([\r\n fs.realpath(resolvedBase),\r\n fs.realpath(resolvedPath),\r\n ]);\r\n\r\n const baseStats = await fs.stat(realBase);\r\n if (!baseStats.isDirectory()) return false;\r\n\r\n const normalizedBase = realBase + path.sep;\r\n const normalizedPath = realPath + path.sep;\r\n\r\n const isInside =\r\n normalizedPath.startsWith(normalizedBase) || realPath === realBase;\r\n\r\n const relative = path.relative(realBase, realPath);\r\n return !relative.startsWith(\"..\") && !path.isAbsolute(relative) && isInside;\r\n } catch {\r\n return false;\r\n }\r\n};\r\n\r\n/**\r\n * Fetches an image from a network source.\r\n *\r\n * @param {string} src - The URL of the image.\r\n * @param {ImageType} [type=\"normal\"] - Type of fallback image in case of an error.\r\n * @returns {Promise<Buffer>} A buffer containing the image data or a fallback image.\r\n */\r\nconst fetchFromNetwork = async (\r\n src: string,\r\n type: ImageType = \"normal\"\r\n): Promise<Buffer> => {\r\n try {\r\n const response = await axios.get(src, {\r\n responseType: \"arraybuffer\",\r\n timeout: 5000,\r\n });\r\n\r\n const contentType = response.headers[\"content-type\"]?.toLowerCase();\r\n const allowedMimeTypes = Object.values(mimeTypes);\r\n\r\n if (allowedMimeTypes.includes(contentType ?? \"\")) {\r\n return Buffer.from(response.data);\r\n }\r\n return await FALLBACKIMAGES[type]();\r\n } catch (error) {\r\n return await FALLBACKIMAGES[type]();\r\n }\r\n};\r\n\r\n/**\r\n * Reads an image from the local file system.\r\n *\r\n * @param {string} filePath - Path to the image file.\r\n * @param {string} baseDir - Base directory to resolve paths.\r\n * @param {ImageType} [type=\"normal\"] - Type of fallback image if the path is invalid.\r\n * @returns {Promise<Buffer>} A buffer containing the image data.\r\n */\r\nexport const readLocalImage = async (\r\n filePath: string,\r\n baseDir: string,\r\n type: ImageType = \"normal\"\r\n) => {\r\n const isValid = await isValidPath(baseDir, filePath);\r\n if (!isValid) {\r\n return await FALLBACKIMAGES[type]();\r\n }\r\n try {\r\n return await fs.readFile(path.resolve(baseDir, filePath));\r\n } catch (error) {\r\n return await FALLBACKIMAGES[type]();\r\n }\r\n};\r\n\r\n/**\r\n * Fetches an image from either a local file or a network source.\r\n *\r\n * @param {string} src - The URL or local path of the image.\r\n * @param {string} baseDir - Base directory to resolve local paths.\r\n * @param {string} websiteURL - The URL of the website.\r\n * @param {ImageType} [type=\"normal\"] - Type of fallback image if the path is invalid.\r\n * @param {RegExp} [apiRegex=API_REGEX] - Regular expression to match API routes.\r\n * @param {string[]} [allowedNetworkList=[]] - List of allowed network hosts.\r\n * @returns {Promise<Buffer>} A buffer containing the image data or a fallback image.\r\n */\r\nexport const fetchImage = (\r\n src: string,\r\n baseDir: string,\r\n websiteURL: string,\r\n type: ImageType = \"normal\",\r\n apiRegex: RegExp = API_REGEX,\r\n allowedNetworkList: string[] = []\r\n) => {\r\n const url = new URL(src);\r\n const isInternal = [websiteURL, `www.${websiteURL}`].includes(url.host);\r\n if (isInternal) {\r\n const localPath = url.pathname.replace(apiRegex, \"\");\r\n return readLocalImage(localPath, baseDir, type);\r\n } else {\r\n const allowedCondition = allowedNetworkList.includes(url.host);\r\n if (!allowedCondition) {\r\n return FALLBACKIMAGES[type]();\r\n }\r\n return fetchFromNetwork(src, type);\r\n }\r\n};\r\n","import { API_REGEX } from \"./variables\";\r\nimport type { Options, UserData } from \"./types\";\r\n\r\n/**\r\n * @typedef {(\"avatar\" | \"normal\")} ImageType\r\n * @description Defines the type of image being processed.\r\n */\r\n\r\n/**\r\n * @typedef {(\"jpeg\" | \"jpg\" | \"png\" | \"webp\" | \"gif\" | \"tiff\" | \"avif\" | \"svg\")} ImageFormat\r\n * @description Supported formats for image processing.\r\n */\r\n\r\n/**\r\n * @typedef {Object} Options\r\n * @property {string} baseDir - The base directory for public image files.\r\n * @property {function(string): string} idHandler - A function to handle user IDs.\r\n * @property {function(string, Request): Promise<string>} getUserFolder - Asynchronous function to retrieve user-specific folders.\r\n * @property {string} websiteURL - The base URL of the website for internal link resolution.\r\n * @property {RegExp} apiRegex - Regex to parse API endpoints from URLs.\r\n * @property {string[]} allowedNetworkList - List of allowed network domains for external image fetching.\r\n */\r\n\r\n/**\r\n * @typedef {Object} UserData\r\n * @property {number|string} quality - Quality of the image (1–100).\r\n * @property {ImageFormat} format - Desired format of the image.\r\n * @property {string} [src] - Source path or URL for the image.\r\n * @property {string} [folder] - The folder type (\"public\" or \"private\").\r\n * @property {ImageType} [type] - Type of the image (\"avatar\" or \"normal\").\r\n * @property {string|null} [userId] - Optional user identifier.\r\n * @property {number|string} [width] - Desired image width.\r\n * @property {number|string} [height] - Desired image height.\r\n */\r\n\r\n/**\r\n * Renders the options object with default values and user-provided values.\r\n *\r\n * @param {Partial<Options>} options - The user-provided options.\r\n * @returns {Options} The rendered options object.\r\n */\r\nexport const renderOptions = (options: Partial<Options>): Options => {\r\n const initialOptions: Options = {\r\n baseDir: \"\",\r\n idHandler: (id: string) => id,\r\n getUserFolder: async () => \"\",\r\n websiteURL: \"\",\r\n apiRegex: API_REGEX,\r\n allowedNetworkList: [],\r\n };\r\n return {\r\n ...initialOptions,\r\n ...options,\r\n };\r\n};\r\n\r\n/**\r\n * Renders the user data object with default values and user-provided values.\r\n *\r\n * @param {Partial<UserData>} userData - The user-provided data.\r\n * @returns {UserData} The rendered user data object.\r\n */\r\nexport const renderUserData = (userData: Partial<UserData>): UserData => {\r\n const initialUserData: UserData = {\r\n quality: 80,\r\n format: \"jpeg\",\r\n src: \"/placeholder/noimage.jpg\",\r\n folder: \"public\",\r\n type: \"normal\",\r\n width: undefined,\r\n height: undefined,\r\n userId: undefined,\r\n };\r\n return {\r\n ...initialUserData,\r\n ...userData,\r\n quality: userData.quality\r\n ? Math.min(Math.max(Number(userData.quality) || 80, 1), 100)\r\n : 100,\r\n width: userData.width\r\n ? Math.min(Math.max(Number(userData.width), 50), 2000)\r\n : undefined,\r\n height: userData.height\r\n ? Math.min(Math.max(Number(userData.height), 50), 2000)\r\n : undefined,\r\n };\r\n};\r\n"],"mappings":"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","names":["index_exports","__export","isValidPath","pixel_default","__toCommonJS","getImportMetaUrl","importMetaUrl","import_node_path","import_sharp","import_promises","import_node_path","import_node_url","__dirname","path","importMetaUrl","getAssetPath","filename","NOT_FOUND_IMAGE","NOT_FOUND_AVATAR","FALLBACKIMAGES","API_REGEX","allowedFormats","mimeTypes","import_node_path","fs","import_axios","isValidPath","basePath","specifiedPath","path","resolvedBase","resolvedPath","realBase","realPath","normalizedBase","isInside","relative","fetchFromNetwork","src","type","response","axios","contentType","mimeTypes","FALLBACKIMAGES","readLocalImage","filePath","baseDir","fetchImage","websiteURL","apiRegex","API_REGEX","allowedNetworkList","url","localPath","renderOptions","options","id","API_REGEX","renderUserData","userData","serveImage","req","res","next","options","userData","renderUserData","parsedOptions","renderOptions","imageBuffer","baseDir","parsedUserId","userIdStr","dir","outputFormat","allowedFormats","fetchImage","readLocalImage","image","sharp","resizeOptions","processedImage","processedFileName","path","mimeTypes","error","registerServe","pixel_default"]}
1
+ {"version":3,"sources":["../node_modules/tsup/assets/cjs_shims.js","../src/variables.ts","../src/functions.ts","../src/schema.ts","../src/renders.ts","../src/pixel.ts"],"names":["getImportMetaUrl","importMetaUrl","moduleDir","path","fileURLToPath","getAssetPath","filename","NOT_FOUND_IMAGE","NOT_FOUND_AVATAR","FALLBACKIMAGES","readFile","API_REGEX","allowedFormats","mimeTypes","isValidPath","basePath","specifiedPath","resolvedBase","resolvedPath","realBase","realPath","d","normalizedBase","isInside","relative","fetchFromNetwork","src","type","timeoutMs","maxBytes","response","axios","status","contentType","readLocalImage","filePath","baseDir","resolvedFile","fetchImage","websiteURL","apiRegex","allowedNetworkList","url","localPath","imageFormatEnum","z","imageTypeEnum","userDataSchema","val","lower","value","optionsSchema","data","renderOptions","options","renderUserData","userData","bounds","parsed","clamp","min","max","serveImage","req","res","next","requestedType","parsedOptions","parsedUserId","folderPromise","timeoutPromise","_","reject","dir","outputFormat","imageBuffer","image","sharp","resizeOptions","processedImage","processedFileName","etag","createHash","fallback","fallbackError","registerServe","pixel_default"],"mappings":"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","file":"index.js","sourcesContent":["// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","import type { ImageFormat } from \"./types\";\r\nimport { readFile } from \"node:fs/promises\";\r\nimport path from \"node:path\";\r\nimport { fileURLToPath } from \"node:url\";\r\n\r\n/**\r\n * Get the directory path for the current module.\r\n * Uses import.meta.url for ESM (tsup provides shims for CJS compatibility).\r\n */\r\nconst moduleDir = path.dirname(fileURLToPath(import.meta.url));\r\n\r\nconst getAssetPath = (filename: string): string => {\r\n return path.join(moduleDir, \"assets\", filename);\r\n};\r\n\r\nconst NOT_FOUND_IMAGE = getAssetPath(\"noimage.jpg\");\r\nconst NOT_FOUND_AVATAR = getAssetPath(\"noavatar.png\");\r\n\r\nexport const FALLBACKIMAGES: Record<\r\n \"normal\" | \"avatar\",\r\n () => Promise<Buffer>\r\n> = {\r\n normal: async (): Promise<Buffer> => readFile(NOT_FOUND_IMAGE),\r\n avatar: async (): Promise<Buffer> => readFile(NOT_FOUND_AVATAR),\r\n};\r\n\r\nexport const API_REGEX: RegExp = /^\\/api\\/v1\\//;\r\n\r\nexport const allowedFormats: ImageFormat[] = [\r\n \"jpeg\",\r\n \"jpg\",\r\n \"png\",\r\n \"webp\",\r\n \"gif\",\r\n \"tiff\",\r\n \"avif\",\r\n \"svg\",\r\n];\r\n\r\nexport const mimeTypes: Readonly<Record<string, string>> = {\r\n jpeg: \"image/jpeg\",\r\n jpg: \"image/jpeg\",\r\n png: \"image/png\",\r\n webp: \"image/webp\",\r\n gif: \"image/gif\",\r\n tiff: \"image/tiff\",\r\n avif: \"image/avif\",\r\n svg: \"image/svg+xml\",\r\n};\r\n","import path from \"node:path\";\r\nimport * as fs from \"node:fs/promises\";\r\nimport axios from \"axios\";\r\nimport { FALLBACKIMAGES, mimeTypes } from \"./variables\";\r\nimport type { ImageType } from \"./types\";\r\n\r\n/**\r\n * @typedef {(\"avatar\" | \"normal\")} ImageType\r\n * @description Defines the type of image being processed.\r\n */\r\n\r\n/**\r\n * Checks if a specified path is valid within a base path.\r\n *\r\n * @param {string} basePath - The base directory to resolve paths.\r\n * @param {string} specifiedPath - The path to check.\r\n * @returns {Promise<boolean>} True if the path is valid, false otherwise.\r\n */\r\nexport const isValidPath = async (\r\n basePath: string,\r\n specifiedPath: string\r\n): Promise<boolean> => {\r\n try {\r\n if (!basePath || !specifiedPath) return false;\r\n if (specifiedPath.includes(\"\\0\")) return false;\r\n if (path.isAbsolute(specifiedPath)) return false;\r\n // eslint-disable-next-line no-control-regex\r\n if (!/^[^\\x00-\\x1F]+$/.test(specifiedPath)) return false;\r\n\r\n const resolvedBase = path.resolve(basePath);\r\n const resolvedPath = path.resolve(resolvedBase, specifiedPath);\r\n\r\n const [realBase, realPath] = await Promise.all([\r\n fs.realpath(resolvedBase),\r\n fs.realpath(resolvedPath),\r\n ]);\r\n\r\n const baseStats = await fs.stat(realBase);\r\n if (!baseStats.isDirectory()) return false;\r\n\r\n const normalizedBase = realBase + path.sep;\r\n const normalizedPath = realPath + path.sep;\r\n\r\n const isInside =\r\n normalizedPath.startsWith(normalizedBase) || realPath === realBase;\r\n\r\n const relative = path.relative(realBase, realPath);\r\n return !relative.startsWith(\"..\") && !path.isAbsolute(relative) && isInside;\r\n } catch {\r\n return false;\r\n }\r\n};\r\n\r\n/**\r\n * Fetches an image from a network source.\r\n *\r\n * @param {string} src - The URL of the image.\r\n * @param {ImageType} [type=\"normal\"] - Type of fallback image in case of an error.\r\n * @returns {Promise<Buffer>} A buffer containing the image data or a fallback image.\r\n */\r\nconst fetchFromNetwork = async (\r\n src: string,\r\n type: ImageType = \"normal\",\r\n {\r\n timeoutMs,\r\n maxBytes,\r\n }: {\r\n timeoutMs: number;\r\n maxBytes: number;\r\n }\r\n): Promise<Buffer> => {\r\n try {\r\n const response = await axios.get(src, {\r\n responseType: \"arraybuffer\",\r\n timeout: timeoutMs,\r\n maxContentLength: maxBytes,\r\n maxBodyLength: maxBytes,\r\n validateStatus: (status) => status >= 200 && status < 300,\r\n });\r\n\r\n const contentType = response.headers[\"content-type\"]\r\n ?.toLowerCase()\r\n ?.split(\";\")[0]\r\n ?.trim();\r\n const allowedMimeTypes = Object.values(mimeTypes);\r\n\r\n if (allowedMimeTypes.includes(contentType ?? \"\")) {\r\n return Buffer.from(response.data);\r\n }\r\n return await FALLBACKIMAGES[type]();\r\n } catch {\r\n return await FALLBACKIMAGES[type]();\r\n }\r\n};\r\n\r\n/**\r\n * Reads an image from the local file system.\r\n *\r\n * @param {string} filePath - Path to the image file.\r\n * @param {string} baseDir - Base directory to resolve paths.\r\n * @param {ImageType} [type=\"normal\"] - Type of fallback image if the path is invalid.\r\n * @returns {Promise<Buffer>} A buffer containing the image data.\r\n */\r\nexport const readLocalImage = async (\r\n filePath: string,\r\n baseDir: string,\r\n type: ImageType = \"normal\",\r\n maxBytes?: number\r\n): Promise<Buffer> => {\r\n const isValid = await isValidPath(baseDir, filePath);\r\n if (!isValid) {\r\n return await FALLBACKIMAGES[type]();\r\n }\r\n try {\r\n const resolvedFile = path.resolve(baseDir, filePath);\r\n if (maxBytes) {\r\n const stats = await fs.stat(resolvedFile);\r\n if (stats.size > maxBytes) {\r\n return await FALLBACKIMAGES[type]();\r\n }\r\n }\r\n return await fs.readFile(resolvedFile);\r\n } catch {\r\n return await FALLBACKIMAGES[type]();\r\n }\r\n};\r\n\r\n/**\r\n * Fetches an image from either a local file or a network source.\r\n *\r\n * @param {string} src - The URL or local path of the image.\r\n * @param {string} baseDir - Base directory to resolve local paths.\r\n * @param {string} websiteURL - The URL of the website.\r\n * @param {ImageType} [type=\"normal\"] - Type of fallback image if the path is invalid.\r\n * @param {string[]} [allowedNetworkList=[]] - List of allowed network hosts.\r\n * @returns {Promise<Buffer>} A buffer containing the image data or a fallback image.\r\n */\r\nexport const fetchImage = (\r\n src: string,\r\n baseDir: string,\r\n websiteURL: string | undefined,\r\n type: ImageType = \"normal\",\r\n apiRegex: RegExp,\r\n allowedNetworkList: string[] = [],\r\n {\r\n timeoutMs,\r\n maxBytes,\r\n }: {\r\n timeoutMs: number;\r\n maxBytes: number;\r\n }\r\n): Promise<Buffer> => {\r\n try {\r\n const url = new URL(src);\r\n const isInternal =\r\n websiteURL !== undefined &&\r\n [websiteURL, `www.${websiteURL}`].includes(url.hostname);\r\n\r\n if (isInternal) {\r\n const localPath = url.pathname.replace(apiRegex, \"\");\r\n return readLocalImage(localPath, baseDir, type, maxBytes);\r\n }\r\n\r\n const allowedCondition =\r\n allowedNetworkList.includes(url.hostname) ||\r\n allowedNetworkList.includes(url.host);\r\n if (!allowedCondition) {\r\n return FALLBACKIMAGES[type]();\r\n }\r\n if (![\"http:\", \"https:\"].includes(url.protocol)) {\r\n return FALLBACKIMAGES[type]();\r\n }\r\n return fetchFromNetwork(src, type, { timeoutMs, maxBytes });\r\n } catch {\r\n return readLocalImage(src, baseDir, type, maxBytes);\r\n }\r\n};\r\n","import { z } from \"zod\";\r\nimport { API_REGEX, allowedFormats } from \"./variables\";\r\n\r\nconst imageFormatEnum = z.enum(allowedFormats as [string, ...string[]]);\r\nconst imageTypeEnum = z.enum([\"avatar\", \"normal\"]);\r\n\r\nexport const userDataSchema = z\r\n .object({\r\n src: z\r\n .string()\r\n .min(1, \"src is required\")\r\n .optional()\r\n .default(\"/placeholder/noimage.jpg\"),\r\n format: z\r\n .string()\r\n .optional()\r\n .transform((val) => {\r\n const lower = val?.toLowerCase();\r\n return lower && imageFormatEnum.options.includes(lower as string)\r\n ? (lower as (typeof imageFormatEnum)[\"options\"][number])\r\n : undefined;\r\n })\r\n .optional(),\r\n width: z\r\n .union([z.number(), z.string()])\r\n .optional()\r\n .transform((value) =>\r\n value === undefined || value === null ? undefined : Number(value)\r\n )\r\n .pipe(\r\n z\r\n .number()\r\n .int()\r\n .min(50, \"width too small\")\r\n .max(4000, \"width too large\")\r\n .optional()\r\n ),\r\n height: z\r\n .union([z.number(), z.string()])\r\n .optional()\r\n .transform((value) =>\r\n value === undefined || value === null ? undefined : Number(value)\r\n )\r\n .pipe(\r\n z\r\n .number()\r\n .int()\r\n .min(50, \"height too small\")\r\n .max(4000, \"height too large\")\r\n .optional()\r\n ),\r\n quality: z\r\n .union([z.number(), z.string()])\r\n .optional()\r\n .transform((value) =>\r\n value === undefined || value === null ? undefined : Number(value)\r\n )\r\n .pipe(z.number().int().min(1).max(100).default(80)),\r\n folder: z.enum([\"public\", \"private\"]).default(\"public\"),\r\n type: imageTypeEnum.default(\"normal\"),\r\n userId: z\r\n .union([z.string(), z.number()])\r\n .optional()\r\n .transform((value) =>\r\n value === undefined || value === null ? undefined : String(value).trim()\r\n )\r\n .pipe(\r\n z\r\n .string()\r\n .min(1, \"userId cannot be empty\")\r\n .max(128, \"userId too long\")\r\n .optional()\r\n ),\r\n })\r\n .strict();\r\n\r\nexport const optionsSchema = z\r\n .object({\r\n baseDir: z.string().min(1, \"baseDir is required\"),\r\n idHandler: z\r\n .custom<\r\n (id: string) => string\r\n >((val) => typeof val === \"function\", { message: \"idHandler must be a function\" })\r\n .optional(),\r\n getUserFolder: z\r\n .custom<\r\n (req: unknown, id?: string) => Promise<string> | string\r\n >((val) => typeof val === \"function\", { message: \"getUserFolder must be a function\" })\r\n .optional(),\r\n websiteURL: z\r\n .union([z.url(), z.string().regex(/^(?![-.])([\\w]+[-.]?)*[\\w]+$/)])\r\n .optional(),\r\n apiRegex: z.instanceof(RegExp).default(API_REGEX),\r\n allowedNetworkList: z.array(z.string()).default([]),\r\n cacheControl: z.string().optional(),\r\n etag: z.boolean().default(true),\r\n minWidth: z.number().int().positive().default(50),\r\n maxWidth: z.number().int().positive().default(4000),\r\n minHeight: z.number().int().positive().default(50),\r\n maxHeight: z.number().int().positive().default(4000),\r\n defaultQuality: z.number().int().min(1).max(100).default(80),\r\n requestTimeoutMs: z.number().int().positive().default(5000),\r\n maxDownloadBytes: z.number().int().positive().default(5_000_000),\r\n })\r\n .strict()\r\n .refine((data) => data.minWidth <= data.maxWidth, {\r\n message: \"minWidth must be less than or equal to maxWidth\",\r\n path: [\"minWidth\"],\r\n })\r\n .refine((data) => data.minHeight <= data.maxHeight, {\r\n message: \"minHeight must be less than or equal to maxHeight\",\r\n path: [\"minHeight\"],\r\n });\r\n\r\nexport type ParsedUserData = z.infer<typeof userDataSchema>;\r\nexport type ParsedOptions = z.infer<typeof optionsSchema>;\r\n","import { optionsSchema, userDataSchema } from \"./schema\";\r\nimport type { ParsedOptions, ParsedUserData } from \"./schema\";\r\nimport type { PixelServeOptions, UserData } from \"./types\";\r\n\r\n/**\r\n * @typedef {(\"avatar\" | \"normal\")} ImageType\r\n * @description Defines the type of image being processed.\r\n */\r\n\r\n/**\r\n * @typedef {(\"jpeg\" | \"jpg\" | \"png\" | \"webp\" | \"gif\" | \"tiff\" | \"avif\" | \"svg\")} ImageFormat\r\n * @description Supported formats for image processing.\r\n */\r\n\r\n/**\r\n * @typedef {Object} Options\r\n * @property {string} baseDir - The base directory for public image files.\r\n * @property {function(string): string} idHandler - A function to handle user IDs.\r\n * @property {function(string, Request): Promise<string>} getUserFolder - Asynchronous function to retrieve user-specific folders.\r\n * @property {string} websiteURL - The base URL of the website for internal link resolution.\r\n * @property {RegExp} apiRegex - Regex to parse API endpoints from URLs.\r\n * @property {string[]} allowedNetworkList - List of allowed network domains for external image fetching.\r\n */\r\n\r\n/**\r\n * @typedef {Object} UserData\r\n * @property {number|string} quality - Quality of the image (1–100).\r\n * @property {ImageFormat} format - Desired format of the image.\r\n * @property {string} [src] - Source path or URL for the image.\r\n * @property {string} [folder] - The folder type (\"public\" or \"private\").\r\n * @property {ImageType} [type] - Type of the image (\"avatar\" or \"normal\").\r\n * @property {string|null} [userId] - Optional user identifier.\r\n * @property {number|string} [width] - Desired image width.\r\n * @property {number|string} [height] - Desired image height.\r\n */\r\n\r\n/**\r\n * Renders the options object with default values and user-provided values.\r\n *\r\n * @param {Partial<Options>} options - The user-provided options.\r\n * @returns {Options} The rendered options object.\r\n */\r\nexport const renderOptions = (options: PixelServeOptions): ParsedOptions =>\r\n optionsSchema.parse(options);\r\n\r\n/**\r\n * Renders the user data object with default values and user-provided values.\r\n *\r\n * @param {Partial<UserData>} userData - The user-provided data.\r\n * @returns {UserData} The rendered user data object.\r\n */\r\nexport const renderUserData = (\r\n userData: Partial<UserData>,\r\n bounds: {\r\n minWidth: number;\r\n maxWidth: number;\r\n minHeight: number;\r\n maxHeight: number;\r\n defaultQuality: number;\r\n }\r\n): ParsedUserData => {\r\n const parsed = userDataSchema.parse(userData);\r\n\r\n const clamp = (value: number | undefined, min: number, max: number): number | undefined => {\r\n if (value === undefined) return undefined;\r\n return Math.min(Math.max(value, min), max);\r\n };\r\n\r\n return {\r\n ...parsed,\r\n width: clamp(parsed.width, bounds.minWidth, bounds.maxWidth),\r\n height: clamp(parsed.height, bounds.minHeight, bounds.maxHeight),\r\n quality: parsed.quality ?? bounds.defaultQuality,\r\n format: parsed.format ?? \"jpeg\",\r\n };\r\n};\r\n","import path from \"node:path\";\r\nimport { createHash } from \"node:crypto\";\r\nimport sharp, { FormatEnum, ResizeOptions } from \"sharp\";\r\nimport type { Request, Response, NextFunction } from \"express\";\r\nimport type {\r\n PixelServeOptions,\r\n UserData,\r\n ImageFormat,\r\n ImageType,\r\n} from \"./types\";\r\nimport { allowedFormats, FALLBACKIMAGES, mimeTypes } from \"./variables\";\r\nimport { fetchImage, readLocalImage } from \"./functions\";\r\nimport { renderOptions, renderUserData } from \"./renders\";\r\n\r\n/**\r\n * @typedef {Object} Options\r\n * @property {string} baseDir - The base directory for public image files.\r\n * @property {function(string): string} idHandler - A function to handle user IDs.\r\n * @property {function(string, Request): Promise<string>} getUserFolder - Asynchronous function to retrieve user-specific folders.\r\n * @property {string} websiteURL - The base URL of the website for internal link resolution.\r\n * @property {RegExp} apiRegex - Regex to parse API endpoints from URLs.\r\n * @property {string[]} allowedNetworkList - List of allowed network domains for external image fetching.\r\n */\r\n\r\n/**\r\n * @function serveImage\r\n * @description Processes and serves an image based on user data and options.\r\n * @param {Request} req - The Express request object.\r\n * @param {Response} res - The Express response object.\r\n * @param {NextFunction} next - The Express next function.\r\n * @param {PixelServeOptions} options - The options object for image processing.\r\n * @returns {Promise<void>}\r\n */\r\nconst serveImage = async (\r\n req: Request,\r\n res: Response,\r\n next: NextFunction,\r\n options: PixelServeOptions\r\n): Promise<void> => {\r\n let requestedType: ImageType = \"normal\";\r\n try {\r\n const parsedOptions = renderOptions(options);\r\n const userData = renderUserData(req.query as Partial<UserData>, {\r\n minWidth: parsedOptions.minWidth,\r\n maxWidth: parsedOptions.maxWidth,\r\n minHeight: parsedOptions.minHeight,\r\n maxHeight: parsedOptions.maxHeight,\r\n defaultQuality: parsedOptions.defaultQuality,\r\n });\r\n\r\n requestedType = (userData.type as ImageType) ?? \"normal\";\r\n\r\n let baseDir = parsedOptions.baseDir;\r\n let parsedUserId: string | undefined;\r\n\r\n if (userData.userId) {\r\n parsedUserId = parsedOptions.idHandler\r\n ? parsedOptions.idHandler(userData.userId)\r\n : userData.userId;\r\n }\r\n\r\n if (userData.folder === \"private\" && parsedOptions.getUserFolder) {\r\n const folderPromise = Promise.resolve(\r\n parsedOptions.getUserFolder(req, parsedUserId)\r\n );\r\n const timeoutPromise = new Promise<never>((_, reject) =>\r\n setTimeout(\r\n () => reject(new Error(\"getUserFolder timed out\")),\r\n parsedOptions.requestTimeoutMs\r\n )\r\n );\r\n try {\r\n const dir = await Promise.race([folderPromise, timeoutPromise]);\r\n if (dir) {\r\n baseDir = dir;\r\n }\r\n } catch {\r\n // getUserFolder timed out or failed β€” use default baseDir\r\n }\r\n }\r\n\r\n const outputFormat = allowedFormats.includes(\r\n (userData.format ?? \"\").toLowerCase() as ImageFormat\r\n )\r\n ? (userData.format as ImageFormat)\r\n : \"jpeg\";\r\n\r\n const resolveBuffer = async (): Promise<Buffer> => {\r\n if (!userData.src) {\r\n return FALLBACKIMAGES[userData.type ?? \"normal\"]();\r\n }\r\n if (\r\n userData.src.startsWith(\"http://\") ||\r\n userData.src.startsWith(\"https://\")\r\n ) {\r\n return fetchImage(\r\n userData.src,\r\n baseDir,\r\n parsedOptions.websiteURL,\r\n userData.type as ImageType,\r\n parsedOptions.apiRegex,\r\n parsedOptions.allowedNetworkList,\r\n {\r\n timeoutMs: parsedOptions.requestTimeoutMs,\r\n maxBytes: parsedOptions.maxDownloadBytes,\r\n }\r\n );\r\n }\r\n return readLocalImage(\r\n userData.src,\r\n baseDir,\r\n userData.type as ImageType,\r\n parsedOptions.maxDownloadBytes\r\n );\r\n };\r\n\r\n const imageBuffer = await resolveBuffer();\r\n let image = sharp(imageBuffer, { failOn: \"truncated\" });\r\n\r\n if (userData.width || userData.height) {\r\n const resizeOptions: ResizeOptions = {\r\n width: userData.width ?? undefined,\r\n height: userData.height ?? undefined,\r\n fit: sharp.fit.cover,\r\n withoutEnlargement: true,\r\n };\r\n image = image.resize(resizeOptions);\r\n }\r\n\r\n const processedImage = await image\r\n .rotate()\r\n .toFormat(outputFormat as keyof FormatEnum, {\r\n quality: userData.quality,\r\n })\r\n .toBuffer();\r\n\r\n const rawName = userData.src\r\n ? path.basename(userData.src, path.extname(userData.src))\r\n : \"image\";\r\n // eslint-disable-next-line no-control-regex\r\n const sourceName = rawName.replace(/[\"\\\\\\x00-\\x1F\\x7F]/g, \"_\");\r\n const processedFileName = `${sourceName}.${outputFormat}`;\r\n\r\n const etag = parsedOptions.etag\r\n ? `\"${createHash(\"sha1\").update(processedImage).digest(\"hex\")}\"`\r\n : undefined;\r\n\r\n if (etag && req.headers[\"if-none-match\"] === etag) {\r\n res.status(304).end();\r\n return;\r\n }\r\n\r\n res.type(mimeTypes[outputFormat]);\r\n res.setHeader(\r\n \"Content-Disposition\",\r\n `inline; filename=\"${processedFileName}\"`\r\n );\r\n res.setHeader(\r\n \"Cache-Control\",\r\n parsedOptions.cacheControl ??\r\n \"public, max-age=86400, stale-while-revalidate=604800\"\r\n );\r\n if (etag) {\r\n res.setHeader(\"ETag\", etag);\r\n }\r\n res.setHeader(\"Content-Length\", processedImage.length.toString());\r\n res.send(processedImage);\r\n } catch {\r\n try {\r\n const fallbackType =\r\n requestedType === \"avatar\" ? \"avatar\" : \"normal\";\r\n const fallback = await FALLBACKIMAGES[fallbackType]();\r\n res.type(mimeTypes.jpeg);\r\n res.setHeader(\"Content-Disposition\", `inline; filename=\"fallback.jpeg\"`);\r\n res.setHeader(\"Cache-Control\", \"public, max-age=60\");\r\n res.send(fallback);\r\n } catch (fallbackError) {\r\n next(fallbackError);\r\n }\r\n }\r\n};\r\n\r\n/**\r\n * @function registerServe\r\n * @description A function to register the serveImage function as middleware for Express.\r\n * @param {PixelServeOptions} options - The options object for image processing.\r\n * @returns {function(Request, Response, NextFunction): Promise<void>} The middleware function.\r\n */\r\nconst registerServe = (\r\n options: PixelServeOptions\r\n): ((req: Request, res: Response, next: NextFunction) => Promise<void>) => {\r\n return async (req: Request, res: Response, next: NextFunction) =>\r\n serveImage(req, res, next, options);\r\n};\r\n\r\nexport default registerServe;\r\n"]}
package/dist/index.mjs CHANGED
@@ -1,2 +1,3 @@
1
- import j from"node:path";import A from"sharp";import{readFile as x}from"node:fs/promises";import F from"node:path";import{fileURLToPath as q}from"node:url";var E=F.dirname(q(import.meta.url)),b=t=>F.join(E,"assets",t),D=b("noimage.jpg"),P=b("noavatar.png"),d={normal:async()=>x(D),avatar:async()=>x(P)},y=/^\/api\/v1\//,v=["jpeg","jpg","png","webp","gif","tiff","avif","svg"],h={jpeg:"image/jpeg",jpg:"image/jpeg",png:"image/png",webp:"image/webp",gif:"image/gif",tiff:"image/tiff",avif:"image/avif",svg:"image/svg+xml"};import n from"node:path";import*as l from"node:fs/promises";import M from"axios";var R=async(t,r)=>{try{if(!t||!r||r.includes("\0")||n.isAbsolute(r)||!/^[^\x00-\x1F]+$/.test(r))return!1;let a=n.resolve(t),i=n.resolve(a,r),[e,s]=await Promise.all([l.realpath(a),l.realpath(i)]);if(!(await l.stat(e)).isDirectory())return!1;let g=e+n.sep,c=(s+n.sep).startsWith(g)||s===e,f=n.relative(e,s);return!f.startsWith("..")&&!n.isAbsolute(f)&&c}catch{return!1}},B=async(t,r="normal")=>{try{let a=await M.get(t,{responseType:"arraybuffer",timeout:5e3}),i=a.headers["content-type"]?.toLowerCase();return Object.values(h).includes(i??"")?Buffer.from(a.data):await d[r]()}catch{return await d[r]()}},I=async(t,r,a="normal")=>{if(!await R(r,t))return await d[a]();try{return await l.readFile(n.resolve(r,t))}catch{return await d[a]()}},O=(t,r,a,i="normal",e=y,s=[])=>{let o=new URL(t);if([a,`www.${a}`].includes(o.host)){let m=o.pathname.replace(e,"");return I(m,r,i)}else return s.includes(o.host)?B(t,i):d[i]()};var U=t=>({...{baseDir:"",idHandler:a=>a,getUserFolder:async()=>"",websiteURL:"",apiRegex:y,allowedNetworkList:[]},...t}),T=t=>({...{quality:80,format:"jpeg",src:"/placeholder/noimage.jpg",folder:"public",type:"normal",width:void 0,height:void 0,userId:void 0},...t,quality:t.quality?Math.min(Math.max(Number(t.quality)||80,1),100):100,width:t.width?Math.min(Math.max(Number(t.width),50),2e3):void 0,height:t.height?Math.min(Math.max(Number(t.height),50),2e3):void 0});var S=async(t,r,a,i)=>{try{let e=T(t.query),s=U(i),o,g=s.baseDir,m;if(e.userId){let p=typeof e.userId=="object"?String(Object.values(e.userId)[0]):String(e.userId);s.idHandler?m=s.idHandler(p):m=p}if(e.folder==="private"){let p=await s?.getUserFolder?.(t,m);p&&(g=p)}let c=v.includes(e?.format?.toLowerCase())?e?.format?.toLowerCase():"jpeg";e?.src?.startsWith("http")?o=await O(e?.src??"",g,s?.websiteURL??"",e?.type,s?.apiRegex,s?.allowedNetworkList):o=await I(e?.src??"",g,e?.type);let f=A(o);if(e?.width||e?.height){let p={width:e?.width??void 0,height:e?.height??void 0,fit:A.fit.cover};f=f.resize(p)}let L=await f.toFormat(c,{quality:e?.quality?Number(e?.quality):80}).toBuffer(),N=`${j.basename(e?.src??"",j.extname(e?.src??""))}.${c}`;r.type(h[c]),r.setHeader("Content-Disposition",`inline; filename="${N}"`),r.send(L)}catch(e){a(e)}},_=t=>async(r,a,i)=>S(r,a,i,t),C=_;export{R as isValidPath,C as registerServe};
1
+ import p from'path';import {createHash}from'crypto';import C from'sharp';import*as d from'fs/promises';import {readFile}from'fs/promises';import {fileURLToPath}from'url';import $ from'axios';import {z as z$1}from'zod';var z=p.dirname(fileURLToPath(import.meta.url)),B=e=>p.join(z,"assets",e),_=B("noimage.jpg"),k=B("noavatar.png"),u={normal:async()=>readFile(_),avatar:async()=>readFile(k)},E=/^\/api\/v1\//,b=["jpeg","jpg","png","webp","gif","tiff","avif","svg"],h={jpeg:"image/jpeg",jpg:"image/jpeg",png:"image/png",webp:"image/webp",gif:"image/gif",tiff:"image/tiff",avif:"image/avif",svg:"image/svg+xml"};var O=async(e,r)=>{try{if(!e||!r||r.includes("\0")||p.isAbsolute(r)||!/^[^\x00-\x1F]+$/.test(r))return !1;let n=p.resolve(e),o=p.resolve(n,r),[s,a]=await Promise.all([d.realpath(n),d.realpath(o)]);if(!(await d.stat(s)).isDirectory())return !1;let m=s+p.sep,f=(a+p.sep).startsWith(m)||a===s,y=p.relative(s,a);return !y.startsWith("..")&&!p.isAbsolute(y)&&f}catch{return false}},G=async(e,r="normal",{timeoutMs:n,maxBytes:o})=>{try{let s=await $.get(e,{responseType:"arraybuffer",timeout:n,maxContentLength:o,maxBodyLength:o,validateStatus:m=>m>=200&&m<300}),a=s.headers["content-type"]?.toLowerCase()?.split(";")[0]?.trim();return Object.values(h).includes(a??"")?Buffer.from(s.data):await u[r]()}catch{return await u[r]()}},w=async(e,r,n="normal",o)=>{if(!await O(r,e))return await u[n]();try{let a=p.resolve(r,e);return o&&(await d.stat(a)).size>o?await u[n]():await d.readFile(a)}catch{return await u[n]()}},U=(e,r,n,o="normal",s,a=[],{timeoutMs:i,maxBytes:m})=>{try{let l=new URL(e);if(n!==void 0&&[n,`www.${n}`].includes(l.hostname)){let v=l.pathname.replace(s,"");return w(v,r,o,m)}return a.includes(l.hostname)||a.includes(l.host)?["http:","https:"].includes(l.protocol)?G(e,o,{timeoutMs:i,maxBytes:m}):u[o]():u[o]()}catch{return w(e,r,o,m)}};var Q=z$1.enum(b),V=z$1.enum(["avatar","normal"]),H=z$1.object({src:z$1.string().min(1,"src is required").optional().default("/placeholder/noimage.jpg"),format:z$1.string().optional().transform(e=>{let r=e?.toLowerCase();return r&&Q.options.includes(r)?r:void 0}).optional(),width:z$1.union([z$1.number(),z$1.string()]).optional().transform(e=>e==null?void 0:Number(e)).pipe(z$1.number().int().min(50,"width too small").max(4e3,"width too large").optional()),height:z$1.union([z$1.number(),z$1.string()]).optional().transform(e=>e==null?void 0:Number(e)).pipe(z$1.number().int().min(50,"height too small").max(4e3,"height too large").optional()),quality:z$1.union([z$1.number(),z$1.string()]).optional().transform(e=>e==null?void 0:Number(e)).pipe(z$1.number().int().min(1).max(100).default(80)),folder:z$1.enum(["public","private"]).default("public"),type:V.default("normal"),userId:z$1.union([z$1.string(),z$1.number()]).optional().transform(e=>e==null?void 0:String(e).trim()).pipe(z$1.string().min(1,"userId cannot be empty").max(128,"userId too long").optional())}).strict(),D=z$1.object({baseDir:z$1.string().min(1,"baseDir is required"),idHandler:z$1.custom(e=>typeof e=="function",{message:"idHandler must be a function"}).optional(),getUserFolder:z$1.custom(e=>typeof e=="function",{message:"getUserFolder must be a function"}).optional(),websiteURL:z$1.union([z$1.url(),z$1.string().regex(/^(?![-.])([\w]+[-.]?)*[\w]+$/)]).optional(),apiRegex:z$1.instanceof(RegExp).default(E),allowedNetworkList:z$1.array(z$1.string()).default([]),cacheControl:z$1.string().optional(),etag:z$1.boolean().default(true),minWidth:z$1.number().int().positive().default(50),maxWidth:z$1.number().int().positive().default(4e3),minHeight:z$1.number().int().positive().default(50),maxHeight:z$1.number().int().positive().default(4e3),defaultQuality:z$1.number().int().min(1).max(100).default(80),requestTimeoutMs:z$1.number().int().positive().default(5e3),maxDownloadBytes:z$1.number().int().positive().default(5e6)}).strict().refine(e=>e.minWidth<=e.maxWidth,{message:"minWidth must be less than or equal to maxWidth",path:["minWidth"]}).refine(e=>e.minHeight<=e.maxHeight,{message:"minHeight must be less than or equal to maxHeight",path:["minHeight"]});var W=e=>D.parse(e),q=(e,r)=>{let n=H.parse(e),o=(s,a,i)=>{if(s!==void 0)return Math.min(Math.max(s,a),i)};return {...n,width:o(n.width,r.minWidth,r.maxWidth),height:o(n.height,r.minHeight,r.maxHeight),quality:n.quality??r.defaultQuality,format:n.format??"jpeg"}};var X=async(e,r,n,o)=>{let s="normal";try{let a=W(o),i=q(e.query,{minWidth:a.minWidth,maxWidth:a.maxWidth,minHeight:a.minHeight,maxHeight:a.maxHeight,defaultQuality:a.defaultQuality});s=i.type??"normal";let m=a.baseDir,l;if(i.userId&&(l=a.idHandler?a.idHandler(i.userId):i.userId),i.folder==="private"&&a.getUserFolder){let P=Promise.resolve(a.getUserFolder(e,l)),L=new Promise((T,j)=>setTimeout(()=>j(new Error("getUserFolder timed out")),a.requestTimeoutMs));try{let T=await Promise.race([P,L]);T&&(m=T);}catch{}}let f=b.includes((i.format??"").toLowerCase())?i.format:"jpeg",v=await(async()=>i.src?i.src.startsWith("http://")||i.src.startsWith("https://")?U(i.src,m,a.websiteURL,i.type,a.apiRegex,a.allowedNetworkList,{timeoutMs:a.requestTimeoutMs,maxBytes:a.maxDownloadBytes}):w(i.src,m,i.type,a.maxDownloadBytes):u[i.type??"normal"]())(),F=C(v,{failOn:"truncated"});if(i.width||i.height){let P={width:i.width??void 0,height:i.height??void 0,fit:C.fit.cover,withoutEnlargement:!0};F=F.resize(P);}let I=await F.rotate().toFormat(f,{quality:i.quality}).toBuffer(),N=`${(i.src?p.basename(i.src,p.extname(i.src)):"image").replace(/["\\\x00-\x1F\x7F]/g,"_")}.${f}`,x=a.etag?`"${createHash("sha1").update(I).digest("hex")}"`:void 0;if(x&&e.headers["if-none-match"]===x){r.status(304).end();return}r.type(h[f]),r.setHeader("Content-Disposition",`inline; filename="${N}"`),r.setHeader("Cache-Control",a.cacheControl??"public, max-age=86400, stale-while-revalidate=604800"),x&&r.setHeader("ETag",x),r.setHeader("Content-Length",I.length.toString()),r.send(I);}catch{try{let i=await u[s==="avatar"?"avatar":"normal"]();r.type(h.jpeg),r.setHeader("Content-Disposition",'inline; filename="fallback.jpeg"'),r.setHeader("Cache-Control","public, max-age=60"),r.send(i);}catch(a){n(a);}}},J=e=>async(r,n,o)=>X(r,n,o,e),Y=J;
2
+ export{O as isValidPath,D as optionsSchema,Y as registerServe,H as userDataSchema};//# sourceMappingURL=index.mjs.map
2
3
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/pixel.ts","../src/variables.ts","../src/functions.ts","../src/renders.ts"],"sourcesContent":["import path from \"node:path\";\r\nimport sharp, { FormatEnum, ResizeOptions } from \"sharp\";\r\nimport type { Request, Response, NextFunction } from \"express\";\r\nimport type { Options, UserData, ImageFormat, ImageType } from \"./types\";\r\nimport { allowedFormats, mimeTypes } from \"./variables\";\r\nimport { fetchImage, readLocalImage } from \"./functions\";\r\nimport { renderOptions, renderUserData } from \"./renders\";\r\n\r\n/**\r\n * @typedef {Object} Options\r\n * @property {string} baseDir - The base directory for public image files.\r\n * @property {function(string): string} idHandler - A function to handle user IDs.\r\n * @property {function(string, Request): Promise<string>} getUserFolder - Asynchronous function to retrieve user-specific folders.\r\n * @property {string} websiteURL - The base URL of the website for internal link resolution.\r\n * @property {RegExp} apiRegex - Regex to parse API endpoints from URLs.\r\n * @property {string[]} allowedNetworkList - List of allowed network domains for external image fetching.\r\n */\r\n\r\n/**\r\n * @function serveImage\r\n * @description Processes and serves an image based on user data and options.\r\n * @param {Request} req - The Express request object.\r\n * @param {Response} res - The Express response object.\r\n * @param {NextFunction} next - The Express next function.\r\n * @param {Options} options - The options object for image processing.\r\n * @returns {Promise<void>}\r\n */\r\nconst serveImage = async (\r\n req: Request,\r\n res: Response,\r\n next: NextFunction,\r\n options: Options\r\n) => {\r\n try {\r\n const userData = renderUserData(req.query as UserData);\r\n const parsedOptions = renderOptions(options);\r\n\r\n let imageBuffer;\r\n let baseDir = parsedOptions.baseDir;\r\n let parsedUserId;\r\n\r\n if (userData.userId) {\r\n const userIdStr =\r\n typeof userData.userId === \"object\"\r\n ? String(Object.values(userData.userId)[0])\r\n : String(userData.userId);\r\n if (parsedOptions.idHandler) {\r\n parsedUserId = parsedOptions.idHandler(userIdStr);\r\n } else {\r\n parsedUserId = userIdStr;\r\n }\r\n }\r\n\r\n if (userData.folder === \"private\") {\r\n const dir = await parsedOptions?.getUserFolder?.(req, parsedUserId);\r\n if (dir) {\r\n baseDir = dir;\r\n }\r\n }\r\n\r\n const outputFormat = allowedFormats.includes(\r\n userData?.format?.toLowerCase() as ImageFormat\r\n )\r\n ? userData?.format?.toLowerCase()\r\n : \"jpeg\";\r\n\r\n if (userData?.src?.startsWith(\"http\")) {\r\n imageBuffer = await fetchImage(\r\n userData?.src ?? \"\",\r\n baseDir,\r\n parsedOptions?.websiteURL ?? \"\",\r\n userData?.type as ImageType,\r\n parsedOptions?.apiRegex,\r\n parsedOptions?.allowedNetworkList\r\n );\r\n } else {\r\n imageBuffer = await readLocalImage(\r\n userData?.src ?? \"\",\r\n baseDir,\r\n userData?.type as ImageType\r\n );\r\n }\r\n\r\n let image = sharp(imageBuffer);\r\n\r\n if (userData?.width || userData?.height) {\r\n const resizeOptions = {\r\n width: userData?.width ?? undefined,\r\n height: userData?.height ?? undefined,\r\n fit: sharp.fit.cover,\r\n };\r\n image = image.resize(resizeOptions as ResizeOptions);\r\n }\r\n\r\n const processedImage = await image\r\n .toFormat(outputFormat as keyof FormatEnum, {\r\n quality: userData?.quality ? Number(userData?.quality) : 80,\r\n })\r\n .toBuffer();\r\n\r\n const processedFileName = `${path.basename(\r\n userData?.src ?? \"\",\r\n path.extname(userData?.src ?? \"\")\r\n )}.${outputFormat}`;\r\n\r\n res.type(mimeTypes[outputFormat]);\r\n res.setHeader(\r\n \"Content-Disposition\",\r\n `inline; filename=\"${processedFileName}\"`\r\n );\r\n res.send(processedImage);\r\n } catch (error) {\r\n next(error);\r\n }\r\n};\r\n\r\n/**\r\n * @function registerServe\r\n * @description A function to register the serveImage function as middleware for Express.\r\n * @param {Options} options - The options object for image processing.\r\n * @returns {function(Request, Response, NextFunction): Promise<void>} The middleware function.\r\n */\r\nconst registerServe = (options: Options) => {\r\n return async (req: Request, res: Response, next: NextFunction) =>\r\n serveImage(req, res, next, options);\r\n};\r\n\r\nexport default registerServe;\r\n","import type { ImageFormat } from \"./types\";\r\nimport { readFile } from \"node:fs/promises\";\r\nimport path from \"node:path\";\r\nimport { fileURLToPath } from \"node:url\";\r\n\r\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\r\n\r\nconst getAssetPath = (filename: string) => {\r\n return path.join(__dirname, \"assets\", filename);\r\n};\r\n\r\nconst NOT_FOUND_IMAGE = getAssetPath(\"noimage.jpg\");\r\nconst NOT_FOUND_AVATAR = getAssetPath(\"noavatar.png\");\r\n\r\nexport const FALLBACKIMAGES = {\r\n normal: async () => readFile(NOT_FOUND_IMAGE),\r\n avatar: async () => readFile(NOT_FOUND_AVATAR),\r\n};\r\n\r\nexport const API_REGEX: RegExp = /^\\/api\\/v1\\//;\r\n\r\nexport const allowedFormats: ImageFormat[] = [\r\n \"jpeg\",\r\n \"jpg\",\r\n \"png\",\r\n \"webp\",\r\n \"gif\",\r\n \"tiff\",\r\n \"avif\",\r\n \"svg\",\r\n];\r\n\r\nexport const mimeTypes: Readonly<Record<string, string>> = {\r\n jpeg: \"image/jpeg\",\r\n jpg: \"image/jpeg\",\r\n png: \"image/png\",\r\n webp: \"image/webp\",\r\n gif: \"image/gif\",\r\n tiff: \"image/tiff\",\r\n avif: \"image/avif\",\r\n svg: \"image/svg+xml\",\r\n};\r\n","import path from \"node:path\";\r\nimport * as fs from \"node:fs/promises\";\r\nimport axios from \"axios\";\r\nimport { mimeTypes, API_REGEX, FALLBACKIMAGES } from \"./variables\";\r\nimport type { ImageType } from \"./types\";\r\n\r\n/**\r\n * @typedef {(\"avatar\" | \"normal\")} ImageType\r\n * @description Defines the type of image being processed.\r\n */\r\n\r\n/**\r\n * Checks if a specified path is valid within a base path.\r\n *\r\n * @param {string} basePath - The base directory to resolve paths.\r\n * @param {string} specifiedPath - The path to check.\r\n * @returns {boolean} True if the path is valid, false otherwise.\r\n */\r\nexport const isValidPath = async (\r\n basePath: string,\r\n specifiedPath: string\r\n): Promise<boolean> => {\r\n try {\r\n if (!basePath || !specifiedPath) return false;\r\n if (specifiedPath.includes(\"\\0\")) return false;\r\n if (path.isAbsolute(specifiedPath)) return false;\r\n if (!/^[^\\x00-\\x1F]+$/.test(specifiedPath)) return false;\r\n\r\n const resolvedBase = path.resolve(basePath);\r\n const resolvedPath = path.resolve(resolvedBase, specifiedPath);\r\n\r\n const [realBase, realPath] = await Promise.all([\r\n fs.realpath(resolvedBase),\r\n fs.realpath(resolvedPath),\r\n ]);\r\n\r\n const baseStats = await fs.stat(realBase);\r\n if (!baseStats.isDirectory()) return false;\r\n\r\n const normalizedBase = realBase + path.sep;\r\n const normalizedPath = realPath + path.sep;\r\n\r\n const isInside =\r\n normalizedPath.startsWith(normalizedBase) || realPath === realBase;\r\n\r\n const relative = path.relative(realBase, realPath);\r\n return !relative.startsWith(\"..\") && !path.isAbsolute(relative) && isInside;\r\n } catch {\r\n return false;\r\n }\r\n};\r\n\r\n/**\r\n * Fetches an image from a network source.\r\n *\r\n * @param {string} src - The URL of the image.\r\n * @param {ImageType} [type=\"normal\"] - Type of fallback image in case of an error.\r\n * @returns {Promise<Buffer>} A buffer containing the image data or a fallback image.\r\n */\r\nconst fetchFromNetwork = async (\r\n src: string,\r\n type: ImageType = \"normal\"\r\n): Promise<Buffer> => {\r\n try {\r\n const response = await axios.get(src, {\r\n responseType: \"arraybuffer\",\r\n timeout: 5000,\r\n });\r\n\r\n const contentType = response.headers[\"content-type\"]?.toLowerCase();\r\n const allowedMimeTypes = Object.values(mimeTypes);\r\n\r\n if (allowedMimeTypes.includes(contentType ?? \"\")) {\r\n return Buffer.from(response.data);\r\n }\r\n return await FALLBACKIMAGES[type]();\r\n } catch (error) {\r\n return await FALLBACKIMAGES[type]();\r\n }\r\n};\r\n\r\n/**\r\n * Reads an image from the local file system.\r\n *\r\n * @param {string} filePath - Path to the image file.\r\n * @param {string} baseDir - Base directory to resolve paths.\r\n * @param {ImageType} [type=\"normal\"] - Type of fallback image if the path is invalid.\r\n * @returns {Promise<Buffer>} A buffer containing the image data.\r\n */\r\nexport const readLocalImage = async (\r\n filePath: string,\r\n baseDir: string,\r\n type: ImageType = \"normal\"\r\n) => {\r\n const isValid = await isValidPath(baseDir, filePath);\r\n if (!isValid) {\r\n return await FALLBACKIMAGES[type]();\r\n }\r\n try {\r\n return await fs.readFile(path.resolve(baseDir, filePath));\r\n } catch (error) {\r\n return await FALLBACKIMAGES[type]();\r\n }\r\n};\r\n\r\n/**\r\n * Fetches an image from either a local file or a network source.\r\n *\r\n * @param {string} src - The URL or local path of the image.\r\n * @param {string} baseDir - Base directory to resolve local paths.\r\n * @param {string} websiteURL - The URL of the website.\r\n * @param {ImageType} [type=\"normal\"] - Type of fallback image if the path is invalid.\r\n * @param {RegExp} [apiRegex=API_REGEX] - Regular expression to match API routes.\r\n * @param {string[]} [allowedNetworkList=[]] - List of allowed network hosts.\r\n * @returns {Promise<Buffer>} A buffer containing the image data or a fallback image.\r\n */\r\nexport const fetchImage = (\r\n src: string,\r\n baseDir: string,\r\n websiteURL: string,\r\n type: ImageType = \"normal\",\r\n apiRegex: RegExp = API_REGEX,\r\n allowedNetworkList: string[] = []\r\n) => {\r\n const url = new URL(src);\r\n const isInternal = [websiteURL, `www.${websiteURL}`].includes(url.host);\r\n if (isInternal) {\r\n const localPath = url.pathname.replace(apiRegex, \"\");\r\n return readLocalImage(localPath, baseDir, type);\r\n } else {\r\n const allowedCondition = allowedNetworkList.includes(url.host);\r\n if (!allowedCondition) {\r\n return FALLBACKIMAGES[type]();\r\n }\r\n return fetchFromNetwork(src, type);\r\n }\r\n};\r\n","import { API_REGEX } from \"./variables\";\r\nimport type { Options, UserData } from \"./types\";\r\n\r\n/**\r\n * @typedef {(\"avatar\" | \"normal\")} ImageType\r\n * @description Defines the type of image being processed.\r\n */\r\n\r\n/**\r\n * @typedef {(\"jpeg\" | \"jpg\" | \"png\" | \"webp\" | \"gif\" | \"tiff\" | \"avif\" | \"svg\")} ImageFormat\r\n * @description Supported formats for image processing.\r\n */\r\n\r\n/**\r\n * @typedef {Object} Options\r\n * @property {string} baseDir - The base directory for public image files.\r\n * @property {function(string): string} idHandler - A function to handle user IDs.\r\n * @property {function(string, Request): Promise<string>} getUserFolder - Asynchronous function to retrieve user-specific folders.\r\n * @property {string} websiteURL - The base URL of the website for internal link resolution.\r\n * @property {RegExp} apiRegex - Regex to parse API endpoints from URLs.\r\n * @property {string[]} allowedNetworkList - List of allowed network domains for external image fetching.\r\n */\r\n\r\n/**\r\n * @typedef {Object} UserData\r\n * @property {number|string} quality - Quality of the image (1–100).\r\n * @property {ImageFormat} format - Desired format of the image.\r\n * @property {string} [src] - Source path or URL for the image.\r\n * @property {string} [folder] - The folder type (\"public\" or \"private\").\r\n * @property {ImageType} [type] - Type of the image (\"avatar\" or \"normal\").\r\n * @property {string|null} [userId] - Optional user identifier.\r\n * @property {number|string} [width] - Desired image width.\r\n * @property {number|string} [height] - Desired image height.\r\n */\r\n\r\n/**\r\n * Renders the options object with default values and user-provided values.\r\n *\r\n * @param {Partial<Options>} options - The user-provided options.\r\n * @returns {Options} The rendered options object.\r\n */\r\nexport const renderOptions = (options: Partial<Options>): Options => {\r\n const initialOptions: Options = {\r\n baseDir: \"\",\r\n idHandler: (id: string) => id,\r\n getUserFolder: async () => \"\",\r\n websiteURL: \"\",\r\n apiRegex: API_REGEX,\r\n allowedNetworkList: [],\r\n };\r\n return {\r\n ...initialOptions,\r\n ...options,\r\n };\r\n};\r\n\r\n/**\r\n * Renders the user data object with default values and user-provided values.\r\n *\r\n * @param {Partial<UserData>} userData - The user-provided data.\r\n * @returns {UserData} The rendered user data object.\r\n */\r\nexport const renderUserData = (userData: Partial<UserData>): UserData => {\r\n const initialUserData: UserData = {\r\n quality: 80,\r\n format: \"jpeg\",\r\n src: \"/placeholder/noimage.jpg\",\r\n folder: \"public\",\r\n type: \"normal\",\r\n width: undefined,\r\n height: undefined,\r\n userId: undefined,\r\n };\r\n return {\r\n ...initialUserData,\r\n ...userData,\r\n quality: userData.quality\r\n ? Math.min(Math.max(Number(userData.quality) || 80, 1), 100)\r\n : 100,\r\n width: userData.width\r\n ? Math.min(Math.max(Number(userData.width), 50), 2000)\r\n : undefined,\r\n height: userData.height\r\n ? Math.min(Math.max(Number(userData.height), 50), 2000)\r\n : undefined,\r\n };\r\n};\r\n"],"mappings":"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","names":["path","sharp","readFile","path","fileURLToPath","__dirname","getAssetPath","filename","NOT_FOUND_IMAGE","NOT_FOUND_AVATAR","FALLBACKIMAGES","API_REGEX","allowedFormats","mimeTypes","path","fs","axios","isValidPath","basePath","specifiedPath","path","resolvedBase","resolvedPath","realBase","realPath","normalizedBase","isInside","relative","fetchFromNetwork","src","type","response","axios","contentType","mimeTypes","FALLBACKIMAGES","readLocalImage","filePath","baseDir","fetchImage","websiteURL","apiRegex","API_REGEX","allowedNetworkList","url","localPath","renderOptions","options","id","API_REGEX","renderUserData","userData","serveImage","req","res","next","options","userData","renderUserData","parsedOptions","renderOptions","imageBuffer","baseDir","parsedUserId","userIdStr","dir","outputFormat","allowedFormats","fetchImage","readLocalImage","image","sharp","resizeOptions","processedImage","processedFileName","path","mimeTypes","error","registerServe","pixel_default"]}
1
+ {"version":3,"sources":["../src/variables.ts","../src/functions.ts","../src/schema.ts","../src/renders.ts","../src/pixel.ts"],"names":["moduleDir","path","fileURLToPath","getAssetPath","filename","NOT_FOUND_IMAGE","NOT_FOUND_AVATAR","FALLBACKIMAGES","readFile","API_REGEX","allowedFormats","mimeTypes","isValidPath","basePath","specifiedPath","resolvedBase","resolvedPath","realBase","realPath","normalizedBase","isInside","relative","fetchFromNetwork","src","type","timeoutMs","maxBytes","response","axios","status","contentType","readLocalImage","filePath","baseDir","resolvedFile","fetchImage","websiteURL","apiRegex","allowedNetworkList","url","localPath","imageFormatEnum","z","imageTypeEnum","userDataSchema","val","lower","value","optionsSchema","data","renderOptions","options","renderUserData","userData","bounds","parsed","clamp","min","max","serveImage","req","res","next","requestedType","parsedOptions","parsedUserId","folderPromise","timeoutPromise","_","reject","dir","outputFormat","imageBuffer","image","sharp","resizeOptions","processedImage","processedFileName","etag","createHash","fallback","fallbackError","registerServe","pixel_default"],"mappings":"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","file":"index.mjs","sourcesContent":["import type { ImageFormat } from \"./types\";\r\nimport { readFile } from \"node:fs/promises\";\r\nimport path from \"node:path\";\r\nimport { fileURLToPath } from \"node:url\";\r\n\r\n/**\r\n * Get the directory path for the current module.\r\n * Uses import.meta.url for ESM (tsup provides shims for CJS compatibility).\r\n */\r\nconst moduleDir = path.dirname(fileURLToPath(import.meta.url));\r\n\r\nconst getAssetPath = (filename: string): string => {\r\n return path.join(moduleDir, \"assets\", filename);\r\n};\r\n\r\nconst NOT_FOUND_IMAGE = getAssetPath(\"noimage.jpg\");\r\nconst NOT_FOUND_AVATAR = getAssetPath(\"noavatar.png\");\r\n\r\nexport const FALLBACKIMAGES: Record<\r\n \"normal\" | \"avatar\",\r\n () => Promise<Buffer>\r\n> = {\r\n normal: async (): Promise<Buffer> => readFile(NOT_FOUND_IMAGE),\r\n avatar: async (): Promise<Buffer> => readFile(NOT_FOUND_AVATAR),\r\n};\r\n\r\nexport const API_REGEX: RegExp = /^\\/api\\/v1\\//;\r\n\r\nexport const allowedFormats: ImageFormat[] = [\r\n \"jpeg\",\r\n \"jpg\",\r\n \"png\",\r\n \"webp\",\r\n \"gif\",\r\n \"tiff\",\r\n \"avif\",\r\n \"svg\",\r\n];\r\n\r\nexport const mimeTypes: Readonly<Record<string, string>> = {\r\n jpeg: \"image/jpeg\",\r\n jpg: \"image/jpeg\",\r\n png: \"image/png\",\r\n webp: \"image/webp\",\r\n gif: \"image/gif\",\r\n tiff: \"image/tiff\",\r\n avif: \"image/avif\",\r\n svg: \"image/svg+xml\",\r\n};\r\n","import path from \"node:path\";\r\nimport * as fs from \"node:fs/promises\";\r\nimport axios from \"axios\";\r\nimport { FALLBACKIMAGES, mimeTypes } from \"./variables\";\r\nimport type { ImageType } from \"./types\";\r\n\r\n/**\r\n * @typedef {(\"avatar\" | \"normal\")} ImageType\r\n * @description Defines the type of image being processed.\r\n */\r\n\r\n/**\r\n * Checks if a specified path is valid within a base path.\r\n *\r\n * @param {string} basePath - The base directory to resolve paths.\r\n * @param {string} specifiedPath - The path to check.\r\n * @returns {Promise<boolean>} True if the path is valid, false otherwise.\r\n */\r\nexport const isValidPath = async (\r\n basePath: string,\r\n specifiedPath: string\r\n): Promise<boolean> => {\r\n try {\r\n if (!basePath || !specifiedPath) return false;\r\n if (specifiedPath.includes(\"\\0\")) return false;\r\n if (path.isAbsolute(specifiedPath)) return false;\r\n // eslint-disable-next-line no-control-regex\r\n if (!/^[^\\x00-\\x1F]+$/.test(specifiedPath)) return false;\r\n\r\n const resolvedBase = path.resolve(basePath);\r\n const resolvedPath = path.resolve(resolvedBase, specifiedPath);\r\n\r\n const [realBase, realPath] = await Promise.all([\r\n fs.realpath(resolvedBase),\r\n fs.realpath(resolvedPath),\r\n ]);\r\n\r\n const baseStats = await fs.stat(realBase);\r\n if (!baseStats.isDirectory()) return false;\r\n\r\n const normalizedBase = realBase + path.sep;\r\n const normalizedPath = realPath + path.sep;\r\n\r\n const isInside =\r\n normalizedPath.startsWith(normalizedBase) || realPath === realBase;\r\n\r\n const relative = path.relative(realBase, realPath);\r\n return !relative.startsWith(\"..\") && !path.isAbsolute(relative) && isInside;\r\n } catch {\r\n return false;\r\n }\r\n};\r\n\r\n/**\r\n * Fetches an image from a network source.\r\n *\r\n * @param {string} src - The URL of the image.\r\n * @param {ImageType} [type=\"normal\"] - Type of fallback image in case of an error.\r\n * @returns {Promise<Buffer>} A buffer containing the image data or a fallback image.\r\n */\r\nconst fetchFromNetwork = async (\r\n src: string,\r\n type: ImageType = \"normal\",\r\n {\r\n timeoutMs,\r\n maxBytes,\r\n }: {\r\n timeoutMs: number;\r\n maxBytes: number;\r\n }\r\n): Promise<Buffer> => {\r\n try {\r\n const response = await axios.get(src, {\r\n responseType: \"arraybuffer\",\r\n timeout: timeoutMs,\r\n maxContentLength: maxBytes,\r\n maxBodyLength: maxBytes,\r\n validateStatus: (status) => status >= 200 && status < 300,\r\n });\r\n\r\n const contentType = response.headers[\"content-type\"]\r\n ?.toLowerCase()\r\n ?.split(\";\")[0]\r\n ?.trim();\r\n const allowedMimeTypes = Object.values(mimeTypes);\r\n\r\n if (allowedMimeTypes.includes(contentType ?? \"\")) {\r\n return Buffer.from(response.data);\r\n }\r\n return await FALLBACKIMAGES[type]();\r\n } catch {\r\n return await FALLBACKIMAGES[type]();\r\n }\r\n};\r\n\r\n/**\r\n * Reads an image from the local file system.\r\n *\r\n * @param {string} filePath - Path to the image file.\r\n * @param {string} baseDir - Base directory to resolve paths.\r\n * @param {ImageType} [type=\"normal\"] - Type of fallback image if the path is invalid.\r\n * @returns {Promise<Buffer>} A buffer containing the image data.\r\n */\r\nexport const readLocalImage = async (\r\n filePath: string,\r\n baseDir: string,\r\n type: ImageType = \"normal\",\r\n maxBytes?: number\r\n): Promise<Buffer> => {\r\n const isValid = await isValidPath(baseDir, filePath);\r\n if (!isValid) {\r\n return await FALLBACKIMAGES[type]();\r\n }\r\n try {\r\n const resolvedFile = path.resolve(baseDir, filePath);\r\n if (maxBytes) {\r\n const stats = await fs.stat(resolvedFile);\r\n if (stats.size > maxBytes) {\r\n return await FALLBACKIMAGES[type]();\r\n }\r\n }\r\n return await fs.readFile(resolvedFile);\r\n } catch {\r\n return await FALLBACKIMAGES[type]();\r\n }\r\n};\r\n\r\n/**\r\n * Fetches an image from either a local file or a network source.\r\n *\r\n * @param {string} src - The URL or local path of the image.\r\n * @param {string} baseDir - Base directory to resolve local paths.\r\n * @param {string} websiteURL - The URL of the website.\r\n * @param {ImageType} [type=\"normal\"] - Type of fallback image if the path is invalid.\r\n * @param {string[]} [allowedNetworkList=[]] - List of allowed network hosts.\r\n * @returns {Promise<Buffer>} A buffer containing the image data or a fallback image.\r\n */\r\nexport const fetchImage = (\r\n src: string,\r\n baseDir: string,\r\n websiteURL: string | undefined,\r\n type: ImageType = \"normal\",\r\n apiRegex: RegExp,\r\n allowedNetworkList: string[] = [],\r\n {\r\n timeoutMs,\r\n maxBytes,\r\n }: {\r\n timeoutMs: number;\r\n maxBytes: number;\r\n }\r\n): Promise<Buffer> => {\r\n try {\r\n const url = new URL(src);\r\n const isInternal =\r\n websiteURL !== undefined &&\r\n [websiteURL, `www.${websiteURL}`].includes(url.hostname);\r\n\r\n if (isInternal) {\r\n const localPath = url.pathname.replace(apiRegex, \"\");\r\n return readLocalImage(localPath, baseDir, type, maxBytes);\r\n }\r\n\r\n const allowedCondition =\r\n allowedNetworkList.includes(url.hostname) ||\r\n allowedNetworkList.includes(url.host);\r\n if (!allowedCondition) {\r\n return FALLBACKIMAGES[type]();\r\n }\r\n if (![\"http:\", \"https:\"].includes(url.protocol)) {\r\n return FALLBACKIMAGES[type]();\r\n }\r\n return fetchFromNetwork(src, type, { timeoutMs, maxBytes });\r\n } catch {\r\n return readLocalImage(src, baseDir, type, maxBytes);\r\n }\r\n};\r\n","import { z } from \"zod\";\r\nimport { API_REGEX, allowedFormats } from \"./variables\";\r\n\r\nconst imageFormatEnum = z.enum(allowedFormats as [string, ...string[]]);\r\nconst imageTypeEnum = z.enum([\"avatar\", \"normal\"]);\r\n\r\nexport const userDataSchema = z\r\n .object({\r\n src: z\r\n .string()\r\n .min(1, \"src is required\")\r\n .optional()\r\n .default(\"/placeholder/noimage.jpg\"),\r\n format: z\r\n .string()\r\n .optional()\r\n .transform((val) => {\r\n const lower = val?.toLowerCase();\r\n return lower && imageFormatEnum.options.includes(lower as string)\r\n ? (lower as (typeof imageFormatEnum)[\"options\"][number])\r\n : undefined;\r\n })\r\n .optional(),\r\n width: z\r\n .union([z.number(), z.string()])\r\n .optional()\r\n .transform((value) =>\r\n value === undefined || value === null ? undefined : Number(value)\r\n )\r\n .pipe(\r\n z\r\n .number()\r\n .int()\r\n .min(50, \"width too small\")\r\n .max(4000, \"width too large\")\r\n .optional()\r\n ),\r\n height: z\r\n .union([z.number(), z.string()])\r\n .optional()\r\n .transform((value) =>\r\n value === undefined || value === null ? undefined : Number(value)\r\n )\r\n .pipe(\r\n z\r\n .number()\r\n .int()\r\n .min(50, \"height too small\")\r\n .max(4000, \"height too large\")\r\n .optional()\r\n ),\r\n quality: z\r\n .union([z.number(), z.string()])\r\n .optional()\r\n .transform((value) =>\r\n value === undefined || value === null ? undefined : Number(value)\r\n )\r\n .pipe(z.number().int().min(1).max(100).default(80)),\r\n folder: z.enum([\"public\", \"private\"]).default(\"public\"),\r\n type: imageTypeEnum.default(\"normal\"),\r\n userId: z\r\n .union([z.string(), z.number()])\r\n .optional()\r\n .transform((value) =>\r\n value === undefined || value === null ? undefined : String(value).trim()\r\n )\r\n .pipe(\r\n z\r\n .string()\r\n .min(1, \"userId cannot be empty\")\r\n .max(128, \"userId too long\")\r\n .optional()\r\n ),\r\n })\r\n .strict();\r\n\r\nexport const optionsSchema = z\r\n .object({\r\n baseDir: z.string().min(1, \"baseDir is required\"),\r\n idHandler: z\r\n .custom<\r\n (id: string) => string\r\n >((val) => typeof val === \"function\", { message: \"idHandler must be a function\" })\r\n .optional(),\r\n getUserFolder: z\r\n .custom<\r\n (req: unknown, id?: string) => Promise<string> | string\r\n >((val) => typeof val === \"function\", { message: \"getUserFolder must be a function\" })\r\n .optional(),\r\n websiteURL: z\r\n .union([z.url(), z.string().regex(/^(?![-.])([\\w]+[-.]?)*[\\w]+$/)])\r\n .optional(),\r\n apiRegex: z.instanceof(RegExp).default(API_REGEX),\r\n allowedNetworkList: z.array(z.string()).default([]),\r\n cacheControl: z.string().optional(),\r\n etag: z.boolean().default(true),\r\n minWidth: z.number().int().positive().default(50),\r\n maxWidth: z.number().int().positive().default(4000),\r\n minHeight: z.number().int().positive().default(50),\r\n maxHeight: z.number().int().positive().default(4000),\r\n defaultQuality: z.number().int().min(1).max(100).default(80),\r\n requestTimeoutMs: z.number().int().positive().default(5000),\r\n maxDownloadBytes: z.number().int().positive().default(5_000_000),\r\n })\r\n .strict()\r\n .refine((data) => data.minWidth <= data.maxWidth, {\r\n message: \"minWidth must be less than or equal to maxWidth\",\r\n path: [\"minWidth\"],\r\n })\r\n .refine((data) => data.minHeight <= data.maxHeight, {\r\n message: \"minHeight must be less than or equal to maxHeight\",\r\n path: [\"minHeight\"],\r\n });\r\n\r\nexport type ParsedUserData = z.infer<typeof userDataSchema>;\r\nexport type ParsedOptions = z.infer<typeof optionsSchema>;\r\n","import { optionsSchema, userDataSchema } from \"./schema\";\r\nimport type { ParsedOptions, ParsedUserData } from \"./schema\";\r\nimport type { PixelServeOptions, UserData } from \"./types\";\r\n\r\n/**\r\n * @typedef {(\"avatar\" | \"normal\")} ImageType\r\n * @description Defines the type of image being processed.\r\n */\r\n\r\n/**\r\n * @typedef {(\"jpeg\" | \"jpg\" | \"png\" | \"webp\" | \"gif\" | \"tiff\" | \"avif\" | \"svg\")} ImageFormat\r\n * @description Supported formats for image processing.\r\n */\r\n\r\n/**\r\n * @typedef {Object} Options\r\n * @property {string} baseDir - The base directory for public image files.\r\n * @property {function(string): string} idHandler - A function to handle user IDs.\r\n * @property {function(string, Request): Promise<string>} getUserFolder - Asynchronous function to retrieve user-specific folders.\r\n * @property {string} websiteURL - The base URL of the website for internal link resolution.\r\n * @property {RegExp} apiRegex - Regex to parse API endpoints from URLs.\r\n * @property {string[]} allowedNetworkList - List of allowed network domains for external image fetching.\r\n */\r\n\r\n/**\r\n * @typedef {Object} UserData\r\n * @property {number|string} quality - Quality of the image (1–100).\r\n * @property {ImageFormat} format - Desired format of the image.\r\n * @property {string} [src] - Source path or URL for the image.\r\n * @property {string} [folder] - The folder type (\"public\" or \"private\").\r\n * @property {ImageType} [type] - Type of the image (\"avatar\" or \"normal\").\r\n * @property {string|null} [userId] - Optional user identifier.\r\n * @property {number|string} [width] - Desired image width.\r\n * @property {number|string} [height] - Desired image height.\r\n */\r\n\r\n/**\r\n * Renders the options object with default values and user-provided values.\r\n *\r\n * @param {Partial<Options>} options - The user-provided options.\r\n * @returns {Options} The rendered options object.\r\n */\r\nexport const renderOptions = (options: PixelServeOptions): ParsedOptions =>\r\n optionsSchema.parse(options);\r\n\r\n/**\r\n * Renders the user data object with default values and user-provided values.\r\n *\r\n * @param {Partial<UserData>} userData - The user-provided data.\r\n * @returns {UserData} The rendered user data object.\r\n */\r\nexport const renderUserData = (\r\n userData: Partial<UserData>,\r\n bounds: {\r\n minWidth: number;\r\n maxWidth: number;\r\n minHeight: number;\r\n maxHeight: number;\r\n defaultQuality: number;\r\n }\r\n): ParsedUserData => {\r\n const parsed = userDataSchema.parse(userData);\r\n\r\n const clamp = (value: number | undefined, min: number, max: number): number | undefined => {\r\n if (value === undefined) return undefined;\r\n return Math.min(Math.max(value, min), max);\r\n };\r\n\r\n return {\r\n ...parsed,\r\n width: clamp(parsed.width, bounds.minWidth, bounds.maxWidth),\r\n height: clamp(parsed.height, bounds.minHeight, bounds.maxHeight),\r\n quality: parsed.quality ?? bounds.defaultQuality,\r\n format: parsed.format ?? \"jpeg\",\r\n };\r\n};\r\n","import path from \"node:path\";\r\nimport { createHash } from \"node:crypto\";\r\nimport sharp, { FormatEnum, ResizeOptions } from \"sharp\";\r\nimport type { Request, Response, NextFunction } from \"express\";\r\nimport type {\r\n PixelServeOptions,\r\n UserData,\r\n ImageFormat,\r\n ImageType,\r\n} from \"./types\";\r\nimport { allowedFormats, FALLBACKIMAGES, mimeTypes } from \"./variables\";\r\nimport { fetchImage, readLocalImage } from \"./functions\";\r\nimport { renderOptions, renderUserData } from \"./renders\";\r\n\r\n/**\r\n * @typedef {Object} Options\r\n * @property {string} baseDir - The base directory for public image files.\r\n * @property {function(string): string} idHandler - A function to handle user IDs.\r\n * @property {function(string, Request): Promise<string>} getUserFolder - Asynchronous function to retrieve user-specific folders.\r\n * @property {string} websiteURL - The base URL of the website for internal link resolution.\r\n * @property {RegExp} apiRegex - Regex to parse API endpoints from URLs.\r\n * @property {string[]} allowedNetworkList - List of allowed network domains for external image fetching.\r\n */\r\n\r\n/**\r\n * @function serveImage\r\n * @description Processes and serves an image based on user data and options.\r\n * @param {Request} req - The Express request object.\r\n * @param {Response} res - The Express response object.\r\n * @param {NextFunction} next - The Express next function.\r\n * @param {PixelServeOptions} options - The options object for image processing.\r\n * @returns {Promise<void>}\r\n */\r\nconst serveImage = async (\r\n req: Request,\r\n res: Response,\r\n next: NextFunction,\r\n options: PixelServeOptions\r\n): Promise<void> => {\r\n let requestedType: ImageType = \"normal\";\r\n try {\r\n const parsedOptions = renderOptions(options);\r\n const userData = renderUserData(req.query as Partial<UserData>, {\r\n minWidth: parsedOptions.minWidth,\r\n maxWidth: parsedOptions.maxWidth,\r\n minHeight: parsedOptions.minHeight,\r\n maxHeight: parsedOptions.maxHeight,\r\n defaultQuality: parsedOptions.defaultQuality,\r\n });\r\n\r\n requestedType = (userData.type as ImageType) ?? \"normal\";\r\n\r\n let baseDir = parsedOptions.baseDir;\r\n let parsedUserId: string | undefined;\r\n\r\n if (userData.userId) {\r\n parsedUserId = parsedOptions.idHandler\r\n ? parsedOptions.idHandler(userData.userId)\r\n : userData.userId;\r\n }\r\n\r\n if (userData.folder === \"private\" && parsedOptions.getUserFolder) {\r\n const folderPromise = Promise.resolve(\r\n parsedOptions.getUserFolder(req, parsedUserId)\r\n );\r\n const timeoutPromise = new Promise<never>((_, reject) =>\r\n setTimeout(\r\n () => reject(new Error(\"getUserFolder timed out\")),\r\n parsedOptions.requestTimeoutMs\r\n )\r\n );\r\n try {\r\n const dir = await Promise.race([folderPromise, timeoutPromise]);\r\n if (dir) {\r\n baseDir = dir;\r\n }\r\n } catch {\r\n // getUserFolder timed out or failed β€” use default baseDir\r\n }\r\n }\r\n\r\n const outputFormat = allowedFormats.includes(\r\n (userData.format ?? \"\").toLowerCase() as ImageFormat\r\n )\r\n ? (userData.format as ImageFormat)\r\n : \"jpeg\";\r\n\r\n const resolveBuffer = async (): Promise<Buffer> => {\r\n if (!userData.src) {\r\n return FALLBACKIMAGES[userData.type ?? \"normal\"]();\r\n }\r\n if (\r\n userData.src.startsWith(\"http://\") ||\r\n userData.src.startsWith(\"https://\")\r\n ) {\r\n return fetchImage(\r\n userData.src,\r\n baseDir,\r\n parsedOptions.websiteURL,\r\n userData.type as ImageType,\r\n parsedOptions.apiRegex,\r\n parsedOptions.allowedNetworkList,\r\n {\r\n timeoutMs: parsedOptions.requestTimeoutMs,\r\n maxBytes: parsedOptions.maxDownloadBytes,\r\n }\r\n );\r\n }\r\n return readLocalImage(\r\n userData.src,\r\n baseDir,\r\n userData.type as ImageType,\r\n parsedOptions.maxDownloadBytes\r\n );\r\n };\r\n\r\n const imageBuffer = await resolveBuffer();\r\n let image = sharp(imageBuffer, { failOn: \"truncated\" });\r\n\r\n if (userData.width || userData.height) {\r\n const resizeOptions: ResizeOptions = {\r\n width: userData.width ?? undefined,\r\n height: userData.height ?? undefined,\r\n fit: sharp.fit.cover,\r\n withoutEnlargement: true,\r\n };\r\n image = image.resize(resizeOptions);\r\n }\r\n\r\n const processedImage = await image\r\n .rotate()\r\n .toFormat(outputFormat as keyof FormatEnum, {\r\n quality: userData.quality,\r\n })\r\n .toBuffer();\r\n\r\n const rawName = userData.src\r\n ? path.basename(userData.src, path.extname(userData.src))\r\n : \"image\";\r\n // eslint-disable-next-line no-control-regex\r\n const sourceName = rawName.replace(/[\"\\\\\\x00-\\x1F\\x7F]/g, \"_\");\r\n const processedFileName = `${sourceName}.${outputFormat}`;\r\n\r\n const etag = parsedOptions.etag\r\n ? `\"${createHash(\"sha1\").update(processedImage).digest(\"hex\")}\"`\r\n : undefined;\r\n\r\n if (etag && req.headers[\"if-none-match\"] === etag) {\r\n res.status(304).end();\r\n return;\r\n }\r\n\r\n res.type(mimeTypes[outputFormat]);\r\n res.setHeader(\r\n \"Content-Disposition\",\r\n `inline; filename=\"${processedFileName}\"`\r\n );\r\n res.setHeader(\r\n \"Cache-Control\",\r\n parsedOptions.cacheControl ??\r\n \"public, max-age=86400, stale-while-revalidate=604800\"\r\n );\r\n if (etag) {\r\n res.setHeader(\"ETag\", etag);\r\n }\r\n res.setHeader(\"Content-Length\", processedImage.length.toString());\r\n res.send(processedImage);\r\n } catch {\r\n try {\r\n const fallbackType =\r\n requestedType === \"avatar\" ? \"avatar\" : \"normal\";\r\n const fallback = await FALLBACKIMAGES[fallbackType]();\r\n res.type(mimeTypes.jpeg);\r\n res.setHeader(\"Content-Disposition\", `inline; filename=\"fallback.jpeg\"`);\r\n res.setHeader(\"Cache-Control\", \"public, max-age=60\");\r\n res.send(fallback);\r\n } catch (fallbackError) {\r\n next(fallbackError);\r\n }\r\n }\r\n};\r\n\r\n/**\r\n * @function registerServe\r\n * @description A function to register the serveImage function as middleware for Express.\r\n * @param {PixelServeOptions} options - The options object for image processing.\r\n * @returns {function(Request, Response, NextFunction): Promise<void>} The middleware function.\r\n */\r\nconst registerServe = (\r\n options: PixelServeOptions\r\n): ((req: Request, res: Response, next: NextFunction) => Promise<void>) => {\r\n return async (req: Request, res: Response, next: NextFunction) =>\r\n serveImage(req, res, next, options);\r\n};\r\n\r\nexport default registerServe;\r\n"]}
package/package.json CHANGED
@@ -1,12 +1,26 @@
1
1
  {
2
2
  "name": "pixel-serve-server",
3
- "version": "0.0.7",
3
+ "version": "1.0.3",
4
4
  "description": "A robust Node.js utility for handling and processing images. This package provides features like resizing, format conversion and etc.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
7
7
  "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./assets/*": "./dist/assets/*",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "sideEffects": false,
8
18
  "scripts": {
9
- "build": "tsup"
19
+ "build": "tsup",
20
+ "lint": "eslint \"src/**/*.{ts,tsx}\"",
21
+ "format": "prettier --check \"src/**/*.{ts,tsx}\"",
22
+ "test": "vitest run --coverage",
23
+ "test:watch": "vitest watch"
10
24
  },
11
25
  "files": [
12
26
  "dist",
@@ -31,18 +45,29 @@
31
45
  "url": "https://github.com/Hiprax/pixel-serve-server/issues"
32
46
  },
33
47
  "homepage": "https://github.com/Hiprax/pixel-serve-server#readme",
48
+ "dependencies": {
49
+ "axios": "^1.13.2",
50
+ "express": "^5.2.1",
51
+ "sharp": "^0.34.5",
52
+ "zod": "^4.1.13"
53
+ },
34
54
  "devDependencies": {
35
- "@types/express": "^5.0.0",
36
- "@types/node": "^22.10.5",
37
- "tsup": "^8.3.5",
38
- "typescript": "^5.7.3"
55
+ "@types/express": "^5.0.6",
56
+ "@types/node": "^24.10.1",
57
+ "@types/supertest": "^6.0.3",
58
+ "@typescript-eslint/eslint-plugin": "^8.48.1",
59
+ "@typescript-eslint/parser": "^8.48.1",
60
+ "@vitest/coverage-v8": "^4.0.15",
61
+ "eslint": "^9.39.1",
62
+ "eslint-config-prettier": "^10.1.8",
63
+ "prettier": "^3.7.4",
64
+ "supertest": "^7.1.4",
65
+ "tsup": "^8.5.1",
66
+ "typescript": "^5.9.3",
67
+ "typescript-eslint": "^8.56.0",
68
+ "vitest": "^4.0.15"
39
69
  },
40
70
  "engines": {
41
- "node": ">=8.x"
42
- },
43
- "dependencies": {
44
- "axios": "^1.7.9",
45
- "express": "^4.21.2",
46
- "sharp": "^0.33.5"
71
+ "node": ">=18"
47
72
  }
48
73
  }