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.
- package/README.md +183 -34
- package/dist/app.d.ts +2 -0
- package/dist/app.js +9 -5
- package/dist/crypto/hasher.d.ts +91 -0
- package/dist/crypto/hasher.js +220 -0
- package/dist/crypto/index.d.ts +2 -0
- package/dist/crypto/index.js +12 -0
- package/dist/crypto/jwt.d.ts +34 -0
- package/dist/crypto/jwt.js +112 -0
- package/dist/exceptions/uploadException.d.ts +6 -0
- package/dist/exceptions/uploadException.js +15 -0
- package/dist/helpers/http.js +1 -3
- package/dist/http/logger.d.ts +2 -3
- package/dist/http/logger.js +2 -2
- package/dist/http/nodeNativeHttp.d.ts +0 -2
- package/dist/http/nodeNativeHttp.js +8 -16
- package/dist/http/request.d.ts +29 -46
- package/dist/http/request.js +40 -62
- package/dist/http/response.d.ts +7 -6
- package/dist/http/response.js +32 -26
- package/dist/index.d.ts +4 -0
- package/dist/index.js +10 -1
- package/dist/middlewares/auth.d.ts +34 -0
- package/dist/middlewares/auth.js +57 -0
- package/dist/middlewares/cors.js +6 -8
- package/dist/middlewares/index.d.ts +1 -0
- package/dist/middlewares/index.js +3 -1
- package/dist/middlewares/session.js +1 -1
- package/dist/parsers/jsonParser.js +2 -1
- package/dist/parsers/multipartParser.d.ts +1 -1
- package/dist/parsers/multipartParser.js +3 -2
- package/dist/parsers/parserInterface.d.ts +28 -2
- package/dist/parsers/parserInterface.js +14 -0
- package/dist/parsers/textParser.js +3 -2
- package/dist/parsers/urlEncodedParser.js +2 -1
- package/dist/parsers/xmlParser.js +3 -2
- package/dist/routing/router.js +3 -3
- package/dist/sessions/index.d.ts +1 -1
- package/dist/static/fileDownload.d.ts +4 -2
- package/dist/static/fileDownload.js +1 -6
- package/dist/storage/storage.d.ts +118 -0
- package/dist/storage/storage.js +178 -0
- package/dist/storage/types.d.ts +128 -0
- package/dist/storage/types.js +31 -0
- package/dist/storage/uploader.d.ts +196 -0
- package/dist/storage/uploader.js +370 -0
- package/dist/types/index.d.ts +0 -13
- package/dist/validators/index.d.ts +0 -1
- package/dist/validators/index.js +1 -3
- package/dist/validators/validationRule.d.ts +11 -1
- package/dist/validators/validationRule.js +14 -1
- package/dist/validators/validationSchema.d.ts +44 -115
- package/dist/validators/validationSchema.js +97 -146
- package/package.json +9 -5
- package/dist/exceptions/index.d.ts +0 -5
- 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.
|
|
70
|
+
return Response.json(request.params);
|
|
67
71
|
});
|
|
68
72
|
|
|
69
73
|
app.post("/posts", (request: Request) => {
|
|
70
|
-
return Response.json(request.
|
|
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.
|
|
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
|
-
##
|
|
128
|
+
## 🌐 CORS Middleware
|
|
125
129
|
|
|
126
|
-
To
|
|
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.
|
|
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
|
-
##
|
|
161
|
+
## ⛔ Data Validation
|
|
137
162
|
|
|
138
163
|
Skyguard.js provides a **declarative validation system** using schemas.
|
|
139
164
|
|
|
140
165
|
```ts
|
|
141
|
-
import {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
.
|
|
145
|
-
.required(
|
|
146
|
-
.
|
|
147
|
-
.
|
|
148
|
-
.
|
|
149
|
-
|
|
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 (
|
|
229
|
-
-
|
|
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
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
|
|
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.
|
|
75
|
-
const staticResponse = await this.staticFileHandler.tryServeFile(request.
|
|
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
|
|
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,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; } });
|