@dwtechs/toker-express 0.7.1 โ†’ 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,16 +2,16 @@
2
2
  [![License: MIT](https://img.shields.io/npm/l/@dwtechs/toker-express.svg?color=brightgreen)](https://opensource.org/licenses/MIT)
3
3
  [![npm version](https://badge.fury.io/js/%40dwtechs%2Ftoker-express.svg)](https://www.npmjs.com/package/@dwtechs/toker-express)
4
4
  [![last version release date](https://img.shields.io/github/release-date/DWTechs/Toker-express.js)](https://www.npmjs.com/package/@dwtechs/toker-express)
5
- ![Jest:coverage](https://img.shields.io/badge/Jest:coverage-94%25-brightgreen.svg)
5
+ ![Jest:coverage](https://img.shields.io/badge/Jest:coverage-92%25-brightgreen.svg)
6
6
 
7
7
 
8
8
  - [Synopsis](#synopsis)
9
- - [Support](#support)
10
9
  - [Installation](#installation)
11
10
  - [Usage](#usage)
12
11
  - [Environment variables](#environment-variables)
13
12
  - [API Reference](#api-reference)
14
13
  - [Logs](#logs)
14
+ - [Support](#support)
15
15
  - [Contributors](#contributors)
16
16
  - [Stack](#stack)
17
17
 
@@ -23,17 +23,10 @@ It includes @dwtechs/toker library and adds Express middlewares to be used in a
23
23
 
24
24
  - ๐Ÿชถ Very lightweight
25
25
  - ๐Ÿงช Thoroughly tested
26
- - ๐Ÿšš Shipped as EcmaScrypt Express module
26
+ - ๐Ÿšš Shipped as ES2022 ECMAScript module
27
27
  - ๐Ÿ“ Written in Typescript
28
28
 
29
29
 
30
- ## Support
31
-
32
- - node: 22
33
-
34
- This is the oldest targeted versions.
35
-
36
-
37
30
  ## Installation
38
31
 
39
32
  ```bash
@@ -110,7 +103,7 @@ You can intialise the library using the following environment variables:
110
103
  These environment variables will update the default values of the lib at start up.
111
104
  So you do not need to init the library in the code.
112
105
 
113
- Note that **TOKEN_SECRET** is mandatory.
106
+ Note that **TOKEN_SECRET** is mandatory and must be at least 32 characters long.
114
107
 
115
108
  Default values :
116
109
 
@@ -187,13 +180,13 @@ function refreshTokens(req: Request, res: Response, next: NextFunction): void {}
187
180
  *
188
181
  * This middleware extracts the JWT token from the Authorization header (Bearer scheme)
189
182
  * and stores it in `res.locals.tokens.access` for use by subsequent middleware (typically decodeAccess).
190
- * It only processes requests that have `res.locals.route.isProtected` set to true.
183
+ * It only processes requests that have `res.locals.route.isProtected` or `res.locals.route.protected` set to true.
191
184
  * For non-protected routes, it simply passes control to the next middleware.
192
185
  *
193
186
  * @param {Request} req - The Express request object. Should contain:
194
187
  * - `req.headers.authorization`: Authorization header with Bearer token format
195
188
  * @param {Response} res - The Express response object. Should contain:
196
- * - `res.locals.route.isProtected`: Boolean flag to determine if route requires JWT protection
189
+ * - `res.locals.route.isProtected` or `res.locals.route.protected`: Boolean flag to determine if route requires JWT protection
197
190
  * Parsed token will be added to `res.locals.tokens.access`
198
191
  * @param {NextFunction} next - The next middleware function to be called
199
192
  *
@@ -215,8 +208,8 @@ function parseBearer(req: Request, res: Response, next: NextFunction): void {}
215
208
  * This middleware validates the JWT access token from `res.locals.tokens.access`,
216
209
  * verifies its signature and structure, validates the issuer (iss) claim,
217
210
  * and stores the decoded token in `res.locals.tokens.decodedAccess` for use by
218
- * subsequent middleware. It only processes requests that have `res.locals.route.isProtected`
219
- * set to true. For non-protected routes, it simply passes control to the next middleware.
211
+ * subsequent middleware. It only processes requests that have `res.locals.route.isProtected`
212
+ * or `res.locals.route.protected` set to true. For non-protected routes, it simply passes control to the next middleware.
220
213
  *
221
214
  * Note: By default, this middleware checks token expiration (exp claim) and will reject
222
215
  * expired tokens. For token refresh flows where you need to identify the user even after
@@ -225,7 +218,7 @@ function parseBearer(req: Request, res: Response, next: NextFunction): void {}
225
218
  *
226
219
  * @param {Request} _req - The Express request object (unused)
227
220
  * @param {Response} res - The Express response object. Should contain:
228
- * - `res.locals.route.isProtected`: Boolean flag to determine if route requires JWT protection
221
+ * - `res.locals.route.isProtected` or `res.locals.route.protected`: Boolean flag to determine if route requires JWT protection
229
222
  * - `res.locals.tokens.access`: The JWT token to decode (from parseBearer middleware)
230
223
  * - `res.locals.tokens.ignoreExpiration`: Optional boolean to skip expiration checking (default: false)
231
224
  * Decoded token will be added to `res.locals.tokens.decodedAccess`
@@ -331,16 +324,16 @@ req.body.rows[0].refreshToken = rt;
331
324
 
332
325
  ### JWT Decoding
333
326
 
334
- #### Route Protection with isProtected
327
+ #### Route Protection with isProtected / protected
335
328
 
336
- The `parseBearer()` and `decodeAccess()` middlewares only process requests when `res.locals.route.isProtected` is set to `true`. This allows you to selectively protect routes that require authentication.
329
+ The `parseBearer()` and `decodeAccess()` middlewares only process requests when `res.locals.route.isProtected` or `res.locals.route.protected` is set to `true`. This allows you to selectively protect routes that require authentication.
337
330
 
338
- You should set this flag in a middleware before calling these functions:
331
+ You should set one of these flags in a middleware before calling these functions:
339
332
 
340
333
  ```Javascript
341
334
  // Example middleware to mark route as protected
342
335
  function protectRoute(req, res, next) {
343
- res.locals.route = { isProtected: true };
336
+ res.locals.route = { isProtected: true }; // or { protected: true }
344
337
  next();
345
338
  }
346
339
 
@@ -348,7 +341,7 @@ function protectRoute(req, res, next) {
348
341
  router.get('/protected-route', protectRoute, tk.parseBearer, tk.decodeAccess, yourHandler);
349
342
  ```
350
343
 
351
- If `res.locals.route.isProtected` is `false`, `undefined`, or `null`, these middlewares will simply call `next()` without processing the token, allowing the request to continue to the next middleware.
344
+ If both `res.locals.route.isProtected` and `res.locals.route.protected` are `false`, `undefined`, or `null`, these middlewares will simply call `next()` without processing the token, allowing the request to continue to the next middleware.
352
345
 
353
346
  #### Access Token Processing
354
347
 
@@ -385,8 +378,6 @@ The decoded token is then stored in `res.locals.tokens.decodedAccess`:
385
378
  res.locals.tokens.decodedAccess = decodedToken;
386
379
  ```
387
380
 
388
- **Important:** This middleware **ignores token expiration** by design. This allows expired access tokens to be decoded, which is useful for token refresh flows where you need to identify the user even after their access token has expired.
389
-
390
381
  **Note:** You should use both middlewares in sequence for full access token processing, or you can use just `parseBearer()` if you only need to extract the token without decoding it.
391
382
 
392
383
  #### Refresh Token Decoding
@@ -410,13 +401,19 @@ res.locals.tokens.decodedRefresh = decodedToken;
410
401
 
411
402
  **Token-express.js** uses **[@dwtechs/Winstan](https://www.npmjs.com/package/@dwtechs/winstan)** library for logging.
412
403
  All logs are in debug mode. Meaning they should not appear in production mode.
404
+ Log messages are passed as lazy functions (`() => string`) so string interpolation and serialization are skipped entirely when debug logging is disabled.
405
+
406
+ ## Support
407
+
408
+ | Environment | Version |
409
+ | :---------- | :-----: |
410
+ | Node.js | >= 22 |
413
411
 
414
412
  ## Contributors
415
413
 
416
414
  **Token-express.js** is still in development and we would be glad to get all the help you can provide.
417
415
  To contribute please read **[contributor.md](https://github.com/DWTechs/Token-express.js/blob/main/contributor.md)** for detailed installation guide.
418
416
 
419
-
420
417
  ## Stack
421
418
 
422
419
  | Purpose | Choice | Motivation |
@@ -34,15 +34,18 @@ if (!TOKEN_SECRET)
34
34
  throw new Error(`${LOGS_PREFIX}Missing TOKEN_SECRET environment variable`);
35
35
  if (!isString(TOKEN_SECRET, "!0"))
36
36
  throw new Error(`${LOGS_PREFIX}Invalid TOKEN_SECRET environment variable`);
37
+ if (TOKEN_SECRET.length < 32)
38
+ throw new Error(`${LOGS_PREFIX}TOKEN_SECRET must be at least 32 characters`);
37
39
  const secrets = [TOKEN_SECRET];
38
40
  const accessDuration = isNumber(ACCESS_TOKEN_DURATION, false) ? Number(ACCESS_TOKEN_DURATION) : 600;
39
41
  const refreshDuration = isNumber(REFRESH_TOKEN_DURATION, false) ? Number(REFRESH_TOKEN_DURATION) : 86400;
40
42
  function createTokens(req, res, next) {
41
- var _a, _b;
42
- const iss = (_b = (_a = res.locals) === null || _a === void 0 ? void 0 : _a.user) === null || _b === void 0 ? void 0 : _b.id;
43
- if (!isValidInteger(iss, 1, 999999999, false))
44
- return next({ statusCode: 400, message: `${LOGS_PREFIX}Missing iss` });
45
- log.debug(`${LOGS_PREFIX}Create tokens for user ${iss}`);
43
+ const iss = res.locals?.user?.id;
44
+ if (!isValidInteger(iss, 1, 999999999, false)) {
45
+ next({ statusCode: 400, message: `${LOGS_PREFIX}Missing iss` });
46
+ return;
47
+ }
48
+ log.debug(() => `${LOGS_PREFIX}Create tokens for user ${iss}`);
46
49
  let at;
47
50
  let rt;
48
51
  try {
@@ -50,19 +53,21 @@ function createTokens(req, res, next) {
50
53
  rt = sign(iss, refreshDuration, "refresh", secrets);
51
54
  }
52
55
  catch (err) {
53
- return next(err);
56
+ next(err);
57
+ return;
54
58
  }
55
- log.debug(`refreshToken='${rt}', accessToken='${at}'`);
59
+ log.debug(() => `${LOGS_PREFIX}Tokens created for user ${iss}`);
56
60
  req.body.rows[0].accessToken = at;
57
61
  req.body.rows[0].refreshToken = rt;
58
62
  next();
59
63
  }
60
64
  function refreshTokens(req, res, next) {
61
- var _a, _b, _c;
62
- const iss = (_c = (_b = (_a = res.locals) === null || _a === void 0 ? void 0 : _a.tokens) === null || _b === void 0 ? void 0 : _b.decodedAccess) === null || _c === void 0 ? void 0 : _c.iss;
63
- if (!isValidInteger(iss, 1, 999999999, false))
64
- return next({ statusCode: 400, message: `${LOGS_PREFIX}Missing iss` });
65
- log.debug(`${LOGS_PREFIX}Create tokens for user ${iss}`);
65
+ const iss = res.locals?.tokens?.decodedAccess?.iss;
66
+ if (!isValidInteger(iss, 1, 999999999, false)) {
67
+ next({ statusCode: 400, message: `${LOGS_PREFIX}Missing iss` });
68
+ return;
69
+ }
70
+ log.debug(() => `${LOGS_PREFIX}Create tokens for user ${iss}`);
66
71
  let at;
67
72
  let rt;
68
73
  try {
@@ -70,65 +75,81 @@ function refreshTokens(req, res, next) {
70
75
  rt = sign(iss, refreshDuration, "refresh", secrets);
71
76
  }
72
77
  catch (err) {
73
- return next(err);
78
+ next(err);
79
+ return;
74
80
  }
75
- log.debug(`refreshToken='${rt}', accessToken='${at}'`);
81
+ log.debug(() => `${LOGS_PREFIX}Tokens refreshed for user ${iss}`);
76
82
  req.body.rows[0].accessToken = at;
77
83
  req.body.rows[0].refreshToken = rt;
78
84
  next();
79
85
  }
80
86
  function parseBearer(req, res, next) {
81
- var _a, _b;
82
- if (!((_b = (_a = res.locals) === null || _a === void 0 ? void 0 : _a.route) === null || _b === void 0 ? void 0 : _b.isProtected))
83
- return next();
84
- log.debug(`${LOGS_PREFIX}parse bearer to get access token`);
87
+ if (!res.locals?.route?.isProtected && !res.locals?.route?.protected) {
88
+ next();
89
+ return;
90
+ }
91
+ log.debug(() => `${LOGS_PREFIX}parse bearer to get access token`);
85
92
  try {
86
- res.locals.tokens = Object.assign(Object.assign({}, res.locals.tokens), { access: parseBearer$1(req.headers.authorization) });
93
+ res.locals.tokens ??= {};
94
+ res.locals.tokens.access = parseBearer$1(req.headers.authorization);
87
95
  }
88
96
  catch (e) {
89
- return next(e);
97
+ next(e);
98
+ return;
90
99
  }
91
100
  next();
92
101
  }
93
102
  function decodeAccess(_req, res, next) {
94
- var _a, _b, _c, _d, _e, _f, _g;
95
- log.debug(`${LOGS_PREFIX}decode access token`);
96
- if (!((_b = (_a = res.locals) === null || _a === void 0 ? void 0 : _a.route) === null || _b === void 0 ? void 0 : _b.isProtected))
97
- return next();
98
- const t = (_d = (_c = res.locals) === null || _c === void 0 ? void 0 : _c.tokens) === null || _d === void 0 ? void 0 : _d.access;
99
- const ignoreExpiration = (_g = (_f = (_e = res.locals) === null || _e === void 0 ? void 0 : _e.tokens) === null || _f === void 0 ? void 0 : _f.ignoreExpiration) !== null && _g !== void 0 ? _g : false;
100
- if (!isJWT(t))
101
- return next({ statusCode: 401, message: `${LOGS_PREFIX}Invalid access token` });
103
+ log.debug(() => `${LOGS_PREFIX}decode access token`);
104
+ if (!res.locals?.route?.isProtected && !res.locals?.route?.protected) {
105
+ next();
106
+ return;
107
+ }
108
+ const t = res.locals?.tokens?.access;
109
+ const ignoreExpiration = res.locals?.tokens?.ignoreExpiration ?? false;
110
+ if (!isJWT(t)) {
111
+ next({ statusCode: 401, message: `${LOGS_PREFIX}Invalid access token` });
112
+ return;
113
+ }
102
114
  let dt = null;
103
115
  try {
104
116
  dt = verify(t, secrets, ignoreExpiration);
105
117
  }
106
118
  catch (e) {
107
- return next(e);
119
+ next(e);
120
+ return;
121
+ }
122
+ if (!isValidInteger(dt.iss, 1, 999999999, false)) {
123
+ next({ statusCode: 400, message: `${LOGS_PREFIX}Missing iss` });
124
+ return;
108
125
  }
109
- if (!isValidInteger(dt.iss, 1, 999999999, false))
110
- return next({ statusCode: 400, message: `${LOGS_PREFIX}Missing iss` });
111
- log.debug(`${LOGS_PREFIX}Decoded access token : ${JSON.stringify(dt)}`);
112
- res.locals.tokens = Object.assign(Object.assign({}, res.locals.tokens), { decodedAccess: dt });
126
+ log.debug(() => `${LOGS_PREFIX}Access token decoded for user ${dt.iss}`);
127
+ res.locals.tokens ??= {};
128
+ res.locals.tokens.decodedAccess = dt;
113
129
  next();
114
130
  }
115
131
  function decodeRefresh(req, res, next) {
116
- var _a;
117
- const t = (_a = req.body) === null || _a === void 0 ? void 0 : _a.refreshToken;
118
- log.debug(`${LOGS_PREFIX}decodeRefresh(token=${t})`);
119
- if (!isJWT(t))
120
- return next({ statusCode: 401, message: `${LOGS_PREFIX}Invalid refresh token` });
132
+ const t = req.body?.refreshToken;
133
+ log.debug(() => `${LOGS_PREFIX}Decoding refresh token`);
134
+ if (!isJWT(t)) {
135
+ next({ statusCode: 401, message: `${LOGS_PREFIX}Invalid refresh token` });
136
+ return;
137
+ }
121
138
  let dt = null;
122
139
  try {
123
140
  dt = verify(t, secrets, false);
124
141
  }
125
142
  catch (e) {
126
- return next(e);
143
+ next(e);
144
+ return;
145
+ }
146
+ if (!isValidInteger(dt.iss, 1, 999999999, false)) {
147
+ next({ statusCode: 400, message: `${LOGS_PREFIX}Missing iss` });
148
+ return;
127
149
  }
128
- if (!isValidInteger(dt.iss, 1, 999999999, false))
129
- return next({ statusCode: 400, message: `${LOGS_PREFIX}Missing iss` });
130
- log.debug(`${LOGS_PREFIX}Decoded refresh token : ${JSON.stringify(dt)}`);
131
- res.locals.tokens = Object.assign(Object.assign({}, res.locals.tokens), { decodedRefresh: dt });
150
+ log.debug(() => `${LOGS_PREFIX}Refresh token decoded for user ${dt.iss}`);
151
+ res.locals.tokens ??= {};
152
+ res.locals.tokens.decodedRefresh = dt;
132
153
  next();
133
154
  }
134
155
 
package/package.json CHANGED
@@ -1,35 +1,41 @@
1
1
  {
2
2
  "name": "@dwtechs/toker-express",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
+ "type": "module",
4
5
  "description": "Open source JWT management library for Express.js to refresh and decode tokens safely.",
5
6
  "keywords": [
6
7
  "JWT",
7
8
  "Express"
8
9
  ],
9
10
  "homepage": "https://github.com/DWTechs/Toker-express.js",
10
- "main": "dist/toker-express",
11
- "types": "dist/toker-express",
11
+ "main": "dist/toker-express.js",
12
+ "types": "dist/toker-express.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./dist/toker-express.js",
16
+ "types": "./dist/toker-express.d.ts"
17
+ }
18
+ },
12
19
  "repository": {
13
20
  "type": "git",
14
21
  "url": "https://github.com/DWTechs/Toker-express.js"
15
22
  },
16
23
  "bugs": {
17
- "url": "https://github.com/DWTechs/Toker-express.js/issues",
18
- "email": ""
24
+ "url": "https://github.com/DWTechs/Toker-express.js/issues"
19
25
  },
20
26
  "license": "MIT",
27
+ "engines": {
28
+ "node": ">=22"
29
+ },
21
30
  "author": {
22
31
  "name": "Ludovic Cluber",
23
32
  "email": "http://www.lcluber.com/contact",
24
33
  "url": "http://www.lcluber.com"
25
34
  },
26
- "contributors": [],
27
35
  "scripts": {
28
- "start": "",
29
36
  "prebuild": "npm install",
30
37
  "build": "node ./scripts/clear && tsc && npm run rollup && node ./scripts/copy && npm run test",
31
38
  "rollup:mjs": "rollup --config rollup.config.mjs",
32
- "rollup:cjs": "rollup --config rollup.config.cjs.mjs",
33
39
  "rollup": "npm run rollup:mjs",
34
40
  "test": "jest --coverage"
35
41
  },
@@ -38,17 +44,15 @@
38
44
  ],
39
45
  "dependencies": {
40
46
  "@dwtechs/checkard": "3.6.0",
41
- "@dwtechs/toker": "0.1.2",
42
- "@dwtechs/winstan": "0.5.0"
47
+ "@dwtechs/toker": "0.2.1",
48
+ "@dwtechs/winstan": "0.7.0"
43
49
  },
44
50
  "devDependencies": {
45
51
  "@babel/preset-env": "7.26.0",
46
- "@rollup/plugin-node-resolve": "15.3.0",
47
- "@types/express": "5.0.3",
52
+ "@types/express": "5.0.6",
48
53
  "babel-jest": "29.7.0",
49
- "core-js": "3.38.1",
50
54
  "jest": "29.7.0",
51
55
  "rollup": "4.24.0",
52
- "typescript": "5.9.2"
56
+ "typescript": "6.0.3"
53
57
  }
54
58
  }