@igxjs/node-components 1.0.11 → 1.0.12

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
@@ -13,6 +13,7 @@ npm install @igxjs/node-components
13
13
  | Component | Description | Documentation |
14
14
  |-----------|-------------|---------------|
15
15
  | **SessionManager** | SSO session management with Redis/memory storage, supporting both session and token-based authentication | [View docs](./docs/session-manager.md) |
16
+ | **Logger** | High-performance logging utility with zero dependencies and smart color detection | [View docs](./docs/logger.md) |
16
17
  | **FlexRouter** | Flexible routing with context paths and middleware | [View docs](./docs/flex-router.md) |
17
18
  | **RedisManager** | Redis connection management with TLS support | [View docs](./docs/redis-manager.md) |
18
19
  | **JWT Manager** | Secure JWT encryption/decryption with JWE | [View docs](./docs/jwt-manager.md) |
@@ -76,10 +77,11 @@ flexRouter.mount(app, '');
76
77
  ```javascript
77
78
  import { JwtManager } from '@igxjs/node-components';
78
79
 
79
- const jwt = new JwtManager({ expirationTime: '1h' });
80
+ // Constructor uses UPPERCASE naming with JWT_ prefix
81
+ const jwt = new JwtManager({ SESSION_AGE: 64800000 });
80
82
  const SECRET = process.env.JWT_SECRET;
81
83
 
82
- // Create token
84
+ // Create token (encrypt method uses camelCase for per-call options)
83
85
  const token = await jwt.encrypt({ userId: '123', email: 'user@example.com' }, SECRET);
84
86
 
85
87
  // Verify token
@@ -143,7 +145,6 @@ Uses JWT bearer tokens instead of session cookies. When a user authenticates via
143
145
  - `SSO_FAILURE_URL`: Redirect URL after failed SSO login
144
146
  - `JWT_ALGORITHM`: JWT algorithm (default: `'dir'`)
145
147
  - `JWT_ENCRYPTION`: Encryption algorithm (default: `'A256GCM'`)
146
- - `JWT_EXPIRATION_TIME`: Token expiration time (default: `'10m'`)
147
148
  - `JWT_CLOCK_TOLERANCE`: Clock skew tolerance in seconds (default: 30)
148
149
 
149
150
  **Auth Methods:**
@@ -154,23 +155,23 @@ Uses JWT bearer tokens instead of session cookies. When a user authenticates via
154
155
 
155
156
  **Token Storage (Client-Side):**
156
157
 
157
- When using token-based authentication, the client-side HTML page stores the token in `localStorage`:
158
-
159
- ```html
160
- <script>
161
- // Store auth data in localStorage
162
- localStorage.setItem('authToken', ${JSON.stringify(token)});
163
- localStorage.setItem('tokenExpiry', ${Date.now() + sessionAge});
164
- localStorage.setItem('user', ${JSON.stringify({
165
- email: user.email,
166
- name: user.name,
167
- })});
168
-
169
- // Redirect to original destination
170
- window.location.replace(redirectUrl);
171
- </script>
158
+ When using token-based authentication, the SSO callback returns an HTML page that stores the token in `localStorage` and redirects the user:
159
+
160
+ ```javascript
161
+ // The token is automatically stored in localStorage by the callback HTML page
162
+ // Default keys (customizable via SESSION_KEY and SESSION_EXPIRY_KEY config):
163
+ localStorage.getItem('session_token'); // JWT token
164
+ localStorage.getItem('session_expires_at'); // Expiry timestamp
165
+
166
+ // Making authenticated requests from the client:
167
+ const token = localStorage.getItem('session_token');
168
+ fetch('/api/protected', {
169
+ headers: { 'Authorization': `Bearer ${token}` }
170
+ });
172
171
  ```
173
172
 
173
+ **Note:** The actual localStorage keys used are determined by the `SESSION_KEY` and `SESSION_EXPIRY_KEY` configuration options (defaults shown above).
174
+
174
175
  ## SessionManager Configuration Options
175
176
 
176
177
  | Option | Type | Default | Description |
@@ -189,7 +190,6 @@ When using token-based authentication, the client-side HTML page stores the toke
189
190
  | `REDIS_CERT_PATH` | string | - | Path to Redis TLS certificate |
190
191
  | `JWT_ALGORITHM` | string | `'dir'` | JWT signing algorithm |
191
192
  | `JWT_ENCRYPTION` | string | `'A256GCM'` | JWE encryption algorithm |
192
- | `JWT_EXPIRATION_TIME` | string | `'10m'` | Token expiration duration |
193
193
  | `JWT_CLOCK_TOLERANCE` | number | 30 | Clock skew tolerance in seconds |
194
194
  | `JWT_SECRET_HASH_ALGORITHM` | string | `'SHA-256'` | Algorithm for hashing secrets |
195
195
  | `JWT_ISSUER` | string | - | JWT issuer identifier |
@@ -0,0 +1,111 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>Sign in IBM Garage</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no,viewport-fit=cover,minimum-scale=1,maximum-scale=1,user-scalable=no">
6
+ <meta name="robots" content="noindex, nofollow" />
7
+ <style>
8
+ :root {
9
+ --foreground-color-primary: #444;
10
+ --font-size: 0.8rem;
11
+ --background-color-primary: #f1f1f199;
12
+ --gap-global: 0.4rem;
13
+ }
14
+ @media (prefers-color-scheme: dark) {
15
+ :root {
16
+ --background-color-primary: black;
17
+ --foreground-color-primary: #bbb;
18
+ }
19
+ }
20
+ html, body {
21
+ padding: 0px;
22
+ margin: 0px auto;
23
+ height: 100%;
24
+ width: 100%;
25
+ }
26
+ body {
27
+ background-image: radial-gradient(circle at 20% 80%, rgba(255, 140, 140, 0.5), transparent 42%),
28
+ radial-gradient(circle at 80% 20%, rgba(140, 200, 255, 0.55), transparent 42%),
29
+ radial-gradient(circle at 40% 40%, rgba(255, 230, 140, 0.45), transparent 32%),
30
+ linear-gradient(135deg, rgba(160, 120, 240, 0.95), rgba(120, 160, 240, 0.95));
31
+ background-size: cover;
32
+ background-repeat: no-repeat;
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: center;
36
+ font-family: Verdana, Helvetica, Arial, sans-serif;
37
+ font-size: var(--font-size);
38
+ color: var(--foreground-color-primary);
39
+ flex-flow: column;
40
+ position: relative;
41
+ z-index: 10;
42
+ overflow: hidden;
43
+ }
44
+ body::before {
45
+ background: var(--background-color-primary);
46
+ content: "";
47
+ position: absolute;
48
+ top: 0;
49
+ left: 0;
50
+ right: 0;
51
+ bottom: 0;
52
+ z-index: -1;
53
+ backdrop-filter: blur(10px);
54
+ -webkit-backdrop-filter: blur(10px);
55
+ }
56
+ .wrapper {
57
+ width: 100%;
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: space-between;
61
+ box-sizing: border-box;
62
+ row-gap: var(--gap-global);
63
+ flex-direction: column;
64
+ padding: var(--gap-global);
65
+ }
66
+ .wrapper #failure {
67
+ display: none;
68
+ }
69
+ </style>
70
+ </head>
71
+ <body>
72
+ <hr />
73
+ <div class="wrapper">
74
+ <div id="success">Redirecting... <a href="{{SSO_SUCCESS_URL}}" target="_blank" style="color: blue; text-decoration: underline;">(click here if not redirected)</a></div>
75
+ <div id="failure">
76
+ <p>Authentication failed. Please try again.</p>
77
+ <a href="{{SSO_FAILURE_URL}}">Return to login</a>
78
+ </div>
79
+ </div>
80
+ <script>
81
+ (function() {
82
+ let success = true;
83
+
84
+ // Check localStorage support first
85
+ if (typeof localStorage !== 'object' || typeof localStorage.setItem !== 'function') {
86
+ console.warn('localStorage not supported, falling back to session cookie');
87
+ success = false;
88
+ }
89
+
90
+ try {
91
+ // Try localStorage first
92
+ if (localStorage.setItem) {
93
+ localStorage.setItem('{{SESSION_DATA_KEY}}', '{{SESSION_DATA_VALUE}}');
94
+ localStorage.setItem('{{SESSION_EXPIRY_KEY}}', '{{SESSION_EXPIRY_VALUE}}');
95
+ }
96
+
97
+ // Fall back to simple navigation
98
+ location.href = '{{SSO_SUCCESS_URL}}';
99
+ } catch (e) {
100
+ console.error('Redirect failed:', e);
101
+ success = false;
102
+ }
103
+
104
+ if (!success) {
105
+ document.getElementById('success').style.display = 'none';
106
+ document.getElementById('failure').style.display = 'block';
107
+ }
108
+ })();
109
+ </script>
110
+ </body>
111
+ </html>
@@ -1,20 +1,11 @@
1
1
  import { STATUS_CODES } from 'node:http';
2
+ import { Logger } from './logger.js';
2
3
 
3
- export const httpMessages = {
4
- OK: 'OK',
5
- CREATED: 'Created',
6
- NO_CONTENT: 'No Content',
7
- BAD_REQUEST: 'Bad Request',
8
- UNAUTHORIZED: 'Unauthorized',
9
- FORBIDDEN: 'Forbidden',
10
- NOT_FOUND: 'Not Found',
11
- NOT_ACCEPTABLE: 'Not Acceptable',
12
- CONFLICT: 'Conflict',
13
- LOCKED: 'Locked',
14
- SYSTEM_FAILURE: 'System Error',
15
- NOT_IMPLEMENTED: 'Not Implemented',
16
- };
4
+ const logger = Logger.getInstance('httpError');
17
5
 
6
+ /**
7
+ * HTTP status codes
8
+ */
18
9
  export const httpCodes = {
19
10
  OK: 200,
20
11
  CREATED: 201,
@@ -30,23 +21,45 @@ export const httpCodes = {
30
21
  NOT_IMPLEMENTED: 501,
31
22
  };
32
23
 
24
+ /**
25
+ * HTTP status messages
26
+ */
27
+ export const httpMessages = {
28
+ OK: 'OK',
29
+ CREATED: 'Created',
30
+ NO_CONTENT: 'No Content',
31
+ BAD_REQUEST: 'Bad Request',
32
+ UNAUTHORIZED: 'Unauthorized',
33
+ FORBIDDEN: 'Forbidden',
34
+ NOT_FOUND: 'Not Found',
35
+ NOT_ACCEPTABLE: 'Not Acceptable',
36
+ CONFLICT: 'Conflict',
37
+ LOCKED: 'Locked',
38
+ SYSTEM_FAILURE: 'Internal Server Error',
39
+ NOT_IMPLEMENTED: 'Not Implemented',
40
+ };
41
+
42
+ /**
43
+ * Custom Error class
44
+ */
33
45
  export class CustomError extends Error {
34
- /** @type {number} */
35
- code;
36
- /** @type {object} */
37
- data;
38
- /** @type {object} */
39
- error;
40
46
  /**
41
- * Construct a custom error
42
- * @param {number} code Error code
43
- * @param {string} message Message
47
+ * @param {number} code HTTP status code
48
+ * @param {string} message Error message
49
+ * @param {Error} [error] Original error object
50
+ * @param {object} [data] Additional error data
44
51
  */
45
- constructor(code, message, error = {}, data = {}) {
52
+ constructor(code, message, error, data) {
46
53
  super(message);
54
+ this.name = 'CustomError';
47
55
  this.code = code;
48
56
  this.error = error;
49
57
  this.data = data;
58
+
59
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
60
+ if (Error.captureStackTrace) {
61
+ Error.captureStackTrace(this, CustomError);
62
+ }
50
63
  }
51
64
  }
52
65
 
@@ -85,21 +98,21 @@ export const httpErrorHandler = (err, req, res, next) => {
85
98
  res.status(responseBody.status).json(responseBody);
86
99
 
87
100
  // Log error details
88
- console.error('### ERROR ###');
89
- console.error(`${req.method} ${req.path}`);
101
+ logger.error('### ERROR ###');
102
+ logger.error(`${req.method} ${req.path}`);
90
103
 
91
104
  // Log based on error type
92
105
  if ([httpCodes.UNAUTHORIZED, httpCodes.FORBIDDEN, httpCodes.NOT_FOUND].includes(err.code)) {
93
- console.error('>>> Auth Error:', err.message);
106
+ logger.error('>>> Auth Error:', err.message);
94
107
  } else {
95
- console.error('>>> Name:', err.name);
96
- console.error('>>> Message:', err.message);
108
+ logger.error('>>> Name:', err.name);
109
+ logger.error('>>> Message:', err.message);
97
110
  if (err.stack) {
98
- console.error('>>> Stack:', err.stack);
111
+ logger.error('>>> Stack:', err.stack);
99
112
  }
100
113
  }
101
114
 
102
- console.error('### /ERROR ###');
115
+ logger.error('### /ERROR ###');
103
116
  };
104
117
 
105
118
  /**
@@ -177,7 +190,7 @@ export const httpHelper = {
177
190
  * @returns {CustomError} Returns CustomError instance
178
191
  */
179
192
  handleAxiosError(error, defaultMessage = 'An error occurred') {
180
- console.warn(`### TRY ERROR: ${defaultMessage} ###`);
193
+ logger.warn(`### TRY ERROR: ${defaultMessage} ###`);
181
194
  // Extract error details
182
195
  const errorCode = _getErrorCode(error);
183
196
  const errorMessage = _getErrorMessage(error, defaultMessage);
@@ -196,4 +209,4 @@ export const httpHelper = {
196
209
  */
197
210
  export const httpError = (code, message, error, data) => {
198
211
  return new CustomError(code, message, error, data);
199
- };
212
+ };
package/components/jwt.js CHANGED
@@ -13,7 +13,7 @@ export class JwtManager {
13
13
  /** @type {string} Encryption method */
14
14
  encryption;
15
15
 
16
- /** @type {string} Token expiration time */
16
+ /** @type {number} Token expiration time */
17
17
  expirationTime;
18
18
 
19
19
  /** @type {number} Clock tolerance in seconds */
@@ -37,7 +37,7 @@ export class JwtManager {
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')
40
- * @property {string} [JWT_EXPIRATION_TIME='10m'] Token expiration time (default: '10m')
40
+ * @property {number} [SESSION_AGE=64800000] Token expiration time in milliseconds (default: 64800000 = 18 hours)
41
41
  * @property {string} [JWT_SECRET_HASH_ALGORITHM='SHA-256'] Hash algorithm (default: 'SHA-256')
42
42
  * @property {string?} [JWT_ISSUER] Optional JWT issuer claim
43
43
  * @property {string?} [JWT_AUDIENCE] Optional JWT audience claim
@@ -48,7 +48,8 @@ export class JwtManager {
48
48
  constructor(options = {}) {
49
49
  this.algorithm = options.JWT_ALGORITHM || 'dir';
50
50
  this.encryption = options.JWT_ENCRYPTION || 'A256GCM';
51
- this.expirationTime = options.JWT_EXPIRATION_TIME || '10m';
51
+ // SESSION_AGE is in milliseconds, convert to seconds for expirationTime
52
+ this.expirationTime = Math.floor((options.SESSION_AGE || 64800000) / 1000);
52
53
  this.secretHashAlgorithm = options.JWT_SECRET_HASH_ALGORITHM || 'SHA-256';
53
54
  this.issuer = options.JWT_ISSUER;
54
55
  this.audience = options.JWT_AUDIENCE;
@@ -62,7 +63,7 @@ export class JwtManager {
62
63
  * @typedef {Object} JwtEncryptOptions Encryption method options
63
64
  * @property {string} [algorithm='dir'] JWE algorithm (overrides instance JWT_ALGORITHM)
64
65
  * @property {string} [encryption='A256GCM'] Encryption method (overrides instance JWT_ENCRYPTION)
65
- * @property {string} [expirationTime='10m'] Token expiration time (overrides instance JWT_EXPIRATION_TIME)
66
+ * @property {number} [expirationTime] Token expiration time in seconds (overrides instance expirationTime derived from SESSION_AGE)
66
67
  * @property {string} [secretHashAlgorithm='SHA-256'] Hash algorithm for secret derivation (overrides instance JWT_SECRET_HASH_ALGORITHM)
67
68
  * @property {string?} [issuer] Optional JWT issuer claim (overrides instance JWT_ISSUER)
68
69
  * @property {string?} [audience] Optional JWT audience claim (overrides instance JWT_AUDIENCE)
@@ -93,10 +94,11 @@ export class JwtManager {
93
94
  const jwt = new EncryptJWT(data)
94
95
  .setProtectedHeader({
95
96
  alg: algorithm,
96
- enc: encryption
97
+ enc: encryption,
98
+ typ: 'JWT',
97
99
  })
98
100
  .setIssuedAt()
99
- .setExpirationTime(expirationTime);
101
+ .setExpirationTime(`${expirationTime}s`); // Pass as string with 's' suffix for seconds
100
102
 
101
103
  // Add optional claims if provided
102
104
  if (issuer) jwt.setIssuer(issuer);
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Logger utility for node-components
3
+ * Provides configurable logging with enable/disable functionality
4
+ * Uses singleton pattern to manage logger instances per component
5
+ *
6
+ * @example
7
+ * import { Logger } from './logger.js';
8
+ *
9
+ * // Recommended: Get logger instance (singleton pattern)
10
+ * const logger = Logger.getInstance('ComponentName');
11
+ *
12
+ * // With explicit enable/disable
13
+ * const logger = Logger.getInstance('ComponentName', true); // Always enabled
14
+ * const logger = Logger.getInstance('ComponentName', false); // Always disabled
15
+ *
16
+ * // Backward compatibility: Constructor still works
17
+ * const logger = new Logger('ComponentName');
18
+ *
19
+ * // Use logger
20
+ * logger.info('Operation completed');
21
+ * logger.error('Error occurred', error);
22
+ */
23
+ export class Logger {
24
+ /** @type {Map<string, Logger>} */
25
+ static #instances = new Map();
26
+
27
+ /** @type {boolean} - Global flag to enable/disable colors */
28
+ static #colorsEnabled = true;
29
+
30
+ /** ANSI color codes for different log levels */
31
+ static #colors = {
32
+ reset: '\x1b[0m',
33
+ dim: '\x1b[2m', // debug - dim/gray
34
+ cyan: '\x1b[36m', // info - cyan
35
+ yellow: '\x1b[33m', // warn - yellow
36
+ red: '\x1b[31m', // error - red
37
+ };
38
+
39
+ /** @type {boolean} */
40
+ #enabled;
41
+ /** @type {string} */
42
+ #prefix;
43
+ /** @type {boolean} */
44
+ #useColors;
45
+
46
+ /**
47
+ * Get or create a Logger instance (singleton pattern)
48
+ * @param {string} componentName Component name for log prefix
49
+ * @param {boolean} [enableLogging] Enable/disable logging. Defaults to NODE_ENV !== 'production'
50
+ * @returns {Logger} Logger instance
51
+ */
52
+ static getInstance(componentName, enableLogging) {
53
+ const key = `${componentName}:${enableLogging ?? 'default'}`;
54
+
55
+ if (!Logger.#instances.has(key)) {
56
+ Logger.#instances.set(key, new Logger(componentName, enableLogging));
57
+ }
58
+
59
+ return Logger.#instances.get(key);
60
+ }
61
+
62
+ /**
63
+ * Clear all logger instances (useful for testing)
64
+ */
65
+ static clearInstances() {
66
+ Logger.#instances.clear();
67
+ }
68
+
69
+ /**
70
+ * Disable colors globally for all logger instances
71
+ */
72
+ static disableColors() {
73
+ Logger.#colorsEnabled = false;
74
+ }
75
+
76
+ /**
77
+ * Enable colors globally for all logger instances
78
+ */
79
+ static enableColors() {
80
+ Logger.#colorsEnabled = true;
81
+ }
82
+
83
+ /**
84
+ * Create a new Logger instance
85
+ * @param {string} componentName Component name for log prefix
86
+ * @param {boolean} [enableLogging] Enable/disable logging. Defaults to NODE_ENV !== 'production'
87
+ */
88
+ constructor(componentName, enableLogging) {
89
+ // If enableLogging is explicitly set (true/false), use it
90
+ // Otherwise, default to enabled in development, disabled in production
91
+ this.#enabled = enableLogging ?? (process.env.NODE_ENV !== 'production');
92
+ this.#prefix = `[${componentName}]`;
93
+
94
+ // Determine if colors should be used:
95
+ // - Colors are globally enabled
96
+ // - Output is a terminal (TTY)
97
+ // - NO_COLOR environment variable is not set
98
+ this.#useColors = Logger.#colorsEnabled &&
99
+ process.stdout.isTTY &&
100
+ !process.env.NO_COLOR;
101
+
102
+ // Assign methods based on enabled flag to eliminate runtime checks
103
+ // This improves performance by avoiding conditional checks on every log call
104
+ if (this.#enabled) {
105
+ const colors = Logger.#colors;
106
+
107
+ if (this.#useColors) {
108
+ // Logging enabled with colors: colorize the prefix
109
+ this.debug = (...args) => console.debug(colors.dim + this.#prefix + colors.reset, ...args);
110
+ this.info = (...args) => console.info(colors.cyan + this.#prefix + colors.reset, ...args);
111
+ this.warn = (...args) => console.warn(colors.yellow + this.#prefix + colors.reset, ...args);
112
+ this.error = (...args) => console.error(colors.red + this.#prefix + colors.reset, ...args);
113
+ this.log = (...args) => console.log(this.#prefix, ...args);
114
+ } else {
115
+ // Logging enabled without colors: plain text
116
+ this.debug = (...args) => console.debug(this.#prefix, ...args);
117
+ this.info = (...args) => console.info(this.#prefix, ...args);
118
+ this.warn = (...args) => console.warn(this.#prefix, ...args);
119
+ this.error = (...args) => console.error(this.#prefix, ...args);
120
+ this.log = (...args) => console.log(this.#prefix, ...args);
121
+ }
122
+ } else {
123
+ // Logging disabled: assign no-op functions that do nothing
124
+ this.debug = () => {};
125
+ this.info = () => {};
126
+ this.warn = () => {};
127
+ this.error = () => {};
128
+ this.log = () => {};
129
+ }
130
+ }
131
+ }
@@ -1,8 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
 
3
3
  import { createClient } from '@redis/client';
4
+ import { Logger } from './logger.js';
4
5
 
5
6
  export class RedisManager {
7
+ /** @type {Logger} */
8
+ #logger = Logger.getInstance('RedisManager');
9
+
6
10
  /** @type {import('@redis/client').RedisClientType} */
7
11
  #client = null;
8
12
  /**
@@ -22,21 +26,23 @@ export class RedisManager {
22
26
  options.socket.ca = caCert;
23
27
  }
24
28
  this.#client = createClient(options);
29
+ this.#logger.info('### REDIS CONNECTING ###');
25
30
  this.#client.on('ready', () => {
26
- console.info('### REDIS READY ###');
31
+ this.#logger.info('### REDIS READY ###');
27
32
  });
28
33
  this.#client.on('reconnecting', (_res) => {
29
- console.warn('### REDIS RECONNECTING ###');
34
+ this.#logger.warn('### REDIS RECONNECTING ###');
30
35
  });
31
36
  this.#client.on('error', (error) => {
32
- console.error(`### REDIS ERROR: ${error.message} ###`);
37
+ this.#logger.error(`### REDIS ERROR: ${error.message} ###`);
33
38
  });
34
39
  await this.#client.connect();
40
+ this.#logger.info('### REDIS CONNECTED SUCCESSFULLY ###');
35
41
  return true;
36
42
  }
37
43
  catch (error) {
38
- console.error('### REDIS CONNECT ERROR ###');
39
- console.error(error);
44
+ this.#logger.error('### REDIS CONNECT ERROR ###', error);
45
+ return false;
40
46
  }
41
47
  }
42
48
  return false;
@@ -60,8 +66,8 @@ export class RedisManager {
60
66
  const pongMessage = await this.#client.ping();
61
67
  return 'PONG' === pongMessage;
62
68
  } catch (error) {
63
- console.error(`### REDIS PING ERROR ###`);
64
- console.error(error);
69
+ this.#logger.error(`### REDIS PING ERROR: ${error.message} ###`);
70
+ return false;
65
71
  }
66
72
  return false;
67
73
  }
@@ -70,7 +76,9 @@ export class RedisManager {
70
76
  * Disconnect with Redis
71
77
  * @returns {Promise<void>} Returns nothing
72
78
  */
73
- disConnect() {
74
- return this.#client.quit();
79
+ async disconnect() {
80
+ this.#logger.info('### REDIS DISCONNECTING ###');
81
+ await this.#client.quit();
82
+ this.#logger.info('### REDIS DISCONNECTED ###');
75
83
  }
76
- }
84
+ }
@@ -1,4 +1,8 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
1
3
  import crypto from 'node:crypto';
4
+ import { fileURLToPath } from 'node:url';
5
+
2
6
  import axios from 'axios';
3
7
  import session from 'express-session';
4
8
  import memStore from 'memorystore';
@@ -7,6 +11,10 @@ import { RedisStore } from 'connect-redis';
7
11
  import { CustomError, httpCodes, httpHelper, httpMessages } from './http-handlers.js';
8
12
  import { JwtManager } from './jwt.js';
9
13
  import { RedisManager } from './redis.js';
14
+ import { Logger } from './logger.js';
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
10
18
 
11
19
  /**
12
20
  * Session authentication mode constants
@@ -21,9 +29,8 @@ export const SessionMode = {
21
29
  */
22
30
  export class SessionConfig {
23
31
  /**
24
- * @type {string}
25
- * Authentication mode for protected routes
26
- * Supported values: SessionMode.SESSION | SessionMode.TOKEN
32
+ * @type {string} Authentication mode for protected routes
33
+ * - Supported values: SessionMode.SESSION | SessionMode.TOKEN
27
34
  * @default SessionMode.SESSION
28
35
  */
29
36
  SESSION_MODE;
@@ -38,35 +45,69 @@ export class SessionConfig {
38
45
  /** @type {string} */
39
46
  SSO_FAILURE_URL;
40
47
 
41
- /** @type {number} */
48
+ /** @type {number} Session age in milliseconds */
42
49
  SESSION_AGE;
43
- /** @type {string} */
50
+ /**
51
+ * @type {string} Session cookie path
52
+ * @default '/'
53
+ */
44
54
  SESSION_COOKIE_PATH;
45
- /** @type {string} */
55
+ /** @type {string} Session secret */
46
56
  SESSION_SECRET;
47
- /** @type {string} */
57
+ /**
58
+ * @type {string}
59
+ * @default 'ibmid:'
60
+ */
48
61
  SESSION_PREFIX;
49
-
50
- /** @type {string} */
62
+ /**
63
+ * @type {string} Session key
64
+ * - In the `SessionMode.SESSION` mode, this is the key used to store the user in the session.
65
+ * - In the `SessionMode.TOKEN` mode, this is the key of localStorage where the user is stored.
66
+ * @default 'session_token'
67
+ */
68
+ SESSION_KEY;
69
+ /**
70
+ * @type {string} Session expiry key
71
+ * - In the `SessionMode.TOKEN` mode, this is the key of localStorage where the session expiry timestamp is stored.
72
+ * @default 'session_expires_at'
73
+ */
74
+ SESSION_EXPIRY_KEY;
75
+ /**
76
+ * @type {string} Path to custom HTML template for TOKEN mode callback
77
+ * - Used to customize the redirect page that stores JWT token and expiry in localStorage
78
+ * - Supports placeholders: {{SESSION_DATA_KEY}}, {{SESSION_DATA_VALUE}}, {{SESSION_EXPIRY_KEY}}, {{SESSION_EXPIRY_VALUE}}, {{SSO_SUCCESS_URL}}, {{SSO_FAILURE_URL}}
79
+ * - If not provided, uses default template
80
+ */
81
+ TOKEN_STORAGE_TEMPLATE_PATH;
82
+ /** @type {string} Redis URL */
51
83
  REDIS_URL;
52
- /** @type {string} */
84
+ /** @type {string} Redis certificate path */
53
85
  REDIS_CERT_PATH;
54
-
55
- /** @type {string} */
86
+ /**
87
+ * @type {string} Algorithm used to encrypt the JWT
88
+ * @default 'dir'
89
+ */
56
90
  JWT_ALGORITHM;
57
- /** @type {string} */
91
+ /**
92
+ * @type {string} Encryption algorithm used to encrypt the JWT
93
+ * @default 'A256GCM'
94
+ */
58
95
  JWT_ENCRYPTION;
59
- /** @type {string} */
60
- JWT_EXPIRATION_TIME;
61
- /** @type {number} */
96
+ /**
97
+ * @type {number} Clock tolerance in seconds
98
+ * @default 30
99
+ */
62
100
  JWT_CLOCK_TOLERANCE;
63
- /** @type {string} */
101
+ /**
102
+ * @type {string} Hash algorithm used to hash the JWT secret
103
+ * @default 'SHA-256'
104
+ */
64
105
  JWT_SECRET_HASH_ALGORITHM;
65
- /** @type {string} */
106
+ /** @type {string?} JWT issuer claim */
66
107
  JWT_ISSUER;
67
- /** @type {string} */
108
+ /** @type {string?} JWT audience claim */
68
109
  JWT_AUDIENCE;
69
- /** @type {string} */
110
+ /** @type {string?} JWT subject claim */
70
111
  JWT_SUBJECT;
71
112
  }
72
113
 
@@ -81,6 +122,8 @@ export class SessionManager {
81
122
  #idpRequest = null;
82
123
  /** @type {import('./jwt.js').JwtManager} */
83
124
  #jwtManager = null;
125
+ /** @type {import('./logger.js').Logger} */
126
+ #logger = Logger.getInstance('SessionManager');
84
127
 
85
128
  /**
86
129
  * Create a new session manager
@@ -95,6 +138,10 @@ export class SessionManager {
95
138
  SESSION_COOKIE_PATH: config.SESSION_COOKIE_PATH || '/',
96
139
  SESSION_SECRET: config.SESSION_SECRET,
97
140
  SESSION_PREFIX: config.SESSION_PREFIX || 'ibmid:',
141
+ SESSION_KEY: config.SESSION_KEY || 'session_token',
142
+ SESSION_EXPIRY_KEY: config.SESSION_EXPIRY_KEY || 'session_expires_at',
143
+ TOKEN_STORAGE_TEMPLATE_PATH: config.TOKEN_STORAGE_TEMPLATE_PATH,
144
+
98
145
  // Identity Provider
99
146
  SSO_ENDPOINT_URL: config.SSO_ENDPOINT_URL,
100
147
  SSO_CLIENT_ID: config.SSO_CLIENT_ID,
@@ -104,10 +151,10 @@ export class SessionManager {
104
151
  // Redis
105
152
  REDIS_URL: config.REDIS_URL,
106
153
  REDIS_CERT_PATH: config.REDIS_CERT_PATH,
154
+
107
155
  // JWT Manager
108
156
  JWT_ALGORITHM: config.JWT_ALGORITHM || 'dir',
109
157
  JWT_ENCRYPTION: config.JWT_ENCRYPTION || 'A256GCM',
110
- JWT_EXPIRATION_TIME: config.JWT_EXPIRATION_TIME || '10m',
111
158
  JWT_CLOCK_TOLERANCE: config.JWT_CLOCK_TOLERANCE ?? 30,
112
159
  JWT_SECRET_HASH_ALGORITHM: config.JWT_SECRET_HASH_ALGORITHM || 'SHA-256',
113
160
  JWT_ISSUER: config.JWT_ISSUER,
@@ -155,18 +202,18 @@ export class SessionManager {
155
202
  * @returns {string} Returns the session key
156
203
  */
157
204
  #getSessionKey() {
158
- return 'user';
205
+ return this.#config.SESSION_KEY;
159
206
  }
160
207
 
161
208
  /**
162
209
  * Get Redis key for token storage
163
210
  * @param {string} email User email
164
- * @param {string} tokenId Token ID
211
+ * @param {string} tid Token ID
165
212
  * @returns {string} Returns the Redis key for token storage
166
213
  * @private
167
214
  */
168
- #getTokenRedisKey(email, tokenId) {
169
- return `${this.#config.SESSION_PREFIX}token:${email}:${tokenId}`;
215
+ #getTokenRedisKey(email, tid) {
216
+ return `${this.#config.SESSION_KEY}:t:${email}:${tid}`;
170
217
  }
171
218
 
172
219
  /**
@@ -176,7 +223,7 @@ export class SessionManager {
176
223
  * @private
177
224
  */
178
225
  #getTokenRedisPattern(email) {
179
- return `${this.#config.SESSION_PREFIX}token:${email}:*`;
226
+ return `${this.#config.SESSION_KEY}:t:${email}:*`;
180
227
  }
181
228
 
182
229
  /**
@@ -189,36 +236,32 @@ export class SessionManager {
189
236
 
190
237
  /**
191
238
  * Generate and store JWT token in Redis
239
+ * - JWT payload contains only { email, tid } for minimal size
240
+ * - Full user data is stored in Redis as single source of truth
192
241
  * @param {object} user User object
193
242
  * @returns {Promise<string>} Returns the generated JWT token
194
243
  * @private
195
244
  */
196
245
  async #generateAndStoreToken(user) {
197
246
  // Generate unique token ID for this device/session
198
- const tokenId = crypto.randomUUID();
199
-
200
- // Create JWT token with email and tokenId
247
+ const tid = crypto.randomUUID();
248
+ const ttlSeconds = Math.floor(this.#config.SESSION_AGE / 1000);
249
+ // Create JWT token with only email and tid (minimal payload)
201
250
  const token = await this.#jwtManager.encrypt(
202
- {
203
- email: user.email,
204
- tokenId
205
- },
251
+ { email: user.email, tid },
206
252
  this.#config.SESSION_SECRET,
207
- { expirationTime: this.#config.JWT_EXPIRATION_TIME }
253
+ { expirationTime: ttlSeconds }
208
254
  );
209
-
255
+
210
256
  // Store user data in Redis with TTL
211
- const redisKey = this.#getTokenRedisKey(user.email, tokenId);
212
- const ttlSeconds = Math.floor(this.#config.SESSION_AGE / 1000);
213
-
257
+ const redisKey = this.#getTokenRedisKey(user.email, tid);
258
+
214
259
  await this.#redisManager.getClient().setEx(
215
260
  redisKey,
216
261
  ttlSeconds,
217
262
  JSON.stringify(user)
218
263
  );
219
-
220
- console.debug(`### TOKEN GENERATED: ${user.email} ###`);
221
-
264
+ this.#logger.debug(`### TOKEN GENERATED: ${user.email} ###`);
222
265
  return token;
223
266
  }
224
267
 
@@ -247,14 +290,14 @@ export class SessionManager {
247
290
  this.#config.SESSION_SECRET
248
291
  );
249
292
 
250
- // Extract email and tokenId
251
- const { email, tokenId } = payload;
252
- if (!email || !tokenId) {
293
+ // Extract email and token ID
294
+ const { email, tid } = payload;
295
+ if (!email || !tid) {
253
296
  throw new CustomError(httpCodes.UNAUTHORIZED, 'Invalid token payload');
254
297
  }
255
298
 
256
299
  // Lookup user in Redis
257
- const redisKey = this.#getTokenRedisKey(email, tokenId);
300
+ const redisKey = this.#getTokenRedisKey(email, tid);
258
301
  const userData = await this.#redisManager.getClient().get(redisKey);
259
302
 
260
303
  if (!userData) {
@@ -275,7 +318,7 @@ export class SessionManager {
275
318
 
276
319
  } catch (error) {
277
320
  if (isDebugging) {
278
- console.warn('### TOKEN VERIFICATION FAILED (debugging mode) ###', error.message);
321
+ this.#logger.warn('### TOKEN VERIFICATION FAILED (debugging mode) ###', error.message);
279
322
  return next();
280
323
  }
281
324
 
@@ -331,11 +374,11 @@ export class SessionManager {
331
374
  throw new CustomError(httpCodes.UNAUTHORIZED, 'User not authenticated');
332
375
  }
333
376
 
334
- // Extract tokenId from current token
377
+ // Extract Token ID from current token
335
378
  const authHeader = req.headers.authorization;
336
379
  const token = authHeader?.substring(7);
337
380
  const { payload } = await this.#jwtManager.decrypt(token, this.#config.SESSION_SECRET);
338
- const oldTokenId = payload.tokenId;
381
+ const { tid: oldTokenId } = payload;
339
382
 
340
383
  // Check refresh lock
341
384
  if (this.hasLock(email)) {
@@ -376,7 +419,7 @@ export class SessionManager {
376
419
  const oldRedisKey = this.#getTokenRedisKey(email, oldTokenId);
377
420
  await this.#redisManager.getClient().del(oldRedisKey);
378
421
 
379
- console.debug('### TOKEN REFRESHED SUCCESSFULLY ###');
422
+ this.#logger.debug('### TOKEN REFRESHED SUCCESSFULLY ###');
380
423
 
381
424
  // Return new token
382
425
  return res.json({
@@ -450,7 +493,7 @@ export class SessionManager {
450
493
  await this.#redisManager.getClient().del(keys);
451
494
  }
452
495
 
453
- console.info(`### ALL TOKENS LOGGED OUT: ${email} (${keys.length} tokens) ###`);
496
+ this.#logger.info(`### ALL TOKENS LOGGED OUT: ${email} (${keys.length} tokens) ###`);
454
497
 
455
498
  if (isRedirect) {
456
499
  return res.redirect(this.#config.SSO_SUCCESS_URL);
@@ -461,7 +504,7 @@ export class SessionManager {
461
504
  redirect_url: this.#config.SSO_SUCCESS_URL
462
505
  });
463
506
  } catch (error) {
464
- console.error('### LOGOUT ALL TOKENS ERROR ###', error);
507
+ this.#logger.error('### LOGOUT ALL TOKENS ERROR ###', error);
465
508
  if (isRedirect) {
466
509
  return res.redirect(this.#config.SSO_FAILURE_URL);
467
510
  }
@@ -487,7 +530,7 @@ export class SessionManager {
487
530
  }
488
531
 
489
532
  try {
490
- // Extract tokenId from current token
533
+ // Extract Token ID from current token
491
534
  const authHeader = req.headers.authorization;
492
535
  const token = authHeader?.substring(7);
493
536
 
@@ -496,13 +539,17 @@ export class SessionManager {
496
539
  }
497
540
 
498
541
  const { payload } = await this.#jwtManager.decrypt(token, this.#config.SESSION_SECRET);
499
- const { email, tokenId } = payload;
542
+ const { email, tid } = payload;
543
+
544
+ if (!email || !tid) {
545
+ throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid token payload');
546
+ }
500
547
 
501
548
  // Remove token from Redis
502
- const redisKey = this.#getTokenRedisKey(email, tokenId);
549
+ const redisKey = this.#getTokenRedisKey(email, tid);
503
550
  await this.#redisManager.getClient().del(redisKey);
504
551
 
505
- console.info('### TOKEN LOGOUT SUCCESSFULLY ###');
552
+ this.#logger.info('### TOKEN LOGOUT SUCCESSFULLY ###');
506
553
 
507
554
  if (isRedirect) {
508
555
  return res.redirect(this.#config.SSO_SUCCESS_URL);
@@ -513,7 +560,7 @@ export class SessionManager {
513
560
  });
514
561
 
515
562
  } catch (error) {
516
- console.error('### TOKEN LOGOUT ERROR ###', error);
563
+ this.#logger.error('### TOKEN LOGOUT ERROR ###', error);
517
564
  if (isRedirect) {
518
565
  return res.redirect(this.#config.SSO_FAILURE_URL);
519
566
  }
@@ -535,16 +582,16 @@ export class SessionManager {
535
582
  try {
536
583
  res.clearCookie('connect.sid');
537
584
  } catch (error) {
538
- console.error('### CLEAR COOKIE ERROR ###');
539
- console.error(error);
585
+ this.#logger.error('### CLEAR COOKIE ERROR ###');
586
+ this.#logger.error(error);
540
587
  }
541
588
  return req.session.destroy((sessionError) => {
542
589
  if (sessionError) {
543
- console.error('### SESSION DESTROY CALLBACK ERROR ###');
544
- console.error(sessionError);
590
+ this.#logger.error('### SESSION DESTROY CALLBACK ERROR ###');
591
+ this.#logger.error(sessionError);
545
592
  return callback(sessionError);
546
593
  }
547
- console.info('### SESSION LOGOUT SUCCESSFULLY ###');
594
+ this.#logger.info('### SESSION LOGOUT SUCCESSFULLY ###');
548
595
  return callback(null);
549
596
  });
550
597
  }
@@ -573,7 +620,7 @@ export class SessionManager {
573
620
  */
574
621
  #redisSession() {
575
622
  // Redis Session
576
- console.log('### Using Redis as the Session Store ###');
623
+ this.#logger.log('### Using Redis as the Session Store ###');
577
624
  return session({
578
625
  cookie: { maxAge: this.#config.SESSION_AGE, path: this.#config.SESSION_COOKIE_PATH, sameSite: false },
579
626
  store: new RedisStore({ client: this.#redisManager.getClient(), prefix: this.#config.SESSION_PREFIX, disableTouch: true }),
@@ -588,7 +635,7 @@ export class SessionManager {
588
635
  */
589
636
  #memorySession() {
590
637
  // Memory Session
591
- console.log('### Using Memory as the Session Store ###');
638
+ this.#logger.log('### Using Memory as the Session Store ###');
592
639
  const MemoryStore = memStore(session);
593
640
  return session({
594
641
  cookie: { maxAge: this.#config.SESSION_AGE, path: this.#config.SESSION_COOKIE_PATH, sameSite: false },
@@ -675,13 +722,13 @@ export class SessionManager {
675
722
  /** @type {{ payload: { user: import('../models/types/user').UserModel, redirect_url: string } }} */
676
723
  const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
677
724
  if (payload?.user) {
678
- console.debug('### CALLBACK USER ###');
725
+ this.#logger.debug('### CALLBACK USER ###');
679
726
  request.session[this.#getSessionKey()] = initUser(payload.user);
680
727
  return new Promise((resolve, reject) => {
681
728
  request.session.touch().save((err) => {
682
729
  if (err) {
683
- console.error('### SESSION SAVE ERROR ###');
684
- console.error(err);
730
+ this.#logger.error('### SESSION SAVE ERROR ###');
731
+ this.#logger.error(err);
685
732
  return reject(new CustomError(httpCodes.SYSTEM_FAILURE, 'Session failed to save', err));
686
733
  }
687
734
  return resolve(payload);
@@ -711,7 +758,9 @@ export class SessionManager {
711
758
  throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
712
759
  }
713
760
 
761
+ /** @type {import('../index.js').SessionUser} */
714
762
  const user = initUser(payload.user);
763
+ /** @type {string} */
715
764
  const redirectUrl = payload.redirect_url || this.#config.SSO_SUCCESS_URL;
716
765
 
717
766
  // Check SESSION_MODE to determine response type
@@ -719,57 +768,26 @@ export class SessionManager {
719
768
  // Token-based: Generate token and return HTML page that stores it
720
769
  const token = await this.#generateAndStoreToken(user);
721
770
 
722
- console.debug('### CALLBACK TOKEN GENERATED ###');
771
+ this.#logger.debug('### CALLBACK TOKEN GENERATED ###');
723
772
 
773
+ const templatePath = this.#config.TOKEN_STORAGE_TEMPLATE_PATH || path.resolve(__dirname, 'assets', 'template.html');
724
774
  // Return HTML page that stores token in localStorage and redirects
725
- return res.send(`
726
- <!DOCTYPE html>
727
- <html>
728
- <head>
729
- <meta charset="UTF-8">
730
- <title>Authentication Complete</title>
731
- <script>
732
- (function() {
733
- try {
734
- // Store auth data in localStorage
735
- localStorage.setItem('authToken', ${JSON.stringify(token)});
736
- localStorage.setItem('tokenExpiry', ${Date.now() + this.#config.SESSION_AGE});
737
- localStorage.setItem('user', ${JSON.stringify({
738
- email: user.email,
739
- name: user.name,
740
- })});
741
-
742
- // Redirect to original destination
743
- window.location.replace(${JSON.stringify(redirectUrl)});
744
- } catch (error) {
745
- console.error('Failed to store authentication:', error);
746
- document.getElementById('error').style.display = 'block';
747
- }
748
- })();
749
- </script>
750
- <style>
751
- body { font-family: system-ui, sans-serif; text-align: center; padding: 50px; }
752
- #error { display: none; color: #d32f2f; margin-top: 20px; }
753
- </style>
754
- </head>
755
- <body>
756
- <p>Completing authentication...</p>
757
- <div id="error">
758
- <p>Authentication failed. Please try again.</p>
759
- <a href="${this.#config.SSO_FAILURE_URL}">Return to login</a>
760
- </div>
761
- </body>
762
- </html>
763
- `);
764
- }
765
- else {
766
- // Session-based: Save to session and redirect
767
- await this.#saveSession(req, jwt, initUser);
768
- return res.redirect(redirectUrl);
775
+ const template = fs.readFileSync(templatePath, 'utf8');
776
+ const html = template
777
+ .replaceAll('{{SESSION_DATA_KEY}}', this.#config.SESSION_KEY)
778
+ .replaceAll('{{SESSION_DATA_VALUE}}', token)
779
+ .replaceAll('{{SESSION_EXPIRY_KEY}}', this.#config.SESSION_EXPIRY_KEY)
780
+ .replaceAll('{{SESSION_EXPIRY_VALUE}}', user.attributes.expires_at)
781
+ .replaceAll('{{SSO_SUCCESS_URL}}', redirectUrl)
782
+ .replaceAll('{{SSO_FAILURE_URL}}', this.#config.SSO_FAILURE_URL);
783
+ return res.send(html);
769
784
  }
785
+ // Session-based: Save to session and redirect
786
+ await this.#saveSession(req, jwt, initUser);
787
+ return res.redirect(redirectUrl);
770
788
  }
771
789
  catch (error) {
772
- console.error('### CALLBACK ERROR ###', error);
790
+ this.#logger.error('### CALLBACK ERROR ###', error);
773
791
  return res.redirect(this.#config.SSO_FAILURE_URL.concat('?message=').concat(encodeURIComponent(error.message)));
774
792
  }
775
793
  };
@@ -832,8 +850,8 @@ export class SessionManager {
832
850
  // Session-based authentication is already single-instance per cookie
833
851
  return this.#logoutSession(req, res, (error) => {
834
852
  if (error) {
835
- console.error('### LOGOUT CALLBACK ERROR ###');
836
- console.error(error);
853
+ this.#logger.error('### LOGOUT CALLBACK ERROR ###');
854
+ this.#logger.error(error);
837
855
  if (isRedirect) {
838
856
  return res.redirect(this.#config.SSO_FAILURE_URL);
839
857
  }
package/index.d.ts CHANGED
@@ -7,6 +7,70 @@ import { Application, RequestHandler, Request, Response, NextFunction, Router }
7
7
 
8
8
  export { JWTPayload } from 'jose';
9
9
 
10
+ // Logger class for configurable logging
11
+ export class Logger {
12
+ /**
13
+ * Get or create a Logger instance (singleton pattern)
14
+ * @param componentName Component name for log prefix
15
+ * @param enableLogging Enable/disable logging (defaults to NODE_ENV !== 'production')
16
+ * @returns Logger instance
17
+ */
18
+ static getInstance(componentName: string, enableLogging?: boolean): Logger;
19
+
20
+ /**
21
+ * Clear all logger instances (useful for testing)
22
+ */
23
+ static clearInstances(): void;
24
+
25
+ /**
26
+ * Disable colors globally for all logger instances
27
+ */
28
+ static disableColors(): void;
29
+
30
+ /**
31
+ * Enable colors globally for all logger instances
32
+ */
33
+ static enableColors(): void;
34
+
35
+ /**
36
+ * Create a new Logger instance (backward compatibility)
37
+ * Note: Use Logger.getInstance() for singleton pattern
38
+ * @param componentName Component name for log prefix
39
+ * @param enableLogging Enable/disable logging (defaults to NODE_ENV !== 'production')
40
+ */
41
+ constructor(componentName: string, enableLogging?: boolean);
42
+
43
+ /**
44
+ * Log debug message
45
+ * @param args Arguments to log
46
+ */
47
+ debug(...args: any[]): void;
48
+
49
+ /**
50
+ * Log info message
51
+ * @param args Arguments to log
52
+ */
53
+ info(...args: any[]): void;
54
+
55
+ /**
56
+ * Log warning message
57
+ * @param args Arguments to log
58
+ */
59
+ warn(...args: any[]): void;
60
+
61
+ /**
62
+ * Log error message
63
+ * @param args Arguments to log
64
+ */
65
+ error(...args: any[]): void;
66
+
67
+ /**
68
+ * Log general message
69
+ * @param args Arguments to log
70
+ */
71
+ log(...args: any[]): void;
72
+ }
73
+
10
74
  // Session Mode constants
11
75
  export const SessionMode: {
12
76
  SESSION: string;
@@ -29,13 +93,15 @@ export interface SessionConfig {
29
93
  SESSION_COOKIE_PATH?: string;
30
94
  SESSION_SECRET?: string;
31
95
  SESSION_PREFIX?: string;
96
+ SESSION_KEY?: string;
97
+ SESSION_EXPIRY_KEY?: string;
98
+ TOKEN_STORAGE_TEMPLATE_PATH?: string;
32
99
 
33
100
  REDIS_URL?: string;
34
101
  REDIS_CERT_PATH?: string;
35
102
 
36
103
  JWT_ALGORITHM?: string;
37
104
  JWT_ENCRYPTION?: string;
38
- JWT_EXPIRATION_TIME?: string;
39
105
  JWT_CLOCK_TOLERANCE?: number;
40
106
  JWT_SECRET_HASH_ALGORITHM?: string;
41
107
  JWT_ISSUER?: string;
@@ -241,32 +307,29 @@ export class RedisManager {
241
307
  * Disconnect from Redis
242
308
  * @returns Returns nothing
243
309
  */
244
- disConnect(): Promise<void>;
310
+ disconnect(): Promise<void>;
245
311
  }
246
312
 
247
313
  // JWT Manager Configuration - uses strict UPPERCASE naming convention with JWT_ prefix for all property names
248
314
  export interface JwtManagerOptions {
249
315
  /** JWE algorithm (default: 'dir') */
250
316
  JWT_ALGORITHM?: string;
251
-
317
+
252
318
  /** JWE encryption method (default: 'A256GCM') */
253
319
  JWT_ENCRYPTION?: string;
254
-
255
- /** Token expiration time (default: '10m') */
256
- JWT_EXPIRATION_TIME?: string;
257
-
320
+
258
321
  /** Clock tolerance in seconds for token validation (default: 30) */
259
322
  JWT_CLOCK_TOLERANCE?: number;
260
-
323
+
261
324
  /** Hash algorithm for secret derivation (default: 'SHA-256') */
262
325
  JWT_SECRET_HASH_ALGORITHM?: string;
263
-
326
+
264
327
  /** Optional JWT issuer claim */
265
328
  JWT_ISSUER?: string;
266
-
329
+
267
330
  /** Optional JWT audience claim */
268
331
  JWT_AUDIENCE?: string;
269
-
332
+
270
333
  /** Optional JWT subject claim */
271
334
  JWT_SUBJECT?: string;
272
335
  }
@@ -282,7 +345,7 @@ export interface JwtEncryptOptions {
282
345
  encryption?: string;
283
346
 
284
347
  /** Override default expiration time */
285
- expirationTime?: string;
348
+ expirationTime?: number;
286
349
 
287
350
  /** Override default hash algorithm */
288
351
  secretHashAlgorithm?: string;
@@ -323,7 +386,7 @@ export type JwtDecryptResult = JWTDecryptResult<EncryptJWT>;
323
386
  export class JwtManager {
324
387
  algorithm: string;
325
388
  encryption: string;
326
- expirationTime: string;
389
+ expirationTime: number;
327
390
  clockTolerance: number;
328
391
  secretHashAlgorithm: string;
329
392
  issuer?: string;
package/index.js CHANGED
@@ -3,3 +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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igxjs/node-components",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "Node components for igxjs",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -26,14 +26,14 @@
26
26
  "axios": "^1.13.6",
27
27
  "connect-redis": "^9.0.0",
28
28
  "express-session": "^1.19.0",
29
- "jose": "^6.2.0",
29
+ "jose": "^6.2.1",
30
30
  "memorystore": "^1.6.7"
31
31
  },
32
32
  "devDependencies": {
33
33
  "chai": "^6.2.2",
34
34
  "express": "^5.2.1",
35
35
  "mocha": "^12.0.0-beta-10",
36
- "sinon": "^21.0.2",
36
+ "sinon": "^21.0.3",
37
37
  "supertest": "^7.0.0"
38
38
  },
39
39
  "files": [