@igxjs/node-components 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,167 @@
1
+ import { STATUS_CODES } from 'node:http';
2
+
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
+ SYSTEM_FAILURE: 'System Error',
14
+ };
15
+
16
+ export const httpCodes = {
17
+ OK: 200,
18
+ CREATED: 201,
19
+ NO_CONTENT: 201,
20
+ BAD_REQUEST: 400,
21
+ UNAUTHORIZED: 401,
22
+ FORBIDDEN: 403,
23
+ NOT_FOUND: 404,
24
+ NOT_ACCEPTABLE: 406,
25
+ CONFLICT: 409,
26
+ SYSTEM_FAILURE: 500,
27
+ };
28
+
29
+ export class CustomError extends Error {
30
+ /** @type {number} */
31
+ code;
32
+ /** @type {object} */
33
+ data;
34
+ /** @type {object} */
35
+ error;
36
+ /**
37
+ * Construct a custom error
38
+ * @param {number} code Error code
39
+ * @param {string} message Message
40
+ */
41
+ constructor(code, message, error = {}, data = {}) {
42
+ super(message);
43
+ this.code = code;
44
+ this.error = error;
45
+ this.data = data;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Custom error handler
51
+ *
52
+ * @param {CustomError} err Error
53
+ * @param {import('@types/express').Request} req Request
54
+ * @param {import('@types/express').Response} res Response
55
+ * @param {import('@types/express').NextFunction} next Next
56
+ */
57
+ export const httpErrorHandler = (err, req, res, next) => {
58
+ // If no error, pass to next middleware
59
+ if (!err) {
60
+ return next();
61
+ }
62
+
63
+ // Build response object with defaults
64
+ const responseBody = {
65
+ status: err.code || httpCodes.BAD_REQUEST,
66
+ message: err.message || httpMessages.BAD_REQUEST,
67
+ };
68
+
69
+ // Merge additional error data if present
70
+ if (err.data && typeof err.data === 'object') {
71
+ Object.entries(err.data).forEach(([key, value]) => {
72
+ responseBody[key] = value;
73
+ });
74
+ }
75
+
76
+ // Set CORS and custom headers
77
+ res.header('Access-Control-Expose-Headers', '*');
78
+ res.header('X-IBM-Override-Error-Pages', 'not-found, client-error, server-error');
79
+
80
+ // Send error response
81
+ res.status(responseBody.status).json(responseBody);
82
+
83
+ // Log error details
84
+ console.error('### ERROR ###');
85
+ console.error(`${req.method} ${req.path}`);
86
+
87
+ // Log based on error type
88
+ if ([httpCodes.UNAUTHORIZED, httpCodes.FORBIDDEN, httpCodes.NOT_FOUND].includes(err.code)) {
89
+ console.error('>>> Auth Error:', err.message);
90
+ } else {
91
+ console.error('>>> Name:', err.name);
92
+ console.error('>>> Message:', err.message);
93
+ if (err.stack) {
94
+ console.error('>>> Stack:', err.stack);
95
+ }
96
+ }
97
+
98
+ console.error('### /ERROR ###');
99
+ };
100
+
101
+ /**
102
+ * Extract HTTP status code from error
103
+ * @param {Error} error Error object
104
+ * @returns {number} HTTP status code
105
+ */
106
+ const _getErrorCode = (error) => {
107
+ const statusCode = error.response?.status;
108
+ // Validate it's a valid HTTP status code
109
+ if (statusCode && typeof statusCode === 'number' && Object.hasOwn(STATUS_CODES, statusCode)) {
110
+ return statusCode;
111
+ }
112
+ return httpCodes.SYSTEM_FAILURE;
113
+ };
114
+
115
+ /**
116
+ * Extract error message from error
117
+ * @param {Error} error Error object
118
+ * @param {string} defaultMessage Default message
119
+ * @returns {string} Error message
120
+ */
121
+ const _getErrorMessage = (error, defaultMessage) => {
122
+ // Priority: response.data.message > response.statusText > error.message > default
123
+ return error.response?.data?.message
124
+ || error.response?.statusText
125
+ || error.message
126
+ || defaultMessage;
127
+ };
128
+
129
+ export const httpHelper = {
130
+ format (str, ...args) {
131
+ const matched = str.match(/{\d}/ig);
132
+ matched.forEach((element, index) => {
133
+ if(args.length > index)
134
+ str = str.replace(element, args[index]);
135
+ });
136
+ return str;
137
+ },
138
+
139
+ /**
140
+ * Generate friendly Zod message
141
+ * @param {Error} error Zod error
142
+ * @returns {string} Returns friendly Zod message
143
+ */
144
+ toZodMessage(error) {
145
+ return error.issues
146
+ .map(issue => {
147
+ const path = issue.path.join('.');
148
+ return (path ? '['.concat(path).concat('] ').concat('') : '').concat(issue.message);
149
+ })
150
+ .join('; ');
151
+ },
152
+
153
+ /**
154
+ * Try to analyze axios Error
155
+ * @param {Error | import('axios').AxiosError} error Error object
156
+ * @param {string} defaultMessage Default error message
157
+ * @returns {CustomError} Returns CustomError instance
158
+ */
159
+ handleAxiosError(error, defaultMessage = 'An error occurred') {
160
+ console.warn(`### TRY ERROR: ${defaultMessage} ###`);
161
+ // Extract error details
162
+ const errorCode = _getErrorCode(error);
163
+ const errorMessage = _getErrorMessage(error, defaultMessage);
164
+ const errorData = error.response?.data || {};
165
+ return new CustomError(errorCode, errorMessage, error, errorData);
166
+ }
167
+ };
@@ -0,0 +1,76 @@
1
+ import fs from 'node:fs';
2
+
3
+ import { createClient } from '@redis/client';
4
+
5
+ export class RedisManager {
6
+ /** @type {import('@redis/client').RedisClientType} */
7
+ #client = null;
8
+ /**
9
+ * Connect with Redis
10
+ * @returns {Promise<boolean>} Returns `true` if Redis server is connected
11
+ */
12
+ async connect(redisUrl, certPath) {
13
+ if (redisUrl?.length > 0) {
14
+ try {
15
+ /** @type {import('@redis/client').RedisClientOptions} */
16
+ const options = {
17
+ url: redisUrl,
18
+ socket: { tls: redisUrl.includes('rediss:'), ca: null },
19
+ };
20
+ if(options.socket.tls) {
21
+ const caCert = fs.readFileSync(certPath);
22
+ options.socket.ca = caCert;
23
+ }
24
+ this.#client = createClient(options);
25
+ this.#client.on('ready', () => {
26
+ console.info('### REDIS READY ###');
27
+ });
28
+ this.#client.on('reconnecting', (_res) => {
29
+ console.warn('### REDIS RECONNECTING ###');
30
+ });
31
+ this.#client.on('error', (error) => {
32
+ console.error(`### REDIS ERROR: ${error.message} ###`);
33
+ });
34
+ await this.#client.connect();
35
+ return true;
36
+ }
37
+ catch (error) {
38
+ console.error('### REDIS CONNECT ERROR ###');
39
+ console.error(error);
40
+ }
41
+ }
42
+ return false;
43
+ }
44
+
45
+ /**
46
+ * Get Redis client
47
+ * @returns {import('@redis/client').RedisClientType} Returns Redis client
48
+ */
49
+ getClient() {
50
+ return this.#client;
51
+ }
52
+
53
+ /**
54
+ * Determine if the Redis server is connected
55
+ * @returns {boolean} Returns `true` if Redis server is connected
56
+ */
57
+ async isConnected() {
58
+ if (this.#client)
59
+ try {
60
+ const pongMessage = await this.#client.ping();
61
+ return 'PONG' === pongMessage;
62
+ } catch (error) {
63
+ console.error(`### REDIS PING ERROR ###`);
64
+ console.error(error);
65
+ }
66
+ return false;
67
+ }
68
+
69
+ /**
70
+ * Disconnect with Redis
71
+ * @returns {Promise<void>} Returns nothing
72
+ */
73
+ disConnect() {
74
+ return this.#client.quit();
75
+ }
76
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * FlexRouter for expressjs
3
+ *
4
+ * @example
5
+ * import { Router } from 'express';
6
+ * import { FlexRouter } from '../models/router.js';
7
+ * import { authenticate } from '../middlewares/common.js';
8
+ *
9
+ * export const publicRouter = Router();
10
+ * export const privateRouter = Router();
11
+ *
12
+ * publicRouter.get('/health', (req, res) => {
13
+ * res.send('OK');
14
+ * });
15
+ *
16
+ * privateRouter.get('/', (req, res) => {
17
+ * res.send('Hello World!');
18
+ * });
19
+ *
20
+ * export const routers = [
21
+ * new FlexRouter('/api/v1/protected', privateRouter, [authenticate]),
22
+ * new FlexRouter('/api/v1/public', healthRouter),
23
+ * ];
24
+ */
25
+ export class FlexRouter {
26
+ /** @type {string} */
27
+ context = '';
28
+ /** @type {import('@types/express').Router} */
29
+ router = null;
30
+ /** @type {import('@types/express').RequestHandler[]} */
31
+ handlers = [];
32
+
33
+ /**
34
+ * Constructor
35
+ * @param {string} context Context path
36
+ * @param {import('@types/express').Router} router Router instance
37
+ * @param {import('@types/express').RequestHandler[]} handlers Request handlers
38
+ */
39
+ constructor(context, router, handlers = []) {
40
+ this.context = context;
41
+ this.router = router;
42
+ this.handlers = handlers;
43
+ }
44
+
45
+ /**
46
+ * Mount router
47
+ * @param {import('@types/express').Express} app Express app
48
+ * @param {string} basePath Base path
49
+ */
50
+ mount(app, basePath) {
51
+ const path = basePath.concat(this.context);
52
+ app.use(path, this.handlers, this.router);
53
+ }
54
+ }
@@ -0,0 +1,376 @@
1
+ import axios from 'axios';
2
+ import session from 'express-session';
3
+ import { jwtDecrypt } from 'jose';
4
+ import { RedisStore } from 'connect-redis';
5
+ import memStore from 'memorystore';
6
+
7
+ import { CustomError, httpCodes, httpHelper, httpMessages } from './http-handlers.js';
8
+ import { RedisManager } from './redis.js';
9
+
10
+ const MemoryStore = memStore(session);
11
+
12
+ export class SessionConfig {
13
+ /** @type {string} */
14
+ SSO_ENDPOINT_URL;
15
+ /** @type {string} */
16
+ SSO_CLIENT_ID;
17
+ /** @type {string} */
18
+ SSO_CLIENT_SECRET;
19
+ /** @type {string} */
20
+ SSO_SUCCESS_URL;
21
+ /** @type {string} */
22
+ SSO_FAILURE_URL;
23
+
24
+ /** @type {number} */
25
+ SESSION_AGE;
26
+ /** @type {string} */
27
+ SESSION_COOKIE_PATH;
28
+ /** @type {string} */
29
+ SESSION_SECRET;
30
+ /** @type {string} */
31
+ SESSION_PREFIX;
32
+
33
+ /** @type {string} */
34
+ REDIS_URL;
35
+ /** @type {string} */
36
+ REDIS_CERT_PATH;
37
+ }
38
+
39
+ export class SessionManager {
40
+ /** @type {SessionConfig} */
41
+ #config = null;
42
+ /** @type {Map<string, number>} */
43
+ #sessionRefreshLocks = new Map();
44
+ /** @type {import('./redis.js').RedisManager} */
45
+ #redisManager = null;
46
+ /** @type {import('axios').AxiosInstance} */
47
+ #idpRequest = null;
48
+
49
+ /**
50
+ * Check if the email has a session refresh lock
51
+ * @param {string} email Email address
52
+ * @returns {boolean} Returns true if the email has a session refresh lock
53
+ */
54
+ hasLock(email) {
55
+ return this.#sessionRefreshLocks.has(email) && this.#sessionRefreshLocks.get(email) > Date.now();
56
+ }
57
+
58
+ /**
59
+ * Lock the email for session refresh
60
+ * @param {string} email Email address
61
+ */
62
+ lock(email) {
63
+ if (email) {
64
+ this.#sessionRefreshLocks.set(email, Date.now() + 60000);
65
+ }
66
+ this.clearLocks();
67
+ }
68
+
69
+ /**
70
+ * Clear session refresh locks
71
+ */
72
+ clearLocks() {
73
+ return setTimeout(() => {
74
+ for (const email of this.#sessionRefreshLocks.keys()) {
75
+ if (this.hasLock(email)) {
76
+ continue;
77
+ }
78
+ this.#sessionRefreshLocks.delete(email);
79
+ }
80
+ }, 1000);
81
+ }
82
+
83
+ /**
84
+ * Get session key
85
+ * @returns {string} Returns the session key
86
+ */
87
+ #getSessionKey() {
88
+ return 'user';
89
+ }
90
+
91
+ /**
92
+ * Get RedisManager instance
93
+ * @returns {import('./redis.js').RedisManager} Returns the RedisManager instance
94
+ */
95
+ redisManager() {
96
+ return this.#redisManager;
97
+ }
98
+
99
+ /**
100
+ * Setup the session/user handlers with configurations
101
+ * @param {import('@types/express').Application} app Express application
102
+ * @param {SessionConfig} config Redis configurations
103
+ * @param {(user: object) => object} updateUser Update user object if user should have proper attributes, e.g. permissions, avatar URL
104
+ */
105
+ async setup(app, config, updateUser) {
106
+ this.#redisManager = new RedisManager();
107
+ this.#config = {
108
+ // Session
109
+ SESSION_AGE: config.SESSION_AGE || 64800000,
110
+ SESSION_COOKIE_PATH: config.SESSION_COOKIE_PATH || '/',
111
+ SESSION_SECRET: config.SESSION_SECRET,
112
+ SESSION_PREFIX: config.SESSION_PREFIX || 'ibmid:',
113
+ // Identity Provider
114
+ SSO_ENDPOINT_URL: config.SSO_ENDPOINT_URL,
115
+ SSO_CLIENT_ID: config.SSO_CLIENT_ID,
116
+ SSO_CLIENT_SECRET: config.SSO_CLIENT_SECRET,
117
+ SSO_SUCCESS_URL: config.SSO_SUCCESS_URL,
118
+ SSO_FAILURE_URL: config.SSO_FAILURE_URL,
119
+ // Redis
120
+ REDIS_URL: config.REDIS_URL,
121
+ REDIS_CERT_PATH: config.REDIS_CERT_PATH,
122
+ };
123
+ // Identity Provider Request
124
+ this.#idpRequest = axios.create({
125
+ baseURL: this.#config.SSO_ENDPOINT_URL,
126
+ timeout: 30000,
127
+ });
128
+ app.set('trust proxy', 1);
129
+ app.use(await this.sessionHandler());
130
+ app.use(this.#userHandler(updateUser));
131
+ }
132
+
133
+ /**
134
+ * Get Redis session RequestHandler
135
+ * @returns {import('@types/express').RequestHandler} Returns RequestHandler instance of Express
136
+ */
137
+ #redisSession() {
138
+ // Redis Session
139
+ console.log('### Using Redis as the Session Store ###');
140
+ return session({
141
+ cookie: { maxAge: this.#config.SESSION_AGE, path: this.#config.SESSION_COOKIE_PATH, sameSite: false },
142
+ store: new RedisStore({ client: this.#redisManager.getClient(), prefix: this.#config.SESSION_PREFIX, disableTouch: true }),
143
+ resave: false, saveUninitialized: false,
144
+ secret: this.#config.SESSION_SECRET,
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Get Memory session RequestHandler
150
+ * @returns {import('@types/express').RequestHandler} Returns RequestHandler instance of Express
151
+ */
152
+ #memorySession() {
153
+ // Memory Session
154
+ console.log('### Using Memory as the Session Store ###');
155
+ return session({
156
+ cookie: { maxAge: this.#config.SESSION_AGE, path: this.#config.SESSION_COOKIE_PATH, sameSite: false },
157
+ store: new MemoryStore({}),
158
+ resave: false, saveUninitialized: false,
159
+ secret: this.#config.SESSION_SECRET,
160
+ });
161
+ }
162
+
163
+ /**
164
+ * Get session RequestHandler
165
+ * @returns {Promise<import('@types/express').RequestHandler>} Returns RequestHandler instance of Express
166
+ */
167
+ async sessionHandler() {
168
+ if(this.#config.REDIS_URL?.length > 0) {
169
+ await this.#redisManager.connect(this.#config.REDIS_URL, this.#config.REDIS_CERT_PATH);
170
+ return this.#redisSession();
171
+ }
172
+ return this.#memorySession();
173
+ }
174
+
175
+ /**
176
+ * User HTTP Handler
177
+ * @param {(user: object) => object} updateUser User wrapper
178
+ * @returns {import('@types/express').RequestHandler} Returns express Request Handler
179
+ */
180
+ #userHandler (updateUser) {
181
+ return (req, res, next) => {
182
+ req.user = req.session[this.#getSessionKey()];
183
+ /** @type {import('@types/express').Request & { user: object }} Session user */
184
+ res.locals.user = updateUser(req.user);
185
+ return next();
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Resource protection
191
+ * @param {boolean} [isDebugging=false] Debugging flag
192
+ * @param {boolean} [redirectUrl=''] Redirect flag
193
+ * @returns {import('@types/express').RequestHandler} Returns express Request Handler
194
+ */
195
+ authenticate (isDebugging = false, redirectUrl = '') {
196
+ return async (req, res, next) => {
197
+ /** @type {{ authorized: boolean }} */
198
+ const { authorized = isDebugging } = req.user ?? { authorized: isDebugging };
199
+ if (authorized) {
200
+ return next();
201
+ }
202
+ if(redirectUrl) {
203
+ return res.redirect(redirectUrl);
204
+ }
205
+ return next(new CustomError(httpCodes.UNAUTHORIZED, httpMessages.UNAUTHORIZED));
206
+ };
207
+ };
208
+
209
+ /**
210
+ * Save session
211
+ * @param {import('@types/express').Request} request Request object
212
+ * @param {string} jwt JWT
213
+ * @param {(user: object) => object} initUser Redirect URL
214
+ * @returns {Promise<{ user: import('../models/types/user').UserModel, redirect_url: string }>} Promise
215
+ */
216
+ #saveSession = async (request, jwt, initUser) => {
217
+ /** @type {{ payload: { user: import('../models/types/user').UserModel, redirect_url: string } }} */
218
+ const { payload } = await this.#decryptJWT(jwt, this.#config.SSO_CLIENT_SECRET);
219
+ if (payload?.user) {
220
+ console.debug('### CALLBACK USER ###');
221
+ request.session[this.#getSessionKey()] = initUser(payload.user);
222
+ return new Promise((resolve, reject) => {
223
+ request.session.touch().save((err) => {
224
+ if (err) {
225
+ console.error('### SESSION SAVE ERROR ###');
226
+ console.error(err);
227
+ return reject(new CustomError(httpCodes.SYSTEM_FAILURE, 'Session failed to save', err));
228
+ }
229
+ return resolve(payload);
230
+ });
231
+ });
232
+ }
233
+ throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
234
+ };
235
+
236
+ /**
237
+ * SSO callback for successful login
238
+ * @param {(user: object) => object} initUser Initialize user object function
239
+ * @returns {import('@types/express').RequestHandler} Returns express Request Handler
240
+ */
241
+ callback(initUser) {
242
+ return async (req, res, next) => {
243
+ const { jwt = '' } = req.query;
244
+ if(!jwt) {
245
+ return next(new CustomError(httpCodes.BAD_REQUEST, 'Missing `jwt` in query parameters'));
246
+ }
247
+ try {
248
+ const payload = await this.#saveSession(req, jwt, initUser);
249
+ return res.redirect(payload?.redirect_url ? payload.redirect_url : this.#config.SSO_SUCCESS_URL);
250
+ }
251
+ catch (error) {
252
+ console.error('### LOGIN ERROR ###');
253
+ console.error(error);
254
+ return res.redirect(this.#config.SSO_FAILURE_URL.concat('?message=').concat(encodeURIComponent(error.message)));
255
+ }
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Get Identity Providers
261
+ * @returns {import('@types/express').RequestHandler} Returns express Request Handler
262
+ */
263
+ identityProviders() {
264
+ const idpUrl = '/auth/providers'.concat('?client_id=').concat(this.#config.SSO_CLIENT_ID);
265
+ return async (_req, res, next) => {
266
+ try {
267
+ const response = await this.#idpRequest.get(idpUrl);
268
+ if(response.status === httpCodes.OK) {
269
+ return res.json(response.data);
270
+ }
271
+ throw new CustomError(response.status, response.statusText);
272
+ }
273
+ catch(error) {
274
+ return next(httpHelper.handleAxiosError(error));
275
+ }
276
+ };
277
+ }
278
+
279
+ /**
280
+ * Application logout (NOT SSO)
281
+ * @returns {import('@types/express').RequestHandler} Returns express Request Handler
282
+ */
283
+ logout() {
284
+ return (req, res) => {
285
+ const { redirect = false } = req.query;
286
+ const isRedirect = (redirect === 'true' || redirect === true);
287
+ return this.#logout(req, res, (error => {
288
+ if (error) {
289
+ console.error('### LOGOUT CALLBACK ERROR ###');
290
+ console.error(error);
291
+ if (isRedirect)
292
+ return res.redirect(this.#config.SSO_FAILURE_URL);
293
+ return res.status(httpCodes.AUTHORIZATION_FAILED).send({ redirect_url: this.#config.SSO_FAILURE_URL });
294
+ }
295
+ if (isRedirect)
296
+ return res.redirect(this.#config.SSO_SUCCESS_URL);
297
+ return res.send({ redirect_url: this.#config.SSO_SUCCESS_URL });
298
+ }));
299
+ };
300
+ }
301
+
302
+ /**
303
+ * Refresh user session
304
+ * @param {(user: object) => object} initUser Initialize user object function
305
+ * @returns {import('@types/express').RequestHandler} Returns express Request Handler
306
+ */
307
+ refresh(initUser) {
308
+ const idpUrl = '/auth/refresh'.concat('?client_id=').concat(this.#config.SSO_CLIENT_ID);
309
+ return async (req, res, next) => {
310
+ try {
311
+ const { email, attributes } = req.user || { email: '', attributes: {} };
312
+ if (this.hasLock(email)) {
313
+ throw new CustomError(httpCodes.CONFLICT, 'User refresh is locked');
314
+ }
315
+ this.lock(email);
316
+ const response = await this.#idpRequest.post(idpUrl, {
317
+ user: {
318
+ email,
319
+ attributes: {
320
+ idp: attributes?.idp,
321
+ refresh_token: attributes?.refresh_token
322
+ },
323
+ }
324
+ });
325
+ if(response.status === httpCodes.OK) {
326
+ /** @type {{ jwt: string }} */
327
+ const { jwt } = response.data;
328
+ const payload = await this.#saveSession(req, jwt, initUser);
329
+ return res.json(payload);
330
+ }
331
+ throw new CustomError(response.status, response.statusText);
332
+ }
333
+ catch(error) {
334
+ return next(httpHelper.handleAxiosError(error));
335
+ }
336
+ };
337
+ }
338
+
339
+ /**
340
+ * Logout
341
+ * @param {import('@types/express').Request} req Request
342
+ * @param {import('@types/express').Response} res Response
343
+ * @param {(error: Error)} callback Callback
344
+ */
345
+ #logout(req, res, callback) {
346
+ try {
347
+ res.clearCookie('connect.sid');
348
+ } catch (error) {
349
+ console.error('### CLEAR COOKIE ERROR ###');
350
+ console.error(error);
351
+ }
352
+ return req.session.destroy((sessionError) => {
353
+ if (sessionError) {
354
+ console.error('### SESSION DESTROY CALLBACK ERROR ###');
355
+ console.error(sessionError);
356
+ return callback(sessionError);
357
+ }
358
+ console.info('### LOGOUT SUCCESSFULLY ###');
359
+ return callback(null);
360
+ });
361
+ }
362
+
363
+ /**
364
+ * Decrypt JWT data for user session
365
+ * @param {string} data JWT data
366
+ * @param {string} input Input string for encryption
367
+ * @returns {Promise<import('jose').JWTDecryptResult<import('jose').EncryptJWT>>} Returns decrypted JWT payload
368
+ */
369
+ async #decryptJWT(data, input) {
370
+ const secret = await crypto.subtle.digest(
371
+ 'SHA-256',
372
+ new TextEncoder().encode(input)
373
+ );
374
+ return await jwtDecrypt(data, new Uint8Array(secret), { clockTolerance: 30 });
375
+ }
376
+ }