@igxjs/node-components 1.0.16 → 1.0.17

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.
@@ -161,12 +161,13 @@ export const httpHelper = {
161
161
  * @returns {string} Formatted string
162
162
  */
163
163
  format (str, ...args) {
164
+ let result = str;
164
165
  const matched = str.match(/{\d}/ig);
165
166
  matched.forEach((element, index) => {
166
167
  if(args.length > index)
167
- str = str.replace(element, args[index]);
168
+ result = result.replace(element, args[index]);
168
169
  });
169
- return str;
170
+ return result;
170
171
  },
171
172
 
172
173
  /**
@@ -209,4 +210,4 @@ export const httpHelper = {
209
210
  */
210
211
  export const httpError = (code, message, error, data) => {
211
212
  return new CustomError(code, message, error, data);
212
- };
213
+ };
package/components/jwt.js CHANGED
@@ -9,31 +9,31 @@ import { jwtDecrypt, EncryptJWT } from 'jose';
9
9
  export class JwtManager {
10
10
  /** @type {string} JWE algorithm */
11
11
  algorithm;
12
-
12
+
13
13
  /** @type {string} Encryption method */
14
14
  encryption;
15
-
15
+
16
16
  /** @type {number} Token expiration time */
17
17
  expirationTime;
18
-
18
+
19
19
  /** @type {number} Clock tolerance in seconds */
20
20
  clockTolerance;
21
-
21
+
22
22
  /** @type {string} Hash algorithm for secret derivation */
23
23
  secretHashAlgorithm;
24
-
24
+
25
25
  /** @type {string|null} Optional JWT issuer claim */
26
26
  issuer;
27
-
27
+
28
28
  /** @type {string|null} Optional JWT audience claim */
29
29
  audience;
30
-
30
+
31
31
  /** @type {string|null} Optional JWT subject claim */
32
32
  subject;
33
33
  /**
34
34
  * Create a new JwtManager instance with configurable defaults
35
35
  * Constructor options use UPPERCASE naming convention with JWT_ prefix (e.g., JWT_ALGORITHM).
36
- *
36
+ *
37
37
  * @typedef {Object} JwtManagerOptions JwtManager configuration options
38
38
  * @property {string} [JWT_ALGORITHM='dir'] JWE algorithm (default: 'dir')
39
39
  * @property {string} [JWT_ENCRYPTION='A256GCM'] Encryption method (default: 'A256GCM')
@@ -59,7 +59,7 @@ export class JwtManager {
59
59
 
60
60
  /**
61
61
  * Encrypt method options (camelCase naming convention, uses instance defaults when not provided)
62
- *
62
+ *
63
63
  * @typedef {Object} JwtEncryptOptions Encryption method options
64
64
  * @property {string} [algorithm='dir'] JWE algorithm (overrides instance JWT_ALGORITHM)
65
65
  * @property {string} [encryption='A256GCM'] Encryption method (overrides instance JWT_ENCRYPTION)
@@ -71,7 +71,7 @@ export class JwtManager {
71
71
  */
72
72
  /**
73
73
  * Generate JWT token for user session
74
- *
74
+ *
75
75
  * @param {import('jose').JWTPayload} data User data payload
76
76
  * @param {string} secret Secret key or password for encryption
77
77
  * @param {JwtEncryptOptions} [options] Per-call configuration overrides (camelCase naming convention)
@@ -113,7 +113,7 @@ export class JwtManager {
113
113
 
114
114
  /**
115
115
  * Decrypt method options (camelCase naming convention, uses instance defaults when not provided)
116
- *
116
+ *
117
117
  * @typedef {Object} JwtDecryptOptions Decryption method options
118
118
  * @property {number} [clockTolerance=30] Clock tolerance in seconds (overrides instance JWT_CLOCK_TOLERANCE)
119
119
  * @property {string} [secretHashAlgorithm='SHA-256'] Hash algorithm for secret derivation (overrides instance JWT_SECRET_HASH_ALGORITHM)
@@ -123,7 +123,7 @@ export class JwtManager {
123
123
  **/
124
124
  /**
125
125
  * Decrypt JWT
126
- *
126
+ *
127
127
  * @param {string} token JWT token to decrypt
128
128
  * @param {string} secret Secret key or password for decryption
129
129
  * @param {JwtDecryptOptions} [options] Per-call configuration overrides (camelCase naming convention)
@@ -2,20 +2,20 @@
2
2
  * Logger utility for node-components
3
3
  * Provides configurable logging with enable/disable functionality
4
4
  * Uses singleton pattern to manage logger instances per component
5
- *
5
+ *
6
6
  * @example
7
7
  * import { Logger } from './logger.js';
8
- *
8
+ *
9
9
  * // Recommended: Get logger instance (singleton pattern)
10
10
  * const logger = Logger.getInstance('ComponentName');
11
- *
11
+ *
12
12
  * // With explicit enable/disable
13
13
  * const logger = Logger.getInstance('ComponentName', true); // Always enabled
14
14
  * const logger = Logger.getInstance('ComponentName', false); // Always disabled
15
- *
15
+ *
16
16
  * // Backward compatibility: Constructor still works
17
17
  * const logger = new Logger('ComponentName');
18
- *
18
+ *
19
19
  * // Use logger
20
20
  * logger.info('Operation completed');
21
21
  * logger.error('Error occurred', error);
@@ -23,10 +23,10 @@
23
23
  export class Logger {
24
24
  /** @type {Map<string, Logger>} */
25
25
  static #instances = new Map();
26
-
26
+
27
27
  /** @type {boolean} - Global flag to enable/disable colors */
28
28
  static #colorsEnabled = true;
29
-
29
+
30
30
  /** ANSI color codes for different log levels */
31
31
  static #colors = {
32
32
  reset: '\x1b[0m',
@@ -35,7 +35,7 @@ export class Logger {
35
35
  yellow: '\x1b[33m', // warn - yellow
36
36
  red: '\x1b[31m', // error - red
37
37
  };
38
-
38
+
39
39
  /** @type {boolean} */
40
40
  #enabled;
41
41
  /** @type {string} */
@@ -51,11 +51,11 @@ export class Logger {
51
51
  */
52
52
  static getInstance(componentName, enableLogging) {
53
53
  const key = `${componentName}:${enableLogging ?? 'default'}`;
54
-
54
+
55
55
  if (!Logger.#instances.has(key)) {
56
56
  Logger.#instances.set(key, new Logger(componentName, enableLogging));
57
57
  }
58
-
58
+
59
59
  return Logger.#instances.get(key);
60
60
  }
61
61
 
@@ -90,20 +90,20 @@ export class Logger {
90
90
  // Otherwise, default to enabled in development, disabled in production
91
91
  this.#enabled = enableLogging ?? (process.env.NODE_ENV !== 'production');
92
92
  this.#prefix = `[${componentName}]`;
93
-
93
+
94
94
  // Determine if colors should be used:
95
95
  // - Colors are globally enabled
96
96
  // - Output is a terminal (TTY)
97
97
  // - NO_COLOR environment variable is not set
98
- this.#useColors = Logger.#colorsEnabled &&
99
- process.stdout.isTTY &&
98
+ this.#useColors = Logger.#colorsEnabled &&
99
+ process.stdout.isTTY &&
100
100
  !process.env.NO_COLOR;
101
-
101
+
102
102
  // Assign methods based on enabled flag to eliminate runtime checks
103
103
  // This improves performance by avoiding conditional checks on every log call
104
104
  if (this.#enabled) {
105
105
  const colors = Logger.#colors;
106
-
106
+
107
107
  if (this.#useColors) {
108
108
  // Logging enabled with colors: colorize the prefix
109
109
  this.debug = (...args) => console.debug(colors.dim + this.#prefix + colors.reset, ...args);
@@ -128,4 +128,4 @@ export class Logger {
128
128
  this.log = () => {};
129
129
  }
130
130
  }
131
- }
131
+ }
@@ -100,4 +100,4 @@ export class RedisManager {
100
100
  await this.#client.quit();
101
101
  this.#logger.info('### REDIS DISCONNECTED ###');
102
102
  }
103
- }
103
+ }
@@ -1,22 +1,22 @@
1
1
  /**
2
2
  * FlexRouter for expressjs
3
- *
3
+ *
4
4
  * @example
5
5
  * import { Router } from 'express';
6
6
  * import { FlexRouter } from '../models/router.js';
7
7
  * import { authenticate } from '../middlewares/common.js';
8
- *
8
+ *
9
9
  * export const publicRouter = Router();
10
10
  * export const privateRouter = Router();
11
- *
11
+ *
12
12
  * publicRouter.get('/health', (req, res) => {
13
13
  * res.send('OK');
14
14
  * });
15
- *
15
+ *
16
16
  * privateRouter.get('/', (req, res) => {
17
17
  * res.send('Hello World!');
18
18
  * });
19
- *
19
+ *
20
20
  * export const routers = [
21
21
  * new FlexRouter('/api/v1/protected', privateRouter, [authenticate]),
22
22
  * new FlexRouter('/api/v1/public', healthRouter),
@@ -28,15 +28,15 @@ export const SessionMode = {
28
28
  * Session configuration options
29
29
  */
30
30
  export class SessionConfig {
31
- /**
31
+ /**
32
32
  * @type {string} Authentication mode for protected routes
33
33
  * - Supported values: SessionMode.SESSION | SessionMode.TOKEN
34
34
  * @default SessionMode.SESSION
35
35
  */
36
36
  SESSION_MODE;
37
- /**
37
+ /**
38
38
  * @type {string} Identity Provider microservice endpoint URL
39
- *
39
+ *
40
40
  * This is a fully customized, independent microservice that provides SSO authentication.
41
41
  * The endpoint serves multiple applications and provides the following APIs:
42
42
  * - GET /auth/providers - List supported identity providers
@@ -44,7 +44,7 @@ export class SessionConfig {
44
44
  * - POST /auth/verify - Verify JWT token validity
45
45
  * - GET /auth/callback/:idp - Validate authentication and return user data
46
46
  * - POST /auth/refresh - Refresh access tokens
47
- *
47
+ *
48
48
  * @example
49
49
  * SSO_ENDPOINT_URL: 'https://idp.example.com/open/api/v1'
50
50
  */
@@ -67,19 +67,19 @@ export class SessionConfig {
67
67
  SESSION_COOKIE_PATH;
68
68
  /** @type {string} Session secret */
69
69
  SESSION_SECRET;
70
- /**
71
- * @type {string}
70
+ /**
71
+ * @type {string}
72
72
  * @default 'ibmid:'
73
73
  */
74
74
  SESSION_PREFIX;
75
- /**
75
+ /**
76
76
  * @type {string} Session key
77
77
  * - In the `SessionMode.SESSION` mode, this is the key used to store the user in the session.
78
78
  * - In the `SessionMode.TOKEN` mode, this is the key of localStorage where the user is stored.
79
79
  * @default 'session_token'
80
80
  */
81
81
  SESSION_KEY;
82
- /**
82
+ /**
83
83
  * @type {string} Session expiry key
84
84
  * - In the `SessionMode.TOKEN` mode, this is the key of localStorage where the session expiry timestamp is stored.
85
85
  * @default 'session_expires_at'
@@ -141,26 +141,33 @@ export class SessionManager {
141
141
  #htmlTemplate = null;
142
142
 
143
143
  /**
144
- * Create a new session manager
145
- * @param {SessionConfig} config Session configuration
146
- * @throws {TypeError} If config is not an object
147
- * @throws {Error} If required configuration fields are missing
144
+ * Validate config is an object
145
+ * @param {SessionConfig} config
146
+ * @throws {TypeError}
148
147
  */
149
- constructor(config) {
150
- // Validate config is an object
148
+ #validateConfigType(config) {
151
149
  if (!config || typeof config !== 'object') {
152
150
  throw new TypeError('SessionManager configuration must be an object');
153
151
  }
152
+ }
154
153
 
155
- // Validate required fields based on SESSION_MODE
156
- const mode = config.SESSION_MODE || SessionMode.SESSION;
157
-
158
- // SESSION_SECRET is always required for both modes
154
+ /**
155
+ * Validate required configuration fields
156
+ * @param {SessionConfig} config
157
+ * @throws {Error}
158
+ */
159
+ #validateRequiredFields(config) {
159
160
  if (!config.SESSION_SECRET) {
160
161
  throw new Error('SESSION_SECRET is required for SessionManager');
161
162
  }
163
+ }
162
164
 
163
- // Validate SSO configuration if SSO endpoints are used
165
+ /**
166
+ * Validate SSO configuration
167
+ * @param {SessionConfig} config
168
+ * @throws {Error}
169
+ */
170
+ #validateSsoConfig(config) {
164
171
  if (config.SSO_ENDPOINT_URL) {
165
172
  if (!config.SSO_CLIENT_ID) {
166
173
  throw new Error('SSO_CLIENT_ID is required when SSO_ENDPOINT_URL is provided');
@@ -169,9 +176,15 @@ export class SessionManager {
169
176
  throw new Error('SSO_CLIENT_SECRET is required when SSO_ENDPOINT_URL is provided');
170
177
  }
171
178
  }
179
+ }
172
180
 
173
- // Validate TOKEN mode specific requirements
174
- if (mode === SessionMode.TOKEN) {
181
+ /**
182
+ * Validate TOKEN mode requirements
183
+ * @param {SessionConfig} config
184
+ * @throws {Error}
185
+ */
186
+ #validateTokenMode(config) {
187
+ if (config.SESSION_MODE === SessionMode.TOKEN) {
175
188
  if (!config.SSO_SUCCESS_URL) {
176
189
  throw new Error('SSO_SUCCESS_URL is required for TOKEN authentication mode');
177
190
  }
@@ -179,11 +192,16 @@ export class SessionManager {
179
192
  throw new Error('SSO_FAILURE_URL is required for TOKEN authentication mode');
180
193
  }
181
194
  }
195
+ }
182
196
 
183
- this.#config = {
184
- // Session Mode
197
+ /**
198
+ * Build configuration object
199
+ * @param {SessionConfig} config
200
+ * @returns {object}
201
+ */
202
+ #buildConfig(config) {
203
+ return {
185
204
  SESSION_MODE: config.SESSION_MODE || SessionMode.SESSION,
186
- // Session - SESSION_AGE is now in seconds (default: 64800 = 18 hours)
187
205
  SESSION_AGE: config.SESSION_AGE || 64800,
188
206
  SESSION_COOKIE_PATH: config.SESSION_COOKIE_PATH || '/',
189
207
  SESSION_SECRET: config.SESSION_SECRET,
@@ -191,18 +209,13 @@ export class SessionManager {
191
209
  SESSION_KEY: config.SESSION_KEY || 'session_token',
192
210
  SESSION_EXPIRY_KEY: config.SESSION_EXPIRY_KEY || 'session_expires_at',
193
211
  TOKEN_STORAGE_TEMPLATE_PATH: config.TOKEN_STORAGE_TEMPLATE_PATH,
194
-
195
- // Identity Provider
196
212
  SSO_ENDPOINT_URL: config.SSO_ENDPOINT_URL,
197
213
  SSO_CLIENT_ID: config.SSO_CLIENT_ID,
198
214
  SSO_CLIENT_SECRET: config.SSO_CLIENT_SECRET,
199
215
  SSO_SUCCESS_URL: config.SSO_SUCCESS_URL,
200
216
  SSO_FAILURE_URL: config.SSO_FAILURE_URL,
201
- // Redis
202
217
  REDIS_URL: config.REDIS_URL,
203
218
  REDIS_CERT_PATH: config.REDIS_CERT_PATH,
204
-
205
- // JWT Manager
206
219
  JWT_ALGORITHM: config.JWT_ALGORITHM || 'dir',
207
220
  JWT_ENCRYPTION: config.JWT_ENCRYPTION || 'A256GCM',
208
221
  JWT_CLOCK_TOLERANCE: config.JWT_CLOCK_TOLERANCE ?? 30,
@@ -213,6 +226,20 @@ export class SessionManager {
213
226
  };
214
227
  }
215
228
 
229
+ /**
230
+ * Create a new session manager
231
+ * @param {SessionConfig} config Session configuration
232
+ * @throws {TypeError} If config is not an object
233
+ * @throws {Error} If required configuration fields are missing
234
+ */
235
+ constructor(config) {
236
+ this.#validateConfigType(config);
237
+ this.#validateRequiredFields(config);
238
+ this.#validateSsoConfig(config);
239
+ this.#validateTokenMode(config);
240
+ this.#config = this.#buildConfig(config);
241
+ }
242
+
216
243
  /**
217
244
  * Check if the email has a session refresh lock
218
245
  * @param {string} email Email address
@@ -462,7 +489,7 @@ export class SessionManager {
462
489
  return next(new CustomError(httpCodes.UNAUTHORIZED, 'Token expired'));
463
490
  }
464
491
 
465
- return next(error instanceof CustomError ? error :
492
+ return next(error instanceof CustomError ? error :
466
493
  new CustomError(httpCodes.UNAUTHORIZED, 'Token verification failed'));
467
494
  }
468
495
  }
@@ -515,7 +542,7 @@ export class SessionManager {
515
542
 
516
543
  // Call SSO refresh endpoint
517
544
  const response = await this.#idpRequest.post(idpRefreshUrl, {
518
- idp: user?.attributes?.idp,
545
+ idp: user?.attributes?.idp,
519
546
  refresh_token: user?.attributes?.refresh_token
520
547
  }, {
521
548
  headers: {
@@ -622,7 +649,7 @@ export class SessionManager {
622
649
  if (isRedirect) {
623
650
  return res.redirect(this.#config.SSO_SUCCESS_URL);
624
651
  }
625
- return res.json({
652
+ return res.json({
626
653
  message: 'All tokens logged out successfully',
627
654
  tokensRemoved: keys.length,
628
655
  redirect_url: this.#config.SSO_SUCCESS_URL
@@ -632,7 +659,7 @@ export class SessionManager {
632
659
  if (isRedirect) {
633
660
  return res.redirect(this.#config.SSO_FAILURE_URL);
634
661
  }
635
- return res.status(httpCodes.SYSTEM_FAILURE).json({
662
+ return res.status(httpCodes.SYSTEM_FAILURE).json({
636
663
  error: 'Logout all failed',
637
664
  redirect_url: this.#config.SSO_FAILURE_URL
638
665
  });
@@ -671,7 +698,7 @@ export class SessionManager {
671
698
  if (isRedirect) {
672
699
  return res.redirect(this.#config.SSO_SUCCESS_URL);
673
700
  }
674
- return res.json({
701
+ return res.json({
675
702
  message: 'Logout successful',
676
703
  redirect_url: this.#config.SSO_SUCCESS_URL
677
704
  });
@@ -681,7 +708,7 @@ export class SessionManager {
681
708
  if (isRedirect) {
682
709
  return res.redirect(this.#config.SSO_FAILURE_URL);
683
710
  }
684
- return res.status(httpCodes.SYSTEM_FAILURE).json({
711
+ return res.status(httpCodes.SYSTEM_FAILURE).json({
685
712
  error: 'Logout failed',
686
713
  redirect_url: this.#config.SSO_FAILURE_URL
687
714
  });
@@ -776,9 +803,9 @@ export class SessionManager {
776
803
  * Resource protection based on configured SESSION_MODE
777
804
  * - SESSION mode: Verifies user exists in session store and is authorized (checks req.session data)
778
805
  * - TOKEN mode: Validates JWT token from Authorization header (lightweight validation)
779
- *
806
+ *
780
807
  * Note: This method verifies authentication only. Use requireUser() after this to populate req.user.
781
- *
808
+ *
782
809
  * @param {string} [errorRedirectUrl=''] Redirect URL on authentication failure
783
810
  * @returns {import('@types/express').RequestHandler} Returns express Request Handler
784
811
  * @example
@@ -786,9 +813,9 @@ export class SessionManager {
786
813
  * app.get('/api/check', session.authenticate(), (req, res) => {
787
814
  * res.json({ authenticated: true });
788
815
  * });
789
- *
816
+ *
790
817
  * // Option 2: Verify authentication AND load user data into req.user
791
- * app.get('/api/profile',
818
+ * app.get('/api/profile',
792
819
  * session.authenticate(), // Verifies session/token
793
820
  * session.requireUser(), // Loads user data into req.user
794
821
  * (req, res) => {
@@ -852,7 +879,7 @@ export class SessionManager {
852
879
  }
853
880
  throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
854
881
  };
855
-
882
+
856
883
  /**
857
884
  * Render HTML template for token storage
858
885
  * @param {string} token JWT token
@@ -870,7 +897,7 @@ export class SessionManager {
870
897
  .replaceAll('{{SSO_SUCCESS_URL}}', sucessRedirectUrl)
871
898
  .replaceAll('{{SSO_FAILURE_URL}}', this.#config.SSO_FAILURE_URL);
872
899
  }
873
-
900
+
874
901
  /**
875
902
  * SSO callback for successful login
876
903
  * @param {(user: object) => object} initUser Initialize user object function
@@ -882,7 +909,7 @@ export class SessionManager {
882
909
  if (!jwt) {
883
910
  return next(new CustomError(httpCodes.BAD_REQUEST, 'Missing `jwt` in query parameters'));
884
911
  }
885
-
912
+
886
913
  try {
887
914
  // Decrypt JWT from Identity Adapter
888
915
  const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
@@ -890,10 +917,10 @@ export class SessionManager {
890
917
  if (!payload?.user) {
891
918
  throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
892
919
  }
893
-
920
+
894
921
  /** @type {string} */
895
922
  const callbackRedirectUrl = payload.redirect_url || this.#config.SSO_SUCCESS_URL;
896
-
923
+
897
924
  // Token mode: Generate token and return HTML page
898
925
  if (this.getSessionMode() === SessionMode.TOKEN) {
899
926
  /** @type {import('../index.js').SessionUser} */
@@ -986,8 +1013,8 @@ export class SessionManager {
986
1013
  if (isRedirect) {
987
1014
  return res.redirect(this.#config.SSO_FAILURE_URL);
988
1015
  }
989
- return res.status(httpCodes.AUTHORIZATION_FAILED).send({
990
- redirect_url: this.#config.SSO_FAILURE_URL
1016
+ return res.status(httpCodes.AUTHORIZATION_FAILED).send({
1017
+ redirect_url: this.#config.SSO_FAILURE_URL
991
1018
  });
992
1019
  }
993
1020
  if (isRedirect) {
package/index.js CHANGED
@@ -3,4 +3,4 @@ export { httpCodes, httpMessages, httpErrorHandler, httpNotFoundHandler, CustomE
3
3
  export { RedisManager } from './components/redis.js';
4
4
  export { FlexRouter } from './components/router.js';
5
5
  export { JwtManager } from './components/jwt.js';
6
- export { Logger } from './components/logger.js';
6
+ export { Logger } from './components/logger.js';
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@igxjs/node-components",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "description": "Node components for igxjs",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
- "test": "mocha tests/**/*.test.js --timeout 5000"
8
+ "test": "mocha tests/**/*.test.js --timeout 5000",
9
+ "lint": "eslint .",
10
+ "lint:fix": "eslint . --fix"
9
11
  },
10
12
  "repository": {
11
13
  "type": "git",
@@ -33,7 +35,10 @@
33
35
  "memorystore": "^1.6.7"
34
36
  },
35
37
  "devDependencies": {
38
+ "@eslint/js": "^10.0.1",
36
39
  "chai": "^6.2.2",
40
+ "eslint": "^10.1.0",
41
+ "globals": "^17.4.0",
37
42
  "mocha": "^12.0.0-beta-10",
38
43
  "sinon": "^21.0.3",
39
44
  "supertest": "^7.0.0"
@@ -54,7 +59,8 @@
54
59
  "README.md"
55
60
  ],
56
61
  "publishConfig": {
57
- "access": "public"
62
+ "access": "public",
63
+ "registry": "https://npm.pkg.github.com"
58
64
  },
59
65
  "types": "./index.d.ts"
60
66
  }