mbkauthe 4.3.0 → 4.3.2

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/docs/api.md CHANGED
@@ -108,7 +108,7 @@ When a user logs in, MBKAuthe creates a session and sets the following cookies:
108
108
  | Cookie Name | Description | HttpOnly | Secure | SameSite |
109
109
  |------------|-------------|----------|--------|----------|
110
110
  | `mbkauthe.sid` | Session identifier | ✓ | Auto* | lax |
111
- | `sessionId` | Encrypted session token (AES-256-GCM). This cookie is encrypted and treated as an opaque value by clients; do not attempt to parse or rely on the raw cookie contents. Use server endpoints (e.g., `/mbkauthe/api/checkSession`) to validate or query session information. | ✓ | Auto* | lax |
111
+ | `sessionId` | Encrypted session token (AES-256-GCM). This cookie is encrypted and treated as an opaque value by clients; do not attempt to parse or rely on the raw cookie contents. Use server endpoints (e.g., `GET /mbkauthe/api/checkSession`, `POST /mbkauthe/api/checkSession` (body) or `POST /mbkauthe/api/verifySession`) to validate or query session information. | ✓ | Auto* | lax |
112
112
  | `username` | Username | ✗ | Auto* | lax |
113
113
 
114
114
  \* `secure` flag is automatically set to `true` in production when `IS_DEPLOYED=true`
@@ -301,6 +301,97 @@ fetch('/mbkauthe/api/checkSession')
301
301
 
302
302
  ---
303
303
 
304
+ #### `POST /mbkauthe/api/checkSession` (body)
305
+
306
+ Validate a session by providing a session identifier in the request body. Useful for server-to-server checks or when you have an encrypted `sessionId` value from a cookie and need to validate it server-side.
307
+
308
+ **Rate Limit:** 8 requests per minute (same limiter used by public session endpoints)
309
+
310
+ **Request Body (JSON):**
311
+ ```json
312
+ {
313
+ "sessionId": "string (uuid or encrypted string)",
314
+ "isEncrypt": "boolean | 'true' (optional, indicates sessionId is encrypted)
315
+ }
316
+ ```
317
+
318
+ **Notes:**
319
+ - The endpoint accepts `isEncrypt` or the misspelled `isEncryt` (both `true` or the string `'true'` are accepted).
320
+ - If `isEncrypt` is true, the server will first attempt `decodeURIComponent()` on the value and then decrypt it (AES) to obtain a UUID session id. If decryption fails or the resulting value is not a UUID, the server returns `400 Bad Request` with `SESSION_INVALID`.
321
+ - A missing `sessionId` returns `400 Bad Request` with `MISSING_REQUIRED_FIELD`.
322
+
323
+ **Success Response (200 OK):**
324
+ ```json
325
+ {
326
+ "sessionValid": true,
327
+ "expiry": "2025-12-27T12:34:56.000Z"
328
+ }
329
+ ```
330
+
331
+ **Invalid/Expired Session:**
332
+ - Returns 200 with `{ "sessionValid": false, "expiry": null }` for unknown/expired/inactive sessions.
333
+
334
+ **Example Request (Fetch):**
335
+ ```javascript
336
+ fetch('/mbkauthe/api/checkSession', {
337
+ method: 'POST',
338
+ headers: { 'Content-Type': 'application/json' },
339
+ body: JSON.stringify({ sessionId: '550e8400-e29b-41d4-a716-446655440000' })
340
+ }).then(r => r.json()).then(console.log);
341
+ ```
342
+
343
+ **Example Request (Encrypted sessionId):**
344
+ ```javascript
345
+ fetch('/mbkauthe/api/checkSession', {
346
+ method: 'POST',
347
+ headers: { 'Content-Type': 'application/json' },
348
+ body: JSON.stringify({ sessionId: 'ENCRYPTED_VALUE', isEncrypt: true })
349
+ }).then(r => r.json()).then(console.log);
350
+ ```
351
+
352
+ ---
353
+
354
+ #### `POST /mbkauthe/api/verifySession`
355
+
356
+ Returns session details for a provided `sessionId`. Intended for server-side validation and to retrieve associated user metadata without relying on an active cookie session.
357
+
358
+ **Request Body (JSON):**
359
+ ```json
360
+ {
361
+ "sessionId": "string (uuid or encrypted string)",
362
+ "isEncrypt": "boolean | 'true' (optional)"
363
+ }
364
+ ```
365
+
366
+ **Behavior and Notes:**
367
+ - `isEncrypt`/`isEncryt` have the same behavior as in `/api/checkSession`.
368
+ - If the session is valid and active, the response includes `username` and `role`.
369
+ - Missing or invalid `sessionId` results in `400 Bad Request` with an appropriate error code (`MISSING_REQUIRED_FIELD` or `SESSION_INVALID`).
370
+
371
+ **Success Response (200 OK):**
372
+ ```json
373
+ {
374
+ "valid": true,
375
+ "expiry": "2025-12-27T12:34:56.000Z",
376
+ "username": "john.doe",
377
+ "role": "NormalUser"
378
+ }
379
+ ```
380
+
381
+ **Invalid/Expired Session:**
382
+ - Returns 200 with `{ "valid": false, "expiry": null }` for unknown/expired/inactive sessions.
383
+
384
+ **Example Request:**
385
+ ```javascript
386
+ fetch('/mbkauthe/api/verifySession', {
387
+ method: 'POST',
388
+ headers: { 'Content-Type': 'application/json' },
389
+ body: JSON.stringify({ sessionId: '550e8400-e29b-41d4-a716-446655440000' })
390
+ }).then(r => r.json()).then(console.log);
391
+ ```
392
+
393
+ ---
394
+
304
395
  #### `GET /mbkauthe/2fa`
305
396
 
306
397
  Renders the Two-Factor Authentication verification page.
@@ -4,9 +4,9 @@ import rateLimit from 'express-rate-limit';
4
4
  import { mbkautheVar, packageJson, appVersion } from "#config.js";
5
5
  import { renderError } from "#response.js";
6
6
  import { authenticate, validateSession, validateApiSession } from "../middleware/auth.js";
7
- import { ErrorCodes, ErrorMessages } from "../utils/errors.js";
7
+ import { ErrorCodes, ErrorMessages, createErrorResponse } from "../utils/errors.js";
8
8
  import { dblogin } from "#pool.js";
9
- import { clearSessionCookies } from "#cookies.js";
9
+ import { clearSessionCookies, decryptSessionId } from "#cookies.js";
10
10
  import { fileURLToPath } from "url";
11
11
  import path from "path";
12
12
  import fs from "fs";
@@ -236,6 +236,107 @@ router.get('/api/checkSession', LoginLimit, async (req, res) => {
236
236
  }
237
237
  });
238
238
 
239
+ // UUID helper used by session endpoints
240
+ const isUuid = (val) => typeof val === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(val);
241
+
242
+ // POST /api/checkSession — accept sessionId in request body { sessionId: "<uuid>" }
243
+ router.post('/api/checkSession', LoginLimit, async (req, res) => {
244
+ try {
245
+ const { sessionId: rawSessionId, isEncrypt, isEncryt } = req.body || {};
246
+ let sessionId = rawSessionId;
247
+ if (!sessionId) {
248
+ return res.status(400).json(createErrorResponse(400, ErrorCodes.MISSING_REQUIRED_FIELD));
249
+ }
250
+
251
+ // If body indicates the sessionId is encrypted, decode (if URL-encoded) then decrypt it first
252
+ const encryptedFlag = isEncrypt === true || isEncrypt === 'true' || isEncryt === true || isEncryt === 'true';
253
+ if (encryptedFlag) {
254
+ // Some clients URL-encode cookie values when posting; safely try to decode first
255
+ let toDecrypt = typeof sessionId === 'string' ? sessionId : String(sessionId);
256
+ try {
257
+ toDecrypt = decodeURIComponent(toDecrypt);
258
+ } catch (decodeErr) {
259
+ // ignore decode errors and continue with original value
260
+ }
261
+ const decrypted = decryptSessionId(toDecrypt);
262
+ if (!decrypted || !isUuid(decrypted)) {
263
+ return res.status(400).json(createErrorResponse(400, ErrorCodes.SESSION_INVALID));
264
+ }
265
+ sessionId = decrypted;
266
+ }
267
+
268
+ const result = await dblogin.query({
269
+ name: 'check-session-validity-by-id',
270
+ text: `SELECT s.expires_at, u."Active" FROM "Sessions" s JOIN "Users" u ON s."UserName" = u."UserName" WHERE s.id = $1 LIMIT 1`,
271
+ values: [sessionId]
272
+ });
273
+
274
+ if (result.rows.length === 0) {
275
+ return res.status(200).json({ sessionValid: false, expiry: null });
276
+ }
277
+
278
+ const row = result.rows[0];
279
+ if ((row.expires_at && new Date(row.expires_at) <= new Date()) || !row.Active) {
280
+ return res.status(200).json({ sessionValid: false, expiry: null });
281
+ }
282
+
283
+ const expiry = row.expires_at ? new Date(row.expires_at).toISOString() : null;
284
+ return res.status(200).json({ sessionValid: true, expiry });
285
+ } catch (err) {
286
+ console.error('[mbkauthe] checkSession (body) error:', err);
287
+ return res.status(200).json({ sessionValid: false, expiry: null });
288
+ }
289
+ });
290
+
291
+ // POST /api/verifySession — returns details about sessionId provided in body
292
+ router.post('/api/verifySession', LoginLimit, async (req, res) => {
293
+ try {
294
+ const { sessionId: rawSessionId, isEncrypt, isEncryt } = req.body || {};
295
+ let sessionId = rawSessionId;
296
+ if (!sessionId) {
297
+ return res.status(400).json(createErrorResponse(400, ErrorCodes.MISSING_REQUIRED_FIELD));
298
+ }
299
+
300
+ // If body indicates the sessionId is encrypted, decode (if URL-encoded) then decrypt it first
301
+ const encryptedFlag = isEncrypt === true || isEncrypt === 'true' || isEncryt === true || isEncryt === 'true';
302
+ if (encryptedFlag) {
303
+ // Some clients URL-encode cookie values when posting; safely try to decode first
304
+ let toDecrypt = typeof sessionId === 'string' ? sessionId : String(sessionId);
305
+ try {
306
+ toDecrypt = decodeURIComponent(toDecrypt);
307
+ } catch (decodeErr) {
308
+ // ignore decode errors and continue with original value
309
+ }
310
+ const decrypted = decryptSessionId(toDecrypt);
311
+ if (!decrypted || !isUuid(decrypted)) {
312
+ return res.status(400).json(createErrorResponse(400, ErrorCodes.SESSION_INVALID));
313
+ }
314
+ sessionId = decrypted;
315
+ }
316
+
317
+ const query = `SELECT s.id as sid, s.expires_at, u.id as uid, u."UserName", u."Active", u."Role", u."AllowedApps"
318
+ FROM "Sessions" s
319
+ JOIN "Users" u ON s."UserName" = u."UserName"
320
+ WHERE s.id = $1 LIMIT 1`;
321
+ const result = await dblogin.query({ name: 'verify-session', text: query, values: [sessionId] });
322
+
323
+ if (result.rows.length === 0) {
324
+ return res.status(200).json({ valid: false, expiry: null });
325
+ }
326
+
327
+ const row = result.rows[0];
328
+ if ((row.expires_at && new Date(row.expires_at) <= new Date()) || !row.Active) {
329
+ return res.status(200).json({ valid: false, expiry: null });
330
+ }
331
+
332
+ const expiry = row.expires_at ? new Date(row.expires_at).toISOString() : null;
333
+ return res.status(200).json({ valid: true, expiry, username: row.UserName, role: row.Role });
334
+ } catch (err) {
335
+ console.error('[mbkauthe] verifySession error:', err);
336
+ return res.status(200).json({ valid: false, expiry: null });
337
+ }
338
+ });
339
+
239
340
  // Error codes page
240
341
  router.get("/ErrorCode", (req, res) => {
241
342
  try {
@@ -427,4 +528,4 @@ router.post("/api/terminateAllSessions", AdminOperationLimit, authenticate(mbkau
427
528
  }
428
529
  });
429
530
 
430
- export default router;
531
+ export default router;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mbkauthe",
3
- "version": "4.3.0",
3
+ "version": "4.3.2",
4
4
  "description": "MBKTech's reusable authentication system for Node.js applications.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,45 +1,76 @@
1
- <div class="header-actions">
1
+ <div class="mbkauthe-header-actions">
2
2
  {{#if userLoggedIn}}
3
- <div class="profile-menu">
4
- <button class="profile-trigger" type="button" aria-expanded="false" aria-haspopup="true">
5
- <img src="/mbkauthe/user/profilepic?u={{username}}" alt="Profile" class="profile-avatar">
3
+ <div class="mbkauthe-profile-menu">
4
+ <button class="mbkauthe-profile-trigger" type="button" aria-expanded="false" aria-haspopup="true">
5
+ <img src="/mbkauthe/user/profilepic?u={{username}}" alt="Profile" class="mbkauthe-profile-avatar">
6
6
  </button>
7
- <div class="profile-dropdown" role="menu" hidden>
8
- <a class="profile-item" role="menuitem" title="{{username}}">
7
+ <div class="mbkauthe-profile-dropdown" role="menu" hidden>
8
+ <a class="mbkauthe-profile-item" role="menuitem" title="{{username}}">
9
9
  <span>@{{username}}</span>
10
10
  </a>
11
- <a class="profile-item" href="https://portal.mbktech.org/user/settings" role="menuitem" title="Settings">
11
+ <a class="mbkauthe-profile-item" href="https://portal.mbktech.org/user/settings" role="menuitem"
12
+ title="Settings">
12
13
  <i class="fas fa-cog"></i>
13
14
  <span>Settings</span>
14
15
  </a>
15
- <a class="profile-item" href="/mbkauthe/accounts" role="menuitem" title="Switch or Add another Account">
16
+ <a class="mbkauthe-profile-item" href="/mbkauthe/accounts" role="menuitem"
17
+ title="Switch or Add another Account">
16
18
  <i class="fa fa-user-group"></i>
17
19
  <span>Switch account</span>
18
20
  </a>
19
- <button class="profile-item" type="button" data-action="logout" role="menuitem" title="Logout">
21
+ <button class="mbkauthe-profile-item" type="button" data-action="logout" role="menuitem" title="Logout">
20
22
  <i class="fas fa-sign-out-alt"></i>
21
23
  <span>Logout</span>
22
24
  </button>
23
25
  </div>
24
26
  </div>
25
27
  {{else}}
26
- <a class="btn-login" style="text-decoration: none;" href="/mbkauthe/login"><i class="fas fa-sign-in-alt"></i>
28
+ <a class="mbkauthe-btn-login" style="text-decoration: none;" href="/mbkauthe/login"><i
29
+ class="fas fa-sign-in-alt"></i>
27
30
  Login</a>
28
31
  {{/if}}
29
32
  </div>
30
33
  <style>
31
- .header-actions {
34
+ .mbkauthe-btn-login {
35
+ width: 100%;
36
+ padding: 8px;
37
+ border-radius: var(--border-radius);
38
+ background: var(--accent);
39
+ color: var(--dark);
40
+ font-weight: 700;
41
+ font-size: 1.2rem;
42
+ border: .1rem solid var(--accent);
43
+ cursor: pointer;
44
+ transition: all 0.4s cubic-bezier(.4, 0, .2, 1);
45
+ box-shadow: var(--box-shadow);
46
+ }
47
+
48
+ .mbkauthe-btn-login:hover {
49
+ background: var(--dark);
50
+ color: var(--accent);
51
+ box-shadow: 0 6px 20px rgba(0, 184, 148, 0.3);
52
+ }
53
+
54
+ .mbkauthe-btn-login:disabled {
55
+ background: var(--dark);
56
+ color: var(--accent);
57
+ cursor: not-allowed;
58
+ transform: none;
59
+ box-shadow: none;
60
+ }
61
+
62
+ .mbkauthe-header-actions {
32
63
  display: flex;
33
64
  align-items: center;
34
65
  gap: 0.75rem;
35
66
  margin-left: auto;
36
67
  }
37
68
 
38
- .profile-menu {
69
+ .mbkauthe-profile-menu {
39
70
  position: relative;
40
71
  }
41
72
 
42
- .profile-trigger {
73
+ .mbkauthe-profile-trigger {
43
74
  width: 45px;
44
75
  height: 45px;
45
76
  border-radius: 999px;
@@ -52,17 +83,17 @@
52
83
  transition: var(--transition);
53
84
  }
54
85
 
55
- .profile-trigger:hover {
86
+ .mbkauthe-profile-trigger:hover {
56
87
  border-color: var(--accent);
57
88
  box-shadow: 0 10px 30px rgba(0, 184, 148, 0.2);
58
89
  }
59
90
 
60
- .profile-trigger:focus-visible {
91
+ .mbkauthe-profile-trigger:focus-visible {
61
92
  outline: 2px solid var(--accent);
62
93
  outline-offset: 2px;
63
94
  }
64
95
 
65
- .profile-avatar {
96
+ .mbkauthe-profile-avatar {
66
97
  width: 40px;
67
98
  height: 40px;
68
99
  border-radius: 999px;
@@ -70,34 +101,46 @@
70
101
  background: var(--dark);
71
102
  }
72
103
 
73
- .profile-dropdown {
104
+ /* Theme-aware dropdown variables and improved visuals */
105
+ .mbkauthe-profile-dropdown {
106
+ --mbk-bg: var(--mbkauthe-dropdown-bg, #ffffff);
107
+ --mbk-border: var(--mbkauthe-dropdown-border, rgba(15, 23, 42, 0.06));
108
+ --mbk-item-hover: var(--mbkauthe-item-hover, rgba(15, 23, 42, 0.03));
109
+ --mbk-text: var(--mbkauthe-text, var(--text, #0f172a));
110
+
74
111
  position: absolute;
75
112
  right: 0;
76
113
  top: calc(100% + 0.5rem);
77
- background: var(--darker);
78
- border: 1px solid rgba(255, 255, 255, 0.08);
114
+ background: var(--mbk-bg);
115
+ border: 1px solid var(--mbk-border);
79
116
  border-radius: var(--border-radius);
80
117
  min-width: 190px;
81
- box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
118
+ box-shadow: 0 10px 30px rgba(2, 6, 23, 0.12);
82
119
  padding: 0;
83
120
  opacity: 0;
84
121
  visibility: hidden;
85
122
  transform: translateY(-6px);
86
123
  transition: var(--transition);
87
124
  z-index: 1000;
125
+ backdrop-filter: blur(6px);
126
+ -webkit-backdrop-filter: blur(6px);
127
+ color: var(--mbk-text);
128
+ overflow: hidden;
88
129
  }
89
130
 
90
- .profile-dropdown.open {
131
+ .mbkauthe-profile-dropdown.open {
132
+ display: block;
91
133
  opacity: 1;
92
134
  visibility: visible;
93
135
  transform: translateY(0);
136
+ border-radius: 3%;
94
137
  }
95
138
 
96
- .profile-item {
139
+ .mbkauthe-profile-item {
97
140
  width: 100%;
98
141
  display: block;
99
142
  padding: 0.85rem 1rem;
100
- color: var(--text);
143
+ color: var(--mbk-text);
101
144
  background: transparent;
102
145
  border: none;
103
146
  text-decoration: none;
@@ -105,61 +148,103 @@
105
148
  font-weight: 600;
106
149
  letter-spacing: 0.01em;
107
150
  cursor: pointer;
108
- transition: var(--transition);
151
+ transition: background 0.12s ease, color 0.12s ease, transform 0.12s ease;
109
152
  }
110
153
 
111
- .profile-item:hover {
112
- background: rgba(255, 255, 255, 0.05);
113
- color: var(--accent);
154
+ .mbkauthe-profile-item:hover {
155
+ background: var(--mbk-item-hover);
156
+ color: var(--color-accent, var(--accent, #0d9488));
114
157
  }
115
158
 
116
- .profile-item:disabled {
159
+ .mbkauthe-profile-item:focus-visible {
160
+ outline: 2px solid color-mix(in srgb, var(--color-accent, var(--accent, #0d9488)) 40%, transparent 60%);
161
+ outline-offset: 2px;
162
+ }
163
+
164
+ .mbkauthe-profile-item:disabled {
117
165
  opacity: 0.6;
118
166
  cursor: not-allowed;
119
167
  }
168
+
169
+ /* Make trigger adapt to theme */
170
+ .mbkauthe-profile-trigger {
171
+ transition: box-shadow 0.15s ease, border-color 0.15s ease, transform 0.12s ease;
172
+ }
173
+
174
+ .mbkauthe-profile-trigger:hover {
175
+ }
176
+
177
+ /* prefers-color-scheme fallbacks scoped to component */
178
+ @media (prefers-color-scheme: dark) {
179
+ .mbkauthe-profile-dropdown {
180
+ --mbk-bg: #0b1220;
181
+ --mbk-border: rgba(255, 255, 255, 0.06);
182
+ --mbk-item-hover: rgba(255, 255, 255, 0.03);
183
+ --mbk-text: #e6eef6;
184
+ box-shadow: 0 12px 36px rgba(0, 0, 0, 0.7);
185
+ }
186
+
187
+ .mbkauthe-profile-trigger {
188
+ border: 1px solid rgba(255, 255, 255, 0.06);
189
+ background: rgba(255, 255, 255, 0.02);
190
+ }
191
+ }
192
+
193
+ @media (prefers-color-scheme: light) {
194
+ .mbkauthe-profile-dropdown {
195
+ --mbk-bg: #ffffff;
196
+ --mbk-border: rgba(15, 23, 42, 0.06);
197
+ --mbk-item-hover: rgba(15, 23, 42, 0.03);
198
+ --mbk-text: var(--dark-color-ml, #0f172a);
199
+ box-shadow: 0 8px 22px rgba(2, 6, 23, 0.08);
200
+ }
201
+
202
+ .mbkauthe-profile-trigger {
203
+ border: 1px solid rgba(2, 6, 23, 0.06);
204
+ background: rgba(255, 255, 255, 0.96);
205
+ }
206
+ }
120
207
  </style>
121
208
  <script>
122
209
  (() => {
123
- const trigger = document.querySelector('header .profile-trigger');
124
- const dropdown = document.querySelector('header .profile-dropdown');
210
+ const mbkTrigger = document.querySelector('.mbkauthe-profile-trigger');
211
+ const mbkDropdown = document.querySelector('.mbkauthe-profile-dropdown');
125
212
 
126
- if (!trigger || !dropdown) {
213
+ if (!mbkTrigger || !mbkDropdown) {
127
214
  return;
128
215
  }
129
216
 
130
217
  const closeMenu = () => {
131
- if (!dropdown.classList.contains('open')) return;
132
- dropdown.classList.remove('open');
133
- trigger.setAttribute('aria-expanded', 'false');
134
-
218
+ if (!mbkDropdown.classList.contains('open')) return;
219
+ mbkDropdown.classList.remove('open');
220
+ mbkTrigger.setAttribute('aria-expanded', 'false');
135
221
  // After transition, hide to prevent FOUC on next paint
136
222
  const onTransition = (e) => {
137
223
  if (e.propertyName === 'opacity') {
138
- dropdown.setAttribute('hidden', '');
139
- dropdown.removeEventListener('transitionend', onTransition);
224
+ mbkDropdown.setAttribute('hidden', '');
225
+ mbkDropdown.removeEventListener('transitionend', onTransition);
140
226
  }
141
227
  };
142
228
 
143
- dropdown.addEventListener('transitionend', onTransition);
229
+ mbkDropdown.addEventListener('transitionend', onTransition);
144
230
  // Fallback in case transitionend doesn't fire
145
231
  setTimeout(() => {
146
- if (!dropdown.classList.contains('open')) {
147
- dropdown.setAttribute('hidden', '');
232
+ if (!mbkDropdown.classList.contains('open')) {
233
+ mbkDropdown.setAttribute('hidden', '');
148
234
  }
149
235
  }, 350);
150
236
  };
151
237
 
152
- trigger.addEventListener('click', (event) => {
238
+ mbkTrigger.addEventListener('click', (event) => {
153
239
  event.stopPropagation();
154
- const isOpening = !dropdown.classList.contains('open');
155
-
240
+ const isOpening = !mbkDropdown.classList.contains('open');
156
241
  if (isOpening) {
157
242
  // Reveal then start transition to open state
158
- dropdown.removeAttribute('hidden');
243
+ mbkDropdown.removeAttribute('hidden');
159
244
  // Ensure the class add happens on next frame for transition to work
160
245
  requestAnimationFrame(() => {
161
- dropdown.classList.add('open');
162
- trigger.setAttribute('aria-expanded', 'true');
246
+ mbkDropdown.classList.add('open');
247
+ mbkTrigger.setAttribute('aria-expanded', 'true');
163
248
  });
164
249
  } else {
165
250
  // Close the menu
@@ -168,7 +253,7 @@
168
253
  });
169
254
 
170
255
  document.addEventListener('click', (event) => {
171
- if (!dropdown.contains(event.target) && !trigger.contains(event.target)) {
256
+ if (!mbkDropdown.contains(event.target) && !mbkTrigger.contains(event.target)) {
172
257
  closeMenu();
173
258
  }
174
259
  });
@@ -179,7 +264,7 @@
179
264
  }
180
265
  });
181
266
 
182
- const logoutButton = dropdown.querySelector('[data-action="logout"]');
267
+ const logoutButton = mbkDropdown.querySelector('[data-action="logout"]');
183
268
  if (logoutButton) {
184
269
  logoutButton.addEventListener('click', async () => {
185
270
  const originalLabel = logoutButton.textContent;
@@ -215,7 +300,7 @@
215
300
 
216
301
  // Populate the "Switch account" link with encoded current page URL in its `redirect` param
217
302
  (function setSwitchAccountRedirect() {
218
- const switchAccountLink = dropdown.querySelector('a[href^="/mbkauthe/accounts"]');
303
+ const switchAccountLink = mbkDropdown.querySelector('a[href^="/mbkauthe/accounts"]');
219
304
  if (!switchAccountLink) return;
220
305
 
221
306
  try {