skyguard-js 1.2.0 → 1.2.1
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 +221 -0
- package/dist/app.d.ts +20 -1
- package/dist/app.js +32 -3
- package/dist/crypto/hasher.js +2 -1
- package/dist/crypto/jwt.js +2 -1
- package/dist/http/index.d.ts +1 -0
- package/dist/http/logger.d.ts +10 -1
- package/dist/http/logger.js +44 -8
- package/dist/http/nodeNativeHttp.d.ts +2 -1
- package/dist/http/nodeNativeHttp.js +2 -2
- package/dist/http/request.js +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/sessions/databaseSessionStorage.d.ts +52 -0
- package/dist/sessions/databaseSessionStorage.js +166 -0
- package/dist/sessions/fileSessionStorage.js +1 -1
- package/dist/sessions/index.d.ts +1 -0
- package/dist/sessions/index.js +3 -1
- package/dist/sessions/memorySessionStorage.js +1 -1
- package/dist/validators/rules/bigIntRule.d.ts +0 -6
- package/dist/validators/rules/bigIntRule.js +0 -24
- package/dist/validators/validationRule.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -428,6 +428,227 @@ app.get("/me", (request: Request) => {
|
|
|
428
428
|
});
|
|
429
429
|
```
|
|
430
430
|
|
|
431
|
+
For **database-backed sessions**, configure `DatabaseSessionStorage` once with an adapter that maps to your DB client/ORM. This keeps the framework **DB-engine agnostic** (MySQL, MariaDB, SQLite, PostgreSQL, SQL Server, Oracle, etc.).
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
import {
|
|
435
|
+
sessions,
|
|
436
|
+
DatabaseSessionStorage,
|
|
437
|
+
type SessionDatabaseAdapter,
|
|
438
|
+
} from "skyguard-js";
|
|
439
|
+
|
|
440
|
+
const sessionAdapter: SessionDatabaseAdapter = {
|
|
441
|
+
async findById(id) {
|
|
442
|
+
// query row by id and return: { data: parsedJson, expiresAt: unixMs }
|
|
443
|
+
return null;
|
|
444
|
+
},
|
|
445
|
+
async upsert(id, payload) {
|
|
446
|
+
// insert/update row depending on your DB driver
|
|
447
|
+
},
|
|
448
|
+
async deleteById(id) {
|
|
449
|
+
// delete row by id
|
|
450
|
+
},
|
|
451
|
+
async deleteExpired(now) {
|
|
452
|
+
// delete rows where expiresAt <= now
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
DatabaseSessionStorage.configure(sessionAdapter);
|
|
457
|
+
|
|
458
|
+
app.middlewares(sessions(DatabaseSessionStorage));
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Concrete DB adapter examples
|
|
462
|
+
|
|
463
|
+
> Suggested table shape (portable across engines):
|
|
464
|
+
>
|
|
465
|
+
> - `id` (string/varchar, primary key)
|
|
466
|
+
> - `data` (JSON/TEXT containing serialized object)
|
|
467
|
+
> - `expires_at` (bigint/timestamp in unix milliseconds)
|
|
468
|
+
|
|
469
|
+
To keep the code cleaner, you should create a separate file where you can configure the database sessions, such as `src/sessions/config.ts`
|
|
470
|
+
|
|
471
|
+
#### Prisma (MySQL / PostgreSQL / SQLite / SQL Server / CockroachDB)
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
import { PrismaClient } from "@prisma/client";
|
|
475
|
+
import {
|
|
476
|
+
DatabaseSessionStorage,
|
|
477
|
+
type SessionDatabaseAdapter,
|
|
478
|
+
} from "skyguard-js";
|
|
479
|
+
|
|
480
|
+
const prisma = new PrismaClient();
|
|
481
|
+
|
|
482
|
+
// model Session {
|
|
483
|
+
// id String @id
|
|
484
|
+
// data String
|
|
485
|
+
// expiresAt BigInt @map("expires_at")
|
|
486
|
+
// @@map("sessions")
|
|
487
|
+
// }
|
|
488
|
+
|
|
489
|
+
const adapter: SessionDatabaseAdapter = {
|
|
490
|
+
async findById(id) {
|
|
491
|
+
const row = await prisma.session.findUnique({ where: { id } });
|
|
492
|
+
if (!row) return null;
|
|
493
|
+
return { data: JSON.parse(row.data), expiresAt: Number(row.expiresAt) };
|
|
494
|
+
},
|
|
495
|
+
async upsert(id, payload) {
|
|
496
|
+
await prisma.session.upsert({
|
|
497
|
+
where: { id },
|
|
498
|
+
update: {
|
|
499
|
+
data: JSON.stringify(payload.data),
|
|
500
|
+
expiresAt: BigInt(payload.expiresAt),
|
|
501
|
+
},
|
|
502
|
+
create: {
|
|
503
|
+
id,
|
|
504
|
+
data: JSON.stringify(payload.data),
|
|
505
|
+
expiresAt: BigInt(payload.expiresAt),
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
},
|
|
509
|
+
async deleteById(id) {
|
|
510
|
+
await prisma.session.deleteMany({ where: { id } });
|
|
511
|
+
},
|
|
512
|
+
async deleteExpired(now) {
|
|
513
|
+
await prisma.session.deleteMany({
|
|
514
|
+
where: { expiresAt: { lte: BigInt(now) } },
|
|
515
|
+
});
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
DatabaseSessionStorage.configure(adapter);
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
#### TypeORM (MySQL / MariaDB / PostgreSQL / SQLite / MSSQL / Oracle)
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
import {
|
|
526
|
+
DataSource,
|
|
527
|
+
Entity,
|
|
528
|
+
Column,
|
|
529
|
+
PrimaryColumn,
|
|
530
|
+
LessThanOrEqual,
|
|
531
|
+
} from "typeorm";
|
|
532
|
+
import {
|
|
533
|
+
DatabaseSessionStorage,
|
|
534
|
+
type SessionDatabaseAdapter,
|
|
535
|
+
} from "skyguard-js";
|
|
536
|
+
|
|
537
|
+
@Entity({ name: "sessions" })
|
|
538
|
+
class SessionEntity {
|
|
539
|
+
@PrimaryColumn({ type: "varchar", length: 64 })
|
|
540
|
+
id!: string;
|
|
541
|
+
|
|
542
|
+
@Column({ type: "text" })
|
|
543
|
+
data!: string;
|
|
544
|
+
|
|
545
|
+
@Column({ name: "expires_at", type: "bigint" })
|
|
546
|
+
expiresAt!: string;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const ds = new DataSource({ /* your db config */ entities: [SessionEntity] });
|
|
550
|
+
await ds.initialize();
|
|
551
|
+
const repo = ds.getRepository(SessionEntity);
|
|
552
|
+
|
|
553
|
+
const adapter: SessionDatabaseAdapter = {
|
|
554
|
+
async findById(id) {
|
|
555
|
+
const row = await repo.findOneBy({ id });
|
|
556
|
+
if (!row) return null;
|
|
557
|
+
return { data: JSON.parse(row.data), expiresAt: Number(row.expiresAt) };
|
|
558
|
+
},
|
|
559
|
+
async upsert(id, payload) {
|
|
560
|
+
await repo.save({
|
|
561
|
+
id,
|
|
562
|
+
data: JSON.stringify(payload.data),
|
|
563
|
+
expiresAt: String(payload.expiresAt),
|
|
564
|
+
});
|
|
565
|
+
},
|
|
566
|
+
async deleteById(id) {
|
|
567
|
+
await repo.delete({ id });
|
|
568
|
+
},
|
|
569
|
+
async deleteExpired(now) {
|
|
570
|
+
await repo.delete({ expiresAt: LessThanOrEqual(String(now)) });
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
DatabaseSessionStorage.configure(adapter);
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
#### mysql2 (MySQL)
|
|
578
|
+
|
|
579
|
+
```ts
|
|
580
|
+
import mysql from "mysql2/promise";
|
|
581
|
+
import {
|
|
582
|
+
DatabaseSessionStorage,
|
|
583
|
+
type SessionDatabaseAdapter,
|
|
584
|
+
} from "skyguard-js";
|
|
585
|
+
|
|
586
|
+
const pool = mysql.createPool({ uri: process.env.DATABASE_URL });
|
|
587
|
+
|
|
588
|
+
const adapter: SessionDatabaseAdapter = {
|
|
589
|
+
async findById(id) {
|
|
590
|
+
const [rows] = await pool.query<any[]>(
|
|
591
|
+
"SELECT data, expires_at FROM sessions WHERE id = ? LIMIT 1",
|
|
592
|
+
[id],
|
|
593
|
+
);
|
|
594
|
+
const row = rows[0];
|
|
595
|
+
if (!row) return null;
|
|
596
|
+
return { data: JSON.parse(row.data), expiresAt: Number(row.expires_at) };
|
|
597
|
+
},
|
|
598
|
+
async upsert(id, payload) {
|
|
599
|
+
await pool.query(
|
|
600
|
+
`INSERT INTO sessions (id, data, expires_at) VALUES (?, ?, ?)
|
|
601
|
+
ON DUPLICATE KEY UPDATE data = VALUES(data), expires_at = VALUES(expires_at)`,
|
|
602
|
+
[id, JSON.stringify(payload.data), payload.expiresAt],
|
|
603
|
+
);
|
|
604
|
+
},
|
|
605
|
+
async deleteById(id) {
|
|
606
|
+
await pool.query("DELETE FROM sessions WHERE id = ?", [id]);
|
|
607
|
+
},
|
|
608
|
+
async deleteExpired(now) {
|
|
609
|
+
await pool.query("DELETE FROM sessions WHERE expires_at <= ?", [now]);
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
DatabaseSessionStorage.configure(adapter);
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
#### sqlite3 (SQLite)
|
|
617
|
+
|
|
618
|
+
```ts
|
|
619
|
+
import sqlite3 from "sqlite3";
|
|
620
|
+
import { open } from "sqlite";
|
|
621
|
+
import { type SessionDatabaseAdapter } from "skyguard-js";
|
|
622
|
+
|
|
623
|
+
const db = await open({ filename: "./sessions.db", driver: sqlite3.Database });
|
|
624
|
+
|
|
625
|
+
const adapter: SessionDatabaseAdapter = {
|
|
626
|
+
async findById(id) {
|
|
627
|
+
const row = await db.get<{ data: string; expires_at: number }>(
|
|
628
|
+
"SELECT data, expires_at FROM sessions WHERE id = ? LIMIT 1",
|
|
629
|
+
[id],
|
|
630
|
+
);
|
|
631
|
+
if (!row) return null;
|
|
632
|
+
return { data: JSON.parse(row.data), expiresAt: Number(row.expires_at) };
|
|
633
|
+
},
|
|
634
|
+
async upsert(id, payload) {
|
|
635
|
+
await db.run(
|
|
636
|
+
`INSERT INTO sessions (id, data, expires_at) VALUES (?, ?, ?)
|
|
637
|
+
ON CONFLICT(id) DO UPDATE SET data = excluded.data, expires_at = excluded.expires_at`,
|
|
638
|
+
[id, JSON.stringify(payload.data), payload.expiresAt],
|
|
639
|
+
);
|
|
640
|
+
},
|
|
641
|
+
async deleteById(id) {
|
|
642
|
+
await db.run("DELETE FROM sessions WHERE id = ?", [id]);
|
|
643
|
+
},
|
|
644
|
+
async deleteExpired(now) {
|
|
645
|
+
await db.run("DELETE FROM sessions WHERE expires_at <= ?", [now]);
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
DatabaseSessionStorage.configure(adapter);
|
|
650
|
+
```
|
|
651
|
+
|
|
431
652
|
---
|
|
432
653
|
|
|
433
654
|
## 🛡️ Security
|
package/dist/app.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RouterGroup } from "./routing";
|
|
2
|
+
import { type LogFormat } from "./http";
|
|
2
3
|
import type { Middleware, RouteHandler } from "./types";
|
|
3
4
|
import { type TemplateEngineFunction } from "./views/engineTemplate";
|
|
4
5
|
/**
|
|
@@ -19,13 +20,15 @@ import { type TemplateEngineFunction } from "./views/engineTemplate";
|
|
|
19
20
|
* from the runtime platform (Node, Bun, Deno, etc.)
|
|
20
21
|
* through {@link HttpAdapter} and {@link Server}.
|
|
21
22
|
*/
|
|
22
|
-
|
|
23
|
+
declare class App {
|
|
23
24
|
/** Main routing system */
|
|
24
25
|
private router;
|
|
25
26
|
/** Static file handler (optional) */
|
|
26
27
|
private staticFileHandler;
|
|
27
28
|
/** View engine for rendering templates (optional) */
|
|
28
29
|
private viewEngine;
|
|
30
|
+
/** Logger configuration */
|
|
31
|
+
private loggerOptions;
|
|
29
32
|
/**
|
|
30
33
|
* Bootstraps and configures the application.
|
|
31
34
|
*
|
|
@@ -107,6 +110,21 @@ export declare class App {
|
|
|
107
110
|
* @param port - TCP port to listen on
|
|
108
111
|
*/
|
|
109
112
|
run(port: number, callback: VoidFunction, hostname?: string): void;
|
|
113
|
+
/**
|
|
114
|
+
* Configures HTTP request logger output format and optional file output.
|
|
115
|
+
*
|
|
116
|
+
* Supported formats are inspired by morgan:
|
|
117
|
+
* - "combined"
|
|
118
|
+
* - "common"
|
|
119
|
+
* - "dev"
|
|
120
|
+
* - "short"
|
|
121
|
+
* - "tiny"
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* app.logger("common");
|
|
125
|
+
* app.logger("combined", "./logs/http.log");
|
|
126
|
+
*/
|
|
127
|
+
logger(format?: LogFormat, filePath?: string): void;
|
|
110
128
|
/**
|
|
111
129
|
* Sets a global prefix for all routes.
|
|
112
130
|
*
|
|
@@ -159,3 +177,4 @@ export declare class App {
|
|
|
159
177
|
private handleError;
|
|
160
178
|
}
|
|
161
179
|
export declare const createApp: () => App;
|
|
180
|
+
export {};
|
package/dist/app.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createApp =
|
|
3
|
+
exports.createApp = void 0;
|
|
4
4
|
const routing_1 = require("./routing");
|
|
5
5
|
const http_1 = require("./http");
|
|
6
6
|
const validationException_1 = require("./exceptions/validationException");
|
|
7
7
|
const node_path_1 = require("node:path");
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
8
9
|
const fileStaticHandler_1 = require("./static/fileStaticHandler");
|
|
9
10
|
const node_http_1 = require("node:http");
|
|
10
11
|
const httpExceptions_1 = require("./exceptions/httpExceptions");
|
|
@@ -35,6 +36,10 @@ class App {
|
|
|
35
36
|
staticFileHandler = null;
|
|
36
37
|
/** View engine for rendering templates (optional) */
|
|
37
38
|
viewEngine;
|
|
39
|
+
/** Logger configuration */
|
|
40
|
+
loggerOptions = {
|
|
41
|
+
format: "dev",
|
|
42
|
+
};
|
|
38
43
|
/**
|
|
39
44
|
* Bootstraps and configures the application.
|
|
40
45
|
*
|
|
@@ -144,12 +149,37 @@ class App {
|
|
|
144
149
|
*/
|
|
145
150
|
run(port, callback, hostname = "127.0.0.1") {
|
|
146
151
|
(0, node_http_1.createServer)((req, res) => {
|
|
147
|
-
const adapter = new http_1.NodeHttpAdapter(req, res);
|
|
152
|
+
const adapter = new http_1.NodeHttpAdapter(req, res, this.loggerOptions);
|
|
148
153
|
void this.handle(adapter);
|
|
149
154
|
}).listen(port, hostname, () => {
|
|
150
155
|
callback();
|
|
151
156
|
});
|
|
152
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Configures HTTP request logger output format and optional file output.
|
|
160
|
+
*
|
|
161
|
+
* Supported formats are inspired by morgan:
|
|
162
|
+
* - "combined"
|
|
163
|
+
* - "common"
|
|
164
|
+
* - "dev"
|
|
165
|
+
* - "short"
|
|
166
|
+
* - "tiny"
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* app.logger("common");
|
|
170
|
+
* app.logger("combined", "./logs/http.log");
|
|
171
|
+
*/
|
|
172
|
+
logger(format = "dev", filePath) {
|
|
173
|
+
this.loggerOptions = {
|
|
174
|
+
...this.loggerOptions,
|
|
175
|
+
format,
|
|
176
|
+
};
|
|
177
|
+
if (filePath) {
|
|
178
|
+
this.loggerOptions.fileStream = (0, node_fs_1.createWriteStream)(filePath, {
|
|
179
|
+
flags: "a",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
153
183
|
/**
|
|
154
184
|
* Sets a global prefix for all routes.
|
|
155
185
|
*
|
|
@@ -234,7 +264,6 @@ class App {
|
|
|
234
264
|
console.error(error);
|
|
235
265
|
}
|
|
236
266
|
}
|
|
237
|
-
exports.App = App;
|
|
238
267
|
const createApp = () => {
|
|
239
268
|
return App.bootstrap();
|
|
240
269
|
};
|
package/dist/crypto/hasher.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Hasher = void 0;
|
|
4
|
+
const container_1 = require("../container/container");
|
|
4
5
|
const node_crypto_1 = require("node:crypto");
|
|
5
6
|
const node_util_1 = require("node:util");
|
|
6
7
|
/**
|
|
@@ -257,4 +258,4 @@ class HasherCrypto {
|
|
|
257
258
|
* - scrypt is intentionally expensive; concurrency is bounded in batch methods to help prevent
|
|
258
259
|
* resource exhaustion (especially relevant in containers and small instances).
|
|
259
260
|
*/
|
|
260
|
-
exports.Hasher =
|
|
261
|
+
exports.Hasher = container_1.Container.singleton(HasherCrypto);
|
package/dist/crypto/jwt.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.JWT = void 0;
|
|
4
|
+
const container_1 = require("../container/container");
|
|
4
5
|
const baseException_1 = require("../exceptions/baseException");
|
|
5
6
|
const node_crypto_1 = require("node:crypto");
|
|
6
7
|
class JWTGeneratorException extends baseException_1.BaseException {
|
|
@@ -308,4 +309,4 @@ class Jwt {
|
|
|
308
309
|
* - Expiration handling is **strict**: `verify()` expects `exp` to exist and be in the future.
|
|
309
310
|
* - Input parsing is defensive: malformed tokens or unsupported algorithms yield `null` on verify/decode.
|
|
310
311
|
*/
|
|
311
|
-
exports.JWT =
|
|
312
|
+
exports.JWT = container_1.Container.singleton(Jwt);
|
package/dist/http/index.d.ts
CHANGED
package/dist/http/logger.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
export type LogFormat = "combined" | "common" | "dev" | "short" | "tiny";
|
|
3
|
+
export type LoggerOptions = {
|
|
4
|
+
format?: LogFormat;
|
|
5
|
+
stream?: NodeJS.WritableStream;
|
|
6
|
+
fileStream?: NodeJS.WritableStream;
|
|
7
|
+
};
|
|
2
8
|
/**
|
|
3
9
|
* Minimal HTTP request logger.
|
|
4
10
|
*
|
|
@@ -17,7 +23,9 @@ import { IncomingMessage, ServerResponse } from "node:http";
|
|
|
17
23
|
*/
|
|
18
24
|
export declare class Logger {
|
|
19
25
|
private stream;
|
|
20
|
-
|
|
26
|
+
private fileStream?;
|
|
27
|
+
private format;
|
|
28
|
+
constructor(options?: LoggerOptions);
|
|
21
29
|
/**
|
|
22
30
|
* Logs an HTTP request/response pair.
|
|
23
31
|
*
|
|
@@ -37,6 +45,7 @@ export declare class Logger {
|
|
|
37
45
|
* @param startTime - High-resolution timestamp captured at request start using `process.hrtime.bigint()`.
|
|
38
46
|
*/
|
|
39
47
|
log(req: IncomingMessage, res: ServerResponse, startTime: bigint): void;
|
|
48
|
+
private buildLogLine;
|
|
40
49
|
/**
|
|
41
50
|
* Applies ANSI color codes to an HTTP status code for terminal output.
|
|
42
51
|
*
|
package/dist/http/logger.js
CHANGED
|
@@ -19,8 +19,12 @@ exports.Logger = void 0;
|
|
|
19
19
|
*/
|
|
20
20
|
class Logger {
|
|
21
21
|
stream;
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
fileStream;
|
|
23
|
+
format;
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
this.stream = options.stream || process.stdout;
|
|
26
|
+
this.fileStream = options.fileStream;
|
|
27
|
+
this.format = options.format || "dev";
|
|
24
28
|
}
|
|
25
29
|
/**
|
|
26
30
|
* Logs an HTTP request/response pair.
|
|
@@ -41,14 +45,46 @@ class Logger {
|
|
|
41
45
|
* @param startTime - High-resolution timestamp captured at request start using `process.hrtime.bigint()`.
|
|
42
46
|
*/
|
|
43
47
|
log(req, res, startTime) {
|
|
44
|
-
const method = req.method || "-";
|
|
45
|
-
const url = req.url || "-";
|
|
46
|
-
const contentLength = res.getHeader("content-length") || "-";
|
|
47
48
|
const diff = process.hrtime.bigint() - startTime;
|
|
48
49
|
const responseTime = (Number(diff) / 1_000_000).toFixed(3);
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
this.stream.write(
|
|
50
|
+
const consoleLine = this.buildLogLine(req, res, responseTime, true);
|
|
51
|
+
const fileLine = this.buildLogLine(req, res, responseTime, false);
|
|
52
|
+
this.stream.write(consoleLine + "\n");
|
|
53
|
+
if (this.fileStream) {
|
|
54
|
+
this.fileStream.write(fileLine + "\n");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
buildLogLine(req, res, responseTime, useColor) {
|
|
58
|
+
const method = req.method || "-";
|
|
59
|
+
const url = req.url || "-";
|
|
60
|
+
const statusCode = useColor
|
|
61
|
+
? this.colorizeStatus(res.statusCode)
|
|
62
|
+
: String(res.statusCode);
|
|
63
|
+
const contentLength = res.getHeader("content-length")?.toString() || "-";
|
|
64
|
+
const remoteAddr = req.socket.remoteAddress || "-";
|
|
65
|
+
const httpVersion = req.httpVersion || "1.1";
|
|
66
|
+
const referrerHeader = req.headers.referer || req.headers.referrer;
|
|
67
|
+
const referrer = Array.isArray(referrerHeader)
|
|
68
|
+
? referrerHeader.join(", ")
|
|
69
|
+
: referrerHeader || "-";
|
|
70
|
+
const userAgentHeader = req.headers["user-agent"];
|
|
71
|
+
const userAgent = Array.isArray(userAgentHeader)
|
|
72
|
+
? userAgentHeader.join(", ")
|
|
73
|
+
: userAgentHeader || "-";
|
|
74
|
+
const date = new Date().toUTCString();
|
|
75
|
+
if (this.format === "tiny") {
|
|
76
|
+
return `${method} ${url} ${statusCode} ${contentLength} - ${responseTime} ms`;
|
|
77
|
+
}
|
|
78
|
+
if (this.format === "short") {
|
|
79
|
+
return `${remoteAddr} ${method} ${url} ${statusCode} ${contentLength} - ${responseTime} ms`;
|
|
80
|
+
}
|
|
81
|
+
if (this.format === "common") {
|
|
82
|
+
return `${remoteAddr} - - [${date}] "${method} ${url} HTTP/${httpVersion}" ${statusCode} ${contentLength}`;
|
|
83
|
+
}
|
|
84
|
+
if (this.format === "combined") {
|
|
85
|
+
return `${remoteAddr} - - [${date}] "${method} ${url} HTTP/${httpVersion}" ${statusCode} ${contentLength} "${referrer}" "${userAgent}"`;
|
|
86
|
+
}
|
|
87
|
+
return `${method} ${url} ${statusCode} ${responseTime} ms - ${contentLength}`;
|
|
52
88
|
}
|
|
53
89
|
/**
|
|
54
90
|
* Applies ANSI color codes to an HTTP status code for terminal output.
|
|
@@ -2,6 +2,7 @@ import { IncomingMessage, ServerResponse } from "node:http";
|
|
|
2
2
|
import type { HttpAdapter } from "./httpAdapter";
|
|
3
3
|
import { Response } from "./response";
|
|
4
4
|
import { Request } from "./request";
|
|
5
|
+
import { type LoggerOptions } from "./logger";
|
|
5
6
|
/**
|
|
6
7
|
* Node.js HTTP adapter.
|
|
7
8
|
*
|
|
@@ -23,7 +24,7 @@ export declare class NodeHttpAdapter implements HttpAdapter {
|
|
|
23
24
|
* @param req - Native Node.js incoming request
|
|
24
25
|
* @param res - Native Node.js server response
|
|
25
26
|
*/
|
|
26
|
-
constructor(req: IncomingMessage, res: ServerResponse);
|
|
27
|
+
constructor(req: IncomingMessage, res: ServerResponse, loggerOptions?: LoggerOptions);
|
|
27
28
|
/**
|
|
28
29
|
* Builds and returns a {@link Request} instance from
|
|
29
30
|
* the incoming Node.js request.
|
|
@@ -26,11 +26,11 @@ class NodeHttpAdapter {
|
|
|
26
26
|
* @param req - Native Node.js incoming request
|
|
27
27
|
* @param res - Native Node.js server response
|
|
28
28
|
*/
|
|
29
|
-
constructor(req, res) {
|
|
29
|
+
constructor(req, res, loggerOptions = {}) {
|
|
30
30
|
this.req = req;
|
|
31
31
|
this.res = res;
|
|
32
32
|
this.startTime = process.hrtime.bigint();
|
|
33
|
-
this.logger = new logger_1.Logger();
|
|
33
|
+
this.logger = new logger_1.Logger(loggerOptions);
|
|
34
34
|
this.contentParser = new contentParserManager_1.ContentParserManager();
|
|
35
35
|
}
|
|
36
36
|
/**
|
package/dist/http/request.js
CHANGED
|
@@ -22,11 +22,11 @@ class Request {
|
|
|
22
22
|
/** Normalized HTTP method */
|
|
23
23
|
_method;
|
|
24
24
|
/** Parsed request body payload */
|
|
25
|
-
_body =
|
|
25
|
+
_body = Object.create(null);
|
|
26
26
|
/** Query string parameters */
|
|
27
|
-
_query =
|
|
27
|
+
_query = Object.create(null);
|
|
28
28
|
/** Dynamic route parameters (path params) */
|
|
29
|
-
_params =
|
|
29
|
+
_params = Object.create(null);
|
|
30
30
|
/** Session associated with the request */
|
|
31
31
|
_session;
|
|
32
32
|
/**
|
|
@@ -35,7 +35,7 @@ class Request {
|
|
|
35
35
|
* This object can be freely used by middlewares and route handlers to store
|
|
36
36
|
* arbitrary data during the request lifecycle.
|
|
37
37
|
*/
|
|
38
|
-
state =
|
|
38
|
+
state = Object.create(null);
|
|
39
39
|
/** Multiple uploaded files metadata. */
|
|
40
40
|
files;
|
|
41
41
|
constructor(url) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { createApp } from "./app";
|
|
2
2
|
export { Request, Response } from "./http";
|
|
3
|
-
export { Middleware, RouteHandler } from "./types";
|
|
3
|
+
export type { Middleware, RouteHandler } from "./types";
|
|
4
4
|
export { RouterGroup } from "./routing";
|
|
5
|
-
export { FileSessionStorage, MemorySessionStorage } from "./sessions";
|
|
5
|
+
export { FileSessionStorage, MemorySessionStorage, DatabaseSessionStorage, type SessionDatabaseAdapter, } from "./sessions";
|
|
6
6
|
export { HttpMethods } from "./http/httpMethods";
|
|
7
7
|
export { createUploader } from "./storage/uploader";
|
|
8
8
|
export { StorageType } from "./storage/types";
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.rateLimit = exports.csrf = exports.cors = exports.sessions = exports.JWT = exports.Hasher = exports.validateRequest = exports.schema = exports.v = exports.StorageType = exports.createUploader = exports.HttpMethods = exports.MemorySessionStorage = exports.FileSessionStorage = exports.RouterGroup = exports.Response = exports.Request = exports.createApp = void 0;
|
|
17
|
+
exports.rateLimit = exports.csrf = exports.cors = exports.sessions = exports.JWT = exports.Hasher = exports.validateRequest = exports.schema = exports.v = exports.StorageType = exports.createUploader = exports.HttpMethods = exports.DatabaseSessionStorage = exports.MemorySessionStorage = exports.FileSessionStorage = exports.RouterGroup = exports.Response = exports.Request = exports.createApp = void 0;
|
|
18
18
|
var app_1 = require("./app");
|
|
19
19
|
Object.defineProperty(exports, "createApp", { enumerable: true, get: function () { return app_1.createApp; } });
|
|
20
20
|
var http_1 = require("./http");
|
|
@@ -25,6 +25,7 @@ Object.defineProperty(exports, "RouterGroup", { enumerable: true, get: function
|
|
|
25
25
|
var sessions_1 = require("./sessions");
|
|
26
26
|
Object.defineProperty(exports, "FileSessionStorage", { enumerable: true, get: function () { return sessions_1.FileSessionStorage; } });
|
|
27
27
|
Object.defineProperty(exports, "MemorySessionStorage", { enumerable: true, get: function () { return sessions_1.MemorySessionStorage; } });
|
|
28
|
+
Object.defineProperty(exports, "DatabaseSessionStorage", { enumerable: true, get: function () { return sessions_1.DatabaseSessionStorage; } });
|
|
28
29
|
var httpMethods_1 = require("./http/httpMethods");
|
|
29
30
|
Object.defineProperty(exports, "HttpMethods", { enumerable: true, get: function () { return httpMethods_1.HttpMethods; } });
|
|
30
31
|
var uploader_1 = require("./storage/uploader");
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type SessionData, type SessionStorage } from "./sessionStorage";
|
|
2
|
+
/**
|
|
3
|
+
* Persistence contract for DB-backed sessions.
|
|
4
|
+
*
|
|
5
|
+
* Implement this adapter with your DB client/ORM (mysql, mariaDB, sqlite,
|
|
6
|
+
* postgresql, mssql, oracle, etc.).
|
|
7
|
+
*/
|
|
8
|
+
export interface SessionDatabaseAdapter {
|
|
9
|
+
findById(id: string): Promise<SessionData | null>;
|
|
10
|
+
upsert(id: string, payload: SessionData): Promise<void>;
|
|
11
|
+
deleteById(id: string): Promise<void>;
|
|
12
|
+
deleteExpired(now: number): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Database-backed session storage.
|
|
16
|
+
*
|
|
17
|
+
* The storage is DB-engine agnostic. Consumers provide a
|
|
18
|
+
* {@link SessionDatabaseAdapter} implementation that translates these methods
|
|
19
|
+
* to the concrete SQL/driver API used by the project.
|
|
20
|
+
*/
|
|
21
|
+
export declare class DatabaseSessionStorage implements SessionStorage {
|
|
22
|
+
private readonly ttlSeconds;
|
|
23
|
+
private static adapter;
|
|
24
|
+
private sessionId;
|
|
25
|
+
private data;
|
|
26
|
+
private expiresAt;
|
|
27
|
+
constructor(ttlSeconds?: number);
|
|
28
|
+
static configure(adapter: SessionDatabaseAdapter): void;
|
|
29
|
+
static clearAdapter(): void;
|
|
30
|
+
static cleanExpiredSessions(now?: number): Promise<void>;
|
|
31
|
+
load(id: string): Promise<void>;
|
|
32
|
+
start(): Promise<void>;
|
|
33
|
+
id(): string | null;
|
|
34
|
+
get<T = unknown>(key: string, defaultValue?: T): T | undefined;
|
|
35
|
+
set(key: string, value: unknown): Promise<void>;
|
|
36
|
+
has(key: string): boolean;
|
|
37
|
+
remove(key: string): Promise<void>;
|
|
38
|
+
all(): Record<string, unknown>;
|
|
39
|
+
clear(): Promise<void>;
|
|
40
|
+
save(): Promise<void>;
|
|
41
|
+
touch(): Promise<void>;
|
|
42
|
+
reload(): Promise<void>;
|
|
43
|
+
destroy(): Promise<void>;
|
|
44
|
+
regenerate(): Promise<void>;
|
|
45
|
+
private static getAdapter;
|
|
46
|
+
private persist;
|
|
47
|
+
private readOrFail;
|
|
48
|
+
private safeDelete;
|
|
49
|
+
private nextExpiry;
|
|
50
|
+
private assertValidId;
|
|
51
|
+
private generateId;
|
|
52
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DatabaseSessionStorage = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const httpExceptions_1 = require("../exceptions/httpExceptions");
|
|
6
|
+
const sessionStorage_1 = require("./sessionStorage");
|
|
7
|
+
/**
|
|
8
|
+
* Database-backed session storage.
|
|
9
|
+
*
|
|
10
|
+
* The storage is DB-engine agnostic. Consumers provide a
|
|
11
|
+
* {@link SessionDatabaseAdapter} implementation that translates these methods
|
|
12
|
+
* to the concrete SQL/driver API used by the project.
|
|
13
|
+
*/
|
|
14
|
+
class DatabaseSessionStorage {
|
|
15
|
+
ttlSeconds;
|
|
16
|
+
static adapter = null;
|
|
17
|
+
sessionId = null;
|
|
18
|
+
data = {};
|
|
19
|
+
expiresAt = 0;
|
|
20
|
+
constructor(ttlSeconds = 86_400) {
|
|
21
|
+
this.ttlSeconds = ttlSeconds;
|
|
22
|
+
}
|
|
23
|
+
static configure(adapter) {
|
|
24
|
+
DatabaseSessionStorage.adapter = adapter;
|
|
25
|
+
}
|
|
26
|
+
static clearAdapter() {
|
|
27
|
+
DatabaseSessionStorage.adapter = null;
|
|
28
|
+
}
|
|
29
|
+
static async cleanExpiredSessions(now = Date.now()) {
|
|
30
|
+
await this.getAdapter().deleteExpired(now);
|
|
31
|
+
}
|
|
32
|
+
async load(id) {
|
|
33
|
+
this.assertValidId(id);
|
|
34
|
+
const payload = await this.readOrFail(id);
|
|
35
|
+
if (payload.expiresAt <= Date.now()) {
|
|
36
|
+
await this.safeDelete(id);
|
|
37
|
+
throw new httpExceptions_1.UnauthorizedError("Session expired");
|
|
38
|
+
}
|
|
39
|
+
this.sessionId = id;
|
|
40
|
+
this.data = { ...payload.data };
|
|
41
|
+
this.expiresAt = payload.expiresAt;
|
|
42
|
+
await this.touch();
|
|
43
|
+
}
|
|
44
|
+
async start() {
|
|
45
|
+
if (this.sessionId)
|
|
46
|
+
return;
|
|
47
|
+
this.sessionId = this.generateId();
|
|
48
|
+
this.data = {};
|
|
49
|
+
this.expiresAt = this.nextExpiry();
|
|
50
|
+
await this.persist();
|
|
51
|
+
}
|
|
52
|
+
id() {
|
|
53
|
+
return this.sessionId;
|
|
54
|
+
}
|
|
55
|
+
get(key, defaultValue) {
|
|
56
|
+
return this.data[key] ?? defaultValue;
|
|
57
|
+
}
|
|
58
|
+
async set(key, value) {
|
|
59
|
+
if (!this.sessionId)
|
|
60
|
+
await this.start();
|
|
61
|
+
this.data[key] = value;
|
|
62
|
+
await this.save();
|
|
63
|
+
}
|
|
64
|
+
has(key) {
|
|
65
|
+
return key in this.data;
|
|
66
|
+
}
|
|
67
|
+
async remove(key) {
|
|
68
|
+
if (!this.sessionId)
|
|
69
|
+
return;
|
|
70
|
+
delete this.data[key];
|
|
71
|
+
await this.save();
|
|
72
|
+
}
|
|
73
|
+
all() {
|
|
74
|
+
return { ...this.data };
|
|
75
|
+
}
|
|
76
|
+
async clear() {
|
|
77
|
+
if (!this.sessionId)
|
|
78
|
+
return;
|
|
79
|
+
this.data = {};
|
|
80
|
+
await this.save();
|
|
81
|
+
}
|
|
82
|
+
async save() {
|
|
83
|
+
if (!this.sessionId)
|
|
84
|
+
return;
|
|
85
|
+
this.expiresAt = this.nextExpiry();
|
|
86
|
+
await this.persist();
|
|
87
|
+
}
|
|
88
|
+
async touch() {
|
|
89
|
+
if (!this.sessionId)
|
|
90
|
+
return;
|
|
91
|
+
this.expiresAt = this.nextExpiry();
|
|
92
|
+
await this.persist();
|
|
93
|
+
}
|
|
94
|
+
async reload() {
|
|
95
|
+
if (!this.sessionId)
|
|
96
|
+
return;
|
|
97
|
+
await this.load(this.sessionId);
|
|
98
|
+
}
|
|
99
|
+
async destroy() {
|
|
100
|
+
if (this.sessionId)
|
|
101
|
+
await this.safeDelete(this.sessionId);
|
|
102
|
+
this.sessionId = null;
|
|
103
|
+
this.data = {};
|
|
104
|
+
this.expiresAt = 0;
|
|
105
|
+
}
|
|
106
|
+
async regenerate() {
|
|
107
|
+
const previous = this.all();
|
|
108
|
+
await this.destroy();
|
|
109
|
+
await this.start();
|
|
110
|
+
this.data = previous;
|
|
111
|
+
await this.save();
|
|
112
|
+
}
|
|
113
|
+
static getAdapter() {
|
|
114
|
+
if (!DatabaseSessionStorage.adapter) {
|
|
115
|
+
throw new httpExceptions_1.InternalServerError("DatabaseSessionStorage adapter not configured");
|
|
116
|
+
}
|
|
117
|
+
return DatabaseSessionStorage.adapter;
|
|
118
|
+
}
|
|
119
|
+
async persist() {
|
|
120
|
+
if (!this.sessionId)
|
|
121
|
+
return;
|
|
122
|
+
const payload = {
|
|
123
|
+
data: { ...this.data },
|
|
124
|
+
expiresAt: this.expiresAt,
|
|
125
|
+
};
|
|
126
|
+
try {
|
|
127
|
+
await DatabaseSessionStorage.getAdapter().upsert(this.sessionId, payload);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw new httpExceptions_1.InternalServerError("Failed to save session data");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async readOrFail(id) {
|
|
134
|
+
try {
|
|
135
|
+
const payload = await DatabaseSessionStorage.getAdapter().findById(id);
|
|
136
|
+
if (!payload || typeof payload.expiresAt !== "number" || !payload.data) {
|
|
137
|
+
throw new httpExceptions_1.UnauthorizedError("Invalid session");
|
|
138
|
+
}
|
|
139
|
+
return payload;
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error instanceof httpExceptions_1.UnauthorizedError)
|
|
143
|
+
throw error;
|
|
144
|
+
throw new httpExceptions_1.InternalServerError("Failed to load session data");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async safeDelete(id) {
|
|
148
|
+
try {
|
|
149
|
+
await DatabaseSessionStorage.getAdapter().deleteById(id);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// best-effort cleanup
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
nextExpiry() {
|
|
156
|
+
return Date.now() + this.ttlSeconds * 1000;
|
|
157
|
+
}
|
|
158
|
+
assertValidId(id) {
|
|
159
|
+
if (!sessionStorage_1.SESSION_ID_REGEX.test(id))
|
|
160
|
+
throw new httpExceptions_1.UnauthorizedError("Invalid session");
|
|
161
|
+
}
|
|
162
|
+
generateId() {
|
|
163
|
+
return (0, node_crypto_1.randomBytes)(32).toString("hex");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
exports.DatabaseSessionStorage = DatabaseSessionStorage;
|
|
@@ -29,7 +29,7 @@ class FileSessionStorage {
|
|
|
29
29
|
/** Currently loaded/active session id, or null when no session is active. */
|
|
30
30
|
sessionId = null;
|
|
31
31
|
/** In-memory session key/value data. */
|
|
32
|
-
data =
|
|
32
|
+
data = Object.create(null);
|
|
33
33
|
/** Expiration timestamp in milliseconds since epoch. */
|
|
34
34
|
expiresAt = 0;
|
|
35
35
|
/**
|
package/dist/sessions/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { MemorySessionStorage } from "./memorySessionStorage";
|
|
2
2
|
export { FileSessionStorage } from "./fileSessionStorage";
|
|
3
|
+
export { DatabaseSessionStorage, SessionDatabaseAdapter, } from "./databaseSessionStorage";
|
|
3
4
|
export { Session } from "./session";
|
|
4
5
|
export { SessionStorage } from "./sessionStorage";
|
|
5
6
|
export { parseCookies, serializeCookie } from "./cookies";
|
package/dist/sessions/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.serializeCookie = exports.parseCookies = exports.Session = exports.FileSessionStorage = exports.MemorySessionStorage = void 0;
|
|
3
|
+
exports.serializeCookie = exports.parseCookies = exports.Session = exports.DatabaseSessionStorage = exports.FileSessionStorage = exports.MemorySessionStorage = void 0;
|
|
4
4
|
var memorySessionStorage_1 = require("./memorySessionStorage");
|
|
5
5
|
Object.defineProperty(exports, "MemorySessionStorage", { enumerable: true, get: function () { return memorySessionStorage_1.MemorySessionStorage; } });
|
|
6
6
|
var fileSessionStorage_1 = require("./fileSessionStorage");
|
|
7
7
|
Object.defineProperty(exports, "FileSessionStorage", { enumerable: true, get: function () { return fileSessionStorage_1.FileSessionStorage; } });
|
|
8
|
+
var databaseSessionStorage_1 = require("./databaseSessionStorage");
|
|
9
|
+
Object.defineProperty(exports, "DatabaseSessionStorage", { enumerable: true, get: function () { return databaseSessionStorage_1.DatabaseSessionStorage; } });
|
|
8
10
|
var session_1 = require("./session");
|
|
9
11
|
Object.defineProperty(exports, "Session", { enumerable: true, get: function () { return session_1.Session; } });
|
|
10
12
|
var cookies_1 = require("./cookies");
|
|
@@ -33,7 +33,7 @@ class MemorySessionStorage {
|
|
|
33
33
|
/** Currently loaded/active session id, or null when no session is active. */
|
|
34
34
|
sessionId = null;
|
|
35
35
|
/** In-memory session key/value data for the current session. */
|
|
36
|
-
data =
|
|
36
|
+
data = Object.create(null);
|
|
37
37
|
/**
|
|
38
38
|
* @param ttlSeconds - Session TTL in seconds. Defaults to 86,400 seconds (24 hours).
|
|
39
39
|
*/
|
|
@@ -11,10 +11,4 @@ export interface BigIntRuleOptions extends RuleOptions {
|
|
|
11
11
|
export declare class BigIntRule extends BaseValidationRule<bigint> {
|
|
12
12
|
constructor();
|
|
13
13
|
validate(context: ValidationContext, options?: BigIntRuleOptions): ValidationError | null;
|
|
14
|
-
gt(limit: bigint, message?: string): this;
|
|
15
|
-
gte(limit: bigint, message?: string): this;
|
|
16
|
-
lt(limit: bigint, message?: string): this;
|
|
17
|
-
lte(limit: bigint, message?: string): this;
|
|
18
|
-
positive(message?: string): this;
|
|
19
|
-
negative(message?: string): this;
|
|
20
14
|
}
|
|
@@ -25,29 +25,5 @@ class BigIntRule extends validationRule_1.BaseValidationRule {
|
|
|
25
25
|
return this.createError(field, `${field} must be less than or equal to ${options.lte}`, value);
|
|
26
26
|
return null;
|
|
27
27
|
}
|
|
28
|
-
gt(limit, message) {
|
|
29
|
-
this.rules.push({ rule: this, options: { gt: limit, message } });
|
|
30
|
-
return this;
|
|
31
|
-
}
|
|
32
|
-
gte(limit, message) {
|
|
33
|
-
this.rules.push({ rule: this, options: { gte: limit, message } });
|
|
34
|
-
return this;
|
|
35
|
-
}
|
|
36
|
-
lt(limit, message) {
|
|
37
|
-
this.rules.push({ rule: this, options: { lt: limit, message } });
|
|
38
|
-
return this;
|
|
39
|
-
}
|
|
40
|
-
lte(limit, message) {
|
|
41
|
-
this.rules.push({ rule: this, options: { lte: limit, message } });
|
|
42
|
-
return this;
|
|
43
|
-
}
|
|
44
|
-
positive(message) {
|
|
45
|
-
this.rules.push({ rule: this, options: { positive: true, message } });
|
|
46
|
-
return this;
|
|
47
|
-
}
|
|
48
|
-
negative(message) {
|
|
49
|
-
this.rules.push({ rule: this, options: { negative: true, message } });
|
|
50
|
-
return this;
|
|
51
|
-
}
|
|
52
28
|
}
|
|
53
29
|
exports.BigIntRule = BigIntRule;
|