skyguard-js 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +183 -34
  2. package/dist/app.d.ts +2 -0
  3. package/dist/app.js +9 -5
  4. package/dist/crypto/hasher.d.ts +91 -0
  5. package/dist/crypto/hasher.js +220 -0
  6. package/dist/crypto/index.d.ts +2 -0
  7. package/dist/crypto/index.js +12 -0
  8. package/dist/crypto/jwt.d.ts +34 -0
  9. package/dist/crypto/jwt.js +112 -0
  10. package/dist/exceptions/uploadException.d.ts +6 -0
  11. package/dist/exceptions/uploadException.js +15 -0
  12. package/dist/helpers/http.js +1 -3
  13. package/dist/http/logger.d.ts +2 -3
  14. package/dist/http/logger.js +2 -2
  15. package/dist/http/nodeNativeHttp.d.ts +0 -2
  16. package/dist/http/nodeNativeHttp.js +8 -16
  17. package/dist/http/request.d.ts +29 -46
  18. package/dist/http/request.js +40 -62
  19. package/dist/http/response.d.ts +7 -6
  20. package/dist/http/response.js +32 -26
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.js +10 -1
  23. package/dist/middlewares/auth.d.ts +34 -0
  24. package/dist/middlewares/auth.js +57 -0
  25. package/dist/middlewares/cors.js +6 -8
  26. package/dist/middlewares/index.d.ts +1 -0
  27. package/dist/middlewares/index.js +3 -1
  28. package/dist/middlewares/session.js +1 -1
  29. package/dist/parsers/jsonParser.js +2 -1
  30. package/dist/parsers/multipartParser.d.ts +1 -1
  31. package/dist/parsers/multipartParser.js +3 -2
  32. package/dist/parsers/parserInterface.d.ts +28 -2
  33. package/dist/parsers/parserInterface.js +14 -0
  34. package/dist/parsers/textParser.js +3 -2
  35. package/dist/parsers/urlEncodedParser.js +2 -1
  36. package/dist/parsers/xmlParser.js +3 -2
  37. package/dist/routing/router.js +3 -3
  38. package/dist/sessions/index.d.ts +1 -1
  39. package/dist/static/fileDownload.d.ts +4 -2
  40. package/dist/static/fileDownload.js +1 -6
  41. package/dist/storage/storage.d.ts +118 -0
  42. package/dist/storage/storage.js +178 -0
  43. package/dist/storage/types.d.ts +128 -0
  44. package/dist/storage/types.js +31 -0
  45. package/dist/storage/uploader.d.ts +196 -0
  46. package/dist/storage/uploader.js +370 -0
  47. package/dist/types/index.d.ts +0 -13
  48. package/dist/validators/index.d.ts +0 -1
  49. package/dist/validators/index.js +1 -3
  50. package/dist/validators/validationRule.d.ts +11 -1
  51. package/dist/validators/validationRule.js +14 -1
  52. package/dist/validators/validationSchema.d.ts +44 -115
  53. package/dist/validators/validationSchema.js +97 -146
  54. package/package.json +9 -5
  55. package/dist/exceptions/index.d.ts +0 -5
  56. package/dist/exceptions/index.js +0 -16
package/README.md CHANGED
@@ -26,6 +26,10 @@ At its current stage, the framework focuses on **routing**, **internal architect
26
26
  - Request / Response abstractions
27
27
  - Declarative data validation
28
28
  - Simple template engine with layouts and helpers
29
+ - Built-in HTTP exceptions
30
+ - Password hashing and JWT token generation
31
+ - CORS middleware
32
+ - File uploads (via middleware)
29
33
  - Static file serving
30
34
  - Session handling (via middleware)
31
35
 
@@ -63,11 +67,11 @@ Routes are registered using HTTP methods on the `app` instance.
63
67
 
64
68
  ```ts
65
69
  app.get("/posts/{id}", (request: Request) => {
66
- return Response.json(request.getParams());
70
+ return Response.json(request.params);
67
71
  });
68
72
 
69
73
  app.post("/posts", (request: Request) => {
70
- return Response.json(request.getData());
74
+ return Response.json(request.data);
71
75
  });
72
76
  ```
73
77
 
@@ -99,7 +103,7 @@ const authMiddleware = async (
99
103
  request: Request,
100
104
  next: RouteHandler,
101
105
  ): Promise<Response> => {
102
- if (request.getHeaders["authorization"] !== "secret") {
106
+ if (request.headers["authorization"] !== "secret") {
103
107
  return Response.json({ message: "Unauthorized" }).setStatus(401);
104
108
  }
105
109
 
@@ -121,37 +125,53 @@ app.get("/secure", () => Response.json({ secure: true }), [authMiddleware]);
121
125
 
122
126
  ---
123
127
 
124
- ## Static Files
128
+ ## 🌐 CORS Middleware
125
129
 
126
- To serve static files, use the `static` method.
130
+ To enable CORS, use the built-in `cors` middleware.
131
+
132
+ ```ts
133
+ import { cors } from "skyguard-js/middlewares";
134
+
135
+ app.middlewares([
136
+ cors({
137
+ origin: ["http://localhost:3000", "https://myapp.com"],
138
+ methods: ["GET", "POST"],
139
+ allowedHeaders: ["Content-Type", "Authorization"],
140
+ credentials: true,
141
+ }),
142
+ ]);
143
+ ```
144
+
145
+ ---
146
+
147
+ ## 📌 Static Files
148
+
149
+ To serve static files, use the application's `staticFiles` method with the directory path. The name of the folder will determine the initial route prefix.
127
150
 
128
151
  ```ts
129
152
  import { join } from "node:path";
130
153
 
131
- app.static(join(__dirname, "..", "static"));
154
+ app.staticFiles(join(__dirname, "..", "static"));
155
+
156
+ // Route http://localhost:3000/static/style.css will serve the file located at ./static/style.css
132
157
  ```
133
158
 
134
159
  ---
135
160
 
136
- ## 📦 Data Validation
161
+ ## Data Validation
137
162
 
138
163
  Skyguard.js provides a **declarative validation system** using schemas.
139
164
 
140
165
  ```ts
141
- import { ValidationSchema } from "skyguard-js/validation";
142
-
143
- export const userSchema = ValidationSchema.create()
144
- .field("name")
145
- .required("Name is required")
146
- .string({ maxLength: 60 })
147
- .field("email")
148
- .required()
149
- .email()
150
- .field("age")
151
- .number({ min: 18, max: 99 })
152
- .field("active")
153
- .boolean()
154
- .build();
166
+ import { validator } from "skyguard-js/validation";
167
+
168
+ const userSchema = validator.schema({
169
+ name: validator.string({ maxLength: 60 }),
170
+ email: validator.email().required(),
171
+ age: validator.number({ min: 18 }),
172
+ active: validator.boolean().required(),
173
+ birthdate: validator.date({ max: new Date() }),
174
+ });
155
175
 
156
176
  app.post("/users", (request: Request) => {
157
177
  const validatedData = request.validateData(userSchema);
@@ -172,6 +192,143 @@ Validation is:
172
192
 
173
193
  ---
174
194
 
195
+ ## 🚨 Exceptions & Error Handling
196
+
197
+ The framework provides a set of built-in HTTP exceptions that can be thrown from route handlers or middleware. When an exception is thrown, the framework detects it and sends an appropriate HTTP response with the status code and message you specified in the class.
198
+
199
+ ```ts
200
+ import { NotFoundError, InternalServerError } from "skyguard-js/exceptions";
201
+
202
+ const listResources = ["1", "2", "3"];
203
+
204
+ app.get("/resource/{id}", (request: Request) => {
205
+ const resource = request.params["id"];
206
+
207
+ if (!listResources.includes(resource)) {
208
+ throw new NotFoundError("Resource not found");
209
+ }
210
+
211
+ return Response.json(resource);
212
+ });
213
+
214
+ app.get("/divide", (request: Request) => {
215
+ try {
216
+ const { a, b } = request.query;
217
+ const result = Number(a) / Number(b);
218
+
219
+ return Response.json({ result });
220
+ } catch (error) {
221
+ throw new InternalServerError(
222
+ "An error occurred while processing your request",
223
+ );
224
+ }
225
+ });
226
+ ```
227
+
228
+ ---
229
+
230
+ ## 🧱 Sessions
231
+
232
+ To handle sessions, you must use the framework’s built-in middleware. Depending on where you want to store them (in memory, in files, or in a database), you need to use the corresponding storage class.
233
+
234
+ ```ts
235
+ import { sessions } from "skyguard-js/middlewares";
236
+ import { FileSessionStorage } from "skyguard-js";
237
+
238
+ app.middlewares([sessions(FileSessionStorage)]);
239
+
240
+ app.post("/login", (request: Request) => {
241
+ const { username, password } = request.data;
242
+
243
+ if (username === "admin" && password === "secret") {
244
+ request.session.set("user", {
245
+ id: 1,
246
+ username: "admin",
247
+ role: "admin",
248
+ });
249
+
250
+ return json({ message: "Logged in" });
251
+ }
252
+
253
+ throw new UnauthorizedError("Invalid credentials");
254
+ });
255
+
256
+ app.get("/me", (request: Request) => {
257
+ const user = request.session.get("user");
258
+
259
+ if (!user) throw new UnauthorizedError("Not authenticated");
260
+ return json({ user });
261
+ });
262
+ ```
263
+
264
+ ---
265
+
266
+ ## 🛡️ Security
267
+
268
+ The framework includes some password hashing and JWT token generation functions, and also includes JWT authentication middleware.
269
+
270
+ ```ts
271
+ import { hash, verify, createJWT } from "skyguard-js/security";
272
+ import { authJWT } from "skyguard-js/middlewares";
273
+
274
+ app.post("/register", async (request: Request) => {
275
+ const { username, password } = request.data;
276
+ const hashedPassword = await hash(password);
277
+
278
+ // Save username and hashedPassword to database
279
+ // ...
280
+
281
+ return Response.json({ message: "User registered" });
282
+ });
283
+
284
+ app.post("/login", async (request: Request) => {
285
+ const { username, password } = request.data;
286
+
287
+ // Retrieve user from database by username
288
+ // ...
289
+
290
+ const isValid = await verify(password, user.hashedPassword);
291
+
292
+ if (!isValid) {
293
+ throw new UnauthorizedError("Invalid credentials");
294
+ }
295
+
296
+ const token = createJWT({ sub: user.id, role: user.role }, "1h");
297
+
298
+ return Response.json({ token });
299
+ });
300
+ ```
301
+
302
+ ---
303
+
304
+ ## 📂 File Uploads
305
+
306
+ To handle file uploads, use the built-in `createUploader` function to create an uploader middleware with the desired storage configuration.
307
+
308
+ ```ts
309
+ import { createUploader, StorageType } from "skyguard-js";
310
+
311
+ const uploader = createUploader({
312
+ storageType: StorageType.DISK,
313
+ storageOptions: {
314
+ destination: "./uploads",
315
+ },
316
+ });
317
+
318
+ app.post(
319
+ "/upload",
320
+ (request: Request) => {
321
+ return Response.json({
322
+ message: "File uploaded successfully",
323
+ file: request.file,
324
+ });
325
+ },
326
+ [uploader.single("file")],
327
+ );
328
+ ```
329
+
330
+ ---
331
+
175
332
  ## 📄 Views & Template Engine
176
333
 
177
334
  To render HTML views, use the `render` helper.
@@ -207,17 +364,6 @@ app.get("/home", () => {
207
364
 
208
365
  ---
209
366
 
210
- ## 🧱 Project Status
211
-
212
- ⚠️ **Early-stage project**
213
-
214
- - Not production-ready
215
- - API may change
216
- - Features are still evolving
217
- - Intended primarily for learning and experimentation
218
-
219
- ---
220
-
221
367
  ## 🔮 Roadmap (Tentative)
222
368
 
223
369
  - Middleware system (✅)
@@ -225,9 +371,12 @@ app.get("/home", () => {
225
371
  - Request / Response abstraction (✅)
226
372
  - Data validation (✅)
227
373
  - Error handling improvements (✅)
228
- - Sessions & cookies (in progress)
229
- - Authentication & authorization
374
+ - Sessions & cookies ()
375
+ - Passoword hashing & JWT tokens (✅)
376
+ - File uploads (✅)
230
377
  - Database & ORM integration
378
+ - Authentication & authorization
379
+ - WebSockets
231
380
 
232
381
  ---
233
382
 
package/dist/app.d.ts CHANGED
@@ -30,6 +30,8 @@ export declare class App {
30
30
  view: View;
31
31
  /** Static file handler (optional) */
32
32
  private staticFileHandler;
33
+ private logger;
34
+ private startTime;
33
35
  /**
34
36
  * Bootstraps and configures the application.
35
37
  *
package/dist/app.js CHANGED
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createApp = exports.App = void 0;
4
4
  const routing_1 = require("./routing");
5
5
  const http_1 = require("./http");
6
- const exceptions_1 = require("./exceptions");
6
+ const validationException_1 = require("./exceptions/validationException");
7
7
  const views_1 = require("./views");
8
8
  const node_path_1 = require("node:path");
9
9
  const app_1 = require("./helpers/app");
@@ -39,6 +39,8 @@ class App {
39
39
  view;
40
40
  /** Static file handler (optional) */
41
41
  staticFileHandler = null;
42
+ logger;
43
+ startTime;
42
44
  /**
43
45
  * Bootstraps and configures the application.
44
46
  *
@@ -52,6 +54,7 @@ class App {
52
54
  const app = (0, app_1.singleton)(App);
53
55
  app.router = new routing_1.Router();
54
56
  app.view = new views_1.RaptorEngine((0, node_path_1.join)(__dirname, "..", "views"));
57
+ app.logger = new http_1.Logger();
55
58
  return app;
56
59
  }
57
60
  /**
@@ -71,8 +74,8 @@ class App {
71
74
  async handle(adapter) {
72
75
  try {
73
76
  const request = await adapter.getRequest();
74
- if (this.staticFileHandler && request.getMethod === http_1.HttpMethods.get) {
75
- const staticResponse = await this.staticFileHandler.tryServeFile(request.getUrl);
77
+ if (this.staticFileHandler && request.method === http_1.HttpMethods.get) {
78
+ const staticResponse = await this.staticFileHandler.tryServeFile(request.url);
76
79
  if (staticResponse) {
77
80
  adapter.sendResponse(staticResponse);
78
81
  return;
@@ -107,8 +110,10 @@ class App {
107
110
  */
108
111
  run(port, callback, hostname = "127.0.0.1") {
109
112
  (0, node_http_1.createServer)((req, res) => {
113
+ this.startTime = process.hrtime.bigint();
110
114
  const adapter = new http_1.NodeHttpAdapter(req, res);
111
115
  void this.handle(adapter);
116
+ this.logger.log(req, res, this.startTime);
112
117
  }).listen(port, hostname, () => {
113
118
  callback();
114
119
  });
@@ -175,9 +180,8 @@ class App {
175
180
  adapter.sendResponse(http_1.Response.json(error.toJSON()).setStatus(error.statusCode));
176
181
  return;
177
182
  }
178
- if (error instanceof exceptions_1.ValidationException) {
183
+ if (error instanceof validationException_1.ValidationException) {
179
184
  adapter.sendResponse(http_1.Response.json({
180
- success: false,
181
185
  errors: error.getErrorsByField(),
182
186
  }).setStatus(400));
183
187
  return;
@@ -0,0 +1,91 @@
1
+ type ScryptOptions = {
2
+ cost: number;
3
+ blockSize: number;
4
+ parallelization: number;
5
+ maxmem?: number;
6
+ };
7
+ /**
8
+ * Hashes a plaintext password using scrypt with an unique random salt.
9
+ *
10
+ * Output format:
11
+ * `scrypt$<cost>$<blockSize>$<parallelization>$<saltHex>$<hashHex>`
12
+ *
13
+ * @param password - Plaintext password.
14
+ * @param saltLength - Salt length in bytes. Default `16` (recommended minimum).
15
+ * @param params - Scrypt work-factor params. Defaults to `DEFAULT_PARAMS`.
16
+ * @param pepper - Optional server secret mixed into the password (e.g., from env var).
17
+ * @returns A compact encoded hash string containing algorithm parameters + salt + derived key.
18
+ */
19
+ export declare const hash: (password: string, saltLength?: number, params?: ScryptOptions, pepper?: string) => Promise<string>;
20
+ /**
21
+ * Verifies a plaintext password against a stored scrypt hash string.
22
+ *
23
+ * Safe failure behavior:
24
+ * - Returns `false` for any parsing error, invalid encoding, mismatched lengths,
25
+ * or scrypt errors. This avoids leaking details about why verification failed.
26
+ *
27
+ * @param password - Plaintext password to verify.
28
+ * @param storedHash - Stored hash string in the compact format.
29
+ * @param pepper - Optional server secret; must match the one used when hashing.
30
+ * @returns `true` if the password matches, otherwise `false`.
31
+ */
32
+ export declare const verify: (password: string, storedHash: string, pepper?: string) => Promise<boolean>;
33
+ /**
34
+ * Indicates whether a stored hash should be regenerated using the current parameters.
35
+ *
36
+ * Use this after successful login:
37
+ * - If `needsRehash(...) === true`, compute a new hash using `hash(...)` with
38
+ * the latest parameters and store it back to the DB.
39
+ *
40
+ * This enables gradual upgrades of the work factor without forcing password resets.
41
+ *
42
+ * @param storedHash - Stored hash string in compact format.
43
+ * @param params - Desired/current scrypt params. Defaults to `DEFAULT_PARAMS`.
44
+ * @returns `true` if the hash is missing/invalid or was produced with different parameters.
45
+ */
46
+ export declare const needsRehash: (storedHash: string, params?: ScryptOptions) => boolean;
47
+ /**
48
+ * Hashes multiple passwords using controlled concurrency.
49
+ *
50
+ * Why limit concurrency?
51
+ * scrypt is intentionally CPU + memory intensive. Running too many in parallel can:
52
+ * - saturate the libuv threadpool,
53
+ * - exceed memory limits (especially in containers),
54
+ * - increase latency for the rest of the application.
55
+ *
56
+ * The `concurrency` option caps the number of simultaneous scrypt operations.
57
+ *
58
+ * @param passwords - List of plaintext passwords.
59
+ * @param options - Optional hashing controls:
60
+ * - saltLength: salt bytes (default 16)
61
+ * - params: scrypt params (default DEFAULT_PARAMS)
62
+ * - pepper: optional server secret
63
+ * - concurrency: max simultaneous operations (default 4)
64
+ * @returns Array of compact hash strings in the same order as input.
65
+ */
66
+ export declare const hashBatch: (passwords: string[], options?: {
67
+ saltLength?: number;
68
+ params?: ScryptOptions;
69
+ pepper?: string;
70
+ concurrency?: number;
71
+ }) => Promise<string[]>;
72
+ /**
73
+ * Verifies multiple password/hash pairs using controlled concurrency.
74
+ *
75
+ * This is useful for bulk checks or migrations. Concurrency is typically higher
76
+ * than hashing, but still should be bounded to avoid saturating the threadpool.
77
+ *
78
+ * @param credentials - Array of `{ password, hash }` pairs.
79
+ * @param options - Optional verification controls:
80
+ * - pepper: optional server secret
81
+ * - concurrency: max simultaneous operations (default 8)
82
+ * @returns Array of booleans in the same order as input.
83
+ */
84
+ export declare const verifyBatch: (credentials: Array<{
85
+ password: string;
86
+ hash: string;
87
+ }>, options?: {
88
+ pepper?: string;
89
+ concurrency?: number;
90
+ }) => Promise<boolean[]>;
91
+ export {};
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyBatch = exports.hashBatch = exports.needsRehash = exports.verify = exports.hash = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const node_util_1 = require("node:util");
6
+ /**
7
+ * Derives password keys using Node.js `crypto.scrypt` but exposed as a Promise.
8
+ *
9
+ * Why the cast?
10
+ * `crypto.scrypt` has multiple overloads (with/without `options`). When wrapped with
11
+ * `util.promisify`, TypeScript often keeps only the 3-argument overload and “forgets”
12
+ * the one that accepts `options`. We re-type it to preserve the 4-argument signature.
13
+ *
14
+ * @param password - Plaintext password (or Buffer) to derive from.
15
+ * @param salt - Random per-password salt (or Buffer).
16
+ * @param keylen - Desired derived key length in bytes (e.g., 64).
17
+ * @param options - Scrypt work-factor parameters (cost, blockSize, parallelization, maxmem).
18
+ * @returns The derived key as a Buffer.
19
+ */
20
+ const scryptAsync = (0, node_util_1.promisify)(node_crypto_1.scrypt);
21
+ const KEY_LENGTH = 64;
22
+ // Reasonable defaults (tune cost by measuring ~100–250ms on your server).
23
+ const DEFAULT_PARAMS = {
24
+ cost: 16384,
25
+ blockSize: 8,
26
+ parallelization: 1,
27
+ maxmem: 64 * 1024 * 1024,
28
+ };
29
+ const isHex = (s) => /^[0-9a-f]+$/i.test(s);
30
+ const isPositiveInt = (n) => Number.isInteger(n) && n > 0;
31
+ /**
32
+ * Parses a compact scrypt password hash string into its components.
33
+ *
34
+ * Expected format:
35
+ * `scrypt$<cost>$<blockSize>$<parallelization>$<saltHex>$<hashHex>`
36
+ *
37
+ * @param hash - Stored hash string in compact format.
38
+ * @returns Parsed fields or `null` if the string is invalid.
39
+ */
40
+ const parseHash = (hash) => {
41
+ try {
42
+ const parts = hash.split("$");
43
+ if (parts.length !== 6)
44
+ return null;
45
+ const [algo, costStr, blockSizeStr, parallelStr, saltHex, hashHex] = parts;
46
+ if (algo !== "scrypt")
47
+ return null;
48
+ const cost = Number(costStr);
49
+ const blockSize = Number(blockSizeStr);
50
+ const parallelization = Number(parallelStr);
51
+ if (!isPositiveInt(cost) ||
52
+ !isPositiveInt(blockSize) ||
53
+ !isPositiveInt(parallelization))
54
+ return null;
55
+ if (!saltHex || !hashHex)
56
+ return null;
57
+ if (!isHex(saltHex) || !isHex(hashHex))
58
+ return null;
59
+ if (saltHex.length % 2 !== 0 || hashHex.length % 2 !== 0)
60
+ return null;
61
+ if (saltHex.length < 32)
62
+ return null;
63
+ const hashBuffer = Buffer.from(hashHex, "hex");
64
+ if (hashBuffer.length === 0)
65
+ return null;
66
+ return {
67
+ cost,
68
+ blockSize,
69
+ parallelization,
70
+ salt: saltHex,
71
+ hash: hashHex,
72
+ keyLength: hashBuffer.length,
73
+ };
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ };
79
+ /**
80
+ * Hashes a plaintext password using scrypt with an unique random salt.
81
+ *
82
+ * Output format:
83
+ * `scrypt$<cost>$<blockSize>$<parallelization>$<saltHex>$<hashHex>`
84
+ *
85
+ * @param password - Plaintext password.
86
+ * @param saltLength - Salt length in bytes. Default `16` (recommended minimum).
87
+ * @param params - Scrypt work-factor params. Defaults to `DEFAULT_PARAMS`.
88
+ * @param pepper - Optional server secret mixed into the password (e.g., from env var).
89
+ * @returns A compact encoded hash string containing algorithm parameters + salt + derived key.
90
+ */
91
+ const hash = async (password, saltLength = 16, params = DEFAULT_PARAMS, pepper) => {
92
+ const salt = (0, node_crypto_1.randomBytes)(saltLength);
93
+ const pwd = pepper ? password + pepper : password;
94
+ const derived = await scryptAsync(pwd, salt, KEY_LENGTH, {
95
+ cost: params.cost,
96
+ blockSize: params.blockSize,
97
+ parallelization: params.parallelization,
98
+ ...(params.maxmem ? { maxmem: params.maxmem } : {}),
99
+ });
100
+ return `scrypt$${params.cost}$${params.blockSize}$${params.parallelization}$${salt.toString("hex")}$${derived.toString("hex")}`;
101
+ };
102
+ exports.hash = hash;
103
+ /**
104
+ * Verifies a plaintext password against a stored scrypt hash string.
105
+ *
106
+ * Safe failure behavior:
107
+ * - Returns `false` for any parsing error, invalid encoding, mismatched lengths,
108
+ * or scrypt errors. This avoids leaking details about why verification failed.
109
+ *
110
+ * @param password - Plaintext password to verify.
111
+ * @param storedHash - Stored hash string in the compact format.
112
+ * @param pepper - Optional server secret; must match the one used when hashing.
113
+ * @returns `true` if the password matches, otherwise `false`.
114
+ */
115
+ const verify = async (password, storedHash, pepper) => {
116
+ const parsed = parseHash(storedHash);
117
+ if (!parsed)
118
+ return false;
119
+ try {
120
+ const pwd = pepper ? password + pepper : password;
121
+ const derived = await scryptAsync(pwd, Buffer.from(parsed.salt, "hex"), parsed.keyLength, {
122
+ cost: parsed.cost,
123
+ blockSize: parsed.blockSize,
124
+ parallelization: parsed.parallelization,
125
+ });
126
+ const storedBuffer = Buffer.from(parsed.hash, "hex");
127
+ if (storedBuffer.length !== derived.length)
128
+ return false;
129
+ return (0, node_crypto_1.timingSafeEqual)(derived, storedBuffer);
130
+ }
131
+ catch {
132
+ return false;
133
+ }
134
+ };
135
+ exports.verify = verify;
136
+ /**
137
+ * Indicates whether a stored hash should be regenerated using the current parameters.
138
+ *
139
+ * Use this after successful login:
140
+ * - If `needsRehash(...) === true`, compute a new hash using `hash(...)` with
141
+ * the latest parameters and store it back to the DB.
142
+ *
143
+ * This enables gradual upgrades of the work factor without forcing password resets.
144
+ *
145
+ * @param storedHash - Stored hash string in compact format.
146
+ * @param params - Desired/current scrypt params. Defaults to `DEFAULT_PARAMS`.
147
+ * @returns `true` if the hash is missing/invalid or was produced with different parameters.
148
+ */
149
+ const needsRehash = (storedHash, params = DEFAULT_PARAMS) => {
150
+ const parsed = parseHash(storedHash);
151
+ if (!parsed)
152
+ return true;
153
+ return (parsed.keyLength !== KEY_LENGTH ||
154
+ parsed.cost !== params.cost ||
155
+ parsed.blockSize !== params.blockSize ||
156
+ parsed.parallelization !== params.parallelization);
157
+ };
158
+ exports.needsRehash = needsRehash;
159
+ /**
160
+ * Simple concurrency limiter for CPU-heavy hashing/verifying.
161
+ */
162
+ const mapLimit = async (items, limit, fn) => {
163
+ const results = new Array(items.length);
164
+ let idx = 0;
165
+ const workers = Array.from({ length: Math.max(1, limit) }, async () => {
166
+ while (true) {
167
+ const i = idx++;
168
+ if (i >= items.length)
169
+ break;
170
+ results[i] = await fn(items[i], i);
171
+ }
172
+ });
173
+ await Promise.all(workers);
174
+ return results;
175
+ };
176
+ /**
177
+ * Hashes multiple passwords using controlled concurrency.
178
+ *
179
+ * Why limit concurrency?
180
+ * scrypt is intentionally CPU + memory intensive. Running too many in parallel can:
181
+ * - saturate the libuv threadpool,
182
+ * - exceed memory limits (especially in containers),
183
+ * - increase latency for the rest of the application.
184
+ *
185
+ * The `concurrency` option caps the number of simultaneous scrypt operations.
186
+ *
187
+ * @param passwords - List of plaintext passwords.
188
+ * @param options - Optional hashing controls:
189
+ * - saltLength: salt bytes (default 16)
190
+ * - params: scrypt params (default DEFAULT_PARAMS)
191
+ * - pepper: optional server secret
192
+ * - concurrency: max simultaneous operations (default 4)
193
+ * @returns Array of compact hash strings in the same order as input.
194
+ */
195
+ const hashBatch = async (passwords, options) => {
196
+ const saltLength = options?.saltLength ?? 16;
197
+ const params = options?.params ?? DEFAULT_PARAMS;
198
+ const pepper = options?.pepper;
199
+ const concurrency = options?.concurrency ?? 4;
200
+ return mapLimit(passwords, concurrency, p => (0, exports.hash)(p, saltLength, params, pepper));
201
+ };
202
+ exports.hashBatch = hashBatch;
203
+ /**
204
+ * Verifies multiple password/hash pairs using controlled concurrency.
205
+ *
206
+ * This is useful for bulk checks or migrations. Concurrency is typically higher
207
+ * than hashing, but still should be bounded to avoid saturating the threadpool.
208
+ *
209
+ * @param credentials - Array of `{ password, hash }` pairs.
210
+ * @param options - Optional verification controls:
211
+ * - pepper: optional server secret
212
+ * - concurrency: max simultaneous operations (default 8)
213
+ * @returns Array of booleans in the same order as input.
214
+ */
215
+ const verifyBatch = async (credentials, options) => {
216
+ const pepper = options?.pepper;
217
+ const concurrency = options?.concurrency ?? 8;
218
+ return mapLimit(credentials, concurrency, c => (0, exports.verify)(c.password, c.hash, pepper));
219
+ };
220
+ exports.verifyBatch = verifyBatch;
@@ -0,0 +1,2 @@
1
+ export { hash, verify, hashBatch, verifyBatch } from "./hasher";
2
+ export { createJWT, verifyJWT, decodeJWT } from "./jwt";
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.decodeJWT = exports.verifyJWT = exports.createJWT = exports.verifyBatch = exports.hashBatch = exports.verify = exports.hash = void 0;
4
+ var hasher_1 = require("./hasher");
5
+ Object.defineProperty(exports, "hash", { enumerable: true, get: function () { return hasher_1.hash; } });
6
+ Object.defineProperty(exports, "verify", { enumerable: true, get: function () { return hasher_1.verify; } });
7
+ Object.defineProperty(exports, "hashBatch", { enumerable: true, get: function () { return hasher_1.hashBatch; } });
8
+ Object.defineProperty(exports, "verifyBatch", { enumerable: true, get: function () { return hasher_1.verifyBatch; } });
9
+ var jwt_1 = require("./jwt");
10
+ Object.defineProperty(exports, "createJWT", { enumerable: true, get: function () { return jwt_1.createJWT; } });
11
+ Object.defineProperty(exports, "verifyJWT", { enumerable: true, get: function () { return jwt_1.verifyJWT; } });
12
+ Object.defineProperty(exports, "decodeJWT", { enumerable: true, get: function () { return jwt_1.decodeJWT; } });